@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.
Files changed (157) hide show
  1. package/README.md +84 -32
  2. package/dist/constants.d.ts +9 -22
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +62 -76
  5. package/dist/constants.js.map +1 -1
  6. package/dist/conversion/estimate.d.ts +33 -0
  7. package/dist/conversion/estimate.d.ts.map +1 -0
  8. package/dist/conversion/estimate.js +117 -0
  9. package/dist/conversion/estimate.js.map +1 -0
  10. package/dist/conversion/maturity.d.ts +15 -0
  11. package/dist/conversion/maturity.d.ts.map +1 -0
  12. package/dist/conversion/maturity.js +113 -0
  13. package/dist/conversion/maturity.js.map +1 -0
  14. package/dist/conversion/plans.d.ts +20 -0
  15. package/dist/conversion/plans.d.ts.map +1 -0
  16. package/dist/conversion/plans.js +106 -0
  17. package/dist/conversion/plans.js.map +1 -0
  18. package/dist/conversion/projection.d.ts +21 -0
  19. package/dist/conversion/projection.d.ts.map +1 -0
  20. package/dist/conversion/projection.js +94 -0
  21. package/dist/conversion/projection.js.map +1 -0
  22. package/dist/conversion/types.d.ts +264 -0
  23. package/dist/conversion/types.d.ts.map +1 -0
  24. package/dist/conversion/types.js +2 -0
  25. package/dist/conversion/types.js.map +1 -0
  26. package/dist/conversion/withdrawal_ring.d.ts +34 -0
  27. package/dist/conversion/withdrawal_ring.d.ts.map +1 -0
  28. package/dist/conversion/withdrawal_ring.js +87 -0
  29. package/dist/conversion/withdrawal_ring.js.map +1 -0
  30. package/dist/dao.d.ts +100 -0
  31. package/dist/dao.d.ts.map +1 -0
  32. package/dist/dao.js +125 -0
  33. package/dist/dao.js.map +1 -0
  34. package/dist/index.d.ts +49 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +20 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/logic.d.ts +81 -0
  39. package/dist/logic.d.ts.map +1 -0
  40. package/dist/logic.js +173 -0
  41. package/dist/logic.js.map +1 -0
  42. package/dist/order/cells.d.ts +168 -0
  43. package/dist/order/cells.d.ts.map +1 -0
  44. package/dist/order/cells.js +309 -0
  45. package/dist/order/cells.js.map +1 -0
  46. package/dist/order/conversion.d.ts +37 -0
  47. package/dist/order/conversion.d.ts.map +1 -0
  48. package/dist/order/conversion.js +120 -0
  49. package/dist/order/conversion.js.map +1 -0
  50. package/dist/order/fee.d.ts +9 -0
  51. package/dist/order/fee.d.ts.map +1 -0
  52. package/dist/order/fee.js +24 -0
  53. package/dist/order/fee.js.map +1 -0
  54. package/dist/order/fill.d.ts +36 -0
  55. package/dist/order/fill.d.ts.map +1 -0
  56. package/dist/order/fill.js +53 -0
  57. package/dist/order/fill.js.map +1 -0
  58. package/dist/order/info.d.ts +62 -0
  59. package/dist/order/info.d.ts.map +1 -0
  60. package/dist/order/info.js +102 -0
  61. package/dist/order/info.js.map +1 -0
  62. package/dist/order/master.d.ts +49 -0
  63. package/dist/order/master.d.ts.map +1 -0
  64. package/dist/order/master.js +27 -0
  65. package/dist/order/master.js.map +1 -0
  66. package/dist/order/matcher.d.ts +43 -0
  67. package/dist/order/matcher.d.ts.map +1 -0
  68. package/dist/order/matcher.js +124 -0
  69. package/dist/order/matcher.js.map +1 -0
  70. package/dist/order/order.d.ts +63 -0
  71. package/dist/order/order.d.ts.map +1 -0
  72. package/dist/order/order.js +234 -0
  73. package/dist/order/order.js.map +1 -0
  74. package/dist/order/order_data.d.ts +51 -0
  75. package/dist/order/order_data.d.ts.map +1 -0
  76. package/dist/order/order_data.js +67 -0
  77. package/dist/order/order_data.js.map +1 -0
  78. package/dist/order/ratio.d.ts +40 -0
  79. package/dist/order/ratio.d.ts.map +1 -0
  80. package/dist/order/ratio.js +74 -0
  81. package/dist/order/ratio.js.map +1 -0
  82. package/dist/order/relative.d.ts +43 -0
  83. package/dist/order/relative.d.ts.map +1 -0
  84. package/dist/order/relative.js +57 -0
  85. package/dist/order/relative.js.map +1 -0
  86. package/dist/owned_owner.d.ts +108 -0
  87. package/dist/owned_owner.d.ts.map +1 -0
  88. package/dist/owned_owner.js +260 -0
  89. package/dist/owned_owner.js.map +1 -0
  90. package/dist/sdk.d.ts +113 -41
  91. package/dist/sdk.d.ts.map +1 -1
  92. package/dist/sdk.js +410 -209
  93. package/dist/sdk.js.map +1 -1
  94. package/dist/send/sign_and_send_transaction.d.ts +61 -0
  95. package/dist/send/sign_and_send_transaction.d.ts.map +1 -0
  96. package/dist/send/sign_and_send_transaction.js +201 -0
  97. package/dist/send/sign_and_send_transaction.js.map +1 -0
  98. package/dist/send/wait_transaction.d.ts +42 -0
  99. package/dist/send/wait_transaction.d.ts.map +1 -0
  100. package/dist/send/wait_transaction.js +172 -0
  101. package/dist/send/wait_transaction.js.map +1 -0
  102. package/dist/udt.d.ts +108 -0
  103. package/dist/udt.d.ts.map +1 -0
  104. package/dist/udt.js +180 -0
  105. package/dist/udt.js.map +1 -0
  106. package/dist/utils/chain.d.ts +3 -0
  107. package/dist/utils/chain.d.ts.map +1 -0
  108. package/dist/utils/chain.js +2 -0
  109. package/dist/utils/chain.js.map +1 -0
  110. package/dist/utils/codec.d.ts +19 -0
  111. package/dist/utils/codec.d.ts.map +1 -0
  112. package/dist/utils/codec.js +69 -0
  113. package/dist/utils/codec.js.map +1 -0
  114. package/dist/utils/utils.d.ts +99 -0
  115. package/dist/utils/utils.d.ts.map +1 -0
  116. package/dist/utils/utils.js +130 -0
  117. package/dist/utils/utils.js.map +1 -0
  118. package/dist/utils/verified_client.d.ts +19 -0
  119. package/dist/utils/verified_client.d.ts.map +1 -0
  120. package/dist/utils/verified_client.js +92 -0
  121. package/dist/utils/verified_client.js.map +1 -0
  122. package/package.json +19 -26
  123. package/src/constants.ts +65 -181
  124. package/src/conversion/estimate.ts +155 -0
  125. package/src/conversion/maturity.ts +158 -0
  126. package/src/conversion/plans.ts +144 -0
  127. package/src/conversion/projection.ts +136 -0
  128. package/src/conversion/types.ts +302 -0
  129. package/src/conversion/withdrawal_ring.ts +122 -0
  130. package/src/dao.ts +196 -0
  131. package/src/index.ts +89 -3
  132. package/src/logic.ts +283 -0
  133. package/src/order/cells.ts +383 -0
  134. package/src/order/conversion.ts +162 -0
  135. package/src/order/fee.ts +30 -0
  136. package/src/order/fill.ts +83 -0
  137. package/src/order/info.ts +181 -0
  138. package/src/order/master.ts +42 -0
  139. package/src/order/matcher.ts +191 -0
  140. package/src/order/order.ts +288 -0
  141. package/src/order/order_data.ts +131 -0
  142. package/src/order/ratio.ts +127 -0
  143. package/src/order/relative.ts +109 -0
  144. package/src/owned_owner.ts +364 -0
  145. package/src/sdk.ts +542 -440
  146. package/src/send/sign_and_send_transaction.ts +256 -0
  147. package/src/send/wait_transaction.ts +227 -0
  148. package/src/udt.ts +249 -0
  149. package/src/utils/chain.ts +2 -0
  150. package/src/utils/codec.ts +78 -0
  151. package/src/utils/utils.ts +208 -0
  152. package/src/utils/verified_client.ts +136 -0
  153. package/dist/codec.d.ts +0 -3
  154. package/dist/codec.d.ts.map +0 -1
  155. package/dist/codec.js +0 -58
  156. package/dist/codec.js.map +0 -1
  157. package/src/codec.ts +0 -138
