diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e0d5df0..ba30bfcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ A breaking change will get clearly marked in this log. - `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. + - separate `sign` and `send` methods so that you can sign a transaction without sending it. You can continue to use `signAndSend` if you prefer. ### Deprecated - In `contract.AssembledTransaction`, `toJSON` and `fromJSON` should be replaced with `toXDR` and diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index 6003de128..e87de33b7 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -303,6 +303,11 @@ export class AssembledTransaction { */ private server: Server; + /** + * The signed transaction. + */ + public signed?: Tx; + /** * A list of the most important errors that various AssembledTransaction * methods can throw. Feel free to catch specific errors in your application @@ -595,13 +600,10 @@ export class AssembledTransaction { } /** - * Sign the transaction with the `wallet`, included previously. If you did - * not previously include one, you need to include one now that at least - * includes the `signTransaction` method. After signing, this method will - * send the transaction to the network and return a `SentTransaction` that - * keeps track of all the attempts to fetch the transaction. + * Sign the transaction with the signTransaction function included previously. + * If you did not previously include one, you need to include one now. */ - signAndSend = async ({ + sign = async ({ force = false, signTransaction = this.options.signTransaction, }: { @@ -613,7 +615,7 @@ export class AssembledTransaction { * You must provide this here if you did not provide one before */ signTransaction?: ClientOptions["signTransaction"]; - } = {}): Promise> => { + } = {}): Promise => { if (!this.built) { throw new Error("Transaction has not yet been simulated"); } @@ -635,16 +637,68 @@ export class AssembledTransaction { 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 timeoutInSeconds = + this.options.timeoutInSeconds ?? DEFAULT_TIMEOUT; + this.built = TransactionBuilder.cloneFrom(this.built!, { + fee: this.built!.fee, + timebounds: undefined, + sorobanData: this.simulationData.transactionData, + }) + .setTimeout(timeoutInSeconds) + .build(); + + const signature = await signTransaction( + this.built.toXDR(), + { + networkPassphrase: this.options.networkPassphrase, + }, ); + + this.signed = TransactionBuilder.fromXDR( + signature, + this.options.networkPassphrase, + ) as Tx; + }; + + /** + * Sends the transaction to the network to return a `SentTransaction` that + * keeps track of all the attempts to fetch the transaction. + */ + async send(){ + if(!this.signed){ + throw new Error("The transaction has not yet been signed. Run `sign` first, or use `signAndSend` instead."); + } + const sent = await SentTransaction.init(undefined, this); return sent; + } + + /** + * Sign the transaction with the `signTransaction` function included previously. + * If you did not previously include one, you need to include one now. + * After signing, this method will send the transaction to the network and + * return a `SentTransaction` that keeps track * of all the attempts to fetch the transaction. + */ + signAndSend = async ({ + force = false, + signTransaction = this.options.signTransaction, + }: { + /** + * If `true`, sign and send the transaction even if it is a read call + */ + force?: boolean; + /** + * You must provide this here if you did not provide one before + */ + signTransaction?: ClientOptions["signTransaction"]; + } = {}): Promise> => { + if(!this.signed){ + await this.sign({ force, signTransaction }); + } + return this.send(); }; private getStorageExpiration = async () => { diff --git a/src/contract/sent_transaction.ts b/src/contract/sent_transaction.ts index 8d033e648..0a449ab3e 100644 --- a/src/contract/sent_transaction.ts +++ b/src/contract/sent_transaction.ts @@ -1,7 +1,6 @@ /* disable max-classes rule, because extending error shouldn't count! */ /* eslint max-classes-per-file: 0 */ -import { TransactionBuilder } from "@stellar/stellar-base"; -import type { ClientOptions, MethodOptions, Tx } from "./types"; +import type { MethodOptions } from "./types"; import { Server } from "../rpc/server" import { Api } from "../rpc/api" import { DEFAULT_TIMEOUT, withExponentialBackoff } from "./utils"; @@ -24,8 +23,6 @@ import type { AssembledTransaction } from "./assembled_transaction"; export class SentTransaction { public server: Server; - public signed?: Tx; - /** * The result of calling `sendTransaction` to broadcast the transaction to the * network. @@ -53,61 +50,32 @@ export class SentTransaction { }; constructor( - public signTransaction: ClientOptions["signTransaction"], + _: any, // deprecated: used to take sentTransaction, need to wait for major release for breaking change public assembled: AssembledTransaction, ) { - if (!signTransaction) { - throw new Error( - "You must provide a `signTransaction` function to send a transaction", - ); - } this.server = new Server(this.assembled.options.rpcUrl, { allowHttp: this.assembled.options.allowHttp ?? false, }); } /** - * Initialize a `SentTransaction` from an existing `AssembledTransaction` and - * a `signTransaction` function. This will also send the transaction to the - * network. + * Initialize a `SentTransaction` from `options` and a `signed` + * AssembledTransaction. This will also send the transaction to the network. */ static init = async ( - /** More info in {@link MethodOptions} */ - signTransaction: ClientOptions["signTransaction"], + /** @deprecated variable is ignored. Now handled by AssembledTransaction. */ + _: any, // eslint-disable-line @typescript-eslint/no-unused-vars /** {@link AssembledTransaction} from which this SentTransaction was initialized */ assembled: AssembledTransaction, ): Promise> => { - const tx = new SentTransaction(signTransaction, assembled); + const tx = new SentTransaction(undefined, assembled); const sent = await tx.send(); return sent; }; private send = async (): Promise => { - const timeoutInSeconds = - this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT; - 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` - this.assembled.built!.toXDR(), - { - networkPassphrase: this.assembled.options.networkPassphrase, - }, - ); - - this.signed = TransactionBuilder.fromXDR( - signature, - this.assembled.options.networkPassphrase, - ) as Tx; - this.sendTransactionResponse = await this.server.sendTransaction( - this.signed, + this.assembled.signed!, ); if (this.sendTransactionResponse.status !== "PENDING") { @@ -122,6 +90,8 @@ export class SentTransaction { const { hash } = this.sendTransactionResponse; + const timeoutInSeconds = + this.assembled.options.timeoutInSeconds ?? DEFAULT_TIMEOUT; this.getTransactionResponseAll = await withExponentialBackoff( () => this.server.getTransaction(hash), (resp) => resp.status === Api.GetTransactionStatus.NOT_FOUND, @@ -183,7 +153,7 @@ export class SentTransaction { // 3. finally, if neither of those are present, throw an error throw new Error( - `Sending transaction failed: ${JSON.stringify(this.assembled)}`, + `Sending transaction failed: ${JSON.stringify(this.assembled.signed)}`, ); } }