> ## Documentation Index
> Fetch the complete documentation index at: https://xoxno.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Runnable TypeScript example

> A complete quote, sign, submit, and confirm lifecycle using only the HTTP API and @stellar/stellar-sdk.

This example uses the HTTP API and `@stellar/stellar-sdk@16` directly. If you would rather not maintain it, the [TypeScript SDK](/docs/stellar-aggregator/sdk) builds and verifies the swap transaction for you.

<Note>
  The code blocks below form one file, `stellar-swap.ts`, in order. It targets a single-signature account using source-account authorization. Multisig wallets and separate Soroban authorization-entry signatures need their wallet's authorization flow. It stops on restoration requirements and on multi-transaction plans. It never touches a secret key.
</Note>

<Steps>
  <Step title="Define your wallet and deployment">
    Supply your wallet's signing adapter, trusted deployment settings, and a `review` callback that decodes the transaction and obtains approval. Throw from `review` on any mismatch.

    ```ts theme={"system"}
    import { Transaction, TransactionBuilder, rpc, xdr } from "@stellar/stellar-sdk";

    type Wallet = {
      address: string;
      // Adapt your wallet's signing API; return its signed transaction XDR.
      signTransaction(xdr: string, networkPassphrase: string): Promise<string>;
    };
    type Context = {
      apiUrl: string;
      rpcUrl: string;
      networkPassphrase: string; // Trusted app configuration, not the quote alone.
      router: string;
      wallet: Wallet;
      // Decode operations and obtain user approval for tokens, amounts, fees,
      // recipient, router, and any outer-contract calls. Throw on mismatch.
      review(transaction: Transaction): Promise<void>;
    };
    type Quote = {
      amountIn: string;
      amountOut: string;
      amountOutMin?: string;
      routeXdr?: string;
      transaction?: {
        envelopeXdr: string;
        networkPassphrase: string;
        routerContract: string;
        simulated: boolean;
      };
      transactions?: unknown[];
    };
    ```
  </Step>

  <Step title="Quote against a verified deployment">
    Read `/api/v1/config` and compare its network and router with your trusted configuration before you trust any quote.

    ```ts theme={"system"}
    async function getJson<T>(url: string): Promise<T> {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
      return response.json() as Promise<T>;
    }

    async function getQuote(ctx: Context, params: URLSearchParams): Promise<Quote> {
      const config = await getJson<{ networkPassphrase: string; router: string | null }>(
        `${ctx.apiUrl}/api/v1/config`,
      );
      if (config.networkPassphrase !== ctx.networkPassphrase || config.router !== ctx.router) {
        throw new Error("Unexpected aggregator deployment");
      }
      return getJson<Quote>(`${ctx.apiUrl}/api/v1/quote?${params}`);
    }
    ```
  </Step>

  <Step title="Sign, submit, and confirm">
    One in-flight transaction per source account. The helper checks the network and source, populates the sequence and prepares resources only when the server has not already prepared the envelope, asks for review, signs, verifies the wallet did not change the transaction, submits, and polls until a final result. Request a new quote if the account has been used since server preparation.

    ```ts theme={"system"}
    // One in-flight transaction per source account. Coordinate all other submitters too.
    async function signSubmitConfirm(ctx: Context, unsignedXdr: string, prepared: boolean) {
      const server = new rpc.Server(ctx.rpcUrl);
      const network = await server.getNetwork();
      if (network.passphrase !== ctx.networkPassphrase) throw new Error("Wrong RPC network");
      const envelope = xdr.TransactionEnvelope.fromXDR(unsignedXdr, "base64");
      if (envelope.switch() !== xdr.EnvelopeType.envelopeTypeTx()) {
        throw new Error("Expected a regular transaction envelope");
      }
      if (envelope.v1().signatures().length !== 0) throw new Error("Expected unsigned XDR");
      const original = new Transaction(envelope, ctx.networkPassphrase);
      if (original.source !== ctx.wallet.address) throw new Error("Wrong source account");
      let transaction = original;
      if (!prepared) {
        const account = await server.getAccount(ctx.wallet.address);
        // Preserve operations, authorizations, fees, footprint, and existing bounds.
        envelope.v1().tx().seqNum(xdr.Int64.fromString(
          (BigInt(account.sequenceNumber()) + 1n).toString(),
        ));
        transaction = new Transaction(envelope, ctx.networkPassphrase);
        const simulation = await server.simulateTransaction(transaction);
        if (!rpc.Api.isSimulationSuccess(simulation) || "restorePreamble" in simulation) {
          throw new Error("Simulation failed or restoration is required; do not sign");
        }
        transaction = rpc.assembleTransaction(transaction, simulation).build();
      }
      await ctx.review(transaction);
      const signedXdr = await ctx.wallet.signTransaction(transaction.toXDR(), ctx.networkPassphrase);
      const signed = TransactionBuilder.fromXDR(signedXdr, ctx.networkPassphrase);
      if (!(signed instanceof Transaction) || !signed.hash().equals(transaction.hash())) {
        throw new Error("Wallet changed the transaction");
      }
      const hash = signed.hash().toString("hex");
      let submitted;
      try {
        submitted = await server.sendTransaction(signed);
      } catch (error) {
        throw new Error(`Submission uncertain; query ${hash} before retrying. ${String(error)}`);
      }
      if (submitted.status === "ERROR") throw new Error(`Submission rejected: ${hash}`);
      if (submitted.status === "TRY_AGAIN_LATER") {
        throw new Error(`Submission deferred; check ${hash} before retrying the same signed XDR`);
      }
      const deadline = Date.now() + 120_000;
      while (Date.now() < deadline) {
        const result = await server.getTransaction(hash).catch(error => {
          throw new Error(`Status lookup failed; query ${hash} before retrying. ${String(error)}`);
        });
        if (result.status === rpc.Api.GetTransactionStatus.SUCCESS) return result;
        if (result.status === rpc.Api.GetTransactionStatus.FAILED) {
          throw new Error(`Transaction failed: ${hash}`);
        }
        await new Promise(resolve => setTimeout(resolve, 2_000));
      }
      throw new Error(`Confirmation timed out; query ${hash} before any new swap`);
    }
    ```
  </Step>

  <Step title="Swap from a wallet">
    Call `swap` from your confirmation handler.

    ```ts theme={"system"}
    export async function swap(
      ctx: Context, from: string, to: string, atomicAmount: string, slippage: string,
    ) {
      const quote = await getQuote(ctx, new URLSearchParams({
        from, to, amount_in: atomicAmount, slippage,
        sender: ctx.wallet.address, fresh: "true",
      }));
      const tx = quote.transaction;
      if (quote.transactions || !tx) throw new Error("This example requires one transaction");
      if (tx.networkPassphrase !== ctx.networkPassphrase || tx.routerContract !== ctx.router) {
        throw new Error("Unexpected transaction deployment");
      }
      return signSubmitConfirm(ctx, tx.envelopeXdr, tx.simulated);
    }
    ```
  </Step>

  <Step title="Compose with lending">
    Pass your lending SDK's `buildControllerTransaction`. It must return the unsigned outer transaction with the intended action, account, amounts, and the opaque route payload. The same helper then simulates, signs, submits, and confirms the complete outer transaction.

    ```ts theme={"system"}
    export async function composeWithLending(
      ctx: Context, from: string, to: string, atomicAmount: string, slippage: string,
      // Supply the current lending SDK builder; it must bind the quote's exact
      // input and minimum output to the intended lending action and account.
      buildControllerTransaction: (routeXdr: string, quote: Quote) => Promise<Transaction>,
    ) {
      const quote = await getQuote(ctx, new URLSearchParams({
        from, to, amount_in: atomicAmount, slippage, fresh: "true",
      }));
      if (!quote.routeXdr) throw new Error("No atomic composition payload available");
      const outer = await buildControllerTransaction(quote.routeXdr, quote);
      // Always simulate the complete outer transaction; inner plan simulation
      // says nothing about this transaction's authorization or resource budget.
      return signSubmitConfirm(ctx, outer.toXDR(), false);
    }
    ```
  </Step>
</Steps>

## Run it on testnet

Use a testnet aggregator deployment and its router, a matching testnet RPC, a funded test wallet, supported token contracts, and enough balance. Check `/api/v1/config` against your trusted network and router. Do not assume the public service URL is testnet.

The example is type-checked against SDK 16. Its sequence and submission flow can be checked offline with mocked RPC. A live run still depends on the deployment, balances, pool liquidity, and wallet authorization.

[Integration guide](/docs/stellar-aggregator/integration-guide) · [Error recovery](/docs/stellar-aggregator/error-reference)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.