@avail-project/nexus-core
v2.2.0
Published
Nexus headless SDK for cross-chain transactions
Readme
@avail-project/nexus-core
A headless TypeScript SDK for cross-chain operations, token bridging, swapping, and unified balance management. Built for backends, CLIs, and custom UI integrations.
Table of Contents
- Prerequisites
- Installation
- Quick Start
- Core Features
- Configuration
- API Reference
- Hooks & Callbacks
- Events & Steps
- Error Handling
- TypeScript Reference
- Utilities
- Smart Optimizations
- Analytics
- Supported Networks & Tokens
- Common Pitfalls
- Resources
Prerequisites
- Node.js
>=18.0.0 - npm
>=9.0.0
Installation
npm install @avail-project/nexus-coreQuick Start
import { createNexusClient } from '@avail-project/nexus-core';
// 1) Create and initialize the client
const client = createNexusClient({ network: 'mainnet' });
await client.initialize();
await client.setEVMProvider(window.ethereum);
// 2) Fetch balances
const balances = await client.getBalancesForBridge();
// 3) Execute a bridge
const result = await client.bridge(
{
toTokenSymbol: 'USDC',
toAmountRaw: 100_000_000n, // 100 USDC (6 decimals)
toChainId: 137, // Polygon
},
{
onEvent: (event) => {
if (event.type === 'status') {
console.log('Bridge status:', event.status);
}
if (event.type === 'plan_preview') {
console.log('Bridge steps:', event.plan.steps);
}
if (event.type === 'plan_progress') {
console.log('Step progress:', event.step.type, event.state);
}
},
hooks: {
onIntent: ({ intent, allow, deny }) => {
if (userConfirmsIntent(intent)) allow();
else deny();
},
onAllowance: ({ sources, allow, deny }) => {
if (userConfirmsAllowance(sources)) allow(['min']);
else deny();
},
},
}
);
console.log('Bridge complete:', result.intentExplorerUrl);Core Features
- Cross-chain bridging — Move tokens seamlessly across 14+ chains
- Cross-chain swaps — Execute EXACT_IN and EXACT_OUT swaps between any supported networks via LiFi, Bebop, and Fibrous aggregators
- Unified balances — Aggregate user assets and balances across all connected chains
- Contract execution — Call smart contracts with automatic bridging or swap funding logic
- Composite operations — Bridge + Execute or Swap + Execute, orchestrated as two sequenced operations (funding, then execution) — not a single atomic transaction
- Transaction simulation — Estimate gas, fees, and required approvals before sending
- Real-time progress — Typed event system for plan previews, step-by-step progress, and status updates
- Complete testnet coverage — Full multi-chain test environment
- Comprehensive utilities — Address, token, and chain helpers with tree-shakeable imports
Configuration
Client Configuration Options
import { createNexusClient } from '@avail-project/nexus-core';
const client = createNexusClient({
// Network: 'mainnet' | 'canary' | 'testnet' | custom NetworkConfig
network: 'mainnet',
// Enable debug logging
debug: false,
// Optional: override the domain used in the ephemeral-key sign message and its
// localStorage cache key. Defaults to `window.location.host` in the browser
// and `'localhost'` in non-browser environments. Set this for mobile wallets
// or native shells where the auto-detected host isn't meaningful.
domain: 'app.example.com',
// Optional: pin every bridge (and the bridge leg of a swap) to the Mayan
// provider, skipping the middleware's provider-selection call and asserting
// the destination is Mayan-supported. Defaults to false (the SDK picks the
// provider per the usual threshold logic).
forceMayan: false,
// Analytics configuration (see Analytics section)
analytics: {
enabled: true,
privacy: {
anonymizeWallets: true,
anonymizeAmounts: true,
},
},
// Developer timing instrumentation
devTiming: {
enabled: true,
captureNetworkTiming: true,
},
});Network Configuration
// Mainnet
const mainnetClient = createNexusClient({ network: 'mainnet' });
// Canary (mainnet-class pre-production environment)
const canaryClient = createNexusClient({ network: 'canary' });
// Testnet
const testnetClient = createNexusClient({ network: 'testnet' });
// Custom network config (advanced)
const customClient = createNexusClient({
network: {
MIDDLEWARE_HTTP_URL: 'https://your-middleware.example.com',
INTENT_EXPLORER_URL: 'https://your-explorer.example.com',
NETWORK_HINT: 'mainnet',
},
});API Reference
Initialization & Lifecycle
initialize()
Fetches deployment data (chains, tokens, vault contracts) from the middleware. Must be called once before any chain-dependent operations.
await client.initialize();setEVMProvider(provider)
Connect or update the EVM-compatible wallet provider.
await client.setEVMProvider(window.ethereum);| Parameter | Type | Description |
|-----------|------|-------------|
| provider | EthereumProvider | EIP-1193 compatible provider (MetaMask, WalletConnect, etc.) |
With an injected wallet you can pass window.ethereum directly. With a wallet library (wagmi, RainbowKit, Web3Modal, …), obtain the EIP-1193 provider from the active connector first — e.g. const provider = await connector.getProvider() — then pass it in. If your library's provider type doesn't structurally match EthereumProvider, cast it (provider as EthereumProvider); the SDK only uses the standard request() surface.
destroy()
Flush analytics and clean up resources. Call when the client is no longer needed.
client.destroy();client.chainList
After initialize() resolves, client.chainList exposes the deployed chain catalogue. Use it for contract-aware lookups instead of bundling chain/token constants in your app.
type ChainListType = {
chains: Chain[]; // all deployed chains
getChainByID(id: number): Chain;
getTokenInfoBySymbol(chainID: number, symbol: string): TokenInfo;
getTokenByAddress(chainID: number, address: Hex): TokenInfo;
getTokenByCurrencyId(chainID: number, currencyId: number): TokenInfo;
getNativeToken(chainID: number): TokenInfo;
getChainAndTokenFromSymbol(chainID: number, tokenSymbol: string): { chain: Chain; token: TokenInfo; isNativeToken: boolean };
getChainAndTokenByAddress(chainID: number, address: Hex): { chain: Chain; token: TokenInfo; isNativeToken: boolean };
getVaultContractAddress(chainID: number): Hex;
};
// `getChainByID` returns a Chain with name, native currency, and block-explorer info:
const chain = client.chainList.getChainByID(8453);
const explorerBase = chain.blockExplorers?.default?.url; // e.g. build a tx link
const nativeSymbol = chain.nativeCurrency.symbol; // e.g. "ETH"
// `getTokenInfoBySymbol` / `getTokenByAddress` return TokenInfo ({ contractAddress, symbol, decimals, logo, ... })
const usdc = client.chainList.getTokenInfoBySymbol(8453, 'USDC');Chain and ChainListType are exported types; chain.blockExplorers is optional ({ default: { name, url } } | undefined), so guard it before building explorer links.
client.hasEvmProvider
Boolean getter that returns true once setEVMProvider() has resolved. Useful for guarding methods that require a connected wallet.
isSupportedChain(chainId)
Returns true if the configured deployment knows about the given chain ID. Accepts a plain number.
client.isSupportedChain(8453); // trueClient Lifecycle Notes
The Nexus client is disposable and does not store durable user state. initialize() and setEVMProvider() are independent — initialize() only loads deployment data from the middleware, and setEVMProvider() only attaches a wallet. You can call them in either order, but most apps run both at startup so chain-dependent calls and wallet-dependent calls both work.
setEVMProvider() short-circuits when called with the same provider instance it already holds, so it cannot be used to swap accounts on a single provider. On account change, build a fresh client and re-run initialize() + setEVMProvider().
Balance Operations
getBalancesForBridge()
Get user's token balances across all supported chains for bridge operations.
const assets = await client.getBalancesForBridge();
// Returns TokenBalance[] - array of assets with per-chain breakdown
// [
// {
// symbol: 'USDC',
// name: 'USDC',
// balance: '1250.50', // Total across all chains
// value: '1250.50', // USD value (string)
// decimals: 6,
// logo: 'https://...',
// currencyId: 1,
// chainBalances: [ // Per-chain balances
// {
// balance: '500.00',
// value: '500.00',
// symbol: 'USDC',
// chain: { id: 1, name: 'Ethereum', logo: '...' },
// contractAddress: '0xa0b86991...',
// decimals: 6,
// universe: 0,
// },
// ...
// ],
// },
// // ... more assets (ETH, USDT, etc.)
// ]getBalancesForSwap()
Get the user's swap-sourced balances across supported chains. This returns the same
TokenBalance[] surface as getBalancesForBridge(), but it is sourced from the swap balance
pipeline used by swap preflight and routing.
const assets = await client.getBalancesForSwap();
console.log(assets[0]?.symbol);
console.log(assets[0]?.chainBalances);Use this when you want to inspect the balances the SDK will consider for swap planning, while still
working with the same grouped TokenBalance[] shape as bridge balances.
TokenBalance:
type TokenBalance = {
name: string; // Display label (e.g. "USDC/USDM")
symbol: string; // Majority symbol by chain count
logo: string; // Token logo URL
balance: string; // Total balance (human-readable)
value: string; // USD value (string for precision)
decimals: number;
currencyId?: number; // Required on BridgeTokenBalance
chainBalances: ChainBalance[];
};
type ChainBalance = {
balance: string;
value: string; // USD value (string)
symbol: string;
chain: { id: number; name: string; logo: string };
contractAddress: `0x${string}`;
decimals: number;
universe: Universe;
};Bridge Operations
bridge(params, options?)
Bridge tokens from one or more source chains to a destination chain.
const result = await client.bridge(
{
toTokenSymbol: 'USDC',
toAmountRaw: 100_000_000n, // 100 USDC
toChainId: 137,
recipient: '0x...', // Optional: defaults to connected wallet
sources: [1, 42161], // Optional: auto-selected if omitted
toNativeAmountRaw: 100000n, // Optional: native token to supply on destination
},
{
onEvent: (event) => {
// Handle progress events
},
hooks: {
onIntent: ({ allow }) => allow(),
onAllowance: ({ allow }) => allow(['min']),
},
fillTimeoutMinutes: 2, // Default: 2
}
);BridgeParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toTokenSymbol | string | Yes | Token symbol: 'ETH', 'USDC', 'USDT' |
| toAmountRaw | bigint | Yes | Amount in smallest unit (e.g., 6 decimals for USDC) |
| toChainId | number | Yes | Destination chain ID |
| recipient | Hex | No | Recipient address (defaults to connected wallet) |
| sources | number[] | No | Specific source chains to use (auto-selected if omitted) |
| toNativeAmountRaw | bigint | No | Native token amount to supply on destination chain |
BridgeResult:
type BridgeResult = {
intentExplorerUrl: string;
sourceTxs: Array<{
chain: { id: number; name: string; logo: string };
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
}>;
intent: BridgeIntent;
};simulateBridge(params)
Simulate a bridge operation to estimate fees and preview the intent.
const simulation = await client.simulateBridge({
toTokenSymbol: 'USDC',
toAmountRaw: 100_000_000n,
toChainId: 137,
});
console.log('Estimated fees:', simulation.intent.fees);
console.log('Source chains:', simulation.intent.selectedSources);BridgeSimulationResult:
type BridgeSimulationResult = {
intent: BridgeIntent;
token: TokenInfo;
};Transfer Operations
bridgeAndTransfer(params, options?)
Bridge tokens and send to a specific recipient address.
const result = await client.bridgeAndTransfer(
{
toTokenSymbol: 'USDC',
toAmountRaw: 50_000_000n, // 50 USDC
toChainId: 42161, // Arbitrum
recipient: '0x742d35Cc6634C0532925a3b8D4C9db96c4b4Db45',
sources: [1], // Optional
},
{
onEvent: (event) => console.log(event),
hooks: {
onIntent: ({ allow }) => allow(),
onAllowance: ({ allow }) => allow(['min']),
},
}
);TransferParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toTokenSymbol | string | Yes | Token symbol |
| toAmountRaw | bigint | Yes | Amount in smallest unit |
| toChainId | number | Yes | Destination chain ID |
| recipient | Hex | Yes | Recipient address |
| sources | number[] | No | Specific source chains |
TransferResult:
type TransferResult = {
approval?: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
execute: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
} & (
| { bridgeSkipped: false; bridgeResult: BridgeResult }
| { bridgeSkipped: true; bridgeResult?: undefined }
);simulateBridgeAndTransfer(params)
Simulate a bridge-and-transfer operation.
const simulation = await client.simulateBridgeAndTransfer({
toTokenSymbol: 'USDC',
toAmountRaw: 50_000_000n,
toChainId: 42161,
recipient: '0x...',
});Execute Operations
execute(params, options?)
Execute a smart contract call on a destination chain.
const result = await client.execute(
{
toChainId: 1,
to: '0xContractAddress',
data: '0x...', // Encoded function call
value: 0n, // ETH value to send
tokenApproval: {
toTokenSymbol: 'USDC',
amount: 1_000_000n,
spender: '0xSpenderAddress',
},
// Advanced options
gasPrice: 'medium', // 'low' | 'medium' | 'high'
waitForReceipt: true,
receiptTimeout: 60000,
requiredConfirmations: 1,
}
);ExecuteParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toChainId | number | Yes | Target chain ID |
| to | Hex | Yes | Contract address |
| data | Hex | No | Encoded function call data |
| value | bigint | No | Native token value to send |
| gas | bigint | No | Gas limit override |
| gasPrice | 'low' \| 'medium' \| 'high' | No | Gas price tier strategy |
| tokenApproval | { toTokenSymbol, amount, spender } | No | Token approval to send before execution |
| enableTransactionPolling | boolean | No | Poll for transaction inclusion via RPC instead of relying solely on receipt waiting |
| transactionTimeout | number | No | Polling timeout in milliseconds |
| waitForReceipt | boolean | No | Wait for transaction receipt before resolving |
| receiptTimeout | number | No | Receipt-wait timeout in milliseconds |
| requiredConfirmations | number | No | Required block confirmations before resolving |
ExecuteResult:
type ExecuteResult = {
approval?: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
execute: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
chainId: number;
confirmations?: number;
gasUsed?: string;
effectiveGasPrice?: string;
};simulateExecute(params)
Simulate contract execution to estimate gas. The combined gas units and total cost include both the optional approval and the execute transaction.
const simulation = await client.simulateExecute({
toChainId: 1,
to: '0x...',
data: '0x...',
});
console.log('Gas units (approval + execute):', simulation.estimatedGasUnits);
console.log('Total cost (wei):', simulation.estimatedTotalCost);
if (simulation.feeParams.type === 'eip1559') {
console.log('Max fee per gas:', simulation.feeParams.maxFeePerGas);
console.log('Max priority fee per gas:', simulation.feeParams.maxPriorityFeePerGas);
} else {
console.log('Legacy gas price:', simulation.feeParams.gasPrice);
}ExecuteSimulation:
type ExecuteFeeParams =
| { type: 'eip1559'; maxFeePerGas: bigint; maxPriorityFeePerGas: bigint }
| { type: 'legacy'; gasPrice: bigint };
type ExecuteSimulation = {
feeParams: ExecuteFeeParams;
/** Combined gas units across approval (if required) and execution transaction. */
estimatedGasUnits: bigint;
/** Combined estimated cost across approval (if required) and execution transaction. */
estimatedTotalCost: bigint;
};bridgeAndExecute(params, options?)
Orchestrates two distinct operations in sequence — not a single atomic transaction:
- Bridge (conditional) — funds the shortfall on the destination chain. Automatically skipped when the destination already holds enough of the token (
result.bridgeSkipped === true). - Execute + approval (execute always, approval optional) — the contract call, preceded by an optional token approval, is always sent from the user's connected wallet on the destination chain.
Because the two steps run one after the other, they succeed or fail independently. This is not atomic: if the execute fails after a bridge, the bridged funds remain in the user's wallet on the destination chain (they are not rolled back).
const result = await client.bridgeAndExecute(
{
toTokenSymbol: 'USDC',
toAmountRaw: 100_000_000n,
toChainId: 1,
sources: [8453], // Optional
execute: {
to: '0xDeFiProtocol',
data: '0x...', // deposit() call
tokenApproval: {
toTokenSymbol: 'USDC',
amount: 100_000_000n,
spender: '0xDeFiProtocol',
},
},
},
{
onEvent: (event) => {
if (event.type === 'status') {
// preparing | intent_building | awaiting_approval | executing | completed
}
if (event.type === 'plan_preview' || event.type === 'plan_confirmed') {
// event.plan.steps: typed composite bridge + execute plan
}
if (event.type === 'plan_progress') {
// bridge or execute progress
}
},
onIntent: ({ allow, deny, refresh, intent }) => {
console.log('Bridge required:', intent.bridgeRequired);
// even if bridge is skipped, allow() still gates execution
allow();
},
}
);
if (result.bridgeSkipped) {
console.log('Used existing balance on destination');
} else {
console.log('Bridge explorer:', result.bridgeResult.intentExplorerUrl);
}BridgeAndExecuteParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toTokenSymbol | string | Yes | Token to bridge |
| toAmountRaw | bigint | Yes | Amount to bridge (raw integer units) |
| toChainId | number | Yes | Destination chain |
| sources | number[] | No | Specific source chains to draw from (auto-selected if omitted) |
| execute | Omit<ExecuteParams, 'toChainId'> | Yes | Contract execution params (destination chain is inherited from the top-level toChainId) |
| enableTransactionPolling | boolean | No | Poll for inclusion via RPC after submission |
| transactionTimeout | number | No | Polling timeout in milliseconds |
| waitForReceipt | boolean | No | Wait for the execute receipt before resolving |
| receiptTimeout | number | No | Receipt-wait timeout in milliseconds |
| requiredConfirmations | number | No | Required block confirmations before resolving |
| recentApprovalTxHash | string | No | Hash of an approval submitted earlier in the same UI flow; lets the SDK skip a redundant approval |
BridgeAndExecuteResult:
type BridgeAndExecuteResult = {
approval?: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
execute: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
} & (
| { bridgeSkipped: false; bridgeResult: BridgeResult }
| { bridgeSkipped: true; bridgeResult?: undefined }
);simulateBridgeAndExecute(params)
Simulate bridge-and-execute to estimate costs.
const simulation = await client.simulateBridgeAndExecute({
toTokenSymbol: 'USDC',
toAmountRaw: 100_000_000n,
toChainId: 1,
execute: { to: '0x...', data: '0x...' },
});
console.log('Bridge simulation:', simulation.bridgeSimulation);
console.log('Execute simulation:', simulation.executeSimulation);BridgeAndExecuteSimulationResult:
type BridgeAndExecuteSimulationResult = {
bridgeSimulation: BridgeSimulationResult | null; // null if bridge not needed
executeSimulation: ExecuteSimulation;
};Swap Operations
Swap APIs use raw integer units (bigint) for on-chain amounts and token contract addresses (not symbols).
swapWithExactIn(input, options?)
Swap tokens specifying the exact input amount from explicit sources.
const result = await client.swapWithExactIn(
{
sources: [
{ chainId: 10, amountRaw: 1_000_000n, tokenAddress: '0xUSDC...' },
{ chainId: 42161, amountRaw: 500_000n, tokenAddress: '0xUSDC...' },
],
toChainId: 8453,
toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
},
{
onEvent: (event) => {
if (event.type === 'plan_progress') {
console.log('Swap progress:', event.step.type, event.state);
}
},
hooks: {
onIntent: ({ intent, allow }) => {
console.log('Swap intent:', intent);
allow();
},
},
slippageTolerance: 0.005, // 0.5% default
}
);SwapExactInParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| sources | Array<{ chainId, tokenAddress, amountRaw? }> | No | Source tokens and amounts (raw integer units). Omit to use all available holdings. |
| toChainId | number | Yes | Destination chain |
| toTokenAddress | Hex | Yes | Output token address |
For Exact In, the approval-time SwapIntent.destination.amount and .value are the route's
expected output, not its slippage-protected minimum. Expected source-swap output sizes the
bridge, expected bridge delivery sizes the destination swap, and the destination quote's expected
output reaches the existing intent fields. Executable calldata, approvals, aggregator selection,
and retry guards still use protected quote amounts. At execution the SDK must read the COT that
actually reached the destination wrapper and resize the destination swap before its first dispatch;
the read/resize has three attempts total, then the swap fails with destination-step context and runs
the normal stranded-COT cleanup. The resized quote must consume the complete measured COT balance;
an under-consuming quote is rejected rather than returning settlement-token dust to the user.
swapWithExactOut(input, options?)
Swap tokens specifying the exact output amount desired.
const result = await client.swapWithExactOut(
{
toChainId: 42161,
toTokenAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', // USDC on Arbitrum
toAmountRaw: 100_000_000n, // 100 USDC (6 decimals)
// Optional: also fund destination native gas
toNativeAmountRaw: 100_000_000_000_000n,
// Optional: restrict route planning to specific source tokens/chains
sources: [{ chainId: 8453, tokenAddress: '0x...' }],
},
{
onEvent: (event) => {
if (event.type === 'status') {
console.log('Swap status:', event.status);
}
if (event.type === 'plan_preview') {
console.log('Swap plan:', event.plan.steps);
console.log('Has bridge:', event.plan.hasBridge);
console.log('Has destination swap:', event.plan.hasDestinationSwap);
}
},
hooks: {
onIntent: ({ allow, deny, refresh, intent }) => {
console.log('Swap intent:', intent);
allow();
},
},
}
);SwapExactOutParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| sources | Array<{ chainId, tokenAddress }> | No | Restrict source chains/tokens for quote routing |
| toChainId | number | Yes | Destination chain |
| toTokenAddress | Hex | Yes | Output token address |
| toAmountRaw | bigint | Yes | Exact output amount desired (raw integer units) |
| toNativeAmountRaw | bigint | No | Optional native gas amount for destination chain |
When eligible sources are already on the destination chain, EXACT_OUT can use the Path A fast path: one atomic, bridge-less batch swaps directly to the requested token and optional native gas amounts. It targets those raw outputs exactly, groups same-token funding into one authorization and transfer, and safely re-quotes stale or definitively reverted batches without blindly replaying ambiguous ones.
SwapResult:
type SwapResult = {
sourceSwaps: ChainSwap[];
intentExplorerUrl: string;
destinationSwap: ChainSwap | null;
intent: SwapIntent;
};
type ChainSwap = {
chainId: number;
swaps: Swap[];
txHash: Hex;
};
type Swap = {
inputAmount: bigint;
inputContract: Hex;
inputDecimals: number;
outputAmount: bigint;
outputContract: Hex;
outputDecimals: number;
};Swap operation options:
| Option | Type | Description |
|--------|------|-------------|
| onEvent | (event: SwapEvent) => void | Receive status, plan preview, and plan progress updates |
| hooks.onIntent | (data: OnIntentHookData) => void | Review/approve the swap intent before execution |
| slippageTolerance | number | Optional slippage override (default 0.005, i.e. 0.5%) |
calculateMaxForSwap(input)
Calculate the maximum amount that can be swapped to a destination token across all available sources. Useful for populating a "Max" button before calling swapWithExactIn. Max calculation deliberately uses protected minimum outputs at every stage (with the existing safety haircut), while reusing the normal quote sequence without extra quote requests.
const max = await client.calculateMaxForSwap({
toChainId: 8453,
toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
});
console.log(`Max swappable: ${max.maxAmount} ${max.symbol}`);
console.log('Sources used:', max.sources);You can also restrict which source chains/tokens are considered:
const max = await client.calculateMaxForSwap({
toChainId: 8453,
toTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
sources: [
{ chainId: 10, tokenAddress: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85' },
],
});SwapMaxParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toChainId | number | Yes | Destination chain ID |
| toTokenAddress | Hex | Yes | Output token address |
| sources | Array<{ chainId, tokenAddress }> | No | Restrict which source tokens to consider |
SwapMaxResult:
type SwapMaxResult = {
toChainId: number;
toTokenAddress: Hex;
maxAmount: string; // Human-readable decimal string
maxAmountRaw: bigint; // Raw amount suitable for toAmount in swapWithExactOut
symbol: string;
decimals: number;
sources: {
chainId: number;
tokenAddress: Hex;
symbol: string;
decimals: number;
amount: string; // Human-readable portion from this source
}[];
};calculateMaxForBridge(input)
Calculate the maximum amount that can be bridged to a destination token across all same-currency holdings on other chains. Useful for populating a "Max" button before calling bridge.
The max is sized against the provider the bridge will actually use: the summed bridge amount is checked against the Mayan threshold (the same decision the real bridge makes), and the receivable max is computed for that provider — Nexus backs out deposit/fulfillment/protocol fees, Mayan sums the per-leg minReceived. A max(3%, $3) safety haircut is applied so the suggested amount survives fee drift before execution. The returned provider tells you which path was used.
const max = await client.calculateMaxForBridge({
toChainId: 8453,
toTokenSymbol: 'USDC',
});
console.log(`Max bridgeable: ${max.maxAmount} ${max.symbol} via ${max.provider}`);
console.log('Sources used:', max.sources);
// Feed straight into a bridge:
await client.bridge({ toChainId: 8453, toTokenSymbol: 'USDC', toAmountRaw: max.maxAmountRaw });You can also restrict which source chains are considered:
const max = await client.calculateMaxForBridge({
toChainId: 8453,
toTokenSymbol: 'USDC',
sources: [10, 42161], // only Optimism + Arbitrum balances
});BridgeMaxParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toChainId | number | Yes | Destination chain ID |
| toTokenSymbol | string | Yes | Destination token symbol |
| sources | number[] | No | Restrict which source chain IDs to consider |
BridgeMaxResult:
type BridgeMaxResult = {
toChainId: number;
toTokenSymbol: string;
provider: 'nexus' | 'mayan'; // Provider the max was sized against
maxAmount: string; // Human-readable decimal string
maxAmountRaw: bigint; // Raw amount suitable for toAmountRaw in bridge()
symbol: string;
decimals: number;
sources: {
chainId: number;
tokenAddress: Hex;
symbol: string;
decimals: number;
amount: string; // Human-readable portion from this source
}[];
};swapAndExecute(params, options?)
Orchestrates two distinct operations in sequence — not a single atomic transaction:
- Swap (conditional) — funds the shortfall on the destination chain. Automatically skipped when the destination already holds enough of the token (
result.swapSkipped === true). - Execute + approval (execute always, approval optional) — the contract call, preceded by an optional token approval, is always sent from the user's connected wallet on the destination chain.
Because the two steps run one after the other, they succeed or fail independently. This is not atomic: if the execute fails after a swap, the swapped funds remain in the user's wallet on the destination chain (they are not rolled back).
const result = await client.swapAndExecute(
{
toChainId: 42161,
toTokenAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831',
toAmountRaw: 100_000_000n,
execute: {
to: '0x3333333333333333333333333333333333333333',
data: '0xdeadbeef',
gas: 100_000n,
value: 0n,
tokenApproval: {
toTokenAddress: '0xaf88d065e77c8cc2239327c5edb3a432268e5831',
amount: 100_000_000n,
spender: '0x3333333333333333333333333333333333333333',
},
},
},
{
onEvent: (event) => {
if (event.type === 'status') {
// preparing | route_building | awaiting_approval | executing | completed
}
},
onIntent: ({ allow, deny, refresh, intent }) => {
console.log('Swap required:', intent.swapRequired);
allow();
},
}
);
console.log(result.swapSkipped);
console.log(result.swapResult);
console.log(result.execute.txHash);SwapAndExecuteParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| toChainId | number | Yes | Destination chain ID |
| toTokenAddress | Hex | Yes | Token address on destination |
| toAmountRaw | bigint | Yes | Token amount needed (raw integer units) |
| sources | Array<{ chainId, tokenAddress }> | No | Restrict source tokens |
| execute | SwapExecuteParams | Yes | Contract execution params |
SwapExecuteParams:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| to | Hex | Yes | Contract address |
| data | Hex | No | Encoded function call |
| gas | bigint | Yes | Gas limit |
| value | bigint | No | Native token value (wei) |
| gasPrice | 'low' \| 'medium' \| 'high' | No | Gas price strategy |
| tokenApproval | { toTokenAddress: Hex, amount: bigint, spender: Hex } | No | Token approval before execution |
SwapAndExecuteResult:
type SwapAndExecuteResult = {
approval?: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
execute: {
txHash: Hex;
txExplorerUrl: string;
receipt?: TransactionReceipt;
};
} & (
| { swapSkipped: false; swapResult: SwapResult }
| { swapSkipped: true; swapResult?: undefined }
);Intent Management
listIntents(params?)
Retrieve the connected wallet's historical intents. Page size is fixed at 20 records — paginate by incrementing page.
import { IntentStatus } from '@avail-project/nexus-core';
const result = await client.listIntents({
page: 1,
status: IntentStatus.Fulfilled, // 'created' | 'deposited' | 'fulfilled' | 'expired'
});
console.log(result.total);
for (const intent of result.intents) {
console.log(intent.requestHash, intent.status);
console.log(intent.destinationChain.name);
for (const dest of intent.destinations) {
console.log(dest.token.symbol, dest.amount);
}
for (const src of intent.sources) {
console.log(src.chain.name, src.token.symbol, src.amount, '(fee:', src.fee, ')');
}
}type ListIntentsParams = {
page?: number;
status?: IntentStatus;
};
type ListIntentsResult = {
intents: IntentRecord[];
total: number;
};
type IntentRecord = {
requestHash: Hex;
explorerUrl: string;
status: IntentStatus;
solver: Hex | null;
createdAt?: number;
updatedAt?: number;
expiry: number;
recipientAddress: Hex;
destinationChain: { id: number; name: string; logo: string; universe: 'EVM' | 'TRON' | 'FUEL' | 'SVM' };
destinations: Array<{
token: { contractAddress: Hex; symbol: string; name: string; logo: string; decimals: number };
amount: string;
amountRaw: bigint;
}>;
sources: Array<{
chain: { id: number; name: string; logo: string; universe: 'EVM' | 'TRON' | 'FUEL' | 'SVM' };
amount: string;
amountRaw: bigint;
fee: string;
feeRaw: bigint;
token: { contractAddress: Hex; symbol: string; name: string; logo: string; decimals: number };
}>;
};Hooks & Callbacks
Hooks are essential for building interactive UIs. They allow users to review and approve operations before execution.
Displayed Intent Freshness
The intent object passed to onIntent is the pricing preview shown in the app's confirmation UI. If that UI waits for interactive confirmation, keep this displayed intent fresh until the user approves or denies it.
| Operation | Freshness guidance |
|-----------|--------------------|
| swapWithExactIn(), swapWithExactOut(), swapAndExecute() when a swap is required | Most underlying swap quotes are valid for roughly 30 seconds. After each refresh completes, wait 20 seconds before starting the next one. |
| bridge(), bridgeAndTransfer(), bridgeAndExecute() when a bridge is required | Bridge quote deadlines vary by provider. After each refresh completes, wait 15 seconds before starting the next one. |
These are conservative UI-refresh cadences, not guaranteed protocol expiry values. This guidance applies only while displaying the pre-approval intent from onIntent; an already signed or submitted intent has separate expiry semantics. The SDK's execution-time requoting is also separate from keeping the confirmation UI current.
Call refresh() without arguments to rebuild using the current source selection, or pass sources to change the selection. Always render the returned intent, avoid overlapping refresh calls, and stop refreshing before allow() or deny(). If a refresh is in flight, disable confirmation until it settles so allow() cannot race an older preview.
Use an async timeout rather than setInterval. The next timeout must be scheduled only after refresh() settles: if refreshing takes 5–10 seconds, a 20-second async timeout waits the full 20 seconds after completion instead of starting the next refresh only 10–15 seconds later.
type ClearAsyncInterval = () => void;
function setAsyncInterval(
cb: () => Promise<void>,
interval: number,
onError: (error: unknown) => void
): ClearAsyncInterval {
let active = true;
let timer: ReturnType<typeof setTimeout>;
const tick = async () => {
if (!active) return;
try {
await cb();
} catch (error) {
onError(error);
} finally {
if (active) timer = setTimeout(tick, interval);
}
};
timer = setTimeout(tick, interval);
return () => {
active = false;
clearTimeout(timer);
};
}
await client.swapWithExactOut(input, {
hooks: {
onIntent: ({ intent, refresh, allow, deny }) => {
let refreshing = false;
renderSwapIntent(intent);
const stopRefreshing = setAsyncInterval(async () => {
refreshing = true;
setIntentRefreshing(true); // disable Confirm in the UI
try {
renderSwapIntent(await refresh());
} finally {
refreshing = false;
setIntentRefreshing(false);
}
}, 20_000, showIntentRefreshError);
showIntentActions({
confirm: () => {
if (refreshing) return;
stopRefreshing();
allow();
},
cancel: () => {
stopRefreshing();
deny();
},
});
},
},
});Use 15_000 instead for an interactive bridge intent. Immediate auto-approval (onIntent: ({ allow }) => allow()) does not need a refresh loop. Composite operations expose the same refresh() callback through their top-level onIntent hook.
Intent Hook
Called when the SDK needs user approval for a bridge/transfer intent. Passed via options.hooks.onIntent for bridge operations.
await client.bridge(params, {
hooks: {
onIntent: async ({ intent, allow, deny, refresh }) => {
// Display intent details to user
console.log('Source chains:', intent.selectedSources);
console.log('Destination:', intent.destination);
console.log('Fees:', intent.fees);
console.log('Total from sources:', intent.sourcesTotal);
// Optionally refresh with different source chains
const refreshedIntent = await refresh([8453, 42161]);
console.log('Refreshed intent:', refreshedIntent);
// User interaction
if (userApproves) {
allow();
} else {
deny(); // Throws USER_DENIED_INTENT error
}
},
},
});OnIntentHookData:
type OnIntentHookData = {
allow: () => void;
deny: () => void;
intent: BridgeIntent;
refresh: (selectedSources?: number[]) => Promise<BridgeIntent>;
};Interactive approval: after refreshing the bridge intent displayed in the confirmation UI, wait 15 seconds before starting the next refresh. Render the intent returned by
refresh()and stop the async timeout before callingallow()ordeny(). See Displayed Intent Freshness.
BridgeIntent Structure:
type BridgeIntent = {
// Bridge provider moving the funds cross-chain
provider: 'nexus' | 'mayan';
// Selected sources (chains funds are pulled from)
selectedSources: Array<{
amount: string;
amountRaw: bigint;
chain: { id: number; name: string; logo: string };
token: { decimals: number; symbol: string; logo: string; contractAddress: Hex };
value: string;
}>;
// All available sources (before selection)
availableSources: Array<{ /* same shape as selectedSources */ }>;
// Destination details
destination: {
amount: string;
amountRaw: bigint;
chain: { id: number; name: string; logo: string };
token: { decimals: number; symbol: string; logo: string; contractAddress: Hex };
value: string;
nativeAmount: string; // Human-readable native token amount
nativeAmountRaw: bigint; // Raw native token amount
nativeAmountValue: string; // USD value of native amount
nativeAmountInToken: string; // Native gas expressed in bridge token units
nativeToken: { decimals: number; symbol: string; logo: string; contractAddress: Hex };
};
// Fee breakdown
fees: {
caGas: string; // Chain abstraction gas fee
protocol: string; // Protocol fee
solver: string; // Solver fee
total: string; // Total fees
totalValue: string; // Total fees in USD
};
// Total amount from all sources
sourcesTotal: string;
sourcesTotalValue: string;
};Allowance Hook
Called when token approval is needed before a transaction. Passed via options.hooks.onAllowance for bridge operations.
await client.bridge(params, {
hooks: {
onAllowance: ({ sources, allow, deny }) => {
// Display approval request to user
sources.forEach((source) => {
console.log(`Chain: ${source.chain.name}`);
console.log(`Token: ${source.token.symbol}`);
console.log(`Current allowance: ${source.allowance.current}`);
console.log(`Required minimum: ${source.allowance.minimum}`);
});
// Approve with options:
allow(['min']); // Approve exact minimum needed
allow(['max']); // Approve unlimited (type(uint256).max)
allow([1000000n]); // Approve specific amount
allow(['min', 'max']); // Different per source (by index)
// Or deny
deny(); // Throws USER_DENIED_ALLOWANCE error
},
},
});OnAllowanceHookData:
type OnAllowanceHookData = {
allow: (amounts: Array<'max' | 'min' | bigint | string>) => void;
deny: () => void;
sources: AllowanceHookSources;
};
type AllowanceHookSources = Array<{
allowance: {
current: string; // Current allowance (human-readable)
currentRaw: bigint; // Current allowance (raw)
minimum: string; // Minimum required (human-readable)
minimumRaw: bigint; // Minimum required (raw)
};
chain: {
id: number;
logo: string;
name: string;
};
token: {
contractAddress: Hex;
decimals: number;
logo: string;
name: string;
symbol: string;
};
}>;Swap Intent Hook
Called when user approval is needed for a swap operation. Passed via options.hooks.onIntent for swapWithExactIn() and swapWithExactOut(). The refresh() callback optionally takes a new sources list to re-quote against a different set of source tokens.
await client.swapWithExactOut(input, {
hooks: {
onIntent: async ({ intent, allow, deny, refresh }) => {
console.log('Swap from:', intent.sources);
console.log('Swap to:', intent.destination);
console.log('Bridge fees:', intent.feesAndBuffer.bridge); // null when no bridge needed
console.log('Bridge provider:', intent.bridgeProvider); // 'nexus' | 'mayan' | null
console.log('Slippage buffer:', intent.feesAndBuffer.buffer);
// Refresh to get an updated quote, optionally restricting sources
const refreshedIntent = await refresh([
{ chainId: 8453, tokenAddress: '0x...' },
]);
console.log('Refreshed swap intent:', refreshedIntent);
if (userApproves) {
allow();
} else {
deny();
}
},
},
});SwapIntent:
type SwapIntent = {
destination: {
amount: string; // Exact In: expected output; Exact Out: requested output
value?: string; // Exact In: expected output USD value when available
chain: { id: number; logo: string; name: string };
token: { contractAddress: Hex; decimals: number; symbol: string };
gas: {
amount: string;
value?: string;
token: { contractAddress: Hex; decimals: number; symbol: string };
};
};
feesAndBuffer: {
buffer: string;
bridge: { caGas: string; protocol: string; solver: string; total: string } | null;
};
bridgeProvider: 'nexus' | 'mayan' | null; // bridge moving COT cross-chain; null when no bridge
sources: Array<{
amount: string;
value?: string;
chain: { id: number; logo: string; name: string };
token: { contractAddress: Hex; decimals: number; symbol: string };
}>;
};
type OnIntentHookData = {
allow: () => void;
deny: () => void;
intent: SwapIntent;
refresh: (sources?: Array<{ chainId: number; tokenAddress: Hex }>) => Promise<SwapIntent>;
};Interactive approval: most swap quotes are valid for roughly 30 seconds. After refreshing the swap intent displayed in the confirmation UI, wait 20 seconds before starting the next refresh. Use an async timeout so sequential routing does not shorten that wait. See Displayed Intent Freshness.
Composite Intent Hooks (Bridge + Execute / Swap + Execute)
bridgeAndExecute() and swapAndExecute() use a top-level onIntent hook (not nested under hooks). The intent data is a composite type that includes the execution requirement, available balances, and whether a bridge/swap is actually needed.
When the composite intent reports bridgeRequired: true or swapRequired: true, keep it fresh with the corresponding 15-second bridge or 20-second swap cadence until approval.
Bridge and Execute Intent
await client.bridgeAndExecute(params, {
onIntent: ({ intent, allow, deny, refresh }) => {
// Execution requirement (always present)
console.log('Contract:', intent.executeRequirement.to);
console.log('Token needed:', intent.executeRequirement.token.amount, intent.executeRequirement.token.symbol);
console.log('Gas estimate:', intent.executeRequirement.gas.estimatedGasUnits);
// Available balances on destination
console.log('Token on-chain:', intent.available.token.amount);
console.log('Gas on-chain:', intent.available.gas.amount);
if (intent.bridgeRequired) {
// Bridge is needed — shortfall and bridge intent available
console.log('Token shortfall:', intent.shortfall.token.amount);
console.log('Gas shortfall:', intent.shortfall.gas.amount);
console.log('Bridge sources:', intent.bridge.selectedSources);
console.log('Bridge fees:', intent.bridge.fees.total);
// Optionally refresh with different source chains
const refreshed = await refresh([8453, 42161]);
console.log('Refreshed bridge:', refreshed.bridgeRequired);
} else {
// Sufficient balance — bridge will be skipped
console.log('No bridge needed, executing directly');
}
allow();
},
});BridgeAndExecuteIntent:
type BridgeAndExecuteIntent = {
executeRequirement: ExecuteRequirement;
available: AvailableBalances;
} & (
| { bridgeRequired: false }
| {
bridgeRequired: true;
shortfall: Shortfall;
bridge: BridgeIntent;
}
);BridgeAndExecuteOnIntentHookData:
type BridgeAndExecuteOnIntentHookData = {
allow: () => void;
deny: () => void;
intent: BridgeAndExecuteIntent;
refresh: (selectedSources?: number[]) => Promise<BridgeAndExecuteIntent>;
};Swap and Execute Intent
await client.swapAndExecute(params, {
onIntent: ({ intent, allow, deny, refresh }) => {
// Execution requirement (always present)
console.log('Contract:', intent.executeRequirement.to);
console.log('Token needed:', intent.executeRequirement.token.amount);
if (intent.swapRequired) {
// Swap is needed
console.log('Token shortfall:', intent.shortfall.token.amount);
console.log('Swap sources:', intent.swap.sources);
console.log('Swap destination:', intent.swap.destination.amount);
// Refresh with different sources
const refreshed = await refresh([{ chainId: 8453, tokenAddress: '0x...' }]);
console.log('Refreshed:', refreshed.swapRequired);
} else {
console.log('No swap needed, executing directly');
}
allow();
},
});SwapAndExecuteIntent:
type SwapAndExecuteIntent = {
executeRequirement: ExecuteRequirement;
available: AvailableBalances;
} & (
| { swapRequired: false }
| {
swapRequired: true;
shortfall: Shortfall;
swap: SwapIntent;
}
);SwapAndExecuteOnIntentHookData:
type SwapAndExecuteOnIntentHookData = {
allow: () => void;
deny: () => void;
intent: SwapAndExecuteIntent;
refresh: (sources?: Source[]) => Promise<SwapAndExecuteIntent>;
};Shared Types
type ExecuteFeeParams =
| { type: 'eip1559'; maxFeePerGas: bigint; maxPriorityFeePerGas: bigint }
| { type: 'legacy'; gasPrice: bigint };
type ExecuteRequirement = {
chain: { id: number; name: string; logo?: string };
to: Hex;
token: {
address: Hex;
symbol: string;
decimals: number;
amount: string;
amountRaw: bigint;
value: string; // USD value
};
gas: {
address: Hex;
symbol: string;
decimals: number;
amount: string;
amountRaw: bigint;
value: string;
estimatedGasUnits: string;
feeParams: ExecuteFeeParams; // EIP-1559 or legacy (Arbitrum) pricing
l1Fee: string;
priceTier: 'low' | 'medium' | 'high';
};
nativeValue: { amount: string; amountRaw: bigint; value: string } | null;
tokenApproval: {
token: { address: Hex; symbol: string; decimals: number };
amount: string;
amountRaw: bigint;
spender: Hex;
} | null;
};
type AvailableBalances = {
token: { amount: string; amountRaw: bigint; value: string };
gas: { amount: string; amountRaw: bigint; value: string };
};
type Shortfall = {
token: { amount: string; amountRaw: bigint; value: string };
gas: { amount: string; amountRaw: bigint; value: string };
};Event Callbacks
All main SDK operations accept an onEvent callback to track progress through a typed event system.
await client.bridge(params, {
onEvent: (event) => {
switch (event.type) {
case 'status':
// Lifecycle phase: intent_building → intent_ready → awaiting_approval → ...
console.log('Status:', event.status);
break;
case 'plan_preview':
// Emitted once with the planned steps before execution
console.log('Plan steps:', event.plan.steps);
break;
case 'plan_confirmed':
// Emitted after user approval with final steps
console.log('Confirmed plan:', event.plan.steps);
break;
case 'plan_progress':
// Per-step progress: wallet_prompted → submitted → confirmed → completed
console.log(`Step ${event.step.type}: ${event.state}`);
break;
}
},
});Hook Placement by Operation:
| Operation | onEvent | onIntent | onAllowance |
|-----------|-----------|------------|----------------|
| bridge() | options.onEvent | options.hooks.onIntent | options.hooks.onAllowance |
| bridgeAndTransfer() | options.onEvent | options.hooks.onIntent | options.hooks.onAllowance |
| bridgeAndExecute() | options.onEvent | options.onIntent (top-level) | automatic (min) |
| swapWithExactIn() | options.onEvent | options.hooks.onIntent | N/A |
| swapWithExactOut() | options.onEvent | options.hooks.onIntent | N/A |
| swapAndExecute() | options.onEvent | options.onIntent (top-level) | N/A |
Use hooks for approval-time intent and allowance data. Use the awaited return value for final result data such as intentExplorerUrl, sourceTxs, or execute.
Events & Steps
The SDK emits typed events during operations, enabling real-time progress UIs.
Event Types
All events follow a discriminated union pattern on event.type:
type BridgeEvent =
| BridgeStatusEvent // { type: 'status'; status: BridgeStatus }
| BridgePlanPreviewEvent // { type: 'plan_preview'; plan: BridgePlan }
| BridgePlanConfirmedEvent // { type: 'plan_confirmed'; plan: BridgePlan }
| BridgePlanProgressEvent; // { type: 'plan_progress'; stepType, state, step, ... }
type BridgeStatus =
| 'intent_building'
| 'intent_ready'
| 'awaiting_approval'
| 'awaiting_allowance_selection'
| 'approved'
| 'executing'
| 'completed';
// Composite bridge + execute flow adds an initial 'preparing' phase
type BridgeAndExecuteStatus = 'preparing' | BridgeStatus;
type SwapEvent =
| SwapStatusEvent
| SwapPlanPreviewEvent
| SwapPlanConfirmedEvent
| SwapPlanProgressEvent;
type SwapStatus =
| 'route_building'
| 'route_ready'
| 'awaiting_approval'
| 'approved'
| 'executing'
| 'completed';
// Composite swap + execute flow adds an initial 'preparing' phase
type SwapAndExecuteStatus = 'preparing' | SwapStatus;Bridge Steps
Bridge plans contain the following step types:
| Step Type | Description |
|-----------|-------------|
| allowance_approval | Token allowance approval on a source chain |
| request_signing | User signs the intent request |
| request_submission | Intent submitted to the network |
| vault_deposit | Deposit into vault on a source chain |
| bridge_fill | Fill received on destination chain |
Swap Steps
Swap plans contain the following step types:
| Step Type | Description |
|-----------|-------------|
| source_swap | Execute a swap on a source chain (ephemeral or Safe smart-account path) |
| eoa_to_ephemeral_transfer | Transfer funds from EOA to ephemeral wallet on a source chain |
| bridge_deposit | Deposit into vault for cross-chain bridge |
| bridge_intent_submission | Submit the bridge intent to the network |
| bridge_fill | Wait for bridge fill on destination chain |
| destination_swap | Execute a swap on the destination chain |
SwapPlan:
type SwapPlan = {
hasBridge: boolean;
hasDestinationSwap: boolean;
steps: SwapPlanStep[];
};
type SwapPlanStep =
| SwapSourceSwapStep
| SwapEoaToEphemeralTransferStep
| SwapBridgeDepositStep
| SwapBridgeIntentSubmissionStep
| BridgeFillStep
| SwapDestinationSwapStep;Each step carries contextual metadata (chain, tokens, wallet path). Progress events report per-step state transitions. The terminal success state varies by step type:
- On-chain transaction steps (
allowance_approval,source_swap,eoa_to_ephemeral_transfer,bridge_deposit,destination_swap,execute_approval,execute_transaction) settle onconfirmed. vault_depositsettles oncompleted— it emitsconfirmedas an on-chain intermediate, thencompletedas its terminal-success state.- Off-chain orchestration steps (
request_signing,request_submission,bridge_intent_submission,bridge_fill) settle oncompleted. - All steps can emit
failed.
For a robust progress UI, treat both 'confirmed' and 'completed' as terminal-success states.
Progress Event Payloads
Each plan_progress event carries type: 'plan_progress', stepType, state, and step (the matching step object from plan.steps) — plus state-specific sibling fields you'll want for the UI. These are fully typed: narrow a BridgeEvent / SwapEvent on (stepType, state) and TypeScript reveals the fields below (the per-step event types like BridgeVaultDepositProgressEvent, ExecuteTransactionProgressEvent are exported).
| Field | On | Meaning |
|-------|----|---------|
| txHash / explorerUrl | on-chain steps in submitted / confirmed (optional on failed): allowance_approval, vault_deposit, execute_approval, execute_transaction, and source/destination swap steps | The submitted transaction hash and its explorer URL — use for "View tx" links |
| intentRequestHash | request_signing (completed), request_submission, bridge_fill | The intent/RFF hash — use for "View intent" links |
| error | every failed state | Failure text (already inlined; the underlying cause is here) |
| approvedAmount / approvedAmountRaw | allowance_approval | Amount approved at this step |
| value / hasData | execute_transaction | Native value sent and whether calldata is present |
The step object carries the contextual metadata — and chain lives on event.step.chain, not event.chain:
client.bridge(params, {
onEvent: (event) => {
if (event.type !== 'plan_progress') return;
const chainName = event.step.chain?.name; // step.chain, not event.chain
if ((event.state === 'submitted' || event.state === 'confirmed') && 'txHash' in event) {
console.log('tx:', event.txHash, event.explorerUrl);
}
if (event.state === 'failed' && 'error' in event) {
console.error(event.step.type, 'failed:', event.error);
}
},
});Per-step step shapes (all include id and type): allowance_approval → chain, token, spender, requiredAmount; vault_deposit → chain, asset, assetType, submissionMode; bridge_fill → chain, asset; execute_approval → chain, token, spender, amount; execute_transaction → chain, to. Swap source/destination steps carry swaps[] with input/output token amounts.
Building Progress UIs
Example using the typed plan/progress event system:
import { createNexusClient } from '@avail-project/nexus-core';
import type { BridgeEvent, BridgePlanStep } from '@avail-project/nexus-core';
let steps: BridgePlanStep[] = [];
const completedSteps = new Set<string>();
await client.bridge(params, {
onEvent: (event: BridgeEvent) => {
switch (event.type) {
case 'plan_preview':
// Initialize UI with planned steps
steps = event.plan.steps;
renderProgress();
break;
case 'plan_progress':
// Both 'confirmed' and 'completed' are terminal success states
if (event.state === 'confirmed' || event.state === 'completed') {
completedSteps.add(event.step.id);
}
renderProgress();
break;
case 'status':
if (event.status === 'completed') {
console.log('Bridge complete!');
}
break;
}
},
hooks: {
onIntent: ({ allow }) => allow(),
onAllowance: ({ allow }) => allow(['min']),
},
});
function renderProgress() {
steps.forEach((step) => {
const done = completedSteps.has(step.id);
console.log(`${done ? '✓' : '○'} ${step.type}`);
});
}Error Handling
NexusError Hierarchy
SDK errors are concrete subclasses of the abstract base NexusError<C>. Each subclass pins its
category and narrows the allowed context.service value. Switch on error.category (or
instanceof) for coarse handling; check error.code for the specific failure mode.
import {
NexusError,
ValidationError,
UserActionError,
BackendError,
ExternalServiceError,
ExecutionError,
ERROR_CODES,
} from '@avail-project/nexus-core';
try {
await client.bridge({ toTokenSymbol: 'USDC', toAmountRaw: 1_000_000n, toChainId: 137 });
} catch (error) {
if (!(error instanceof NexusError)) {
console.error('Unexpected error:', error);
throw error;
}
// Coarse handling via category
switch (error.category) {
case 'user_action':
// User cancelled — typically not shown as a failure
return;
case 'validation':
showValidationError(error.message);
return;
case 'backend':
case 'external_service':
showRetryableUpstreamError(error);
return;
}
// Specific code handling
switch (error.code) {
case ERROR_CODES.INSUFFICIENT_BALANCE:
showInsufficientBalanceUI();
break;
case ERROR_CODES.EXEC_TX_RECEIPT_WAIT_TIMEOUT:
showRetryOption();
break;
case ERROR_CODES.EXEC_TX_ONCHAIN_REVERTED:
showOnChainRevert();
break;
default:
showGenericError(error.message);
}
// Errors are flat — the underlying cause (viem revert, HTTP failure, …) is already
// inlined into error.message. Log the queryable axes for support/forensics.
console.error(error.code, error.message, error.context, error.details);
}Hierarchy:
abstract class NexusError<C extends ErrorCategory = ErrorCategory> extends Error {
readonly category: C;
readonly code: ErrorCode;
readonly context: ErrorContext<C>; // service narrowed per category
readonly details?: Record<string, unknown>;
toJSON(): object; // flat, single-level
}
class ValidationError extends NexusError<'validation'> {} // no service
class UserActionError extends NexusError<'user_action'> {} // wallet | hook
class SimulationError extends NexusError<'simulation'> {} // rpc
class ExecutionError extends NexusError<'execution'> {} // wallet | rpc
class BackendError extends NexusError<'backend'> {} // middleware
class ExternalServiceError extends NexusError<'external_service'> {} // lifi | bebop | fibrous | coinbase
class InternalError extends NexusError<'internal'> {} // no serviceErrors are flat: there is no native cause capture and no chain-walking
(walk / find / chain rendering). When the SDK catches an underlying error
(a viem revert, an HTTP failure), its text is inlined into error.message, so
message is self-contained for logs and toasts.
Step-bound failures are not a separate class — they're whichever subclass actually applies,
carrying context.stepId / context.stepType / context.chainId. Replace any prior
error instanceof NexusStepError check with
error instanceof NexusError && error.context.stepId !== undefined.
Error Codes Reference
Codes follow category/specific_noun_suffix. Suffixes: _failed, _timeout, _reverted,
_denied, _exceeded — or no suffix for non-failure terminal states.
| Error Code | Description | User Action |
|------------|-------------|-------------|
| user_action/* | | |
| user_action/intent_hook_denied | User rejected intent via dApp hook | None — user cancelled |
| user_action/intent_signature_denied | User rejected EIP-191 sign in wallet | None — user cancelled |
| user_action/allowance_approval_denied | User rejected token approve tx | None — user cancelled |
| user_action/siwe_signature_denied | User rejected SIWE signature | None — user cancelled |
| user_action/tx_send_denied | User rejected a tx send (execute / vault deposit / atomic batch) | None — user cancelled |
| user_action/ephemeral_key_denied | User rejected the ephemeral-key derivation signature | None — user cancelled |
| validation/* | | |
| validation/insufficient_balance | Not enough tokens for operation | Show balance, suggest deposit |
| validation/no_balance_for_address | No balance found for address | Verify address |
| validation/invalid_input | Invalid parameters provided | Check input values |
| validation/invalid_address_length | Address has wrong length | Verify address format |
| validation/invalid_allowance_hook | Invalid allowance hook values | Check allow() arguments |
| validation/token_not_supported | Token not supported on chain | Use supported token |
| validation/sdk_not_initialized | SDK not initialized | Call initialize() first |
| validation/sdk_init_state_unexpected | Unexpected init state | Re-initialize SDK |
| validation/wallet_not_connected | No wallet connected | Connect wallet |
| validation/chain_not_found | Chain id not
