@ickb/sdk 1000.0.82 → 9000.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -32
- package/dist/constants.d.ts +13 -22
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +80 -76
- package/dist/constants.js.map +1 -1
- package/dist/conversion/estimate.d.ts +39 -0
- package/dist/conversion/estimate.d.ts.map +1 -0
- package/dist/conversion/estimate.js +117 -0
- package/dist/conversion/estimate.js.map +1 -0
- package/dist/conversion/maturity.d.ts +15 -0
- package/dist/conversion/maturity.d.ts.map +1 -0
- package/dist/conversion/maturity.js +112 -0
- package/dist/conversion/maturity.js.map +1 -0
- package/dist/conversion/plans.d.ts +20 -0
- package/dist/conversion/plans.d.ts.map +1 -0
- package/dist/conversion/plans.js +106 -0
- package/dist/conversion/plans.js.map +1 -0
- package/dist/conversion/projection.d.ts +22 -0
- package/dist/conversion/projection.d.ts.map +1 -0
- package/dist/conversion/projection.js +91 -0
- package/dist/conversion/projection.js.map +1 -0
- package/dist/conversion/types.d.ts +252 -0
- package/dist/conversion/types.d.ts.map +1 -0
- package/dist/conversion/types.js +2 -0
- package/dist/conversion/types.js.map +1 -0
- package/dist/conversion/withdrawal_ring.d.ts +36 -0
- package/dist/conversion/withdrawal_ring.d.ts.map +1 -0
- package/dist/conversion/withdrawal_ring.js +87 -0
- package/dist/conversion/withdrawal_ring.js.map +1 -0
- package/dist/dao.d.ts +86 -0
- package/dist/dao.d.ts.map +1 -0
- package/dist/dao.js +113 -0
- package/dist/dao.js.map +1 -0
- package/dist/index.d.ts +51 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/logic.d.ts +85 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +184 -0
- package/dist/logic.js.map +1 -0
- package/dist/order/cells.d.ts +207 -0
- package/dist/order/cells.d.ts.map +1 -0
- package/dist/order/cells.js +427 -0
- package/dist/order/cells.js.map +1 -0
- package/dist/order/conversion.d.ts +43 -0
- package/dist/order/conversion.d.ts.map +1 -0
- package/dist/order/conversion.js +123 -0
- package/dist/order/conversion.js.map +1 -0
- package/dist/order/fee.d.ts +9 -0
- package/dist/order/fee.d.ts.map +1 -0
- package/dist/order/fee.js +24 -0
- package/dist/order/fee.js.map +1 -0
- package/dist/order/fill.d.ts +36 -0
- package/dist/order/fill.d.ts.map +1 -0
- package/dist/order/fill.js +53 -0
- package/dist/order/fill.js.map +1 -0
- package/dist/order/info.d.ts +62 -0
- package/dist/order/info.d.ts.map +1 -0
- package/dist/order/info.js +99 -0
- package/dist/order/info.js.map +1 -0
- package/dist/order/master.d.ts +49 -0
- package/dist/order/master.d.ts.map +1 -0
- package/dist/order/master.js +27 -0
- package/dist/order/master.js.map +1 -0
- package/dist/order/matcher.d.ts +43 -0
- package/dist/order/matcher.d.ts.map +1 -0
- package/dist/order/matcher.js +123 -0
- package/dist/order/matcher.js.map +1 -0
- package/dist/order/order.d.ts +59 -0
- package/dist/order/order.d.ts.map +1 -0
- package/dist/order/order.js +239 -0
- package/dist/order/order.js.map +1 -0
- package/dist/order/order_data.d.ts +51 -0
- package/dist/order/order_data.d.ts.map +1 -0
- package/dist/order/order_data.js +64 -0
- package/dist/order/order_data.js.map +1 -0
- package/dist/order/ratio.d.ts +42 -0
- package/dist/order/ratio.d.ts.map +1 -0
- package/dist/order/ratio.js +82 -0
- package/dist/order/ratio.js.map +1 -0
- package/dist/order/relative.d.ts +43 -0
- package/dist/order/relative.d.ts.map +1 -0
- package/dist/order/relative.js +54 -0
- package/dist/order/relative.js.map +1 -0
- package/dist/owned_owner.d.ts +113 -0
- package/dist/owned_owner.d.ts.map +1 -0
- package/dist/owned_owner.js +280 -0
- package/dist/owned_owner.js.map +1 -0
- package/dist/sdk.d.ts +71 -42
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +388 -209
- package/dist/sdk.js.map +1 -1
- package/dist/send/sign_and_send_transaction.d.ts +46 -0
- package/dist/send/sign_and_send_transaction.d.ts.map +1 -0
- package/dist/send/sign_and_send_transaction.js +101 -0
- package/dist/send/sign_and_send_transaction.js.map +1 -0
- package/dist/send/wait_transaction.d.ts +41 -0
- package/dist/send/wait_transaction.d.ts.map +1 -0
- package/dist/send/wait_transaction.js +204 -0
- package/dist/send/wait_transaction.js.map +1 -0
- package/dist/udt.d.ts +123 -0
- package/dist/udt.d.ts.map +1 -0
- package/dist/udt.js +244 -0
- package/dist/udt.js.map +1 -0
- package/dist/utils/chain.d.ts +38 -0
- package/dist/utils/chain.d.ts.map +1 -0
- package/dist/utils/chain.js +20 -0
- package/dist/utils/chain.js.map +1 -0
- package/dist/utils/codec.d.ts +19 -0
- package/dist/utils/codec.d.ts.map +1 -0
- package/dist/utils/codec.js +66 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/transaction_header.d.ts +17 -0
- package/dist/utils/transaction_header.d.ts.map +1 -0
- package/dist/utils/transaction_header.js +44 -0
- package/dist/utils/transaction_header.js.map +1 -0
- package/dist/utils/utils.d.ts +126 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +151 -0
- package/dist/utils/utils.js.map +1 -0
- package/package.json +19 -26
- package/src/constants.ts +84 -181
- package/src/conversion/estimate.ts +157 -0
- package/src/conversion/maturity.ts +159 -0
- package/src/conversion/plans.ts +144 -0
- package/src/conversion/projection.ts +128 -0
- package/src/conversion/types.ts +287 -0
- package/src/conversion/withdrawal_ring.ts +126 -0
- package/src/dao.ts +172 -0
- package/src/index.ts +89 -3
- package/src/logic.ts +295 -0
- package/src/order/cells.ts +520 -0
- package/src/order/conversion.ts +173 -0
- package/src/order/fee.ts +30 -0
- package/src/order/fill.ts +83 -0
- package/src/order/info.ts +178 -0
- package/src/order/master.ts +42 -0
- package/src/order/matcher.ts +191 -0
- package/src/order/order.ts +302 -0
- package/src/order/order_data.ts +128 -0
- package/src/order/ratio.ts +144 -0
- package/src/order/relative.ts +106 -0
- package/src/owned_owner.ts +394 -0
- package/src/sdk.ts +555 -440
- package/src/send/sign_and_send_transaction.ts +120 -0
- package/src/send/wait_transaction.ts +271 -0
- package/src/udt.ts +317 -0
- package/src/utils/chain.ts +43 -0
- package/src/utils/codec.ts +74 -0
- package/src/utils/transaction_header.ts +66 -0
- package/src/utils/utils.ts +237 -0
- package/dist/codec.d.ts +0 -3
- package/dist/codec.d.ts.map +0 -1
- package/dist/codec.js +0 -58
- package/dist/codec.js.map +0 -1
- package/src/codec.ts +0 -138
package/src/order/fee.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import { ceilDiv } from "../utils/utils.ts";
|
|
3
|
+
import type { OrderGroup } from "./cells.ts";
|
|
4
|
+
|
|
5
|
+
const CELL_INPUT_SERIALIZED_SIZE = 44;
|
|
6
|
+
// CellOutput table/script wrappers plus DynVec offset; output data Bytes plus offset.
|
|
7
|
+
const CELL_OUTPUT_SERIALIZATION_OVERHEAD = 60;
|
|
8
|
+
const OUTPUT_DATA_SERIALIZATION_OVERHEAD = 8;
|
|
9
|
+
// The supported SDK completion prepares a later signer witness, inserting one
|
|
10
|
+
// empty witness-vector entry for each preceding order input.
|
|
11
|
+
const EMPTY_WITNESS_SERIALIZATION_SIZE = 8;
|
|
12
|
+
const PREPARED_PARTIAL_SERIALIZATION_OVERHEAD =
|
|
13
|
+
CELL_INPUT_SERIALIZED_SIZE +
|
|
14
|
+
CELL_OUTPUT_SERIALIZATION_OVERHEAD +
|
|
15
|
+
OUTPUT_DATA_SERIALIZATION_OVERHEAD +
|
|
16
|
+
EMPTY_WITNESS_SERIALIZATION_SIZE;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The mining fee one more matched order adds to a prepared transaction at the fee rate
|
|
20
|
+
* (shannons per thousand bytes): its input, its replacement output with data, and its
|
|
21
|
+
* empty witness, sized for the largest order of the pool.
|
|
22
|
+
*/
|
|
23
|
+
export function partialOrderFee(orderPool: OrderGroup[], feeRate: ccc.Num): bigint {
|
|
24
|
+
const orderSize = orderPool.reduce(
|
|
25
|
+
(largest, group) => Math.max(largest, group.order.cell.occupiedSize),
|
|
26
|
+
0,
|
|
27
|
+
);
|
|
28
|
+
const bytes = ccc.numFrom(orderSize + PREPARED_PARTIAL_SERIALIZATION_OVERHEAD);
|
|
29
|
+
return ceilDiv(bytes * feeRate, 1000n);
|
|
30
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import type { ExchangeRatio } from "../utils/utils.ts";
|
|
3
|
+
import type { OrderGroup } from "./cells.ts";
|
|
4
|
+
import { partialOrderFee } from "./fee.ts";
|
|
5
|
+
import { type Match, OrderMatcher } from "./matcher.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What a fill must return before the bot takes it, in mining fees of its own: the buffer
|
|
9
|
+
* for the deposits and withdrawals that rebalance the inventory it moves (user decision
|
|
10
|
+
* 2026-09-10, tuned on testnet). One rule for the bot's fills and the refused-buy
|
|
11
|
+
* classification used by the generator and interface.
|
|
12
|
+
*/
|
|
13
|
+
const FILL_COST_FEES = 10n;
|
|
14
|
+
|
|
15
|
+
/** The cost of taking one fill, in the exchange value's unit. */
|
|
16
|
+
export function fillCost(fee: bigint, { ckbScale }: ExchangeRatio): bigint {
|
|
17
|
+
return FILL_COST_FEES * fee * ckbScale;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A fill's exchange value at the DAO ratio, net of the cost of taking it. */
|
|
21
|
+
export function netOf(
|
|
22
|
+
fill: Match,
|
|
23
|
+
cost: bigint,
|
|
24
|
+
{ ckbScale, udtScale }: ExchangeRatio,
|
|
25
|
+
): bigint {
|
|
26
|
+
return fill.ckbDelta * ckbScale + fill.udtDelta * udtScale - cost;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether the matcher's whole fill returns its cost. */
|
|
30
|
+
export function returnsCost(
|
|
31
|
+
matcher: OrderMatcher,
|
|
32
|
+
cost: bigint,
|
|
33
|
+
exchangeRatio: ExchangeRatio,
|
|
34
|
+
): boolean {
|
|
35
|
+
return netOf(matcher.match(matcher.bMaxMatch), cost, exchangeRatio) > 0n;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether the bot would take the whole order in the given direction. Judged on the order
|
|
40
|
+
* alone, independently of the bot's current inventory. Price and the sampled fee rate
|
|
41
|
+
* decide profitability; balances decide whether a particular turn can afford it.
|
|
42
|
+
*/
|
|
43
|
+
export function fillsWhole(
|
|
44
|
+
group: OrderGroup,
|
|
45
|
+
isCkb2Udt: boolean,
|
|
46
|
+
exchangeRatio: ExchangeRatio,
|
|
47
|
+
feeRate: ccc.Num,
|
|
48
|
+
): boolean {
|
|
49
|
+
const fee = partialOrderFee([group], feeRate);
|
|
50
|
+
const matcher = OrderMatcher.from(group, isCkb2Udt, fee);
|
|
51
|
+
return (
|
|
52
|
+
matcher !== undefined &&
|
|
53
|
+
returnsCost(matcher, fillCost(fee, exchangeRatio), exchangeRatio)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether a buy fails today's whole-fill profitability rule and should be collected.
|
|
59
|
+
* DAO growth makes it less attractive, though a lower fee rate could change this result.
|
|
60
|
+
* Sells are not refused: growth can make them profitable later, so unfilled sells return
|
|
61
|
+
* only through the age cutoff ({@link isStale}).
|
|
62
|
+
*/
|
|
63
|
+
export function isRefused(
|
|
64
|
+
group: OrderGroup,
|
|
65
|
+
{ exchangeRatio, feeRate }: { exchangeRatio: ExchangeRatio; feeRate: ccc.Num },
|
|
66
|
+
): boolean {
|
|
67
|
+
return (
|
|
68
|
+
group.order.data.info.isCkb2Udt() && !fillsWhole(group, true, exchangeRatio, feeRate)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Thirty days of eight-second blocks. */
|
|
73
|
+
export const STALE_ORDER_BLOCKS = (30n * 24n * 60n * 60n) / 8n;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether the order has sat on the book for thirty days: whatever the reason (dust under
|
|
77
|
+
* the fill's cost, a remainder another matcher left, an ask above the market), it is
|
|
78
|
+
* collected and its funds returned. Age from the origin's block, so an uncommitted origin
|
|
79
|
+
* is as fresh as an order gets. The block count assumes eight-second blocks, not wall time.
|
|
80
|
+
*/
|
|
81
|
+
export function isStale(group: OrderGroup, tip: ccc.ClientBlockHeader): boolean {
|
|
82
|
+
return (group.blockNumber ?? tip.number) + STALE_ORDER_BLOCKS <= tip.number;
|
|
83
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ccc, mol } from "@ckb-ccc/core";
|
|
2
|
+
import { CheckedUint8 } from "../utils/codec.ts";
|
|
3
|
+
import type { ExchangeRatio } from "../utils/utils.ts";
|
|
4
|
+
import { Ratio } from "./ratio.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wire shape for an order's directional ratios and minimum CKB match.
|
|
8
|
+
*/
|
|
9
|
+
export interface InfoLike {
|
|
10
|
+
/** Populated when the order can trade CKB for UDT; empty otherwise. */
|
|
11
|
+
ckbToUdt: ExchangeRatio;
|
|
12
|
+
/** Populated when the order can trade UDT for CKB; empty otherwise. */
|
|
13
|
+
udtToCkb: ExchangeRatio;
|
|
14
|
+
/** Base-2 exponent for the minimum CKB match amount. */
|
|
15
|
+
ckbMinMatchLog: ccc.FixedPointLike;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const InfoCodec = mol.struct({
|
|
19
|
+
ckbToUdt: Ratio,
|
|
20
|
+
udtToCkb: Ratio,
|
|
21
|
+
ckbMinMatchLog: CheckedUint8,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const InfoBase = ccc.Entity.Base<InfoLike, Info>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Order price and minimum-match metadata.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Exactly one populated ratio describes a directional order. Two populated
|
|
31
|
+
* ratios describe a dual-ratio order. Validation rejects empty/invalid pairs and
|
|
32
|
+
* ratio pairs that allow value extraction.
|
|
33
|
+
*/
|
|
34
|
+
export interface Info {
|
|
35
|
+
/** Ratio for CKB-to-UDT matching, or empty when unavailable. */
|
|
36
|
+
ckbToUdt: Ratio;
|
|
37
|
+
/** Ratio for UDT-to-CKB matching, or empty when unavailable. */
|
|
38
|
+
udtToCkb: Ratio;
|
|
39
|
+
/** Base-2 exponent for the minimum CKB match amount. */
|
|
40
|
+
ckbMinMatchLog: number;
|
|
41
|
+
/** Creates a copy of this order info. */
|
|
42
|
+
clone(): Info;
|
|
43
|
+
/** Returns whether another value has the same order info. */
|
|
44
|
+
eq(other: InfoLike): boolean;
|
|
45
|
+
/** Returns `2 ** ckbMinMatchLog` as the minimum CKB match amount. */
|
|
46
|
+
getCkbMinMatch(): ccc.FixedPoint;
|
|
47
|
+
/** Returns the CKB hash of the serialized order info. */
|
|
48
|
+
hash(): ccc.Hex;
|
|
49
|
+
/** Returns whether the CKB-to-UDT ratio is populated. */
|
|
50
|
+
isCkb2Udt(): boolean;
|
|
51
|
+
/** Returns whether both directional ratios are populated. */
|
|
52
|
+
isDualRatio(): boolean;
|
|
53
|
+
/** Returns whether the UDT-to-CKB ratio is populated. */
|
|
54
|
+
isUdt2Ckb(): boolean;
|
|
55
|
+
/** Serializes the order info to bytes. */
|
|
56
|
+
toBytes(): ccc.Bytes;
|
|
57
|
+
/** Serializes the order info to full-width hexadecimal. */
|
|
58
|
+
toHex(): ccc.Hex;
|
|
59
|
+
/** Throws if the ratios or minimum-match exponent are invalid. */
|
|
60
|
+
validate(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2^36 shannons, about 687 CKB, leaves fresh remainders headroom above the fill-cost floor.
|
|
64
|
+
// Orders below twice that minimum are whole-only. Converted to iCKB, this band must stay
|
|
65
|
+
// below `ICKB_REFILL_BELOW` so a buyer too small to split triggers a refill when inventory
|
|
66
|
+
// cannot fill it whole. Fee changes and an aging buy can still make a remainder unprofitable.
|
|
67
|
+
export const CKB_MIN_MATCH_LOG_DEFAULT = 36;
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow -- Preserve the runtime constructor name.
|
|
70
|
+
const InfoImplementation = class Info extends InfoBase {
|
|
71
|
+
static {
|
|
72
|
+
ccc.codec(InfoCodec)(this);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Ratio for CKB-to-UDT matching, or empty when unavailable. */
|
|
76
|
+
public readonly ckbToUdt: Ratio;
|
|
77
|
+
/** Ratio for UDT-to-CKB matching, or empty when unavailable. */
|
|
78
|
+
public readonly udtToCkb: Ratio;
|
|
79
|
+
/** Base-2 exponent for the minimum CKB match amount. */
|
|
80
|
+
public readonly ckbMinMatchLog: number;
|
|
81
|
+
|
|
82
|
+
/** Creates order info from normalized ratio objects. */
|
|
83
|
+
constructor(ckbToUdt: Ratio, udtToCkb: Ratio, ckbMinMatchLog: number) {
|
|
84
|
+
super();
|
|
85
|
+
this.ckbToUdt = ckbToUdt;
|
|
86
|
+
this.udtToCkb = udtToCkb;
|
|
87
|
+
this.ckbMinMatchLog = ckbMinMatchLog;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Normalizes an `InfoLike` wire object or existing entity into `Info`. */
|
|
91
|
+
public static override from(info: InfoLike): Info {
|
|
92
|
+
if (info instanceof Info) {
|
|
93
|
+
return info;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { ckbToUdt, udtToCkb, ckbMinMatchLog } = info;
|
|
97
|
+
return new Info(Ratio.from(ckbToUdt), Ratio.from(udtToCkb), Number(ckbMinMatchLog));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Creates directional order info from one ratio, at the default minimum match. */
|
|
101
|
+
public static create(isCkb2Udt: boolean, ratioLike: ExchangeRatio): Info {
|
|
102
|
+
return Info.from({
|
|
103
|
+
ckbToUdt: isCkb2Udt ? ratioLike : Ratio.empty(),
|
|
104
|
+
udtToCkb: isCkb2Udt ? Ratio.empty() : ratioLike,
|
|
105
|
+
ckbMinMatchLog: CKB_MIN_MATCH_LOG_DEFAULT,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Throws when ratio pairing or minimum-match exponent is invalid. */
|
|
110
|
+
public validate(): void {
|
|
111
|
+
if (!isValidCkbMinMatchLog(this.ckbMinMatchLog)) {
|
|
112
|
+
throw new Error("ckbMinMatchLog invalid");
|
|
113
|
+
}
|
|
114
|
+
this.ckbToUdt.validate();
|
|
115
|
+
this.udtToCkb.validate();
|
|
116
|
+
|
|
117
|
+
if (this.ckbToUdt.isEmpty()) {
|
|
118
|
+
if (this.udtToCkb.isPopulated()) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw new Error("ckbToUdt is Empty, but udtToCkb is not Populated");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.udtToCkb.isEmpty()) {
|
|
125
|
+
if (this.ckbToUdt.isPopulated()) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new Error("udtToCkb is Empty, but ckbToUdt is not Populated");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!this.ckbToUdt.isPopulated() || !this.udtToCkb.isPopulated()) {
|
|
132
|
+
throw new Error("One ratio is invalid, so not Empty and not Populated");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
this.ckbToUdt.ckbScale * this.udtToCkb.udtScale <
|
|
137
|
+
this.ckbToUdt.udtScale * this.udtToCkb.ckbScale
|
|
138
|
+
) {
|
|
139
|
+
throw new Error("udtToCkb and ckbToUdt allow order value to be extracted");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Returns the minimum CKB match amount. */
|
|
144
|
+
public getCkbMinMatch(): ccc.FixedPoint {
|
|
145
|
+
return 1n << BigInt(this.ckbMinMatchLog);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Returns true when CKB-to-UDT matching is enabled. */
|
|
149
|
+
public isCkb2Udt(): boolean {
|
|
150
|
+
return this.ckbToUdt.isPopulated();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Returns true when UDT-to-CKB matching is enabled. */
|
|
154
|
+
public isUdt2Ckb(): boolean {
|
|
155
|
+
return this.udtToCkb.isPopulated();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Returns true when both directions are enabled. */
|
|
159
|
+
public isDualRatio(): boolean {
|
|
160
|
+
return this.isCkb2Udt() && this.isUdt2Ckb();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** CCC-backed order-info constructor and codec. */
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare -- The public type and runtime constructor intentionally share a name.
|
|
166
|
+
export const Info: {
|
|
167
|
+
byteLength?: number;
|
|
168
|
+
new (ckbToUdt: Ratio, udtToCkb: Ratio, ckbMinMatchLog: number): Info;
|
|
169
|
+
create: (isCkb2Udt: boolean, ratioLike: ExchangeRatio) => Info;
|
|
170
|
+
decode: (encoded: ccc.BytesLike) => Info;
|
|
171
|
+
encode: (info: InfoLike) => ccc.Bytes;
|
|
172
|
+
from: (info: InfoLike) => Info;
|
|
173
|
+
fromBytes: (encoded: ccc.BytesLike) => Info;
|
|
174
|
+
} = InfoImplementation;
|
|
175
|
+
|
|
176
|
+
function isValidCkbMinMatchLog(value: number): boolean {
|
|
177
|
+
return Number.isInteger(value) && value >= 0 && value <= 64;
|
|
178
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ccc, mol } from "@ckb-ccc/core";
|
|
2
|
+
import { Relative, type RelativeLike } from "./relative.ts";
|
|
3
|
+
|
|
4
|
+
// Both variants are 36 bytes, and OrderData is a struct, so the union must be fixed-size:
|
|
5
|
+
// CCC 1.23+ sizes a plain union dynamically and rejects it as a struct field.
|
|
6
|
+
const MasterCodec = mol.fixedUnion({
|
|
7
|
+
relative: Relative,
|
|
8
|
+
absolute: ccc.OutPoint,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Master pointer before normalization.
|
|
13
|
+
*/
|
|
14
|
+
export type MasterLike =
|
|
15
|
+
| { type: "relative"; value: RelativeLike }
|
|
16
|
+
| { type: "absolute"; value: ccc.OutPointLike };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Master pointer stored in order data.
|
|
20
|
+
*/
|
|
21
|
+
export type Master =
|
|
22
|
+
{ type: "relative"; value: Relative } | { type: "absolute"; value: ccc.OutPoint };
|
|
23
|
+
|
|
24
|
+
/** Normalizes a master pointer into entity values. */
|
|
25
|
+
export function masterFrom(master: MasterLike): Master {
|
|
26
|
+
if (master.type === "relative") {
|
|
27
|
+
return { type: "relative", value: Relative.from(master.value) };
|
|
28
|
+
}
|
|
29
|
+
return { type: "absolute", value: ccc.OutPoint.from(master.value) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Validates a normalized master pointer. */
|
|
33
|
+
export function masterValidate(master: Master): void {
|
|
34
|
+
const { type, value } = master;
|
|
35
|
+
if (type === "relative") {
|
|
36
|
+
value.validate();
|
|
37
|
+
} else if (!/^0x[0-9a-f]{64}$/i.test(value.txHash) || value.index < 0) {
|
|
38
|
+
throw new Error("OutPoint invalid");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { MasterCodec };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import { ceilDiv, minBigInt } from "../utils/utils.ts";
|
|
3
|
+
import type { OrderCell, OrderGroup } from "./cells.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The fills of one or more orders from the matcher caller's perspective.
|
|
7
|
+
*/
|
|
8
|
+
export interface Match {
|
|
9
|
+
/** Net CKB change from the match. */
|
|
10
|
+
ckbDelta: bigint;
|
|
11
|
+
|
|
12
|
+
/** Net UDT change from the match. */
|
|
13
|
+
udtDelta: bigint;
|
|
14
|
+
|
|
15
|
+
/** Partial order outputs that must replace matched order inputs. */
|
|
16
|
+
partials: Array<{
|
|
17
|
+
/** Resolved group that proves the matched order's mint origin. */
|
|
18
|
+
group: OrderGroup;
|
|
19
|
+
|
|
20
|
+
/** CKB capacity for the replacement partial order output. */
|
|
21
|
+
ckbOut: ccc.FixedPoint;
|
|
22
|
+
|
|
23
|
+
/** UDT amount for the replacement partial order output. */
|
|
24
|
+
udtOut: ccc.FixedPoint;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One order's fills in one direction, in the contract's terms: `a` is the asset the order
|
|
30
|
+
* gives, `b` the asset the caller pays. A partial pays `b` between `bMinMatch` and
|
|
31
|
+
* `bMaxMatch`; paying `bMaxMatch` completes the order.
|
|
32
|
+
*/
|
|
33
|
+
export class OrderMatcher {
|
|
34
|
+
public readonly group: OrderGroup;
|
|
35
|
+
public readonly isCkb2Udt: boolean;
|
|
36
|
+
public readonly aScale: ccc.Num;
|
|
37
|
+
public readonly bScale: ccc.Num;
|
|
38
|
+
public readonly aIn: ccc.FixedPoint;
|
|
39
|
+
public readonly bIn: ccc.FixedPoint;
|
|
40
|
+
public readonly aMin: ccc.FixedPoint;
|
|
41
|
+
public readonly bMinMatch: ccc.FixedPoint;
|
|
42
|
+
public readonly bMaxMatch: ccc.FixedPoint;
|
|
43
|
+
public readonly bMaxOut: ccc.FixedPoint;
|
|
44
|
+
|
|
45
|
+
private constructor(
|
|
46
|
+
group: OrderGroup,
|
|
47
|
+
isCkb2Udt: boolean,
|
|
48
|
+
{ aScale, bScale, aIn, bIn, aMin, bMinMatch, bMaxMatch, bMaxOut }: OrderMatcherTerms,
|
|
49
|
+
) {
|
|
50
|
+
this.group = group;
|
|
51
|
+
this.isCkb2Udt = isCkb2Udt;
|
|
52
|
+
this.aScale = aScale;
|
|
53
|
+
this.bScale = bScale;
|
|
54
|
+
this.aIn = aIn;
|
|
55
|
+
this.bIn = bIn;
|
|
56
|
+
this.aMin = aMin;
|
|
57
|
+
this.bMinMatch = bMinMatch;
|
|
58
|
+
this.bMaxMatch = bMaxMatch;
|
|
59
|
+
this.bMaxOut = bMaxOut;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The order's matcher in one direction, or `undefined` when nothing gains in it. */
|
|
63
|
+
public static from(
|
|
64
|
+
group: OrderGroup,
|
|
65
|
+
isCkb2Udt: boolean,
|
|
66
|
+
ckbMiningFee: ccc.FixedPoint,
|
|
67
|
+
): OrderMatcher | undefined {
|
|
68
|
+
const terms = orderMatcherTerms(group.order, isCkb2Udt, ckbMiningFee);
|
|
69
|
+
return terms === undefined ? undefined : new OrderMatcher(group, isCkb2Udt, terms);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public match(bAllowance: ccc.FixedPoint): Match {
|
|
73
|
+
if (bAllowance < this.bMinMatch) {
|
|
74
|
+
return { ckbDelta: 0n, udtDelta: 0n, partials: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (bAllowance >= this.bMaxMatch) {
|
|
78
|
+
return this.create(this.aMin, this.bMaxOut);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const bOut = this.bIn + bAllowance;
|
|
82
|
+
const aOut = nonDecreasing({
|
|
83
|
+
aScale: this.bScale,
|
|
84
|
+
bScale: this.aScale,
|
|
85
|
+
aIn: this.bIn,
|
|
86
|
+
bIn: this.aIn,
|
|
87
|
+
aOut: bOut,
|
|
88
|
+
});
|
|
89
|
+
return this.create(aOut, bOut);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public create(aOut: ccc.FixedPoint, bOut: ccc.FixedPoint): Match {
|
|
93
|
+
return this.isCkb2Udt
|
|
94
|
+
? {
|
|
95
|
+
ckbDelta: this.aIn - aOut,
|
|
96
|
+
udtDelta: this.bIn - bOut,
|
|
97
|
+
partials: [{ group: this.group, ckbOut: aOut, udtOut: bOut }],
|
|
98
|
+
}
|
|
99
|
+
: {
|
|
100
|
+
ckbDelta: this.bIn - bOut,
|
|
101
|
+
udtDelta: this.aIn - aOut,
|
|
102
|
+
partials: [{ group: this.group, ckbOut: bOut, udtOut: aOut }],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface OrderMatcherTerms {
|
|
108
|
+
aScale: ccc.Num;
|
|
109
|
+
bScale: ccc.Num;
|
|
110
|
+
aIn: ccc.FixedPoint;
|
|
111
|
+
bIn: ccc.FixedPoint;
|
|
112
|
+
aMin: ccc.FixedPoint;
|
|
113
|
+
bMinMatch: ccc.FixedPoint;
|
|
114
|
+
bMaxMatch: ccc.FixedPoint;
|
|
115
|
+
bMaxOut: ccc.FixedPoint;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function orderMatcherTerms(
|
|
119
|
+
order: OrderCell,
|
|
120
|
+
isCkb2Udt: boolean,
|
|
121
|
+
ckbMiningFee: ccc.FixedPoint,
|
|
122
|
+
): OrderMatcherTerms | undefined {
|
|
123
|
+
if (isCkb2Udt ? !order.isCkb2UdtMatchable() : !order.isUdt2CkbMatchable()) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const { info } = order.data;
|
|
127
|
+
const ckbMinMatch = info.getCkbMinMatch();
|
|
128
|
+
let terms: Omit<OrderMatcherTerms, "bMaxMatch" | "bMaxOut">;
|
|
129
|
+
if (isCkb2Udt) {
|
|
130
|
+
const { ckbScale: aScale, udtScale: bScale } = info.ckbToUdt;
|
|
131
|
+
terms = {
|
|
132
|
+
aScale,
|
|
133
|
+
bScale,
|
|
134
|
+
aIn: order.ckbValue,
|
|
135
|
+
bIn: order.udtValue,
|
|
136
|
+
aMin: order.cell.cellOutput.capacity - order.ckbUnoccupied,
|
|
137
|
+
// The contract's ckb2udt minimum is a plain CKB delta (limit_order entry.rs:116);
|
|
138
|
+
// a value-conserving partial moving u UDT moves u*udtScale/ckbScale CKB, so the
|
|
139
|
+
// UDT-denominated floor is ceil(minMatch * ckbScale / udtScale).
|
|
140
|
+
bMinMatch: ceilDiv(ckbMinMatch * aScale, bScale),
|
|
141
|
+
};
|
|
142
|
+
} else {
|
|
143
|
+
const { ckbScale: bScale, udtScale: aScale } = info.udtToCkb;
|
|
144
|
+
// The contract's udt2ckb minimum values the UDT moved at the ratio, and a payment of
|
|
145
|
+
// p CKB moves floor(p*bScale/aScale) UDT, so the smallest payment the contract accepts
|
|
146
|
+
// is the one whose floor reaches ceil(min*bScale/aScale) UDT: the plain minimum,
|
|
147
|
+
// rounded up to the ratio's grid.
|
|
148
|
+
const udtMinMatch = ceilDiv(ckbMinMatch * bScale, aScale);
|
|
149
|
+
terms = {
|
|
150
|
+
aScale,
|
|
151
|
+
bScale,
|
|
152
|
+
aIn: order.udtValue,
|
|
153
|
+
bIn: order.ckbValue,
|
|
154
|
+
aMin: 0n,
|
|
155
|
+
bMinMatch: ceilDiv(udtMinMatch * aScale, bScale),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { aScale, bScale, aIn, bIn, aMin, bMinMatch } = terms;
|
|
160
|
+
// A buyer pays the fill's mining fee out of the CKB it gives, so nothing gains once the
|
|
161
|
+
// fee eats that; a seller's fill brings CKB in, and its caller nets the fee (`netOf`).
|
|
162
|
+
if (aIn <= aMin + (isCkb2Udt ? ckbMiningFee : 0n)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
const bMaxOut = nonDecreasing({ aScale, bScale, aIn, bIn, aOut: aMin });
|
|
166
|
+
const bMaxMatch = bMaxOut - bIn;
|
|
167
|
+
return {
|
|
168
|
+
...terms,
|
|
169
|
+
// An order under its own minimum can only be completed.
|
|
170
|
+
bMinMatch: minBigInt(bMinMatch, bMaxMatch),
|
|
171
|
+
bMaxMatch,
|
|
172
|
+
bMaxOut,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The smallest `b` output that keeps the order's value non-decreasing at its ratio. */
|
|
177
|
+
function nonDecreasing({
|
|
178
|
+
aScale,
|
|
179
|
+
bScale,
|
|
180
|
+
aIn,
|
|
181
|
+
bIn,
|
|
182
|
+
aOut,
|
|
183
|
+
}: {
|
|
184
|
+
aScale: ccc.Num;
|
|
185
|
+
bScale: ccc.Num;
|
|
186
|
+
aIn: ccc.FixedPoint;
|
|
187
|
+
bIn: ccc.FixedPoint;
|
|
188
|
+
aOut: ccc.FixedPoint;
|
|
189
|
+
}): ccc.FixedPoint {
|
|
190
|
+
return ceilDiv(aScale * (aIn - aOut) + bScale * bIn, bScale);
|
|
191
|
+
}
|