@@ -0,0 +1,181 @@
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
+ // An interface, this class expression and a typed const rather than one plain class: a
70
+ // class extending `ccc.Entity.Base<...>()` emits an unexported `*_base` declaration, which
71
+ // API Extractor's ae-forgotten-export rejects (tried 2026-10-30).
72
+ // eslint-disable-next-line @typescript-eslint/no-shadow -- Preserve the runtime constructor name.
73
+ const InfoImplementation = class Info extends InfoBase {
74
+ static {
75
+ ccc.codec(InfoCodec)(this);
76
+ }
77
+
78
+ /** Ratio for CKB-to-UDT matching, or empty when unavailable. */
79
+ public readonly ckbToUdt: Ratio;
80
+ /** Ratio for UDT-to-CKB matching, or empty when unavailable. */
81
+ public readonly udtToCkb: Ratio;
82
+ /** Base-2 exponent for the minimum CKB match amount. */
83
+ public readonly ckbMinMatchLog: number;
84
+
85
+ /** Creates order info from normalized ratio objects. */
86
+ constructor(ckbToUdt: Ratio, udtToCkb: Ratio, ckbMinMatchLog: number) {
87
+ super();
88
+ this.ckbToUdt = ckbToUdt;
89
+ this.udtToCkb = udtToCkb;
90
+ this.ckbMinMatchLog = ckbMinMatchLog;
91
+ }
92
+
93
+ /** Normalizes an `InfoLike` wire object or existing entity into `Info`. */
94
+ public static override from(info: InfoLike): Info {
95
+ if (info instanceof Info) {
96
+ return info;
97
+ }
98
+
99
+ const { ckbToUdt, udtToCkb, ckbMinMatchLog } = info;
100
+ return new Info(Ratio.from(ckbToUdt), Ratio.from(udtToCkb), Number(ckbMinMatchLog));
101
+ }
102
+
103
+ /** Creates directional order info from one ratio, at the default minimum match. */
104
+ public static create(isCkb2Udt: boolean, ratioLike: ExchangeRatio): Info {
105
+ return Info.from({
106
+ ckbToUdt: isCkb2Udt ? ratioLike : Ratio.empty(),
107
+ udtToCkb: isCkb2Udt ? Ratio.empty() : ratioLike,
108
+ ckbMinMatchLog: CKB_MIN_MATCH_LOG_DEFAULT,
109
+ });
110
+ }
111
+
112
+ /** Throws when ratio pairing or minimum-match exponent is invalid. */
113
+ public validate(): void {
114
+ if (!isValidCkbMinMatchLog(this.ckbMinMatchLog)) {
115
+ throw new Error("ckbMinMatchLog invalid");
116
+ }
117
+ this.ckbToUdt.validate();
118
+ this.udtToCkb.validate();
119
+
120
+ if (this.ckbToUdt.isEmpty()) {
121
+ if (this.udtToCkb.isPopulated()) {
122
+ return;
123
+ }
124
+ throw new Error("ckbToUdt is Empty, but udtToCkb is not Populated");
125
+ }
126
+
127
+ if (this.udtToCkb.isEmpty()) {
128
+ if (this.ckbToUdt.isPopulated()) {
129
+ return;
130
+ }
131
+ throw new Error("udtToCkb is Empty, but ckbToUdt is not Populated");
132
+ }
133
+
134
+ if (!this.ckbToUdt.isPopulated() || !this.udtToCkb.isPopulated()) {
135
+ throw new Error("One ratio is invalid, so not Empty and not Populated");
136
+ }
137
+
138
+ if (
139
+ this.ckbToUdt.ckbScale * this.udtToCkb.udtScale <
140
+ this.ckbToUdt.udtScale * this.udtToCkb.ckbScale
141
+ ) {
142
+ throw new Error("udtToCkb and ckbToUdt allow order value to be extracted");
143
+ }
144
+ }
145
+
146
+ /** Returns the minimum CKB match amount. */
147
+ public getCkbMinMatch(): ccc.FixedPoint {
148
+ return 1n << BigInt(this.ckbMinMatchLog);
149
+ }
150
+
151
+ /** Returns true when CKB-to-UDT matching is enabled. */
152
+ public isCkb2Udt(): boolean {
153
+ return this.ckbToUdt.isPopulated();
154
+ }
155
+
156
+ /** Returns true when UDT-to-CKB matching is enabled. */
157
+ public isUdt2Ckb(): boolean {
158
+ return this.udtToCkb.isPopulated();
159
+ }
160
+
161
+ /** Returns true when both directions are enabled. */
162
+ public isDualRatio(): boolean {
163
+ return this.isCkb2Udt() && this.isUdt2Ckb();
164
+ }
165
+ };
166
+
167
+ /** CCC-backed order-info constructor and codec. */
168
+ // eslint-disable-next-line @typescript-eslint/no-redeclare -- The public type and runtime constructor intentionally share a name.
169
+ export const Info: {
170
+ byteLength?: number;
171
+ new (ckbToUdt: Ratio, udtToCkb: Ratio, ckbMinMatchLog: number): Info;
172
+ create: (isCkb2Udt: boolean, ratioLike: ExchangeRatio) => Info;
173
+ decode: (encoded: ccc.BytesLike) => Info;
174
+ encode: (info: InfoLike) => ccc.Bytes;
175
+ from: (info: InfoLike) => Info;
176
+ fromBytes: (encoded: ccc.BytesLike) => Info;
177
+ } = InfoImplementation;
178
+
179
+ function isValidCkbMinMatchLog(value: number): boolean {
180
+ return Number.isInteger(value) && value >= 0 && value <= 64;
181
+ }
@@ -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 { ccc } from "@ckb-ccc/core";
2
+ import { ceilDiv } 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: ccc.numMin(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
+ }
@@ -0,0 +1,288 @@
1
+ import { ccc } from "@ckb-ccc/core";
2
+ import { findCells, type ScriptDeps, type ValueComponents } from "../utils/utils.ts";
3
+ import { MasterCell, OrderCell, OrderGroup } from "./cells.ts";
4
+ import { Info, type InfoLike } from "./info.ts";
5
+ import type { Match } from "./matcher.ts";
6
+ import { OrderData } from "./order_data.ts";
7
+ import { Relative } from "./relative.ts";
8
+
9
+ /**
10
+ * Builds and scans iCKB Stack order cells for one order script deployment.
11
+ */
12
+ export class OrderManager implements ScriptDeps {
13
+ /** Order lock script this manager scans and builds for. */
14
+ public readonly script: ccc.Script;
15
+ /** Cell deps required to execute the order script. */
16
+ public readonly cellDeps: ccc.CellDep[];
17
+ /** UDT type script accepted by this order market. */
18
+ public readonly udtScript: ccc.Script;
19
+
20
+ /**
21
+ * Creates an order manager for one order script, its cell deps, and UDT type.
22
+ */
23
+ constructor(script: ccc.Script, cellDeps: ccc.CellDep[], udtScript: ccc.Script) {
24
+ this.script = script;
25
+ this.cellDeps = cellDeps;
26
+ this.udtScript = udtScript;
27
+ }
28
+
29
+ /** Returns true when the cell is an order cell for this manager's scripts. */
30
+ public isOrder(cell: ccc.Cell): boolean {
31
+ return (
32
+ cell.cellOutput.lock.eq(this.script) &&
33
+ Boolean(cell.cellOutput.type?.eq(this.udtScript))
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Adds a new order cell and its master cell to a partial transaction.
39
+ *
40
+ * @remarks
41
+ * The order output is locked by the order script and typed by the configured
42
+ * UDT. The following master output is typed by the order script and locked by
43
+ * the caller-provided lock.
44
+ */
45
+ public mint(
46
+ txLike: ccc.TransactionLike,
47
+ lock: ccc.Script,
48
+ infoLike: InfoLike,
49
+ { ckbValue, udtValue }: ValueComponents,
50
+ ): ccc.Transaction {
51
+ const tx = ccc.Transaction.from(txLike);
52
+ const data = OrderData.from({
53
+ udtValue,
54
+ master: { type: "relative", value: Relative.create(1n) },
55
+ info: Info.from(infoLike),
56
+ });
57
+ data.validate();
58
+ if (ckbValue < 0n) {
59
+ throw new Error("ckbValue invalid, negative");
60
+ }
61
+
62
+ tx.addCellDeps(this.cellDeps);
63
+ const outputCount = tx.addOutput(
64
+ { lock: this.script, type: this.udtScript },
65
+ data.toBytes(),
66
+ );
67
+ const orderOutput = tx.outputs[outputCount - 1];
68
+ if (orderOutput === undefined) {
69
+ throw new Error("Failed to append order output");
70
+ }
71
+ orderOutput.capacity += ckbValue;
72
+ tx.addOutput({ lock, type: this.script });
73
+ return tx;
74
+ }
75
+
76
+ /**
77
+ * Adds inputs and partial outputs for a chosen match.
78
+ */
79
+ public addMatch(txLike: ccc.TransactionLike, match: Match): ccc.Transaction {
80
+ const tx = ccc.Transaction.from(txLike);
81
+ const partials = match.partials.map((partial) => this.validatedPartial(partial));
82
+ let ckbDelta = 0n;
83
+ let udtDelta = 0n;
84
+ for (const { group, ckbOut, udtOut } of partials) {
85
+ ckbDelta += group.order.ckbValue - ckbOut;
86
+ udtDelta += group.order.udtValue - udtOut;
87
+ }
88
+ if (match.ckbDelta !== ckbDelta || match.udtDelta !== udtDelta) {
89
+ throw new Error("Match deltas do not match partial order accounting");
90
+ }
91
+ if (partials.length === 0) {
92
+ return tx;
93
+ }
94
+
95
+ tx.addCellDeps(this.cellDeps);
96
+ for (const { group, ckbOut, udtOut } of partials) {
97
+ const { order } = group;
98
+ tx.addInput(order.cell);
99
+ tx.addOutput(
100
+ { lock: this.script, type: this.udtScript, capacity: ckbOut },
101
+ OrderData.from({
102
+ udtValue: udtOut,
103
+ master: { type: "absolute", value: order.getMaster() },
104
+ info: order.data.info,
105
+ }).toBytes(),
106
+ );
107
+ }
108
+ return tx;
109
+ }
110
+
111
+ /**
112
+ * Adds order groups and their master cells as melt inputs.
113
+ *
114
+ * @remarks Melts exactly the groups it is given: callers pass fulfilled groups
115
+ * to collect and a live group to cancel it.
116
+ */
117
+ public melt(txLike: ccc.TransactionLike, groups: OrderGroup[]): ccc.Transaction {
118
+ const tx = ccc.Transaction.from(txLike);
119
+ if (groups.length === 0) {
120
+ return tx;
121
+ }
122
+ for (const group of groups) {
123
+ if (!this.isOrder(group.order.cell)) {
124
+ throw new Error(
125
+ `Melt order ${group.order.cell.outPoint.toHex()} does not match this order manager`,
126
+ );
127
+ }
128
+ }
129
+ tx.addCellDeps(this.cellDeps);
130
+ for (const group of groups) {
131
+ tx.addInput(group.order.cell);
132
+ tx.addInput(group.master.cell);
133
+ }
134
+ return tx;
135
+ }
136
+
137
+ /**
138
+ * Finds valid order groups by scanning order cells and master cells. Unresolved or
139
+ * invalid groups are skipped.
140
+ */
141
+ public async findOrders(client: ccc.Client): Promise<OrderGroup[]> {
142
+ const [orders, masters] = await Promise.all([
143
+ this.findSimpleOrders(client),
144
+ this.findAllMasters(client),
145
+ ]);
146
+ const rawGroups = new Map(
147
+ masters.map(({ master, blockNumber }) => [
148
+ master.cell.outPoint.toHex(),
149
+ { master, blockNumber, orders: new Array<OrderCell>() },
150
+ ]),
151
+ );
152
+ for (const order of orders) {
153
+ rawGroups.get(order.getMaster().toHex())?.orders.push(order);
154
+ }
155
+
156
+ // Each group's origin is one transaction read; resolving them together turns a book of
157
+ // fifty orders from seconds of round trips into one.
158
+ const groups = await Promise.all(
159
+ [...rawGroups.values()]
160
+ .filter((rawGroup) => rawGroup.orders.length > 0)
161
+ .map(async ({ master, blockNumber, orders: candidates }) =>
162
+ this.resolveOrderGroup(client, master, blockNumber, candidates),
163
+ ),
164
+ );
165
+ return groups.filter((group) => group !== undefined);
166
+ }
167
+
168
+ private async findSimpleOrders(client: ccc.Client): Promise<OrderCell[]> {
169
+ const cells = await findCells(client, {
170
+ script: this.script,
171
+ scriptType: "lock",
172
+ filter: { script: this.udtScript },
173
+ scriptSearchMode: "exact",
174
+ withData: true,
175
+ });
176
+ return cells.flatMap(({ cell }) => {
177
+ const order = OrderCell.tryFrom(cell);
178
+ return order !== undefined && this.isOrder(cell) ? [order] : [];
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Every master with the block that committed it, which is its mint order's block too:
184
+ * a master is created beside its mint order and never changes until melted, so this
185
+ * dates how long the order has been on the book.
186
+ */
187
+ private async findAllMasters(
188
+ client: ccc.Client,
189
+ ): Promise<Array<{ master: MasterCell; blockNumber: ccc.Num }>> {
190
+ // An exact type search returns master cells only, so nothing re-filters here.
191
+ const cells = await findCells(client, {
192
+ script: this.script,
193
+ scriptType: "type",
194
+ scriptSearchMode: "exact",
195
+ withData: true,
196
+ });
197
+ return cells.map(({ cell, blockNumber }) => ({
198
+ master: new MasterCell(cell),
199
+ blockNumber,
200
+ }));
201
+ }
202
+
203
+ /** Resolves one master and its descendant orders into a validated order group. */
204
+ private async resolveOrderGroup(
205
+ client: ccc.Client,
206
+ master: MasterCell,
207
+ blockNumber: ccc.Num,
208
+ orders: OrderCell[],
209
+ ): Promise<OrderGroup | undefined> {
210
+ const origin = await this.findOrigin(client, master.cell.outPoint);
211
+ const order = origin?.resolve(orders);
212
+ if (origin === undefined || order === undefined) {
213
+ return undefined;
214
+ }
215
+ const group = OrderGroup.tryFrom(master, order, origin, blockNumber);
216
+ // A dual-ratio order, valid on chain but placed by nothing in the stack, is left to
217
+ // whoever placed it: neither matched, estimated, shown nor melted here.
218
+ if (group === undefined || group.order.data.info.isDualRatio()) {
219
+ return undefined;
220
+ }
221
+ return group;
222
+ }
223
+
224
+ /**
225
+ * The mint order created beside the master, read from the master's transaction: the
226
+ * client cache first, then the node, which records the response for the next read.
227
+ */
228
+ private async findOrigin(
229
+ client: ccc.Client,
230
+ master: ccc.OutPoint,
231
+ ): Promise<OrderCell | undefined> {
232
+ const { txHash, index: masterIndex } = master;
233
+ // A cached body is trusted like the scan that found the master: an order is a node's
234
+ // claim until the send checks every cell it spends against its creating transaction.
235
+ const response =
236
+ (await client.cache.getTransactionResponse(txHash)) ??
237
+ (await client.getTransaction(txHash));
238
+ if (response === undefined) {
239
+ return undefined;
240
+ }
241
+
242
+ let origin: OrderCell | undefined;
243
+ for (const [i, output] of [...response.transaction.outputCells].entries()) {
244
+ const index = BigInt(i);
245
+ if (index === masterIndex) {
246
+ continue;
247
+ }
248
+ const cell = ccc.Cell.from({
249
+ cellOutput: output.cellOutput,
250
+ outputData: output.outputData,
251
+ outPoint: { txHash, index },
252
+ });
253
+ const order = OrderCell.tryFrom(cell);
254
+ if (
255
+ order === undefined ||
256
+ !this.isOrder(cell) ||
257
+ !order.data.isMint() ||
258
+ !order.getMaster().eq(master)
259
+ ) {
260
+ continue;
261
+ }
262
+ if (origin !== undefined) {
263
+ // Two mint orders pointing at one master: the group is ambiguous, skip it.
264
+ return undefined;
265
+ }
266
+ origin = order;
267
+ }
268
+ return origin;
269
+ }
270
+
271
+ private validatedPartial({
272
+ group,
273
+ ckbOut,
274
+ udtOut,
275
+ }: Match["partials"][number]): Match["partials"][number] {
276
+ const outPoint = group.order.cell.outPoint.toHex();
277
+ if (!this.isOrder(group.order.cell)) {
278
+ throw new Error(`Match order ${outPoint} does not match this order manager`);
279
+ }
280
+ if (ckbOut < 0n) {
281
+ throw new Error(`Match order ${outPoint} has negative CKB output`);
282
+ }
283
+ if (udtOut < 0n) {
284
+ throw new Error(`Match order ${outPoint} has negative UDT output`);
285
+ }
286
+ return { group, ckbOut, udtOut };
287
+ }
288
+ }