# Token Swaps

Every swap starts with a LI.FI quote. What happens next depends on the source chain, on who holds the wallet key, and on whether the pair qualifies for the gasless flow.

## Overview

Swaps are routed through **LI.FI**. [Get Quote](https://docs.aureahub.com/docs/swap-quote.md) returns a `quoteId`, a `transactionData` object and approval information. Aurea does not store or re-fetch the quote: when you execute, it signs or prepares the `transactionData` you send, so pass it through unchanged.

All swap endpoints take camelCase JSON bodies and require `Authorization: Bearer <token>`. The exception is [List Swap Routes](https://docs.aureahub.com/docs/swap-routes-list.md), where the token is optional.

## Choose a Flow

- **EVM, custodial wallet** — quote → approve if `needsApproval` (confirmed when the call returns) → execute → poll status.
- **EVM, non-custodial wallet** (key management scheme `client_side`, `client_side_pending` or `mpc_tss`) — quote → approve and broadcast-approve if `needsApproval` → execute → sign → broadcast → poll status.
- **Solana source, custodial wallet** — quote → execute (or its alias `execute-solana`); confirmed when the call returns. Non-custodial Solana wallets are not supported.
- **EUR.e → xDAI on Gnosis**, when the quote has a `gasless` object — quote → sign-permit → execute-gasless → for non-custodial wallets that pay their own swap gas: sign → broadcast-gasless.

Quotes with `isTestnet: true` are currently rejected. To exercise UI and history without LI.FI or a chain, [Simulate Swap](https://docs.aureahub.com/docs/swap-simulate.md) creates a confirmed swap record.

## Regular Swap (EVM)

```typescript
const API = 'https://api.aureahub.com';

async function post(path: string, body: unknown): Promise<any> {
  const res = await fetch(API + path, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${json.code ?? ''}: ${json.message}`);
  return json;
}

// 1. Quote
const quote = await post('/v1/swap/quote', {
  walletId,
  fromChain: 'polygon',
  toChain: 'polygon',
  fromToken: FROM_TOKEN,                                 // must be in the token registry
  toToken: '0x0000000000000000000000000000000000000000', // native coin
  fromAmount,                                            // string, forwarded to LI.FI unchanged
  slippage: 0.5                                          // percent: 0.5%
});

// 2. Approval (ERC-20 source token whose current allowance is zero)
if (quote.needsApproval) {
  const approval = await post('/v1/swap/approve', {
    walletId,
    chain: 'polygon',
    tokenAddress: FROM_TOKEN,
    spenderAddress: quote.approvalAddress
    // amount omitted → unlimited allowance
  });
  if (approval.requiresClientSigning) {
    await signAndBroadcast('/v1/swap/broadcast-approve', approval);
    // No transaction record for approvals: poll /v1/swap/check-allowance before executing
  } else if (approval.status !== 'confirmed') {
    throw new Error('Approval failed');
  }
}

// 3. Execute — transactionData exactly as quoted
const exec = await post('/v1/swap/execute', {
  walletId,
  chain: 'polygon',
  quoteId: quote.quoteId,
  transactionData: quote.transactionData,
  fromAmount: quote.fromAmount,          // optional history metadata
  toAmount: quote.toAmount,
  fromTokenSymbol: quote.fromToken.symbol,
  toTokenSymbol: quote.toToken.symbol
});

const swap = exec.requiresClientSigning
  ? await signAndBroadcast('/v1/swap/broadcast', exec, { transactionId: exec.transactionId })
  : exec;

// 4. Track: GET /v1/transactions/{transactionId}/status while status is 'pending'
```

## Non-Custodial Signing

For non-custodial wallets, `approve`, `execute` and `execute-gasless` can return `requiresClientSigning: true` with a `preparedTxId` and a `txHashToSign`. The hash is the keccak-256 of an unsigned EIP-1559 transaction that Aurea prepared. Sign the hash with the wallet key and send only `v` (0 or 1), `r` and `s`. Aurea rebuilds the transaction, checks that the signature recovers to the wallet's address, and broadcasts it.

```typescript
import { SigningKey } from 'ethers';

async function signAndBroadcast(path: string, prepared: any, extra: object = {}) {
  const sig = new SigningKey(privateKey).sign(prepared.txHashToSign); // or your MPC / device signer
  return post(path, {
    ...extra,                            // /v1/swap/broadcast also needs transactionId
    preparedTxId: prepared.preparedTxId,
    v: sig.yParity,                      // 0 or 1
    r: sig.r,
    s: sig.s
  });
}
```

> ⚠️ A prepared transaction expires after 5 minutes and can be submitted once — it is marked as used even if the signature is then rejected. A non-custodial swap broadcast is also refused with `QUOTE_EXPIRED` after the `quoteExpiry` you sent to `/execute`, or 60 seconds after `/execute` if you sent none.

## Solana Swap

For a quote with `sourceChainType: "solana"`, LI.FI returns a base64 `VersionedTransaction` in `transactionData.data`. Send it to `/v1/swap/execute` or its alias `/v1/swap/execute-solana`. Aurea signs it with the wallet's server-held keypair, waits for confirmation, and returns the signature as `txHash`. There is no approval, client signing or broadcast step. Non-custodial Solana wallets get `400`.

```typescript
const quote = await post('/v1/swap/quote', {
  walletId: solanaWalletId,
  fromChain: 'solana',
  toChain: 'solana',
  fromToken: 'So11111111111111111111111111111111111111112', // native SOL
  toToken: TO_MINT,
  fromAmount
});

const { transactionId, txHash, status } = await post('/v1/swap/execute-solana', {
  walletId: solanaWalletId,
  chain: 'solana',
  quoteId: quote.quoteId,
  transactionData: quote.transactionData
});
// status === 'confirmed'; txHash is the Solana signature
```

## Gasless Swap (EUR.e → xDAI on Gnosis)

This flow requires gasless swaps to be enabled on the deployment and a quote that contains a `gasless` object. The approval is an ERC-2612 permit. [Sign EIP-712 Permit](https://docs.aureahub.com/docs/swap-sign-permit.md) has Aurea sign it server-side and returns `{ v, r, s, deadline, nonce, permitRequired }`; nothing is signed as typed data on the client. See [Execute Gasless](https://docs.aureahub.com/docs/swap-gasless.md) for limits and response paths.

```typescript
const EURE = '0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430'; // EUR.e on Gnosis

const quote = await post('/v1/swap/quote', {
  walletId,
  fromChain: 'gnosis',
  toChain: 'gnosis',
  fromToken: EURE,
  toToken: '0x0000000000000000000000000000000000000000', // xDAI
  fromAmount: amountWei                                   // forwarded to LI.FI unchanged
});
if (!quote.gasless) throw new Error('Gasless swap not available');

// Permit for spender = transactionData.to and value = amount (wei)
const permitSignature = await post('/v1/swap/sign-permit', {
  walletId,
  quoteId: quote.quoteId,
  spenderAddress: quote.transactionData.to,
  amount: amountWei
});

const result = await post('/v1/swap/execute-gasless', {
  walletId,
  quoteId: quote.quoteId,
  amount: amountWei,
  tokenAddress: EURE,
  transactionData: quote.transactionData,
  permitSignature
});

if (result.requiresClientSigning) {
  // Non-custodial wallet that pays its own swap gas
  await signAndBroadcast('/v1/swap/broadcast-gasless', result);
}
// Track with result.transactionId
```

## Slippage & Amounts

- `slippage` is accepted only by `POST /v1/swap/quote`. It is a **percentage** from 0 to 50 — `0.5` = 0.5% — and defaults to `0.5`. Aurea divides it by 100 before sending it to LI.FI. There is no `slippage_bps` field, and sending `50` means 50%.
- `fromAmount` on the quote must match `^\d+(\.\d+)?$` and is forwarded to LI.FI unchanged; Aurea does not scale it by token decimals.
- `amount` on `/approve`, `/sign-permit` and `/execute-gasless` is digits only (`^\d+$`), in the token's smallest unit (wei for EUR.e). `fromAmount` on `/simulate` is digits only as well.
- The optional amount and token fields on `/execute` (`fromAmount`, `toAmount`, symbols, decimals, addresses) are stored for transaction history. They do not change what is executed.

## Tracking & Retries

- Swap statuses are `pending`, `confirmed` and `failed`. Custodial EVM swaps and non-custodial broadcasts return `pending`; poll [Get Tx Status](https://docs.aureahub.com/docs/tx-status.md) with `transactionId`. Solana swaps and completed gasless swaps return `confirmed`.
- An expired quote is an error, not a status: `400` with a message starting `QUOTE_EXPIRED`. Request a new quote and execute again.
- Swap endpoints do not implement idempotency keys and do not de-duplicate requests, so a repeated `/execute` sends or prepares another transaction. After a timeout, check the wallet's transactions before retrying.
- The three broadcast endpoints are limited to 10 requests per 10 seconds.
- Errors have the shape `{ statusCode, error, message }`, plus `details` when available; blockchain errors also carry `code` and `retryable`.

---

Web version: https://docs.aureahub.com/#guide-swap
