Skip to main content

Getting Started

Building a frontend?

This guide uses privateKeyToAccount and a plain Node.js script to keep the flow focused on the SDK. If you're wiring the SDK into a React / Next.js app with an injected wallet, jump to Frontend Integration for the wagmi-based pattern.

In this tutorial, you'll execute a cross-chain token transfer using the Interop SDK. By the end, you'll know how to create a provider, fetch a quote, send a transaction, and discover which chains and tokens are supported.

Prerequisites

  • Node.js v20.x or higher
  • A private key with testnet funds
  • An RPC URL for your origin chain (e.g., Sepolia)

Install the package

viem is a peer dependency (^2.28.0) — install it alongside the package:

npm install @wonderland/interop-cross-chain viem
# or
yarn add @wonderland/interop-cross-chain viem
# or
pnpm add @wonderland/interop-cross-chain viem

Create a provider

The SDK uses a factory pattern. Let's start with Relay on testnet:

import { createCrossChainProvider, PROTOCOLS } from "@wonderland/interop-cross-chain";

const provider = createCrossChainProvider(PROTOCOLS.RELAY, { isTestnet: true });

Examples throughout the docs use the PROTOCOLS constant (PROTOCOLS.RELAY, PROTOCOLS.ACROSS, PROTOCOLS.OIF, PROTOCOLS.BUNGEE, PROTOCOLS.LIFI_INTENTS) rather than the literal strings it maps to — it keeps the protocol name typo-safe and discoverable in your IDE.

Other available providers: Across, OIF. See Supported Providers for the full list.

Set up your wallet

You'll need viem clients to interact with the blockchain:

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const RPC_URL = "https://sepolia.infura.io/v3/YOUR_API_KEY";

const publicClient = createPublicClient({
chain: sepolia,
transport: http(RPC_URL),
});

const walletClient = createWalletClient({
chain: sepolia,
transport: http(RPC_URL),
account,
});

Fetch a quote

Request a quote for a cross-chain transfer:

const quotes = await provider.getQuotes({
user: account.address,
input: {
chainId: 11155111, // Sepolia
assetAddress: "0xInputTokenAddress",
amount: "100000000000000000", // 0.1 tokens in wei
},
output: {
chainId: 84532, // Base Sepolia
assetAddress: "0xOutputTokenAddress",
recipient: account.address,
},
swapType: "exact-input",
});

const quote = quotes[0];
console.log(`Quote from ${quote.provider}`);

Native tokens (ETH, MATIC, etc.)

To bridge a native asset, use NATIVE_ASSET_ADDRESS as the assetAddress:

import { createCrossChainProvider, NATIVE_ASSET_ADDRESS } from "@wonderland/interop-cross-chain";

const quotes = await provider.getQuotes({
user: account.address,
input: {
chainId: 11155111, // Sepolia
assetAddress: NATIVE_ASSET_ADDRESS,
amount: "100000000000000000", // 0.1 ETH in wei
},
output: {
chainId: 84532, // Base Sepolia
assetAddress: NATIVE_ASSET_ADDRESS,
recipient: account.address,
},
swapType: "exact-input",
});
info

Both 0x0000000000000000000000000000000000000000 (zero address) and 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee (EIP-7528) are accepted by the SDK as native asset sentinels. NATIVE_ASSET_ADDRESS exports the EIP-7528 form (0xeeee...eeee) and is the canonical constant used in all SDK examples.

Handle ERC-20 approvals

ERC-20 inputs need an approve before the transfer step. The SDK can do this for you: wire an approvalService into the aggregator and every returned quote already has the necessary approve TransactionSteps prepended to order.steps. Iterate the steps in order and each approve fires before the step that needs it.

See Automatic ERC-20 Approvals for the setup, or the Execute Intent guide for a full runnable example.

Execute the transaction

Quotes contain either signature steps (gasless) or transaction steps (user pays gas). Handle both:

import {
getSignatureSteps,
getTransactionSteps,
isSignatureOnlyOrder,
} from "@wonderland/interop-cross-chain";

