-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #285 from joe-p/arc59
ARC59: ASA Inbox Router
- Loading branch information
Showing
37 changed files
with
25,778 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}, | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules/ | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"recommendations": [ | ||
"dbaeumer.vscode-eslint", | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll.eslint": "explicit" | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Oops, something went wrong.