diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts index 7fc46ea60..9e218fbab 100644 --- a/src/client/v2/algod/models/types.ts +++ b/src/client/v2/algod/models/types.ts @@ -5784,6 +5784,124 @@ export class PostTransactionsResponse implements Encodable { } } +/** + * Resource arrays that are required for a transaction + */ +export class ResourceArrays implements Encodable { + private static encodingSchemaValue: Schema | undefined; + + static get encodingSchema(): Schema { + if (!this.encodingSchemaValue) { + this.encodingSchemaValue = new NamedMapSchema([]); + (this.encodingSchemaValue as NamedMapSchema).pushEntries( + { + key: 'accounts', + valueSchema: new OptionalSchema(new ArraySchema(new StringSchema())), + omitEmpty: true, + }, + { + key: 'apps', + valueSchema: new OptionalSchema(new ArraySchema(new Uint64Schema())), + omitEmpty: true, + }, + { + key: 'assets', + valueSchema: new OptionalSchema(new ArraySchema(new Uint64Schema())), + omitEmpty: true, + }, + { + key: 'boxes', + valueSchema: new OptionalSchema( + new ArraySchema(BoxReference.encodingSchema) + ), + omitEmpty: true, + } + ); + } + return this.encodingSchemaValue; + } + + public accounts?: Address[]; + + public apps?: bigint[]; + + public assets?: bigint[]; + + public boxes?: BoxReference[]; + + /** + * Creates a new `ResourceArrays` object. + * @param accounts - + * @param apps - + * @param assets - + * @param boxes - + */ + constructor({ + accounts, + apps, + assets, + boxes, + }: { + accounts?: (Address | string)[]; + apps?: (number | bigint)[]; + assets?: (number | bigint)[]; + boxes?: BoxReference[]; + }) { + this.accounts = + typeof accounts !== 'undefined' + ? accounts.map((addr) => + typeof addr === 'string' ? Address.fromString(addr) : addr + ) + : undefined; + this.apps = + typeof apps === 'undefined' ? undefined : apps.map(ensureBigInt); + this.assets = + typeof assets === 'undefined' ? undefined : assets.map(ensureBigInt); + this.boxes = boxes; + } + + // eslint-disable-next-line class-methods-use-this + getEncodingSchema(): Schema { + return ResourceArrays.encodingSchema; + } + + toEncodingData(): Map { + return new Map([ + [ + 'accounts', + typeof this.accounts !== 'undefined' + ? this.accounts.map((v) => v.toString()) + : undefined, + ], + ['apps', this.apps], + ['assets', this.assets], + [ + 'boxes', + typeof this.boxes !== 'undefined' + ? this.boxes.map((v) => v.toEncodingData()) + : undefined, + ], + ]); + } + + static fromEncodingData(data: unknown): ResourceArrays { + if (!(data instanceof Map)) { + throw new Error(`Invalid decoded ResourceArrays: ${data}`); + } + return new ResourceArrays({ + accounts: data.get('accounts'), + apps: data.get('apps'), + assets: data.get('assets'), + boxes: + typeof data.get('boxes') !== 'undefined' + ? data + .get('boxes') + .map((v: unknown) => BoxReference.fromEncodingData(v)) + : undefined, + }); + } +} + /** * A write operation into a scratch slot. */ @@ -5971,6 +6089,11 @@ export class SimulateRequest implements Encodable { valueSchema: new OptionalSchema(new BooleanSchema()), omitEmpty: true, }, + { + key: 'populate-resources', + valueSchema: new OptionalSchema(new BooleanSchema()), + omitEmpty: true, + }, { key: 'round', valueSchema: new OptionalSchema(new Uint64Schema()), @@ -6018,6 +6141,12 @@ export class SimulateRequest implements Encodable { */ public fixSigners?: boolean; + /** + * If true, return populated resource arrays for each transaction based on unnamed + * resources + */ + public populateResources?: boolean; + /** * If provided, specifies the round preceding the simulation. State changes through * this round will be used to run this simulation. Usually only the 4 most recent @@ -6037,6 +6166,8 @@ export class SimulateRequest implements Encodable { * @param extraOpcodeBudget - Applies extra opcode budget during simulation for each transaction group. * @param fixSigners - If true, signers for transactions that are missing signatures will be fixed * during evaluation. + * @param populateResources - If true, return populated resource arrays for each transaction based on unnamed + * resources * @param round - If provided, specifies the round preceding the simulation. State changes through * this round will be used to run this simulation. Usually only the 4 most recent * rounds will be available (controlled by the node config value MaxAcctLookback). @@ -6050,6 +6181,7 @@ export class SimulateRequest implements Encodable { execTraceConfig, extraOpcodeBudget, fixSigners, + populateResources, round, }: { txnGroups: SimulateRequestTransactionGroup[]; @@ -6059,6 +6191,7 @@ export class SimulateRequest implements Encodable { execTraceConfig?: SimulateTraceConfig; extraOpcodeBudget?: number | bigint; fixSigners?: boolean; + populateResources?: boolean; round?: number | bigint; }) { this.txnGroups = txnGroups; @@ -6071,6 +6204,7 @@ export class SimulateRequest implements Encodable { ? undefined : ensureSafeInteger(extraOpcodeBudget); this.fixSigners = fixSigners; + this.populateResources = populateResources; this.round = typeof round === 'undefined' ? undefined : ensureBigInt(round); } @@ -6093,6 +6227,7 @@ export class SimulateRequest implements Encodable { ], ['extra-opcode-budget', this.extraOpcodeBudget], ['fix-signers', this.fixSigners], + ['populate-resources', this.populateResources], ['round', this.round], ]); } @@ -6114,6 +6249,7 @@ export class SimulateRequest implements Encodable { : undefined, extraOpcodeBudget: data.get('extra-opcode-budget'), fixSigners: data.get('fix-signers'), + populateResources: data.get('populate-resources'), round: data.get('round'), }); } @@ -6480,6 +6616,13 @@ export class SimulateTransactionGroupResult implements Encodable { valueSchema: new OptionalSchema(new Uint64Schema()), omitEmpty: true, }, + { + key: 'extra-resource-arrays', + valueSchema: new OptionalSchema( + new ArraySchema(ResourceArrays.encodingSchema) + ), + omitEmpty: true, + }, { key: 'failed-at', valueSchema: new OptionalSchema(new ArraySchema(new Uint64Schema())), @@ -6517,6 +6660,12 @@ export class SimulateTransactionGroupResult implements Encodable { */ public appBudgetConsumed?: number; + /** + * Present if populate-resource-arrays is true in the request and additional + * tranactions are needed to name all the accessed resources. + */ + public extraResourceArrays?: ResourceArrays[]; + /** * If present, indicates which transaction in this group caused the failure. This * array represents the path to the failing transaction. Indexes are zero based, @@ -6549,6 +6698,8 @@ export class SimulateTransactionGroupResult implements Encodable { * @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 extraResourceArrays - Present if populate-resource-arrays is true in the request and additional + * tranactions are needed to name all the accessed resources. * @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 @@ -6569,6 +6720,7 @@ export class SimulateTransactionGroupResult implements Encodable { txnResults, appBudgetAdded, appBudgetConsumed, + extraResourceArrays, failedAt, failureMessage, unnamedResourcesAccessed, @@ -6576,6 +6728,7 @@ export class SimulateTransactionGroupResult implements Encodable { txnResults: SimulateTransactionResult[]; appBudgetAdded?: number | bigint; appBudgetConsumed?: number | bigint; + extraResourceArrays?: ResourceArrays[]; failedAt?: (number | bigint)[]; failureMessage?: string; unnamedResourcesAccessed?: SimulateUnnamedResourcesAccessed; @@ -6589,6 +6742,7 @@ export class SimulateTransactionGroupResult implements Encodable { typeof appBudgetConsumed === 'undefined' ? undefined : ensureSafeInteger(appBudgetConsumed); + this.extraResourceArrays = extraResourceArrays; this.failedAt = typeof failedAt === 'undefined' ? undefined @@ -6607,6 +6761,12 @@ export class SimulateTransactionGroupResult implements Encodable { ['txn-results', this.txnResults.map((v) => v.toEncodingData())], ['app-budget-added', this.appBudgetAdded], ['app-budget-consumed', this.appBudgetConsumed], + [ + 'extra-resource-arrays', + typeof this.extraResourceArrays !== 'undefined' + ? this.extraResourceArrays.map((v) => v.toEncodingData()) + : undefined, + ], ['failed-at', this.failedAt], ['failure-message', this.failureMessage], [ @@ -6630,6 +6790,12 @@ export class SimulateTransactionGroupResult implements Encodable { ), appBudgetAdded: data.get('app-budget-added'), appBudgetConsumed: data.get('app-budget-consumed'), + extraResourceArrays: + typeof data.get('extra-resource-arrays') !== 'undefined' + ? data + .get('extra-resource-arrays') + .map((v: unknown) => ResourceArrays.fromEncodingData(v)) + : undefined, failedAt: data.get('failed-at'), failureMessage: data.get('failure-message'), unnamedResourcesAccessed: @@ -6679,6 +6845,11 @@ export class SimulateTransactionResult implements Encodable { valueSchema: new OptionalSchema(new Uint64Schema()), omitEmpty: true, }, + { + key: 'populated-resource-arrays', + valueSchema: new OptionalSchema(ResourceArrays.encodingSchema), + omitEmpty: true, + }, { key: 'unnamed-resources-accessed', valueSchema: new OptionalSchema( @@ -6720,6 +6891,12 @@ export class SimulateTransactionResult implements Encodable { */ public logicSigBudgetConsumed?: number; + /** + * Present if populate-resource-arrays is true in the request. In this case, it + * will be all of the resources this transaction needs to be evaluated. + */ + public populatedResourceArrays?: ResourceArrays; + /** * These are resources that were accessed by this group that would normally have * caused failure, but were allowed in simulation. Depending on where this object @@ -6744,6 +6921,8 @@ export class SimulateTransactionResult implements Encodable { * @param fixedSigner - The account that needed to sign this transaction when no signature was provided * and the provided signer was incorrect. * @param logicSigBudgetConsumed - Budget used during execution of a logic sig transaction. + * @param populatedResourceArrays - Present if populate-resource-arrays is true in the request. In this case, it + * will be all of the resources this transaction needs to be evaluated. * @param unnamedResourcesAccessed - These are resources that were accessed by this group that would normally have * caused failure, but were allowed in simulation. Depending on where this object * is in the response, the unnamed resources it contains may or may not qualify for @@ -6760,6 +6939,7 @@ export class SimulateTransactionResult implements Encodable { execTrace, fixedSigner, logicSigBudgetConsumed, + populatedResourceArrays, unnamedResourcesAccessed, }: { txnResult: PendingTransactionResponse; @@ -6767,6 +6947,7 @@ export class SimulateTransactionResult implements Encodable { execTrace?: SimulationTransactionExecTrace; fixedSigner?: Address | string; logicSigBudgetConsumed?: number | bigint; + populatedResourceArrays?: ResourceArrays; unnamedResourcesAccessed?: SimulateUnnamedResourcesAccessed; }) { this.txnResult = txnResult; @@ -6783,6 +6964,7 @@ export class SimulateTransactionResult implements Encodable { typeof logicSigBudgetConsumed === 'undefined' ? undefined : ensureSafeInteger(logicSigBudgetConsumed); + this.populatedResourceArrays = populatedResourceArrays; this.unnamedResourcesAccessed = unnamedResourcesAccessed; } @@ -6808,6 +6990,12 @@ export class SimulateTransactionResult implements Encodable { : undefined, ], ['logic-sig-budget-consumed', this.logicSigBudgetConsumed], + [ + 'populated-resource-arrays', + typeof this.populatedResourceArrays !== 'undefined' + ? this.populatedResourceArrays.toEncodingData() + : undefined, + ], [ 'unnamed-resources-accessed', typeof this.unnamedResourcesAccessed !== 'undefined' @@ -6834,6 +7022,12 @@ export class SimulateTransactionResult implements Encodable { : undefined, fixedSigner: data.get('fixed-signer'), logicSigBudgetConsumed: data.get('logic-sig-budget-consumed'), + populatedResourceArrays: + typeof data.get('populated-resource-arrays') !== 'undefined' + ? ResourceArrays.fromEncodingData( + data.get('populated-resource-arrays') + ) + : undefined, unnamedResourcesAccessed: typeof data.get('unnamed-resources-accessed') !== 'undefined' ? SimulateUnnamedResourcesAccessed.fromEncodingData( diff --git a/tests/cucumber/integration.tags b/tests/cucumber/integration.tags index ed8782d44..9ca5da561 100644 --- a/tests/cucumber/integration.tags +++ b/tests/cucumber/integration.tags @@ -17,3 +17,4 @@ @simulate.extra_opcode_budget @simulate.exec_trace_with_stack_scratch @simulate.exec_trace_with_state_change_and_hash +@simulate.populate_resources diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js index 2a27d309f..251f6be7f 100644 --- a/tests/cucumber/steps/steps.js +++ b/tests/cucumber/steps/steps.js @@ -5,6 +5,7 @@ const path = require('path'); const algosdk = require('../../../src/index'); const nacl = require('../../../src/nacl/naclWrappers'); +const { BoxReference } = require('../../../src/client/v2/algod/models/types'); const maindir = path.dirname(path.dirname(path.dirname(__dirname))); @@ -5833,6 +5834,62 @@ module.exports = function getSteps(options) { } ); + When('I set unnamed-resources {string}', function (string) { + this.simulateRequest.allowUnnamedResources = string === 'true'; + }); + + When('I set populate-resources {string}', function (string) { + this.simulateRequest.populateResources = string === 'true'; + }); + + Then( + 'the response should include populated-resource-arrays for the transaction', + function () { + // const resp: SimulateResponse = this.simulateResponse; + + const group = this.simulateResponse.txnGroups[0]; + const txnResult = group.txnResults[0]; + + const name = new Uint8Array(Buffer.from('box_key')); + + assert.deepEqual( + txnResult.populatedResourceArrays.apps, + [10000, 20000, 30000] + ); + assert.deepEqual(txnResult.populatedResourceArrays.assets, undefined); + assert.deepEqual(txnResult.populatedResourceArrays.boxes, [ + new BoxReference({ app: this.currentApplicationIndex, name }), + ]); + assert.deepEqual( + txnResult.populatedResourceArrays.accounts.map((a) => a.toString()), + [ + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ', + 'AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKE3PRHE', + 'AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGFFWAF4', + 'AMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANVWEXNA', + ] + ); + } + ); + + Then( + 'the response should include extra-resource-arrays for the group', + function () { + // Write code here that turns the phrase above into concrete actions + const { extraResourceArrays } = this.simulateResponse.txnGroups[0]; + + assert.deepEqual(extraResourceArrays[0].apps, [40000]); + assert.deepEqual(extraResourceArrays[0].assets, [10001]); + assert.deepEqual(extraResourceArrays[0].boxes, [ + new BoxReference({ app: 0, name: '' }), + ]); + assert.deepEqual( + extraResourceArrays[0].accounts.map((a) => a.toString()), + ['AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJVBPJXY'] + ); + } + ); + if (!options.ignoreReturn) { return steps; }