Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/globals.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ local constants = require(".src.constants")
local utils = require(".src.utils")
local globals = {}

--[[
HyperbeamSync is a table that is used to track changes to our lua state that need to be synced to the Hyperbeam.
the principal of using it is to set the key:value pairs that need to be synced, then
Copy link
Contributor

@arielmelendez arielmelendez Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits: The principle and weird tabbing on the line below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the patch function will pull that from the global state to build the patch message.
After, the HyperbeamSync table is cleared and the next message run will start fresh.
]]
HyperbeamSync = HyperbeamSync
or {
---@type table<string, boolean> addresses that have had balance changes
balances = {},
primaryNames = {
---@type table<string, boolean> addresses that have had name changes
names = {},
---@type table<string, boolean> addresses that have had owner changes
owners = {},
---@type table<string, boolean> addresses that have had request changes
requests = {},
},
}

--[[
Constants
]]
Expand Down
92 changes: 92 additions & 0 deletions src/hb.lua
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,96 @@ function hb.patchBalances(oldBalances)
return affectedBalancesAddresses
end

---@return PrimaryNames|nil affectedPrimaryNamesAddresses
function hb.createPrimaryNamesPatch()
---@type PrimaryNames
local affectedPrimaryNamesAddresses = {
names = {},
owners = {},
requests = {},
}

-- if no changes, return early. This will allow downstream code to not send the patch state for this key ('primary-names')
if
next(_G.HyperbeamSync.primaryNames.names) == nil
and next(_G.HyperbeamSync.primaryNames.owners) == nil
and next(_G.HyperbeamSync.primaryNames.requests) == nil
then
return nil
end

-- build the affected primary names addresses table for the patch message
for name, _ in pairs(_G.HyperbeamSync.primaryNames.names) do
-- we need to send an empty string to remove the name
affectedPrimaryNamesAddresses.names[name] = PrimaryNames.names[name] or ""
end
for owner, _ in pairs(_G.HyperbeamSync.primaryNames.owners) do
-- we need to send an empty table to remove the owner primary name data
affectedPrimaryNamesAddresses.owners[owner] = PrimaryNames.owners[owner] or {}
end
for address, _ in pairs(_G.HyperbeamSync.primaryNames.requests) do
-- we need to send an empty table to remove the request
affectedPrimaryNamesAddresses.requests[address] = PrimaryNames.requests[address] or {}
end

if next(PrimaryNames.names) == nil then
affectedPrimaryNamesAddresses.names = {}
elseif next(affectedPrimaryNamesAddresses.names) == nil then
affectedPrimaryNamesAddresses.names = nil
end

if next(PrimaryNames.owners) == nil then
affectedPrimaryNamesAddresses.owners = {}
elseif next(affectedPrimaryNamesAddresses.owners) == nil then
affectedPrimaryNamesAddresses.owners = nil
end

if next(PrimaryNames.requests) == nil then
affectedPrimaryNamesAddresses.requests = {}
elseif next(affectedPrimaryNamesAddresses.requests) == nil then
affectedPrimaryNamesAddresses.requests = nil
end

-- if we're not sending any data, return nil which will allow downstream code to not send the patch message
if next(affectedPrimaryNamesAddresses) == nil then
return nil
end

return affectedPrimaryNamesAddresses
end

