diff --git a/src/client/v2/algod/algod.ts b/src/client/v2/algod/algod.ts index 9dbb0c197..a794c90ce 100644 --- a/src/client/v2/algod/algod.ts +++ b/src/client/v2/algod/algod.ts @@ -32,6 +32,8 @@ import LightBlockHeaderProof from './lightBlockHeaderProof'; import StateProof from './stateproof'; import Disassemble from './disassemble'; import SimulateRawTransactions from './simulateTransaction'; +import { EncodedSignedTransaction } from '../../../types'; +import * as encoding from '../../../encoding/encoding'; /** * Algod client connects an application to the Algorand blockchain. The algod client requires a valid algod REST endpoint IP address and algod token from an Algorand node that is connected to the network you plan to interact with. @@ -582,6 +584,15 @@ export default class AlgodClient extends ServiceClient { * * #### Example * ```typescript + * const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject(txn1Params); + * const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject(txn2Params); + * const txgroup = algosdk.assignGroupID([txn1, txn2]); + * + * // Actually sign the first transaction + * const signedTxn1 = txgroup[0].signTxn(senderSk).blob; + * // Simulate does not require signed transactions -- use this method to encode an unsigned transaction + * const signedTxn2 = algosdk.encodeUnsignedSimulateTransaction(txgroup[1]); + * * const resp = await client.simulateRawTransactions([signedTxn1, signedTxn2]).do(); * ``` * @@ -590,6 +601,54 @@ export default class AlgodClient extends ServiceClient { * @category POST */ simulateRawTransactions(stxOrStxs: Uint8Array | Uint8Array[]) { - return new SimulateRawTransactions(this.c, stxOrStxs); + const txnObjects: EncodedSignedTransaction[] = []; + if (Array.isArray(stxOrStxs)) { + for (const stxn of stxOrStxs) { + txnObjects.push(encoding.decode(stxn) as EncodedSignedTransaction); + } + } else { + txnObjects.push(encoding.decode(stxOrStxs) as EncodedSignedTransaction); + } + const request = new modelsv2.SimulateRequest({ + txnGroups: [ + new modelsv2.SimulateRequestTransactionGroup({ + txns: txnObjects, + }), + ], + }); + return this.simulateTransactions(request); + } + + /** + * Simulate transactions being sent to the network. + * + * #### Example + * ```typescript + * const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject(txn1Params); + * const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject(txn2Params); + * const txgroup = algosdk.assignGroupID([txn1, txn2]); + * + * // Actually sign the first transaction + * const signedTxn1 = txgroup[0].signTxn(senderSk).blob; + * // Simulate does not require signed transactions -- use this method to encode an unsigned transaction + * const signedTxn2 = algosdk.encodeUnsignedSimulateTransaction(txgroup[1]); + * + * const request = new modelsv2.SimulateRequest({ + * txnGroups: [ + * new modelsv2.SimulateRequestTransactionGroup({ + * // Must decode the signed txn bytes into an object + * txns: [algosdk.decodeObj(signedTxn1), algosdk.decodeObj(signedTxn2)] + * }), + * ], + * }); + * const resp = await client.simulateRawTransactions(request).do(); + * ``` + * + * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/#post-v2transactionssimulate) + * @param request + * @category POST + */ + simulateTransactions(request: modelsv2.SimulateRequest) { + return new SimulateRawTransactions(this.c, request); } } diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index 066863396..192a3735c 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -877,17 +877,17 @@ export class ApplicationParams extends BaseModel { public extraProgramPages?: number | bigint; /** - * [\gs) global schema + * (gs) global state */ public globalState?: TealKeyValue[]; /** - * [\gsch) global schema + * (gsch) global schema */ public globalStateSchema?: ApplicationStateSchema; /** - * [\lsch) local schema + * (lsch) local schema */ public localStateSchema?: ApplicationStateSchema; @@ -898,9 +898,9 @@ export class ApplicationParams extends BaseModel { * @param creator - The address that created this application. This is the address where the * parameters and global state for this application can be found. * @param extraProgramPages - (epp) the amount of extra program pages available to this app. - * @param globalState - [\gs) global schema - * @param globalStateSchema - [\gsch) global schema - * @param localStateSchema - [\lsch) local schema + * @param globalState - (gs) global state + * @param globalStateSchema - (gsch) global schema + * @param localStateSchema - (lsch) local schema */ constructor({ approvalProgram, @@ -2825,7 +2825,7 @@ export class PendingTransactionResponse extends BaseModel { public confirmedRound?: number | bigint; /** - * (gd) Global state key/value changes for the application being executed by this + * Global state key/value changes for the application being executed by this * transaction. */ public globalStateDelta?: EvalDeltaKeyValue[]; @@ -2836,13 +2836,13 @@ export class PendingTransactionResponse extends BaseModel { public innerTxns?: PendingTransactionResponse[]; /** - * (ld) Local state key/value changes for the application being executed by this + * Local state key/value changes for the application being executed by this * transaction. */ public localStateDelta?: AccountStateDelta[]; /** - * (lg) Logs for the application being executed by this transaction. + * Logs for the application being executed by this transaction. */ public logs?: Uint8Array[]; @@ -2869,12 +2869,12 @@ export class PendingTransactionResponse extends BaseModel { * @param closeRewards - Rewards in microalgos applied to the close remainder to account. * @param closingAmount - Closing amount for the transaction. * @param confirmedRound - The round where this transaction was confirmed, if present. - * @param globalStateDelta - (gd) Global state key/value changes for the application being executed by this + * @param globalStateDelta - Global state key/value changes for the application being executed by this * transaction. * @param innerTxns - Inner transactions produced by application execution. - * @param localStateDelta - (ld) Local state key/value changes for the application being executed by this + * @param localStateDelta - Local state key/value changes for the application being executed by this * transaction. - * @param logs - (lg) Logs for the application being executed by this transaction. + * @param logs - Logs for the application being executed by this transaction. * @param receiverRewards - Rewards in microalgos applied to the receiver account. * @param senderRewards - Rewards in microalgos applied to the sender account. */ @@ -3084,6 +3084,82 @@ export class PostTransactionsResponse extends BaseModel { } } +/** + * Request type for simulation endpoint. + */ +export class SimulateRequest extends BaseModel { + /** + * The transaction groups to simulate. + */ + public txnGroups: SimulateRequestTransactionGroup[]; + + /** + * Creates a new `SimulateRequest` object. + * @param txnGroups - The transaction groups to simulate. + */ + constructor({ txnGroups }: { txnGroups: SimulateRequestTransactionGroup[] }) { + super(); + this.txnGroups = txnGroups; + + this.attribute_map = { + txnGroups: 'txn-groups', + }; + } + + // eslint-disable-next-line camelcase + static from_obj_for_encoding(data: Record): SimulateRequest { + /* eslint-disable dot-notation */ + if (!Array.isArray(data['txn-groups'])) + throw new Error( + `Response is missing required array field 'txn-groups': ${data}` + ); + return new SimulateRequest({ + txnGroups: data['txn-groups'].map( + SimulateRequestTransactionGroup.from_obj_for_encoding + ), + }); + /* eslint-enable dot-notation */ + } +} + +/** + * A transaction group to simulate. + */ +export class SimulateRequestTransactionGroup extends BaseModel { + /** + * An atomic transaction group. + */ + public txns: EncodedSignedTransaction[]; + + /** + * Creates a new `SimulateRequestTransactionGroup` object. + * @param txns - An atomic transaction group. + */ + constructor({ txns }: { txns: EncodedSignedTransaction[] }) { + super(); + this.txns = txns; + + this.attribute_map = { + txns: 'txns', + }; + } + + // eslint-disable-next-line camelcase + static from_obj_for_encoding( + data: Record + ): SimulateRequestTransactionGroup { + /* eslint-disable dot-notation */ + if (!Array.isArray(data['txns'])) + throw new Error( + `Response is missing required array field 'txns': ${data}` + ); + return new SimulateRequestTransactionGroup({ + txns: data['txns'], + }); + /* eslint-enable dot-notation */ + } +} + /** * Result of a transaction group simulation. */ @@ -3184,6 +3260,16 @@ export class SimulateTransactionGroupResult extends BaseModel { */ public txnResults: SimulateTransactionResult[]; + /** + * Total budget added during execution of app calls in the transaction group. + */ + public appBudgetAdded?: number | bigint; + + /** + * Total budget consumed during execution of app calls in the transaction group. + */ + public appBudgetConsumed?: number | bigint; + /** * If present, indicates which transaction in this group caused the failure. This * array represents the path to the failing transaction. Indexes are zero based, @@ -3201,6 +3287,8 @@ export class SimulateTransactionGroupResult extends BaseModel { /** * Creates a new `SimulateTransactionGroupResult` object. * @param txnResults - Simulation result for individual transactions + * @param appBudgetAdded - Total budget added during execution of app calls in the transaction group. + * @param appBudgetConsumed - Total budget consumed during execution of app calls in the transaction group. * @param failedAt - If present, indicates which transaction in this group caused the failure. This * array represents the path to the failing transaction. Indexes are zero based, * the first element indicates the top-level transaction, and successive elements @@ -3210,20 +3298,28 @@ export class SimulateTransactionGroupResult extends BaseModel { */ constructor({ txnResults, + appBudgetAdded, + appBudgetConsumed, failedAt, failureMessage, }: { txnResults: SimulateTransactionResult[]; + appBudgetAdded?: number | bigint; + appBudgetConsumed?: number | bigint; failedAt?: (number | bigint)[]; failureMessage?: string; }) { super(); this.txnResults = txnResults; + this.appBudgetAdded = appBudgetAdded; + this.appBudgetConsumed = appBudgetConsumed; this.failedAt = failedAt; this.failureMessage = failureMessage; this.attribute_map = { txnResults: 'txn-results', + appBudgetAdded: 'app-budget-added', + appBudgetConsumed: 'app-budget-consumed', failedAt: 'failed-at', failureMessage: 'failure-message', }; @@ -3242,6 +3338,8 @@ export class SimulateTransactionGroupResult extends BaseModel { txnResults: data['txn-results'].map( SimulateTransactionResult.from_obj_for_encoding ), + appBudgetAdded: data['app-budget-added'], + appBudgetConsumed: data['app-budget-consumed'], failedAt: data['failed-at'], failureMessage: data['failure-message'], }); @@ -3259,6 +3357,17 @@ export class SimulateTransactionResult extends BaseModel { */ public txnResult: PendingTransactionResponse; + /** + * Budget used during execution of an app call transaction. This value includes + * budged used by inner app calls spawned by this transaction. + */ + public appBudgetConsumed?: number | bigint; + + /** + * Budget used during execution of a logic sig transaction. + */ + public logicSigBudgetConsumed?: number | bigint; + /** * A boolean indicating whether this transaction is missing signatures */ @@ -3268,21 +3377,32 @@ export class SimulateTransactionResult extends BaseModel { * Creates a new `SimulateTransactionResult` object. * @param txnResult - Details about a pending transaction. If the transaction was recently confirmed, * includes confirmation details like the round and reward details. + * @param appBudgetConsumed - Budget used during execution of an app call transaction. This value includes + * budged used by inner app calls spawned by this transaction. + * @param logicSigBudgetConsumed - Budget used during execution of a logic sig transaction. * @param missingSignature - A boolean indicating whether this transaction is missing signatures */ constructor({ txnResult, + appBudgetConsumed, + logicSigBudgetConsumed, missingSignature, }: { txnResult: PendingTransactionResponse; + appBudgetConsumed?: number | bigint; + logicSigBudgetConsumed?: number | bigint; missingSignature?: boolean; }) { super(); this.txnResult = txnResult; + this.appBudgetConsumed = appBudgetConsumed; + this.logicSigBudgetConsumed = logicSigBudgetConsumed; this.missingSignature = missingSignature; this.attribute_map = { txnResult: 'txn-result', + appBudgetConsumed: 'app-budget-consumed', + logicSigBudgetConsumed: 'logic-sig-budget-consumed', missingSignature: 'missing-signature', }; } @@ -3300,6 +3420,8 @@ export class SimulateTransactionResult extends BaseModel { txnResult: PendingTransactionResponse.from_obj_for_encoding( data['txn-result'] ), + appBudgetConsumed: data['app-budget-consumed'], + logicSigBudgetConsumed: data['logic-sig-budget-consumed'], missingSignature: data['missing-signature'], }); /* eslint-enable dot-notation */ diff --git a/src/client/v2/algod/simulateTransaction.ts b/src/client/v2/algod/simulateTransaction.ts index 1bfcd49c7..80581c244 100644 --- a/src/client/v2/algod/simulateTransaction.ts +++ b/src/client/v2/algod/simulateTransaction.ts @@ -1,9 +1,8 @@ import { Buffer } from 'buffer'; import * as encoding from '../../../encoding/encoding'; -import { concatArrays } from '../../../utils/utils'; import HTTPClient from '../../client'; import JSONRequest from '../jsonrequest'; -import { SimulateResponse } from './models/types'; +import { SimulateRequest, SimulateResponse } from './models/types'; /** * Sets the default header (if not previously set) for simulating a raw @@ -14,15 +13,11 @@ export function setSimulateTransactionsHeaders(headers = {}) { let hdrs = headers; if (Object.keys(hdrs).every((key) => key.toLowerCase() !== 'content-type')) { hdrs = { ...headers }; - hdrs['Content-Type'] = 'application/x-binary'; + hdrs['Content-Type'] = 'application/msgpack'; } return hdrs; } -function isByteArray(array: any): array is Uint8Array { - return array && array.byteLength !== undefined; -} - /** * Simulates signed txns. */ @@ -30,23 +25,12 @@ export default class SimulateRawTransactions extends JSONRequest< SimulateResponse, Uint8Array > { - private txnBytesToPost: Uint8Array; + private requestBytes: Uint8Array; - constructor(c: HTTPClient, stxOrStxs: Uint8Array | Uint8Array[]) { + constructor(c: HTTPClient, request: SimulateRequest) { super(c); this.query.format = 'msgpack'; - - let forPosting = stxOrStxs; - if (Array.isArray(stxOrStxs)) { - if (!stxOrStxs.every(isByteArray)) { - throw new TypeError('Array elements must be byte arrays'); - } - // Flatten into a single Uint8Array - forPosting = concatArrays(...stxOrStxs); - } else if (!isByteArray(forPosting)) { - throw new TypeError('Argument must be byte array'); - } - this.txnBytesToPost = forPosting; + this.requestBytes = encoding.encode(request.get_obj_for_encoding(true)); } // eslint-disable-next-line class-methods-use-this @@ -58,7 +42,7 @@ export default class SimulateRawTransactions extends JSONRequest< const txHeaders = setSimulateTransactionsHeaders(headers); const res = await this.c.post( this.path(), - Buffer.from(this.txnBytesToPost), + Buffer.from(this.requestBytes), txHeaders, this.query, false diff --git a/src/transaction.ts b/src/transaction.ts index 2041a8a37..884de26bf 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -1290,10 +1290,9 @@ export function encodeUnsignedSimulateTransaction( transactionObject: Transaction ) { const objToEncode: EncodedSignedTransaction = { - sig: null, txn: transactionObject.get_obj_for_encoding(), }; - return encoding.rawEncode(objToEncode); + return encoding.encode(objToEncode); } /** diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 90e09b13e..27f3e1da5 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -4680,17 +4680,15 @@ module.exports = function getSteps(options) { const failedMessage = this.simulateResponse.txnGroups[groupNum] .failureMessage; - assert.deepStrictEqual(false, this.simulateResponse.wouldSucceed); - const errorContainsString = failedMessage.includes(errorMsg); - assert.deepStrictEqual(true, errorContainsString); + assert.ok(!this.simulateResponse.wouldSucceed); + assert.ok( + failedMessage.includes(errorMsg), + `Error message: "${failedMessage}" does not contain "${errorMsg}"` + ); // Check path array - // deepStrictEqual fails for firefox tests, so compare array manually. const { failedAt } = this.simulateResponse.txnGroups[groupNum]; - assert.strictEqual(failPath.length, failedAt.length); - for (let i = 0; i < failPath.length; i++) { - assert.strictEqual(failPath[i], failedAt[i]); - } + assert.deepStrictEqual(makeArray(...failedAt), makeArray(...failPath)); } );