Skip to main content

Order Tracking

The SDK includes order tracking to monitor cross-chain transfers from initiation to completion.

Tracking supports two ways of observing the same lifecycle, depending on the provider and how the order is created:

  • Onchain tracking: derive tracking data from the origin transaction (e.g. ERC-7683 open event), then watch the fill on the destination chain.
  • Offchain tracking: query a provider API for order state transitions (e.g. polling an "order status / deposit status" endpoint).

Overview

The OrderTracker streams updates using the full OIF OrderStatus set (from @openintentsframework/oif-specs). Not every provider emits every status — the table below shows which statuses each provider actually produces:

StatusDescriptionAcrossRelayOIFBungeeLiFi Intents
CreatedOrder created on-chain
PendingAwaiting execution
ExecutingFiller is processing the order
ExecutedFill transaction submitted
SettlingSettlement in progress
SettledSettlement complete (reserved)
FinalizedOrder fully complete
FailedOrder failed
RefundedFunds returned to sender

You can subscribe to any OrderStatus via tracker.on(OrderStatus.<status>, ...) — the examples below show the most common ones.

In addition, the tracker can emit:

  • Timeout - the SDK stopped watching (the order may still finalize before its onchain deadline)
  • Error - an unexpected error occurred

Which Tracker API to Use?

The SDK exposes three ways to track an order. Choose based on your use case:

APIHow to callInputUse when
aggregator.track()Event-emittertxHash + providerId + originChainId + destinationChainIdYou have a tx hash and want real-time events
tracker.watchOrder()Async generator(txHash + originChainId) or (orderId + originChainId + destinationChainId)You need orderId-based tracking (required for signature-based/gasless orders), or prefer async iteration
aggregator.getOrderStatus()One-shot promisetxHash + providerId + originChainIdYou only need the current status without streaming
warning

watchOrder is currently the only path that supports tracking by orderId. Signature-based (gasless) orders may not have a txHash at open time — for those, orderId tracking is required. Pass a WatchOrderByOrderId param (with orderId, originChainId, and destinationChainId) instead of WatchOrderByTxHash.

Mixed-step orders

Mixed-step orders (containing both transaction steps and signature steps) are not currently emitted by any supported provider. Consumers can safely handle either isSignatureOnlyOrder() or isTransactionOnlyOrder() without defensive mixed-order handling.

Basic Usage

The recommended way to track orders is through the Aggregator, which handles tracker creation and caching automatically:

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

const acrossProvider = createCrossChainProvider("across", { isTestnet: true });

const aggregator = createAggregator({
providers: [acrossProvider],
trackerFactory: new OrderTrackerFactory({
rpcUrls: {
11155111: "https://sepolia.infura.io/v3/YOUR_API_KEY",
84532: "https://base-sepolia.g.alchemy.com/v2/YOUR_API_KEY",
},
}),
});

Tracking an Order

After sending the transaction, use aggregator.track() for real-time updates:

import {
getTransactionSteps,
OrderStatus,
OrderTrackerEvent,
} from "@wonderland/interop-cross-chain";

const quote = response.quotes[0];
const step = getTransactionSteps(quote.order)[0];
const hash = await walletClient.sendTransaction({
to: step.transaction.to,
data: step.transaction.data,
value: step.transaction.value ? BigInt(step.transaction.value) : undefined,
});

const tracker = aggregator.track({
txHash: hash,
providerId: quote.provider,
originChainId: 11155111,
destinationChainId: 84532,
timeout: 300000, // 5 minutes
});

tracker.on(OrderStatus.Pending, (update) => console.log("Pending:", update.message));
tracker.on(OrderStatus.Finalized, (update) => {
console.log("Finalized!", update.fillTxHash);
if (update.warnings?.length) {
// e.g. destination swap failed, user received fallback token
console.warn("Warnings:", update.warnings);
}
});
tracker.on(OrderStatus.Failed, (update) => console.log("Failed:", update.failureReason));
tracker.on(OrderStatus.Refunded, () => console.log("Refunded"));
tracker.on(OrderTrackerEvent.Timeout, (payload) => console.log("Timeout:", payload.message));
tracker.on(OrderTrackerEvent.Error, (error) => console.error("Error:", error));