if (isSignatureOnlyOrder(quote.order)) {
// Gasless: sign EIP-712 typed data, solver executes on your behalf
const step = getSignatureSteps(quote.order)[0];
const { signatureType, ...typedData } = step.signaturePayload;
const signature = await walletClient.signTypedData(typedData);
await provider.submitOrder(quote, signature);
console.log("Order submitted via signature");
} else {
// User pays gas: send every transaction step in order.
// A quote may contain one or more transaction steps depending on the provider.
// Quotes from an `Aggregator` configured with `approvalService` may also have
// `approve` steps prepended before the transfer.
for (const step of getTransactionSteps(quote.order)) {
const hash = await walletClient.sendTransaction({
to: step.transaction.to,
data: step.transaction.data,
value: step.transaction.value ? BigInt(step.transaction.value) : undefined,
});
console.log("Transaction sent:", hash);

const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error(`Step failed: ${step.description ?? "transaction"}`);
}
console.log("Confirmed: Success");
}
}

Which chains and tokens are supported?

Rather than hard-coding a supported-tokens list, ask the aggregator at runtime. discoverAssets() queries every configured provider in parallel and returns a pre-processed map keyed by numeric chain ID:

const discovered = await aggregator.discoverAssets({ chainIds: [1, 8453] });

// Token addresses available on each chain
console.log(discovered.tokensByChain[1]); // Ethereum
console.log(discovered.tokensByChain[8453]); // Base

// Token metadata (nested by chain ID then lowercase address)
const usdc = discovered.tokenMetadata[1]?.["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"];
console.log(usdc?.symbol, usdc?.decimals); // "USDC", 6

Omit chainIds to discover across every chain each provider supports. For a single-provider variant and the full DiscoveredAssets shape, see the API Reference.

Compare quotes from multiple providers

Use the Aggregator to fetch and sort quotes from multiple providers at once:

import {
createAggregator,
createCrossChainProvider,
PROTOCOLS,
} from "@wonderland/interop-cross-chain";

const acrossProvider = createCrossChainProvider(PROTOCOLS.ACROSS, { isTestnet: true });

const aggregator = createAggregator({
providers: [provider, acrossProvider],
});

const response = await aggregator.getQuotes({
/* same QuoteRequest as above */
});

console.log(`Got ${response.quotes.length} quotes`);
response.errors.forEach((err) => console.warn(`Provider error: ${err.errorMsg}`));

Quick reference

Execution flow

  1. Create providercreateCrossChainProvider(PROTOCOLS.ACROSS) (or use createAggregator for multiple — wire an approvalService to enrich quotes with ERC-20 approve steps automatically)
  2. Get quotesprovider.getQuotes(request) or aggregator.getQuotes(request)
  3. Check order typeisSignatureOnlyOrder(quote.order)
    • Signature (gasless): signTypedData()provider.submitOrder(quote, signature)
    • Transaction (user pays gas): iterate getTransactionSteps(quote.order) and send each — approval steps (when present) come first (see example above — convert string value to BigInt)
  4. TrackcreateOrderTracker(provider) for single-provider or aggregator.track({ txHash, providerId, originChainId, destinationChainId }) for aggregator

Which function should I use?

I want to...Use
Get quotes from one providerprovider.getQuotes(request)
Get quotes from multiple providersaggregator.getQuotes(request)
Build a quote locally (no provider API)aggregator.buildQuote(providerId, request)
Submit a signed orderprovider.submitOrder(quote, signature)
Check if order is gaslessisSignatureOnlyOrder(quote.order)
Get signature steps from an ordergetSignatureSteps(quote.order)
Get transaction steps from an ordergetTransactionSteps(quote.order)
Track an order (single provider)createOrderTracker(provider)tracker.watchOrder({ txHash, ... })
Track an order (aggregator)aggregator.track({ txHash, providerId, originChainId, ... })
Discover supported tokensaggregator.discoverAssets()

Next steps