For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

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.

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.

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.

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: truewalletProvider 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:

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:

Result<T> — No Throws Across Service Boundaries

Every public async method returns Promise<Result<T, 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:

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

Discrimination example

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).

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>

CreateLimitOrderParams<K>

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


Get Fees

Partner Fee

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


Get Swap Deadline

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

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

Raw Approval Transaction

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


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.


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

Raw transaction


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)

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

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.

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.

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):


Submit Intent to Relay API

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


Get Intent Submit Tx Extra Data

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


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.


Get Intent

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


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.

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.


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.


Get Intent Hash

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


Error Handling Examples

The full reference is in 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:

Handling createIntent Errors

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

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:

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

Last updated