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:
| Status | Description | Across | Relay | OIF | Bungee | LiFi Intents |
|---|---|---|---|---|---|---|
Created | Order created on-chain | — | — | ✓ | — | — |
Pending | Awaiting execution | ✓ | ✓ | ✓ | ✓ | ✓ |
Executing | Filler is processing the order | — | ✓ | ✓ | ✓ | — |
Executed | Fill transaction submitted | — | — | ✓ | — | — |
Settling | Settlement in progress | — | ✓ | — | — | ✓ |
Settled | Settlement complete (reserved) | — | — | — | — | — |
Finalized | Order fully complete | ✓ | ✓ | ✓ | ✓ | ✓ |
Failed | Order failed | ✓ | ✓ | ✓ | ✓ | ✓ |
Refunded | Funds 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:
| API | How to call | Input | Use when |
|---|---|---|---|
aggregator.track() | Event-emitter | txHash + providerId + originChainId + destinationChainId | You 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 promise | txHash + providerId + originChainId | You only need the current status without streaming |
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 (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/v3endpoint. - 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/indexto 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
- Always set an appropriate timeout for watching
- Handle all
OrderStatusupdates appropriately in your UI - Use
getOrderStatus()for one-time checks instead of watching - Provide custom RPC URLs for better reliability (origin chain always; destination chain for event-based fill tracking)
- Treat
timeoutas non-terminal (the order can still finalize onchain)
Next Step
Explore more complex scenarios: Advanced Usage
References
- EIP-7683: Open Intent Framework
- Order Tracking Types
- Concepts — how intent-based transfers and tracking work