From 8307a746570f1d6f52f04548de18a2a725107490 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Tue, 28 May 2024 11:08:34 -0400 Subject: [PATCH 01/21] add auto restore functionality for contract client --- src/contract/assembled_transaction.ts | 183 +++++++++++++++--- src/contract/sent_transaction.ts | 26 +-- src/contract/types.ts | 6 + .../soroban/assembled_transaction_test.js | 123 ++++++++++++ 4 files changed, 304 insertions(+), 34 deletions(-) create mode 100644 test/unit/server/soroban/assembled_transaction_test.js diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index e4d627c0f..8ab1250d3 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -5,6 +5,7 @@ import { BASE_FEE, Contract, Operation, + SorobanDataBuilder, StrKey, TransactionBuilder, authorizeEntry, @@ -308,6 +309,7 @@ export class AssembledTransaction { */ static Errors = { ExpiredState: class ExpiredStateError extends Error { }, + RestoreFailure: class RestoreFailureError extends Error { }, NeedsMoreSignatures: class NeedsMoreSignaturesError extends Error { }, NoSignatureNeeded: class NoSignatureNeededError extends Error { }, NoUnsignedNonInvokerAuthEntries: class NoUnsignedNonInvokerAuthEntriesError extends Error { }, @@ -413,6 +415,21 @@ export class AssembledTransaction { }); } + private static async getAccount( + options: AssembledTransactionOptions, + server?: Server + ): Promise { + if (!server) { + server = new Server(options.rpcUrl, { + allowHttp: options.allowHttp ?? false, + }); + } + const account = options.publicKey + ? await server.getAccount(options.publicKey) + : new Account(NULL_ACCOUNT, "0"); + return account; + } + /** * Construct a new AssembledTransaction. This is the only way to create a new * AssembledTransaction; the main constructor is private. @@ -437,9 +454,10 @@ export class AssembledTransaction { const tx = new AssembledTransaction(options); const contract = new Contract(options.contractId); - const account = options.publicKey - ? await tx.server.getAccount(options.publicKey) - : new Account(NULL_ACCOUNT, "0"); + const account = await AssembledTransaction.getAccount( + options, + tx.server + ); tx.raw = new TransactionBuilder(account, { fee: options.fee ?? BASE_FEE, @@ -453,27 +471,74 @@ export class AssembledTransaction { return tx; } - simulate = async (): Promise => { - if (!this.built) { - if (!this.raw) { - throw new Error( - "Transaction has not yet been assembled; " + - "call `AssembledTransaction.build` first.", - ); - } + private static async buildFootprintRestoreTransaction( + options: AssembledTransactionOptions, + sorobanData: SorobanDataBuilder | xdr.SorobanTransactionData, + account: Account, + fee: string + ): Promise> { + const tx = new AssembledTransaction(options); + tx.raw = new TransactionBuilder(account, { + fee, + networkPassphrase: options.networkPassphrase, + }) + .setSorobanData(sorobanData instanceof SorobanDataBuilder ? sorobanData.build() : sorobanData) + .addOperation(Operation.restoreFootprint({})) + .setTimeout(options.timeoutInSeconds ?? DEFAULT_TIMEOUT); + await tx.simulate(false); + return tx; + } - this.built = this.raw.build(); + simulate = async (restore?: boolean): Promise => { + if (!this.raw) { + throw new Error( + "Transaction has not yet been assembled; " + + "call `AssembledTransaction.build` first." + ); } - this.simulation = await this.server.simulateTransaction(this.built); + + restore = restore ?? this.options.restore; + this.built = this.raw.build(); // need to force re-calculation of simulationData for new simulation delete this.simulationResult; delete this.simulationTransactionData; + this.simulation = await this.server.simulateTransaction(this.built); + + if (Api.isSimulationRestore(this.simulation) && restore) { + const account = await AssembledTransaction.getAccount(this.options, this.server); + const result = await this.restoreFootprint( + this.simulation.restorePreamble, + account + ); + if (result.status === Api.GetTransactionStatus.SUCCESS) { + // need to rebuild the transaction with bumped account sequence number + const contract = new Contract(this.options.contractId); + this.raw = new TransactionBuilder(account, { + fee: this.options.fee ?? BASE_FEE, + networkPassphrase: this.options.networkPassphrase, + }) + .addOperation( + contract.call( + this.options.method, + ...(this.options.args ?? []) + ) + ) + .setTimeout( + this.options.timeoutInSeconds ?? DEFAULT_TIMEOUT + ); + await this.simulate(); + return this; + } + throw new AssembledTransaction.Errors.RestoreFailure( + `You need to restore some contract state before invoking this method. Automatic restore failed:\n${JSON.stringify(result)}` + ); + } if (Api.isSimulationSuccess(this.simulation)) { this.built = assembleTransaction( this.built, - this.simulation, + this.simulation ).build(); } @@ -502,11 +567,9 @@ export class AssembledTransaction { if (Api.isSimulationRestore(simulation)) { throw new AssembledTransaction.Errors.ExpiredState( - `You need to restore some contract state before you can invoke this method. ${JSON.stringify( - simulation, - null, - 2, - )}`, + `You need to restore some contract state before you can invoke this method.\n` + + 'You can set `restore` to true in the method options in order to ' + + 'automatically restore the contract state when needed.' ); } @@ -561,6 +624,7 @@ export class AssembledTransaction { signAndSend = async ({ force = false, signTransaction = this.options.signTransaction, + updateTimeout = true, }: { /** * If `true`, sign and send the transaction even if it is a read call @@ -570,6 +634,11 @@ export class AssembledTransaction { * You must provide this here if you did not provide one before */ signTransaction?: ClientOptions["signTransaction"]; + /** + * Whether or not to update the timeout value before signing + * and sending the transaction + */ + updateTimeout?: boolean; } = {}): Promise> => { if (!this.built) { throw new Error("Transaction has not yet been simulated"); @@ -578,26 +647,30 @@ export class AssembledTransaction { if (!force && this.isReadCall) { throw new AssembledTransaction.Errors.NoSignatureNeeded( "This is a read call. It requires no signature or sending. " + - "Use `force: true` to sign and send anyway.", + "Use `force: true` to sign and send anyway." ); } if (!signTransaction) { throw new AssembledTransaction.Errors.NoSigner( "You must provide a signTransaction function, either when calling " + - "`signAndSend` or when initializing your Client", + "`signAndSend` or when initializing your Client" ); } if (this.needsNonInvokerSigningBy().length) { throw new AssembledTransaction.Errors.NeedsMoreSignatures( "Transaction requires more signatures. " + - "See `needsNonInvokerSigningBy` for details.", + "See `needsNonInvokerSigningBy` for details." ); } const typeChecked: AssembledTransaction = this; - const sent = await SentTransaction.init(signTransaction, typeChecked); + const sent = await SentTransaction.init( + signTransaction, + typeChecked, + updateTimeout + ); return sent; }; @@ -789,4 +862,68 @@ export class AssembledTransaction { .readWrite().length; return authsCount === 0 && writeLength === 0; } + + /** + * Restores the footprint (resource ledger entries that can be read or written) + * of an expired transaction. + * + * The method will: + * 1. Build a new transaction aimed at restoring the necessary resources. + * 2. Sign this new transaction if a `signTransaction` handler is provided. + * 3. Send the signed transaction to the network. + * 4. Await and return the response from the network. + * + * Preconditions: + * - A `signTransaction` function must be provided during the Client initialization. + * - The provided `restorePreamble` should include a minimum resource fee and valid + * transaction data. + * - The `account` parameter should be an instance of a valid blockchain account. + * + * @param {Object} restorePreamble - The preamble object containing data required to + * build the restore transaction. + * @param {string} restorePreamble.minResourceFee - The minimum fee required to restore + * the necessary resources. + * @param {SorobanDataBuilder} restorePreamble.transactionData - The transaction data + * required for the restoration process. + * @param {Account} account - The account that is executing the footprint restore operation. + * + * @returns {Promise} - A promise resolving to the response + * from the network after the restore transaction is submitted. + * + * @throws {Error} - Throws an error if no `signTransaction` function is provided during + * Client initialization. + * @throws {AssembledTransaction.Errors.RestoreFailure} - Throws a custom error if the + * restore transaction fails, providing the details of the failure. + */ + async restoreFootprint( + restorePreamble: { + minResourceFee: string; + transactionData: SorobanDataBuilder; + }, + account: Account + ): Promise { + if(!this.options.signTransaction){ + throw new Error("For automatic restore to work you must provide a signTransaction function when initializing your Client"); + } + // first try restoring the contract + const restoreTx = await AssembledTransaction.buildFootprintRestoreTransaction( + { ...this.options }, + restorePreamble.transactionData, + account, + restorePreamble.minResourceFee + ); + const sentTransaction = await restoreTx.signAndSend({ + updateTimeout: false, + force: true, + }); + if (!sentTransaction.getTransactionResponse) { + // todo make better error message + throw new AssembledTransaction.Errors.RestoreFailure( + `Failure during restore. \n${JSON.stringify(sentTransaction)}` + ); + } + return sentTransaction.getTransactionResponse; + } + + } diff --git a/src/contract/sent_transaction.ts b/src/contract/sent_transaction.ts index d6f275d78..347b8111e 100644 --- a/src/contract/sent_transaction.ts +++ b/src/contract/sent_transaction.ts @@ -66,6 +66,7 @@ export class SentTransaction { }); } + /** * Initialize a `SentTransaction` from an existing `AssembledTransaction` and * a `signTransaction` function. This will also send the transaction to the @@ -76,24 +77,27 @@ export class SentTransaction { signTransaction: ClientOptions["signTransaction"], /** {@link AssembledTransaction} from which this SentTransaction was initialized */ assembled: AssembledTransaction, + updateTimeout: boolean = true, ): Promise> => { const tx = new SentTransaction(signTransaction, assembled); - const sent = await tx.send(); + const sent = await tx.send(updateTimeout); return sent; }; - private send = async (): Promise => { + private send = async (updateTimeout: boolean = true): Promise => { const timeoutInSeconds = this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT; - this.assembled.built = TransactionBuilder.cloneFrom(this.assembled.built!, { - fee: this.assembled.built!.fee, - timebounds: undefined, - sorobanData: new SorobanDataBuilder( - this.assembled.simulationData.transactionData.toXDR(), - ).build(), - }) - .setTimeout(timeoutInSeconds) - .build(); + if(updateTimeout) { + this.assembled.built = TransactionBuilder.cloneFrom(this.assembled.built!, { + fee: this.assembled.built!.fee, + timebounds: undefined, + sorobanData: new SorobanDataBuilder( + this.assembled.simulationData.transactionData.toXDR(), + ).build(), + }) + .setTimeout(timeoutInSeconds) + .build(); + } const signature = await this.signTransaction!( // `signAndSend` checks for `this.built` before calling `SentTransaction.init` diff --git a/src/contract/types.ts b/src/contract/types.ts index 8b92276c2..d329bd0da 100644 --- a/src/contract/types.ts +++ b/src/contract/types.ts @@ -109,6 +109,12 @@ export type MethodOptions = { * AssembledTransaction. Default: true */ simulate?: boolean; + + /** + * If true, will automatically attempt to restore the transaction if there + * are archived entries that need renewal. @default false + */ + restore?: boolean; }; export type AssembledTransactionOptions = MethodOptions & diff --git a/test/unit/server/soroban/assembled_transaction_test.js b/test/unit/server/soroban/assembled_transaction_test.js new file mode 100644 index 000000000..a97aef1eb --- /dev/null +++ b/test/unit/server/soroban/assembled_transaction_test.js @@ -0,0 +1,123 @@ +const { + Account, + Keypair, + Networks, + rpc, + SorobanDataBuilder, + xdr, + contract, +} = StellarSdk; +const { Server, AxiosClient, parseRawSimulation } = StellarSdk.rpc; + +const randomSecret = Keypair.random().secret(); +const restoreTxnData = StellarSdk.SorobanDataBuilder.fromXDR("AAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQ=="); + +describe("Server#assembledTransaction", () => { + const keypair = Keypair.random(); + const contractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"; + const networkPassphrase = "Standalone Network ; February 2017"; + const wallet = contract.basicNodeSigner(keypair, networkPassphrase); + const options = { + networkPassphrase, + contractId, + rpcUrl: serverUrl, + allowHttp: true, + publicKey: keypair.publicKey(), + ...wallet, + } + + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + + + it("assembled transaction restore footprint works", function (done) { + const simulateTransactionResponse = { + transactionData: restoreTxnData, + minResourceFee: "52641", + cost: { cpuInsns: "0", memBytes: "0" }, + latestLedger: 17027, + }; + + const sendTransactionResponse = { + "status": "PENDING", + "hash": "05870e35fc94e5424f72d125959760b5f60631d91452bde2d11126fb5044e35d", + "latestLedger": 17034, + "latestLedgerCloseTime": "1716483573" + }; + const getTransactionResponse = { + status: "SUCCESS", + latestLedger: 17037, + latestLedgerCloseTime: "1716483576", + oldestLedger: 15598, + oldestLedgerCloseTime: "1716482133", + applicationOrder: 1, + envelopeXdr: "AAAAAgAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQABm0IAAAvWAAAAAwAAAAEAAAAAAAAAAAAAAABmT3cbAAAAAAAAAAEAAAAAAAAAGgAAAAAAAAABAAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQAAAAG+w3upAAAAQGBfsx+gyi/2Dh6i+7Vbb6Ongw3HDcFDZ48eoadkUUvkq97zdPe3wYGFswZgT5/GXPqGDBi+iqHuZiYx5eSy3Qk=", + resultXdr: "AAAAAAAAiRkAAAAAAAAAAQAAAAAAAAAaAAAAAAAAAAA=", + resultMetaXdr: "AAAAAwAAAAAAAAACAAAAAwAAQowAAAAAAAAAABHCklg6riUqP9F21Lt2zdyIZIx9lSn7t3jGCD2+w3upAAAAF0h1Pp0AAAvWAAAAAgAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAMMQAAAABmTz9yAAAAAAAAAAEAAEKMAAAAAAAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQAAABdIdT6dAAAL1gAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAQowAAAAAZk919wAAAAAAAAABAAAACAAAAAMAAAwrAAAACc4pIDe7y0sRFHAghrdpB7ypfj4BVuZStvX4u0BC1S/YAAANVgAAAAAAAAABAABCjAAAAAnOKSA3u8tLERRwIIa3aQe8qX4+AVbmUrb1+LtAQtUv2AAAQ7cAAAAAAAAAAwAADCsAAAAJikpmJa7Pr3lTb+dhRP2N4TOYCqK4tL4tQhDYnNEijtgAAA1WAAAAAAAAAAEAAEKMAAAACYpKZiWuz695U2/nYUT9jeEzmAqiuLS+LUIQ2JzRIo7YAABDtwAAAAAAAAADAAAMMQAAAAlT7LdEin/CaQA3iscHqkwnEFlSh8jfTPTIhSQ5J8Ao0wAADVwAAAAAAAAAAQAAQowAAAAJU+y3RIp/wmkAN4rHB6pMJxBZUofI30z0yIUkOSfAKNMAAEO3AAAAAAAAAAMAAAwxAAAACQycyCYjh7j9CHnTm9OKCYXhgmXw6jdtoMsGHyPk8Aa+AAANXAAAAAAAAAABAABCjAAAAAkMnMgmI4e4/Qh505vTigmF4YJl8Oo3baDLBh8j5PAGvgAAQ7cAAAAAAAAAAgAAAAMAAEKMAAAAAAAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQAAABdIdT6dAAAL1gAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAQowAAAAAZk919wAAAAAAAAABAABCjAAAAAAAAAAAEcKSWDquJSo/0XbUu3bN3IhkjH2VKfu3eMYIPb7De6kAAAAXSHWDiQAAC9YAAAADAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAEKMAAAAAGZPdfcAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA", + ledger: 17036, + createdAt: "1716483575", + }; + this.axiosMock + .expects("post") + .withArgs( + serverUrl, + sinon.match({ + jsonrpc: "2.0", + id: 1, + method: "simulateTransaction", + }) + ) + .returns(Promise.resolve({ data: { result: simulateTransactionResponse } })); + this.axiosMock + .expects("post") + .withArgs(serverUrl, + sinon.match({ + jsonrpc: "2.0", + id: 1, + method: "getTransaction", + }) + ) + .returns(Promise.resolve({ data: { result: getTransactionResponse } })); + + this.axiosMock + .expects("post") + .withArgs(serverUrl, + sinon.match({ + jsonrpc: "2.0", + id: 1, + method: "sendTransaction", + }) + ) + .returns(Promise.resolve({ data: { result: sendTransactionResponse } })); + + contract.AssembledTransaction.buildFootprintRestoreTransaction( + options, + restoreTxnData, + new StellarSdk.Account( + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI", + "1", + ), + 52641, + ) + .then((txn) => txn.signAndSend({force: true, ...wallet, + updateTimeout: false + })) + .then((result) => { + expect(result.getTransactionResponse.status).to.equal(rpc.Api.GetTransactionStatus.SUCCESS); + done(); + }) + .catch((error) => { + // handle any errors that occurred during the promise chain + done(error); + }); + + }) +}); \ No newline at end of file From 56ccdd8406256d787309062bf382f0fe592c0759 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Wed, 29 May 2024 15:07:57 -0400 Subject: [PATCH 02/21] Update test/unit/server/soroban/assembled_transaction_test.js fix name Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> --- test/unit/server/soroban/assembled_transaction_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/server/soroban/assembled_transaction_test.js b/test/unit/server/soroban/assembled_transaction_test.js index a97aef1eb..fc438ef45 100644 --- a/test/unit/server/soroban/assembled_transaction_test.js +++ b/test/unit/server/soroban/assembled_transaction_test.js @@ -12,7 +12,7 @@ const { Server, AxiosClient, parseRawSimulation } = StellarSdk.rpc; const randomSecret = Keypair.random().secret(); const restoreTxnData = StellarSdk.SorobanDataBuilder.fromXDR("AAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQ=="); -describe("Server#assembledTransaction", () => { +describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { const keypair = Keypair.random(); const contractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"; const networkPassphrase = "Standalone Network ; February 2017"; From 5ba241ac251fbfcf2dc09c8bdb52930b95c7c1cc Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Wed, 29 May 2024 15:09:06 -0400 Subject: [PATCH 03/21] Update test/unit/server/soroban/assembled_transaction_test.js better description Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> --- test/unit/server/soroban/assembled_transaction_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/server/soroban/assembled_transaction_test.js b/test/unit/server/soroban/assembled_transaction_test.js index fc438ef45..5ee57bc27 100644 --- a/test/unit/server/soroban/assembled_transaction_test.js +++ b/test/unit/server/soroban/assembled_transaction_test.js @@ -38,7 +38,7 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { - it("assembled transaction restore footprint works", function (done) { + it("makes expected RPC calls", function (done) { const simulateTransactionResponse = { transactionData: restoreTxnData, minResourceFee: "52641", From 133051a7b6e68357d877417f30e83c9b8a61aafd Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Wed, 29 May 2024 15:29:13 -0400 Subject: [PATCH 04/21] Update src/contract/assembled_transaction.ts Better auto restore failure message. Co-authored-by: Chad Ostrowski <221614+chadoh@users.noreply.github.com> --- src/contract/assembled_transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 8ab1250d3..103d4983b 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -531,7 +531,7 @@ export class AssembledTransaction { return this; } throw new AssembledTransaction.Errors.RestoreFailure( - `You need to restore some contract state before invoking this method. Automatic restore failed:\n${JSON.stringify(result)}` + `Automatic restore failed! You set 'restore: true' but the attempted restore did not work. Result:\n${JSON.stringify(result)}` ); } From f5092b22abb618574ee473288d69bbf39dee8ef4 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 29 May 2024 17:20:25 -0400 Subject: [PATCH 05/21] extract getAccount to utils --- src/contract/assembled_transaction.ts | 20 ++++---------------- src/contract/utils.ts | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 103d4983b..d690aead3 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -27,6 +27,7 @@ import { DEFAULT_TIMEOUT, contractErrorPattern, implementsToString, + getAccount } from "./utils"; import { SentTransaction } from "./sent_transaction"; import { Spec } from "./spec"; @@ -415,20 +416,7 @@ export class AssembledTransaction { }); } - private static async getAccount( - options: AssembledTransactionOptions, - server?: Server - ): Promise { - if (!server) { - server = new Server(options.rpcUrl, { - allowHttp: options.allowHttp ?? false, - }); - } - const account = options.publicKey - ? await server.getAccount(options.publicKey) - : new Account(NULL_ACCOUNT, "0"); - return account; - } + /** * Construct a new AssembledTransaction. This is the only way to create a new @@ -454,7 +442,7 @@ export class AssembledTransaction { const tx = new AssembledTransaction(options); const contract = new Contract(options.contractId); - const account = await AssembledTransaction.getAccount( + const account = await getAccount( options, tx.server ); @@ -506,7 +494,7 @@ export class AssembledTransaction { this.simulation = await this.server.simulateTransaction(this.built); if (Api.isSimulationRestore(this.simulation) && restore) { - const account = await AssembledTransaction.getAccount(this.options, this.server); + const account = await getAccount(this.options, this.server); const result = await this.restoreFootprint( this.simulation.restorePreamble, account diff --git a/src/contract/utils.ts b/src/contract/utils.ts index 44e7b8b02..db99d7cbb 100644 --- a/src/contract/utils.ts +++ b/src/contract/utils.ts @@ -1,5 +1,8 @@ -import { xdr, cereal } from "@stellar/stellar-base"; -import type { AssembledTransaction } from "./assembled_transaction"; +import { xdr, cereal, Account } from "@stellar/stellar-base"; +import { Server } from "../rpc/server"; +import { NULL_ACCOUNT, type AssembledTransaction } from "./assembled_transaction"; +import { AssembledTransactionOptions } from "./types"; + /** * The default timeout for waiting for a transaction to be included in a block. @@ -107,3 +110,12 @@ export function processSpecEntryStream(buffer: Buffer) { } return res; } + +export async function getAccount( + options: AssembledTransactionOptions, + server: Server +): Promise { + return options.publicKey + ? await server.getAccount(options.publicKey) + : new Account(NULL_ACCOUNT, "0"); +} From 2dcfbaa32e5e8a4cba7c8b09eaa114c052a4854f Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 29 May 2024 17:26:14 -0400 Subject: [PATCH 06/21] TSDoc format --- src/contract/assembled_transaction.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index d690aead3..38911adbc 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -867,27 +867,23 @@ export class AssembledTransaction { * transaction data. * - The `account` parameter should be an instance of a valid blockchain account. * - * @param {Object} restorePreamble - The preamble object containing data required to - * build the restore transaction. - * @param {string} restorePreamble.minResourceFee - The minimum fee required to restore - * the necessary resources. - * @param {SorobanDataBuilder} restorePreamble.transactionData - The transaction data - * required for the restoration process. - * @param {Account} account - The account that is executing the footprint restore operation. - * - * @returns {Promise} - A promise resolving to the response - * from the network after the restore transaction is submitted. - * * @throws {Error} - Throws an error if no `signTransaction` function is provided during * Client initialization. * @throws {AssembledTransaction.Errors.RestoreFailure} - Throws a custom error if the * restore transaction fails, providing the details of the failure. */ async restoreFootprint( + /** + * The preamble object containing data required to + * build the restore transaction. + */ restorePreamble: { minResourceFee: string; transactionData: SorobanDataBuilder; }, + /** + * The account that is executing the footprint restore operation. + */ account: Account ): Promise { if(!this.options.signTransaction){ From 986ee7ca7c2d4f79ef15b493f115dcb5ca83ec8b Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 29 May 2024 17:29:05 -0400 Subject: [PATCH 07/21] make account optional for restoreFootprint --- src/contract/assembled_transaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 38911adbc..4e323db46 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -865,7 +865,6 @@ export class AssembledTransaction { * - A `signTransaction` function must be provided during the Client initialization. * - The provided `restorePreamble` should include a minimum resource fee and valid * transaction data. - * - The `account` parameter should be an instance of a valid blockchain account. * * @throws {Error} - Throws an error if no `signTransaction` function is provided during * Client initialization. @@ -884,11 +883,12 @@ export class AssembledTransaction { /** * The account that is executing the footprint restore operation. */ - account: Account + account?: Account ): Promise { if(!this.options.signTransaction){ throw new Error("For automatic restore to work you must provide a signTransaction function when initializing your Client"); } + account = account ?? await getAccount(this.options, this.server); // first try restoring the contract const restoreTx = await AssembledTransaction.buildFootprintRestoreTransaction( { ...this.options }, From 26a26488e48adf19e1acef0083fb2eefa8c7831c Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 29 May 2024 17:39:56 -0400 Subject: [PATCH 08/21] remove bald booleans --- src/contract/assembled_transaction.ts | 4 ++-- src/contract/sent_transaction.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 4e323db46..c1530b9a0 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -473,11 +473,11 @@ export class AssembledTransaction { .setSorobanData(sorobanData instanceof SorobanDataBuilder ? sorobanData.build() : sorobanData) .addOperation(Operation.restoreFootprint({})) .setTimeout(options.timeoutInSeconds ?? DEFAULT_TIMEOUT); - await tx.simulate(false); + await tx.simulate({ restore: false }); return tx; } - simulate = async (restore?: boolean): Promise => { + simulate = async ({ restore }: {restore?: boolean} = {}): Promise => { if (!this.raw) { throw new Error( "Transaction has not yet been assembled; " + diff --git a/src/contract/sent_transaction.ts b/src/contract/sent_transaction.ts index 347b8111e..697d129dc 100644 --- a/src/contract/sent_transaction.ts +++ b/src/contract/sent_transaction.ts @@ -66,7 +66,6 @@ export class SentTransaction { }); } - /** * Initialize a `SentTransaction` from an existing `AssembledTransaction` and * a `signTransaction` function. This will also send the transaction to the @@ -80,11 +79,11 @@ export class SentTransaction { updateTimeout: boolean = true, ): Promise> => { const tx = new SentTransaction(signTransaction, assembled); - const sent = await tx.send(updateTimeout); + const sent = await tx.send({ updateTimeout }); return sent; }; - private send = async (updateTimeout: boolean = true): Promise => { + private send = async ({ updateTimeout }: {updateTimeout?: boolean } = { updateTimeout: true }): Promise => { const timeoutInSeconds = this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT; if(updateTimeout) { From ad02f9fcd034c94ec8c54e743938768e1fa8618c Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 5 Jun 2024 17:30:33 -0400 Subject: [PATCH 09/21] cleanup, remove updateTimeout workaround, dont rebuild sorobandata --- src/contract/assembled_transaction.ts | 44 +++++++------------ src/contract/sent_transaction.ts | 25 +++++------ .../soroban/assembled_transaction_test.js | 4 +- 3 files changed, 27 insertions(+), 46 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index c1530b9a0..53033e03d 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -310,7 +310,7 @@ export class AssembledTransaction { */ static Errors = { ExpiredState: class ExpiredStateError extends Error { }, - RestoreFailure: class RestoreFailureError extends Error { }, + RestorationFailure: class RestoreFailureError extends Error { }, NeedsMoreSignatures: class NeedsMoreSignaturesError extends Error { }, NoSignatureNeeded: class NoSignatureNeededError extends Error { }, NoUnsignedNonInvokerAuthEntries: class NoUnsignedNonInvokerAuthEntriesError extends Error { }, @@ -330,8 +330,8 @@ export class AssembledTransaction { method: this.options.method, tx: this.built?.toXDR(), simulationResult: { - auth: this.simulationData.result.auth.map((a) => a.toXDR("base64")), - retval: this.simulationData.result.retval.toXDR("base64"), + auth: this.simulationData.result?.auth.map((a) => a.toXDR("base64")), + retval: this.simulationData.result?.retval.toXDR("base64"), }, simulationTransactionData: this.simulationData.transactionData.toXDR("base64"), @@ -416,8 +416,6 @@ export class AssembledTransaction { }); } - - /** * Construct a new AssembledTransaction. This is the only way to create a new * AssembledTransaction; the main constructor is private. @@ -518,7 +516,7 @@ export class AssembledTransaction { await this.simulate(); return this; } - throw new AssembledTransaction.Errors.RestoreFailure( + throw new AssembledTransaction.Errors.RestorationFailure( `Automatic restore failed! You set 'restore: true' but the attempted restore did not work. Result:\n${JSON.stringify(result)}` ); } @@ -534,7 +532,7 @@ export class AssembledTransaction { }; get simulationData(): { - result: Api.SimulateHostFunctionResult; + result?: Api.SimulateHostFunctionResult; transactionData: xdr.SorobanTransactionData; } { if (this.simulationResult && this.simulationTransactionData) { @@ -561,7 +559,7 @@ export class AssembledTransaction { ); } - if (!simulation.result) { + /*if (!simulation.result) { throw new Error( `Expected an invocation simulation, but got no 'result' field. Simulation: ${JSON.stringify( simulation, @@ -569,7 +567,7 @@ export class AssembledTransaction { 2, )}`, ); - } + }*/ // add to object for serialization & deserialization this.simulationResult = simulation.result; @@ -583,7 +581,10 @@ export class AssembledTransaction { get result(): T { try { - return this.options.parseResultXdr(this.simulationData.result.retval); + if(!this.simulationData.result){ + throw new Error("No simulation result!"); + } + return this.options.parseResultXdr(this.simulationData.result?.retval); } catch (e) { if (!implementsToString(e)) throw e; const err = this.parseError(e.toString()); @@ -612,7 +613,6 @@ export class AssembledTransaction { signAndSend = async ({ force = false, signTransaction = this.options.signTransaction, - updateTimeout = true, }: { /** * If `true`, sign and send the transaction even if it is a read call @@ -622,11 +622,6 @@ export class AssembledTransaction { * You must provide this here if you did not provide one before */ signTransaction?: ClientOptions["signTransaction"]; - /** - * Whether or not to update the timeout value before signing - * and sending the transaction - */ - updateTimeout?: boolean; } = {}): Promise> => { if (!this.built) { throw new Error("Transaction has not yet been simulated"); @@ -657,7 +652,6 @@ export class AssembledTransaction { const sent = await SentTransaction.init( signTransaction, typeChecked, - updateTimeout ); return sent; }; @@ -843,7 +837,7 @@ export class AssembledTransaction { * returns `false`, then you need to call `signAndSend` on this transaction. */ get isReadCall(): boolean { - const authsCount = this.simulationData.result.auth.length; + const authsCount = this.simulationData.result?.auth.length; const writeLength = this.simulationData.transactionData .resources() .footprint() @@ -880,9 +874,7 @@ export class AssembledTransaction { minResourceFee: string; transactionData: SorobanDataBuilder; }, - /** - * The account that is executing the footprint restore operation. - */ + /** The account that is executing the footprint restore operation. */ account?: Account ): Promise { if(!this.options.signTransaction){ @@ -896,14 +888,10 @@ export class AssembledTransaction { account, restorePreamble.minResourceFee ); - const sentTransaction = await restoreTx.signAndSend({ - updateTimeout: false, - force: true, - }); + const sentTransaction = await restoreTx.signAndSend(); if (!sentTransaction.getTransactionResponse) { - // todo make better error message - throw new AssembledTransaction.Errors.RestoreFailure( - `Failure during restore. \n${JSON.stringify(sentTransaction)}` + throw new AssembledTransaction.Errors.RestorationFailure( + `The attempt at automatic restore failed. \n${JSON.stringify(sentTransaction)}` ); } return sentTransaction.getTransactionResponse; diff --git a/src/contract/sent_transaction.ts b/src/contract/sent_transaction.ts index 697d129dc..8d033e648 100644 --- a/src/contract/sent_transaction.ts +++ b/src/contract/sent_transaction.ts @@ -1,6 +1,6 @@ /* disable max-classes rule, because extending error shouldn't count! */ /* eslint max-classes-per-file: 0 */ -import { SorobanDataBuilder, TransactionBuilder } from "@stellar/stellar-base"; +import { TransactionBuilder } from "@stellar/stellar-base"; import type { ClientOptions, MethodOptions, Tx } from "./types"; import { Server } from "../rpc/server" import { Api } from "../rpc/api" @@ -76,27 +76,22 @@ export class SentTransaction { signTransaction: ClientOptions["signTransaction"], /** {@link AssembledTransaction} from which this SentTransaction was initialized */ assembled: AssembledTransaction, - updateTimeout: boolean = true, ): Promise> => { const tx = new SentTransaction(signTransaction, assembled); - const sent = await tx.send({ updateTimeout }); + const sent = await tx.send(); return sent; }; - private send = async ({ updateTimeout }: {updateTimeout?: boolean } = { updateTimeout: true }): Promise => { + private send = async (): Promise => { const timeoutInSeconds = this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT; - if(updateTimeout) { - this.assembled.built = TransactionBuilder.cloneFrom(this.assembled.built!, { - fee: this.assembled.built!.fee, - timebounds: undefined, - sorobanData: new SorobanDataBuilder( - this.assembled.simulationData.transactionData.toXDR(), - ).build(), - }) - .setTimeout(timeoutInSeconds) - .build(); - } + this.assembled.built = TransactionBuilder.cloneFrom(this.assembled.built!, { + fee: this.assembled.built!.fee, + timebounds: undefined, // intentionally don't clone timebounds + sorobanData: this.assembled.simulationData.transactionData + }) + .setTimeout(timeoutInSeconds) + .build(); const signature = await this.signTransaction!( // `signAndSend` checks for `this.built` before calling `SentTransaction.init` diff --git a/test/unit/server/soroban/assembled_transaction_test.js b/test/unit/server/soroban/assembled_transaction_test.js index 5ee57bc27..f4428341b 100644 --- a/test/unit/server/soroban/assembled_transaction_test.js +++ b/test/unit/server/soroban/assembled_transaction_test.js @@ -107,9 +107,7 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { ), 52641, ) - .then((txn) => txn.signAndSend({force: true, ...wallet, - updateTimeout: false - })) + .then((txn) => txn.signAndSend({ ...wallet })) .then((result) => { expect(result.getTransactionResponse.status).to.equal(rpc.Api.GetTransactionStatus.SUCCESS); done(); From 2a2ce15175feb3ecb986392dcfbbfb93a2d365e6 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 5 Jun 2024 18:16:19 -0400 Subject: [PATCH 10/21] add changelog entry for auto restore functionality --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2642090a..25daad175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,12 @@ interface LedgerEntryChange { } ``` +- `contract.AssembledTransaction` now has a `restoreFootprint` method which accepts the +`restorePreamble` returned when a simulation call fails due to some contract state that +has expired. When invoking a contract function, one can now set `restore` to `true` in the +`MethodOptions`. When enabled, a `restoreFootprint` transaction will be created and await +signing whenever contract state is required to be restored before a contract function can +be invoked. ## [v12.0.0-rc.3](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.0-rc.3) From 0d096d1d3a6c7e7971be2b8f872767be09786183 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 5 Jun 2024 18:17:06 -0400 Subject: [PATCH 11/21] remove comment --- src/contract/assembled_transaction.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 53033e03d..8d644685d 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -559,16 +559,6 @@ export class AssembledTransaction { ); } - /*if (!simulation.result) { - throw new Error( - `Expected an invocation simulation, but got no 'result' field. Simulation: ${JSON.stringify( - simulation, - null, - 2, - )}`, - ); - }*/ - // add to object for serialization & deserialization this.simulationResult = simulation.result; this.simulationTransactionData = simulation.transactionData.build(); From 08ab2695dd0992fc7131c2406a64082482581fc4 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 5 Jun 2024 18:18:57 -0400 Subject: [PATCH 12/21] remove unused var --- test/unit/server/soroban/assembled_transaction_test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/unit/server/soroban/assembled_transaction_test.js b/test/unit/server/soroban/assembled_transaction_test.js index f4428341b..6b433fae6 100644 --- a/test/unit/server/soroban/assembled_transaction_test.js +++ b/test/unit/server/soroban/assembled_transaction_test.js @@ -9,7 +9,6 @@ const { } = StellarSdk; const { Server, AxiosClient, parseRawSimulation } = StellarSdk.rpc; -const randomSecret = Keypair.random().secret(); const restoreTxnData = StellarSdk.SorobanDataBuilder.fromXDR("AAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQ=="); describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { @@ -118,4 +117,4 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { }); }) -}); \ No newline at end of file +}); From e5db079f38925f9d88e13d40bfeb64c901ae8498 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 5 Jun 2024 18:27:35 -0400 Subject: [PATCH 13/21] simpler wording --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25daad175..af29cb81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,8 +43,7 @@ interface LedgerEntryChange { `restorePreamble` returned when a simulation call fails due to some contract state that has expired. When invoking a contract function, one can now set `restore` to `true` in the `MethodOptions`. When enabled, a `restoreFootprint` transaction will be created and await -signing whenever contract state is required to be restored before a contract function can -be invoked. +signing when required. ## [v12.0.0-rc.3](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.0-rc.3) From 1dccd9ba25be42e420e346a69dffe28be2df10d4 Mon Sep 17 00:00:00 2001 From: blaineheffron Date: Wed, 5 Jun 2024 18:46:15 -0400 Subject: [PATCH 14/21] fixed position of changelog entry --- CHANGELOG.md | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af29cb81c..04e0d5df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,9 @@ A breaking change will get clearly marked in this log. ## Unreleased ### Added -- `contract.AssembledTransaction` now has a `toXDR` and `fromXDR` method for serializing the -transaction to and from XDR. Additionally, `contract.Client` now has a `txFromXDR`. These methods -should be used in place of `AssembledTransaction.toJSON`, `AssembledTransaction.fromJSON`, and -`Client.txFromJSON` for multi-auth signing. The JSON methods are now deprecated. **Note you must now -call `simulate` on the transaction before the final `signAndSend` call after all required signatures -are gathered when using the XDR methods. +- `contract.AssembledTransaction` now has: + - `toXDR` and `fromXDR` methods for serializing the transaction to and from XDR. Additionally, `contract.Client` now has a `txFromXDR`. These methods should be used in place of `AssembledTransaction.toJSON`, `AssembledTransaction.fromJSON`, and `Client.txFromJSON` for multi-auth signing. The JSON methods are now deprecated. **Note you must now call `simulate` on the transaction before the final `signAndSend` call after all required signatures are gathered when using the XDR methods. + - a `restoreFootprint` method which accepts the `restorePreamble` returned when a simulation call fails due to some contract state that has expired. When invoking a contract function, one can now set `restore` to `true` in the `MethodOptions`. When enabled, a `restoreFootprint` transaction will be created and await signing when required. ### Deprecated - In `contract.AssembledTransaction`, `toJSON` and `fromJSON` should be replaced with `toXDR` and @@ -39,11 +36,6 @@ interface LedgerEntryChange { } ``` -- `contract.AssembledTransaction` now has a `restoreFootprint` method which accepts the -`restorePreamble` returned when a simulation call fails due to some contract state that -has expired. When invoking a contract function, one can now set `restore` to `true` in the -`MethodOptions`. When enabled, a `restoreFootprint` transaction will be created and await -signing when required. ## [v12.0.0-rc.3](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.0-rc.3) From 87ca94f97004150ed7353190291acf388e531ea0 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:45:26 -0400 Subject: [PATCH 15/21] add space after `if` in src/contract/assembled_transaction.ts Co-authored-by: George --- src/contract/assembled_transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 8d644685d..b54052fe1 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -571,7 +571,7 @@ export class AssembledTransaction { get result(): T { try { - if(!this.simulationData.result){ + if (!this.simulationData.result) { throw new Error("No simulation result!"); } return this.options.parseResultXdr(this.simulationData.result?.retval); From 319d57735e4d931b3b0da117e3240202c457c4c6 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:47:30 -0400 Subject: [PATCH 16/21] add space after `if` in src/contract/assembled_transaction.ts Co-authored-by: George --- src/contract/assembled_transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index b54052fe1..5548e025d 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -867,7 +867,7 @@ export class AssembledTransaction { /** The account that is executing the footprint restore operation. */ account?: Account ): Promise { - if(!this.options.signTransaction){ + if (!this.options.signTransaction) { throw new Error("For automatic restore to work you must provide a signTransaction function when initializing your Client"); } account = account ?? await getAccount(this.options, this.server); From d009c0e464044f1f584b9dbc4a6586f804eab306 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 13 Jun 2024 17:55:03 -0400 Subject: [PATCH 17/21] switch order of isSimulationRestore and `restore` check Co-authored-by: George --- src/contract/assembled_transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 5548e025d..a6664d6e1 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -491,7 +491,7 @@ export class AssembledTransaction { delete this.simulationTransactionData; this.simulation = await this.server.simulateTransaction(this.built); - if (Api.isSimulationRestore(this.simulation) && restore) { + if (restore && Api.isSimulationRestore(this.simulation)) { const account = await getAccount(this.options, this.server); const result = await this.restoreFootprint( this.simulation.restorePreamble, From ead51f580a6c9c4371ca2856ed1fed8739c13359 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:02:45 -0400 Subject: [PATCH 18/21] add stub result when simulation returns blank --- src/contract/assembled_transaction.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index a6664d6e1..9cf5c2c99 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -330,8 +330,8 @@ export class AssembledTransaction { method: this.options.method, tx: this.built?.toXDR(), simulationResult: { - auth: this.simulationData.result?.auth.map((a) => a.toXDR("base64")), - retval: this.simulationData.result?.retval.toXDR("base64"), + auth: this.simulationData.result.auth.map((a) => a.toXDR("base64")), + retval: this.simulationData.result.retval.toXDR("base64"), }, simulationTransactionData: this.simulationData.transactionData.toXDR("base64"), @@ -532,7 +532,7 @@ export class AssembledTransaction { }; get simulationData(): { - result?: Api.SimulateHostFunctionResult; + result: Api.SimulateHostFunctionResult; transactionData: xdr.SorobanTransactionData; } { if (this.simulationResult && this.simulationTransactionData) { @@ -560,7 +560,7 @@ export class AssembledTransaction { } // add to object for serialization & deserialization - this.simulationResult = simulation.result; + this.simulationResult = simulation.result ?? { auth: [], retval: new xdr.ScVal() }; this.simulationTransactionData = simulation.transactionData.build(); return { @@ -574,7 +574,7 @@ export class AssembledTransaction { if (!this.simulationData.result) { throw new Error("No simulation result!"); } - return this.options.parseResultXdr(this.simulationData.result?.retval); + return this.options.parseResultXdr(this.simulationData.result.retval); } catch (e) { if (!implementsToString(e)) throw e; const err = this.parseError(e.toString()); @@ -827,7 +827,7 @@ export class AssembledTransaction { * returns `false`, then you need to call `signAndSend` on this transaction. */ get isReadCall(): boolean { - const authsCount = this.simulationData.result?.auth.length; + const authsCount = this.simulationData.result.auth.length; const writeLength = this.simulationData.transactionData .resources() .footprint() From 2654c10e855aed43f06675131f14fbb7fd2d0c22 Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:04:30 -0400 Subject: [PATCH 19/21] add note about restoreTransaction arg --- src/contract/assembled_transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 9cf5c2c99..4a4739fa5 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -864,7 +864,7 @@ export class AssembledTransaction { minResourceFee: string; transactionData: SorobanDataBuilder; }, - /** The account that is executing the footprint restore operation. */ + /** The account that is executing the footprint restore operation. If omitted, will use the account from the AssembledTransaction. */ account?: Account ): Promise { if (!this.options.signTransaction) { From c96e8c60f90913e4d0ed522360fa68eb69b9a5be Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Fri, 14 Jun 2024 09:08:56 -0400 Subject: [PATCH 20/21] fix missing check from merge with toXDR / fromXDR feature --- src/contract/assembled_transaction.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 4a4739fa5..1a975a199 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -476,15 +476,16 @@ export class AssembledTransaction { } simulate = async ({ restore }: {restore?: boolean} = {}): Promise => { - if (!this.raw) { - throw new Error( - "Transaction has not yet been assembled; " + - "call `AssembledTransaction.build` first." - ); + if (!this.built){ + if(!this.raw) { + throw new Error( + "Transaction has not yet been assembled; " + + "call `AssembledTransaction.build` first." + ); + } + this.built = this.raw.build(); } - restore = restore ?? this.options.restore; - this.built = this.raw.build(); // need to force re-calculation of simulationData for new simulation delete this.simulationResult; From 1c761efcc0c307135a07ac8abd8fe278555af64b Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Fri, 14 Jun 2024 09:32:35 -0400 Subject: [PATCH 21/21] fix empty scVal construction --- src/contract/assembled_transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 1a975a199..6003de128 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -561,7 +561,7 @@ export class AssembledTransaction { } // add to object for serialization & deserialization - this.simulationResult = simulation.result ?? { auth: [], retval: new xdr.ScVal() }; + this.simulationResult = simulation.result ?? { auth: [], retval: xdr.ScVal.scvVoid() }; this.simulationTransactionData = simulation.transactionData.build(); return {