AssembledTransaction

public final class AssembledTransaction : @unchecked Sendable

The main workhorse of SorobanClient. 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 SorobanClient.buildInvokeMethodTx method.

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:


let clientOptions = ClientOptions(sourceAccountKeyPair: sourceAccountKeyPair,
                                  contractId: "C123…",
                                  network: Network.testnet,
                                  rpcUrl: "https://…")

let txOptions = AssembledTransactionOptions(clientOptions: clientOptions,
                                            methodOptions: MethodOptions(),
                                            method: "myReadMethod",
                                            arguments: args)
let tx = try await AssembledTransaction.build(options: txOptions)
let result = try tx.getSimulationData().returnedValue

While that looks pretty complicated, most of the time you will use this in conjunction with SorobanClient, which simplifies it to:

let result = try await client.invokeMethod(name: "myReadMethod", args: args)

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:

let tx = try await AssembledTransaction.build(options: txOptions)
let response = try await tx.signAndSend()
if response.status == GetTransactionResponse.STATUS_SUCCESS {
    let result = response.resultValue
}

If you are using it in conjunction with SorobanClient:

let result = try await client.invokeMethod(name: "myWriteMethod", args: args)

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 SorobanClient, this can be passed as an argument when calling invokeMethod or buildInvokeMethodTx :

let methodOptions = MethodOptions(fee: 10000,
                                  timeoutInSeconds: 20,
                                  simulate: false)

let tx = try await client.buildInvokeMethodTx(name: "myWriteMethod",
                                              args: args,
                                              methodOptions: methodOptions)

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

tx.raw?.setMemo(memo: Memo.text("Hello"))
try await tx.simulate()

If you need to inspect the simulation later, you can access it with let data = try tx.getSimulationData()

#### 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 some of her Token A tokens to Bob for some of his Token B tokens.

 let swapMethodName = "swap"
 let amountA = SCValXDR.i128(Int128PartsXDR(hi: 0, lo: 1000))
 let minBForA = SCValXDR.i128(Int128PartsXDR(hi: 0, lo: 4500))

 let amountB = SCValXDR.i128(Int128PartsXDR(hi: 0, lo: 5000))
 let minAForB = SCValXDR.i128(Int128PartsXDR(hi: 0, lo: 950))
 let args:[SCValXDR] = [try SCValXDR.address(SCAddressXDR(accountId: aliceId)),
                        try SCValXDR.address(SCAddressXDR(accountId: bobId)),
                        try SCValXDR.address(SCAddressXDR(contractId: tokenAContractId)),
                        try SCValXDR.address(SCAddressXDR(contractId: tokenBContractId)),
                        amountA,
                        minBForA,
                        amountB,
                        minAForB]

Let’s say Alice is also going to be the one signing the final transaction envelope, meaning she is the invoker. So your app, she simulates the swap call:

let tx = try await atomicSwapClient.buildInvokeMethodTx(name: swapMethodName,
                                                        args: args)

But your app can’t signAndSend this right away, because Bob needs to sign it first. You can check this:

let whoElseNeedsToSign = try tx.needsNonInvokerSigningBy()

You can verify that whoElseNeedsToSign is an array of length 1, containing only Bob’s public key.

If you have Bob’s secret key, you can sign it right away with:

let bobsKeyPair = try KeyPair(secretSeed: "S...")
try await tx.signAuthEntries(signerKeyPair: bobsKeyPair)

But if you don’t have Bob’s private key, and e.g. need to send it to another server for signing, you can provide a callback function for signing the auth entry:

let bobPublicKeyKeypair = try KeyPair(accountId: bobsAccountId)
try await tx.signAuthEntries(signerKeyPair: bobPublicKeyKeypair, authorizeEntryCallback: { (entry, network) async throws in

       // You can send it to some other server for signing by encoding it as a base64xdr string
       let base64Entry = entry.xdrEncoded!

       // send for signing ...
       // and on the other server you can decode it:
       var entryToSign = try SorobanAuthorizationEntryXDR.init(fromBase64: base64Entry)

       // sign it
       try entryToSign.sign(signer: bobsSecretKeyPair, network: network)

       // encode as a base64xdr string and send it back
       let signedBase64Entry = entryToSign.xdrEncoded!

       // here you can now decode it and return it
       return try SorobanAuthorizationEntryXDR.init(fromBase64: signedBase64Entry)
})

To see an even more complicated example, where Alice swaps with Bob but the transaction is invoked by yet another party, check out in the SorobanClientTest.atomicSwapTest()