@ickb/order
Version:
UDT Limit Order utilities built on top of CCC
466 lines • 16.3 kB
JavaScript
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