Skip to content

Initiating Orders

There are 2 different initiation paths, EVM and Bitcoin. Generally, all orders have to be initiated on a VM using a reactor. This allows the reactor to pull the input funds while awaiting proof of the output. Since Bitcoin does not these spending conditions, the order flow is reversed coma and the solver is responsible for signing the VM order. We shall start by going over EVM initiation.

EVM initiation

After collecting an order – either through pulling or websockets – and verifying it, the next step is to submit it on-chain. Catalyst Orders are accompanied by a user signature (OrderDto.signature) that serves a dual purpose:

  1. Permit2 Signature: This signature acts as a Permit2 allowance, authorizing the Catalyst contracts to withdraw the submitter’s tokens directly. This streamlines the process by eliminating the need for separate approval transactions.

  2. User Order Authorization: The signature also confirms that the user has approved the order, ensuring consent and alignment with the terms of execution.

Orders are processed on a first-come, first-served basis, emphasizing the importance of swift submission to secure the desired transaction. By leveraging the Permit2 signature mechanism, Catalyst simplifies the initiation process, reducing overhead and ensuring seamless order execution.

// This tutorial uses ethersjs but you can easily replace it by similar libraries.
import { ethers } from "ethers";
const reactorAbi = "...";
const signer = "ethers.signer...";
async function initiateOrder() {
// Get an order
const orders = await getOrders();
const order = orders.orders[0];
// Define the reactor we will call. You can get the reactor address from the order
const reactorAddress = order.order.settlementContract;
const reactor = new ethers.Contract(reactorAddress, reactorAbi, signer);
// TODO: Set approvals for the reactorAddress for all inputs & collateral.
// The order arrives almost ready to use,
// we just need to remove the type from the orderdata.
const { type: _, ...cleanedOrderData } = order.order.orderData;
const cleanedOrder = { ...order.order, orderData: cleanedOrderData };
const signature = order.signature;
const fillerData = "0x"; // #custom-fillerdata--underwriting
// Call the reactor to initiate the order.
return reactor.initiate(cleanedOrder, signature, fillerData);
}

Custom FillerData & Underwriting

By default, if fillerData is not specified, the input assets (provided by the user) are sent directly to the caller (msg.sender). This behavior is generally suitable for most use cases, eliminating the need for additional customization.

However, if there is a need to direct the input assets to another address or to enable underwriting, a customized fillerData must be utilized. Currently, only one custom version (v1) is supported. The v1 structure includes:

  • Version Byte (0x01): Identifies the custom data version.
  • fillerAddress: The address that will receive the input assets and collateral.
  • orderPurchaseDeadline: A timestamp that allows an alternative buyer to purchase the order before this time. “Buying” the order in this context means transferring all of the input assets and collateral to the fillerAddress.
  • orderDiscount: Provides buyers with a discount on the inputs, represented as a fraction of 2^16 - 1. For example, to offer a 1% discount, the value would be calculated as 0.01 * (2^16 - 1) = 655. This feature is particularly useful for chains with slower block confirmations (e.g., Bitcoin), enabling the solver to be paid after 0-1 confirmations while assuring the user of higher finality (3-6 confirmations).

Another version (v2) allows for adding additional logic when the inputs are paid. You can find more information on v2 in our smart contracts or by reaching out.

const fillerDataVersion = "0x01";
const fillerAddress = "0x....".replace("0x", "");
// fillerAddress.length === 20*2;
const orderPurchaseDeadline = Number(1723199919)
.toString(16)
.padStart("0", 4 * 2);
//orderPurchaseDeadline.length === 4*2
const orderDiscount = Math.floor(0.01 * (2 ** 16 - 1))
.toString(16)
.padStart("0", 2 * 2);
// orderDiscount.length === 2*2
const fillerData =
fillerDataVersion + fillerAddress + orderPurchaseDeadline + orderDiscount;

Bitcoin Initiation

Bitcoin operates differently from VM chains in terms of script functionality. Bitcoin scripts only define spending conditions, meaning that, unlike VM chains, we cannot pull funds from a user after they have signed a message. Consequently, the order flow is reversed, and the solver is responsible for signing the VM order.

Additionally, as previous chapters have described you cannot pull for BTC -> EVM orders. Instead you have to respond to the quote-request-binding event and get selected for a non-vm-order event.

Encode your Bitcoin address

Your solver must be capable of generating 1 or multiple Bitcoin deposit address. CrossCats supports all 5 address types in common use: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR. We recommend using either P2WPKH, P2TR, or P2WSH.

