@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
TypeScript
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>;
}