Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add acct_params_get opcode #618

Merged
merged 8 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/runtime/src/errors/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,13 @@ maximun uint128`,
title: "Execution mode not valid",
description: `Execution mode not valid`,
},
UNKNOWN_ACCT_FIELD: {
number: 1053,
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 = {
Expand Down
38 changes: 32 additions & 6 deletions packages/runtime/src/interpreter/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -107,14 +108,28 @@ export class Interpreter {
return this.runtime.ctx.getApp(appID, line);
}

/**
* Create new account with `address` if this is undefined in ctx and return
* else 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
* @param accountPk public key of account
* @param line line number in TEAL file
* 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, strict = true): AccountStoreI {
const txAccounts = this.runtime.ctx.tx.apat; // tx.Accounts array
const appID = this.runtime.ctx.tx.apid ?? 0;
if (this.tealVersion <= 3) {
Expand Down Expand Up @@ -145,7 +160,13 @@ export class Interpreter {
compareArray(accountPk, decodeAddress(getApplicationAddress(appID)).publicKey)
) {
const address = encodeAddress(pkBuffer);
const account = this.runtime.ctx.state.accounts.get(address);
let account = this.runtime.ctx.state.accounts.get(address);
// strict is turn on we will create account store
// we will create account if it not exist
if (!strict) {
account = this.createAccountIfAbsent(address);
}

return this.runtime.assertAccountDefined(address, account, line);
} else {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.ADDR_NOT_FOUND_IN_TXN_ACCOUNT, {
Expand All @@ -158,12 +179,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 `strict` is true we will throws exception if account is not found.
* when `strict` 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 strict strict flag
* NOTE: index 0 represents txn sender account
*/
getAccount(accountRef: StackElem, line: number): AccountStoreI {
getAccount(accountRef: StackElem, line: number, strict = true): AccountStoreI {
let account: AccountStoreI | undefined;
let address: string;
if (typeof accountRef === "bigint") {
Expand All @@ -181,9 +204,12 @@ export class Interpreter {
}
address = encodeAddress(pkBuffer);
account = this.runtime.ctx.state.accounts.get(address);
if (!strict) {
account = this.createAccountIfAbsent(address);
}
}
} else {
return this._getAccountFromAddr(accountRef, line);
return this._getAccountFromAddr(accountRef, line, strict);
}

return this.runtime.assertAccountDefined(address, account, line);
Expand Down
68 changes: 68 additions & 0 deletions packages/runtime/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -4562,3 +4563,70 @@ export class AppParamsGet extends Op {
}
}
}

export class AcctParamsGet extends Op {
readonly interpreter: Interpreter;
readonly line: number;
readonly field: string;
/**
* Asserts 1 arguments are passed.
* @param args Expected arguments: [] // none
* @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<StackElem>): void {
this.assertMinStackLen(stack, 1, this.line);

const acctAddress = this.assertAlgorandAddress(stack.pop(), this.line);

// get account from current context
const accountInfo = this.interpreter.getAccount(acctAddress, this.line, false);

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);
}
}
}
7 changes: 7 additions & 0 deletions packages/runtime/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ export const AppParamDefined: { [key: number]: Set<string> } = {

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]+$/;
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -374,6 +375,7 @@ opCodeMap[6] = {
divw: Divw,
bsqrt: Bsqrt,
gloadss: Gloadss,
acct_params_get: AcctParamsGet,
};

// list of opcodes with exactly one parameter.
Expand Down Expand Up @@ -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"]);
Expand Down Expand Up @@ -460,6 +463,7 @@ const applicationModeOps = new Set([
"itxn",
"itxna",
"gloadss",
"acct_params_get",
]);

// opcodes allowed in both application and signature mode
Expand Down Expand Up @@ -575,6 +579,7 @@ const commonModeOps = new Set([
"divw",
"bsqrt",
"gloadss",
"acct_params_get",
]);

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/test/fixtures/teal-files/assets/teal-v6.teal
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#pragma version 6
bsqrt
divw
gloadss
acct_params_get AcctBalance
120 changes: 118 additions & 2 deletions packages/runtime/test/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -6189,7 +6191,7 @@ describe("Teal Opcodes", function () {
});
});

describe("TEALv6 opcodes", function () {
describe("TEALv6: divw and bsqrt opcodes", function () {
let stack: Stack<StackElem>;

const initStack = (values: StackElem[]): Stack<StackElem> => {
Expand Down Expand Up @@ -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<StackElem>();
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 address type", () => {
op = new AcctParamsGet(["AcctBalance"], 1, interpreter);
stack.push(parsing.stringToBytes("ABCDE"));

expectRuntimeError(
() => op.execute(stack),
RUNTIME_ERRORS.TEAL.INVALID_ADDR
)
});
});
});
Loading