Skip to content

Collecting Orders

The Catalyst order server is a helpful intermediary that allows users to submit intents (orders) and broadcasts those intents to solvers. It serves as a central hub that:

  1. Collects user intents from various sources
  2. Broadcasts these intents to connected solvers in real-time
  3. Tracks the on-chain status of all transactions necessary for the intent to work end-to-end

While the order server is the recommended integration surface for most solvers due to its convenience and comprehensive order delivery, it’s important to note that – for on-chain ordersit’s entirely optional. Catalyst is designed to be fully decentralized, and solvers can choose to directly monitor on-chain events for a more permissionless approach with potentially lower latency.

Below we will go over how to collect Catalyst orders. Alternatively see the example solver here: https://github.com/catalystsystem/catalyst-example-solver/blob/main/src/handlers/vm-order.handler.ts

The order subscription system allows solvers to receive real-time order notifications through a WebSocket connection. This subscription-based approach ensures that solvers can immediately process new orders as they enter the system, without needing to continuously poll for updates.

When subscribing to orders, solvers receive structured data containing all necessary information to evaluate and potentially fulfill each order. The subscription system handles automatic reconnection in case of connection issues and provides a robust way to maintain a persistent connection to the order server.

Catalyst webscoket message are formatted with a event and a data field:

interface CatalystEvent<T> {
event: CatalystWsEventType;
data: T;
}

The Datafield depends on the event type::

export enum WebSocketEvents {
/// @dev Heartbeat event.
PING = "ping",
/// @dev Intent has been submitted and is being broadcast
USER_ORDER_VM = "user:vm-order-submit",
/// @dev Intent status has been changed. This may indicate that an intent has been filled.
APP_ORDER_STATUS_CHANGED = "app:order-status-change",
}

When connected to the order server, the order server will continuously send a ping messages to which the client is expected to respond with a pongs. This is used to disconnect misbehaving clients and for debugging purposes.

For order collection, the important event is USER_ORDER_VM. The Data will be formatted as a CatalystOrder:

export interface CatalystOrder {
order: CompactOrder;
quotes: QuoteContext;
meta: CatalystOrderMeta;
sponsorSignature: string;
allocatorSignature: string;
}
export interface QuoteContext {
toAsset: string;
toPrice: string;
discount: string;
fromAsset: string;
fromPrice: string;
intermediary: string;
}
export interface CompactOrder {
type: "CompactOrder"; // Used to identify this as a compact order
user: string;
nonce: number;
originChainId: number;
fillDeadline: number;
localOracle: string;
inputs: [number, number][];
outputs: OutputDescription[];
}
export interface OutputDescription {
remoteOracle: string;
remoteFiller: string;
token: string;
amount: number;
recipient: string;
chainId: number;
remoteCall: string;
fulfillmentContext: string;
}
export interface CatalystOrderMeta {
submitTime: number;
orderIdentifier?: string;
orderStatus?: string;
connectedWalletId?: string;
destinationAddress?: string;
originId?: string;
confirmationsCount?: number;
requiredConfirmationsCount?: number;
orderInitiatedTxHash?: string;
orderPurchasedTxHash?: string;
orderProvenTxHash?: string;
nonVmTxHash?: string;
signedAt?: Date;
initiatedAt?: Date;
pendingTransferAt?: Date;
settledTransferAt?: Date;
purchasedAt?: Date;
provenAt?: Date;
failedAt?: Date;
expiredAt?: Date;
}

Below you can find an example script to connect to the websocket server.

import { WebSocket } from "ws";
class WebSocketClient {
private ws: WebSocket;
private reconnectInterval = 5000; // Reconnect interval in milliseconds
private wsUri: string;
constructor(wsUri: string) {
this.wsUri = wsUri;
this.connect();
}
private connect(): void {
// Initialize WebSocket connection
this.ws = new WebSocket(this.wsUri);
// Connection opened
this.ws.on("open", () => {
console.log("Connected to WebSocket server");
// You can send an initial message here if needed
this.ws.send(
JSON.stringify({ type: "hello", message: "Connected to server" })
);
});
// Listen for messages
this.ws.on("message", async (data: RawData) => {
try {
const parsedData: CatalystEvent<unknown> = JSON.parse(data.toString());
switch (parsedData.event) {
case CatalystWsEventType.PING:
this.handleReceivePing();
break;
case CatalystWsEventType.VM_ORDER:
console.log(`[${CatalystWsEventType.VM_ORDER}]`, parsedData);
// add your custom filling logic in this function
await this.handleVmOrder(
parsedData as CatalystEvent<CatalystOrder>
);
break;
default:
console.log("Unknown message type:", parsedData);
}
} catch (error) {
console.error("Error parsing JSON:", error);
}
});
// Handle errors
this.ws.on("error", (error) => {
console.error("WebSocket error:", error);
});
// Handle disconnection
this.ws.on("close", () => {
console.log("Disconnected from WebSocket server");
this.reconnect();
});
}
private reconnect(): void {
console.log(
`Attempting to reconnect in ${this.reconnectInterval / 1000} seconds...`
);
setTimeout(() => {
this.connect();
}, this.reconnectInterval);
}
public sendMessage(message: any): void {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
console.warn("WebSocket is not open. Cannot send message.");
}
}
private sendPong(): void {
this.sendMessage({ type: "pong" });
}
public close(): void {
this.ws.close();
}
}
// Usage example
const wsUri = "order-server-uri";
// Create WebSocket client
const client = new WebSocketClient(wsUri);
// Keep the process running
process.on("SIGINT", () => {
console.log("Closing WebSocket connection...");
client.close();
process.exit(0);
});

