UNPKG

@ickb/order

Version:

UDT Limit Order utilities built on top of CCC

466 lines 16.3 kB
import { ccc } from "@ckb-ccc/core"; import { BufferedGenerator, defaultFindCellsLimit, hexFrom, } from "@ickb/utils"; import { Info, OrderData, Ratio, Relative } from "./entities.js"; import { MasterCell, OrderCell, OrderGroup } from "./cells.js"; export class OrderManager { constructor(script, cellDeps, udtHandler) { Object.defineProperty(this, "script", { enumerable: true, configurable: true, writable: true, value: script }); Object.defineProperty(this, "cellDeps", { enumerable: true, configurable: true, writable: true, value: cellDeps }); Object.defineProperty(this, "udtHandler", { enumerable: true, configurable: true, writable: true, value: udtHandler }); } isOrder(cell) { return (cell.cellOutput.lock.eq(this.script) && Boolean(cell.cellOutput.type?.eq(this.udtHandler.script))); } isMaster(cell) { return Boolean(cell.cellOutput.type?.eq(this.script)); } static convert(isCkb2Udt, midpoint, amounts, options) { const fee = options?.fee ?? 0n; const feeBase = options?.feeBase ?? 100000n; const base = Ratio.from(midpoint); const adjusted = base.applyFee(isCkb2Udt, fee, feeBase); const amount = isCkb2Udt ? amounts.ckbValue : amounts.udtValue; const convertedAmount = adjusted.convert(isCkb2Udt, amount, true); let ckbFee = 0n; if (amount > 0n && fee !== 0n) { ckbFee = isCkb2Udt ? amount - base.convert(false, convertedAmount, false) : base.convert(true, amount, false) - convertedAmount; } const info = Info.create(isCkb2Udt, adjusted, options?.ckbMinMatchLog); return { convertedAmount, ckbFee, info }; } mint(tx, lock, info, amounts) { const { ckbValue, udtValue } = amounts; const data = OrderData.from({ udtValue, master: { type: "relative", value: Relative.create(1n), }, info, }); tx.addCellDeps(this.cellDeps); tx.addUdtHandlers(this.udtHandler); const position = tx.addOutput({ lock: this.script, type: this.udtHandler.script, }, data.toBytes()); tx.outputs[position].capacity += ckbValue; tx.addOutput({ lock, type: this.script, }); } addMatch(tx, match) { const partials = match.partials; if (partials.length === 0) { return; } tx.addCellDeps(this.cellDeps); tx.addUdtHandlers(this.udtHandler); for (const { order, ckbOut, udtOut } of partials) { tx.addInput(order.cell); tx.addOutput({ lock: this.script, type: this.udtHandler.script, capacity: ckbOut, }, OrderData.from({ udtValue: udtOut, master: { type: "absolute", value: order.getMaster(), }, info: order.data.info, }).toBytes()); } } match(order, isCkb2Udt, allowance) { return (OrderMatcher.from(order, isCkb2Udt, 0n)?.match(allowance) ?? { ckbDelta: 0n, udtDelta: 0n, partials: [], }); } static bestMatch(orderPool, allowance, exchangeRate, options) { const orderSize = orderPool[0]?.cell.occupiedSize ?? 0; if (!orderSize) { return { ckbDelta: 0n, udtDelta: 0n, partials: [], }; } const { ckbScale, udtScale } = exchangeRate; const feeRate = options?.feeRate ?? 1000n; const ckbMiningFee = (ccc.numFrom(36 + orderSize) * feeRate + 999n) / 1000n; const ckbAllowanceStep = options?.ckbAllowanceStep ?? ccc.fixedPointFrom("1000"); const udtAllowanceStep = (ckbAllowanceStep * ckbScale + udtScale - 1n) / udtScale; const ckb2UdtMatches = new BufferedGenerator(OrderManager.sequentialMatcher(orderPool, true, ckbAllowanceStep, ckbMiningFee), 2); const udt2CkbMatches = new BufferedGenerator(OrderManager.sequentialMatcher(orderPool, false, udtAllowanceStep, ckbMiningFee), 2); let best = { i: -1, j: -1, ckbDelta: 0n, udtDelta: 0n, partials: [], ckbAllowance: allowance.ckbValue, udtAllowance: allowance.udtValue, gain: -1n << 256n, }; while (best.i !== 0 && best.j !== 0) { ckb2UdtMatches.next(best.i); udt2CkbMatches.next(best.j); best.i = 0; best.j = 0; for (const [i, c2u] of ckb2UdtMatches.buffer.entries()) { for (const [j, u2c] of udt2CkbMatches.buffer.entries()) { const ckbDelta = c2u.ckbDelta + u2c.ckbDelta; const udtDelta = c2u.udtDelta + u2c.udtDelta; const partials = c2u.partials.concat(u2c.partials); const ckbFee = ckbMiningFee * ccc.fixedPointFrom(partials.length); const ckbAllowance = allowance.ckbValue + ckbDelta - ckbFee; const udtAllowance = allowance.udtValue + udtDelta; const gain = ckbDelta * ckbScale + udtDelta * udtScale; if (ckbAllowance >= 0n && udtAllowance >= 0n && gain > best.gain) { best = { i, j, ckbDelta, udtDelta, partials, ckbAllowance, udtAllowance, gain, }; } } } } const { ckbDelta, udtDelta, partials } = best; return { ckbDelta, udtDelta, partials, }; } static *sequentialMatcher(orderPool, isCkb2Udt, allowanceStep, ckbMiningFee) { const matchers = orderPool .map((o) => OrderMatcher.from(o, isCkb2Udt, ckbMiningFee)) .filter((m) => m !== undefined) .sort((a, b) => b.realRatio - a.realRatio); let acc = { ckbDelta: 0n, udtDelta: 0n, partials: [], }; let curr = acc; yield curr; loop: for (const matcher of matchers) { const maxMatch = matcher.bMaxMatch; const N = (maxMatch + allowanceStep - 1n) / allowanceStep; const q = maxMatch / N; const r = maxMatch % N; let allowance = 0n; for (let i = 0n; i < N; i++) { allowance += i < r ? q + 1n : q; const m = matcher.match(allowance); if (m.partials.length === 0) { continue loop; } curr = { ckbDelta: acc.ckbDelta + m.ckbDelta, udtDelta: acc.udtDelta + m.udtDelta, partials: acc.partials.concat(m.partials), }; yield curr; } acc = curr; } } melt(tx, groups, options) { const isFulfilledOnly = options?.isFulfilledOnly ?? false; if (isFulfilledOnly) { groups = groups.filter((g) => g.order.isFulfilled()); } if (groups.length === 0) { return; } tx.addCellDeps(this.cellDeps); tx.addUdtHandlers(this.udtHandler); for (const group of groups) { tx.addInput(group.order.cell); tx.addInput(group.master.cell); } } async *findOrders(client, options) { const limit = options?.limit ?? defaultFindCellsLimit; const [simpleOrders, allMasters] = await Promise.all([ this.findSimpleOrders(client, limit), this.findAllMasters(client, limit), ]); const rawGroups = new Map(allMasters.map((master) => [ hexFrom(master.cell.outPoint), { master, origin: undefined, orders: [], }, ])); for (const order of simpleOrders) { const master = order.getMaster(); const key = hexFrom(master); const rawGroup = rawGroups.get(key); if (!rawGroup) { continue; } rawGroup.orders.push(order); rawGroup.origin ?? (rawGroup.origin = this.findOrigin(client, master)); } for (const { master, origin: originPromise, orders, } of rawGroups.values()) { if (orders.length === 0 || !originPromise) { continue; } const origin = await originPromise; if (!origin) { continue; } const order = origin.resolve(orders); if (!order) { continue; } const orderGroup = OrderGroup.tryFrom(master, order, origin); if (!orderGroup) { continue; } yield orderGroup; } } async findSimpleOrders(client, limit) { const orders = []; for await (const cell of client.findCellsOnChain({ script: this.script, scriptType: "lock", filter: { script: this.udtHandler.script, }, scriptSearchMode: "exact", withData: true, }, "asc", limit)) { const order = OrderCell.tryFrom(cell); if (!order || !this.isOrder(cell)) { continue; } orders.push(order); } return orders; } async findAllMasters(client, limit) { const masters = []; for await (const cell of client.findCellsOnChain({ script: this.script, scriptType: "type", scriptSearchMode: "exact", withData: true, }, "asc", limit)) { if (!this.isMaster(cell)) { continue; } masters.push(new MasterCell(cell)); } return masters; } async findOrigin(client, master) { const { txHash, index: mIndex } = master; for (let index = mIndex - 1n; index >= 0n; index--) { const cell = await client.getCell({ txHash, index }); if (!cell) { return; } const order = OrderCell.tryFrom(cell); if (order?.getMaster().eq(master)) { return order; } } for (let index = mIndex + 1n; true; index++) { const cell = await client.getCell({ txHash, index }); if (!cell) { return; } const order = OrderCell.tryFrom(cell); if (order?.getMaster().eq(master)) { return order; } } } } export class OrderMatcher { constructor(order, isCkb2Udt, aScale, bScale, aIn, bIn, aMin, bMinMatch, bMaxMatch, bMaxOut, realRatio) { Object.defineProperty(this, "order", { enumerable: true, configurable: true, writable: true, value: order }); Object.defineProperty(this, "isCkb2Udt", { enumerable: true, configurable: true, writable: true, value: isCkb2Udt }); Object.defineProperty(this, "aScale", { enumerable: true, configurable: true, writable: true, value: aScale }); Object.defineProperty(this, "bScale", { enumerable: true, configurable: true, writable: true, value: bScale }); Object.defineProperty(this, "aIn", { enumerable: true, configurable: true, writable: true, value: aIn }); Object.defineProperty(this, "bIn", { enumerable: true, configurable: true, writable: true, value: bIn }); Object.defineProperty(this, "aMin", { enumerable: true, configurable: true, writable: true, value: aMin }); Object.defineProperty(this, "bMinMatch", { enumerable: true, configurable: true, writable: true, value: bMinMatch }); Object.defineProperty(this, "bMaxMatch", { enumerable: true, configurable: true, writable: true, value: bMaxMatch }); Object.defineProperty(this, "bMaxOut", { enumerable: true, configurable: true, writable: true, value: bMaxOut }); Object.defineProperty(this, "realRatio", { enumerable: true, configurable: true, writable: true, value: realRatio }); } static from(order, isCkb2Udt, ckbMiningFee) { let aScale; let bScale; let aIn; let bIn; let aMin; let bMinMatch; let aMiningFee; let bMiningFee; if (isCkb2Udt) { ({ ckbScale: aScale, udtScale: bScale } = order.data.info.ckbToUdt); [aIn, bIn] = [order.ckbValue, order.udtValue]; bMinMatch = (order.data.info.getCkbMinMatch() * bScale + aScale - 1n) / aScale; aMin = order.cell.cellOutput.capacity - order.ckbUnoccupied; aMiningFee = ckbMiningFee; bMiningFee = 0n; } else { ({ ckbScale: bScale, udtScale: aScale } = order.data.info.ckbToUdt); [bIn, aIn] = [order.ckbValue, order.udtValue]; bMinMatch = order.data.info.getCkbMinMatch(); aMin = 0n; aMiningFee = 0n; bMiningFee = ckbMiningFee; } if (aIn <= aMin + aMiningFee || aScale <= 0n || bScale <= 0n) { return; } const bMaxOut = OrderMatcher.nonDecreasing(aScale, bScale, aIn, bIn, aMin); const bMaxMatch = bMaxOut - bIn; if (bMinMatch > bMaxMatch) { bMinMatch = bMaxMatch; } const realRatio = Number(aIn - aMin - aMiningFee) / Number(bMaxMatch + bMiningFee); if (realRatio <= 0) { return; } return new OrderMatcher(order, isCkb2Udt, aScale, bScale, aIn, bIn, aMin, bMinMatch, bMaxMatch, bMaxOut, realRatio); } match(bAllowance) { if (bAllowance < this.bMinMatch) { return { ckbDelta: 0n, udtDelta: 0n, partials: [], }; } if (bAllowance >= this.bMaxMatch) { return this.create(this.aMin, this.bMaxOut); } const bOut = this.bIn + bAllowance; const aOut = OrderMatcher.nonDecreasing(this.bScale, this.aScale, this.bIn, this.aIn, bOut); return this.create(aOut, bOut); } create(aOut, bOut) { return this.isCkb2Udt ? { ckbDelta: this.aIn - aOut, udtDelta: this.bIn - bOut, partials: [ { order: this.order, ckbOut: aOut, udtOut: bOut, }, ], } : { ckbDelta: this.bIn - bOut, udtDelta: this.aIn - aOut, partials: [ { order: this.order, ckbOut: bOut, udtOut: aOut, }, ], }; } static nonDecreasing(aScale, bScale, aIn, bIn, aOut) { return (aScale * (aIn - aOut) + bScale * (bIn + 1n) - 1n) / bScale; } } //# sourceMappingURL=order.js.map