npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  • Node.js >=18.0.0
  • npm >=9.0.0

Installation

npm install @avail-project/nexus-core

Quick 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); // true

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

  1. Bridge (conditional) — funds the shortfall on the destination chain. Automatically skipped when the destination already holds enough of the token (result.bridgeSkipped === true).
  2. 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:

  1. Swap (conditional) — funds the shortfall on the destination chain. Automatically skipped when the destination already holds enough of the token (result.swapSkipped === true).
  2. 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 calling allow() or deny(). 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 on confirmed.
  • vault_deposit settles on completed — it emits confirmed as an on-chain intermediate, then completed as its terminal-success state.
  • Off-chain orchestration steps (request_signing, request_submission, bridge_intent_submission, bridge_fill) settle on completed.
  • 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_approvalchain, token, spender, requiredAmount; vault_depositchain, asset, assetType, submissionMode; bridge_fillchain, asset; execute_approvalchain, token, spender, amount; execute_transactionchain, 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 service

Errors 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