# SODAX for all networks

One SDK. Every network. Scope the integration from your own repo before you commit.

{% hint style="success" %}
**See what integration takes before you commit.** Run the SODAX Builders MCP locally against your own codebase. Your AI assistant reads your repo and maps exactly what a SODAX integration looks like and how light the work is. One integration reaches all networks. No call required.

[Try the Builders MCP](https://builders.sodax.com/)
{% endhint %}

***

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td>Integrate swaps to xStocks across 19 networks.<br><a href="/pages/ZC6DR6i4mBkZjH3anqSG" class="button primary" data-icon="magnifying-glass-arrows-rotate">Make a swap</a></td><td><a href="/files/eapLWEDWb90nZfEcKDpa">/files/eapLWEDWb90nZfEcKDpa</a></td></tr><tr><td><p>Integrate Bitcoin as a source or destination network</p><p><a href="https://docs.sodax.com/developers/how-to/bitcoin-integration" class="button primary" data-icon="bitcoin">Bitcoin Integration</a></p></td><td><a href="/files/1dMN7t1A6xlzpJl1B8kg">/files/1dMN7t1A6xlzpJl1B8kg</a></td></tr></tbody></table>

***

### What SODAX gives you

SODAX is execution infrastructure for modern money. You integrate one SDK, and your app can exchange, lend, borrow, and settle across blockchain networks as if there were no boundaries.

Most cross-network systems move assets. SODAX coordinates execution: it plans liquidity, timing, and recovery so an action started on one network completes predictably on another, even when conditions change mid-flight.

You integrate through a single SDK surface and keep full control of your user experience, pricing logic, and risk parameters. SODAX handles how execution behaves when networks are slow, fragmented, or partially available.

Three things you get out of one integration:

* **One surface, every network.** Build once against @sodax/sdk and reach all 18 connected networks.
* **Execution that settles, not just routes.** Swaps, borrows, and deposits complete across networks under real conditions, with explicit handling for delays and partial completion.
* **Your app stays yours.** You own the UX, the pricing, and the risk parameters. SODAX is infrastructure underneath, not a front end on top.

***

### The SDK stack

The SODAX developer suite is a dependency stack. Integrate at the foundation for maximum control, or use the higher layers for speed.

#### 1. Foundation: @sodax/sdk

The core logic layer that powers everything else. Raw functional modules to build with SODAX programmatically.

Functional modules:

* **Swaps** — quote and execute cross-network intents, filled by solvers on the SODAX marketplace.
* **Lend / Borrow** — interact directly with the SODAX money market.
* **Bridge** — low-level primitives for asset transfer.
* **Staking** — manage SODA staking and governance positions.
* **Migration** — utilities for migrating ICX to SODA.

Tooling modules:

* **Backend API** — useful data points for each feature.
* **Intent Relay API** — direct access to the intent propagation network.

#### 2. Connection layer

Manages connecting user wallets across heterogeneous networks (EVM, SVM, and non-EVM blockchain networks). Two flavors:

* **@sodax/wallet-sdk-core (TypeScript)** — a pure TypeScript implementation of wallet providers. Use this for a custom frontend framework or a non-React app.
* **@sodax/wallet-sdk-react (React adapter)** — an opinionated wrapper for React, with pre-built context providers and state management for wallet connections.

#### 3. Experience layer: @sodax/dapp-kit

The highest-level layer, built for speed. An opinionated set of UI components, hooks, and utilities built on the layers below.

* Under the hood: automatically wires @sodax/wallet-sdk for connection and @sodax/sdk for execution.
* What it offers: React hooks, contexts, and utilities for SODAX features.

***

### How execution works

Cross-network execution is asynchronous by nature, depends on liquidity fragmented across networks, and is exposed to volatility and partial completion. Asset transfers can succeed while the action they were meant to enable fails: collateral arrives but the borrow does not fill, a quoted price cannot be filled when execution begins.

SODAX is built to coordinate execution across those realities, end to end, so DeFi actions complete predictably. Three components make that possible.

#### Intent-based execution

You express an intent, a desired outcome. SODAX routes and settles it; independent solvers fill it. Users do not hand-route transactions.

* **Solver execution.** Solvers on the SODAX marketplace plan fills across networks based on liquidity, pricing, network constraints, and your builder-defined parameters. They decide how to fill; SODAX provides the routing and settlement rails they execute on.
* **Outcome-oriented settlement.** Routes are quoted from coordinated liquidity or external venues based on current conditions, then executed explicitly once approved.
* **Explicit asynchronous handling.** Multi-step execution, partial completion, and recovery paths are handled deliberately, so flows that cannot complete atomically still complete reliably.

#### Unified liquidity

SODAX treats liquidity as one system-level inventory, not isolated pools per network.

* **Global execution inventory.** Assets are accounted for across networks and coordinated to fulfill cross-network intents.
* **Solver-accessed at execution time.** Solvers draw on this liquidity when planning and executing fills, then redistribute it to keep the system balanced.
* **Less fragmentation risk.** Execution no longer depends on the right liquidity sitting on a specific network at a specific moment.

#### Smart wallet abstraction

SODAX coordinates cross-network account state as part of the execution layer, rather than relying on separate wallets per network.

* **Deterministic execution wallets.** Users get deterministic smart wallets that act as one consistent execution identity across networks.
* **Unified execution account.** Apps execute cross-network actions through a single wallet context, not separate accounts and approvals per network.
* **Simplified coordination.** SODAX handles wallet creation and execution routing, so you focus on what happens after execution.

***

### What you can build

SODAX is a modular execution system. Integrate each SDK module on its own, or combine them.

<table><thead><tr><th width="222.0390625">Partner Type</th><th>What you can build with SODAX</th></tr></thead><tbody><tr><td><strong>Wallets</strong></td><td>Use <strong>Swaps (Solver)</strong> to offer cross-network swaps in your UI, plus <strong>Bridge</strong> primitives where needed for asset transfer. Use <strong>Lend / Borrow (Money Market)</strong> to integrate lending primitives.</td></tr><tr><td><strong>DEXs &#x26; Aggregators</strong></td><td>Use <strong>Swaps (Solver)</strong> to quote and execute cross-network intents and expand routing beyond single-network liquidity</td></tr><tr><td><strong>Lending Protocols</strong></td><td>Use <strong>Lend / Borrow (Money Market)</strong> to integrate lending primitives and support multi-network user flows around collateral and borrowing.</td></tr><tr><td><strong>Perp DEXs / Yield Apps</strong></td><td>Use <strong>Swaps (Solver)</strong> to accept deposits from other networks via swap-into-your-asset flows, then complete the deposit inside your app. Use <strong>Lend / Borrow (Money Market)</strong> to enable borrowed asset deposits with user collateral on other networks.</td></tr><tr><td><strong>New Networks</strong></td><td>Integrate SODAX to provide builders with ready-made cross-network execution capabilities and liquidity access from day one.</td></tr></tbody></table>

<p align="center"><a href="https://sodax.com/partners" class="button primary" data-icon="handshake">Go to sodax.com/partners</a></p>

### Why build with SODAX

* **Execution beyond routing.** Routes move assets. SODAX coordinates liquidity so swaps, borrows, and deposits actually settle across networks.
* **One SDK, modular usage.** Integrate @sodax/sdk once, then use only the modules you need.
* **Built for real execution conditions.** Asynchronous by nature, with explicit timeouts and clear completion or failure handling.
* **Your control preserved.** You keep ownership of user experience, pricing logic, and risk parameters.
* **Proven in production.** Live cross-network flows across heterogeneous networks, settled by the protocol and filled by independent solvers. 21 protocols have integrated SODAX so far.

***

### Ecosystem reach

* **Networks:** 18+ networks spanning EVM and non-EVM environments, including Ethereum, Arbitrum, Base, BNB Chain, Avalanche, Optimism, Polygon, Solana, Sui, Stellar, Injective, and ICON.
* **Money market:** 26 assets available for lending and borrowing across networks.
* **Liquidity venues:** solvers fill intents through deep venues including Uniswap V3, PancakeSwap, Raydium, Pharaoh, Cetus, and DojoSwap.
* **Infrastructure compatibility:** designed to work alongside major messaging standards (GMP) for secure intent propagation, not replace them.

***

Together, SODAX provides the execution infrastructure required for modern money across networks.

***

### Next

* Install [@sodax/sdk](#id-1.-foundation-sodax-sdk) and ship your first cross-network action.
* Or scope it first: run the [Builders MCP](https://builders.sodax.com/) against your repo.


# SODAX for Solana

Reach the users and liquidity of every SODAX network from your Solana product. No new programs, your entire integration is TypeScript.

{% hint style="success" %}
**No new programs.** On Solana, smart contracts are called programs, and SODAX's are already live and [independently audited](https://github.com/icon-project/sodax-sdks/blob/main/Audits/Sodax%20%28Solana%29%20Smart%20Contract%20Audit%20Report%20-%20Final%20Report%20v2%20%281%29.pdf); your entire integration is TypeScript. Your users sign familiar Solana transactions, and SODAX coordinates cross-network execution, routing, and recovery behind the scenes.
{% endhint %}

You keep shipping on Solana. SODAX absorbs the cross-network execution layer, so your product can offer trading and lending against every asset in the SODAX System, 137 across all supported networks as of July 2026, without leaving your stack. Integration happens in TypeScript, at the SDK or React layer. There is nothing to deploy, audit, or maintain on-chain.

Using SODAX is free. Trades carry a fixed 0.1% base fee taken by the protocol, and you set your own platform fee on top. That part is fully yours, both the rate and the revenue. See [Monetize SDK](https://docs.sodax.com/developers/how-to/monetize_sdk).

Not sure where SODAX fits in your product? Paste your protocol's URL into the generator and get a guide written for your stack:

<a href="https://sodax.com/solana" class="button primary">Generate your integration guide</a>

## What you can build

Two SODAX modules are relevant to Solana builders:

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Swaps</strong></td><td>Intent-based cross-network swaps. Users trade between SPL assets and assets on any supported network with one signed transaction on Solana. Solvers compete to fill; unified pricing across networks.</td><td><a href="/pages/qsKuXf20YuHGk5ZUnlgN">/pages/qsKuXf20YuHGk5ZUnlgN</a></td></tr><tr><td><strong>Money Market</strong></td><td>Cross-network lending and borrowing. Depositors source collateral from any supported network and settle into positions that back Solana-native flows: supply, borrow, repay, withdraw.</td><td><a href="/pages/wyhd1pQD2enRYNJ4is0g">/pages/wyhd1pQD2enRYNJ4is0g</a></td></tr></tbody></table>

## Choose your integration path

| Path                                                                                                                                                                                      | Best for                                                          | Start here                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------- |
| [`@sodax/sdk`](https://docs.sodax.com/developers/packages/foundation/sdk)                                                                                                                 | Full control from any TypeScript backend or frontend              | [Quickstart](/solana/quickstart) |
| [`@sodax/dapp-kit`](https://docs.sodax.com/developers/packages/experience/dapp-kit) + [`@sodax/wallet-sdk-react`](https://docs.sodax.com/developers/packages/connection/wallet-sdk-react) | React apps that want hooks and wallet connectivity out of the box | [Wallets](/solana/wallets)       |
| [Builders MCP](https://builders.sodax.com)                                                                                                                                                | Building with AI agents against the full SODAX stack              | See below                        |

## Scope and build with AI

Point your AI coding assistant at the [SODAX Builders MCP](https://builders.sodax.com/) and it gains live access to the whole SODAX System: supported chains and tokens, real swap quotes, money market rates, intent history, and these docs. 40 tools in all, working in Claude, Cursor, VS Code, Windsurf, and any other MCP-capable client.

```json
{
  "mcpServers": {
    "sodax-builders": {
      "url": "https://builders.sodax.com/mcp"
    }
  }
}
```

Use it to scope before you commit: your assistant can read your own codebase, pull real quotes and token lists, and map exactly where SODAX fits in your product. Then let it build against the same live data. Full tool list and per-client setup at [builders.sodax.com](https://builders.sodax.com/).

## In this section

* [Quickstart](/solana/quickstart): install the SDK, connect a Solana wallet, make your first cross-network swap.
* [Swaps on Solana](/solana/swaps): how intents work from a Solana perspective, quoting, fees, and settlement.
* [Money Market on Solana](/solana/money-market): supply, borrow, repay, and withdraw with cross-network collateral.
* [Wallets](/solana/wallets): Phantom, Backpack, Solflare, and every other network's wallets through one interface.
* [Networks & Assets](/solana/networks-and-assets): what is live on Solana today and what your users can reach.
* [Solana FAQ](/solana/faq): the questions Solana engineers actually ask.

## Out in the ecosystem

You will run into us where Solana builders already are: Breakpoint, Superteam events, hackathon season.

| ![SODAX at a Superteam workshop](/files/EYVUSgvZ8j80QZO53wLr) | ![SODAX at Solana Breakpoint](/files/OnpZWB4C9KaH2jIgKzWh) | ![SODAX booth at a Solana event](/files/uSSTucOHaSvuIEClQHiv) |
| ------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- |

If you are getting oriented in Solana development more broadly, these are the resources we point builders to:

* [**Superteam**](https://superteam.fun/): the global collective of Solana builders: bounties, grants, local chapters, and the fastest way to find collaborators and your first users.
* [**Colosseum**](https://colosseum.com/hackathon): Solana's flagship online hackathon and accelerator. A cross-network swap or lending feature built on SODAX makes a strong hackathon wedge; ship it in a weekend, no new programs to write.
* [**Solana developer docs**](https://solana.com/developers): the canonical starting point for the runtime, SPL tokens, and tooling.
* [**Anchor**](https://www.anchor-lang.com/): the standard framework for writing Solana programs; a SODAX integration lives alongside it in your TypeScript client, not inside it.
* [**Solana Stack Exchange**](https://solana.stackexchange.com/): where the sharp edges get answered.

***

Building something? Generate a guide at [sodax.com/solana](https://sodax.com/solana), or start with the [Quickstart](/solana/quickstart).


# Quickstart

Install the SDK, wire up a Solana wallet provider, and execute your first cross-network swap from Solana.

This page takes you from zero to a cross-network swap sourced from Solana. No new programs, no token approvals, one signed transaction for your user.

{% hint style="info" %}
Building with an AI assistant? Add the [SODAX Builders MCP](https://builders.sodax.com/) (`https://builders.sodax.com/mcp`) to Claude, Cursor, or any MCP-capable tool and it can pull live token lists, real quotes, and these docs while it writes your integration.
{% endhint %}

## 1. Install

```bash
npm install @sodax/sdk
# or: pnpm add @sodax/sdk / yarn add @sodax/sdk
```

## 2. Create a Solana wallet provider

The SDK talks to Solana through `SolanaWalletProvider`, built on `@solana/web3.js`. Use private-key mode for scripts and bots, or browser-extension mode to wrap a connected wallet (Phantom, Backpack, Solflare, or anything exposing `publicKey` and `signTransaction`).

```typescript
import { SolanaWalletProvider } from '@sodax/sdk';

// Private-key mode (scripts / bots)
const walletProvider = new SolanaWalletProvider({
  privateKey: keypairBytes, // Uint8Array, raw keypair bytes
  endpoint: 'https://api.mainnet-beta.solana.com',
});

// Browser-extension mode (dApps)
const walletProvider = new SolanaWalletProvider({
  wallet: walletContextState, // { publicKey, signTransaction }
  endpoint: 'https://api.mainnet-beta.solana.com',
});
```

Building in React? [`@sodax/wallet-sdk-react`](/solana/wallets) discovers and connects wallets for you and hands back a ready-made provider via `useWalletProvider`.

## 3. Look up supported tokens

Token addresses and decimals come from the SDK config, so you never hard-code mints:

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax();
await sodax.initialize(); // optional: pulls the latest token config

const solanaTokens = sodax.swaps.getSupportedSwapTokensByChainId(ChainKeys.SOLANA_MAINNET);
const sol = solanaTokens.find(t => t.symbol === 'SOL');

const arbTokens = sodax.swaps.getSupportedSwapTokensByChainId(ChainKeys.ARBITRUM_MAINNET);
const usdcArb = arbTokens.find(t => t.symbol === 'USDC');
```

## 4. Quote and swap

`swap()` runs the full lifecycle: it creates the intent on Solana, verifies the transaction landed, relays it to the hub, and notifies the solver to fill on the destination network. The Solana-specific relay data is handled for you.

```typescript
import type { SolverIntentQuoteRequest } from '@sodax/sdk';

// Quote: 0.1 SOL -> USDC on Arbitrum
const quoteResult = await sodax.swaps.getQuote({
  token_src: sol.address,
  token_dst: usdcArb.address,
  token_src_blockchain_id: ChainKeys.SOLANA_MAINNET,
  token_dst_blockchain_id: ChainKeys.ARBITRUM_MAINNET,
  amount: 100_000_000n, // 0.1 SOL (9 decimals)
  quote_type: 'exact_input',
} satisfies SolverIntentQuoteRequest);

if (!quoteResult.ok) throw new Error('Quote failed');
const { quoted_amount } = quoteResult.value;

// Execute
const swapResult = await sodax.swaps.swap({
  params: {
    inputToken: sol.address,
    outputToken: usdcArb.address,
    inputAmount: 100_000_000n,
    minOutputAmount: (quoted_amount * 99n) / 100n, // your slippage policy
    deadline: 300n, // or use sodax.swaps.getSwapDeadline()
    allowPartialFill: false,
    srcChainKey: ChainKeys.SOLANA_MAINNET,
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    srcAddress: await walletProvider.getWalletAddress(),
    dstAddress: '0x...', // recipient on the destination network
    solver: '0x0000000000000000000000000000000000000000',
    data: '0x',
  },
  walletProvider,
  timeout: 120_000,
});

if (swapResult.ok) {
  console.log('Filled. Hub tx:', swapResult.value.intentDeliveryInfo.dstTxHash);
} else {
  console.error('Swap failed:', swapResult.error);
}
```

A few things you did not have to do:

* **No approval step.** Allowances are an EVM and Stellar concept; on Solana `isAllowanceValid` passes without an on-chain transaction.
* **No relay bookkeeping.** Manually orchestrated Solana intents need relay extra data (`getIntentSubmitTxExtraData`); `swap()` takes care of it.
* **No error guessing.** Every method returns `Result<T>`. Check `result.ok` and branch on typed error codes instead of catching throws.

## Next steps

* [Swaps on Solana](/solana/swaps): quoting modes, fees, limit orders, manual orchestration.
* [Money Market on Solana](/solana/money-market): supply and borrow with cross-network collateral.
* Full API reference: [Swaps (Solver)](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps) in the SDK docs.


# Swaps on Solana

Intent-based cross-network swaps from a Solana perspective: one signed transaction, solver-filled, settled across networks.

SODAX swaps are intents, not bridges. Your user signs one transaction on Solana that locks the input and declares what they want on the destination network. Solvers compete to fill it; the SODAX relayer carries proof between Solana, the hub (Sonic), and the destination. If nothing fills, the intent can be cancelled and funds recovered.

From your side it is one SDK call. From your user's side it is one Phantom prompt.

## What this looks like on Solana

* **Source**: the intent transaction is a normal Solana transaction signed by the user's wallet. `ChainKeys.SOLANA_MAINNET` (`'solana'`) identifies the chain everywhere in the SDK.
* **Reach**: any solver-compatible asset on any SODAX network is a valid destination, 137 assets as of July 2026. The [on-Solana asset list](/solana/networks-and-assets) only bounds what users hold on Solana itself, not what they can swap into.
* **No approvals**: there is no ERC-20 style allowance on Solana. `isAllowanceValid` always returns `true`; you can go straight from quote to swap.
* **Settlement**: output lands on the destination network at `dstAddress`. Swaps into Solana settle to the user's address as SPL balances.
* **Raw mode**: methods that support `raw: true` return a `SolanaRawTransaction` when the source is Solana, TypeScript-narrowed from `srcChainKey`, if you want to control signing and broadcast yourself.

## Quoting

The solver API quotes both directions:

```typescript
const quoteResult = await sodax.swaps.getQuote({
  token_src: sol.address,             // token on Solana
  token_dst: usdc.address,            // token on the destination network
  token_src_blockchain_id: ChainKeys.SOLANA_MAINNET,
  token_dst_blockchain_id: ChainKeys.ARBITRUM_MAINNET,
  amount: 100_000_000n,
  quote_type: 'exact_input',          // or 'exact_output'
});
```

`getQuote` deducts any configured partner fee before quoting, so `quoted_amount` is the net output the user actually receives.

## Fees

Using SODAX is free to integrate. Two fees can apply to a trade:

* **Base fee**: a fixed 0.1% of the input, taken by the protocol. Not configurable; compute it ahead of time with `getSolverFee(inputAmount)`.
* **Your fee**: optional platform fee on top, set by you and paid to you. It is the only fee you control. Configure it at SDK setup and check it with `getPartnerFee(inputAmount)`. See [Monetize SDK](https://docs.sodax.com/developers/how-to/monetize_sdk).

## Executing

Prefer `swap()`. It creates the intent on Solana, verifies it landed, submits it to the relay, waits for the packet on the hub, and notifies the solver:

```typescript
const result = await sodax.swaps.swap({
  params: {
    inputToken: sol.address,
    outputToken: usdc.address,
    inputAmount: 100_000_000n,
    minOutputAmount: minOut,
    deadline: 300n,
    allowPartialFill: false,
    srcChainKey: ChainKeys.SOLANA_MAINNET,
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    srcAddress: await walletProvider.getWalletAddress(),
    dstAddress: recipient,
    solver: '0x0000000000000000000000000000000000000000',
    data: '0x',
  },
  walletProvider, // ISolanaWalletProvider, narrowed by srcChainKey
});
```

Track progress with `getStatus(request)`, and cancel unfilled intents with `cancelIntent`. Limit orders (intents without deadlines) work from Solana too, via `createLimitOrder`.

## Orchestrating manually? One Solana-specific step

If you use `createIntent` + `submitIntent` instead of `swap()`, note that **Solana (and Bitcoin) intents require relay extra data**. Fetch it with `getIntentSubmitTxExtraData` and pass it as `data` in the relay submission:

```typescript
const extraData = await sodax.swaps.getIntentSubmitTxExtraData({ txHash: hubTxHash });

await sodax.swaps.submitIntent({
  action: 'submit',
  params: {
    chain_id: 'solana',
    tx_hash: spokeTxHash,
    data: extraData.value, // required for Solana sources
  },
});
```

`swap()` does this automatically; most integrations never touch it.

## Error handling

Every method returns `Result<T>` instead of throwing. The core methods (`swap`, `createIntent`, `postExecution`) return a typed `SodaxError` union you can `switch` on by `error.code`.

***

Full reference, including raw mode, limit orders, cancellation, and the complete error-code table: [Swaps (Solver)](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps).


# Money Market on Solana

Cross-network lending and borrowing from Solana: supply, borrow, repay, and withdraw against collateral sourced on any SODAX network.

Fragmented money markets mean lower capital efficiency and worse rates. The SODAX money market pools lending liquidity on the hub while letting users act from any supported network, including Solana. A depositor can supply collateral held on another network and borrow into their Solana workflow, or lend idle SPL balances without your product running its own lending infrastructure.

Four actions, all through `sodax.moneyMarket`: `supply`, `borrow`, `withdraw`, `repay`. Each has a complete one-call form (handles the relay end-to-end) and a `create*Intent` form when you want the transaction only.

## Supply from Solana

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax();

const solanaTokens = sodax.moneyMarket.getSupportedTokensByChainId(ChainKeys.SOLANA_MAINNET);
const sol = solanaTokens.find(t => t.symbol === 'SOL');

const result = await sodax.moneyMarket.supply({
  params: {
    srcChainKey: ChainKeys.SOLANA_MAINNET,
    srcAddress: await walletProvider.getWalletAddress(),
    token: sol.address,
    amount: 1_000_000_000n, // 1 SOL (9 decimals)
    action: 'supply',
  },
  walletProvider, // ISolanaWalletProvider, narrowed by srcChainKey
  timeout: 120_000, // optional relay timeout (default 120 s)
});
```

## Borrow to Solana

`dstChainKey` and `dstAddress` default to the source, but any supported network can be the destination. That is the cross-network story in one parameter: collateral supplied from one network, borrowed liquidity delivered to another.

```typescript
const result = await sodax.moneyMarket.borrow({
  params: {
    srcChainKey: ChainKeys.SOLANA_MAINNET,   // where the caller acts from
    srcAddress: userSolanaAddress,
    token: bnUSD.address,                     // token on the destination chain
    amount: 500_000_000_000_000_000_000n,
    action: 'borrow',
    dstChainKey: ChainKeys.SOLANA_MAINNET,    // deliver on Solana
    dstAddress: userSolanaAddress,
  },
  walletProvider,
});
```

`withdraw` and `repay` follow the same shape. Withdraw and borrow need no approval on any network; for supply and repay from Solana there is no ERC-20 style allowance either, so no extra transaction precedes the action.

## Building a lending UI

`sodax.moneyMarket.data` exposes what you need for rates, positions, and health factors without extra indexing:

* `getReservesHumanized()`: all reserves, human-readable.
* `getUserReservesHumanized(spokeChainKey, userAddress)`: a user's positions.
* `formatReservesUSD(request)` / `formatUserSummary(request)`: USD-converted reserves and portfolio summaries.

## Error handling

The module returns typed results. Discriminate on `result.error.code` (`'RELAY_TIMEOUT'`, `'EXECUTION_FAILED'`, …) with structured context on `result.error.context`.

***

Full reference, including intent-only methods, gas estimation, and the per-method error-code table: [Lend / Borrow (Money Market)](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market).


# Wallets

Phantom, Backpack, Solflare, and every other network's wallets through one interface, with @sodax/wallet-sdk-react or a direct provider.

Welcoming cross-network users means supporting the wallets they already use. On Solana that is Phantom, Backpack, Solflare, and anything else speaking the wallet-standard; on the networks your users arrive from, it is MetaMask, Rabby, Suiet, Lobstr, and more. SODAX gives you both through one interface, so wallet plumbing never becomes the integration.

## React apps: `@sodax/wallet-sdk-react`

The React package wraps each chain family's native wallet SDK (`@solana/wallet-adapter` for Solana) behind a uniform connector interface with a single persisted store.

```bash
pnpm add @sodax/wallet-sdk-react
```

Mount the provider with the chains you serve. Chains are opt-in; add Solana with an RPC endpoint:

```tsx
import { SodaxWalletProvider, type SodaxWalletConfig } from '@sodax/wallet-sdk-react';
import { ChainKeys } from '@sodax/types';

const config: SodaxWalletConfig = {
  SOLANA: {
    chains: {
      [ChainKeys.SOLANA_MAINNET]: { rpcUrl: 'https://api.mainnet-beta.solana.com' },
    },
  },
  EVM: {
    chains: {
      [ChainKeys.SONIC_MAINNET]: { rpcUrl: 'https://rpc.soniclabs.com' },
    },
  },
};
```

Then connect and read accounts with the same three hooks for every chain family:

```tsx
import { useXConnectors, useXConnect, useXAccount } from '@sodax/wallet-sdk-react';

function SolanaConnect() {
  const connectors = useXConnectors({ xChainType: 'SOLANA' }); // Phantom, Backpack, ...
  const { mutateAsync: connect } = useXConnect();
  const account = useXAccount({ xChainType: 'SOLANA' });

  if (account.address) return <p>Connected: {account.address}</p>;

  return connectors.map(c => (
    <button key={c.id} onClick={() => connect(c)}>{c.name}</button>
  ));
}
```

### Bridging to SDK calls

`useWalletProvider` returns a typed `ISolanaWalletProvider` ready to pass into any `@sodax/sdk` method. The [swap](/solana/swaps) and [money market](/solana/money-market) calls take it directly. A headless `useWalletModal` state machine is available if you are building a multi-chain connect modal.

## Scripts and backends: `SolanaWalletProvider`

Outside React, construct the provider from `@sodax/sdk` directly:

```typescript
import { SolanaWalletProvider } from '@sodax/sdk';

const walletProvider = new SolanaWalletProvider({
  privateKey: keypairBytes, // Uint8Array; or pass `wallet` for extension mode
  endpoint: 'https://api.mainnet-beta.solana.com',
});
```

`SolanaWalletDefaults` lets you tune Solana-specific behavior per provider: `connectionCommitment`, `connectionConfig`, `sendOptions`, and `confirmCommitment`.

***

* Full React reference: [@sodax/wallet-sdk-react](https://docs.sodax.com/developers/packages/connection/wallet-sdk-react)
* All nine chain families' providers: [Setup Wallet Providers](https://docs.sodax.com/developers/how-to/wallet_providers)


# Networks & Assets

What is live on Solana today, and the networks and assets your users can reach from it.

Solana is a spoke network in the SODAX System. User funds stay on Solana; the hub (Sonic) coordinates execution, and solvers settle against deep native liquidity on each network rather than isolated bridge pools.

There are two asset lists to keep straight, and the difference is the point of SODAX:

* **Assets on Solana**: the 21 assets that live on Solana itself (July 2026). These are what your users hold, send, and receive on Solana; the entry and exit points of any Solana-side flow.
* **Assets across the SODAX System**: 137 assets across all supported networks (July 2026). Every one of them is a valid swap destination or collateral source for your Solana users, because every SODAX action is cross-network by default.

A user holding SOL is not limited to the 21. They can swap SOL into ETH on Ethereum, USDC on Base, BTC on Bitcoin, or any other listed asset on any network, in one signed transaction. The 21 on-Solana assets are simply where those journeys start and end on your side.

## Assets on Solana

As of July 2026, 21 assets are live for swaps on Solana:

| Category                     | Assets                                                |
| ---------------------------- | ----------------------------------------------------- |
| Core & stablecoins           | SOL, USDC, USDT, bnUSD, SODA, WBTC (Wormhole)         |
| Solana ecosystem             | BONK, JUP, RAY, PYTH, JTO                             |
| Liquid staking               | JitoSOL, mSOL                                         |
| Tokenized equities (xStocks) | TSLAx, NVDAx, GOOGLx, COINx, MSTRx, CRCLx, SPYx, QQQx |

Six of these are also live in the [Money Market](/solana/money-market) for supply, borrow, repay, and withdraw: **SOL, USDC, USDT, bnUSD, SODA, and JitoSOL**.

The list grows continuously. The complete, always-current view is the [asset directory](https://sodax.com/partners/asset-directory), which reads the same configuration the SDK uses. Hub vault addresses per asset are listed in [Swaps: Compatible Assets](https://docs.sodax.com/developers/deployments/swaps-compatible-assets#solana).

## Assets your users can reach

From a Solana-sourced action, users can trade, lend, and borrow against the full 137-asset directory across every SODAX-supported network:

* **EVM**: Sonic (hub), Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia, Hedera
* **Non-EVM**: Solana, Sui, Stellar, ICON, Injective, NEAR, Stacks, Bitcoin

Chain identifiers come from `ChainKeys.*` in the SDK (`ChainKeys.SOLANA_MAINNET` = `'solana'`), and the live chain list from `sodax.config.getSupportedSpokeChains()`. Prefer the config call over hard-coding: networks are added over time.

## Asset discovery in code

Never hard-code mints or asset lists; read them from the SDK config at runtime:

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax();
await sodax.initialize(); // pull the latest asset config

// Swappable assets on Solana
const swapTokens = sodax.swaps.getSupportedSwapTokensByChainId(ChainKeys.SOLANA_MAINNET);

// Money market assets on Solana
const mmTokens = sodax.moneyMarket.getSupportedTokensByChainId(ChainKeys.SOLANA_MAINNET);
```

***

* All deployment addresses by network: [Mainnet Deployments](https://docs.sodax.com/developers/deployments/mainnet)
* The full cross-network asset directory: [sodax.com/partners/asset-directory](https://sodax.com/partners/asset-directory)


# Solana FAQ

The questions Solana engineers actually ask about integrating SODAX.

#### Do I need to write or deploy a program?

No. SODAX's Solana programs, its on-chain smart contracts, are already deployed and audited; your integration is TypeScript against `@sodax/sdk` (or the React packages on top of it). You write no on-chain code and take on no program upgrade authority for any part of the flow.

#### How does a swap from Solana actually settle?

Your user signs one Solana transaction that creates an intent. The SODAX relayer carries proof of that transaction to the hub network (Sonic), a solver fills the intent on the destination network, and the output lands at the recipient address there. Swaps into Solana settle as SPL balances at the user's address. If an intent is never filled, it can be cancelled and the funds recovered.

#### Which assets are supported on Solana?

Two answers, and the difference matters. On Solana itself, 21 assets are live as of July 2026: SOL, USDC, USDT, bnUSD, SODA, WBTC, ecosystem tokens like BONK, JUP, RAY, PYTH, and JTO, the liquid staking tokens JitoSOL and mSOL, and eight tokenized equities (xStocks). Six of these also work in the money market.

But your users are not limited to those 21. Every swap is cross-network, so from Solana they can reach all 137 assets across every SODAX-supported network (July 2026): SOL into ETH on Ethereum, USDC on Base, BTC on Bitcoin, and so on. The 21 are just the Solana-side entry and exit points. Both lists grow; read them from `sodax.config` at runtime or check [Networks & Assets](/solana/networks-and-assets).

#### Do my users need a new wallet?

No. Phantom, Backpack, Solflare, and other wallet-standard wallets work through `@sodax/wallet-sdk-react` (which wraps `@solana/wallet-adapter`) or directly via `SolanaWalletProvider`. See [Wallets](/solana/wallets).

#### Is there an approve step before swapping or supplying?

No. Token allowances are an EVM and Stellar concept. On Solana the SDK's `isAllowanceValid` check passes without an on-chain transaction, so the quote-to-action path is a single signature.

#### Can I tune commitment levels and send options?

Yes. `SolanaWalletProvider` accepts `SolanaWalletDefaults`: `connectionCommitment`, `connectionConfig`, `sendOptions`, and `confirmCommitment`. Bring your own RPC endpoint (Helius, Triton, or your own node) via the `endpoint` field.

#### What does it cost?

Using SODAX is free: no license, no integration fee, no SDK cost. Trades carry a fixed 0.1% base fee taken by the protocol; on top of that you set your own platform fee and keep it. Your integration is a revenue line, not a cost center. See [Monetize SDK](https://docs.sodax.com/developers/how-to/monetize_sdk).

#### How do I know if something went wrong?

Every SDK method returns `Result<T>` rather than throwing. Core swap and money market methods return typed error codes (for example `'RELAY_TIMEOUT'`, `'EXECUTION_FAILED'`) you can branch on, with structured context attached.

#### Where are the audits?

SODAX's Solana programs have their own published report: [SODAX (Solana) Smart Contract Audit (Final Report v2)](https://github.com/icon-project/sodax-sdks/blob/main/Audits/Sodax%20%28Solana%29%20Smart%20Contract%20Audit%20Report%20-%20Final%20Report%20v2%20%281%29.pdf). Audits for the rest of the protocol are collected on the [Audits](https://docs.sodax.com/welcome-to-sodax/audits) page.

#### Can my AI coding assistant do the integration?

Largely, yes. Add the [SODAX Builders MCP](https://builders.sodax.com/) to Claude, Cursor, VS Code, Windsurf, or any MCP-capable client (`https://builders.sodax.com/mcp`) and it gets 40 tools with live access to the SODAX System: chains, tokens, swap quotes, money market rates, intent history, and auto-synced SDK docs. Use it to scope the integration against your own codebase first, then to write and verify the code. Setup per client is at [builders.sodax.com](https://builders.sodax.com/).

#### I want a plan specific to my protocol. Who do I talk to?

Generate a tailored integration guide at [sodax.com/solana](https://sodax.com/solana): paste your protocol's URL and it maps SODAX onto your stack. From there you can reach the BD team directly.


# Contact Form

Want to integrate with SODAX or explore a partnership? Visit our [partners page](https://sodax.com/partners) for an overview of integration options, or reach out directly at <partnerships@sodax.com>.


# Audits

This directory contains all audit reports for the Sodax project.

## Table of Contents

* [Sodax (Sui) Smart Contract Audit](https://github.com/icon-project/sodax-sdks/tree/main/Audits/Sodax%20\(Sui\)%20Smart%20Contract%20Audit%20Report%20-%20Final%20Report%20v2%20\(1\).pdf)
* [Sodax (Solana) Smart Contract Audit](https://github.com/icon-project/sodax-sdks/tree/main/Audits/Sodax%20\(Solana\)%20Smart%20Contract%20Audit%20Report%20-%20Final%20Report%20v2%20\(1\).pdf)
* [Sodax Soroban Smart Contract Audit](https://github.com/icon-project/sodax-sdks/tree/main/Audits/SODAX_Soroban_Smart_Contract_Audit_Report_Final_Report%20\(2\)%20\(1\).pdf)
* [Sodax Relay Audit (November 2025)](https://github.com/icon-project/sodax-sdks/tree/main/Audits/2025_11_12_Final_Sodax_Relay_Audit_Report_1762953394%20\(1\).pdf)
* [Sodax Collaborative Audit (August 2025)](https://github.com/icon-project/sodax-sdks/tree/main/Audits/2025.08.01%20-%20Final%20-%20Sodax%20Collaborative%20Audit%20Report%201754078902%20\(1\)%20\(1\).pdf)
* [Sodax (Near) Smart Contract Audit](https://github.com/icon-project/sodax-sdks/tree/main/Audits/Final%20Report-%20Near%20Sodax%20contracts%20audit.pdf)
* [Sodax (Stacks) Smart Contract Audit](https://github.com/icon-project/sodax-sdks/tree/main/Audits/2026.02.26%20-%20Final%20-%20Sodax%20Collaborative%20Audit%20Report%201772065303.pdf)
* [Sodax Collaborative Audit (LST / Leverage Yield Vault, May 2026)](https://github.com/icon-project/sodax-sdks/tree/main/Audits/2026.05.29%20-%20Final%20-%20Sodax%20Collaborative%20Audit%20Report%20Sherlock.pdf)


# Deployments


# Mainnet

Mainnet smart contract deployments.

## Mainnet

EVM Owner: [0x36BB43bED1338Ab7D9F07e06bF5e330D4FdBfd6f](https://sonicscan.org/address/0x36BB43bED1338Ab7D9F07e06bF5e330D4FdBfd6f)

Lightlink Owner: [0xa5178EfEf61c1877B664269bAedE296e1f451B6a](https://phoenix.lightlink.io/address/0xa5178EfEf61c1877B664269bAedE296e1f451B6a)

HyperEVM Owner: [0x9eD17ecBB8c94f1bE13D4BcC676d346d9779A132](https://liquidscan.io/address/0x9eD17ecBB8c94f1bE13D4BcC676d346d9779A132)

Solana multisig: [5eJYXYhqxsTgBZmp7CJkoBQpRe2bEWnQZdT3iwaqh715](https://app.squads.so/squads/5eJYXYhqxsTgBZmp7CJkoBQpRe2bEWnQZdT3iwaqh715/home)

SUI Multisig: [0x06b9841e882aeb5dd1ea2c67256d8667110acfe19031140be209fd04d4ee6c79](https://sui.m-safe.io/transactions?msafe=0x06b9841e882aeb5dd1ea2c67256d8667110acfe19031140be209fd04d4ee6c79)

Sonic treasury: [0x93D5CE288b3BF6b33F913b98FD1fA844Acc462d4](https://sonicscan.org/address/0x93D5CE288b3BF6b33F913b98FD1fA844Acc462d4)

Intent Canceller: [0x7Ec046CEB0C01A2Ef16C77d2e6bB56E3AF3464cf](https://sonicscan.org/address/0x7Ec046CEB0C01A2Ef16C77d2e6bB56E3AF3464cf)

## Infrastructure HUB

| Contracts                                      | Address                                                                                                                |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| WalletFactory                                  | [0xA0ed3047D358648F2C0583B415CffCA571FDB544](https://sonicscan.org/address/0xA0ed3047D358648F2C0583B415CffCA571FDB544) |
| AssetManager                                   | [0x60c5681bD1DB4e50735c4cA3386005A4BA4937C0](https://sonicscan.org/address/0x60c5681bD1DB4e50735c4cA3386005A4BA4937C0) |
| RateLimits                                     | [0xA1986e8B7FE5657822e593297cfA1A85354911CC](https://sonicscan.org/address/0xA1986e8B7FE5657822e593297cfA1A85354911CC) |
| VaultTokenImpl                                 | [0xBA5208C1EE558c8e150Bf9A0CF6CC4b43Cdbbee8](https://sonicscan.org/address/0xBA5208C1EE558c8e150Bf9A0CF6CC4b43Cdbbee8) |
| Intents                                        | [0x6382D6ccD780758C5e8A6123c33ee8F4472F96ef](https://sonicscan.org/address/0x6382D6ccD780758C5e8A6123c33ee8F4472F96ef) |
| Lending Manager                                | [0x73135D19C488EA5b002E0e07135D992FF7e6f070](https://sonicscan.org/address/0x73135D19C488EA5b002E0e07135D992FF7e6f070) |
| Solver Router                                  | [0xA6F5733DB72975c0e9AF1eE4B95904F7ca16A7c0](https://sonicscan.org/address/0xA6F5733DB72975c0e9AF1eE4B95904F7ca16A7c0) |
| Sonic Router                                   | [0xC67C3e55c665E78b25dc9829B3Aa5af47d914733](https://sonicscan.org/address/0xC67C3e55c665E78b25dc9829B3Aa5af47d914733) |
| Hub Manager                                    | [0x84173cB4dc74686fD15054eDC7AdE9BD3b66cB77](https://sonicscan.org/address/0x84173cB4dc74686fD15054eDC7AdE9BD3b66cB77) |
| ICX Migration                                  | [0x8294DE9fc60F5ABCc19245E5857071d7C42B9875](https://sonicscan.org/address/0x8294DE9fc60F5ABCc19245E5857071d7C42B9875) |
| Baln Migration                                 | [0x610a90B61b89a98b954d5750E94834Aa45d08d10](https://sonicscan.org/address/0x610a90B61b89a98b954d5750E94834Aa45d08d10) |
| sodaVault                                      | [0x21685E341DE7844135329914Be6Bd8D16982d834](https://sonicscan.org/address/0x21685E341DE7844135329914Be6Bd8D16982d834) |
| stSoda                                         | [0x4333B324102d00392038ca92537DfbB8CB0DAc68](https://sonicscan.org/address/0x4333B324102d00392038ca92537DfbB8CB0DAc68) |
| xSoda                                          | [0xADC6561Cc8FC31767B4917CCc97F510D411378d9](https://sonicscan.org/address/0xADC6561Cc8FC31767B4917CCc97F510D411378d9) |
| StakingRouter                                  | [0xE287Cd568543d880e0F0DfaDCE18B44930759367](https://sonicscan.org/address/0xE287Cd568543d880e0F0DfaDCE18B44930759367) |
| ProtocolIntents (pending ownership)            | [0xaFf2EDb3057ed6f9C1dA6c930b8ddDf2beE573A5](https://sonicscan.org/address/0xaFf2EDb3057ed6f9C1dA6c930b8ddDf2beE573A5) |
| FeeTreasury (pending ownership) (pending feem) | [0xf2E26765949731f251D5d15f30f483b7a321b3A4](https://sonicscan.org/address/0xf2E26765949731f251D5d15f30f483b7a321b3A4) |
| Trickler                                       | [0xFB64C51484B7E0B0aAc3bb05f9a4425bf39d6176](https://sonicscan.org/address/0xFB64C51484B7E0B0aAc3bb05f9a4425bf39d6176) |
| Spokes Manager                                 | [0x84173cB4dc74686fD15054eDC7AdE9BD3b66cB77](https://sonicscan.org/address/0x84173cB4dc74686fD15054eDC7AdE9BD3b66cB77) |
| Liquidator (pending Ownership and Feem)        | [0xc3297fbe4c404b3902852c227fefcccfa48fb7ec](https://sonicscan.org/address/0xc3297fbe4c404b3902852c227fefcccfa48fb7ec) |
| MarketMaking (pending Ownership and Feem)      | [0x44f5830bed0916e26746095aac195794ecad1e07](https://sonicscan.org/address/0x44f5830bed0916e26746095aac195794ecad1e07) |

## SodaTokens (Vault tokens)

| Token      | Address                                                                                                                | Assets                                                                               |
| ---------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| sodaBNB    | [0x40Cd41b35DB9e5109ae7E54b44De8625dB320E6b](https://sonicscan.org/address/0x40Cd41b35DB9e5109ae7E54b44De8625dB320E6b) | BNB                                                                                  |
| sodaAVAX   | [0x14238D267557E9d799016ad635B53CD15935d290](https://sonicscan.org/address/0x14238D267557E9d799016ad635B53CD15935d290) | AVAX                                                                                 |
| sodaETH    | [0x4effB5813271699683C25c734F4daBc45B363709](https://sonicscan.org/address/0x4effB5813271699683C25c734F4daBc45B363709) | ETH, ETHB, Sonic WETH                                                                |
| sodaBTC    | [0x7A1A5555842Ad2D0eD274d09b5c4406a95799D5d](https://sonicscan.org/address/0x7A1A5555842Ad2D0eD274d09b5c4406a95799D5d) | BTC, BTCB, cbBTC, tBTC, BTC.LL, rBTC (wBTC/WBTC deposits paused — moved to sodaWBTC) |
| sodaWBTC   | [0x811C3fCc13f9c2a23AE2Ae2DCadacFAC6eb5f0eB](https://sonicscan.org/address/0x811C3fCc13f9c2a23AE2Ae2DCadacFAC6eb5f0eB) | wBTC (Arbitrum), WBTC (Ethereum)                                                     |
| sodaHASUI  | [0x1F489952aCa084a5594cF3c206a47d63782E9e66](https://sonicscan.org/address/0x1F489952aCa084a5594cF3c206a47d63782E9e66) | haSUI                                                                                |
| sodaSOL    | [0xdEa692287E2cE8Cb08FA52917Be0F16b1DACDC87](https://sonicscan.org/address/0xdEa692287E2cE8Cb08FA52917Be0F16b1DACDC87) | SOL                                                                                  |
| sodaXLM    | [0x6BC8C37cba91F76E68C9e6d689A9C21E4d32079B](https://sonicscan.org/address/0x6BC8C37cba91F76E68C9e6d689A9C21E4d32079B) | XLM                                                                                  |
| sodaINJ    | [0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13](https://sonicscan.org/address/0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13) | INJ                                                                                  |
| sodaNIBI   | [0xc6c85287a8b173A509C2F198bB719A8a5a2d0C68](https://sonicscan.org/address/0xc6c85287a8b173A509C2F198bB719A8a5a2d0C68) | NIBI                                                                                 |
| sodaSUI    | [0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3](https://sonicscan.org/address/0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3) | SUI                                                                                  |
| bnUSD      | [0xE801CA34E19aBCbFeA12025378D19c4FBE250131](https://sonicscan.org/address/0xE801CA34E19aBCbFeA12025378D19c4FBE250131) | bnUSD, USDC, USDT                                                                    |
| sodaUSDC   | [0xAbbb91c0617090F0028BDC27597Cd0D038F3A833](https://sonicscan.org/address/0xAbbb91c0617090F0028BDC27597Cd0D038F3A833) | USDC                                                                                 |
| sodaUSDT   | [0xbDf1F453FCB61424011BBDDCB96cFDB30f3Fe876](https://sonicscan.org/address/0xbDf1F453FCB61424011BBDDCB96cFDB30f3Fe876) | USDT                                                                                 |
| ICON bnUSD | [0x9D4b663Eb075d2a1C7B8eaEFB9eCCC0510388B51](https://sonicscan.org/address/0x9D4b663Eb075d2a1C7B8eaEFB9eCCC0510388B51) | ICON bnUSD (Migration)                                                               |
| sodaS      | [0x62ecc3Eeb80a162c57624B3fF80313FE69f5203e](https://sonicscan.org/address/0x62ecc3Eeb80a162c57624B3fF80313FE69f5203e) | Wrapped Sonic                                                                        |
| sodaPOL    | [0x208ED38f4783328aA9eBFeC360D32e7520A9B779](https://sonicscan.org/address/0x208ED38f4783328aA9eBFeC360D32e7520A9B779) | POL                                                                                  |
| sodaHYPE   | [0x6E81124fC5d2Bf666B16a0A5d90066eBf35c7411](https://sonicscan.org/address/0x6E81124fC5d2Bf666B16a0A5d90066eBf35c7411) | HYPE                                                                                 |
| sodaSODA   | [0x21685E341DE7844135329914Be6Bd8D16982d834](https://sonicscan.org/address/0x21685E341DE7844135329914Be6Bd8D16982d834) | SODA                                                                                 |
| sodaLL     | [0x14C5eB2D25dFb834852dFc85744875d1eCb09748](https://sonicscan.org/address/0x14C5eB2D25dFb834852dFc85744875d1eCb09748) | LL                                                                                   |
| sodaRBNT   | [0x4B207114F9118dEAC56436e1aE3c45648783c7Ac](https://sonicscan.org/address/0x4B207114F9118dEAC56436e1aE3c45648783c7Ac) | RBNT                                                                                 |
| sodaWEETH  | [0xCb6B152D3a943f25157381aFcA7fEFCD2ef5a357](https://sonicscan.org/address/0xCb6B152D3a943f25157381aFcA7fEFCD2ef5a357) | weETH                                                                                |
| sodaWSTETH | [0x58b0538D7EEaeE69EF32f9F1dE5cbF32A10a977B](https://sonicscan.org/address/0x58b0538D7EEaeE69EF32f9F1dE5cbF32A10a977B) | wstETH                                                                               |
| sodaNEAR   | [0xf4ba497c9b805e4bd88a8a9e6a7b8f74984c3e39](https://sonicscan.org/address/0xf4ba497c9b805e4bd88a8a9e6a7b8f74984c3e39) | NEAR                                                                                 |
| sodaKAIA   | [0xD7d41b5f803b6A40F8A6eAa34E459A4564e39891](https://sonicscan.org/address/0xD7d41b5f803b6A40F8A6eAa34E459A4564e39891) | KAIA                                                                                 |
| sodaSUSDS  | [0x243b0c26c8b38793908d7C64e8510f21B19B4613](https://sonicscan.org/address/0x243b0c26c8b38793908d7C64e8510f21B19B4613) | sUSDS                                                                                |
| sodaSTX    | [0x1Fbe5229e9d189F26bEE77E5bFa24309FdA90483](https://sonicscan.org/address/0x1Fbe5229e9d189F26bEE77E5bFa24309FdA90483) | STX                                                                                  |
| sodastfUSD | [0x0902B2Bc326AB373bE4fE20605690b0422998685](https://sonicscan.org/address/0x0902B2Bc326AB373bE4fE20605690b0422998685) | stfUSD                                                                               |
| sodaHBAR   | [0x3BB956cc8922E1Ba4148dc10eD1b4Fa19aa599c4](https://sonicscan.org/address/0x3BB956cc8922E1Ba4148dc10eD1b4Fa19aa599c4) | HBAR                                                                                 |
| sodaUSSD   | [0xb780e09576C2667ba9F5B80FbAb2e6b8A0a21e37](https://sonicscan.org/address/0xb780e09576C2667ba9F5B80FbAb2e6b8A0a21e37) | USSD                                                                                 |
| sodaUSDS   | [0xA3AeFa2BAfEAB479c4Aca6024A16906bbC75566e](https://sonicscan.org/address/0xA3AeFa2BAfEAB479c4Aca6024A16906bbC75566e) | USDS                                                                                 |

## Spokes

### Sonic

**Assets**

| Token             | Address                                                                                                                |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| wETH(18)          | [0x50c42dEAcD8Fc9773493ED674b675bE577f2634b](https://sonicscan.org/address/0x50c42dEAcD8Fc9773493ED674b675bE577f2634b) |
| USDC(6)           | [0x29219dd400f2Bf60E5a23d13Be72B486D4038894](https://sonicscan.org/address/0x29219dd400f2Bf60E5a23d13Be72B486D4038894) |
| USDT(6)           | [0x6047828dc181963ba44974801ff68e538da5eaf9](https://sonicscan.org/address/0x6047828dc181963ba44974801ff68e538da5eaf9) |
| Wrapped Sonic(18) | [0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38](https://sonicscan.org/address/0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38) |
| Soda(18)          | [0x7c7d53eecda37a87ce0d5bf8e0b24512a48dc963](https://sonicscan.org/address/0x7c7d53eecda37a87ce0d5bf8e0b24512a48dc963) |
| USSD(18)          | [0x000000000eCcFf26B795F73fb0A70d48da657fEf](https://sonicscan.org/address/0x000000000eCcFf26B795F73fb0A70d48da657fEf) |

### Ethereum

| Contract        | Address                                                                                                               |
| --------------- | --------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4](https://etherscan.io/address/0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4) |
| RateLimit       | [0xB25508092CF56EaF32c6919A05d07016daC40a70](https://etherscan.io/address/0xB25508092CF56EaF32c6919A05d07016daC40a70) |
| Balanced Dollar | [0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13](https://etherscan.io/address/0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13) |
| Intent Filler   | [0xF8D13cAcb8E2B6BA8396DbA35a7365EF6b603cd6](https://etherscan.io/address/0xF8D13cAcb8E2B6BA8396DbA35a7365EF6b603cd6) |

**Assets**

| Token                                                                               | Address                                                                                                                |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ETH(18)                                                                             | [0xaeafa26e43f46cd83efe89b1e57c858eb5685a24](https://sonicscan.org/address/0xaeafa26e43f46cd83efe89b1e57c858eb5685a24) |
| [USDC(6)](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48)    | [0x46bd0ce9b2b455ac4377cd142ecb8b719715197d](https://sonicscan.org/address/0x46bd0ce9b2b455ac4377cd142ecb8b719715197d) |
| [bnUSD(18)](https://etherscan.io/token/0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13)  | [0x13df82eb4c6b7d4bb85669d227e6d24342e4f588](https://sonicscan.org/address/0x13df82eb4c6b7d4bb85669d227e6d24342e4f588) |
| [SODA(18)](https://etherscan.io/token/0x4A1C82744cDDeE675A255fB289Cb0917A482e7C7)   | [0x12affee59ceb8be6788a25f9b36149a717795a51](https://sonicscan.org/address/0x12affee59ceb8be6788a25f9b36149a717795a51) |
| [LL(18)](https://etherscan.io/token/0x0921799CB1d702148131024d18fCdE022129Dc73)     | [0xee6236c791db0755c9bc333b4c7c85ab754f2a0a](https://sonicscan.org/address/0xee6236c791db0755c9bc333b4c7c85ab754f2a0a) |
| [wstETH(18)](https://etherscan.io/token/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0) | [0xa2b668c577ab44301ebb820cb29c2a233d1607ab](https://sonicscan.org/address/0xa2b668c577ab44301ebb820cb29c2a233d1607ab) |
| [weETH(18)](https://etherscan.io/token/0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee)  | [0x37a3e4ae512b7132c139643a5ac4b7148997d0e8](https://sonicscan.org/address/0x37a3e4ae512b7132c139643a5ac4b7148997d0e8) |
| [USDT(6)](https://etherscan.io/token/0xdAC17F958D2ee523a2206206994597C13D831ec7)    | [0xd1a1c0ae27f661887fe76b0fd609eb2f6846a8b3](https://sonicscan.org/address/0xd1a1c0ae27f661887fe76b0fd609eb2f6846a8b3) |
| [WBTC(8)](https://etherscan.io/token/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599)    | [0x4ccbe4c2cf2aeed19314790622efd71dc0b67acb](https://sonicscan.org/address/0x4ccbe4c2cf2aeed19314790622efd71dc0b67acb) |

### Avalanche

| Contract        | Address                                                                                                               |
| --------------- | --------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x5bDD1E1C5173F4c912cC919742FB94A55ECfaf86](https://snowtrace.io/address/0x5bDD1E1C5173F4c912cC919742FB94A55ECfaf86) |
| RateLimit       | [0xc3F44F5CBCE27E19F02e66D0584B24767Bc6c8BE](https://snowtrace.io/address/0xc3F44F5CBCE27E19F02e66D0584B24767Bc6c8BE) |
| Balanced Dollar | [0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F](https://snowtrace.io/address/0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F) |
| Intent Filler   | [0xd891190A2382c75e3091Bbff3a119e4C578E8ebb](https://snowtrace.io/address/0xd891190A2382c75e3091Bbff3a119e4C578E8ebb) |

**Assets**

| Token                                                                              | Address                                                                                                                |
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| AVAX(18)                                                                           | [0xc9e4f0B6195F389D9d2b639f2878B7674eB9D8cD](https://sonicscan.org/address/0xc9e4f0B6195F389D9d2b639f2878B7674eB9D8cD) |
| [USDT(6)](https://snowtrace.io/token/0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7)   | [0x41Fd5c169e014e2A657B9de3553f7a7b735Fe47A](https://sonicscan.org/address/0x41Fd5c169e014e2A657B9de3553f7a7b735Fe47A) |
| [USDC(6)](https://snowtrace.io/token/0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E)   | [0x41abF4B1559FF709Ef8150079BcB26DB1Fffd117](https://sonicscan.org/address/0x41abF4B1559FF709Ef8150079BcB26DB1Fffd117) |
| [bnUSD(18)](https://snowtrace.io/token/0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F) | [0x289cDa1043b4Ce26BDCa3c12E534f56b24308A5B](https://sonicscan.org/address/0x289cDa1043b4Ce26BDCa3c12E534f56b24308A5B) |
| [Soda(18)](https://snowtrace.io/token/0x390ceed555905ec225Da330A188EA04e85570f00)  | [0xf51d7082375cdca8c19c74e1a0c77da482afda4e](https://sonicscan.org/address/0xf51d7082375cdca8c19c74e1a0c77da482afda4e) |

### BASE

| Contract        | Address                                                                                                               |
| --------------- | --------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3](https://basescan.org/address/0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3) |
| RateLimit       | [0x674A76BE36eE36f201df3F918fe0d30AC969FFaB](https://basescan.org/address/0x674A76BE36eE36f201df3F918fe0d30AC969FFaB) |
| Balanced Dollar | [0xAcfab3F31C0a18559D78556BBf297EC29c6cf8aa](https://basescan.org/address/0xAcfab3F31C0a18559D78556BBf297EC29c6cf8aa) |
| Intent Filler   | [0xDDF645F33eDAD18fC23E01416eD0267A1bF59D45](https://basescan.org/address/0xDDF645F33eDAD18fC23E01416eD0267A1bF59D45) |

**Assets**

| Token                                                                                 | Address                                                                                                                |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ETH(18)                                                                               | [0x70178089842be7f8e4726b33f0d1569db8021faa](https://sonicscan.org/address/0x70178089842be7f8e4726b33f0d1569db8021faa) |
| [bnUSD(18)](https://basescan.org/address/0xAcfab3F31C0a18559D78556BBf297EC29c6cf8aa)  | [0xDF5639D91359866f266b56D60d98edE9fEEDd100](https://sonicscan.org/address/0xDF5639D91359866f266b56D60d98edE9fEEDd100) |
| [Soda(18)](https://basescan.org/address/0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3)   | [0x17ff8ad5ebe6ca8b15751067cd0c89f0e580cd17](https://sonicscan.org/address/0x17ff8ad5ebe6ca8b15751067cd0c89f0e580cd17) |
| [weETH(18)](https://basescan.org/address/0x04c0599ae5a44757c0af6f9ec3b93da8976c150a)  | [0x55e0Ad45eB97493B3045eEE417fb6726CB85dfd4](https://sonicscan.org/address/0x55e0Ad45eB97493B3045eEE417fb6726CB85dfd4) |
| [USDC(6)](https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)    | [0x72E852545B024ddCbc5b70C1bCBDAA025164259C](https://sonicscan.org/address/0x72E852545B024ddCbc5b70C1bCBDAA025164259C) |
| [wstETH(18)](https://basescan.org/address/0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452) | [0x494aaEaEfDF5964d4Ed400174e8c5b98C00957aA](https://sonicscan.org/address/0x494aaEaEfDF5964d4Ed400174e8c5b98C00957aA) |
| [cbBTC(8)](https://basescan.org/address/0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf)   | [0x2803a23a3BA6b09e57D1c71deC0D9eFdBB00A27F](https://sonicscan.org/address/0x2803a23a3BA6b09e57D1c71deC0D9eFdBB00A27F) |
| [USDT(6)](https://basescan.org/token/0xfde4c96c8593536e31f229ea8f37b2ada2699bb2)      | [0x23889bd0e8e718d47a72fdd8b82da39ec856240f](https://sonicscan.org/address/0x23889bd0e8e718d47a72fdd8b82da39ec856240f) |
| [AERO(18)](https://basescan.org/token/0x940181a94A35A4569E4529A3CDfB74e38FD98631)     | [0x348e086ab6f424dfc575a325c28624ed23973173](https://sonicscan.org/address/0x348e086ab6f424dfc575a325c28624ed23973173) |

### Optimism

| Contract        | Address                                                                                                                          |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3](https://optimistic.etherscan.io/address/0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3) |
| RateLimit       | [0x3919f86cB8dE6c5b9d8c6B2be6361D6b9029038C](https://optimistic.etherscan.io/address/0x3919f86cB8dE6c5b9d8c6B2be6361D6b9029038C) |
| Balanced Dollar | [0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8](https://optimistic.etherscan.io/address/0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8) |
| Intent Filler   | [0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3](https://optimistic.etherscan.io/address/0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3) |

**Assets**

| Token                                                                                            | Address                                                                                                                |
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| ETH(18)                                                                                          | [0xad332860dd3b6f0e63f4f66e9457900917ac78cd](https://sonicscan.org/address/0xad332860dd3b6f0e63f4f66e9457900917ac78cd) |
| [bnUSD(18)](https://optimistic.etherscan.io/address/0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8)  | [0x238384AE2b4F0EC189ecB5031859bA306B2679c5](https://sonicscan.org/address/0x238384AE2b4F0EC189ecB5031859bA306B2679c5) |
| [Soda(18)](https://optimistic.etherscan.io/address/0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13)   | [0x0ed0d274dc77ef460dc96b9fbaff3edb074e0471](https://sonicscan.org/address/0x0ed0d274dc77ef460dc96b9fbaff3edb074e0471) |
| [USDC(6)](https://optimistic.etherscan.io/address/0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85)    | [0xb7C213CbD24967dE9838fa014668FDDB338f724B](https://sonicscan.org/address/0xb7C213CbD24967dE9838fa014668FDDB338f724B) |
| [wstETH(18)](https://optimistic.etherscan.io/address/0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb) | [0x61e26f611090CdC6bc79A7Bf156b0fD10f1fC212](https://sonicscan.org/address/0x61e26f611090CdC6bc79A7Bf156b0fD10f1fC212) |
| [weETH(18)](https://optimistic.etherscan.io/address/0x5A7fACB970D094B6C7FF1df0eA68D99E6e73CBFF)  | [0xE121c0Dc2B33c00ff31ee3D902D248cc3f19Ea50](https://sonicscan.org/address/0xE121c0Dc2B33c00ff31ee3D902D248cc3f19Ea50) |
| [USDT(6)](https://optimistic.etherscan.io/address/0x94b008aA00579c1307B0EF2c499aD98a8ce58e58)    | [0xc168067d95109003805aC865ae556e8476DC69bc](https://sonicscan.org/address/0xc168067d95109003805aC865ae556e8476DC69bc) |

### Arbitrum

| Contract        | Address                                                                                                              |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3](https://arbiscan.io/address/0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3) |
| RateLimit       | [0x3919f86cB8dE6c5b9d8c6B2be6361D6b9029038C](https://arbiscan.io/address/0x3919f86cB8dE6c5b9d8c6B2be6361D6b9029038C) |
| Balanced Dollar | [0xA256dd181C3f6E5eC68C6869f5D50a712d47212e](https://arbiscan.io/address/0xA256dd181C3f6E5eC68C6869f5D50a712d47212e) |
| Intent Filler   | [0x4A1C82744cDDeE675A255fB289Cb0917A482e7C7](https://arbiscan.io/address/0x4A1C82744cDDeE675A255fB289Cb0917A482e7C7) |

**Assets**

| Token                                                                                  | Address                                                                                                                |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ETH(18)                                                                                | [0xdcd9578b51ef55239b6e68629d822a8d97c95b86](https://sonicscan.org/address/0xdcd9578b51ef55239b6e68629d822a8d97c95b86) |
| [bnUSD(18)](https://arbiscan.io/address/0xA256dd181C3f6E5eC68C6869f5D50a712d47212e)    | [0x419cA9054E44E94ceAb52846eCdC3997439BBcA6](https://sonicscan.org/address/0x419cA9054E44E94ceAb52846eCdC3997439BBcA6) |
| [Soda(18)](https://arbiscan.io/address/0x5bda87f18109CA85fa7ADDf1D48B97734e9dc6F5)     | [0x66be65dd6d2401c179a2f7ab2d5cdfb040567727](https://sonicscan.org/address/0x93a367e5b37a1b9e8d04ef25a6af40d181a3dfff) |
| [wBTC(8)](https://arbiscan.io/address/0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f)      | [0xfB0ACB1b2720B620935F50a6dd3F7FEA52b2FCBe](https://sonicscan.org/address/0xfB0ACB1b2720B620935F50a6dd3F7FEA52b2FCBe) |
| [weETH(18)](https://arbiscan.io/address/0x35751007a407ca6FEFfE80b3cB397736D2cf4dbe)    | [0x08D5cf039De35627fD5C0f48B8AF4a1647a462E8](https://sonicscan.org/address/0x08D5cf039De35627fD5C0f48B8AF4a1647a462E8) |
| [wstETH(18)](https://arbiscan.io/address/0x5979D7b546E38E414F7E9822514be443A4800529)   | [0x2D5A7837D68b0c2CC4b14C2af2a1F0Ef420DDDc5](https://sonicscan.org/address/0x2D5A7837D68b0c2CC4b14C2af2a1F0Ef420DDDc5) |
| [tBTC(18)](https://arbiscan.io/address/0x6c84a8f1c29108F47a79964b5Fe888D4f4D0dE40)     | [0x96Fc8540736f1598b7E235e6dE8814062b3b5d3B](https://sonicscan.org/address/0x96Fc8540736f1598b7E235e6dE8814062b3b5d3B) |
| [USDT(6)](https://arbiscan.io/address/0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9)      | [0x3C0a80C6a1110fC80309382b3989eC626c135eE9](https://sonicscan.org/address/0x3C0a80C6a1110fC80309382b3989eC626c135eE9) |
| [USDC(6)](https://arbiscan.io/address/0xaf88d065e77c8cC2239327C5EDb3A432268e5831)      | [0xdB7BdA65c3a1C51D64dC4444e418684677334109](https://sonicscan.org/address/0xdB7BdA65c3a1C51D64dC4444e418684677334109) |
| [Arbitrum(18)](https://arbiscan.io/address/0x912CE59144191C1204E64559FE8253a0e49E6548) | [0xe93d74056c7e431aaa33155c9c994116c562e5e2](https://sonicscan.org/address/0xe93d74056c7e431aaa33155c9c994116c562e5e2) |
| [SUSDS(18)](https://arbiscan.io/address/0xddb46999f8891663a8f2828d25298f70416d7610)    | [0x8794A87979efA70bBE29Ed3357e9A93932B48675](https://sonicscan.org/address/0x8794A87979efA70bBE29Ed3357e9A93932B48675) |

### Polygon

| Contract        | Address                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------ |
| AssetManager    | [0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3](https://polygonscan.com/address/0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3) |
| RateLimit       | [0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F](https://polygonscan.com/address/0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F) |
| Balanced Dollar | [0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4](https://polygonscan.com/address/0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4) |
| Intent Filler   | [0x772FFE538E45b2cDdFB5823041EC26C44815B9AB](https://polygonscan.com/address/0x772FFE538E45b2cDdFB5823041EC26C44815B9AB) |

**Assets**

| Token                                                                                   | Address                                                                                                                |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| POL(18)                                                                                 | [0x9ee17486571917837210824b0d4cadfe3b324d12](https://sonicscan.org/address/0x9ee17486571917837210824b0d4cadfe3b324d12) |
| [bnUSD(18)](https://polygonscan.com/address/0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4) | [0x18f85f9E80ff9496EeBD5979a051AF16Ce751567](https://sonicscan.org/address/0x18f85f9E80ff9496EeBD5979a051AF16Ce751567) |
| [Soda(18)](https://polygonscan.com/address/0xDDF645F33eDAD18fC23E01416eD0267A1bF59D45)  | [0x8d78a620e009ba751eb40d77a5e9db48a3f2016b](https://sonicscan.org/address/0x8d78a620e009ba751eb40d77a5e9db48a3f2016b) |
| [USDC(6)](https://polygonscan.com/address/0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359)   | [0xa36893ba308b332FDEbfa95916D1dF3a2e3CF8B3](https://sonicscan.org/address/0xa36893ba308b332FDEbfa95916D1dF3a2e3CF8B3) |
| [wstETH(18)](https://polygonscan.com/token/0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd)  | [0xa95e972dbff57f3b561763ae88fd5f235a8f2711](https://sonicscan.org/address/0xa95e972dbff57f3b561763ae88fd5f235a8f2711) |
| [USDT(6)](https://polygonscan.com/token/0xc2132d05d31c914a87c6611c10748aeb04b58e8f)     | [0xc5dfa58a0f80646d1067d894920acd870ff1888b](https://sonicscan.org/address/0xc5dfa58a0f80646d1067d894920acd870ff1888b) |

### Nibiru

| Contract        | Address                                                                                                              |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F](https://nibiscan.io/address/0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F) |
| RateLimit       | [0xFA5859156bce2617D7152DbD8108fd81eCb96C2A](https://nibiscan.io/address/0xFA5859156bce2617D7152DbD8108fd81eCb96C2A) |
| Balanced Dollar | [0x043fb7e23350Dd5b77dE5E228B528763DEcb9131](https://nibiscan.io/address/0x043fb7e23350Dd5b77dE5E228B528763DEcb9131) |
| Intent Fillers  | [0x85c0c9669460822E40335Ab1c67606F9141ee1ef](https://nibiscan.io/address/0x85c0c9669460822E40335Ab1c67606F9141ee1ef) |

**Assets**

| Token                                                                               | Address                                                                                                                |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| NIBI(18)                                                                            | [0xe0064414c2c1a636a9424C7a17D86fbF7FD3f190](https://sonicscan.org/address/0xe0064414c2c1a636a9424C7a17D86fbF7FD3f190) |
| [bnUSD(18)](https://nibiscan.io/address/0x043fb7e23350Dd5b77dE5E228B528763DEcb9131) | [0x11b93C162aABFfD026539bb3B9F9eC22c8b7ef8a](https://sonicscan.org/address/0x11b93C162aABFfD026539bb3B9F9eC22c8b7ef8a) |
| [Soda(18)](https://nibiscan.io/address/0x5bda87f18109CA85fa7ADDf1D48B97734e9dc6F5)  | [0x26cd76cb5622dc8638670a16e0da5a51394a8db1](https://sonicscan.org/address/0x26cd76cb5622dc8638670a16e0da5a51394a8db1) |
| [USDC(6)](https://nibiscan.io/address/0x0829F361A05D993d5CEb035cA6DF3446b060970b)   | [0x3d73437dd81b3F9EC82752Beb1752f03a8531710](https://sonicscan.org/address/0x3d73437dd81b3F9EC82752Beb1752f03a8531710) |

### Botanix

| Contract        | Address                                                                                                                 |
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0xA8f30Fd43c7537Dfd175CD117BC2880aB964C192](https://botanixscan.io/address/0xA8f30Fd43c7537Dfd175CD117BC2880aB964C192) |
| RateLimit       | [0xDDF645F33eDAD18fC23E01416eD0267A1bF59D45](https://botanixscan.io/address/0xDDF645F33eDAD18fC23E01416eD0267A1bF59D45) |
| Balanced Dollar | [0xb5239140745067502ee35B4E2dC2869418Db8309](https://botanixscan.io/address/0xb5239140745067502ee35B4E2dC2869418Db8309) |
| Intent Fillers  | [0x79F9B344bB64aF1E89B0C3d20ded4f299F4ff262](https://botanixscan.io/address/0x79F9B344bB64aF1E89B0C3d20ded4f299F4ff262) |

**Assets**

| Token                                                                                  | Address                                                                                                                |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| BTC\_Native(18)                                                                        | [0x03e99853c6376b13a4c6e4d0a115f1639c9fa14e](https://sonicscan.org/address/0x03e99853c6376b13a4c6e4d0a115f1639c9fa14e) |
| [bnUSD(18)](https://botanixscan.io/address/0xb5239140745067502ee35B4E2dC2869418Db8309) | [0xd1d14bf3324c901855a1f7d0d5ca4c8458d2a780](https://sonicscan.org/address/0xd1d14bf3324c901855a1f7d0d5ca4c8458d2a780) |
| [Soda(18)](https://botanixscan.io/address/0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F)  | [0x655730024B673B3378CD6031B1Cd01eaE9afb138](https://sonicscan.org/address/0x655730024B673B3378CD6031B1Cd01eaE9afb138) |
| [USDC(6)](https://botanixscan.io/address/0x29eE6138DD4C9815f46D34a4A1ed48F46758A402)   | [0xc1df02fb7b1b06be886592c89f6955387998b2f7](https://sonicscan.org/address/0xc1df02fb7b1b06be886592c89f6955387998b2f7) |

### Kaia

#### Core Contracts

| Contract          | Address                                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Connection        | [0x4555aC13D7338D9E671584C1D118c06B2a3C88eD](https://www.kaiascan.io/address/0x4555aC13D7338D9E671584C1D118c06B2a3C88eD) |
| RateLimit         | [0xA143488cDc5B74B366231E6A4d5a55A2D9Dc8484](https://www.kaiascan.io/address/0xA143488cDc5B74B366231E6A4d5a55A2D9Dc8484) |
| SpokeAssetManager | [0x6D2126DB97dd88AfA85127253807D04A066b6746](https://www.kaiascan.io/address/0x6D2126DB97dd88AfA85127253807D04A066b6746) |
| SolverRouter      | [0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4](https://www.kaiascan.io/address/0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4) |
| IntentFiller      | [0x3cd95C469be0EDFD12Bd4F3a4436B132B7908DF4](https://www.kaiascan.io/address/0x3cd95C469be0EDFD12Bd4F3a4436B132B7908DF4) |

#### Assets

| Token                                                                                  | Asset Address                                                                                                          |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [bnUSD (18)](https://www.kaiascan.io/token/0xF8D13cAcb8E2B6BA8396DbA35a7365EF6b603cd6) | [0x60cc9bd812bec2fb65242e9cd0a3ead4a8bf7488](https://sonicscan.org/address/0x60cc9bd812bec2fb65242e9cd0a3ead4a8bf7488) |
| [SODA (18)](https://www.kaiascan.io/token/0x772FFE538E45b2cDdFB5823041EC26C44815B9AB)  | [0x7c5a1f6d39befef7dd2b2be38dd98d323d42ab8d](https://sonicscan.org/address/0x7c5a1f6d39befef7dd2b2be38dd98d323d42ab8d) |
| Kaia (Native)                                                                          | [0x1950c088bc12398240224808167a166f220c8d0e](https://sonicscan.org/address/0x1950c088bc12398240224808167a166f220c8d0e) |
| [USDT (6)](https://www.kaiascan.io/token/0xd077a400968890eacc75cdc901f0356c943e4fdb)   | [0x99ecd19afce86766a417956971cdc5e026284454](https://sonicscan.org/address/0x99ecd19afce86766a417956971cdc5e026284454) |

### BSC

| Contract        | Address                                                                                                              |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3](https://bscscan.com/address/0x348BE44F63A458be9C1b13D6fD8e99048F297Bc3) |
| RateLimit       | [0x36134A03dcD03Bbe858B8F7ED28a71AAC608F9E7](https://bscscan.com/address/0x36134A03dcD03Bbe858B8F7ED28a71AAC608F9E7) |
| Balanced Dollar | [0x8428FedC020737a5A2291F46cB1B80613eD71638](https://bscscan.com/address/0x8428FedC020737a5A2291F46cB1B80613eD71638) |
| Intent Filler   | [0x1D6182aaB371D1B9619198A3c553D14c27140559](https://bscscan.com/address/0x1D6182aaB371D1B9619198A3c553D14c27140559) |

**Assets**

| Token                                                                               | Address                                                                                                                |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| BNB(18)                                                                             | [0x13b70564b1ec12876b20fab5d1bb630311312f4f](https://sonicscan.org/address/0x13b70564b1ec12876B20FAb5D1Bb630311312f4f) |
| [ETHB(18)](https://bscscan.com/address/0x2170Ed0880ac9A755fd29B2688956BD959F933F8)  | [0x57fC2aC5701e463ae261AdBd6C99FBeB48Ce5293](https://sonicscan.org/address/0x57fC2aC5701e463ae261AdBd6C99FBeB48Ce5293) |
| [BTCB(18)](https://bscscan.com/address/0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c)  | [0xD8A24c71FEa5bB81c66C01e532dE7d9B11e13905](https://sonicscan.org/address/0xD8A24c71FEa5bB81c66C01e532dE7d9B11e13905) |
| [bnUSD(18)](https://bscscan.com/address/0x8428FedC020737a5A2291F46cB1B80613eD71638) | [0x5Ce6C1c51ff762cF3acD21396257046f694168b6](https://sonicscan.org/address/0x5Ce6C1c51ff762cF3acD21396257046f694168b6) |
| [Soda(18)](https://bscscan.com/address/0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3)  | [0x4d12c72a8633588097d10e57b559ed642588e4c6](https://sonicscan.org/address/0x4d12c72a8633588097d10e57b559ed642588e4c6) |
| [USDC(18)](https://bscscan.com/address/0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d)  | [0x9D58508AD10d34048a11640735Ca5075bbA07b35](https://sonicscan.org/address/0x9D58508AD10d34048a11640735Ca5075bbA07b35) |
| [weETH(18)](https://bscscan.com/token/0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A)   | [0xc1a14e759e8c2a8128a1fe0288c12390fbaee6d2](https://sonicscan.org/address/0xc1a14e759e8c2a8128a1fe0288c12390fbaee6d2) |
| [BSC-USD(18)](https://bscscan.com/token/0x55d398326f99059ff775485246999027b3197955) | [0x627c7369e9cfb99e11ee7163efb23f12bfd8cce6](https://sonicscan.org/address/0x627c7369e9cfb99e11ee7163efb23f12bfd8cce6) |

### HyperEvm

| Contract        | Address                                                                                                              |
| --------------- | -------------------------------------------------------------------------------------------------------------------- |
| AssetManager    | [0xAfd6A6e4287A511D3BAAd013093815268846FBb7](https://purrsec.com/address/0xAfd6A6e4287A511D3BAAd013093815268846FBb7) |
| RateLimit       | [0xA6934BCeD5351D7465a2A6Cd13c1936E8d590988](https://purrsec.com/address/0xA6934BCeD5351D7465a2A6Cd13c1936E8d590988) |
| Balanced Dollar | [0x506ba7c8d91dadf7a91ee677a205d9687b751579](https://purrsec.com/address/0x506ba7c8d91dadf7a91ee677a205d9687b751579) |
| Intent Filler   | [0x1F3d6180eed6A46650D0cFB416874bEABf695457](https://purrsec.com/address/0x1F3d6180eed6A46650D0cFB416874bEABf695457) |

**Assets**

| Token                                                                               | Address                                                                                                                |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| HYPE(18)                                                                            | [0x7288622bc2D39553f34D5b81c88c3F979d91dbc7](https://sonicscan.org/address/0x7288622bc2D39553f34D5b81c88c3F979d91dbc7) |
| [bnUSD(18)](https://purrsec.com/address/0x506ba7c8d91dadf7a91ee677a205d9687b751579) | [0x19feAf3043DFA69b365D05495630E840A2b9a9dC](https://sonicscan.org/address/0x19feAf3043DFA69b365D05495630E840A2b9a9dC) |
| [Soda(18)](https://purrsec.com/address/0xA28C70F92a1B2513edCdDD29c2E5195a4B785aB2)  | [0x4cf5ce9594aeddc5d3efe9d4cdf0b944b4e73a53](https://sonicscan.org/address/0x4cf5ce9594aeddc5d3efe9d4cdf0b944b4e73a53) |
| [USDC(6)](https://purrsec.com/address/0xb88339CB7199b77E23DB6E890353E22632Ba630f)   | [0x0f78b995d113712deeb17d96638e9d7525d409c6](https://sonicscan.org/address/0x0f78b995d113712deeb17d96638e9d7525d409c6) |
| [USDT(6)](https://purrsec.com/address/0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb)   | [0x87f73744d919447e6f8220f268bcbbfa0cbfafa3](https://sonicscan.org/address/0x87f73744d919447e6f8220f268bcbbfa0cbfafa3) |

### Hedera

#### Core Contracts

| Contract          | Address                                                                                                                       |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Connection        | [0x4555aC13D7338D9E671584C1D118c06B2a3C88eD](https://hashscan.io/mainnet/contract/0x4555aC13D7338D9E671584C1D118c06B2a3C88eD) |
| RateLimit         | [0x6d2126db97dd88afa85127253807d04a066b6746](https://hashscan.io/mainnet/contract/0x6d2126db97dd88afa85127253807d04a066b6746) |
| SpokeAssetManager | [0x0df73542cc68bdc01b361d231c60f726b0e0bc05](https://hashscan.io/mainnet/contract/0x0df73542cc68bdc01b361d231c60f726b0e0bc05) |

#### Assets

| Token                                                       | Hedera Address                                                                                                                | Asset Address                                                                                                          |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| HBAR (Native, 8)                                            | —                                                                                                                             | [0x5c18c543b6B6EA97dE739F48C49CfC291B3AD465](https://sonicscan.org/address/0x5c18c543b6B6EA97dE739F48C49CfC291B3AD465) |
| [bnUSD (8)](https://hashscan.io/mainnet/token/0.0.10496106) | [0x0000000000000000000000000000000000a0286a](https://hashscan.io/mainnet/contract/0x0000000000000000000000000000000000a0286a) | [0x44be1984cd279334a630469fa357305c7dba2837](https://sonicscan.org/address/0x44be1984cd279334a630469fa357305c7dba2837) |
| [SODA (8)](https://hashscan.io/mainnet/token/0.0.10496105)  | [0x0000000000000000000000000000000000a02869](https://hashscan.io/mainnet/contract/0x0000000000000000000000000000000000a02869) | [0x1217721376839dbffe78093ddd5d8e50d0239b3f](https://sonicscan.org/address/0x1217721376839dbffe78093ddd5d8e50d0239b3f) |
| [USDC (6)](https://hashscan.io/mainnet/token/0.0.456858)    | [0x000000000000000000000000000000000006f89a](https://hashscan.io/mainnet/contract/0x000000000000000000000000000000000006f89a) | [0xafafae0c1476424c4b81f09095bd7dcb858047c8](https://sonicscan.org/address/0xafafae0c1476424c4b81f09095bd7dcb858047c8) |

### Lightlink

| Contract     | Address                                                                                                                     |
| ------------ | --------------------------------------------------------------------------------------------------------------------------- |
| AssetManager | [0x4A1C82744cDDeE675A255fB289Cb0917A482e7C7](https://phoenix.lightlink.io/token/0x4A1C82744cDDeE675A255fB289Cb0917A482e7C7) |
| RateLimit    | [0xb5239140745067502ee35B4E2dC2869418Db8309](https://phoenix.lightlink.io/token/0xb5239140745067502ee35B4E2dC2869418Db8309) |
| IntentFiller | [0x043fb7e23350Dd5b77dE5E228B528763DEcb9131](https://phoenix.lightlink.io/token/0x043fb7e23350Dd5b77dE5E228B528763DEcb9131) |
| Bnusd        | [0x36134A03dcD03Bbe858B8F7ED28a71AAC608F9E7](https://phoenix.lightlink.io/token/0x36134A03dcD03Bbe858B8F7ED28a71AAC608F9E7) |

**Assets**

| Token                                                                                           | Address                                                                                                                |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Native (ETH 18)                                                                                 | [0x19920ef8fe1a9d51fdb0914abbb2f970c74dca68](https://sonicscan.org/address/0x19920ef8fe1a9d51fdb0914abbb2f970c74dca68) |
| [LightLink (18)](https://phoenix.lightlink.io/token/0x519d3443cACc61bD844546eDAea48E5502021802) | [0x547845f26a7ea68a4202387a536071ff320c1f38](https://sonicscan.org/address/0x547845f26a7ea68a4202387a536071ff320c1f38) |
| [Bnusd (18)](https://phoenix.lightlink.io/token/0x36134A03dcD03Bbe858B8F7ED28a71AAC608F9E7)     | [0x14ab2ab7a76838b9205488efc3f700d0632ce8c7](https://sonicscan.org/address/0x14ab2ab7a76838b9205488efc3f700d0632ce8c7) |
| [USDC (6)](https://phoenix.lightlink.io/token/0xbCF8C1B03bBDDA88D579330BDF236B58F8bb2cFd)       | [0xe5159e2a89aeec2fc63d6c25adda3541670af101](https://sonicscan.org/address/0xe5159e2a89aeec2fc63d6c25adda3541670af101) |
| [USDT (6)](https://phoenix.lightlink.io/token/0x6308fa9545126237158778e74AE1b6b89022C5c0)       | [0x46b95b9c77bfbd0fe647091ccdf5dccf279ec149](https://sonicscan.org/address/0x46b95b9c77bfbd0fe647091ccdf5dccf279ec149) |
| [SODA (18)](https://phoenix.lightlink.io/token/0x6BC8C37cba91F76E68C9e6d689A9C21E4d32079B)      | [0xe4faab621fb6716e32057b7ea7356219936519ac](https://sonicscan.org/address/0xe4faab621fb6716e32057b7ea7356219936519ac) |
| [BTC.LL (18)](https://phoenix.lightlink.io/token/0x5E921D8B7709b409132628258A53449D1fD82341)    | [0x54582cdb0c145398b532e34f0851ffc1bbc88b4e](https://sonicscan.org/address/0x54582cdb0c145398b532e34f0851ffc1bbc88b4e) |
| [AVAX.LL (18)](https://phoenix.lightlink.io/token/0x373d9c5390535e9e30185E52826d45b76df09aBb)   | [0x415ff3ea8c905ef65cd82665c5534d0e4ff9221a](https://sonicscan.org/address/0x415ff3ea8c905ef65cd82665c5534d0e4ff9221a) |
| [BNB.LL (18)](https://phoenix.lightlink.io/token/0xe80c2B7674dCdF47b199697E2c61730231b8da89)    | [0xd32e524da9d64db4c37a50f1ee197b591e97dc05](https://sonicscan.org/address/0xd32e524da9d64db4c37a50f1ee197b591e97dc05) |
| [SOL.LL (18)](https://phoenix.lightlink.io/token/0xba9af7029Ae5c1054Fc367d5D6a47Dc3D5c0D6bA)    | [0xd70c16f1e2c4207dc4c44bc69c8a1b82b50cf9e2](https://sonicscan.org/address/0xd70c16f1e2c4207dc4c44bc69c8a1b82b50cf9e2) |
| [XLM.LL (18)](https://phoenix.lightlink.io/token/0x03beDD719b6d8de11f5B2671b2f30085a626F8Da)    | [0x18008b18964bc2c90521a7596ff9988743c97fd4](https://sonicscan.org/address/0x18008b18964bc2c90521a7596ff9988743c97fd4) |
| [INJ.LL (18)](https://phoenix.lightlink.io/token/0x8a4C8B1A899Fa9D9246a112E8D43EC0C97b77B8C)    | [0xb036daab0c772b080babb84b8695c3ca858925b6](https://sonicscan.org/address/0xb036daab0c772b080babb84b8695c3ca858925b6) |
| [SUI.LL (18)](https://phoenix.lightlink.io/token/0x59e68e2F5147F74F27FD173397a7419C1e5d9999)    | [0x425852cca84e5f44d7b8ebb964b21bde585b4571](https://sonicscan.org/address/0x425852cca84e5f44d7b8ebb964b21bde585b4571) |
| [S.LL (18)](https://phoenix.lightlink.io/token/0xb3A47798CB6585Ea0d31a7986f2a04b25C60247f)      | [0xb592d2631ccf245119532e025d11188cfadb6777](https://sonicscan.org/address/0xb592d2631ccf245119532e025d11188cfadb6777) |
| [POL.LL (18)](https://phoenix.lightlink.io/token/0xE963bfb4757fC8Ae66BC68E11e636f8fbafAfCb4)    | [0x15ba35f87f259b9b50d53a2fec271bb10020b090](https://sonicscan.org/address/0x15ba35f87f259b9b50d53a2fec271bb10020b090) |
| [HYPE.LL (18)](https://phoenix.lightlink.io/token/0x127b64fb645279F8aca786c507b94dde81F02d16)   | [0xea7e97b4597f724844426c32248ba80bd29c75f8](https://sonicscan.org/address/0xea7e97b4597f724844426c32248ba80bd29c75f8) |

### RedBelly Network

| Contract        | Address                                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| AssetManager    | [0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4](https://redbelly.routescan.io/address/0x39E77f86C1B1f3fbAb362A82b49D2E86C09659B4) |
| RateLimit       | [0xB25508092CF56EaF32c6919A05d07016daC40a70](https://redbelly.routescan.io/address/0xB25508092CF56EaF32c6919A05d07016daC40a70) |
| Balanced Dollar | [0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8](https://redbelly.routescan.io/address/0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8) |
| Intent Filler   | [0xF8D13cAcb8E2B6BA8396DbA35a7365EF6b603cd6](https://redbelly.routescan.io/address/0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8) |

**Assets**

| Token                                                                                         | Address                                                                                                                |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| \[RBNT(18)]                                                                                   | [0x4081e0676c5137161b61601756e6524058a9c5c5](https://sonicscan.org/address/0x4081e0676c5137161b61601756e6524058a9c5c5) |
| [bnUSD(18)](https://redbelly.routescan.io/address/0xF4f7dC27c17470a26d0de9039Cf0EA5045F100E8) | [0x650a42ad3017190399b1797b6a7f65240c258e5d](https://sonicscan.org/address/0x650a42ad3017190399b1797b6a7f65240c258e5d) |
| [Soda(18)](https://redbelly.routescan.io/address/0x5034479da62Ec95360165BB45ead266A48519E85)  | [0x855ecace4fe3f9f1e648591af6ba4e9fbd2bf987](https://sonicscan.org/address/0x855ecace4fe3f9f1e648591af6ba4e9fbd2bf987) |
| [USDC(6)](https://redbelly.routescan.io/address/0x8201c02d4AB2214471E8C3AD6475C8b0CD9F2D06)   | [0xedbe83a638da53d0f31f51b9f6b65ea0e8c3b4bf](https://sonicscan.org/address/0xedbe83a638da53d0f31f51b9f6b65ea0e8c3b4bf) |
| [USDT(6)](https://redbelly.routescan.io/address/0x8C4aCd74Ff4385f3B7911432FA6787Aa14406f8B)   | [0x81afe70d3b8266395b9f508c95cfc05ce7e01609](https://sonicscan.org/address/0x81afe70d3b8266395b9f508c95cfc05ce7e01609) |
| [rETH(18)](https://redbelly.routescan.io/address/0xb5239140745067502ee35B4E2dC2869418Db8309)  | [0x87baf5eb9c67a86a5260b7ec739d0c4a4a450ae3](https://sonicscan.org/address/0x87baf5eb9c67a86a5260b7ec739d0c4a4a450ae3) |
| [rBTC(18)](https://redbelly.routescan.io/address/0x79F9B344bB64aF1E89B0C3d20ded4f299F4ff262)  | [0xe88e7117a2f84bad69cb35440617cb4cbf5d39d1](https://sonicscan.org/address/0xe88e7117a2f84bad69cb35440617cb4cbf5d39d1) |
| [rSOL(18)](https://redbelly.routescan.io/address/0x6958a4CBFe11406E2a1c1d3a71A1971aD8B3b92F)  | [0x12cb9f7ea402a75dc0b670097c9c376e778b3480](https://sonicscan.org/address/0x12cb9f7ea402a75dc0b670097c9c376e778b3480) |
| [rBNB(18)](https://redbelly.routescan.io/address/0x674A76BE36eE36f201df3F918fe0d30AC969FFaB)  | [0x1926ea1492bc849865b9dc40576226d0f2ac2865](https://sonicscan.org/address/0x1926ea1492bc849865b9dc40576226d0f2ac2865) |
| [rHYPE(18)](https://redbelly.routescan.io/address/0x30E603F6f4A0642579A9Eb87F39E97C3d63c5185) | [0xc01d5d8fed35e897be5b402c96edb3b6cee50223](https://sonicscan.org/address/0xc01d5d8fed35e897be5b402c96edb3b6cee50223) |
| [rAVAX(18)](https://redbelly.routescan.io/address/0xdEa692287E2cE8Cb08FA52917Be0F16b1DACDC87) | [0x9cb238c213d490d64e9be025e3e9f80c49068b74](https://sonicscan.org/address/0x9cb238c213d490d64e9be025e3e9f80c49068b74) |
| [rXLM(18)](https://redbelly.routescan.io/address/0xD6728c68158F8B1c2DD9C2c075b7Ec14Be82B056)  | [0x38e2866396e5246072450fdde24b67cec6113987](https://sonicscan.org/address/0x38e2866396e5246072450fdde24b67cec6113987) |
| [rSUI(18)](https://redbelly.routescan.io/address/0x5a075872cb75F68668364Fa9295eCFcE9A1114e2)  | [0xa98bf33d2bc3fdf851c3d680db4676bba33b7142](https://sonicscan.org/address/0xa98bf33d2bc3fdf851c3d680db4676bba33b7142) |
| [rS(18)](https://redbelly.routescan.io/address/0xd891190A2382c75e3091Bbff3a119e4C578E8ebb)    | [0x8f3a13d920fe2e0b80fe4981c430e9ee528e80aa](https://sonicscan.org/address/0x8f3a13d920fe2e0b80fe4981c430e9ee528e80aa) |
| [rPOL(18)](https://redbelly.routescan.io/address/0x390ceed555905ec225Da330A188EA04e85570f00)  | [0x3afeb3584649342a967629bb39a135dce2db8ebf](https://sonicscan.org/address/0x3afeb3584649342a967629bb39a135dce2db8ebf) |

### ICON

| Contract     | Address                                                                                                                          |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager | [cx1be33c283c7dc7617181d1b21a6a2309e71b1ee7](https://tracker.icon.community/contract/cx1be33c283c7dc7617181d1b21a6a2309e71b1ee7) |
| RateLimit    | [cxef8e45df75dfd744589f0605ae882c85bb058696](https://tracker.icon.community/contract/cxef8e45df75dfd744589f0605ae882c85bb058696) |

**Assets**

| Token                                                                                                                                                                                       | Address                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| wICX(18)                                                                                                                                                                                    | [0xb66cB7D841272AF6BaA8b8119007EdEE35d2C24F](https://sonicscan.org/address/0xb66cB7D841272AF6BaA8b8119007EdEE35d2C24F) |
| bnUSD(18)                                                                                                                                                                                   | [0x654dddf32a9a2ac53f5fb54bf1e93f66791f8047](https://sonicscan.org/address/0x654dddf32a9a2ac53f5fb54bf1e93f66791f8047) |
| [BALN(18)](https://github.com/icon-project/sodax-document/tree/main/developers/deployments/\(https:/tracker.icon.community/contract/cxf61cd5a45dc9f91c15aa65831a30a90d59a09619\)/README.md) | [0xde8e19a099fedf9d617599f62c5f7f020d92b572](https://sonicscan.org/address/0xde8e19a099fedf9d617599f62c5f7f020d92b572) |
| [OMM(18)](https://tracker.icon.community/contract/cx1a29259a59f463a67bb2ef84398b30ca56b5830a)                                                                                               | [0x730a6899f0153ea13c6910417a9f07f7d0f39a18](https://sonicscan.org/address/0x730a6899f0153ea13c6910417a9f07f7d0f39a18) |

### Injective \[Encoding=utf8]

| Contract      | Address                                                                                                                              |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| AssetManager  | [inj1dg6tm62uup53wn2kn97caeqfwt0sukx3qjk8rw](https://explorer.injective.network/contract/inj1dg6tm62uup53wn2kn97caeqfwt0sukx3qjk8rw) |
| RateLimit     | [inj15ek3c2c60qhtkuqd83ngz9hcrua6vme2t2jxq3](https://explorer.injective.network/contract/inj15ek3c2c60qhtkuqd83ngz9hcrua6vme2t2jxq3) |
| bnUSD         | factory/inj1d036ftaatxpkqsu9hja8r24rv3v33chz3appxp/bnUSD                                                                             |
| Intent Filler | [inj1nra9ju80dmfxs7326qusfxmeas0aeghy3sp4qd](https://explorer.injective.network/contract/inj1nra9ju80dmfxs7326qusfxmeas0aeghy3sp4qd) |

**Assets**

| Token                                                                                                                       | Address                                                                                                                |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| INJ(18)                                                                                                                     | [0xd375590b4955f6ea5623f799153f9b787a3bd319](https://sonicscan.org/address/0xd375590b4955f6ea5623f799153f9b787a3bd319) |
| bnUSD(18)                                                                                                                   | [0x69425FFb14704124A58d6F69d510f74A59D9a5bC](https://sonicscan.org/address/0x69425FFb14704124A58d6F69d510f74A59D9a5bC) |
| [Soda(18)](https://injscan.com/asset/factory%2Finj1d036ftaatxpkqsu9hja8r24rv3v33chz3appxp%2Fsoda/)                          | [0x20ce75cdcee44b1308365447b91b9c26e2b71ffd](https://sonicscan.org/address/0x20ce75cdcee44b1308365447b91b9c26e2b71ffd) |
| [USDC(6)](https://explorer.injective.network/asset/ibc%2F2CBC2EA121AE42563B08028466F37B600F2D7D4282342DE938283CC3FB2BC00E/) | [0x4Bc1211fAA06Fb50Ff61a70331F56167AE511057](https://sonicscan.org/address/0x4Bc1211fAA06Fb50Ff61a70331F56167AE511057) |
| [USDT(6)](https://explorer.injective.network/asset/peggy0xdAC17F958D2ee523a2206206994597C13D831ec7/)                        | [0xb3bdde94ee6ec50c06291d8e49dfeae12992ce01](https://sonicscan.org/address/0xb3bdde94ee6ec50c06291d8e49dfeae12992ce01) |

### NEAR \[Encoding=utf8]

| Contract      | Address                                                                            |
| ------------- | ---------------------------------------------------------------------------------- |
| AssetManager  | [asset-manager.sodax.near](https://nearblocks.io/address/asset-manager.sodax.near) |
| RateLimit     | [rate-limit.sodax.near](https://nearblocks.io/address/rate-limit.sodax.near)       |
| bnUSD         | [bnusd.sodax.near](https://nearblocks.io/address/bnusd.sodax.near)                 |
| Intent Filler | [intent-filler.sodax.near](https://nearblocks.io/address/intent-filler.sodax.near) |
| Router        | [router.sodax.near](https://nearblocks.io/address/router.sodax.near)               |

**Assets**

| Token                                                                                                    | Address                                                                                                                |
| -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [NEAR (24)](https://nearblocks.io/)                                                                      | [0xda7a39b44d5aa0b99bd3c16110f79fa43d3bf4a4](https://sonicscan.org/address/0xda7a39b44d5aa0b99bd3c16110f79fa43d3bf4a4) |
| [bnUSD (24)](https://nearblocks.io/token/bnusd.sodax.near)                                               | [0x1979904d6d5ef1178e242471f7091f36d79f8ab4](https://sonicscan.org/address/0x1979904d6d5ef1178e242471f7091f36d79f8ab4) |
| [Soda (24)](https://nearblocks.io/token/soda.sodax.near)                                                 | [0x4edaa15e106910dbf2b73e6a36828755ebd38668](https://sonicscan.org/address/0x4edaa15e106910dbf2b73e6a36828755ebd38668) |
| [USDC (6)](https://nearblocks.io/token/17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1) | [0x0d7209e6a6922a9f8e42bf04dc263dcdd0a0c20a](https://sonicscan.org/address/0x0d7209e6a6922a9f8e42bf04dc263dcdd0a0c20a) |
| [USDT (6)](https://nearblocks.io/token/usdt.tether-token.near)                                           | [0x54e3c4b335caff41577e187fb9be1c2b35a1e151](https://sonicscan.org/address/0x54e3c4b335caff41577e187fb9be1c2b35a1e151) |
| [Wrapped Near (24)](https://nearblocks.io/token/wrap.near)                                               | [0x2ec38a4ddfb76e1aae04f392b55a28f0d659bcd4](https://sonicscan.org/address/0x2ec38a4ddfb76e1aae04f392b55a28f0d659bcd4) |

### Stellar \[Encoding=xdr]

| Contract      | Address                                                                                                                                                              |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager  | [CCGF33A4CO6D3BXFEKPXVCFCZBK76I3AQOZK6KIKRPAWAZR3632WHCJ3](https://stellar.expert/explorer/public/contract/CCGF33A4CO6D3BXFEKPXVCFCZBK76I3AQOZK6KIKRPAWAZR3632WHCJ3) |
| RateLimit     | [CD7Q7M4Z4QWVTSJV6ELRFIBKTOLIW5KNCAL7R7T3W5PQ36MDPPN7DV3R](https://stellar.expert/explorer/public/contract/CD7Q7M4Z4QWVTSJV6ELRFIBKTOLIW5KNCAL7R7T3W5PQ36MDPPN7DV3R) |
| bnUSD         | [CD6YBFFWMU2UJHX2NGRJ7RN76IJVTCC7MRA46DUBXNB7E6W7H7JRJ2CX](https://stellar.expert/explorer/public/contract/CD6YBFFWMU2UJHX2NGRJ7RN76IJVTCC7MRA46DUBXNB7E6W7H7JRJ2CX) |
| Intent Filler | [CDSDDKUHGVHTKXTM655D2TCVRET7TYMK3KSOY4WLSMBRHNNFYJXMSUGG](https://stellar.expert/explorer/public/contract/CDSDDKUHGVHTKXTM655D2TCVRET7TYMK3KSOY4WLSMBRHNNFYJXMSUGG) |

**Assets**

| Token                                                                                                                  | Address                                                                                                                |
| ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| XLM(7)                                                                                                                 | [0x8ac68af223907fb1b893086601a3d99e00f2fa9d](https://sonicscan.org/address/0x8ac68af223907fb1b893086601a3d99e00f2fa9d) |
| bnUSD(7)                                                                                                               | [0x23225Ab8E63FCa4070296678cb46566d57E1BBe3](https://sonicscan.org/address/0x23225Ab8E63FCa4070296678cb46566d57E1BBe3) |
| [Soda(7)](https://stellar.expert/explorer/public/contract/CAH5LKJC2ZB4RVUVEVL2QWJWNJLHQE2UF767ILLQ5EQ4O3OURR2XIUGM)    | [0xD749B5FfFED7cEDaa3239abDd16D677179C29AEc](https://sonicscan.org/address/0xD749B5FfFED7cEDaa3239abDd16D677179C29AEc) |
| [USDC(7)](https://stellar.expert/explorer/public/contract/CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75)    | [0x348007B53F25A9A857aB8eA81ec9E3CCBCf440f2](https://sonicscan.org/address/0x348007B53F25A9A857aB8eA81ec9E3CCBCf440f2) |
| [sodaBTC(7)](https://stellar.expert/explorer/public/contract/CD6XWBW74YVFDQQYUM2GALCULMA5MAWGP6NTCWF3ZYXP4Z7MEVY4JKBX) | [0xDDEE01F63C18843e2BaC30cb702864D7632C83a2](https://sonicscan.org/address/0xDDEE01F63C18843e2BaC30cb702864D7632C83a2) |
| [sodaETH(7)](https://stellar.expert/explorer/public/contract/CDK5EWVTZLGSLI6D5OSES7XUKWZUKBXDRNOWUVDNPP5RJRP5EYWCW7SL) | [0x4985a4B72Ac723723e9ae82382D12D77e9A715DE](https://sonicscan.org/address/0x4985a4B72Ac723723e9ae82382D12D77e9A715DE) |
| [sodaBNB(7)](https://stellar.expert/explorer/public/contract/CCXTXZAFLVNTMORVWYB6BGL7YEW3U3ONDAL2FGBRGDUQH7AGANVQPRS6) | [0xa10be5f5C2deA7d272555dC73ea2a7317C3c5B63](https://sonicscan.org/address/0xa10be5f5C2deA7d272555dC73ea2a7317C3c5B63) |
| Legacy bnUSD(18)                                                                                                       | [0x1559b52d2e165da1505a542ea37c543c9137f52a](https://sonicscan.org/address/0x1559b52d2e165da1505a542ea37c543c9137f52a) |

### SOLANA \[Encoding=base58]

| Contract      | Address                                                                                                                          |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager  | [AnCCJjheynmGqPp6Vgat9DTirGKD4CtQzP8cwTYV8qKH](https://explorer.solana.com/address/AnCCJjheynmGqPp6Vgat9DTirGKD4CtQzP8cwTYV8qKH) |
| RateLimit     | [Dv2eWUQmZ5vqqLvvPMq7Z54tW3eAwJ5chAeXYWYnmCPk](https://explorer.solana.com/address/Dv2eWUQmZ5vqqLvvPMq7Z54tW3eAwJ5chAeXYWYnmCPk) |
| Intent Filler | [ABBqXPEtnEnT2QywyRcpmwqHnGFXCpRSspL2Y4dB7YMA](https://explorer.solana.com/address/ABBqXPEtnEnT2QywyRcpmwqHnGFXCpRSspL2Y4dB7YMA) |
| bnUSD         | [3rSPCLNEF7Quw4wX8S1NyKivELoyij8eYA2gJwBgt4V5](https://explorer.solana.com/address/3rSPCLNEF7Quw4wX8S1NyKivELoyij8eYA2gJwBgt4V5) |

**Assets**

| Token                                                                                       | Address                                                                                                                |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| SOL(9)                                                                                      | [0x0c09e69a4528945de6d16c7e469dea6996fdf636](https://sonicscan.org/address/0x0c09e69a4528945de6d16c7e469dea6996fdf636) |
| bnUSD(9)                                                                                    | [0x14C65b1CDc0B821569081b1F77342dA0D0CbF439](https://sonicscan.org/address/0x14C65b1CDc0B821569081b1F77342dA0D0CbF439) |
| [Soda(9)](https://explorer.solana.com/address/8Bj8gSbga8My8qRkT1RrvgxFBExiGFgdRNHFaR9o2T3Q) | [0x07db7b1a96ebe474b20f52ff487cee415adee79e](https://sonicscan.org/address/0x07db7b1a96ebe474b20f52ff487cee415adee79e) |
| [USDC(6)](https://explorer.solana.com/address/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v) | [0xC3f020057510ffE10Ceb882e1B48238b43d78a5e](https://sonicscan.org/address/0xC3f020057510ffE10Ceb882e1B48238b43d78a5e) |
| [USDT(6)](https://explorer.solana.com/address/Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB) | [0x891c7ab9047eb087b5a988a2638e7056f67adc5c](https://sonicscan.org/address/0x891c7ab9047eb087b5a988a2638e7056f67adc5c) |

**Asset Managers Vault and Token Creation Fee**

| Token      | Address                                                                                                                          | Token Creation Fee |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| SOL(9)     | [FRNR8SpqXU3YL7FAeVqBf9vqU2An4SxS2pGYxXCrV4aY](https://explorer.solana.com/address/FRNR8SpqXU3YL7FAeVqBf9vqU2An4SxS2pGYxXCrV4aY) |                    |
| bnUSD(9)   | [GLBsG4mF51C8nmXNStu6FsAi5rreyVatpimh7DiwhFbi](https://explorer.solana.com/address/GLBsG4mF51C8nmXNStu6FsAi5rreyVatpimh7DiwhFbi) | 0.2 bnUSD          |
| \[Soda(9)] | [ArKwiwH2zaqE1nGeu76qnsqvfdpKFwM4TqJqrdbuSvHJ](https://explorer.solana.com/address/ArKwiwH2zaqE1nGeu76qnsqvfdpKFwM4TqJqrdbuSvHJ) | 4 SODA             |
| \[USDC(6)] | [28MrCrLHN8aoQd4KLX6dsqYxNZ6XxbZTeGb4a6bqBvhg](https://explorer.solana.com/address/28MrCrLHN8aoQd4KLX6dsqYxNZ6XxbZTeGb4a6bqBvhg) | 0.2 USDC           |
| \[USDT(6)] | [Ce7B1M9u5yVEQWcHv8SPfgKJSN9N9FLAwL5hc5NVwHkP](https://explorer.solana.com/address/Ce7B1M9u5yVEQWcHv8SPfgKJSN9N9FLAwL5hc5NVwHkP) | 0.5 USDT           |

### SUI \[Encoding=utf8]

| Contract      | Address                                                                                                                                                                                                                                                             |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager  | [0xa17a409164d1676db71b411ab50813ba2c7dd547d2df538c699049566f1ff922::asset\_manager::0xcb7346339340b7f8dea40fcafb70721dc2fcfa7e8626a89fd954d46c1f928b61](https://suivision.xyz/package/0xa17a409164d1676db71b411ab50813ba2c7dd547d2df538c699049566f1ff922)          |
| RateLimit     | [0x4a25336d949648eddf4b5cf78e46d67bc529efffe4a5cca5169164116a56ea48::rate\_limits::0x63f2e2cca8c11993af03c66672a19461ab336f10d5ccbaea15aadb807df774eb](https://suivision.xyz/package/0xf796d36fada0fedbc2c1a3b31980fdc9028faea6f3750ec7819d15e991995a0e?tab=Code)   |
| bnUSD         | [0xff4de2b2b57dd7611d2812d231a467d007b702a101fd5c7ad3b278257cddb507::bnusd::BNUSD](https://suivision.xyz/package/0xff4de2b2b57dd7611d2812d231a467d007b702a101fd5c7ad3b278257cddb507?tab=Code)                                                                       |
| Soda          | [0x0a0393721732617a2a771535e83c0a46f04aeef7d03239bbbb1249bc0981b952::soda::SODA](https://suivision.xyz/package/0x0a0393721732617a2a771535e83c0a46f04aeef7d03239bbbb1249bc0981b952?tab=Code)                                                                         |
| Intent Filler | [0x024faa722661aa85cfbc88ceb9fca6fe940e0c74ff07bcb0f5663c0ae163d36b::intent\_filler::0x8c089426e8d64e8b6989013d3cf4a858c88775aeca7b6214bc62746a28c9b638](https://suivision.xyz/package/0x024faa722661aa85cfbc88ceb9fca6fe940e0c74ff07bcb0f5663c0ae163d36b?tab=Code) |

**Assets**

| Token                                                                                                                                 | Address                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| SUI(9)                                                                                                                                | [0x4676b2a551b25c04e235553c1c81019337384673](https://sonicscan.org/address/0x4676b2a551b25c04e235553c1c81019337384673) |
| bnUSD(9)                                                                                                                              | [0xDf23097B9AEb917Bf8fb70e99b6c528fffA35364](https://sonicscan.org/address/0xDf23097B9AEb917Bf8fb70e99b6c528fffA35364) |
| [Soda(9)](https://suivision.xyz/package/0x0a0393721732617a2a771535e83c0a46f04aeef7d03239bbbb1249bc0981b952?tab=Code)                  | [0x5db9cec919f40c50809d9490dc3bba4f05b0a1d7](https://sonicscan.org/address/0x5db9cec919f40c50809d9490dc3bba4f05b0a1d7) |
| [USDC(6)](https://suivision.xyz/coin/0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC)                  | [0x5635369c8a29A081d26C2e9e28012FCa548BA0Cb](https://sonicscan.org/address/0x5635369c8a29A081d26C2e9e28012FCa548BA0Cb) |
| [USDT(6)](https://suivision.xyz/coin/0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT)                  | [0xad8901fe2c8defe467fa1df7af78d755e53485ab](https://sonicscan.org/address/0xad8901fe2c8defe467fa1df7af78d755e53485ab) |
| [afSUI(9)](https://suivision.xyz/coin/0xf325ce1300e8dac124071d3152c5c5ee6174914f8bc2161e88329cf579246efc::afsui::AFSUI)               | [0x039666bd0cbc96a66c40e8541af465beaa81aa7e](https://sonicscan.org/address/0x039666bd0cbc96a66c40e8541af465beaa81aa7e) |
| [mSUI(9)](https://suivision.xyz/coin/0x922d15d7f55c13fd790f6e54397470ec592caa2b508df292a2e8553f3d3b274f::msui::MSUI)                  | [0xb202c674c9a79b5681e981ba0daa782b3ceeebbe](https://sonicscan.org/address/0xb202c674c9a79b5681e981ba0daa782b3ceeebbe) |
| [haSUI(9)](https://suivision.xyz/coin/0xbde4ba4c2e274a60ce15c1cfff9e5c42e41654ac8b6d906a57efa4bd3c29f47d::hasui::HASUI)               | [0x67a26d11fce15e8b33ac97230d36cae1c52c35e7](https://sonicscan.org/address/0x67a26d11fce15e8b33ac97230d36cae1c52c35e7) |
| [vSUI(9)](https://suivision.xyz/coin/0x549e8b69270defbfafd4f94e17ec44cdbdd99820b33bda2278dea3b9a32d3f55::cert::CERT)                  | [0x025715bcda08db06c795cd5bf944e2081468d99a](https://sonicscan.org/address/0x025715bcda08db06c795cd5bf944e2081468d99a) |
| [yapSUI(9)](https://suivision.xyz/coin/0x83f1bb8c91ecd1fd313344058b0eed94d63c54e41d8d1ae5bff1353443517d65::yap_sui::YAP_SUI)          | [0xac509404f3a3ca3f7766baf65be45a52b1cfccd7](https://sonicscan.org/address/0xac509404f3a3ca3f7766baf65be45a52b1cfccd7) |
| [trivenSUI(9)](https://suivision.xyz/coin/0x502867b177303bf1bf226245fcdd3403c177e78d175a55a56c0602c7ff51c7fa::trevin_sui::TREVIN_SUI) | [0x514569c788b096595672e0f68ec72387a22ac67b](https://sonicscan.org/address/0x514569c788b096595672e0f68ec72387a22ac67b) |
| legacy bnUSD(9)                                                                                                                       | [0xddf6ad38f9c9451c1f4cdf369040f6869e37393e](https://sonicscan.org/address/0xddf6ad38f9c9451c1f4cdf369040f6869e37393e) |
| [WAL(9)](https://suivision.xyz/coin/0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL)                     | [0x72a726ab67c382e8946a79e9872ea32695661e11](https://sonicscan.org/address/0x72a726ab67c382e8946a79e9872ea32695661e11) |
| [DEEP(6)](https://suivision.xyz/coin/0xdeeb7a4662eec9f2f3def03fb937a663dddaa2e215b8078a284d026b7946c270::deep::DEEP)                  | [0x93057085a4627cec26b31477a02f62412cc0485f](https://sonicscan.org/address/0x93057085a4627cec26b31477a02f62412cc0485f) |

### Bitcoin \[Encoding=utf8]

| Contract               | Address                                                                                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager (Staging) | [bc1pcz4pyrfgv7v6tx8a404mafyvt73cnm80yuv8tqwrywxmqxpja8ys4pjyl5](https://mempool.space/address/bc1pcz4pyrfgv7v6tx8a404mafyvt73cnm80yuv8tqwrywxmqxpja8ys4pjyl5)          |
| BTC\_Registry          | \[0xe242683faACEB5D2586a6c89e30c533D41FAA2a2]\(0x0c09e69a4528945de6d16c7e469dea6996fdf636]\(<https://sonicscan.org/address/0xe242683faACEB5D2586a6c89e30c533D41FAA2a2>) |

**Assets**

| Token   | Radfi Id  | Address                                                                                                                |
| ------- | --------- | ---------------------------------------------------------------------------------------------------------------------- |
| BTC(8)  | 0:0       | [0xeB0393893b5bf98a50073d6740738B08e575058b](https://sonicscan.org/address/0xeB0393893b5bf98a50073d6740738B08e575058b) |
| BUSD(6) | 897442:43 | [0xdb41c7d09406026d4582bc2fc6d6319c323fe1bb](https://sonicscan.org/address/0xdb41c7d09406026d4582bc2fc6d6319c323fe1bb) |

### Stacks \[Encoding=stacks-consensus-buff]

| Contract            | Address                                                                                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AssetManager Impl   | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.asset-manager-impl](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.asset-manager-impl?chain=mainnet)   |
| AssetManager State  | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.asset-manager-state](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.asset-manager-state?chain=mainnet) |
| RateLimit Impl      | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.rate-limit-impl](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.rate-limit-impl?chain=mainnet)         |
| RateLimit State     | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.rate-limit-state](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.rate-limit-state?chain=mainnet)       |
| Connection v3       | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.connection-v3](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.connection-v3?chain=mainnet)             |
| Connection v3 State | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.connection-v3-state](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.connection-v3-state?chain=mainnet) |
| Intent Filler Impl  | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.intent-filler-impl](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.intent-filler-impl?chain=mainnet)   |
| Intent Filler State | [SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.intent-filler-state](https://explorer.hiro.so/txid/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.intent-filler-state?chain=mainnet) |

**Assets**

| Token                                                                                                        | Address                                                                                                                |
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| \[STX(6)]                                                                                                    | [0x30b3b1a3e4F1235472772e60a8cd9C0165db641e](https://sonicscan.org/address/0x30b3b1a3e4F1235472772e60a8cd9C0165db641e) |
| [bnUSD(6)](https://explorer.hiro.so/token/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.bnusd?chain=mainnet)     | [0x78093c056fb37ffc1a9ae96daa0a7dd9d372b64d](https://sonicscan.org/address/0x78093c056fb37ffc1a9ae96daa0a7dd9d372b64d) |
| [SODA(6)](https://explorer.hiro.so/token/SP3031RGK734636C8KGW2Y76TEQBTVX59Q472EQH0.soda?chain=mainnet)       | [0xebd34ddf9f47f9bcdf706e87206d40a579c34822](https://sonicscan.org/address/0xebd34ddf9f47f9bcdf706e87206d40a579c34822) |
| [sBTC(8)](https://explorer.hiro.so/token/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token?chain=mainnet) | [0x6f6c039b07e25bb86d8223643a84122404963d9b](https://sonicscan.org/address/0x6f6c039b07e25bb86d8223643a84122404963d9b) |
| [USDC(6)](https://explorer.hiro.so/token/SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx?chain=mainnet)      | [0x2336917df30f3417986a29ec384274a83adbef16](https://sonicscan.org/address/0x2336917df30f3417986a29ec384274a83adbef16) |

### Tron

Internal chain id (NID): `1953656686`

| Contract          | Tron Address                                                                                             | EVM Address (hex)                            |
| ----------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| Connection (v3)   | [TYKiFusswwe3FTp9GwY61wwxtr2sVyxrVq](https://tronscan.org/#/contract/TYKiFusswwe3FTp9GwY61wwxtr2sVyxrVq) | `0xf533821bd00e66a993218829e7e1d37f0225b096` |
| RateLimit         | [TKuLbpwBzTMwtfvRXATFhx39eLRNzUuQqY](https://tronscan.org/#/contract/TKuLbpwBzTMwtfvRXATFhx39eLRNzUuQqY) | `0x6cf5f00cbea9bbef20d05498a335b21b3f7bf425` |
| SpokeAssetManager | [THxCcZAuGAnSD8aCit9mX7c9tHhdcTNAAL](https://tronscan.org/#/contract/THxCcZAuGAnSD8aCit9mX7c9tHhdcTNAAL) | `0x57904434b635c26a523cfc9c7ec14b738f764a81` |

#### Assets

*Pending hub registration (native TRX / USDT).*

## Pyth setup

Adapter factory: [0x67F0848F787a30dD278dcF451Ae47e79aF026d56](https://sonicscan.org/address/0x67F0848F787a30dD278dcF451Ae47e79aF026d56)

Redemption Factory OLD: [0x59F1D8860B09b1169F9Ca4A308428638583a4E4F](https://sonicscan.org/address/0x59F1D8860B09b1169F9Ca4A308428638583a4E4F)

Redemption Factory: [0xe198593FC8edBC4a2eA2e0Ef48A30e992e59303A](https://sonicscan.org/address/0xe198593FC8edBC4a2eA2e0Ef48A30e992e59303A)

ERC4626 Oracle Factory: [0xFDD3cD97d4639bf49985F172ef43585A5fEf5f37](https://sonicscan.org/address/0xFDD3cD97d4639bf49985F172ef43585A5fEf5f37)

Provider main: [0x92698b844e52056Ca3ed5924F876C31E02f281E3](https://sonicscan.org/address/0x92698b844e52056Ca3ed5924F876C31E02f281E3)

USD Feed: [0x79fa150c700adeaf618475E8CB17933e7A9c3214](https://sonicscan.org/address/0x79fa150c700adeaf618475E8CB17933e7A9c3214)

| Pair         | Price Feed                                                         | Adapter Address                                                                                                        |
| ------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| ETH/USD      | 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace | [0x6C2E4F326006aB95770f471D79bF0b29A52c7ff7](https://sonicscan.org/address/0x6C2E4F326006aB95770f471D79bF0b29A52c7ff7) |
| BTC/USD      | 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43 | [0x73ec9a7d193D703863B5E3AD00EB06692768216F](https://sonicscan.org/address/0x73ec9a7d193D703863B5E3AD00EB06692768216F) |
| AVAX/USD     | 0x93da3352f9f1d105fdfe4971cfa80e9dd777bfc5d0f683ebb6e1294b92137bb7 | [0xEAe6eFe36e9b171bb4111caa4413D5BF0cB1570f](https://sonicscan.org/address/0xEAe6eFe36e9b171bb4111caa4413D5BF0cB1570f) |
| SUI/USD      | 0x23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744 | [0x6bCCf7284b9189270AcEE9ce59827BC67215E94f](https://sonicscan.org/address/0x6bCCf7284b9189270AcEE9ce59827BC67215E94f) |
| BNB/USD      | 0x2f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f | [0x399a3EB1d2356CB679A564BC63B43DD265eD8151](https://sonicscan.org/address/0x399a3EB1d2356CB679A564BC63B43DD265eD8151) |
| SOL/USD      | 0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d | [0xD641d745B4547A8695e37dB26ad328199CA4dCa6](https://sonicscan.org/address/0xD641d745B4547A8695e37dB26ad328199CA4dCa6) |
| XLM/USD      | 0xb7a8eba68a997cd0210c2e1e4ee811ad2d174b3611c22d9ebf16f4cb7e9ba850 | [0x21949C2CC01Eda621aC719963C542059Ef03810d](https://sonicscan.org/address/0x21949C2CC01Eda621aC719963C542059Ef03810d) |
| INJ/USD      | 0x7a5bc1d2b56ad029048cd63964b3ad2776eadf812edc1a43a31406cb54bff592 | [0x731018eE9803D7cD9F5691Eab99eC31B5d452d1d](https://sonicscan.org/address/0x731018eE9803D7cD9F5691Eab99eC31B5d452d1d) |
| POL/USD      | 0xffd11c5a1cfd42f80afb2df4d9f264c15f956d68153335374ec10722edd70472 | [0xb43801461573d08d33db6255c195edf6d87b779d](https://sonicscan.org/address/0xb43801461573d08d33db6255c195edf6d87b779d) |
| S/USD        | 0xf490b178d0c85683b7a0f2388b40af2e6f7c90cbe0f96b31f315f08d0e5a2d6d | [0xb5e27a0b12bda48b4437b5cb0e3990ffc3816b16](https://sonicscan.org/address/0xb5e27a0b12bda48b4437b5cb0e3990ffc3816b16) |
| HYPE/USD     | 0x4279e31cc369bbcc2faf022b382b080e32a8e689ff20fbc530d2a603eb6cd98b | [0x56A99B2C140CC4B46796B5895FAC925F3567EDec](https://sonicscan.org/address/0x56A99B2C140CC4B46796B5895FAC925F3567EDec) |
| ICX/USD      | 0x843bb7ab4846e3f233f34082ef188a94f79517f66c224bd85b2b5cb34d10d745 | [0xfbc6abb0b9dd3848d6b3716e895409dacffe3549](https://sonicscan.org/address/0xfbc6abb0b9dd3848d6b3716e895409dacffe3549) |
| xSODA/USD    | Redemption                                                         | [0xe31d37a976c488bcdacf93bc7bfe5e5bd4e2e9dc](https://sonicscan.org/address/0xe31d37a976c488bcdacf93bc7bfe5e5bd4e2e9dc) |
| WEETH/ETH    | 0x343558e79f587e098c321218ecb34d031ba709ab3e84133126f3c98511b91f64 | [0x6fa5fc7f9b7af222ec2348135b7de53c142c2f1f](https://sonicscan.org/address/0x6fa5fc7f9b7af222ec2348135b7de53c142c2f1f) |
| WSTETH/ETH   | 0xf59ead01ed0faba85332a1e2feae8ddb14a1c94ebac259f1c982c92fc7ce333e | [0x73267f316b8625916c895c235c44579472a992f0](https://sonicscan.org/address/0x73267f316b8625916c895c235c44579472a992f0) |
| WEETH/USD    | Redemption                                                         | [0x9156a9d86d11468f280109ba723e3ca865d4f583](https://sonicscan.org/address/0x9156a9d86d11468f280109ba723e3ca865d4f583) |
| WSTETH/USD   | Redemption                                                         | [0x8efd6b8371a962eed30ded5a457dabe3294313fb](https://sonicscan.org/address/0x8efd6b8371a962eed30ded5a457dabe3294313fb) |
| NEAR/USD     | 0xc415de8d2eba7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750 | [0x797c72569b4640ae472ec8c18715ece81b3ded57](https://sonicscan.org/address/0x797c72569b4640ae472ec8c18715ece81b3ded57) |
| KAIA/USD     | 0x452d40e01473f95aa9930911b4392197b3551b37ac92a049e87487b654b4ebbe | [0x24fbfeb70EB4f2C61BECa6B196f6C3F132Cc6845](https://sonicscan.org/address/0x24fbfeb70EB4f2C61BECa6B196f6C3F132Cc6845) |
| USDS/USD     | 0x77f0971af11cc8bac224917275c1bf55f2319ed5c654a1ca955c82fa2d297ea1 | [0xa4737c36dc8ce50c8471f2ab9ab56b232b6013eb](https://sonicscan.org/address/0xa4737c36dc8ce50c8471f2ab9ab56b232b6013eb) |
| sUSDS/USDS   | 0x6968a8641208463d17ae3b9cfa0e4841a7aa7a5d54122b9f692b84fe9ce3409f | [0x1a40cd75b16be1fac50d887b2c9fe2a6ebbf4c6b](https://sonicscan.org/address/0x1a40cd75b16be1fac50d887b2c9fe2a6ebbf4c6b) |
| sUSDS/USD    | Redemption                                                         | [0xc091574432fd73fed2fcf658e6da248e3b391328](https://sonicscan.org/address/0xc091574432fd73fed2fcf658e6da248e3b391328) |
| sftUSD/ftUSD | ERC4624                                                            | [0xf3d83b7271df2d63368c8b5824a5fd39bfb3041d](https://sonicscan.org/address/0xf3d83b7271df2d63368c8b5824a5fd39bfb3041d) |
| sftUSD/USD   | Redmption (Fixed to USDC, not ftUSD)                               | [0x7c97cd6bf7fa9e4a21ba23ef0d8202e5409df6f2](https://sonicscan.org/address/0x7c97cd6bf7fa9e4a21ba23ef0d8202e5409df6f2) |
| STX/USD      | 0xec7a775f46379b5e943c3526b1c8d54cd49749176b0b98e02dde68d1bd335c17 | [0xa5217f64b1bf0b4200e68446af053ee8e8d95e38](https://sonicscan.org/address/0xa5217f64b1bf0b4200e68446af053ee8e8d95e38) |
| LL/USD       | 0x2805c8894235111024c54253267f2b325be23763d534d2051742e39234b5835a | [0x616e8ffc3fd58901dbc76ea88d3255c5f6f8d819](https://sonicscan.org/address/0x616e8ffc3fd58901dbc76ea88d3255c5f6f8d819) |
| HyTbills     | Redmeption                                                         | [0x270e3677f93709ba67411b4273a1dfddf9aa689b](https://sonicscan.org/address/0x270e3677f93709ba67411b4273a1dfddf9aa689b) |
| JitoSOL/USD  | Redemption                                                         | [0xee9a7f4d1771d6f6c4c31f15edf42bab4201def2](https://sonicscan.org/address/0xee9a7f4d1771d6f6c4c31f15edf42bab4201def2) |
| JitoSOL/SOL  | 0x01d577b07031e12635d2fb86af6ae938bdc2b6dba9602d8e8af34d44587566fc | [0xefac93e112ce6579e4ea2fca01f46f52aacf11bd](https://sonicscan.org/address/0xefac93e112ce6579e4ea2fca01f46f52aacf11bd) |
| WBTC/USD     | 0xc9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33 | [0xfc35fddde7d5a55ce4e569a7e4290ac60b9b68f9](https://sonicscan.org/address/0xfc35fddde7d5a55ce4e569a7e4290ac60b9b68f9) |

## Money Market

| Contract                          | Address                                                                                                                |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| aToken                            | [0xC090cc802fe67eadCC7ab317e5618a649A83EC70](https://sonicscan.org/address/0xC090cc802fe67eadCC7ab317e5618a649A83EC70) |
| aaveOracle                        | [0x12835f0B71D7C94B23b64f765Ab9cbCb8F5250B9](https://sonicscan.org/address/0x12835f0B71D7C94B23b64f765Ab9cbCb8F5250B9) |
| aclManager                        | [0x33C1BBf230e57ef2baB3CC9C68a553B77DbC4Dc1](https://sonicscan.org/address/0x33C1BBf230e57ef2baB3CC9C68a553B77DbC4Dc1) |
| configEngine                      | [0xaB2cFC7574Ee260564506670824ddEc44B8d773b](https://sonicscan.org/address/0xaB2cFC7574Ee260564506670824ddEc44B8d773b) |
| defaultInterestRateStrategy       | [0x6142E3bbc9deFf9b3036d5BA9617CBEA867aB785](https://sonicscan.org/address/0x6142E3bbc9deFf9b3036d5BA9617CBEA867aB785) |
| emissionManager                   | [0xA4E93b5A356D405Baac6a324550575d2CFE956E5](https://sonicscan.org/address/0xA4E93b5A356D405Baac6a324550575d2CFE956E5) |
| poolAddressesProvider             | [0x036aDe0aBAA4c82445Cb7597f2d6d6130C118c7b](https://sonicscan.org/address/0x036aDe0aBAA4c82445Cb7597f2d6d6130C118c7b) |
| poolAddressesProviderRegistry     | [0x3F0988C93a6498901fE177E6fcA944c9947e1Eb9](https://sonicscan.org/address/0x3F0988C93a6498901fE177E6fcA944c9947e1Eb9) |
| poolConfiguratorImplementation    | [0x53F512d9cC52602759C0EEbb3f95Cb2D5e39Fe6F](https://sonicscan.org/address/0x53F512d9cC52602759C0EEbb3f95Cb2D5e39Fe6F) |
| poolConfiguratorProxy             | [0x728eBf949B93ED082b8EeCbB21d3D5E3cC1F91bA](https://sonicscan.org/address/0x728eBf949B93ED082b8EeCbB21d3D5E3cC1F91bA) |
| poolImplementation                | [0x83D7b2F35b1993C49C0a4Ee99A0e472228E82560](https://sonicscan.org/address/0x83D7b2F35b1993C49C0a4Ee99A0e472228E82560) |
| poolProxy                         | [0x553434896D39F867761859D0FE7189d2Af70514E](https://sonicscan.org/address/0x553434896D39F867761859D0FE7189d2Af70514E) |
| protocolDataProvider              | [0x775A1ab37c1d9537B0887909Ba6A2ddFa82082Ed](https://sonicscan.org/address/0x775A1ab37c1d9537B0887909Ba6A2ddFa82082Ed) |
| proxyAdmin                        | [0x126335eC4e2c784b47E7788D169771Eaf82f9Bb6](https://sonicscan.org/address/0x126335eC4e2c784b47E7788D169771Eaf82f9Bb6) |
| rewardsControllerImplementation   | [0x9fD302d80e2dF6f59C5684275857c5D91FA88eDc](https://sonicscan.org/address/0x9fD302d80e2dF6f59C5684275857c5D91FA88eDc) |
| rewardsControllerProxy            | [0x1450FFb20f7897D9041c534B610fD4649Fd066eb](https://sonicscan.org/address/0x1450FFb20f7897D9041c534B610fD4649Fd066eb) |
| staticATokenFactoryImplementation | [0x71CfeDBe5b569C8d6FfcE8C611629F6e74266Eaa](https://sonicscan.org/address/0x71CfeDBe5b569C8d6FfcE8C611629F6e74266Eaa) |
| staticATokenFactoryProxy          | [0x9120956787FcE7D7082C52CDCAafb7F4B88272d4](https://sonicscan.org/address/0x9120956787FcE7D7082C52CDCAafb7F4B88272d4) |
| staticATokenImplementation        | [0xCfBc1e319c46236066968Bb3b074Dc755cFc3371](https://sonicscan.org/address/0xCfBc1e319c46236066968Bb3b074Dc755cFc3371) |
| transparentProxyFactory           | [0x9529b2d472f947C99dF47e14fA63199C4C3fDC0b](https://sonicscan.org/address/0x9529b2d472f947C99dF47e14fA63199C4C3fDC0b) |
| treasury                          | [0x996752752F887000C8136ceFB023d12719DAD24a](https://sonicscan.org/address/0x996752752F887000C8136ceFB023d12719DAD24a) |
| treasuryImplementation            | [0xd3a1d2dFAA475f14fb35506D1F453dfE789174F9](https://sonicscan.org/address/0xd3a1d2dFAA475f14fb35506D1F453dfE789174F9) |
| uiIncentiveDataProvider           | [0x34D22034Ed75831EEAE978F019034829b31A29b2](https://sonicscan.org/address/0x34D22034Ed75831EEAE978F019034829b31A29b2) |
| uiPoolDataProvider                | [0xC04d746C38f1E51C8b3A3E2730250bbAC2F271bf](https://sonicscan.org/address/0xC04d746C38f1E51C8b3A3E2730250bbAC2F271bf) |
| variableDebtToken                 | [0x9F982b6C9fdFF122D95129b15cD8C4D0797884b1](https://sonicscan.org/address/0x9F982b6C9fdFF122D95129b15cD8C4D0797884b1) |
| walletBalanceProvider             | [0x3d4C1D61e0F58aC4cA12CD68B2e29807C5bFA0f0](https://sonicscan.org/address/0x3d4C1D61e0F58aC4cA12CD68B2e29807C5bFA0f0) |

## Balanced Dollar minting

| Contract               | Address                                                                                                                |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| bnUSD                  | [0x94dC79ce9C515ba4AE4D195da8E6AB86c69BFc38](https://sonicscan.org/address/0x94dC79ce9C515ba4AE4D195da8E6AB86c69BFc38) |
| Discount Rate Strategy | [0x7bd188F1027fB05719D3D5A925d941855791072E](https://sonicscan.org/address/0x7bd188F1027fB05719D3D5A925d941855791072E) |
| aToken                 | [0x7fBaD0c671c096Ae0173A67f5f2a14d6F5cbCB19](https://sonicscan.org/address/0x7fBaD0c671c096Ae0173A67f5f2a14d6F5cbCB19) |
| Variable Debt Token    | [0xe9c8Ae0631230104d6a8d8f3aBD36b8f53Cd6161](https://sonicscan.org/address/0xe9c8Ae0631230104d6a8d8f3aBD36b8f53Cd6161) |
| USD ORACLE             | [0xaa782030432CA2e00C2DB56f357343910Da553E2](https://sonicscan.org/address/0xaa782030432CA2e00C2DB56f357343910Da553E2) |
| GhoSteward             | [0x11896C4ecfaf8e451a74eBC5722c50aE60D59A22](https://sonicscan.org/address/0x11896C4ecfaf8e451a74eBC5722c50aE60D59A22) |
| aTokenProxy            | [0xa2cDA49735e42f0905496E40a66B3C5475Ed69dF](https://sonicscan.org/address/0xa2cDA49735e42f0905496E40a66B3C5475Ed69dF) |
| variableDebtTokenProxy | [0x9d6658BfDF512Bc08EdE9eF75d64A9e5d4f321de](https://sonicscan.org/address/0x9d6658BfDF512Bc08EdE9eF75d64A9e5d4f321de) |

## DEX

| Name                             | Address                                                                                                                |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| permit2                          | [0x000000000022D473030F116dDEE9F6B43aC78BA3](https://sonicscan.org/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) |
| create3Factory                   | [0xdC44dEedCb144108052C29c73dB025E69F0e12B6](https://sonicscan.org/address/0xdC44dEedCb144108052C29c73dB025E69F0e12B6) |
| vault                            | [0x15d1e1eBe0c054791D6bff6D430b7e25b18C0545](https://sonicscan.org/address/0x15d1e1eBe0c054791D6bff6D430b7e25b18C0545) |
| clPoolManager                    | [0xA3256ab552A271A16AcDfdB521B32ef82d481F43](https://sonicscan.org/address/0xA3256ab552A271A16AcDfdB521B32ef82d481F43) |
| clProtocolFeeController          | [0x2C4b558C7B1E5D11E2726f507D8d152e7c240be1](https://sonicscan.org/address/0x2C4b558C7B1E5D11E2726f507D8d152e7c240be1) |
| clPoolManagerOwnerContract       | [0x00dDC4a5a935ab4C19b310c6fD556c69B4235C02](https://sonicscan.org/address/0x00dDC4a5a935ab4C19b310c6fD556c69B4235C02) |
| clPositionManager (FEEM missing) | [0xcc08a04d9E5766c7A20FE6bb32cAa40EA0e7e9e1](https://sonicscan.org/address/0xcc08a04d9E5766c7A20FE6bb32cAa40EA0e7e9e1) |
| clPositionDescriptor             | [0x83Ff9FC474DBe927BA5BB822571e0814122655bB](https://sonicscan.org/address/0x83Ff9FC474DBe927BA5BB822571e0814122655bB) |
| clQuoter                         | [0x5f46CB668D39496b41CE8E19D6A7fE893826E363](https://sonicscan.org/address/0x5f46CB668D39496b41CE8E19D6A7fE893826E363) |
| clTickLens                       | [0xb3e77dD9b1f206A2b797B3fE900b50cC92A38d26](https://sonicscan.org/address/0xb3e77dD9b1f206A2b797B3fE900b50cC92A38d26) |
| Router (FEEM missing)            | [0x5bFB058c65E4c1DEC1cFF0Ff2cBd8522b4c3feBB](https://sonicscan.org/address/0x5bFB058c65E4c1DEC1cFF0Ff2cBd8522b4c3feBB) |
| Default Hook Implementation      | [0xd36Abc974bCB62B8ED7105b5EE58f064b1135843](https://sonicscan.org/address/0xd36Abc974bCB62B8ED7105b5EE58f064b1135843) |
| Default Hook Proxy               | [0x598448d8f8553b9c6f27E52a92E2cCf27cDEF229](https://sonicscan.org/address/0x598448d8f8553b9c6f27E52a92E2cCf27cDEF229) |
| Simlpe router                    | [0x50BE112da327301254c477d8AFa55694Dc0eFCDa](https://sonicscan.org/address/0x50BE112da327301254c477d8AFa55694Dc0eFCDa) |


# Relayer API endpoints

> **Error handling conventions:** Relay-layer failures emit one of two stable strings on `error.message`: `'SUBMIT_TX_FAILED'` or `'RELAY_TIMEOUT'`, also exported as `RELAY_ERROR_CODES` from `@sodax/sdk`. Modules other than swap propagate these errors raw. The **swap module** wraps them into `SodaxError<SwapErrorCode>` with `context.relayCode` (see [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md) Error Handling).

The intent relay service bridges spoke-chain transactions to the SODAX hub (Sonic). All cross-chain operations — swaps, bridges, money market deposits/withdrawals, staking — submit a spoke-chain transaction hash to the relay, then poll until the hub confirms execution.

## Mainnet

URL: `https://xcall-relay.nw.iconblockchain.xyz`

This is the default value of `DEFAULT_RELAYER_API_ENDPOINT` (exported from `@sodax/sdk`). It is set automatically in `relayConfig.relayerApiEndpoint` and picked up by `ConfigService` — no manual configuration is needed unless you are overriding the endpoint.

## Testnet

URL: `https://testnet-xcall-relay.nw.iconblockchain.xyz`

Pass this URL as the `relayerApiEndpoint` override in your `SodaxConfig` when targeting testnet.

***

## SDK integration

`IntentRelayApiService` (`packages/sdk/src/shared/services/intentRelay/IntentRelayApiService.ts`) is an **internal** module. Callers never construct it directly. The relay is accessed through high-level service methods:

* `sodax.swaps.createIntent(...)` — submits the spoke tx and waits for hub execution internally
* `sodax.bridge.bridge(...)` — similarly manages the full relay lifecycle
* `sodax.moneyMarket.*`, `sodax.staking.*`, and related methods do the same

All of these methods return `Promise<Result<T>>`. On relay failure the `Result` carries an error whose `message` is `'RELAY_TIMEOUT'` or `'SUBMIT_TX_FAILED'` (CODE form — see [error convention](#error-message-convention)).

***

## Chain IDs used by the relay

The relay API identifies chains by **`IntentRelayChainId`** — a `bigint` value that is distinct from the `ChainKeys.*` string keys used everywhere else in the SDK.

Use `getIntentRelayChainId(chainKey)` (exported from `@sodax/sdk`) to convert a `SpokeChainKey` to its relay chain ID. The SDK does this conversion internally; callers only need it when constructing raw relay requests directly (advanced usage).

Full mapping (`RelayChainIdMap` in `@sodax/sdk`):

| Chain key (`ChainKeys.*`) | Relay chain ID |
| ------------------------- | -------------- |
| `AVALANCHE_MAINNET`       | `6n`           |
| `ARBITRUM_MAINNET`        | `23n`          |
| `BASE_MAINNET`            | `30n`          |
| `BSC_MAINNET`             | `4n`           |
| `INJECTIVE_MAINNET`       | `19n`          |
| `SONIC_MAINNET`           | `146n`         |
| `OPTIMISM_MAINNET`        | `24n`          |
| `POLYGON_MAINNET`         | `5n`           |
| `SOLANA_MAINNET`          | `1n`           |
| `SUI_MAINNET`             | `21n`          |
| `STELLAR_MAINNET`         | `27n`          |
| `ICON_MAINNET`            | `1768124270n`  |
| `HYPEREVM_MAINNET`        | `26745n`       |
| `LIGHTLINK_MAINNET`       | `27756n`       |
| `NEAR_MAINNET`            | `15n`          |
| `ETHEREUM_MAINNET`        | `2n`           |
| `BITCOIN_MAINNET`         | `627463n`      |
| `REDBELLY_MAINNET`        | `726564n`      |
| `KAIA_MAINNET`            | `27489n`       |
| `HEDERA_MAINNET`          | `18501n`       |
| `STACKS_MAINNET`          | `60n`          |

***

## Relay API actions

All requests are JSON `POST` to the relay URL. The `action` field selects the operation.

### `submit` — submit a transaction for relaying

```ts
// TypeScript type (from IntentRelayApiService.ts)
type SubmitTxParams = {
  chain_id: string;   // relay chain ID as a decimal string (e.g. "2" for Ethereum)
  tx_hash: string;    // spoke-chain transaction hash
  data?: RelayExtraData; // required only for Solana and Bitcoin (split-tx chains)
};
```

`RelayExtraData` (`{ address: Hex; payload: Hex }`) carries the hub destination address and the full call payload. Solana and Bitcoin use split transactions: the on-chain tx stores only a verification hash; the full call data is submitted off-chain here.

```
curl --location 'https://xcall-relay.nw.iconblockchain.xyz/' \
--header 'Content-Type: application/json' \
--data '{
    "action": "submit",
    "params": {
        "chain_id": "2",
        "tx_hash": "0x882370113410cf4db551d89f2a8dc1819a2e4d9e1d5efe19068156d3ff1b91b7"
    }
}'
```

Response (`SubmitTxResponse`):

```json
{ "success": true, "message": "..." }
```

### `get_transaction_packets` — poll for relay packets by tx hash

```ts
type GetTransactionPacketsParams = {
  chain_id: string;
  tx_hash: string;
};
```

```
curl --location 'https://xcall-relay.nw.iconblockchain.xyz/' \
--header 'Content-Type: application/json' \
--data '{
    "action": "get_transaction_packets",
    "params": {
        "chain_id": "2",
        "tx_hash": "0x882370113410cf4db551d89f2a8dc1819a2e4d9e1d5efe19068156d3ff1b91b7"
    }
}'
```

Response (`GetTransactionPacketsResponse`):

```ts
type RelayTxStatus = 'pending' | 'validating' | 'executing' | 'executed';

type PacketData = {
  src_chain_id: number;
  src_tx_hash: string;
  src_address: string;
  status: RelayTxStatus;
  dst_chain_id: number;
  conn_sn: number;
  dst_address: string;
  dst_tx_hash: string;   // hub-chain tx hash — use this for subsequent solver queries
  signatures: string[];
  payload: string;
};

type GetTransactionPacketsResponse = {
  success: boolean;
  data: PacketData[];
};
```

The packet is complete when `status === 'executed'`. Use `dst_tx_hash` as the hub-chain transaction hash for subsequent solver interactions.

### `get_packet` — fetch a single packet by connection serial number

```ts
type GetPacketParams = {
  chain_id: string;
  tx_hash: string;
  conn_sn: string;   // connection serial number
};
```

```
curl --location 'https://xcall-relay.nw.iconblockchain.xyz/' \
--header 'Content-Type: application/json' \
--data '{
    "action": "get_packet",
    "params": {
        "chain_id": "2",
        "tx_hash": "0x882370113410cf4db551d89f2a8dc1819a2e4d9e1d5efe19068156d3ff1b91b7",
        "conn_sn": "169"
    }
}'
```

Response (`GetPacketResponse`):

```ts
type GetPacketResponse =
  | { success: true; data: PacketData }
  | { success: false; message: string };
```

***

## Low-level functions (advanced usage)

These are exported from `IntentRelayApiService` for callers that need direct relay access (e.g. custom orchestration, bots):

| Function                  | Signature                                                             | Description                                                                                              |
| ------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `submitTransaction`       | `(payload, apiUrl) => Promise<Result<SubmitTxResponse>>`              | Submit a tx to the relay.                                                                                |
| `getTransactionPackets`   | `(payload, apiUrl) => Promise<Result<GetTransactionPacketsResponse>>` | Fetch packets for a tx hash.                                                                             |
| `getPacket`               | `(payload, apiUrl) => Promise<Result<GetPacketResponse>>`             | Fetch a single packet by `conn_sn`.                                                                      |
| `waitUntilIntentExecuted` | `(payload) => Promise<Result<PacketData>>`                            | Poll until a packet reaches `'executed'` status or times out.                                            |
| `relayTxAndWaitPacket`    | `(params: RelayAndWaitParams) => Promise<Result<PacketData>>`         | Submit + poll in one call. Handles `getIntentRelayChainId` conversion and split-tx chains automatically. |

All functions return `Promise<Result<T>>` — no throws across service boundaries. Check `result.ok` before using `result.value`. On failure, `result.error` is an `Error` instance:

* `result.error.message === 'RELAY_TIMEOUT'` — packet did not arrive within the timeout (default: 120 000 ms)
* `result.error.message === 'SUBMIT_TX_FAILED'` — the relay rejected the submission; check `result.error.cause.message` for the relay's rejection reason
* `result.error.message === 'HTTP_REQUEST_FAILED'` — network-level failure; check `result.error.cause` for details

`RelayAndWaitParams`:

```ts
type RelayAndWaitParams = {
  srcTxHash: string;
  data: RelayExtraData;         // required for Solana/Bitcoin; ignored for all other chains
  chainKey: SpokeChainKey;      // e.g. ChainKeys.ETHEREUM_MAINNET
  relayerApiEndpoint: HttpUrl;  // relay base URL
  timeout: number | undefined;  // ms; defaults to DEFAULT_RELAY_TX_TIMEOUT (120 000 ms)
};
```

Example:

```ts
import { relayTxAndWaitPacket, getIntentRelayChainId } from '@sodax/sdk';
import { ChainKeys, DEFAULT_RELAY_TX_TIMEOUT } from '@sodax/sdk';

// relayData comes from the preceding spoke operation (createIntent / createBridgeIntent).
// It is always required — for non-Solana/Bitcoin chains it is accepted but not forwarded
// to the relay submit payload.
const result = await relayTxAndWaitPacket({
  srcTxHash: '0x...',
  data: relayData,  // RelayExtraData from the preceding spoke operation
  chainKey: ChainKeys.ETHEREUM_MAINNET,
  relayerApiEndpoint: 'https://xcall-relay.nw.iconblockchain.xyz',
  timeout: DEFAULT_RELAY_TX_TIMEOUT,
});

if (!result.ok) {
  if (result.error instanceof Error && result.error.message === 'RELAY_TIMEOUT') {
    // timed out waiting for hub execution
  }
  return;
}

const hubTxHash = result.value.dst_tx_hash;
```


# Solver API endpoints

> **Error handling conventions:** Direct callers of `SolverApiService` (used by lower-level scripts and tests) still receive `SolverErrorResponse` with `detail.code` / `detail.message`. The **swap module's** `postExecution` wraps these into `SodaxError` with code `EXTERNAL_API_ERROR`; the original `SolverIntentErrorCode` is on `result.error.context.solverCode` and the full `detail` is on `result.error.context.solverDetail` — see [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md) Error Handling.

## Mainnet production

URL: <https://api.sodax.com/v1/intent>

### Mainnet staging

URL: [https://staging-new-world.iconblockchain.xyz](https://staging-new-world.iconblockchain.xyz/)

**Note** Staging endpoint contains features to be potentially released and is subject to frequent change!

***

## Overview

The solver API drives the intent-based swap feature. `SwapService` (accessed via `sodax.swaps`) is the public entry point — it delegates all HTTP communication to the stateless `SolverApiService` class. External callers should use `SwapService` rather than calling `SolverApiService` directly.

Three endpoints are exposed:

| Endpoint   | Method | Purpose                                                   |
| ---------- | ------ | --------------------------------------------------------- |
| `/quote`   | `POST` | Get a price quote for a token pair and amount             |
| `/execute` | `POST` | Notify the solver that an intent is live on the hub chain |
| `/status`  | `POST` | Poll the execution status of a submitted intent           |

***

## Error handling

All three solver methods return `Promise<Result<T, SolverErrorResponse>>`. On HTTP errors or network failures, `result.ok` is `false` and `result.error` is a `SolverErrorResponse`:

```ts
type SolverErrorResponse = {
  detail: {
    code: SolverIntentErrorCode;
    message: string;
  };
};
```

`SolverIntentErrorCode` is an enum defined in `@sodax/sdk`. On unhandled exceptions the code is `SolverIntentErrorCode.UNKNOWN`.

To branch on solver errors, inspect `result.error.detail.code`:

```ts
import { SolverIntentErrorCode } from '@sodax/sdk';

const quoteResult = await sodax.swaps.getQuote(payload);
if (!quoteResult.ok) {
  if (quoteResult.error.detail.code === SolverIntentErrorCode.UNKNOWN) {
    // network / unexpected failure
  }
}
```

***

## `POST /quote` — Get a price quote

Called via `SwapService.getQuote(payload)`.

### Request (`SolverIntentQuoteRequest`)

| Field                     | Type     | Description                                                 |
| ------------------------- | -------- | ----------------------------------------------------------- |
| `token_src`               | `string` | Source token address on its spoke chain                     |
| `token_dst`               | `string` | Destination token address on its spoke chain                |
| `token_src_blockchain_id` | `string` | Source spoke chain relay ID (e.g. `'0x38.bsc'`)             |
| `token_dst_blockchain_id` | `string` | Destination spoke chain relay ID (e.g. `'0xa4b1.arbitrum'`) |
| `amount`                  | `bigint` | Input amount in the source token's smallest unit            |
| `quote_type`              | `string` | `'exact_input'` or `'exact_output'`                         |

`SwapService.getQuote` automatically adjusts `amount` by the configured partner fee before forwarding to the solver, so the returned `quoted_amount` reflects the net output the user receives.

Token addresses are validated against the active `ConfigService` and translated to their hub (Sonic) equivalents before the request is sent.

### Response (`SolverIntentQuoteResponse`)

```ts
{ quoted_amount: bigint }
```

`quoted_amount` is in the destination token's smallest unit.

### Example

```ts
import { ChainKeys } from '@sodax/sdk';

const quoteResult = await sodax.swaps.getQuote({
  token_src: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
  token_dst: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
  token_src_blockchain_id: '0x38.bsc',
  token_dst_blockchain_id: '0xa4b1.arbitrum',
  amount: 1_000_000_000_000_000n,
  quote_type: 'exact_input',
});

if (quoteResult.ok) {
  console.log('Quoted amount:', quoteResult.value.quoted_amount);
}
```

***

## `POST /execute` — Notify solver of a live intent

Called via `SwapService.postExecution(request)`. Invoked automatically by `SwapService.swap()` after the relay packet lands on the hub — call this manually only when orchestrating swap steps yourself.

### Request (`SolverExecutionRequest`)

| Field            | Type  | Description                                                        |
| ---------------- | ----- | ------------------------------------------------------------------ |
| `intent_tx_hash` | `Hex` | Hub-chain (Sonic) transaction hash where the intent was registered |

The request is retried automatically on transient network failures.

### Response (`SolverExecutionResponse`)

```ts
{ answer: 'OK'; intent_hash: Hex }
```

### Example

```ts
const execResult = await sodax.swaps.postExecution({
  intent_tx_hash: '0xabc123…',
});

if (execResult.ok) {
  console.log('Intent hash:', execResult.value.intent_hash);
}
```

***

## `POST /status` — Poll intent execution status

Called via `SwapService.getStatus(request)`.

### Request (`SolverIntentStatusRequest`)

| Field            | Type  | Description                                                                                                                              |
| ---------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `intent_tx_hash` | `Hex` | Hub-chain (Sonic) tx hash of the intent. This is the `dst_tx_hash` from the relay packet returned by `swap()` or `relayTxAndWaitPacket`. |

### Response (`SolverIntentStatusResponse`)

| Field          | Type                     | Description                                                                              |
| -------------- | ------------------------ | ---------------------------------------------------------------------------------------- |
| `status`       | `SolverIntentStatusCode` | Numeric status code (see below)                                                          |
| `fill_tx_hash` | `string \| undefined`    | Solver's fill tx hash — present only when `status === SolverIntentStatusCode.SOLVED (3)` |

`SolverIntentStatusCode` is an enum in `@sodax/sdk`. The value `3` (`SOLVED`) indicates the solver has filled the intent.

### Example

```ts
import { SolverIntentStatusCode } from '@sodax/sdk';

const statusResult = await sodax.swaps.getStatus({
  intent_tx_hash: '0xabc123…',
});

if (statusResult.ok && statusResult.value.status === SolverIntentStatusCode.SOLVED) {
  const fillTxHash = statusResult.value.fill_tx_hash;
  // use getSolvedIntentPacket to wait for delivery on the destination chain
  const packetResult = await sodax.swaps.getSolvedIntentPacket({
    chainId: ChainKeys.ARBITRUM_MAINNET,
    fillTxHash,
  });
}
```

***

## Full swap flow

`SwapService.swap()` orchestrates the complete lifecycle. The steps below show what happens internally and where each solver endpoint is called:

```
1. createIntent()         → spoke chain tx (deposit / send message)
2. verifyTxHash()         → confirm spoke tx is on-chain
3. relayTxAndWaitPacket() → relay spoke tx to hub (skipped when srcChainKey is the hub)
4. postExecution()        → POST /execute  (notify solver; uses hub dst_tx_hash)
```

Polling intent status and waiting for fill delivery are separate steps the caller performs after `swap()` returns:

```
5. getStatus()            → POST /status  (poll until SOLVED)
6. getSolvedIntentPacket()→ wait for fill relay packet on destination chain
```

### Complete example

```ts
import { ChainKeys, SolverIntentStatusCode } from '@sodax/sdk';

// 1. Execute the swap (steps 1–4 above)
const swapResult = await sodax.swaps.swap({
  params: {
    srcChainKey: ChainKeys.BSC_MAINNET,
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    inputToken: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
    outputToken: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
    inputAmount: 1_000_000_000_000_000n,
    minOutputAmount: 900_000_000_000_000n,
    srcAddress: '0xYourAddress',
    dstAddress: '0xYourAddress',
    deadline: await sodax.swaps.getSwapDeadline(),
    allowPartialFill: false,
  },
  walletProvider: evmWalletProvider,  // IEvmWalletProvider — narrows from srcChainKey
});

if (!swapResult.ok) {
  // result.error.message is a phase tag: 'POST_EXECUTION_FAILED' | 'RELAY_TIMEOUT'
  // result.error.cause holds the underlying error
  console.error(swapResult.error);
  return;
}

const { intentDeliveryInfo } = swapResult.value;

// 2. Poll until the solver fills the intent (step 5)
let fillTxHash: string | undefined;
while (!fillTxHash) {
  const statusResult = await sodax.swaps.getStatus({
    intent_tx_hash: intentDeliveryInfo.dstTxHash as `0x${string}`,
  });
  if (statusResult.ok && statusResult.value.status === SolverIntentStatusCode.SOLVED) {
    fillTxHash = statusResult.value.fill_tx_hash;
  } else {
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
}

// 3. Wait for the fill to land on the destination chain (step 6)
const packetResult = await sodax.swaps.getSolvedIntentPacket({
  chainId: ChainKeys.ARBITRUM_MAINNET,
  fillTxHash,
});

if (packetResult.ok) {
  console.log('Destination tx:', packetResult.value.dst_tx_hash);
}
```

***

## Chain keys

Use `ChainKeys.*` from `@sodax/sdk` for all chain references. `SpokeChainKey` is the union of `ChainKeys` values. `XToken.chainKey` (not `xChainId`) carries the chain key on token objects.

```ts
import { ChainKeys } from '@sodax/sdk';

ChainKeys.SONIC_MAINNET       // hub chain
ChainKeys.ARBITRUM_MAINNET
ChainKeys.BSC_MAINNET
// … see CHAIN_ID_MIGRATION.md for full mapping from old *_CHAIN_ID constants
```

`Intent.srcChain` and `Intent.dstChain` are `bigint` relay chain IDs (not chain keys) — use `getIntentRelayChainId(chainKey)` from `@sodax/sdk` to convert between them.

***

## Related source files

* `packages/sdk/src/swap/SolverApiService.ts` — stateless HTTP client for the three solver endpoints
* `packages/sdk/src/swap/SwapService.ts` — public service facade; use `sodax.swaps`
* `packages/sdk/src/swap/EvmSolverService.ts` — EVM-level intent ABI encoding/decoding and event parsing
* `packages/sdk/docs/SWAPS.md` — full swap feature documentation
* `packages/sdk/docs/ARCHITECTURE_REFACTOR_SUMMARY.md` — v2 architecture reference (chain keys, `Result<T>`, error convention)
* `packages/sdk/CHAIN_ID_MIGRATION.md` — mapping from old `*_CHAIN_ID` constants to `ChainKeys.*`


# xCall Scanner

### Mainnet

URL: <https://xcall-scan.nw.iconblockchain.xyz>

### Testnet

URL: <https://testnet-xcall-scan.nw.iconblockchain.xyz>


# Swaps: Compatible Assets

Assets (tokens) supported by mainnet solver (swaps).

Generated: 2026-06-18 02:12 UTC

There are two solver environments on the SODAX marketplace. The **staging** solver supports every production token **plus** the additional staging-only tokens listed below.

**Legend:** ✅ supported in **production** · 🚧 **staging-only** (not yet live on the production solver).

Tokens are grouped by their spoke chain; the address shown is the token's **hub asset address on Sonic** (links to sonicscan.org).

Oracles: production `https://sodax-solver.iconblockchain.xyz/oracle` · staging `https://sodax-solver-staging.iconblockchain.xyz/oracle`.

### Supported Swap Tokens

#### Sonic

| Token         | Hub asset (Sonic)                                                                                                        |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ S           | [`0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38`](https://sonicscan.org/address/0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38) |
| ✅ WETH        | [`0x50c42dEAcD8Fc9773493ED674b675bE577f2634b`](https://sonicscan.org/address/0x50c42dEAcD8Fc9773493ED674b675bE577f2634b) |
| ✅ USDC        | [`0x29219dd400f2Bf60E5a23d13Be72B486D4038894`](https://sonicscan.org/address/0x29219dd400f2Bf60E5a23d13Be72B486D4038894) |
| ✅ USDT        | [`0x6047828dc181963ba44974801FF68e538dA5eaF9`](https://sonicscan.org/address/0x6047828dc181963ba44974801FF68e538dA5eaF9) |
| ✅ wS          | [`0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38`](https://sonicscan.org/address/0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38) |
| ✅ SODA        | [`0x7c7d53EEcda37a87ce0D5bf8E0b24512A48dC963`](https://sonicscan.org/address/0x7c7d53EEcda37a87ce0D5bf8E0b24512A48dC963) |
| ✅ sodaBNB     | [`0x40Cd41b35DB9e5109ae7E54b44De8625dB320E6b`](https://sonicscan.org/address/0x40Cd41b35DB9e5109ae7E54b44De8625dB320E6b) |
| ✅ sodaAVAX    | [`0x14238D267557E9d799016ad635B53CD15935d290`](https://sonicscan.org/address/0x14238D267557E9d799016ad635B53CD15935d290) |
| ✅ sodaETH     | [`0x4effB5813271699683C25c734F4daBc45B363709`](https://sonicscan.org/address/0x4effB5813271699683C25c734F4daBc45B363709) |
| ✅ sodaBTC     | [`0x7A1A5555842Ad2D0eD274d09b5c4406a95799D5d`](https://sonicscan.org/address/0x7A1A5555842Ad2D0eD274d09b5c4406a95799D5d) |
| ✅ sodaWBTC    | [`0x811C3fCc13f9c2a23AE2Ae2DCadacFAC6eb5f0eB`](https://sonicscan.org/address/0x811C3fCc13f9c2a23AE2Ae2DCadacFAC6eb5f0eB) |
| ✅ sodaSOL     | [`0xdEa692287E2cE8Cb08FA52917Be0F16b1DACDC87`](https://sonicscan.org/address/0xdEa692287E2cE8Cb08FA52917Be0F16b1DACDC87) |
| ✅ sodaXLM     | [`0x6BC8C37cba91F76E68C9e6d689A9C21E4d32079B`](https://sonicscan.org/address/0x6BC8C37cba91F76E68C9e6d689A9C21E4d32079B) |
| ✅ sodaINJ     | [`0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13`](https://sonicscan.org/address/0x1f22279C89B213944b7Ea41daCB0a868DdCDFd13) |
| ✅ sodaSUI     | [`0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3`](https://sonicscan.org/address/0xdc5B4b00F98347E95b9F94911213DAB4C687e1e3) |
| ✅ bnUSD       | [`0xE801CA34E19aBCbFeA12025378D19c4FBE250131`](https://sonicscan.org/address/0xE801CA34E19aBCbFeA12025378D19c4FBE250131) |
| ✅ sodaUSDC    | [`0xAbbb91c0617090F0028BDC27597Cd0D038F3A833`](https://sonicscan.org/address/0xAbbb91c0617090F0028BDC27597Cd0D038F3A833) |
| ✅ sodaUSDT    | [`0xbDf1F453FCB61424011BBDDCB96cFDB30f3Fe876`](https://sonicscan.org/address/0xbDf1F453FCB61424011BBDDCB96cFDB30f3Fe876) |
| ✅ IbnUSD      | [`0x9D4b663Eb075d2a1C7B8eaEFB9eCCC0510388B51`](https://sonicscan.org/address/0x9D4b663Eb075d2a1C7B8eaEFB9eCCC0510388B51) |
| ✅ sodaS       | [`0x62ecc3Eeb80a162c57624B3fF80313FE69f5203e`](https://sonicscan.org/address/0x62ecc3Eeb80a162c57624B3fF80313FE69f5203e) |
| ✅ sodaPOL     | [`0x208ED38f4783328aA9eBFeC360D32e7520A9B779`](https://sonicscan.org/address/0x208ED38f4783328aA9eBFeC360D32e7520A9B779) |
| ✅ sodaSODA    | [`0x21685E341DE7844135329914Be6Bd8D16982d834`](https://sonicscan.org/address/0x21685E341DE7844135329914Be6Bd8D16982d834) |
| ✅ sodaHYPE    | [`0x6E81124fC5d2Bf666B16a0A5d90066eBf35c7411`](https://sonicscan.org/address/0x6E81124fC5d2Bf666B16a0A5d90066eBf35c7411) |
| ✅ sodaRBNT    | [`0x4B207114F9118dEAC56436e1aE3c45648783c7Ac`](https://sonicscan.org/address/0x4B207114F9118dEAC56436e1aE3c45648783c7Ac) |
| ✅ sodaLL      | [`0x14C5eB2D25dFb834852dFc85744875d1eCb09748`](https://sonicscan.org/address/0x14C5eB2D25dFb834852dFc85744875d1eCb09748) |
| ✅ sodaWEETH   | [`0xCb6B152D3a943f25157381aFcA7fEFCD2ef5a357`](https://sonicscan.org/address/0xCb6B152D3a943f25157381aFcA7fEFCD2ef5a357) |
| ✅ sodaWSTETH  | [`0x58b0538D7EEaeE69EF32f9F1dE5cbF32A10a977B`](https://sonicscan.org/address/0x58b0538D7EEaeE69EF32f9F1dE5cbF32A10a977B) |
| ✅ sodaNEAR    | [`0xf4ba497c9b805e4bd88a8a9e6a7b8f74984c3e39`](https://sonicscan.org/address/0xf4ba497c9b805e4bd88a8a9e6a7b8f74984c3e39) |
| ✅ sodaKAIA    | [`0xD7d41b5f803b6A40F8A6eAa34E459A4564e39891`](https://sonicscan.org/address/0xD7d41b5f803b6A40F8A6eAa34E459A4564e39891) |
| ✅ sodaSTX     | [`0x1Fbe5229e9d189F26bEE77E5bFa24309FdA90483`](https://sonicscan.org/address/0x1Fbe5229e9d189F26bEE77E5bFa24309FdA90483) |
| ✅ lsodaWEETH  | [`0xD09de2f5070699A909c0FD32fb5A909d3886701D`](https://sonicscan.org/address/0xD09de2f5070699A909c0FD32fb5A909d3886701D) |
| ✅ lsodaWSTETH | [`0x136E5D1CEC5db1829E24941Eddd9C8640E02Ce7a`](https://sonicscan.org/address/0x136E5D1CEC5db1829E24941Eddd9C8640E02Ce7a) |
| 🚧 sodaUSDS   | [`0x243b0c26c8b38793908d7C64e8510f21B19B4613`](https://sonicscan.org/address/0x243b0c26c8b38793908d7C64e8510f21B19B4613) |

#### Avalanche

| Token    | Hub asset (Sonic)                                                                                                        |
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ AVAX   | [`0xc9e4f0B6195F389D9d2b639f2878B7674eB9D8cD`](https://sonicscan.org/address/0xc9e4f0B6195F389D9d2b639f2878B7674eB9D8cD) |
| ✅ USDT   | [`0x41Fd5c169e014e2A657B9de3553f7a7b735Fe47A`](https://sonicscan.org/address/0x41Fd5c169e014e2A657B9de3553f7a7b735Fe47A) |
| ✅ USDC   | [`0x41abF4B1559FF709Ef8150079BcB26DB1Fffd117`](https://sonicscan.org/address/0x41abF4B1559FF709Ef8150079BcB26DB1Fffd117) |
| ✅ bnUSD  | [`0x289cDa1043b4Ce26BDCa3c12E534f56b24308A5B`](https://sonicscan.org/address/0x289cDa1043b4Ce26BDCa3c12E534f56b24308A5B) |
| ✅ SODA   | [`0xf51d7082375cdca8C19C74e1A0c77dA482aFDa4e`](https://sonicscan.org/address/0xf51d7082375cdca8C19C74e1A0c77dA482aFDa4e) |
| ✅ WETH.e | [`0x3492987e8c83e1628ce8ff48f5ae127105df304a`](https://sonicscan.org/address/0x3492987e8c83e1628ce8ff48f5ae127105df304a) |

#### Arbitrum

| Token    | Hub asset (Sonic)                                                                                                        |
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ ETH    | [`0xdcd9578b51ef55239b6e68629d822a8d97c95b86`](https://sonicscan.org/address/0xdcd9578b51ef55239b6e68629d822a8d97c95b86) |
| ✅ bnUSD  | [`0x419cA9054E44E94ceAb52846eCdC3997439BBcA6`](https://sonicscan.org/address/0x419cA9054E44E94ceAb52846eCdC3997439BBcA6) |
| ✅ WBTC   | [`0xfB0ACB1b2720B620935F50a6dd3F7FEA52b2FCBe`](https://sonicscan.org/address/0xfB0ACB1b2720B620935F50a6dd3F7FEA52b2FCBe) |
| ✅ weETH  | [`0x08D5cf039De35627fD5C0f48B8AF4a1647a462E8`](https://sonicscan.org/address/0x08D5cf039De35627fD5C0f48B8AF4a1647a462E8) |
| ✅ wstETH | [`0x2D5A7837D68b0c2CC4b14C2af2a1F0Ef420DDDc5`](https://sonicscan.org/address/0x2D5A7837D68b0c2CC4b14C2af2a1F0Ef420DDDc5) |
| ✅ tBTC   | [`0x96Fc8540736f1598b7E235e6dE8814062b3b5d3B`](https://sonicscan.org/address/0x96Fc8540736f1598b7E235e6dE8814062b3b5d3B) |
| ✅ USDC   | [`0xdB7BdA65c3a1C51D64dC4444e418684677334109`](https://sonicscan.org/address/0xdB7BdA65c3a1C51D64dC4444e418684677334109) |
| ✅ USDT   | [`0x3C0a80C6a1110fC80309382b3989eC626c135eE9`](https://sonicscan.org/address/0x3C0a80C6a1110fC80309382b3989eC626c135eE9) |
| ✅ ARB    | [`0xe93d74056c7e431aaa33155c9c994116c562e5e2`](https://sonicscan.org/address/0xe93d74056c7e431aaa33155c9c994116c562e5e2) |
| ✅ AAVE   | [`0x33e8d19d2ae8b6f81919e5458478caa2617b7f96`](https://sonicscan.org/address/0x33e8d19d2ae8b6f81919e5458478caa2617b7f96) |
| ✅ LINK   | [`0x68908cf348e8c1d672180e1e6472b2661cdec34b`](https://sonicscan.org/address/0x68908cf348e8c1d672180e1e6472b2661cdec34b) |
| ✅ UNI    | [`0x0f1368a9819aafad5be65ff779c02b86770628ab`](https://sonicscan.org/address/0x0f1368a9819aafad5be65ff779c02b86770628ab) |
| ✅ CRV    | [`0x82629407a43156cdef1e03ac0601ce2422fc8c2e`](https://sonicscan.org/address/0x82629407a43156cdef1e03ac0601ce2422fc8c2e) |
| ✅ PENDLE | [`0xd1d850a2f30178d4fc4b490b330bc7ca05c5ddde`](https://sonicscan.org/address/0xd1d850a2f30178d4fc4b490b330bc7ca05c5ddde) |
| ✅ rETH   | [`0xd8270661d8bdc7c6d80c2a04f1d9775ea5db0553`](https://sonicscan.org/address/0xd8270661d8bdc7c6d80c2a04f1d9775ea5db0553) |
| ✅ SODA   | [`0x66be65dd6d2401c179a2f7ab2d5cdfb040567727`](https://sonicscan.org/address/0x66be65dd6d2401c179a2f7ab2d5cdfb040567727) |

#### Base

| Token     | Hub asset (Sonic)                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ ETH     | [`0x70178089842be7f8e4726b33f0d1569db8021faa`](https://sonicscan.org/address/0x70178089842be7f8e4726b33f0d1569db8021faa) |
| ✅ weETH   | [`0x55e0Ad45eB97493B3045eEE417fb6726CB85dfd4`](https://sonicscan.org/address/0x55e0Ad45eB97493B3045eEE417fb6726CB85dfd4) |
| ✅ USDC    | [`0x72E852545B024ddCbc5b70C1bCBDAA025164259C`](https://sonicscan.org/address/0x72E852545B024ddCbc5b70C1bCBDAA025164259C) |
| ✅ wstETH  | [`0x494aaEaEfDF5964d4Ed400174e8c5b98C00957aA`](https://sonicscan.org/address/0x494aaEaEfDF5964d4Ed400174e8c5b98C00957aA) |
| ✅ cbBTC   | [`0x2803a23a3BA6b09e57D1c71deC0D9eFdBB00A27F`](https://sonicscan.org/address/0x2803a23a3BA6b09e57D1c71deC0D9eFdBB00A27F) |
| ✅ VIRTUAL | [`0xade9366b9e6765b5fc2034aa03f388ea0c980088`](https://sonicscan.org/address/0xade9366b9e6765b5fc2034aa03f388ea0c980088) |
| ✅ cbETH   | [`0xaa81bb5ba3c972ab53605d3ba7e443a3de6d1cb9`](https://sonicscan.org/address/0xaa81bb5ba3c972ab53605d3ba7e443a3de6d1cb9) |
| ✅ SODA    | [`0x17fF8Ad5EBe6CA8B15751067cD0c89f0E580CD17`](https://sonicscan.org/address/0x17fF8Ad5EBe6CA8B15751067cD0c89f0E580CD17) |
| 🚧 bnUSD  | [`0xDF5639D91359866f266b56D60d98edE9fEEDd100`](https://sonicscan.org/address/0xDF5639D91359866f266b56D60d98edE9fEEDd100) |
| 🚧 AERO   | [`0x348e086ab6f424dfc575a325c28624ed23973173`](https://sonicscan.org/address/0x348e086ab6f424dfc575a325c28624ed23973173) |

#### Optimism

| Token    | Hub asset (Sonic)                                                                                                        |
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ ETH    | [`0xad332860dd3b6f0e63f4f66e9457900917ac78cd`](https://sonicscan.org/address/0xad332860dd3b6f0e63f4f66e9457900917ac78cd) |
| ✅ USDC   | [`0xb7C213CbD24967dE9838fa014668FDDB338f724B`](https://sonicscan.org/address/0xb7C213CbD24967dE9838fa014668FDDB338f724B) |
| ✅ wstETH | [`0x61e26f611090CdC6bc79A7Bf156b0fD10f1fC212`](https://sonicscan.org/address/0x61e26f611090CdC6bc79A7Bf156b0fD10f1fC212) |
| ✅ USDT   | [`0xc168067d95109003805aC865ae556e8476DC69bc`](https://sonicscan.org/address/0xc168067d95109003805aC865ae556e8476DC69bc) |
| ✅ SODA   | [`0x0eD0d274dC77ef460DC96b9fBaFF3EDB074e0471`](https://sonicscan.org/address/0x0eD0d274dC77ef460DC96b9fBaFF3EDB074e0471) |
| ✅ OP     | [`0x2805549d170406be1aa2eb665110bec612495f7e`](https://sonicscan.org/address/0x2805549d170406be1aa2eb665110bec612495f7e) |
| ✅ WBTC   | [`0xd08375176410febee9cf4591532455a4cdfe4463`](https://sonicscan.org/address/0xd08375176410febee9cf4591532455a4cdfe4463) |
| ✅ bnUSD  | [`0x238384AE2b4F0EC189ecB5031859bA306B2679c5`](https://sonicscan.org/address/0x238384AE2b4F0EC189ecB5031859bA306B2679c5) |

#### Polygon

| Token     | Hub asset (Sonic)                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ POL     | [`0x9ee17486571917837210824b0d4cadfe3b324d12`](https://sonicscan.org/address/0x9ee17486571917837210824b0d4cadfe3b324d12) |
| ✅ bnUSD   | [`0x18f85f9E80ff9496EeBD5979a051AF16Ce751567`](https://sonicscan.org/address/0x18f85f9E80ff9496EeBD5979a051AF16Ce751567) |
| ✅ USDC    | [`0xa36893ba308b332FDEbfa95916D1dF3a2e3CF8B3`](https://sonicscan.org/address/0xa36893ba308b332FDEbfa95916D1dF3a2e3CF8B3) |
| ✅ SODA    | [`0x8D78A620E009Ba751Eb40d77A5e9Db48A3F2016b`](https://sonicscan.org/address/0x8D78A620E009Ba751Eb40d77A5e9Db48A3F2016b) |
| ✅ WBTC    | [`0xf3182ae79be69d9e0cad5da8273d4300a7d585d3`](https://sonicscan.org/address/0xf3182ae79be69d9e0cad5da8273d4300a7d585d3) |
| ✅ AAVE    | [`0x70c28df02916ec15e4146523bd704cc9ec1fa01e`](https://sonicscan.org/address/0x70c28df02916ec15e4146523bd704cc9ec1fa01e) |
| ✅ LINK    | [`0x30d1be6ae96e9832674f1fcf429c5176bd891e1f`](https://sonicscan.org/address/0x30d1be6ae96e9832674f1fcf429c5176bd891e1f) |
| ✅ DAI     | [`0x4416e9b2f1a7fb153663b56d6bf5c47d565ddeb6`](https://sonicscan.org/address/0x4416e9b2f1a7fb153663b56d6bf5c47d565ddeb6) |
| 🚧 USDT   | [`0xc5dfa58a0f80646d1067d894920acd870ff1888b`](https://sonicscan.org/address/0xc5dfa58a0f80646d1067d894920acd870ff1888b) |
| 🚧 wstETH | [`0xa95e972dbff57f3b561763ae88fd5f235a8f2711`](https://sonicscan.org/address/0xa95e972dbff57f3b561763ae88fd5f235a8f2711) |

#### BNB Chain

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ BNB   | [`0x13b70564b1ec12876b20fab5d1bb630311312f4f`](https://sonicscan.org/address/0x13b70564b1ec12876b20fab5d1bb630311312f4f) |
| ✅ ETHB  | [`0x57fC2aC5701e463ae261AdBd6C99FBeB48Ce5293`](https://sonicscan.org/address/0x57fC2aC5701e463ae261AdBd6C99FBeB48Ce5293) |
| ✅ BTCB  | [`0xD8A24c71FEa5bB81c66C01e532dE7d9B11e13905`](https://sonicscan.org/address/0xD8A24c71FEa5bB81c66C01e532dE7d9B11e13905) |
| ✅ bnUSD | [`0x5Ce6C1c51ff762cF3acD21396257046f694168b6`](https://sonicscan.org/address/0x5Ce6C1c51ff762cF3acD21396257046f694168b6) |
| ✅ USDC  | [`0x9d58508ad10d34048a11640735ca5075bba07b35`](https://sonicscan.org/address/0x9d58508ad10d34048a11640735ca5075bba07b35) |
| ✅ USDT  | [`0x627c7369e9cfb99e11ee7163efb23f12bfd8cce6`](https://sonicscan.org/address/0x627c7369e9cfb99e11ee7163efb23f12bfd8cce6) |
| ✅ SODA  | [`0x4d12c72A8633588097D10e57b559ed642588e4C6`](https://sonicscan.org/address/0x4d12c72A8633588097D10e57b559ed642588e4C6) |
| ✅ CAKE  | [`0xb08585ed250718906d1be395650f47a5d4746ef3`](https://sonicscan.org/address/0xb08585ed250718906d1be395650f47a5d4746ef3) |
| ✅ FDUSD | [`0x052dfbf50514197e540b2f631b81702f3eab3bdb`](https://sonicscan.org/address/0x052dfbf50514197e540b2f631b81702f3eab3bdb) |
| ✅ USD1  | [`0x813d8d9d9e519eae90cc13a50154c40f567e1f1f`](https://sonicscan.org/address/0x813d8d9d9e519eae90cc13a50154c40f567e1f1f) |
| ✅ ASTER | [`0x1239f4073bc237330d472e442801efc6db0c348c`](https://sonicscan.org/address/0x1239f4073bc237330d472e442801efc6db0c348c) |
| ✅ XRP   | [`0x3f4e092a73ed4e5f0c08c62c95059a9572288331`](https://sonicscan.org/address/0x3f4e092a73ed4e5f0c08c62c95059a9572288331) |
| ✅ ADA   | [`0x40fbca9d5d0c298ceb95b15ef2e5dcd7fe573e2a`](https://sonicscan.org/address/0x40fbca9d5d0c298ceb95b15ef2e5dcd7fe573e2a) |
| ✅ DOGE  | [`0x2035dd517ffc6713ce588773dee317eb196c21ba`](https://sonicscan.org/address/0x2035dd517ffc6713ce588773dee317eb196c21ba) |
| ✅ SOL   | [`0x407241e195212edc001d5ecb35c3d3026f5c35bf`](https://sonicscan.org/address/0x407241e195212edc001d5ecb35c3d3026f5c35bf) |
| ✅ DOT   | [`0xc2ada401337fa0d78db9a59d5b7d1797878df2fd`](https://sonicscan.org/address/0xc2ada401337fa0d78db9a59d5b7d1797878df2fd) |
| ✅ LINK  | [`0x63c29285bfda811899ec09d311856df9de37950d`](https://sonicscan.org/address/0x63c29285bfda811899ec09d311856df9de37950d) |

#### Hyper

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ HYPE  | [`0x7288622bc2D39553f34D5b81c88c3F979d91dbc7`](https://sonicscan.org/address/0x7288622bc2D39553f34D5b81c88c3F979d91dbc7) |
| ✅ bnUSD | [`0x19feaf3043dfa69b365d05495630e840a2b9a9dc`](https://sonicscan.org/address/0x19feaf3043dfa69b365d05495630e840a2b9a9dc) |
| ✅ SODA  | [`0x4cf5ce9594aeddc5d3efe9d4cdf0b944b4e73a53`](https://sonicscan.org/address/0x4cf5ce9594aeddc5d3efe9d4cdf0b944b4e73a53) |
| ✅ USDC  | [`0x0f78b995d113712deeb17d96638e9d7525d409c6`](https://sonicscan.org/address/0x0f78b995d113712deeb17d96638e9d7525d409c6) |
| ✅ USDT0 | [`0x87f73744d919447e6f8220f268bcbbfa0cbfafa3`](https://sonicscan.org/address/0x87f73744d919447e6f8220f268bcbbfa0cbfafa3) |
| ✅ UBTC  | [`0xa7debbd73a0259f32b11803ddf1da7e6b44b2a38`](https://sonicscan.org/address/0xa7debbd73a0259f32b11803ddf1da7e6b44b2a38) |
| ✅ UETH  | [`0x91a924d75f89fd4a77cd89b311b8c62507057ebd`](https://sonicscan.org/address/0x91a924d75f89fd4a77cd89b311b8c62507057ebd) |
| ✅ kHYPE | [`0x4858a9f1050c89b14665e2c75aaf216e37fe4daf`](https://sonicscan.org/address/0x4858a9f1050c89b14665e2c75aaf216e37fe4daf) |
| ✅ USDH  | [`0xacf9085e5b805a5e18fb637c2c3a02586d7289af`](https://sonicscan.org/address/0xacf9085e5b805a5e18fb637c2c3a02586d7289af) |

#### LightLink

| Token      | Hub asset (Sonic)                                                                                                        |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ ETH      | [`0x19920ef8fe1a9d51fdb0914abbb2f970c74dca68`](https://sonicscan.org/address/0x19920ef8fe1a9d51fdb0914abbb2f970c74dca68) |
| ✅ bnUSD    | [`0x14ab2ab7a76838b9205488efc3f700d0632ce8c7`](https://sonicscan.org/address/0x14ab2ab7a76838b9205488efc3f700d0632ce8c7) |
| ✅ SODA     | [`0xe4faab621fb6716e32057b7ea7356219936519ac`](https://sonicscan.org/address/0xe4faab621fb6716e32057b7ea7356219936519ac) |
| ✅ USDC     | [`0xe5159e2a89aeec2fc63d6c25adda3541670af101`](https://sonicscan.org/address/0xe5159e2a89aeec2fc63d6c25adda3541670af101) |
| ✅ BTC.LL   | [`0x54582cdb0c145398b532e34f0851ffc1bbc88b4e`](https://sonicscan.org/address/0x54582cdb0c145398b532e34f0851ffc1bbc88b4e) |
| ✅ AVAX.LL  | [`0x415ff3ea8c905ef65cd82665c5534d0e4ff9221a`](https://sonicscan.org/address/0x415ff3ea8c905ef65cd82665c5534d0e4ff9221a) |
| ✅ BNB.LL   | [`0xd32e524da9d64db4c37a50f1ee197b591e97dc05`](https://sonicscan.org/address/0xd32e524da9d64db4c37a50f1ee197b591e97dc05) |
| ✅ SOL.LL   | [`0xd70c16f1e2c4207dc4c44bc69c8a1b82b50cf9e2`](https://sonicscan.org/address/0xd70c16f1e2c4207dc4c44bc69c8a1b82b50cf9e2) |
| ✅ XLM.LL   | [`0x18008b18964bc2c90521a7596ff9988743c97fd4`](https://sonicscan.org/address/0x18008b18964bc2c90521a7596ff9988743c97fd4) |
| ✅ INJ.LL   | [`0xb036daab0c772b080babb84b8695c3ca858925b6`](https://sonicscan.org/address/0xb036daab0c772b080babb84b8695c3ca858925b6) |
| ✅ SUI.LL   | [`0x425852cca84e5f44d7b8ebb964b21bde585b4571`](https://sonicscan.org/address/0x425852cca84e5f44d7b8ebb964b21bde585b4571) |
| ✅ S.LL     | [`0xb592d2631ccf245119532e025d11188cfadb6777`](https://sonicscan.org/address/0xb592d2631ccf245119532e025d11188cfadb6777) |
| ✅ POL.LL   | [`0x15ba35f87f259b9b50d53a2fec271bb10020b090`](https://sonicscan.org/address/0x15ba35f87f259b9b50d53a2fec271bb10020b090) |
| ✅ LL       | [`0x547845f26a7ea68a4202387a536071ff320c1f38`](https://sonicscan.org/address/0x547845f26a7ea68a4202387a536071ff320c1f38) |
| 🚧 HYPE.LL | [`0xea7e97b4597f724844426c32248ba80bd29c75f8`](https://sonicscan.org/address/0xea7e97b4597f724844426c32248ba80bd29c75f8) |

#### Solana

| Token     | Hub asset (Sonic)                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ SOL     | [`0x0c09e69a4528945de6d16c7e469dea6996fdf636`](https://sonicscan.org/address/0x0c09e69a4528945de6d16c7e469dea6996fdf636) |
| ✅ bnUSD   | [`0x14C65b1CDc0B821569081b1F77342dA0D0CbF439`](https://sonicscan.org/address/0x14C65b1CDc0B821569081b1F77342dA0D0CbF439) |
| ✅ USDC    | [`0xC3f020057510ffE10Ceb882e1B48238b43d78a5e`](https://sonicscan.org/address/0xC3f020057510ffE10Ceb882e1B48238b43d78a5e) |
| ✅ SODA    | [`0x07Db7b1a96ebE474B20F52fF487cEE415adee79e`](https://sonicscan.org/address/0x07Db7b1a96ebE474B20F52fF487cEE415adee79e) |
| ✅ USDT    | [`0x891c7ab9047eb087b5a988a2638e7056f67adc5c`](https://sonicscan.org/address/0x891c7ab9047eb087b5a988a2638e7056f67adc5c) |
| ✅ BONK    | [`0x12d60e1bc3819a6c1b12c620fc32d354d7a31937`](https://sonicscan.org/address/0x12d60e1bc3819a6c1b12c620fc32d354d7a31937) |
| ✅ JUP     | [`0xad3c536478f77985fb39c43f0f77236cb10b15b7`](https://sonicscan.org/address/0xad3c536478f77985fb39c43f0f77236cb10b15b7) |
| ✅ JitoSOL | [`0x942f89a324d2652e447b9fcf08effc9f9776a680`](https://sonicscan.org/address/0x942f89a324d2652e447b9fcf08effc9f9776a680) |
| ✅ mSOL    | [`0x6c0b39345ad65e8ee4e1c415444fde7b1e3c3fb6`](https://sonicscan.org/address/0x6c0b39345ad65e8ee4e1c415444fde7b1e3c3fb6) |
| ✅ RAY     | [`0x0a37cf7f0ac5d95cb708fff85aab24c028feb3d4`](https://sonicscan.org/address/0x0a37cf7f0ac5d95cb708fff85aab24c028feb3d4) |
| ✅ PYTH    | [`0x5f53f8b171112232133e1491e537fdecb2aa34c5`](https://sonicscan.org/address/0x5f53f8b171112232133e1491e537fdecb2aa34c5) |
| ✅ JTO     | [`0x9f093d00fa660da0aca764065f8750eeb6c0e1f4`](https://sonicscan.org/address/0x9f093d00fa660da0aca764065f8750eeb6c0e1f4) |
| ✅ WBTC    | [`0x0076b67235bfc69b649d29dd7cb66e5b92b618aa`](https://sonicscan.org/address/0x0076b67235bfc69b649d29dd7cb66e5b92b618aa) |
| ✅ CRCLx   | [`0x64659130a3373b3527146b38fc1fccf017bc0c61`](https://sonicscan.org/address/0x64659130a3373b3527146b38fc1fccf017bc0c61) |
| ✅ TSLAx   | [`0x3cc867f6f4d1817b6230b781125301363fce370c`](https://sonicscan.org/address/0x3cc867f6f4d1817b6230b781125301363fce370c) |
| ✅ SPYx    | [`0xea66dbe82ebcb6c3a55b2db3d722b676be63a26e`](https://sonicscan.org/address/0xea66dbe82ebcb6c3a55b2db3d722b676be63a26e) |
| ✅ NVDAx   | [`0x48303c90f4136bc3101b308c8b50c55745aaf317`](https://sonicscan.org/address/0x48303c90f4136bc3101b308c8b50c55745aaf317) |
| ✅ QQQx    | [`0x4448b894740198ab76c83c8850d73f7dc8e4b9b3`](https://sonicscan.org/address/0x4448b894740198ab76c83c8850d73f7dc8e4b9b3) |
| ✅ MSTRx   | [`0xc46c34961802355c5223a115568fdf18a51ad6f6`](https://sonicscan.org/address/0xc46c34961802355c5223a115568fdf18a51ad6f6) |
| ✅ COINx   | [`0xf444586e95166da0754052f03f344cf1152abe7d`](https://sonicscan.org/address/0xf444586e95166da0754052f03f344cf1152abe7d) |
| ✅ GOOGLx  | [`0x024230dd63b27df90d988d6f37a69d4de627ce89`](https://sonicscan.org/address/0x024230dd63b27df90d988d6f37a69d4de627ce89) |

#### ICON

| Token            | Hub asset (Sonic)                                                                                                        |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ ICX            | [`0xb66cB7D841272AF6BaA8b8119007EdEE35d2C24F`](https://sonicscan.org/address/0xb66cB7D841272AF6BaA8b8119007EdEE35d2C24F) |
| ✅ wICX           | [`0xb66cB7D841272AF6BaA8b8119007EdEE35d2C24F`](https://sonicscan.org/address/0xb66cB7D841272AF6BaA8b8119007EdEE35d2C24F) |
| ✅ bnUSD (legacy) | [`0x654dddf32a9a2ac53f5fb54bf1e93f66791f8047`](https://sonicscan.org/address/0x654dddf32a9a2ac53f5fb54bf1e93f66791f8047) |

#### Stellar

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ XLM   | [`0x8ac68af223907fb1b893086601a3d99e00f2fa9d`](https://sonicscan.org/address/0x8ac68af223907fb1b893086601a3d99e00f2fa9d) |
| ✅ bnUSD | [`0x23225Ab8E63FCa4070296678cb46566d57E1BBe3`](https://sonicscan.org/address/0x23225Ab8E63FCa4070296678cb46566d57E1BBe3) |
| ✅ USDC  | [`0x348007B53F25A9A857aB8eA81ec9E3CCBCf440f2`](https://sonicscan.org/address/0x348007B53F25A9A857aB8eA81ec9E3CCBCf440f2) |
| ✅ SODA  | [`0xD749B5FfFED7cEDaa3239abDd16D677179C29AEc`](https://sonicscan.org/address/0xD749B5FfFED7cEDaa3239abDd16D677179C29AEc) |

#### SUI

| Token       | Hub asset (Sonic)                                                                                                        |
| ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ SUI       | [`0x4676b2a551b25c04e235553c1c81019337384673`](https://sonicscan.org/address/0x4676b2a551b25c04e235553c1c81019337384673) |
| ✅ bnUSD     | [`0xDf23097B9AEb917Bf8fb70e99b6c528fffA35364`](https://sonicscan.org/address/0xDf23097B9AEb917Bf8fb70e99b6c528fffA35364) |
| ✅ USDC      | [`0x5635369c8a29A081d26C2e9e28012FCa548BA0Cb`](https://sonicscan.org/address/0x5635369c8a29A081d26C2e9e28012FCa548BA0Cb) |
| ✅ SODA      | [`0x5Db9CEc919f40C50809D9490DC3BbA4F05b0a1D7`](https://sonicscan.org/address/0x5Db9CEc919f40C50809D9490DC3BbA4F05b0a1D7) |
| ✅ afSUI     | [`0x039666bd0cbc96a66c40e8541af465beaa81aa7e`](https://sonicscan.org/address/0x039666bd0cbc96a66c40e8541af465beaa81aa7e) |
| ✅ mSUI      | [`0xb202c674c9a79b5681e981ba0daa782b3ceeebbe`](https://sonicscan.org/address/0xb202c674c9a79b5681e981ba0daa782b3ceeebbe) |
| ✅ haSUI     | [`0x67a26d11fce15e8b33ac97230d36cae1c52c35e7`](https://sonicscan.org/address/0x67a26d11fce15e8b33ac97230d36cae1c52c35e7) |
| ✅ vSUI      | [`0x025715bcda08db06c795cd5bf944e2081468d99a`](https://sonicscan.org/address/0x025715bcda08db06c795cd5bf944e2081468d99a) |
| ✅ yapSUI    | [`0xac509404f3a3ca3f7766baf65be45a52b1cfccd7`](https://sonicscan.org/address/0xac509404f3a3ca3f7766baf65be45a52b1cfccd7) |
| ✅ trevinSUI | [`0x514569c788b096595672e0f68ec72387a22ac67b`](https://sonicscan.org/address/0x514569c788b096595672e0f68ec72387a22ac67b) |
| ✅ DEEP      | [`0x93057085a4627cec26b31477a02f62412cc0485f`](https://sonicscan.org/address/0x93057085a4627cec26b31477a02f62412cc0485f) |
| ✅ WAL       | [`0x72a726ab67c382e8946a79e9872ea32695661e11`](https://sonicscan.org/address/0x72a726ab67c382e8946a79e9872ea32695661e11) |
| ✅ NAVX      | [`0xb78c7c96ae996b4d479bb81b7569a5c6a79c88cb`](https://sonicscan.org/address/0xb78c7c96ae996b4d479bb81b7569a5c6a79c88cb) |
| 🚧 USDT     | [`0xad8901fe2c8defe467fa1df7af78d755e53485ab`](https://sonicscan.org/address/0xad8901fe2c8defe467fa1df7af78d755e53485ab) |

#### Injective

| Token    | Hub asset (Sonic)                                                                                                        |
| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| 🚧 INJ   | [`0xd375590b4955f6ea5623f799153f9b787a3bd319`](https://sonicscan.org/address/0xd375590b4955f6ea5623f799153f9b787a3bd319) |
| 🚧 bnUSD | [`0x69425FFb14704124A58d6F69d510f74A59D9a5bC`](https://sonicscan.org/address/0x69425FFb14704124A58d6F69d510f74A59D9a5bC) |
| 🚧 USDC  | [`0x4bc1211faa06fb50ff61a70331f56167ae511057`](https://sonicscan.org/address/0x4bc1211faa06fb50ff61a70331f56167ae511057) |

#### Near

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ NEAR  | [`0xda7a39b44d5aa0b99bd3c16110f79fa43d3bf4a4`](https://sonicscan.org/address/0xda7a39b44d5aa0b99bd3c16110f79fa43d3bf4a4) |
| ✅ bnUSD | [`0x1979904d6d5ef1178e242471f7091f36d79f8ab4`](https://sonicscan.org/address/0x1979904d6d5ef1178e242471f7091f36d79f8ab4) |
| ✅ SODA  | [`0x4edaa15e106910dbf2b73e6a36828755ebd38668`](https://sonicscan.org/address/0x4edaa15e106910dbf2b73e6a36828755ebd38668) |
| ✅ USDC  | [`0x0d7209e6a6922a9f8e42bf04dc263dcdd0a0c20a`](https://sonicscan.org/address/0x0d7209e6a6922a9f8e42bf04dc263dcdd0a0c20a) |
| ✅ USDT  | [`0x54e3c4b335caff41577e187fb9be1c2b35a1e151`](https://sonicscan.org/address/0x54e3c4b335caff41577e187fb9be1c2b35a1e151) |

#### Bitcoin

| Token | Hub asset (Sonic)                                                                                                        |
| ----- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ BTC | [`0xeb0393893b5bf98a50073d6740738b08e575058b`](https://sonicscan.org/address/0xeb0393893b5bf98a50073d6740738b08e575058b) |

#### Ethereum

| Token     | Hub asset (Sonic)                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ ETH     | [`0xaeafa26e43f46cd83efe89b1e57c858eb5685a24`](https://sonicscan.org/address/0xaeafa26e43f46cd83efe89b1e57c858eb5685a24) |
| ✅ bnUSD   | [`0x13df82eb4c6b7d4bb85669d227e6d24342e4f588`](https://sonicscan.org/address/0x13df82eb4c6b7d4bb85669d227e6d24342e4f588) |
| ✅ USDC    | [`0x46bd0ce9b2b455ac4377cd142ecb8b719715197d`](https://sonicscan.org/address/0x46bd0ce9b2b455ac4377cd142ecb8b719715197d) |
| ✅ USDT    | [`0xd1a1c0ae27f661887fe76b0fd609eb2f6846a8b3`](https://sonicscan.org/address/0xd1a1c0ae27f661887fe76b0fd609eb2f6846a8b3) |
| ✅ SODA    | [`0x12affee59ceb8be6788a25f9b36149a717795a51`](https://sonicscan.org/address/0x12affee59ceb8be6788a25f9b36149a717795a51) |
| ✅ LL      | [`0xee6236c791db0755c9bc333b4c7c85ab754f2a0a`](https://sonicscan.org/address/0xee6236c791db0755c9bc333b4c7c85ab754f2a0a) |
| ✅ sUSDat  | [`0x67f45e9815c17ec690950d0fd7f6a7cdcceb46d0`](https://sonicscan.org/address/0x67f45e9815c17ec690950d0fd7f6a7cdcceb46d0) |
| ✅ AAVE    | [`0xf021361997f819c4d64904af8af3e2fe49d2d768`](https://sonicscan.org/address/0xf021361997f819c4d64904af8af3e2fe49d2d768) |
| ✅ LINK    | [`0x70a26a25d58c219b01516f5f858645e99e03e6ab`](https://sonicscan.org/address/0x70a26a25d58c219b01516f5f858645e99e03e6ab) |
| ✅ UNI     | [`0x1c4a0f9a03ad0725ed93ee8ed9f52940cb65071c`](https://sonicscan.org/address/0x1c4a0f9a03ad0725ed93ee8ed9f52940cb65071c) |
| ✅ PEPE    | [`0xbc12f111bb64d8cfeb704dee219ea9d9e935e833`](https://sonicscan.org/address/0xbc12f111bb64d8cfeb704dee219ea9d9e935e833) |
| ✅ ENA     | [`0x97458414ab7eab10ef2e819ab2a5a93c4b05bd42`](https://sonicscan.org/address/0x97458414ab7eab10ef2e819ab2a5a93c4b05bd42) |
| ✅ USDe    | [`0x4ffd8b51f5adf177ac11236e54cf6216cded31a6`](https://sonicscan.org/address/0x4ffd8b51f5adf177ac11236e54cf6216cded31a6) |
| ✅ sUSDe   | [`0x70be6173edb5de693b7af07f39abd974c8035fdb`](https://sonicscan.org/address/0x70be6173edb5de693b7af07f39abd974c8035fdb) |
| ✅ PYUSD   | [`0xe92198c343cafaec759926261afd2f6000fa43e2`](https://sonicscan.org/address/0xe92198c343cafaec759926261afd2f6000fa43e2) |
| ✅ ZRO     | [`0x9f3721960a9d0c185c853558923217ba66032818`](https://sonicscan.org/address/0x9f3721960a9d0c185c853558923217ba66032818) |
| ✅ PAXG    | [`0x8f46d2e6e2c4fe9166aa9e809cfadf2ebe37bda4`](https://sonicscan.org/address/0x8f46d2e6e2c4fe9166aa9e809cfadf2ebe37bda4) |
| ✅ XAUt    | [`0xacfae4351e9b59feb5f35dc2807b3c78c63c2020`](https://sonicscan.org/address/0xacfae4351e9b59feb5f35dc2807b3c78c63c2020) |
| ✅ USD1    | [`0x18dd31ec75f1d619a8e78a50413f980fa0d918e3`](https://sonicscan.org/address/0x18dd31ec75f1d619a8e78a50413f980fa0d918e3) |
| ✅ DAI     | [`0x80048d5bbb244602441f0e7dc8322070359ade2e`](https://sonicscan.org/address/0x80048d5bbb244602441f0e7dc8322070359ade2e) |
| ✅ cbBTC   | [`0x39dadf0241c94c05a89c4f7cda58e75b9793a6f5`](https://sonicscan.org/address/0x39dadf0241c94c05a89c4f7cda58e75b9793a6f5) |
| ✅ rETH    | [`0x40bea4f921355a749f460760ba6f8edd78888164`](https://sonicscan.org/address/0x40bea4f921355a749f460760ba6f8edd78888164) |
| 🚧 wstETH | [`0xa2b668c577ab44301ebb820cb29c2a233d1607ab`](https://sonicscan.org/address/0xa2b668c577ab44301ebb820cb29c2a233d1607ab) |
| 🚧 weETH  | [`0x37a3e4ae512b7132c139643a5ac4b7148997d0e8`](https://sonicscan.org/address/0x37a3e4ae512b7132c139643a5ac4b7148997d0e8) |

#### Redbelly

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ RBNT  | [`0x4081e0676c5137161b61601756e6524058a9c5c5`](https://sonicscan.org/address/0x4081e0676c5137161b61601756e6524058a9c5c5) |
| ✅ bnUSD | [`0x650a42ad3017190399b1797b6a7f65240c258e5d`](https://sonicscan.org/address/0x650a42ad3017190399b1797b6a7f65240c258e5d) |
| ✅ SODA  | [`0x855ecace4fe3f9f1e648591af6ba4e9fbd2bf987`](https://sonicscan.org/address/0x855ecace4fe3f9f1e648591af6ba4e9fbd2bf987) |
| ✅ USDC  | [`0xedbe83a638da53d0f31f51b9f6b65ea0e8c3b4bf`](https://sonicscan.org/address/0xedbe83a638da53d0f31f51b9f6b65ea0e8c3b4bf) |
| ✅ USDT  | [`0x81afe70d3b8266395b9f508c95cfc05ce7e01609`](https://sonicscan.org/address/0x81afe70d3b8266395b9f508c95cfc05ce7e01609) |
| ✅ rETH  | [`0x87baf5eb9c67a86a5260b7ec739d0c4a4a450ae3`](https://sonicscan.org/address/0x87baf5eb9c67a86a5260b7ec739d0c4a4a450ae3) |
| ✅ rBTC  | [`0xe88e7117a2f84bad69cb35440617cb4cbf5d39d1`](https://sonicscan.org/address/0xe88e7117a2f84bad69cb35440617cb4cbf5d39d1) |
| ✅ rSOL  | [`0x12cb9f7ea402a75dc0b670097c9c376e778b3480`](https://sonicscan.org/address/0x12cb9f7ea402a75dc0b670097c9c376e778b3480) |
| ✅ rBNB  | [`0x1926ea1492bc849865b9dc40576226d0f2ac2865`](https://sonicscan.org/address/0x1926ea1492bc849865b9dc40576226d0f2ac2865) |
| ✅ rHYPE | [`0xc01d5d8fed35e897be5b402c96edb3b6cee50223`](https://sonicscan.org/address/0xc01d5d8fed35e897be5b402c96edb3b6cee50223) |
| ✅ rAVAX | [`0x9cb238c213d490d64e9be025e3e9f80c49068b74`](https://sonicscan.org/address/0x9cb238c213d490d64e9be025e3e9f80c49068b74) |
| ✅ rXLM  | [`0x38e2866396e5246072450fdde24b67cec6113987`](https://sonicscan.org/address/0x38e2866396e5246072450fdde24b67cec6113987) |
| ✅ rSUI  | [`0xa98bf33d2bc3fdf851c3d680db4676bba33b7142`](https://sonicscan.org/address/0xa98bf33d2bc3fdf851c3d680db4676bba33b7142) |
| ✅ rS    | [`0x8f3a13d920fe2e0b80fe4981c430e9ee528e80aa`](https://sonicscan.org/address/0x8f3a13d920fe2e0b80fe4981c430e9ee528e80aa) |
| ✅ rPOL  | [`0x3afeb3584649342a967629bb39a135dce2db8ebf`](https://sonicscan.org/address/0x3afeb3584649342a967629bb39a135dce2db8ebf) |

#### Kaia

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ KAIA  | [`0x1950c088bc12398240224808167a166f220c8d0e`](https://sonicscan.org/address/0x1950c088bc12398240224808167a166f220c8d0e) |
| ✅ USDT  | [`0x99ecd19afce86766a417956971cdc5e026284454`](https://sonicscan.org/address/0x99ecd19afce86766a417956971cdc5e026284454) |
| ✅ bnUSD | [`0x60cc9bd812bec2fb65242e9cd0a3ead4a8bf7488`](https://sonicscan.org/address/0x60cc9bd812bec2fb65242e9cd0a3ead4a8bf7488) |
| ✅ SODA  | [`0x7c5a1f6d39befef7dd2b2be38dd98d323d42ab8d`](https://sonicscan.org/address/0x7c5a1f6d39befef7dd2b2be38dd98d323d42ab8d) |

#### Stacks

| Token   | Hub asset (Sonic)                                                                                                        |
| ------- | ------------------------------------------------------------------------------------------------------------------------ |
| ✅ STX   | [`0x30b3b1a3e4f1235472772e60a8cd9c0165db641e`](https://sonicscan.org/address/0x30b3b1a3e4f1235472772e60a8cd9c0165db641e) |
| ✅ bnUSD | [`0x78093c056fb37ffc1a9ae96daa0a7dd9d372b64d`](https://sonicscan.org/address/0x78093c056fb37ffc1a9ae96daa0a7dd9d372b64d) |
| ✅ SODA  | [`0xebd34ddf9f47f9bcdf706e87206d40a579c34822`](https://sonicscan.org/address/0xebd34ddf9f47f9bcdf706e87206d40a579c34822) |
| ✅ sBTC  | [`0x6f6c039b07e25bb86d8223643a84122404963d9b`](https://sonicscan.org/address/0x6f6c039b07e25bb86d8223643a84122404963d9b) |
| ✅ USDC  | [`0x2336917df30f3417986a29ec384274a83adbef16`](https://sonicscan.org/address/0x2336917df30f3417986a29ec384274a83adbef16) |


# SDKs

Software development kits available to the integrators.

The SODAX developer suite is architected as a dependency stack. Builders can choose to integrate at the foundational level for maximum control or use opinionated layers for speed.

**1. The Foundation:** [@sodax/sdk](/developers/packages/foundation/sdk)

This is the core logic layer that powers the entire ecosystem. It provides the raw functional modules required to build with SODAX programmatically.

* **Functional Modules:**
  * `Swaps`: Quote and execute cross-chain intents via the solver.
  * `Lend/Borrow`: Interact directly with the SODAX money market logic.
  * `Bridge`: Core bridging primitives for asset transfer.
  * `Staking`: Management of SODA staking and governance positions.
  * `Migration`: Utilities for migrating ICX to SODA tokens.
* **Tooling Modules:**
  * `Backend API`: Provides useful data points for each feature
  * `Intent Relay API`: Direct access to the intent propagation network.

**2. The Connection Layer**

Sitting above the core SDK, this package manages the complexity of connecting user wallets across heterogeneous chains (EVM, SVM, non-EVM). It is available in two flavors:

* [@sodax/wallet-sdk-core](/developers/packages/connection/wallet-sdk-core) **Core (TypeScript)**: A pure TypeScript implementation of wallet providers. Use this if you are building a custom frontend framework or a non-React application.
* [@sodax/wallet-sdk-react](/developers/packages/connection/wallet-sdk-react) **React Adapter**: An opinionated wrapper optimized for React applications, providing pre-built context providers and state management for wallet connections.

**3. The Experience Layer:** [@sodax/dapp-kit](/developers/packages/experience/dapp-kit)

The fastest way to build with SODAX. This is an opinionated collection of UI components, hooks, and utilities that leverages the layers below it.

* **Under the Hood:** It automatically implements `@sodax/wallet-sdk` for connection and `@sodax/sdk` for execution.
* **What it offers:** React based hooks, contexts, and utilities for SODAX features


# 1. The Foundation

This is the core logic layer that powers the entire ecosystem. It provides the raw functional modules required to build with SODAX programmatically.

## Main SDK

The SODAX SDK provides a comprehensive interface for interacting with the SODAX protocol, enabling cross-chain swaps, money market, cross-chain bridging, migration and staking SODA token.

Get started: <a href="/pages/hTgS9geulHVOE4B6vaYd" class="button primary" data-icon="cup-straw">@sodax/sdk</a>

#### Functional Modules inside [@sodax/sdk](/developers/packages/foundation/sdk)

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps" class="button secondary" data-icon="rotate">Swaps (Solver)</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market" class="button secondary" data-icon="sack-dollar">Lend / Borrow (Money Market)</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/bridge" class="button secondary" data-icon="bridge-suspension">Bridge</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/staking" class="button secondary" data-icon="seedling">Staking</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/migration" class="button secondary" data-icon="truck">Migration</a>

#### Tooling Modules inside [@sodax/sdk](/developers/packages/foundation/sdk)

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/backend_api" class="button secondary" data-icon="plug">Backend API</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/intent_relay_api" class="button secondary" data-icon="envelope">Intent Relay API</a>


# @sodax/sdk

The SODAX SDK provides a comprehensive interface for interacting with the SODAX protocol, enabling cross-chain swaps, money market, cross-chain bridging, migration and staking SODA token.

## Get started

### Installation

```bash
# Using npm
npm install @sodax/sdk

# Using yarn
yarn add @sodax/sdk

# Using pnpm
pnpm add @sodax/sdk
```

### Local Installation

Package can be locally installed by following this steps:

1. Clone this repository to your local machine.
2. `cd` into repository folder location.
3. Execute `pnpm install` command in your CLI to install dependencies.
4. Execute `pnpm run build` to build the packages.
5. In your app repository `package.json` file, define dependency named `"@sodax/sdk"` under `"dependencies"`. Instead of version define absolute path to your SDK repository `"file:<sdk-repository-path>"` (e.g. `"file:/Users/dev/.../operation-liquidity-layer/packages/sdk"`). Full example: `"@sodax/sdk": "file:/Users/dev/operation-liquidity-layer/sdk-new/packages/sdk"`.

### Local Development

How to setup local development

1. Clone repository.
2. Make sure you have [Node.js](https://nodejs.org/en/download/package-manager) v18+ and corresponding npm installed on your system.
3. Execute `pnpm install` command (from root of the project) in your CLI to install dependencies.
4. Make code changes.
   1. Do not forget to export TS files in same folder `index.ts`.
   2. Always import files using `.js` postfix.

### Functional Modules inside the SDK

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps" class="button secondary" data-icon="rotate">Swaps (Solver)</a> - Cross-chain intent-based swaps

* EVM (Sonic, Ethereum, Arbitrum, Avalanche, Base, BSC, Optimism, Polygon, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) ✅
* Sui ✅
* Stellar ✅
* ICON ✅
* Solana ✅
* Injective ✅
* NEAR ✅
* Stacks ✅
* Bitcoin ✅

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market" class="button secondary" data-icon="sack-dollar">Lend / Borrow (Money Market)</a>- Cross-chain lending and borrowing

* EVM (Sonic, Ethereum, Arbitrum, Avalanche, Base, BSC, Optimism, Polygon, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) ✅
* Sui ✅
* Stellar ✅
* ICON ✅ (bnUSD only)
* Solana ✅
* Injective ✅
* NEAR ✅
* Stacks ✅
* Bitcoin ✅ (BTC only)

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/bridge" class="button secondary" data-icon="bridge-suspension">Bridge</a>- Cross-chain token bridging

* EVM (Sonic, Ethereum, Arbitrum, Avalanche, Base, BSC, Optimism, Polygon, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) ✅
* Sui ✅
* Stellar ✅
* ICON ✅
* Solana ✅
* Injective ✅
* NEAR ✅
* Stacks ✅
* Bitcoin ✅

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/migration" class="button secondary" data-icon="truck">Migration</a>- Token migration (ICX, bnUSD, BALN)

* ICX / wICX → SODA: source chain ICON only
* BALN → SODA: source chain ICON only
* bnUSD: between legacy chains (ICON, Sui, Stellar) and the new bnUSD on any other supported chain

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/staking" class="button secondary" data-icon="seedling">Staking</a>- SODA token staking

* EVM (Sonic, Ethereum, Arbitrum, Avalanche, Base, BSC, Optimism, Polygon, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) ✅
* Sui ✅
* Stellar ✅
* Solana ✅
* Injective ✅
* NEAR ✅
* Stacks ✅

### Tooling Modules inside the SDK

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/backend_api" class="button secondary" data-icon="plug">Backend API</a>- Solver API endpoint documentation

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/intent_relay_api" class="button secondary" data-icon="envelope">Intent Relay API</a>- Relayer API endpoint documentation

## AI agent docs

AI-readable docs for `@sodax/sdk` (and the other `@sodax/*` packages) are shipped via [`@sodax/skills`](https://github.com/icon-project/sodax-sdks/tree/main/packages/skills) — a separate npm package bundling Claude-Code SKILL.md files and a long-form knowledge tree.

**Recommended:** [**`skills` CLI**](https://github.com/vercel-labs/skills) — from your project root:

```bash
npx skills@latest add icon-project/sodax-sdks/packages/skills
```

**npm + `AGENTS.md` pointer** (fallback for web chats, or when you prefer a devDependency over the CLI):

```bash
pnpm add -D @sodax/skills
```

Then point your agent at `node_modules/@sodax/skills/AGENTS.md`. See [docs/ai-integration-guide.md](https://github.com/icon-project/sodax-sdks/blob/main/docs/ai-integration-guide.md) for all install modes and per-tool wiring.

***

## Contributing

We welcome contributions! Please see our [Contributing Guide](https://github.com/icon-project/sodax-sdks/blob/main/CONTRIBUTING.md) for details.

***

## Development Commands

```bash
# Install dependencies
pnpm install

# Build the package
pnpm build

# Run in development mode
pnpm dev

# Run type checking
pnpm checkTs

# Format code
pnpm pretty

# Lint code
pnpm lint
```

***

## License

* [MIT](https://github.com/icon-project/sodax-sdks/blob/main/LICENSE)

***

## Support

* [GitHub Issues](https://github.com/icon-project/sodax-sdks/issues)
* [Discord Community](https://discord.gg/xM2Nh4S6vN)


# Functional Modules

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps" class="button secondary" data-icon="rotate">Swaps (Solver)</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market" class="button secondary" data-icon="sack-dollar">Lend / Borrow (Money Market)</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/leverage_yield" class="button secondary" data-icon="money-bill-trend-up">Leverage Yield</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/bridge" class="button secondary" data-icon="bridge-suspension">Bridge</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/staking" class="button secondary" data-icon="seedling">Staking</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/migration" class="button secondary" data-icon="truck">Migration</a>


# Swaps (Solver)

The swap module provides abstractions for interacting with cross-chain Intent Smart Contracts, the solver API, and the Relay API.

All swap operations are accessed through the `swaps` property of a `Sodax` instance:

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax();

// All swap methods are available through sodax.swaps
const quote = await sodax.swaps.getQuote(quoteRequest);
```

> **`sodax.swaps` vs `sodax.api.swaps`.** This page documents `sodax.swaps` (`SwapService`) — the end-to-end intent orchestrator that creates, relays, and finalizes swaps on-chain. The lower-level typed HTTP client for the backend Swaps API v2 (quote, create-intent, submit-tx, status, fees — 21 endpoints) is `sodax.api.swaps` (`SwapsApiService`); see [`SWAPS_API.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS_API.md).

## Using SDK Config and Constants

The SDK includes predefined configurations of supported chains, tokens, and other relevant information. All configurations are reachable through the `config` property of the `Sodax` instance.

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';
import type { SpokeChainKey, XToken } from '@sodax/sdk';

const sodax = new Sodax();

// If you want dynamic (backend API-based) configuration, initialize the instance before use.
// By default the configuration bundled in the SDK version you are using is applied.
await sodax.initialize();

// All supported spoke chain keys
const spokeChains: SpokeChainKey[] = sodax.config.getSupportedSpokeChains();

// Supported swap tokens for a specific spoke chain key
const supportedTokens: readonly XToken[] = sodax.swaps.getSupportedSwapTokensByChainId(ChainKeys.BSC_MAINNET);

// All supported swap tokens across every spoke chain
const allTokens: Record<SpokeChainKey, readonly XToken[]> = sodax.swaps.getSupportedSwapTokens();
```

## Available Methods

All swap methods are accessible through `sodax.swaps`:

### Quote & Fee Methods

* `getQuote(payload)` — Request a price quote from the solver API
* `getPartnerFee(inputAmount)` — Calculate the partner fee for a given input amount
* `getSolverFee(inputAmount)` — Calculate the solver protocol fee (0.1%) for a given input amount
* `getSwapDeadline(offset?)` — Compute an absolute deadline timestamp for an intent

### Intent Creation & Execution

* `swap(params)` — Full end-to-end swap (recommended — handles all steps automatically); signed execution only
* `createIntent(params)` — Create an intent on the source spoke chain; supports both signed (`raw: false`) and raw (`raw: true`) modes
* `createLimitOrder(params)` — Full end-to-end limit order (no deadline, must be cancelled manually); signed execution only
* `createLimitOrderIntent(params)` — Create a limit order intent only (no relay/solver notify); supports raw and signed modes
* `submitIntent(payload)` — Submit a spoke tx to the relay API (low-level, called automatically by `swap`)
* `postExecution(request)` — Notify the solver that an intent is live on the hub chain (low-level, called automatically by `swap`)

#### Backend 2-step submit (opt-in)

By default `swap()` relays + post-executes entirely client-side. Opt into a backend-driven 2-step flow with `new Sodax({ swapsOptions: { useBackendSubmitTx: true } })`: after creating + verifying the intent tx, `swap()` hands it to the backend (`sodax.api.swaps.submitTx`), which relays + post-executes server-side; the SDK polls submit-tx status and returns the same `SwapResponse`.

On **any** non-success (submission rejected, terminal `failed`/abandoned, or poll timeout) `swap()` **falls back** to the client-side relay so the swap still completes — identical `SwapResponse` either way. This is **safe**: re-relaying / re-posting an already-processed swap is idempotent — the relay dedups and returns the existing `executed` packet, and the solver re-affirms the intent (no double-fill), verified live by `e2e-tests/e2e-relay.test.ts`. The backend poll and the fallback also share one `timeout` budget, so total latency never exceeds a single `timeout`. `swapsOptions` is a client-side runtime option (like `logger`), not part of the backend `SodaxConfig`. See [CONFIGURE\_SDK.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/CONFIGURE_SDK.md#backend-submit-tx-2-step-swapsoptionsusebackendsubmittx).

### Intent Management

* `getIntent(txHash)` — Retrieve an `Intent` from a hub-chain transaction hash
* `getFilledIntent(txHash)` — Retrieve the fill state of an intent from the solver's fill tx hash
* `getIntentSubmitTxExtraData(params)` — Get the relay extra data (`address` + `payload`) needed to submit a Solana/Bitcoin intent
* `getSolvedIntentPacket(params)` — Poll the relayer until a solved intent's fill packet arrives on the destination chain
* `getIntentHash(intent)` — Compute the keccak256 hash of an intent (its on-chain ID)
* `getStatus(request)` — Poll the solver API for current intent execution status
* `cancelIntent(params)` — Cancel an active intent and wait for hub confirmation
* `createCancelIntent(params)` — Build (and optionally broadcast) only the cancel tx; supports raw and signed modes
* `cancelLimitOrder(params)` — Alias for `cancelIntent` with domain-specific naming

### Token Approval

* `isAllowanceValid(params)` — Check if the spender contract has sufficient token allowance
* `approve(params)` — Approve token spend (EVM/Sonic/Stellar); supports raw and signed modes

### Utility Methods

* `getSupportedSwapTokensByChainId(chainId)` — Get supported swap tokens for a spoke chain
* `getSupportedSwapTokens()` — Get all supported swap tokens per chain
* `estimateGas(params)` — Estimate gas for a raw transaction on any spoke chain

## Core Concepts

### `srcChainKey` / `dstChainKey`

All action params use `srcChainKey` and `dstChainKey` (not `srcChain` / `dstChain`). These are `SpokeChainKey` strings from `ChainKeys.*`.

The on-chain `Intent` struct has `Intent.srcChain` / `Intent.dstChain` as `IntentRelayChainId` (bigint relay IDs) — these are different from the action param fields and should not be confused with them.

### Signed vs Raw Mode (`raw: true / false`)

Methods that accept a `raw` flag return different types depending on the value:

* **`raw: false`** (default) — requires a `walletProvider` matching the source chain type; signs and broadcasts the transaction; returns a tx hash.
* **`raw: true`** — `walletProvider` must be absent (passing one is a compile error); returns an unsigned raw transaction payload.

TypeScript enforces this at compile time via the `WalletProviderSlot<K, Raw>` discriminated union:

```typescript
// Signed execution — walletProvider required, chain-narrowed by srcChainKey
await sodax.swaps.createIntent({
  params: { srcChainKey: ChainKeys.BSC_MAINNET, ...otherParams },
  walletProvider: evmWalletProvider, // must be IEvmWalletProvider for BSC
});

// Raw mode — walletProvider must be absent
await sodax.swaps.createIntent({
  params: { srcChainKey: ChainKeys.BSC_MAINNET, ...otherParams },
  raw: true,
  // walletProvider here would be a compile error
});
```

**Methods with raw support:** `createIntent`, `createLimitOrderIntent`, `createCancelIntent`, `approve`

**Methods without raw support (signed execution only):** `swap`, `createLimitOrder`, `cancelIntent`, `cancelLimitOrder`

### `ChainKeys.*` Constants

All chain identifiers come from `ChainKeys`:

```typescript
import { ChainKeys } from '@sodax/sdk';

ChainKeys.BSC_MAINNET         // '0x38.bsc'
ChainKeys.ARBITRUM_MAINNET    // '0xa4b1.arbitrum'
ChainKeys.ETHEREUM_MAINNET    // 'ethereum'
ChainKeys.SOLANA_MAINNET      // 'solana'
ChainKeys.SONIC_MAINNET       // 'sonic'  (hub chain)
```

### `Result<T>` — No Throws Across Service Boundaries

Every public async method returns `Promise<Result<T, E>>`:

```typescript
type Result<T, E = Error | unknown> =
  | { ok: true; value: T }
  | { ok: false; error: E };
```

Check `result.ok` before accessing `result.value` or `result.error`. **For the swap module's user-facing methods**, the error type is narrowed to `SodaxError<NarrowCode>` per method — see "Error Handling" below.

### Error Handling

The swap module's three core methods (`swap`, `createIntent`, `postExecution`) return a deterministic, narrow `SodaxError` union. `createLimitOrder` / `createLimitOrderIntent` inherit the same shape because they delegate.

#### The canonical error: `SodaxError<C>`

All swap-module errors are instances of `SodaxError`, exported from `@sodax/sdk`:

```typescript
import { SodaxError, isSodaxError } from '@sodax/sdk';

class SodaxError<C extends string = string> extends Error {
  readonly code: C;                  // string-literal discriminator
  readonly cause?: unknown;          // ES2022 cause chain
  readonly context?: Record<string, unknown>;
  toJSON(): { name, code, message, stack, context, cause };
}

function isSodaxError(e: unknown): e is SodaxError;
```

**Rules:**

* Discriminate on `error.code` — never on `error.message` (the message is a human-readable explanation, not a stable contract).
* `error.cause` walks the underlying error chain (ES2022). Loggers like Sentry/Pino/Datadog walk this automatically.
* `error.context` carries structured metadata: `srcChainKey`, `dstChainKey`, `phase`, plus per-code extras (`solverCode`, `relayCode`, `field`, …).
* `error.toJSON()` is the canonical logger surface: `JSON.stringify(error)` invokes it automatically and produces a logger-safe payload (bigints in `context` are coerced to strings, cause walked depth-3, no circular hazards).
* Use `isSodaxError(e)` instead of `instanceof SodaxError` in dapp/app code — it survives duplicate-bundle and dual-package scenarios.

#### Per-method error code unions

| Method                                    | Error type           | Codes                                                                                                                                                                                              |
| ----------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `swap`                                    | `SwapError`          | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `EXTERNAL_API_ERROR`, `UNKNOWN` |
| `createIntent` / `createLimitOrderIntent` | `CreateIntentError`  | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                                                          |
| `postExecution`                           | `PostExecutionError` | `EXECUTION_FAILED`, `EXTERNAL_API_ERROR`, `UNKNOWN`                                                                                                                                                |
| `createLimitOrder`                        | `SwapError`          | (same as `swap`)                                                                                                                                                                                   |

**Important:** `postExecution` alone never emits relay/verify codes — those appear only on `swap` because only `swap` orchestrates verify + relay. Don't write a unified switch that handles both with the same union.

#### Standard `context` fields

```typescript
{
  srcChainKey?: SpokeChainKey;
  dstChainKey?: SpokeChainKey;
  phase?: 'validate' | 'createIntent' | 'verify' | 'submit' | 'relay' | 'postExecution';
  // Only on EXTERNAL_API_ERROR:
  api?: 'solver';                // discriminator for upstream API errors (used as Sentry/Datadog tag)
  solverCode?: SolverIntentErrorCode;
  solverDetail?: SolverErrorResponse['detail'];
  // Only on RELAY_TIMEOUT / TX_SUBMIT_FAILED / RELAY_FAILED:
  relayCode?: 'SUBMIT_TX_FAILED' | 'RELAY_TIMEOUT' | 'RELAY_POLLING_FAILED' | 'UNKNOWN';
  // Only on VALIDATION_FAILED:
  field?: string;
  reason?: string;
}
```

#### Discrimination example

```typescript
import { isSodaxError, SolverIntentErrorCode, type SwapError } from '@sodax/sdk';

const result = await sodax.swaps.swap({ params, walletProvider });

if (!result.ok) {
  const err: SwapError = result.error;

  switch (err.code) {
    case 'VALIDATION_FAILED':
      // Human-readable reason in err.message; structured details in err.context.
      console.error('Bad input:', err.message);
      break;

    case 'INTENT_CREATION_FAILED':
      // Spoke deposit / intent construction failed.
      console.error('Intent creation failed:', err.cause);
      break;

    case 'TX_VERIFICATION_FAILED':
      // Spoke tx could not be verified on-chain.
      break;

    case 'TX_SUBMIT_FAILED':
      // CRITICAL: spoke tx landed but relay submission failed. Persist the spoke tx hash
      // and retry submission — funds may otherwise be inaccessible.
      console.error('Relay submit failed; retry needed:', err.context?.relayCode);
      break;

    case 'RELAY_TIMEOUT':
      // Relay packet didn't confirm in time. Check intent status and retry with longer timeout.
      break;

    case 'RELAY_FAILED':
      // Polling-side failure or unrecognised relay error. Distinguish via err.context.relayCode:
      //   - 'RELAY_POLLING_FAILED' — the polling endpoint failed (network down, malformed
      //     response). The packet's actual status is unknown; query the hub directly to confirm.
      //   - 'UNKNOWN' — forward-compat fallback for new relay error codes.
      break;

    case 'EXECUTION_FAILED':
      // Solver notify failed at the transport layer (network down, etc.).
      break;

    case 'EXTERNAL_API_ERROR': {
      // Solver returned a typed error. Original SolverIntentErrorCode is in context.
      const solverCode = err.context?.solverCode as SolverIntentErrorCode | undefined;
      if (solverCode === SolverIntentErrorCode.NO_PATH_FOUND) {
        // …
      }
      break;
    }

    case 'UNKNOWN':
      console.error('Unexpected error:', err.cause);
      break;
  }
}
```

#### Relay-layer contract

The lower-level relay helpers `relayTxAndWaitPacket` and `submitTransaction` (in `packages/sdk/src/shared/services/intentRelay/IntentRelayApiService.ts`) emit two stable error message strings on failure: `'SUBMIT_TX_FAILED'` and `'RELAY_TIMEOUT'`. These are exported as `RELAY_ERROR_CODES` and form a public contract that other modules (moneyMarket, bridge, dex, migration, staking) still rely on directly.

The swap module wraps these via the unified `mapRelayFailure`, surfacing the original code on `error.context.relayCode` so swap callers don't need to inspect `error.cause.message`.

#### Migration from pre-`SodaxError` (breaking)

If you were on the previous `Error.message`-based pattern:

| Before                                                                              | After                                                                    |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `result.error instanceof Error && result.error.message === 'POST_EXECUTION_FAILED'` | `result.error.code === 'EXECUTION_FAILED'`                               |
| `result.error.message === 'RELAY_TIMEOUT'`                                          | `result.error.code === 'RELAY_TIMEOUT'`                                  |
| `result.error.message === 'SUBMIT_TX_FAILED'`                                       | `result.error.code === 'TX_SUBMIT_FAILED'`                               |
| `(result.error as SolverErrorResponse).detail.code` (from `postExecution`)          | `result.error.context?.solverCode`                                       |
| `(result.error as SolverErrorResponse).detail`                                      | `result.error.context?.solverDetail`                                     |
| Prose `error.message` for invariants                                                | `error.code === 'VALIDATION_FAILED'`; the prose stays on `error.message` |

The full `SolverErrorResponse` payload is preserved on `error.context.solverDetail`, so anything you read from `.detail.*` previously is still reachable.

Other swap methods (`getQuote`, `getStatus`, `submitIntent`, `cancelIntent`, etc.) and other modules (`moneyMarket`, `bridge`, `dex`, …) **remain unchanged in this release** — they still use the legacy `Error | unknown` / `SolverErrorResponse` patterns documented per-module.

***

## Request a Quote

Requesting a quote requires the user's input amount scaled by the token's decimals. All token addresses and decimals are available via `sodax.config`.

The quoting API supports `'exact_input'` (user specifies the amount to swap) and `'exact_output'` (user specifies the amount to receive).

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';
import type { SolverIntentQuoteRequest, SolverErrorResponse } from '@sodax/sdk';

const sodax = new Sodax();

const bscEthToken = '0x2170Ed0880ac9A755fd29B2688956BD959F933F8';  // ETH on BSC
const arbWbtcToken = '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f'; // wBTC on Arbitrum

const quoteRequest = {
  token_src: bscEthToken,
  token_dst: arbWbtcToken,
  token_src_blockchain_id: ChainKeys.BSC_MAINNET,
  token_dst_blockchain_id: ChainKeys.ARBITRUM_MAINNET,
  amount: 1_000_000_000_000_000n, // 1 WETH (18 decimals)
  quote_type: 'exact_input',
} satisfies SolverIntentQuoteRequest;

const result = await sodax.swaps.getQuote(quoteRequest);

if (result.ok) {
  const { quoted_amount } = result.value;
  console.log('Quoted amount:', quoted_amount);
} else {
  // result.error is SolverErrorResponse — check result.error.detail.code
  console.error('Quote failed:', result.error);
}
```

**Note:** `getQuote` automatically deducts the configured partner fee from `payload.amount` before forwarding to the solver, so the returned `quoted_amount` reflects the net output the user actually receives.

***

## Intent Parameters

### `CreateIntentParams<K>`

```typescript
import type { CreateIntentParams } from '@sodax/sdk';
import { ChainKeys } from '@sodax/sdk';

const createIntentParams = {
  inputToken: '0x...',           // Input token address on the source spoke chain
  outputToken: '0x...',          // Output token address on the destination spoke chain
  inputAmount: 1_000_000n,       // Gross input amount (fee will be deducted from this)
  minOutputAmount: 900_000n,     // Minimum acceptable output amount
  deadline: 0n,                  // Unix timestamp after which intent expires (0 = no expiry)
  allowPartialFill: false,       // Whether the intent can be partially filled
  srcChainKey: ChainKeys.BSC_MAINNET,       // Source chain key
  dstChainKey: ChainKeys.ARBITRUM_MAINNET,  // Destination chain key
  srcAddress: '0x...',           // User's address on the source chain
  dstAddress: '0x...',           // Recipient address on the destination chain
  solver: '0x0000000000000000000000000000000000000000', // Optional: Specific solver (address(0) = any)
  data: '0x',                    // Additional arbitrary data
} satisfies CreateIntentParams<typeof ChainKeys.BSC_MAINNET>;
```

### `CreateLimitOrderParams<K>`

Same as `CreateIntentParams` but `deadline` is optional (it is forced to `0n` by `createLimitOrder` / `createLimitOrderIntent`):

```typescript
import type { CreateLimitOrderParams } from '@sodax/sdk';

const limitOrderParams = {
  inputToken: '0x...',
  outputToken: '0x...',
  inputAmount: 1_000_000n,
  minOutputAmount: 900_000n,
  // deadline omitted — will be set to 0n automatically
  allowPartialFill: false,
  srcChainKey: ChainKeys.BSC_MAINNET,
  dstChainKey: ChainKeys.ARBITRUM_MAINNET,
  srcAddress: '0x...',
  dstAddress: '0x...',
  solver: '0x0000000000000000000000000000000000000000', // optional solver address
  data: '0x',
} satisfies CreateLimitOrderParams<typeof ChainKeys.BSC_MAINNET>;
```

***

## Get Fees

### Partner Fee

```typescript
// Calculate partner fee for a given input amount
const inputAmount = 1_000_000_000_000_000n; // 1 WETH (18 decimals)
const partnerFee = sodax.swaps.getPartnerFee(inputAmount);

console.log('Partner fee:', partnerFee);               // Returns 0n if no partner fee configured
console.log('Net swap amount:', inputAmount - partnerFee);
```

The partner fee is deducted from the input amount before the intent is created. If no partner fee is configured on the `Sodax` instance, `getPartnerFee` returns `0n`.

### Solver Fee

```typescript
// Calculate the 0.1% solver protocol fee
const inputAmount = 1_000_000_000_000_000n;
const solverFee = sodax.swaps.getSolverFee(inputAmount);

console.log('Solver fee (0.1%):', solverFee);
```

***

## Get Swap Deadline

Fetches the current hub-chain (Sonic) block timestamp and adds a deadline offset. Pass the result as `CreateIntentParams.deadline`.

```typescript
// Default 5-minute offset (DEFAULT_DEADLINE_OFFSET = 300 seconds)
const deadlineResult = await sodax.swaps.getSwapDeadline();
if (deadlineResult.ok) {
  console.log('Deadline (5 min from now):', deadlineResult.value);
}

// Custom 10-minute offset
const customDeadlineResult = await sodax.swaps.getSwapDeadline(600n); // 600 seconds
if (customDeadlineResult.ok) {
  const createIntentParams = {
    // ...
    deadline: customDeadlineResult.value,
    // ...
  };
}
```

For limit orders, pass `deadline: 0n` directly to `createIntent` (or use `createLimitOrder` / `createLimitOrderIntent` which force `0n` automatically).

***

## Token Approval Flow

Before creating an intent, check whether the relevant spender contract already has permission to spend the user's input tokens.

* **Hub (Sonic)**: checks allowance against the intents contract
* **EVM spoke chains**: checks allowance against the spoke's asset manager
* **Stellar**: checks trustline sufficiency
* **Other chains (Solana, NEAR, etc.)**: always returns `true` — no on-chain allowance concept

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';
import type { IEvmWalletProvider } from '@sodax/sdk';

// evmWalletProvider comes from wallet-sdk-core (EvmWalletProvider) or
// useWalletProvider({ xChainId: ChainKeys.BSC_MAINNET }) from wallet-sdk-react
declare const evmWalletProvider: IEvmWalletProvider;

const createIntentParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  inputToken: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
  inputAmount: 1_000_000n,
  srcAddress: await evmWalletProvider.getWalletAddress(),
  // ... other params
};

const allowanceResult = await sodax.swaps.isAllowanceValid({
  params: createIntentParams,
  walletProvider: evmWalletProvider,
});

if (!allowanceResult.ok) {
  console.error('Allowance check failed:', allowanceResult.error);
} else if (!allowanceResult.value) {
  // Approval needed
  const approveResult = await sodax.swaps.approve({
    params: createIntentParams,
    walletProvider: evmWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Approve failed:', approveResult.error);
  } else {
    console.log('Approval tx hash:', approveResult.value);
    // Wait for the approval tx to be mined before proceeding
  }
}
```

### Raw Approval Transaction

```typescript
// Get raw (unsigned) approval tx data — no walletProvider needed
const approveResult = await sodax.swaps.approve({
  params: createIntentParams,
  raw: true,
});

if (approveResult.ok) {
  const rawTx = approveResult.value; // EvmRawTransaction: { from, to, value, data }
  console.log('Raw approval tx:', rawTx);
}
```

### Stellar Trustline

For Stellar as the source chain, `isAllowanceValid` checks trustline balance sufficiency and `approve` adds/increases the trustline. For Stellar as the **destination** chain, frontends must manually establish trustlines before executing swaps. See `packages/sdk/docs/STELLAR_TRUSTLINE.md` for details.

***

## Estimate Gas for Raw Transactions

```typescript
import { ChainKeys } from '@sodax/sdk';
import type { EstimateGasParams } from '@sodax/sdk';

// Get a raw intent tx first
const createIntentResult = await sodax.swaps.createIntent({
  params: createIntentParams,
  raw: true,
});

if (createIntentResult.ok) {
  const { tx: rawTx } = createIntentResult.value;

  // Estimate gas — provide the chain key and the raw tx
  const gasResult = await sodax.swaps.estimateGas({
    chainKey: ChainKeys.BSC_MAINNET,
    tx: rawTx,
  } satisfies EstimateGasParams<typeof ChainKeys.BSC_MAINNET>);

  if (gasResult.ok) {
    console.log('Estimated gas:', gasResult.value);
  }
}
```

***

## Swap (Recommended)

The `swap` method is the recommended way to perform a complete cross-chain swap. It orchestrates the full lifecycle automatically:

1. Calls `createIntent` to submit the intent transaction on the source spoke chain
2. Verifies the spoke transaction landed on-chain
3. For non-hub source chains: submits the spoke tx to the relayer and waits for the relay packet to land on the hub (Sonic)
4. Calls `postExecution` to notify the solver, triggering it to fill the intent

`swap` is signed-only (no `raw: true` mode) — use `createIntent` if you need raw transaction data.

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';
import type { IEvmWalletProvider, SwapResponse } from '@sodax/sdk';

declare const evmWalletProvider: IEvmWalletProvider;

const swapResult = await sodax.swaps.swap({
  params: {
    inputToken: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
    outputToken: '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f',
    inputAmount: 1_000_000_000_000_000n,
    minOutputAmount: 900_000n,
    deadline: 300n, // or use getSwapDeadline()
    allowPartialFill: false,
    srcChainKey: ChainKeys.BSC_MAINNET,
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    srcAddress: await evmWalletProvider.getWalletAddress(),
    dstAddress: '0x...',
    solver: '0x0000000000000000000000000000000000000000', // optional
    data: '0x',
  },
  walletProvider: evmWalletProvider,
  timeout: 120_000,        // optional — relay timeout in ms (default: DEFAULT_RELAY_TX_TIMEOUT = 120 s)
  skipSimulation: false,   // optional — skip spoke tx simulation (default: false)
});

if (!swapResult.ok) {
  // See Error Handling section for how to branch on specific error codes
  console.error('Swap failed:', swapResult.error);
} else {
  const { solverExecutionResponse, intent, intentDeliveryInfo }: SwapResponse = swapResult.value;
  console.log('Solver acknowledged:', solverExecutionResponse.answer); // 'OK'
  console.log('Hub tx hash:', intentDeliveryInfo.dstTxHash);
}
```

***

## Create Intent Only

Use `createIntent` when you need raw transaction data or want to control the relay step yourself. For the full lifecycle, prefer `swap`.

### Signed execution

```typescript
const createIntentResult = await sodax.swaps.createIntent({
  params: createIntentParams, // CreateIntentParams<typeof ChainKeys.BSC_MAINNET>
  walletProvider: evmWalletProvider,
  skipSimulation: false, // optional
});

if (createIntentResult.ok) {
  const { tx: spokeTxHash, intent, relayData } = createIntentResult.value;
  console.log('Spoke tx hash:', spokeTxHash);
  console.log('Intent fee amount:', intent.feeAmount);
}
```

### Raw transaction

```typescript
const createIntentResult = await sodax.swaps.createIntent({
  params: createIntentParams,
  raw: true, // walletProvider must be absent
});

if (createIntentResult.ok) {
  const { tx: rawTx, intent, relayData } = createIntentResult.value;
  // rawTx is EvmRawTransaction: { from, to, value, data }
  // relayData is RelayExtraData: { address, payload } — needed for submitIntent
  console.log('Raw tx:', rawTx);
}
```

***

## Limit Orders

A limit order is an intent with `deadline = 0n` — it stays active indefinitely until filled at `minOutputAmount` or manually cancelled.

### Create Limit Order (full lifecycle)

```typescript
import type { IEvmWalletProvider } from '@sodax/sdk';

declare const evmWalletProvider: IEvmWalletProvider;

const limitOrderResult = await sodax.swaps.createLimitOrder({
  params: {
    inputToken: '0x...',
    outputToken: '0x...',
    inputAmount: 1_000_000n,
    minOutputAmount: 900_000n,
    // deadline omitted — forced to 0n
    allowPartialFill: false,
    srcChainKey: ChainKeys.BSC_MAINNET,
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    srcAddress: '0x...',
    dstAddress: '0x...',
    solver: '0x0000000000000000000000000000000000000000', // optional
    data: '0x',
  },
  walletProvider: evmWalletProvider,
});

if (limitOrderResult.ok) {
  const { intent } = limitOrderResult.value;
  const intentHash = sodax.swaps.getIntentHash(intent);
  console.log('Limit order intent hash:', intentHash);
}
```

### Create Limit Order Intent (intent tx only — no relay/solver notify)

```typescript
// Signed
const result = await sodax.swaps.createLimitOrderIntent({
  params: limitOrderParams,
  walletProvider: evmWalletProvider,
});

// Raw
const rawResult = await sodax.swaps.createLimitOrderIntent({
  params: limitOrderParams,
  raw: true,
});
```

### Cancel Limit Order / Cancel Intent

`cancelLimitOrder` is a domain-specific alias for `cancelIntent`. Both take an object with `srcChainKey` and `intent`.

**Important:** `cancelIntent` takes `{ params: CancelIntentParams<K>, walletProvider }` — not positional arguments. You must supply `srcChainKey` explicitly because `Intent.srcChain` is a bigint relay ID that cannot narrow to a `SpokeChainKey` at the type level.

```typescript
import type { Intent, TxHashPair } from '@sodax/sdk';

// Retrieve the intent by hub-chain tx hash (or use the intent from createIntent/swap response)
const intentResult = await sodax.swaps.getIntent(hubTxHash);
if (!intentResult.ok) {
  console.error('Failed to fetch intent:', intentResult.error);
  return;
}
const intent: Intent = intentResult.value;

// Cancel the intent
const cancelResult = await sodax.swaps.cancelIntent({
  params: {
    srcChainKey: ChainKeys.BSC_MAINNET, // must match intent.srcChain
    intent,
  },
  walletProvider: evmWalletProvider,
  timeout: 60_000, // optional relay timeout (ms)
});

if (cancelResult.ok) {
  const { srcChainTxHash, dstChainTxHash }: TxHashPair = cancelResult.value;
  console.log('Cancel spoke tx:', srcChainTxHash);
  console.log('Cancel hub tx:', dstChainTxHash);
} else {
  console.error('Cancel failed:', cancelResult.error);
}
```

> **Error-type note:** `cancelIntent` and `cancelLimitOrder` return `Result<TxHashPair, Error | unknown>` — they were **not** migrated to the `SodaxError<C>` family. Don't `switch` on `error.code` here; treat the error as an opaque `Error` and use `instanceof Error` / `error.message` for diagnostics. The rest of this module (swap, createIntent, postExecution, createLimitOrder, createLimitOrderIntent) uses `SodaxError<SwapErrorCode>` — see [Error Handling](#error-handling).

### Build Cancel Intent (raw or signed — no relay wait)

Use `createCancelIntent` when you need only the cancel transaction (e.g. for gas estimation or manual relay):

```typescript
// Raw cancel tx
const rawCancelResult = await sodax.swaps.createCancelIntent({
  params: { srcChainKey: ChainKeys.BSC_MAINNET, intent },
  raw: true,
});

if (rawCancelResult.ok) {
  // rawTx is the chain-specific raw transaction (EvmRawTransaction for EVM chains,
  // SolanaRawTransaction for Solana, etc.) — TypeScript narrows it from `srcChainKey`.
  const rawTx = rawCancelResult.value;
}
```

***

## Submit Intent to Relay API

Called automatically by `swap`. Use this manually if you called `createIntent` separately.

```typescript
import type { IntentRelayRequest } from '@sodax/sdk';

const submitPayload = {
  action: 'submit',
  params: {
    chain_id: '0x38.bsc', // intentRelayChainId string for the source chain
    tx_hash: spokeTxHash,
    // data: relayData, // include when srcChain is Solana or Bitcoin
  },
} satisfies IntentRelayRequest<'submit'>;

const submitResult = await sodax.swaps.submitIntent(submitPayload);

if (submitResult.ok) {
  console.log('Relay accepted:', submitResult.value.success);
} else {
  console.error('Relay submission failed:', submitResult.error);
  // IMPORTANT: Store spokeTxHash + submitPayload in local storage and retry.
  // If the user leaves before this succeeds, funds may be stuck until re-submission.
}
```

***

## Get Intent Submit Tx Extra Data

Required only when the source chain is **Solana** or **Bitcoin**. Pass the returned `RelayExtraData` as `data` in `submitIntent`.

```typescript
import type { RelayExtraData } from '@sodax/sdk';

// Option 1: derive from hub-chain tx hash
const extraDataResult = await sodax.swaps.getIntentSubmitTxExtraData({
  txHash: '0x9b8c...', // hub-chain tx hash
});

// Option 2: derive from a pre-fetched Intent
const intentResult = await sodax.swaps.getIntent(hubTxHash);
if (intentResult.ok) {
  const extraDataResult2 = await sodax.swaps.getIntentSubmitTxExtraData({
    intent: intentResult.value,
  });

  if (extraDataResult2.ok) {
    const extraData: RelayExtraData = extraDataResult2.value;
    // Use extraData.address and extraData.payload in the relay submit request
  }
}
```

***

## Post Execution to Solver API

Called automatically by `swap` after the relay packet lands on the hub. Use this manually when orchestrating the swap steps yourself.

```typescript
import type { SolverExecutionRequest } from '@sodax/sdk';

const postExecutionResult = await sodax.swaps.postExecution({
  intent_tx_hash: hubTxHash, // hub-chain (Sonic) tx hash where the intent was registered
} satisfies SolverExecutionRequest);

if (postExecutionResult.ok) {
  const { answer, intent_hash } = postExecutionResult.value;
  console.log('Solver answer:', answer);         // 'OK'
  console.log('Intent hash:', intent_hash);
} else {
  console.error('Post execution failed:', postExecutionResult.error);
}
```

***

## Get Intent

Retrieve an `Intent` from the `IntentCreated` event on the hub chain.

```typescript
import type { Intent } from '@sodax/sdk';

const intentResult = await sodax.swaps.getIntent(hubTxHash);
if (intentResult.ok) {
  const intent: Intent = intentResult.value;
  console.log('Intent ID:', intent.intentId);
  console.log('Input amount:', intent.inputAmount);
  // Note: intent.srcChain / intent.dstChain are IntentRelayChainId (bigint),
  // not SpokeChainKey strings — use them only for relay ID comparisons.
}
```

***

## Get Filled Intent

Retrieve the fill state of an intent from the `IntentFilled` event log, emitted when a solver fills an intent on the hub chain.

```typescript
import type { IntentState } from '@sodax/sdk';

const filledIntentResult = await sodax.swaps.getFilledIntent(solverFillTxHash);

if (filledIntentResult.ok) {
  const state: IntentState = filledIntentResult.value;
  console.log('Exists:', state.exists);
  console.log('Remaining input:', state.remainingInput);
  console.log('Received output:', state.receivedOutput);
  console.log('Pending payment:', state.pendingPayment);
} else {
  console.error('No IntentFilled event found in tx:', filledIntentResult.error);
}
```

**`IntentState` fields:**

* `exists` — whether the intent exists on-chain
* `remainingInput` — unfilled input amount
* `receivedOutput` — output tokens received so far
* `pendingPayment` — whether a payment is pending

***

## Get Intent Status

Poll the solver API for the current execution status of an intent. The `intent_tx_hash` must be the hub-chain tx hash where the intent was registered.

```typescript
import { SolverIntentStatusCode } from '@sodax/sdk';
import type { SolverIntentStatusRequest } from '@sodax/sdk';

const statusResult = await sodax.swaps.getStatus({
  intent_tx_hash: hubTxHash,
} satisfies SolverIntentStatusRequest);

if (statusResult.ok) {
  const { status, fill_tx_hash } = statusResult.value;
  console.log('Status:', status); // SolverIntentStatusCode enum value

  if (status === SolverIntentStatusCode.SOLVED && fill_tx_hash) {
    console.log('Fill tx hash:', fill_tx_hash);
  }
}
```

***

## Get Solved Intent Packet

Poll the relayer until the solver's fill tx has been delivered to the destination chain. Call this after `getStatus` returns `SolverIntentStatusCode.SOLVED`.

```typescript
import { Sodax, ChainKeys, SolverIntentStatusCode } from '@sodax/sdk';
import type { PacketData } from '@sodax/sdk';

const statusResult = await sodax.swaps.getStatus({ intent_tx_hash: hubTxHash });

if (statusResult.ok && statusResult.value.status === SolverIntentStatusCode.SOLVED) {
  const { fill_tx_hash } = statusResult.value;

  if (fill_tx_hash) {
    const packetResult = await sodax.swaps.getSolvedIntentPacket({
      chainId: ChainKeys.ARBITRUM_MAINNET, // destination spoke chain key
      fillTxHash: fill_tx_hash,
      timeout: 120_000, // optional, default: 120 s
    });

    if (packetResult.ok) {
      const packet: PacketData = packetResult.value;
      console.log('Dst chain tx hash:', packet.dst_tx_hash);
      console.log('Status:', packet.status);
    } else {
      // packetResult.error is a plain Error from the relay layer (this method does NOT
      // adopt the SodaxError shape). error.message === 'RELAY_TIMEOUT' if the packet
      // didn't arrive in time. Discriminate via RELAY_ERROR_CODES.
      console.error('Packet not delivered:', packetResult.error);
    }
  }
}
```

***

## Get Intent Hash

Compute the keccak256 hash of an intent (its unique ID on the hub chain).

```typescript
import type { Intent, Hex } from '@sodax/sdk';

const intentHash: Hex = sodax.swaps.getIntentHash(intent);
console.log('Intent hash:', intentHash);
```

***

## Error Handling Examples

The full reference is in [**Error Handling**](#error-handling) above. The examples below show the common discrimination patterns end-to-end.

### Handling `swap` / `createLimitOrder` Errors

These methods perform multiple operations in sequence. On failure, `result.error` is a `SodaxError<SwapErrorCode>` — discriminate on `result.error.code`:

```typescript
import { isSodaxError } from '@sodax/sdk';

const swapResult = await sodax.swaps.swap({
  params: createIntentParams,
  walletProvider: evmWalletProvider,
});

if (!swapResult.ok) {
  const error = swapResult.error; // SwapError = SodaxError<SwapErrorCode>

  switch (error.code) {
    case 'EXECUTION_FAILED':
      // Solver notification failed — the intent may have been created and relayed
      // successfully. Check intent status manually, then retry postExecution.
      console.error('Underlying cause:', error.cause);
      break;

    case 'RELAY_TIMEOUT':
      // Relay didn't confirm within the timeout. Check intent status; resubmit with longer timeout.
      console.error('Underlying cause:', error.cause);
      break;

    case 'TX_SUBMIT_FAILED':
      // CRITICAL: spoke tx landed but relay submission failed.
      // Store spokeTxHash + submitPayload in local storage and retry promptly.
      console.error('Underlying cause:', error.cause);
      break;

    case 'RELAY_FAILED':
      // Other relay failure. error.context.relayCode disambiguates:
      //   'RELAY_POLLING_FAILED' — polling endpoint outage; query the hub directly.
      //   'UNKNOWN'              — forward-compat fallback for new relay error codes.
      break;

    case 'TX_VERIFICATION_FAILED':
      // Spoke tx could not be verified.
      break;

    case 'INTENT_CREATION_FAILED':
      // Spoke deposit / intent creation failed.
      break;

    case 'VALIDATION_FAILED':
      // Precondition failure — human-readable prose in error.message; details in error.context.
      console.error('Bad input:', error.message);
      break;

    case 'EXTERNAL_API_ERROR':
      // Solver returned a typed error. error.context.solverCode is the SolverIntentErrorCode.
      console.error('Solver code:', error.context?.solverCode);
      break;

    case 'UNKNOWN':
      console.error('Unexpected:', error.cause);
      break;
  }
}
```

### Handling `createIntent` Errors

`createIntent` returns `Result<CreateIntentResult, CreateIntentError>`. The narrow union is `'VALIDATION_FAILED' | 'INTENT_CREATION_FAILED' | 'UNKNOWN'`:

```typescript
const createIntentResult = await sodax.swaps.createIntent({
  params: createIntentParams,
  walletProvider: evmWalletProvider,
});

if (!createIntentResult.ok) {
  const error = createIntentResult.error;
  switch (error.code) {
    case 'VALIDATION_FAILED':
      // Unsupported token / invalid chain key / Bitcoin dust below 546 sats / wallet provider mismatch
      console.error('Validation failed:', error.message);
      break;
    case 'INTENT_CREATION_FAILED':
      // Spoke deposit failed (insufficient balance, network issues, simulation failure).
      console.error('Intent creation failed:', error.cause);
      break;
    case 'UNKNOWN':
      console.error('Unexpected:', error.cause);
      break;
  }
}
```

### Solver API Errors

`postExecution` errors are wrapped as `SodaxError<PostExecutionErrorCode>` (`EXECUTION_FAILED | EXTERNAL_API_ERROR | UNKNOWN`). The original `SolverErrorResponse.detail` is preserved on `error.context.solverDetail`:

```typescript
import { SolverIntentErrorCode } from '@sodax/sdk';

const postExecResult = await sodax.swaps.postExecution({ intent_tx_hash: hubTxHash });
if (!postExecResult.ok && postExecResult.error.code === 'EXTERNAL_API_ERROR') {
  const solverCode = postExecResult.error.context?.solverCode as SolverIntentErrorCode | undefined;
  if (solverCode === SolverIntentErrorCode.QUOTE_EXPIRED) {
    // refresh quote and retry
  }
}
```

`getQuote` and `getStatus` are **unchanged in this release** — they still return `Result<T, SolverErrorResponse>`:

```typescript
import { SolverIntentErrorCode } from '@sodax/sdk';
import type { SolverErrorResponse } from '@sodax/sdk';

const quoteResult = await sodax.swaps.getQuote(quoteRequest);
if (!quoteResult.ok) {
  const solverError = quoteResult.error as SolverErrorResponse;
  console.error('Solver error code:', solverError.detail.code);    // SolverIntentErrorCode
  console.error('Solver error message:', solverError.detail.message);
}
```


# Lend / Borrow (Money Market)

> **Error handling conventions:** This module uses the canonical `SodaxError<MoneyMarketErrorCode>` shape (same family as the swap module). Discriminate on `result.error.code` (e.g. `'RELAY_TIMEOUT'`, `'EXECUTION_FAILED'`); structured details live on `result.error.context` (`action`, `phase`, `relayCode`, `field`). See the **Error Handling** section below for the full per-method code table and migration notes from the legacy `error.message`-based pattern.

Money Market part of SDK provides abstractions to assist you with interacting with the cross-chain Money Market Smart Contracts.

All money market operations are accessed through the `moneyMarket` property of a `Sodax` instance:

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax();

// All money market methods are available through sodax.moneyMarket
const supplyResult = await sodax.moneyMarket.supply({
  params: {
    srcChainKey: ChainKeys.BSC_MAINNET,
    srcAddress: '0x...',
    token: '0x...',
    amount: 1000n,
    action: 'supply',
  },
  walletProvider: evmWalletProvider,
});
```

## Using SDK Config and Constants

SDK includes predefined configurations of supported chains, tokens and other relevant information for the client to consume. All configurations are accessible through the `config` property of the Sodax instance (`sodax.config`), or through service-specific properties for convenience.

**IMPORTANT**: If you want dynamic (backend API based - contains latest tokens) configuration, make sure to initialize the instance before usage:

```typescript
await sodax.config.initialize();
```

By default, configuration from the specific SDK version you are using is used.

```typescript
import { Sodax, ChainKeys, type SpokeChainKey, type Address } from '@sodax/sdk';

const sodax = new Sodax();
await sodax.config.initialize(); // Initialize for dynamic config (optional)

// All supported spoke chains (general config)
const spokeChains: SpokeChainKey[] = sodax.config.getSupportedSpokeChains();

// Get supported money market tokens for a specific chain
const supportedMoneyMarketTokens = sodax.moneyMarket.getSupportedTokensByChainId(ChainKeys.BSC_MAINNET);

// Get all supported money market tokens per chain
const allMoneyMarketTokens = sodax.moneyMarket.getSupportedTokens();

// Get all supported reserves (hub chain token addresses, i.e. money market on Sonic chain)
const supportedReserves: readonly Address[] = sodax.moneyMarket.getSupportedReserves();

// Check if token address for given spoke chain key is supported (through config service)
const isMoneyMarketSupportedToken: boolean = sodax.config.isMoneyMarketSupportedToken(ChainKeys.BSC_MAINNET, tokenAddress);

// Alternative: Access through config service
const moneyMarketTokensFromConfig = sodax.config.getSupportedMoneyMarketTokensByChainId(ChainKeys.BSC_MAINNET);
const allMoneyMarketTokensFromConfig = sodax.config.getSupportedMoneyMarketTokens();
```

Chain constants are available under the `ChainKeys` namespace (e.g. `ChainKeys.BSC_MAINNET`, `ChainKeys.SONIC_MAINNET`). The old `*_CHAIN_ID` constants have been replaced — see `packages/sdk/CHAIN_ID_MIGRATION.md` for the full rename mapping.

## Available Methods

All money market methods are accessible through `sodax.moneyMarket`:

### Token & Reserve Configuration

* `getSupportedTokensByChainId(chainKey)` - Get supported money market tokens for a specific chain
* `getSupportedTokens()` - Get all supported money market tokens per chain
* `getSupportedReserves()` - Get all supported money market reserves (hub chain addresses)

### Allowance & Approval

* `isAllowanceValid({ params })` - Check if token approval/trustline is sufficient
* `approve({ params, walletProvider, raw? })` - Approve tokens or establish Stellar trustline

### Money Market Operations

* `supply({ params, walletProvider, timeout? })` - Supply tokens (complete operation with relay)
* `createSupplyIntent({ params, walletProvider?, raw?, skipSimulation? })` - Create supply intent only
* `borrow({ params, walletProvider, timeout? })` - Borrow tokens (complete operation with relay)
* `createBorrowIntent({ params, walletProvider?, raw?, skipSimulation? })` - Create borrow intent only
* `withdraw({ params, walletProvider, timeout? })` - Withdraw tokens (complete operation with relay)
* `createWithdrawIntent({ params, walletProvider?, raw?, skipSimulation? })` - Create withdraw intent only
* `repay({ params, walletProvider, timeout? })` - Repay tokens (complete operation with relay)
* `createRepayIntent({ params, walletProvider?, raw?, skipSimulation? })` - Create repay intent only

### Gas Estimation

* `estimateGas(params)` - Estimate gas for an encoded transaction on a given spoke chain

### Data Retrieval & Formatting

* `data.getReservesList()` - Get list of all reserve addresses
* `data.getReservesData()` - Get raw aggregated reserve data
* `data.getReservesHumanized()` - Get humanized reserve data
* `data.getReserveData(asset)` - Get specific reserve data
* `data.getReserveNormalizedIncome(asset)` - Get normalized income for a specific asset (RAY precision)
* `data.getUserReservesData(spokeChainKey, userAddress)` - Get raw user reserve data
* `data.getUserReservesHumanized(spokeChainKey, userAddress)` - Get humanized user reserve data
* `data.getEModes()` - Get raw E-Mode data
* `data.getEModesHumanized()` - Get humanized E-Mode data
* `data.formatReservesUSD(request)` - Format reserves with USD conversions
* `data.formatReserveUSD(request)` - Format a single reserve with USD conversion
* `data.formatUserSummary(request)` - Format user portfolio summary with USD conversions

### Function Parameters Structure

All money market exec methods use a single `SpokeExecActionParams`-shaped object:

* **`params`**: The money market operation parameters (`MoneyMarketSupplyParams`, `MoneyMarketBorrowParams`, `MoneyMarketWithdrawParams`, or `MoneyMarketRepayParams`). Every params type carries:
  * `srcChainKey: K` — the source spoke chain (drives TypeScript narrowing of `walletProvider`)
  * `srcAddress: string` — the caller's address on the source chain
  * `token: string` — token address on the source chain (or destination chain for borrow/withdraw)
  * `amount: bigint` — amount in token's native decimals
  * `action: 'supply' | 'borrow' | 'withdraw' | 'repay'`
  * `dstChainKey?: SpokeChainKey` — optional destination chain (defaults to `srcChainKey`)
  * `dstAddress?: string` — optional destination address (defaults to `srcAddress`)
* **`walletProvider`**: The wallet provider for the source chain. Required when `raw` is `false` (or omitted); forbidden when `raw: true`. The type is automatically narrowed to the correct interface for the given `srcChainKey` (e.g. `IEvmWalletProvider` for EVM chains).
* **`raw`**: (Optional, default `false`) When `true`, returns unsigned transaction data instead of executing. When `true`, `walletProvider` must not be passed. Used in `create*Intent` and `approve` methods.
* **`skipSimulation`**: (Optional, default `false`) Skip transaction simulation before broadcast. Used in `create*Intent` methods.
* **`timeout`**: (Optional, default: `DEFAULT_RELAY_TX_TIMEOUT` = 120 seconds) Timeout in milliseconds for relay operations. Used in `supply`, `borrow`, `withdraw`, and `repay` methods.

## Allowance and Approval

Before making a money market action (supply, repay), you need to ensure the money market contract has sufficient allowance to spend your tokens. The SDK provides methods to check and set allowances for different types of spoke providers.

**Note**: For Stellar-based operations, the allowance and approval system works differently:

* **Source Chain (Stellar)**: The standard `isAllowanceValid` and `approve` methods check and establish trustlines automatically.
* **Destination Chain (Stellar)**: When Stellar is specified as the destination chain, the SDK checks both the sender's and recipient's trustlines via `isAllowanceValid`.

**Withdraw and borrow**: No on-chain approval is required for these actions. `isAllowanceValid` always returns `true` for them (though it validates the token is supported on the destination chain).

### Checking Allowance

The `isAllowanceValid` method checks if the current allowance is sufficient for the specified action:

```typescript
import { type MoneyMarketSupplyParams, ChainKeys } from '@sodax/sdk';

const supplyParams: MoneyMarketSupplyParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...', // Caller's address on the source chain
  token: '0x...', // Address of the token (spoke chain) to supply
  amount: 1000n, // Amount to supply (in token decimals)
  action: 'supply',
};

const isAllowanceValid = await sodax.moneyMarket.isAllowanceValid({ params: supplyParams });

if (!isAllowanceValid.ok) {
  // Handle error
  return;
}

if (!isAllowanceValid.value) {
  // Need to approve — allowance is insufficient
}
```

### Setting Allowance

The `approve` method sets the allowance for the specified action. The spender address is resolved internally based on the chain:

* **EVM Spoke Chains**: The spender is the spoke asset manager contract
* **Sonic (Hub) Chain**: The spender is the user's hub router contract
* **Stellar**: Creates/updates the required trustline

```typescript
import { type MoneyMarketSupplyParams, ChainKeys } from '@sodax/sdk';

const supplyParams: MoneyMarketSupplyParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...',
  token: '0x...',
  amount: 1000n,
  action: 'supply',
};

// First check if allowance is sufficient
const isAllowanceValid = await sodax.moneyMarket.isAllowanceValid({ params: supplyParams });

if (!isAllowanceValid.ok) {
  // Handle error
  return;
}

if (!isAllowanceValid.value) {
  // Approve — signed execution (raw: false or omitted)
  const approveResult = await sodax.moneyMarket.approve({
    params: supplyParams,
    walletProvider: evmWalletProvider,
  });

  if (!approveResult.ok) {
    // Handle approval error
    return;
  }

  // Transaction hash
  const txHash = approveResult.value;
}
```

To obtain unsigned approval calldata without broadcasting:

```typescript
// Raw transaction — walletProvider must NOT be passed
const rawApproveResult = await sodax.moneyMarket.approve({
  params: supplyParams,
  raw: true,
});

if (rawApproveResult.ok) {
  const rawTx = rawApproveResult.value; // EvmRawTransaction (or chain-specific equivalent)
}
```

### Supported Actions by Provider Type

The allowance and approval system supports different actions depending on the spoke chain type:

**EVM Spoke Providers:**

* `supply` - Approves the asset manager contract to spend tokens
* `repay` - Approves the asset manager contract to spend tokens

**Sonic Spoke Provider (Hub Chain):**

* `supply` - Approves the user hub router to spend tokens
* `repay` - Approves the user hub router to spend tokens

**Stellar:**

* `supply` / `repay` / `withdraw` / `borrow` — Checks and establishes trustlines

**Borrow and withdraw on EVM/hub chains do not require approval.**

### Stellar Trustline Requirements

For Stellar-based money market operations, you need to handle trustlines differently depending on whether Stellar is the source or destination chain. See [Stellar Trustline Requirements](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STELLAR_TRUSTLINE.md#money-market) for detailed information and code examples.

### Complete Example

Here's a complete example showing the allowance check and approval flow:

```typescript
import { type MoneyMarketSupplyParams, ChainKeys } from '@sodax/sdk';

const supplyParams: MoneyMarketSupplyParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...',
  token: '0x...',
  amount: 1000n,
  action: 'supply',
};

// Step 1: Check if allowance is sufficient
const allowanceCheck = await sodax.moneyMarket.isAllowanceValid({ params: supplyParams });

if (!allowanceCheck.ok) {
  console.error('Allowance check failed:', allowanceCheck.error);
  return;
}

// Step 2: Approve if allowance is insufficient
if (!allowanceCheck.value) {
  console.log('Insufficient allowance, approving...');

  const approveResult = await sodax.moneyMarket.approve({
    params: supplyParams,
    walletProvider: evmWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Approval failed:', approveResult.error);
    return;
  }

  console.log('Approval successful:', approveResult.value);
}

// Step 3: Now you can proceed with supply
const supplyResult = await sodax.moneyMarket.supply({
  params: supplyParams,
  walletProvider: evmWalletProvider,
});

if (supplyResult.ok) {
  const { srcChainTxHash, dstChainTxHash } = supplyResult.value;
  console.log('Supply successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Supply failed:', supplyResult.error);
}
```

### Estimate Gas for Raw Transactions

The `estimateGas` method estimates gas for an already-encoded transaction on a given spoke chain. Use this after obtaining a raw transaction from a `create*Intent` or `approve` call.

```typescript
import { type MoneyMarketSupplyParams, ChainKeys } from '@sodax/sdk';

// Example: Get raw supply transaction, then estimate its gas
const supplyIntentResult = await sodax.moneyMarket.createSupplyIntent({
  params: supplyParams,
  raw: true, // walletProvider must not be passed
});

if (supplyIntentResult.ok) {
  const { tx: rawTx } = supplyIntentResult.value;

  const gasEstimate = await sodax.moneyMarket.estimateGas({
    tx: rawTx,
    chainKey: ChainKeys.BSC_MAINNET,
  });

  if (gasEstimate.ok) {
    console.log('Estimated gas for supply:', gasEstimate.value);
  }
}
```

## Supply Tokens

Supply tokens to the money market pool. There are two methods available:

1. `supply`: Executes the spoke-side deposit, relays to the hub, and waits for the relay to settle.
2. `createSupplyIntent`: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay. Useful when you need manual relay control.

```typescript
import { type MoneyMarketSupplyParams, DEFAULT_RELAY_TX_TIMEOUT, ChainKeys } from '@sodax/sdk';

const supplyParams: MoneyMarketSupplyParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...',
  token: '0x...',
  amount: 1000n,
  action: 'supply',
};

// First check and set allowance if needed
const isAllowanceValid = await sodax.moneyMarket.isAllowanceValid({ params: supplyParams });

if (!isAllowanceValid.ok) {
  console.error('Allowance check failed:', isAllowanceValid.error);
  return;
}

if (!isAllowanceValid.value) {
  const approveResult = await sodax.moneyMarket.approve({
    params: supplyParams,
    walletProvider: evmWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Approval failed:', approveResult.error);
    return;
  }

  console.log('Approval transaction:', approveResult.value);
}

// Supply and relay (complete operation)
const supplyAndSubmitResult = await sodax.moneyMarket.supply({
  params: supplyParams,
  walletProvider: evmWalletProvider,
  timeout: DEFAULT_RELAY_TX_TIMEOUT, // Optional: timeout in milliseconds (default: 120 seconds)
});

if (supplyAndSubmitResult.ok) {
  const { srcChainTxHash, dstChainTxHash } = supplyAndSubmitResult.value;
  console.log('Supply successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Supply failed:', supplyAndSubmitResult.error);
}

// Create supply intent only (no relay wait)
const supplyIntentResult = await sodax.moneyMarket.createSupplyIntent({
  params: supplyParams,
  walletProvider: evmWalletProvider,
});

if (supplyIntentResult.ok) {
  const { tx: txHash, relayData } = supplyIntentResult.value;
  console.log('Supply intent created:', txHash);
} else {
  console.error('Supply intent creation failed:', supplyIntentResult.error);
}

// Get raw supply calldata (no wallet, no broadcast)
const rawSupplyResult = await sodax.moneyMarket.createSupplyIntent({
  params: supplyParams,
  raw: true,
});

if (rawSupplyResult.ok) {
  const { tx: rawTx, relayData } = rawSupplyResult.value;
  console.log('Raw supply tx:', rawTx);
}
```

## Borrow Tokens

Borrow tokens from the money market pool. Borrowed tokens can be delivered to a different spoke chain by specifying `dstChainKey` and `dstAddress`.

1. `borrow`: Executes the spoke-side message, relays to the hub, and waits for the relay to settle.
2. `createBorrowIntent`: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay.

```typescript
import { type MoneyMarketBorrowParams, DEFAULT_RELAY_TX_TIMEOUT, ChainKeys } from '@sodax/sdk';

const borrowParams: MoneyMarketBorrowParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...',
  token: '0x...', // Token address on the destination chain (defaults to srcChainKey)
  amount: 1000n,
  action: 'borrow',
  // Optional: deliver borrowed tokens to a different chain
  // dstChainKey: ChainKeys.ETHEREUM_MAINNET,
  // dstAddress: '0x...',
};

// Borrow and relay (complete operation)
const borrowAndSubmitResult = await sodax.moneyMarket.borrow({
  params: borrowParams,
  walletProvider: evmWalletProvider,
  timeout: DEFAULT_RELAY_TX_TIMEOUT,
});

if (borrowAndSubmitResult.ok) {
  const { srcChainTxHash, dstChainTxHash } = borrowAndSubmitResult.value;
  console.log('Borrow successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Borrow failed:', borrowAndSubmitResult.error);
}

// Create borrow intent only (no relay wait)
const borrowIntentResult = await sodax.moneyMarket.createBorrowIntent({
  params: borrowParams,
  walletProvider: evmWalletProvider,
});

if (borrowIntentResult.ok) {
  const { tx: txHash, relayData } = borrowIntentResult.value;
  console.log('Borrow intent created:', txHash);
} else {
  console.error('Borrow intent creation failed:', borrowIntentResult.error);
}
```

## Withdraw Tokens

Withdraw previously supplied tokens from the money market pool. Withdrawn tokens can be delivered to a different spoke chain by specifying `dstChainKey` and `dstAddress`.

1. `withdraw`: Executes the spoke-side message, relays to the hub, and waits for the relay to settle.
2. `createWithdrawIntent`: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay.

```typescript
import { type MoneyMarketWithdrawParams, DEFAULT_RELAY_TX_TIMEOUT, ChainKeys } from '@sodax/sdk';

const withdrawParams: MoneyMarketWithdrawParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...',
  token: '0x...', // Token address on the destination chain (defaults to srcChainKey)
  amount: 1000n,
  action: 'withdraw',
};

// Withdraw and relay (complete operation)
const withdrawAndSubmitResult = await sodax.moneyMarket.withdraw({
  params: withdrawParams,
  walletProvider: evmWalletProvider,
  timeout: DEFAULT_RELAY_TX_TIMEOUT,
});

if (withdrawAndSubmitResult.ok) {
  const { srcChainTxHash, dstChainTxHash } = withdrawAndSubmitResult.value;
  console.log('Withdraw successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Withdraw failed:', withdrawAndSubmitResult.error);
}

// Create withdraw intent only (no relay wait)
const withdrawIntentResult = await sodax.moneyMarket.createWithdrawIntent({
  params: withdrawParams,
  walletProvider: evmWalletProvider,
});

if (withdrawIntentResult.ok) {
  const { tx: txHash, relayData } = withdrawIntentResult.value;
  console.log('Withdraw intent created:', txHash);
} else {
  console.error('Withdraw intent creation failed:', withdrawIntentResult.error);
}
```

## Repay Tokens

Repay a borrowed position in the money market pool.

1. `repay`: Executes the spoke-side deposit, relays to the hub, and waits for the relay to settle.
2. `createRepayIntent`: Builds (and optionally broadcasts) only the spoke-side transaction without waiting for the relay.

```typescript
import { type MoneyMarketRepayParams, DEFAULT_RELAY_TX_TIMEOUT, ChainKeys } from '@sodax/sdk';

const repayParams: MoneyMarketRepayParams = {
  srcChainKey: ChainKeys.BSC_MAINNET,
  srcAddress: '0x...',
  token: '0x...',
  amount: 1000n,
  action: 'repay',
};

// First check and set allowance if needed
const isAllowanceValid = await sodax.moneyMarket.isAllowanceValid({ params: repayParams });

if (!isAllowanceValid.ok) {
  console.error('Allowance check failed:', isAllowanceValid.error);
  return;
}

if (!isAllowanceValid.value) {
  const approveResult = await sodax.moneyMarket.approve({
    params: repayParams,
    walletProvider: evmWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Approval failed:', approveResult.error);
    return;
  }

  console.log('Approval transaction:', approveResult.value);
}

// Repay and relay (complete operation)
const repayAndSubmitResult = await sodax.moneyMarket.repay({
  params: repayParams,
  walletProvider: evmWalletProvider,
  timeout: DEFAULT_RELAY_TX_TIMEOUT,
});

if (repayAndSubmitResult.ok) {
  const { srcChainTxHash, dstChainTxHash } = repayAndSubmitResult.value;
  console.log('Repay successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Repay failed:', repayAndSubmitResult.error);
}

// Create repay intent only (no relay wait)
const repayIntentResult = await sodax.moneyMarket.createRepayIntent({
  params: repayParams,
  walletProvider: evmWalletProvider,
});

if (repayIntentResult.ok) {
  const { tx: txHash, relayData } = repayIntentResult.value;
  console.log('Repay intent created:', txHash);
} else {
  console.error('Repay intent creation failed:', repayIntentResult.error);
}
```

## Error Handling

The Money Market module's user-facing methods return `Promise<Result<T, SodaxError<NarrowCode>>>`. Discriminate on `result.error.code` (a string literal) — never on `result.error.message`. This is the same canonical shape used by the swap module.

### The canonical error: `SodaxError<C>`

All MM-module errors are instances of `SodaxError`, exported from `@sodax/sdk`:

```typescript
import { SodaxError, isSodaxError } from '@sodax/sdk';

class SodaxError<C extends string = string> extends Error {
  readonly code: C;                         // string-literal discriminator
  readonly cause?: unknown;                 // ES2022 cause chain
  readonly context?: Record<string, unknown>;
  toJSON(): { name, code, message, stack, context, cause };
}
```

**Rules:**

* Discriminate on `error.code` — never on `error.message` (which is human-readable, may change).
* `error.cause` walks the underlying error chain (loggers like Sentry/Pino/Datadog walk this automatically).
* `error.context` carries structured metadata: `srcChainKey`, `dstChainKey`, `action`, `phase`, plus per-code extras (`relayCode`, `field`, …).
* `error.toJSON()` is the canonical logger surface; `JSON.stringify(error)` invokes it automatically and produces a logger-safe payload (bigints in `context` are coerced to strings, cause walked depth-3, no circular hazards).
* Use `isMoneyMarketError(e)` (broad) or one of the narrow guards `isMoneyMarketOrchestrationError(e)` / `isMoneyMarketCreateIntentError(e)` / `isMoneyMarketApproveError(e)` / `isMoneyMarketAllowanceCheckError(e)` / `isMoneyMarketGasEstimationError(e)` from `@sodax/sdk` instead of `instanceof SodaxError` in dapp/app code (bundle-safe).

### Per-method error type unions

The 4 orchestrators (`supply`/`borrow`/`withdraw`/`repay`) share **one** type — `MoneyMarketOrchestrationError`. They are not partitioned at the type level; instead, discriminate operations at runtime via `error.context.action`. Similarly, the 4 `create*Intent` methods share `MoneyMarketCreateIntentError`.

| Method                                                                                     | Error type                       | Codes                                                                                                                                                                        |
| ------------------------------------------------------------------------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `supply` / `borrow` / `withdraw` / `repay`                                                 | `MoneyMarketOrchestrationError`  | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN` |
| `createSupplyIntent` / `createBorrowIntent` / `createWithdrawIntent` / `createRepayIntent` | `MoneyMarketCreateIntentError`   | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                                    |
| `approve`                                                                                  | `MoneyMarketApproveError`        | `USER_REJECTED`, `VALIDATION_FAILED`, `APPROVE_FAILED`, `UNKNOWN`                                                                                                            |
| `isAllowanceValid`                                                                         | `MoneyMarketAllowanceCheckError` | `VALIDATION_FAILED`, `ALLOWANCE_CHECK_FAILED`, `UNKNOWN`                                                                                                                     |
| `estimateGas`                                                                              | `MoneyMarketGasEstimationError`  | `VALIDATION_FAILED`, `GAS_ESTIMATION_FAILED`, `UNKNOWN`                                                                                                                      |

Use `error.context.action` (`'supply' | 'borrow' | 'withdraw' | 'repay'`) to discriminate which orchestrator surfaced the error.

### Standard `context` fields

```typescript
{
  srcChainKey?: SpokeChainKey;
  dstChainKey?: SpokeChainKey;
  action?: 'supply' | 'borrow' | 'withdraw' | 'repay';  // on relay/verify codes
  phase?: 'validate' | 'intentCreation' | 'verify' | 'submit' | 'relay' |
          'approve' | 'allowanceCheck' | 'gasEstimation';
  relayCode?: 'SUBMIT_TX_FAILED' | 'RELAY_TIMEOUT' | 'RELAY_POLLING_FAILED' | 'UNKNOWN';
  field?: string;     // on VALIDATION_FAILED
  reason?: string;
}
```

### Discrimination example

```typescript
import { isMoneyMarketOrchestrationError } from '@sodax/sdk';

const result = await sodax.moneyMarket.supply({
  params: supplyParams,
  walletProvider: evmWalletProvider,
});

if (!result.ok) {
  // result.error is MoneyMarketOrchestrationError = SodaxError<MoneyMarketOrchestrationErrorCode>
  // with context.action === 'supply'
  switch (result.error.code) {
    case 'VALIDATION_FAILED':
      // Bad input — error.message is human-readable; error.context.field tells you which.
      console.error('Bad input:', result.error.message);
      break;

    case 'INTENT_CREATION_FAILED':
      // Spoke deposit failed.
      console.error('Intent creation failed:', result.error.cause);
      break;

    case 'TX_VERIFICATION_FAILED':
      // Spoke tx couldn't be verified on-chain.
      break;

    case 'TX_SUBMIT_FAILED':
      // CRITICAL: spoke tx landed but the relay submission failed. Funds may be in flight.
      // Persist the original supply params (or just the spoke tx hash) and retry submission.
      console.error('Relay submit failed; retry needed:', result.error.context?.relayCode);
      break;

    case 'RELAY_TIMEOUT':
      // Relay packet didn't confirm in time. Check intent status and retry with longer timeout.
      break;

    case 'RELAY_FAILED':
      // Other relay failure. error.context.relayCode disambiguates:
      //   'RELAY_POLLING_FAILED' — polling endpoint outage; query hub directly to confirm packet status.
      //   'UNKNOWN' — forward-compat fallback for new relay error codes.
      break;

    case 'EXECUTION_FAILED':
      // Catch-all for the supply orchestration; cause has the original.
      console.error('Supply failed:', result.error.cause);
      break;

    case 'UNKNOWN':
      console.error('Unexpected:', result.error.cause);
      break;
  }
}
```

### Handling create-intent errors

`create*Intent` methods only cover the spoke-side transaction. Their narrow union excludes relay/verify codes:

```typescript
const r = await sodax.moneyMarket.createSupplyIntent({
  params: supplyParams,
  walletProvider: evmWalletProvider,
});

if (!r.ok) {
  switch (r.error.code) {
    case 'VALIDATION_FAILED':
      // Input validation: bad amount, unsupported token, wrong wallet provider type.
      break;
    case 'INTENT_CREATION_FAILED':
      // Spoke deposit failed (insufficient balance, network issues, simulation revert).
      break;
    case 'UNKNOWN':
      break;
  }
}
```

### Handling allowance + approval errors

```typescript
const a = await sodax.moneyMarket.isAllowanceValid({ params: supplyParams });
if (!a.ok && a.error.code === 'ALLOWANCE_CHECK_FAILED') {
  // Network / RPC issue; surface to user, retry.
}

if (a.ok && !a.value) {
  const ap = await sodax.moneyMarket.approve({ params: supplyParams, walletProvider: evmWalletProvider });
  if (!ap.ok && ap.error.code === 'APPROVE_FAILED') {
    // Approval transaction failed.
  }
}
```

### Migration from the legacy `error.message`-based pattern

If you were on the previous CODE-string-on-`error.message` pattern (or the older `MoneyMarketError<Code>` typed shape that the public docs at <https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market#error-handling> document), here are the mappings:

| Before                                                 | After                                                                                                                                                |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `error.message === 'SUBMIT_TX_FAILED'`                 | `error.code === 'TX_SUBMIT_FAILED'`                                                                                                                  |
| `error.message === 'RELAY_TIMEOUT'`                    | `error.code === 'RELAY_TIMEOUT'`                                                                                                                     |
| `error.message === 'CREATE_SUPPLY_INTENT_FAILED'`      | `error.code === 'INTENT_CREATION_FAILED'`                                                                                                            |
| `error.message === 'CREATE_BORROW_INTENT_FAILED'` etc. | `error.code === 'INTENT_CREATION_FAILED'` etc.                                                                                                       |
| `error.message === 'SUPPLY_UNKNOWN_ERROR'` etc.        | `error.code === 'EXECUTION_FAILED'` etc. (with cause)                                                                                                |
| `isMoneyMarketSubmitTxFailedError(e)`                  | `e.code === 'TX_SUBMIT_FAILED'` (after `isMoneyMarketOrchestrationError(e)` and `e.context?.action === 'supply'`)                                    |
| Prose `error.message` for invariants                   | `error.code === 'VALIDATION_FAILED'`; the prose stays on `error.message`                                                                             |
| `error.data.payload` (historical)                      | **Not preserved.** Capture input params before calling if you need them for retry; this is the one departure from the historical published guidance. |

### Best practices

1. **Always handle `TX_SUBMIT_FAILED`**. Critical — the spoke tx landed but the relay submission failed. Funds may be in flight; persist the user's input and retry.
2. **Handle `RELAY_TIMEOUT` gracefully**. The spoke tx succeeded; the relay just didn't deliver in time. Check on-chain status before retrying.
3. **Discriminate `RELAY_FAILED` via `context.relayCode`**. `'RELAY_POLLING_FAILED'` (polling outage — packet status unknown) needs different UX from generic `'UNKNOWN'`.
4. **Use `error.cause` for forensics**. Every wrapped error preserves the original on `cause`. Loggers walk it automatically.
5. **Use `JSON.stringify(error)` for logging**. The `toJSON()` method handles bigint coercion + cause-chain truncation safely.
6. **Type-guard, don't `as`-cast**. Use the narrow guards (`isMoneyMarketOrchestrationError`, `isMoneyMarketCreateIntentError`, etc.) to narrow; an `as MoneyMarketOrchestrationError` cast after a generic `isSodaxError` check would silently widen the contract.

## Data Retrieval and Formatting

The Money Market SDK provides comprehensive data retrieval and formatting capabilities through the `MoneyMarketDataService`, accessible as `sodax.moneyMarket.data`. This service allows you to fetch reserve data, user data, and format them into human-readable values with USD conversions.

### Available Data Methods

#### Reserve Data

* `getReservesList(unfiltered?)` - Get list of all reserve addresses (bnUSD debt reserve filtered by default)
* `getReservesData()` - Get raw aggregated reserve data (bigint fields)
* `getReservesHumanized()` - Get humanized reserve data with decimal strings
* `getReserveData(asset)` - Get specific reserve data for an asset
* `getReserveNormalizedIncome(asset)` - Get normalized income for a specific asset (RAY precision)

#### User Data

* `getUserReservesData(spokeChainKey, userAddress)` - Get raw user reserve data
* `getUserReservesHumanized(spokeChainKey, userAddress)` - Get humanized user reserve data

#### E-Mode Data

* `getEModes()` - Get raw E-Mode data
* `getEModesHumanized()` - Get humanized E-Mode data

### Data Formatting

#### Formatting Reserve Data

* `formatReservesUSD(request)` - Format an array of reserves with USD conversions
* `formatReserveUSD(request)` - Format a single reserve with USD conversion

#### Formatting User Data

* `formatUserSummary(request)` - Format user portfolio summary with USD conversions

**NOTE**: If you need more customized formatting, see [math-utils](https://github.com/icon-project/sodax-sdks/tree/main/packages/sdk/src/moneyMarket/math-utils).

### Complete Example: Fetching and Formatting Data

```typescript
import { ChainKeys } from '@sodax/sdk';

// Fetch reserves data
const reserves = await sodax.moneyMarket.data.getReservesHumanized();

// Format reserves with USD conversions
const formattedReserves = sodax.moneyMarket.data.formatReservesUSD(
  sodax.moneyMarket.data.buildReserveDataWithPrice(reserves),
);

// Fetch user reserves data
const userReserves = await sodax.moneyMarket.data.getUserReservesHumanized(
  ChainKeys.BSC_MAINNET,
  userAddress,
);

// Format user summary with USD conversions
const userSummary = sodax.moneyMarket.data.formatUserSummary(
  sodax.moneyMarket.data.buildUserSummaryRequest(reserves, formattedReserves, userReserves),
);

console.log('formattedReserves:', formattedReserves);
console.log('userSummary:', userSummary);
```

### Step-by-Step Data Retrieval Process

#### 1. Fetch Raw Data

```typescript
import { ChainKeys } from '@sodax/sdk';

// Get humanized reserves data (decimal strings, no bigint)
const reserves = await sodax.moneyMarket.data.getReservesHumanized();

// Get user reserves data for a specific spoke chain
const userReserves = await sodax.moneyMarket.data.getUserReservesHumanized(
  ChainKeys.BSC_MAINNET,
  userAddress,
);
```

#### 2. Build Formatting Requests

```typescript
// Build request for reserve formatting
const reserveFormatRequest = sodax.moneyMarket.data.buildReserveDataWithPrice(reserves);

// Build request for user summary formatting
const userSummaryRequest = sodax.moneyMarket.data.buildUserSummaryRequest(
  reserves,
  formattedReserves,
  userReserves,
);
```

#### 3. Format Data

```typescript
// Format reserves with USD values
const formattedReserves = sodax.moneyMarket.data.formatReservesUSD(reserveFormatRequest);

// Format user summary with USD values
const userSummary = sodax.moneyMarket.data.formatUserSummary(userSummaryRequest);
```

### Data Structure Examples

#### Formatted Reserve Data

The `formattedReserves` array entries extend the humanized reserve shape with USD-denominated fields computed by `formatReservesUSD`:

```typescript
// Key USD-formatted fields added by formatReservesUSD:
{
  // Supply / borrow APY as decimal strings
  supplyAPY: string;
  variableBorrowAPY: string;

  // USD totals as decimal strings
  totalLiquidityUSD: string;
  totalVariableDebtUSD: string;
  availableLiquidityUSD: string;

  // Utilisation as decimal string
  utilizationRate: string;

  // Price from base currency
  priceInMarketReferenceCurrency: string;
  priceInUSD: string;

  // Original humanized fields (symbol, decimals, rates as decimal strings, etc.)
  // are preserved on the same object
}
```

#### Formatted User Summary

The `userSummary` object contains the user's portfolio information:

```typescript
// Key fields in FormatUserSummaryResponse:
{
  totalCollateralUSD: string;
  totalBorrowsUSD: string;
  totalLiquidityUSD: string;
  healthFactor: string;
  availableBorrowsUSD: string;
  currentTimestamp: number;
  userEmodeCategoryId: number;
  // ... additional fields
}
```

### Utility Functions

The SDK also provides utility functions for formatting specific values:

```typescript
import { formatPercentage, formatBasisPoints } from '@sodax/sdk';

// Format RAY-precision (27 decimal) rate as a percentage string
const rateValue = 52500000000000000000000000n; // 5.25% in RAY
const formattedRate = formatPercentage(rateValue, 27); // Returns "5.25%"

// Format basis points (1 bp = 0.01%) as a percentage string
const basisPointsValue = 250n; // 250 bps = 2.50%
const formattedBasisPoints = formatBasisPoints(basisPointsValue); // Returns "2.50%"
```


# Leverage Yield

> **Error handling conventions:** This module uses the canonical `SodaxError<LeverageYieldErrorCode>` shape (same family as the swap, bridge, and money market modules). Discriminate on `result.error.code` (e.g. `'INTENT_CREATION_FAILED'`, `'LOOKUP_FAILED'`); structured details live on `result.error.context` (`srcChainKey`, `action`, `method`, `phase`, `field`). See the **Error Handling** section below for the full per-method code table.

The `LeverageYieldService` class, reachable via `sodax.leverageYield`, exposes the SODAX **leverage-yield vaults** — leveraged-yield strategy vaults deployed on the Sonic hub. This page explains what a leverage-yield vault is, how the strategy works on-chain, and how the SDK lets you enter and exit a position from any spoke chain.

## How the leverage-yield vault works

A leverage-yield vault automates a **leveraged-lending loop** to turn a small yield spread into a larger one. It takes a yield-bearing deposit, uses it as collateral to borrow a correlated asset, swaps the borrowed amount back into more of the deposit asset, and re-supplies — repeating until it reaches a target leverage. The position is a **leveraged long on the `asset` / `borrowToken` peg**.

### The loop

```
            ┌───────────────────────────────────────────────┐
            │                                               │
            ▼                                               │
   deposit asset ──▶ supply as collateral ──▶ borrow borrowToken
   (e.g. sodaWEETH)   (earns supply APR)       (pays borrow APR)
                                                     │
                                                     ▼
                          re-supply ◀── swap back into asset
                                        (borrowToken → asset)
            │                                               ▲
            └───────────────── repeat until targetLTV ──────┘
```

Each pass adds more collateral (and more debt). The loop stops at the vault's **target loan-to-value (`targetLTV`)** — the fraction of collateral value the vault is willing to borrow against. The geometric series of repeated borrows converges to a fixed multiple of the original deposit.

### Leverage multiplier and where the yield comes from

At a target LTV `L`, the steady-state amounts (as a multiple of your principal) are:

| Quantity                                          | Formula       | Example at `L = 85%` |
| ------------------------------------------------- | ------------- | -------------------- |
| Total collateral supplied                         | `1 / (1 − L)` | `6.67×` principal    |
| Total borrowed                                    | `L / (1 − L)` | `5.67×` principal    |
| **Leverage multiplier** (`leverageMultiplierWad`) | `L / (1 − L)` | `5.67×`              |

Your net yield is the base supply rate on your principal **plus** the leverage multiplier applied to the *spread* between the supply and borrow rates:

```
netApr = supplyApr + leverageMultiplier × (supplyApr − borrowApr)
```

**Worked example** — supply 5%, borrow 3%, `targetLTV` 85%:

```
netApr = 5% + 5.67 × (5% − 3%) = 5% + 11.33% = 16.33%
```

A 2-point raw spread becomes a \~16% headline APR. That amplification is the entire point of the vault — and also its risk.

### The risk: the spread can invert

The multiplier cuts both ways. The same `5.67×` that amplifies a positive spread amplifies a negative one:

* If the **borrow rate rises above the supply rate**, every loop becomes a net cost and `netApr` goes **negative** (e.g. supply 3% / borrow 5% at 85% LTV → `netApr ≈ −8.3%`). The SDK returns `netAprRay` as a signed bigint precisely so this case is representable.
* Because the position holds real debt, a **depeg or price move** between `asset` and `borrowToken` can erode the health factor. `getPosition()` exposes the live `healthFactor`, `ltv`, `collateral`, and `debt` so a UI can warn before liquidation territory.
* A `targetLTV ≥ 100%` would imply infinite leverage and is rejected by `getApr()` with `VALIDATION_FAILED`.

> The APR from `getApr()` is **steady-state**, not realised APY. It assumes AAVE rates stay constant and the vault holds continuously at `targetLTV`. Real returns depend on rate volatility and rebalance cadence.

### The vault on-chain

Each vault is a deployed contract on the **Sonic hub** that follows the **ERC-4626** standard. A depositor's position is represented by the vault's ERC-4626 share token — the **`lsoda*` token** (e.g. `lsodaWEETH`). Two consequences:

* **The share token address&#x20;*****is*****&#x20;the vault proxy address.** `vault` and the `lsoda*` token are the same address.
* Standard ERC-4626 views (`previewDeposit`, `previewWithdraw`, `previewRedeem`, `maxWithdraw`, `totalAssets`) work, plus a non-standard `getPositionDetails()` that returns the live leveraged-position snapshot.

The lending pool is a **Sodax fork of AAVE**; the vault reads its reserve rates to compute APR and manages collateral/debt through it.

### A vault's descriptor

Each registered vault carries four static fields:

| Field         | Meaning                                                             | Example        |
| ------------- | ------------------------------------------------------------------- | -------------- |
| `name`        | Lookup key — the `lsoda*` share-token symbol                        | `'lsodaWEETH'` |
| `vault`       | Deployed vault proxy on Sonic — **also the `lsoda*` token address** | `0xD09d…701D`  |
| `asset`       | Underlying collateral (a Sodax vault token)                         | `sodaWEETH`    |
| `borrowToken` | Token borrowed against `asset`                                      | `sodaETH`      |

The registry lives in `@sodax/types` (`leverageYieldConfig`) and derives every address from the canonical `LsodaTokens` / `SodaTokens` registries, so a deployment-address change lives in exactly one place. Look vaults up with `listVaults()`, `getVault(name)`, or `getVaultByAddress(address)`.

## The SDK model: shares as solver-tradeable tokens

The service does **not** expose bespoke "deposit into vault" / "redeem from vault" calls. Instead, the `lsoda*` share token is registered as an ordinary **solver-tradeable token** (it is spread into the swap-supported tokens for Sonic). So entering and exiting a leveraged position are just **intent-based swaps**, executed by the service's own `vaultSwap()`:

* **Enter** a position = swap *any token → `lsoda*` shares*.
* **Exit** a position = swap *`lsoda*` shares → any token*.

`LeverageYieldService`'s job is to build the correct swap payload (the `CreateIntentParams` plus any execution flags) via `deposit()` / `withdraw()`, then execute it via `vaultSwap()` (or `createVaultIntent()` for manual relay control); the solver (plus the vault's ERC-4626 mechanics) does the rest. This is why deposits and withdrawals are cross-chain by default and require no vault-specific approvals on the spoke side.

`createVaultIntent` / `vaultSwap` are leverage-yield copies of the swap domain's `createIntent` / `swap()` — duplicated **deliberately** so the vault-specific execution modifiers (`hubWalletSwap`, per-intent `partnerFee`) live on the leverage-yield action wrapper (`VaultSwapActionParams`) and never leak into the generic swap surface.

### Partner fee

Because entering and exiting a position are ordinary intent-based swaps, `vaultSwap()` inherits the **global partner fee** configured on the Sodax instance by default. Set `config.swaps.partnerFee` and every leverage-vault **deposit** (and withdraw) deducts it from the input amount, exactly like any other swap:

```typescript
const sodax = new Sodax({
  swaps: { partnerFee: { address: '0xYourFeeReceiver...', percentage: 100 } }, // 100 bps = 1%
  // ...rest of config
});
```

To charge a fee **only on leverage-vault deposits** (or a different fee than the global one), pass `partnerFee` to `deposit()`. It rides on the returned payload as the per-intent fee override (`VaultSwapActionParams.partnerFee`) and takes precedence over `config.swaps.partnerFee` for that intent only:

```typescript
const intentResult = await sodax.leverageYield.deposit({
  // ...deposit params
  partnerFee: { address: '0xYourFeeReceiver...', percentage: 100 },
});
// intentResult.value = { params, partnerFee } — spread into vaultSwap() as usual.
```

The fee is taken inside `createVaultIntent()` (which `vaultSwap()` delegates to): the effective fee (per-intent override, falling back to the global config) is deducted from `inputAmount` and encoded into the intent's `data` as the `IntentDataType.FEE` envelope, so the intents contract routes it to the partner address on the hub. `LeverageYieldService.deposit()` builds the `CreateIntentParams` with `data: '0x'`; the fee `data` is then constructed at intent-creation time, so the deposit needs no fee plumbing of its own. When neither fee is set, no fee is taken (the historical "fee not charged" case).

## Flows

### Deposit (any token → `lsoda*`)

`deposit()` builds the `LeverageYieldSwapPayload` — `{ params: CreateIntentParams }` — for swapping any solver-supported `inputToken` on a spoke chain into the vault's `lsoda*` share token. The output is delivered to the user's **hub wallet** on Sonic (not back to the spoke) so a later `withdraw()` can spend it from there. The `deadline` defaults to the hub (Sonic) block timestamp + 5 minutes — anchored to on-chain time rather than the client clock; `solver` defaults to `0x0` (any solver). To size `minOutputAmount`, quote via `sodax.swaps.getQuote` with the vault address as the destination token (`token_dst`) — `lsoda*` shares are solver-tradeable — then subtract your slippage tolerance.

```typescript
import { ChainKeys } from '@sodax/sdk';

const vault = sodax.leverageYield.getVault('lsodaWEETH');

const intentResult = await sodax.leverageYield.deposit({
  vault: vault.vault,
  srcChainKey: ChainKeys.ARBITRUM_MAINNET,
  srcAddress: '0xYourArbitrumEOA...',
  inputToken: '0x...weETHonArbitrum',
  inputAmount: 1_000_000_000_000_000_000n, // input-token decimals
  minOutputAmount: 900_000_000_000_000_000n, // lsoda* (18 dp), slippage already applied
});

if (intentResult.ok) {
  // Spread the payload straight into the vault-swap executor.
  const swapResult = await sodax.leverageYield.vaultSwap({
    ...intentResult.value,
    walletProvider: evmWalletProvider,
  });
}
```

### Withdraw (`lsoda*` → any token)

`withdraw()` builds the `LeverageYieldSwapPayload` for swapping the vault's `lsoda*` shares — which sit in the user's hub wallet — back into any solver-supported token on any chain. The payload carries **`hubWalletSwap: true`**: `vaultSwap()` then authorises the hub wallet to spend the shares via a `Connection.sendMessage` the user signs on `srcChainKey`, instead of a spoke-side asset-manager deposit. `withdraw()` is `async` (it reads the hub block timestamp for the default `deadline`) and returns a `Result` for a call shape uniform with `deposit()`. Size `minOutputAmount` the same way as a deposit — `sodax.swaps.getQuote` with the vault address as the **source** token (`token_src`) — then subtract slippage.

```typescript
const intentResult = await sodax.leverageYield.withdraw({
  vault: vault.vault,
  srcChainKey: ChainKeys.ARBITRUM_MAINNET, // chain the user signs the sendMessage on
  srcAddress: '0xYourArbitrumEOA...',
  dstChainKey: ChainKeys.ARBITRUM_MAINNET, // where the swapped-back token is delivered
  outputToken: '0x...weETHonArbitrum',
  inputAmount: shareBalance, // lsoda* shares (18 dp)
  minOutputAmount: 900_000_000_000_000_000n,
  // recipient?: defaults to srcAddress
});

if (intentResult.ok) {
  const swapResult = await sodax.leverageYield.vaultSwap({
    ...intentResult.value, // hubWalletSwap: true is already set on the payload
    walletProvider: evmWalletProvider,
  });
}
```

To size a full exit, read the withdrawable balance with `getMaxWithdrawForUser(vault, srcChainKey, srcAddress)` (already dust-buffered) or the raw share balance with `getShareBalanceForUser(...)`.

### Direct allowance management (hub-side)

`approve()` and `isAllowanceValid()` manage the allowance of the vault's underlying `asset` to the vault on Sonic. These are for callers interacting with the vault **directly on the hub** — the swap-style `deposit()` flow handles its own approvals, so most integrations never need them.

```typescript
const ok = await sodax.leverageYield.isAllowanceValid({
  vault: vault.vault,
  amount: 1_000_000_000_000_000_000n,
  owner: '0xHubWallet...',
});

if (ok.ok && !ok.value) {
  await sodax.leverageYield.approve({
    vault: vault.vault,
    amount: 1_000_000_000_000_000_000n,
    walletProvider: evmWalletProvider,
  });
}
```

## Methods

### deposit

Builds the `LeverageYieldSwapPayload` for a deposit (any token → `lsoda*`, delivered to the hub wallet). An optional `partnerFee` is forwarded on the payload as the swap layer's per-intent fee override. **Returns:** `Promise<Result<LeverageYieldSwapPayload, LeverageYieldCreateIntentError>>`. `context.action` is `'deposit'`.

### withdraw

Builds the `LeverageYieldSwapPayload` for a withdraw (`lsoda*` → any token), with `hubWalletSwap: true` set on the payload. Synchronous. **Returns:** `Result<LeverageYieldSwapPayload, LeverageYieldCreateIntentError>`. `context.action` is `'withdraw'`.

### createVaultIntent

Creates the vault swap intent on the user's source spoke chain without submitting it to the solver — the leverage-yield copy of the swap domain's `createIntent`, honouring `hubWalletSwap` (withdraw routes via a hub-wallet `Connection.sendMessage`) and the per-intent `partnerFee` override. With `raw: true` returns unsigned tx data. Use it directly when you drive the relay yourself (e.g. the backend submit-tx path): relay the returned `relayData` with the shared `relayTxAndWaitPacket` helper, then call `notifySolver` with the hub-side intent tx hash to complete the flow. **Returns:** `Promise<Result<CreateVaultIntentResult<K, Raw>, LeverageYieldCreateIntentError>>` — `tx`, the constructed `intent` (with `feeAmount`), and `relayData`.

### vaultSwap

Executes the full end-to-end vault swap: `createVaultIntent` → verify the spoke tx → relay to the hub (skipped when the source is Sonic) → notify the solver. Spread a `LeverageYieldSwapPayload` into it alongside the wallet provider: `vaultSwap({ ...payload, walletProvider })`. **Returns:** `Promise<Result<VaultSwapResponse, LeverageYieldSwapError>>` — `solverExecutionResponse`, `intent`, and `intentDeliveryInfo`. `context.action` is `'vaultSwap'`.

### notifySolver

Notifies the solver that a vault intent has landed on the hub, triggering it to fill — the leverage-yield copy of the swap domain's `postExecution`. `vaultSwap` calls it automatically; it is public so callers who created the intent with `createVaultIntent` and relayed it themselves can finish the flow manually. Pass `{ intent_tx_hash }` — the hub-chain (Sonic) tx hash where the intent registered (the relay packet's `dst_tx_hash`, or the spoke tx hash for hub-sourced intents). **Returns:** `Promise<Result<SolverExecutionResponse, LeverageYieldPostExecutionError>>` — emits only `EXECUTION_FAILED` / `EXTERNAL_API_ERROR` / `UNKNOWN`.

### approve

Approves the vault's underlying `asset` to the vault on Sonic. Resolves `asset()` on-chain, then delegates to `Erc20Service.approve`. With `raw: true` returns unsigned tx data and does not broadcast. **Returns:** `Promise<Result<TxReturnType<HubChainKey, R> | EvmReturnType<true>, LeverageYieldApproveError>>`. `context.action` is `'approve'`.

### isAllowanceValid

Reads the on-chain allowance of the vault's `asset` for `owner → vault` and returns `true` when it covers `amount`. **Returns:** `Promise<Result<boolean, LeverageYieldAllowanceCheckError>>`. The error carries `phase: 'allowanceCheck'`.

### getApr

Computes the **steady-state** APR of a vault from the AAVE supply/borrow rates of its `asset` and `borrowToken`, scaled by the vault's target leverage (see *How the leverage-yield vault works* above for the formula and caveats). **Returns:** `Promise<Result<LeverageYieldApr, LeverageYieldLookupError>>`. Rates are in RAY (`1e27`); the leverage multiplier is in WAD (`1e18`). `netAprRay` can be negative; `targetLTV ≥ 100%` is rejected with `VALIDATION_FAILED`.

### getLsdApr / getEffectiveApr

`getLsdApr` fetches the underlying LSD's staking yield from DefiLlama; `getEffectiveApr` folds that yield into `getApr`'s AAVE-only view for the honest leveraged APR. **Returns:** `Promise<Result<LeverageYieldLsdApr | LeverageYieldEffectiveApr, LeverageYieldLookupError>>`.

> **No SDK-level caching.** Each call hits the DefiLlama HTTP API fresh — there is no built-in cache or rate limiting. UIs are typically protected by their query layer (the demo uses a 60 s `refetchInterval`), but server-side aggregators or tight polling loops should add their own caching to avoid DefiLlama rate limits. A failed/timed-out DefiLlama fetch falls back gracefully rather than throwing.

### getPosition

Reads the live leveraged-position snapshot (`collateral`, `debt`, `ltv`, `healthFactor`, `idleAsset`) via the non-standard `getPositionDetails()` view. **Returns:** `Promise<Result<LeverageYieldPosition, LeverageYieldLookupError>>`.

### getMaxWithdraw / getMaxWithdrawForUser

`getMaxWithdraw(vault, owner)` returns the ERC-4626 `maxWithdraw` for an owner. `getMaxWithdrawForUser(vault, srcChainKey, srcAddress)` resolves the user's hub wallet first, then returns `maxWithdraw` **less a small dust buffer** (1 000 wei) — trimming sidesteps an asset-denominated `withdraw` round-up that asks for one more share than the user holds. Clamps to `0n` rather than underflowing when the balance is below the buffer. **Returns:** `Promise<Result<bigint, LeverageYieldLookupError>>`.

### getShareBalance / getShareBalanceForUser

`lsoda*` share balance for an address, or for a user via their resolved hub wallet. **Returns:** `Promise<Result<bigint, LeverageYieldLookupError>>`.

### getTotalAssets / previewDeposit / previewWithdraw / previewRedeem / getAsset

Thin ERC-4626 reads: total assets held by the vault (TVL), share/asset previews, and the vault's `asset()`. All **return** `Promise<Result<bigint, LeverageYieldLookupError>>` (`getAsset` returns `Result<Address, …>`).

### listVaults / getVault / getVaultByAddress

Registry lookups (synchronous, no `Result`). `listVaults()` returns the registry from the active config; `getVault(name)` looks up by the `lsoda*` symbol; `getVaultByAddress(address)` looks up by the proxy address (case-insensitive). Return `undefined` for unknown keys.

## Types

### LeverageYieldVault

```typescript
type LeverageYieldVault = {
  name: string;        // lookup key — the lsoda* share-token symbol (e.g. 'lsodaWEETH')
  vault: Address;      // deployed vault proxy on Sonic — also the lsoda* token address
  asset: Address;      // underlying Sodax vault token (e.g. sodaWEETH)
  borrowToken: Address; // token borrowed against asset (e.g. sodaETH)
};
```

### LeverageYieldApr

```typescript
type LeverageYieldApr = {
  supplyAprRay: bigint;          // AAVE supply rate of asset, in RAY (1e27)
  borrowAprRay: bigint;          // AAVE variable borrow rate of borrowToken, in RAY
  targetLtvBps: bigint;          // vault targetLTV(), in basis points
  leverageMultiplierWad: bigint; // targetLTV / (1 - targetLTV), in WAD (1e18)
  netAprRay: bigint;             // net APR at targetLTV, in RAY — can be negative
};
```

### LeverageYieldPosition

```typescript
type LeverageYieldPosition = {
  collateral: bigint;   // collateral supplied to the pool — vault-asset units (18 decimals)
  debt: bigint;         // outstanding borrowToken debt — vault-asset units (18 decimals)
  ltv: bigint;          // current loan-to-value in basis points (out of 10_000; 8_500 = 85%)
  healthFactor: bigint; // AAVE health factor in WAD (1e18); below 1e18 implies liquidation risk, type(uint256).max = no debt
  idleAsset: bigint;    // asset held by the vault but not yet supplied — vault-asset units (18 decimals)
};
```

## Error Handling

All async public methods return `Promise<Result<T, SodaxError<NarrowCode>>>`. Discriminate on `result.error.code` (a string literal) — never on `result.error.message`. Same canonical shape used by swap, bridge, and money market.

The service owns the full vault-swap lifecycle: `deposit` / `withdraw` build swap payloads, `createVaultIntent` submits the intent on the source spoke chain, `vaultSwap` orchestrates create → verify → relay → notify-solver, `approve` / `isAllowanceValid` manage the Sonic allowance, and the read methods query on-chain state. Relay/tx-verification codes appear **only** on `vaultSwap`. `deposit` / `withdraw` can additionally emit `LOOKUP_FAILED` (`method: 'resolveDeadline'`) when the default-`deadline` hub-block read fails — an RPC outage, not an intent-build failure. Every other method stays within the create-intent, approve, allowance-check, and lookup subsets.

### Per-method error code unions

| Method                | Narrow type                                                  | Codes                                                                                                                                            |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `deposit`, `withdraw` | `LeverageYieldCreateIntentError \| LeverageYieldLookupError` | `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `LOOKUP_FAILED`, `UNKNOWN`                                                                        |
| `createVaultIntent`   | `LeverageYieldCreateIntentError`                             | `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                         |
| `vaultSwap`           | `LeverageYieldSwapError`                                     | create-intent codes plus `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `EXTERNAL_API_ERROR` |
| `approve`             | `LeverageYieldApproveError`                                  | `VALIDATION_FAILED`, `APPROVE_FAILED`, `UNKNOWN`                                                                                                 |
| `isAllowanceValid`    | `LeverageYieldAllowanceCheckError`                           | `VALIDATION_FAILED`, `ALLOWANCE_CHECK_FAILED`, `UNKNOWN`                                                                                         |
| all read methods      | `LeverageYieldLookupError`                                   | `VALIDATION_FAILED`, `LOOKUP_FAILED`, `UNKNOWN`                                                                                                  |

The broad union type is `LeverageYieldError` (`SodaxError<LeverageYieldErrorCode>`).

### Discriminators

* **`context.action`** — the user-facing operation (`'deposit' | 'withdraw' | 'approve' | 'vaultSwap'`).
* **`context.method`** — partitions `LOOKUP_FAILED` across the read methods (`'getApr'`, `'getPosition'`, `'getMaxWithdrawForUser'`, `'getShareBalance'`, …) and `'resolveDeadline'` for the `deposit` / `withdraw` default-deadline read.
* **`context.field`** — set on `VALIDATION_FAILED` (`'inputAmount'`, `'vault'`, `'inputToken'`, `'outputToken'`, `'amount'`, `'targetLtvBps'`).
* **`context.phase`** — `'intentCreation' | 'approve' | 'allowanceCheck' | 'lookup' | 'validate' | 'verify' | 'relay' | 'postExecution'`.

### Guards

Use the exported guards instead of `instanceof SodaxError` (bundle-safe):

* `isLeverageYieldError(e)` — broad guard for any leverage-yield error.
* `isLeverageYieldCreateIntentError(e)` — `createVaultIntent` and the create-intent arm of `deposit` / `withdraw` (whose default-deadline read can instead yield a `LOOKUP_FAILED` caught by `isLeverageYieldLookupError`).
* `isLeverageYieldSwapError(e)` — `vaultSwap`.
* `isLeverageYieldApproveError(e)` — `approve`.
* `isLeverageYieldAllowanceCheckError(e)` — `isAllowanceValid`.
* `isLeverageYieldLookupError(e)` — read methods.

### Discrimination example

```typescript
const result = await sodax.leverageYield.deposit({ /* ... */ });

if (!result.ok) {
  switch (result.error.code) {
    case 'VALIDATION_FAILED':
      // Bad input — error.context.field names the offending parameter.
      console.error('Bad input:', result.error.message, result.error.context?.field);
      break;
    case 'INTENT_CREATION_FAILED':
      // Building the swap params failed (e.g. hub-wallet resolution). cause has the original.
      console.error('Intent creation failed:', result.error.cause);
      break;
    case 'LOOKUP_FAILED':
      // An on-chain read failed — e.g. the default-deadline hub-block read (method='resolveDeadline').
      console.error('Lookup failed:', result.error.context?.method, result.error.cause);
      break;
    case 'UNKNOWN':
      console.error('Unexpected:', result.error.cause);
      break;
  }
}
```

### Best practices

1. **Discriminate on `error.code`, not `error.message`.** Messages are human-readable and may change.
2. **Partition reads via `context.method`.** All read methods share `LeverageYieldLookupError`; `context.method` tells you which read failed.
3. **Use `error.cause` for forensics.** Every wrapped error preserves the original on `cause`; loggers walk it automatically.
4. **Use `JSON.stringify(error)` for logging.** `toJSON()` handles bigint coercion + cause-chain truncation safely.
5. **Type-guard, don't `as`-cast.** Use the `isLeverageYield*Error` guards to narrow.

## Chain Keys

Vaults live on the Sonic hub; `deposit` / `withdraw` route by the user's spoke-side chain (`srcChainKey`) and the output chain (`dstChainKey`). Use `ChainKeys` from `@sodax/sdk` for chain-key constants.


# Effective APR

How the SDK computes the headline APR for a leverage-yield vault — combining on-chain AAVE rates with the underlying LSD's native staking yield.

## TL;DR

A leverage-yield vault deposits an LSD (weETH, wstETH) into AAVE, borrows ETH against it, swaps the borrowed ETH back to the LSD, and re-deposits — looping to a multiple of the user's principal. Yield comes from three places:

1. **AAVE supply rate** on the collateral leg (paid by other AAVE borrowers).
2. **LSD staking yield** baked into the token price (the LSD appreciates against ETH).
3. The above two **amplified by leverage**, minus the AAVE borrow rate paid on the borrow leg.

The SDK's `getApr()` reads only AAVE rates and misses (2). `getEffectiveApr()` adds the LSD yield from DefiLlama and reports the honest number.

## Formula

```
effectiveSupply = aaveSupply + lsdStaking
spread          = effectiveSupply − aaveBorrow
effectiveNet    = effectiveSupply + leverage × spread

leverage        = targetLTV / (1 − targetLTV)
```

All terms are APRs (annualised, in percent). `targetLTV` comes from the vault contract (`targetLTV()`, returned in basis points), `aaveSupply` / `aaveBorrow` come from the AAVE pool's `getReserveData()`, and `lsdStaking` comes from DefiLlama (see [LSD Source](#lsd-source-defillama)).

## Derivation

The formula is the closed-form limit of the vault's recursive supply→borrow→re-supply loop.

Start with deposit `X` of LSD as collateral:

| Step | Supplied                     | Borrowed                             |
| ---- | ---------------------------- | ------------------------------------ |
| 0    | `X`                          | 0                                    |
| 1    | `X`                          | `LTV · X`                            |
| 2    | `X · (1 + LTV)`              | `LTV · X · (1 + LTV)`                |
| 3    | `X · (1 + LTV + LTV²)`       | `LTV · X · (1 + LTV + LTV²)`         |
| ∞    | `X · Σ LTVⁿ = X / (1 − LTV)` | `LTV · X / (1 − LTV) = X · leverage` |

So in the steady state, per `X` of original deposit:

* **Total supplied** = `1 / (1 − LTV) = 1 + leverage`
* **Total borrowed** = `leverage`

Earnings:

```
supply earnings = (1 + leverage) × supplyRate
borrow cost     = leverage × borrowRate

net = (1 + leverage) × supplyRate − leverage × borrowRate
    = supplyRate + leverage × (supplyRate − borrowRate)
```

That last line is the formula. The `+ supplyRate` is your **principal** still earning yield; the `leverage × spread` is the **amplified spread** from the loop.

## The LSD twist

For LSD-backed vaults, the collateral leg earns yield from **two independent sources** that compose linearly:

* **AAVE's `currentLiquidityRate`** for the LSD — depends on who else is borrowing it on AAVE. Often near zero for LSDs because they're not heavily borrowed.
* **The LSD's native staking yield** — the LSD token (weETH, wstETH) appreciates against ETH at the issuer's staking rate. This is *off-chain* yield as far as AAVE is concerned — it doesn't show in `currentLiquidityRate`. It just accrues to the token-price.

So `supplyRate → aaveSupply + lsdStaking`. The formula stays identical, only the supply-side input changes:

```
effectiveNet = (aaveSupply + lsdStaking) + leverage × ((aaveSupply + lsdStaking) − aaveBorrow)
```

When the LSD's staking yield is the dominant component (it usually is), ignoring it makes the headline APR look negative — even when the position is genuinely profitable.

## LSD source: DefiLlama

The SDK fetches `lsdStaking` from DefiLlama's per-pool chart endpoint:

```
GET https://yields.llama.fi/chart/<poolId>
→ { status: 'success', data: [{ timestamp, tvlUsd, apy, apyBase, apyReward, ... }, ...] }
```

`apy` is the **compounded yield including reward tokens** (what the depositor actually earns). The SDK takes `data[data.length - 1].apy` (latest entry) and folds it into the formula above.

Each vault registers a DefiLlama `poolId` in `@sodax/types`:

| Vault         | LSD    | DefiLlama project  | poolId                                 |
| ------------- | ------ | ------------------ | -------------------------------------- |
| `lsodaWEETH`  | weETH  | `ether.fi-stake`   | `46bd2bdf-6d92-4066-b482-e885ee172264` |
| `lsodaWSTETH` | wstETH | `lido` (via stETH) | `747c1d2a-c668-4682-b9f9-296708a3dd90` |

The endpoint is CORS-enabled (`access-control-allow-origin: *`) so a browser-side dApp can hit it directly — no backend required.

If the live fetch fails, the SDK falls back to a hardcoded `fallbackAprPct` in the same registry entry and flags the response with `stale: true` so the UI can show the value as an estimate.

## Worked example

A snapshot of the weETH vault at the moment of writing:

| Term                     | Value                     | Source                                                       |
| ------------------------ | ------------------------- | ------------------------------------------------------------ |
| `aaveSupply` (sodaWEETH) | 0.00%                     | AAVE `getReserveData(asset).currentLiquidityRate`            |
| `lsdStaking` (weETH)     | 2.92%                     | DefiLlama pool `46bd2bdf-…`, latest `apy`                    |
| `effectiveSupply`        | 0.00 + 2.92 = **2.92%**   | sum                                                          |
| `aaveBorrow` (sodaETH)   | 1.04%                     | AAVE `getReserveData(borrowToken).currentVariableBorrowRate` |
| `spread`                 | 2.92 − 1.04 = **1.88%**   | difference                                                   |
| `targetLTV`              | 85.00%                    | vault contract `targetLTV()`                                 |
| `leverage`               | 0.85 / 0.15 = **5.67×**   | derived                                                      |
| `leverage × spread`      | 5.67 × 1.88 = **10.66%**  | multiplication                                               |
| **`effectiveNet`**       | 2.92 + 10.66 = **13.58%** | final                                                        |

For comparison, the **AAVE-only** number (what `getApr()` returns, ignoring the LSD's staking yield) on the same vault is:

```
aaveOnlyNet = 0% + 5.67 × (0% − 1.04%) = −5.90%
```

The 19.5-percentage-point gap between `effectiveNet` and `aaveOnlyNet` is **the whole reason these vaults exist**: it's the LSD's staking yield multiplied by leverage.

## Code references

* Formula implementation: [`LeverageYieldService.getEffectiveApr`](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/src/leverageYield/LeverageYieldService.ts) (`packages/sdk/src/leverageYield/LeverageYieldService.ts`)
* LSD fetcher: `fetchDefillamaApr` in the same file
* AAVE-only calculation: `LeverageYieldService.getApr` (also same file)
* Vault registry (with `lsdSource.poolId`): [`packages/types/src/leverageYield/leverageYield.ts`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/leverageYield/leverageYield.ts)

## Caveats

* **Steady-state, not realised**: this is the APR a position earns *at* `targetLTV` if rates stay constant. Realised APY depends on rebalance cadence, rate volatility, and how long the vault sits off-target.
* **`lsdStaking` is a moving average**: DefiLlama publishes the trailing yield, typically a 7- to 30-day window. Spot yield can differ by a few bp.
* **Vault drift**: if `position.ltv` < `targetLTV`, the actual position is under-leveraged and earns less than the headline number. Watch `LeverageYieldService.getPosition()` for the live LTV.
* **Negative spread case**: if `aaveBorrow > effectiveSupply`, leverage *amplifies the loss*. The formula handles this correctly (`effectiveNet` goes negative) — it's a real signal, not a bug.


# Bridge

> **Error handling conventions:** This module uses the canonical `SodaxError<BridgeErrorCode>` shape (same family as the swap and money market modules). Discriminate on `result.error.code` (e.g. `'RELAY_TIMEOUT'`, `'INTENT_CREATION_FAILED'`); structured details live on `result.error.context` (`srcChainKey`, `dstChainKey`, `phase`, `relayCode`, `field`). See the **Error Handling** section below for the full per-method code table and migration notes from the legacy `error.message`-based pattern.

The `BridgeService` class, reachable via `sodax.bridge`, orchestrates cross-chain token transfers within the SODAX hub-and-spoke architecture.

Bridging works by depositing tokens into a spoke vault on the source chain, which triggers a cross-chain message relayed to the Sonic hub. The hub then performs vault transformations (deposit/withdraw) and forwards the tokens to the destination chain via the asset manager.

Three transfer directions are supported:

* **Spoke → Hub** — deposit into hub vault
* **Hub → Spoke** — withdrawal from hub vault
* **Spoke → Spoke** — deposit on source + withdraw on destination

## Methods

### isAllowanceValid

Checks whether the caller has sufficient token allowance to execute the bridge.

The required spender varies by chain type:

* **Hub (Sonic)**: the caller's hub wallet router contract
* **EVM spoke**: the spoke chain's asset manager contract
* **Stellar**: validated by the Stellar spoke service (no explicit spender needed)
* **All other chain types** (e.g. Solana, NEAR, Bitcoin): returns `true` — approvals are not applicable

**Parameters:**

* `_params`: `BridgeParams<S, Raw>` — bridge parameters including source chain key, token, amount, and sender address

**Returns:** `Promise<Result<boolean>>`

**Note**: For Stellar-based operations, the allowance system works differently:

* **Source chain (Stellar)**: this method checks and establishes trustlines automatically via the Stellar spoke service.
* **Destination chain (Stellar)**: clients must manually check trustlines using `StellarSpokeService.hasSufficientTrustline` before executing bridge operations.

**Example:**

```typescript
import { ChainKeys } from '@sodax/sdk';

const result = await sodax.bridge.isAllowanceValid({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0xYourAddress...',
    srcToken: '0x1234567890abcdef...',
    amount: 1000000000000000000n, // 1 token in base units
    dstChainKey: ChainKeys.POLYGON_MAINNET,
    dstToken: '0xabcdef1234567890...',
    recipient: '0x9876543210fedcba...',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok && result.value) {
  console.log('Allowance is sufficient');
} else {
  console.log('Need to approve tokens first');
}
```

### approve

Grants token spending approval required before executing a bridge.

Approval targets differ by chain:

* **Hub (Sonic)**: approves the caller's hub wallet router contract.
* **EVM spoke**: approves the spoke chain's asset manager contract.
* **Stellar**: delegates to the Stellar spoke service for trustline/allowance handling.
* **All other chain types**: returns an error — approvals are not supported.

When `raw` is `true`, the encoded transaction is returned without broadcasting. When `raw` is `false`, the transaction is signed and submitted via the provided wallet provider.

**Parameters:**

* `_params`: `BridgeParams<K, Raw>` — bridge parameters including source chain key, token, amount, wallet provider, and `raw` flag

**Returns:** `Promise<Result<TxReturnType<K, Raw>>>`

**Note**: For Stellar-based operations, the approval system works differently:

* **Source chain (Stellar)**: this method establishes trustlines automatically.
* **Destination chain (Stellar)**: clients must manually establish trustlines using `StellarSpokeService.requestTrustline` before executing bridge operations.

**Example (signed):**

```typescript
import { ChainKeys } from '@sodax/sdk';

const result = await sodax.bridge.approve({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0xYourAddress...',
    srcToken: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    dstChainKey: ChainKeys.POLYGON_MAINNET,
    dstToken: '0xabcdef1234567890...',
    recipient: '0x9876543210fedcba...',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok) {
  console.log('Approval transaction hash:', result.value);
} else {
  console.error('Approval failed:', result.error.message);
}
```

**Example (raw):**

```typescript
const result = await sodax.bridge.approve({
  params: { /* ... */ },
  raw: true,
  // walletProvider must NOT be passed when raw: true
});
```

### Stellar Trustline Requirements

For Stellar-based bridge operations, trustlines must be handled depending on whether Stellar is the source or destination chain. See the [Stellar Trustline Requirements](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STELLAR_TRUSTLINE.md#bridge) doc for detailed information and code examples.

### bridge

Executes a full end-to-end bridge transfer: spoke deposit → relay → hub settlement.

Internally calls `createBridgeIntent()` to submit the spoke-side deposit transaction, then waits for the cross-chain relay packet to be confirmed on the hub (Sonic). Use this method for the typical "fire and wait" bridge UX.

This method is signed-execution only (`raw: false`). For raw transaction building, use `createBridgeIntent()` directly.

**Parameters:**

* `_params`: `BridgeParams<K, false>` — bridge parameters including source/destination chain keys, token addresses, amount, recipient, wallet provider, and optional `timeout`

**Returns:** `Promise<Result<TxHashPair>>` — `{ srcChainTxHash, dstChainTxHash }` on success, where `srcChainTxHash` is the spoke deposit tx and `dstChainTxHash` is the hub settlement tx.

**Example:**

```typescript
import { ChainKeys } from '@sodax/sdk';

const result = await sodax.bridge.bridge({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0xYourAddress...',
    srcToken: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    dstChainKey: ChainKeys.POLYGON_MAINNET,
    dstToken: '0xabcdef1234567890...',
    recipient: '0x9876543210fedcba...',
  },
  walletProvider: evmWalletProvider,
  timeout: 30_000, // optional, defaults to 120 000 ms
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('Bridge successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Bridge failed:', result.error.message);
}
```

### createBridgeIntent

Submits the spoke-side deposit transaction that initiates a bridge transfer, without waiting for the cross-chain relay to complete.

This is the first step of a bridge operation. After this call succeeds you must relay the returned `relayData` to the hub (Sonic) via `relayTxAndWaitPacket` or the intent relay API to complete the transfer. The higher-level `bridge()` method does this automatically — use `createBridgeIntent()` only when you need manual relay control.

When `raw` is `true`, returns the encoded transaction without broadcasting (useful for simulation or batching). When `raw` is `false`, signs and submits the deposit transaction via the provided wallet provider.

**Bitcoin note:** Bitcoin is only supported with `raw: false` because it requires the Bound Exchange trading wallet derivation flow.

**Parameters:**

* `_params`: `BridgeParams<K, Raw>` — bridge parameters including source/destination chain keys, token addresses, amount, recipient, wallet provider, `raw` flag, and optional `skipSimulation`

**Returns:** `Promise<Result<IntentTxResult<K, Raw>>>` — on success, `{ tx, relayData }` where `tx` is the spoke deposit tx hash (or encoded call data when raw), and `relayData` contains the hub wallet address and encoded hub execution payload needed for relay.

**Example (signed):**

```typescript
import { ChainKeys } from '@sodax/sdk';

const result = await sodax.bridge.createBridgeIntent({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0xYourAddress...',
    srcToken: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    dstChainKey: ChainKeys.POLYGON_MAINNET,
    dstToken: '0xabcdef1234567890...',
    recipient: '0x9876543210fedcba...',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok) {
  console.log('Spoke deposit tx:', result.value.tx);
  console.log('Relay data:', result.value.relayData);
} else {
  console.error('Bridge intent creation failed:', result.error.message);
}
```

**Note:** This method only executes the transaction on the spoke chain and creates the bridge intent. To successfully bridge tokens you need to:

1. Check if the allowance is sufficient using `isAllowanceValid`
2. Approve the appropriate contract to spend the tokens using `approve`
3. Create the bridge intent using this method
4. Relay the transaction to the hub and await completion (or use the `bridge()` method which handles this automatically)

### getFee

Calculates the partner fee deducted from a given bridge input amount.

Returns `0n` when no partner fee is configured. The fee is denominated in the same units as `inputAmount` (vault token decimals, 18 dp).

**Parameters:**

* `inputAmount`: `bigint` — gross amount being bridged, in vault token base units

**Returns:** `bigint` — fee amount to be deducted, in the same units as `inputAmount`

**Example:**

```typescript
const feeAmount = sodax.bridge.getFee(1000000000000000000n);
console.log('Fee:', feeAmount.toString());
```

### getBridgeableAmount

Returns the maximum amount that can currently be bridged between two tokens, taking into account both deposit capacity on the source side and withdrawal liquidity on the destination side.

The limit type depends on the transfer direction:

* **Spoke → Hub**: constrained by the source vault's remaining deposit capacity (`DEPOSIT_LIMIT`).
* **Hub → Spoke**: constrained by the asset manager balance on the destination spoke (`WITHDRAWAL_LIMIT`).
* **Spoke → Spoke**: the minimum of the deposit capacity (source) and the asset manager balance (destination), normalised to a common unit. The returned `type` indicates which side is the binding constraint.

Returns `{ amount: 0n, type: 'DEPOSIT_LIMIT' }` when the source token is not yet supported by the vault.

**Parameters:**

* `from`: `XToken` — source token (chain key + address) to bridge from
* `to`: `XToken` — destination token (chain key + address) to bridge to

**Returns:** `Promise<Result<BridgeLimit>>` — `{ amount, decimals, type }` where `amount` is the maximum bridgeable quantity in the token's native base units and `decimals` is its decimal precision.

**Example:**

```typescript
import { ChainKeys } from '@sodax/sdk';

const result = await sodax.bridge.getBridgeableAmount(
  {
    address: '0x1234567890abcdef...',
    chainKey: ChainKeys.BASE_MAINNET,
    symbol: 'USDC',
    name: 'USD Coin',
    decimals: 6,
  },
  {
    address: '0xabcdef1234567890...',
    chainKey: ChainKeys.POLYGON_MAINNET,
    symbol: 'USDC',
    name: 'USD Coin',
    decimals: 6,
  },
);

if (result.ok) {
  console.log('Max bridgeable:', result.value.amount.toString());
  console.log('Decimals:', result.value.decimals);
  console.log('Limit type:', result.value.type); // 'DEPOSIT_LIMIT' | 'WITHDRAWAL_LIMIT'
} else {
  console.error('Error getting bridgeable amount:', result.error);
}
```

### isBridgeable

Determines whether two tokens (potentially on different chains) can be bridged to each other.

Two tokens are bridgeable if they resolve to the same vault address on the Sonic hub, meaning they represent the same underlying asset across chains (e.g. USDC on Base and USDC on Arbitrum both map to the same hub vault).

Returns `false` — rather than throwing — on any resolution or validation error.

**Parameters:**

* `from`: `XToken` — source token to bridge from
* `to`: `XToken` — destination token to bridge to
* `unchecked`: `boolean` (optional, default `false`) — when `true`, skips the `isValidSpokeChainKey` guard. Useful for checking theoretical bridgeability without requiring both chains to be in the active config.

**Returns:** `boolean` — `true` if the tokens share the same hub vault; `false` otherwise.

**Example:**

```typescript
import { ChainKeys } from '@sodax/sdk';

const isBridgeable = sodax.bridge.isBridgeable({
  from: {
    address: '0x1234567890abcdef...',
    chainKey: ChainKeys.BASE_MAINNET,
    symbol: 'USDC',
    name: 'USD Coin',
    decimals: 6,
  },
  to: {
    address: '0xabcdef1234567890...',
    chainKey: ChainKeys.POLYGON_MAINNET,
    symbol: 'USDC',
    name: 'USD Coin',
    decimals: 6,
  },
});

console.log('Assets are bridgeable:', isBridgeable);
```

### getBridgeableTokens

Returns all tokens on the destination chain that can receive a bridge from the given source token. Filters the destination chain's supported tokens to those that share the same hub vault as the source token.

**Parameters:**

* `from`: `SpokeChainKey` — source chain key
* `to`: `SpokeChainKey` — destination chain key whose supported tokens are searched
* `token`: `string` — source token address on `from`

**Returns:** `Result<XToken[]>` — array of destination-chain tokens bridgeable from the source token; error result if the source token is not found in config.

**Example:**

```typescript
import { ChainKeys } from '@sodax/sdk';

const result = sodax.bridge.getBridgeableTokens(
  ChainKeys.BASE_MAINNET,
  ChainKeys.POLYGON_MAINNET,
  '0x1234567890abcdef...',
);

if (result.ok) {
  console.log('Bridgeable tokens on Polygon:', result.value);
} else {
  console.error('Error getting bridgeable tokens:', result.error);
}
```

## Types

### CreateBridgeIntentParams

```typescript
export type CreateBridgeIntentParams<K extends SpokeChainKey = SpokeChainKey> = {
  srcAddress: string;
  srcChainKey: K;
  srcToken: string;
  amount: bigint;
  dstChainKey: SpokeChainKey;
  dstToken: string;
  recipient: string; // non-encoded recipient address
};
```

### BridgeParams

`BridgeParams` is an alias for `SpokeExecActionParams`, which is a discriminated union combining the intent params with the `WalletProviderSlot`:

```typescript
export type BridgeParams<ChainKey extends SpokeChainKey, Raw extends boolean> = SpokeExecActionParams<
  ChainKey,
  Raw,
  CreateBridgeIntentParams<ChainKey>
>;
```

The `WalletProviderSlot<K, Raw>` discriminant enforces at compile time:

* `{ raw: true }` — `walletProvider` is **forbidden**; returns raw tx payload
* `{ raw: false, walletProvider: GetWalletProviderType<K> }` — `walletProvider` is **required** and chain-narrowed; signs and broadcasts

### BridgeLimit

```typescript
type BridgeLimit = {
  amount: bigint;
  decimals: number;
  type: 'DEPOSIT_LIMIT' | 'WITHDRAWAL_LIMIT';
};
```

### TxHashPair

```typescript
type TxHashPair = {
  srcChainTxHash: string;
  dstChainTxHash: string;
};
```

### PartnerFee

```typescript
type PartnerFee = {
  address: string;
  percentage: number; // e.g. 0.1 for 10%
};
```

## Error Handling

The Bridge module's user-facing methods return `Promise<Result<T, SodaxError<NarrowCode>>>`. Discriminate on `result.error.code` (a string literal) — never on `result.error.message`. Same canonical shape used by swap and money market.

### The canonical error: `SodaxError<C>`

All bridge-module errors are instances of `SodaxError`, exported from `@sodax/sdk`:

```typescript
import { SodaxError, isSodaxError } from '@sodax/sdk';

class SodaxError<C extends string = string> extends Error {
  readonly code: C;                         // string-literal discriminator
  readonly cause?: unknown;                 // ES2022 cause chain
  readonly context?: Record<string, unknown>;
  toJSON(): { name, code, message, stack, context, cause };
}
```

**Rules:**

* Discriminate on `error.code` — never on `error.message` (which is human-readable, may change).
* `error.cause` walks the underlying error chain (loggers like Sentry/Pino/Datadog walk this automatically).
* `error.context` carries structured metadata: `srcChainKey`, `dstChainKey`, `phase`, plus per-code extras (`relayCode`, `field`).
* `error.toJSON()` is the canonical logger surface; `JSON.stringify(error)` invokes it automatically and produces a logger-safe payload (bigints in `context` are coerced to strings, cause walked depth-3, no circular hazards).
* Use `isBridgeError(e)` (broad) or one of the narrow guards `isBridgeOrchestrationError(e)` / `isBridgeCreateIntentError(e)` / `isBridgeApproveError(e)` / `isBridgeAllowanceCheckError(e)` / `isBridgeLookupError(e)` from `@sodax/sdk` instead of `instanceof SodaxError` in dapp/app code (bundle-safe).

### Per-method error code unions

| Method                | Codes                                                                                                                                                                        |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bridge`              | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN` |
| `createBridgeIntent`  | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                                    |
| `approve`             | `USER_REJECTED`, `VALIDATION_FAILED`, `APPROVE_FAILED`, `UNKNOWN`                                                                                                            |
| `isAllowanceValid`    | `VALIDATION_FAILED`, `ALLOWANCE_CHECK_FAILED`, `UNKNOWN`                                                                                                                     |
| `getBridgeableAmount` | `VALIDATION_FAILED`, `LOOKUP_FAILED`, `UNKNOWN`                                                                                                                              |
| `getBridgeableTokens` | `VALIDATION_FAILED`, `LOOKUP_FAILED`, `UNKNOWN`                                                                                                                              |

The exported narrow types are `BridgeOrchestrationError` (for `bridge`), `BridgeCreateIntentError` (for `createBridgeIntent`), `BridgeApproveError`, `BridgeAllowanceCheckError`, and a single `BridgeLookupError` shared by `getBridgeableAmount` and `getBridgeableTokens` (discriminate them at runtime via `error.context.method`). Each has a matching narrow guard listed above.

### Standard `context` fields

```typescript
{
  srcChainKey?: SpokeChainKey;
  dstChainKey?: SpokeChainKey;
  phase?: 'validate' | 'intentCreation' | 'verify' | 'submit' | 'relay'
        | 'approve' | 'allowanceCheck' | 'lookup';
  relayCode?: 'SUBMIT_TX_FAILED' | 'RELAY_TIMEOUT' | 'RELAY_POLLING_FAILED' | 'UNKNOWN';
  field?: string;     // on VALIDATION_FAILED
  reason?: string;
}
```

### Discrimination example

```typescript
import { isBridgeOrchestrationError } from '@sodax/sdk';

const result = await sodax.bridge.bridge({
  params: { /* ... */ },
  raw: false,
  walletProvider: evmWalletProvider,
});

if (!result.ok) {
  // result.error is BridgeOrchestrationError = SodaxError<BridgeOrchestrationErrorCode>
  switch (result.error.code) {
    case 'VALIDATION_FAILED':
      // Bad input — error.message is human-readable; error.context.field tells you which.
      console.error('Bad input:', result.error.message);
      break;

    case 'INTENT_CREATION_FAILED':
      // Spoke deposit failed.
      console.error('Intent creation failed:', result.error.cause);
      break;

    case 'TX_VERIFICATION_FAILED':
      // Spoke tx couldn't be verified on-chain.
      break;

    case 'TX_SUBMIT_FAILED':
      // CRITICAL: spoke tx landed but the relay submission failed. Funds may be in flight.
      // Persist the input params and retry submission.
      break;

    case 'RELAY_TIMEOUT':
      // Relay packet didn't confirm in time. Check intent status and retry with longer timeout.
      break;

    case 'RELAY_FAILED':
      // Other relay failure. error.context.relayCode disambiguates:
      //   'RELAY_POLLING_FAILED' — polling endpoint outage; query hub directly to confirm packet status.
      //   'UNKNOWN' — forward-compat fallback for new relay error codes.
      break;

    case 'EXECUTION_FAILED':
      // Catch-all for the bridge orchestration; cause has the original.
      console.error('Bridge failed:', result.error.cause);
      break;

    case 'UNKNOWN':
      console.error('Unexpected:', result.error.cause);
      break;
  }
}
```

### Migration from the legacy pattern

If you were on the previous CODE-on-`error.message` pattern (or the older `BridgeError<Code>` typed shape that the published docs at <https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/bridge#error-handling> document), here are the mappings:

| Before                                            | After                                                                    |
| ------------------------------------------------- | ------------------------------------------------------------------------ |
| `error.message === 'RELAY_TIMEOUT'`               | `error.code === 'RELAY_TIMEOUT'`                                         |
| `error.message === 'SUBMIT_TX_FAILED'`            | `error.code === 'TX_SUBMIT_FAILED'`                                      |
| `error.message === 'CREATE_BRIDGE_INTENT_FAILED'` | `error.code === 'INTENT_CREATION_FAILED'`                                |
| `error.message === 'EXECUTION_FAILED'`            | `error.code === 'EXECUTION_FAILED'` (now narrow-typed)                   |
| `error.message === 'ALLOWANCE_CHECK_FAILED'`      | `error.code === 'ALLOWANCE_CHECK_FAILED'`                                |
| `error.message === 'APPROVAL_FAILED'`             | `error.code === 'APPROVE_FAILED'`                                        |
| Prose `error.message` for invariants              | `error.code === 'VALIDATION_FAILED'`; the prose stays on `error.message` |

### Best practices

1. **Always handle `TX_SUBMIT_FAILED`**. Critical — the spoke tx landed but the relay submission failed. Funds may be in flight; persist the user's input and retry.
2. **Handle `RELAY_TIMEOUT` gracefully**. The spoke tx succeeded; the relay just didn't deliver in time. Check on-chain status before retrying.
3. **Discriminate `RELAY_FAILED` via `context.relayCode`**. `'RELAY_POLLING_FAILED'` (polling outage — packet status unknown) needs different UX from generic `'UNKNOWN'`.
4. **Use `error.cause` for forensics**. Every wrapped error preserves the original on `cause`. Loggers walk it automatically.
5. **Use `JSON.stringify(error)` for logging**. The `toJSON()` method handles bigint coercion + cause-chain truncation safely.
6. **Type-guard, don't `as`-cast**. Use `is<Op>Error(error)` to narrow; an `as <Op>Error` cast after a generic `isSodaxError` check would silently widen the contract.

## Usage Flow

The typical bridge operation follows this sequence:

1. **Check allowance** using `isAllowanceValid()`
2. **Approve tokens** using `approve()` if needed
3. **For Stellar destination chains**: check and establish trustlines (see [Stellar Trustline Requirements](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STELLAR_TRUSTLINE.md#bridge))
4. **Execute bridge** using `bridge()` for the full lifecycle, or `createBridgeIntent()` for manual relay control
5. **Monitor progress** using the returned transaction hashes

## Chain Keys

Use `ChainKeys.*` constants from `@sodax/sdk` instead of raw string chain IDs:

```typescript
import { ChainKeys } from '@sodax/sdk';

// Examples
ChainKeys.BASE_MAINNET      // '0x2105.base'
ChainKeys.POLYGON_MAINNET   // '0x89.polygon'
ChainKeys.SONIC_MAINNET     // hub chain
ChainKeys.ETHEREUM_MAINNET
ChainKeys.ARBITRUM_MAINNET
// ... all 20 supported chains
```

The chain key in the request payload (e.g. `srcChainKey`) drives both TypeScript narrowing — so `walletProvider` is automatically typed to the correct interface — and runtime routing inside the SDK.

## Supported Chains

The service supports all 21 chains in the SODAX network:

* **EVM (13):** Sonic (hub), Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia, Hedera
* **Non-EVM (8):** Solana, Sui, Stellar, ICON, Injective, NEAR, Stacks, Bitcoin

## Partner Fees

Partner fees are configured at `Sodax` construction time via `config.bridge.partnerFee`. They are automatically applied inside `bridge()` and `createBridgeIntent()`. Use `getFee()` to preview the fee amount for a given input:

```typescript
const feeAmount = sodax.bridge.getFee(inputAmount);
const netAmount = inputAmount - feeAmount;
```

Fees are denominated in vault token decimals (18 dp).


# Staking (SODA asset)

> **Error handling conventions:** This module returns `Result<T, SodaxError<NarrowCode>>` from every async public method. Discriminate on `error.code` (a closed reason-only union) and `error.feature === 'staking'`. See [Error Handling](#error-handling) below.

The `StakingService` class, reachable through `sodax.staking`, provides functionality for staking SODA tokens, unstaking, claiming rewards, and retrieving staking information. It supports operations across all spoke chains with automatic hub chain integration.

## Setup

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax();
const initResult = await sodax.config.initialize();
if (!initResult.ok) {
  console.error('SDK initialization failed:', initResult.error.message);
}
```

## Calling Convention

All mutating methods accept a single `SpokeExecActionParams` object with the following shape:

```typescript
{
  params: <StakingParams>,   // action-specific params (includes srcChainKey)
  walletProvider: ...,       // required when raw: false; chain-narrowed by srcChainKey
  raw?: boolean,             // false (default) → sign & broadcast; true → return raw tx payload
  skipSimulation?: boolean,  // optional: skip preflight simulation
  timeout?: number,          // optional: relay timeout in milliseconds (default: 120 000)
}
```

**`raw: true` / `raw: false` rules (enforced at compile time):**

* `{ raw: true }` — `walletProvider` is **forbidden**; returns an unsigned transaction payload.
* `{ raw: false, walletProvider }` — `walletProvider` is **required** and chain-narrowed from `srcChainKey`.

## Methods

### isAllowanceValid

Checks whether the current token allowance is sufficient for a `stake`, `unstake`, or `instantUnstake` action.

* **EVM spoke chains**: checks the asset-manager allowance.
* **Hub chain (Sonic)**: checks the user's hub wallet allowance.
* **Stellar**: delegates to the Stellar spoke allowance check (trustlines).
* **Other non-EVM chains**: no on-chain allowance required; always resolves `true`.

**Signature:**

```typescript
async isAllowanceValid<K extends SpokeChainKey, Raw extends boolean>(
  _params: StakingParamsUnion<K, Raw>,
): Promise<Result<boolean>>
```

**Example:**

```typescript
const result = await sodax.staking.isAllowanceValid({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n, // 1 SODA
    minReceive: 950000000000000000n,
    action: 'stake',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok && result.value) {
  console.log('Allowance is sufficient');
} else if (result.ok) {
  console.log('Need to approve tokens first');
} else {
  console.error('Allowance check failed:', result.error.message);
}
```

### approve

Submits a token-spending approval on the source chain for a `stake`, `unstake`, or `instantUnstake` action.

Supported chains: EVM spoke chains, hub chain (Sonic), and Stellar. All other chains return an error.

The spender address is resolved automatically:

* Hub chain: the user's hub wallet (derived from spoke address).
* EVM spoke chain: the chain's asset-manager contract.

Must be called before executing the corresponding action whenever `isAllowanceValid` returns `false`.

**Signature:**

```typescript
async approve<K extends SpokeChainKey, Raw extends boolean>(
  _params: StakingParamsUnion<K, Raw>,
): Promise<Result<TxReturnType<K, Raw>>>
```

**Example (signed):**

```typescript
const result = await sodax.staking.approve({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    minReceive: 950000000000000000n,
    action: 'stake',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok) {
  console.log('Approval tx hash:', result.value);
} else {
  console.error('Approval failed:', result.error.message);
}
```

**Example (raw tx):**

```typescript
const result = await sodax.staking.approve({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    minReceive: 950000000000000000n,
    action: 'stake',
  },
  raw: true,
  // walletProvider must not be passed when raw: true
});
```

### Stellar Trustline Requirements

For Stellar-based staking operations, `isAllowanceValid` and `approve` handle trustlines automatically when Stellar is the source chain. See [Stellar Trustline Requirements](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STELLAR_TRUSTLINE.md#staking) for details.

Staking operations always flow from spoke chains (including Stellar) to the hub chain (Sonic), so Stellar is only used as a source chain.

### stake

Stakes SODA tokens from a spoke chain, relays the intent to the hub, and waits for hub confirmation.

Internally calls `createStakeIntent` to submit on the spoke, then relays the cross-chain packet and waits for the hub transaction to land. For hub-chain callers (`srcChainKey: ChainKeys.SONIC_MAINNET`) the spoke and hub hashes are identical.

**Prerequisite:** call `isAllowanceValid` + `approve` before staking on EVM chains.

**Signature:**

```typescript
async stake<K extends SpokeChainKey>(
  _params: StakeAction<K, false>,
): Promise<Result<TxHashPair>>
```

**Example:**

```typescript
const result = await sodax.staking.stake({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n, // 1 SODA
    minReceive: 950000000000000000n, // 0.95 xSODA minimum
    action: 'stake',
  },
  walletProvider: evmWalletProvider,
  timeout: 30000,
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('Stake successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Stake failed:', result.error.message);
}
```

### createStakeIntent

Submits the stake transaction on the spoke chain **without** relaying to the hub.

Returns `IntentTxResult` containing the spoke tx result and the `relayData` (hub wallet address + encoded payload) needed for a subsequent manual relay step. Use `stake` for the full end-to-end flow.

**Signature:**

```typescript
async createStakeIntent<K extends SpokeChainKey, Raw extends boolean>(
  _params: StakeAction<K, Raw>,
): Promise<Result<IntentTxResult<K, Raw>>>
```

**Example:**

```typescript
const result = await sodax.staking.createStakeIntent({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    minReceive: 950000000000000000n,
    action: 'stake',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok) {
  console.log('Spoke tx:', result.value.tx);
  console.log('Relay data:', result.value.relayData);
}
```

**Full manual flow:**

1. `isAllowanceValid` → check allowance
2. `approve` → approve if needed
3. `createStakeIntent` → spoke tx only
4. `stake` → full relay + hub confirmation (or relay manually using `relayData`)

### unstake

Initiates an unstake request for xSoda shares, relays the intent to the hub, and waits for confirmation.

Unstaking begins a waiting period. The user receives SODA only after calling `claim` once the period elapses. Early claims incur a linear penalty (see `getStakingConfig`). For immediate redemption without a waiting period, use `instantUnstake` instead.

**Prerequisite:** call `isAllowanceValid` + `approve` before unstaking on EVM chains.

**Signature:**

```typescript
async unstake<K extends SpokeChainKey>(
  _params: UnstakeAction<K, false>,
): Promise<Result<TxHashPair>>
```

**Example:**

```typescript
const result = await sodax.staking.unstake({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n, // 1 xSODA
    action: 'unstake',
  },
  walletProvider: evmWalletProvider,
  timeout: 30000,
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('Unstake successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Unstake failed:', result.error.message);
}
```

### createUnstakeIntent

Submits the unstake transaction on the spoke chain **without** relaying to the hub.

**Signature:**

```typescript
async createUnstakeIntent<K extends SpokeChainKey, Raw extends boolean>(
  _params: UnstakeAction<K, Raw>,
): Promise<Result<IntentTxResult<K, Raw>>>
```

**Example:**

```typescript
const result = await sodax.staking.createUnstakeIntent({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    action: 'unstake',
  },
  walletProvider: evmWalletProvider,
});

if (result.ok) {
  console.log('Spoke tx:', result.value.tx);
  console.log('Relay data:', result.value.relayData);
}
```

### instantUnstake

Instantly redeems xSoda shares for SODA without a waiting period, relays the intent to the hub, and waits for confirmation. Routes through the StakingRouter, which provides immediate liquidity at the cost of slippage. Use `getInstantUnstakeRatio` to preview the SODA output before calling this method.

**Prerequisite:** call `isAllowanceValid` + `approve` before instant unstaking on EVM chains.

**Signature:**

```typescript
async instantUnstake<K extends SpokeChainKey>(
  _params: InstantUnstakeAction<K, false>,
): Promise<Result<TxHashPair>>
```

**Example:**

```typescript
const result = await sodax.staking.instantUnstake({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n, // 1 xSODA
    minAmount: 950000000000000000n, // 0.95 SODA minimum
    action: 'instantUnstake',
  },
  walletProvider: evmWalletProvider,
  timeout: 30000,
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('Instant unstake successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Instant unstake failed:', result.error.message);
}
```

### createInstantUnstakeIntent

Submits the instant-unstake transaction on the spoke chain **without** relaying to the hub.

**Signature:**

```typescript
async createInstantUnstakeIntent<K extends SpokeChainKey, Raw extends boolean>(
  _params: InstantUnstakeAction<K, Raw>,
): Promise<Result<IntentTxResult<K, Raw>>>
```

**Example:**

```typescript
const result = await sodax.staking.createInstantUnstakeIntent({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    amount: 1000000000000000000n,
    minAmount: 950000000000000000n,
    action: 'instantUnstake',
  },
  walletProvider: evmWalletProvider,
});
```

### claim

Claims SODA from an unstake request, relays the intent to the hub, and waits for confirmation.

Can be invoked before the unstaking period has fully elapsed, in which case the claim incurs a linear penalty against the claimable amount. Use `getUnstakingInfoWithPenalty` first to preview the exact claimable amount given the current penalty.

**Signature:**

```typescript
async claim<K extends SpokeChainKey>(
  _params: ClaimAction<K, false>,
): Promise<Result<TxHashPair>>
```

**Example:**

```typescript
const result = await sodax.staking.claim({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    requestId: 1n,
    amount: 950000000000000000n, // claimable amount after penalty
    action: 'claim',
  },
  walletProvider: evmWalletProvider,
  timeout: 30000,
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('Claim successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Claim failed:', result.error.message);
}
```

### createClaimIntent

Submits the claim transaction on the spoke chain **without** relaying to the hub.

**Signature:**

```typescript
async createClaimIntent<K extends SpokeChainKey, Raw extends boolean>(
  _params: ClaimAction<K, Raw>,
): Promise<Result<IntentTxResult<K, Raw>>>
```

**Example:**

```typescript
const result = await sodax.staking.createClaimIntent({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    requestId: 1n,
    amount: 950000000000000000n,
    action: 'claim',
  },
  walletProvider: evmWalletProvider,
});
```

### cancelUnstake

Cancels a pending unstake request and re-stakes the underlying SODA as xSoda shares, relays to the hub, and waits for confirmation. Aborts the waiting period and redeposits SODA back into the xSoda vault so the user continues earning staking rewards.

**Signature:**

```typescript
async cancelUnstake<K extends SpokeChainKey>(
  _params: CancelUnstakeAction<K, false>,
): Promise<Result<TxHashPair>>
```

**Example:**

```typescript
const result = await sodax.staking.cancelUnstake({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    requestId: 1n,
    action: 'cancelUnstake',
  },
  walletProvider: evmWalletProvider,
  timeout: 30000,
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('Cancel unstake successful:', { srcChainTxHash, dstChainTxHash });
} else {
  console.error('Cancel unstake failed:', result.error.message);
}
```

### createCancelUnstakeIntent

Submits the cancel-unstake transaction on the spoke chain **without** relaying to the hub.

**Signature:**

```typescript
async createCancelUnstakeIntent<K extends SpokeChainKey, Raw extends boolean>(
  _params: CancelUnstakeAction<K, Raw>,
): Promise<Result<IntentTxResult<K, Raw>>>
```

**Example:**

```typescript
const result = await sodax.staking.createCancelUnstakeIntent({
  params: {
    srcChainKey: ChainKeys.BASE_MAINNET,
    srcAddress: '0x1234567890abcdef...',
    requestId: 1n,
    action: 'cancelUnstake',
  },
  walletProvider: evmWalletProvider,
});
```

### getStakingInfoFromSpoke

Fetches comprehensive staking information for a user identified by their spoke-chain address and chain key.

Resolves the hub wallet address from the spoke address, then delegates to `getStakingInfo`.

**Signature:**

```typescript
async getStakingInfoFromSpoke<K extends SpokeChainKey>(
  srcAddress: Address,
  srcChainKey: K,
): Promise<Result<StakingInfo>>
```

**Example:**

```typescript
const result = await sodax.staking.getStakingInfoFromSpoke(
  '0x1234567890abcdef...',
  ChainKeys.BASE_MAINNET,
);

if (result.ok) {
  const { totalStaked, userXSodaBalance, userXSodaValue } = result.value;
  console.log('Total staked:', totalStaked.toString());
  console.log('User xSODA balance:', userXSodaBalance.toString());
  console.log('User xSODA value in SODA:', userXSodaValue.toString());
} else {
  console.error('Failed to get staking info:', result.error.message);
}
```

### getStakingInfo

Fetches comprehensive staking information for a hub wallet address directly.

**Signature:**

```typescript
async getStakingInfo(userAddress: Address): Promise<Result<StakingInfo>>
```

**Example:**

```typescript
const result = await sodax.staking.getStakingInfo('0xHubWalletAddress...');

if (result.ok) {
  const { totalStaked, userXSodaBalance, userXSodaValue } = result.value;
  console.log('Total staked:', totalStaked.toString());
  console.log('User xSODA balance:', userXSodaBalance.toString());
  console.log('User xSODA value in SODA:', userXSodaValue.toString());
} else {
  console.error('Failed to get staking info:', result.error.message);
}
```

### getUnstakingInfo

Fetches all pending unstake requests and the total SODA amount currently unstaking for a user.

**Signature:**

```typescript
async getUnstakingInfo<K extends SpokeChainKey>(
  srcAddress: Address,
  srcChainKey: K,
): Promise<Result<UnstakingInfo>>
```

**Example:**

```typescript
const result = await sodax.staking.getUnstakingInfo(
  '0x1234567890abcdef...',
  ChainKeys.BASE_MAINNET,
);

if (result.ok) {
  const { totalUnstaking, userUnstakeSodaRequests } = result.value;
  console.log('Total unstaking:', totalUnstaking.toString());
  console.log('Number of requests:', userUnstakeSodaRequests.length);
} else {
  console.error('Failed to get unstaking info:', result.error.message);
}
```

### getUnstakingInfoWithPenalty

Fetches all pending unstake requests enriched with current penalty calculations.

Applies the linear penalty model to each request based on elapsed time:

* Before `minUnstakingPeriod`: `maxPenalty` applies in full.
* Between `minUnstakingPeriod` and `unstakingPeriod`: penalty decreases linearly to zero.
* After `unstakingPeriod`: no penalty.

**Signature:**

```typescript
async getUnstakingInfoWithPenalty<K extends SpokeChainKey>(
  srcAddress: Address,
  srcChainKey: K,
): Promise<Result<UnstakingInfo & { requestsWithPenalty: UnstakeRequestWithPenalty[] }>>
```

**Example:**

```typescript
const result = await sodax.staking.getUnstakingInfoWithPenalty(
  '0x1234567890abcdef...',
  ChainKeys.BASE_MAINNET,
);

if (result.ok) {
  const { totalUnstaking, requestsWithPenalty } = result.value;
  console.log('Total unstaking:', totalUnstaking.toString());

  for (const req of requestsWithPenalty) {
    console.log('Amount:', req.request.amount.toString());
    console.log('Penalty:', req.penalty.toString());
    console.log('Penalty %:', req.penaltyPercentage);
    console.log('Claimable:', req.claimableAmount.toString());
  }
} else {
  console.error('Failed to get unstaking info with penalty:', result.error.message);
}
```

### getStakingConfig

Reads the current staking configuration from the StakedSoda contract.

**Signature:**

```typescript
async getStakingConfig(): Promise<Result<StakingConfig>>
```

**Example:**

```typescript
const result = await sodax.staking.getStakingConfig();

if (result.ok) {
  const { unstakingPeriod, minUnstakingPeriod, maxPenalty } = result.value;
  console.log('Unstaking period (s):', unstakingPeriod.toString());
  console.log('Min unstaking period (s):', minUnstakingPeriod.toString());
  console.log('Max penalty (%):', maxPenalty.toString());
} else {
  console.error('Failed to get staking config:', result.error.message);
}
```

### getInstantUnstakeRatio

Estimates the SODA amount receivable from instantly unstaking a given quantity of xSoda shares.

Calls `StakingRouter.estimateInstantUnstake` on-chain. Use this before calling `instantUnstake` to set an appropriate `minAmount` slippage guard.

**Signature:**

```typescript
async getInstantUnstakeRatio(amount: bigint): Promise<Result<bigint>>
```

**Example:**

```typescript
const result = await sodax.staking.getInstantUnstakeRatio(1000000000000000000n);

if (result.ok) {
  console.log('Estimated SODA output:', result.value.toString());
} else {
  console.error('Failed to get instant unstake ratio:', result.error.message);
}
```

### getConvertedAssets

Converts a quantity of xSoda shares to its current underlying SODA value using the vault's `convertToAssets` view function. The result increases over time as staking rewards accrue.

**Signature:**

```typescript
async getConvertedAssets(amount: bigint): Promise<Result<bigint>>
```

**Example:**

```typescript
const result = await sodax.staking.getConvertedAssets(1000000000000000000n);

if (result.ok) {
  console.log('SODA equivalent:', result.value.toString());
} else {
  console.error('Failed to convert assets:', result.error.message);
}
```

### getStakeRatio

Estimates the xSoda shares and preview-deposit amount for a given SODA input. Calls `StakingRouter.estimateXSodaAmount` on-chain. Use this to display expected output before a stake transaction.

**Signature:**

```typescript
async getStakeRatio(amount: bigint): Promise<Result<[bigint, bigint]>>
```

**Example:**

```typescript
const result = await sodax.staking.getStakeRatio(1000000000000000000n);

if (result.ok) {
  const [xSodaAmount, previewDepositAmount] = result.value;
  console.log('xSODA you will receive:', xSodaAmount.toString());
  console.log('Preview deposit amount:', previewDepositAmount.toString());
} else {
  console.error('Failed to get stake ratio:', result.error.message);
}
```

## Types

### StakeParams

```typescript
export type StakeParams<K extends SpokeChainKey> = {
  srcChainKey: K;      // chain key of the spoke chain to stake from
  srcAddress: Address; // account to stake from
  amount: bigint;      // SODA amount to stake
  minReceive: bigint;  // minimum xSODA shares to receive (slippage guard)
  action: 'stake';
};
```

### UnstakeParams

```typescript
export type UnstakeParams<K extends SpokeChainKey> = {
  srcChainKey: K;      // chain key of the spoke chain to unstake from
  srcAddress: Address; // account to unstake from
  amount: bigint;      // xSoda share amount to unstake
  action: 'unstake';
};
```

### InstantUnstakeParams

```typescript
export type InstantUnstakeParams<K extends SpokeChainKey> = {
  srcChainKey: K;      // chain key of the spoke chain to instant unstake from
  srcAddress: Address;
  amount: bigint;      // xSoda share amount to redeem
  minAmount: bigint;   // minimum SODA to receive (slippage guard)
  action: 'instantUnstake';
};
```

### ClaimParams

```typescript
export type ClaimParams<K extends SpokeChainKey> = {
  srcChainKey: K;      // chain key of the spoke chain to claim from
  srcAddress: Address;
  requestId: bigint;
  amount: bigint;      // claimable SODA amount after penalty calculation
  action: 'claim';
};
```

### CancelUnstakeParams

```typescript
export type CancelUnstakeParams<K extends SpokeChainKey> = {
  srcChainKey: K;      // chain key of the spoke chain to cancel unstake from
  srcAddress: Address;
  requestId: bigint;
  action: 'cancelUnstake';
};
```

### StakingInfo

```typescript
export type StakingInfo = {
  totalStaked: bigint;      // Total SODA staked (totalAssets from xSODA vault)
  totalUnderlying: bigint;  // Total underlying SODA assets in the vault
  userXSodaBalance: bigint; // User's xSODA shares (raw balance)
  userXSodaValue: bigint;   // User's xSODA value in SODA (converted)
  userUnderlying: bigint;   // User's underlying SODA amount
};
```

### UnstakingInfo

```typescript
export type UnstakingInfo = {
  userUnstakeSodaRequests: readonly UserUnstakeInfo[];
  totalUnstaking: bigint;
};
```

### UnstakeRequestWithPenalty

```typescript
export type UnstakeRequestWithPenalty = UserUnstakeInfo & {
  penalty: bigint;           // SODA withheld as penalty
  penaltyPercentage: number; // 0–100
  claimableAmount: bigint;   // net SODA receivable after penalty
};
```

### StakingConfig

```typescript
export type StakingConfig = {
  unstakingPeriod: bigint;    // full wait duration in seconds; no penalty after this
  minUnstakingPeriod: bigint; // minimum wait in seconds; max penalty before this
  maxPenalty: bigint;         // maximum penalty percentage (1–100)
};
```

### StakingActionType

```typescript
export type StakingActionType = 'stake' | 'unstake' | 'claim' | 'cancelUnstake' | 'instantUnstake';
```

### TxHashPair

Returned by the full relay methods (`stake`, `unstake`, `instantUnstake`, `claim`, `cancelUnstake`):

```typescript
type TxHashPair = {
  srcChainTxHash: string; // transaction hash on the source (spoke) chain
  dstChainTxHash: string; // transaction hash on the destination (hub) chain
};
```

## Error Handling

All async public methods on `StakingService` return `Promise<Result<T, SodaxError<NarrowCode>>>` where `NarrowCode` is a narrow per-method union of `StakingErrorCode`. Discriminate on `error.code`, never on `error.message`. The original lower-level failure (a viem revert, a fetch error, a relay timeout) is preserved on `error.cause`; structured metadata (chain, action, phase, relayCode) is on `error.context`.

```typescript
import { isStakeOrchestrationError, type StakeOrchestrationError } from '@sodax/sdk';

const result = await sodax.staking.stake({ /* params */ });
if (!result.ok) {
  // result.error is typed as `StakeOrchestrationError = SodaxError<StakeOrchestrationErrorCode>`
  switch (result.error.code) {
    case 'VALIDATION_FAILED':       // precondition tripped (see context.field)
    case 'INTENT_CREATION_FAILED':
    case 'TX_VERIFICATION_FAILED':           // spoke tx not verifiable on-chain
    case 'TX_SUBMIT_FAILED':        // relay submit failed
    case 'RELAY_TIMEOUT':           // relay packet did not arrive within timeout
    case 'RELAY_FAILED':            // relay polling failure / unknown relay error
    case 'EXECUTION_FAILED':            // generic catch-all (see error.cause)
    case 'UNKNOWN':
      handleStakeError(result.error);
      break;
  }
}
```

### Per-method error code unions

| Method                                                                                                                           | Codes                                                                                                                                                                        |
| -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stake`                                                                                                                          | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN` |
| `unstake`                                                                                                                        | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN`                           |
| `instantUnstake`                                                                                                                 | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN`                           |
| `claim`                                                                                                                          | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN`                           |
| `cancelUnstake`                                                                                                                  | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN`                           |
| `create<Op>Intent`                                                                                                               | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                                    |
| `approve`                                                                                                                        | `USER_REJECTED`, `VALIDATION_FAILED`, `APPROVE_FAILED`, `UNKNOWN`                                                                                                            |
| `isAllowanceValid`                                                                                                               | `VALIDATION_FAILED`, `ALLOWANCE_CHECK_FAILED`, `UNKNOWN`                                                                                                                     |
| `getStakingInfo*` / `getUnstakingInfo*` / `getStakingConfig` / `getInstantUnstakeRatio` / `getConvertedAssets` / `getStakeRatio` | `VALIDATION_FAILED`, `LOOKUP_FAILED`, `UNKNOWN`                                                                                                                              |

Note: `TX_VERIFICATION_FAILED` only appears in `StakeOrchestrationErrorCode` because `stake` is the only orchestrator that calls `spoke.verifyTxHash`. The 4 non-stake orchestrators (`unstake`/`instantUnstake`/`claim`/`cancelUnstake`) share `StakingOrchestrationErrorCode`.

### Structured `context`

Every staking error carries an `error.context` payload. Fields vary by code:

| Field              | Set on                                                | Notes                                                                                                                                             |
| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `srcChainKey`      | all orchestrator + intent codes                       | low-cardinality — suitable as a logger / Sentry tag                                                                                               |
| `action`           | all orchestrator + intent codes                       | one of `'stake' \| 'unstake' \| 'instantUnstake' \| 'claim' \| 'cancelUnstake'`                                                                   |
| `phase`            | most codes                                            | `'validate' \| 'intentCreation' \| 'verify' \| 'submit' \| 'relay' \| 'approve' \| 'allowanceCheck' \| 'infoFetch'`                               |
| `relayCode`        | `RELAY_TIMEOUT` / `TX_SUBMIT_FAILED` / `RELAY_FAILED` | mirrors the relay-layer `RELAY_ERROR_CODES` contract; carries `'RELAY_POLLING_FAILED'` so polling outage is distinguishable from generic failure  |
| `field` / `reason` | `VALIDATION_FAILED`                                   | which precondition tripped                                                                                                                        |
| `method`           | `LOOKUP_FAILED`                                       | the read-only method name (`'getStakingInfo'`, `'getUnstakingInfo'`, `'getStakingConfig'`, …) — partitions the 8 readers without per-method codes |

### Type guards

Per-method type guards are runtime-checked and compile-checked in lockstep with the union types. Use them in `catch` blocks to short-circuit when a foreign code escapes:

```typescript
import { isStakeOrchestrationError, isStakingError } from '@sodax/sdk';

try {
  // ... call sodax.staking.stake ...
} catch (e) {
  if (isStakeOrchestrationError(e)) console.error('typed stake error:', e.code, e.context);
  else if (isStakingError(e)) console.error('staking error from another method:', e.code);
  else throw e; // not a staking error — bubble up
}
```

Available guards: `isStakingError` (broad — any staking error), `isStakeOrchestrationError` (the `stake` orchestrator only, the one path with `TX_VERIFICATION_FAILED`), `isStakingOrchestrationError` (the 4 non-stake orchestrators), `isStakingCreateIntentError` (shared by all 5 `create*Intent` methods), `isStakingApproveError`, `isStakingAllowanceCheckError`, `isStakingInfoFetchError`. Per-operation discrimination across the orchestrators is via `error.context.action` (one of `'stake' | 'unstake' | 'instantUnstake' | 'claim' | 'cancelUnstake'`).

### Validation invariant

Precondition failures throw a typed `VALIDATION_FAILED` from inside the public method's `try/catch`, surfacing as a typed `Result.error` rather than a generic prose `Error`. This means consumers can discriminate validation failures the same way as any other code.

```typescript
import { stakingInvariant } from '@sodax/sdk';

stakingInvariant(amount > 0n, 'Amount must be greater than 0', { field: 'amount' });
```

### Migration from the pre-v2 taxonomy

The published v1 `StakingError<Code>` shape (8 codes: `STAKE_FAILED`, `UNSTAKE_FAILED`, `INSTANT_UNSTAKE_FAILED`, `CLAIM_FAILED`, `CANCEL_UNSTAKE_FAILED`, `INFO_FETCH_FAILED`, `ALLOWANCE_CHECK_FAILED`, `APPROVAL_FAILED`) is restored here with module-prefixed names and cause-preservation:

| v1 code                  | v2 code                                               | Notes                                                                              |
| ------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `STAKE_FAILED`           | `EXECUTION_FAILED`                                    | Generic stake catch-all. Underlying cause on `error.cause`.                        |
| `UNSTAKE_FAILED`         | `EXECUTION_FAILED`                                    | Generic unstake catch-all.                                                         |
| `INSTANT_UNSTAKE_FAILED` | `EXECUTION_FAILED`                                    | Generic instant-unstake catch-all.                                                 |
| `CLAIM_FAILED`           | `EXECUTION_FAILED`                                    | Generic claim catch-all.                                                           |
| `CANCEL_UNSTAKE_FAILED`  | `EXECUTION_FAILED`                                    | Generic cancel-unstake catch-all.                                                  |
| `INFO_FETCH_FAILED`      | `LOOKUP_FAILED`                                       | Shared by all 8 read-only methods; partition via `context.method`.                 |
| `ALLOWANCE_CHECK_FAILED` | `ALLOWANCE_CHECK_FAILED`                              | Allowance check failed at the spoke layer.                                         |
| `APPROVAL_FAILED`        | `APPROVE_FAILED`                                      | Approve operation failed.                                                          |
| (none)                   | `VALIDATION_FAILED`                                   | New: typed precondition failures (replaces prose `Error` throws from `invariant`). |
| (none)                   | `INTENT_CREATION_FAILED`                              | New: per-op intent-creation phase tag (e.g. spoke deposit revert).                 |
| (none)                   | `TX_VERIFICATION_FAILED`                              | New: spoke tx verification phase tag (only set by `stake`).                        |
| (none)                   | `TX_SUBMIT_FAILED` / `RELAY_TIMEOUT` / `RELAY_FAILED` | New: typed relay-phase codes mapped from the shared `RELAY_ERROR_CODES` contract.  |
| (none)                   | `UNKNOWN`                                             | Reserved fallback for never-classified errors.                                     |

## Usage Flow

### Full stake flow (EVM spoke chain)

```typescript
// 1. Check allowance
const allowanceResult = await sodax.staking.isAllowanceValid({
  params: { srcChainKey: ChainKeys.BASE_MAINNET, srcAddress, amount, minReceive, action: 'stake' },
  walletProvider: evmWalletProvider,
});
if (!allowanceResult.ok) throw allowanceResult.error;

// 2. Approve if needed
if (!allowanceResult.value) {
  const approveResult = await sodax.staking.approve({
    params: { srcChainKey: ChainKeys.BASE_MAINNET, srcAddress, amount, minReceive, action: 'stake' },
    walletProvider: evmWalletProvider,
  });
  if (!approveResult.ok) throw approveResult.error;
}

// 3. Stake (spoke tx + relay + hub confirmation)
const stakeResult = await sodax.staking.stake({
  params: { srcChainKey: ChainKeys.BASE_MAINNET, srcAddress, amount, minReceive, action: 'stake' },
  walletProvider: evmWalletProvider,
  timeout: 60000,
});
if (!stakeResult.ok) throw stakeResult.error;

const { srcChainTxHash, dstChainTxHash } = stakeResult.value;
```

### Full unstake + claim flow

```typescript
// Unstake (starts waiting period)
const unstakeResult = await sodax.staking.unstake({
  params: { srcChainKey: ChainKeys.BASE_MAINNET, srcAddress, amount, action: 'unstake' },
  walletProvider: evmWalletProvider,
});

// ... wait for unstaking period ...

// Preview claimable amount with penalty info
const penaltyResult = await sodax.staking.getUnstakingInfoWithPenalty(srcAddress, ChainKeys.BASE_MAINNET);
if (penaltyResult.ok) {
  for (const req of penaltyResult.value.requestsWithPenalty) {
    // Claim with the pre-computed claimable amount
    await sodax.staking.claim({
      params: {
        srcChainKey: ChainKeys.BASE_MAINNET,
        srcAddress,
        requestId: req.id,
        amount: req.claimableAmount,
        action: 'claim',
      },
      walletProvider: evmWalletProvider,
    });
  }
}
```

## Supported Chains

All staking operations accept any `SpokeChainKey` as the source chain. The hub chain (Sonic, `ChainKeys.SONIC_MAINNET`) may also be used as the source — in that case, spoke and hub tx hashes are identical. Example chains:

* EVM spoke chains: `ChainKeys.BASE_MAINNET`, `ChainKeys.ETHEREUM_MAINNET`, `ChainKeys.ARBITRUM_MAINNET`, etc.
* Hub chain: `ChainKeys.SONIC_MAINNET`
* Non-EVM chains: `ChainKeys.ICON_MAINNET`, `ChainKeys.SUI_MAINNET`, `ChainKeys.STELLAR_MAINNET`, etc.

Approval support: EVM spoke chains, hub chain, and Stellar only. All other non-EVM chains do not require on-chain approval.

## Penalty System

The staking system includes a penalty mechanism for early unstaking:

* **`minUnstakingPeriod`** — maximum penalty applies if claiming before this time elapses.
* **`unstakingPeriod`** — no penalty once this full period elapses.
* **Linear reduction** — penalty decreases linearly between `minUnstakingPeriod` and `unstakingPeriod`.

Use `getStakingConfig` to read the current parameters, and `getUnstakingInfoWithPenalty` to see the exact penalty for each pending request.

## Instant Unstaking

Instant unstaking allows users to immediately receive SODA tokens in exchange for xSoda shares, bypassing the waiting period, but receiving a reduced amount due to the immediate-liquidity mechanism. The actual amount received depends on current pool conditions and is estimated by `getInstantUnstakeRatio`.

Always call `getInstantUnstakeRatio` before `instantUnstake` to set an appropriate `minAmount` slippage guard.


# Migration

> **Error handling conventions:** This module returns `Result<T, SodaxError<NarrowCode>>` from every async public method. Discriminate on `error.code` (a closed reason-only union) and `error.feature === 'migration'`. See [Error Handling](#error-handling) below.

Migration part of the SDK provides abstractions to assist you with migrating tokens between ICON and the hub chain (Sonic). The service supports multiple migration types including ICX/wICX → SODA, bnUSD legacy → new bnUSD, BALN → SODA, and their reverse operations.

## Using SDK Config and Constants

SDK includes predefined configurations of supported chains, tokens and other relevant information for the client to consume.

```typescript
import { 
  ChainKeys,
  type HubChainKey,
  type SpokeChainKey,
} from "@sodax/sdk"

// Supported migration chains
const hubChainKey: HubChainKey = ChainKeys.SONIC_MAINNET;
const iconChainKey: SpokeChainKey = ChainKeys.ICON_MAINNET;

// Migration tokens
const migrationTokens = ['ICX', 'bnUSD', 'BALN'] as const;
```

Please refer to [SDK ChainKeys](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/chains/chain-keys.ts) for more. For a direct mapping from old `*_CHAIN_ID` constants to `ChainKeys.*` see `packages/sdk/CHAIN_ID_MIGRATION.md`.

### Wallet Providers

All execution methods accept a `walletProvider` inside the action params object — no spoke provider classes need to be constructed by callers. The wallet provider type is chain-narrowed from the `srcChainKey` in the params.

```typescript
import { EvmWalletProvider, IconWalletProvider } from '@sodax/wallet-sdk-core';

// ICON wallet provider (for ICX / BALN / legacy bnUSD migrations originating on ICON)
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

// Sonic wallet provider (for reverse ICX migration originating on Sonic)
const sonicWalletProvider = new EvmWalletProvider({ privateKey: '...', rpcUrl: '...' });
```

## Migration Types

The MigrationService supports multiple types of migrations:

1. **ICX/wICX → SODA**: Migrate ICX or wICX tokens from ICON to SODA tokens on the hub chain
2. **SODA → wICX**: Revert SODA tokens from the hub chain back to wICX tokens on ICON
3. **bnUSD Legacy ↔ New bnUSD**: Unified migration between legacy and new bnUSD tokens across supported chains
4. **BALN → SODA**: Migrate BALN tokens to SODA tokens on the hub chain

## Calling Convention

All exec methods on `MigrationService` follow the `SpokeExecActionParams` wrapper pattern. The wrapper carries the migration params, the wallet provider, and optional flags:

```typescript
// Signed execution (raw: false, walletProvider required)
const result = await sodax.migration.migrateIcxToSoda({
  params: { srcChainKey: ChainKeys.ICON_MAINNET, /* ... */ },
  walletProvider: iconWalletProvider,
  timeout: 30000,       // optional, ms; default 120000
  skipSimulation: false, // optional
});

// Raw transaction (raw: true, walletProvider forbidden)
const rawResult = await sodax.migration.createMigrateIcxToSodaIntent({
  params: { srcChainKey: ChainKeys.ICON_MAINNET, /* ... */ },
  raw: true,
});
```

TypeScript enforces the pairing: `walletProvider` when `raw: true` is a compile error; omitting it when `raw: false` is also a compile error.

## Common Operations

### Check Allowance

Before creating migration intents, you should check if the allowance is valid. For forward migrations (ICX/wICX, bnUSD from ICON, BALN), no allowance is required as these tokens do not require approval.

**Note**: For Stellar-based operations, the allowance system works differently:

* **Source Chain (Stellar)**: The standard `isAllowanceValid` method works as expected for EVM chains, but for Stellar as the source chain, this method checks and establishes trustlines instead.
* **Destination Chain (Stellar)**: When Stellar is specified as the destination chain, frontends/clients need to manually check trustlines using `StellarSpokeService.hasSufficientTrustline` before executing migration operations.

```typescript
const sodax = new Sodax();

// For forward ICX/BALN migration (no allowance required — returns true immediately for ICON source)
const migrationParams: IcxMigrateParams = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...',
  address: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // wICX address
  amount: BigInt(1000000000000000000), // 1 ICX (18 decimals)
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on hub chain
};

const isAllowed = await sodax.migration.isAllowanceValid(migrationParams, 'migrate');

if (!isAllowed.ok) {
  console.error('Failed to check allowance:', isAllowed.error);
} else {
  console.log('Allowance is valid:', isAllowed.value);
}

// For reverse ICX migration (SODA tokens require allowance check — source chain is Sonic)
const revertParams: IcxCreateRevertMigrationParams = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...', // Sonic address
  amount: BigInt(1000000000000000000), // 1 SODA token (18 decimals)
  dstAddress: 'hx1234567890123456789012345678901234567890', // ICON address to receive wICX
};

const isAllowedRevert = await sodax.migration.isAllowanceValid(revertParams, 'revert');

if (!isAllowedRevert.ok) {
  console.error('Failed to check allowance:', isAllowedRevert.error);
} else if (!isAllowedRevert.value) {
  console.log('Approval needed for SODA tokens');
} else {
  console.log('Allowance is valid');
}
```

### Approve Tokens

For reverse migrations, if the allowance check returns false, you need to approve the tokens before creating the revert migration intent.

**Note**: For Stellar-based operations, the approval system works differently:

* **Source Chain (Stellar)**: The standard `approve` method works as expected for EVM chains, but for Stellar as the source chain, this method establishes trustlines instead.
* **Destination Chain (Stellar)**: When Stellar is specified as the destination chain, frontends/clients need to manually establish trustlines using `StellarSpokeService.requestTrustline` before executing migration operations.

```typescript
const sodax = new Sodax();

const revertParams: IcxCreateRevertMigrationParams = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...',
  amount: BigInt(1000000000000000000),
  dstAddress: 'hx1234567890123456789012345678901234567890',
};

// Approve SODA tokens for reverse migration
const approveResult = await sodax.migration.approve(
  {
    params: revertParams,
    walletProvider: sonicWalletProvider,
  },
  'revert',
);

if (approveResult.ok) {
  console.log('Approval transaction hash:', approveResult.value);
  // Wait for approval transaction to be mined
  const approveTxResult = await sonicWalletProvider.waitForTransactionReceipt(approveResult.value);
  console.log('Approval transaction confirmed:', approveTxResult);
} else {
  console.error('Failed to approve tokens:', approveResult.error);
}
```

### Stellar Trustline Requirements

For Stellar-based migration operations, you need to handle trustlines differently depending on whether Stellar is the source or destination chain. See [Stellar Trustline Requirements](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STELLAR_TRUSTLINE.md#migration) for detailed information and code examples.

## ICX Migration (ICX/wICX → SODA)

### Migrate ICX to SODA

Migrate ICX or wICX tokens to SODA tokens on the hub chain.

```typescript
const sodax = new Sodax();

const migrationParams: IcxMigrateParams = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...', // ICON wallet address
  address: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // wICX address
  amount: BigInt(1000000000000000000), // 1 ICX (18 decimals)
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on hub chain
};

// Migrate ICX to SODA
const result = await sodax.migration.migrateIcxToSoda({
  params: migrationParams,
  walletProvider: iconWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('ICX migration successful!');
  console.log('Spoke transaction hash:', srcChainTxHash);
  console.log('Hub transaction hash:', dstChainTxHash);
} else {
  console.error('ICX migration failed:', result.error);
}
```

## Reverse ICX Migration (SODA → wICX)

### Revert SODA to ICX

Revert SODA tokens back to wICX tokens on ICON. A SODA approval from the caller to their hub wallet must be set before calling this method (use `isAllowanceValid` to check and `approve` to set it).

```typescript
const sodax = new Sodax();

const revertParams: IcxCreateRevertMigrationParams = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...', // Sonic wallet address
  amount: BigInt(1000000000000000000), // 1 SODA token (18 decimals)
  dstAddress: 'hx1234567890123456789012345678901234567890', // ICON address to receive wICX
};

// Revert SODA to ICX
const result = await sodax.migration.revertMigrateSodaToIcx({
  params: revertParams,
  walletProvider: sonicWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('SODA to ICX revert successful!');
  console.log('Sonic transaction hash:', srcChainTxHash);
  console.log('Hub packet receipt:', dstChainTxHash);
} else {
  console.error('SODA to ICX revert failed:', result.error);
}
```

## bnUSD Migration (Legacy ↔ New bnUSD)

The bnUSD migration now uses a unified API that handles both forward (legacy → new) and reverse (new → legacy) migrations. The system automatically determines the migration direction based on the token addresses provided.

### bnUSD Constants and Helper Functions

The SDK provides several constants and helper functions to work with legacy and new bnUSD tokens across different chains:

```typescript
import {
  bnUSDLegacySpokeChainIds,
  newbnUSDSpokeChainIds,
  bnUSDLegacyTokens,
  isLegacybnUSDChainId,
  isNewbnUSDChainId,
  isLegacybnUSDToken,
  isNewbnUSDToken,
  getAllLegacybnUSDTokens,
} from '@sodax/sdk';

// Get all chains that support legacy bnUSD
console.log('Legacy bnUSD chains:', bnUSDLegacySpokeChainIds);
// Output: ['0x1.icon', 'sui', 'stellar']

// Get all chains that support new bnUSD
console.log('New bnUSD chains:', newbnUSDSpokeChainIds);
// Output: ['sonic', 'arbitrum', 'base', 'polygon', ...] (all chains except Icon)

// Check if a chain supports legacy bnUSD
const isLegacyChain = isLegacybnUSDChainId(ChainKeys.ICON_MAINNET); // true
const isNewChain = isNewbnUSDChainId(ChainKeys.SONIC_MAINNET); // true

// Check if a token address is legacy bnUSD
const isLegacyToken = isLegacybnUSDToken('cx88fd7df7ddff82f7cc735c871dc519838cb235bb'); // true
const isNewToken = isNewbnUSDToken('0xE801CA34E19aBCbFeA12025378D19c4FBE250131'); // true
```

### Migrate Legacy bnUSD to New bnUSD

Migrate legacy bnUSD tokens to new bnUSD tokens on any spoke chain (besides Icon — which has only legacy bnUSD).

**Note**: When migrating to Stellar as the destination chain, ensure you have established the necessary trustlines using `StellarSpokeService.hasSufficientTrustline` and `StellarSpokeService.requestTrustline` before executing the migration.

```typescript
const sodax = new Sodax();

const migrationParams: UnifiedBnUSDMigrateParams<typeof ChainKeys.ICON_MAINNET> = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...', // ICON wallet address
  dstChainKey: ChainKeys.SONIC_MAINNET,
  srcbnUSD: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // Legacy bnUSD address on ICON
  dstbnUSD: '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', // New bnUSD address on Sonic
  amount: BigInt(1000000000000000000), // 1 bnUSD (18 decimals)
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on Sonic
};

// Migrate legacy bnUSD to new bnUSD
const result = await sodax.migration.migratebnUSD({
  params: migrationParams,
  walletProvider: iconWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('bnUSD migration successful!');
  console.log('Spoke transaction hash:', srcChainTxHash);
  console.log('Hub transaction hash:', dstChainTxHash);
} else {
  console.error('bnUSD migration failed:', result.error);
}
```

### Reverse Migrate New bnUSD to Legacy bnUSD

Revert new bnUSD tokens back to legacy bnUSD tokens. Legacy bnUSD exists on Icon, Sui or Stellar chains.

**Note**: When migrating to Stellar as the destination chain, ensure you have established the necessary trustlines using `StellarSpokeService.hasSufficientTrustline` and `StellarSpokeService.requestTrustline` before executing the migration.

```typescript
const sodax = new Sodax();

const revertParams: UnifiedBnUSDMigrateParams<typeof ChainKeys.SONIC_MAINNET> = {
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xabc...', // Sonic wallet address
  dstChainKey: ChainKeys.ICON_MAINNET,
  srcbnUSD: '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', // New bnUSD address on Sonic
  dstbnUSD: 'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // Legacy bnUSD address on ICON
  amount: BigInt(1000000000000000000), // 1 new bnUSD (18 decimals)
  dstAddress: 'hx1234567890123456789012345678901234567890', // Recipient address on ICON
};

// Check allowance for reverse migration
const isAllowed = await sodax.migration.isAllowanceValid(revertParams, 'revert');

if (!isAllowed.ok) {
  console.error('Failed to check allowance:', isAllowed.error);
} else if (!isAllowed.value) {
  // Approve if needed
  const approveResult = await sodax.migration.approve(
    {
      params: revertParams,
      walletProvider: sonicWalletProvider,
    },
    'revert',
  );

  if (approveResult.ok) {
    console.log('Approval transaction hash:', approveResult.value);
    // Wait for approval transaction to be mined
    await sonicWalletProvider.waitForTransactionReceipt(approveResult.value);
  } else {
    console.error('Failed to approve tokens:', approveResult.error);
    return;
  }
}

// Reverse migrate new bnUSD to legacy bnUSD
const result = await sodax.migration.migratebnUSD({
  params: revertParams,
  walletProvider: sonicWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('bnUSD reverse migration successful!');
  console.log('Sonic transaction hash:', srcChainTxHash);
  console.log('Hub packet receipt:', dstChainTxHash);
} else {
  console.error('bnUSD reverse migration failed:', result.error);
}
```

## BALN Migration (BALN → SODA)

### Migrate BALN to SODA

Migrate BALN tokens to SODA tokens on the hub chain. Use `LockupPeriod` enum values for the `lockupPeriod` field — longer lock-ups yield higher SODA multipliers (0.5×–1.5×).

```typescript
import { LockupPeriod } from '@sodax/sdk';

const sodax = new Sodax();

const migrationParams: BalnMigrateParams = {
  srcChainKey: ChainKeys.ICON_MAINNET,
  srcAddress: 'hx...', // ICON wallet address
  amount: BigInt(1000000000000000000), // 1 BALN (18 decimals)
  lockupPeriod: LockupPeriod.SIX_MONTHS, // 0.75x multiplier; use LockupPeriod enum
  dstAddress: '0x1234567890123456789012345678901234567890', // Recipient address on hub chain
  stake: true, // Whether to immediately stake the received SODA tokens
};

// Migrate BALN to SODA
const result = await sodax.migration.migrateBaln({
  params: migrationParams,
  walletProvider: iconWalletProvider,
  timeout: 30000, // Optional timeout in milliseconds (default: 120000)
});

if (result.ok) {
  const { srcChainTxHash, dstChainTxHash } = result.value;
  console.log('BALN migration successful!');
  console.log('Spoke transaction hash:', srcChainTxHash);
  console.log('Hub transaction hash:', dstChainTxHash);
} else {
  console.error('BALN migration failed:', result.error);
}
```

### BALN Lock Periods and Multipliers

| Lock Period | Enum Value                        | Multiplier |
| ----------- | --------------------------------- | ---------- |
| No lock     | `LockupPeriod.NO_LOCKUP`          | 0.5×       |
| 6 months    | `LockupPeriod.SIX_MONTHS`         | 0.75×      |
| 12 months   | `LockupPeriod.TWELVE_MONTHS`      | 1.0×       |
| 18 months   | `LockupPeriod.EIGHTEEN_MONTHS`    | 1.25×      |
| 24 months   | `LockupPeriod.TWENTY_FOUR_MONTHS` | 1.5×       |

### BALN Lock Management

After migrating BALN, the resulting SODA (or xSoda) is held in locks managed by `sodax.migration.balnSwapService`. These methods act directly on the hub chain:

```typescript
const { balnSwapService } = sodax.migration;

const lockId = 1n;

// Claim unlocked SODA tokens from a completed lock
await balnSwapService.claim(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Initiate unstaking of xSoda tokens from a lock
await balnSwapService.unstake(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Claim tokens after the unstaking waiting period expires
await balnSwapService.claimUnstaked(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Cancel a pending unstake request
await balnSwapService.cancelUnstake(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Stake SODA tokens held in a lock into the xSoda vault
await balnSwapService.stake(userAddress, { lockId }, { walletProvider: sonicWalletProvider });

// Read all locks for a user
const locks = await balnSwapService.getDetailedUserLocks(publicClient, userAddress);
```

## Complete Examples

### ICX Migration Example

```typescript
import { Sodax, ChainKeys, type IcxMigrateParams } from '@sodax/sdk';
import { IconWalletProvider } from '@sodax/wallet-sdk-core';
import type { Address } from 'viem';

const sodax = new Sodax();
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function migrateIcx(amount: bigint, recipient: Address): Promise<void> {
  const params: IcxMigrateParams = {
    srcChainKey: ChainKeys.ICON_MAINNET,
    srcAddress: await iconWalletProvider.getWalletAddress(),
    address: '0x0000000000000000000000000000000000000000', // wICX or native ICX token address
    amount,
    dstAddress: recipient,
  };

  const result = await sodax.migration.migrateIcxToSoda({
    params,
    walletProvider: iconWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[migrateIcx] Migration successful!');
    console.log('[migrateIcx] Spoke transaction hash:', srcChainTxHash);
    console.log('[migrateIcx] Hub transaction hash:', dstChainTxHash);
  } else {
    console.error('[migrateIcx] Migration failed:', result.error);
  }
}

// Usage
await migrateIcx(BigInt(1000000000000000000), '0x1234567890123456789012345678901234567890');
```

### Reverse ICX Migration Example

```typescript
import { Sodax, ChainKeys, type IcxCreateRevertMigrationParams } from '@sodax/sdk';
import { EvmWalletProvider } from '@sodax/wallet-sdk-core';
import type { IconEoaAddress } from '@sodax/sdk';

const sodax = new Sodax();
const sonicWalletProvider = new EvmWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function reverseMigrateIcx(amount: bigint, to: IconEoaAddress): Promise<void> {
  const srcAddress = await sonicWalletProvider.getWalletAddress();

  const params: IcxCreateRevertMigrationParams = {
    srcChainKey: ChainKeys.SONIC_MAINNET,
    srcAddress,
    amount,
    dstAddress: to,
  };

  // Check allowance
  const isAllowed = await sodax.migration.isAllowanceValid(params, 'revert');

  if (!isAllowed.ok) {
    console.error('[reverseMigrateIcx] Allowance check failed:', isAllowed.error);
    return;
  }

  if (!isAllowed.value) {
    // Approve if needed
    const approveResult = await sodax.migration.approve(
      { params, walletProvider: sonicWalletProvider },
      'revert',
    );
    if (approveResult.ok) {
      console.log('[reverseMigrateIcx] Approval hash:', approveResult.value);
      await sonicWalletProvider.waitForTransactionReceipt(approveResult.value);
    } else {
      console.error('[reverseMigrateIcx] Approval failed:', approveResult.error);
      return;
    }
  }

  // Create and submit revert migration intent
  const result = await sodax.migration.revertMigrateSodaToIcx({
    params,
    walletProvider: sonicWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[reverseMigrateIcx] Revert migration successful!');
    console.log('[reverseMigrateIcx] Sonic transaction hash:', srcChainTxHash);
    console.log('[reverseMigrateIcx] Hub packet receipt:', dstChainTxHash);
  } else {
    console.error('[reverseMigrateIcx] Revert migration failed:', result.error);
  }
}

// Usage
await reverseMigrateIcx(BigInt(1000000000000000000), 'hx1234567890123456789012345678901234567890');
```

### bnUSD Migration Example

```typescript
import { Sodax, ChainKeys, type UnifiedBnUSDMigrateParams, type SpokeChainKey } from '@sodax/sdk';
import { IconWalletProvider } from '@sodax/wallet-sdk-core';
import type { Address } from 'viem';

const sodax = new Sodax();
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function migrateBnUSD(
  amount: bigint,
  recipient: Address,
  legacybnUSD: string,
  newbnUSD: string,
  dstChainKey: SpokeChainKey,
): Promise<void> {
  const params: UnifiedBnUSDMigrateParams<typeof ChainKeys.ICON_MAINNET> = {
    srcChainKey: ChainKeys.ICON_MAINNET,
    srcAddress: await iconWalletProvider.getWalletAddress(),
    dstChainKey,
    srcbnUSD: legacybnUSD,
    dstbnUSD: newbnUSD,
    amount,
    dstAddress: recipient,
  };

  const result = await sodax.migration.migratebnUSD({
    params,
    walletProvider: iconWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[migrateBnUSD] Migration successful!');
    console.log('[migrateBnUSD] Spoke transaction hash:', srcChainTxHash);
    console.log('[migrateBnUSD] Hub transaction hash:', dstChainTxHash);
  } else {
    console.error('[migrateBnUSD] Migration failed:', result.error);
  }
}

// Usage — forward migration (legacy to new)
await migrateBnUSD(
  BigInt(1000000000000000000),
  '0x1234567890123456789012345678901234567890',
  'cx88fd7df7ddff82f7cc735c871dc519838cb235bb', // Legacy bnUSD on ICON
  '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', // New bnUSD on Sonic
  ChainKeys.SONIC_MAINNET,
);
```

### BALN Migration Example

```typescript
import { Sodax, ChainKeys, LockupPeriod, type BalnMigrateParams } from '@sodax/sdk';
import { IconWalletProvider } from '@sodax/wallet-sdk-core';
import type { Address } from 'viem';

const sodax = new Sodax();
const iconWalletProvider = new IconWalletProvider({ privateKey: '...', rpcUrl: '...' });

async function migrateBaln(amount: bigint, recipient: Address): Promise<void> {
  const params: BalnMigrateParams = {
    srcChainKey: ChainKeys.ICON_MAINNET,
    srcAddress: await iconWalletProvider.getWalletAddress(),
    amount,
    lockupPeriod: LockupPeriod.SIX_MONTHS, // 0.75x multiplier
    dstAddress: recipient,
    stake: true,
  };

  const result = await sodax.migration.migrateBaln({
    params,
    walletProvider: iconWalletProvider,
  });

  if (result.ok) {
    const { srcChainTxHash, dstChainTxHash } = result.value;
    console.log('[migrateBaln] Migration successful!');
    console.log('[migrateBaln] Spoke transaction hash:', srcChainTxHash);
    console.log('[migrateBaln] Hub transaction hash:', dstChainTxHash);
  } else {
    console.error('[migrateBaln] Migration failed:', result.error);
  }
}

// Usage
await migrateBaln(BigInt(1000000000000000000), '0x1234567890123456789012345678901234567890');
```

## Error Handling

All async public methods on `MigrationService` (and `IcxMigrationService.getAvailableAmount`) return `Promise<Result<T, SodaxError<NarrowCode>>>` where `NarrowCode` is a narrow per-method union of `MigrationErrorCode`. Discriminate on `error.code`, never on `error.message`. The original lower-level failure (a viem revert, a fetch error, a relay timeout) is preserved on `error.cause`; structured metadata (chain, action, direction, phase, relayCode) is on `error.context`.

```typescript
import { isMigrateOrchestrationError, type MigrateOrchestrationError } from '@sodax/sdk';

const result = await sodax.migration.migrateIcxToSoda({ /* params */ });
if (!result.ok) {
  // result.error is typed as `MigrateOrchestrationError = SodaxError<MigrateOrchestrationErrorCode>`
  switch (result.error.code) {
    case 'VALIDATION_FAILED':       // precondition tripped (see context.field)
    case 'INTENT_CREATION_FAILED':  // spoke-side intent creation failed
    case 'TX_VERIFICATION_FAILED':           // spoke tx not verifiable on-chain (only migratebnUSD calls verifyTxHash)
    case 'TX_SUBMIT_FAILED':        // relay submit failed
    case 'RELAY_TIMEOUT':           // relay packet did not arrive within timeout
    case 'RELAY_FAILED':            // relay polling failure / unknown relay error
    case 'EXECUTION_FAILED':                  // generic forward-orchestrator catch-all (see error.cause)
    case 'UNKNOWN':
      handleMigrationError(result.error);
      break;
  }
}
```

### Per-method error code unions

| Method                                                                                                            | Codes                                                                                                                                                                        |
| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `migratebnUSD` / `migrateIcxToSoda` / `migrateBaln` (forward orchestrators)                                       | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_VERIFICATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN` |
| `revertMigrateSodaToIcx` (reverse orchestrator)                                                                   | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `TX_SUBMIT_FAILED`, `RELAY_TIMEOUT`, `RELAY_FAILED`, `EXECUTION_FAILED`, `UNKNOWN`                           |
| `createMigratebnUSDIntent` / `createMigrateIcxToSodaIntent` / `createMigrateBalnIntent` (forward intent creators) | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                                    |
| `createRevertSodaToIcxMigrationIntent` (reverse intent creator)                                                   | `USER_REJECTED`, `VALIDATION_FAILED`, `INTENT_CREATION_FAILED`, `UNKNOWN`                                                                                                    |
| `approve`                                                                                                         | `USER_REJECTED`, `VALIDATION_FAILED`, `APPROVE_FAILED`, `UNKNOWN`                                                                                                            |
| `isAllowanceValid`                                                                                                | `VALIDATION_FAILED`, `ALLOWANCE_CHECK_FAILED`, `UNKNOWN`                                                                                                                     |
| `IcxMigrationService.getAvailableAmount`                                                                          | `VALIDATION_FAILED`, `LOOKUP_FAILED`, `UNKNOWN`                                                                                                                              |

Note: `TX_VERIFICATION_FAILED` only appears in the forward-orchestrator union because `migratebnUSD` is the only orchestrator that calls `spoke.verifyTxHash`. The other forward orchestrators technically can't produce it, but the shared narrow union keeps callers working symmetrically across the three "migrate" methods.

### Structured `context`

Every migration error carries an `error.context` payload. Fields vary by code:

| Field              | Set on                                                | Notes                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `srcChainKey`      | all orchestrator + intent + approve + allowance codes | low-cardinality — suitable as a logger / Sentry tag                                                                                                                                                                                                                                                                                                                                  |
| `dstChainKey`      | `migratebnUSD` orchestrator + intent codes            | bnUSD-only (the other orchestrators have a fixed destination)                                                                                                                                                                                                                                                                                                                        |
| `action`           | all orchestrator + intent codes                       | one of `'migratebnUSD' \| 'migrateIcxToSoda' \| 'revertMigrateSodaToIcx' \| 'migrateBaln'`                                                                                                                                                                                                                                                                                           |
| `direction`        | only on `migratebnUSD` errors                         | `'forward'` (legacy → new) or `'reverse'` (new → legacy). The error code stays `EXECUTION_FAILED` regardless — this is purely a forensics hint                                                                                                                                                                                                                                       |
| `phase`            | most codes                                            | `'validate' \| 'intentCreation' \| 'verify' \| 'submit' \| 'relay' \| 'destinationExecution' \| 'approve' \| 'allowanceCheck' \| 'lookup'`. `'destinationExecution'` is set on `RELAY_TIMEOUT / RELAY_FAILED / TX_SUBMIT_FAILED` errors that originate from `migratebnUSD`'s secondary `waitUntilIntentExecuted` watcher (vs. `'relay'` for the primary `relayTxAndWaitPacket` call) |
| `relayCode`        | `RELAY_TIMEOUT` / `TX_SUBMIT_FAILED` / `RELAY_FAILED` | mirrors the relay-layer `RELAY_ERROR_CODES` contract; carries `'RELAY_POLLING_FAILED'` so polling outage is distinguishable from generic failure                                                                                                                                                                                                                                     |
| `field` / `reason` | `VALIDATION_FAILED`                                   | which precondition tripped                                                                                                                                                                                                                                                                                                                                                           |

### Type guards

Per-method type guards are runtime-checked and compile-checked in lockstep with the union types. Use them in `catch` blocks to short-circuit when a foreign code escapes:

```typescript
import { isMigrateOrchestrationError, isMigrationError } from '@sodax/sdk';

try {
  // ... call sodax.migration.migratebnUSD ...
} catch (e) {
  if (isMigrateOrchestrationError(e)) console.error('typed forward-migration error:', e.code, e.context);
  else if (isMigrationError(e)) console.error('migration error from another method:', e.code);
  else throw e; // not a migration error — bubble up
}
```

Available guards: `isMigrationError` (broad — any migration error), `isMigrateOrchestrationError` (forward `migrateIcxToSoda` / `migratebnUSD` / `migrateBaln`), `isRevertMigrationOrchestrationError` (`revertMigrateSodaToIcx`), `isMigrationCreateIntentError` (shared by all 4 `create*Intent` methods), `isMigrationApproveError`, `isMigrationAllowanceCheckError`, `isMigrationLookupError`. Per-operation discrimination across the orchestrators is via `error.context.action` (one of `'migrateIcxToSoda' | 'migratebnUSD' | 'migrateBaln' | 'revertMigrateSodaToIcx'`).

### Validation invariant

Precondition failures throw a typed `VALIDATION_FAILED` from inside the public method's `try/catch`, surfacing as a typed `Result.error` rather than a generic prose `Error`. Consumers discriminate validation failures the same way as any other code.

```typescript
import { migrationInvariant } from '@sodax/sdk';

migrationInvariant(amount > 0n, 'Amount must be greater than 0', { field: 'amount' });
```

### Migration from the pre-v2 taxonomy

The published v1 4-code shape (`EXECUTION_FAILED`, `CREATE_MIGRATION_INTENT_FAILED`, `REVERT_MIGRATION_FAILED`, `CREATE_REVERT_MIGRATION_INTENT_FAILED`) is restored here with module-prefixed names and cause-preservation. Sub-modules (ICX, bnUSD, BALN) remain undifferentiated at the code level — fine-grained partitioning is delegated to `context.action`, faithful to v1 which also did not distinguish them.

| v1 code                                 | v2 code                                                       | Notes                                                                                                                   |
| --------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `EXECUTION_FAILED`                      | `EXECUTION_FAILED`                                            | Forward-orchestrator catch-all (`migratebnUSD`/`migrateIcxToSoda`/`migrateBaln`). Use `context.action` to discriminate. |
| `CREATE_MIGRATION_INTENT_FAILED`        | `INTENT_CREATION_FAILED`                                      | Forward intent-creation phase.                                                                                          |
| `REVERT_MIGRATION_FAILED`               | `EXECUTION_FAILED`                                            | Reverse-orchestrator catch-all (`revertMigrateSodaToIcx`).                                                              |
| `CREATE_REVERT_MIGRATION_INTENT_FAILED` | `INTENT_CREATION_FAILED`                                      | Reverse intent-creation phase.                                                                                          |
| (none)                                  | `VALIDATION_FAILED`                                           | New: typed precondition failures (replaces prose `Error` throws from `invariant`).                                      |
| (none)                                  | `TX_VERIFICATION_FAILED`                                      | New: spoke tx verification phase tag (only set by `migratebnUSD`, the only orchestrator that calls `verifyTxHash`).     |
| (none)                                  | `TX_SUBMIT_FAILED` / `RELAY_TIMEOUT` / `RELAY_FAILED`         | New: typed relay-phase codes mapped from the shared `RELAY_ERROR_CODES` contract.                                       |
| (none)                                  | `APPROVE_FAILED` / `ALLOWANCE_CHECK_FAILED` / `LOOKUP_FAILED` | New: typed phase codes for `approve` / `isAllowanceValid` / `IcxMigrationService.getAvailableAmount`.                   |
| (none)                                  | `UNKNOWN`                                                     | Reserved fallback for never-classified errors.                                                                          |

## Configuration

The MigrationService is wired internally by the `Sodax` facade. Custom relay endpoints are passed via the `Sodax` constructor config:

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax({
  relay: {
    relayerApiEndpoint: 'https://custom-relay-api.example.com',
  },
});

await sodax.config.initialize(); // optional: fetch dynamic chain config from backend
```

Default configuration:

* `relayerApiEndpoint`: `https://relay.soniclabs.com`
* `timeout`: 120000 ms (120 seconds) — overridable per call via the `timeout` field in action params


# Tooling Modules

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/backend_api" class="button secondary" data-icon="plug">Backend API</a>

<a href="https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/intent_relay_api" class="button secondary" data-icon="envelope">Intent Relay API</a>


# Backend API

The `BackendApiService` provides a comprehensive HTTP client for the SODAX backend API, covering intent lookup, swap submission, solver orderbook, money market data, and runtime configuration. It implements `IConfigApi` so that `ConfigService` and other services can fetch dynamic chain/token configuration without coupling to a concrete HTTP implementation.

The service is automatically instantiated when you create a `Sodax` instance and is available as `sodax.backendApi`.

All public methods return `Promise<Result<T>>` — they never throw. On network failure, timeout, or a non-2xx HTTP response the returned `Result` has `ok: false` with a descriptive `Error` in the `error` field.

## Table of Contents

* [Initialization](#initialization)
* [Configuration](#configuration)
* [Result\<T> and Error Handling](#resultt-and-error-handling)
* [Intent Endpoints](#intent-endpoints)
* [Swap Endpoints](#swap-endpoints)
* [Solver Endpoints](#solver-endpoints)
* [Money Market Endpoints](#money-market-endpoints)
* [Config Endpoints](#config-endpoints)
* [Utility Methods](#utility-methods)
* [Complete Example](#complete-example)
* [Notes](#notes)

## Initialization

`BackendApiService` is automatically created when you construct a `Sodax` instance. Access it via `sodax.backendApi`.

### Basic Initialization

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax();
const backendApi = sodax.backendApi;
```

### Custom Configuration

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax({
  api: {
    baseURL: 'https://your-custom-api-endpoint.com',
    timeout: 60000, // 60 seconds
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      'X-Custom-Header': 'custom-value',
    },
  },
});

const backendApi = sodax.backendApi;
```

### Initialization with `sodax.config`

If you need dynamic chain/token config fetched from the backend, call `initialize()` on `ConfigService` after construction. `BackendApiService` is the underlying HTTP transport that `ConfigService` uses.

```typescript
const sodax = new Sodax();
const result = await sodax.config.initialize();
if (!result.ok) {
  console.error('Config initialization failed:', result.error);
}
```

## Configuration

### `ApiConfig` Type

`ApiConfig` is either a flat `BaseApiConfig` (shared by `sodax.backendApi` and the swaps client `sodax.api.swaps`) or a nested `CustomApiConfig` that points the swaps API at its own endpoint:

```typescript
type BaseApiConfig = {
  baseURL: string;                   // API endpoint URL (default: 'https://api.sodax.com/v1/be')
  timeout: number;                   // Request timeout in milliseconds (default: 30000)
  headers: Record<string, string>;   // Request headers (default: Content-Type and Accept)
};

// Point the swaps API at its own host, separate from the base backend API (at least one slice required):
type CustomApiConfig =
  | { baseApiConfig: BaseApiConfig; swapsApiConfig?: BaseApiConfig }
  | { baseApiConfig?: BaseApiConfig; swapsApiConfig: BaseApiConfig };

type ApiConfig = BaseApiConfig | CustomApiConfig;
```

### `RequestOverrideConfig` Type

Every public method accepts an optional `RequestOverrideConfig` as its last argument. These per-call overrides take precedence over the `ApiConfig` the service was constructed with.

```typescript
type RequestOverrideConfig = {
  baseURL?: string;
  timeout?: number;
  headers?: Record<string, string>;
};
```

### Default Configuration

```typescript
const DEFAULT_BACKEND_API_ENDPOINT = 'https://api.sodax.com/v1/be';
const DEFAULT_BACKEND_API_TIMEOUT = 30000; // 30 seconds
const DEFAULT_BACKEND_API_HEADERS = {
  'Content-Type': 'application/json',
  Accept: 'application/json',
};
```

## Result\<T> and Error Handling

All public methods return `Promise<Result<T>>`, defined as:

```typescript
type Result<T, E = Error | unknown> =
  | { ok: true; value: T }
  | { ok: false; error: E };
```

**Never use `try/catch` around `backendApi` calls** — errors are always captured in the `Result`.

**Response validation.** Data, token, and money-market responses are validated at runtime against valibot schemas (mirroring `sodax.api.swaps`): a 2xx body that fails the contract resolves to `EXTERNAL_API_ERROR` with `context.reason: 'invalid_response_shape'`, not a mistyped value. The config/relay reads — `getAllConfig`, `getSpokeChainConfig`, `getRelayChainIdMap` — are not schema-validated (`SodaxConfig` is too large to mirror faithfully, and `ConfigService` already version-gates and falls back to packaged defaults).

### Checking results

```typescript
const result = await sodax.backendApi.getOrderbook({ offset: '0', limit: '10' });
if (!result.ok) {
  // result.error is an Error instance
  console.error(result.error.message); // e.g. 'HTTP_REQUEST_FAILED', 'REQUEST_TIMEOUT'
  if (result.error instanceof Error && result.error.cause) {
    console.error('Underlying cause:', result.error.cause);
  }
  return;
}
console.log(result.value); // OrderbookResponse
```

### Error codes on `error.message`

| `error.message`           | Meaning                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------- |
| `'HTTP_REQUEST_FAILED'`   | Non-2xx HTTP response. Check `error.cause` for `HTTP <status>: <body>`.                |
| `'REQUEST_TIMEOUT'`       | Request exceeded the configured timeout. Check `error.cause` for the timeout duration. |
| `'UNKNOWN_REQUEST_ERROR'` | Any other unexpected failure.                                                          |

## Intent Endpoints

### Get Intent by Transaction Hash

Retrieves swap intent details using a hub-chain transaction hash.

```typescript
const result = await sodax.backendApi.getIntentByTxHash('0x123...abc');
if (result.ok) {
  console.log(result.value); // IntentResponse
}
```

**Signature:**

```typescript
getIntentByTxHash(
  txHash: string,
  config?: RequestOverrideConfig,
): Promise<Result<IntentResponse>>
```

* **Method:** GET
* **Endpoint:** `/intent/tx/{txHash}`

**Response type:**

```typescript
interface IntentResponse {
  intentHash: string;
  txHash: string;
  logIndex: number;
  chainId: number;
  blockNumber: number;
  open: boolean;
  intent: {
    intentId: string;
    creator: string;
    inputToken: `0x${string}`;
    outputToken: `0x${string}`;
    inputAmount: string;
    minOutputAmount: string;
    deadline: string;
    allowPartialFill: boolean;
    srcChain: number;
    dstChain: number;
    srcAddress: `0x${string}`;
    dstAddress: `0x${string}`;
    solver: string;
    data: string;
  };
  events: unknown[];
}
```

### Get Intent by Intent Hash

Retrieves swap intent details using a canonical intent hash.

```typescript
const result = await sodax.backendApi.getIntentByHash('0x456...def');
```

**Signature:**

```typescript
getIntentByHash(
  intentHash: string,
  config?: RequestOverrideConfig,
): Promise<Result<IntentResponse>>
```

* **Method:** GET
* **Endpoint:** `/intent/{intentHash}`

**Response:** Same as `IntentResponse` above.

### Get User Intents

Retrieves a paginated list of all swap intents created by a specific wallet address, with optional date-range filtering.

`startDate` and `endDate` are Unix timestamps in **milliseconds**.

```typescript
const result = await sodax.backendApi.getUserIntents({
  userAddress: '0x789...ghi',
  startDate: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
  endDate: Date.now(),
  limit: '20',
  offset: '0',
});
```

**Signature:**

```typescript
getUserIntents(
  params: {
    userAddress: Address;
    startDate?: number;
    endDate?: number;
    limit?: string;
    offset?: string;
  },
  config?: RequestOverrideConfig,
): Promise<Result<UserIntentsResponse>>
```

* **Method:** GET
* **Endpoint:** `/intent/user/{userAddress}?startDate=…&endDate=…&limit=…&offset=…`

**Response type:**

```typescript
interface UserIntentsResponse {
  total: number;
  offset: number;
  limit: number;
  items: IntentResponse[];
}
```

## Swap Endpoints

Swap-tx submission and the rest of the typed Swaps API v2 moved off `BackendApiService` onto the swaps client — `sodax.api.swaps` (`SwapsApiService`). Submit a signed spoke-chain swap transaction with `sodax.api.swaps.submitTx(...)` and poll it with `sodax.api.swaps.getSubmitTxStatus({ txHash, srcChainKey })` (both fields required). See [`SWAPS_API.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS_API.md) for the full 21-endpoint reference (quote, create-intent, submit-tx, status, fees, …).

## Solver Endpoints

### Get Orderbook

Retrieves a paginated snapshot of the solver orderbook — open swap intents waiting to be filled.

```typescript
const result = await sodax.backendApi.getOrderbook({ offset: '0', limit: '10' });
if (result.ok) {
  console.log(result.value.total, result.value.data);
}
```

**Signature:**

```typescript
getOrderbook(
  params: { offset: string; limit: string },
  config?: RequestOverrideConfig,
): Promise<Result<OrderbookResponse>>
```

* **Method:** GET
* **Endpoint:** `/solver/orderbook?offset={offset}&limit={limit}`

**Response type:**

```typescript
interface OrderbookResponse {
  total: number;
  data: Array<{
    intentState: {
      exists: boolean;
      remainingInput: string;
      receivedOutput: string;
      pendingPayment: boolean;
    };
    intentData: {
      intentId: string;
      creator: string;
      inputToken: string;
      outputToken: string;
      inputAmount: string;
      minOutputAmount: string;
      deadline: string;
      allowPartialFill: boolean;
      srcChain: number;
      dstChain: number;
      srcAddress: string;
      dstAddress: string;
      solver: string;
      data: string;
      intentHash: string;
      txHash: string;
      blockNumber: number;
    };
  }>;
}
```

## Money Market Endpoints

### Get User Position

Retrieves the current money market position for a wallet address — all reserves where the user holds aTokens (supplied collateral) or variable-debt tokens (outstanding borrows).

```typescript
const result = await sodax.backendApi.getMoneyMarketPosition('0x789...ghi');
if (result.ok) {
  console.log(result.value.positions);
}
```

**Signature:**

```typescript
getMoneyMarketPosition(
  userAddress: string,
  config?: RequestOverrideConfig,
): Promise<Result<MoneyMarketPosition>>
```

* **Method:** GET
* **Endpoint:** `/moneymarket/position/{userAddress}`

**Response type:**

```typescript
interface MoneyMarketPosition {
  userAddress: string;
  positions: Array<{
    reserveAddress: string;
    aTokenAddress: string;
    variableDebtTokenAddress: string;
    aTokenBalance: string;
    variableDebtTokenBalance: string;
    blockNumber: number;
  }>;
}
```

### Get All Money Market Assets

Retrieves on-chain state snapshots for every active money market reserve asset.

```typescript
const result = await sodax.backendApi.getAllMoneyMarketAssets();
if (result.ok) {
  console.log(result.value); // MoneyMarketAsset[]
}
```

**Signature:**

```typescript
getAllMoneyMarketAssets(
  config?: RequestOverrideConfig,
): Promise<Result<MoneyMarketAsset[]>>
```

* **Method:** GET
* **Endpoint:** `/moneymarket/asset/all`

**Response type:**

```typescript
interface MoneyMarketAsset {
  reserveAddress: string;
  aTokenAddress: string;
  totalATokenBalance: string;
  variableDebtTokenAddress: string;
  totalVariableDebtTokenBalance: string;
  liquidityRate: string;
  symbol: string;
  totalSuppliers: number;
  totalBorrowers: number;
  variableBorrowRate: string;
  stableBorrowRate: string;
  liquidityIndex: string;
  variableBorrowIndex: string;
  blockNumber: number;
}
```

### Get Specific Money Market Asset

Retrieves the on-chain state snapshot for a single money market reserve asset.

```typescript
const result = await sodax.backendApi.getMoneyMarketAsset('0xabc...123');
```

**Signature:**

```typescript
getMoneyMarketAsset(
  reserveAddress: string,
  config?: RequestOverrideConfig,
): Promise<Result<MoneyMarketAsset>>
```

* **Method:** GET
* **Endpoint:** `/moneymarket/asset/{reserveAddress}`

**Response:** `MoneyMarketAsset` (same interface as above).

### Get Asset Borrowers

Retrieves a paginated list of wallets with an outstanding borrow against a specific reserve.

```typescript
const result = await sodax.backendApi.getMoneyMarketAssetBorrowers(
  '0xabc...123',
  { offset: '0', limit: '10' },
);
```

**Signature:**

```typescript
getMoneyMarketAssetBorrowers(
  reserveAddress: string,
  params: { offset: string; limit: string },
  config?: RequestOverrideConfig,
): Promise<Result<MoneyMarketAssetBorrowers>>
```

* **Method:** GET
* **Endpoint:** `/moneymarket/asset/{reserveAddress}/borrowers?offset={offset}&limit={limit}`

**Response type:**

```typescript
interface MoneyMarketAssetBorrowers {
  borrowers: string[];
  total: number;
  offset: number;
  limit: number;
}
```

### Get Asset Suppliers

Retrieves a paginated list of wallets with an active supply (aToken balance) in a specific reserve.

```typescript
const result = await sodax.backendApi.getMoneyMarketAssetSuppliers(
  '0xabc...123',
  { offset: '0', limit: '10' },
);
```

**Signature:**

```typescript
getMoneyMarketAssetSuppliers(
  reserveAddress: string,
  params: { offset: string; limit: string },
  config?: RequestOverrideConfig,
): Promise<Result<MoneyMarketAssetSuppliers>>
```

* **Method:** GET
* **Endpoint:** `/moneymarket/asset/{reserveAddress}/suppliers?offset={offset}&limit={limit}`

**Response type:**

```typescript
interface MoneyMarketAssetSuppliers {
  suppliers: string[];
  total: number;
  offset: number;
  limit: number;
}
```

### Get All Money Market Borrowers

Retrieves a paginated list of all wallet addresses that hold an active borrow position across any reserve.

```typescript
const result = await sodax.backendApi.getAllMoneyMarketBorrowers({
  offset: '0',
  limit: '10',
});
```

**Signature:**

```typescript
getAllMoneyMarketBorrowers(
  params: { offset: string; limit: string },
  config?: RequestOverrideConfig,
): Promise<Result<MoneyMarketBorrowers>>
```

* **Method:** GET
* **Endpoint:** `/moneymarket/borrowers?offset={offset}&limit={limit}`

**Response type:**

```typescript
interface MoneyMarketBorrowers {
  borrowers: string[];
  total: number;
  offset: number;
  limit: number;
}
```

## Config Endpoints

These methods implement `IConfigApi` and are consumed internally by `ConfigService`. You generally do not call them directly — use `sodax.config` instead. They are documented here for completeness and for custom `IConfigApi` implementations.

| Method                                    | Endpoint                                     | Returns                                                                       |
| ----------------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- |
| `getAllConfig()`                          | `GET /config/all`                            | `GetAllConfigApiResponse` — full `SodaxConfig` bundle                         |
| `getChains()`                             | `GET /config/spoke/chains`                   | `GetChainsApiResponse` — array of supported `SpokeChainKey` strings           |
| `getSwapTokens()`                         | `GET /config/swap/tokens`                    | `GetSwapTokensApiResponse` — `Record<SpokeChainKey, readonly XToken[]>`       |
| `getSwapTokensByChainId(chainKey)`        | `GET /config/swap/{chainKey}/tokens`         | `GetSwapTokensByChainIdApiResponse`                                           |
| `getMoneyMarketTokens()`                  | `GET /config/money-market/tokens`            | `GetMoneyMarketTokensApiResponse`                                             |
| `getMoneyMarketTokensByChainId(chainKey)` | `GET /config/money-market/{chainKey}/tokens` | `GetMoneyMarketTokensByChainIdApiResponse`                                    |
| `getMoneyMarketReserveAssets()`           | `GET /config/money-market/reserve-assets`    | `GetMoneyMarketReserveAssetsApiResponse` — array of reserve `Address` strings |
| `getRelayChainIdMap()`                    | `GET /config/relay/chain-id-map`             | `GetRelayChainIdMapApiResponse` — `SpokeChainKey → relay chain ID` map        |
| `getSpokeChainConfig()`                   | `GET /config/spoke/all-chains-configs`       | `GetSpokeChainConfigApiResponse` — full `SpokeChainConfigMap`                 |

All methods accept an optional `RequestOverrideConfig` as their last argument and return `Promise<Result<T>>`.

## Utility Methods

### Set Custom Headers

Merges additional headers into the service's default header set. Useful for injecting authentication tokens or tracing headers at runtime without constructing a new service instance.

```typescript
sodax.backendApi.setHeaders({
  'Authorization': 'Bearer new-token',
  'X-Trace-Id': 'req-abc123',
});
```

### Get Base URL

Returns the base URL the service is currently pointing at.

```typescript
const baseURL = sodax.backendApi.getBaseURL();
console.log('API Base URL:', baseURL);
```

## Complete Example

```typescript
import { Sodax } from '@sodax/sdk';

async function example() {
  const sodax = new Sodax({
    api: {
      baseURL: 'https://api.sodax.com/v1/be',
      timeout: 60000,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        'Authorization': 'Bearer your-api-token',
      },
    },
  });

  // Get solver orderbook
  const orderbookResult = await sodax.backendApi.getOrderbook({ offset: '0', limit: '5' });
  if (!orderbookResult.ok) {
    console.error('Orderbook error:', orderbookResult.error.message);
    return;
  }
  console.log('Orderbook:', orderbookResult.value);

  // Get user's money market position
  const positionResult = await sodax.backendApi.getMoneyMarketPosition('0x789...ghi');
  if (!positionResult.ok) {
    console.error('Position error:', positionResult.error.message);
    return;
  }
  console.log('User Position:', positionResult.value);

  // Get all money market assets
  const assetsResult = await sodax.backendApi.getAllMoneyMarketAssets();
  if (!assetsResult.ok) {
    console.error('Assets error:', assetsResult.error.message);
    return;
  }
  console.log('Available Assets:', assetsResult.value);

  // Get intent by transaction hash
  const intentResult = await sodax.backendApi.getIntentByTxHash('0x123...abc');
  if (!intentResult.ok) {
    console.error('Intent error:', intentResult.error.message);
    if (intentResult.error instanceof Error && intentResult.error.cause) {
      console.error('Cause:', intentResult.error.cause);
    }
    return;
  }
  console.log('Intent Details:', intentResult.value);
}

example();
```

## Notes

* All string amounts in responses are in wei format (18 decimals) unless the specific field description says otherwise.
* Pagination parameters (`offset` and `limit`) are strings, not numbers.
* All endpoints return JSON responses.
* Error messages follow the CODE form (`SCREAMING_SNAKE_CASE`) for transport failures (`HTTP_REQUEST_FAILED`, `REQUEST_TIMEOUT`). Check `error.cause` for the underlying detail.
* `XToken.chainKey` is the field used on token objects to identify the chain (not `xChainId`).
* Chain constants are accessed via `ChainKeys.*` (e.g. `ChainKeys.ETHEREUM_MAINNET`), not legacy `*_CHAIN_ID` constants.


# Intent Relay API

> **Error handling conventions:** Failures from `submitTransaction` / `relayTxAndWaitPacket` follow the **relay-layer contract**: `error.message` is one of the literal strings exported as `RELAY_ERROR_CODES` (`'SUBMIT_TX_FAILED'`, `'RELAY_TIMEOUT'`). Modules other than swap (moneyMarket, bridge, dex, migration, staking) propagate these errors raw. The **swap module** wraps them into `SodaxError<SwapErrorCode>` with `context.relayCode` (see [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md) Error Handling).

The Intent Relay API Service provides functionality for submitting transactions and retrieving transaction packets across different chains. This service is part of the cross-chain communication infrastructure.

Source: `packages/sdk/src/shared/services/intentRelay/IntentRelayApiService.ts`

## Available Actions

1. `submit` — Submit a transaction to the intent relay service.
2. `get_transaction_packets` — Get all packets associated with a transaction.
3. `get_packet` — Get a specific packet by connection sequence number.

## Transaction Status Types

* `pending` — No signatures yet.
* `validating` — Not enough signatures collected.
* `executing` — Enough signatures collected, no confirmed destination tx hash yet.
* `executed` — Has a confirmed destination transaction hash.

## Chain IDs vs Chain Keys

The relay API uses its own numeric chain ID space (`IntentRelayChainId`) — bigint values defined in `RelayChainIdMap` — that is **distinct** from the `SpokeChainKey` string keys used everywhere else in the SDK. For example, Sonic's relay chain ID is `146n` while its chain key is `ChainKeys.SONIC_MAINNET`.

Use `getIntentRelayChainId(chainKey)` (from `@sodax/sdk`) to convert a `SpokeChainKey` to its `IntentRelayChainId`. The relay API wire format expects these IDs serialized as strings (call `.toString()` before passing them in `chain_id` fields).

`PacketData` fields `src_chain_id` and `dst_chain_id` are returned as `number` by the relay API; use `getChainKeyFromRelayChainId()` to convert back to a `SpokeChainKey` if needed.

## Result\<T> Return Types

All public functions in this module return `Promise<Result<T>>`:

```ts
type Result<T> = { ok: true; value: T } | { ok: false; error: Error | unknown };
```

On failure, check `result.error.message` for CODE-form errors such as `'SUBMIT_TX_FAILED'` or `'RELAY_TIMEOUT'`. Check `result.error.cause` for the underlying error when present. There are no typed error discriminators (`RelayError`, etc.) — those have been removed.

## High-Level Entry Point: `relayTxAndWaitPacket`

For most use cases, call `relayTxAndWaitPacket` rather than invoking `submitTransaction` / `waitUntilIntentExecuted` separately. It submits the transaction and polls until the relay packet reaches `'executed'` status.

```typescript
import { relayTxAndWaitPacket } from '@sodax/sdk';
import { ChainKeys } from '@sodax/sdk';

// relayData ({ address, payload }) comes from the preceding spoke operation,
// e.g. createIntent() or createBridgeIntent() returns { tx, relayData }.
// It is always required — pass it through even for non-Solana/Bitcoin chains.
const result = await relayTxAndWaitPacket({
  srcTxHash: '0x123...',
  data: relayData,          // RelayExtraData from the preceding spoke operation
  chainKey: ChainKeys.ETHEREUM_MAINNET,
  relayerApiEndpoint: 'https://api.example.com/relay' as HttpUrl,
  timeout: 120_000,         // optional; defaults to DEFAULT_RELAY_TX_TIMEOUT (120 s)
});

if (!result.ok) {
  if (result.error instanceof Error && result.error.message === 'RELAY_TIMEOUT') {
    // packet did not arrive within timeout
  }
  // handle other errors via result.error.message / result.error.cause
  return;
}

const packet = result.value; // PacketData — status === 'executed'
console.log('hub tx hash:', packet.dst_tx_hash);
```

**Solana and Bitcoin note:** These chains use split transactions — the on-chain tx carries only a verification hash, while the full call data is submitted off-chain via the relayer. Pass a `RelayExtraData` object as `data`:

```typescript
const result = await relayTxAndWaitPacket({
  srcTxHash: '5xL...',
  data: { address: '0xabc...', payload: '0xcafe...' }, // required for Solana/Bitcoin
  chainKey: ChainKeys.SOLANA_MAINNET,
  relayerApiEndpoint: 'https://api.example.com/relay' as HttpUrl,
  timeout: undefined, // use default
});
```

## Low-Level API Examples

### Submit Transaction

```typescript
import { submitTransaction } from '@sodax/sdk';

const request = {
  action: 'submit',
  params: {
    chain_id: '146',    // IntentRelayChainId as string — use getIntentRelayChainId(chainKey).toString()
    tx_hash: '0x123',
  },
} satisfies IntentRelayRequest<'submit'>;

const result = await submitTransaction(request, 'https://api.example.com/relay' as HttpUrl);

if (!result.ok) {
  // result.error.message === 'SUBMIT_TX_FAILED' on relay rejection
  // result.error.cause contains the underlying relay error message
  return;
}
// result.value: SubmitTxResponse
// { success: true, message: 'Transaction registered' }
```

### Get Transaction Packets

```typescript
import { getTransactionPackets } from '@sodax/sdk';

const request = {
  action: 'get_transaction_packets',
  params: {
    chain_id: '146',
    tx_hash: '0x123',
  },
} satisfies IntentRelayRequest<'get_transaction_packets'>;

const result = await getTransactionPackets(request, 'https://api.example.com/relay' as HttpUrl);

if (!result.ok) return;
// result.value: GetTransactionPacketsResponse
// {
//   "success": true,
//   "data": [
//     {
//       "src_chain_id": 6,
//       "src_tx_hash": "0x23a7eae34f6acf5cfadc43e714a4d188b0d6526b95c82c9b969e69d7222df5de",
//       "src_address": "a8e168789b1fa96de2fb816df56757ad950438a4",
//       "status": "executed",
//       "dst_chain_id": 146,
//       "conn_sn": 54,
//       "dst_address": "67a8cf2543a30b292a443430df213983951dca08",
//       "dst_tx_hash": "0xd7f1cf40154d3123eda3a94622bae13d879307fd3526cb45dd50951fee9cd244",
//       "signatures": [
//         "c172723dba3aec0f98d6602fcfbbcae9873ce3f4fc0eded70d64b6ad3f7806aa0b22d0fa3ea57679ec05f8c51a8562c9c979d247330966e9aaaf34a4dfae64e001"
//       ],
//       "payload": "cafebabe"
//     }
//   ]
// }
```

### Get Packet

```typescript
import { getPacket } from '@sodax/sdk';

const request = {
  action: 'get_packet',
  params: {
    chain_id: '146',
    tx_hash: '0x123...abc',
    conn_sn: '54',
  },
} satisfies IntentRelayRequest<'get_packet'>;

const result = await getPacket(request, 'https://api.example.com/relay' as HttpUrl);

if (!result.ok) return;
// result.value: GetPacketResponse
// On success:
// {
//   "success": true,
//   "data": {
//     "src_chain_id": 6,
//     "src_tx_hash": "0x781554a94bbd2ebd79ebaa01c645781ddf46610e5f1af8e5735d58b95ca6fbd6",
//     "src_address": "1d790ac96a0da4c249fd8838a7cc46b91fee3c5a",
//     "status": "executing",
//     "dst_chain_id": 21,
//     "dst_address": "0x26f83c5996f79229ef16cf7ca49eeb8682535e81ab59c30e561cc317bcc96a4a::sampledapp::...",
//     "conn_sn": 14,
//     "signatures": ["c172723dba3aec0f98d6602fcfbbcae9873ce3f4fc0eded70d64b6ad3f7806aa0b22d0fa3ea57679ec05f8c51a8562c9c979d247330966e9aaaf34a4dfae64e001"],
//     "payload": "cafebabe"
//   }
// }
```

## Type Definitions

All types are exported from `packages/sdk/src/shared/services/intentRelay/IntentRelayApiService.ts`.

```typescript
export type RelayTxStatus = 'pending' | 'validating' | 'executing' | 'executed';

export type RelayAction = 'submit' | 'get_transaction_packets' | 'get_packet';

// chain_id is always a string representation of an IntentRelayChainId bigint value
export type IntentRelayRequest<T extends RelayAction> = {
  action: T;
  params: T extends 'submit'
    ? { chain_id: string; tx_hash: string; data?: RelayExtraData }
    : T extends 'get_transaction_packets'
      ? { chain_id: string; tx_hash: string }
      : T extends 'get_packet'
        ? { chain_id: string; tx_hash: string; conn_sn: string }
        : never;
};

// Extra data required for Solana and Bitcoin split-tx chains
export type RelayExtraData = { address: Hex; payload: Hex };

export type PacketData = {
  src_chain_id: number;    // IntentRelayChainId as number (not a SpokeChainKey)
  src_tx_hash: string;
  src_address: string;
  status: RelayTxStatus;
  dst_chain_id: number;
  conn_sn: number;
  dst_address: string;
  dst_tx_hash: string;
  signatures: string[];
  payload: string;
};

export type SubmitTxResponse = {
  success: boolean;
  message: string;
};

export type GetTransactionPacketsResponse = {
  success: boolean;
  data: PacketData[];
};

export type GetPacketResponse =
  | { success: true; data: PacketData }
  | { success: false; message: string };

export type RelayAndWaitParams = {
  srcTxHash: string;
  data: RelayExtraData;         // always required; only used in submit payload for Solana/Bitcoin
  chainKey: SpokeChainKey;
  relayerApiEndpoint: HttpUrl;
  timeout: number | undefined;
};

export type IntentDeliveryInfo = {
  srcChainKey: SpokeChainKey;
  srcTxHash: string;
  srcAddress: string;
  dstChainKey: SpokeChainKey;
  dstTxHash: string;
  dstAddress: string;
};

export type WaitUntilIntentExecutedPayload = {
  intentRelayChainId: string;   // IntentRelayChainId serialized as string
  srcTxHash: string;
  timeout?: number;
  apiUrl: HttpUrl;
};
```


# @sodax/swaps-api

Minimal, type-safe HTTP client for the SODAX backend Swaps API v2 — the wire client that @sodax/sdk's sodax.api.swaps wraps.

Minimal, type-safe HTTP client for the SODAX backend **Swaps API v2**.

* Implements the `ISwapsApiV2` contract from `@sodax/types` over `fetch`.
* Validates every response at runtime with [valibot](https://valibot.dev), and transforms each chain-specific unsigned `tx` back to its domain shape (decimal-string → `bigint`, Injective index-object bytes → `Uint8Array`).
* Zero dependency on `@sodax/sdk`, viem, or wallet providers — only `@sodax/types` (types) and `valibot`.

It is the single source of the swaps wire client: `@sodax/sdk`'s `SwapsApiService` (`sodax.api.swaps`) is a thin adapter over this package, adding the SDK's `Result<T>` contract, logger, and config resolution on top. Use `@sodax/swaps-api` directly when you want just the swaps backend without pulling in the full SDK.

## Install

```bash
pnpm add @sodax/swaps-api valibot
```

## Usage

```ts
import { SwapsApi, SwapsApiError } from '@sodax/swaps-api';

const api = new SwapsApi({ baseUrl: 'https://<swaps-api-host>' });

const tokens = await api.getTokens();
const quote = await api.getQuote({
  tokenSrc,
  tokenSrcChainKey,
  tokenDst,
  tokenDstChainKey,
  amount,
  quoteType: 'exact_input',
});
```

`baseUrl` is required and injected by the caller — the package never hardcodes environment URLs. Optionally set `timeout` (ms — an overall per-call deadline that includes retries; on expiry the call throws `TIMEOUT_ERROR`), a custom `fetch` (for tests or non-standard runtimes; it receives the timeout `AbortSignal`), and extra `headers`.

## Errors

Every method **throws** a `SwapsApiError` on failure — a single typed error whose `code` is one of `NETWORK_ERROR` / `TIMEOUT_ERROR` / `HTTP_ERROR` / `PARSE_ERROR` / `VALIDATION_ERROR`, with diagnostic `context` (endpoint, method, path, HTTP status, validation issues) and the underlying failure on `.cause`. Idempotent calls (reads, polls, pure-compute POSTs like `getQuote`) are retried a few times on transient HTTP / network failures; a `timeout` and mutating calls are never retried.

> Note: this throwing contract is intentional and distinct from `@sodax/sdk`'s `sodax.api.swaps`, which wraps these calls and returns `Result<T>` instead of throwing.


# 2. The Connection Layer

Sitting above the core SDK, this package manages the complexity of connecting user wallets across heterogeneous chains (EVM, SVM, non-EVM).

<a href="https://docs.sodax.com/developers/packages/connection/wallet-sdk-core" class="button secondary" data-icon="wallet">Wallet SDK Core (Typescript)</a>

<a href="https://docs.sodax.com/developers/packages/connection/wallet-sdk-react" class="button secondary" data-icon="react">Wallet SDK React</a>


# @sodax/wallet-sdk-core

The Sodax wallet-sdk-core is a core wallet SDK package containing implementations of wallet providers that enable multi-chain wallet connectivity. This package provides TypeScript implementations of wallet providers for various blockchain networks, making them compatible with the Core Sodax SDK (@sodax/sdk).

> **AI-friendly docs:** shipped via [`@sodax/skills`](https://github.com/icon-project/sodax-sdks/tree/main/packages/skills) — [`skills` CLI](https://github.com/vercel-labs/skills) recommended; npm + `AGENTS.md` pointer as fallback. See [docs/ai-integration-guide.md](https://github.com/icon-project/sodax-sdks/blob/main/docs/ai-integration-guide.md) for all install modes and per-tool wiring.

## Installation

```bash
# Using pnpm (recommended)
pnpm add @sodax/wallet-sdk-core

# Using npm
npm install @sodax/wallet-sdk-core

# Using yarn
yarn add @sodax/wallet-sdk-core
```

## Features

* **Multi-chain Support**: Wallet provider implementations for multiple blockchain networks
* **TypeScript Compatibility**: Fully typed implementations compatible with @sodax/sdk
* **Wallet Provider Interface**: Standardized interface for wallet connectivity across different chains
* **Core Integration**: Seamless integration with the Core Sodax SDK

## Supported Wallet Providers

The package includes wallet provider implementations for:

* EVM-compatible chains ✅
* Solana ✅
* Sui ✅
* Stellar ✅
* ICON ✅
* Injective ✅
* Near ✅
* Stacks ✅
* Bitcoin ✅

## Public API surface

The package root exports:

* Wallet providers from `src/wallet-providers/*` (e.g. `EvmWalletProvider`, `SolanaWalletProvider`, `BitcoinWalletProvider`)
* `library-exports` from `src/types/library-exports.ts` (types and a few runtime values re-exported from upstream chain SDKs)

Internal utilities (e.g. `shallowMerge` in `src/utils/merge.ts`) are **not** exported from the package root.

## Config variants (private key vs browser/extension)

All providers support two modes, but **the union discriminant depends on the provider**:

* **Field presence (no `type` field in config)**: EVM, Solana, Sui, Near, Stacks, Icon, Injective
* **Explicit uppercase `type` field**: Bitcoin, Stellar (`'PRIVATE_KEY' | 'BROWSER_EXTENSION'`)

### EVM

```ts
import { EvmWalletProvider } from '@sodax/wallet-sdk-core';
import { ChainKeys } from '@sodax/types';

// Private key (Node/scripts/CI)
const evmPk = new EvmWalletProvider({
  privateKey: '0x...',
  chainId: ChainKeys.SONIC_MAINNET,
  rpcUrl: 'https://...',
  defaults: {
    sendTransaction: { gas: 3_000_000n },
  },
});

// Browser/extension (consumer supplies viem clients)
const evmBrowser = new EvmWalletProvider({
  walletClient: myViemWalletClient,
  publicClient: myViemPublicClient,
});
```

### Solana

```ts
import { SolanaWalletProvider } from '@sodax/wallet-sdk-core';

const solanaPk = new SolanaWalletProvider({
  privateKey: new Uint8Array([]),
  endpoint: 'https://api.mainnet-beta.solana.com',
});

const solanaBrowser = new SolanaWalletProvider({
  wallet: {
    publicKey: myPublicKeyOrNull,
    signTransaction: mySignTransactionOrUndefined,
  },
  endpoint: 'https://api.mainnet-beta.solana.com',
});
```

### Bitcoin

```ts
import { BitcoinWalletProvider } from '@sodax/wallet-sdk-core';

const btcPk = new BitcoinWalletProvider({
  type: 'PRIVATE_KEY',
  privateKey: '0x...',
  network: 'TESTNET',
  defaults: { defaultFinalize: true },
});

const btcBrowser = new BitcoinWalletProvider({
  type: 'BROWSER_EXTENSION',
  walletsKit: myWalletsKit,
  network: 'TESTNET',
});
```

### Sui

Sui uses `mnemonics` (not `privateKey`) for private-key mode. Browser extension requires a pre-constructed `SuiClient`, wallet object, and active `WalletAccount`.

```ts
import { SuiWalletProvider } from '@sodax/wallet-sdk-core';

// Private key (Node/scripts/CI) — field presence discriminant
const suiPk = new SuiWalletProvider({
  rpcUrl: 'https://...',
  mnemonics: '...',
});

// Browser/extension
const suiBrowser = new SuiWalletProvider({
  client: mySuiClient,
  wallet: myWalletWithFeatures,
  account: myWalletAccount,
});
```

### Stellar

Stellar uses an explicit uppercase `type` field (`'PRIVATE_KEY' | 'BROWSER_EXTENSION'`).

```ts
import { StellarWalletProvider } from '@sodax/wallet-sdk-core';

// Private key (Node/scripts/CI)
const stellarPk = new StellarWalletProvider({
  type: 'PRIVATE_KEY',
  privateKey: '0x...',
  network: 'PUBLIC',
  rpcUrl: 'https://...',
});

// Browser/extension
const stellarBrowser = new StellarWalletProvider({
  type: 'BROWSER_EXTENSION',
  walletsKit: myStellarWalletsKit,
  network: 'PUBLIC',
});
```

### Stacks

Stacks discriminates by field presence (no `type` field). The private-key config has `privateKey`; the browser-extension config has `address` (and optionally a `StacksProvider`).

```ts
import { StacksWalletProvider } from '@sodax/wallet-sdk-core';

// Private key (Node/scripts/CI)
const stacksPk = new StacksWalletProvider({
  privateKey: '...',
  endpoint: 'https://...',
});

// Browser/extension
const stacksBrowser = new StacksWalletProvider({
  address: 'SP...',
  endpoint: 'https://...',
  provider: myStacksProvider,
});
```

### ICON

ICON discriminates by field presence. The browser-extension config uses an optional `walletAddress` field (not a client object); `rpcUrl` is required in both modes.

```ts
import { IconWalletProvider } from '@sodax/wallet-sdk-core';

// Private key (Node/scripts/CI)
const iconPk = new IconWalletProvider({
  privateKey: '0x...',
  rpcUrl: 'https://...',
});

// Browser/extension (Hana wallet)
const iconBrowser = new IconWalletProvider({
  walletAddress: 'hx...',
  rpcUrl: 'https://...',
});
```

### Injective

Injective discriminates by field presence. The private-key config uses a nested `secret` object that accepts either `{ privateKey }` or `{ mnemonics }` — it is named `SecretInjectiveWalletConfig` rather than `PrivateKey*` to reflect this dual credential shape.

```ts
import { InjectiveWalletProvider } from '@sodax/wallet-sdk-core';

// Private key path — via secret credential
const injectivePk = new InjectiveWalletProvider({
  secret: { privateKey: '...' },
  chainId: myChainId,
  network: myNetwork,
});

// Mnemonics path — same config shape, different secret variant
const injectiveMnemonic = new InjectiveWalletProvider({
  secret: { mnemonics: '...' },
  chainId: myChainId,
  network: myNetwork,
});

// Browser/extension
const injectiveBrowser = new InjectiveWalletProvider({
  msgBroadcaster: myMsgBroadcaster,
});
```

### NEAR

NEAR discriminates by field presence. The private-key config requires `rpcUrl`, `accountId`, and `privateKey`; the browser-extension config wraps a `NearConnector`.

```ts
import { NearWalletProvider } from '@sodax/wallet-sdk-core';

// Private key (Node/scripts/CI)
const nearPk = new NearWalletProvider({
  rpcUrl: 'https://...',
  accountId: '...',
  privateKey: '...',
});

// Browser/extension
const nearBrowser = new NearWalletProvider({
  wallet: myNearConnector,
});
```

## Defaults and merge semantics

Each provider accepts optional `defaults`, and most methods accept per-call options. The SDK combines layers using a **shallow merge**:

* Only top-level keys are merged; **nested objects are replaced**, not deep-merged.
* `undefined` layers are skipped.
* `undefined` values inside a layer are skipped (so `{ field: undefined }` means “don’t override the previous layer”).

## `library-exports`

`library-exports` re-exports types (and a few runtime values) from underlying chain SDKs so consumers can reduce direct dependencies.

Example:

```ts
import type { WalletClient, PublicClient } from '@sodax/wallet-sdk-core';
```


# @sodax/wallet-sdk-react

React layer over [`@sodax/wallet-sdk-core`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-core/README.md) — wallet connection, signing, and account management for the SODAX cross-chain ecosystem. Hooks read from a single Zustand store; per-chain native SDKs (wagmi, `@solana/wallet-adapter`, `@mysten/dapp-kit`, …) are wrapped behind a uniform `IXService` / `IXConnector` interface.

## Features

* **Unified wallet connectivity** for 9 chain families across 21 chains
  * EVM (Sonic hub, Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) — EIP-6963 + WalletConnect
  * Solana, Sui, Stellar, ICON, Injective, Bitcoin, NEAR, Stacks
* **Single-store state** — `useXAccount`, `useXConnection`, `useXAccounts` all read the same Zustand slice; persisted to `localStorage`
* **Bridge to `@sodax/sdk`** — `useWalletProvider` returns a typed `IXxxWalletProvider` ready to plug into any SDK call
* **Headless wallet modal** — `useWalletModal` state machine (chainSelect → walletSelect → connecting → success | error), render-agnostic
* **Batch operations** — connect/disconnect every chain a wallet identifier covers, in sequence
* **WalletConnect** — opt-in for enterprise custody (Fireblocks, etc.) via `config.EVM.walletConnect`

## Installation

```bash
pnpm add @sodax/wallet-sdk-react
# or
npm install @sodax/wallet-sdk-react
# or
yarn add @sodax/wallet-sdk-react
```

## Peer dependencies

```json
{
  "react": ">=19",
  "@tanstack/react-query": "5.x"
}
```

## Quick start

```tsx
import {
  SodaxWalletProvider,
  type SodaxWalletConfig,
  useXAccount,
  useXConnect,
  useXConnectors,
} from '@sodax/wallet-sdk-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ChainKeys } from '@sodax/types';

const queryClient = new QueryClient();

const config: SodaxWalletConfig = {
  EVM: {
    ssr: true,
    chains: {
      [ChainKeys.SONIC_MAINNET]: { rpcUrl: 'https://rpc.soniclabs.com' },
      [ChainKeys.ETHEREUM_MAINNET]: { rpcUrl: 'https://ethereum-rpc.publicnode.com' },
    },
    // Optional: add WalletConnect support (requires wc projectId)
    // walletConnect: { projectId: '...' },
  },
  ICON: {
    chains: {
      [ChainKeys.ICON_MAINNET]: { rpcUrl: 'https://ctz.solidwallet.io/api/v3' },
    },
  },
};

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <SodaxWalletProvider config={config}>
        <WalletConnect />
      </SodaxWalletProvider>
    </QueryClientProvider>
  );
}

function WalletConnect() {
  const connectors = useXConnectors({ xChainType: 'EVM' });
  const { mutateAsync: connect } = useXConnect();
  const account = useXAccount({ xChainType: 'EVM' });

  if (account.address) {
    return <p>Connected: {account.address}</p>;
  }

  return (
    <div>
      {connectors.map(connector => (
        <button key={connector.id} onClick={() => connect(connector)}>
          {connector.icon && <img src={connector.icon} alt="" width={20} height={20} />}
          {connector.name}
        </button>
      ))}
    </div>
  );
}
```

## Documentation

The full guide lives in [`docs/`](https://github.com/icon-project/sodax-sdks/tree/main/packages/wallet-sdk-react/docs). Start with the topic that matches what you're building.

| Topic                                                                                                                                      | What it covers                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| [Configure SodaxWalletProvider](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONFIGURE_PROVIDER.md) | `SodaxWalletConfig` shape, opt-in chain mounting, per-chain RPC + wallet defaults, breaking changes from v1          |
| [Connect Flow](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md)                        | Discover connectors, connect, read account, disconnect; provider-managed vs non-provider chains; persisted reconnect |
| [Wallet Provider Bridge](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLET_PROVIDER_BRIDGE.md)    | `useWalletProvider` → typed `IXxxWalletProvider` for `@sodax/sdk` calls; `useXService` / `useXServices`              |
| [Wallet Modal](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLET_MODAL.md)                        | Headless state machine for multi-chain modal UIs; `useConnectionFlow` non-modal alternative                          |
| [WalletConnect](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLETCONNECT.md)                      | Enterprise custody integration (Fireblocks, Ledger); `qrModalOptions` filtering                                      |
| [Batch Operations](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/BATCH_OPERATIONS.md)                | Sequential multi-chain connect/disconnect by wallet identifier                                                       |
| [Chain Detection](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CHAIN_DETECTION.md)                  | `useChainGroups`, `useConnectedChains`, `useIsWalletInstalled`, `useEnabledChains`; hydration status                 |
| [Sign Message](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/SIGN_MESSAGE.md)                        | `useXSignMessage` cross-chain; Bitcoin BIP-322 vs ECDSA auto-detect                                                  |
| [EVM Switch Chain](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/EVM_SWITCH_CHAIN.md)                | Single wagmi connection across all configured EVM networks                                                           |
| [Connectors](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECTORS.md)                            | `IXConnector` contract, deep-import concrete classes, custom connectors                                              |
| [Architecture](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/ARCHITECTURE.md)                        | Zustand store, Provider/Hydrator/Actions trio, persist hydration caveat                                              |
| [Adding a New Chain](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/ADDING_A_NEW_CHAIN.md)            | `ChainMeta` extension, chain registry, sub-path export wiring                                                        |

## AI agent docs

AI-readable docs for `@sodax/wallet-sdk-react` (and the other `@sodax/*` packages) are shipped via [`@sodax/skills`](https://github.com/icon-project/sodax-sdks/tree/main/packages/skills) — a separate npm package bundling Claude-Code SKILL.md files and a long-form knowledge tree.

**Recommended:** [**`skills` CLI**](https://github.com/vercel-labs/skills) — from your project root:

```bash
npx skills@latest add icon-project/sodax-sdks/packages/skills
```

**npm + `AGENTS.md` pointer** (fallback for web chats, or when you prefer a devDependency over the CLI):

```bash
pnpm add -D @sodax/skills
```

Then point your agent at `node_modules/@sodax/skills/AGENTS.md`. See [docs/ai-integration-guide.md](https://github.com/icon-project/sodax-sdks/blob/main/docs/ai-integration-guide.md) for all install modes and per-tool wiring.

***

## Sub-path exports

Concrete connector / service classes are **not** exported from the package barrel — they live behind sub-path imports to prevent accidental coupling to internals:

```typescript
// ✅ Normal usage — barrel import
import { useXConnect, useXAccount, type IXConnector } from '@sodax/wallet-sdk-react';

// ✅ Advanced — concrete class via deep import
import { XverseXConnector } from '@sodax/wallet-sdk-react/xchains/bitcoin';
if (connector instanceof XverseXConnector) {
  connector.setAddressPurpose('payment');
}
```

See [Connectors](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECTORS.md) for the full list of deep-import sub-paths.

## Requirements

* Node.js >= 20.12.0
* React >= 19
* TypeScript

## Development

```bash
pnpm install     # Install dependencies
pnpm build       # Build the package (ESM + CJS, multi-entry)
pnpm dev         # Watch mode
pnpm checkTs     # Type checking
pnpm test        # Run tests
pnpm pretty      # Format code
pnpm lint        # Lint code
```

## Contributing

Contributions welcome — see the repo [Contributing Guide](https://github.com/icon-project/sodax-sdks/blob/main/CONTRIBUTING.md). For onboarding a new chain family, follow [`docs/ADDING_A_NEW_CHAIN.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/ADDING_A_NEW_CHAIN.md).

## License

[MIT](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/LICENSE)

## Support

* [GitHub Issues](https://github.com/icon-project/sodax-sdks/issues)
* [Discord Community](https://discord.gg/sodax-formerly-icon-880651922682560582)


# 3. The Experience Layer

The fastest way to build with SODAX. This is an opinionated collection of UI components, hooks, and utilities that leverages the layers below it.

<a href="https://docs.sodax.com/developers/packages/experience/dapp-kit" class="button secondary" data-icon="browser">dApp kit</a>


# @sodax/dapp-kit

High-level React hooks library for dApp developers. Wraps `@sodax/sdk` with React Query into feature-organized hooks. Used alongside `@sodax/wallet-sdk-react` (no direct dependency — shared types come from `@sodax/sdk`).

## Features

* **Swap/Intent** — `useQuote`, `useSwap`, `useSwapAllowance`, `useSwapApprove`, `useCancelSwap`, `useCreateLimitOrder`, `useCancelLimitOrder`, `useStatus`
* **Bridge** — `useBridge`, `useBridgeAllowance`, `useBridgeApprove`, `useGetBridgeableAmount`, `useGetBridgeableTokens`
* **Money Market** — `useSupply`, `useWithdraw`, `useBorrow`, `useRepay`, `useMMAllowance`, `useMMApprove`, plus reserves data hooks
* **Staking** — `useStake`, `useUnstake`, `useInstantUnstake`, `useClaim`, `useCancelUnstake`, approval hooks, info/config/ratio queries
* **DEX** — `useDexDeposit`, `useDexWithdraw`, `useSupplyLiquidity`, `useDecreaseLiquidity`, `useClaimRewards`, pool/position queries, param builders
* **Migration** — `useMigrateIcxToSoda`, `useRevertMigrateSodaToIcx`, `useMigratebnUSD`, `useMigrateBaln`, `useMigrationApprove`, `useMigrationAllowance`
* **Bitcoin (Bound Exchange)** — `useRadfiAuth`, `useRadfiSession`, `useTradingWallet`, `useTradingWalletBalance`, `useBitcoinBalance`, `useFundTradingWallet`, `useRadfiWithdraw`, `useExpiredUtxos`, `useRenewUtxos`
* **Partner** — `useFetchAssetsBalances`, `useGetAutoSwapPreferences`, `useIsTokenApproved`, `useApproveToken`, `useSetSwapPreference`, `useFeeClaimSwap`
* **Recovery** — `useHubAssetBalances`, `useWithdrawHubAsset`
* **Backend Queries** — Intent tracking, swap-tx submission + status, orderbook, money market position queries
* **Shared** — `useXBalances`, `useDeriveUserWalletAddress`, `useGetUserHubWalletAddress`, `useStellarTrustlineCheck`, `useRequestTrustline`, `useEstimateGas`

## Installation

```bash
pnpm add @sodax/dapp-kit @tanstack/react-query
# Optional: wallet connectivity
pnpm add @sodax/wallet-sdk-react
```

## Quick Start

### 1. Set up providers

RPC URLs are injected through `config.chains`. `SodaxProvider` is the outermost wrapper; `QueryClientProvider` wraps everything inside it.

```tsx
// providers.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { SodaxProvider, createSodaxQueryClient } from '@sodax/dapp-kit';
import { SodaxWalletProvider, type SodaxWalletConfig } from '@sodax/wallet-sdk-react';
import { ChainKeys, type SodaxOptions } from '@sodax/sdk';

const queryClient = createSodaxQueryClient();

const sodaxConfig: SodaxOptions = {
  chains: {
    [ChainKeys.SONIC_MAINNET]: { rpcUrl: 'https://sonic-rpc.publicnode.com' },
    [ChainKeys.BSC_MAINNET]: { rpcUrl: 'https://bsc-dataseed.binance.org' },
    [ChainKeys.BASE_MAINNET]: { rpcUrl: 'https://base.drpc.org' },
    [ChainKeys.ARBITRUM_MAINNET]: { rpcUrl: 'https://arb1.arbitrum.io/rpc' },
    // Add chains your dApp needs
  },
};

const walletConfig: SodaxWalletConfig = {
  EVM: {
    chains: {
      [ChainKeys.BSC_MAINNET]: { rpcUrl: 'https://bsc-dataseed.binance.org' },
      [ChainKeys.BASE_MAINNET]: { rpcUrl: 'https://base.drpc.org' },
    },
  },
};

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SodaxProvider config={sodaxConfig}>
      <QueryClientProvider client={queryClient}>
        <SodaxWalletProvider config={walletConfig}>
          {children}
        </SodaxWalletProvider>
      </QueryClientProvider>
    </SodaxProvider>
  );
}
```

### 2. Get a wallet provider

```tsx
import { useWalletProvider } from '@sodax/wallet-sdk-react';
import { ChainKeys } from '@sodax/sdk';

function MyFeature() {
  const walletProvider = useWalletProvider(ChainKeys.BSC_MAINNET);
  // undefined until wallet is connected
}
```

### 3. Use feature hooks

All mutation hooks accept no arguments at initialization. Domain inputs (params, walletProvider) flow through `mutate(vars)`:

```tsx
import { useSwap } from '@sodax/dapp-kit';
import { useWalletProvider } from '@sodax/wallet-sdk-react';
import { ChainKeys } from '@sodax/sdk';
import type { CreateIntentParams } from '@sodax/sdk';

function SwapButton({ intentParams }: { intentParams: CreateIntentParams }) {
  const walletProvider = useWalletProvider(ChainKeys.BSC_MAINNET);
  const { mutateAsyncSafe: swap, isPending } = useSwap();

  const handleSwap = async () => {
    if (!walletProvider) return;
    const result = await swap({ params: intentParams, walletProvider });
    if (!result.ok) {
      alert(result.error instanceof Error ? result.error.message : 'Swap failed');
      return;
    }
    console.log('Swap submitted!', result.value);
  };

  return (
    <button onClick={handleSwap} disabled={isPending || !walletProvider}>
      {isPending ? 'Swapping...' : 'Swap'}
    </button>
  );
}
```

## Requirements

* Node.js >= 20.12.0
* React >= 18
* TypeScript

## API Reference

### Provider

* [`SodaxProvider`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/providers/SodaxProvider.tsx) — Wraps your app, creates the `Sodax` SDK instance. Accepts `config?: SodaxOptions`.
* [`createSodaxQueryClient()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/providers/createSodaxQueryClient.ts) — Factory for a `QueryClient` with global mutation observability (`onMutationError` hook, `meta.silent` opt-out).

### Swap Hooks

* [`useQuote()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useQuote.ts) — Real-time quote (auto-refreshes every 3s)
* [`useSwap()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useSwap.ts) — Submit a cross-chain swap intent
* [`useSwapAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useSwapAllowance.ts) — Check token approval
* [`useSwapApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useSwapApprove.ts) — Approve token spending
* [`useStatus()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useStatus.ts) — Track intent execution status
* [`useCancelSwap()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useCancelSwap.ts) — Cancel a pending swap
* [`useCreateLimitOrder()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useCreateLimitOrder.ts) — Create a limit order (no deadline)
* [`useCancelLimitOrder()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swap/useCancelLimitOrder.ts) — Cancel a limit order

### Money Market Hooks

* [`useSupply()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useSupply.ts) — Supply collateral
* [`useWithdraw()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useWithdraw.ts) — Withdraw supplied tokens
* [`useBorrow()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useBorrow.ts) — Borrow against collateral
* [`useRepay()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useRepay.ts) — Repay borrowed tokens
* [`useMMAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useMMAllowance.ts) — Check approval (auto-skips for borrow/withdraw)
* [`useMMApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useMMApprove.ts) — Approve token spending
* [`useReservesData()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useReservesData.ts) — All reserve data
* [`useReservesHumanized()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useReservesHumanized.ts) — Reserves in decimal-normalized format
* [`useReservesList()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useReservesList.ts) — List of reserve asset addresses
* [`useReservesUsdFormat()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useReservesUsdFormat.ts) — Reserves with USD values
* [`useUserFormattedSummary()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useUserFormattedSummary.ts) — User portfolio summary (health factor, collateral, debt)
* [`useUserReservesData()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useUserReservesData.ts) — User reserve positions
* [`useAToken()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useAToken.ts) — aToken metadata
* [`useATokensBalances()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/mm/useATokensBalances.ts) — aToken balances

### Bridge Hooks

* [`useBridge()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bridge/useBridge.ts) — Execute a cross-chain bridge transfer
* [`useBridgeAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bridge/useBridgeAllowance.ts) — Check token approval
* [`useBridgeApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bridge/useBridgeApprove.ts) — Approve token spending
* [`useGetBridgeableAmount()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bridge/useGetBridgeableAmount.ts) — Max bridgeable amount between two tokens
* [`useGetBridgeableTokens()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bridge/useGetBridgeableTokens.ts) — Tokens bridgeable to a destination chain

### Staking Hooks

* [`useStake()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useStake.ts) — Stake SODA, receive xSODA
* [`useUnstake()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useUnstake.ts) — Request unstake (waiting period)
* [`useInstantUnstake()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useInstantUnstake.ts) — Instant unstake with slippage
* [`useClaim()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useClaim.ts) — Claim SODA after waiting period
* [`useCancelUnstake()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useCancelUnstake.ts) — Cancel pending unstake
* [`useStakeApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useStakeApprove.ts) — Approve SODA for staking
* [`useUnstakeApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useUnstakeApprove.ts) — Approve xSODA for unstaking
* [`useInstantUnstakeApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useInstantUnstakeApprove.ts) — Approve xSODA for instant unstaking
* [`useStakeAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useStakeAllowance.ts) — Check SODA approval
* [`useUnstakeAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useUnstakeAllowance.ts) — Check xSODA approval for unstaking
* [`useInstantUnstakeAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useInstantUnstakeAllowance.ts) — Check xSODA approval for instant unstaking
* [`useStakingInfo()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useStakingInfo.ts) — User staking position
* [`useUnstakingInfo()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useUnstakingInfo.ts) — Pending unstake requests
* [`useUnstakingInfoWithPenalty()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useUnstakingInfoWithPenalty.ts) — Unstake requests with penalty calcs
* [`useStakingConfig()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useStakingConfig.ts) — Unstaking period, max penalty
* [`useStakeRatio()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useStakeRatio.ts) — SODA-to-xSODA exchange rate
* [`useInstantUnstakeRatio()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useInstantUnstakeRatio.ts) — Instant unstake rate
* [`useConvertedAssets()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/staking/useConvertedAssets.ts) — xSODA to SODA conversion

### DEX Hooks

* [`usePools()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/usePools.ts) — List available pools
* [`usePoolData()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/usePoolData.ts) — Pool details (price, tick, liquidity)
* [`usePoolBalances()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/usePoolBalances.ts) — User pool token balances
* [`usePositionInfo()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/usePositionInfo.ts) — Position details by token ID
* [`useDexDeposit()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useDexDeposit.ts) — Deposit assets into pool tokens
* [`useDexWithdraw()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useDexWithdraw.ts) — Withdraw assets from pool tokens
* [`useDexAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useDexAllowance.ts) — Check approval for deposit
* [`useDexApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useDexApprove.ts) — Approve token spending
* [`useLiquidityAmounts()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useLiquidityAmounts.ts) — Token amounts for a tick range
* [`useSupplyLiquidity()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useSupplyLiquidity.ts) — Supply liquidity to a position
* [`useDecreaseLiquidity()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useDecreaseLiquidity.ts) — Remove liquidity
* [`useClaimRewards()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useClaimRewards.ts) — Claim trading fees
* [`useCreateDepositParams()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useCreateDepositParams.ts) — Build deposit params with ERC-4626 conversion
* [`useCreateWithdrawParams()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useCreateWithdrawParams.ts) — Build withdraw params
* [`useCreateSupplyLiquidityParams()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useCreateSupplyLiquidityParams.ts) — Build tick range + liquidity params
* [`useCreateDecreaseLiquidityParams()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/dex/useCreateDecreaseLiquidityParams.ts) — Build decrease params from position state

### Migration Hooks

* [`useMigrateIcxToSoda()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/migrate/useMigrateIcxToSoda.ts) — ICX/wICX (ICON) → SODA (Sonic)
* [`useRevertMigrateSodaToIcx()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/migrate/useRevertMigrateSodaToIcx.ts) — SODA (Sonic) → wICX (ICON)
* [`useMigratebnUSD()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/migrate/useMigratebnUSD.ts) — Legacy bnUSD ↔ new bnUSD (bidirectional)
* [`useMigrateBaln()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/migrate/useMigrateBaln.ts) — BALN (ICON) → SODA with optional lock period
* [`useMigrationApprove()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/migrate/useMigrationApprove.ts) — Approve token spending before migration
* [`useMigrationAllowance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/migrate/useMigrationAllowance.ts) — Check if approval is needed

### Bitcoin (Bound Exchange) Hooks

* [`useRadfiSession()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useRadfiSession.ts) — Manage Bound Exchange session (login, auto-refresh)
* [`useRadfiAuth()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useRadfiAuth.ts) — Authenticate with Bound Exchange via BIP322 signing
* [`useTradingWallet()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useTradingWallet.ts) — Get trading wallet address from persisted session
* [`useBitcoinBalance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useBitcoinBalance.ts) — BTC balance for any address
* [`useTradingWalletBalance()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useTradingWalletBalance.ts) — Trading wallet balance from Bound Exchange API
* [`useFundTradingWallet()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useFundTradingWallet.ts) — Fund trading wallet from personal wallet
* [`useRadfiWithdraw()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useRadfiWithdraw.ts) — Withdraw from trading wallet
* [`useExpiredUtxos()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useExpiredUtxos.ts) — Fetch expired UTXOs (polls every 60s)
* [`useRenewUtxos()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/bitcoin/useRenewUtxos.ts) — Renew expired UTXOs

### Partner Hooks

* [`useFetchAssetsBalances()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/partner/useFetchAssetsBalances.ts) — Fetch partner asset balances
* [`useGetAutoSwapPreferences()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/partner/useGetAutoSwapPreferences.ts) — Get auto-swap preferences
* [`useIsTokenApproved()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/partner/useIsTokenApproved.ts) — Check token approval
* [`useApproveToken()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/partner/useApproveToken.ts) — Approve token
* [`useSetSwapPreference()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/partner/useSetSwapPreference.ts) — Set swap preference
* [`useFeeClaimSwap()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/partner/useFeeClaimSwap.ts) — Claim partner fees via swap

### Recovery Hooks

* [`useHubAssetBalances()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/recovery/useHubAssetBalances.ts) — Get hub asset balances
* [`useWithdrawHubAsset()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/recovery/useWithdrawHubAsset.ts) — Withdraw hub asset

### Shared Hooks

* [`useSodaxContext()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useSodaxContext.ts) — Access the `Sodax` SDK instance
* [`useHubProvider()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/provider/useHubProvider.ts) — Access the hub chain (Sonic) provider
* [`useXBalances()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useXBalances.ts) — Cross-chain token balances for an address
* [`useDeriveUserWalletAddress()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useDeriveUserWalletAddress.ts) — Derive hub wallet address (CREATE3)
* [`useGetUserHubWalletAddress()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useGetUserHubWalletAddress.ts) — Derive hub wallet address (wallet router)
* [`useEstimateGas()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useEstimateGas.ts) — Estimate gas for transactions
* [`useStellarTrustlineCheck()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useStellarTrustlineCheck.ts) — Check Stellar trustline
* [`useRequestTrustline()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/shared/useRequestTrustline.ts) — Request Stellar trustline

### Backend Query Hooks

* [`useBackendIntentByTxHash()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendIntentByTxHash.ts) — Get intent by hub tx hash (polls 1s)
* [`useBackendIntentByHash()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendIntentByHash.ts) — Get intent by intent hash
* [`useBackendUserIntents()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendUserIntents.ts) — All intents for a user with date filtering
* [`useBackendOrderbook()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendOrderbook.ts) — Solver orderbook (cached 30s, no auto-refetch)
* [`useBackendMoneyMarketPosition()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendMoneyMarketPosition.ts) — User money market position
* [`useBackendAllMoneyMarketAssets()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendAllMoneyMarketAssets.ts) — All MM assets
* [`useBackendMoneyMarketAsset()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendMoneyMarketAsset.ts) — Single MM asset details
* [`useBackendMoneyMarketAssetBorrowers()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendMoneyMarketAssetBorrowers.ts) — Asset borrowers
* [`useBackendMoneyMarketAssetSuppliers()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendMoneyMarketAssetSuppliers.ts) — Asset suppliers
* [`useBackendAllMoneyMarketBorrowers()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/backend/useBackendAllMoneyMarketBorrowers.ts) — All borrowers

### Swaps API Hooks (`sodax.api.swaps`)

Typed wrappers over the backend Swaps API v2 — one `useSwapsApi*` hook per endpoint (21 total). Highlights:

* [`useSwapsApiQuote()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swapsApi/useSwapsApiQuote.ts) — Solver quote for a cross-chain swap
* [`useSwapsApiCreateIntent()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swapsApi/useSwapsApiCreateIntent.ts) — Build an unsigned create-intent tx
* [`useSwapsApiSubmitTx()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swapsApi/useSwapsApiSubmitTx.ts) — Submit swap tx to backend
* [`useSwapsApiSubmitTxStatus()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/hooks/swapsApi/useSwapsApiSubmitTxStatus.ts) — Check submitted swap status

See [`src/hooks/swapsApi/`](https://github.com/icon-project/sodax-sdks/tree/main/packages/dapp-kit/src/hooks/swapsApi) for the full set (tokens, deadline, allowance, approve, submit/cancel intent, status, hash, packet, extra-data, intent lookups, limit orders, gas, fees).

### DEX Utils

* [`createDepositParamsProps()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/utils/dex-utils.ts) — Build deposit params from pool data and spoke asset info
* [`createWithdrawParamsProps()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/utils/dex-utils.ts) — Build withdraw params with optional destination info
* [`createSupplyLiquidityParamsProps()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/utils/dex-utils.ts) — Build concentrated liquidity supply params
* [`createDecreaseLiquidityParamsProps()`](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/src/utils/dex-utils.ts) — Build decrease liquidity params

## Development

```bash
pnpm install     # Install dependencies
pnpm build       # Build the package (ESM + CJS)
pnpm dev         # Watch mode
pnpm checkTs     # Type checking
pnpm test        # Run tests
pnpm pretty      # Format code
pnpm lint        # Lint code
```

## AI agent docs

AI-readable docs for `@sodax/dapp-kit` (and the other `@sodax/*` packages) are shipped via [`@sodax/skills`](https://github.com/icon-project/sodax-sdks/tree/main/packages/skills) — a separate npm package bundling Claude-Code SKILL.md files and a long-form knowledge tree.

**Recommended:** [**`skills` CLI**](https://github.com/vercel-labs/skills) — from your project root:

```bash
npx skills@latest add icon-project/sodax-sdks/packages/skills
```

**npm + `AGENTS.md` pointer** (fallback for web chats, or when you prefer a devDependency over the CLI):

```bash
pnpm add -D @sodax/skills
```

Then point your agent at `node_modules/@sodax/skills/AGENTS.md`. See [docs/ai-integration-guide.md](https://github.com/icon-project/sodax-sdks/blob/main/docs/ai-integration-guide.md) for all install modes and per-tool wiring.

## License

[MIT](https://github.com/icon-project/sodax-sdks/blob/main/packages/dapp-kit/LICENSE)

## Support

* [GitHub Issues](https://github.com/icon-project/sodax-sdks/issues)
* [Discord Community](https://discord.gg/sodax-formerly-icon-880651922682560582)


# @sodax/skills

Consumer-facing AI skills and knowledge so coding agents (Claude Code, Cursor, Copilot, Codex) write v2-correct @sodax/\* SDK code.

AI-agent skills and knowledge for building on the **SODAX** cross-chain DeFi platform. Drop this into your repo and your AI coding agent writes v2-correct `@sodax/*` SDK code on the first try.

**Full setup** (skills CLI, npm, monorepo/local install, wiring agents to `AGENTS.md`): [docs/ai-integration-guide.md](https://github.com/icon-project/sodax-sdks/blob/main/docs/ai-integration-guide.md).

## Install

Using the [`skills` CLI](https://github.com/vercel-labs/skills) from Vercel Labs — the open agent-skills ecosystem CLI (supports Claude Code, Cursor, Codex, GitHub Copilot, and 50+ other agents):

```bash
# From the root of your consumer repo
npx skills@latest add icon-project/sodax-sdks/packages/skills
```

The installable skills listed in `.claude-plugin/plugin.json` land in your repo — the `sodax-build` front-door / ideation skill plus the mode-gated per-SDK-package skills (each with two knowledge subtrees: `integration/` for new v2 code, `migration-v1-to-v2/` for v1→v2 porting) — alongside a router `AGENTS.md`. Re-running the command picks up the latest content.

> **npm fallback** (web chats or when you prefer a devDependency): `pnpm add -D @sodax/skills`, then point your agent at `node_modules/@sodax/skills/AGENTS.md`. See the [integration guide](https://github.com/icon-project/sodax-sdks/blob/main/docs/ai-integration-guide.md#wire-your-agent).

## What you get

| Bundle                                                                                            | Contains                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Front-door skill** at `skills/sodax-build/SKILL.md`                                             | A cross-cutting ideation skill for when you don't know what to build yet. It interviews you, turns the idea into a product brief, and hands off to the right developer skill(s). Writes no app code; flat `knowledge/` tree (no integration/migration split).                                                                                                                                                                                                                                                                                                                                                                                |
| **Mode-gated per-SDK-package skills** under `skills/sodax-<pkg>/SKILL.md`                         | One skill per SODAX SDK package. `<pkg>` ∈ `sdk`, `wallet-sdk-core`, `wallet-sdk-react`, `dapp-kit`. Each SKILL.md gates by mode (integration vs migration) at the top of the body.                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| **Granular skills** bundled inside each broad skill at `skills/sodax-<pkg>/<sub-domain>/SKILL.md` | Every broad skill ships focused single-domain children: `sodax-sdk` / `sodax-dapp-kit` per feature (swap, money-market, bridge, staking, dex, …); `sodax-wallet-sdk-core` per chain (evm, solana, sui, bitcoin, stellar, icon, injective, near, stacks); `sodax-wallet-sdk-react` per connectivity concern (connect, wallet-modal, bridge-to-sdk, switch-chain, sign-message, walletconnect). They install **with** their parent broad skill (not as separate packages); once installed, load one when the task is already scoped to a single sub-domain — it points at exactly the knowledge files for it instead of the whole broad skill. |
| **Knowledge** under `skills/sodax-<pkg>/{integration,migration-v1-to-v2}/knowledge/`              | Long-form supporting docs — features, recipes, reference tables, breaking-change writeups, code examples. Each skill ships both mode subtrees so `npx skills add` copies the full reference together.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| **`AGENTS.md`** at the package root                                                               | Tool-neutral router: maps the consumer's stated task to the right skill + mode.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

Skills are short and action-oriented (workflow + anti-patterns + links). Knowledge is the lookup material. Don't read knowledge files top-to-bottom — the skill tells the agent which file is relevant for the current task.

## Which skill applies?

After install, your agent picks based on what you're building. Quick guide:

| You're building                                    | Load these skills (mode)                                                                                                                  |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Not sure what to build yet / not a developer       | `sodax-build` — it interviews you, writes a product brief, and names the skill to load next                                               |
| Backend / Node app (no React) using `@sodax/sdk`   | `sodax-sdk` (integration) + `sodax-wallet-sdk-core` (integration; if signing)                                                             |
| React dapp using `@sodax/dapp-kit`                 | `sodax-dapp-kit` (integration) + `sodax-wallet-sdk-react` (integration; always) + `sodax-sdk` (integration; for any unwrapped operations) |
| React app calling the SDK directly (no `dapp-kit`) | `sodax-sdk` (integration) + `sodax-wallet-sdk-react` (integration)                                                                        |
| **Porting v1 code**                                | Same skills, switched to migration mode (each SKILL.md mode-gates by consumer signal).                                                    |

`AGENTS.md` says the same thing in router form — your agent reads it first and picks.

## Why this exists

LLM training data drifts: snippets from chat often use stale method names, reshaped types, or outdated error codes. Public docs help humans, not agents — an agent only reads what's in its context window. This package ships the right material in agent-native form so the agent reads it before generating code. The content is version-locked to the SDK — upgrade `@sodax/skills`, the docs upgrade with it.

## Feedback

If your agent generates wrong code despite reading the docs, that's a doc bug — please open an issue on the [Sodax SDKs repo](https://github.com/icon-project/sodax-sdks/issues) with the prompt and the incorrect output. The per-skill `knowledge/` subtrees are structurally CI-guarded (frontmatter, link resolution); prose claims benefit from real-world feedback.


# Technical Overview

### Overview

This repository contains ICON's crosschain infrastructure and dapps built on it.

<figure><img src="/files/BNMbWU2KhgR2FOC4itKH" alt=""><figcaption></figcaption></figure>

### Infra Goals

* Any EVM dapp should be able to access any user on any connected chain without changes
* Any EVM dapp should be able to access any token on any connected chain without changes
* Any token should be able to be bridged to any other connected chain
* Crosschain messaging should be simple and reliable with little overhead

### App Goals

* Build a money market such that we can delegate liquidity to many different products and services
* Establish a new HUB for current ICON tokens

### Bridging

#### **AssetManager**

The **AssetManager** serves two primary functions:

1. **Value Transfer**: It facilitates the transfer of assets between different blockchains, enabling users to move value seamlessly across ecosystems.
2. **Hooks**: The bridge can act on behalf of the caller using the transferred tokens, allowing for complex, multi-chain user interactions and actions.

For more detailed specifications regarding the **AssetManager**, please refer to the full documentation [here](/developers/technical-overview/asset-manager).

#### **Hub Token Management**

With crosschain dapps there is usually lot of fragmentation. Same token represented on many chains or tokens priced very closely together such as wBTC and tBTC.\
So we need contract and logic to unify this fragmentation in a composable way. This can be done by a Tokenized Vault built to comply with the ICON crosschain token standard. Giving us full flexibility in solving liquidity fragmentation while also being able to take any token crosschain.

<figure><img src="/files/OdjhZyu4wr4AbqCGzHiR" alt=""><figcaption></figcaption></figure>

For more detailed specifications regarding the **Hub Token Management**, please refer to the full documentation [here](/developers/technical-overview/vault-token).

### Wallet Abstraction

The Wallet abstraction module's goal is to map each user on each chain to a wallet on the HUB. Making it a default entry point for users interacting from different ecosystems. This also gives us a flexible tool to integrate other interop solutions or bridges to use in our ecosystem, by giving them access to integrate into the wallet implementation.

For more detailed specifications regarding the **Wallet Abstraction**, please refer to the full documentation [here](/developers/technical-overview/hub-wallet-abstraction).

### Intents

The **Intent** infrastructure is another key component of ICON's crosschain capabilities. Built on top of GMP, the Intent system resolves crosschain intents efficiently, enabling swapping and solving across any chain.

<figure><img src="/files/ykr0CGWTIFw5sRuNmSGx" alt=""><figcaption></figcaption></figure>

For more detailed specifications regarding the **Intents**, please refer to the full documentation [here](/developers/technical-overview/intents).

### GMP (General Messaging Protocol)

The General Messaging Protocol (GMP) is a fundamental component designed to enable seamless communication between different blockchains and decentralized systems. Its primary objective is to maintain simplicity while offering maximum flexibility for building upon it.

The message structure in GMP contains all essential data:

* **srcChainId**: The identifier of the source blockchain
* **dstChainId**: The identifier of the destination blockchain
* **fromAddress**: The address from which the message is being sent
* **toAddress**: The address to which the message is being sent
* **data**: The payload or data being transferred between the two chains

#### Connection

The **Connection** verifies incoming messages and acts as a gateway for chains to send messages to any other connected chain. For more detailed specifications, please refer to the full documentation [here](/developers/technical-overview/generalized-messaging-protocol).

#### Relayer

The **Relayer** verifies and delivers messages across blockchains, confirming events and providing necessary signatures.


# Asset Manager

### Overview

This document outlines the design for a hub-and-spoke asset management system that enables cross-network asset transfers with optional execution of additional logic. The system consists of a central Hub Asset Manager (in Solidity) and multiple Spoke Asset Managers that can be implemented in any language/platform.

***

## Hub Asset Manager

### System Design

#### Core Components

The hub asset manager maintains several key components:

* **Connection Module**: Handles cross-chain message verification
* **Wallet Factory**: Generates deterministic user wallets on the hub chain
* **Asset Tokens**: Manages wrapped versions of spoke chain assets
* **Network Mappings**: Maps spoke chain asset managers and their assets

#### State Variables

```solidity:evm/contracts/assetmanager/assetmanager.sol
IConnection public connection;                    // Cross-chain messaging
IWalletFactory public walletFactory;             // User wallet management
address public assetImplementation;              // Asset token implementation
mapping(uint256 => bytes) public spokeAssetManager;  // Authorized spoke managers
mapping(uint256 => mapping(bytes => address)) public assets;  // Spoke assets to hub tokens
```

#### Transfer Data Structure

```solidity
struct Transfer {
    bytes token;      // Original token address on source chain
    bytes from;       // Sender address on source chain
    bytes to;         // Recipient address on destination chain
    uint256 amount;   // Amount to transfer
    bytes data;       // Optional execution data
}
```

#### Core Operations

1. **Receiving Cross-Chain Transfers**

```solidity:evm/contracts/assetmanager/assetmanager.sol
function recvMessage(
    uint256 nonce,
    uint256 srcChainId,
    bytes calldata srcAddress,
    bytes memory payload,
    bytes[] memory signatures
) external {
    // Verify the sender is authorized
    require(keccak256(srcAddress) == keccak256(spokeAssetManager[srcChainId]));
    
    // Verify cross-chain message
    connection.verifyMessage(srcChainId, srcAddress, payload, signatures);

    Transfer memory _transfer = payload.decode();

    // Mint wrapped tokens
    address token = assets[srcChainId][_transfer.token];
    IAssetToken(token).mintTo(_transfer.to.toAddress(), _transfer.amount);

    // Execute optional actions through user's wallet
    if (_transfer.data.length > 0) {
        address wallet = walletFactory.getWallet(srcChainId, _transfer.from);
        IWallet(wallet).assetManagerHook(_transfer.data);
    }
}
```

2. **Initiating Cross-Chain Transfers**

```solidity:evm/contracts/assetmanager/assetmanager.sol
function transfer(
    address token,
    bytes calldata to,
    uint256 amount,
    bytes calldata data
) external payable {
    // Burn wrapped tokens on hub
    AssetInfo memory info = assetInfo[token];
    IAssetToken(token).burnFrom(msg.sender, amount);

    // Construct transfer message
    Transfer memory _transfer = Transfer(
        info.spokeAddress,
        msg.sender.toBytes(),
        to,
        amount,
        data
    );

    // Send message to spoke chain
    bytes memory manager = spokeAssetManager[info.chainID];
    connection.sendMessage{value:msg.value}(
        info.chainID,
        manager,
        _transfer.encode()
    );
}
```

***

## Spoke Asset Manager Specification

### Required Implementation

Spoke chains must implement an asset manager that can:

1. Receive and verify cross-chain messages from the hub
2. Lock/unlock native assets
3. Send properly formatted messages to the hub
4. Enforce rate limits on withdrawals

#### Required Functions

1. **Receive Message**

```typescript
// Implementation language may vary
function recvMessage(
    sourceChainId: number,
    sourceAddress: Bytes,
    connSn: number,
    payload: Bytes,
    signatures: Bytes[]
): void {
    // 1. Verify message is from hub
    // 2. Decode transfer data
    // 3. Verify withdrawal is within rate limits
    // 4. Release locked tokens to recipient
    // 5. Execute any additional logic in data
}
```

2. **Send Transfer**

```typescript
// Implementation language may vary
function transfer(
    token: Address,
    recipient: Bytes,
    amount: BigInt,
    data: Bytes
): void {
    // 1. Lock tokens
    // 2. Construct transfer message
    // 3. Send message to hub
}
```

### Security Considerations

1. **Hub Chain**

* Only authorized spoke managers can send messages
* Asset tokens must be properly wrapped/unwrapped
* Wallet hooks must be properly validated

2. **Spoke Chains**

* Must verify messages come from hub
* Must properly lock/unlock native assets
* Must implement proper access controls
* Must enforce rate limits on withdrawals
* Must use consistent function naming (recvMessage)

3. **Cross-Chain**

* Asset mappings must be carefully managed
* Withdrawal limits must be properly configured and enforced


# Vault Token

### Overview

The Vault Token is designed to wrap multiple variants of the same underlying asset (e.g., USDC from different chains) into a single, unified token.

### Core Features

1. **Multi-Asset Support**
   * Supports multiple underlying tokens
   * Configurable deposit limits per token
   * Decimal normalization for different token standards
   * Fee mechanisms for deposits and withdrawals
2. **Rate Limiting**
   * Built-in rate limiting for all transfers
   * Consults rate limiter for mints, burns, and transfers
   * Ensures controlled token movement

### Token Configuration

#### Token Info Structure

```solidity
struct TokenInfo {
    uint8 decimals;      // Decimals of the underlying token
    uint256 depositFee;  // Fee in basis points (e.g., 50 = 0.5%)
    uint256 withdrawalFee; // Fee in basis points
    uint256 maxDeposit;  // Maximum allowable deposit
    bool isSupported;    // Whether the token is supported
}
```

#### Core Operations

1. **Deposits**

```solidity
function deposit(address token, uint256 amount) external {
    // 1. Verify token is supported
    // 2. Check deposit limits
    // 3. Transfer underlying token to vault
    // 4. Apply deposit fee if configured
    // 5. Mint vault tokens to user
}
```

2. **Withdrawals**

```solidity
function withdraw(address token, uint256 amount) external {
    // 1. Burn vault tokens from user
    // 2. Apply withdrawal fee if configured
    // 3. Transfer underlying token to user
}
```

3. **Rate-Limited Transfers**

```solidity
function _update(address from, address to, uint256 value) internal override {
    // Consult rate limiter before any transfer
    rateLimits.update(from, to, value, totalSupply());
    super._update(from, to, value);
}
```

### Security Features

1. **Deposit Controls**
   * Maximum deposit limits per token
   * Rate limiting on all transfers
   * Decimal normalization for safety
2. **Access Control**
   * Owner-controlled token configuration
   * Protected fee recipient updates
   * Restricted token support management
3. **Fee Management**
   * Configurable deposit/withdrawal fees
   * Protected fee collection mechanism
   * Fee recipient management

### Use Cases

1. **Asset Unification**
   * Wrap USDC from multiple chains into a single vault token
   * Simplify cross-chain asset management
   * Enable unified liquidity pools
2. **Fee Generation**
   * Collect fees on deposits/withdrawals
   * Generate revenue from cross-chain movements
   * Incentivize liquidity provision
3. **Rate-Limited Transfers**
   * Control token velocity
   * Prevent large sudden movements
   * Enhance security through movement restrictions
4. **Cross chain token managment**
   * Controls movement and accounting for tokens deployed across multiple chains

### Configuration Methods

```solidity
// Add support for a new underlying token
function addSupportedToken(
    address token,
    uint8 decimals,
    uint256 depositFee,
    uint256 withdrawalFee,
    uint256 maxDeposit
) external onlyOwner;
```

```solidity
// Add liqudity for a new crosschain token
function depositAndBurn(address token, uint256 amount) external;
```

// Update existing token configuration\
function updateToken(\
address token,\
uint256 depositFee,\
uint256 withdrawalFee,\
uint256 maxDeposit\
) external onlyOwner;

````

## Query Methods

```solidity
// Get vault reserves for all supported tokens
function getVaultReserves() external view returns (
    address[] memory tokens,
    uint256[] memory balances
);

// Get complete token configuration and balances
function getAllTokenInfo() external view returns (
    address[] memory tokens,
    TokenInfo[] memory infos,
    uint256[] memory reserves
);
````


# Hub Wallet Abstraction

### Overview

The Hub Wallet Abstraction is a system that enables seamless cross-chain user interactions by creating deterministic wallet proxies on the Hub chain. This allows users from any connected chain to have a corresponding wallet on the Hub chain, effectively enabling cross-chain operations without requiring users to understand the underlying complexity.

### Architecture

#### Core Components

1. **WalletFactory (Beacon Proxy)**
   * Manages the creation and tracking of user wallets
   * Uses CREATE3 for deterministic deployments
   * Implements upgradeable proxy pattern for future improvements
   * Maps any (chainId, userAddress) pair to a unique Hub chain wallet address
2. **Wallet Implementation**
   * Handles cross-chain message execution
   * Manages permissions through specialized hooks
   * Executes arbitrary contract calls on behalf of users

#### How It Works

1. **Wallet Creation**

```solidity:evm/contracts/wallet/walletfactory.sol
function getWallet(uint256 chainId, bytes calldata user) external returns (address computedAddress) {
    bytes32 salt = keccak256(abi.encodePacked(address(this), chainId, user));
    computedAddress = CREATE3.getDeployed(salt);
    if (computedAddress.code.length <= 0) {
        this.deploy(salt);
    }
    return computedAddress;
}
```

* When a user first interacts with the system, a deterministic wallet is created
* The wallet address is derived from the user's origin chain ID and address
* CREATE3 ensures the same wallet address is always generated for a given user

2. **Cross-Chain Interaction**

```solidity:evm/contracts/wallet/wallet.sol
function recvMessage(
    uint256 srcChainId,
    bytes calldata srcAddress,
    uint256 _connSn,
    bytes memory _payload,
    bytes[] calldata signatures
) external {
    connection.verifyMessage(srcChainId, srcAddress, _connSn, _payload, signatures);
    require(
        address(this) == factory.getWallet(originChainId, originAddress),
        "Mismatched address and caller"
    );
    executeCalls(payload);
}
```

* Users send messages from their origin chain
* Messages are verified through the connection contract
* The wallet executes the requested actions on the Hub chain

3. **Specialized Access**

* **Asset Manager Hook**: Allows asset manager to execute on behalf of the user
* **XToken Manager Hook**: Allows XToken manager to execute on behalf of the user

### Hashed Calls

In order to support spoke chains with limited call data size, we support hashed calls.

When a user sends a message to the Hub chain, the message of length 32 bytes it will be treated as keccak256 hashed data and stored in the wallet.

When the user wants to execute the stored call, they can call the `executeStored` function with the same data. This will generallay be handled by the relayer.

This allows the user to store a call on the Hub chain and execute it later on the spoke chain.

### Key Features

1. **Deterministic Addressing**
   * Every user gets a predictable wallet address on the Hub chain
   * No need for explicit wallet deployment transactions
   * Gas-efficient through CREATE3
2. **Upgradeable Architecture**
   * Beacon proxy pattern allows wallet implementation upgrades
   * All user wallets can be upgraded simultaneously
   * Maintains consistent addresses across upgrades
3. **Flexible Execution**
   * Supports arbitrary contract calls
   * Batched transactions through ContractCall structs
   * Secure permission system for different operation types

### Use Cases

#### Example: Cross-Chain Money Market Interaction

1. User initiates action from their origin chain
2. Message is verified and routed to their Hub wallet
3. Wallet executes complex operations (e.g., deposit, borrow, withdraw)
4. Results can be bridged back to any desired chain

#### Future Extensions

The system is designed to support future enhancements such as:

* Multi-wallet linking across chains
* Customizable security parameters
* Signature-based operations without cross-chain messages
* Rate limiting and additional security features
* Adding more specialized hooks for systems such as LayerZero

### Security Considerations

* All cross-chain messages must be properly verified
* Only authorized contracts (AssetManager) can access specialized hooks
* Wallet addresses are deterministically generated to prevent conflicts
* Upgrades are controlled by the factory owner through UUPS pattern

### Asset Manager Integration Showcase

#### How Asset Manager Access Works

1. **Cross-Chain Asset Transfer Reception**

```solidity:evm/contracts/assetmanager/assetmanager.sol
function recvMessage(
    uint256 nonce,
    uint256 srcChainId,
    bytes calldata fromAddress,
    bytes memory payload,
    bytes[] memory signatures
) external {
    // Verify the sender is the authorized asset manager for the source chain
    require(keccak256(fromAddress) == keccak256(spokeAssetManager[srcChainId]));
    
    // Verify the cross-chain message
    connection.(srcChainId, fromAddress, noncepayload, signatures);

    Transfer memory _transfer = payload.decode();

    // Mint tokens to the recipient
    address token = assets[srcChainId][_transfer.token];
    IAssetToken(token).mintTo(_transfer.to.toAddress(), _transfer.amount);

    // If additional actions are needed, call the user's wallet
    if (_transfer.data.length > 0) {
        address wallet = walletFactory.getWallet(srcChainId, _transfer.from);
        IWallet(wallet).assetManagerHook(_transfer.data);
    }
}
```

2. **Wallet Hook Execution**

```solidity:evm/contracts/wallet/wallet.sol
function assetManagerHook(bytes memory data) external {
    // Only the authorized AssetManager can call this function
    require(msg.sender == assetManager, "Only AssetManager is allowed");

    // Execute the calls described in the data
    executeCalls(data);
}
```

#### Asset Manager Flow

1. **Initial Transfer**
   * User initiates a cross-chain transfer from the source chain
   * Transfer includes optional additional actions (encoded in `data`)
2. **Hub Chain Processing**
   * AssetManager receives and verifies the cross-chain message
   * Mints corresponding tokens on the Hub chain
   * If additional actions are specified:
     * Locates the user's wallet using WalletFactory
     * Calls the wallet's assetManagerHook with the specified actions
3. **Security Measures**
   * Only the authorized AssetManager contract can call the wallet's hook
   * Messages must come from authorized spoke AssetManagers
   * All cross-chain messages are verified through the connection contract

#### Example Use Case: Transfer and Stake

```solidity
// Example of transfer data that includes staking action
Transfer {
    token: "0x...", // Source chain token address
    from: userAddress,
    to: hubWalletAddress,
    amount: 1000,
    data: abi.encode([
        ContractCall({
            addr: STAKING_CONTRACT,
            value: 0,
            data: abi.encodeWithSignature("stake(uint256)", 1000)
        })
    ])
}
```

This transfer would:

1. Move tokens from the source chain to the Hub chain
2. Mint tokens to the user's Hub wallet
3. Automatically stake the tokens through the wallet's assetManagerHook

#### Security Considerations

* Only the designated AssetManager contract can invoke the assetManagerHook
* Cross-chain messages must come from authorized spoke AssetManagers
* All operations are executed in the context of the user's wallet
* The hook system prevents unauthorized access to wallet functions
* Asset transfers and subsequent actions are atomic (they either all succeed or all fail)


# Intents

### Overview

The Intents system is a cross-chain execution infrastructure that enables users to express their desired outcomes ("intents") on a central hub chain while allowing solvers to fulfill these intents from any supported spoke chain. The system consists of two main contracts:

1. **Intents Contract (Hub)** - The central coordinator that:
   * Manages intent creation and lifecycle
   * Handles settlement of trades
   * Coordinates cross-chain communication
   * Maintains the source of truth for all intents
2. **IntentFiller Contract (Spokes)** - Deployed on spoke chains to:
   * Enable local fulfillment of intents
   * Lock solver funds during cross-chain settlement
   * Relay fill confirmations back to the hub

### Core Architecture

#### Centralized Settlement

All intent logic and final settlement occurs on the hub chain. This design choice provides several benefits:

* Single source of truth for intent status
* Simplified accounting and settlement
* Reduced cross-chain complexity
* Atomic execution of trades

#### Spoke Chain Role

Spoke chains serve as execution venues that:

* Allow solvers to fill intents directly on destination chains
* Provide faster user experience by avoiding cross-chain delays
* Act as temporary fund escrow for cross-chain settlements
* Do not maintain any intent state - they only facilitate fills

#### Flow Example

1. User creates intent on hub chain
2. Solver sees intent and locks funds on spoke chain
3. Spoke chain notifies hub of the fill attempt
4. Hub validates and settles the trade
5. Hub notifies spoke to release funds to user
6. Solver receives payment on hub chain

This architecture ensures that while execution can happen anywhere, settlement and intent state management remain centralized on the hub chain, simplifying the system while maintaining security and atomicity.

## Intents System - Data Structures and Events

### Intent Structure

The core `Intent` structure represents a user's trading desire with the following fields:

```solidity
struct Intent {
    uint256 intentId;        // Unique identifier for the intent
    address creator;         // Address that created the intent
    address inputToken;      // Token the user is providing
    address outputToken;     // Token the user wants to receive
    uint256 inputAmount;     // Amount of input tokens
    uint256 minOutputAmount; // Minimum amount of output tokens to accept
    uint256 deadline;        // Optional timestamp after which intent expires (0 = no deadline)
    bool allowPartialFill;   // Whether the intent can be partially filled
    uint256 srcChain;       // Chain ID where input tokens originate
    uint256 dstChain;       // Chain ID where output tokens should be delivered
    bytes srcAddress;       // Source address in bytes (for cross-chain compatibility)
    bytes dstAddress;       // Destination address in bytes (for cross-chain compatibility)
    address solver;         // Optional specific solver address (address(0) = any solver)
    bytes data;            // Additional arbitrary data
}
```

### Events

#### Intent Lifecycle Events

```solidity
event IntentCreated(
    bytes32 intentHash,    // Hash of the intent for unique identification
    Intent intent          // Full intent details
);

event IntentFilled(
    bytes32 intentHash,    // Hash of the intent
    IntentState intentState // Current state after fill
);

event IntentCancelled(
    bytes32 intentHash     // Hash of the cancelled intent
);

event ExternalFillFailed(
    uint256 fillId,        // ID of the failed cross-chain fill
    ExternalFill fill      // Details of the failed fill
);
```

### Supporting Structures

#### IntentState

Tracks the current state of an intent:

```solidity
struct IntentState {
    bool exists;           // Whether the intent exists
    uint256 remainingInput;// Amount of input tokens still to be filled
    uint256 receivedOutput;// Amount of output tokens received so far
    bool pendingPayment;   // Whether there are pending payments to solvers
}
```

#### PendingIntentState

Tracks cross-chain fills in progress:

```solidity
struct PendingIntentState {
    uint256 pendingInput;  // Amount of input tokens locked in pending fills
    uint256 pendingOutput; // Amount of output tokens expected from pending fills
}
```

#### ExternalFill

Records details of cross-chain fills:

```solidity
struct ExternalFill {
    bytes32 intentHash;    // Hash of the intent being filled
    address to;            // Solver address to receive payment
    address token;         // Token to pay the solver
    uint256 inputAmount;   // Amount of input tokens being filled
    uint256 outputAmount;  // Amount of output tokens promised
}
```

#### Payout

Tracks pending payments to solvers:

```solidity
struct Payout {
    address solver;        // Address of the solver
    uint256 amount;        // Amount to be paid
}
```

## Intents System - Function Interface

### Hub Chain (Intents Contract)

#### Intent Management

```solidity
function createIntent(Intent calldata intent) external
```

Creates a new intent and transfers input tokens from the creator.

* `intent`: The complete intent specification
* Emits: `IntentCreated`
* Requirements:
  * `inputAmount` must be > 0
  * Input tokens must be approved to contract

```solidity
function cancelIntent(Intent calldata intent) external
```

Cancels an existing intent and returns remaining input tokens.

* `intent`: The intent to cancel
* Emits: `IntentCancelled`
* Requirements:
  * Intent must exist
  * Caller must be creator OR deadline must have passed
  * No pending fills can exist

#### Fill Operations

```solidity
function fillIntent(
    Intent calldata intent,
    uint256 _inputAmount,
    uint256 _outputAmount,
    uint256 _externalFillId
) external
```

Fills an intent either partially or fully.

* `intent`: The intent to fill
* `_inputAmount`: Amount of input tokens to consume
* `_outputAmount`: Amount of output tokens to provide
* `_externalFillId`: ID for cross-chain fills (0 for hub-chain)
* Requirements:
  * Intent must exist and not be pending payment
  * Fill amount must be valid
  * Output amount must meet minimum requirements
  * Solver restrictions must be met

```solidity
function preFillIntent(
    Intent calldata intent,
    uint256 _inputAmount,
    uint256 _outputAmount,
    uint256 _externalFillId
) external
```

Pre-fills an intent before input tokens are received.

* Similar to `fillIntent` but creates intent if it doesn't exist
* Marks intent as pending payment
* Used for optimistic filling scenarios

#### Administrative Functions

```solidity
function addSpoke(
    uint256 chainID,
    bytes memory spokeAddress
) external onlyOwner
```

Registers a spoke chain filler contract.

* `chainID`: Chain ID of the spoke
* `spokeAddress`: Address of the IntentFiller contract on spoke chain

```solidity
function setWhitelistedSolver(
    address solver,
    bool whitelisted
) external onlyOwner
```

Manages solver whitelist for cross-chain fills.

* `solver`: Address of the solver
* `whitelisted`: Whether solver is approved

### Spoke Chain (IntentFiller Contract)

This section will be implemented by the spoke chain and thus wont be exactly the same as the EVM implementation.

#### Fill Data Structure

The `Fill` struct is the core data structure used to represent fill attempts on spoke chains:

```solidity
struct Fill {
    uint256 fillID;        // Unique identifier for the cross-chain fill
    bytes intentHash;      // Hash of the intent being filled (in bytes for cross-chain compatibility)
    bytes solver;          // Solver's address in bytes format
    bytes receiver;        // Recipient's address in bytes format
    bytes token;          // Token address in bytes format
    uint256 amount;       // Amount of tokens being transferred
}
```

#### Field Details:

* `fillID`:
  * Unique identifier generated by the solver
  * Used to track the fill across chains
  * Must be unique per intent-solver combination
  * Used to prevent double-fills
* `intentHash`:
  * The keccak256 hash of the original intent from the hub
  * Stored in bytes format for cross-chain compatibility
  * Used to link the fill to the original intent
* `solver`:
  * Address of the solving entity in bytes format
  * Will receive payment on the hub chain upon successful fill
  * Cross-chain format allows for different address formats across chains
* `receiver`:
  * Destination address for the filled tokens
  * Matches the `dstAddress` from the original intent
  * Stored in bytes for cross-chain compatibility
* `token`:
  * Address of the token being transferred
  * Stored in bytes format for cross-chain compatibility
  * Can be the zero address (represented in bytes) for native currency
* `amount`:
  * Quantity of tokens being transferred

#### Fill Operations

```solidity
function fillIntent(Fill memory fill) external payable
```

Creates a new fill on spoke chain.

* `fill`: Fill details including amount and recipient
* Requirements:
  * For native token, msg.value must match fill amount
  * For ERC20, tokens must be transferred to contract

```solidity
function cancelFill(Fill memory fill) external
```

Cancels a pending fill and returns tokens.

* `fill`: Fill to cancel
* Requirements:
  * Fill must exist
  * Caller must be original filler

## Token Management and Cross-Chain Asset Handling

### Native Token Support

The Intents system supports native tokens (ETH) as both input and output tokens. Native tokens are represented by the zero address (`address(0)`) in the system.

#### Native Token Usage

1. **As Input Token**:

   * When creating an intent with native token as input, the user must send the correct amount of ETH with the transaction
   * The amount must include both the input amount and any fees
   * Example:

   ```solidity
   // Creating an intent with 1 ETH input and 0.1 ETH fee
   FeeData memory feeData = FeeData({
       fee: 0.1 ether,
       receiver: feeReceiver
   });
   intent.inputToken = address(0);  // Native token
   intent.inputAmount = 1 ether;
   intents.createIntent{value: 1.1 ether}(intent);  // Send 1.1 ETH
   ```
2. **As Output Token**:

   * When filling an intent with native token as output, the solver must send the correct amount of ETH with the transaction
   * The native token can only be sent to the hub chain (no cross-chain native token transfers)
   * Example:

   ```solidity
   // Filling an intent with 1 ETH output
   intent.outputToken = address(0);  // Native token
   intent.minOutputAmount = 1 ether;
   intents.fillIntent{value: 1 ether}(intent, inputAmount, 1 ether, 0);
   ```

#### Native Token Restrictions

1. **Cross-Chain Limitations**:
   * Native tokens can only be used on the hub chain
   * Cross-chain transfers of native tokens are not supported
   * When using native tokens, the `dstChain` must be set to the hub chain ID
2. **Fee Handling**:
   * Fees in native tokens work the same way as ERC20 tokens
   * The total amount sent must include both the input amount and the fee
   * Fees are distributed proportionally for partial fills

#### Examples

1. **Native to ERC20 Intent**:

```solidity
Intent memory intent = Intent({
    inputToken: address(0),  // Native token
    outputToken: erc20Token,
    inputAmount: 1 ether,
    minOutputAmount: 1000,
    dstChain: HUB_CHAIN_ID,  // Must be hub chain for native token
    // ... other fields ...
});

// Create intent with native token
intents.createIntent{value: 1.1 ether}(intent);  // Including 0.1 ETH fee
```

2. **ERC20 to Native Intent**:

```solidity
Intent memory intent = Intent({
    inputToken: erc20Token,
    outputToken: address(0),  // Native token
    inputAmount: 1000,
    minOutputAmount: 1 ether,
    dstChain: HUB_CHAIN_ID,  // Must be hub chain for native token
    // ... other fields ...
});

// Fill intent with native token
intents.fillIntent{value: 1 ether}(intent, 1000, 1 ether, 0);
```

### Token Mapping System

The system uses the `AssetManager` contract to maintain mappings between tokens across different chains. Each token has:

* A home chain (where it originates)
* A address on each chain where it exists
* A relationship with the hub chain representation

```solidity
// From AssetManager interface
function assetInfo(address token) external view returns (
    uint256 chainID,    // Home chain of the token
    bytes memory spokeAddress  // Address on home chain
);
```

### Token Transfer Logic

The hub's `sendToken` function handles all token transfers when filling an intent based on destination chain configuration:

```solidity
function sendToken(
    address token,
    uint256 dstChainID,
    bytes memory to,
    uint256 amount
) internal {
    if (amount == 0) revert InvalidAmount();
    
    (uint256 chainID, bytes memory spokeAddress) = assetManager.assetInfo(token);
    
    // Case 1: Sending to hub chain
    if (dstChainID == HUB_CHAIN_ID) {
        IERC20(token).transfer(AddressLib.toAddress(to), amount);
    } 
    // Case 2: Sending to non-home chain
    else if (dstChainID != chainID) {
        // Route through wallet factory to appropriate chain
        IERC20(token).transfer(
            walletFactory.getWallet(chainID, to), 
            amount
        );
    } 
    // Case 3: Sending to token's home chain
    else {
        assetManager.transfer(token, to, amount, "");
    }
}
```

#### Transfer Cases

1. **To Hub Chain** (dstChainID == HUB\_CHAIN\_ID):\
   When the dst in the intent is set to the hub chain, the token is transferred directly to the user.
2. **To Non-Home Chain** (dstChainID != chainID):\
   If the intent is done to a different chain than the token's home chain, the token is transferred to a wallet abstraction on the hub chain for that specific user.
3. **To Home Chain** (dstChainID == chainID):\
   If the intent is done to the token's home chain, the token is transferred directly to the user via the AssetManager.

### Token Flow Example

For a cross-chain intent where:

* Input token is native to Chain A
* Output token is native to Chain B
* Intent is created and settled on Hub Chain

The flow would be:

1. User sends tokens from Chain A to Hub
2. Solver fills the intent on the Hub Chain
3. Hub uses AssetManager to bridge directly to Chain B
4. Solver receives payment on Hub chain

### External Fill token mappings

While all intents created needs to be between tokens represented on the hub chain, when filling externally the solver must fill the correct token representation on the spoke chain. Which can be queried from the asset manager.

## Fee System

### Overview

The Intents system supports a flexible fee mechanism that allows for partner fees to be collected during intent execution. Fees are specified in the intent's data field and are handled automatically during the fill process.

### Fee Data Structure

Fees are encoded in the intent's data field using the following structure:

```solidity
struct FeeData {
    uint256 fee;          // Amount of fee in input token
    address receiver;     // Address to receive the fee
}
```

### Fee Encoding

Fees are encoded in the intent's data field with a type identifier:

```solidity
bytes data = abi.encodePacked(uint8(1), abi.encode(FeeData));
```

### Fee Collection Process

#### During Intent Creation

1. User must approve the contract for `inputAmount + fee`
2. The full amount (including fee) is transferred to the contract
3. The fee data is stored in the contract's state

#### During Fill Operations

1. For full fills:
   * The fee is sent to the fee receiver
   * The remaining amount is sent to the solver
2. For partial fills:
   * The fee is proportionally split
   * The filled portion's fee is sent to the fee receiver
   * The remaining fee is returned to the creator upon cancellation

#### During Cancellation

1. If the intent is cancelled before any fills:
   * The full fee is returned to the creator
2. If the intent is partially filled:
   * The filled portion's fee is sent to the fee receiver
   * The remaining fee is returned to the creator

### Fee Examples

#### Full Fill

```solidity
// Intent with 1 ETH input and 0.1 ETH fee
FeeData memory feeData = FeeData({
    fee: 0.1 ether,
    receiver: feeReceiver
});

// After fill:
// - Fee receiver gets 0.1 ETH
// - Solver gets 1 ETH
```

#### Partial Fill (50%)

```solidity
// Intent with 1 ETH input and 0.1 ETH fee
// After 50% fill:
// - Fee receiver gets 0.05 ETH (50% of fee)
// - Solver gets 0.5 ETH (50% of input)
// - Creator gets 0.55 ETH back (remaining input + remaining fee)
```

#### Cancellation

```solidity
// Before any fills:
// - Creator gets 1.1 ETH back (input + fee)

// After 50% fill:
// - Fee receiver keeps 0.05 ETH
// - Creator gets 0.55 ETH back
```

### Important Considerations

1. Fees are always denominated in the input token
2. The fee amount must be approved along with the input amount
3. Fees are handled automatically by the contract
4. Partial fills result in proportional fee distribution
5. Cancellation returns any unclaimed fees to the creator


# Generalized Messaging Protocol

### Overview

Streamlined, simple and secure cross-chain messaging system that allows DApps to send and receive messages across networks reliably, enabling faster development and implementation of on-chain logic.

***

### Contract Design

The contract manages cross-network message handling with secure verification to prevent double-processing and unauthorized access. It includes upgrade permissions for the owner, a list of trusted relayers, and event-driven logging to track each message's journey.

#### Events

The contract emits events to notify external systems about outgoing messages, helping to maintain an auditable record of cross-chain communication.

```solidity
event Message(
    uint256 indexed srcChainId,
    bytes   indexed srcAddress,
    uint256 indexed connSn,
    uint256 dstChainId,
    bytes dstAddress,
    bytes payload
);
```

#### State Variables

```solidity
address public owner;                      // Contract owner with privileges to update configurations and relayers, (how to upgrade and manage relays safely?)
address public feeHandler;                 // Contract owner with privileges to update configurations and relayers, (how to upgrade and manage relays safely?)
bytes32[] public relayers;                 // List of authorized relayers for message verification
uint256 public connSn;                      // Serial number for outgoing messages, ensuring unique identification
uint256 public chainId;                    // Chain ID of the current contract’s chain
mapping(uint256 => mapping(uint256 => bool)) public receipts; // Tracks processed messages to prevent duplication
```

* **owner**: Has authority to update contract configurations and manage authorized relayers.
* **relayers**: Holds addresses of trusted entities that validate messages, reducing single points of failure.
* **connSn**: A serial number for each message to uniquely identify and track outgoing messages.
* **nid**: Represents the network ID of this contract's chain, enabling accurate identification in cross-chain operations.
* **receipts**: Tracks processed messages per network ID to prevent repeated handling of the same message.

***

#### Fees

Fees are not coded into the contract but just accepted, that a fee has been paid is verified off chain if a fee is needed.

### Functions

#### `sendMessage`

The `sendMessage` function is responsible for dispatching messages to a specified network and address. It assigns a unique serial number (connSn) to each outgoing message, increments the connSn, and emits an event to signal the message dispatch.

```solidity
function sendMessage(uint256 dstChainId, bytes dstAddress, bytes memory payload) external payable {
    connSn++;
    feeHandler.transfer(msg.value);
    emit Message(connSn, chainID, msg.sender, dstChainId, dstAddress, payload);
}
```

**Parameters**

* **dstChainId**: The Chain ID of the destination network.
* **dstAddress**: The target address on the destination network.
* **payload**: The message payload to be transmitted.

***

#### `verifyMessage`

The `verifyMessage` function validates incoming messages using signatures provided by authorized relayers. It checks the `receipts` mapping to prevent processing the same message multiple times, and invokes `verifySignatures` to authenticate the message’s integrity.

```solidity
function verifyMessage(
    uint256 srcChainId,
    bytes calldata srcAddress,
    uint256 connSn,
    bytes memory payload,
    bytes[] calldata signatures
) external {
    require(!receipts[originChainId][connSn], "Message already processed");

    // Mark the message as processed to prevent re-processing
    receipts[originChainId][connSn] = true;

    // Verify the signatures to confirm authenticity
    require(
        verifySignatures(connSn, originChainId, originAddress, nid, msg.sender, payload, signatures),
        "Invalid signatures"
    );
}
```

***

### Example DApp: Using ICON GMP for Cross-Chain Messaging

#### Sending Messages

DApps can initiate cross-chain messages using `sendMessage` in the connection contract. This allows a DApp to send a signed message to a target contract on another network, which the relayers can then verify and forward to the destination network.

```solidity
function gmpAction() external {
    // Application logic here
    gmp.sendMessage(targetNetworkID, targetAddress, messagePayload);
}
```

#### Receiving Messages

SODAX GMP provides relayers to automatically submit messages to the designated `recvMessage` endpoint. This endpoint will verify the message’s authenticity using `verifyMessage` and then process it according to application-specific logic.

```solidity
function recvMessage(
    uint256 srcChainId,
    bytes calldata srcAddress,
    uint256 connSn,
    bytes memory payload,
    bytes[] calldata signatures
) external {
    // Verify the incoming message
    gmp.verifyMessage(srcChainId, srcAddress, connSn, payload, signatures);
 
    // Application-specific logic to handle the message
}
```

#### Alternative Message Consumption

In cases where a message requires special handling (e.g., error resolution or rollback), DApps can implement alternative message interfaces. This allows for customized message processing, including recovery mechanisms or other error-handling strategies.

```solidity
function revertInterface(
    uint256 srcChainId,
    bytes calldata srcAddress,
    uint256 _onnSn,
    bytes memory payload,
    bytes[] calldata signatures
) external {
    // Verify the message authenticity before proceeding
    gmp.verifyMessage(srcChainId, srcAddress, connSn, payload, signatures);

    // Implement revert or recovery logic here for special handling
}
```

### Encoding Compliance


# How to


# Monetize SDK

Learn how to configure fees and monetize your Sodax SDK integration.

When using the SODAX SDK, you can monetize your integration by collecting fees from the transactions processed through your application. Fees are configured globally per feature when creating the `Sodax` instance, and the swap feature additionally accepts a per-action override: `getQuote()` takes an optional `partnerFee` argument, and `swap()` / `createIntent()` read `extras.partnerFee`. When omitted, the configured fee applies.

## Defining Fee

```typescript
import { PartnerFee } from '@sodax/sdk';

// Partner fee can be defined as a percentage or a definite token amount.
// Fee is optional, you can leave it empty/undefined.
const partnerFeePercentage = {
  address: '0x0000000000000000000000000000000000000000', // EVM (Sonic) address to receive fee
  percentage: 100, // 100 = 1%, 10000 = 100%
} satisfies PartnerFee;

const partnerFeeAmount = {
  address: '0x0000000000000000000000000000000000000000', // EVM (Sonic) address to receive fee
  amount: 1000n, // definite amount denominated in token decimal precision
} satisfies PartnerFee;
```

## Global fee configuration

The recommended approach is to configure fees globally per feature when creating your SDK config using `new Sodax({...configuration})`. This ensures all requests use the same fee configuration automatically:

```typescript
import { Sodax, PartnerFee } from '@sodax/sdk';

// both partnerFeePercentage or partnerFeeAmount can be used

// apply fee to swap feature
const sodaxWithSwapFees = new Sodax({
  swaps: { partnerFee: partnerFeePercentage },
});

// apply fee to money market feature
const sodaxWithMoneyMarketFees = new Sodax({
  moneyMarket: { partnerFee: partnerFeePercentage },
});

// apply fee to bridge feature
const sodaxWithBridgeFees = new Sodax({
  bridge: { partnerFee: partnerFeePercentage },
});

// apply fee to multiple features
const sodaxWithFees = new Sodax({
  swaps: { partnerFee: partnerFeePercentage },
  moneyMarket: { partnerFee: partnerFeePercentage },
  bridge: { partnerFee: partnerFeePercentage },
});
```

## Per-request fee configuration

The swap feature supports a per-action fee override that beats the configured `swaps.partnerFee` (per-feature override, else global). When omitted, the configured fee applies. This is what lets a backend construct swap intents on behalf of partners whose fee differs per request.

### Quote request

`SwapService.getQuote()` deducts the partner fee from the `amount` before forwarding to the solver, so `quoted_amount` reflects the net output. No fee field appears in the request payload. Pass an optional `partnerFee` second argument to match a per-action override used on `createIntent` / `swap`; omit it to use the configured swap fee.

```typescript
import {
  type SolverIntentQuoteRequest,
} from "@sodax/sdk";

const quoteRequest = {
  token_src: '0x...', // The address of the source token on the spoke chain
  token_dst: '0x...', // The address of the destination token on the spoke chain
  token_src_blockchain_id: ChainKeys.BSC_MAINNET,  // Source chain key (e.g. Binance Smart Chain)
  token_dst_blockchain_id: ChainKeys.ARBITRUM_MAINNET, // Destination chain key (e.g. Arbitrum)
  amount: 1000000000000000n, // token amount in scaled token decimal precision (e.g. 1 ETH = 1e18)
  quote_type: 'exact_input', // type of quote
} satisfies SolverIntentQuoteRequest;

// Uses the configured swaps.partnerFee:
const result = await sodax.swaps.getQuote(quoteRequest);

// Or override the fee just for this quote (matches an extras.partnerFee passed to createIntent/swap):
const overriddenResult = await sodax.swaps.getQuote({ ...quoteRequest, partnerFee: partnerFeePercentage });

if (result.ok) {
  const { quoted_amount } = result.value;
  console.log('Quoted amount:', quoted_amount);
} else {
  // handle error
  console.error('Quote failed:', result.error);
}
```

### Swap request

The fee is applied automatically by the service. No fee field appears on the wire. Pass `extras.partnerFee` to override the configured `swaps.partnerFee` for this single action — omit `extras` (or `extras.partnerFee`) to use the configured fee.

```typescript
const swapResult = await sodax.swaps.swap({
  params: {
    inputToken: '0x...',  // The address of the input token on the spoke chain
    outputToken: '0x...', // The address of the output token on the spoke chain
    inputAmount: 1_000_000n, // Amount of input tokens, fee will be deducted from this amount
    minOutputAmount: 900_000n, // Minimum output tokens expected
    deadline: 0n, // Optional: timestamp after which intent expires (0 = no deadline)
    allowPartialFill: false, // Whether intent can be partially filled
    srcChainKey: ChainKeys.BSC_MAINNET, // Source chain key
    dstChainKey: ChainKeys.ARBITRUM_MAINNET, // Destination chain key
    srcAddress: '0x...', // Originating address on source chain
    dstAddress: '0x...', // Destination address on destination chain
    solver: '0x0000000000000000000000000000000000000000', // Optional: specific solver, address(0) means any solver
    data: '0x', // Arbitrary additional data
  },
  extras: { partnerFee: partnerFeePercentage }, // optional per-action fee override; falls back to the configured swaps.partnerFee
  walletProvider, // chain-narrowed wallet provider for the source chain
  timeout, // optional, request timeout in ms if needed
  skipSimulation, // optional - whether to skip transaction simulation (default: false)
});
```

## Partner Fee Claiming

Partners earn fees from every swap or bridge operation they facilitate. Those fees accrue as wrapped ERC-20 tokens on the Sonic hub chain. The `sodax.partners` service exposes the full lifecycle for retrieving and converting those balances.

### Accessing the partner service

```typescript
// Access via the Sodax facade — property is `partners` (not `partner`)
const { feeClaim } = sodax.partners;
```

### Step 1 — Query accrued balances

`fetchAssetsBalances` issues a multicall to the hub chain and returns only non-zero balances, keyed by the wrapped asset address on Sonic.

```typescript
import { ChainKeys } from '@sodax/sdk';

const balancesResult = await sodax.partners.feeClaim.fetchAssetsBalances(
  '0xYourSonicAddress',
);

if (balancesResult.ok) {
  for (const [assetAddress, balance] of balancesResult.value) {
    console.log(`${balance.symbol}: ${balance.balance} (decimals: ${balance.decimal})`);
    console.log(`  Hub address:      ${balance.address}`);
    console.log(`  Original chain:   ${balance.originalChain}`);
    console.log(`  Original address: ${balance.originalAddress}`);
  }
} else {
  // result.error.code === 'LOOKUP_FAILED' (context.method === 'fetchAssetsBalances')
  console.error('Balance fetch failed:', balancesResult.error);
}
```

### Step 2 — Configure auto-swap preferences

Before claiming, configure where swapped proceeds should be delivered. Preferences are stored on-chain and applied automatically to every future `createIntentAutoSwap` call.

```typescript
import { ChainKeys } from '@sodax/sdk';

// Read current preferences
const prefsResult = await sodax.partners.feeClaim.getAutoSwapPreferences('0xYourSonicAddress');
if (prefsResult.ok) {
  const { outputToken, dstChainKey, dstAddress } = prefsResult.value;
  // dstChainKey === 'not configured' when no destination chain has been set yet
  console.log('Current preferences:', { outputToken, dstChainKey, dstAddress });
}

// Write new preferences
const setResult = await sodax.partners.feeClaim.setSwapPreference({
  params: {
    srcChainKey: ChainKeys.SONIC_MAINNET, // must be the hub chain key
    srcAddress: '0xYourSonicAddress',
    outputToken: '0xDesiredOutputTokenAddress', // spoke-chain or hub-chain address
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,   // chain to receive proceeds
    dstAddress: '0xYourArbitrumAddress',
  },
  walletProvider, // EVM wallet provider for Sonic
});

if (!setResult.ok) {
  console.error('Set preference failed:', setResult.error);
}
```

`setSwapPreference` supports both signed execution (`raw: false`) and raw transaction building (`raw: true`). When `raw: true`, `walletProvider` must be omitted — the method returns the unsigned transaction object instead.

### Step 3 — Approve the fee token

Before swapping, ensure the ProtocolIntents contract is approved to spend the fee token. Native tokens are pre-approved and always return `true` from `isTokenApproved`.

```typescript
const approvedResult = await sodax.partners.feeClaim.isTokenApproved({
  srcChainKey: ChainKeys.SONIC_MAINNET,
  srcAddress: '0xYourSonicAddress',
  token: '0xFeeTokenHubAddress',
});

if (approvedResult.ok && !approvedResult.value) {
  // Not yet approved — send the approval transaction
  const approveResult = await sodax.partners.feeClaim.approveToken({
    params: {
      srcChainKey: ChainKeys.SONIC_MAINNET,
      srcAddress: '0xYourSonicAddress',
      token: '0xFeeTokenHubAddress',
    },
    walletProvider, // EVM wallet provider for Sonic
  });

  if (!approveResult.ok) {
    // result.error.code === 'APPROVE_FAILED'
    console.error('Approval failed:', approveResult.error);
  }
}
```

### Step 4 — Claim fees (end-to-end swap)

`swap` is the high-level method that submits the auto-swap intent on-chain and notifies the solver to execute it in one call.

```typescript
const claimResult = await sodax.partners.feeClaim.swap({
  params: {
    srcChainKey: ChainKeys.SONIC_MAINNET, // must be the hub chain key
    srcAddress: '0xYourSonicAddress',
    fromToken: '0xFeeTokenHubAddress',
    amount: 1_000_000_000_000_000_000n, // amount in token's native decimals
    timeout: 30_000, // optional, ms to wait for tx confirmation
  },
  walletProvider, // EVM wallet provider for Sonic
});

if (claimResult.ok) {
  const { srcTxHash, intentTxHash, solverExecutionResponse } = claimResult.value;
  console.log('Intent submitted:', srcTxHash);
  console.log('Intent confirmed:', intentTxHash);
  console.log('Solver response:', solverExecutionResponse);
} else {
  // result.error.code may be:
  //   'EXECUTION_FAILED' (context.action === 'waitAutoSwap') — receipt polling failed after submission
  //   error from createIntentAutoSwap — if the initial tx failed
  //   error from SolverApiService.postExecution — if solver notification failed
  console.error('Claim failed:', claimResult.error.code, claimResult.error.cause);
}
```

Use `createIntentAutoSwap` instead of `swap` when you need manual control over the solver notification step (e.g. to retry independently).

### Step 5 — Same-token claims (no conversion) and recovery

The solver cannot fill a swap whose output token equals its input token. If a partner's configured output token is the same asset as the fee token they are claiming (e.g. claiming BTC fees while the auto-swap output is BTC), `swap` rejects it up front with `VALIDATION_FAILED` instead of creating an unfillable intent that would lock the funds.

To deliver such a fee **as-is**, skip the swap and move the wrapped fee token off Sonic with the bridge — to its native chain, or to a Sonic address for same-chain delivery:

```typescript
const withdrawResult = await sodax.bridge.bridge({
  params: {
    srcChainKey: ChainKeys.SONIC_MAINNET,
    srcAddress: '0xYourSonicAddress',
    srcToken: '0xFeeTokenHubAddress', // the wrapped fee token's address on Sonic
    amount: 1_000_000n,
    dstChainKey: ChainKeys.SONIC_MAINNET, // or the fee token's native chain
    dstToken: '0xFeeTokenHubAddress', // same hub asset for Sonic; the original token address on a native chain
    recipient: '0xRecipient',
  },
  walletProvider,
});
```

Bridging from Sonic pulls the token via the partner's hub-wallet router, so it needs a bridge allowance first (`sodax.bridge.isAllowanceValid` / `sodax.bridge.approve`) — a different spender than the ProtocolIntents approval used by `swap`.

If a same-token claim was already submitted before this guard existed, the funds sit in an unfillable intent. Recover them with `cancelIntent`, which calls ProtocolIntents' own `cancelIntent(fromToken, toToken)` and refunds the locked amount to the partner. This is the only authorized cancel path: the intent's creator is the ProtocolIntents contract, so the generic `SwapService.cancelIntent` reverts `Unauthorized()`.

```typescript
// Detect a stuck intent for a token pair (0x0…0 == none):
const intentHash = await sodax.partners.feeClaim.getUserIntent({
  user: '0xYourSonicAddress',
  fromToken: '0xFeeTokenHubAddress',
  toToken: '0xOutputTokenHubAddress', // same as fromToken for a same-token claim
});

// Recover (refunds the locked tokens to your wallet):
const recoverResult = await sodax.partners.feeClaim.cancelIntent({
  params: {
    srcChainKey: ChainKeys.SONIC_MAINNET,
    srcAddress: '0xYourSonicAddress',
    fromToken: '0xFeeTokenHubAddress',
    toToken: '0xOutputTokenHubAddress',
  },
  walletProvider,
});
```

### Error handling

All `partners.feeClaim` methods return `Promise<Result<T, SodaxError<PartnerErrorCode>>>` from the unified vocabulary. Discriminate on `error.code` (closed reason-only union) and `error.feature === 'partner'`. The original lower-level failure is preserved on `error.cause`; operation/method partition is on `error.context.action` / `error.context.method`.

```typescript
import { isPartnerError, type PartnerError } from '@sodax/sdk';

if (!result.ok) {
  // result.error: PartnerError = SodaxError<PartnerErrorCode>
  switch (result.error.code) {
    case 'VALIDATION_FAILED':
      // Bad input — see context.field.
      break;
    case 'LOOKUP_FAILED':
      // Read failed — context.method is one of:
      //   'fetchAssetsBalances' | 'getAutoSwapPreferences' | 'isTokenApproved'
      //   | 'getUserIntent' | 'getIntentDetails'
      break;
    case 'APPROVE_FAILED':
      // approveToken transaction failed.
      break;
    case 'EXECUTION_FAILED':
      // Orchestrator-level failure — context.action === 'waitAutoSwap'.
      break;
    case 'UNKNOWN':
      break;
  }
  console.error('Partner error:', result.error.toJSON());
}
```

`PartnerErrorCode` is the narrow union `'VALIDATION_FAILED' | 'LOOKUP_FAILED' | 'APPROVE_FAILED' | 'EXECUTION_FAILED' | 'UNKNOWN'`. Use `isPartnerError(e)` instead of `instanceof SodaxError` in dapp/app code (bundle-safe).

### Raw transaction mode

Every write method on `PartnerFeeClaimService` supports `raw: true` to obtain the unsigned transaction instead of broadcasting it. When `raw: true`, the `walletProvider` field must be omitted — TypeScript enforces this at compile time.

```typescript
// Build the unsigned setSwapPreference transaction without broadcasting
const rawTxResult = await sodax.partners.feeClaim.setSwapPreference({
  params: {
    srcChainKey: ChainKeys.SONIC_MAINNET,
    srcAddress: '0xYourSonicAddress',
    outputToken: '0xOutputTokenAddress',
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    dstAddress: '0xYourArbitrumAddress',
  },
  raw: true,
  // walletProvider must NOT be passed when raw: true
});

if (rawTxResult.ok) {
  const { from, to, data, value } = rawTxResult.value;
  // Sign and broadcast yourself
}
```


# Configure SDK

Learn how to configure the Sodax SDK for your application. The SDK supports Swaps (intent-based solver swaps), Money Market (cross-chain lending and borrowing), and many other cross-chain DeFi services. All feature configurations are optional—you can use just the features you need.

`new Sodax(...)` accepts [`SodaxOptions`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/sodax-config/sodax-config.ts) — a deep-partial override of the static [`SodaxDefaultConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/sodax-config/sodax-config.ts) data shape plus client-side options (`logger`, global `fee`, and per-feature `partnerFee` options). The merged result is [`SodaxConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/sodax-config/sodax-config.ts) (exposed as `sodax.instanceConfig`). All three live in `@sodax/types` and are re-exported from `@sodax/sdk`.

## Basic Configuration

### Default Configuration

Initialize the SDK with default Sonic mainnet configurations (no fees):

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax();
```

The constructor signature is `new Sodax(config?: SodaxOptions)`, where `SodaxOptions = DeepPartial<SodaxDefaultConfig> & SodaxOptionalConfig` — a deep-partial override of the `SodaxDefaultConfig` data contract plus the client-side options: the `logger` sink (see [LOGGING.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/LOGGING.md)), the global partner `fee`, per-feature `partnerFee` options, and `swapsOptions` (see [Backend submit-tx 2-step](#backend-submit-tx-2-step-swapsoptionsusebackendsubmittx)). The `logger`, global `fee`, and `swapsOptions` are kept off the data contract: they are resolved once and never fetched from or overwritten by the backend config. When called with no arguments the SDK merges your overrides with the packaged static defaults ([`sodaxConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/sodax-config/sodax-config.ts)) using a recursive `deepMerge`. Omitted keys keep their default values.

### Dynamic Configuration

For the latest tokens and chains, call `initialize()` before usage. Without this call the SDK falls back to the static defaults bundled with the installed version:

```typescript
const initResult = await sodax.initialize();
if (!initResult.ok) {
  console.error('Initialization failed:', initResult.error);
}
```

`initialize()` returns `Promise<Result<void>>`. On success, `ConfigService` is populated with up-to-date chain and token data fetched from the backend API. On failure the SDK continues to work with the packaged defaults — the error is informational only.

## SodaxConfig overview

Top-level data keys (the `SodaxDefaultConfig` shape carried inside `SodaxConfig`):

| Key             | Type (summary)                            | Role                                                                                                                                                              |
| --------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chains`        | `Record<SpokeChainKey, SpokeChainConfig>` | Per-spoke chain addresses, tokens, RPC settings, polling.                                                                                                         |
| `swaps`         | `SwapsConfig`                             | Per-chain solver-supported token lists, plus an optional per-feature `partnerFee`.                                                                                |
| `moneyMarket`   | `MoneyMarketConfig`                       | Lending pool addresses, reserve assets, supported tokens, plus an optional per-feature `partnerFee`.                                                              |
| `bridge`        | `BridgeConfig`                            | Optional bridge per-feature `partnerFee`.                                                                                                                         |
| `dex`           | `DexConfig`                               | Concentrated liquidity contract set and pool keys (Sonic hub).                                                                                                    |
| `leverageYield` | `LeverageYieldConfig`                     | Registry of leverage-yield ERC-4626 vaults on the hub, plus an optional per-feature `partnerFee`.                                                                 |
| `hub`           | `HubConfig`                               | Hub chain (Sonic) metadata, contract addresses, and `rpcUrl` used by `EvmHubProvider`.                                                                            |
| `api`           | `ApiConfig`                               | Backend API config — flat `BaseApiConfig` (`{ baseURL, timeout, headers }`, shared by `sodax.api.swaps`) or `CustomApiConfig` to point swaps at its own endpoint. |
| `solver`        | `SolverConfig`                            | Intents contract addresses and solver HTTP API endpoint.                                                                                                          |
| `relay`         | `RelayConfig`                             | Relayer HTTP endpoint and spoke-to-intent relay chain ID map.                                                                                                     |

The global partner `fee` is **not** a data key — it is a `SodaxOptions` client-side option (like `logger`). Set it via `new Sodax({ fee })` and read the resolved value back on `sodax.config.fee`. It is the default applied to any feature whose own `partnerFee` is unset (see [Partner Fees](#partner-fees)).

### Partner Fees

Set a global `fee` once, override it per feature, or both. The effective fee for a feature is `featureFee ?? fee` — a feature's own `partnerFee` wins, otherwise the global `fee` applies. Services read the resolved value through `ConfigService` getters: `SwapService` reads `config.swapPartnerFee`, `MoneyMarketService` reads `config.moneyMarketPartnerFee`, `BridgeService` reads `config.bridgePartnerFee`, and `LeverageYieldService` reads `config.leverageYieldPartnerFee`. See [Monetize SDK](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/MONETIZE_SDK.md) for usage details and per-request overrides.

```typescript
import { Sodax, PartnerFee } from '@sodax/sdk';

const partnerFee: PartnerFee = {
  address: '0x0000000000000000000000000000000000000000',
  percentage: 100, // basis points: 100 = 1%, 10_000 = 100%
};

// Global fee applied to every feature that has no per-feature override
const sodaxWithGlobalFee = new Sodax({ fee: partnerFee });
```

```typescript
import { Sodax, PartnerFee } from '@sodax/sdk';

const partnerFee: PartnerFee = {
  address: '0x0000000000000000000000000000000000000000', // fee recipient (hub / EVM address)
  percentage: 100, // basis points: 100 = 1%, 10_000 = 100%
};

// Fee on swaps only
const sodaxWithSwapFees = new Sodax({
  swaps: { partnerFee },
});

// Fee on money market only
const sodaxWithMoneyMarketFees = new Sodax({
  moneyMarket: { partnerFee },
});

// Fee on bridge only
const sodaxWithBridgeFees = new Sodax({
  bridge: { partnerFee },
});

// Fees on multiple features
const sodaxWithFees = new Sodax({
  swaps: { partnerFee },
  moneyMarket: { partnerFee },
  bridge: { partnerFee },
});
```

### Partner fee shapes

Partner fees are either percentage-based or amount-based (`PartnerFee` is a discriminated union—use one shape per fee object).

```typescript
import { PartnerFee } from '@sodax/sdk';

const partnerFeePercentage: PartnerFee = {
  address: '0x0000000000000000000000000000000000000000',
  percentage: 100, // basis points: 100 = 1%, 10_000 = 100%
};

const partnerFeeAmount: PartnerFee = {
  address: '0x0000000000000000000000000000000000000000',
  amount: 1000n, // fixed amount in token base units (decimals of the token being charged)
};
```

## Custom configuration

### Solver (`solver`)

Intent-based swaps use the top-level **`solver`** block (not nested under `swaps`). Defaults match [`solverConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/common/constants.ts) in `@sodax/types`.

```typescript
import { Sodax, getSolverConfig, type SolverConfig } from '@sodax/sdk';

// Packaged defaults: omit `solver` on `new Sodax()`, or pass `getSolverConfig()` explicitly (same object as `solverConfig`)
new Sodax({ solver: getSolverConfig() });

const customSolver: SolverConfig = {
  intentsContract: '0x6382D6ccD780758C5e8A6123c33ee8F4472F96ef',
  solverApiEndpoint: 'https://api.sodax.com/v1/intent',
  protocolIntentsContract: '0xaFf2EDb3057ed6f9C1dA6c930b8ddDf2beE573A5',
};

const sodax = new Sodax({ solver: customSolver });
```

`getSolverConfig()` takes no parameters and returns the same object as the exported `solverConfig` constant from `@sodax/sdk`.

Partner fees for swaps belong in **`swaps.partnerFee`**, not inside `solver`.

### Swaps token lists (`swaps.supportedTokens`)

`SwapsConfig` includes `supportedTokens: Record<SpokeChainKey, readonly XToken[]>`. Normally you rely on the packaged lists. If you override them, remember that **`deepMerge` replaces arrays wholesale**—provide the full list for any chain you touch, or omit `supportedTokens` to keep defaults.

### Backend submit-tx 2-step (`swapsOptions.useBackendSubmitTx`)

`swapsOptions` is a **client-side runtime option** on `SodaxOptions` (like `logger`) — it is NOT part of the backend-fetched `SodaxConfig`. Setting `useBackendSubmitTx: true` opts `sodax.swaps.swap()` into a backend-driven **2-step flow**: after the intent tx is created + verified on the source chain, the SDK hands it to the backend swaps API (`sodax.api.swaps.submitTx`), which relays and post-executes server-side; the SDK polls submit-tx status and returns the same `SwapResponse`.

```typescript
const sodax = new Sodax({ swapsOptions: { useBackendSubmitTx: true } });
```

If the backend path does not reach `solved` for **any** reason (submission rejected, terminal `failed`/abandoned status, or poll timeout), `swap()` automatically falls back to the fully client-side relay + post-execution so the swap still completes — **safely**, because re-relaying / re-posting an already-processed swap is idempotent (no double-fill; verified by `e2e-tests/e2e-relay.test.ts`), and the backend poll + fallback share one `timeout` budget (total latency ≤ one `timeout`). Default is `false`. See [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md#backend-2-step-submit-opt-in) for the flow.

### Money market (`moneyMarket`)

`MoneyMarketConfig` includes `lendingPool`, `uiPoolDataProvider`, `poolAddressesProvider`, `bnUSD`, `bnUSDVault`, `bnUSDAToken`, `supportedTokens`, `supportedReserveAssets`, and `partnerFee`. The packaged default is [`moneyMarketConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/moneyMarket/moneyMarket.ts).

```typescript
import { Sodax, moneyMarketConfig, type MoneyMarketConfig } from '@sodax/sdk';

// Start from defaults and override specific fields
const sodax = new Sodax({
  moneyMarket: {
    ...moneyMarketConfig,
    lendingPool: '0x553434896D39F867761859D0FE7189d2Af70514E',
    partnerFee: {
      address: '0x0000000000000000000000000000000000000000',
      percentage: 50,
    },
  } satisfies MoneyMarketConfig,
});
```

### Hub (`hub`)

The hub is a single **`HubConfig`**: chain metadata, hub contract addresses, and **`rpcUrl`** used when creating the hub JSON-RPC client. Override RPC or addresses with a partial under `hub`:

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax({
  hub: {
    rpcUrl: 'https://rpc.soniclabs.com',
  },
});
```

After construction, the merged hub is **`sodax.instanceConfig.hub`** (and `sodax.hubProvider.chainConfig`). **`sodax.config.getHubChainConfig()`** returns the static packaged hub snapshot, not the merged instance config—if you customize `hub`, treat `instanceConfig.hub` as the source of truth for your overrides.

### Per-chain RPC and endpoints (`chains`)

There is no separate `sharedConfig`. Spoke RPC URLs and chain-specific settings live on each entry in **`chains[SpokeChainKey]`**. Partial objects are merged into the defaults for that key:

```typescript
import { Sodax, ChainKeys } from '@sodax/sdk';

const sodax = new Sodax({
  chains: {
    [ChainKeys.STELLAR_MAINNET]: {
      horizonRpcUrl: 'https://horizon.stellar.org',
      sorobanRpcUrl: 'https://rpc.ankr.com/stellar_soroban',
    },
  },
});
```

EVM spokes use `rpcUrl` on their spoke config; Stellar uses `horizonRpcUrl` and `sorobanRpcUrl`; Bitcoin includes `radfi` and related fields—mirror the shape of the default `SpokeChainConfig` for the chain you change.

### Backend API (`api`)

[`ApiConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/common/constants.ts) controls `baseURL`, `timeout`, and `headers` for `BackendApiService` (used by `ConfigService` and `initialize()`). It is either a flat `BaseApiConfig` (shown below — shared by `sodax.backendApi` and the swaps client `sodax.api.swaps`) or a nested `CustomApiConfig` (`{ baseApiConfig?, swapsApiConfig? }`) to point the swaps API at its own endpoint.

```typescript
import { Sodax } from '@sodax/sdk';

const sodax = new Sodax({
  api: {
    baseURL: 'https://api.sodax.com/v1/be',
    timeout: 30_000,
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  },
});
```

### Relayer (`relay`)

[`RelayConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/common/constants.ts) sets `relayerApiEndpoint` and **`relayChainIdMap`** (mapping each `SpokeChainKey` to the hub intent-relay bigint ID). Override only when pointing at a different relayer or custom map.

### DEX (`dex`)

[`DexConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/dex/dex.ts) holds concentrated-liquidity addresses and pool keys for Sonic. Most integrations keep the packaged [`dexConfig`](https://github.com/icon-project/sodax-sdks/blob/main/packages/types/src/dex/dex.ts) default.

### Complete custom configuration

Combine the pieces that matter for your deployment:

```typescript
import {
  Sodax,
  ChainKeys,
  getSolverConfig,
  moneyMarketConfig,
  type PartnerFee,
} from '@sodax/sdk';

const partnerFee = {
  address: '0x0000000000000000000000000000000000000000',
  percentage: 10,
} satisfies PartnerFee;

const sodax = new Sodax({
  solver: getSolverConfig(),
  swaps: { partnerFee },
  moneyMarket: { ...moneyMarketConfig, partnerFee },
  bridge: { partnerFee },
  hub: { rpcUrl: 'https://rpc.soniclabs.com' },
  chains: {
    [ChainKeys.STELLAR_MAINNET]: {
      horizonRpcUrl: 'https://horizon.stellar.org',
      sorobanRpcUrl: 'https://rpc.ankr.com/stellar_soroban',
    },
  },
});

const initResult = await sodax.initialize();
if (!initResult.ok) {
  console.error('Initialization failed:', initResult.error);
}
```

## Service Properties

After construction, the `Sodax` instance exposes the following read-only service properties:

| Property               | Type                 | Description                                       |
| ---------------------- | -------------------- | ------------------------------------------------- |
| `sodax.swaps`          | `SwapService`        | Intent-based swaps via solver                     |
| `sodax.moneyMarket`    | `MoneyMarketService` | Cross-chain lending and borrowing                 |
| `sodax.bridge`         | `BridgeService`      | Cross-chain token transfers                       |
| `sodax.staking`        | `StakingService`     | SODA token staking operations                     |
| `sodax.dex`            | `DexService`         | Concentrated liquidity / AMM                      |
| `sodax.migration`      | `MigrationService`   | ICX / bnUSD / BALN token migration                |
| `sodax.partners`       | `PartnerService`     | Partner fee claiming and operations               |
| `sodax.recovery`       | `RecoveryService`    | Withdraw stuck hub-wallet assets to a spoke chain |
| `sodax.backendApi`     | `BackendApiService`  | Raw backend API access                            |
| `sodax.config`         | `ConfigService`      | Chain/token config and lookup helpers             |
| `sodax.hubProvider`    | `EvmHubProvider`     | Hub chain (Sonic) contract interactions           |
| `sodax.spoke`          | `SpokeService`       | Spoke chain routing facade                        |
| `sodax.instanceConfig` | `SodaxConfig`        | Resolved config after merging with defaults       |

## Chain Keys

All chain constants live under `ChainKeys.*` — import them from `@sodax/sdk`:

```typescript
import { ChainKeys } from '@sodax/sdk';

ChainKeys.SONIC_MAINNET;
ChainKeys.ETHEREUM_MAINNET;
ChainKeys.ARBITRUM_MAINNET;
ChainKeys.SOLANA_MAINNET;
// … and so on for all 20 supported chains
```

`SpokeChainKey` is the union type of all `ChainKeys` values. Use it to type any parameter that accepts a chain identifier.

## Additional Resources

* [Monetize SDK](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/MONETIZE_SDK.md) - Detailed fee configuration guide
* [Architecture Reference](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/ARCHITECTURE_REFACTOR_SUMMARY.md) - Spoke services, raw tx handling, `Result<T>`, error conventions


# Make a Swap

> **Error handling conventions:** The swap module returns `SodaxError<SwapErrorCode>` from `swap`, `createIntent`, `postExecution`, `createLimitOrder`, and `createLimitOrderIntent`. Discriminate on `result.error.code` (e.g. `'RELAY_TIMEOUT'`) — not `result.error.message`. See [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md#error-handling) for the full per-method code unions. The lower-level methods (`getQuote`, `getStatus`, `submitIntent`, `getSolvedIntentPacket`, `cancelIntent`, …) still return `Result<T, SolverErrorResponse>` or `Result<T, Error | unknown>` — `cancelIntent`/`cancelLimitOrder` were not migrated to `SodaxError`, so don't `switch (error.code)` on those.

This guide provides a step-by-step walkthrough for executing a cross-chain swap using the Sodax SDK. It covers everything from initializing the SDK to handling errors during the swap process.

For detailed API reference, see [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md).

**Example Source Code**: A complete working example can be found in [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts). This example demonstrates a full swap implementation from Arbitrum ETH to Polygon POL, including all error handling and status polling.

## Prerequisites

Before you begin, ensure you have:

* A wallet provider implementation (e.g., `IEvmWalletProvider` for EVM chains). You can use existing wallet provider implementations from the [`@sodax/wallet-sdk-core`](https://www.npmjs.com/package/@sodax/wallet-sdk-core) npm package, or use the local package [@wallet-sdk-core](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-core/README.md) if working within the Sodax monorepo.
* The `@sodax/sdk` package installed
* Sufficient token balance to cover the swap amount and fees
* RPC URLs for the chains you're interacting with (we recommend having a dedicated node provider like Alchemy, Quicknode, etc.)
* Private key or wallet (browser) connection for signing transactions. For React applications, you can use the [`@sodax/wallet-sdk-react`](https://www.npmjs.com/package/@sodax/wallet-sdk-react) npm package, or use the local package [@wallet-sdk-react](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/README.md) if working within the Sodax monorepo.

## Step 1: Initialize Sodax Instance

First, create and initialize a Sodax instance. The Sodax constructor defaults to mainnet configuration, so no configuration is required for basic usage.

```typescript
import { Sodax } from "@sodax/sdk";

// Create Sodax instance (defaults to mainnet configs)
const sodax = new Sodax();

// Initialize to fetch latest configuration from the backend API (optional, use version-based
// approach without initialize for more stability).
// Initialization fetches the latest configuration from the backend API, including supported
// tokens and chains. This ensures you have the most up-to-date token and chain information.
const initResult = await sodax.initialize();
if (!initResult.ok) {
  console.warn('Initialization failed, using packaged defaults:', initResult.error);
  // The SDK continues to work with built-in default config — this is non-fatal.
}
```

**Note**:

* The `new Sodax()` constructor defaults to mainnet configuration automatically. No configuration is required for basic usage.
* `initialize()` returns `Promise<Result<void>>`. If it fails the SDK falls back to the configuration packaged with the SDK version you installed.
* If you skip `initialize()`, the SDK will use the configuration from the specific SDK version you're using. Initialization is recommended for production applications to ensure you have the latest supported tokens and chains.

### Optional: Custom Configuration

If you need to use custom solver configuration or hub provider settings, you can pass them when creating the Sodax instance:

```typescript
import { Sodax, getSolverConfig } from "@sodax/sdk";

const sodax = new Sodax({
  solver: getSolverConfig(), // Custom solver config (takes no args; returns the packaged default object)
  hub: {
    rpcUrl: 'https://rpc.soniclabs.com', // override only what you need; the rest merges with the packaged HubConfig
  },
});

const initResult = await sodax.config.initialize();
if (!initResult.ok) {
  console.warn('Initialization failed, using packaged defaults:', initResult.error);
}
```

See [CONFIGURE\_SDK.md](/developers/how-to/configure_sdk) for the full `SodaxConfig` shape and canonical override patterns.

## Step 2: Obtain a Wallet Provider

The SDK does not require you to construct a spoke provider object. Instead, you supply a wallet provider directly in each service call. The `srcChainKey` field in the call params tells the SDK which chain to route to, and the `walletProvider` field is type-narrowed to the correct interface for that chain.

**Note**: For Node.js environments, we suggest you provide RPC URLs when creating wallet providers (default public ones might not work). For browser environments, wallet providers are typically injected by wallet extensions.

For EVM chains (Arbitrum, Polygon, BSC, etc.):

```typescript
import {
  ChainKeys,
  type IEvmWalletProvider,
  type Hex
} from "@sodax/sdk";
import { EvmWalletProvider } from "@sodax/wallet-sdk-core";

const evmWalletProvider: IEvmWalletProvider = new EvmWalletProvider({
  privateKey: '0x...' as Hex, // Your private key
  chainId: ChainKeys.ARBITRUM_MAINNET,
  rpcUrl: 'https://arb1.arbitrum.io/rpc', // Arbitrum RPC URL
});

// For browser: use the injected wallet provider from wallet extension or
// the @sodax/wallet-sdk-react package.
// const evmWalletProvider: IEvmWalletProvider = /* injected by wallet */;
```

**Important**: For the Sonic hub chain, pass an EVM wallet provider as well — Sonic is an EVM chain. The SDK distinguishes hub-vs-spoke behavior internally via `srcChainKey`.

### Getting Supported Tokens

Before creating a swap, you may want to check which tokens are supported for swaps on each chain:

```typescript
// Get all supported swap tokens for a specific chain
const supportedTokens = sodax.swaps.getSupportedSwapTokensByChainId(ChainKeys.ARBITRUM_MAINNET);
console.log('Supported tokens on Arbitrum:', supportedTokens);

// Get all supported swap tokens across all chains
const allSupportedTokens = sodax.swaps.getSupportedSwapTokens();
console.log('All supported tokens:', allSupportedTokens);

// Each token object contains address, decimals, symbol, etc.
supportedTokens.forEach(token => {
  console.log(`Token: ${token.symbol}, Address: ${token.address}, Decimals: ${token.decimals}`);
});
```

## Step 3: Get a Quote (Optional but Recommended)

Before executing a swap, it is good practice to get a quote to show users the expected output amount. This helps set proper expectations and allows you to calculate slippage tolerance.

**Example**: See how quotes are obtained in the example file: [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts#L62-L80).

```typescript
import {
  ChainKeys,
  type SolverIntentQuoteRequest
} from "@sodax/sdk";

// Read chain config off the Sodax instance — this picks up any config overrides
// passed to `new Sodax(...)` and any dynamic updates loaded by `sodax.config.initialize()`.
// Do NOT import `spokeChainConfig` directly from `@sodax/types` (or its `@sodax/sdk`
// re-export): that's a packaged-default snapshot frozen at SDK release time and will
// silently miss your overrides.
const arbEthToken = sodax.config.spokeChainConfig[ChainKeys.ARBITRUM_MAINNET].nativeToken; // ETH on Arbitrum
const polygonPolToken = sodax.config.spokeChainConfig[ChainKeys.POLYGON_MAINNET].nativeToken; // POL on Polygon

// Amount to swap — IMPORTANT: Amount must be in the token's smallest unit.
// For example, ETH has 18 decimals, so 0.0001 ETH = 100000000000000n (0.0001 * 10^18).
const inputAmount = 100000000000000n; // 0.0001 ETH (18 decimals)

const quoteRequest = {
  token_src: arbEthToken,
  token_dst: polygonPolToken,
  token_src_blockchain_id: ChainKeys.ARBITRUM_MAINNET,
  token_dst_blockchain_id: ChainKeys.POLYGON_MAINNET,
  amount: inputAmount,
  quote_type: 'exact_input',
} satisfies SolverIntentQuoteRequest;

const quoteResult = await sodax.swaps.getQuote(quoteRequest);

if (!quoteResult.ok) {
  console.error('Failed to get quote:', quoteResult.error);
  // Handle error — could be no path found, invalid tokens, etc.
} else {
  const { quoted_amount } = quoteResult.value;
  console.log('Quoted output amount:', quoted_amount);
  // Use quoted_amount to set minOutputAmount in your intent params
}
```

## Step 4: Check Token Allowance

Before creating a swap intent, check whether the Asset Manager contract already has permission to spend your tokens. If not, you will need to approve it first.

**Example**: See how allowance checking is implemented in the example file: [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts#L82-L112).

```typescript
import type { CreateIntentParams } from "@sodax/sdk";

const walletAddress = await evmWalletProvider.getWalletAddress();

// Prepare intent parameters (we'll complete this in Step 6)
const createIntentParams: CreateIntentParams<typeof ChainKeys.ARBITRUM_MAINNET> = {
  inputToken: arbEthToken,
  outputToken: polygonPolToken,
  inputAmount: inputAmount,
  minOutputAmount: 900000n, // Minimum output you're willing to accept
  deadline: 0n, // 0 = no deadline, or use sodax.swaps.getSwapDeadline() for a time-bounded deadline
  allowPartialFill: false,
  srcChainKey: ChainKeys.ARBITRUM_MAINNET,
  dstChainKey: ChainKeys.POLYGON_MAINNET,
  srcAddress: walletAddress,
  dstAddress: walletAddress, // Destination address (where output tokens are delivered)
  solver: '0x0000000000000000000000000000000000000000', // address(0) = any solver
  data: '0x',
};

// Check if approval is needed.
// isAllowanceValid accepts the same SwapActionParams shape as swap() / createIntent().
const allowanceResult = await sodax.swaps.isAllowanceValid({
  params: createIntentParams,
  walletProvider: evmWalletProvider,
});

if (!allowanceResult.ok) {
  console.error('Failed to check allowance:', allowanceResult.error);
  // Handle error — could be network issue, invalid token, etc.
} else if (!allowanceResult.value) {
  console.log('Approval required. Proceeding to Step 5...');
} else {
  console.log('Allowance is sufficient. Proceeding to Step 6...');
}
```

**Note on field names**: `CreateIntentParams` uses `srcChainKey` and `dstChainKey` (not `srcChain` / `dstChain`). The on-chain `Intent` type has `Intent.srcChain` / `Intent.dstChain` as bigint relay chain IDs — these are internal identifiers and should not be confused with the user-facing chain key fields.

## Step 5: Approve Tokens (If Needed)

If the allowance check returned `false`, approve the Asset Manager contract to spend your tokens. The approval amount matches the `inputAmount` in your intent parameters (fees are automatically deducted from this amount).

**Example**: See how token approval is handled in the example file: [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts#L114-L135).

```typescript
if (!allowanceResult.value) {
  console.log('Approving tokens...');

  const approveResult = await sodax.swaps.approve({
    params: createIntentParams,
    walletProvider: evmWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Failed to approve tokens:', approveResult.error);
    return; // Stop execution if approval fails
  }

  const approvalTxHash = approveResult.value;
  console.log('Approval transaction hash:', approvalTxHash);

  // IMPORTANT: Wait for the approval transaction to be confirmed before proceeding.
  await evmWalletProvider.waitForTransactionReceipt(approvalTxHash);
  console.log('Approval confirmed. Proceeding with swap...');
}
```

**Important**: Always wait for the approval transaction to be confirmed before proceeding with the swap.

**Raw mode**: `approve` also supports `raw: true` to return the unsigned transaction payload instead of broadcasting:

```typescript
const rawApproveResult = await sodax.swaps.approve({
  params: createIntentParams,
  raw: true,
  // walletProvider must be omitted when raw: true
});
```

## Step 6: Prepare Intent Parameters

Now that you have approval (if needed), prepare the complete intent parameters. Make sure to:

* Use the quoted amount from Step 3 to set a reasonable `minOutputAmount`
* Set an appropriate `deadline` (or use `0n` for no deadline / limit-order behavior)
* Ensure `srcAddress` matches your wallet address
* Set `dstAddress` to where you want to receive the output tokens

```typescript
// Optionally get a deadline (5 minutes from now by default)
const deadlineResult = await sodax.swaps.getSwapDeadline(); // or use 0n for no deadline
if (!deadlineResult.ok) {
  console.error('Failed to get deadline:', deadlineResult.error);
  return;
}
const deadline = deadlineResult.value;

// Prepare complete intent parameters
const createIntentParams: CreateIntentParams<typeof ChainKeys.ARBITRUM_MAINNET> = {
  inputToken: arbEthToken,
  outputToken: polygonPolToken,
  inputAmount: inputAmount,
  minOutputAmount: 900000n, // Minimum output (should be based on quote from Step 3)
  deadline: deadline,        // or 0n for no deadline
  allowPartialFill: false,   // Set to true to allow partial fills
  srcChainKey: ChainKeys.ARBITRUM_MAINNET,
  dstChainKey: ChainKeys.POLYGON_MAINNET,
  srcAddress: walletAddress, // Must match your wallet address
  dstAddress: walletAddress, // Where to receive output tokens
  solver: '0x0000000000000000000000000000000000000000', // address(0) = any solver
  data: '0x', // Additional arbitrary data
};
```

## Step 7: Execute the Swap

Now you're ready to execute the swap. The `swap` method orchestrates the complete lifecycle automatically:

1. Creates the intent deposit transaction on the source chain
2. Verifies the spoke transaction landed on-chain
3. Submits the transaction to the relayer and waits for the relay packet to land on the hub (Sonic). This step is skipped when `srcChainKey` is the hub itself.
4. Calls `postExecution` to notify the solver, triggering it to fill the intent

**Example**: See how the swap is executed in the example file: [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts#L137-L183).

```typescript
const swapResult = await sodax.swaps.swap({
  params: createIntentParams,
  walletProvider: evmWalletProvider,
  // Optional parameters:
  // timeout: 120000, // Timeout in milliseconds waiting for hub relay (default: DEFAULT_RELAY_TX_TIMEOUT = 120 s)
  // skipSimulation: false, // Whether to skip transaction simulation (default: false)
});

if (!swapResult.ok) {
  // Handle error — see Step 9 for detailed error handling
  console.error('Swap failed:', swapResult.error.message);
} else {
  const { solverExecutionResponse, intent, intentDeliveryInfo } = swapResult.value;

  console.log('Swap successful!');
  console.log('Solver execution response:', solverExecutionResponse);
  console.log('Intent:', intent);
  console.log('Source transaction hash:', intentDeliveryInfo.srcTxHash);
  console.log('Destination transaction hash:', intentDeliveryInfo.dstTxHash);

  // Use intentDeliveryInfo.dstTxHash to poll for solver fill status (Step 8)
}
```

## Step 8: Check Intent Status

After a successful swap submission, continuously monitor the intent status until it reaches a terminal state. Poll every 5 seconds until the swap is completed, failed, or not found.

**Example**: See the complete status polling implementation in the example file: [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts#L189-L289).

```typescript
import type { SolverIntentStatusRequest, SolverIntentStatusCode } from "@sodax/sdk";

/**
 * Polls the solver API until the intent reaches a terminal state.
 * Pass the hub-chain (destination) tx hash from the swap result.
 */
async function checkIntentStatus(
  sodax: Sodax,
  dstTxHash: string,
  maxAttempts = 60,
  intervalMs = 5000,
): Promise<void> {
  const statusRequest: SolverIntentStatusRequest = {
    intent_tx_hash: dstTxHash as `0x${string}`,
  };

  let attempt = 0;
  let lastStatus: SolverIntentStatusCode | null = null;
  let notFoundCount = 0;

  while (attempt < maxAttempts) {
    attempt++;
    const statusResult = await sodax.swaps.getStatus(statusRequest);

    if (!statusResult.ok) {
      console.error(`[Attempt ${attempt}] Failed to check intent status:`, statusResult.error);
      // Continue polling — may be a transient API issue
      await new Promise(resolve => setTimeout(resolve, intervalMs));
      continue;
    }

    const { status, fill_tx_hash } = statusResult.value;

    if (status === SolverIntentStatusCode.SOLVED) {
      console.log(`[Attempt ${attempt}] Status: Swap completed successfully!`);
      if (fill_tx_hash) {
        console.log(`Fill transaction hash: ${fill_tx_hash}`);
      }
      return;
    }

    if (status === SolverIntentStatusCode.FAILED) {
      console.log(`[Attempt ${attempt}] Status: Swap failed`);
      return;
    }

    if (status === SolverIntentStatusCode.NOT_FOUND) {
      notFoundCount++;
      if (notFoundCount >= 3) {
        console.log(`[Attempt ${attempt}] Intent not found after ${notFoundCount} attempts. Check tx hash manually.`);
        return;
      }
      await new Promise(resolve => setTimeout(resolve, intervalMs));
      continue;
    }

    if (status !== lastStatus) {
      switch (status) {
        case SolverIntentStatusCode.NOT_STARTED_YET:
          console.log(`[Attempt ${attempt}] Status: Intent queued, waiting to be processed`);
          break;
        case SolverIntentStatusCode.STARTED_NOT_FINISHED:
          console.log(`[Attempt ${attempt}] Status: Intent is being processed`);
          break;
        default:
          console.log(`[Attempt ${attempt}] Unknown status (${status})`);
          return;
      }
      lastStatus = status;
    } else {
      console.log(`[Attempt ${attempt}] Still processing... (status: ${status})`);
    }

    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }

  console.log(`Status polling reached maximum attempts (${maxAttempts}).`);
  console.log(`Last known status: ${lastStatus ?? 'unknown'}`);
  console.log(`Check manually using destination tx hash: ${dstTxHash}`);
}

// After successful swap, start polling for status
await checkIntentStatus(sodax, intentDeliveryInfo.dstTxHash);
```

**Status Codes**:

* `NOT_FOUND (-1)`: Intent not found in the solver system (may appear immediately after creation). After 3 consecutive NOT\_FOUND responses, polling stops.
* `NOT_STARTED_YET (1)`: Intent is queued and waiting to be processed (continues polling)
* `STARTED_NOT_FINISHED (2)`: Intent is currently being processed (continues polling)
* `SOLVED (3)`: Swap completed successfully (includes `fill_tx_hash` when available) — **Terminal state**
* `FAILED (4)`: Swap failed to complete — **Terminal state**

**Polling Behavior**:

* Polls every 5 seconds (configurable via `intervalMs` parameter)
* Continues until a terminal state is reached (SOLVED, FAILED, or NOT\_FOUND after 3 attempts)
* Maximum polling duration: 5 minutes by default (60 attempts × 5 seconds, configurable via `maxAttempts`)
* Handles temporary API errors gracefully by continuing to poll

**Note**: The `fill_tx_hash` field is only present when the status is `SOLVED (3)`. This is the transaction hash of the fill transaction on the destination chain.

## Step 9: Handle Errors

All swap methods return `Result<T, SodaxError<SwapErrorCode>>`. Discriminate on **`result.error.code`** (a closed reason-only union), never on `error.message` (human-readable, may change). The original lower-level failure is preserved on `error.cause`; structured metadata is on `error.context`.

See [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md#error-handling) for the full per-method code unions and `context` schema.

```typescript
if (!swapResult.ok) {
  const error = swapResult.error;
  switch (error.code) {
    case 'EXECUTION_FAILED':
      // Catch-all for the swap orchestrator. context.phase tells you which stage
      // ('postExecution' for the solver-API step).
      console.error('Swap orchestration failed:', error.cause);
      break;

    case 'RELAY_TIMEOUT':
      // Spoke transaction was submitted but the hub packet did not arrive within timeout.
      // The relay may still complete — poll the relayer API.
      console.error('Relay timed out. Cause:', error.cause);
      break;

    case 'TX_SUBMIT_FAILED':
      // CRITICAL: spoke tx landed but relay submission failed. Funds may be in flight.
      // Persist the user's input + spoke tx hash and retry submission.
      console.error('Relay submit failed:', error.context?.relayCode, error.cause);
      break;

    case 'RELAY_FAILED':
      // error.context.relayCode disambiguates:
      //   'RELAY_POLLING_FAILED' — polling endpoint outage; query the hub directly.
      //   'UNKNOWN'              — forward-compat fallback for new relay error codes.
      break;

    case 'VALIDATION_FAILED':
      // Bad input — error.context.field tells you which.
      console.error('Bad input:', error.message);
      break;

    case 'INTENT_CREATION_FAILED':
    case 'TX_VERIFICATION_FAILED':
    case 'EXTERNAL_API_ERROR':
    case 'UNKNOWN':
    default:
      console.error('Swap error:', error.code, error.cause);
  }
}
```

**Note**: The swap module exports narrow guards `isSwapError`, `isSwapCreateIntentError`, `isPostExecutionError` from `@sodax/sdk`. Use them in `catch` blocks for cross-bundle type safety; see [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md#error-handling).

## Complete Example

Here's a complete end-to-end example combining all the steps. For a production-ready implementation, see the example source code in [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts):

```typescript
import {
  Sodax,
  ChainKeys,
  type CreateIntentParams,
  type SolverIntentQuoteRequest,
  type SolverIntentStatusRequest,
  SolverIntentStatusCode,
  type IEvmWalletProvider
} from "@sodax/sdk";

async function executeSwap(
  evmWalletProvider: IEvmWalletProvider,
  inputAmount: bigint
): Promise<void> {
  try {
    // Step 1: Initialize Sodax
    console.log('Step 1: Initializing Sodax...');
    const sodax = new Sodax();
    const initResult = await sodax.initialize();
    if (!initResult.ok) {
      console.warn('Initialization failed, using packaged defaults:', initResult.error);
    }

    // Read chain config off the Sodax instance — picks up any constructor overrides
    // and any dynamic config loaded by initialize(). Never use a static import of
    // `spokeChainConfig` from `@sodax/types` here — overrides will be silently lost.
    const arbEthToken = sodax.config.spokeChainConfig[ChainKeys.ARBITRUM_MAINNET].nativeToken; // ETH on Arbitrum
    const polygonPolToken = sodax.config.spokeChainConfig[ChainKeys.POLYGON_MAINNET].nativeToken; // POL on Polygon

    // Step 2: Get Quote
    console.log('Step 2: Getting quote...');
    const quoteRequest: SolverIntentQuoteRequest = {
      token_src: arbEthToken,
      token_dst: polygonPolToken,
      token_src_blockchain_id: ChainKeys.ARBITRUM_MAINNET,
      token_dst_blockchain_id: ChainKeys.POLYGON_MAINNET,
      amount: inputAmount,
      quote_type: 'exact_input',
    };

    const quoteResult = await sodax.swaps.getQuote(quoteRequest);
    if (!quoteResult.ok) {
      console.error('Failed to get quote:', quoteResult.error);
      return;
    }

    const quotedAmount = quoteResult.value.quoted_amount;
    console.log('Quoted amount:', quotedAmount);

    // Step 3: Prepare intent parameters
    const walletAddress = await evmWalletProvider.getWalletAddress();
    const deadlineResult = await sodax.swaps.getSwapDeadline(300n); // 5 minutes
    if (!deadlineResult.ok) {
      console.error('Failed to compute deadline:', deadlineResult.error);
      return;
    }

    const createIntentParams: CreateIntentParams<typeof ChainKeys.ARBITRUM_MAINNET> = {
      inputToken: arbEthToken,
      outputToken: polygonPolToken,
      inputAmount: inputAmount,
      minOutputAmount: (quotedAmount * 95n) / 100n, // 5% slippage tolerance
      deadline: deadlineResult.value,
      allowPartialFill: false,
      srcChainKey: ChainKeys.ARBITRUM_MAINNET,
      dstChainKey: ChainKeys.POLYGON_MAINNET,
      srcAddress: walletAddress,
      dstAddress: walletAddress,
      solver: '0x0000000000000000000000000000000000000000',
      data: '0x',
    };

    // Step 4: Check Allowance
    console.log('Step 4: Checking allowance...');
    const allowanceResult = await sodax.swaps.isAllowanceValid({
      params: createIntentParams,
      walletProvider: evmWalletProvider,
    });

    if (!allowanceResult.ok) {
      console.error('Failed to check allowance:', allowanceResult.error);
      return;
    }

    // Step 5: Approve if Needed
    if (!allowanceResult.value) {
      console.log('Step 5: Approving tokens...');
      const approveResult = await sodax.swaps.approve({
        params: createIntentParams,
        walletProvider: evmWalletProvider,
      });

      if (!approveResult.ok) {
        console.error('Failed to approve tokens:', approveResult.error);
        return;
      }

      const approvalTxHash = approveResult.value;
      console.log('Approval transaction hash:', approvalTxHash);

      // Wait for approval confirmation
      await evmWalletProvider.waitForTransactionReceipt(approvalTxHash);
      console.log('Approval confirmed');
    } else {
      console.log('Step 5: Approval not needed');
    }

    // Step 6: Execute Swap
    console.log('Step 6: Executing swap...');
    const swapResult = await sodax.swaps.swap({
      params: createIntentParams,
      walletProvider: evmWalletProvider,
    });

    // Step 7: Handle Swap Result
    if (!swapResult.ok) {
      const error = swapResult.error;
      console.error('Swap failed');

      if (error instanceof Error) {
        switch (error.message) {
          case 'POST_EXECUTION_FAILED':
            console.error('Post execution failed. Cause:', error.cause);
            break;
          case 'RELAY_TIMEOUT':
            console.error('Hub relay timed out. Cause:', error.cause);
            break;
          default:
            console.error('Error:', error.message, error.cause ?? '');
        }
      } else {
        console.error('Non-Error failure:', error);
      }
      return;
    }

    // Success!
    const { solverExecutionResponse, intent, intentDeliveryInfo } = swapResult.value;
    console.log('Step 7: Swap transaction submitted successfully!');
    console.log('Solver execution response:', solverExecutionResponse);
    console.log('Intent:', intent);
    console.log('Source transaction hash:', intentDeliveryInfo.srcTxHash);
    console.log('Destination transaction hash:', intentDeliveryInfo.dstTxHash);

    // Step 8: Check Intent Status (with continuous polling)
    console.log('Step 8: Checking intent status...');
    await checkIntentStatus(sodax, intentDeliveryInfo.dstTxHash);
  } catch (error) {
    console.error('Unexpected error during swap:', error);
  }
}

/**
 * Polls the solver API until the intent reaches a terminal state.
 * Pass the hub-chain (destination) tx hash from the swap result.
 */
async function checkIntentStatus(
  sodax: Sodax,
  dstTxHash: string,
  maxAttempts = 60,
  intervalMs = 5000,
): Promise<void> {
  const statusRequest: SolverIntentStatusRequest = {
    intent_tx_hash: dstTxHash as `0x${string}`,
  };

  let attempt = 0;
  let lastStatus: SolverIntentStatusCode | null = null;
  let notFoundCount = 0;

  while (attempt < maxAttempts) {
    attempt++;
    const statusResult = await sodax.swaps.getStatus(statusRequest);

    if (!statusResult.ok) {
      console.error(`[Attempt ${attempt}] Failed to check intent status:`, statusResult.error);
      await new Promise(resolve => setTimeout(resolve, intervalMs));
      continue;
    }

    const { status, fill_tx_hash } = statusResult.value;

    if (status === SolverIntentStatusCode.SOLVED) {
      console.log(`[Attempt ${attempt}] Swap completed successfully!`);
      if (fill_tx_hash) {
        console.log(`Fill transaction hash: ${fill_tx_hash}`);
      }
      return;
    }

    if (status === SolverIntentStatusCode.FAILED) {
      console.log(`[Attempt ${attempt}] Swap failed`);
      return;
    }

    if (status === SolverIntentStatusCode.NOT_FOUND) {
      notFoundCount++;
      if (notFoundCount >= 3) {
        console.log(`[Attempt ${attempt}] Intent not found after ${notFoundCount} attempts. Check tx hash manually.`);
        return;
      }
      await new Promise(resolve => setTimeout(resolve, intervalMs));
      continue;
    }

    if (status !== lastStatus) {
      switch (status) {
        case SolverIntentStatusCode.NOT_STARTED_YET:
          console.log(`[Attempt ${attempt}] Intent queued, waiting to be processed`);
          break;
        case SolverIntentStatusCode.STARTED_NOT_FINISHED:
          console.log(`[Attempt ${attempt}] Intent is being processed`);
          break;
        default:
          console.log(`[Attempt ${attempt}] Unknown status (${status})`);
          return;
      }
      lastStatus = status;
    } else {
      console.log(`[Attempt ${attempt}] Still processing... (status: ${status})`);
    }

    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }

  console.log(`Status polling reached maximum attempts (${maxAttempts}).`);
  console.log(`Last known status: ${lastStatus ?? 'unknown'}`);
  console.log(`Check manually using destination tx hash: ${dstTxHash}`);
}

// Usage
await executeSwap(evmWalletProvider, 100000000000000n); // 0.0001 ETH
```

## Next Steps

* **See the complete example**: Check out the working implementation in [`apps/node/src/swap.ts`](https://github.com/icon-project/sodax-sdks/blob/main/apps/node/src/swap.ts) for a production-ready swap example
* Learn more about swap configuration and advanced features in [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md)
* Explore other SDK features like [Money Market](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/MONEY_MARKET.md), [Bridge](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/BRIDGE.md), and [Staking](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STAKING.md)
* Check the [README.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/README.md) for general SDK usage and configuration
* Read [ARCHITECTURE\_REFACTOR\_SUMMARY.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/ARCHITECTURE_REFACTOR_SUMMARY.md) for the full architecture reference


# Setup Wallet Providers

The SODAX SDK does not force you to use a specific wallet library. Instead, SDK calls accept an `IWalletProvider` implementation that you supply — either by using the ready-made implementations in `@sodax/wallet-sdk-core`, or by writing your own against the interface contracts in `@sodax/sdk`.

## Table of Contents

1. [Supported provider interfaces](#1-supported-provider-interfaces)
2. [WalletProviderSlot — compile-time enforcement](#2-walletproviderslot--compile-time-enforcement)
3. [wallet-sdk-core: ready-to-use implementations](#3-wallet-sdk-core-ready-to-use-implementations)
   * [BaseWalletProvider](#basewalletprovider)
   * [chainType discriminant](#chaintype-discriminant)
   * [Dual config modes (private-key vs browser-extension)](#dual-config-modes-private-key-vs-browser-extension)
   * [Provider reference](#provider-reference)
4. [Config type reference per chain](#4-config-type-reference-per-chain)
5. [React integration — useWalletProvider](#5-react-integration--usewalletprovider)
6. [Custom implementations](#6-custom-implementations)

***

## 1. Supported provider interfaces

Every chain family has a named interface that `@sodax/sdk` exports:

| Interface                  | `chainType` literal | Chains covered                                                                                                        |
| -------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `IEvmWalletProvider`       | `'EVM'`             | Sonic (hub), Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia, Hedera |
| `ISolanaWalletProvider`    | `'SOLANA'`          | Solana                                                                                                                |
| `ISuiWalletProvider`       | `'SUI'`             | Sui                                                                                                                   |
| `IIconWalletProvider`      | `'ICON'`            | ICON                                                                                                                  |
| `IInjectiveWalletProvider` | `'INJECTIVE'`       | Injective                                                                                                             |
| `IStellarWalletProvider`   | `'STELLAR'`         | Stellar                                                                                                               |
| `IStacksWalletProvider`    | `'STACKS'`          | Stacks                                                                                                                |
| `IBitcoinWalletProvider`   | `'BITCOIN'`         | Bitcoin                                                                                                               |
| `INearWalletProvider`      | `'NEAR'`            | NEAR                                                                                                                  |

All interfaces extend `WalletAddressProvider` (from `@sodax/sdk`), which requires:

```ts
getWalletAddress(): Promise<string>;
```

The full union of all nine interfaces is `IWalletProvider`. `GetWalletProviderType<K>` maps a chain key or `ChainType` literal to the appropriate specific interface.

***

## 2. WalletProviderSlot — compile-time enforcement

Every SDK method that executes a transaction uses `WalletProviderSlot<K, Raw>` (defined in `packages/types/src/common/common.ts`) to enforce the pairing between `raw` mode and the presence of a wallet provider at compile time:

```ts
export type WalletProviderSlot<K extends SpokeChainKey | ChainType, Raw extends boolean = false> =
  Raw extends true
    ? { raw: true; walletProvider?: never }
    : { raw?: false; walletProvider: GetWalletProviderType<K> };
```

Three rules enforced by TypeScript:

1. **`raw: true`** — `walletProvider` is forbidden (`?: never` makes any value a type error). The SDK returns an unsigned transaction payload (`TxReturnType<K, true>`, e.g. `EvmRawTransaction`).
2. **`raw: false` (or omitted)** — `walletProvider` is required and is chain-narrowed to the exact interface for `K` via `GetWalletProviderType<K>`. The SDK signs and broadcasts, returning a transaction hash (`TxReturnType<K, false>`, e.g. `Hash`).
3. **Chain narrowing flows from the chain key** — when the caller passes a literal chain key such as `ChainKeys.ETHEREUM_MAINNET`, TypeScript preserves that as a value type, resolving `GetWalletProviderType<typeof ChainKeys.ETHEREUM_MAINNET>` to `IEvmWalletProvider` automatically.

```ts
// Raw — walletProvider is a compile error if passed
const rawTx = await sodax.swaps.createIntent({ params, raw: true });

// Signed — walletProvider is required; passing an ISolanaWalletProvider here is a compile error
const result = await sodax.swaps.createIntent({
  params,
  raw: false,
  walletProvider: evmWalletProvider,   // must be IEvmWalletProvider
});
```

***

## 3. wallet-sdk-core: ready-to-use implementations

Install the package:

```bash
npm install @sodax/wallet-sdk-core
# or
yarn add @sodax/wallet-sdk-core
# or
pnpm add @sodax/wallet-sdk-core
```

The package is dependency-free from React and can be used directly in Node.js scripts, bots, and server environments, as well as in browser dApps.

Each chain provider lives under `src/wallet-providers/<chain>/`:

```
wallet-providers/
├── BaseWalletProvider.ts      # Abstract base
├── evm/                       # EvmWalletProvider + types
├── solana/                    # SolanaWalletProvider + types
├── sui/                       # SuiWalletProvider + types
├── icon/                      # IconWalletProvider + types
├── injective/                 # InjectiveWalletProvider + types
├── stellar/                   # StellarWalletProvider + types
├── stacks/                    # StacksWalletProvider + types
├── bitcoin/                   # BitcoinWalletProvider + types
└── near/                      # NearWalletProvider + types
```

### BaseWalletProvider

All nine provider classes extend `BaseWalletProvider<TDefaults>`:

```ts
abstract class BaseWalletProvider<TDefaults extends object> {
  protected readonly defaults: TDefaults;

  abstract getWalletAddress(): Promise<string>;

  // Merge per-call options over defaults[key] (used for per-method defaults groups, e.g. EVM)
  protected mergePolicy<K extends keyof TDefaults>(key: K, options?: …): …

  // Merge per-call options over the entire defaults object (used for flat defaults, e.g. ICON)
  protected mergeDefaults(options?: Partial<TDefaults>): TDefaults
}
```

Subclass constructors call `super(config.defaults)`. Per-call overrides shallow-merge over the stored defaults at invocation time — nested objects replace wholesale rather than deep-merging.

### chainType discriminant

Every `I*WalletProvider` interface declares `readonly chainType` as a string literal. This lets both the SDK and application code discriminate at runtime without `instanceof`:

```ts
// Discriminate without instanceof
if (walletProvider.chainType === 'EVM') {
  // walletProvider is narrowed to IEvmWalletProvider
}
if (walletProvider.chainType === 'SOLANA') {
  // walletProvider is narrowed to ISolanaWalletProvider
}
```

Valid `chainType` values: `'EVM'`, `'BITCOIN'`, `'SOLANA'`, `'STELLAR'`, `'SUI'`, `'ICON'`, `'INJECTIVE'`, `'STACKS'`, `'NEAR'`.

`packages/sdk/src/shared/guards.ts` also exposes named guards for the most common cases:

```ts
isEvmWalletProviderType(wp)      // IEvmWalletProvider
isStellarWalletProviderType(wp)  // IStellarWalletProvider
isBitcoinWalletProviderType(wp)  // IBitcoinWalletProvider
```

### Dual config modes (private-key vs browser-extension)

Every provider accepts a discriminated-union config: one variant for server-side/script usage (private key) and one for dApp usage (pre-built client from a browser wallet). Discriminant mechanism varies by chain:

| Mechanism                                                                             | Chains                                          |
| ------------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Field presence** — `privateKey` field present vs. absent                            | EVM, ICON, Solana, Sui (uses `mnemonics`), NEAR |
| **Explicit `type` field** (`'PRIVATE_KEY'` / `'BROWSER_EXTENSION'`)                   | Bitcoin, Stellar                                |
| **`secret` nested object** (`{ privateKey }` or `{ mnemonics }`) vs. `msgBroadcaster` | Injective                                       |
| **Field presence** — `privateKey` present vs. absent                                  | Stacks                                          |

All config types include an optional `defaults` field for per-method behavioral overrides.

### Provider reference

| Chain           | Provider class            | Native SDK            | `chainType`   |
| --------------- | ------------------------- | --------------------- | ------------- |
| EVM (12 chains) | `EvmWalletProvider`       | viem                  | `'EVM'`       |
| Solana          | `SolanaWalletProvider`    | @solana/web3.js       | `'SOLANA'`    |
| Sui             | `SuiWalletProvider`       | @mysten/sui           | `'SUI'`       |
| ICON            | `IconWalletProvider`      | icon-sdk-js           | `'ICON'`      |
| Injective       | `InjectiveWalletProvider` | @injectivelabs/sdk-ts | `'INJECTIVE'` |
| Stellar         | `StellarWalletProvider`   | @stellar/stellar-sdk  | `'STELLAR'`   |
| Stacks          | `StacksWalletProvider`    | @stacks/transactions  | `'STACKS'`    |
| Bitcoin         | `BitcoinWalletProvider`   | bitcoinjs-lib (PSBT)  | `'BITCOIN'`   |
| NEAR            | `NearWalletProvider`      | near-api-js           | `'NEAR'`      |

***

## 4. Config type reference per chain

### EVM (`EvmWalletProvider`)

```ts
// Private-key mode (scripts / bots)
new EvmWalletProvider({
  privateKey: '0x…',              // hex private key
  chainId: ChainKeys.ARBITRUM_MAINNET,
  rpcUrl: 'https://…',           // optional; falls back to viem default
  defaults?: EvmWalletDefaults,
});

// Browser-extension mode (dApps — wagmi supplies the clients)
new EvmWalletProvider({
  walletClient: viemWalletClient,  // WalletClient<Transport, Chain, Account>
  publicClient: viemPublicClient,
  defaults?: EvmWalletDefaults,
});
```

`EvmWalletDefaults` accepts: `sendTransaction`, `waitForTransactionReceipt`, `publicClient`, `walletClient`, `transport` (all optional; applied per-call via `mergePolicy`).

### Solana (`SolanaWalletProvider`)

```ts
// Private-key mode
new SolanaWalletProvider({
  privateKey: Uint8Array,   // raw keypair bytes
  endpoint: 'https://…',
  defaults?: SolanaWalletDefaults,
});

// Browser-extension mode
new SolanaWalletProvider({
  wallet: walletContextState,  // { publicKey, signTransaction }
  endpoint: 'https://…',
  defaults?: SolanaWalletDefaults,
});
```

`SolanaWalletDefaults` accepts: `connectionCommitment`, `connectionConfig`, `sendOptions`, `confirmCommitment`.

### Sui (`SuiWalletProvider`)

```ts
// Private-key mode (mnemonic-derived)
new SuiWalletProvider({
  rpcUrl: 'https://…',
  mnemonics: 'word1 word2 …',
  defaults?: SuiWalletDefaults,
});

// Browser-extension mode
new SuiWalletProvider({
  client: suiClient,
  wallet: walletWithFeatures,
  account: walletAccount,
  defaults?: SuiWalletDefaults,
});
```

`SuiWalletDefaults` accepts: `signAndExecuteTxn` (dry-run toggle + response options), `getCoins` (pagination limit).

### ICON (`IconWalletProvider`)

```ts
// Private-key mode
new IconWalletProvider({
  privateKey: '0x…',
  rpcUrl: 'https://…',
  defaults?: IconWalletDefaults,
});

// Browser-extension mode (Hana Wallet)
new IconWalletProvider({
  walletAddress?: '0xhx…',   // optional pre-known address
  rpcUrl: 'https://…',
  defaults?: IconWalletDefaults,
});
```

`IconWalletDefaults` accepts: `stepLimit`, `version`, `timestampProvider`, `jsonRpcId`.

> **Note:** `rpcUrl` is typed as `` `http${string}` `` (template literal), not a bare `string`. EVM and Injective `rpcUrl` fields have the same constraint. If you pass a `string` from an environment variable, either narrow it explicitly (e.g. `process.env.RPC_URL as \`http${string}\`\`) or validate at the boundary.

### Injective (`InjectiveWalletProvider`)

```ts
// Private-key / mnemonic mode (credentials nested under `secret`)
new InjectiveWalletProvider({
  secret: { privateKey: '0x…' },   // OR { mnemonics: 'word1 word2 …' }
  chainId: ChainId.Mainnet,
  network: Network.Mainnet,
  defaults?: InjectiveWalletDefaults,
});

// Browser-extension mode
new InjectiveWalletProvider({
  msgBroadcaster: msgBroadcaster,   // MsgBroadcaster from @injectivelabs/wallet-core
  defaults?: InjectiveWalletDefaults,
});
```

`InjectiveWalletDefaults` accepts: `defaultFunds`, `defaultMemo`, `sequence`, `accountNumber`.

### Stellar (`StellarWalletProvider`)

```ts
// Private-key mode (explicit `type` field)
new StellarWalletProvider({
  type: 'PRIVATE_KEY',
  privateKey: 'S…',                // Stellar secret key (S-prefixed), typed as `Hex` string alias
  network: 'PUBLIC',               // or 'TESTNET'
  rpcUrl?: 'https://…',
  defaults?: StellarWalletDefaults,
});

// Browser-extension mode
new StellarWalletProvider({
  type: 'BROWSER_EXTENSION',
  walletsKit: stellarWalletsKit,   // StellarWalletsKit interface
  network: 'PUBLIC',
  rpcUrl?: 'https://…',
  defaults?: StellarWalletDefaults,
});
```

`StellarWalletDefaults` accepts: `pollInterval`, `pollTimeout`, `networkPassphrase`.

### Stacks (`StacksWalletProvider`)

```ts
// Private-key mode
new StacksWalletProvider({
  privateKey: 'string',
  endpoint?: 'https://…',
  defaults?: StacksWalletDefaults,
});

// Browser-extension mode
new StacksWalletProvider({
  address: 'string',
  endpoint?: 'https://…',
  provider?: stacksProvider,   // StacksProvider from @stacks/connect
  defaults?: StacksWalletDefaults,
});
```

`StacksWalletDefaults` accepts: `network` (`'mainnet'` | `'testnet'`), `postConditionMode`.

### Bitcoin (`BitcoinWalletProvider`)

```ts
// Private-key mode (explicit `type` field)
new BitcoinWalletProvider({
  type: 'PRIVATE_KEY',
  privateKey: '0x…',
  network: 'MAINNET',            // or 'TESTNET'
  addressType?: BtcAddressType,  // 'P2PKH' | 'P2SH' | 'P2WPKH' | 'P2TR'
  defaults?: BitcoinWalletDefaults,
});

// Browser-extension mode
new BitcoinWalletProvider({
  type: 'BROWSER_EXTENSION',
  walletsKit: bitcoinWalletsKit,  // BitcoinWalletsKit interface
  network: 'MAINNET',
  defaults?: BitcoinWalletDefaults,
});
```

`BitcoinWalletDefaults` accepts: `defaultFinalize` (whether to finalize PSBTs before returning).

### NEAR (`NearWalletProvider`)

```ts
// Private-key mode
new NearWalletProvider({
  rpcUrl: 'https://…',
  accountId: 'alice.near',
  privateKey: 'ed25519:…',
  defaults?: NearWalletDefaults,
});

// Browser-extension mode
new NearWalletProvider({
  wallet: nearConnector,   // NearConnector from @hot-labs/near-connect
  defaults?: NearWalletDefaults,
});
```

`NearWalletDefaults` accepts: `throwOnFailure`, `waitUntil`, `gasDefault`, `depositDefault`.

***

## 5. React integration — useWalletProvider

`packages/wallet-sdk-react` provides `useWalletProvider` — a hook that reads the chain-appropriate provider from the Zustand store and returns it typed to the correct `I*WalletProvider` interface.

```ts
import { useWalletProvider } from '@sodax/wallet-sdk-react';
import { ChainKeys } from '@sodax/sdk';

// By chain key — return type is narrowed to IEvmWalletProvider | undefined
const walletProvider = useWalletProvider({ xChainId: ChainKeys.ETHEREUM_MAINNET });

// By chain type — return type is IEvmWalletProvider | undefined
const walletProvider = useWalletProvider({ xChainType: 'EVM' });
```

Pass `xChainId` **or** `xChainType`, never both — the hook asserts this at runtime and the overloads enforce it at compile time.

The returned provider is ready to pass directly into any SDK call's `walletProvider` slot:

```ts
const result = await sodax.swaps.createIntent({
  params: { srcChainKey: ChainKeys.ETHEREUM_MAINNET, … },
  raw: false,
  walletProvider,   // typed as IEvmWalletProvider; compile error if chain mismatch
});
```

Wallet providers are populated into the store by:

* **Provider-managed chains (EVM, Solana, Sui)** — Hydrator components (`EvmHydrator`, `SolanaHydrator`, `SuiHydrator`) sync the native SDK state into the store as the sole writers.
* **Non-provider chains (Bitcoin, ICON, Injective, Stellar, NEAR, Stacks)** — providers are created as a side-effect of `setXConnection()` in the store, triggered when a user connects a wallet through `ChainActions`.

Configure which chains are active by passing a `config` prop to `SodaxWalletProvider`:

```ts
import { SodaxWalletProvider } from '@sodax/wallet-sdk-react';
import { ChainKeys } from '@sodax/sdk';

<SodaxWalletProvider config={{
  EVM: {
    chains: {
      [ChainKeys.ARBITRUM_MAINNET]: { rpcUrl: 'https://…' },
      [ChainKeys.BASE_MAINNET]:     { rpcUrl: 'https://…' },
    },
    // EVM adapter also supports:
    walletConnect: { projectId: 'YOUR_WC_PROJECT_ID' }, // enables WalletConnect-based wallets (Fireblocks, Ledger Live, etc.)
    ssr: true, // safe SSR hydration when rendering in Next.js
  },
  SOLANA: { chains: { [ChainKeys.SOLANA_MAINNET]: { rpcUrl: 'https://…' } } },
  BITCOIN: {},   // mount with SDK defaults
}}>
  {children}
</SodaxWalletProvider>
```

Omit a chain-type key entirely to skip mounting that adapter. `useWalletProvider` will return `undefined` for disabled chains and emit a one-time console warning.

***

## 6. Custom implementations

You can implement the SDK interfaces directly without using `@sodax/wallet-sdk-core`. Each interface is defined in `@sodax/sdk` (e.g. `IEvmWalletProvider` in `@sodax/sdk`):

```ts
import type { IEvmWalletProvider, EvmRawTransaction, EvmRawTransactionReceipt } from '@sodax/sdk';
import type { Hash } from 'viem';

class MyCustomEvmProvider implements IEvmWalletProvider {
  readonly chainType = 'EVM' as const;   // required literal — used as runtime discriminant

  async getWalletAddress(): Promise<string> { … }
  async sendTransaction(tx: EvmRawTransaction): Promise<Hash> { … }
  async waitForTransactionReceipt(hash: Hash): Promise<EvmRawTransactionReceipt> { … }
}
```

Requirements for a valid custom implementation:

1. **Declare `readonly chainType = '<CHAIN>' as const`** — the SDK and `useWalletProvider` both read this field for runtime dispatch. The value must exactly match the `chainType` literal of the target interface.
2. **Implement every method on the interface** — TypeScript will flag missing methods at compile time.
3. **No base class required** — extending `BaseWalletProvider` is optional; it only provides the `defaults` storage and merge helpers from `@sodax/wallet-sdk-core`.


# Estimate Gas

`estimateGas` estimates the gas cost of a raw (unsigned) transaction before execution. The typical flow is:

1. Call any service method with `raw: true` to get an unsigned transaction payload.
2. Pass that payload to `estimateGas` to get a chain-specific gas estimate.

`estimateGas` is available as an instance method on three services — all delegate to `SpokeService.estimateGas` internally:

* `sodax.swaps.estimateGas(params)` — for swap / intent transactions
* `sodax.moneyMarket.estimateGas(params)` — for money market transactions
* `sodax.spoke.estimateGas(params)` — for any raw spoke transaction (approvals, deposits, etc.)

***

## Method signature

All three methods share the same signature:

```typescript
public async estimateGas<C extends SpokeChainKey>(
  params: EstimateGasParams<C>,
): Promise<Result<GetEstimateGasReturnType<C>>>
```

`EstimateGasParams<C>` is defined in `packages/sdk/src/shared/types/spoke-types.ts`:

```typescript
export type EstimateGasParams<C extends SpokeChainKey> = {
  tx: TxReturnType<C, true>; // the raw tx payload from a raw: true call
  chainKey: C;               // the chain key the transaction is for
};
```

***

## Return type: `GetEstimateGasReturnType<C>`

The return type is conditional on the source chain's chain family. The concrete type per chain family is:

| Chain family                                                                                                   | Type                                        | Notes                                                                                                   |
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| EVM (Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) | `bigint` (`EvmGasEstimate`)                 | Gas units                                                                                               |
| EVM hub (Sonic)                                                                                                | `bigint` (`EvmGasEstimate`)                 | Gas units                                                                                               |
| Solana                                                                                                         | `number \| undefined` (`SolanaGasEstimate`) | Compute units; `undefined` if simulation unavailable                                                    |
| Stellar                                                                                                        | `bigint` (`StellarGasEstimate`)             | Fee in stroops                                                                                          |
| ICON                                                                                                           | `bigint` (`IconGasEstimate`)                | Step count                                                                                              |
| Sui                                                                                                            | `SuiGasEstimate`                            | Object with `computationCost`, `storageCost`, `storageRebate`, `nonRefundableStorageFee` (all `string`) |
| Injective                                                                                                      | `InjectiveGasEstimate`                      | Object with `gasWanted: number` and `gasUsed: number`                                                   |
| Bitcoin                                                                                                        | `bigint` (`BitcoinGasEstimate`)             | Fee in satoshis                                                                                         |
| NEAR                                                                                                           | `bigint` (`NearGasEstimate`)                | Gas in yoctoNEAR                                                                                        |
| Stacks                                                                                                         | `FeeEstimateTransaction`                    | Object with `low`, `medium`, `high` tiers, each `{ fee: number, fee_rate: number }`                     |

The full type definitions are in `packages/types/src/common/common.ts`.

***

## `raw: true` / `raw: false` — the `WalletProviderSlot<K, Raw>` constraint

`estimateGas` always takes a raw transaction, so you must first get one by calling a service method with `raw: true`. The `WalletProviderSlot<K, Raw>` type in `packages/types/src/common/common.ts` enforces this at compile time:

* `raw: true` → `walletProvider` is **forbidden** (typed as `never`). The method returns an unsigned payload (`TxReturnType<K, true>`, e.g. `EvmRawTransaction`).
* `raw: false` → `walletProvider` is **required** and chain-narrowed to `GetWalletProviderType<K>`. The method broadcasts and returns a tx hash.

Wallet providers are obtained from `packages/wallet-sdk-core` — never constructed manually in caller code.

***

## `ChainKeys.*` constants

All chain constants live under `ChainKeys.*` from `@sodax/sdk`. There are no separate `*_CHAIN_ID` exports.

```typescript
import { ChainKeys } from '@sodax/sdk';

ChainKeys.BSC_MAINNET        // was: BSC_MAINNET_CHAIN_ID
ChainKeys.ETHEREUM_MAINNET   // was: ETHEREUM_MAINNET_CHAIN_ID
ChainKeys.SONIC_MAINNET      // was: SONIC_MAINNET_CHAIN_ID
```

***

## Error handling: `Result<T>`

Every `estimateGas` call returns `Promise<Result<GetEstimateGasReturnType<C>>>` where `Result<T>` is:

```typescript
type Result<T, E = Error | unknown> =
  | { ok: true; value: T }
  | { ok: false; error: E };
```

Always check `result.ok` before accessing `result.value`.

***

## Examples

**Example 1: Estimate gas for a swap intent transaction (EVM spoke)**

```typescript
import { ChainKeys } from '@sodax/sdk';
import type { EstimateGasParams } from '@sodax/sdk';

// Step 1: Get a raw transaction from createIntent
const intentResult = await sodax.swaps.createIntent({
  params: {
    srcChainKey: ChainKeys.BSC_MAINNET,
    dstChainKey: ChainKeys.ARBITRUM_MAINNET,
    srcAddress: '0xYourBscAddress',
    inputToken: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',  // BEP-20 ETH on BSC
    outputToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', // WETH on Arbitrum
    inputAmount: 1_000_000_000_000_000n,
    minOutputAmount: 980_000_000_000_000n,
    deadline: 0n,
  },
  raw: true, // walletProvider must be absent when raw: true
});

if (!intentResult.ok) {
  console.error('Failed to build intent tx:', intentResult.error);
} else {
  const { tx: rawTx } = intentResult.value;

  // Step 2: Estimate gas for the raw transaction
  const gasResult = await sodax.swaps.estimateGas({
    tx: rawTx,
    chainKey: ChainKeys.BSC_MAINNET,
  });

  if (gasResult.ok) {
    // EVM → bigint (gas units)
    console.log('Estimated gas (BSC):', gasResult.value);
  } else {
    console.error('Gas estimation failed:', gasResult.error);
  }
}
```

**Example 2: Estimate gas for a money market supply transaction (EVM spoke)**

```typescript
import { ChainKeys } from '@sodax/sdk';

// Step 1: Get a raw supply transaction
const supplyResult = await sodax.moneyMarket.createSupplyIntent({
  params: {
    srcChainKey: ChainKeys.ETHEREUM_MAINNET,
    // ... other supply params
  },
  raw: true,
});

if (supplyResult.ok) {
  // Step 2: Estimate gas
  const gasResult = await sodax.moneyMarket.estimateGas({
    tx: supplyResult.value,
    chainKey: ChainKeys.ETHEREUM_MAINNET,
  });

  if (gasResult.ok) {
    // EVM → bigint
    console.log('Estimated gas (Ethereum):', gasResult.value);
  } else {
    console.error('Gas estimation failed:', gasResult.error);
  }
}
```

**Example 3: Estimate gas for an approval transaction**

```typescript
import { ChainKeys } from '@sodax/sdk';

// Step 1: Get a raw approval transaction
const approveResult = await sodax.swaps.approve({
  params: {
    srcChainKey: ChainKeys.BSC_MAINNET,
    srcAddress: '0xYourBscAddress',
    inputToken: '0x2170Ed0880ac9A755fd29B2688956BD959F933F8',
    inputAmount: 1_000_000_000_000_000n,
    // ... other swap params
  },
  raw: true,
});

if (approveResult.ok) {
  // Step 2: Estimate gas via spoke directly
  const gasResult = await sodax.spoke.estimateGas({
    tx: approveResult.value,
    chainKey: ChainKeys.BSC_MAINNET,
  });

  if (gasResult.ok) {
    console.log('Estimated gas for approval:', gasResult.value);
  } else {
    console.error('Gas estimation failed:', gasResult.error);
  }
}
```

**Example 4: Non-EVM chain — Sui**

```typescript
import { ChainKeys } from '@sodax/sdk';

const intentResult = await sodax.swaps.createIntent({
  params: {
    srcChainKey: ChainKeys.SUI_MAINNET,
    // ... other intent params
  },
  raw: true,
});

if (intentResult.ok) {
  const gasResult = await sodax.swaps.estimateGas({
    tx: intentResult.value.tx,
    chainKey: ChainKeys.SUI_MAINNET,
  });

  if (gasResult.ok) {
    // Sui → { computationCost, storageCost, storageRebate, nonRefundableStorageFee }
    const { computationCost, storageCost, storageRebate, nonRefundableStorageFee } = gasResult.value;
    console.log('Sui gas estimate:', { computationCost, storageCost, storageRebate, nonRefundableStorageFee });
  }
}
```

**Example 5: Stacks**

```typescript
import { ChainKeys } from '@sodax/sdk';

const rawTxResult = await sodax.swaps.createIntent({
  params: { srcChainKey: ChainKeys.STACKS_MAINNET, /* ... */ },
  raw: true,
});

if (rawTxResult.ok) {
  const gasResult = await sodax.swaps.estimateGas({
    tx: rawTxResult.value.tx,
    chainKey: ChainKeys.STACKS_MAINNET,
  });

  if (gasResult.ok) {
    // Stacks → { low, medium, high } — each { fee: number, fee_rate: number }
    const { low, medium, high } = gasResult.value;
    console.log('Stacks fee tiers:', { low, medium, high });
  }
}
```


# Handle Stellar Trustline

Stellar blockchain requires trustlines to be established before you can receive or hold tokens. This document explains how to handle trustlines when using Stellar with the Sodax SDK across different operations.

## Overview

In Stellar, trustlines are required to:

* **Receive tokens**: You must establish a trustline before receiving any token on Stellar
* **Hold tokens**: You cannot hold tokens without an active trustline

The SDK handles trustlines differently depending on whether Stellar is used as the source chain or destination chain:

* **Source Chain (Stellar)**: The SDK automatically handles trustlines through the standard `isAllowanceValid` and `approve` methods on each feature service (e.g. `sodax.swaps.isAllowanceValid`, `sodax.bridge.isAllowanceValid`).
* **Destination Chain (Stellar)**: You must manually check and establish trustlines before executing operations.

## Architecture

In the v2 SDK there are no caller-constructed spoke provider objects. The `Sodax` facade exposes a `spoke` property of type `SpokeService`, which owns a `stellar: StellarSpokeService` instance. All Stellar-specific logic is accessed through that path.

The Stellar wallet provider is `IStellarWalletProvider` (from `@sodax/wallet-sdk-core`). When calling methods with `raw: false`, the chain-narrowed provider type is resolved from the `srcChainKey` via `GetWalletProviderType<ChainKeys.STELLAR_MAINNET>` — there is no manual spoke provider construction.

## StellarSpokeService Methods

`StellarSpokeService` (accessed via `sodax.spoke.stellar`) provides three methods for managing Stellar trustlines.

### hasSufficientTrustline

Checks if a sufficient trustline exists for a given token and wallet address.

```typescript
import { ChainKeys } from '@sodax/sdk';

const hasTrustline = await sodax.spoke.stellar.hasSufficientTrustline(
  tokenAddress,    // The Stellar token contract ID
  amount,          // The amount you need to receive (bigint, in stroops)
  walletAddress,   // The Stellar wallet address to check
);
```

**Returns:** `Promise<boolean>` — `true` if the trustline exists and has sufficient available limit, `false` otherwise. Native XLM and legacy bnUSD always return `true` (no trustline required).

### requestTrustline

Establishes (or increases) a trustline for a given token. Accepts `RequestTrustlineParams<StellarChainKey, Raw>`:

```typescript
import { ChainKeys } from '@sodax/sdk';
import type { IStellarWalletProvider } from '@sodax/wallet-sdk-core';

// Executed mode (raw: false) — signs and broadcasts immediately
const txHash = await sodax.spoke.stellar.requestTrustline({
  srcChainKey: ChainKeys.STELLAR_MAINNET,
  srcAddress: walletAddress,
  token: tokenAddress,
  amount: amount,
  raw: false,
  walletProvider: stellarWalletProvider, // IStellarWalletProvider
});

// Raw mode (raw: true) — returns unsigned transaction XDR; walletProvider must be omitted
const rawTx = await sodax.spoke.stellar.requestTrustline({
  srcChainKey: ChainKeys.STELLAR_MAINNET,
  srcAddress: walletAddress,
  token: tokenAddress,
  amount: amount,
  raw: true,
});
```

**Returns:** `Promise<TxReturnType<StellarChainKey, Raw>>`

* `raw: false` → transaction hash (`string`)
* `raw: true` → `{ from, to, value, data }` where `data` is the unsigned transaction XDR string

## Source-Chain Trustline Flow (Automated)

When Stellar is the **source chain**, `isAllowanceValid` delegates to `hasSufficientTrustline` and `approve` delegates to `requestTrustline` internally. The exact signatures for swaps and bridge are shown below.

### SwapService

```typescript
import { ChainKeys } from '@sodax/sdk';

// Check if the trustline covers the input amount
const allowanceResult = await sodax.swaps.isAllowanceValid({
  params: {
    srcChainKey: ChainKeys.STELLAR_MAINNET,
    inputToken: tokenAddress,
    inputAmount: amount,
    srcAddress: walletAddress,
    // … other CreateIntentParams fields
  },
  raw: false,
  walletProvider: stellarWalletProvider,
});

if (allowanceResult.ok && !allowanceResult.value) {
  // Establish the trustline
  const approveResult = await sodax.swaps.approve({
    params: {
      srcChainKey: ChainKeys.STELLAR_MAINNET,
      inputToken: tokenAddress,
      inputAmount: amount,
      srcAddress: walletAddress,
      // … other CreateIntentParams fields
    },
    raw: false,
    walletProvider: stellarWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Trustline establishment failed:', approveResult.error.message);
  }
}
```

### BridgeService

```typescript
import { ChainKeys } from '@sodax/sdk';

// Check if the trustline covers the bridge amount
const allowanceResult = await sodax.bridge.isAllowanceValid({
  params: {
    srcChainKey: ChainKeys.STELLAR_MAINNET,
    srcToken: tokenAddress,
    amount: amount,
    srcAddress: walletAddress,
    // … other CreateBridgeIntentParams fields
  },
  raw: false,
  walletProvider: stellarWalletProvider,
});

if (allowanceResult.ok && !allowanceResult.value) {
  const approveResult = await sodax.bridge.approve({
    params: {
      srcChainKey: ChainKeys.STELLAR_MAINNET,
      srcToken: tokenAddress,
      amount: amount,
      srcAddress: walletAddress,
      // … other CreateBridgeIntentParams fields
    },
    raw: false,
    walletProvider: stellarWalletProvider,
  });

  if (!approveResult.ok) {
    console.error('Trustline establishment failed:', approveResult.error.message);
  }
}
```

## Destination-Chain Trustline Flow (Manual)

When Stellar is the **destination chain**, the SDK cannot establish a trustline on your behalf — you must check and establish it before executing any operation that delivers tokens to a Stellar address.

```typescript
import { ChainKeys } from '@sodax/sdk';

async function ensureTrustline(
  tokenAddress: string,
  amount: bigint,
  walletAddress: string,
  stellarWalletProvider: IStellarWalletProvider,
): Promise<void> {
  const hasTrustline = await sodax.spoke.stellar.hasSufficientTrustline(
    tokenAddress,
    amount,
    walletAddress,
  );

  if (!hasTrustline) {
    const txHash = await sodax.spoke.stellar.requestTrustline({
      srcChainKey: ChainKeys.STELLAR_MAINNET,
      srcAddress: walletAddress,
      token: tokenAddress,
      amount: amount,
      raw: false,
      walletProvider: stellarWalletProvider,
    });

    // Wait for the trustline transaction to be confirmed before proceeding
    const receipt = await sodax.spoke.stellar.waitForTransactionReceipt({
      txHash,
      chainKey: ChainKeys.STELLAR_MAINNET,
    });

    if (!receipt.ok || receipt.value.status !== 'success') {
      throw new Error('Trustline transaction failed or timed out');
    }
  }
}
```

## Usage by Operation Type

### Swaps

* **Source Chain (Stellar)**: Trustlines are automatically handled by `sodax.swaps.isAllowanceValid` and `sodax.swaps.approve`.
* **Destination Chain (Stellar)**: Call `ensureTrustline` (above) for the destination token before calling `sodax.swaps.swap`.

### Money Market

* **Source Chain (Stellar)**: Trustlines are automatically handled by `sodax.moneyMarket.isAllowanceValid` and `sodax.moneyMarket.approve`.
* **Destination Chain (Stellar)**: Call `ensureTrustline` for the destination token before executing money market actions.

### Bridge

* **Source Chain (Stellar)**: Trustlines are automatically handled by `sodax.bridge.isAllowanceValid` and `sodax.bridge.approve`.
* **Destination Chain (Stellar)**: Call `ensureTrustline` for the destination token before calling `sodax.bridge.bridge`.

### Migration

* **Source Chain (Stellar)**: Trustlines are automatically handled by the migration service's allowance/approve methods.
* **Destination Chain (Stellar)**: Call `ensureTrustline` for the destination token before executing migration operations.

### Staking

* **Source Chain (Stellar)**: Trustlines are automatically handled by `sodax.staking.isAllowanceValid` and `sodax.staking.approve`.
* **Note**: Staking operations always flow from spoke chains (including Stellar) to the hub chain (Sonic), so Stellar is only ever the source chain for staking.

## Best Practices

1. **Always check trustlines before operations**: Use `hasSufficientTrustline` to verify trustline status before any operation where Stellar is the destination chain.
2. **Set appropriate trustline limits**: When establishing a trustline via `requestTrustline`, the limit is set to the Stellar maximum by default (`Operation.changeTrust` without an explicit limit). Ensure the wallet has sufficient XLM for the transaction fee.
3. **Wait for confirmation**: Always wait for the trustline transaction to be confirmed (use `waitForTransactionReceipt`) before proceeding with the main operation.
4. **Handle errors via `Result<T>`**: All public service methods return `Promise<Result<T>>`. Check `result.ok` before using `result.value`. On failure, inspect `result.error.message` and `result.error.cause`.
5. **Reuse trustlines**: Once established, trustlines persist on the Stellar ledger. You do not need to recreate them for subsequent operations with the same token.

## Common Patterns

### Complete Example: Swap with Stellar Destination

```typescript
import { ChainKeys } from '@sodax/sdk';
import type { IStellarWalletProvider } from '@sodax/wallet-sdk-core';

async function swapWithStellarDestination(
  swapParams: CreateIntentParams<typeof ChainKeys.SOLANA_MAINNET>,
  srcWalletProvider: ISolanaWalletProvider,
  stellarWalletProvider: IStellarWalletProvider,
): Promise<void> {
  const destinationTokenAddress = swapParams.outputToken;
  const minOutputAmount = swapParams.minOutputAmount;
  const dstAddress = swapParams.dstAddress; // Stellar wallet address

  // Step 1: Check and establish trustline if needed
  const hasTrustline = await sodax.spoke.stellar.hasSufficientTrustline(
    destinationTokenAddress,
    minOutputAmount,
    dstAddress,
  );

  if (!hasTrustline) {
    console.log('Establishing trustline...');
    const txHash = await sodax.spoke.stellar.requestTrustline({
      srcChainKey: ChainKeys.STELLAR_MAINNET,
      srcAddress: dstAddress,
      token: destinationTokenAddress,
      amount: minOutputAmount,
      raw: false,
      walletProvider: stellarWalletProvider,
    });

    const receipt = await sodax.spoke.stellar.waitForTransactionReceipt({
      txHash,
      chainKey: ChainKeys.STELLAR_MAINNET,
    });

    if (!receipt.ok || receipt.value.status !== 'success') {
      throw new Error('Trustline transaction failed');
    }

    console.log('Trustline established successfully');
  }

  // Step 2: Proceed with the swap
  const swapResult = await sodax.swaps.swap({
    params: swapParams,
    raw: false,
    walletProvider: srcWalletProvider,
  });

  if (swapResult.ok) {
    console.log('Swap completed:', swapResult.value.intentDeliveryInfo);
  } else {
    console.error('Swap failed:', swapResult.error);
  }
}
```

## Related Documentation

* [Swaps](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md) - Cross-chain intent-based swaps
* [Money Market](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/MONEY_MARKET.md) - Cross-chain lending and borrowing
* [Bridge](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/BRIDGE.md) - Cross-chain token bridging
* [Migration](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/MIGRATION.md) - Token migration
* [Staking](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/STAKING.md) - SODA token staking
* [Architecture Reference](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/ARCHITECTURE_REFACTOR_SUMMARY.md) - Full v2 architecture reference


# Bitcoin Integration

This guide is a step-by-step walkthrough for integrating Bitcoin as a source or destination chain in a SODAX-powered dApp.

## Bitcoin Integration

#### Why Bitcoin is different

Bitcoin's UTXO model and \~10-minute block time make it behave very differently from account-based chains like EVM. A naive integration would require a fresh on-chain Bitcoin transaction for every swap — slow (10+ minutes per operation) and expensive (miner fees per swap). To deliver an EVM-like UX, SODAX routes every Bitcoin operation through a per-user **trading wallet**: a 2-of-2 multisig the user controls jointly with a transaction-signing partner.

The trading wallet amortizes Bitcoin's settlement cost. Users sign in once (BIP322 signature), fund the trading wallet once (\~10-minute on-chain BTC tx), and from then on every swap or bridge executes immediately on the hub chain without waiting for new Bitcoin block confirmations. The multisig commitment is what enables this: because the partner co-signs every outgoing transaction, the SDK can trust the deposit signal before the on-chain BTC transaction confirms — the user retains full custody (the partner cannot move funds alone), but settlement on the hub is decoupled from Bitcoin's block time.

The trade-off is the one-time setup (sign in + fund) and a custody disclosure to end-users (the partner is one of two multisig keys). Once that's done, the dApp UX matches any other spoke chain.

The transaction-signing partner is [**Bound Exchange**](https://bound.exchange) (formerly Radfi). SDK hook names and config field names still use the `Radfi*` prefix for API-compatibility (`useRadfiSession`, `useRadfiWithdraw`, `radfiApiUrl`, etc.), but the live endpoints now resolve to `api.bound.exchange`. For the rest of this guide, "the partner" refers to Bound Exchange.

For the generic intent flow, see [SWAPS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/SWAPS.md) and [BRIDGE.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/sdk/docs/BRIDGE.md). This guide only covers Bitcoin-specific differences.

> **IMPORTANT — Before you start**
>
> * **Local development:** the Bitcoin transaction-signing partner only accepts requests from `http://localhost:1993`. Run your dApp dev server on port `1993` (e.g. `vite --port 1993`). Other localhost ports are rejected at the API layer.
> * **Production release:** before deploying to a production domain, the partner must whitelist your origin. Submit your production URL to the SODAX team ahead of release — non-whitelisted origins are rejected and Bitcoin features will appear broken to end-users.

#### Integration Scope

The trading-wallet flow described in this guide is the only Bitcoin path currently supported end-to-end by the SDK frontend layer (`@sodax/dapp-kit`, `@sodax/wallet-sdk-react`).

A second flow — **direct deposit** from the user's personal wallet without an intermediate trading wallet — exists at the SDK core level but does not yet have frontend hooks or production usage. In that flow, every swap requires its own on-chain Bitcoin transaction and a fresh \~10-minute block confirmation, with miner fees paid each time. It is on the roadmap for users who prefer fully non-custodial execution and infrequent swaps, and will be exposed through a separate service so that future Bitcoin integrations (Lightning, additional partners, alternative custody models) can sit alongside the existing one as parallel services rather than modes of the same provider.

**For builders evaluating Bitcoin today:** only the trading-wallet flow is production-ready. Plan against it; the direct-deposit path will be additive when released and will not break existing integrations.

#### Prerequisites

* An app already using `@sodax/sdk`, `@sodax/dapp-kit`, and `@sodax/wallet-sdk-react`
* React Query installed and `QueryClientProvider` mounted
* A Bitcoin wallet extension installed (Unisat, Xverse, or OKX)

#### Step 1: Enable Bitcoin in your wallet provider

Add `BITCOIN: {}` to `SodaxWalletProvider`. Defaults already point at production endpoints — no API key, no contract addresses, no per-dApp registration.

```tsx
import { SodaxProvider } from "@sodax/dapp-kit";
import { SodaxWalletProvider } from "@sodax/wallet-sdk-react";

<SodaxProvider testnet={false}>
  <QueryClientProvider client={queryClient}>
    <SodaxWalletProvider config={{ BITCOIN: {} }}>
      {children}
    </SodaxWalletProvider>
  </QueryClientProvider>
</SodaxProvider>
```

To override RPC endpoints (canary, signet), pass them under `BITCOIN.chains[ChainKeys.BITCOIN_MAINNET]`:

```tsx
import { ChainKeys } from "@sodax/types";

<SodaxWalletProvider
  config={{
    BITCOIN: {
      chains: {
        [ChainKeys.BITCOIN_MAINNET]: {
          rpcUrl: "https://mempool.space/api",
          radfiApiUrl: "https://api.bound.exchange/api",
          radfiUmsUrl: "https://api.ums.bound.exchange/api",
        },
      },
    },
  }}
>
  {children}
</SodaxWalletProvider>
```

#### Step 2: Connect a Bitcoin wallet

`useBitcoinXConnectors()` returns the wallets installed in the browser. Pass one to `useXConnect`:

```tsx
import { useBitcoinXConnectors } from "@sodax/wallet-sdk-react/xchains/bitcoin";
import { useXConnect } from "@sodax/wallet-sdk-react";

const connectors = useBitcoinXConnectors();
const { mutateAsync: connect } = useXConnect();

await connect(connectors[0]);   // user picks one in your UI
```

After connect, get the wallet provider:

```tsx
import { useWalletProvider } from "@sodax/wallet-sdk-react";
import { ChainKeys } from "@sodax/types";

const walletProvider = useWalletProvider(ChainKeys.BITCOIN_MAINNET);
```

#### Step 3: Sign in and provision the trading wallet

The user signs a BIP322 (or ECDSA) message proving control of their personal Bitcoin address. The server provisions a multisig trading wallet on first sign-in and returns its address. `useRadfiSession` handles the signature, JWT lifecycle, and `localStorage` persistence:

```tsx
import { useRadfiSession } from "@sodax/dapp-kit";

const { isAuthed, tradingAddress, login, isLoginPending } =
  useRadfiSession(walletProvider);

if (!isAuthed) {
  await login();              // user signs in their wallet
}

// tradingAddress is now defined
```

The session is silently refreshed every 5 minutes. After 7 days of inactivity the user must sign in again.

#### Step 4: Fund the trading wallet

The user sends BTC from their personal wallet to the trading-wallet address. This is a normal on-chain BTC transaction — wallet selects UTXOs, computes change, and pays the network fee.

```tsx
import { useFundTradingWallet } from "@sodax/dapp-kit";
import { parseUnits } from "viem";

const { mutateAsync: fund } = useFundTradingWallet();

const txId = await fund({
  amount: parseUnits("0.001", 8),   // 100,000 sats
  walletProvider,
});
```

**Confirmation takes \~10 minutes.** Until confirmed, the deposit appears in `tradingBalance.externalPendingSatoshi`. Surface this pending state in your UI so users do not double-fund.

This is a **one-time wait per top-up** — once the trading wallet has a confirmed balance, subsequent swaps and bridges execute immediately without further Bitcoin block confirmations.

#### Step 5: Build the readiness gate

Before submitting any swap or bridge, verify the user is ready. Compose three hooks into a boolean:

```tsx
import {
  useRadfiSession,
  useTradingWalletBalance,
  useExpiredUtxos,
} from "@sodax/dapp-kit";

const { isAuthed, tradingAddress } = useRadfiSession(walletProvider);
const { data: tradingBalance } = useTradingWalletBalance({
  params: { walletProvider, tradingAddress },
});
const { data: expiredUtxos } = useExpiredUtxos({
  params: { walletProvider, tradingAddress },
});

const isReady =
  isAuthed &&
  !!tradingAddress &&
  (isDestination ||
    ((tradingBalance?.btcSatoshi ?? 0n) > 0n &&
      (!expiredUtxos || expiredUtxos.length === 0)));
```

| Unmet condition           | User action | Hook to invoke                        |
| ------------------------- | ----------- | ------------------------------------- |
| `!isAuthed`               | Sign In     | `useRadfiSession.login` (Step 3)      |
| `tradingBalance === 0n`   | Top Up      | `useFundTradingWallet` (Step 4)       |
| `expiredUtxos.length > 0` | Renew UTXOs | `useRenewUtxos` (see Auxiliary Flows) |

Destination-side readiness (Bitcoin is being received) only requires `isAuthed && tradingAddress`. The trading wallet does not need a balance to receive.

#### Step 6: Build `CreateIntentParams` with Bitcoin-specific overrides

Three fields differ from the standard intent params when source or destination is Bitcoin:

```tsx
import { loadRadfiSession } from "@sodax/dapp-kit";
import { ChainKeys, type CreateIntentParams } from "@sodax/sdk";

const DUST_LIMIT = 546n;

const srcAddress = await sourceWalletProvider.getWalletAddress();   // personal address

const rawMinOutput = (quote.quoted_amount * (100n - slippageBps)) / 100n;

const params = {
  inputToken: src.token.address,
  outputToken: dst.token.address,
  inputAmount: parseUnits(amount, src.token.decimals),

  // dstAddress: trading wallet when destination is Bitcoin, personal otherwise
  dstAddress:
    dst.chain === ChainKeys.BITCOIN_MAINNET
      ? loadRadfiSession(destAccount.address)?.tradingAddress
      : destAccount.address,

  // srcAddress: personal wallet (the SDK derives the trading wallet internally)
  srcAddress,

  // minOutputAmount: clamp to 546 sats when destination is BTC
  minOutputAmount:
    dst.chain === ChainKeys.BITCOIN_MAINNET && dst.token.symbol === "BTC"
      ? rawMinOutput > DUST_LIMIT ? rawMinOutput : DUST_LIMIT
      : rawMinOutput,

  srcChainKey: src.chain,
  dstChainKey: dst.chain,
  deadline: BigInt(Math.floor(Date.now() / 1000) + 60 * 5),
  allowPartialFill: false,
  solver: "0x0000000000000000000000000000000000000000",
  data: "0x",
} satisfies CreateIntentParams;
```

**Three rules to remember:**

1. `srcAddress` = personal wallet (not trading). The SDK derives the trading address internally for hub wallet derivation.
2. `dstAddress` = trading wallet (not personal) when destination is Bitcoin. Forgetting this is the most common Bitcoin integration bug — the swap submits successfully but settles to an address the user cannot interpret.
3. `minOutputAmount` ≥ 546 satoshis when destination is BTC. The SDK throws an invariant otherwise.

#### Step 7: Submit the swap

Use `sodax.swaps.swap()` (or the equivalent bridge call) — same as any other spoke chain:

```tsx
const swapResult = await sodax.swaps.swap({
  params,
  walletProvider,   // IBitcoinWalletProvider — the personal-wallet provider
});

if (!swapResult.ok) {
  // handle error — see SWAPS.md Error Handling
  return;
}

const { solverExecutionResponse, intent, intentDeliveryInfo } = swapResult.value;
```

**Constraints when source is Bitcoin:**

* `walletProvider` must be `IBitcoinWalletProvider` — the SDK throws `Invalid wallet provider for chain key` otherwise.
* Raw mode (`raw: true`) is **not supported**. Only `raw: false` execution. Server-side flows that emit raw transactions cannot use Bitcoin as source.
* The session access token is auto-refreshed inside `swap` / `createIntent` — no manual token handling needed.

#### Auxiliary Flows

These are not part of the swap path but commonly required.

**Withdraw from the trading wallet**

PSBT round-trip: server builds, user signs, server co-signs and broadcasts.

```tsx
import { useRadfiWithdraw } from "@sodax/dapp-kit";

const { mutateAsync: withdraw } = useRadfiWithdraw();

const result = await withdraw({
  amount: "50000",              // sats as string
  tokenId: "0:0",               // native BTC
  withdrawTo: "bc1q...",        // any valid BTC address
  walletProvider,               // IBitcoinWalletProvider
});
// { txId, fee }
```

For a "Max" button that nets out fees:

```tsx
import { useSodaxContext, loadRadfiSession } from "@sodax/dapp-kit";

const { sodax } = useSodaxContext();
const session = loadRadfiSession(walletAddress);

const max = await sodax.spoke.bitcoin.radfi.getMaxWithdrawable(
  { userAddress: walletAddress, amount, tokenId: "0:0", withdrawTo },
  session.accessToken,
);
// { maxSatsAmt, feeRate, fee }
```

**Renew expired UTXOs**

Trading-wallet UTXOs have a time-bounded script. `useExpiredUtxos` polls every 60s and includes UTXOs within two weeks of expiry:

```tsx
import { useExpiredUtxos, useRenewUtxos } from "@sodax/dapp-kit";

const { data: expiredUtxos } = useExpiredUtxos({
  params: { walletProvider, tradingAddress },
});
const { mutateAsync: renew } = useRenewUtxos();

if (expiredUtxos?.length) {
  await renew({
    txIdVouts: expiredUtxos.map((u) => u.txidVout ?? `${u.txid}:${u.vout}`),
    walletProvider,
  });
}
```

#### Special Considerations

* **Custody.** Funds in the trading wallet sit in a 2-of-2 multisig: the user holds one key, the Bitcoin transaction-signing partner holds the other. The partner's co-signing authority on each trading wallet expires three months after creation. Disclose this in your UI copy.
* **No `approve()` step.** Bitcoin uses a UTXO model — `useSwapAllowance` / `useSwapApprove` short-circuit when source is Bitcoin. Hide the approve button in your UI.
* **Chain ID is a string** (`'bitcoin'`, not numeric). Any chain-id branching, switch statement, or dictionary key in your code must handle this.
* **Wallet switching.** `useRadfiSession` resets `isAuthed` immediately when `walletProvider` changes. Trading-wallet addresses persist in `localStorage` keyed by personal wallet address — they do not transfer across wallets.
* **Address types.** P2WPKH and P2TR sign with BIP322; P2SH and P2PKH sign with ECDSA. The SDK auto-dispatches; builders do not need to choose. Xverse can switch between Taproot and Segwit at runtime via `setAddressPurpose()` (requires reconnect).
* **API availability.** Trading-wallet operations (sign in, withdraw, UTXO renewal) depend on the partner's API. Personal-wallet → trading-wallet top-up via `walletProvider.sendBitcoin` works without the API. Plan a degraded-state UI for outages.

#### Reference Implementation

A complete working example is in [`apps/demo/src/components/swaps/SwapCard.tsx`](https://github.com/icon-project/sodax-sdks/blob/main/apps/demo/src/components/swaps/SwapCard.tsx) — it composes the readiness gate, applies the three intent-param overrides, and gates the swap button. The demo's default dev script binds port 3000 (`pnpm dev:demo`); for end-to-end Bitcoin testing against the partner API, start it on the whitelisted port instead — e.g. `pnpm --filter sodax-demo-v2 exec vite --port 1993 --host` (see the IMPORTANT note at the top of this guide).

***

## Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.sodax.com/developers/how-to/bitcoin-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language. The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.


# Is SODAX on Testnet?

SODAX is mainnet-only. Here's why there is no testnet, and how to test and explore SODAX safely on mainnet instead.

**Short answer: no.** The SODAX SDKs and protocol run on **mainnet only**. There is no SODAX testnet, and the SDK has no testnet mode or network toggle.

## Is SODAX available on a testnet?

No. Every chain the SDK supports is a mainnet. The canonical chain list in `@sodax/types` (`ChainKeys`) contains only `*_MAINNET` entries — there are no testnet chain configs, no testnet RPC endpoints, and no `testnet` switch anywhere in the SDK.

{% hint style="info" %}
Any `testnet` / `TESTNET` value you may see in wallet configuration (for example a Bitcoin or Sui wallet provider's `network` field) is an **upstream parameter of that wallet library** — it does not enable a SODAX testnet.
{% endhint %}

## Why is SODAX not available on testnets?

SODAX is a cross-chain **intents** system. Swaps, lending, borrowing, staking and bridging are settled through the Sonic hub against **real, deployed contracts** and **live, solver-provided liquidity**. Those primitives have no meaning without production liquidity and deployed spoke contracts, so there is no parallel testnet deployment to point the SDK at.

For how settlement works, see [Intents](https://docs.sodax.com/developers/technical-overview/intents) and the [Technical Overview](https://docs.sodax.com/developers/technical-overview).

## Can I integrate SODAX on testnet?

No — integrate against mainnet. To build and test safely:

* **Use small amounts on mainnet.** Every flow is real, so start with minimal values while you wire things up.
* **Drive flows headlessly** with the private-key wallet providers — see the mainnet smoke scripts in [`apps/node`](https://github.com/icon-project/sodax-sdks/tree/main/apps/node) to exercise integrations without a browser wallet.
* **Point your AI coding agent at the SODAX skills** for per-chain, v2-correct integration guidance — see [AI Integration](https://docs.sodax.com/developers/ai-integration).

## Keep exploring

* [Make a Swap](https://docs.sodax.com/developers/how-to/how_to_make_a_swap) — end-to-end swap walkthrough.
* [Configure the SDK](https://docs.sodax.com/developers/how-to/configure_sdk) — initialization and configuration.
* [SDKs overview](https://docs.sodax.com/developers/packages) — the full package stack (Foundation, Connection, Experience).
* [FAQ](https://docs.sodax.com/developers/faq) — common questions.


# AI Integration

Every @sodax/\* package on npm ships AI-readable docs at ai-exported/. Point Cursor, Claude Code, Copilot, or another coding agent at those files for v2-correct SDK code on the first try.

SODAX ships agent-native documentation as [`@sodax/skills`](https://github.com/icon-project/sodax-sdks/tree/main/packages/skills) — four mode-gated skills (one per `@sodax/*` SDK package, each bundling `integration/knowledge/` for new v2 code and `migration-v1-to-v2/knowledge/` for v1→v2 porting). The goal is v2-correct code on the first try instead of stale training-data APIs.

LLM training data drifts; public docs at [docs.sodax.com](https://docs.sodax.com) are for humans. Agents only use what you install or attach. For what’s in the bundle (skills, knowledge layout, routing table), see [packages/skills/README.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/skills/README.md).

## Pick a setup

| You are…                                                                                                     | Do this                                                                            | Entry point                                                                                                                                                                                                                  |
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Using Claude Code, Cursor, Copilot, Codex, etc. with the [skills CLI](https://github.com/vercel-labs/skills) | `npx skills@latest add icon-project/sodax-sdks/packages/skills`                    | Auto-discovered — each `.<agent>/skills/sodax-<pkg>/SKILL.md` is loaded by the agent via its frontmatter `description`. No `AGENTS.md` lands in the install path; SKILL.md mode-gates internally (integration vs migration). |
| Any project; npm from the registry                                                                           | `pnpm add -D @sodax/skills`                                                        | `node_modules/@sodax/skills/AGENTS.md`                                                                                                                                                                                       |
| Unreleased docs; app lives **inside** the sodax-sdks pnpm workspace                                          | `pnpm add -D @sodax/skills@workspace:*`                                            | `node_modules/@sodax/skills/AGENTS.md`                                                                                                                                                                                       |
| Unreleased docs; sodax-sdks is a **sibling repo, submodule, or separate clone**                              | `pnpm add -D @sodax/skills@file:../sodax-sdks/packages/skills` (not `workspace:*`) | `node_modules/@sodax/skills/AGENTS.md`                                                                                                                                                                                       |
| Sodax-sdks clone on disk, no install                                                                         | Point your agent rules at the checkout                                             | `/path/to/sodax-sdks/packages/skills/AGENTS.md`                                                                                                                                                                              |
| Web chat (ChatGPT, Claude.ai, etc.)                                                                          | Attach files to the conversation                                                   | `AGENTS.md` + one relevant `skills/sodax-<pkg>/SKILL.md`                                                                                                                                                                     |

**Canonical entry point:** [`AGENTS.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/skills/AGENTS.md) — tool-neutral router (shipped on npm). [`CLAUDE.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/skills/CLAUDE.md) in `packages/skills/` is optional and **git-only** (not in the npm package); use it when you maintain sodax-sdks or want maintainer layout context. You can also paste a one-line pointer into your own project `CLAUDE.md`.

## Install

### skills CLI (recommended for IDE agents)

From your consumer repo root:

```bash
npx skills@latest add icon-project/sodax-sdks/packages/skills
```

The CLI detects your tool and installs the four mode-gated skill directories into the conventional discovery path (e.g. `.claude/skills/sodax-<pkg>/`, `.cursor/skills/sodax-<pkg>/`). Each skill's `SKILL.md` carries trigger phrases for both integration and migration in its frontmatter `description` — the agent picks the right skill (and the SKILL.md body's mode gate picks the right subtree) automatically. **Note:** the CLI does **not** copy the package-level `AGENTS.md`; agents auto-discover skills via `SKILL.md` frontmatter. **Upgrade (CLI only):** when you bump `@sodax/*` SDK packages, re-run the same `npx skills@latest add …` command — the CLI refreshes from GitHub; it does not follow a `@sodax/skills` entry in `package.json`.

Useful flags:

* `-a <agent>` — target a specific agent (e.g. `-a cursor`).
* `-g, --global` — install into your user-global agent directory instead of the project.

### npm from the registry

```bash
pnpm add -D @sodax/skills
# or: npm install --save-dev @sodax/skills
```

Published files: `skills/` (each skill ships its `integration/knowledge/` + `migration-v1-to-v2/knowledge/` subtrees inside), `AGENTS.md`, `.claude-plugin/`, `README.md` — not `CLAUDE.md`. Wire your agent to `node_modules/@sodax/skills/AGENTS.md` (see below). **Upgrade (npm / `file:` / `workspace:`):** `pnpm add -D @sodax/skills@<version>` to match your `@sodax/*` release (package is published on `@sodax/skills@*.*.*` git tags).

### Local / monorepo

`@workspace:*` only resolves when your app is a package **in the same pnpm workspace** as `packages/skills` (i.e. your `package.json` is listed in sodax-sdks’s `pnpm-workspace.yaml`). If sodax-sdks is a sibling directory, git submodule, or second clone, use `@file:…` instead — `workspace:*` will fail with “not found in workspace”.

```bash
# Your app is packages/my-dapp (or similar) inside the sodax-sdks repo
pnpm add -D @sodax/skills@workspace:*

# Your app is a separate repo; sodax-sdks checkout is next to it (adjust path)
pnpm add -D @sodax/skills@file:../sodax-sdks/packages/skills
```

Then use `node_modules/@sodax/skills/AGENTS.md` like the registry install.

### Path-only (no package install)

If you keep a sodax-sdks clone next to your app, reference `packages/skills/AGENTS.md` in rules or `@`-mentions. Prefer the skills CLI when your agent supports it so skills land in the native directory.

## Wire your agent

**If you used the skills CLI**, no manual wiring is required: each agent reads `SKILL.md` files from its skills directory and routes via the frontmatter `description` automatically. Skip to *Prompt naturally*.

**For npm, `file:`, `workspace:`, or path-only installs**, paste this into project rules (adapt `<ENTRY_POINT>` to your install path):

```markdown
Before writing or changing Sodax (@sodax/*) code, read and follow:
`<ENTRY_POINT>/AGENTS.md`
Load only the skills it routes you to; each SKILL.md mode-gates internally
(integration for new v2 code, migration for v1 → v2 porting).
```

| Tool                | How                                                                                                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Claude Code**     | Prefer the skills CLI (skills auto-discovered from `.claude/skills/`). For npm install, add the block above to project `CLAUDE.md` with `node_modules/@sodax/skills/AGENTS.md`. |
| **Cursor**          | Prefer `npx skills@latest add ... -a cursor` (skills auto-discovered from `.cursor/skills/`). For npm install, add `.cursor/rules` with the same pointer.                       |
| **Codex / Copilot** | Prefer the skills CLI. For npm install, use a rules file or repo `AGENTS.md` symlink/copy pointing at the installed bundle.                                                     |
| **Web UIs**         | Attach `AGENTS.md`; add one `SKILL.md` if the task is narrow (e.g. swap only).                                                                                                  |

For non-CLI installs, your agent must read **`AGENTS.md` from your install path** (e.g. `node_modules/@sodax/skills/AGENTS.md`) — not the live `main` branch on GitHub. That file routes by intent; each skill links into its own `integration/knowledge/` or `migration-v1-to-v2/knowledge/` subtree as needed. Don't read knowledge top-to-bottom. **Web chat with no install:** attach `AGENTS.md` (and any needed `SKILL.md` files) from your checkout or a Git ref that matches your SDK version. **Browse only:** [router source on GitHub (`main`)](https://github.com/icon-project/sodax-sdks/blob/main/packages/skills/AGENTS.md).

## Prompt naturally

Once wired, describe the task in plain language.

> "Swap 100 USDC on Ethereum for SOL on Solana using `@sodax/sdk`."

For v1 → v2 ports, say something like “migrate my project to Sodax v2” — each skill auto-gates into its **migration mode** when it sees v1 fingerprints (`useSpokeProvider`, `*_MAINNET_CHAIN_ID`, `useXWagmiStore`, etc.), and `AGENTS.md` (or frontmatter discovery, on CLI installs) handles skill selection.

## Tips

* **Keep agent docs in sync when you upgrade `@sodax/*`** — docs should match the SDKs you ship. **skills CLI:** re-run `npx skills@latest add icon-project/sodax-sdks/packages/skills`. **npm / local package install:** bump `@sodax/skills` (e.g. `pnpm add -D @sodax/skills@1.2.3`). Using both CLI and npm is optional; if you use only the CLI, you do not need `@sodax/skills` in `package.json`.
* **Start from `AGENTS.md`** (or, on CLI installs, let the agent pick a skill via its frontmatter) — not random files under `integration/knowledge/` or `migration-v1-to-v2/knowledge/`.
* **Don’t add `@sodax/types` as a separate dependency** — types are re-exported from `@sodax/sdk`.

## Further reading and feedback

* Bundle overview: [packages/skills/README.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/skills/README.md)
* Router (agents): [packages/skills/AGENTS.md](https://github.com/icon-project/sodax-sdks/blob/main/packages/skills/AGENTS.md)

Wrong output despite the skills? That’s a doc bug — [open an issue](https://github.com/icon-project/sodax-sdks/issues) with the prompt and what the agent generated.


# FAQ

#### 1. Which chains does SODAX support?

SODAX runs on a hub-and-spoke network of **mainnet** chains. Sonic is the hub; spokes span EVM chains (Ethereum, Arbitrum, Base, BSC, Optimism, Polygon, Avalanche, HyperEVM, Lightlink, Redbelly, Kaia, Hedera) and non-EVM chains (Solana, Sui, Stellar, ICON, Injective, NEAR, Stacks, Bitcoin). Reference any chain via `ChainKeys.*`, which — together with the backend config — is the source of truth. The legacy `*_CHAIN_ID` constants are deprecated.

Full list with relay IDs: [Relayer API endpoints](https://docs.sodax.com/developers/deployments/relayer_api_endpoints).

#### 2. Is SODAX available on a testnet?

No — SODAX is **mainnet-only**. Every chain in the canonical `@sodax/types` config is a `*_MAINNET` entry; there are no testnet chain configs, RPC endpoints, or a network toggle, because SODAX's cross-chain intents settle against real deployed contracts and live solver liquidity that only exist on mainnet. To build and test, use small amounts on mainnet and drive flows with the private-key wallet providers (see `apps/node`) rather than a testnet.

Full explanation and next steps: [Is SODAX on Testnet?](https://docs.sodax.com/developers/how-to/testnet).

#### 3. What is the difference between the hub and a spoke chain?

Sonic is the hub. All swap, lend, borrow, bridge and stake actions are coordinated by hub contracts (Intents, Asset Manager, Hub Wallet Factory). Spoke chains hold user funds and act as deposit and execution venues. The SODAX relayer carries cross-chain messages between them.

Deep dives: [Technical Overview](https://docs.sodax.com/developers/technical-overview), [Asset Manager](https://docs.sodax.com/developers/technical-overview/asset-manager), [Generalized Messaging Protocol](https://docs.sodax.com/developers/technical-overview/generalized-messaging-protocol).

#### 4. Do I need to call `sodax.initialize()` before using the SDK?

Not strictly. The constructor uses defaults packaged with the SDK version you installed. `await sodax.initialize()` fetches the latest tokens and chains from the backend API. Recommended for production, optional for prototypes. If it fails, the SDK falls back to packaged defaults rather than throwing.

See [Configure SDK](https://docs.sodax.com/developers/how-to/configure_sdk).

#### 5. How do I override the hub RPC or contract addresses?

Pass a partial `hub` block to the constructor:

```tsx
new Sodax({ hub: { rpcUrl: '<https://rpc.soniclabs.com>' } })
```

Read the merged config from `sodax.instanceConfig.hub`. Note that `sodax.config.getHubChainConfig()` returns the packaged snapshot, not your overrides.

Full config reference: [Configure SDK](https://docs.sodax.com/developers/how-to/configure_sdk).

### SDK behaviour

#### 6. Why don't SDK methods throw?

Every public method returns `Result<T, E>` shaped as `{ ok: true, value }` or `{ ok: false, error }`. Do not wrap SDK calls in try/catch. Check `result.ok` first, then discriminate on `result.error.code`.

The pattern is canonical across modules: [Backend API](https://docs.sodax.com/developers/packages/foundation/sdk/tooling-modules/backend_api).

#### 7. How should I handle errors properly?

Switch on the narrow `error.code` union (for example `VALIDATION_FAILED`, `RELAY_TIMEOUT`, `TX_SUBMIT_FAILED`). Never branch on `error.message`, it is human-readable and may change. The original lower-level error is preserved on `error.cause`, structured metadata on `error.context`. Use the exported guards (`isSwapError`, `isStakeOrchestrationError`, `isMigrateOrchestrationError`, `isPartnerError`) in dapp code for cross-bundle type safety.

Per-module code tables: [Swaps](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps), [Money Market](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market), [Bridge](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/bridge), [Staking](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/staking), [Migration](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/migration).

#### 8. What should I do when a swap returns `RELAY_TIMEOUT`?

The spoke transaction landed but the hub packet has not arrived within the timeout window. The relay may still complete. Persist the spoke tx hash and poll the relayer API. Do not re-submit from the user side.

Error semantics: [Make a Swap](https://docs.sodax.com/developers/how-to/how_to_make_a_swap), [Relayer API endpoints](https://docs.sodax.com/developers/deployments/relayer_api_endpoints).

#### 9. What does `TX_SUBMIT_FAILED` mean?

The critical case. The spoke tx landed but the relay submission itself failed. Funds may be in flight. Persist the user's input plus spoke tx hash and retry submission against the relay API. Do not retry the user-facing transaction.

Full code reference: [Make a Swap](https://docs.sodax.com/developers/how-to/how_to_make_a_swap).

### Swaps and intents

#### 10. What is the difference between `swap()`, `createIntent()` and `createLimitOrder()`?

`swap()` is the recommended end-to-end path. It handles approval, intent creation, relay submission and solver notification automatically (signed execution only).

`createIntent()` is the lower-level primitive and supports both signed and raw modes (`raw: true` for custom signing flows).

`createLimitOrder()` produces an intent with no deadline. The user must cancel it manually.

Full method list: [Swaps (Solver)](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps).

#### 11. How do I get a swap quote and feed it into `minOutputAmount`?

Call `sodax.swaps.getQuote(payload)` with `token_src`, `token_dst`, source and destination `ChainKeys`, an amount in the token's smallest unit, and `quote_type: 'exact_input'`. Use `quoted_amount` from the response to set `minOutputAmount` on the intent.

Walkthrough with code: [Make a Swap](https://docs.sodax.com/developers/how-to/how_to_make_a_swap).

#### 12. Can I cancel an intent?

Yes. Call `cancelIntent(intent)` on the Intents contract. The caller must be the creator, or the deadline must have passed. Intents with pending fills cannot be cancelled. Limit orders (`deadline = 0`) always require manual cancellation.

Contract interface: [Intents](https://docs.sodax.com/developers/technical-overview/intents).

#### 13. Are intents trustless? Can a solver run off with funds?

No. Solvers cannot exit with user funds. They lock collateral in the `IntentFiller` contract on the destination spoke when filling. The hub is the source of truth for intent state and settlement, spokes act only as escrow and execution venues. Settlement reconciles cross-chain via the relay.

Detailed flow: [Intents](https://docs.sodax.com/developers/technical-overview/intents).

### Lend, borrow, bridge, stake

#### 14. Which actions actually need on-chain approval?

EVM spokes: `supply` and `repay` approve the Asset Manager contract.

Sonic hub: `supply` and `repay` approve the user's hub router.

Stellar: `supply`, `repay`, `withdraw`, `borrow` all check and establish trustlines.

Borrow and withdraw on EVM and hub do not require approval. Most non-EVM chains require no on-chain approval at all.

Full matrix: [Money Market](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/money_market).

#### 15. How is the bridge different from a swap?

Bridge moves the same asset across chains using the hub vault, with no price discovery. Swap routes through the solver network for cross-chain price execution. The bridge supports three directions: spoke to hub, hub to spoke, and spoke to spoke.

See [Bridge](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/bridge) and [Swaps (Solver)](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/swaps).

#### 16. How do I estimate gas across different chain families?

Build a raw tx with `raw: true` from any `createIntent`, `createSupplyIntent`, `approve`, etc. Then call the matching module's `estimateGas({ tx, chainKey })`. The return shape depends on the chain family.

EVM, ICON, Stellar, Bitcoin, NEAR return a `bigint`. Sui returns `{ computationCost, storageCost, storageRebate, nonRefundableStorageFee }`. Injective returns `{ gasWanted, gasUsed }`. Stacks returns `{ low, medium, high }` fee tiers. Solana returns `number | undefined` compute units.

Examples per chain: [Estimate Gas](https://docs.sodax.com/developers/how-to/estimate_gas).

#### 17. Can I stake SODA from a non-EVM chain like Sui or Stellar?

Yes. Every `StakingService` method accepts any `SpokeChainKey` as the source. If you pass `ChainKeys.SONIC_MAINNET`, the spoke and hub tx hashes are identical. Approval is required only on EVM spokes, the hub, and Stellar.

See [Staking](https://docs.sodax.com/developers/packages/foundation/sdk/functional-modules/staking).

### Monetization, integration, tooling

#### 18. How do partner fees work and how do I claim them?

Set `swaps.partnerFee`, `moneyMarket.partnerFee` and `bridge.partnerFee` independently on `SodaxConfig`. `getQuote` deducts the swap partner fee from the input amount before forwarding to the solver, so no fee field appears in the request payload. Claim accrued fees via `sodax.partners.feeClaim*` methods, which return `Result<T, PartnerError>`.

Setup and claim flows: [Monetize SDK](https://docs.sodax.com/developers/how-to/monetize_sdk).

#### 19. When do I need the `IntentRelayChainId` versus `ChainKeys`?

The relay API identifies chains by a numeric `IntentRelayChainId` (for example `BASE_MAINNET = 30n`, `SOLANA_MAINNET = 1n`, `BITCOIN_MAINNET = 627463n`). The SDK converts internally. You only need `getIntentRelayChainId(chainKey)` when constructing raw relay requests directly, which is advanced usage.

Full mapping: [Relayer API endpoints](https://docs.sodax.com/developers/deployments/relayer_api_endpoints).

#### 20. How do I wire SODAX into my AI coding agent (Claude Code, Cursor, Codex)?

From your project root run `npx skills@latest add icon-project/sodax-sdks/packages/skills`. The CLI detects your tool (Claude Code, Cursor, Codex, Copilot) and installs `AGENTS.md` plus per-feature `SKILL.md` files into the conventional location. Point your agent rules at the installed `AGENTS.md`, not the GitHub main branch, so version drift does not corrupt the routing.

See [AI Integration](https://docs.sodax.com/developers/ai-integration).

#### 21. What is hub wallet abstraction and when do I touch it directly?

The hub generates a deterministic user wallet on Sonic for every spoke address. For spoke chains with limited calldata, the SDK supports hashed calls: send a 32-byte `keccak256` payload to the hub, then execute the stored call later with the same data. The relayer handles this in normal flows. You only touch it directly when building custom orchestration or recovering stuck cross-chain executions.

See [Hub Wallet Abstraction](https://docs.sodax.com/developers/technical-overview/hub-wallet-abstraction).

***

Workshop notes: rewrite, cut, or expand any of these. Once locked, the answers can drop straight into the existing FAQ page on [docs.sodax.com](http://docs.sodax.com) (currently a TODO stub).


