UNPKG

@stellar/stellar-sdk

Version:

A library for working with the Stellar network, including communication with the Horizon and Soroban RPC servers.

522 lines (521 loc) 21 kB
import { Account, SorobanDataBuilder, TransactionBuilder, authorizeEntry as stellarBaseAuthorizeEntry, xdr } from "@stellar/stellar-base"; import type { AssembledTransactionOptions, ClientOptions, Tx, XDR_BASE64 } from "./types"; import { Api } from "../rpc/api"; import { SentTransaction, Watcher } from "./sent_transaction"; import { Spec } from "./spec"; import { ExpiredStateError, ExternalServiceError, FakeAccountError, InternalWalletError, InvalidClientRequestError, NeedsMoreSignaturesError, NoSignatureNeededError, NoSignerError, NotYetSimulatedError, NoUnsignedNonInvokerAuthEntriesError, RestoreFailureError, SimulationFailedError, UserRejectedError } from "./errors"; /** @module contract */ /** * The main workhorse of {@link Client}. This class is used to wrap a * transaction-under-construction and provide high-level interfaces to the most * common workflows, while still providing access to low-level stellar-sdk * transaction manipulation. * * Most of the time, you will not construct an `AssembledTransaction` directly, * but instead receive one as the return value of a `Client` method. If * you're familiar with the libraries generated by soroban-cli's `contract * bindings typescript` command, these also wraps `Client` and return * `AssembledTransaction` instances. * * Let's look at examples of how to use `AssembledTransaction` for a variety of * use-cases: * * #### 1. Simple read call * * Since these only require simulation, you can get the `result` of the call * right after constructing your `AssembledTransaction`: * * ```ts * const { result } = await AssembledTransaction.build({ * method: 'myReadMethod', * args: spec.funcArgsToScVals('myReadMethod', { * args: 'for', * my: 'method', * ... * }), * contractId: 'C123…', * networkPassphrase: '…', * rpcUrl: 'https://…', * publicKey: undefined, // irrelevant, for simulation-only read calls * parseResultXdr: (result: xdr.ScVal) => * spec.funcResToNative('myReadMethod', result), * }) * ``` * * While that looks pretty complicated, most of the time you will use this in * conjunction with {@link Client}, which simplifies it to: * * ```ts * const { result } = await client.myReadMethod({ * args: 'for', * my: 'method', * ... * }) * ``` * * #### 2. Simple write call * * For write calls that will be simulated and then sent to the network without * further manipulation, only one more step is needed: * * ```ts * const assembledTx = await client.myWriteMethod({ * args: 'for', * my: 'method', * ... * }) * const sentTx = await assembledTx.signAndSend() * ``` * * Here we're assuming that you're using a {@link Client}, rather than * constructing `AssembledTransaction`'s directly. * * Note that `sentTx`, the return value of `signAndSend`, is a * {@link SentTransaction}. `SentTransaction` is similar to * `AssembledTransaction`, but is missing many of the methods and fields that * are only relevant while assembling a transaction. It also has a few extra * methods and fields that are only relevant after the transaction has been * sent to the network. * * Like `AssembledTransaction`, `SentTransaction` also has a `result` getter, * which contains the parsed final return value of the contract call. Most of * the time, you may only be interested in this, so rather than getting the * whole `sentTx` you may just want to: * * ```ts * const tx = await client.myWriteMethod({ args: 'for', my: 'method', ... }) * const { result } = await tx.signAndSend() * ``` * * #### 3. More fine-grained control over transaction construction * * If you need more control over the transaction before simulating it, you can * set various {@link MethodOptions} when constructing your * `AssembledTransaction`. With a {@link Client}, this is passed as a * second object after the arguments (or the only object, if the method takes * no arguments): * * ```ts * const tx = await client.myWriteMethod( * { * args: 'for', * my: 'method', * ... * }, { * fee: '10000', // default: {@link BASE_FEE} * simulate: false, * timeoutInSeconds: 20, // default: {@link DEFAULT_TIMEOUT} * } * ) * ``` * * Since we've skipped simulation, we can now edit the `raw` transaction and * then manually call `simulate`: * * ```ts * tx.raw.addMemo(Memo.text('Nice memo, friend!')) * await tx.simulate() * ``` * * If you need to inspect the simulation later, you can access it with * `tx.simulation`. * * #### 4. Multi-auth workflows * * Soroban, and Stellar in general, allows multiple parties to sign a * transaction. * * Let's consider an Atomic Swap contract. Alice wants to give 10 of her Token * A tokens to Bob for 5 of his Token B tokens. * * ```ts * const ALICE = 'G123...' * const BOB = 'G456...' * const TOKEN_A = 'C123…' * const TOKEN_B = 'C456…' * const AMOUNT_A = 10n * const AMOUNT_B = 5n * ``` * * Let's say Alice is also going to be the one signing the final transaction * envelope, meaning she is the invoker. So your app, from Alice's browser, * simulates the `swap` call: * * ```ts * const tx = await swapClient.swap({ * a: ALICE, * b: BOB, * token_a: TOKEN_A, * token_b: TOKEN_B, * amount_a: AMOUNT_A, * amount_b: AMOUNT_B, * }) * ``` * * But your app can't `signAndSend` this right away, because Bob needs to sign * it first. You can check this: * * ```ts * const whoElseNeedsToSign = tx.needsNonInvokerSigningBy() * ``` * * You can verify that `whoElseNeedsToSign` is an array of length `1`, * containing only Bob's public key. * * Then, still on Alice's machine, you can serialize the * transaction-under-assembly: * * ```ts * const json = tx.toJSON() * ``` * * And now you need to send it to Bob's browser. How you do this depends on * your app. Maybe you send it to a server first, maybe you use WebSockets, or * maybe you have Alice text the JSON blob to Bob and have him paste it into * your app in his browser (note: this option might be error-prone 😄). * * Once you get the JSON blob into your app on Bob's machine, you can * deserialize it: * * ```ts * const tx = swapClient.txFromJSON(json) * ``` * * Or, if you're using a client generated with `soroban contract bindings * typescript`, this deserialization will look like: * * ```ts * const tx = swapClient.fromJSON.swap(json) * ``` * * Then you can have Bob sign it. What Bob will actually need to sign is some * _auth entries_ within the transaction, not the transaction itself or the * transaction envelope. Your app can verify that Bob has the correct wallet * selected, then: * * ```ts * await tx.signAuthEntries() * ``` * * Under the hood, this uses `signAuthEntry`, which you either need to inject * during initial construction of the `Client`/`AssembledTransaction`, * or which you can pass directly to `signAuthEntries`. * * Now Bob can again serialize the transaction and send back to Alice, where * she can finally call `signAndSend()`. * * To see an even more complicated example, where Alice swaps with Bob but the * transaction is invoked by yet another party, check out * [test-swap.js](../../test/e2e/src/test-swap.js). * * @memberof module:contract */ export declare class AssembledTransaction<T> { options: AssembledTransactionOptions<T>; /** * The TransactionBuilder as constructed in `{@link * AssembledTransaction}.build`. Feel free set `simulate: false` to modify * this object before calling `tx.simulate()` manually. Example: * * ```ts * const tx = await myContract.myMethod( * { args: 'for', my: 'method', ... }, * { simulate: false } * ); * tx.raw.addMemo(Memo.text('Nice memo, friend!')) * await tx.simulate(); * ``` */ raw?: TransactionBuilder; /** * The Transaction as it was built with `raw.build()` right before * simulation. Once this is set, modifying `raw` will have no effect unless * you call `tx.simulate()` again. */ built?: Tx; /** * The result of the transaction simulation. This is set after the first call * to `simulate`. It is difficult to serialize and deserialize, so it is not * included in the `toJSON` and `fromJSON` methods. See `simulationData` * cached, serializable access to the data needed by AssembledTransaction * logic. */ simulation?: Api.SimulateTransactionResponse; /** * Cached simulation result. This is set after the first call to * {@link AssembledTransaction#simulationData}, and is used to facilitate * serialization and deserialization of the AssembledTransaction. * * Most of the time, if you need this data, you can call * `tx.simulation.result`. * * If you need access to this data after a transaction has been serialized * and then deserialized, you can call `simulationData.result`. */ private simulationResult?; /** * Cached simulation transaction data. This is set after the first call to * {@link AssembledTransaction#simulationData}, and is used to facilitate * serialization and deserialization of the AssembledTransaction. * * Most of the time, if you need this data, you can call * `simulation.transactionData`. * * If you need access to this data after a transaction has been serialized * and then deserialized, you can call `simulationData.transactionData`. */ private simulationTransactionData?; /** * The Soroban server to use for all RPC calls. This is constructed from the * `rpcUrl` in the options. */ private server; /** * The signed transaction. */ signed?: Tx; /** * A list of the most important errors that various AssembledTransaction * methods can throw. Feel free to catch specific errors in your application * logic. */ static Errors: { ExpiredState: typeof ExpiredStateError; RestorationFailure: typeof RestoreFailureError; NeedsMoreSignatures: typeof NeedsMoreSignaturesError; NoSignatureNeeded: typeof NoSignatureNeededError; NoUnsignedNonInvokerAuthEntries: typeof NoUnsignedNonInvokerAuthEntriesError; NoSigner: typeof NoSignerError; NotYetSimulated: typeof NotYetSimulatedError; FakeAccount: typeof FakeAccountError; SimulationFailed: typeof SimulationFailedError; InternalWalletError: typeof InternalWalletError; ExternalServiceError: typeof ExternalServiceError; InvalidClientRequest: typeof InvalidClientRequestError; UserRejected: typeof UserRejectedError; }; /** * Serialize the AssembledTransaction to a JSON string. This is useful for * saving the transaction to a database or sending it over the wire for * multi-auth workflows. `fromJSON` can be used to deserialize the * transaction. This only works with transactions that have been simulated. */ toJSON(): string; static fromJSON<T>(options: Omit<AssembledTransactionOptions<T>, "args">, { tx, simulationResult, simulationTransactionData, }: { tx: XDR_BASE64; simulationResult: { auth: XDR_BASE64[]; retval: XDR_BASE64; }; simulationTransactionData: XDR_BASE64; }): AssembledTransaction<T>; /** * Serialize the AssembledTransaction to a base64-encoded XDR string. */ toXDR(): string; /** * Deserialize the AssembledTransaction from a base64-encoded XDR string. */ static fromXDR<T>(options: Omit<AssembledTransactionOptions<T>, "args" | "method" | "parseResultXdr">, encodedXDR: string, spec: Spec): AssembledTransaction<T>; private handleWalletError; private constructor(); /** * Construct a new AssembledTransaction. This is the main way to create a new * AssembledTransaction; the constructor is private. * * This is an asynchronous constructor for two reasons: * * 1. It needs to fetch the account from the network to get the current * sequence number. * 2. It needs to simulate the transaction to get the expected fee. * * If you don't want to simulate the transaction, you can set `simulate` to * `false` in the options. * * If you need to create an operation other than `invokeHostFunction`, you * can use {@link AssembledTransaction.buildWithOp} instead. * * @example * const tx = await AssembledTransaction.build({ * ..., * simulate: false, * }) */ static build<T>(options: AssembledTransactionOptions<T>): Promise<AssembledTransaction<T>>; /** * Construct a new AssembledTransaction, specifying an Operation other than * `invokeHostFunction` (the default used by {@link AssembledTransaction.build}). * * Note: `AssembledTransaction` currently assumes these operations can be * simulated. This is not true for classic operations; only for those used by * Soroban Smart Contracts like `invokeHostFunction` and `createCustomContract`. * * @example * const tx = await AssembledTransaction.buildWithOp( * Operation.createCustomContract({ ... }); * { * ..., * simulate: false, * } * ) */ static buildWithOp<T>(operation: xdr.Operation, options: AssembledTransactionOptions<T>): Promise<AssembledTransaction<T>>; private static buildFootprintRestoreTransaction; simulate: ({ restore }?: { restore?: boolean; }) => Promise<this>; get simulationData(): { result: Api.SimulateHostFunctionResult; transactionData: xdr.SorobanTransactionData; }; get result(): T; private parseError; /** * Sign the transaction with the signTransaction function included previously. * If you did not previously include one, you need to include one now. */ sign: ({ force, 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<void>; /** * Sends the transaction to the network to return a `SentTransaction` that * keeps track of all the attempts to fetch the transaction. Optionally pass * a {@link Watcher} that allows you to keep track of the progress as the * transaction is sent and processed. */ send(watcher?: Watcher): Promise<SentTransaction<T>>; /** * 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. You may pass a {@link Watcher} to keep * track of this progress. */ signAndSend: ({ force, signTransaction, watcher, }?: { /** * 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"]; /** * A {@link Watcher} to notify after the transaction is successfully * submitted to the network (`onSubmitted`) and as the transaction is * processed (`onProgress`). */ watcher?: Watcher; }) => Promise<SentTransaction<T>>; /** * Get a list of accounts, other than the invoker of the simulation, that * need to sign auth entries in this transaction. * * Soroban allows multiple people to sign a transaction. Someone needs to * sign the final transaction envelope; this person/account is called the * _invoker_, or _source_. Other accounts might need to sign individual auth * entries in the transaction, if they're not also the invoker. * * This function returns a list of accounts that need to sign auth entries, * assuming that the same invoker/source account will sign the final * transaction envelope as signed the initial simulation. * * One at a time, for each public key in this array, you will need to * serialize this transaction with `toJSON`, send to the owner of that key, * deserialize the transaction with `txFromJson`, and call * {@link AssembledTransaction#signAuthEntries}. Then re-serialize and send to * the next account in this list. */ needsNonInvokerSigningBy: ({ includeAlreadySigned, }?: { /** * Whether or not to include auth entries that have already been signed. * Default: false */ includeAlreadySigned?: boolean; }) => string[]; /** * If {@link AssembledTransaction#needsNonInvokerSigningBy} returns a * non-empty list, you can serialize the transaction with `toJSON`, send it to * the owner of one of the public keys in the map, deserialize with * `txFromJSON`, and call this method on their machine. Internally, this will * use `signAuthEntry` function from connected `wallet` for each. * * Then, re-serialize the transaction and either send to the next * `needsNonInvokerSigningBy` owner, or send it back to the original account * who simulated the transaction so they can {@link AssembledTransaction#sign} * the transaction envelope and {@link AssembledTransaction#send} it to the * network. * * Sending to all `needsNonInvokerSigningBy` owners in parallel is not * currently supported! */ signAuthEntries: ({ expiration, signAuthEntry, address, authorizeEntry, }?: { /** * When to set each auth entry to expire. Could be any number of blocks in * the future. Can be supplied as a promise or a raw number. Default: * about 8.3 minutes from now. */ expiration?: number | Promise<number>; /** * Sign all auth entries for this account. Default: the account that * constructed the transaction */ address?: string; /** * You must provide this here if you did not provide one before and you are not passing `authorizeEntry`. Default: the `signAuthEntry` function from the `Client` options. Must sign things as the given `publicKey`. */ signAuthEntry?: ClientOptions["signAuthEntry"]; /** * If you have a pro use-case and need to override the default `authorizeEntry` function, rather than using the one in `@stellar/stellar-base`, you can do that! Your function needs to take at least the first argument, `entry: xdr.SorobanAuthorizationEntry`, and return a `Promise<xdr.SorobanAuthorizationEntry>`. * * Note that you if you pass this, then `signAuthEntry` will be ignored. */ authorizeEntry?: typeof stellarBaseAuthorizeEntry; }) => Promise<void>; /** * Whether this transaction is a read call. This is determined by the * simulation result and the transaction data. If the transaction is a read * call, it will not need to be signed and sent to the network. If this * returns `false`, then you need to call `signAndSend` on this transaction. */ get isReadCall(): boolean; /** * 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. * * @throws {Error} - Throws an error if no `signTransaction` function is provided during * Client initialization. * @throws {RestoreFailureError} - Throws a custom error if the * restore transaction fails, providing the details of the failure. */ 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. If omitted, will use the account from the AssembledTransaction. */ account?: Account): Promise<Api.GetTransactionResponse>; }