diff --git a/CHANGELOG.md b/CHANGELOG.md index a01da8e84..dd48510b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,8 @@ Added: - Teal V6 support: - Add new opcode bsqrt and divw([##605](https://github.com/scale-it/algo-builder/pull/605)). - Add new opcode gloadss([#606](https://github.com/scale-it/algo-builder/pull/606)). - + - Add new opcode acct_params_get([#618](https://github.com/scale-it/algo-builder/pull/618)). + ### Template improvements - Using App instead of Lsig (Smart Signature) in `examples/dao` to simplify deposit management. diff --git a/packages/runtime/src/errors/errors-list.ts b/packages/runtime/src/errors/errors-list.ts index 61309bcad..b91873ecf 100644 --- a/packages/runtime/src/errors/errors-list.ts +++ b/packages/runtime/src/errors/errors-list.ts @@ -377,6 +377,13 @@ maximun uint128`, title: "Execution mode not valid", description: `Execution mode not valid`, }, + UNKNOWN_ACCT_FIELD: { + number: 1055, + message: + "Account Field Error - Unknown Field: %field% at line %line% for teal version #%tealV%", + title: "Account Field Error at line %line%", + description: `Account field unknown`, + }, }; const runtimeGeneralErrors = { diff --git a/packages/runtime/src/interpreter/interpreter.ts b/packages/runtime/src/interpreter/interpreter.ts index 55e16dd37..26f6eafcb 100644 --- a/packages/runtime/src/interpreter/interpreter.ts +++ b/packages/runtime/src/interpreter/interpreter.ts @@ -8,7 +8,7 @@ import { import { RUNTIME_ERRORS } from "../errors/errors-list"; import { RuntimeError } from "../errors/runtime-errors"; -import { Runtime } from "../index"; +import { AccountStore, Runtime } from "../index"; import { checkIndexBound, compareArray } from "../lib/compare"; import { ALGORAND_MAX_APP_ARGS_LEN, @@ -22,6 +22,7 @@ import { keyToBytes } from "../lib/parsing"; import { Stack } from "../lib/stack"; import { assertMaxCost, parser } from "../parser/parser"; import { + AccountAddress, AccountStoreI, AppInfo, BaseTxReceipt, @@ -107,14 +108,35 @@ export class Interpreter { return this.runtime.ctx.getApp(appID, line); } + /** + * Create new account with `address` if this is undefined in ctx. + * return state of this account in ctx. + * @param addr address we want to query. + */ + private createAccountIfAbsent(addr: AccountAddress): AccountStoreI { + let account = this.runtime.ctx.state.accounts.get(addr); + if (!account) { + account = new AccountStore(0, { addr, sk: new Uint8Array(0) }); + this.runtime.ctx.state.accounts.set(addr, account); + } + return account; + } + /** * Beginning from TEALv4, user can directly pass address instead of index to Txn.Accounts. * However, the address must still be present in tx.Accounts OR should be equal to Txn.Sender + * When `create` flag is true we will throws exception if account is not found. + * When `create` flag is false we will create new account and add it to context. * @param accountPk public key of account * @param line line number in TEAL file + * @param create create flag * https://developer.algorand.org/articles/introducing-algorand-virtual-machine-avm-09-release/ */ - private _getAccountFromAddr(accountPk: Uint8Array, line: number): AccountStoreI { + private _getAccountFromAddr( + accountPk: Uint8Array, + line: number, + create: boolean + ): AccountStoreI { const txAccounts = this.runtime.ctx.tx.apat; // tx.Accounts array const appID = this.runtime.ctx.tx.apid ?? 0; if (this.tealVersion <= 3) { @@ -145,7 +167,10 @@ export class Interpreter { compareArray(accountPk, decodeAddress(getApplicationAddress(appID)).publicKey) ) { const address = encodeAddress(pkBuffer); - const account = this.runtime.ctx.state.accounts.get(address); + const account = create + ? this.createAccountIfAbsent(address) + : this.runtime.ctx.state.accounts.get(address); + return this.runtime.assertAccountDefined(address, account, line); } else { throw new RuntimeError(RUNTIME_ERRORS.TEAL.ADDR_NOT_FOUND_IN_TXN_ACCOUNT, { @@ -158,12 +183,14 @@ export class Interpreter { /** * Queries account by accountIndex or `ctx.tx.snd` (if `accountIndex==0`). * If account address is passed, then queries account by address. - * Throws exception if account is not found. + * When `create` flag is true we will throws exception if account is not found. + * When `create` flag is false we will create new account and add it to context. * @param accountRef index of account to fetch from account list * @param line line number + * @param create create flag, default is true * NOTE: index 0 represents txn sender account */ - getAccount(accountRef: StackElem, line: number): AccountStoreI { + getAccount(accountRef: StackElem, line: number, create = false): AccountStoreI { let account: AccountStoreI | undefined; let address: string; if (typeof accountRef === "bigint") { @@ -180,10 +207,12 @@ export class Interpreter { throw new Error("pk Buffer not found"); } address = encodeAddress(pkBuffer); - account = this.runtime.ctx.state.accounts.get(address); + account = create + ? this.createAccountIfAbsent(address) + : this.runtime.ctx.state.accounts.get(address); } } else { - return this._getAccountFromAddr(accountRef, line); + return this._getAccountFromAddr(accountRef, line, create); } return this.runtime.assertAccountDefined(address, account, line); diff --git a/packages/runtime/src/interpreter/opcode-list.ts b/packages/runtime/src/interpreter/opcode-list.ts index 3685342bd..d68c81168 100644 --- a/packages/runtime/src/interpreter/opcode-list.ts +++ b/packages/runtime/src/interpreter/opcode-list.ts @@ -20,6 +20,7 @@ import { RUNTIME_ERRORS } from "../errors/errors-list"; import { RuntimeError } from "../errors/runtime-errors"; import { compareArray } from "../lib/compare"; import { + AcctParamQueryFields, ALGORAND_MAX_LOGS_COUNT, ALGORAND_MAX_LOGS_LENGTH, AppParamDefined, @@ -4562,3 +4563,70 @@ export class AppParamsGet extends Op { } } } + +export class AcctParamsGet extends Op { + readonly interpreter: Interpreter; + readonly line: number; + readonly field: string; + /** + * @param args Expected arguments: [account_param] + * @param line line number in TEAL file + * @param interpreter interpreter object + */ + constructor(args: string[], line: number, interpreter: Interpreter) { + super(); + this.line = line; + this.interpreter = interpreter; + assertLen(args.length, 1, line); + + if ( + !AcctParamQueryFields[args[0]] || + AcctParamQueryFields[args[0]].version > interpreter.tealVersion + ) { + throw new RuntimeError(RUNTIME_ERRORS.TEAL.UNKNOWN_ACCT_FIELD, { + field: args[0], + line: line, + tealV: interpreter.tealVersion, + }); + } + + this.field = args[0]; + } + + execute(stack: Stack): void { + this.assertMinStackLen(stack, 1, this.line); + + const acctAddress = this.assertAlgorandAddress(stack.pop(), this.line); + + // get account from current context + // not `create` flag = true + const accountInfo = this.interpreter.getAccount(acctAddress, this.line, true); + + let value: StackElem = 0n; + switch (this.field) { + case "AcctBalance": { + value = BigInt(accountInfo.balance()); + break; + } + case "AcctMinBalance": { + value = BigInt(accountInfo.minBalance); + break; + } + case "AcctAuthAddr": { + if (accountInfo.getSpendAddress() === accountInfo.address) { + value = ZERO_ADDRESS; + } else { + value = Buffer.from(decodeAddress(accountInfo.getSpendAddress()).publicKey); + } + break; + } + } + stack.push(value); + + if (accountInfo.balance() > 0) { + stack.push(1n); + } else { + stack.push(0n); + } + } +} diff --git a/packages/runtime/src/lib/constants.ts b/packages/runtime/src/lib/constants.ts index 888e499e8..31e9e2a1e 100644 --- a/packages/runtime/src/lib/constants.ts +++ b/packages/runtime/src/lib/constants.ts @@ -293,6 +293,13 @@ export const AppParamDefined: { [key: number]: Set } = { AppParamDefined[6] = cloneDeep(AppParamDefined[5]); +// param use for query acct_params_get opcode + +export const AcctParamQueryFields: { [key: string]: { version: number } } = { + AcctBalance: { version: 6 }, + AcctMinBalance: { version: 6 }, + AcctAuthAddr: { version: 6 }, +}; export const reDigit = /^\d+$/; export const reDec = /^(0|[1-9]\d*)$/; export const reHex = /^0x[0-9a-fA-F]+$/; diff --git a/packages/runtime/src/parser/parser.ts b/packages/runtime/src/parser/parser.ts index 9c9328c20..28d0172a0 100644 --- a/packages/runtime/src/parser/parser.ts +++ b/packages/runtime/src/parser/parser.ts @@ -2,6 +2,7 @@ import { RUNTIME_ERRORS } from "../errors/errors-list"; import { RuntimeError } from "../errors/runtime-errors"; import { Interpreter } from "../interpreter/interpreter"; import { + AcctParamsGet, Add, Addr, Addw, @@ -374,6 +375,7 @@ opCodeMap[6] = { divw: Divw, bsqrt: Bsqrt, gloadss: Gloadss, + acct_params_get: AcctParamsGet, }; // list of opcodes with exactly one parameter. @@ -430,6 +432,7 @@ const interpreterReqList = new Set([ "log", "app_params_get", "gloadss", + "acct_params_get", ]); const signatureModeOps = new Set(["arg", "args", "arg_0", "arg_1", "arg_2", "arg_3"]); @@ -460,6 +463,7 @@ const applicationModeOps = new Set([ "itxn", "itxna", "gloadss", + "acct_params_get", ]); // opcodes allowed in both application and signature mode @@ -575,6 +579,7 @@ const commonModeOps = new Set([ "divw", "bsqrt", "gloadss", + "acct_params_get", ]); /** diff --git a/packages/runtime/test/fixtures/teal-files/assets/teal-v6.teal b/packages/runtime/test/fixtures/teal-files/assets/teal-v6.teal new file mode 100644 index 000000000..180d41e18 --- /dev/null +++ b/packages/runtime/test/fixtures/teal-files/assets/teal-v6.teal @@ -0,0 +1,5 @@ +#pragma version 6 +bsqrt +divw +gloadss +acct_params_get AcctBalance \ No newline at end of file diff --git a/packages/runtime/test/src/interpreter/opcode-list.ts b/packages/runtime/test/src/interpreter/opcode-list.ts index 7e6771b7a..8c5c5a29f 100644 --- a/packages/runtime/test/src/interpreter/opcode-list.ts +++ b/packages/runtime/test/src/interpreter/opcode-list.ts @@ -15,6 +15,7 @@ import { RUNTIME_ERRORS } from "../../../src/errors/errors-list"; import { getProgram, Runtime } from "../../../src/index"; import { Interpreter } from "../../../src/interpreter/interpreter"; import { + AcctParamsGet, Add, Addr, Addw, @@ -2382,7 +2383,8 @@ describe("Teal Opcodes", function () { describe("Gtxn", function () { before(function () { const tx = interpreter.runtime.ctx.tx; - // a) 'apas' represents 'foreignAssets', b) 'apfa' represents 'foreignApps' (id's of foreign apps) + // a) 'apas' represents 'foreignAssets', b) + // 'apfa' represents 'foreignApps' (id's of foreign apps) // https://developer.algorand.org/docs/reference/transactions/ const tx2 = { ...tx, fee: 2222, apas: [3033, 4044], apfa: [5005, 6006, 7077] }; interpreter.runtime.ctx.gtxs = [tx, tx2]; @@ -6189,7 +6191,7 @@ describe("Teal Opcodes", function () { }); }); - describe("TEALv6 opcodes", function () { + describe("TEALv6: divw and bsqrt opcodes", function () { let stack: Stack; const initStack = (values: StackElem[]): Stack => { @@ -6252,4 +6254,118 @@ describe("Teal Opcodes", function () { expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.BYTES_LEN_EXCEEDED); }); }); + + + describe("Tealv6: acct_params_get opcode", function(){ + const stack = new Stack(); + let interpreter: Interpreter; + let alice: AccountStoreI; + let bob: AccountStoreI; + let op: AcctParamsGet; + const zeroBalanceAddr = "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"; + + this.beforeEach(() => { + interpreter = new Interpreter(); + interpreter.runtime = new Runtime([]); + [alice, bob] = interpreter.runtime.defaultAccounts() + // init tx + interpreter.runtime.ctx.tx = {...TXN_OBJ, apat: [ + Buffer.from(decodeAddress(alice.address).publicKey), + Buffer.from(decodeAddress(zeroBalanceAddr).publicKey) + ]}; + interpreter.tealVersion = MaxTEALVersion; + interpreter.runtime.ctx.gtxs = [TXN_OBJ]; + interpreter.tealVersion = 6; + stack.push(decodeAddress(alice.address).publicKey); + }); + + it("Should return balance", () => { + op = new AcctParamsGet(["AcctBalance"], 1, interpreter); + op.execute(stack); + assert.equal(stack.pop(), 1n); // balance > 0 + assert.equal(stack.pop(), alice.balance()); + }); + + it("Should return min balance", () => { + op = new AcctParamsGet(["AcctMinBalance"], 1, interpreter); + op.execute(stack); + assert.equal(stack.pop(), 1n); // balance > 0 + assert.equal(stack.pop(), BigInt(alice.minBalance)); + }); + + it("Should return Auth Address", () => { + op = new AcctParamsGet(["AcctAuthAddr"], 1, interpreter); + op.execute(stack); + assert.equal(stack.pop(), 1n); // balance > 0 + assert.deepEqual(stack.pop(), ZERO_ADDRESS); + }); + + it("Shoud return Auth Address - rekey case", () => { + // set spend key for alice is bob + alice.rekeyTo(bob.address); + interpreter.runtime.ctx.state.accounts.set(alice.address, alice); + op = new AcctParamsGet(["AcctAuthAddr"], 1, interpreter); + op.execute(stack); + assert.equal(stack.pop(), 1n); // balance > 0 + assert.deepEqual(stack.pop(), decodeAddress(bob.address).publicKey); + }); + + it("Should return balance with account own zero balance", () => { + op = new AcctParamsGet(["AcctBalance"], 1, interpreter); + stack.push(decodeAddress(zeroBalanceAddr).publicKey); + op.execute(stack); + assert.equal(stack.pop(), 0n); // balance = 0 + assert.equal(stack.pop(), 0n); + }); + + it("Should return min balance with account own zero balance", () => { + op = new AcctParamsGet(["AcctMinBalance"], 1, interpreter); + stack.push(decodeAddress(zeroBalanceAddr).publicKey); + op.execute(stack); + assert.equal(stack.pop(), 0n); // balance = 0 + assert.equal(stack.pop(), BigInt(ALGORAND_ACCOUNT_MIN_BALANCE)); + }); + + it("Should return Auth Address with account own zero balance", () => { + op = new AcctParamsGet(["AcctAuthAddr"], 1, interpreter); + stack.push(decodeAddress(zeroBalanceAddr).publicKey); + op.execute(stack); + assert.equal(stack.pop(), 0n); // balance = 0 + assert.deepEqual(stack.pop(), ZERO_ADDRESS); + }); + + it("Should throw error when query unknow field", () => { + expectRuntimeError( + () => new AcctParamsGet(["Miles"], 1, interpreter), + RUNTIME_ERRORS.TEAL.UNKNOWN_ACCT_FIELD + ); + }); + + it("Should throw error if query account not in ref account list", () => { + op = new AcctParamsGet(["AcctBalance"], 1, interpreter); + stack.push(decodeAddress(bob.address).publicKey) + + expectRuntimeError( + () => op.execute(stack), + RUNTIME_ERRORS.TEAL.ADDR_NOT_FOUND_IN_TXN_ACCOUNT + ) + + // valid address but not in tx accounts list + stack.push(parsing.stringToBytes("01234567890123456789012345678901")); + expectRuntimeError( + () => op.execute(stack), + RUNTIME_ERRORS.TEAL.ADDR_NOT_FOUND_IN_TXN_ACCOUNT + ) + }); + + it("Should throw error if top element in stack is not an address", () => { + op = new AcctParamsGet(["AcctBalance"], 1, interpreter); + stack.push(parsing.stringToBytes("ABCDE")); + + expectRuntimeError( + () => op.execute(stack), + RUNTIME_ERRORS.TEAL.INVALID_ADDR + ) + }); + }); }); diff --git a/packages/runtime/test/src/parser/parser.ts b/packages/runtime/test/src/parser/parser.ts index c5bac6292..d302297a7 100644 --- a/packages/runtime/test/src/parser/parser.ts +++ b/packages/runtime/test/src/parser/parser.ts @@ -4,6 +4,7 @@ import { getProgram } from "../../../src"; import { RUNTIME_ERRORS } from "../../../src/errors/errors-list"; import { Interpreter } from "../../../src/interpreter/interpreter"; import { + AcctParamsGet, Add, Addr, Addw, @@ -29,6 +30,7 @@ import { Branch, BranchIfNotZero, BranchIfZero, + Bsqrt, Btoi, Byte, Bytec, @@ -115,6 +117,7 @@ import { Uncover, } from "../../../src/interpreter/opcode-list"; import { + AcctParamQueryFields, AppParamDefined, MAX_UINT64, MaxTEALVersion, @@ -1990,6 +1993,30 @@ describe("Parser", function () { RUNTIME_ERRORS.TEAL.EXECUTION_MODE_NOT_VALID ); }); + + it("acct_params_get", () => { + Object.keys(AcctParamQueryFields).forEach((appParam: string) => { + const res = opcodeFromSentence( + ["acct_params_get", appParam], + 1, + interpreter, + ExecutionMode.APPLICATION + ); + const expected = new AcctParamsGet([appParam], 1, interpreter); + assert.deepEqual(res, expected); + }); + + expectRuntimeError( + () => + opcodeFromSentence( + ["acct_params_get", "unknow", "hello"], + 1, + interpreter, + ExecutionMode.APPLICATION + ), + RUNTIME_ERRORS.TEAL.ASSERT_LENGTH + ); + }); }); }); @@ -2307,6 +2334,21 @@ describe("Parser", function () { assert.deepEqual(res, expected); }); + + it("should rreturn correct opcode list for `teal v6`", async () => { + const file = "teal-v6.teal"; + + const res = parser(getProgram(file), ExecutionMode.APPLICATION, interpreter); + const expected = [ + new Pragma(["version", "6"], 1, interpreter), + new Divw([], 2), + new Bsqrt([], 3), + new Gloadss([], 4, interpreter), + new AcctParamsGet(["AcctBalance"], 5, interpreter), + ]; + + assert.deepEqual(res, expected); + }); }); describe("Gas cost of Opcodes from TEAL file", () => {