UNPKG

@ickb/order

Version:

UDT Limit Order utilities built on top of CCC

397 lines (356 loc) 11.4 kB
import { ccc } from "@ckb-ccc/core"; import { OrderData } from "./entities.js"; import type { ValueComponents } from "@ickb/utils"; /** * Represents a parsed order cell on the blockchain. * * Implements `ValueComponents` to expose the cell's raw data and its * value breakdown (CKB and UDT). */ export class OrderCell implements ValueComponents { /** * Creates an instance of OrderCell. * * @param cell - The raw cell fetched from the chain. * @param data - Decoded order data with parameters (rates, directions, etc.). * @param ckbUnoccupied - Amount of CKB in this cell not used in state rent. * @param absTotal - Absolute total value of this order (in base units). * @param absProgress - Absolute amount filled so far (in base units). * @param maturity - Estimated completion time: * - `bigint` Unix timestamp in milliseconds if scheduled, * - `0n` if already completed, * - `undefined` if no estimate is available. */ constructor( public cell: ccc.Cell, public data: OrderData, public ckbUnoccupied: ccc.FixedPoint, public absTotal: ccc.Num, public absProgress: ccc.Num, public maturity: bigint | undefined, ) {} /** * Gets the order CKB amount. * * @returns The CKB amount as a `ccc.FixedPoint`. */ get ckbValue(): ccc.FixedPoint { return this.cell.cellOutput.capacity; } /** * Gets the order UDT amount. * * @returns The UDT amount as a `ccc.FixedPoint`. */ get udtValue(): ccc.FixedPoint { return this.data.udtValue; } /** * Attempts to parse an OrderCell from a raw cell. * * Returns `undefined` if parsing or validation fails. * * @param cell - The raw chain cell to convert. * @returns An `OrderCell` instance or `undefined`. */ static tryFrom(cell: ccc.Cell): OrderCell | undefined { try { return OrderCell.mustFrom(cell); } catch { return undefined; } } /** * Parses and validates a raw cell as OrderCell. * * Throws if the cell is invalid or data validation fails. * * @param cell - The raw chain cell to convert. * @returns A new `OrderCell` instance. * @throws When decoding or validation fails. */ static mustFrom(cell: ccc.Cell): OrderCell { // Decode and validate the order payload const data = OrderData.decode(cell.outputData); data.validate(); const udtValue = data.udtValue; const ckbUnoccupied = cell.capacityFree; const { ckbToUdt, udtToCkb } = data.info; const isCkb2Udt = data.info.isCkb2Udt(); const isUdt2Ckb = data.info.isUdt2Ckb(); const isDualRatio = isCkb2Udt && isUdt2Ckb; // Compute total values in base units const ckb2UdtValue = isCkb2Udt ? ckbUnoccupied * ckbToUdt.ckbScale + udtValue * ckbToUdt.udtScale : 0n; const udt2CkbValue = isUdt2Ckb ? ckbUnoccupied * udtToCkb.ckbScale + udtValue * udtToCkb.udtScale : 0n; // Determine absolute total: single or average for dual-ratio const absTotal = ckb2UdtValue === 0n ? udt2CkbValue : udt2CkbValue === 0n ? ckb2UdtValue : // Take the average of the two values for dual ratio orders (ckb2UdtValue * udtToCkb.ckbScale * udtToCkb.udtScale + udt2CkbValue * ckbToUdt.ckbScale * ckbToUdt.udtScale) >> 1n; // Compute progress: full for dual, else based on direction const absProgress = isDualRatio ? absTotal : isCkb2Udt ? udtValue * ckbToUdt.udtScale : ckbUnoccupied * udtToCkb.ckbScale; // Maturity: undefined if in-progress or dual; zero if complete const maturity = isDualRatio || absTotal !== absProgress ? undefined : 0n; return new OrderCell( cell, data, ckbUnoccupied, absTotal, absProgress, maturity, ); } /** * Checks if the order is is dual ratio. * * @returns True if the order is dual ratio (liquidity provider), otherwise false. */ isDualRatio(): boolean { return this.data.info.isDualRatio(); } /** * Checks if the order can be matched as a CKB to UDT order. * @returns True if the order is matchable as CKB to UDT, otherwise false. */ isCkb2UdtMatchable(): boolean { return this.data.info.isCkb2Udt() && this.ckbUnoccupied > 0n; } /** * Checks if the order can be matched as a UDT to CKB order. * @returns True if the order is matchable as UDT to CKB, otherwise false. */ isUdt2CkbMatchable(): boolean { return this.data.info.isUdt2Ckb() && this.data.udtValue > 0n; } /** * Checks if the order is matchable in any way. * @returns True if the order is matchable, otherwise false. */ isMatchable(): boolean { return this.isCkb2UdtMatchable() || this.isUdt2CkbMatchable(); } /** * Checks if the order is fulfilled. * * @returns True if the order is fulfilled (not matchable), otherwise false. */ isFulfilled(): boolean { return !this.isMatchable(); } /** * Retrieves the master out point of the order. * @returns The master out point associated with the order. */ getMaster(): ccc.OutPoint { return this.data.getMaster(this.cell.outPoint); } /** * Countermeasure to Confusion Attack https://github.com/ickb/whitepaper/issues/19 * Validates the order against a descendant order. * @param descendant - The descendant order to validate against. * @throws Will throw an error if validation fails. */ validate(descendant: OrderCell): void { // Same cell, nothing to check if (this.cell.outPoint.eq(descendant.cell.outPoint)) { return; } if (!this.cell.cellOutput.lock.eq(descendant.cell.cellOutput.lock)) { throw Error("Order script different"); } const udt = this.cell.cellOutput.type; if (!udt || !descendant.cell.cellOutput.type?.eq(udt)) { throw Error("UDT type is different"); } if (!descendant.getMaster().eq(this.getMaster())) { throw Error("Master is different"); } if (!this.data.info.eq(this.data.info)) { throw Error("Info is different"); } if (this.absTotal > descendant.absTotal) { throw Error("Total value is lower than the original one"); } if (this.absProgress > descendant.absProgress) { throw Error("Progress is lower than the original one"); } } /** * Checks if the descendant order is valid against this order. * @param descendant - The descendant order to validate. * @returns True if the descendant is valid, otherwise false. */ isValid(descendant: OrderCell): boolean { try { this.validate(descendant); return true; } catch { return false; } } /** * Countermeasure to Confusion Attack https://github.com/ickb/whitepaper/issues/19 * Resolves the best descendant order from a list of descendants. * @param descendants - The list of descendant orders to resolve. * @returns The best matching descendant order or undefined if none is valid. */ resolve(descendants: OrderCell[]): OrderCell | undefined { let best: OrderCell | undefined = undefined; for (const descendant of descendants) { if (!this.isValid(descendant)) { continue; } // Pick order with best absProgress. At equality of absProgress, give preference to newly minted orders if ( !best || best.absProgress < descendant.absProgress || (best.absProgress === descendant.absProgress && !best.data.isMint()) ) { best = descendant; } } return best; } } /** * Represents a master cell */ export class MasterCell implements ValueComponents { /** * Creates an instance of MasterCell. * @param cell - The ccc.Cell instance to be wrapped by the MasterCell. */ constructor(public cell: ccc.Cell) {} /** * Creates a MasterCell instance from a cell-like object. * @param cellLike - An object that can be converted to a ccc.Cell. * @returns A new instance of MasterCell. */ static from(cellLike: ccc.CellLike): MasterCell { return new MasterCell(ccc.Cell.from(cellLike)); } /** * Validates the MasterCell against an OrderCell. * @param order - The OrderCell to validate against. * @throws Error if the order script is different or if the master is different. */ validate(order: OrderCell): void { if (!this.cell.cellOutput.type?.eq(order.cell.cellOutput.lock)) { throw Error("Order script different"); } if (!order.getMaster().eq(this.cell.outPoint)) { throw Error("Master is different"); } } /** * Gets the CKB value of the cell. * * @returns The total CKB amount as a `ccc.FixedPoint`. */ get ckbValue(): ccc.FixedPoint { return this.cell.cellOutput.capacity; } /** * Gets the UDT value of the cell. * * For a Master Cell, the UDT amount is always zero. * * @returns The UDT amount as a `ccc.FixedPoint` (0n). */ readonly udtValue = 0n; } /** * Represents a group of orders associated with a master cell. */ export class OrderGroup implements ValueComponents { /** * Creates an instance of OrderGroup. * @param master - The master cell associated with the order group. * @param order - The order within the group. * @param origin - The original order associated with the group. */ constructor( public master: MasterCell, public order: OrderCell, public origin: OrderCell, ) {} /** * Tries to create an OrderGroup from the provided parameters. * @param master - The master cell. * @param order - The order within the group. * @param origin - The original order. * @returns An OrderGroup instance or undefined if creation fails. */ static tryFrom( master: MasterCell, order: OrderCell, origin: OrderCell, ): OrderGroup | undefined { const og = new OrderGroup(master, order, origin); if (og.isValid()) { return og; } return undefined; } /** * Validates the order group against its master and origin orders. * @throws Will throw an error if validation fails. */ validate(): void { this.master.validate(this.order); this.origin.validate(this.order); } /** * Checks if the order group is valid. * @returns True if the order group is valid, otherwise false. */ isValid(): boolean { try { this.validate(); return true; } catch { return false; } } /** * Checks if the specified lock is the owner of the master cell. * * @param locks - The locks to check ownership against. * @returns True if the lock is the owner, otherwise false. */ isOwner(...locks: ccc.Script[]): boolean { const lock = this.master.cell.cellOutput.lock; return locks.some((l) => lock.eq(l)); } /** * Gets the CKB value of the group. * * @returns The total CKB amount as a `ccc.FixedPoint`, which is the sum of the CKB values of the order cell and the master cell. */ get ckbValue(): ccc.FixedPoint { return ( this.order.cell.cellOutput.capacity + this.master.cell.cellOutput.capacity ); } /** * Gets the UDT value of the group. * * @returns The UDT amount as a `ccc.FixedPoint`, derived from the order cell. */ get udtValue(): ccc.FixedPoint { return this.order.data.udtValue; } }