Skip to content

Commit

Permalink
Merge pull request #285 from joe-p/arc59
Browse files Browse the repository at this point in the history
ARC59: ASA Inbox Router
  • Loading branch information
SudoWeezy authored Jun 21, 2024
2 parents 1913bbb + 74c1575 commit 2d3735a
Show file tree
Hide file tree
Showing 37 changed files with 25,778 additions and 0 deletions.
726 changes: 726 additions & 0 deletions ARCs/arc-0059.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions assets/arc-0059/.devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"forwardPorts": [4001, 4002, 8980],
"portsAttributes": {
"4001": {
"label": "algod"
},
"4002": {
"label": "kmd"
},
"8980": {
"label": "indexer"
}
},
"postCreateCommand": "pipx install algokit-cli",
"postStartCommand": "algokit localnet start"
}
56 changes: 56 additions & 0 deletions assets/arc-0059/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'airbnb-base',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/ban-ts-comment': 'warn',
'import/prefer-default-export': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['**/*.test.ts'],
},
],
},
overrides: [
{
files: ['*.algo.ts'],
rules: {
'import/no-extraneous-dependencies': 'off',
'object-shorthand': 'off',
'class-methods-use-this': 'off',
'no-undef': 'off',
'max-classes-per-file': 'off',
'no-bitwise': 'off',
'operator-assignment': 'off',
'prefer-template': 'off',
'prefer-destructuring': 'off',
},
},
],
};
2 changes: 2 additions & 0 deletions assets/arc-0059/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.env
6 changes: 6 additions & 0 deletions assets/arc-0059/.prettierrc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# .prettierrc.toml
trailingComma = "es5"
tabWidth = 2
semi = true
singleQuote = true
printWidth = 120
5 changes: 5 additions & 0 deletions assets/arc-0059/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
]
}
5 changes: 5 additions & 0 deletions assets/arc-0059/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
}
272 changes: 272 additions & 0 deletions assets/arc-0059/__test__/arc59.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import { describe, test, expect, beforeAll, beforeEach } from '@jest/globals';
import { algorandFixture } from '@algorandfoundation/algokit-utils/testing';
import * as algokit from '@algorandfoundation/algokit-utils';
import algosdk from 'algosdk';
import { Arc59Client } from '../contracts/clients/Arc59Client';

const fixture = algorandFixture();
algokit.Config.configure({ populateAppCallResources: true });

/**
* Send an asset to a receiver using the ARC59 router
*
* @param appClient The ARC59 client generated by algokit
* @param assetId The ID of the asset to send
* @param sender The address of the sender
* @param receiver The address of the receiver
* @param algorand The AlgorandClient instance to use to send transactions
* @param sendAlgoForNewAccount Whether to send 201_000 uALGO to the receiver so they can claim the asset with a 0-ALGO balance
*/
async function arc59SendAsset(
appClient: Arc59Client,
assetId: bigint,
sender: string,
receiver: string,
algorand: algokit.AlgorandClient
) {
// Get the address of the ARC59 router
const arc59RouterAddress = (await appClient.appClient.getAppReference()).appAddress;

// Call arc59GetSendAssetInfo to get the following:
// itxns - The number of transactions needed to send the asset
// mbr - The minimum balance that must be sent to the router
// routerOptedIn - Whether the router has opted in to the asset
// receiverOptedIn - Whether the receiver has opted in to the asset
const [itxns, mbr, routerOptedIn, receiverOptedIn, receiverAlgoNeededForClaim] = (
await appClient.arc59GetSendAssetInfo({ asset: assetId, receiver })
).return!;

// If the receiver has opted in, just send the asset directly
if (receiverOptedIn) {
await algorand.send.assetTransfer({
sender,
receiver,
assetId,
amount: 1n,
});

return;
}

// Create a composer to form an atomic transaction group
const composer = appClient.compose();

const signer = algorand.account.getSigner(sender);

// If the MBR is non-zero, send the MBR to the router
if (mbr || receiverAlgoNeededForClaim) {
const mbrPayment = await algorand.transactions.payment({
sender,
receiver: arc59RouterAddress,
amount: algokit.microAlgos(Number(mbr + receiverAlgoNeededForClaim)),
});

composer.addTransaction({ txn: mbrPayment, signer });
}

// If the router is not opted in, add a call to arc59OptRouterIn to do so
if (!routerOptedIn) composer.arc59OptRouterIn({ asa: assetId });

/** The box of the receiver's pubkey will always be needed */
const boxes = [algosdk.decodeAddress(receiver).publicKey];

/** The address of the receiver's inbox */
const inboxAddress = (await appClient.compose().arc59GetInbox({ receiver }, { boxes }).simulate()).returns[0];

// The transfer of the asset to the router
const axfer = await algorand.transactions.assetTransfer({
sender,
receiver: arc59RouterAddress,
assetId,
amount: 1n,
});

// An extra itxn is if we are also sending ALGO for the receiver claim
const totalItxns = itxns + (receiverAlgoNeededForClaim === 0n ? 0n : 1n);

composer.arc59SendAsset(
{ axfer, receiver, additionalReceiverFunds: receiverAlgoNeededForClaim },
{
sendParams: { fee: algokit.microAlgos(1000 + 1000 * Number(totalItxns)) },
boxes, // The receiver's pubkey
// Always good to include both accounts here, even if we think only the receiver is needed. This is to help protect against race conditions within a block.
accounts: [receiver, inboxAddress],
// Even though the asset is available in the group, we need to explicitly define it here because we will be checking the asset balance of the receiver
assets: [Number(assetId)],
}
);

// Disable resource population to ensure that our manually defined resources are correct
algokit.Config.configure({ populateAppCallResources: false });

// Send the transaction group
await composer.execute();

// Re-enable resource population
algokit.Config.configure({ populateAppCallResources: true });
}

