@ickb/sdk 1000.0.82 → 9001.0.0
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 +84 -32
- package/dist/constants.d.ts +9 -22
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +62 -76
- package/dist/constants.js.map +1 -1
- package/dist/conversion/estimate.d.ts +33 -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 +113 -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 +21 -0
- package/dist/conversion/projection.d.ts.map +1 -0
- package/dist/conversion/projection.js +94 -0
- package/dist/conversion/projection.js.map +1 -0
- package/dist/conversion/types.d.ts +264 -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 +34 -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 +100 -0
- package/dist/dao.d.ts.map +1 -0
- package/dist/dao.js +125 -0
- package/dist/dao.js.map +1 -0
- package/dist/index.d.ts +49 -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 +81 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +173 -0
- package/dist/logic.js.map +1 -0
- package/dist/order/cells.d.ts +168 -0
- package/dist/order/cells.d.ts.map +1 -0
- package/dist/order/cells.js +309 -0
- package/dist/order/cells.js.map +1 -0
- package/dist/order/conversion.d.ts +37 -0
- package/dist/order/conversion.d.ts.map +1 -0
- package/dist/order/conversion.js +120 -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 +102 -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 +124 -0
- package/dist/order/matcher.js.map +1 -0
- package/dist/order/order.d.ts +63 -0
- package/dist/order/order.d.ts.map +1 -0
- package/dist/order/order.js +234 -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 +67 -0
- package/dist/order/order_data.js.map +1 -0
- package/dist/order/ratio.d.ts +40 -0
- package/dist/order/ratio.d.ts.map +1 -0
- package/dist/order/ratio.js +74 -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 +57 -0
- package/dist/order/relative.js.map +1 -0
- package/dist/owned_owner.d.ts +108 -0
- package/dist/owned_owner.d.ts.map +1 -0
- package/dist/owned_owner.js +260 -0
- package/dist/owned_owner.js.map +1 -0
- package/dist/sdk.d.ts +113 -41
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +410 -209
- package/dist/sdk.js.map +1 -1
- package/dist/send/sign_and_send_transaction.d.ts +61 -0
- package/dist/send/sign_and_send_transaction.d.ts.map +1 -0
- package/dist/send/sign_and_send_transaction.js +201 -0
- package/dist/send/sign_and_send_transaction.js.map +1 -0
- package/dist/send/wait_transaction.d.ts +42 -0
- package/dist/send/wait_transaction.d.ts.map +1 -0
- package/dist/send/wait_transaction.js +172 -0
- package/dist/send/wait_transaction.js.map +1 -0
- package/dist/udt.d.ts +108 -0
- package/dist/udt.d.ts.map +1 -0
- package/dist/udt.js +180 -0
- package/dist/udt.js.map +1 -0
- package/dist/utils/chain.d.ts +3 -0
- package/dist/utils/chain.d.ts.map +1 -0
- package/dist/utils/chain.js +2 -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 +69 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/utils.d.ts +99 -0
- package/dist/utils/utils.d.ts.map +1 -0
- package/dist/utils/utils.js +130 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/utils/verified_client.d.ts +19 -0
- package/dist/utils/verified_client.d.ts.map +1 -0
- package/dist/utils/verified_client.js +92 -0
- package/dist/utils/verified_client.js.map +1 -0
- package/package.json +19 -26
- package/src/constants.ts +65 -181
- package/src/conversion/estimate.ts +155 -0
- package/src/conversion/maturity.ts +158 -0
- package/src/conversion/plans.ts +144 -0
- package/src/conversion/projection.ts +136 -0
- package/src/conversion/types.ts +302 -0
- package/src/conversion/withdrawal_ring.ts +122 -0
- package/src/dao.ts +196 -0
- package/src/index.ts +89 -3
- package/src/logic.ts +283 -0
- package/src/order/cells.ts +383 -0
- package/src/order/conversion.ts +162 -0
- package/src/order/fee.ts +30 -0
- package/src/order/fill.ts +83 -0
- package/src/order/info.ts +181 -0
- package/src/order/master.ts +42 -0
- package/src/order/matcher.ts +191 -0
- package/src/order/order.ts +288 -0
- package/src/order/order_data.ts +131 -0
- package/src/order/ratio.ts +127 -0
- package/src/order/relative.ts +109 -0
- package/src/owned_owner.ts +364 -0
- package/src/sdk.ts +542 -440
- package/src/send/sign_and_send_transaction.ts +256 -0
- package/src/send/wait_transaction.ts +227 -0
- package/src/udt.ts +249 -0
- package/src/utils/chain.ts +2 -0
- package/src/utils/codec.ts +78 -0
- package/src/utils/utils.ts +208 -0
- package/src/utils/verified_client.ts +136 -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
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import type { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import type { ValueComponents } from "../utils/utils.ts";
|
|
3
|
+
import { OrderData } from "./order_data.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a parsed order cell on the blockchain.
|
|
7
|
+
*
|
|
8
|
+
* Implements `ValueComponents` to expose the cell's raw data and its
|
|
9
|
+
* value breakdown (CKB and UDT).
|
|
10
|
+
*/
|
|
11
|
+
export class OrderCell implements ValueComponents {
|
|
12
|
+
/** Raw live cell that carries the order lock and UDT type. */
|
|
13
|
+
public readonly cell: ccc.Cell;
|
|
14
|
+
/** Decoded order payload from `cell.outputData`. */
|
|
15
|
+
public readonly data: OrderData;
|
|
16
|
+
/** CKB capacity available for matching after occupied capacity is reserved. */
|
|
17
|
+
public readonly ckbUnoccupied: ccc.FixedPoint;
|
|
18
|
+
/** Absolute total order value in the order's comparison units. */
|
|
19
|
+
public readonly absTotal: ccc.Num;
|
|
20
|
+
/** Absolute matched progress in the order's comparison units. */
|
|
21
|
+
public readonly absProgress: ccc.Num;
|
|
22
|
+
|
|
23
|
+
constructor({
|
|
24
|
+
cell,
|
|
25
|
+
data,
|
|
26
|
+
ckbUnoccupied,
|
|
27
|
+
absTotal,
|
|
28
|
+
absProgress,
|
|
29
|
+
}: {
|
|
30
|
+
cell: ccc.Cell;
|
|
31
|
+
data: OrderData;
|
|
32
|
+
ckbUnoccupied: ccc.FixedPoint;
|
|
33
|
+
absTotal: ccc.Num;
|
|
34
|
+
absProgress: ccc.Num;
|
|
35
|
+
}) {
|
|
36
|
+
this.cell = cell;
|
|
37
|
+
this.data = data;
|
|
38
|
+
this.ckbUnoccupied = ckbUnoccupied;
|
|
39
|
+
this.absTotal = absTotal;
|
|
40
|
+
this.absProgress = absProgress;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the order CKB amount.
|
|
45
|
+
*
|
|
46
|
+
* @returns The CKB amount as a `ccc.FixedPoint`.
|
|
47
|
+
*/
|
|
48
|
+
public get ckbValue(): ccc.FixedPoint {
|
|
49
|
+
return this.cell.cellOutput.capacity;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gets the order UDT amount.
|
|
54
|
+
*
|
|
55
|
+
* @returns The UDT amount as a `ccc.FixedPoint`.
|
|
56
|
+
*/
|
|
57
|
+
public get udtValue(): ccc.FixedPoint {
|
|
58
|
+
return this.data.udtValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Attempts to parse an OrderCell from a raw cell.
|
|
63
|
+
*
|
|
64
|
+
* Returns `undefined` if parsing or validation fails.
|
|
65
|
+
*
|
|
66
|
+
* @param cell - The raw chain cell to convert.
|
|
67
|
+
* @returns An `OrderCell` instance or `undefined`.
|
|
68
|
+
*/
|
|
69
|
+
public static tryFrom(cell: ccc.Cell): OrderCell | undefined {
|
|
70
|
+
try {
|
|
71
|
+
return OrderCell.mustFrom(cell);
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parses and validates a raw cell as OrderCell.
|
|
79
|
+
*
|
|
80
|
+
* Throws if the cell is invalid or data validation fails.
|
|
81
|
+
*
|
|
82
|
+
* @param cell - The raw chain cell to convert.
|
|
83
|
+
* @returns A new `OrderCell` instance.
|
|
84
|
+
* @throws When decoding or validation fails.
|
|
85
|
+
*/
|
|
86
|
+
public static mustFrom(cell: ccc.Cell): OrderCell {
|
|
87
|
+
let data: OrderData;
|
|
88
|
+
try {
|
|
89
|
+
data = OrderData.decode(cell.outputData);
|
|
90
|
+
data.validate();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(`Invalid order payload at ${cell.outPoint.toHex()}`, {
|
|
93
|
+
cause: error,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const udtValue = data.udtValue;
|
|
98
|
+
const ckbUnoccupied = cell.capacityFree;
|
|
99
|
+
const { ckbToUdt, udtToCkb } = data.info;
|
|
100
|
+
const isCkb2Udt = data.info.isCkb2Udt();
|
|
101
|
+
const isUdt2Ckb = data.info.isUdt2Ckb();
|
|
102
|
+
const isDualRatio = isCkb2Udt && isUdt2Ckb;
|
|
103
|
+
|
|
104
|
+
// Compute total values in base units
|
|
105
|
+
const ckb2UdtValue = isCkb2Udt
|
|
106
|
+
? ckbUnoccupied * ckbToUdt.ckbScale + udtValue * ckbToUdt.udtScale
|
|
107
|
+
: 0n;
|
|
108
|
+
const udt2CkbValue = isUdt2Ckb
|
|
109
|
+
? ckbUnoccupied * udtToCkb.ckbScale + udtValue * udtToCkb.udtScale
|
|
110
|
+
: 0n;
|
|
111
|
+
|
|
112
|
+
const absTotal = absoluteOrderTotal(ckb2UdtValue, udt2CkbValue, data.info);
|
|
113
|
+
// What the order already holds of its output side; a dual-ratio order is complete.
|
|
114
|
+
let absProgress = ckbUnoccupied * udtToCkb.ckbScale;
|
|
115
|
+
if (isDualRatio) {
|
|
116
|
+
absProgress = absTotal;
|
|
117
|
+
} else if (isCkb2Udt) {
|
|
118
|
+
absProgress = udtValue * ckbToUdt.udtScale;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return new OrderCell({ cell, data, ckbUnoccupied, absTotal, absProgress });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Checks whether the CKB-to-UDT side is enabled and has nonzero inventory.
|
|
126
|
+
*
|
|
127
|
+
* @remarks Executable-match checks for fee, minimum output, and ratio bounds happen during matching.
|
|
128
|
+
*/
|
|
129
|
+
public isCkb2UdtMatchable(): boolean {
|
|
130
|
+
return this.data.info.isCkb2Udt() && this.ckbUnoccupied > 0n;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks whether the UDT-to-CKB side is enabled and has nonzero inventory.
|
|
135
|
+
*
|
|
136
|
+
* @remarks Executable-match checks for fee, minimum output, and ratio bounds happen during matching.
|
|
137
|
+
*/
|
|
138
|
+
public isUdt2CkbMatchable(): boolean {
|
|
139
|
+
return this.data.info.isUdt2Ckb() && this.data.udtValue > 0n;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Returns true when either side is enabled and has nonzero inventory. */
|
|
143
|
+
public isMatchable(): boolean {
|
|
144
|
+
return this.isCkb2UdtMatchable() || this.isUdt2CkbMatchable();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Checks if the order is fulfilled.
|
|
149
|
+
*
|
|
150
|
+
* @returns True if the order is fulfilled (not matchable), otherwise false.
|
|
151
|
+
*/
|
|
152
|
+
public isFulfilled(): boolean {
|
|
153
|
+
return !this.isMatchable();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Retrieves the master out point of the order.
|
|
158
|
+
* @returns The master out point associated with the order.
|
|
159
|
+
*/
|
|
160
|
+
public getMaster(): ccc.OutPoint {
|
|
161
|
+
return this.data.getMaster(this.cell.outPoint);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether `descendant` can be this origin order's live continuation.
|
|
166
|
+
*
|
|
167
|
+
* @remarks
|
|
168
|
+
* This confusion-attack guard requires the same order script, UDT type,
|
|
169
|
+
* master, and info, while forbidding lower total value or lower progress.
|
|
170
|
+
* See {@link https://github.com/ickb/whitepaper/issues/19}.
|
|
171
|
+
*/
|
|
172
|
+
public isValid(descendant: OrderCell): boolean {
|
|
173
|
+
const udt = this.cell.cellOutput.type;
|
|
174
|
+
return (
|
|
175
|
+
this.data.isMint() &&
|
|
176
|
+
this.cell.cellOutput.lock.eq(descendant.cell.cellOutput.lock) &&
|
|
177
|
+
udt !== undefined &&
|
|
178
|
+
descendant.cell.cellOutput.type?.eq(udt) === true &&
|
|
179
|
+
descendant.getMaster().eq(this.getMaster()) &&
|
|
180
|
+
this.data.info.eq(descendant.data.info) &&
|
|
181
|
+
this.absTotal <= descendant.absTotal &&
|
|
182
|
+
this.absProgress <= descendant.absProgress
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolves the best valid descendant order.
|
|
188
|
+
*
|
|
189
|
+
* @remarks
|
|
190
|
+
* The genuine mint origin is the immutable validation baseline. Resolution
|
|
191
|
+
* prefers greater progress and then greater total value; candidate identity is
|
|
192
|
+
* irrelevant. Equal-score non-mint candidates are ambiguous, while the mint
|
|
193
|
+
* origin wins an equal-score tie when it is still live.
|
|
194
|
+
*/
|
|
195
|
+
public resolve(descendants: OrderCell[]): OrderCell | undefined {
|
|
196
|
+
let resolution: OrderResolution | undefined;
|
|
197
|
+
for (const descendant of descendants) {
|
|
198
|
+
if (!this.isValid(descendant)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
resolution = betterOrderResolution(resolution, descendant);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (resolution?.isAmbiguous === true) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return resolution?.order;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface OrderResolution {
|
|
214
|
+
isAmbiguous: boolean;
|
|
215
|
+
order: OrderCell;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function absoluteOrderTotal(
|
|
219
|
+
ckb2UdtValue: bigint,
|
|
220
|
+
udt2CkbValue: bigint,
|
|
221
|
+
info: OrderData["info"],
|
|
222
|
+
): bigint {
|
|
223
|
+
if (ckb2UdtValue === 0n) {
|
|
224
|
+
return udt2CkbValue;
|
|
225
|
+
}
|
|
226
|
+
if (udt2CkbValue === 0n) {
|
|
227
|
+
return ckb2UdtValue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { ckbToUdt, udtToCkb } = info;
|
|
231
|
+
return (
|
|
232
|
+
(ckb2UdtValue * udtToCkb.ckbScale * udtToCkb.udtScale +
|
|
233
|
+
udt2CkbValue * ckbToUdt.ckbScale * ckbToUdt.udtScale) >>
|
|
234
|
+
1n
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function betterOrderResolution(
|
|
239
|
+
current: OrderResolution | undefined,
|
|
240
|
+
candidate: OrderCell,
|
|
241
|
+
): OrderResolution {
|
|
242
|
+
if (current === undefined || compareOrderScore(current.order, candidate) < 0) {
|
|
243
|
+
return { isAmbiguous: false, order: candidate };
|
|
244
|
+
}
|
|
245
|
+
if (compareOrderScore(current.order, candidate) > 0) {
|
|
246
|
+
return current;
|
|
247
|
+
}
|
|
248
|
+
if (current.order.cell.outPoint.eq(candidate.cell.outPoint)) {
|
|
249
|
+
return current;
|
|
250
|
+
}
|
|
251
|
+
if (candidate.data.isMint() && !current.order.data.isMint()) {
|
|
252
|
+
return { isAmbiguous: false, order: candidate };
|
|
253
|
+
}
|
|
254
|
+
if (!candidate.data.isMint() && current.order.data.isMint()) {
|
|
255
|
+
return current;
|
|
256
|
+
}
|
|
257
|
+
return { ...current, isAmbiguous: true };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function compareOrderScore(left: OrderCell, right: OrderCell): number {
|
|
261
|
+
if (left.absProgress !== right.absProgress) {
|
|
262
|
+
return left.absProgress < right.absProgress ? -1 : 1;
|
|
263
|
+
}
|
|
264
|
+
if (left.absTotal !== right.absTotal) {
|
|
265
|
+
return left.absTotal < right.absTotal ? -1 : 1;
|
|
266
|
+
}
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Represents a master cell
|
|
272
|
+
*/
|
|
273
|
+
export class MasterCell {
|
|
274
|
+
/** Raw live master cell that anchors an order group. */
|
|
275
|
+
public readonly cell: ccc.Cell;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Creates an instance of MasterCell.
|
|
279
|
+
* @param cell - The ccc.Cell instance to be wrapped by the MasterCell.
|
|
280
|
+
*/
|
|
281
|
+
constructor(cell: ccc.Cell) {
|
|
282
|
+
this.cell = cell;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Whether this master anchors `order`: its type is the order script and the order names it. */
|
|
286
|
+
public anchors(order: OrderCell): boolean {
|
|
287
|
+
return (
|
|
288
|
+
this.cell.cellOutput.type?.eq(order.cell.cellOutput.lock) === true &&
|
|
289
|
+
order.getMaster().eq(this.cell.outPoint)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Represents a group of orders associated with a master cell.
|
|
296
|
+
*/
|
|
297
|
+
export class OrderGroup implements ValueComponents {
|
|
298
|
+
/** Master cell that authorizes and anchors the current order. */
|
|
299
|
+
public readonly master: MasterCell;
|
|
300
|
+
/** Current resolved order cell. */
|
|
301
|
+
public readonly order: OrderCell;
|
|
302
|
+
/** Origin order used to validate descendant progress and identity. */
|
|
303
|
+
public readonly origin: OrderCell;
|
|
304
|
+
/** The block that committed the origin and its master: how long the order has been on the book. */
|
|
305
|
+
public readonly blockNumber: ccc.Num;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Creates an instance of OrderGroup.
|
|
309
|
+
* @param master - The master cell associated with the order group.
|
|
310
|
+
* @param order - The order within the group.
|
|
311
|
+
* @param origin - The original order associated with the group.
|
|
312
|
+
* @param blockNumber - The block that committed the origin and its master.
|
|
313
|
+
*/
|
|
314
|
+
constructor(
|
|
315
|
+
master: MasterCell,
|
|
316
|
+
order: OrderCell,
|
|
317
|
+
origin: OrderCell,
|
|
318
|
+
blockNumber: ccc.Num,
|
|
319
|
+
) {
|
|
320
|
+
this.master = master;
|
|
321
|
+
this.order = order;
|
|
322
|
+
this.origin = origin;
|
|
323
|
+
this.blockNumber = blockNumber;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Gets the CKB value of the group.
|
|
328
|
+
*
|
|
329
|
+
* @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.
|
|
330
|
+
*/
|
|
331
|
+
public get ckbValue(): ccc.FixedPoint {
|
|
332
|
+
return this.order.cell.cellOutput.capacity + this.master.cell.cellOutput.capacity;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Gets the UDT value of the group.
|
|
337
|
+
*
|
|
338
|
+
* @returns The UDT amount as a `ccc.FixedPoint`, derived from the order cell.
|
|
339
|
+
*/
|
|
340
|
+
public get udtValue(): ccc.FixedPoint {
|
|
341
|
+
return this.order.data.udtValue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Tries to create an OrderGroup from the provided parameters.
|
|
346
|
+
* @param master - The master cell.
|
|
347
|
+
* @param order - The order within the group.
|
|
348
|
+
* @param origin - The original order.
|
|
349
|
+
* @returns An OrderGroup instance or undefined if creation fails.
|
|
350
|
+
*/
|
|
351
|
+
public static tryFrom(
|
|
352
|
+
master: MasterCell,
|
|
353
|
+
order: OrderCell,
|
|
354
|
+
origin: OrderCell,
|
|
355
|
+
blockNumber: ccc.Num,
|
|
356
|
+
): OrderGroup | undefined {
|
|
357
|
+
const og = new OrderGroup(master, order, origin, blockNumber);
|
|
358
|
+
if (og.isValid()) {
|
|
359
|
+
return og;
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Whether the master anchors both orders and the order continues its origin. */
|
|
365
|
+
public isValid(): boolean {
|
|
366
|
+
return (
|
|
367
|
+
this.master.anchors(this.origin) &&
|
|
368
|
+
this.master.anchors(this.order) &&
|
|
369
|
+
this.origin.isValid(this.order)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Checks if the specified lock is the owner of the master cell.
|
|
375
|
+
*
|
|
376
|
+
* @param locks - The locks to check ownership against.
|
|
377
|
+
* @returns True if the lock is the owner, otherwise false.
|
|
378
|
+
*/
|
|
379
|
+
public isOwner(...locks: ccc.Script[]): boolean {
|
|
380
|
+
const lock = this.master.cell.cellOutput.lock;
|
|
381
|
+
return locks.some((l) => lock.eq(l));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { ccc } from "@ckb-ccc/core";
|
|
2
|
+
import { ckbFromUdt } from "../udt.ts";
|
|
3
|
+
import { ceilDiv, type ExchangeRatio, type ValueComponents } from "../utils/utils.ts";
|
|
4
|
+
import { Info } from "./info.ts";
|
|
5
|
+
import { Ratio } from "./ratio.ts";
|
|
6
|
+
|
|
7
|
+
const maxUint64 = (1n << 64n) - 1n;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when an exact quote cannot be represented by the order ratio format.
|
|
11
|
+
*/
|
|
12
|
+
export class OrderConversionRepresentabilityError extends Error {
|
|
13
|
+
/** Creates a representability error with the default public conversion message. */
|
|
14
|
+
constructor(
|
|
15
|
+
message = "Order conversion quote cannot be represented as Uint64 ratio",
|
|
16
|
+
options?: ErrorOptions,
|
|
17
|
+
) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.name = "OrderConversionRepresentabilityError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Quotes a new order: the output-side amount, the CKB fee, and the order info.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* The returned `Info` preserves the quoted amount after fee adjustment so the
|
|
28
|
+
* minted order records the executable limit price.
|
|
29
|
+
*/
|
|
30
|
+
export function quoteConversion(
|
|
31
|
+
isCkb2Udt: boolean,
|
|
32
|
+
midpoint: ExchangeRatio,
|
|
33
|
+
amounts: ValueComponents,
|
|
34
|
+
{ fee, feeBase }: { fee: ccc.Num; feeBase: ccc.Num },
|
|
35
|
+
): { convertedAmount: ccc.FixedPoint; ckbFee: ccc.FixedPoint; info: Info } {
|
|
36
|
+
const base = Ratio.from(midpoint);
|
|
37
|
+
const amount = isCkb2Udt ? amounts.ckbValue : amounts.udtValue;
|
|
38
|
+
const { aScale, bScale } = feeAdjustedScales(base, isCkb2Udt, fee, feeBase);
|
|
39
|
+
const convertedAmount = ceilDiv(amount * aScale, bScale);
|
|
40
|
+
let ckbFee = 0n;
|
|
41
|
+
|
|
42
|
+
if (amount > 0n && fee !== 0n) {
|
|
43
|
+
ckbFee = isCkb2Udt
|
|
44
|
+
? amount - ckbFromUdt(convertedAmount, base)
|
|
45
|
+
: ckbFromUdt(amount, base) - convertedAmount;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Every order this SDK creates carries the default minimum match, the one the bot's
|
|
49
|
+
// remainder rule is sized to.
|
|
50
|
+
const info = Info.create(
|
|
51
|
+
isCkb2Udt,
|
|
52
|
+
quotePreservingRatio(amount, convertedAmount, isCkb2Udt),
|
|
53
|
+
);
|
|
54
|
+
return { convertedAmount, ckbFee, info };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The direction's scales with the fee taken out of the output side, reduced. */
|
|
58
|
+
function feeAdjustedScales(
|
|
59
|
+
ratio: Ratio,
|
|
60
|
+
isCkb2Udt: boolean,
|
|
61
|
+
fee: ccc.Num,
|
|
62
|
+
feeBase: ccc.Num,
|
|
63
|
+
): { aScale: ccc.Num; bScale: ccc.Num } {
|
|
64
|
+
if (fee < 0n) {
|
|
65
|
+
throw new Error("Fee cannot be negative");
|
|
66
|
+
}
|
|
67
|
+
if (feeBase <= 0n) {
|
|
68
|
+
throw new Error("Fee base must be positive");
|
|
69
|
+
}
|
|
70
|
+
if (fee >= feeBase) {
|
|
71
|
+
throw new Error("Fee too big relative to feeBase");
|
|
72
|
+
}
|
|
73
|
+
if (!ratio.isPopulated()) {
|
|
74
|
+
throw new Error("Invalid ExchangeRatio");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let { ckbScale: aScale, udtScale: bScale } = ratio;
|
|
78
|
+
if (!isCkb2Udt) {
|
|
79
|
+
[aScale, bScale] = [bScale, aScale];
|
|
80
|
+
}
|
|
81
|
+
aScale *= feeBase - fee;
|
|
82
|
+
bScale *= feeBase;
|
|
83
|
+
const divisor = ccc.gcd(aScale, bScale);
|
|
84
|
+
return { aScale: aScale / divisor, bScale: bScale / divisor };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The ratio that reproduces the quote exactly: the greatest Uint64 fraction at most
|
|
89
|
+
* `quotedOutput / inputAmount`, which must still round the input up to the quoted output.
|
|
90
|
+
*/
|
|
91
|
+
function quotePreservingRatio(
|
|
92
|
+
inputAmount: ccc.FixedPoint,
|
|
93
|
+
quotedOutput: ccc.FixedPoint,
|
|
94
|
+
isCkb2Udt: boolean,
|
|
95
|
+
): Ratio {
|
|
96
|
+
if (inputAmount < 0n || quotedOutput < 0n) {
|
|
97
|
+
throw new Error("Order conversion amounts cannot be negative");
|
|
98
|
+
}
|
|
99
|
+
if (inputAmount === 0n || quotedOutput === 0n) {
|
|
100
|
+
throw new OrderConversionRepresentabilityError(
|
|
101
|
+
"Order conversion quote must have positive input and output",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { numerator, denominator } = greatestBoundedFractionAtMost(
|
|
106
|
+
quotedOutput,
|
|
107
|
+
inputAmount,
|
|
108
|
+
maxUint64,
|
|
109
|
+
);
|
|
110
|
+
if (numerator <= 0n || (quotedOutput - 1n) * denominator >= inputAmount * numerator) {
|
|
111
|
+
throw new OrderConversionRepresentabilityError();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Ratio.from({
|
|
115
|
+
ckbScale: isCkb2Udt ? numerator : denominator,
|
|
116
|
+
udtScale: isCkb2Udt ? denominator : numerator,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The greatest fraction at most `numerator / denominator` whose terms are at most
|
|
122
|
+
* `maxTerm`, reduced. It walks the continued fraction's convergents while they fit; the
|
|
123
|
+
* first that does not is replaced by the largest semiconvergent the bound allows, which
|
|
124
|
+
* is closer to the target than the last fitting convergent on the same side of it. Every
|
|
125
|
+
* other convergent lands above the target and is skipped.
|
|
126
|
+
*/
|
|
127
|
+
export function greatestBoundedFractionAtMost(
|
|
128
|
+
numerator: bigint,
|
|
129
|
+
denominator: bigint,
|
|
130
|
+
maxTerm: bigint,
|
|
131
|
+
): { numerator: bigint; denominator: bigint } {
|
|
132
|
+
let best = { numerator: 0n, denominator: 1n };
|
|
133
|
+
let [previousP, previousQ, p, q] = [0n, 1n, 1n, 0n];
|
|
134
|
+
let [n, d] = [numerator, denominator];
|
|
135
|
+
while (d !== 0n) {
|
|
136
|
+
const quotient = n / d;
|
|
137
|
+
let term = quotient;
|
|
138
|
+
if (p !== 0n) {
|
|
139
|
+
term = ccc.numMin(term, (maxTerm - previousP) / p);
|
|
140
|
+
}
|
|
141
|
+
if (q !== 0n) {
|
|
142
|
+
term = ccc.numMin(term, (maxTerm - previousQ) / q);
|
|
143
|
+
}
|
|
144
|
+
const candidate = {
|
|
145
|
+
numerator: term * p + previousP,
|
|
146
|
+
denominator: term * q + previousQ,
|
|
147
|
+
};
|
|
148
|
+
if (
|
|
149
|
+
candidate.denominator > 0n &&
|
|
150
|
+
candidate.numerator * denominator <= candidate.denominator * numerator &&
|
|
151
|
+
candidate.numerator * best.denominator > best.numerator * candidate.denominator
|
|
152
|
+
) {
|
|
153
|
+
best = candidate;
|
|
154
|
+
}
|
|
155
|
+
if (term < quotient) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
[previousP, previousQ, p, q] = [p, q, candidate.numerator, candidate.denominator];
|
|
159
|
+
[n, d] = [d, n % d];
|
|
160
|
+
}
|
|
161
|
+
return best;
|
|
162
|
+
}
|
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. The block count assumes
|
|
79
|
+
* eight-second blocks, not wall time.
|
|
80
|
+
*/
|
|
81
|
+
export function isStale(group: OrderGroup, tip: ccc.ClientBlockHeader): boolean {
|
|
82
|
+
return group.blockNumber + STALE_ORDER_BLOCKS <= tip.number;
|
|
83
|
+
}
|