AssembledTransaction

AssembledTransaction

The main workhorse of ContractClient. 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 ContractClient method. If you're familiar with the libraries generated by soroban-cli's contract bindings typescript command, these also wraps ContractClient 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:

const { result } = await AssembledTransaction.build({
  method: 'myReadMethod',
  args: spec.funcArgsToScVals('myReadMethod', { args: 'for', my: 'method', ... }),
  contractId: 'C123…',
  networkPassphrase: '…',
  rpcUrl: 'https://…',
  publicKey: Keypair.random().publicKey(), // keypairs are 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 ContractClient, which simplifies it to:

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:

const assembledTx = await client.myWriteMethod({ args: 'for', my: 'method', ... })
const sentTx = await assembledTx.signAndSend()

Here we're assuming that you're using a ContractClient, rather than constructing AssembledTransaction's directly.

Note that sentTx, the return value of signAndSend, is a 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:

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 MethodOptions when constructing your AssembledTransaction. With a ContractClient, this is passed as a second object after the arguments (or the only object, if the method takes no arguments):

const tx = await client.myWriteMethod(
  {
    args: 'for',
    my: 'method',
    ...
  }, {
    fee: '10000', // default: BASE_FEE
    simulate: false,
    timeoutInSeconds: 20, // default: DEFAULT_TIMEOUT
  }
)

Since we've skipped simulation, we can now edit the raw transaction and then manually call simulate:

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.

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:

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:

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:

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:

const tx = swapClient.txFromJSON(json)

Or, if you're using a client generated with soroban contract bindings typescript, this deserialization will look like:

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:

await tx.signAuthEntries()

Under the hood, this uses signAuthEntry, which you either need to inject during initial construction of the ContractClient/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.

Constructor

new AssembledTransaction()

Source:

Members

Errors

A list of the most important errors that various AssembledTransaction methods can throw. Feel free to catch specific errors in your application logic.

Source:

isReadCall

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.

Source:

needsNonInvokerSigningBy

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 signAuthEntries. Then re-serialize and send to the next account in this list.

Source:

signAndSend

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.

Source:

signAuthEntries

If 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 sign the transaction envelope and send it to the network.

Sending to all needsNonInvokerSigningBy owners in parallel is not currently supported!

Source:

Methods

toJSON()

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.

Source:

(async, static) build()

Construct a new AssembledTransaction. This is the only way to create a new AssembledTransaction; the main 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.

const tx = await AssembledTransaction.build({
  ...,
  simulate: false,
})
Source: