Executing Swaps

Once you have a quote, executing the swap involves building a transaction group, getting the user's signature, and submitting to the network.

Building the Transaction Group

import { buildSwapTransactionGroup } from '@taurus-swap/sdk';

const { txGroup, signer } = await buildSwapTransactionGroup(
  algodClient,
  POOL_APP_ID,
  account,
  {
    tokenInIndex: 0,
    tokenOutIndex: 1,
    amountIn: 100_000_000n,
    minOut: quote.amountOut * 995n / 1000n,  // 0.5% slippage
    claimedOut: quote.amountOut
  }
);

Transaction Group Structure

The swap transaction group contains:

  1. ASA Transfer — User transfers input tokens to the pool
  2. App Call — Calls the swap or swap_with_crossings method

The contract emits an inner transaction that transfers output tokens back to the user.

┌─────────────────────────────────────┐
│ Tx 0: ASA Transfer                  │
│  - Sender: User                     │
│  - Receiver: Pool                   │
│  - Amount: amountIn                 │
├─────────────────────────────────────┤
│ Tx 1: App Call (swap)               │
│  - Sender: User                     │
│  - Method: swap()                   │
│  - Args: [tokenInIdx, tokenOutIdx,  │
│           amountIn, minOut,         │
│           claimedOut]               │
│  - Inner Tx: Pool → User (output)   │
└─────────────────────────────────────┘

Signing with a Wallet

Pera Wallet

import { PeraWalletConnect } from '@perawallet/connect';

const pera = new PeraWalletConnect();

const handleSwap = async () => {
  const { txGroup } = await buildSwapTransactionGroup(
    algodClient,
    POOL_APP_ID,
    account,
    tradeParams
  );

  // Pera signs the group
  const signedTxns = await pera.signTransaction([
    txGroup.map((tx) => tx.txn)
  ]);

  // Send signed transactions
  const result = await algodClient
    .sendGroupTransaction(signedTxns)
    .do();

  console.log('TX ID:', result.txId);
};

Defly Wallet

import { DeflyWalletConnect } from '@blockshake/defly-connect';

const defly = new DeflyWalletConnect();

const signedTxns = await defly.signTransaction(
  txGroup.map((tx) => tx.txn)
);

use-wallet-react

import { useWallet } from '@txnlab/use-wallet-react';

function SwapForm() {
  const { signer } = useWallet();

  const handleSwap = async () => {
    if (!signer) {
      alert('Connect wallet first');
      return;
    }

    const { txGroup } = await buildSwapTransactionGroup(
      algodClient,
      POOL_APP_ID,
      account,
      tradeParams
    );

    // Signer from use-wallet handles the group
    const result = await signer.signGroupTransaction(txGroup);

    // Send...
  };

  return <button onClick={handleSwap}>Swap</button>;
}

Sending the Transaction

const result = await algodClient
  .sendGroupTransaction(signedTxns)
  .do();

console.log('Transaction sent:', result.txId);

// Wait for confirmation
const confirmation = await algosdk.waitForConfirmation(
  algodClient,
  result.txId,
  4
);

console.log('Confirmed in round:', confirmation['confirmed-round']);

Error Handling

import {
  InsufficientLiquidityError,
  SlippageExceededError,
  InvariantCheckFailedError
} from '@taurus-swap/sdk';

async function executeSwap() {
  try {
    const { txGroup } = await buildSwapTransactionGroup(
      algodClient,
      POOL_APP_ID,
      account,
      tradeParams
    );

    const result = await algodClient
      .sendGroupTransaction(txGroup)
      .do();

    return { success: true, txId: result.txId };

  } catch (err) {
    if (err instanceof InsufficientLiquidityError) {
      return {
        success: false,
        error: 'Pool does not have enough output tokens'
      };
    }

    if (err instanceof SlippageExceededError) {
      return {
        success: false,
        error: 'Price moved too much. Try again with a fresh quote.'
      };
    }

    if (err.message.includes('invariant check failed')) {
      return {
        success: false,
        error: 'Pool state changed. Refresh and try again.'
      };
    }

    // Generic error
    console.error('Swap failed:', err);
    return {
      success: false,
      error: err.message
    };
  }
}

Full Integration Example

async function executeSwapWithRetry(
  poolState: PoolState,
  tradeParams: SwapParams,
  maxRetries = 2
): Promise<{ success: boolean; txId?: string; error?: string }> {

  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      // Refresh pool state on retry
      if (attempt > 0) {
        poolState = await readPoolState(algodClient, POOL_APP_ID);
      }

      // Get fresh quote
      const quote = await getSwapQuote(poolState, tradeParams);

      // Build transaction
      const { txGroup } = await buildSwapTransactionGroup(
        algodClient,
        POOL_APP_ID,
        account,
        {
          ...tradeParams,
          claimedOut: quote.amountOut,
          minOut: quote.amountOut * 995n / 1000n
        }
      );

      // Sign and send
      const signedTxns = await wallet.signTransaction(
        txGroup.map((tx) => tx.txn)
      );

      const result = await algodClient
        .sendGroupTransaction(signedTxns)
        .do();

      // Wait for confirmation
      await algosdk.waitForConfirmation(algodClient, result.txId, 4);

      return { success: true, txId: result.txId };

    } catch (err) {
      lastError = err as Error;

      // Don't retry on certain errors
      if (err instanceof InsufficientLiquidityError) {
        break;
      }
    }
  }

  return {
    success: false,
    error: lastError?.message || 'Unknown error'
  };
}

Monitoring Transaction Status

function useTransactionStatus(txId: string | null) {
  const [status, setStatus] = useState<'pending' | 'confirmed' | 'failed'>('pending');
  const [round, setRound] = useState<number | null>(null);

  useEffect(() => {
    if (!txId) return;

    const checkStatus = async () => {
      try {
        const pending = await algodClient.pendingTransactionInformation(txId).do();

        if (pending['pool-error']) {
          setStatus('failed');
          return;
        }

        if (pending['confirmed-round']) {
          setStatus('confirmed');
          setRound(pending['confirmed-round']);
          return;
        }

        // Still pending - check again in 1 second
        setTimeout(checkStatus, 1000);
      } catch (err) {
        setStatus('failed');
      }
    };

    checkStatus();
  }, [txId]);

  return { status, round };
}
Next: See Adding Liquidity for the LP flow.