function hb.resetHyperbeamSync()
HyperbeamSync = {
balances = {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

primaryNames = {
names = {},
owners = {},
requests = {},
},
}
end

--[[
1. Create the data patches
2. Send the patch message if there are any data patches
3. Reset the hyperbeam sync
]]
function hb.patchHyperbeamState()
local patchMessageFields = {}

-- Only add patches that have data
local primaryNamesPatch = hb.createPrimaryNamesPatch()
if primaryNamesPatch then
patchMessageFields["primary-names"] = primaryNamesPatch
end

--- Send patch message if there are any patches
if next(patchMessageFields) ~= nil then
patchMessageFields.device = "[email protected]"
ao.send(patchMessageFields)
end

hb.resetHyperbeamSync()
end

return hb
6 changes: 6 additions & 0 deletions src/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -477,8 +477,10 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical, prin
-- Store the old balances to compare after the handler has run for patching state
-- Only do this for the last handler to avoid unnecessary copying
local oldBalances = nil
local shouldPatchHbState = false
if pattern(msg) ~= "continue" then
oldBalances = utils.deepCopy(Balances)
shouldPatchHbState = true
end
-- add an ARIOEvent to the message if it doesn't exist
msg.ioEvent = msg.ioEvent or ARIOEvent(msg)
Expand Down Expand Up @@ -508,6 +510,10 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical, prin
hb.patchBalances(oldBalances)
end

if shouldPatchHbState then
hb.patchHyperbeamState()
end

msg.ioEvent:addField("Handler-Memory-KiB-Used", collectgarbage("count"), false)
collectgarbage("collect")
msg.ioEvent:addField("Final-Memory-KiB-Used", collectgarbage("count"), false)
Expand Down
25 changes: 25 additions & 0 deletions src/primary_names.lua
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ function primaryNames.createPrimaryNameRequest(name, initiator, timestamp, msgId
else
-- otherwise store the request for asynchronous approval
PrimaryNames.requests[initiator] = request
-- track the changes in the hyperbeam sync
HyperbeamSync.primaryNames.requests[initiator] = true
primaryNames.scheduleNextPrimaryNamesPruning(request.endTimestamp)
end

Expand Down Expand Up @@ -205,6 +207,7 @@ function primaryNames.approvePrimaryNameRequest(recipient, name, from, timestamp

-- set the primary name
local newPrimaryName = primaryNames.setPrimaryNameFromRequest(recipient, request, timestamp)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: stray whitespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return {
newPrimaryName = newPrimaryName,
request = request,
Expand All @@ -228,6 +231,12 @@ function primaryNames.setPrimaryNameFromRequest(recipient, request, startTimesta
startTimestamp = startTimestamp,
}
PrimaryNames.requests[recipient] = nil

-- track the changes in the hyperbeam sync
HyperbeamSync.primaryNames.names[request.name] = true
HyperbeamSync.primaryNames.owners[recipient] = true
HyperbeamSync.primaryNames.requests[recipient] = true

return {
name = request.name,
owner = recipient,
Expand All @@ -247,6 +256,10 @@ function primaryNames.removePrimaryNames(names, from)
local removedPrimaryNamesAndOwners = {}
for _, name in pairs(names) do
local removedPrimaryNameAndOwner = primaryNames.removePrimaryName(name, from)
-- track the changes in the hyperbeam sync
HyperbeamSync.primaryNames.names[name] = true
HyperbeamSync.primaryNames.owners[from] = true

table.insert(removedPrimaryNamesAndOwners, removedPrimaryNameAndOwner)
end
return removedPrimaryNamesAndOwners
Expand All @@ -272,6 +285,12 @@ function primaryNames.removePrimaryName(name, from)
if PrimaryNames.requests[primaryName.owner] and PrimaryNames.requests[primaryName.owner].name == name then
PrimaryNames.requests[primaryName.owner] = nil
end

-- track the changes in the hyperbeam sync
HyperbeamSync.primaryNames.names[name] = true
HyperbeamSync.primaryNames.owners[primaryName.owner] = true
HyperbeamSync.primaryNames.requests[primaryName.owner] = true

return {
name = name,
owner = primaryName.owner,
Expand Down Expand Up @@ -348,6 +367,9 @@ function primaryNames.removePrimaryNamesForBaseName(baseName)
local primaryNamesForBaseName = primaryNames.getPrimaryNamesForBaseName(baseName)
for _, nameData in pairs(primaryNamesForBaseName) do
local removedName = primaryNames.removePrimaryName(nameData.name, nameData.owner)
-- track the changes in the hyperbeam sync
HyperbeamSync.primaryNames.names[nameData.name] = true
HyperbeamSync.primaryNames.owners[nameData.owner] = true
table.insert(removedNames, removedName)
end
return removedNames
Expand Down Expand Up @@ -410,6 +432,9 @@ function primaryNames.prunePrimaryNameRequests(timestamp)
if request.endTimestamp <= timestamp then
PrimaryNames.requests[initiator] = nil
prunedNameRequests[initiator] = request

-- track the changes in the hyperbeam sync
HyperbeamSync.primaryNames.requests[initiator] = true
else
primaryNames.scheduleNextPrimaryNamesPruning(request.endTimestamp)
end
Expand Down
106 changes: 106 additions & 0 deletions tests/patch-primary-names.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
assertNoResultError,
buyRecord,
handle,
startMemory,
totalTokenSupply,
transfer,
} from './helpers.mjs';
import assert from 'assert';
import { describe, it } from 'node:test';
import { STUB_TIMESTAMP } from '../tools/constants.mjs';

describe('Primary Names Hyperbeam Patching', function () {
it('should send patch with request when primary name request is created', async () => {
const testTimestamp = STUB_TIMESTAMP + 1000;
const testCaller = 'test-caller-address-'.padEnd(43, '1');
const baseNameOwner = 'base-name-owner-address-'.padEnd(43, '2');
const testName = 'test-name';
const testProcessId = 'test-process-id-'.padEnd(43, '1');

// Initialize token supply
const { Memory: totalTokenSupplyMemory } = await totalTokenSupply({
memory: startMemory,
});
let memory = totalTokenSupplyMemory;

// Give both addresses balance
let result = await transfer({
recipient: baseNameOwner,
quantity: 10000000000,
memory,
timestamp: testTimestamp,
});
memory = result;

result = await transfer({
recipient: testCaller,
quantity: 10000000000,
memory,
timestamp: testTimestamp + 10,
});
memory = result;

// Base name owner buys the ArNS record
result = await buyRecord({
memory,
from: baseNameOwner,
name: testName,
processId: testProcessId,
type: 'permabuy',
timestamp: testTimestamp + 100,
});
memory = result.memory;

// Request primary name from a different caller than the base name owner
const requestResult = await handle({
options: {
From: testCaller,
Owner: testCaller,
Timestamp: testTimestamp + 200,
Tags: [
{ name: 'Action', value: 'Request-Primary-Name' },
{ name: 'Name', value: testName },
],
},
memory,
});

assertNoResultError(requestResult);

// Find the patch message with device: "[email protected]"
const messages = requestResult.Messages || [];
let patchMessage = null;
for (let i = messages.length - 1; i >= 0; i--) {
const tags = messages[i].Tags || [];
const deviceTag = tags.find((t) => t.name === 'device');
if (deviceTag && deviceTag.value === '[email protected]') {
patchMessage = messages[i];
break;
}
}

assert(patchMessage, 'Should send a patch message');

// Get the primary-names tag
const primaryNamesTag = patchMessage.Tags.find(
(t) => t.name === 'primary-names',
);
assert(primaryNamesTag, 'Patch should have primary-names tag');

// Patch messages don't JSON-encode on purpose, so the value is already an object
const patch = primaryNamesTag.value;

assert(patch, 'Should have primary names patch data');
assert(patch.requests, 'Patch should include requests');
assert(
patch.requests[testCaller],
'Patch should include the request for the caller',
);
assert.strictEqual(
patch.requests[testCaller].name,
testName,
'Request should have correct name',
);
});
});
Loading
Loading