Catalyst allows highly customizable orders. As a result, you may want to add further validation to ensure that you support the order being relayed to you. As a result, it is important to properly validate orders.

Below the term whitelisted will be used to describe trusted and validated by you. When it is written that a token has to be whitelisted, it means you trust the token. If a validation layer is whitelisted, it means you trust the validation layer, etc. Whitelisted does not mean permissioned by a central entity, it means trusted by you.

The following is an attempt at an exhausive list of validations that solvers have to implement. Note! While this list seems excessive, you likely do some of these already.

  1. fillDeadline Ensure that you have sufficient time to fill the order:

    • Time to fill on destination chain, including potential source chain finality.
  2. expiry Ensure that you have sufficient time to fill, relay, and claim the order:

    • Time to fill on destination chain, including potential source chain finality.
    • Time to send message validation proof to input chain.
    • Time to claim order after validation has been delivered.
  3. Validation layer. Ensure that you support submitting proofs (and if not automatic, also relaying) to the validation layer. Additionally, the localOracle and remoteOracles needs to be of the same validation layer.

  4. Ensure that you have whitelisted every single input token. If one input token is malicious the order may be unclaimable. Additionally, for blacklistable tokens like USDC, ensure that you is not on a blacklist.

  5. Ensure that the potential reset period for a resource lock extends beyond the fillDeadline AND there is no active withdrawal.

  6. Ensure that the allocatorID is whitelisted. The allocator can block claims from processing (by withdrawing signatures or reusing nonces.)

    • The allocatorID is part of the lock tag for inputs. (first 12 bytes).
    • Optionally, ensure that the user has sufficient tokens. This should have been validated by the allocator though.
  7. For each output:

    1. output.chainId is whitelisted.
    2. remoteOracle and localOracle has been correct configured regarding originChainId and output.chainId. The config is immutable so this can be done once for each pair.
    3. output.remoteFiller is whitelisted.
    4. output.fulfillmentContext is decodable and the order type is supported and compatible with output.remoteFiller.
    5. output.token is whitelisted. Additionally, for blacklistable tokens like USDC, ensure that neither you nor the recipient is on a blacklist.
    6. You have sufficient tokens for output.amount.
    7. If the output has calldata, that you can execute it and other outputs atomically. For output on different chains, you may have to whitelist recipients if there is calldata.
      • On OP-chains, CrossL2Inbox needs to be blacklisted in the entire call tree.
  8. If the order has multiple outputs, ensure that you can fill all outputs and the first output is set to your solver identifier.

  9. Validate that the sponsor signature resolves via an ECDSA recover. Otherwise you may have to submit the registration on-chain.

  10. Validate the allocatorData. You may have to do an on-chain call.

  11. Validate that the allocator nonce has not been spent previously.

If an order has a 0 input or output of a token you have not whitelisted, the order may not be fillable. Be careful about filling orders containing strange tokens.

The CatalystCompactOrder will be used to interface all functions on the input chain. Additionally, once hydrated with a signature, it allows one to verify the validity of an order.

The CatalystCompactOrder struct will be signed and stored as a witness in the appropriate lock/claim structure. For TheCompact, this is:

struct BatchCompact {
address arbiter; // Associated settlement contract
address sponsor; // CatalystCompactOrder.user
uint256 nonce; // CatalystCompactOrder.nonce
uint256 expires; // CatalystCompactOrder.fillDeadline
uint256[2][] idsAndAmounts; // CatalystCompactOrder.inputs
CatalystWitness witness;
}
struct CatalystWitness {
uint32 fillDeadline; // CatalystCompactOrder.fillDeadline
address localOracle; // CatalystCompactOrder.localOracle
OutputDescription[] outputs; // CatalystCompactOrder.outputs
}

To validate an order, ensure that the sponsor and allocator signatures are valid for this EIP-712 signed structure.

Output settlement schemes supporting on-chain orders like CompactSettlerWithDeposit.sol or the LI.FI facet allow anyone to broadcast and collect orders on-chain through either Deposited event:

/// @dev LI.FI facet since it does not have access to the orderId.
event Deposited(CatalystCompactOrder order);
/// @dev Catalyst Settler since it does has access to the orderId.
event Deposited(bytes32 indexed orderId, CatalystCompactOrder order);