/**
* Claim an asset from the ARC59 inbox
*
* @param appClient The ARC59 client generated by algokit
* @param assetId The ID of the asset to claim
* @param claimer The address of the account claiming the asset
* @param algorand The AlgorandClient instance to use to send transactions
*/
async function arc59Claim(appClient: Arc59Client, assetId: bigint, claimer: string, algorand: algokit.AlgorandClient) {
const composer = appClient.compose();

// Check if the claimer has opted in to the asset
let claimerOptedIn = false;
try {
await algorand.account.getAssetInformation(claimer, assetId);
claimerOptedIn = true;
} catch (e) {
// Do nothing
}

const inbox = (
await appClient.compose().arc59GetInbox({ receiver: claimer }).simulate({ allowUnnamedResources: true })
).returns[0];

let totalTxns = 3;

// If the inbox has extra ALGO, claim it
const inboxInfo = await algorand.account.getInformation(inbox);
if (inboxInfo.minBalance < inboxInfo.amount) {
totalTxns += 2;
composer.arc59ClaimAlgo(
{},
{ sender: algorand.account.getAccount(claimer), sendParams: { fee: algokit.algos(0) } }
);
}

// If the claimer hasn't already opted in, add a transaction to do so
if (!claimerOptedIn) {
composer.addTransaction({
txn: await algorand.transactions.assetOptIn({ assetId, sender: claimer }),
signer: algorand.account.getSigner(claimer),
});
}

composer.arc59Claim(
{ asa: assetId },
{ sender: algorand.account.getAccount(claimer), sendParams: { fee: algokit.microAlgos(1000 * totalTxns) } }
);

await composer.execute();
}

describe('Arc59', () => {
let appClient: Arc59Client;
let assetOne: bigint;
let assetTwo: bigint;
let alice: algosdk.Account;
let bob: algosdk.Account;

beforeEach(fixture.beforeEach);

beforeAll(async () => {
await fixture.beforeEach();
const { testAccount } = fixture.context;
const { algorand } = fixture;

appClient = new Arc59Client(
{
sender: testAccount,
resolveBy: 'id',
id: 0,
},
algorand.client.algod
);

const oneResult = await algorand.send.assetCreate({
sender: testAccount.addr,
unitName: 'one',
total: 100n,
});
assetOne = BigInt(oneResult.confirmation.assetIndex!);

const twoResult = await algorand.send.assetCreate({
sender: testAccount.addr,
unitName: 'two',
total: 100n,
});
assetTwo = BigInt(twoResult.confirmation.assetIndex!);

alice = testAccount;

await appClient.create.createApplication({});

await appClient.appClient.fundAppAccount({ amount: algokit.microAlgos(100_000) });
});

test('routerOptIn', async () => {
await appClient.appClient.fundAppAccount({ amount: algokit.microAlgos(100_000) });
await appClient.arc59OptRouterIn({ asa: assetOne }, { sendParams: { fee: algokit.microAlgos(2_000) } });
});

test('Brand new account getSendAssetInfo', async () => {
const res = await appClient.arc59GetSendAssetInfo({ asset: assetOne, receiver: algosdk.generateAccount().addr });

const itxns = res.return![0];
const mbr = res.return![1];

expect(itxns).toBe(5n);
expect(mbr).toBe(228_100n);
});

test('Brand new account sendAsset', async () => {
const { algorand } = fixture;
const { testAccount } = fixture.context;
bob = testAccount;

await arc59SendAsset(appClient, assetOne, alice.addr, bob.addr, algorand);
});

test('Existing inbox sendAsset (existing asset)', async () => {
const { algorand } = fixture;

await arc59SendAsset(appClient, assetOne, alice.addr, bob.addr, algorand);
});

test('Existing inbox sendAsset (new asset)', async () => {
const { algorand } = fixture;

await arc59SendAsset(appClient, assetTwo, alice.addr, bob.addr, algorand);
});

test('claim', async () => {
const { algorand } = fixture;

await arc59Claim(appClient, assetOne, bob.addr, algorand);

const bobAssetInfo = await algorand.account.getAssetInformation(bob.addr, assetOne);

expect(bobAssetInfo.balance).toBe(2n);
});

test('reject', async () => {
const { algorand } = fixture;
const newAsset = BigInt(
(await algorand.send.assetCreate({ sender: alice.addr, total: 1n })).confirmation.assetIndex!
);
await arc59SendAsset(appClient, newAsset, alice.addr, bob.addr, algorand);

await appClient.arc59Reject({ asa: newAsset }, { sender: bob, sendParams: { fee: algokit.algos(0.003) } });
});

test('claim from 0-ALGO account', async () => {
const { algorand } = fixture;
const receiver = algorand.account.random();

await arc59SendAsset(appClient, assetOne, alice.addr, receiver.addr, algorand);

await arc59Claim(appClient, assetOne, receiver.addr, algorand);

const receiverAssetInfo = await algorand.account.getAssetInformation(receiver.addr, assetOne);

expect(receiverAssetInfo.balance).toBe(1n);
});
});
Binary file added assets/arc-0059/bun.lockb
Binary file not shown.
Loading

0 comments on commit 2d3735a

Please sign in to comment.