import { NonEmptyString, Positive, v, validate, } from "@dicedhq/core/validation"; import { type Hex, zeroAddress } from "viem"; import type { BaseClient } from "../client/base.js"; import { type SignatureType, signatureTypeToNumber, signOrder, } from "../core/eip712.js"; import { roundTo } from "../utils.js"; import type { MarketApi, TickSize } from "./market.js"; export class OrderApi { constructor( private readonly client: BaseClient, private readonly market: MarketApi, ) {} /** * Get an order by ID */ async getOrder(id: string): Promise { validate(NonEmptyString, id, "id"); return this.client.request({ method: "GET", path: `/data/order/${id}`, auth: { kind: "l2", }, }); } /** * List active orders for a given market */ async listOrders(params: ListOrderParams): Promise { const validated = validate(ListOrderSchema, params); return this.client.request({ method: "GET", path: "/data/orders", auth: { kind: "l2" }, options: { params: { id: validated.orderId, market: validated.marketId, asset_id: validated.assetId, }, }, }); } /** * Check if an order is eligible or scoring for Rewards purposes */ async checkOrderRewardScoring(id: string): Promise { validate(NonEmptyString, id, "id"); const response = await this.client.request<{ scoring: boolean }>({ method: "GET", path: "/order-scoring", auth: { kind: "l2" }, options: { params: { order_id: id }, }, }); return response.scoring; } /** * Create an order */ async createOrder(params: CreateOrderParams): Promise { const validated = validate(CreateOrderSchema, params); const [tickSize, feeRateBps, nonce] = await Promise.all([ this.market.getTickSize(validated.tokenId), this.market.getFeeRateBps(validated.tokenId), this.getNonce(), ]); // Build the unsigned order const signer = this.client.wallet.account.address; const maker = (validated.funderAddress as Hex) ?? signer; const signatureType = validated.signatureType ?? "eoa"; const amounts = this.calculateOrderAmounts({ price: validated.price, side: validated.side, size: validated.size, tickSize, }); const order: Order = { signer, maker, taker: validated.taker === "anyone" ? zeroAddress : (validated.taker as Hex), tokenId: validated.tokenId, nonce: nonce.toString(), salt: this.generateSalt().toString(), feeRateBps: feeRateBps.toString(), expiration: validated.expiration.toString(), side: validated.side, signatureType, makerAmount: amounts.maker, takerAmount: amounts.taker, }; // Sign the order const signature = await signOrder(this.client.wallet, order); return { ...order, signature }; } /** * Post an order to the order book */ async postOrder({ order, kind, }: { order: SignedOrder; kind: OrderKind; }): Promise { const payload = { owner: this.client.credentials.key, orderType: kind, order: { salt: parseInt(order.salt, 10), maker: order.maker, signer: order.signer, taker: order.taker, tokenId: order.tokenId, makerAmount: order.makerAmount, takerAmount: order.takerAmount, expiration: order.expiration, nonce: order.nonce, feeRateBps: order.feeRateBps, side: order.side === "BUY" ? "0" : "1", signatureType: signatureTypeToNumber(order.signatureType), signature: order.signature, }, }; return this.client.request({ method: "POST", path: "/order", auth: { kind: "l2-with-attribution", headerArgs: payload, }, options: { body: payload }, }); } /** * Create and post an order in one step */ async createAndPostOrder( params: CreateOrderAndPostParams, ): Promise { const signedOrder = await this.createOrder(params.order); return this.postOrder({ order: signedOrder, kind: params.kind, }); } /** * Cancel an order */ async cancelOrder(id: string): Promise { validate(NonEmptyString, id, "id"); return this.client.request({ method: "DELETE", path: "/order", auth: { kind: "l2" }, options: { body: { orderID: id }, }, }); } /** * Cancel multiple orders */ async cancelOrders(orderIds: string[]): Promise { validate(CancelOrdersSchema, orderIds); return this.client.request({ method: "DELETE", path: "/orders", auth: { kind: "l2" }, options: { body: { orderIDs: orderIds }, }, }); } /** * Cancel all orders */ async cancelAllOrders(): Promise { return this.client.request({ method: "DELETE", path: "/cancel-all", auth: { kind: "l2", }, }); } /** * Get the current nonce for the wallet */ private async getNonce(): Promise { const { account } = this.client.wallet; return account.getNonce ? await account.getNonce() : 0n; } /** * Generate a cryptographically secure random salt for order uniqueness */ private generateSalt(): bigint { const bytes = new Uint8Array(8); crypto.getRandomValues(bytes); const view = new DataView(bytes.buffer); return view.getBigUint64(0); } private calculateOrderAmounts({ side, size, price, tickSize, }: { side: OrderSide; size: number; price: number; tickSize: TickSize; }) { const tickDecimals = tickSize.split(".")[1]?.length || 0; const sizeDecimals = 2; const amountDecimals = tickDecimals + sizeDecimals; const roundedPrice = roundTo(price, tickDecimals); const shares = roundTo(size, sizeDecimals); const cost = roundTo(shares * roundedPrice, amountDecimals); // Convert to raw integers (no decimals) for smart contract // e.g., "2.00" with 6 decimals -> "2000000" const sharesRaw = Math.floor(shares * 10 ** sizeDecimals).toString(); const costRaw = Math.floor(cost * 10 ** amountDecimals).toString(); if (side === "BUY") { // BUY: maker gives USDC, gets shares return { maker: costRaw, taker: sharesRaw, }; } else { // SELL: maker gives shares, gets USDC return { maker: sharesRaw, taker: costRaw, }; } } } // ============================================================================ // Parameter Schemas // ============================================================================ const ListOrderSchema = v.pipe( v.object({ orderId: v.optional(v.string()), marketId: NonEmptyString, assetId: v.optional(v.string()), }), v.metadata({ title: "ListOrderParams" }), ); const SignatureTypeSchema = v.picklist([ "eoa", "poly-proxy", "poly-gnosis-safe", ]); const CreateOrderSchema = v.pipe( v.object({ tokenId: NonEmptyString, price: Positive, size: Positive, side: v.picklist(["BUY", "SELL"]), expiration: v.pipe(v.number(), v.minValue(0)), taker: v.union([ v.pipe(v.string(), v.startsWith("0x")), v.literal("anyone"), ]), /** Optional funder address (e.g., a Safe contract). If provided, this becomes the maker address. */ funderAddress: v.optional(v.pipe(v.string(), v.startsWith("0x"))), /** Signature type. Defaults to "eoa". Use "poly-gnosis-safe" for Safe wallet signing. */ signatureType: v.optional(SignatureTypeSchema), }), v.metadata({ title: "CreateOrderParams" }), ); const CancelOrdersSchema = v.pipe( v.array(NonEmptyString), v.minLength(1, "orderIds must not be empty"), v.metadata({ title: "CancelOrdersParams" }), ); export type ListOrderParams = v.InferInput; export type CreateOrderParams = v.InferInput; // ============================================================================ // Response Types (from API, not validated by us) // ============================================================================ export type Order = { salt: string; maker: Hex; signer: Hex; taker: Hex; tokenId: string; makerAmount: string; takerAmount: string; expiration: string; nonce: string; feeRateBps: string; side: OrderSide; signatureType: SignatureType; }; export type SignedOrder = Order & { signature: string }; export type OpenOrder = { id: string; market: string; asset_id: string; owner: string; side: OrderSide; size: string; original_size: string; price: string; type: OrderKind; fee_rate_bps: string; status: string; created_at?: string; last_update?: string; outcome?: string; expiration?: string; maker_address?: string; associate_trades?: AssociateTrade[]; }; export type OrderSide = "BUY" | "SELL"; // Good till cancelled | Fill or kill | Good till date | Fill and kill export type OrderKind = "GTC" | "FOK" | "GTD" | "FAK"; export type OrderResponse = { success: boolean; errorMsg?: string; orderID?: string; transactionsHashes?: string[]; }; export type CreateOrderAndPostParams = { kind: OrderKind; order: CreateOrderParams; }; export type CancelResponse = { success: boolean; errorMsg?: string; }; export type AssociateTrade = { id: string; order_id: string; market: string; asset_id: string; side: OrderSide; size: string; fee_rate_bps: string; price: string; status: string; match_time?: string; last_update?: string; outcome?: string; owner?: string; maker_address?: string; transaction_hash?: string; };