From 0d96d3afa709c9d3ef7314ebcf1f8bf11b59ca13 Mon Sep 17 00:00:00 2001 From: atticusofsparta Date: Wed, 15 Jan 2025 14:55:43 -0600 Subject: [PATCH 1/2] fix(spec): update handlers to integrate with token spec --- src/common/main.lua | 114 ++++++++++++++----- src/common/utils.lua | 17 ++- test/balances.test.mjs | 8 +- test/specs/token.test.mjs | 230 ++++++++++++++++++++++++++++++++++++++ tools/constants.mjs | 2 + 5 files changed, 336 insertions(+), 35 deletions(-) create mode 100644 test/specs/token.test.mjs diff --git a/src/common/main.lua b/src/common/main.lua index e3c1812..e6dfd7f 100644 --- a/src/common/main.lua +++ b/src/common/main.lua @@ -88,40 +88,92 @@ function ant.init() createActionHandler(TokenSpecActionMap.Transfer, function(msg) local recipient = msg.Tags.Recipient - utils.validateOwner(msg.From) + if msg.From ~= Owner then + if msg.reply then + msg.reply({ + Action = "Transfer-Error", + ["Message-Id"] = msg.Id, + Error = "Insufficient Balance!", + }) + else + ao.send({ + Target = msg.From, + Action = "Transfer-Error", + ["Message-Id"] = msg.Id, + Error = "Insufficient Balance!", + }) + end + end balances.transfer(recipient, msg.Tags["Allow-Unsafe-Addresses"]) + if not msg.Cast then - ao.send(notices.debit(msg)) - ao.send(notices.credit(msg)) + if msg.reply then + msg.reply(notices.debit(msg)) + msg.reply(notices.credit(msg)) + else + ao.send(notices.debit(msg)) + ao.send(notices.credit(msg)) + end end end) createActionHandler(TokenSpecActionMap.Balance, function(msg) - local balRes = balances.balance(msg.Tags.Recipient or msg.From, msg.Tags["Allow-Unsafe-Addresses"]) - - ao.send({ - Target = msg.From, - Action = "Balance-Notice", - Balance = tostring(balRes), - Ticker = Ticker, - Address = msg.Tags.Recipient or msg.From, - Data = balRes, - }) + local addressToCheck = msg.Tags.Recipient or msg.Tags.Target or msg.From + local balRes = balances.balance(addressToCheck, msg.Tags["Allow-Unsafe-Addresses"]) + + if msg.reply then + msg.reply({ + Action = "Balance-Notice", + Balance = tostring(balRes), + Ticker = Ticker, + Account = addressToCheck, + Address = addressToCheck, + Data = tostring(balRes), + }) + else + ao.send({ + Target = msg.From, + Action = "Balance-Notice", + Balance = tostring(balRes), + Ticker = Ticker, + Address = msg.Tags.Recipient or msg.From, + Data = tostring(balRes), + }) + end end) - createActionHandler(TokenSpecActionMap.Balances, function() - return balances.balances() + createActionHandler(TokenSpecActionMap.Balances, function(msg) + local bals = {} + for k, v in pairs(balances.balances()) do + bals[k] = tostring(v) + end + + if msg.reply then + msg.reply({ + Data = json.encode(bals), + }) + else + ao.send({ Target = msg.From, Data = json.encode(bals) }) + end end) createActionHandler(TokenSpecActionMap.TotalSupply, function(msg) assert(msg.From ~= ao.id, "Cannot call Total-Supply from the same process!") - ao.send({ - Target = msg.From, - Action = "Total-Supply-Notice", - Data = TotalSupply, - Ticker = Ticker, - }) + if msg.reply then + msg.reply({ + Action = "Total-Supply", + Data = tostring(TotalSupply), + Ticker = Ticker, + }) + else + ao.send({ + Target = msg.From, + Action = "Total-Supply", + Data = tostring(TotalSupply), + Ticker = Ticker, + }) + end end) createActionHandler(TokenSpecActionMap.Info, function(msg) @@ -136,12 +188,20 @@ function ant.init() Owner = Owner, Handlers = utils.getHandlerNames(Handlers), } - ao.send({ - Target = msg.From, - Action = "Info-Notice", - Tags = info, - Data = json.encode(info), - }) + if msg.reply then + msg.reply({ + Action = "Info-Notice", + Tags = info, + Data = json.encode(info), + }) + else + ao.send({ + Target = msg.From, + Action = "Info-Notice", + Tags = info, + Data = json.encode(info), + }) + end end) -- ActionMap (ANT Spec) diff --git a/src/common/utils.lua b/src/common/utils.lua index 7726547..0ec9251 100644 --- a/src/common/utils.lua +++ b/src/common/utils.lua @@ -291,20 +291,29 @@ function utils.createHandler(tagName, tagValue, handler, position) return handler(msg) end, utils.errorHandler) + local resultNotice = nil if not handlerStatus then - ao.send(notices.addForwardedTags(msg, { + resultNotice = notices.addForwardedTags(msg, { Target = msg.From, Action = "Invalid-" .. tagValue .. "-Notice", Error = tagValue .. "-Error", ["Message-Id"] = msg.Id, Data = handlerRes, - })) + }) elseif handlerRes then - ao.send(notices.addForwardedTags(msg, { + resultNotice = notices.addForwardedTags(msg, { Target = msg.From, Action = tagValue .. "-Notice", Data = type(handlerRes) == "string" and handlerRes or json.encode(handlerRes), - })) + }) + end + + if resultNotice then + if msg.reply then + msg.reply(resultNotice) + else + ao.send(resultNotice) + end end local hasNewOwner = Owner ~= prevOwner diff --git a/test/balances.test.mjs b/test/balances.test.mjs index 9137b70..e3a7fab 100644 --- a/test/balances.test.mjs +++ b/test/balances.test.mjs @@ -71,7 +71,7 @@ describe('aos Balances', async () => { if (shouldPass === true) { const targetBalance = result.Messages[0].Data; - assert.equal(typeof targetBalance === 'number', shouldPass); + assert.equal(typeof targetBalance === 'string', shouldPass); } else { assert.strictEqual( result.Messages[0].Tags.find((t) => t.name === 'Error')?.value, @@ -97,7 +97,7 @@ describe('aos Balances', async () => { transferResult.Memory, ); const balances = JSON.parse(balancesResult.Messages[0].Data); - assert.equal(balances[target_address] === 1, shouldPass); + assert.equal(balances[target_address] === '1', shouldPass); } else { assert.strictEqual( transferResult.Messages[0].Tags.find((t) => t.name === 'Error') @@ -157,7 +157,7 @@ describe('aos Balances', async () => { const [ownerAddress, ownerBalance] = ownerEntry; assert(Object.entries(balances).length === 1); assert(ownerAddress === STUB_ADDRESS); - assert(ownerBalance === 1); + assert(ownerBalance === '1'); }); it('should set the logo of the ant', async () => { @@ -174,6 +174,6 @@ describe('aos Balances', async () => { }); it('should get total supply', async () => { const res = await getTotalSupply(); - assert.strictEqual(res, 1, 'total supply should be equal to 1'); + assert.strictEqual(res, '1', 'total supply should be equal to 1'); }); }); diff --git a/test/specs/token.test.mjs b/test/specs/token.test.mjs new file mode 100644 index 0000000..92850a2 --- /dev/null +++ b/test/specs/token.test.mjs @@ -0,0 +1,230 @@ +import { createAntAosLoader } from '../utils.mjs'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { + AO_LOADER_HANDLER_ENV, + DEFAULT_HANDLE_OPTIONS, + STUB_ADDRESS, +} from '../../tools/constants.mjs'; + +/** + * Background + * + * This test is for testing the compliance with https://github.com/permaweb/aos/blob/main/blueprints/token.lua "spec" + * + * The reason we attempt to comply to this is for integration with other platforms like bazar.arweave.net and botega.arweave.net + */ + +describe('Token spec compliance', async () => { + const { handle: originalHandle, memory: startMemory } = + await createAntAosLoader(); + + async function handle(options = {}, mem = startMemory) { + return originalHandle( + mem, + { + ...DEFAULT_HANDLE_OPTIONS, + ...options, + }, + AO_LOADER_HANDLER_ENV, + ); + } + + /** + * Handlers: + * - info + * - total supply + * - balance + * - balances + * - transfer + + We do not implement: + * - mint + * - burn + * + */ + + it('should get the process info', async () => { + const result = await handle({ + Tags: [{ name: 'Action', value: 'Info' }], + }); + /** + * Check for: + * - Name (string) + * - Ticker (string) + * - Logo (arweave ID) + * - Denomination (number as string) + */ + + const tags = result.Messages[0].Tags; + const ref = tags.find((t) => t.name === 'X-Reference')?.value; + const name = tags.find((t) => t.name === 'Name')?.value; + const ticker = tags.find((t) => t.name === 'Ticker')?.value; + const logo = tags.find((t) => t.name === 'Logo')?.value; + const denomination = tags.find((t) => t.name === 'Denomination')?.value; + + assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof ticker, 'string'); + assert.strictEqual(typeof logo, 'string'); + assert.strictEqual(typeof denomination, 'string'); + assert.strictEqual(Number.isInteger(parseInt(denomination)), true); + assert(ref, 'Not a reply message'); + }); + + it('should get the balance of an account', async () => { + /** + * Test with caller as: + * - Recipient + * - Target + * - From + * Test as: + * - Owner + * - Anonymous + * + * Check for: + * - Balance (string int) + * - Ticker (string) + * - Account (string equal to target address we are checking balance of) + * - Data (same as balance, both in type and qty) + */ + const targetAccount = ''.padEnd(43, 'target-0-'); + const targetAccountTags = [ + // Should return 0 + { name: 'Recipient', value: targetAccount }, + { name: 'Target', value: targetAccount }, + // happy path (owner calls for balance) should return 1 + { name: 'Recipient', value: STUB_ADDRESS }, + { name: 'Target', value: STUB_ADDRESS }, + null, // "equivalent to "from" + ]; + + for (const targetAccountTag of targetAccountTags) { + const result = await handle({ + Tags: [ + { name: 'Action', value: 'Balance' }, + ...(targetAccountTag ? [targetAccountTag] : []), + ], + }); + const tags = result.Messages[0].Tags; + const ref = tags.find((t) => t.name === 'X-Reference')?.value; + + const balanceData = result.Messages[0].Data; + const balanceTag = tags.find((t) => t.name === 'Balance')?.value; + const ticker = tags.find((t) => t.name === 'Ticker')?.value; + const account = tags.find((t) => t.name === 'Account')?.value; + + assert.strictEqual(typeof balanceData, 'string'); + assert.strictEqual(typeof balanceTag, 'string'); + assert.strictEqual(balanceData, balanceTag); + assert.strictEqual(Number.isInteger(parseInt(balanceData)), true); + assert.strictEqual(typeof ticker, 'string'); + assert.strictEqual(targetAccountTag?.value ?? STUB_ADDRESS, account); + assert(ref, 'Not a reply message'); + } + }); + + it('should get the balances', async () => { + /** + * Assert each balance is a string and int, + */ + const result = await handle({ + Tags: [{ name: 'Action', value: 'Balances' }], + }); + + const tags = result.Messages[0].Tags; + const ref = tags.find((t) => t.name === 'X-Reference')?.value; + assert(ref, 'Not a reply message'); + + for (const bal of Object.values(JSON.parse(result.Messages[0].Data))) { + assert.strictEqual(typeof bal, 'string'); + assert.strictEqual(Number.isInteger(parseInt(bal)), true); + } + }); + + it('should transfer', async () => { + /** + * Check as: + * - Owner (sufficient balance) + * - Not owner (will send error message) + */ + const recipient = ''.padEnd(43, 'recipient-1'); + const result = await handle({ + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: recipient }, + ], + }); + + const creditNotice = result.Messages.find(({ Tags }) => + Tags.find((t) => t.name === 'Action' && t.value === 'Credit-Notice'), + ); + assert(creditNotice, 'missing credit notice'); + const debitNotice = result.Messages.find(({ Tags }) => + Tags.find((t) => t.name === 'Action' && t.value === 'Debit-Notice'), + ); + const ref = debitNotice.Tags.find((t) => t.name === 'X-Reference')?.value; + assert(ref, 'Credit notice is not a reply message'); + assert(debitNotice, 'Missing debit notice'); + + const infoRes = await handle( + { + Tags: [{ name: 'Action', value: 'Info' }], + }, + result.Memory, + ); + const ownerAfterTransfer = infoRes.Messages[0].Tags.find( + (t) => t.name === 'Owner', + )?.value; + assert.strictEqual( + ownerAfterTransfer, + recipient, + 'Owner after transfer not equal to recipient', + ); + + // non owner transfer + const badResult = await handle({ + From: recipient, + Owner: recipient, + Tags: [ + { name: 'Action', value: 'Transfer' }, + { name: 'Recipient', value: STUB_ADDRESS }, + ], + }); + + // should return an insufficient funds error + const errorMessage = badResult.Messages[0]; + const errorRef = debitNotice.Tags.find( + (t) => t.name === 'X-Reference', + )?.value; + assert(errorRef, 'Error notice is not a reply message'); + const errorTag = errorMessage.Tags.find((t) => t.name === 'Error'); + assert.strictEqual(errorTag.value, 'Insufficient Balance!'); + const errorActionTag = errorMessage.Tags.find((t) => t.name === 'Action'); + assert.strictEqual(errorActionTag.value, 'Transfer-Error'); + }); + + it('should get the total supply', async () => { + /** + * Check: + * - data for supply (string int) + * - action is Total-Supply + * - Ticker tag + * - response is reply + */ + const result = await handle({ + From: ''.padEnd(43, 'rando'), + Owner: ''.padEnd(43, 'rando'), + Tags: [{ name: 'Action', value: 'Total-Supply' }], + }); + + const totalSupply = result.Messages[0]; + const ref = totalSupply.Tags.find((t) => t.name === 'X-Reference')?.value; + assert(ref, 'Total supply is not a reply message'); + const action = totalSupply.Tags.find((t) => t.name === 'Action'); + const ticker = totalSupply.Tags.find((t) => t.name === 'Ticker'); + assert.strictEqual(action.value, 'Total-Supply'); + assert.strictEqual(typeof ticker.value, 'string'); + assert.strictEqual(typeof totalSupply.Data, 'string'); + assert.strictEqual(Number.isInteger(parseInt(totalSupply.Data)), true); + }); +}); diff --git a/tools/constants.mjs b/tools/constants.mjs index 09d4561..5bc29a0 100644 --- a/tools/constants.mjs +++ b/tools/constants.mjs @@ -68,4 +68,6 @@ export const DEFAULT_HANDLE_OPTIONS = { Target: STUB_ADDRESS, From: STUB_ADDRESS, Timestamp: Date.now(), + // for msg.reply + Reference: '1', }; From 79bd00130f8891521da4dfa74b5c0e2c9870fd21 Mon Sep 17 00:00:00 2001 From: atticusofsparta Date: Thu, 23 Jan 2025 14:24:26 -0600 Subject: [PATCH 2/2] fix(send): use wrapped send to handle msg reply --- src/common/main.lua | 101 +++++++++++-------------------------------- src/common/utils.lua | 17 +++++--- 2 files changed, 38 insertions(+), 80 deletions(-) diff --git a/src/common/main.lua b/src/common/main.lua index e6dfd7f..90a07f9 100644 --- a/src/common/main.lua +++ b/src/common/main.lua @@ -89,31 +89,17 @@ function ant.init() createActionHandler(TokenSpecActionMap.Transfer, function(msg) local recipient = msg.Tags.Recipient if msg.From ~= Owner then - if msg.reply then - msg.reply({ - Action = "Transfer-Error", - ["Message-Id"] = msg.Id, - Error = "Insufficient Balance!", - }) - else - ao.send({ - Target = msg.From, - Action = "Transfer-Error", - ["Message-Id"] = msg.Id, - Error = "Insufficient Balance!", - }) - end + utils.Send(msg, { + Action = "Transfer-Error", + ["Message-Id"] = msg.Id, + Error = "Insufficient Balance!", + }) end balances.transfer(recipient, msg.Tags["Allow-Unsafe-Addresses"]) if not msg.Cast then - if msg.reply then - msg.reply(notices.debit(msg)) - msg.reply(notices.credit(msg)) - else - ao.send(notices.debit(msg)) - ao.send(notices.credit(msg)) - end + utils.Send(msg, notices.debit(msg)) + utils.Send(msg, notices.credit(msg)) end end) @@ -121,25 +107,14 @@ function ant.init() local addressToCheck = msg.Tags.Recipient or msg.Tags.Target or msg.From local balRes = balances.balance(addressToCheck, msg.Tags["Allow-Unsafe-Addresses"]) - if msg.reply then - msg.reply({ - Action = "Balance-Notice", - Balance = tostring(balRes), - Ticker = Ticker, - Account = addressToCheck, - Address = addressToCheck, - Data = tostring(balRes), - }) - else - ao.send({ - Target = msg.From, - Action = "Balance-Notice", - Balance = tostring(balRes), - Ticker = Ticker, - Address = msg.Tags.Recipient or msg.From, - Data = tostring(balRes), - }) - end + utils.Send(msg, { + Action = "Balance-Notice", + Balance = tostring(balRes), + Ticker = Ticker, + Account = addressToCheck, + Address = addressToCheck, + Data = tostring(balRes), + }) end) createActionHandler(TokenSpecActionMap.Balances, function(msg) @@ -148,32 +123,17 @@ function ant.init() bals[k] = tostring(v) end - if msg.reply then - msg.reply({ - Data = json.encode(bals), - }) - else - ao.send({ Target = msg.From, Data = json.encode(bals) }) - end + utils.Send(msg, { Data = json.encode(bals) }) end) createActionHandler(TokenSpecActionMap.TotalSupply, function(msg) assert(msg.From ~= ao.id, "Cannot call Total-Supply from the same process!") - if msg.reply then - msg.reply({ - Action = "Total-Supply", - Data = tostring(TotalSupply), - Ticker = Ticker, - }) - else - ao.send({ - Target = msg.From, - Action = "Total-Supply", - Data = tostring(TotalSupply), - Ticker = Ticker, - }) - end + utils.Send(msg, { + Action = "Total-Supply", + Data = tostring(TotalSupply), + Ticker = Ticker, + }) end) createActionHandler(TokenSpecActionMap.Info, function(msg) @@ -188,20 +148,11 @@ function ant.init() Owner = Owner, Handlers = utils.getHandlerNames(Handlers), } - if msg.reply then - msg.reply({ - Action = "Info-Notice", - Tags = info, - Data = json.encode(info), - }) - else - ao.send({ - Target = msg.From, - Action = "Info-Notice", - Tags = info, - Data = json.encode(info), - }) - end + utils.Send(msg, { + Action = "Info-Notice", + Tags = info, + Data = json.encode(info), + }) end) -- ActionMap (ANT Spec) diff --git a/src/common/utils.lua b/src/common/utils.lua index 0ec9251..61103a3 100644 --- a/src/common/utils.lua +++ b/src/common/utils.lua @@ -309,11 +309,7 @@ function utils.createHandler(tagName, tagValue, handler, position) end if resultNotice then - if msg.reply then - msg.reply(resultNotice) - else - ao.send(resultNotice) - end + utils.Send(msg, resultNotice) end local hasNewOwner = Owner ~= prevOwner @@ -362,4 +358,15 @@ function utils.validateKeywords(keywords) end end +--- @param msg AoMessage +--- @param response table +function utils.Send(msg, response) + if msg.reply then + --- Reference: https://github.com/permaweb/aos/blob/main/blueprints/patch-legacy-reply.lua + msg.reply(response) + else + ao.send(response) + end +end + return utils