Skip to content

Commit

Permalink
Merge pull request #59 from ar-io/PE-7344-update-token-spec
Browse files Browse the repository at this point in the history
fix(PE-7344): update handlers to integrate with token spec
  • Loading branch information
atticusofsparta authored Jan 24, 2025
2 parents 7c6e10e + 1865810 commit e3863a5
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 24 deletions.
43 changes: 27 additions & 16 deletions src/common/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -88,38 +88,50 @@ function ant.init()

createActionHandler(TokenSpecActionMap.Transfer, function(msg)
local recipient = msg.Tags.Recipient
utils.validateOwner(msg.From)
if msg.From ~= Owner then
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
ao.send(notices.debit(msg))
ao.send(notices.credit(msg))
utils.Send(msg, notices.debit(msg))
utils.Send(msg, notices.credit(msg))
end
end)

createActionHandler(TokenSpecActionMap.Balance, function(msg)
local balRes = balances.balance(msg.Tags.Recipient or msg.From, msg.Tags["Allow-Unsafe-Addresses"])
local addressToCheck = msg.Tags.Recipient or msg.Tags.Target or msg.From
local balRes = balances.balance(addressToCheck, msg.Tags["Allow-Unsafe-Addresses"])

ao.send({
Target = msg.From,
utils.Send(msg, {
Action = "Balance-Notice",
Balance = tostring(balRes),
Ticker = Ticker,
Address = msg.Tags.Recipient or msg.From,
Data = balRes,
Account = addressToCheck,
Address = addressToCheck,
Data = tostring(balRes),
})
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

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!")

ao.send({
Target = msg.From,
Action = "Total-Supply-Notice",
Data = TotalSupply,
utils.Send(msg, {
Action = "Total-Supply",
Data = tostring(TotalSupply),
Ticker = Ticker,
})
end)
Expand All @@ -136,8 +148,7 @@ function ant.init()
Owner = Owner,
Handlers = utils.getHandlerNames(Handlers),
}
ao.send({
Target = msg.From,
utils.Send(msg, {
Action = "Info-Notice",
Tags = info,
Data = json.encode(info),
Expand Down
24 changes: 20 additions & 4 deletions src/common/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,25 @@ 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
utils.Send(msg, resultNotice)
end

local hasNewOwner = Owner ~= prevOwner
Expand Down Expand Up @@ -358,4 +363,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
8 changes: 4 additions & 4 deletions test/balances.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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')
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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');
});
});
230 changes: 230 additions & 0 deletions test/specs/token.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions tools/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,6 @@ export const DEFAULT_HANDLE_OPTIONS = {
Target: STUB_ADDRESS,
From: STUB_ADDRESS,
Timestamp: Date.now(),
// for msg.reply
Reference: '1',
};

0 comments on commit e3863a5

Please sign in to comment.