Bitcoin addresses are encoded in two fields: token and address.

  • token: This field serves to differentiate VM tokens from Bitcoin orders, encode relevant context like confirmations and address version. Refer to the UTXO type table for details on converting address types to versions. The field should consists of the bitcoin signifier (BC in the 12’th byte), number of confirmations in the 31st, UTXO type in the 32’th, and otherwise 0s.

  • address: This field encodes the public key hash, script hash, or witness hash. Use the decoding schemes listed in the UTXO type table for various address versions. For addresses involving hashes of 20 bytes (P2PKH, P2SH, and P2WPKH), end pad the hashes with zeros (e.g., 0xabcdef...00000).

Quote Open Order (WIP)

For solvers handling BTC to VM conversions, quoting orders for comparison with other solvers is essential. To begin quoting:

  1. Subscription: Solvers must subscribe to quote requests from the order server.
  2. Response: When a quote request is received, the solver must respond with a quote that remains valid for at least 60 seconds. If a signed order is requested within 30 seconds of the quote, the solver should provide a signed order with a lifetime of 30 seconds. The 60 seconds is the sum of these.

Failure to respond with a signed order that matches or improves upon the quote within the 30-second window may result in blacklisting of the solver.

Ensure your quoting mechanism is robust and timely to maintain competitive standing and avoid potential blacklisting.

export interface CatalystEvent<T> {
event: string;
data: T;
}
export interface CatalystQuoteRequestData {
quoteRequestId: string;
fromChain: string;
toChain: string;
fromAsset: string;
toAsset: string;
expirationTime: string;
amount: string;
}
async function handleReceiveQuoteRequest(
parsedData: CatalystEvent<CatalystQuoteRequestData>,
ws: WebSocket
) {
try {
// an example of your custom function that will get the quote
const quote = await getSolverQuote(
parsedData.data.fromAsset,
parsedData.data.toAsset,
parsedData.data.amount
);
ws.send(
JSON.stringify({
event: "solver-quote",
data: {
origin: "your_identifier", // required
quoteRequestId: parsedData.data.quoteRequestId, // required
...quote,
},
})
);
} catch (error) {
console.error("Error simulating quote:", error);
}
}

Return Signed Orders (WIP)

Once the Order Server has evaluated all quotes from solvers, it selects the most favorable quote. After the user’s deposit is confirmed, the Order Server will request the selected solver to provide a signed order.

Responsibilities of the Solver:

  1. Provide a Signed Order: The solver must return a signed order that aligns with the quoted terms and remains valid within the specified expiry period.

  2. Order Exclusivity: It is crucial that the signed order is exclusive to the user and the Order Server’s executor. This exclusivity ensures that the order cannot be fulfilled by other solvers or reused. (TODO)

  3. Asset Delivery Assurance: The CrossCats Order Server guarantees asset delivery through a Bitcoin address controlled by the Order Server but owned by the user. This setup ensures that the assets are securely delivered as promised.

Ensuring these conditions helps maintain the integrity and efficiency of the order fulfillment process. It is essential that solvers adhere strictly to these requirements to ensure smooth operations and avoid any potential issues.

export interface CatalystEvent<T> {
event: string;
data: T;
}
export interface CrossChainOrder {
settlementContract: string;
swapper: string;
nonce: string;
originChainId: number;
initiateDeadline: number;
fillDeadline: number;
orderData: DutchOrderData | LimitOrderData;
}
export interface QuoteContext {
toAsset: string;
toPrice: string;
discount: string;
fromAsset: string;
fromPrice: string;
intermediary: string;
}
export interface CatalystOrderData {
order: CrossChainOrder;
quote: QuoteContext;
signature: string;
}
async function signOrderRequest(signOrderRequest: any): any | undefined {
const evaluationResponse = orderEvaluations(signOrderRequest);
if (evaluationResponse === undefined) return undefined;
...
}
async function handleReceiveOrder(
parsedData: CatalystEvent<CatalystOrderData>,
ws: WebSocket
) {
try {
// an example of your custom function that will get the quote
const signedOrder = await signOrderRequest(...);
ws.send(
JSON.stringify({
event: "solver-order-signed",
data: {
origin: "your_identifier", // required
...signedOrder, // TODO: determine shape of signed order
},
})
);
} catch (error) {
console.error("Error signing order:", error);
}
}

Once the signedOrder is sent & it correctly matches the expected quote, the vast vast majority of orders will be filled.

TODO: The Order Server may respond back with a transaction hash.