diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5eea27c..783740157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Added: Changed: - `bond-token-flow` test to also use runtime.defaultAccounts. (see [example](https://github.com/scale-it/algo-builder/blob/develop/examples/bond/test/bond-token-flow.js)) - The `compile.ts` has been updated and now the tealCode is stored in cache when `scTmplParams` are used to compile TEAL with hardcoded params. +- Support execution of algo-sdk-js `transactionAndSign` in Runtime [#601](https://github.com/scale-it/algo-builder/pull/601). - Added support for checking against opcode their execution mode in runtime. For eg. `arg` can only be run in *signature* mode, and parser will reject the execution if run in application mode. - Support RekeyTo field in the inner transaction for TEAL v6. - Support `keyreg` transaction in inner transaction. diff --git a/packages/runtime/src/interpreter/opcode-list.ts b/packages/runtime/src/interpreter/opcode-list.ts index 806eb8d95..e3db1b4d0 100644 --- a/packages/runtime/src/interpreter/opcode-list.ts +++ b/packages/runtime/src/interpreter/opcode-list.ts @@ -1,6 +1,6 @@ /* eslint sonarjs/no-identical-functions: 0 */ /* eslint sonarjs/no-duplicate-string: 0 */ -import { parsing } from "@algo-builder/web"; +import { parsing, types } from "@algo-builder/web"; import algosdk, { ALGORAND_MIN_TX_FEE, decodeAddress, decodeUint64, encodeAddress, encodeUint64, getApplicationAddress, isValidAddress, modelsv2, verifyBytes } from "algosdk"; import { ec as EC } from "elliptic"; import { Message, sha256 } from "js-sha256"; @@ -19,13 +19,13 @@ import { MAX_UINT64, MAX_UINT128, MaxTEALVersion, TxArrFields, ZERO_ADDRESS } from "../lib/constants"; -import { parseEncodedTxnToExecParams, setInnerTxField } from "../lib/itxn"; +import { setInnerTxField } from "../lib/itxn"; import { assertLen, assertNumber, assertOnlyDigits, bigEndianBytesToBigInt, bigintToBigEndianBytes, convertToBuffer, convertToString, getEncoding, parseBinaryStrToBigInt } from "../lib/parsing"; import { Stack } from "../lib/stack"; -import { txAppArg, txnSpecbyField } from "../lib/txn"; +import { encTxToExecParams, txAppArg, txnSpecbyField } from "../lib/txn"; import { DecodingMode, EncodingType, StackElem, TEALStack, TxnType, TxOnComplete, TxReceipt } from "../types"; import { Interpreter } from "./interpreter"; import { Op } from "./opcode"; @@ -1616,8 +1616,8 @@ export class Global extends Op { break; } case 'CreatorAddress': { - const appID = this.interpreter.runtime.ctx.tx.apid as number; - const app = this.interpreter.getApp(appID, this.line); + const appID = this.interpreter.runtime.ctx.tx.apid; + const app = this.interpreter.getApp(appID as number, this.line); result = decodeAddress(app.creator).publicKey; break; } @@ -3925,8 +3925,22 @@ export class ITxnSubmit extends Op { ); } + // initial contract account. + const appID = this.interpreter.runtime.ctx.tx.apid as number; + const contractAddress = getApplicationAddress(appID); + const contractAccount = this.interpreter.runtime.getAccount(contractAddress).account; + // get execution txn params (parsed from encoded sdk txn obj) - const execParams = parseEncodedTxnToExecParams(this.interpreter.subTxn, this.interpreter, this.line); + // singer will be contractAccount + const execParams = encTxToExecParams( + this.interpreter.subTxn, + { + sign: types.SignType.SecretKey, + fromAccount: contractAccount + }, + this.interpreter.runtime.ctx, + this.line + ); const baseCurrTx = this.interpreter.runtime.ctx.tx; const baseCurrTxGrp = this.interpreter.runtime.ctx.gtxs; diff --git a/packages/runtime/src/lib/itxn.ts b/packages/runtime/src/lib/itxn.ts index 981a8ee95..3985fba23 100644 --- a/packages/runtime/src/lib/itxn.ts +++ b/packages/runtime/src/lib/itxn.ts @@ -1,15 +1,14 @@ -import { types } from "@algo-builder/web"; -import { decodeAddress, encodeAddress, getApplicationAddress } from "algosdk"; +import { decodeAddress } from "algosdk"; import cloneDeep from "lodash.clonedeep"; import { Interpreter } from ".."; import { RUNTIME_ERRORS } from "../errors/errors-list"; import { RuntimeError } from "../errors/runtime-errors"; import { Op } from "../interpreter/opcode"; -import { MaxTxnNoteBytes, TxnFields, TxnTypeMap, ZERO_ADDRESS_STR } from "../lib/constants"; -import { AccountAddress, EncTx, RuntimeAccountI, StackElem } from "../types"; +import { MaxTxnNoteBytes, TxnFields, TxnTypeMap } from "../lib/constants"; +import { EncTx, StackElem } from "../types"; import { convertToString } from "./parsing"; -import { assetTxnFields, isEncTxAssetConfig, isEncTxAssetDeletion } from "./txn"; +import { assetTxnFields } from "./txn"; // requires their type as number const numberTxnFields: {[key: number]: Set} = { @@ -257,140 +256,3 @@ export function setInnerTxField ( return subTxn; } - -const _getRuntimeAccount = (publickey: Buffer | undefined, - interpreter: Interpreter, line: number): RuntimeAccountI | undefined => { - if (publickey === undefined) { return undefined; } - const address = encodeAddress(Uint8Array.from(publickey)); - const runtimeAcc = interpreter.runtime.assertAccountDefined( - address, - interpreter.runtime.ctx.state.accounts.get(address), - line - ); - return runtimeAcc.account; -}; - -const _getRuntimeAccountAddr = (publickey: Buffer | undefined, - interpreter: Interpreter, line: number): AccountAddress | undefined => { - return _getRuntimeAccount(publickey, interpreter, line)?.addr; -}; - -const _getASAConfigAddr = (addr?: Uint8Array): string => { - if (addr) { - return encodeAddress(addr); - } - return ""; -}; - -const _getAddress = (addr?: Uint8Array): string | undefined => { - if (addr) { return encodeAddress(addr); } - return undefined; -}; - -// parse encoded txn obj to execParams (params passed by user in algob) -/* eslint-disable sonarjs/cognitive-complexity */ -export function parseEncodedTxnToExecParams (tx: EncTx, - interpreter: Interpreter, line: number): types.ExecParams { - // signer is the contract - const appID = interpreter.runtime.ctx.tx.apid ?? 0; - const appAddress = getApplicationAddress(appID); - - // initial common fields - const execParams: any = { - sign: types.SignType.SecretKey, - fromAccount: { addr: appAddress, sk: Buffer.from([]) }, // signer is the contract - fromAccountAddr: encodeAddress(tx.snd), - payFlags: { - totalFee: tx.fee, - firstValid: tx.fv, - note: tx.note - } - }; - - switch (tx.type) { - case 'pay': { - execParams.type = types.TransactionType.TransferAlgo; - execParams.toAccountAddr = - _getRuntimeAccountAddr(tx.rcv, interpreter, line) ?? ZERO_ADDRESS_STR; - execParams.amountMicroAlgos = tx.amt ?? 0n; - execParams.payFlags.closeRemainderTo = _getRuntimeAccountAddr(tx.close, interpreter, line); - execParams.payFlags.rekeyTo = _getAddress(tx.rekey); - break; - } - case 'afrz': { - execParams.type = types.TransactionType.FreezeAsset; - execParams.assetID = tx.faid; - execParams.freezeTarget = _getRuntimeAccountAddr(tx.fadd, interpreter, line); - execParams.freezeState = BigInt(tx.afrz ?? 0n) === 1n; - execParams.payFlags.rekeyTo = _getAddress(tx.rekey); - break; - } - case 'axfer': { - if (tx.asnd !== undefined) { // if 'AssetSender' is set, it is clawback transaction - execParams.type = types.TransactionType.RevokeAsset; - execParams.recipient = - _getRuntimeAccountAddr(tx.arcv, interpreter, line) ?? ZERO_ADDRESS_STR; - execParams.revocationTarget = _getRuntimeAccountAddr(tx.asnd, interpreter, line); - } else { // asset transfer - execParams.type = types.TransactionType.TransferAsset; - execParams.toAccountAddr = - _getRuntimeAccountAddr(tx.arcv, interpreter, line) ?? ZERO_ADDRESS_STR; - } - // set common fields (asset amount, index, closeRemTo) - execParams.amount = tx.aamt ?? 0n; - execParams.assetID = tx.xaid ?? 0; - execParams.payFlags.closeRemainderTo = _getRuntimeAccountAddr(tx.aclose, interpreter, line); - execParams.payFlags.rekeyTo = _getAddress(tx.rekey); - break; - } - case 'acfg': { // can be asset modification, destroy, or deployment(create) - if (isEncTxAssetDeletion(tx)) { - execParams.type = types.TransactionType.DestroyAsset; - execParams.assetID = tx.caid; - } else if (isEncTxAssetConfig(tx)) { - // from the docs: all fields must be reset, otherwise they will be cleared - // https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/#asset-configuration - execParams.type = types.TransactionType.ModifyAsset; - execParams.assetID = tx.caid; - execParams.fields = { - manager: _getASAConfigAddr(tx.apar?.m), - reserve: _getASAConfigAddr(tx.apar?.r), - clawback: _getASAConfigAddr(tx.apar?.c), - freeze: _getASAConfigAddr(tx.apar?.f) - }; - } else { // if not delete or modify, it's ASA deployment - execParams.type = types.TransactionType.DeployASA; - execParams.asaName = tx.apar?.an; - execParams.asaDef = { - name: tx.apar?.an, - total: tx.apar?.t, - decimals: tx.apar?.dc !== undefined ? Number(tx.apar.dc) : undefined, - defaultFrozen: BigInt(tx.apar?.df ?? 0n) === 1n, - unitName: tx.apar?.un, - url: tx.apar?.au, - metadataHash: tx.apar?.am, - manager: _getASAConfigAddr(tx.apar?.m), - reserve: _getASAConfigAddr(tx.apar?.r), - clawback: _getASAConfigAddr(tx.apar?.c), - freeze: _getASAConfigAddr(tx.apar?.f) - }; - } - execParams.payFlags.rekeyTo = _getAddress(tx.rekey); - break; - } - - case 'keyreg': - execParams.type = types.TransactionType.KeyRegistration; - execParams.voteKey = tx.votekey; - execParams.selectionKey = tx.selkey; - execParams.voteFirst = tx.votefst; - execParams.voteLast = tx.votelst; - execParams.voteKeyDilution = tx.votekd; - break; - default: { - throw new Error(`unsupported type for itxn_submit at line ${line}, for version ${interpreter.tealVersion}`); - } - } - - return execParams; -} diff --git a/packages/runtime/src/lib/txn.ts b/packages/runtime/src/lib/txn.ts index 47670f661..32df1f7aa 100644 --- a/packages/runtime/src/lib/txn.ts +++ b/packages/runtime/src/lib/txn.ts @@ -1,11 +1,12 @@ -import { parsing } from "@algo-builder/web"; -import { EncodedAssetParams, EncodedGlobalStateSchema, Transaction } from "algosdk"; +import { parsing, types } from "@algo-builder/web"; +import { encodeAddress, EncodedAssetParams, EncodedGlobalStateSchema, Transaction } from "algosdk"; import { RUNTIME_ERRORS } from "../errors/errors-list"; import { RuntimeError } from "../errors/runtime-errors"; import { Op } from "../interpreter/opcode"; -import { TxFieldDefaults, TxnFields } from "../lib/constants"; -import { EncTx, StackElem, TxField, TxnType } from "../types"; +import { TransactionTypeEnum, TxFieldDefaults, TxnFields, ZERO_ADDRESS_STR } from "../lib/constants"; +import { Context, EncTx, RuntimeAccountI, StackElem, TxField, TxnType } from "../types"; +import { convertToString } from "./parsing"; export const assetTxnFields = new Set([ 'ConfigAssetTotal', @@ -50,7 +51,7 @@ export function parseToStackElem (a: unknown, field: TxField): StackElem { * https://github.com/algorand/js-algorand-sdk/blob/e07d99a2b6bd91c4c19704f107cfca398aeb9619/src/transaction.ts#L528 */ export function checkIfAssetDeletionTx (txn: Transaction): boolean { - return txn.type === 'acfg' && // type should be asset config + return (String(txn.type) === TransactionTypeEnum.ASSET_CONFIG) && // type should be asset config (txn.assetIndex > 0) && // assetIndex should not be 0 !(txn.assetClawback || txn.assetFreeze || txn.assetManager || txn.assetReserve); // fields should be empty } @@ -177,7 +178,7 @@ export function txAppArg (txField: TxField, tx: EncTx, idx: number, op: Op, * https://github.com/algorand/js-algorand-sdk/blob/e07d99a2b6bd91c4c19704f107cfca398aeb9619/src/transaction.ts#L528 */ export function isEncTxAssetDeletion (txn: EncTx): boolean { - return txn.type === 'acfg' && // type should be asset config + return txn.type === TransactionTypeEnum.ASSET_CONFIG && // type should be asset config (txn.caid !== undefined && txn.caid !== 0) && // assetIndex should not be 0 !(txn.apar?.m ?? txn.apar?.r ?? txn.apar?.f ?? txn.apar?.c); // fields should be empty } @@ -187,7 +188,192 @@ export function isEncTxAssetDeletion (txn: EncTx): boolean { * @param txn Encoded EncTx Object */ export function isEncTxAssetConfig (txn: EncTx): boolean { - return txn.type === 'acfg' && // type should be asset config + return txn.type === TransactionTypeEnum.ASSET_CONFIG && // type should be asset config (txn.caid !== undefined && txn.caid !== 0) && // assetIndex should not be 0 !isEncTxAssetDeletion(txn); // AND should not be asset deletion } + +/** + * Check if given encoded transaction object is app creation + * @param txn Encoded EncTx Object + */ +export function isEncTxApplicationCreate (txn: EncTx): boolean { + return txn.type === TransactionTypeEnum.APPLICATION_CALL && (txn.apan === 0 || txn.apan === undefined); +} + +/** + * + * @param txAndSign transaction and sign + * @param ctx context which is tx and sign apply + * @returns ExecParams object equivalent with txAndSign + */ +export function transactionAndSignToExecParams ( + txAndSign: types.TransactionAndSign, ctx: Context +): types.ExecParams { + const transaction = txAndSign.transaction as any; + const encTx = transaction.get_obj_for_encoding() as EncTx; + // inject approval Program and clear program with string format. + // TODO: should create function to convert TEAL in Uint8Array to string format? + encTx.approvalProgram = transaction.approvalProgram; + encTx.clearProgram = transaction.clearProgram; + const sign = txAndSign.sign; + return encTxToExecParams(encTx, sign, ctx); +} + +/* eslint-disable sonarjs/cognitive-complexity */ +export function encTxToExecParams ( + encTx: EncTx, sign: types.Sign, ctx: Context, line?: number +): types.ExecParams { + const execParams: any = { + ...sign, + payFlags: {} as types.ExecParams + }; + + execParams.payFlags.totalFee = encTx.fee; + + switch (encTx.type) { + case TransactionTypeEnum.APPLICATION_CALL: { + if (isEncTxApplicationCreate(encTx)) { + execParams.type = types.TransactionType.DeployApp; + execParams.approvalProgram = encTx.approvalProgram; + execParams.clearProgram = encTx.clearProgram; + execParams.localInts = encTx.apgs?.nui; + execParams.localBytes = encTx.apgs?.nbs; + execParams.globalInts = encTx.apgs?.nui; + execParams.globalBytes = encTx.apgs?.nbs; + } + break; + } + + case TransactionTypeEnum.PAYMENT: { + execParams.type = types.TransactionType.TransferAlgo; + execParams.fromAccountAddr = _getAddress(encTx.snd); + execParams.toAccountAddr = + getRuntimeAccountAddr(encTx.rcv, ctx, line) ?? ZERO_ADDRESS_STR; + execParams.amountMicroAlgos = encTx.amt ?? 0n; + if (encTx.close) { + execParams.payFlags.closeRemainderTo = getRuntimeAccountAddr(encTx.close, ctx, line); + } + if (encTx.rekey) { + execParams.payFlags.rekeyTo = _getAddress(encTx.rekey); + } + break; + } + case TransactionTypeEnum.ASSET_FREEZE: { + execParams.type = types.TransactionType.FreezeAsset; + execParams.assetID = encTx.faid; + execParams.freezeTarget = getRuntimeAccountAddr(encTx.fadd, ctx, line); + execParams.freezeState = BigInt(encTx.afrz ?? 0n) === 1n; + if (encTx.rekey) { + execParams.payFlags.rekeyTo = _getAddress(encTx.rekey); + } + break; + } + case TransactionTypeEnum.ASSET_TRANSFER: { + if (encTx.asnd !== undefined) { // if 'AssetSender' is set, it is clawback transaction + execParams.type = types.TransactionType.RevokeAsset; + execParams.recipient = + getRuntimeAccountAddr(encTx.arcv, ctx, line) ?? ZERO_ADDRESS_STR; + execParams.revocationTarget = getRuntimeAccountAddr(encTx.asnd, ctx, line); + } else { // asset transfer + execParams.type = types.TransactionType.TransferAsset; + execParams.toAccountAddr = + getRuntimeAccountAddr(encTx.arcv, ctx) ?? ZERO_ADDRESS_STR; + } + // set common fields (asset amount, index, closeRemainderTo) + execParams.amount = encTx.aamt ?? 0n; + execParams.assetID = encTx.xaid ?? 0; + // option fields + if (encTx.aclose) { + execParams.payFlags.closeRemainderTo = getRuntimeAccountAddr(encTx.aclose, ctx, line); + } + if (encTx.rekey) { + execParams.payFlags.rekeyTo = _getAddress(encTx.rekey); + } + break; + } + + case TransactionTypeEnum.ASSET_CONFIG: { + if (isEncTxAssetDeletion(encTx)) { + execParams.type = types.TransactionType.DestroyAsset; + execParams.assetID = encTx.caid; + } else if (isEncTxAssetConfig(encTx)) { + // from the docs: all fields must be reset, otherwise they will be cleared + // https://developer.algorand.org/docs/get-details/dapps/smart-contracts/apps/#asset-configuration + execParams.type = types.TransactionType.ModifyAsset; + execParams.assetID = encTx.caid; + execParams.fields = { + manager: _getASAConfigAddr(encTx.apar?.m), + reserve: _getASAConfigAddr(encTx.apar?.r), + clawback: _getASAConfigAddr(encTx.apar?.c), + freeze: _getASAConfigAddr(encTx.apar?.f) + }; + } else { // if not delete or modify, it's ASA deployment + execParams.type = types.TransactionType.DeployASA; + execParams.asaName = encTx.apar?.an; + execParams.asaDef = { + name: encTx.apar?.an, + total: Number(encTx.apar?.t), + decimals: encTx.apar?.dc !== undefined ? Number(encTx.apar.dc) : 0, + defaultFrozen: BigInt(encTx.apar?.df ?? 0n) === 1n, + unitName: encTx.apar?.un, + url: encTx.apar?.au, + metadataHash: encTx.apar?.am ? convertToString(encTx.apar?.am) : undefined, + manager: _getASAConfigAddr(encTx.apar?.m), + reserve: _getASAConfigAddr(encTx.apar?.r), + clawback: _getASAConfigAddr(encTx.apar?.c), + freeze: _getASAConfigAddr(encTx.apar?.f) + }; + } + break; + } + + case TransactionTypeEnum.KEY_REGISTRATION: { + execParams.type = types.TransactionType.KeyRegistration; + execParams.voteKey = encTx.votekey?.toString('base64'); + execParams.selectionKey = encTx.selkey?.toString('base64'); + execParams.voteFirst = encTx.votefst; + execParams.voteLast = encTx.votelst; + execParams.voteKeyDilution = encTx.votekd; + break; + } + + default: { + // if line is defined => called from ItxnSubmit + // => throw error with itxn_submit + if (line) { + throw new Error(`unsupported type for itxn_submit at line ${line}`); + } else { + throw new Error("Can't convert encode tx to execParams"); + } + } + }; + return execParams as types.ExecParams; +} + +const _getASAConfigAddr = (addr?: Uint8Array): string => { + if (addr) { + return encodeAddress(addr); + } + return ""; +}; + +const getRuntimeAccount = (publicKey: Buffer | undefined, + ctx: Context, line?: number): RuntimeAccountI | undefined => { + if (publicKey === undefined) { return undefined; } + const address = encodeAddress(Uint8Array.from(publicKey)); + const runtimeAcc = ctx.getAccount( + address + ); + return runtimeAcc.account; +}; + +const getRuntimeAccountAddr = (publickey: Buffer | undefined, + ctx: Context, line?: number): types.AccountAddress | undefined => { + return getRuntimeAccount(publickey, ctx, line)?.addr; +}; + +const _getAddress = (addr?: Uint8Array): string | undefined => { + if (addr) { return encodeAddress(addr); } + return undefined; +}; diff --git a/packages/runtime/src/runtime.ts b/packages/runtime/src/runtime.ts index a5512d2e9..945cb22d6 100644 --- a/packages/runtime/src/runtime.ts +++ b/packages/runtime/src/runtime.ts @@ -14,6 +14,7 @@ import { ZERO_ADDRESS_STR } from "./lib/constants"; import { convertToString } from "./lib/parsing"; +import { transactionAndSignToExecParams } from "./lib/txn"; import { LogicSigAccount } from "./logicsig"; import { mockSuggestedParams } from "./mock/tx"; import { @@ -807,22 +808,42 @@ export class Runtime { * @param debugStack: if passed then TEAL Stack is logged to console after * each opcode execution (upto depth = debugStack) */ - executeTx (txnParams: types.ExecParams | types.ExecParams[], debugStack?: number): TxReceipt | TxReceipt[] { - const txnParameters = Array.isArray(txnParams) ? txnParams : [txnParams]; - for (const txn of txnParameters) { - switch (txn.type) { - case types.TransactionType.DeployASA: { - if (txn.asaDef === undefined) txn.asaDef = this.loadedAssetsDefs[txn.asaName]; - break; - } - case types.TransactionType.DeployApp: { - txn.approvalProg = new Uint8Array(32); // mock approval program - txn.clearProg = new Uint8Array(32); // mock clear program - break; + executeTx ( + txnParams: types.ExecParams | types.ExecParams[] | types.TransactionAndSign | types.TransactionAndSign[], + debugStack?: number + ): TxReceipt | TxReceipt[] { + // TODO: union above and create new type in task below: + // https://www.pivotaltracker.com/n/projects/2452320/stories/181295625 + const txnParamerters = Array.isArray(txnParams) ? txnParams : [txnParams]; + + let tx, gtxs; + + if (types.isSDKTransactionAndSign(txnParamerters[0])) { + const sdkTxns: EncTx[] = txnParamerters.map((txnParamerter): EncTx => { + const txn = txnParamerter as types.TransactionAndSign; + return txn.transaction.get_obj_for_encoding() as EncTx; + }); + tx = sdkTxns[0]; + gtxs = sdkTxns; + } else { + for (const txnParamerter of txnParamerters) { + const txn = txnParamerter as types.ExecParams; + switch (txn.type) { + case types.TransactionType.DeployASA: { + if (txn.asaDef === undefined) txn.asaDef = this.loadedAssetsDefs[txn.asaName]; + break; + } + case types.TransactionType.DeployApp: { + txn.approvalProg = new Uint8Array(32); // mock approval program + txn.clearProg = new Uint8Array(32); // mock clear program + break; + } } } + // get current txn and txn group (as encoded obj) + [tx, gtxs] = this.createTxnContext(txnParamerters as types.ExecParams[]); } - const [tx, gtxs] = this.createTxnContext(txnParameters); // get current txn and txn group (as encoded obj) + // validate first and last rounds this.validateTxRound(gtxs); @@ -833,7 +854,12 @@ export class Runtime { // Run TEAL program associated with each transaction and // then execute the transaction without interacting with store. - const txReceipts = this.ctx.processTransactions(txnParameters); + const runtimeTxnParams: types.ExecParams[] = + txnParamerters.map( + (txn) => types.isSDKTransactionAndSign(txn) ? transactionAndSignToExecParams(txn, this.ctx) : txn + ); + + const txReceipts = this.ctx.processTransactions(runtimeTxnParams); // update store only if all the transactions are passed this.store = this.ctx.state; diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index ebd983f5b..33a9cf0d0 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -23,6 +23,11 @@ export type TEALStack = IStack; export interface EncTx extends EncodedTransaction { txID: string + // user should push raw string TEAL code - not compiled code + // for approvalProgram and clearProgram + approvalProgram?: string + clearProgram?: string + } export type TxField = keyof typeof TxnFields[2]; diff --git a/packages/runtime/test/integration/SDKTxnRuntime.ts b/packages/runtime/test/integration/SDKTxnRuntime.ts new file mode 100644 index 000000000..30d4fa1b5 --- /dev/null +++ b/packages/runtime/test/integration/SDKTxnRuntime.ts @@ -0,0 +1,144 @@ +import { tx as webTx, types } from "@algo-builder/web"; +import { assert } from "chai"; + +import { AccountStore } from "../../src/account"; +import { mockSuggestedParams } from "../../src/mock/tx"; +import { Runtime } from "../../src/runtime"; +import { AccountStoreI } from "../../src/types"; +import { useFixture } from "../helpers/integration"; + +const minBalance = BigInt(10 * 1e6); + +// TODO: add more test for all transaction types. +// https://www.pivotaltracker.com/n/projects/2452320/stories/181383052 +describe("Should execute SDK transaction object using runtime", function () { + const fee = 1000; + + let alice: AccountStoreI; + let smith: AccountStoreI; + let runtime: Runtime; + + let execParams: types.ExecParams; + + function mkTransactionAndSign (runtime: Runtime, execParams: types.ExecParams): types.TransactionAndSign { + const suggestedParams = mockSuggestedParams(execParams.payFlags, runtime.getRound()); + const transaction = webTx.mkTransaction(execParams, suggestedParams) as any; + let sign: types.Sign; + + // extract `sign` from execParams + if (execParams.sign === types.SignType.SecretKey) { + sign = { + sign: execParams.sign, + fromAccount: execParams.fromAccount + }; + } else { + sign = { + sign: execParams.sign, + lsig: execParams.lsig, + fromAccountAddr: execParams.fromAccountAddr + }; + } + + // inject approval and clear program in string format to transaction object. + // TODO: Should we create disassemble method to convert Uint8Array program format to string??? + if (execParams.type === types.TransactionType.DeployApp) { + transaction.approvalProgram = execParams.approvalProgram; + transaction.clearProgram = execParams.clearProgram; + } + return { + transaction, + sign + }; + } + + this.beforeEach(() => { + alice = new AccountStore(minBalance * 10n); + smith = new AccountStore(minBalance * 10n); + runtime = new Runtime([alice, smith]); + }); + + describe("ASA transaction", function () { + useFixture('asa-check'); + it("Should deploy ASA transaction", () => { + const asaName = 'gold'; + const asaDef = runtime.loadedAssetsDefs[asaName]; + + execParams = { + sign: types.SignType.SecretKey, + fromAccount: alice.account, + type: types.TransactionType.DeployASA, + asaName, + asaDef, + payFlags: { + totalFee: fee + } + }; + + const txAndSign = mkTransactionAndSign(runtime, execParams); + assert.doesNotThrow(() => runtime.executeTx(txAndSign)); + + const asaInfo = runtime.getAssetInfoFromName(asaName); + if (asaInfo) { + assert.isDefined(asaInfo); + assert.equal(asaInfo.creator, alice.address); + assert.isFalse(asaInfo.deleted); + assert.equal(asaDef.name, asaInfo.assetDef.name); + assert.equal(asaDef.defaultFrozen, asaInfo.assetDef.defaultFrozen); + assert.equal(asaDef.decimals, asaInfo.assetDef.decimals); + assert.equal(asaDef.total, asaInfo.assetDef.total); + assert.equal(asaDef.clawback, asaInfo.assetDef.clawback); + assert.equal(asaDef.freeze, asaInfo.assetDef.freeze); + assert.equal(asaDef.manager, asaInfo.assetDef.manager); + assert.equal(asaDef.reserve, asaInfo.assetDef.reserve); + } + }); + }); + + describe("Application Transaction", function () { + useFixture('stateful'); + let execParams: types.ExecParams; + this.beforeEach(() => { + execParams = { + type: types.TransactionType.DeployApp, + sign: types.SignType.SecretKey, + fromAccount: alice.account, + approvalProgram: 'counter-approval.teal', + clearProgram: 'clear.teal', + localBytes: 10, + localInts: 10, + globalBytes: 10, + globalInts: 10, + payFlags: { + totalFee: fee + } + }; + + const txAndSign = mkTransactionAndSign(runtime, execParams); + runtime.executeTx(txAndSign); + }); + + it("Should check if application exists after deployment", () => { + const appInfo = runtime.getAppInfoFromName('counter-approval.teal', 'clear.teal'); + assert.isDefined(appInfo); + }); + }); + + describe("Payment transaciton", function () { + it("Should transfer ALGO transaction", () => { + execParams = { + sign: types.SignType.SecretKey, + type: types.TransactionType.TransferAlgo, + fromAccount: alice.account, + toAccountAddr: smith.address, + amountMicroAlgos: 1000n, + payFlags: { + totalFee: fee + } + }; + + const txAndSign = mkTransactionAndSign(runtime, execParams); + + assert.doesNotThrow(() => runtime.executeTx(txAndSign)); + }); + }); +}); diff --git a/packages/runtime/test/src/interpreter/inner-transaction.ts b/packages/runtime/test/src/interpreter/inner-transaction.ts index 525d1066b..06d709c15 100644 --- a/packages/runtime/test/src/interpreter/inner-transaction.ts +++ b/packages/runtime/test/src/interpreter/inner-transaction.ts @@ -1036,6 +1036,8 @@ describe("Inner Transactions", function () { itxn_field ConfigAssetName byte "https://gold.rush/" itxn_field ConfigAssetURL + byte "12312442142141241244444411111133" + itxn_field ConfigAssetMetadataHash itxn_submit int 1 `; diff --git a/packages/runtime/test/src/lib/txn.ts b/packages/runtime/test/src/lib/txn.ts new file mode 100644 index 000000000..d7f834857 --- /dev/null +++ b/packages/runtime/test/src/lib/txn.ts @@ -0,0 +1,182 @@ +import { types } from "@algo-builder/web"; +import { stringToBytes } from "@algo-builder/web/build/lib/parsing"; +import { assert } from "chai"; +import { encodeBase64 } from "tweetnacl-ts"; + +import { AccountStore } from "../../../src"; +import { encTxToExecParams } from "../../../src/lib/txn"; +import { Runtime } from "../../../src/runtime"; +import { AccountStoreI } from "../../../src/types"; +import { useFixture } from "../../helpers/integration"; + +describe("Convert encoded Txn to ExecParams", function () { + let john: AccountStoreI; + let smith: AccountStoreI; + + let runtime: Runtime; + let execParams: types.ExecParams; + this.beforeEach(() => { + john = new AccountStore(1e9); + smith = new AccountStore(1e9); + + runtime = new Runtime([john, smith]); + }); + + // helper - help convert and check param from EncTx to ExecParams + function assertEncTxConvertedToExecParam (runtime: Runtime, execParams: types.ExecParams): void { + const [encTx] = runtime.createTxnContext(execParams); + const sign = { + sign: types.SignType.SecretKey, + fromAccount: execParams.fromAccount + }; + // add approvalProgram and clearProgram to encTx + if (execParams.type === types.TransactionType.DeployApp) { + encTx.approvalProgram = execParams.approvalProgram; + encTx.clearProgram = execParams.clearProgram; + } + + assert.deepEqual(encTxToExecParams(encTx, sign as types.Sign, runtime.ctx), execParams); + }; + + describe("Case pay transaction types", function () { + it("Should convert SDK Payment Txn(pay) to ExecParams(TransferAlgo)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.TransferAlgo, + fromAccountAddr: john.address, + toAccountAddr: smith.address, + amountMicroAlgos: 1000n, + payFlags: { + totalFee: 1000, + closeRemainderTo: smith.address, + rekeyTo: smith.address + } + }; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + }); + + describe("Case acfg,axfer,afrz transaction types", function () { + useFixture("asa-check"); + it("Should convert SDK Deploy ASA Txn to ExecParams(DeployASA)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.DeployASA, + asaName: 'gold', + payFlags: { totalFee: 1000 } + }; + + execParams.asaDef = runtime.loadedAssetsDefs[execParams.asaName]; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + + it("Should convert SDK FreezeAsset ASA Txn to ExecParams(FreezeAsset)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.FreezeAsset, + payFlags: { + totalFee: 1000 + }, + assetID: 7, + freezeTarget: smith.address, + freezeState: true + }; + assertEncTxConvertedToExecParam(runtime, execParams); + }); + + it("Should convert SDK Transfer ASA Txn to ExecParams(TransferAsset)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.TransferAsset, + toAccountAddr: smith.address, + amount: 10, + assetID: 10, + payFlags: { + totalFee: 1000 + } + }; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + + it("Should convert SDK Destroy ASA Txn to ExecParams(DestroyAsset)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.DestroyAsset, + assetID: 10, + payFlags: { + totalFee: 1000 + } + }; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + + it("Should convert SDK Modify ASA Txn to ExecParams(ModifyAsset)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.ModifyAsset, + assetID: 10, + fields: { + clawback: smith.address, + freeze: smith.address, + manager: john.address, + reserve: smith.address + }, + payFlags: { + totalFee: 1000 + } + }; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + }); + + describe("Case keyreg transaction type", function () { + it("should convert SDK Keyreg Txn to ExecParams(KeyRegistration)", () => { + execParams = { + type: types.TransactionType.KeyRegistration, // payment + sign: types.SignType.SecretKey, + fromAccount: john.account, + voteKey: encodeBase64(stringToBytes('this-is-vote-key')), + selectionKey: encodeBase64(stringToBytes("this-is-selection-key")), + voteFirst: 43, + voteLast: 1000, + voteKeyDilution: 5, + payFlags: { totalFee: 1000 } + }; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + }); + + describe("Case appl transaction type", function () { + useFixture('stateful'); + it("should convert SDK Deploy Application Txn to ExecParams(DeployApp)", () => { + execParams = { + sign: types.SignType.SecretKey, + fromAccount: john.account, + type: types.TransactionType.DeployApp, + approvalProgram: "counter-approval.teal", + clearProgram: "clear.teal", + globalBytes: 10, + globalInts: 10, + localBytes: 10, + localInts: 10, + payFlags: { + totalFee: 1000 + } + }; + + assertEncTxConvertedToExecParam(runtime, execParams); + }); + }); +}); diff --git a/packages/runtime/test/src/runtime.ts b/packages/runtime/test/src/runtime.ts index e83c5fa1f..79230e491 100644 --- a/packages/runtime/test/src/runtime.ts +++ b/packages/runtime/test/src/runtime.ts @@ -1,5 +1,4 @@ import { types } from "@algo-builder/web"; -import { AccountAddress, AlgoTransferParam } from "@algo-builder/web/build/types"; import algosdk, { LogicSigAccount } from "algosdk"; import { assert } from "chai"; import sinon from "sinon"; @@ -134,7 +133,7 @@ describe("Transfer Algo Transaction", function () { this.beforeEach(function () { externalAccount = new AccountStore(0).account; - const transferAlgoTx: AlgoTransferParam = { + const transferAlgoTx: types.AlgoTransferParam = { type: types.TransactionType.TransferAlgo, sign: types.SignType.SecretKey, fromAccount: alice.account, @@ -155,7 +154,7 @@ describe("Transfer Algo Transaction", function () { }); it("Can create transaction from external account", () => { - const transferAlgoTx: AlgoTransferParam = { + const transferAlgoTx: types.AlgoTransferParam = { type: types.TransactionType.TransferAlgo, sign: types.SignType.SecretKey, fromAccount: externalRuntimeAccount.account,