diff --git a/spec/utils_spec.lua b/spec/utils_spec.lua index e285a773..ca5ce50c 100644 --- a/spec/utils_spec.lua +++ b/spec/utils_spec.lua @@ -23,6 +23,20 @@ describe("utils", function() end) end) + describe("isValidUnformattedEthAddress", function() + it("should return true on a valid unformatted ETH address", function() + assert.is_true(utils.isValidUnformattedEthAddress(testEthAddress)) + end) + + it("should return false on a non-string value", function() + assert.is_false(utils.isValidUnformattedEthAddress(3)) + end) + + it("should return false on an invalid unformatted ETH address", function() + assert.is_false(utils.isValidUnformattedEthAddress("ZxFCAd0B19bB29D4674531d6f115237E16AfCE377C")) + end) + end) + describe("formatAddress", function() it("should format ETH address to lowercase", function() assert.is.equal(testEthAddress, utils.formatAddress(testEthAddress)) diff --git a/src/main.lua b/src/main.lua index a01531ec..726e50e0 100644 --- a/src/main.lua +++ b/src/main.lua @@ -24,6 +24,8 @@ NameRegistry = NameRegistry or {} Epochs = Epochs or {} LastTickedEpochIndex = LastTickedEpochIndex or -1 LastGracePeriodEntryEndTimestamp = LastGracePeriodEntryEndTimestamp or 0 +LastKnownMessageTimestamp = LastKnownMessageTimestamp or 0 +LastKnownMessageId = LastKnownMessageId or "" local utils = require("utils") local json = require("json") @@ -383,8 +385,34 @@ local function addPrimaryNameRequestData(ioEvent, primaryNameResult) addPrimaryNameCounts(ioEvent) end -local function addEventingHandler(handlerName, pattern, handleFn, critical) +-- Sanitize inputs before every interaction +local function assertAndSanitizeInputs(msg) + assert( + msg.Timestamp and msg.Timestamp >= LastKnownMessageTimestamp, + "Timestamp must be greater than or equal to the last known message timestamp of " + .. LastKnownMessageTimestamp + .. " but was " + .. msg.Timestamp + ) + assert(msg.From, "From is required") + assert(msg.Id, "Id is required") + assert(msg.Tags and type(msg.Tags) == "table", "Tags are required") + + msg.Tags = utils.validateAndSanitizeInputs(msg.Tags) + msg.From = utils.formatAddress(msg.From) + msg.Timestamp = msg.Timestamp and tonumber(msg.Timestamp) or tonumber(msg.Tags.Timestamp) or nil +end + +local function updateLastKnownMessage(msg) + if msg.Timestamp >= LastKnownMessageTimestamp then + LastKnownMessageTimestamp = msg.Timestamp + LastKnownMessageId = msg.Id + end +end + +local function addEventingHandler(handlerName, pattern, handleFn, critical, printEvent) critical = critical or false + printEvent = printEvent == nil and true or printEvent Handlers.add(handlerName, pattern, function(msg) -- add an IOEvent to the message if it doesn't exist msg.ioEvent = msg.ioEvent or ARIOEvent(msg) @@ -408,21 +436,24 @@ local function addEventingHandler(handlerName, pattern, handleFn, critical) local errorWithEvent = tostring(resultOrError) .. "\n" .. errorEvent:toJSON() error(errorWithEvent, 0) -- 0 ensures not to include this line number in the error message end - -- isolate out prune handler here when printing - if handlerName ~= "prune" then + if printEvent then msg.ioEvent:printEvent() end end) end --- prune state before every interaction +addEventingHandler("sanitize", function() + return "continue" +end, function(msg) + assertAndSanitizeInputs(msg) + updateLastKnownMessage(msg) +end, CRITICAL, false) + -- NOTE: THIS IS A CRITICAL HANDLER AND WILL DISCARD THE MEMORY ON ERROR addEventingHandler("prune", function() return "continue" -- continue is a pattern that matches every message and continues to the next handler that matches the tags end, function(msg) - local msgTimestamp = tonumber(msg.Timestamp or msg.Tags.Timestamp) - assert(msgTimestamp, "Timestamp is required for a tick interaction") - local epochIndex = epochs.getEpochIndexForTimestamp(msgTimestamp) + local epochIndex = epochs.getEpochIndexForTimestamp(msg.Timestamp) msg.ioEvent:addField("epochIndex", epochIndex) local previousStateSupplies = { @@ -435,54 +466,6 @@ end, function(msg) lastKnownTotalSupply = token.lastKnownTotalTokenSupply(), } - msg.From = utils.formatAddress(msg.From) - msg.Timestamp = msg.Timestamp and tonumber(msg.Timestamp) or nil - - local knownAddressTags = { - "Recipient", - "Initiator", - "Target", - "Source", - "Address", - "Vault-Id", - "Process-Id", - "Observer-Address", - } - - for _, tagName in ipairs(knownAddressTags) do - -- Format all incoming addresses - msg.Tags[tagName] = msg.Tags[tagName] and utils.formatAddress(msg.Tags[tagName]) or nil - end - - local knownNumberTags = { - "Quantity", - "Lock-Length", - "Operator-Stake", - "Delegated-Stake", - "Withdraw-Stake", - "Timestamp", - "Years", - "Min-Delegated-Stake", - "Port", - "Extend-Length", - "Delegate-Reward-Share-Ratio", - "Epoch-Index", - "Price-Interval-Ms", - "Block-Height", - } - for _, tagName in ipairs(knownNumberTags) do - -- Format all incoming numbers - msg.Tags[tagName] = msg.Tags[tagName] and tonumber(msg.Tags[tagName]) or nil - end - - local knownBooleanTags = { - "Allow-Unsafe-Addresses", - "Force-Prune", - } - for _, tagName in ipairs(knownBooleanTags) do - msg.Tags[tagName] = utils.booleanOrBooleanStringToBoolean(msg.Tags[tagName]) - end - if msg.Tags["Force-Prune"] then gar.scheduleNextGatewaysPruning(0) gar.scheduleNextRedelegationsPruning(0) @@ -492,9 +475,8 @@ end, function(msg) vaults.scheduleNextVaultsPruning(0) end - local msgId = msg.Id - print("Pruning state at timestamp: " .. msgTimestamp) - local prunedStateResult = prune.pruneState(msgTimestamp, msgId, LastGracePeriodEntryEndTimestamp) + print("Pruning state at timestamp: " .. msg.Timestamp) + local prunedStateResult = prune.pruneState(msg.Timestamp, msg.Id, LastGracePeriodEntryEndTimestamp) if prunedStateResult then local prunedRecordsCount = utils.lengthOfTable(prunedStateResult.prunedRecords or {}) @@ -573,7 +555,7 @@ end, function(msg) end return prunedStateResult -end, CRITICAL) +end, CRITICAL, false) -- Write handlers addEventingHandler(ActionMap.Transfer, utils.hasMatchingTag("Action", ActionMap.Transfer), function(msg) diff --git a/src/utils.lua b/src/utils.lua index 48a37430..6faeb564 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -256,11 +256,18 @@ function utils.isValidArweaveAddress(address) return type(address) == "string" and #address == 43 and string.match(address, "^[%w-_]+$") ~= nil end ---- Checks if an address is a valid Ethereum address +--- Checks if an address looks like an unformatted Ethereum address +--- @param address string The address to check +--- @return boolean isValidUnformattedEthAddress - whether the address is a valid unformatted Ethereum address +function utils.isValidUnformattedEthAddress(address) + return type(address) == "string" and #address == 42 and string.match(address, "^0x[%x]+$") ~= nil +end + +--- Checks if an address is a valid Ethereum address and is in EIP-55 checksum format --- @param address string The address to check --- @return boolean isValidEthAddress - whether the address is a valid Ethereum address function utils.isValidEthAddress(address) - return type(address) == "string" and #address == 42 and string.match(address, "^0x[%x]+$") ~= nil + return utils.isValidUnformattedEthAddress(address) and address == utils.formatEIP55Address(address) end function utils.isValidUnsafeAddress(address) @@ -318,7 +325,7 @@ end --- @param address string The address to format --- @return string formattedAddress - the EIP-55 checksum formatted address function utils.formatAddress(address) - if utils.isValidEthAddress(address) then + if utils.isValidUnformattedEthAddress(address) then return utils.formatEIP55Address(address) end return address @@ -611,6 +618,82 @@ function utils.filterDictionary(tbl, predicate) return filtered end +--- Sanitizes inputs to ensure they are valid strings +--- @param table table The table to sanitize +--- @return table sanitizedTable - the sanitized table +function utils.validateAndSanitizeInputs(table) + assert(type(table) == "table", "Table must be a table") + local sanitizedTable = {} + for key, value in pairs(table) do + assert(type(key) == "string", "Key must be a string") + assert( + type(value) == "string" or type(value) == "number" or type(value) == "boolean", + "Value must be a string, integer, or boolean" + ) + if type(value) == "string" then + assert(#key > 0, "Key cannot be empty") + assert(#value > 0, "Value cannot be empty") + assert(not string.match(key, "^%s+$"), "Key cannot be only whitespace") + assert(not string.match(value, "^%s+$"), "Value cannot be only whitespace") + end + if type(value) == "boolean" then + assert(value == true or value == false, "Boolean value must be true or false") + end + if type(value) == "number" then + assert(utils.isInteger(value), "Number must be an integer") + end + sanitizedTable[key] = value + end + + local knownAddressTags = { + "Recipient", + "Initiator", + "Target", + "Source", + "Address", + "Vault-Id", + "Process-Id", + "Observer-Address", + } + + for _, tagName in ipairs(knownAddressTags) do + -- Format all incoming addresses + sanitizedTable[tagName] = sanitizedTable[tagName] and utils.formatAddress(sanitizedTable[tagName]) or nil + end + + local knownNumberTags = { + "Quantity", + "Lock-Length", + "Operator-Stake", + "Delegated-Stake", + "Withdraw-Stake", + "Timestamp", + "Years", + "Min-Delegated-Stake", + "Port", + "Extend-Length", + "Delegate-Reward-Share-Ratio", + "Epoch-Index", + "Price-Interval-Ms", + "Block-Height", + } + for _, tagName in ipairs(knownNumberTags) do + -- Format all incoming numbers + sanitizedTable[tagName] = sanitizedTable[tagName] and tonumber(sanitizedTable[tagName]) or nil + end + + local knownBooleanTags = { + "Allow-Unsafe-Addresses", + "Force-Prune", + } + for _, tagName in ipairs(knownBooleanTags) do + sanitizedTable[tagName] = sanitizedTable[tagName] + and utils.booleanOrBooleanStringToBoolean(sanitizedTable[tagName]) + or nil + end + return sanitizedTable +end + --- @param value string|boolean --- @return boolean function utils.booleanOrBooleanStringToBoolean(value) diff --git a/tests/arns.test.mjs b/tests/arns.test.mjs index a3f407fd..9d60fcd6 100644 --- a/tests/arns.test.mjs +++ b/tests/arns.test.mjs @@ -74,6 +74,7 @@ describe('ArNS', async () => { { name: 'Process-Id', value: processId }, { name: 'Years', value: '1' }, ], + Timestamp: STUB_TIMESTAMP, }, memory, }); @@ -359,7 +360,6 @@ describe('ArNS', async () => { { name: 'Quantity', value: `${650000000}` }, // delegate all of their balance { name: 'Address', value: STUB_OPERATOR_ADDRESS }, // our gateway address ], - Timestamp: STUB_TIMESTAMP + 1, }, memory, }); @@ -687,6 +687,7 @@ describe('ArNS', async () => { memory, transferQty: 700000000, // 600000000 for name purchase + 100000000 for extending the lease stakeQty: 650000000, // delegate most of their balance so that name purchase uses balance and stakes + timestamp: STUB_TIMESTAMP, }); memory = stakeResult.memory; @@ -821,6 +822,7 @@ describe('ArNS', async () => { transferQty: 3_100_000_000, // 60,000,0000 for name purchase + 2,500,000,000 for upgrading the name stakeQty: 3_100_000_000 - 50_000_000, // delegate most of their balance so that name purchase uses balance and stakes stakerAddress: STUB_ADDRESS, + timestamp: STUB_TIMESTAMP, }); memory = stakeResult.memory; @@ -1055,6 +1057,7 @@ describe('ArNS', async () => { const balancesResult = await handle({ options: { Tags: [{ name: 'Action', value: 'Balances' }], + Timestamp: newBuyTimestamp, }, memory: newBuyResult.Memory, }); @@ -1064,6 +1067,7 @@ describe('ArNS', async () => { initialRecord.purchasePrice + expectedRewardForProtocol; const balances = JSON.parse(balancesResult.Messages[0].Data); + assert.equal(balances[initiator], expectedRewardForInitiator); assert.equal(balances[PROCESS_ID], expectedProtocolBalance); assert.equal(balances[newBuyerAddress], 0); @@ -1075,6 +1079,7 @@ describe('ArNS', async () => { processId: ''.padEnd(43, 'a'), type: 'lease', years: 1, + timestamp: STUB_TIMESTAMP, memory: sharedMemory, }); @@ -1148,6 +1153,7 @@ describe('ArNS', async () => { recipient: bidderAddress, quantity: expectedPurchasePrice, memory: tickResult.Memory, + timestamp: bidTimestamp, }); let memoryToUse = transferMemory; @@ -1158,6 +1164,7 @@ describe('ArNS', async () => { transferQty: 0, stakeQty: expectedPurchasePrice, stakerAddress: bidderAddress, + timestamp: bidTimestamp, }); memoryToUse = stakeResult.memory; } @@ -1291,6 +1298,7 @@ describe('ArNS', async () => { const balancesResult = await handle({ options: { Tags: [{ name: 'Action', value: 'Balances' }], + Timestamp: bidTimestamp, }, memory: buyReturnedNameResult.Memory, }); @@ -1559,6 +1567,7 @@ describe('ArNS', async () => { describe('Paginated-Records', () => { it('should paginate records correctly', async () => { // buy 3 records + let lastTimestamp = STUB_TIMESTAMP; let buyRecordsMemory = sharedMemory; // updated after each purchase const recordsCount = 3; for (let i = 0; i < recordsCount; i++) { @@ -1569,11 +1578,12 @@ describe('ArNS', async () => { { name: 'Name', value: `test-name-${i}` }, { name: 'Process-Id', value: ''.padEnd(43, `${i}`) }, ], - Timestamp: STUB_TIMESTAMP + i * 1000, // order of names is based on timestamp + Timestamp: lastTimestamp + i * 1000, // order of names is based on timestamp }, memory: buyRecordsMemory, }); buyRecordsMemory = buyRecordsResult.Memory; + lastTimestamp = lastTimestamp + i * 1000; } // call the paginated records handler repeatedly until all records are fetched @@ -1587,6 +1597,7 @@ describe('ArNS', async () => { { name: 'Cursor', value: cursor }, { name: 'Limit', value: 1 }, ], + Timestamp: lastTimestamp, }, memory: buyRecordsMemory, }); @@ -1661,6 +1672,7 @@ describe('ArNS', async () => { { name: 'Action', value: 'Gateway' }, { name: 'Address', value: joinedGateway }, ], + Timestamp: afterDistributionTimestamp, }, memory: firstTickAndDistribution.Memory, }); @@ -1680,7 +1692,7 @@ describe('ArNS', async () => { const transferMemory = await transfer({ recipient: nonEligibleAddress, quantity: 200_000_000_000, - timestamp: afterDistributionTimestamp - 1, + timestamp: afterDistributionTimestamp, memory: firstTickAndDistribution.Memory, }); arnsDiscountMemory = transferMemory; @@ -1725,6 +1737,7 @@ describe('ArNS', async () => { { name: 'Intent', value: 'Buy-Record' }, { name: 'Name', value: 'test-name' }, ], + Timestamp: afterDistributionTimestamp, }, memory: arnsDiscountMemory, }); @@ -1918,6 +1931,7 @@ describe('ArNS', async () => { From: nonEligibleAddress, Owner: nonEligibleAddress, Tags: upgradeToPermabuyTags, + Timestamp: upgradeToPermabuyTimestamp, }, memory: buyRecordResult.Memory, }); diff --git a/tests/gar.test.mjs b/tests/gar.test.mjs index 664161fc..a1a02d96 100644 --- a/tests/gar.test.mjs +++ b/tests/gar.test.mjs @@ -45,6 +45,7 @@ describe('GatewayRegistry', async () => { const STUB_ADDRESS_9 = ''.padEnd(43, '9'); let sharedMemory = startMemory; // memory we'll use across unique tests; + let lastTimestamp = STUB_TIMESTAMP; beforeEach(async () => { const { Memory: totalTokenSupplyMemory } = await totalTokenSupply({ @@ -56,11 +57,12 @@ describe('GatewayRegistry', async () => { }); // NOTE: all tests will start with this gateway joined to the network - use `sharedMemory` for the first interaction for each test to avoid having to join the network again sharedMemory = joinNetworkMemory; + lastTimestamp = STUB_TIMESTAMP + 1000 * 60; // Default 60s after the stubbed timestamp, some tests will override this }); afterEach(async () => { await assertNoInvariants({ - timestamp: STUB_TIMESTAMP, + timestamp: lastTimestamp, memory: sharedMemory, }); }); @@ -320,6 +322,7 @@ describe('GatewayRegistry', async () => { const gateway = await getGateway({ memory: sharedMemory, address: STUB_ADDRESS, + timestamp: STUB_TIMESTAMP, }); // leave at timestamp @@ -334,6 +337,7 @@ describe('GatewayRegistry', async () => { const leavingGateway = await getGateway({ memory: leaveNetworkMemory, address: STUB_ADDRESS, + timestamp: leavingTimestamp, }); assert.deepStrictEqual(leavingGateway, { ...gateway, @@ -347,6 +351,7 @@ describe('GatewayRegistry', async () => { await getGatewayVaultsItems({ memory: leaveNetworkMemory, gatewayAddress: STUB_ADDRESS, + timestamp: leavingTimestamp, }), [ { @@ -360,6 +365,7 @@ describe('GatewayRegistry', async () => { ); sharedMemory = leaveNetworkMemory; + lastTimestamp = leavingTimestamp; }); }); @@ -372,11 +378,13 @@ describe('GatewayRegistry', async () => { expectedDelegates, expectedAllowedDelegates, inputMemory = sharedMemory, + timestamp = STUB_TIMESTAMP, }) { // gateway before const gateway = await getGateway({ address: STUB_ADDRESS, memory: inputMemory, + timestamp, }); const { memory: updatedSettingsMemory } = await updateGatewaySettings({ @@ -386,12 +394,14 @@ describe('GatewayRegistry', async () => { { name: 'Action', value: 'Update-Gateway-Settings' }, ...settingsTags, ], + timestamp, }); // check the gateway record from contract const updatedGateway = await getGateway({ address: STUB_ADDRESS, memory: updatedSettingsMemory, + timestamp, }); // should match old gateway, with new settings @@ -409,10 +419,10 @@ describe('GatewayRegistry', async () => { for (const delegateAddress of delegateAddresses) { const maybeDelegateResult = await delegateStake({ memory: nextMemory, - timestamp: STUB_TIMESTAMP, delegatorAddress: delegateAddress, quantity: 10_000_000, gatewayAddress: STUB_ADDRESS, + timestamp, }).catch(() => {}); if (maybeDelegateResult?.memory) { nextMemory = maybeDelegateResult.memory; @@ -421,6 +431,7 @@ describe('GatewayRegistry', async () => { const updatedGatewayDelegates = await getDelegatesItems({ memory: nextMemory, gatewayAddress: STUB_ADDRESS, + timestamp, }); assert.deepStrictEqual( updatedGatewayDelegates @@ -432,7 +443,7 @@ describe('GatewayRegistry', async () => { await getAllowedDelegates({ memory: nextMemory, from: STUB_ADDRESS, - timestamp: STUB_TIMESTAMP, + timestamp, gatewayAddress: STUB_ADDRESS, }); const updatedAllowedDelegates = JSON.parse( @@ -535,6 +546,7 @@ describe('GatewayRegistry', async () => { gatewayAddress: STUB_ADDRESS, }); const updatedMemory = await updateGatewaySettingsTest({ + timestamp: STUB_TIMESTAMP + 1, inputMemory: stakedMemory, settingsTags: [ { name: 'Allow-Delegated-Staking', value: 'allowlist' }, @@ -561,11 +573,12 @@ describe('GatewayRegistry', async () => { Tags: [ { name: 'Action', value: 'Paginated-Delegations' }, { name: 'Limit', value: '100' }, - { name: 'Sort-Order', value: 'desc' }, + { name: 'Sort-Order', value: 'asc' }, { name: 'Sort-By', value: 'startTimestamp' }, ], }, memory: updatedMemory, + timestamp: STUB_TIMESTAMP + 5, }); assertNoResultError(delegationsResult); assert.deepStrictEqual( @@ -581,11 +594,11 @@ describe('GatewayRegistry', async () => { { type: 'vault', gatewayAddress: STUB_ADDRESS, - delegationId: `${STUB_ADDRESS}_${STUB_TIMESTAMP}`, + delegationId: `${STUB_ADDRESS}_${STUB_TIMESTAMP + 1}`, vaultId: 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm', balance: 10_000_000, - endTimestamp: 90 * 24 * 60 * 60 * 1000 + STUB_TIMESTAMP, - startTimestamp: STUB_TIMESTAMP, + endTimestamp: 90 * 24 * 60 * 60 * 1000 + STUB_TIMESTAMP + 1, + startTimestamp: STUB_TIMESTAMP + 1, }, ], JSON.parse(delegationsResult.Messages[0].Data).items, @@ -603,6 +616,7 @@ describe('GatewayRegistry', async () => { delegateAddresses: [STUB_ADDRESS_6], // not allowed to delegate expectedDelegates: [STUB_ADDRESS_7, STUB_ADDRESS_8, STUB_ADDRESS_9], // Leftover from previous test and being forced to exit expectedAllowedDelegates: [], + timestamp: STUB_TIMESTAMP + 3, }); }); @@ -625,11 +639,13 @@ describe('GatewayRegistry', async () => { delegateAddresses: [STUB_ADDRESS_9], // no one is allowed yet expectedDelegates: [STUB_ADDRESS_8], // 8 is exiting expectedAllowedDelegates: [], + timestamp: STUB_TIMESTAMP + 1, }); const delegateItems = await getDelegatesItems({ memory: updatedMemory, gatewayAddress: STUB_ADDRESS, + timestamp: STUB_TIMESTAMP + 1, }); assert.deepStrictEqual( [ @@ -645,7 +661,7 @@ describe('GatewayRegistry', async () => { const { result: getAllowedDelegatesResult } = await getAllowedDelegates({ memory: updatedMemory, from: STUB_ADDRESS, - timestamp: STUB_TIMESTAMP, + timestamp: STUB_TIMESTAMP + 1, gatewayAddress: STUB_ADDRESS, }); assert.deepStrictEqual( @@ -722,6 +738,7 @@ describe('GatewayRegistry', async () => { const updatedGateway = await getGateway({ address: STUB_ADDRESS, memory: decreaseStakeMemory, + timestamp: decreaseTimestamp, }); assert.deepStrictEqual(updatedGateway, { ...gatewayBefore, @@ -731,6 +748,7 @@ describe('GatewayRegistry', async () => { await getGatewayVaultsItems({ memory: decreaseStakeMemory, gatewayAddress: STUB_ADDRESS, + timestamp: decreaseTimestamp, }), [ { @@ -878,6 +896,7 @@ describe('GatewayRegistry', async () => { const gatewayAfter = await getGateway({ address: STUB_ADDRESS, memory: delegatedStakeMemory, + timestamp: delegationTimestamp, }); assert.deepStrictEqual(gatewayAfter, { ...gatewayBefore, @@ -886,6 +905,7 @@ describe('GatewayRegistry', async () => { const delegateItems = await getDelegatesItems({ memory: delegatedStakeMemory, gatewayAddress: STUB_ADDRESS, + timestamp: delegationTimestamp, }); assert.deepStrictEqual( [ @@ -917,6 +937,7 @@ describe('GatewayRegistry', async () => { const gatewayBefore = await getGateway({ address: STUB_ADDRESS, memory: delegatedStakeMemory, + timestamp: STUB_TIMESTAMP, }); const { memory: decreaseStakeMemory } = await decreaseDelegateStake({ memory: delegatedStakeMemory, @@ -930,6 +951,7 @@ describe('GatewayRegistry', async () => { const gatewayAfter = await getGateway({ address: STUB_ADDRESS, memory: decreaseStakeMemory, + timestamp: decreaseStakeTimestamp, }); assert.deepStrictEqual(gatewayAfter, { ...gatewayBefore, @@ -938,6 +960,7 @@ describe('GatewayRegistry', async () => { const delegateItems = await getDelegatesItems({ memory: decreaseStakeMemory, gatewayAddress: STUB_ADDRESS, + timestamp: decreaseStakeTimestamp, }); assert.deepStrictEqual(delegateItems, [ @@ -954,6 +977,7 @@ describe('GatewayRegistry', async () => { const delegationsForDelegator = await getDelegations({ memory: decreaseStakeMemory, address: delegatorAddress, + timestamp: decreaseStakeTimestamp, }); assert.deepStrictEqual(delegationsForDelegator.items, [ { @@ -974,6 +998,7 @@ describe('GatewayRegistry', async () => { }, ]); sharedMemory = decreaseStakeMemory; + lastTimestamp = decreaseStakeTimestamp; }); it('should fail to withdraw a delegated stake if below the minimum withdrawal limitation', async () => { @@ -993,6 +1018,7 @@ describe('GatewayRegistry', async () => { const gatewayBefore = await getGateway({ address: STUB_ADDRESS, memory: delegatedStakeMemory, + timestamp: STUB_TIMESTAMP, }); const { memory: decreaseStakeMemory, result } = await decreaseDelegateStake({ @@ -1018,9 +1044,11 @@ describe('GatewayRegistry', async () => { const gatewayAfter = await getGateway({ address: STUB_ADDRESS, memory: decreaseStakeMemory, + timestamp: decreaseStakeTimestamp, }); assert.deepStrictEqual(gatewayAfter, gatewayBefore); sharedMemory = decreaseStakeMemory; + lastTimestamp = decreaseStakeTimestamp; }); }); @@ -1062,11 +1090,14 @@ describe('GatewayRegistry', async () => { const gatewayAfter = await getGateway({ address: STUB_ADDRESS, memory: cancelWithdrawalMemory, + timestamp: decreaseStakeTimestamp, }); // no changes to the gateway after a withdrawal is cancelled assert.deepStrictEqual(gatewayAfter, gatewayBefore); sharedMemory = cancelWithdrawalMemory; + lastTimestamp = decreaseStakeTimestamp; }); + it('should allow cancelling an operator withdrawal', async () => { const decreaseStakeTimestamp = STUB_TIMESTAMP + 1000 * 60 * 15; // 15 minutes after stubbedTimestamp const stakeQty = INITIAL_OPERATOR_STAKE; @@ -1105,6 +1136,7 @@ describe('GatewayRegistry', async () => { const gatewayAfter = await getGateway({ address: STUB_ADDRESS, memory: cancelWithdrawalMemory, + timestamp: decreaseStakeTimestamp, }); // no changes to the gateway after a withdrawal is cancelled assert.deepStrictEqual(gatewayAfter, { @@ -1112,6 +1144,7 @@ describe('GatewayRegistry', async () => { operatorStake: INITIAL_OPERATOR_STAKE + decreaseQty, // the decrease was cancelled and returned to the operator }); sharedMemory = cancelWithdrawalMemory; + lastTimestamp = decreaseStakeTimestamp; }); }); @@ -1196,7 +1229,6 @@ describe('GatewayRegistry', async () => { const { memory: addGatewayMemory2 } = await joinNetwork({ address: secondGatewayAddress, memory: sharedMemory, - timestamp: STUB_TIMESTAMP - 1, }); let cursor; let fetchedGateways = []; @@ -1403,7 +1435,6 @@ describe('GatewayRegistry', async () => { const { memory: addGatewayMemory2 } = await joinNetwork({ address: secondGatewayAddress, memory: sharedMemory, - timestamp: STUB_TIMESTAMP - 1, }); // Stake to both gateways @@ -1447,6 +1478,7 @@ describe('GatewayRegistry', async () => { { name: 'Sort-Order', value: sortOrder }, ...(cursor ? [{ name: 'Cursor', value: `${cursor}` }] : []), ], + Timestamp: STUB_TIMESTAMP + 2, }, memory: decreaseStakeMemory, }); @@ -1604,6 +1636,7 @@ describe('GatewayRegistry', async () => { const delegateItems = await getDelegatesItems({ memory: delegatedStakeMemory, gatewayAddress: sourceAddress, + timestamp: STUB_TIMESTAMP, }); assert.deepStrictEqual( [ @@ -1728,6 +1761,7 @@ describe('GatewayRegistry', async () => { const delegateItems = await getDelegatesItems({ memory: delegatedStakeMemory, gatewayAddress: sourceAddress, + timestamp: STUB_TIMESTAMP, }); assert.deepStrictEqual( [ @@ -1770,6 +1804,7 @@ describe('GatewayRegistry', async () => { await getDelegatesItems({ memory: redelegateStakeMemory, gatewayAddress: targetAddress, + timestamp: STUB_TIMESTAMP + 2, }), [ { @@ -1790,6 +1825,7 @@ describe('GatewayRegistry', async () => { await getDelegatesItems({ memory: redelegateStakeMemory, gatewayAddress: sourceAddress, + timestamp: STUB_TIMESTAMP + 2, }), [], ); diff --git a/tests/handlers.test.mjs b/tests/handlers.test.mjs index 1b2492dc..8e2e2090 100644 --- a/tests/handlers.test.mjs +++ b/tests/handlers.test.mjs @@ -21,15 +21,18 @@ describe('handlers', async () => { const { Handlers: handlersList } = JSON.parse(handlers.Messages[0].Data); assert.ok(handlersList.includes('_eval')); assert.ok(handlersList.includes('_default')); + assert.ok(handlersList.includes('sanitize')); assert.ok(handlersList.includes('prune')); const evalIndex = handlersList.indexOf('_eval'); const defaultIndex = handlersList.indexOf('_default'); + const sanitizeIndex = handlersList.indexOf('sanitize'); const pruneIndex = handlersList.indexOf('prune'); - const expectedHandlerCount = 71; // TODO: update this if more handlers are added + const expectedHandlerCount = 72; // TODO: update this if more handlers are added assert.ok(evalIndex === 0); assert.ok(defaultIndex === 1); - assert.ok(pruneIndex === 2); + assert.ok(sanitizeIndex === 2); + assert.ok(pruneIndex === 3); assert.ok( handlersList.length === expectedHandlerCount, 'should have ' + diff --git a/tests/helpers.mjs b/tests/helpers.mjs index 1319c11d..cda56bfd 100644 --- a/tests/helpers.mjs +++ b/tests/helpers.mjs @@ -39,7 +39,10 @@ export async function handle({ options = {}, memory = startMemory, shouldAssertNoResultError = true, + timestamp = STUB_TIMESTAMP, }) { + options.Timestamp ??= timestamp; + const result = await originalHandle( memory, { @@ -96,8 +99,8 @@ export const getBalances = async ({ memory, timestamp = STUB_TIMESTAMP }) => { const result = await handle({ options: { Tags: [{ name: 'Action', value: 'Balances' }], - Timestamp: timestamp, }, + timestamp, memory, }); @@ -119,6 +122,7 @@ export const transfer = async ({ quantity = initialOperatorStake, memory = startMemory, cast = false, + timestamp = STUB_TIMESTAMP, } = {}) => { if (quantity === 0) { // Nothing to do @@ -135,6 +139,7 @@ export const transfer = async ({ { name: 'Quantity', value: quantity }, { name: 'Cast', value: cast }, ], + Timestamp: timestamp, }, memory, }); @@ -149,11 +154,11 @@ export const joinNetwork = async ({ tags = validGatewayTags({ observerAddress }), quantity = 100_000_000_000, }) => { - // give them the join network token amount const transferMemory = await transfer({ recipient: address, quantity, memory, + timestamp, }); const joinNetworkResult = await handle({ options: { @@ -186,6 +191,7 @@ export const setUpStake = async ({ quantity: transferQty, memory, cast: true, + timestamp, }); // Stake a gateway for the user to delegate to @@ -193,7 +199,7 @@ export const setUpStake = async ({ memory, address: gatewayAddress, tags: gatewayTags, - timestamp: timestamp - 1, + timestamp: timestamp, }); assertNoResultError(joinNetworkResult); memory = joinNetworkResult.memory; @@ -270,17 +276,21 @@ export const getDelegates = async ({ }; }; -export const getDelegatesItems = async ({ memory, gatewayAddress }) => { +export const getDelegatesItems = async ({ + memory, + gatewayAddress, + timestamp = STUB_TIMESTAMP, +}) => { const { result } = await getDelegates({ memory, from: STUB_ADDRESS, - timestamp: STUB_TIMESTAMP, + timestamp, gatewayAddress, }); return JSON.parse(result.Messages?.[0]?.Data).items; }; -export const getDelegations = async ({ memory, address }) => { +export const getDelegations = async ({ memory, address, timestamp }) => { const result = await handle({ options: { Tags: [ @@ -289,6 +299,7 @@ export const getDelegations = async ({ memory, address }) => { ], }, memory, + timestamp, }); return JSON.parse(result.Messages?.[0]?.Data); }; @@ -299,6 +310,7 @@ export const getVaults = async ({ limit, sortBy, sortOrder, + timestamp = STUB_TIMESTAMP, }) => { const { Memory, ...rest } = await handle({ options: { @@ -309,6 +321,7 @@ export const getVaults = async ({ ...(sortBy ? [{ name: 'Sort-By', value: sortBy }] : []), ...(sortOrder ? [{ name: 'Sort-Order', value: sortOrder }] : []), ], + Timestamp: timestamp, }, memory, }); @@ -318,13 +331,18 @@ export const getVaults = async ({ }; }; -export const getGatewayVaultsItems = async ({ memory, gatewayAddress }) => { +export const getGatewayVaultsItems = async ({ + memory, + gatewayAddress, + timestamp = STUB_TIMESTAMP, +}) => { const gatewayVaultsResult = await handle({ options: { Tags: [ { name: 'Action', value: 'Paginated-Gateway-Vaults' }, { name: 'Address', value: gatewayAddress }, ], + Timestamp: timestamp, }, memory, }); @@ -422,6 +440,7 @@ export const delegateStake = async ({ recipient: delegatorAddress, quantity, memory, + timestamp, }); const delegateResult = await handle({ @@ -455,8 +474,8 @@ export const getGateway = async ({ { name: 'Action', value: 'Gateway' }, { name: 'Address', value: address }, ], - Timestamp: timestamp, }, + timestamp, memory, }); const gateway = JSON.parse(gatewayResult.Messages?.[0]?.Data); @@ -705,8 +724,8 @@ export const buyRecord = async ({ { name: 'Process-Id', value: processId }, { name: 'Years', value: `${years}` }, ], - Timestamp: timestamp, }, + timestamp, memory, }); return { diff --git a/tests/invariants.mjs b/tests/invariants.mjs index 7efe43a3..b962bac0 100644 --- a/tests/invariants.mjs +++ b/tests/invariants.mjs @@ -45,6 +45,7 @@ async function assertNoBalanceVaultInvariants({ timestamp, memory }) { const { result } = await getVaults({ memory, limit: 1_000_000, // egregiously large limit to make sure we get them all + timestamp, }); for (const vault of JSON.parse(result.Messages?.[0]?.Data).items) { diff --git a/tests/primary.test.mjs b/tests/primary.test.mjs index 35f3669f..1b83e263 100644 --- a/tests/primary.test.mjs +++ b/tests/primary.test.mjs @@ -33,6 +33,7 @@ describe('primary names', function () { processId, type = 'permabuy', years = 1, + timestamp = STUB_TIMESTAMP, memory = sharedMemory, }) => { const buyRecordResult = await handle({ @@ -44,6 +45,7 @@ describe('primary names', function () { { name: 'Years', value: years }, { name: 'Process-Id', value: processId }, ], + Timestamp: timestamp, }, memory, }); @@ -66,6 +68,7 @@ describe('primary names', function () { recipient: caller, quantity: 100000000, // primary name cost memory, + timestamp, }); memory = transferMemory; } @@ -88,7 +91,7 @@ describe('primary names', function () { }; }; - const getPrimaryNameRequest = async ({ initiator, memory }) => { + const getPrimaryNameRequest = async ({ initiator, memory, timestamp }) => { const getPrimaryNameRequestResult = await handle({ options: { Tags: [ @@ -97,6 +100,7 @@ describe('primary names', function () { ], }, memory, + timestamp, }); return { result: getPrimaryNameRequestResult, @@ -130,11 +134,17 @@ describe('primary names', function () { }; }; - const removePrimaryNames = async ({ names, caller, memory }) => { + const removePrimaryNames = async ({ + names, + caller, + memory, + timestamp = STUB_TIMESTAMP, + }) => { const removePrimaryNamesResult = await handle({ options: { From: caller, Owner: caller, + Timestamp: timestamp, Tags: [ { name: 'Action', value: 'Remove-Primary-Names' }, { name: 'Names', value: names.join(',') }, @@ -151,6 +161,7 @@ describe('primary names', function () { const getPrimaryNameForAddress = async ({ address, memory, + timestamp = STUB_TIMESTAMP, shouldAssertNoResultError = true, }) => { const getPrimaryNameResult = await handle({ @@ -159,6 +170,7 @@ describe('primary names', function () { { name: 'Action', value: 'Primary-Name' }, { name: 'Address', value: address }, ], + Timestamp: timestamp, }, memory, shouldAssertNoResultError, @@ -169,13 +181,18 @@ describe('primary names', function () { }; }; - const getOwnerOfPrimaryName = async ({ name, memory }) => { + const getOwnerOfPrimaryName = async ({ + name, + memory, + timestamp = STUB_TIMESTAMP, + }) => { const getOwnerResult = await handle({ options: { Tags: [ { name: 'Action', value: 'Primary-Name' }, { name: 'Name', value: name }, ], + Timestamp: timestamp, }, memory, }); @@ -188,9 +205,11 @@ describe('primary names', function () { it('should allow creating and approving a primary name for an existing base name when the recipient is not the base name owner and is funding from stakes', async function () { const processId = ''.padEnd(43, 'a'); const recipient = ''.padEnd(43, 'b'); - const { memory: buyRecordMemory } = await buyRecord({ + const requestTimestamp = 1234567890; + const { memory: buyRecordMemory, result } = await buyRecord({ name: 'test-name', processId, + timestamp: requestTimestamp, }); const stakeResult = await setUpStake({ @@ -198,12 +217,13 @@ describe('primary names', function () { stakerAddress: recipient, transferQty: 550000000, stakeQty: 500000000, + timestamp: requestTimestamp, }); const { result: requestPrimaryNameResult } = await requestPrimaryName({ name: 'test-name', caller: recipient, - timestamp: 1234567890, + timestamp: requestTimestamp, memory: stakeResult.memory, fundFrom: 'stakes', }); @@ -235,6 +255,7 @@ describe('primary names', function () { { initiator: recipient, memory: requestPrimaryNameResult.Memory, + timestamp: requestTimestamp, }, ); @@ -315,6 +336,7 @@ describe('primary names', function () { await getPrimaryNameForAddress({ address: recipient, memory: approvePrimaryNameRequestResult.Memory, + timestamp: approvedTimestamp, }); const primaryNameLookupResult = JSON.parse( @@ -329,6 +351,7 @@ describe('primary names', function () { const { result: ownerOfPrimaryNameResult } = await getOwnerOfPrimaryName({ name: 'test-name', memory: approvePrimaryNameRequestResult.Memory, + timestamp: approvedTimestamp, }); const ownerResult = JSON.parse(ownerOfPrimaryNameResult.Messages[0].Data); @@ -340,9 +363,11 @@ describe('primary names', function () { it('should immediately approve a primary name for an existing base name when the caller of the request is the base name owner', async function () { const processId = ''.padEnd(43, 'a'); + const requestTimestamp = 1234567890; const { memory: buyRecordMemory } = await buyRecord({ name: 'test-name', processId, + timestamp: requestTimestamp, }); const approvalTimestamp = 1234567899; @@ -421,6 +446,7 @@ describe('primary names', function () { await getPrimaryNameForAddress({ address: processId, memory: requestPrimaryNameResult.Memory, + timestamp: approvalTimestamp, }); const primaryNameLookupResult = JSON.parse( @@ -435,6 +461,7 @@ describe('primary names', function () { const { result: ownerOfPrimaryNameResult } = await getOwnerOfPrimaryName({ name: 'test-name', memory: requestPrimaryNameResult.Memory, + timestamp: approvalTimestamp, }); const ownerResult = JSON.parse(ownerOfPrimaryNameResult.Messages[0].Data); @@ -447,15 +474,17 @@ describe('primary names', function () { it('should allow removing a primary named by the owner or the owner of the base record', async function () { const processId = ''.padEnd(43, 'a'); const recipient = ''.padEnd(43, 'b'); + const requestTimestamp = 1234567890; const { memory: buyRecordMemory } = await buyRecord({ name: 'test-name', processId, + timestamp: requestTimestamp, }); // create a primary name claim const { result: requestPrimaryNameResult } = await requestPrimaryName({ name: 'test-name', caller: recipient, - timestamp: 1234567890, + timestamp: requestTimestamp, memory: buyRecordMemory, }); // claim the primary name @@ -464,7 +493,7 @@ describe('primary names', function () { name: 'test-name', caller: processId, recipient: recipient, - timestamp: 1234567890, + timestamp: requestTimestamp, memory: requestPrimaryNameResult.Memory, }); @@ -473,6 +502,7 @@ describe('primary names', function () { names: ['test-name'], caller: processId, memory: approvePrimaryNameRequestResult.Memory, + timestamp: requestTimestamp, }); // assert no error @@ -507,7 +537,7 @@ describe('primary names', function () { Action: 'Remove-Primary-Names', Cast: false, Cron: false, - 'Epoch-Index': -19657, + 'Epoch-Index': -5618, From: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'From-Formatted': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'Message-Id': 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm', @@ -515,7 +545,7 @@ describe('primary names', function () { 'Num-Removed-Primary-Names': 1, 'Removed-Primary-Names': ['test-name'], 'Removed-Primary-Name-Owners': [recipient], - Timestamp: 21600000, + Timestamp: requestTimestamp, 'Total-Primary-Name-Requests': 0, 'Total-Primary-Names': 0, }); @@ -524,6 +554,7 @@ describe('primary names', function () { await getPrimaryNameForAddress({ address: recipient, memory: removePrimaryNameResult.Memory, + timestamp: requestTimestamp, shouldAssertNoResultError: false, // we expect an error here, don't throw }); diff --git a/tests/tick.test.mjs b/tests/tick.test.mjs index 5ec35998..56e611ac 100644 --- a/tests/tick.test.mjs +++ b/tests/tick.test.mjs @@ -50,6 +50,7 @@ describe('Tick', async () => { recipient = STUB_ADDRESS, quantity = 100_000_000_000, memory = sharedMemory, + timestamp = STUB_TIMESTAMP, } = {}) => { const transferResult = await handle({ options: { @@ -63,6 +64,7 @@ describe('Tick', async () => { ], }, memory, + timestamp, }); // assert no error tag @@ -112,10 +114,8 @@ describe('Tick', async () => { buyRecordData.endTimestamp + 1000 * 60 * 60 * 24 * 14 + 1; const futureTickResult = await handle({ options: { - Tags: [ - { name: 'Action', value: 'Tick' }, - { name: 'Timestamp', value: futureTimestamp.toString() }, - ], + Tags: [{ name: 'Action', value: 'Tick' }], + Timestamp: futureTimestamp, }, memory: buyRecordResult.Memory, }); @@ -136,6 +136,7 @@ describe('Tick', async () => { { name: 'Action', value: 'Record' }, { name: 'Name', value: 'test-name' }, ], + Timestamp: futureTimestamp, }, memory: futureTickResult.Memory, }); @@ -213,10 +214,8 @@ describe('Tick', async () => { const futureTimestamp = leavingGateway.endTimestamp + 1; const futureTick = await handle({ options: { - Tags: [ - { name: 'Action', value: 'Tick' }, - { name: 'Timestamp', value: futureTimestamp.toString() }, - ], + Tags: [{ name: 'Action', value: 'Tick' }], + Timestamp: futureTimestamp, }, memory: leaveNetworkResult.Memory, }); @@ -225,6 +224,7 @@ describe('Tick', async () => { const prunedGateway = await getGateway({ memory: futureTick.Memory, address: STUB_ADDRESS, + timestamp: futureTimestamp, }); assert.deepEqual(undefined, prunedGateway); @@ -327,6 +327,7 @@ describe('Tick', async () => { const prunedVault = await handle({ options: { Tags: [{ name: 'Action', value: 'Vault' }], + Timestamp: futureTimestamp, }, memory: futureTick.Memory, shouldAssertNoResultError: false, @@ -344,6 +345,7 @@ describe('Tick', async () => { { name: 'Action', value: 'Balance' }, { name: 'Target', value: DEFAULT_HANDLE_OPTIONS.Owner }, ], + Timestamp: futureTimestamp, }, memory: futureTick.Memory, }); @@ -574,6 +576,7 @@ describe('Tick', async () => { const gateway = await getGateway({ memory: distributionTick.Memory, address: STUB_ADDRESS, + timestamp: distributionTimestamp, }); assert.deepStrictEqual(gateway, { status: 'joined', @@ -615,6 +618,7 @@ describe('Tick', async () => { const delegateItems = await getDelegatesItems({ memory: distributionTick.Memory, gatewayAddress: STUB_ADDRESS, + timestamp: distributionTimestamp, }); assert.deepEqual(delegateItems, [ { @@ -651,6 +655,7 @@ describe('Tick', async () => { recipient: fundedUser, quantity: 100_000_000_000_000, memory: genesisEpochTick.Memory, + timestamp: genesisEpochStart, }); // Buy records in this epoch @@ -662,6 +667,7 @@ describe('Tick', async () => { name: `test-name-${i}`, purchaseType: 'permabuy', processId: processId, + timestamp: genesisEpochStart, }); buyRecordMemory = buyRecordResult.Memory; } @@ -733,10 +739,7 @@ describe('Tick', async () => { const epochTimestamp = genesisEpochStart + (epochDurationMs + 1) * i; const { Memory } = await handle({ options: { - Tags: [ - { name: 'Action', value: 'Tick' }, - { name: 'Timestamp', value: epochTimestamp.toString() }, - ], + Tags: [{ name: 'Action', value: 'Tick' }], Timestamp: epochTimestamp, }, memory: tickMemory, diff --git a/tests/vaults.test.mjs b/tests/vaults.test.mjs index 7f64658d..49403b30 100644 --- a/tests/vaults.test.mjs +++ b/tests/vaults.test.mjs @@ -42,6 +42,7 @@ describe('Vaults', async () => { ], }, memory, + shouldAssertNoResultError: false, }); assertNoResultError(vault); // make sure it is a vault