Getting Current Status

Check the current status of an order without watching:

const status = await aggregator.getOrderStatus({
txHash: "0xabc...",
providerId: "across",
originChainId: 11155111,
});

console.log(status.status); // OrderStatus
console.log(status.orderId); // Order ID

if (status.fillEvent) {
console.log(`Filled by: ${status.fillEvent.relayer}`);
console.log(`Fill tx: ${status.fillEvent.fillTxHash}`);
if (status.warnings?.length) {
// e.g. destination swap failed, user received fallback token
console.warn("Warnings:", status.warnings);
}
}

Provider Notes (Across)

  • Mainnet: Across uses API-based fill tracking by default (polls GET /deposit/status?depositTxnRef=...). This reduces reliance on destination-chain RPCs.
  • Testnet: Across uses event-based fill tracking by default (Across testnet API is not reliable), so you should provide RPC URLs for both origin and destination chains.

Provider Notes (Relay)

  • Relay uses API-based tracking for both mainnet and testnet. Both opened intent parsing and fill watching use the /intents/status/v3 endpoint.
  • No RPC URLs required — all tracking is done through the Relay API.
  • Transaction notification is automatic — when tracking starts, the pre-tracker calls POST /transactions/index to accelerate solver indexing. No manual step required.
const originChainId = 11155111;
const destinationChainId = 84532;

const hash = await walletClient.sendTransaction({ ... });

// Start tracking — Relay is automatically notified via the pre-tracker
const tracker = aggregator.track({
txHash: hash,
providerId: quote.provider,
originChainId,
destinationChainId,
});

Advanced: Standalone Tracker

For advanced use cases, you can create a tracker directly without using the aggregator:

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

const acrossProvider = createCrossChainProvider("across", { isTestnet: true });

const tracker = createOrderTracker(acrossProvider, {
rpcUrls: {
11155111: "https://sepolia.infura.io/v3/YOUR_API_KEY",
84532: "https://base-sepolia.g.alchemy.com/v2/YOUR_API_KEY",
},
});

Watching an Order

Watch an order with real-time updates using an async generator:

import { OrderStatus, OrderTrackerYieldType } from "@wonderland/interop-cross-chain";

for await (const item of tracker.watchOrder({
txHash: "0xabc...",
originChainId: 11155111,
destinationChainId: 84532,
timeout: 300000, // 5 minutes
})) {
if (item.type === OrderTrackerYieldType.Timeout) {
console.log(`Timeout: ${item.payload.message}`);
break;
}

console.log(`Status: ${item.update.status}`);
console.log(`Message: ${item.update.message}`);

if (item.update.status === OrderStatus.Finalized) {
console.log(`Filled in tx: ${item.update.fillTxHash}`);
break;
} else if (item.update.status === OrderStatus.Failed) {
console.log("Order failed");
break;
}
}

Custom Public Client

You can also provide a custom viem PublicClient:

import { createOrderTracker } from "@wonderland/interop-cross-chain";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";

const publicClient = createPublicClient({
chain: sepolia,
transport: http("https://sepolia.infura.io/v3/YOUR_API_KEY"),
});

const tracker = createOrderTracker(acrossProvider, {
publicClient,
});

Error Handling

The tracker handles errors gracefully:

import { OrderTrackerYieldType } from "@wonderland/interop-cross-chain";

try {
for await (const item of tracker.watchOrder({
txHash: "0x...",
originChainId: 11155111,
destinationChainId: 84532,
})) {
if (item.type === OrderTrackerYieldType.Timeout) {
// SDK stopped watching; order may still finalize before onchain deadline
break;
}

// Handle item.update
}
} catch (error) {
if (error instanceof Error) {
console.error("Tracking error:", error.message);
}
}

Best Practices

  1. Always set an appropriate timeout for watching
  2. Handle all OrderStatus updates appropriately in your UI
  3. Use getOrderStatus() for one-time checks instead of watching
  4. Provide custom RPC URLs for better reliability (origin chain always; destination chain for event-based fill tracking)
  5. Treat timeout as non-terminal (the order can still finalize onchain)

Next Step

Explore more complex scenarios: Advanced Usage

References