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:
- Collects user intents from various sources
- Broadcasts these intents to connected solvers in real-time
- 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 orders – it’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.
Order subscription
Section titled “Order subscription”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.
Order Server events
Section titled “Order Server events”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 exampleconst wsUri = "order-server-uri";
// Create WebSocket clientconst client = new WebSocketClient(wsUri);
// Keep the process runningprocess.on("SIGINT", () => { console.log("Closing WebSocket connection..."); client.close(); process.exit(0);});
Order Validation
Section titled “Order Validation”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.
-
fillDeadline
Ensure that you have sufficient time to fill the order:- Time to fill on destination chain, including potential source chain finality.
-
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.
-
Validation layer. Ensure that you support submitting proofs (and if not automatic, also relaying) to the validation layer. Additionally, the
localOracle
andremoteOracle
s needs to be of the same validation layer. -
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. -
Ensure that the potential
reset period
for a resource lock extends beyond thefillDeadline
AND there is no active withdrawal. -
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
forinputs
. (first 12 bytes). - Optionally, ensure that the user has sufficient tokens. This should have been validated by the allocator though.
- The allocatorID is part of the
-
For each output:
output.chainId
is whitelisted.remoteOracle
andlocalOracle
has been correct configured regardingoriginChainId
andoutput.chainId
. The config is immutable so this can be done once for each pair.output.remoteFiller
is whitelisted.output.fulfillmentContext
is decodable and the order type is supported and compatible withoutput.remoteFiller
.output.token
is whitelisted. Additionally, for blacklistable tokens like USDC, ensure that neither you nor the recipient is on a blacklist.- You have sufficient tokens for
output.amount
. - 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 iscalldata
.- On OP-chains, CrossL2Inbox needs to be blacklisted in the entire call tree.
-
If the order has multiple outputs, ensure that you can fill all outputs and the first output is set to your solver identifier.
-
Validate that the
sponsor signature
resolves via an ECDSA recover. Otherwise you may have to submit the registration on-chain. -
Validate the
allocatorData
. You may have to do an on-chain call. -
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.
Signature Validation
Section titled “Signature Validation”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.
On-chain Order Broadcast
Section titled “On-chain Order Broadcast”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);