UNPKG

@ickb/sdk

Version:

iCKB SDK built on top of CCC

514 lines (467 loc) 16.4 kB
import { ccc } from "@ckb-ccc/core"; import { collect, CapacityManager, SmartTransaction, binarySearch, type ValueComponents, hexFrom, getHeader, Epoch, } from "@ickb/utils"; import { convert, ICKB_DEPOSIT_CAP, ickbExchangeRatio, type LogicManager, type OwnedOwnerManager, } from "@ickb/core"; import { Info, OrderManager, Ratio, type OrderCell, type OrderGroup, } from "@ickb/order"; import { getConfig } from "./constants.js"; import { PoolSnapshot } from "./codec.js"; /** * SDK for managing iCKB operations. */ export class IckbSdk { /** * Creates an instance of IckbSdk. * * @param ownedOwner - The manager for owned owner operations. * @param ickbLogic - The manager for iCKB logic operations. * @param order - The manager for order operations. * @param capacity - The capacity manager instance. * @param bots - An array of bot lock scripts. */ constructor( private readonly ownedOwner: OwnedOwnerManager, private readonly ickbLogic: LogicManager, private readonly order: OrderManager, private readonly capacity: CapacityManager, private readonly bots: ccc.Script[], ) {} /** * Creates an instance of IckbSdk from script dependencies. * * @param args - Parameters matching those of getConfig. * @returns A new instance of IckbSdk. */ static from(...args: Parameters<typeof getConfig>): IckbSdk { const { managers: { ownedOwner, logic, order, capacity }, bots, } = getConfig(...args); return new IckbSdk(ownedOwner, logic, order, capacity, bots); } /** * Previews the conversion between CKB and UDT. * * This method calculates a conversion preview using an exchange ratio midpoint. * Optionally, a fee may be applied that influences the effective conversion rate, * scaling the converted amount by (feeBase - fee) / feeBase. * * @param isCkb2Udt - Indicates the conversion direction: * - true: Convert CKB to UDT. * - false: Convert UDT to CKB. * @param amounts - An object containing value components (amounts for CKB and UDT). * @param system - The current system state containing exchange ratio, fee rate, tip, and order-related information. * @param options - Optional parameters for fee and matching: * - fee: The fee to apply in integer terms (defaults to 1n for 0.001% fee). * - feeBase: The base used for fee scaling (defaults to 100000n). * * @returns An object with: * - convertedAmount: The estimated converted amount as a FixedPoint. * - ckbFee: The fee (or gain) in CKB, as a FixedPoint. * - info: Additional conversion metadata. * - maturity: Optional maturity information (as ccc.Num) if meets criteria. */ static estimate( isCkb2Udt: boolean, amounts: ValueComponents, system: SystemState, options?: { fee?: ccc.Num; feeBase?: ccc.Num; }, ): { convertedAmount: ccc.FixedPoint; ckbFee: ccc.FixedPoint; info: Info; maturity: ccc.Num | undefined; } { // Apply a 0.001% default fee if none provided. options = { fee: 1n, feeBase: 100000n, ...options, }; const { convertedAmount, ckbFee, info } = OrderManager.convert( isCkb2Udt, system.exchangeRatio, amounts, options, ); // If the fee meets a threshold, calculate the order maturity; otherwise, maturity is undefined. const maturity = ckbFee >= 10n * system.feeRate ? IckbSdk.maturity({ info, amounts }, system) : undefined; return { convertedAmount, ckbFee, info, maturity }; } /** * Estimates the maturity for an order formatted as a Unix timestamp. * * Depending on the order type and amount remaining, the method calculates an estimated timestamp * when the order (or part thereof) might be fulfilled. * * @param o - Either an OrderCell or an object containing order Info and value components. * @param system - The current system state. * @returns The Unix timestamp of estimated maturity as a bigint (in milliseconds) or undefined if not applicable. */ static maturity( o: | OrderCell | { info: Info; amounts: ValueComponents; }, system: SystemState, ): bigint | undefined { const info = "info" in o ? o.info : o.data.info; const amounts = "amounts" in o ? o.amounts : { ckbValue: o.ckbUnoccupied, udtValue: o.udtValue }; // Dual-ratio orders have no fixed maturity. if (info.isDualRatio()) { return; } const isCkb2Udt = info.isCkb2Udt(); const amount = isCkb2Udt ? amounts.ckbValue : amounts.udtValue; const ratio = isCkb2Udt ? info.ckbToUdt : info.udtToCkb; // If order is already fulfilled. if (amount === 0n) { return 0n; } const { tip, exchangeRatio, orderPool, ckbAvailable, ckbMaturing } = system; // Create a reference ratio instance for comparison. const b = new Info(ratio, ratio, 1); let ckb = isCkb2Udt ? amount : 0n; let udt = isCkb2Udt ? 0n : amount; for (const o of orderPool) { const a = o.data.info; if (a.isCkb2Udt()) { // If not isCkb2Udt or a worse ratio, add available CKB. if (!isCkb2Udt || a.ckb2UdtCompare(b) < 0) { ckb += o.ckbUnoccupied; } } else { // Conversely for UDT to CKB orders. if (isCkb2Udt || a.udt2CkbCompare(b) < 0) { udt += o.udtValue; } } } // Adjust ckb by the converted UDT amount. ckb -= convert(false, udt, exchangeRatio); // Minimum maturity of 10 minutes (in milliseconds). let maturity = 10n * 60n * 1000n; if (isCkb2Udt) { // For CKB to UDT orders, extend maturity based on the CKB amount. if (ckb > 0n) { maturity *= 1n + ckb / ccc.fixedPointFrom("200000"); } return maturity + ("info" in o ? BigInt(Date.now()) : tip.timestamp); } // For UDT to CKB orders, add available CKB. ckb += ckbAvailable; if (ckb >= 0) { return maturity + ("info" in o ? BigInt(Date.now()) : tip.timestamp); } // Find the earliest maturity in the ckbMaturing array that satisfies the required CKB. const ckbNeeded = -ckb; const i = binarySearch( ckbMaturing.length, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (n) => ckbMaturing[n]!.ckbCumulative >= ckbNeeded, ); return ckbMaturing[i]?.maturity; } /** * Mints a new order cell and appends it to the transaction. * * The method performs the following operations: * - Creates order cell data using provided amounts and order information. * - Adds required cell dependencies and UDT handlers to the transaction. * - Appends the order cell to the transaction outputs. * * @param tx - The smart transaction to which the order cell is added. * @param user - The user, represented either as a Signer or a Script. * @param info - The order information meta data (usually computed via OrderManager.convert). * @param amounts - The value components for the order, including: * - ckbValue: The CKB amount (may include an internal surplus). * - udtValue: The UDT amount. * * @returns A Promise resolving to void. */ async request( tx: SmartTransaction, user: ccc.Signer | ccc.Script, info: Info, amounts: ValueComponents, ): Promise<void> { // If the user is provided as a Signer, extract the recommended lock script. user = "codeHash" in user ? user : (await user.getRecommendedAddressObj()).script; this.order.mint(tx, user, info, amounts); } /** * Melts (cancels) the specified order groups. * * For each order group, if the option is set to process fulfilled orders only, * it filters accordingly. Then, for every valid group, the master and order cells are added * as inputs to the transaction. * * @param tx - The smart transaction to which the inputs are added. * @param groups - An array of order groups to be melted. * @param options - Optional parameters: * - isFulfilledOnly: If true, only order groups with fully or partially fulfilled orders are processed. * * @returns void */ collect( tx: SmartTransaction, groups: OrderGroup[], options?: { isFulfilledOnly?: boolean; }, ): void { this.order.melt(tx, groups, options); } /** * Retrieves the L1 state from the blockchain. * * The method performs the following: * - Obtains the current block tip and calculates the exchange ratio. * - Fetches available CKB and the maturing CKB based on bot capacities and deposit snapshots. * - Filters orders into user-owned and system orders based on the provided locks. * - Estimates user-owned orders maturity. * * @param client - The blockchain client interface. * @param locks - An array of lock scripts used to filter user cells. * @returns A promise that resolves to an object containing: * - system: The system state (fee rate, tip header, exchange ratio, order pool, etc.). * - user: The user's orders grouped as an array of OrderGroup. */ async getL1State( client: ccc.Client, locks: ccc.Script[], ): Promise<{ system: SystemState; user: { orders: OrderGroup[] } }> { const tip = await client.getTipHeader(); const exchangeRatio = Ratio.from(ickbExchangeRatio(tip)); // Parallel fetching of system components. const [{ ckbAvailable, ckbMaturing }, orders, feeRate] = await Promise.all([ this.getCkb(client, tip), collect(this.order.findOrders(client)), client.getFeeRate(), ]); const midInfo = new Info(exchangeRatio, exchangeRatio, 1); const userOrders: OrderGroup[] = []; const systemOrders: OrderCell[] = []; for (const group of orders) { if (group.isOwner(...locks)) { userOrders.push(group); } const { order } = group; const info = order.data.info; if ( (order.isCkb2UdtMatchable() && info.ckb2UdtCompare(midInfo) < 0) || (order.isUdt2CkbMatchable() && info.udt2CkbCompare(midInfo) < 0) ) { systemOrders.push(order); } } const system = { feeRate, tip, exchangeRatio, orderPool: systemOrders, ckbAvailable, ckbMaturing, }; // Estimates user orders maturity. for (const { order } of userOrders) { order.maturity = IckbSdk.maturity(order, system); } return { system, user: { orders: userOrders, }, }; } /** * Retrieves available CKB and maturing CKB values from the blockchain. * * This method: * - Fetches bot withdrawal requests and deposit snapshots. * - Aggregates available CKB balances from bot capacities. * - Calculates maturing CKB values (with their expected maturity timestamps) * based on deposit pool snapshots or via direct deposit cell lookups. * - Sorts and cumulatively sums the maturing values for later lookup. * * @param client - The blockchain client used for fetching data. * @param tip - The current block tip header. * @returns A Promise that resolves with: * - ckbAvailable: The total available CKB (as a FixedPoint). * - ckbMaturing: An array of maturing CKB objects containing the cumulative CKB * and the associated maturity timestamp. */ private async getCkb( client: ccc.Client, tip: ccc.ClientBlockHeader, ): Promise<{ ckbAvailable: ccc.FixedPoint; ckbMaturing: CkbCumulative[]; }> { const opts = { onChain: true, tip, }; // Start fetching bot iCKB withdrawal requests. const promiseBotWithdrawals = collect( this.ownedOwner.findWithdrawalGroups(client, this.bots, opts), ); // Initialize deposit pool snapshot. let poolSnapshotHex: ccc.Hex = "0x"; let poolSnapshotEpoch = Epoch.from([0n, 0n, 1n]); // Map to track each bot's available CKB (minus a reserved amount for internal operations). const bot2Ckb = new Map<string, ccc.FixedPoint>(); const reserved = -ccc.fixedPointFrom("2000"); for await (const c of this.capacity.findCapacities( client, this.bots, opts, )) { const key = hexFrom(c.cell.cellOutput.lock); const ckb = (bot2Ckb.get(key) ?? reserved) + c.ckbValue; bot2Ckb.set(key, ckb); // Find the most recent deposit pool snapshot from bot cell output data. const outputData = c.cell.outputData; if (outputData.length % 256 === 2) { const h = await getHeader(client, { type: "txHash", value: c.cell.outPoint.txHash, }); const e = Epoch.from(h.epoch); if (poolSnapshotEpoch.compare(e) < 0) { poolSnapshotHex = outputData; poolSnapshotEpoch = e; } } } const ckbMaturing = new Array<{ ckbValue: ccc.FixedPoint; maturity: ccc.Num; }>(); for (const wr of await promiseBotWithdrawals) { if (wr.owned.isReady) { // Update the bot's CKB based on withdrawal if the bot is ready. const key = hexFrom(wr.owner.cell.cellOutput.lock); const ckb = (bot2Ckb.get(key) ?? reserved) + wr.ckbValue; bot2Ckb.set(key, ckb); continue; } // Otherwise, add to maturing amounts. ckbMaturing.push({ ckbValue: wr.ckbValue, maturity: wr.owned.maturity.toUnix(tip), }); } // Sum available CKB across all bot lock scripts. let ckbAvailable = 0n; for (const ckb of bot2Ckb.values()) { if (ckb > 0n) { ckbAvailable += ckb; } } // Estimate available CKB from deposit pool snapshot. const tipEpoch = Epoch.from(tip.epoch); const oneCycle = Epoch.from([180n, 0n, 1n]); if (poolSnapshotHex !== "0x") { const eNumber = tip.epoch[0]; let start = Epoch.from([eNumber - (eNumber % 180n), 0n, 1n]); const step = Epoch.from([0n, 180n, 1024n]); const depositSize = convert(false, ICKB_DEPOSIT_CAP, tip); for (const binAmount of PoolSnapshot.decode(poolSnapshotHex)) { const end = start.add(step); if (binAmount > 0) { ckbMaturing.push({ ckbValue: BigInt(binAmount) * depositSize, maturity: tipEpoch.compare(tipEpoch) < 0 ? // If the bin has already started, assume worst-case timing. end.add(oneCycle).toUnix(tip) : // Otherwise, use the bin end as the maturity. end.toUnix(tip), }); } start = end; } } else { // Without snapshot data, fetch deposits directly. for await (const d of this.ickbLogic.findDeposits(client, opts)) { ckbMaturing.push({ ckbValue: d.ckbValue, maturity: d.maturity.toUnix(tip), }); } } // Sort maturing CKB entries by their maturity timestamp. ckbMaturing.sort((a, b) => Number(a.maturity - b.maturity)); // Calculate cumulative maturing CKB values. let cumulative = 0n; const ckbCumulativeMaturing: CkbCumulative[] = []; for (const { ckbValue, maturity } of ckbMaturing) { cumulative += ckbValue; ckbCumulativeMaturing.push({ ckbCumulative: cumulative, maturity }); } return { ckbAvailable, ckbMaturing: ckbCumulativeMaturing, }; } } /** * Represents the state of the system. */ export interface SystemState { /** The fee rate for transactions. */ feeRate: ccc.Num; /** The tip for the current block header. */ tip: ccc.ClientBlockHeader; /** The exchange ratio between CKB and UDT. */ exchangeRatio: Ratio; /** The order pool containing order cells matching system criteria. */ orderPool: OrderCell[]; /** The total available CKB (as FixedPoint). */ ckbAvailable: ccc.FixedPoint; /** Array of CKB maturing entries with cumulative amounts and maturity timestamps. */ ckbMaturing: CkbCumulative[]; } /** * Represents a cumulative CKB maturing entry. */ export interface CkbCumulative { /** The cumulative CKB value (as FixedPoint) up to this maturity. */ ckbCumulative: ccc.FixedPoint; /** The maturity timestamp (as ccc.Num). */ maturity: ccc.Num; }