@ickb/sdk 1000.0.81 → 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.
Files changed (157) hide show
  1. package/README.md +81 -32
  2. package/dist/constants.d.ts +13 -22
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +80 -76
  5. package/dist/constants.js.map +1 -1
  6. package/dist/conversion/estimate.d.ts +39 -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 +112 -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 +22 -0
  19. package/dist/conversion/projection.d.ts.map +1 -0
  20. package/dist/conversion/projection.js +91 -0
  21. package/dist/conversion/projection.js.map +1 -0
  22. package/dist/conversion/types.d.ts +252 -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 +36 -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 +86 -0
  31. package/dist/dao.d.ts.map +1 -0
  32. package/dist/dao.js +113 -0
  33. package/dist/dao.js.map +1 -0
  34. package/dist/index.d.ts +51 -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 +85 -0
  39. package/dist/logic.d.ts.map +1 -0
  40. package/dist/logic.js +184 -0
  41. package/dist/logic.js.map +1 -0
  42. package/dist/order/cells.d.ts +207 -0
  43. package/dist/order/cells.d.ts.map +1 -0
  44. package/dist/order/cells.js +427 -0
  45. package/dist/order/cells.js.map +1 -0
  46. package/dist/order/conversion.d.ts +43 -0
  47. package/dist/order/conversion.d.ts.map +1 -0
  48. package/dist/order/conversion.js +123 -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 +99 -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 +123 -0
  69. package/dist/order/matcher.js.map +1 -0
  70. package/dist/order/order.d.ts +59 -0
  71. package/dist/order/order.d.ts.map +1 -0
  72. package/dist/order/order.js +239 -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 +64 -0
  77. package/dist/order/order_data.js.map +1 -0
  78. package/dist/order/ratio.d.ts +42 -0
  79. package/dist/order/ratio.d.ts.map +1 -0
  80. package/dist/order/ratio.js +82 -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 +54 -0
  85. package/dist/order/relative.js.map +1 -0
  86. package/dist/owned_owner.d.ts +113 -0
  87. package/dist/owned_owner.d.ts.map +1 -0
  88. package/dist/owned_owner.js +280 -0
  89. package/dist/owned_owner.js.map +1 -0
  90. package/dist/sdk.d.ts +71 -42
  91. package/dist/sdk.d.ts.map +1 -1
  92. package/dist/sdk.js +388 -209
  93. package/dist/sdk.js.map +1 -1
  94. package/dist/send/sign_and_send_transaction.d.ts +46 -0
  95. package/dist/send/sign_and_send_transaction.d.ts.map +1 -0
  96. package/dist/send/sign_and_send_transaction.js +101 -0
  97. package/dist/send/sign_and_send_transaction.js.map +1 -0
  98. package/dist/send/wait_transaction.d.ts +41 -0
  99. package/dist/send/wait_transaction.d.ts.map +1 -0
  100. package/dist/send/wait_transaction.js +204 -0
  101. package/dist/send/wait_transaction.js.map +1 -0
  102. package/dist/udt.d.ts +123 -0
  103. package/dist/udt.d.ts.map +1 -0
  104. package/dist/udt.js +244 -0
  105. package/dist/udt.js.map +1 -0
  106. package/dist/utils/chain.d.ts +38 -0
  107. package/dist/utils/chain.d.ts.map +1 -0
  108. package/dist/utils/chain.js +20 -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 +66 -0
  113. package/dist/utils/codec.js.map +1 -0
  114. package/dist/utils/transaction_header.d.ts +17 -0
  115. package/dist/utils/transaction_header.d.ts.map +1 -0
  116. package/dist/utils/transaction_header.js +44 -0
  117. package/dist/utils/transaction_header.js.map +1 -0
  118. package/dist/utils/utils.d.ts +126 -0
  119. package/dist/utils/utils.d.ts.map +1 -0
  120. package/dist/utils/utils.js +151 -0
  121. package/dist/utils/utils.js.map +1 -0
  122. package/package.json +19 -26
  123. package/src/constants.ts +84 -181
  124. package/src/conversion/estimate.ts +157 -0
  125. package/src/conversion/maturity.ts +159 -0
  126. package/src/conversion/plans.ts +144 -0
  127. package/src/conversion/projection.ts +128 -0
  128. package/src/conversion/types.ts +287 -0
  129. package/src/conversion/withdrawal_ring.ts +126 -0
  130. package/src/dao.ts +172 -0
  131. package/src/index.ts +89 -3
  132. package/src/logic.ts +295 -0
  133. package/src/order/cells.ts +520 -0
  134. package/src/order/conversion.ts +173 -0
  135. package/src/order/fee.ts +30 -0
  136. package/src/order/fill.ts +83 -0
  137. package/src/order/info.ts +178 -0
  138. package/src/order/master.ts +42 -0
  139. package/src/order/matcher.ts +191 -0
  140. package/src/order/order.ts +302 -0
  141. package/src/order/order_data.ts +128 -0
  142. package/src/order/ratio.ts +144 -0
  143. package/src/order/relative.ts +106 -0
  144. package/src/owned_owner.ts +394 -0
  145. package/src/sdk.ts +555 -440
  146. package/src/send/sign_and_send_transaction.ts +120 -0
  147. package/src/send/wait_transaction.ts +271 -0
  148. package/src/udt.ts +317 -0
  149. package/src/utils/chain.ts +43 -0
  150. package/src/utils/codec.ts +74 -0
  151. package/src/utils/transaction_header.ts +66 -0
  152. package/src/utils/utils.ts +237 -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
package/src/sdk.ts CHANGED
@@ -1,513 +1,628 @@
1
1
  import { ccc } from "@ckb-ccc/core";
2
+ import { getConfig } from "./constants.ts";
3
+ import { minimumOrderAmount } from "./conversion/estimate.ts";
2
4
  import {
3
- collect,
4
- CapacityManager,
5
- SmartTransaction,
6
- binarySearch,
7
- type ValueComponents,
8
- hexFrom,
9
- getHeader,
10
- Epoch,
11
- } from "@ickb/utils";
5
+ ckbToIckbConversionPlans,
6
+ ickbToCkbConversionPlans,
7
+ } from "./conversion/plans.ts";
8
+ import type {
9
+ AccountState,
10
+ CompleteIckbTransactionOptions,
11
+ ConversionTransactionContext,
12
+ ConversionTransactionFailureReason,
13
+ ConversionTransactionOptions,
14
+ ConversionTransactionResult,
15
+ SdkManagers,
16
+ SystemState,
17
+ } from "./conversion/types.ts";
12
18
  import {
13
- convert,
14
- ICKB_DEPOSIT_CAP,
15
- ickbExchangeRatio,
16
- type LogicManager,
17
- type OwnedOwnerManager,
18
- } from "@ickb/core";
19
- import {
20
- Info,
21
- OrderManager,
22
- Ratio,
23
- type OrderCell,
24
- type OrderGroup,
25
- } from "@ickb/order";
26
- import { getConfig } from "./constants.js";
27
- import { PoolSnapshot } from "./codec.js";
19
+ assertDaoOutputLimit,
20
+ broadcastDeadline,
21
+ DaoHeaderIndexError,
22
+ DaoOutputLimitError,
23
+ type LockUpPolicy,
24
+ } from "./dao.ts";
25
+ import type { IckbDepositCell, LogicManager } from "./logic.ts";
26
+ import type { OrderGroup } from "./order/cells.ts";
27
+ import type { OrderManager } from "./order/order.ts";
28
+ import { Ratio } from "./order/ratio.ts";
29
+ import type { OwnedOwnerManager } from "./owned_owner.ts";
30
+ import { ickbExchangeRatio, type IckbUdt } from "./udt.ts";
31
+ import type { SupportedChain } from "./utils/chain.ts";
32
+ import { compareBigInt, findCells, isPlainCapacityCell, unique } from "./utils/utils.ts";
28
33
 
29
34
  /**
30
- * SDK for managing iCKB operations.
35
+ * The whole SDK, which the Node actors drive by relative import; the package barrel
36
+ * exposes the two conversion-workflow methods, and its assignment checks they agree.
31
37
  */
32
- export class IckbSdk {
33
- /**
34
- * Creates an instance of IckbSdk.
35
- *
36
- * @param ownedOwner - The manager for owned owner operations.
37
- * @param ickbLogic - The manager for iCKB logic operations.
38
- * @param order - The manager for order operations.
39
- * @param capacity - The capacity manager instance.
40
- * @param bots - An array of bot lock scripts.
41
- */
42
- constructor(
43
- private readonly ownedOwner: OwnedOwnerManager,
44
- private readonly ickbLogic: LogicManager,
45
- private readonly order: OrderManager,
46
- private readonly capacity: CapacityManager,
47
- private readonly bots: ccc.Script[],
48
- ) {}
38
+ export interface IckbSdk {
39
+ /** Builds and completes a conversion, or returns a typed planning failure. */
40
+ buildConversionTransaction(
41
+ txLike: ccc.TransactionLike,
42
+ options: ConversionTransactionOptions,
43
+ ): Promise<ConversionTransactionResult>;
44
+ /** Reads system, user-order, and account state against one sampled tip, under one timing policy. */
45
+ getL1AccountState(
46
+ client: ccc.Client,
47
+ locks: ccc.Script[],
48
+ lockUp: LockUpPolicy,
49
+ ): Promise<{
50
+ system: SystemState;
51
+ user: { orders: OrderGroup[] };
52
+ account: AccountState;
53
+ }>;
54
+ /** Adds requested withdrawal, collection, receipt, and ready-withdrawal steps. */
55
+ buildBaseTransaction(
56
+ txLike: ccc.TransactionLike,
57
+ steps: CollectSteps,
58
+ withdrawalRequest?: { deposits: IckbDepositCell[]; lock: ccc.Script },
59
+ ): ccc.Transaction;
60
+ /** Completes iCKB inputs and fees without signing or sending the transaction. */
61
+ completeTransaction(
62
+ txLike: ccc.TransactionLike,
63
+ options: CompleteIckbTransactionOptions,
64
+ ): Promise<ccc.Transaction>;
65
+ }
49
66
 
50
- /**
51
- * Creates an instance of IckbSdk from script dependencies.
52
- *
53
- * @param args - Parameters matching those of getConfig.
54
- * @returns A new instance of IckbSdk.
55
- */
56
- static from(...args: Parameters<typeof getConfig>): IckbSdk {
57
- const {
58
- managers: { ownedOwner, logic, order, capacity },
59
- bots,
60
- } = getConfig(...args);
67
+ /**
68
+ * Input count up to which one own transaction sweeps liquid cells.
69
+ *
70
+ * @remarks A compaction target, not a validity bound: required funding and the fee may go
71
+ * past it. Counted rather than measured in bytes because a same-lock input costs a fixed
72
+ * 44 bytes (52 with an extra witness slot), so 1,000 inputs stay near 50 KB, far under the
73
+ * node's 512,000-byte limit, and near the one measured point (1,000 inputs: 44 KB, about
74
+ * 180 ms of local completion). A miner paying the bot adds about forty cellbase cells per
75
+ * turn, which this outpaces; a larger backlog takes several transactions.
76
+ */
77
+ export const SWEEP_INPUT_LIMIT = 1000;
78
+
79
+ /** The options with the lock resolved to the signer's recommended one when absent. */
80
+ type ResolvedConversionOptions = ConversionTransactionOptions & { lock: ccc.Script };
81
+
82
+ /** What every own transaction collects along: the context's collectable positions. */
83
+ export type CollectSteps = Pick<
84
+ ConversionTransactionContext,
85
+ "availableOrders" | "receipts" | "readyWithdrawals"
86
+ >;
87
+
88
+ // eslint-disable-next-line @typescript-eslint/no-shadow -- Preserve the runtime constructor name.
89
+ const IckbSdkImplementation = class IckbSdk {
90
+ private readonly ickbUdt: IckbUdt;
91
+ private readonly ownedOwner: OwnedOwnerManager;
92
+ private readonly ickbLogic: LogicManager;
93
+ private readonly order: OrderManager;
94
+
95
+ constructor({ ickbUdt, ownedOwner, ickbLogic, order }: SdkManagers) {
96
+ this.ickbUdt = ickbUdt;
97
+ this.ownedOwner = ownedOwner;
98
+ this.ickbLogic = ickbLogic;
99
+ this.order = order;
100
+ }
61
101
 
62
- return new IckbSdk(ownedOwner, logic, order, capacity, bots);
102
+ /** Creates the SDK for one chain's deployment. */
103
+ public static fromChain(chain: SupportedChain): IckbSdk {
104
+ return new IckbSdk(getConfig(chain));
63
105
  }
64
106
 
107
+ // ---- State reads -------------------------------------------------------------------
108
+
65
109
  /**
66
- * Previews the conversion between CKB and UDT.
110
+ * Reads system, user-order, and account state against one sampled tip, under the
111
+ * caller's withdrawal timing rules (`BOT_LOCK_UP` or `WALLET_LOCK_UP`).
67
112
  *
68
- * This method calculates a conversion preview using an exchange ratio midpoint.
69
- * Optionally, a fee may be applied that influences the effective conversion rate,
70
- * scaling the converted amount by (feeBase - fee) / feeBase.
71
- *
72
- * @param isCkb2Udt - Indicates the conversion direction:
73
- * - true: Convert CKB to UDT.
74
- * - false: Convert UDT to CKB.
75
- * @param amounts - An object containing value components (amounts for CKB and UDT).
76
- * @param system - The current system state containing exchange ratio, fee rate, tip, and order-related information.
77
- * @param options - Optional parameters for fee and matching:
78
- * - fee: The fee to apply in integer terms (defaults to 1n for 0.001% fee).
79
- * - feeBase: The base used for fee scaling (defaults to 100000n).
80
- *
81
- * @returns An object with:
82
- * - convertedAmount: The estimated converted amount as a FixedPoint.
83
- * - ckbFee: The fee (or gain) in CKB, as a FixedPoint.
84
- * - info: Additional conversion metadata.
85
- * - maturity: Optional maturity information (as ccc.Num) if meets criteria.
113
+ * @remarks Every read is complete and uncapped: a large book or pool costs a slower
114
+ * read, never a partial or failed one. Each account lock is enumerated by one uncached
115
+ * unfiltered exact-lock scan and classified client-side.
86
116
  */
87
- static estimate(
88
- isCkb2Udt: boolean,
89
- amounts: ValueComponents,
90
- system: SystemState,
91
- options?: {
92
- fee?: ccc.Num;
93
- feeBase?: ccc.Num;
94
- },
95
- ): {
96
- convertedAmount: ccc.FixedPoint;
97
- ckbFee: ccc.FixedPoint;
98
- info: Info;
99
- maturity: ccc.Num | undefined;
100
- } {
101
- // Apply a 0.001% default fee if none provided.
102
- options = {
103
- fee: 1n,
104
- feeBase: 100000n,
105
- ...options,
117
+ public async getL1AccountState(
118
+ client: ccc.Client,
119
+ locks: ccc.Script[],
120
+ lockUp: LockUpPolicy,
121
+ ): Promise<{
122
+ system: SystemState;
123
+ user: { orders: OrderGroup[] };
124
+ account: AccountState;
125
+ }> {
126
+ const tip = await client.getTipHeader();
127
+ const exchangeRatio = Ratio.from(ickbExchangeRatio(tip));
128
+ const [poolDeposits, orders, feeRate, accounts] = await Promise.all([
129
+ this.ickbLogic.findDeposits(client, tip),
130
+ this.order.findOrders(client),
131
+ getFeeRate(client),
132
+ Promise.all(
133
+ [...unique(locks)].map(async (lock) => this.readLockCells(client, lock, tip)),
134
+ ),
135
+ ]);
136
+ return {
137
+ system: {
138
+ feeRate,
139
+ tip,
140
+ exchangeRatio,
141
+ // One book, two filters: the market side is every order
142
+ // past par, the wallet's own included, since the bot fills by price, not by owner.
143
+ orderPool: orders.filter((group) => isPastPar(group, exchangeRatio)),
144
+ poolDeposits,
145
+ lockUp,
146
+ },
147
+ user: { orders: orders.filter((group) => group.isOwner(...locks)) },
148
+ account: {
149
+ capacityCells: accounts.flatMap((account) => account.capacityCells),
150
+ nativeUdtCells: accounts.flatMap((account) => account.nativeUdtCells),
151
+ receipts: accounts.flatMap((account) => account.receipts),
152
+ withdrawalGroups: accounts.flatMap((account) => account.withdrawalGroups),
153
+ },
106
154
  };
107
- const { convertedAmount, ckbFee, info } = OrderManager.convert(
108
- isCkb2Udt,
109
- system.exchangeRatio,
110
- amounts,
111
- options,
112
- );
113
-
114
- // If the fee meets a threshold, calculate the order maturity; otherwise, maturity is undefined.
115
- const maturity =
116
- ckbFee >= 10n * system.feeRate
117
- ? IckbSdk.maturity({ info, amounts }, system)
118
- : undefined;
155
+ }
119
156
 
120
- return { convertedAmount, ckbFee, info, maturity };
157
+ /** Every Stack cell one lock owns, classified from a single exact-lock scan. */
158
+ private async readLockCells(
159
+ client: ccc.Client,
160
+ lock: ccc.Script,
161
+ tip: ccc.ClientBlockHeader,
162
+ ): Promise<AccountState> {
163
+ const cells = await findCells(client, {
164
+ script: lock,
165
+ scriptType: "lock",
166
+ scriptSearchMode: "exact",
167
+ withData: true,
168
+ });
169
+ const [receipts, withdrawalGroups] = await Promise.all([
170
+ this.ickbLogic.receiptsFrom(client, cells),
171
+ this.ownedOwner.withdrawalGroupsFrom(client, cells, tip),
172
+ ]);
173
+ return {
174
+ capacityCells: cells.filter(isPlainCapacityCell),
175
+ nativeUdtCells: cells.filter((cell) => this.ickbUdt.isUdt(cell)),
176
+ receipts,
177
+ withdrawalGroups,
178
+ };
121
179
  }
122
180
 
181
+ // ---- Conversion planning -----------------------------------------------------------
182
+
123
183
  /**
124
- * Estimates the maturity for an order formatted as a Unix timestamp.
125
- *
126
- * Depending on the order type and amount remaining, the method calculates an estimated timestamp
127
- * when the order (or part thereof) might be fulfilled.
184
+ * Builds and completes one conversion transaction from a conversion context.
128
185
  *
129
- * @param o - Either an OrderCell or an object containing order Info and value components.
130
- * @param system - The current system state.
131
- * @returns The Unix timestamp of estimated maturity as a bigint (in milliseconds) or undefined if not applicable.
186
+ * @remarks
187
+ * Every branch returns a transaction funded from the signer's committed cells: the
188
+ * candidate plans (deposit counts, or prefixes of the greedy withdrawal selection with
189
+ * their rebuilt remainder order) are completed most direct first and the first fundable one
190
+ * wins, so a wallet short of CKB degrades to fewer direct actions plus a larger standing
191
+ * order rather than failing. Failure results are
192
+ * expected planning outcomes; when no plan can be funded the last completion error throws.
132
193
  */
133
- static maturity(
134
- o:
135
- | OrderCell
136
- | {
137
- info: Info;
138
- amounts: ValueComponents;
139
- },
140
- system: SystemState,
141
- ): bigint | undefined {
142
- const info = "info" in o ? o.info : o.data.info;
143
- const amounts =
144
- "amounts" in o
145
- ? o.amounts
146
- : { ckbValue: o.ckbUnoccupied, udtValue: o.udtValue };
147
-
148
- // Dual-ratio orders have no fixed maturity.
149
- if (info.isDualRatio()) {
150
- return;
194
+ public async buildConversionTransaction(
195
+ txLike: ccc.TransactionLike,
196
+ options: ConversionTransactionOptions,
197
+ ): Promise<ConversionTransactionResult> {
198
+ const { amount, context, direction } = options;
199
+ if (amount < 0n) {
200
+ return conversionFailure("amount-negative", context.estimatedMaturity);
201
+ }
202
+ if (direction === "ckb-to-ickb" && amount > context.ckbAvailable) {
203
+ return conversionFailure("insufficient-ckb", context.estimatedMaturity);
204
+ }
205
+ if (direction === "ickb-to-ckb" && amount > context.ickbAvailable) {
206
+ return conversionFailure("insufficient-ickb", context.estimatedMaturity);
151
207
  }
152
208
 
153
- const isCkb2Udt = info.isCkb2Udt();
154
- const amount = isCkb2Udt ? amounts.ckbValue : amounts.udtValue;
155
- const ratio = isCkb2Udt ? info.ckbToUdt : info.udtToCkb;
156
-
157
- // If order is already fulfilled.
209
+ const baseTx = ccc.Transaction.from(txLike);
210
+ const resolved = {
211
+ ...options,
212
+ lock: options.lock ?? (await options.signer.getRecommendedAddressObj()).script,
213
+ };
158
214
  if (amount === 0n) {
159
- return 0n;
215
+ return this.buildCollectOnlyConversion(baseTx, resolved);
160
216
  }
217
+ return direction === "ckb-to-ickb"
218
+ ? this.buildCkbToIckbConversion(baseTx, resolved)
219
+ : this.buildIckbToCkbConversion(baseTx, resolved);
220
+ }
161
221
 
162
- const { tip, exchangeRatio, orderPool, ckbAvailable, ckbMaturing } = system;
163
-
164
- // Create a reference ratio instance for comparison.
165
- const b = new Info(ratio, ratio, 1);
166
- let ckb = isCkb2Udt ? amount : 0n;
167
- let udt = isCkb2Udt ? 0n : amount;
168
- for (const o of orderPool) {
169
- const a = o.data.info;
170
- if (a.isCkb2Udt()) {
171
- // If not isCkb2Udt or a worse ratio, add available CKB.
172
- if (!isCkb2Udt || a.ckb2UdtCompare(b) < 0) {
173
- ckb += o.ckbUnoccupied;
174
- }
175
- } else {
176
- // Conversely for UDT to CKB orders.
177
- if (isCkb2Udt || a.udt2CkbCompare(b) < 0) {
178
- udt += o.udtValue;
179
- }
180
- }
181
- }
182
- // Adjust ckb by the converted UDT amount.
183
- ckb -= convert(false, udt, exchangeRatio);
184
-
185
- // Minimum maturity of 10 minutes (in milliseconds).
186
- let maturity = 10n * 60n * 1000n;
187
- if (isCkb2Udt) {
188
- // For CKB to UDT orders, extend maturity based on the CKB amount.
189
- if (ckb > 0n) {
190
- maturity *= 1n + ckb / ccc.fixedPointFrom("200000");
191
- }
192
- return maturity + ("info" in o ? BigInt(Date.now()) : tip.timestamp);
222
+ private async buildCollectOnlyConversion(
223
+ baseTx: ccc.Transaction,
224
+ options: ResolvedConversionOptions,
225
+ ): Promise<ConversionTransactionResult> {
226
+ const { context, lock, signer } = options;
227
+ const tx = this.buildBaseTransaction(baseTx, context);
228
+ // An empty collection to a lock of the signer's own would only compact the account;
229
+ // to another lock it moves the liquid cells, which completion builds from the sweep.
230
+ if (
231
+ !hasTransactionActivity(tx) &&
232
+ (context.cells.length === 0 || (await isOwnLock(signer, lock)))
233
+ ) {
234
+ return conversionFailure("nothing-to-do", context.estimatedMaturity);
193
235
  }
236
+ const completed = await this.completeConversion(tx, options);
237
+ return {
238
+ ok: true,
239
+ tx: completed,
240
+ estimatedMaturity: context.estimatedMaturity,
241
+ conversion: { kind: "collect-only" },
242
+ isSweepComplete: sweepsAll(completed, context.cells),
243
+ };
244
+ }
194
245
 
195
- // For UDT to CKB orders, add available CKB.
196
- ckb += ckbAvailable;
197
- if (ckb >= 0) {
198
- return maturity + ("info" in o ? BigInt(Date.now()) : tip.timestamp);
246
+ private async buildCkbToIckbConversion(
247
+ baseTx: ccc.Transaction,
248
+ options: ResolvedConversionOptions,
249
+ ): Promise<ConversionTransactionResult> {
250
+ const { context, lock } = options;
251
+ // A plan is dropped only for an unrepresentable remainder order.
252
+ const plans = ckbToIckbConversionPlans(options);
253
+ if (plans.length === 0) {
254
+ return conversionFailure(
255
+ "amount-too-small",
256
+ context.estimatedMaturity,
257
+ minimumOrderAmount(true, context.system),
258
+ );
199
259
  }
200
-
201
- // Find the earliest maturity in the ckbMaturing array that satisfies the required CKB.
202
- const ckbNeeded = -ckb;
203
- const i = binarySearch(
204
- ckbMaturing.length,
205
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
206
- (n) => ckbMaturing[n]!.ckbCumulative >= ckbNeeded,
260
+ const { candidate: plan, tx } = await completeFirstFundable(
261
+ plans,
262
+ (candidate) => {
263
+ let partial = this.buildBaseTransaction(baseTx.clone(), context);
264
+ if (candidate.depositCount > 0) {
265
+ partial = this.ickbLogic.deposit(
266
+ partial,
267
+ candidate.depositCount,
268
+ candidate.depositCapacity,
269
+ lock,
270
+ );
271
+ }
272
+ if (candidate.order !== undefined) {
273
+ partial = this.order.mint(
274
+ partial,
275
+ lock,
276
+ candidate.order.estimate.info,
277
+ candidate.order.amounts,
278
+ );
279
+ }
280
+ return partial;
281
+ },
282
+ async (partial) => this.completeConversion(partial, options),
207
283
  );
208
-
209
- return ckbMaturing[i]?.maturity;
284
+ return {
285
+ ok: true,
286
+ tx,
287
+ estimatedMaturity: plan.estimatedMaturity,
288
+ conversion: {
289
+ kind: conversionKind(plan.depositCount > 0, plan.order !== undefined),
290
+ },
291
+ isSweepComplete: sweepsAll(tx, context.cells),
292
+ };
210
293
  }
211
294
 
212
- /**
213
- * Mints a new order cell and appends it to the transaction.
214
- *
215
- * The method performs the following operations:
216
- * - Creates order cell data using provided amounts and order information.
217
- * - Adds required cell dependencies and UDT handlers to the transaction.
218
- * - Appends the order cell to the transaction outputs.
219
- *
220
- * @param tx - The smart transaction to which the order cell is added.
221
- * @param user - The user, represented either as a Signer or a Script.
222
- * @param info - The order information meta data (usually computed via OrderManager.convert).
223
- * @param amounts - The value components for the order, including:
224
- * - ckbValue: The CKB amount (may include an internal surplus).
225
- * - udtValue: The UDT amount.
226
- *
227
- * @returns A Promise resolving to void.
228
- */
229
- async request(
230
- tx: SmartTransaction,
231
- user: ccc.Signer | ccc.Script,
232
- info: Info,
233
- amounts: ValueComponents,
234
- ): Promise<void> {
235
- // If the user is provided as a Signer, extract the recommended lock script.
236
- user =
237
- "codeHash" in user
238
- ? user
239
- : (await user.getRecommendedAddressObj()).script;
295
+ private async buildIckbToCkbConversion(
296
+ baseTx: ccc.Transaction,
297
+ options: ResolvedConversionOptions,
298
+ ): Promise<ConversionTransactionResult> {
299
+ const { context, lock } = options;
300
+ const plans = ickbToCkbConversionPlans(options, context.system.poolDeposits);
301
+ if (plans.length === 0) {
302
+ return conversionFailure(
303
+ "amount-too-small",
304
+ context.estimatedMaturity,
305
+ minimumOrderAmount(false, context.system),
306
+ );
307
+ }
308
+ const { candidate: plan, tx } = await completeFirstFundable(
309
+ plans,
310
+ (candidate) => {
311
+ let partial = this.buildBaseTransaction(baseTx.clone(), context, {
312
+ deposits: candidate.selectedDeposits,
313
+ lock,
314
+ });
315
+ if (candidate.order !== undefined) {
316
+ partial = this.order.mint(
317
+ partial,
318
+ lock,
319
+ candidate.order.estimate.info,
320
+ candidate.order.amounts,
321
+ );
322
+ }
323
+ return partial;
324
+ },
325
+ async (partial) => this.completeConversion(partial, options),
326
+ );
327
+ const notice = plan.order?.estimate.notice;
328
+ const broadcastBefore = broadcastDeadline(
329
+ plan.selectedDeposits.map((deposit) => deposit.claimEpoch),
330
+ context.system.lockUp,
331
+ );
332
+ return {
333
+ ok: true,
334
+ tx,
335
+ estimatedMaturity: plan.estimatedMaturity,
336
+ conversion: {
337
+ kind: conversionKind(plan.selectedDeposits.length > 0, plan.order !== undefined),
338
+ },
339
+ ...(notice === undefined ? {} : { conversionNotice: notice }),
340
+ isSweepComplete: sweepsAll(tx, context.cells),
341
+ ...(broadcastBefore === undefined ? {} : { broadcastBefore }),
342
+ };
343
+ }
240
344
 
241
- this.order.mint(tx, user, info, amounts);
345
+ private async completeConversion(
346
+ tx: ccc.Transaction,
347
+ options: ResolvedConversionOptions,
348
+ ): Promise<ccc.Transaction> {
349
+ return this.completeTransaction(tx, {
350
+ signer: options.signer,
351
+ lock: options.lock,
352
+ feeRate: options.context.system.feeRate,
353
+ cells: options.context.cells,
354
+ });
242
355
  }
243
356
 
357
+ // ---- Transaction building ----------------------------------------------------------
358
+
244
359
  /**
245
- * Melts (cancels) the specified order groups.
246
- *
247
- * For each order group, if the option is set to process fulfilled orders only,
248
- * it filters accordingly. Then, for every valid group, the master and order cells are added
249
- * as inputs to the transaction.
360
+ * Adds the context's collect steps to a partial transaction: the withdrawal requests when
361
+ * given, then the collectable orders, the receipts, and the ready withdrawals.
250
362
  *
251
- * @param tx - The smart transaction to which the inputs are added.
252
- * @param groups - An array of order groups to be melted.
253
- * @param options - Optional parameters:
254
- * - isFulfilledOnly: If true, only order groups with fully or partially fulfilled orders are processed.
255
- *
256
- * @returns void
363
+ * @remarks
364
+ * The result is still partial. Callers should use `completeTransaction` before
365
+ * signing and sending. The withdrawals move their deposit headers to the front of
366
+ * `headerDeps`, so the given transaction must not reference a header by position.
257
367
  */
258
- collect(
259
- tx: SmartTransaction,
260
- groups: OrderGroup[],
261
- options?: {
262
- isFulfilledOnly?: boolean;
263
- },
264
- ): void {
265
- this.order.melt(tx, groups, options);
368
+ public buildBaseTransaction(
369
+ txLike: ccc.TransactionLike,
370
+ steps: CollectSteps,
371
+ withdrawalRequest?: { deposits: IckbDepositCell[]; lock: ccc.Script },
372
+ ): ccc.Transaction {
373
+ let tx = ccc.Transaction.from(txLike);
374
+ if (withdrawalRequest !== undefined && withdrawalRequest.deposits.length > 0) {
375
+ tx = this.ownedOwner.requestWithdrawal(
376
+ tx,
377
+ withdrawalRequest.deposits,
378
+ withdrawalRequest.lock,
379
+ );
380
+ }
381
+ if (steps.availableOrders.length > 0) {
382
+ tx = this.order.melt(tx, steps.availableOrders);
383
+ }
384
+ if (steps.receipts.length > 0) {
385
+ tx = this.ickbLogic.completeDeposit(tx, steps.receipts);
386
+ }
387
+ if (steps.readyWithdrawals.length > 0) {
388
+ tx = this.ownedOwner.withdraw(tx, steps.readyWithdrawals);
389
+ }
390
+ return tx;
266
391
  }
267
392
 
268
393
  /**
269
- * Retrieves the L1 state from the blockchain.
394
+ * Completes iCKB inputs, iCKB change, plain change, and the fee from the cells it is given.
270
395
  *
271
- * The method performs the following:
272
- * - Obtains the current block tip and calculates the exchange ratio.
273
- * - Fetches available CKB and the maturing CKB based on bot capacities and deposit snapshots.
274
- * - Filters orders into user-owned and system orders based on the provided locks.
275
- * - Estimates user-owned orders maturity.
276
- *
277
- * @param client - The blockchain client interface.
278
- * @param locks - An array of lock scripts used to filter user cells.
279
- * @returns A promise that resolves to an object containing:
280
- * - system: The system state (fee rate, tip header, exchange ratio, order pool, etc.).
281
- * - user: The user's orders grouped as an array of OrderGroup.
396
+ * @remarks Completion never scans. `cells` are the signer's known liquid cells, plain
397
+ * CKB and iCKB, from the account state already read: the largest ones fund what the
398
+ * outputs need, the rest ride along as a sweep while the transaction has fewer than
399
+ * {@link SWEEP_INPUT_LIMIT} inputs, so every own transaction compacts the account.
400
+ * Ordinary change is always a plain cell; existing outputs
401
+ * are never reinterpreted or resized as fee change. This does not sign or send.
282
402
  */
283
- async getL1State(
284
- client: ccc.Client,
285
- locks: ccc.Script[],
286
- ): Promise<{ system: SystemState; user: { orders: OrderGroup[] } }> {
287
- const tip = await client.getTipHeader();
288
- const exchangeRatio = Ratio.from(ickbExchangeRatio(tip));
289
-
290
- // Parallel fetching of system components.
291
- const [{ ckbAvailable, ckbMaturing }, orders, feeRate] = await Promise.all([
292
- this.getCkb(client, tip),
293
- collect(this.order.findOrders(client)),
294
- client.getFeeRate(),
295
- ]);
296
-
297
- const midInfo = new Info(exchangeRatio, exchangeRatio, 1);
298
- const userOrders: OrderGroup[] = [];
299
- const systemOrders: OrderCell[] = [];
300
- for (const group of orders) {
301
- if (group.isOwner(...locks)) {
302
- userOrders.push(group);
403
+ public async completeTransaction(
404
+ txLike: ccc.TransactionLike,
405
+ options: CompleteIckbTransactionOptions,
406
+ ): Promise<ccc.Transaction> {
407
+ const { signer, feeRate } = options;
408
+ const tx = this.ickbUdt.addCellDeps(ccc.Transaction.from(txLike).clone());
409
+ const changeLock = options.lock ?? (await signer.getRecommendedAddressObj()).script;
410
+ // Inputs come from one state read, each cell once, and each action spends its own
411
+ // cell kind, so a repeated out point is a builder bug: this is the one place that
412
+ // names it, the node would refuse the transaction anyway.
413
+ const spent = new Set<string>();
414
+ for (const { previousOutput } of tx.inputs) {
415
+ const key = previousOutput.toHex();
416
+ if (spent.has(key)) {
417
+ throw new Error(`Input ${key} is spent twice`);
303
418
  }
419
+ spent.add(key);
420
+ }
421
+ const unspent = options.cells.filter((cell) => !spent.has(cell.outPoint.toHex()));
422
+ const ickbCells = unspent
423
+ .filter((cell) => this.ickbUdt.isUdt(cell))
424
+ .toSorted((left, right) => compareBigInt(udtBalance(right), udtBalance(left)));
425
+ const plainCells = unspent
426
+ .filter(isPlainCapacityCell)
427
+ .toSorted((left, right) =>
428
+ compareBigInt(right.cellOutput.capacity, left.cellOutput.capacity),
429
+ );
430
+
431
+ await this.completeIckb(tx, signer.client, changeLock, ickbCells);
432
+ // Plain CKB: the sweep first, then whatever the fee still needs beyond the limit.
433
+ const swept = sweep(tx, plainCells);
434
+ await completeFee(tx, signer, changeLock, feeRate, plainCells.slice(swept));
435
+ assertDaoOutputLimit(tx, this.ickbLogic.dao.script);
436
+ return tx;
437
+ }
304
438
 
305
- const { order } = group;
306
- const info = order.data.info;
307
- if (
308
- (order.isCkb2UdtMatchable() && info.ckb2UdtCompare(midInfo) < 0) ||
309
- (order.isUdt2CkbMatchable() && info.udt2CkbCompare(midInfo) < 0)
310
- ) {
311
- systemOrders.push(order);
439
+ /** iCKB: what the outputs need first, then the sweep while the limit allows, then change. */
440
+ private async completeIckb(
441
+ tx: ccc.Transaction,
442
+ client: ccc.Client,
443
+ changeLock: ccc.Script,
444
+ ickbCells: readonly ccc.Cell[],
445
+ ): Promise<void> {
446
+ const required = this.ickbUdt.outputBalance(tx);
447
+ let balance = await this.ickbUdt.inputBalance(tx, client);
448
+ for (const cell of ickbCells) {
449
+ if (balance >= required && tx.inputs.length >= SWEEP_INPUT_LIMIT) {
450
+ break;
312
451
  }
452
+ tx.addInput(cell);
453
+ balance += udtBalance(cell);
313
454
  }
314
-
315
- const system = {
316
- feeRate,
317
- tip,
318
- exchangeRatio,
319
- orderPool: systemOrders,
320
- ckbAvailable,
321
- ckbMaturing,
322
- };
323
-
324
- // Estimates user orders maturity.
325
- for (const { order } of userOrders) {
326
- order.maturity = IckbSdk.maturity(order, system);
455
+ if (balance < required) {
456
+ throw new IckbError(`Insufficient iCKB, need ${String(required - balance)} more`, {
457
+ code: "insufficient_ickb",
458
+ });
327
459
  }
328
-
329
- return {
330
- system,
331
- user: {
332
- orders: userOrders,
333
- },
334
- };
460
+ this.ickbUdt.addChange(tx, changeLock, balance - required);
335
461
  }
462
+ };
463
+
464
+ /** Concrete iCKB SDK constructor and static estimators. */
465
+ // eslint-disable-next-line @typescript-eslint/no-redeclare -- The public type and runtime constructor intentionally share a name.
466
+ export const IckbSdk: {
467
+ new (managers: SdkManagers): IckbSdk;
468
+ fromChain: (chain: SupportedChain) => IckbSdk;
469
+ } = IckbSdkImplementation;
470
+
471
+ /** Whether the transaction already carries any input or output. */
472
+ export function hasTransactionActivity(tx: ccc.Transaction): boolean {
473
+ return tx.inputs.length > 0 || tx.outputs.length > 0;
474
+ }
336
475
 
337
- /**
338
- * Retrieves available CKB and maturing CKB values from the blockchain.
339
- *
340
- * This method:
341
- * - Fetches bot withdrawal requests and deposit snapshots.
342
- * - Aggregates available CKB balances from bot capacities.
343
- * - Calculates maturing CKB values (with their expected maturity timestamps)
344
- * based on deposit pool snapshots or via direct deposit cell lookups.
345
- * - Sorts and cumulatively sums the maturing values for later lookup.
346
- *
347
- * @param client - The blockchain client used for fetching data.
348
- * @param tip - The current block tip header.
349
- * @returns A Promise that resolves with:
350
- * - ckbAvailable: The total available CKB (as a FixedPoint).
351
- * - ckbMaturing: An array of maturing CKB objects containing the cumulative CKB
352
- * and the associated maturity timestamp.
353
- */
354
- private async getCkb(
355
- client: ccc.Client,
356
- tip: ccc.ClientBlockHeader,
357
- ): Promise<{
358
- ckbAvailable: ccc.FixedPoint;
359
- ckbMaturing: CkbCumulative[];
360
- }> {
361
- const opts = {
362
- onChain: true,
363
- tip,
364
- };
476
+ /** An order the market can fill at a gain: priced past the DAO ratio on its matchable side. */
477
+ function isPastPar(group: OrderGroup, exchangeRatio: Ratio): boolean {
478
+ const { order } = group;
479
+ const { info } = order.data;
480
+ return (
481
+ (order.isCkb2UdtMatchable() && info.ckbToUdt.compare(exchangeRatio) < 0) ||
482
+ (order.isUdt2CkbMatchable() && exchangeRatio.compare(info.udtToCkb) < 0)
483
+ );
484
+ }
365
485
 
366
- // Start fetching bot iCKB withdrawal requests.
367
- const promiseBotWithdrawals = collect(
368
- this.ownedOwner.findWithdrawalGroups(client, this.bots, opts),
369
- );
486
+ function conversionFailure(
487
+ reason: ConversionTransactionFailureReason,
488
+ estimatedMaturity: bigint,
489
+ minimum?: bigint,
490
+ ): ConversionTransactionResult {
491
+ return {
492
+ ok: false,
493
+ reason,
494
+ estimatedMaturity,
495
+ ...(minimum === undefined ? {} : { minimum }),
496
+ };
497
+ }
370
498
 
371
- // Initialize deposit pool snapshot.
372
- let poolSnapshotHex: ccc.Hex = "0x";
373
- let poolSnapshotEpoch = Epoch.from([0n, 0n, 1n]);
374
- // Map to track each bot's available CKB (minus a reserved amount for internal operations).
375
- const bot2Ckb = new Map<string, ccc.FixedPoint>();
376
- const reserved = -ccc.fixedPointFrom("2000");
377
- for await (const c of this.capacity.findCapacities(
378
- client,
379
- this.bots,
380
- opts,
381
- )) {
382
- const key = hexFrom(c.cell.cellOutput.lock);
383
- const ckb = (bot2Ckb.get(key) ?? reserved) + c.ckbValue;
384
- bot2Ckb.set(key, ckb);
385
-
386
- // Find the most recent deposit pool snapshot from bot cell output data.
387
- const outputData = c.cell.outputData;
388
- if (outputData.length % 256 === 2) {
389
- const h = await getHeader(client, {
390
- type: "txHash",
391
- value: c.cell.outPoint.txHash,
392
- });
393
- const e = Epoch.from(h.epoch);
394
- if (poolSnapshotEpoch.compare(e) < 0) {
395
- poolSnapshotHex = outputData;
396
- poolSnapshotEpoch = e;
397
- }
398
- }
399
- }
499
+ /** A plan always carries a deposit or an order, else it would be a collection. */
500
+ function conversionKind(
501
+ hasDirect: boolean,
502
+ hasOrder: boolean,
503
+ ): "direct" | "order" | "direct-plus-order" {
504
+ if (hasDirect && hasOrder) {
505
+ return "direct-plus-order";
506
+ }
507
+ return hasDirect ? "direct" : "order";
508
+ }
400
509
 
401
- const ckbMaturing = new Array<{
402
- ckbValue: ccc.FixedPoint;
403
- maturity: ccc.Num;
404
- }>();
405
- for (const wr of await promiseBotWithdrawals) {
406
- if (wr.owned.isReady) {
407
- // Update the bot's CKB based on withdrawal if the bot is ready.
408
- const key = hexFrom(wr.owner.cell.cellOutput.lock);
409
- const ckb = (bot2Ckb.get(key) ?? reserved) + wr.ckbValue;
410
- bot2Ckb.set(key, ckb);
411
- continue;
412
- }
510
+ /** Whether every liquid cell became an input; false when the input limit cut the sweep. */
511
+ function sweepsAll(tx: ccc.Transaction, cells: readonly ccc.Cell[]): boolean {
512
+ const spent = new Set(tx.inputs.map((input) => input.previousOutput.toHex()));
513
+ return cells.every((cell) => spent.has(cell.outPoint.toHex()));
514
+ }
413
515
 
414
- // Otherwise, add to maturing amounts.
415
- ckbMaturing.push({
416
- ckbValue: wr.ckbValue,
417
- maturity: wr.owned.maturity.toUnix(tip),
418
- });
419
- }
516
+ async function isOwnLock(signer: ccc.Signer, lock: ccc.Script): Promise<boolean> {
517
+ return (await signer.getAddressObjs()).some(({ script }) => script.eq(lock));
518
+ }
420
519
 
421
- // Sum available CKB across all bot lock scripts.
422
- let ckbAvailable = 0n;
423
- for (const ckb of bot2Ckb.values()) {
424
- if (ckb > 0n) {
425
- ckbAvailable += ckb;
426
- }
427
- }
520
+ async function getFeeRate(client: ccc.Client): Promise<ccc.Num> {
521
+ const feeRate = await client.getFeeRate();
522
+ if (feeRate < 0n) {
523
+ throw new Error("Client fee rate must be non-negative");
524
+ }
525
+ return feeRate;
526
+ }
428
527
 
429
- // Estimate available CKB from deposit pool snapshot.
430
- const tipEpoch = Epoch.from(tip.epoch);
431
- const oneCycle = Epoch.from([180n, 0n, 1n]);
432
- if (poolSnapshotHex !== "0x") {
433
- const eNumber = tip.epoch[0];
434
- let start = Epoch.from([eNumber - (eNumber % 180n), 0n, 1n]);
435
- const step = Epoch.from([0n, 180n, 1024n]);
436
- const depositSize = convert(false, ICKB_DEPOSIT_CAP, tip);
437
- for (const binAmount of PoolSnapshot.decode(poolSnapshotHex)) {
438
- const end = start.add(step);
439
-
440
- if (binAmount > 0) {
441
- ckbMaturing.push({
442
- ckbValue: BigInt(binAmount) * depositSize,
443
- maturity:
444
- tipEpoch.compare(tipEpoch) < 0
445
- ? // If the bin has already started, assume worst-case timing.
446
- end.add(oneCycle).toUnix(tip)
447
- : // Otherwise, use the bin end as the maturity.
448
- end.toUnix(tip),
449
- });
450
- }
528
+ function udtBalance(cell: ccc.Cell): ccc.Num {
529
+ return ccc.udtBalanceFrom(cell.outputData);
530
+ }
451
531
 
452
- start = end;
532
+ /** Adds cells while the input count stays under the limit; returns how many were added. */
533
+ function sweep(tx: ccc.Transaction, cells: readonly ccc.Cell[]): number {
534
+ const added = Math.min(cells.length, Math.max(0, SWEEP_INPUT_LIMIT - tx.inputs.length));
535
+ for (const cell of cells.slice(0, added)) {
536
+ tx.addInput(cell);
537
+ }
538
+ return added;
539
+ }
540
+
541
+ /** Completes the fee, adding reserve cells one at a time while capacity is short. */
542
+ async function completeFee(
543
+ tx: ccc.Transaction,
544
+ signer: ccc.Signer,
545
+ changeLock: ccc.Script,
546
+ feeRate: ccc.Num,
547
+ reserve: readonly ccc.Cell[],
548
+ ): Promise<void> {
549
+ const remaining = [...reserve];
550
+ for (;;) {
551
+ try {
552
+ await tx.completeFeeChangeToLock(signer, changeLock, feeRate, undefined, {
553
+ shouldAddInputs: false,
554
+ });
555
+ return;
556
+ } catch (error) {
557
+ if (!(error instanceof ccc.ErrorTransactionInsufficientCapacity)) {
558
+ throw error;
453
559
  }
454
- } else {
455
- // Without snapshot data, fetch deposits directly.
456
- for await (const d of this.ickbLogic.findDeposits(client, opts)) {
457
- ckbMaturing.push({
458
- ckbValue: d.ckbValue,
459
- maturity: d.maturity.toUnix(tip),
560
+ const cell = remaining.shift();
561
+ if (cell === undefined) {
562
+ throw new IckbError(error.message, {
563
+ code: "insufficient_capacity",
564
+ cause: error,
460
565
  });
461
566
  }
567
+ tx.addInput(cell);
462
568
  }
569
+ }
570
+ }
463
571
 
464
- // Sort maturing CKB entries by their maturity timestamp.
465
- ckbMaturing.sort((a, b) => Number(a.maturity - b.maturity));
572
+ /** Stable machine-readable failures owned by the Phase-2 SDK. */
573
+ export type IckbErrorCode = "insufficient_capacity" | "insufficient_ickb";
466
574
 
467
- // Calculate cumulative maturing CKB values.
468
- let cumulative = 0n;
469
- const ckbCumulativeMaturing: CkbCumulative[] = [];
470
- for (const { ckbValue, maturity } of ckbMaturing) {
471
- cumulative += ckbValue;
472
- ckbCumulativeMaturing.push({ ckbCumulative: cumulative, maturity });
473
- }
575
+ /** Typed SDK failure with a stable machine-readable code. */
576
+ export class IckbError extends Error {
577
+ /** Stable failure code for callers and observability. */
578
+ public readonly code: IckbErrorCode;
474
579
 
475
- return {
476
- ckbAvailable,
477
- ckbMaturing: ckbCumulativeMaturing,
478
- };
580
+ /** Creates a typed SDK failure while preserving its cause. */
581
+ constructor(message: string, options: ErrorOptions & { code: IckbErrorCode }) {
582
+ super(message, options);
583
+ this.name = "IckbError";
584
+ this.code = options.code;
479
585
  }
480
586
  }
481
587
 
482
588
  /**
483
- * Represents the state of the system.
589
+ * Completes the first candidate that the real completer can fund.
590
+ *
591
+ * @remarks
592
+ * Only completion knows what a transaction costs once markers, remainder orders, change, and
593
+ * fee are in, so no count is computed up front: each candidate is built and completed in turn.
594
+ * Capacity, DAO output-limit and DAO header-index failures advance
595
+ * the walk; transport, scan, signer, and malformed-transaction errors propagate. Exhausting
596
+ * the candidates throws the last advancing failure.
484
597
  */
485
- export interface SystemState {
486
- /** The fee rate for transactions. */
487
- feeRate: ccc.Num;
488
-
489
- /** The tip for the current block header. */
490
- tip: ccc.ClientBlockHeader;
491
-
492
- /** The exchange ratio between CKB and UDT. */
493
- exchangeRatio: Ratio;
494
-
495
- /** The order pool containing order cells matching system criteria. */
496
- orderPool: OrderCell[];
497
-
498
- /** The total available CKB (as FixedPoint). */
499
- ckbAvailable: ccc.FixedPoint;
500
-
501
- /** Array of CKB maturing entries with cumulative amounts and maturity timestamps. */
502
- ckbMaturing: CkbCumulative[];
598
+ export async function completeFirstFundable<T>(
599
+ candidates: Iterable<T>,
600
+ build: (candidate: T) => ccc.Transaction,
601
+ complete: (tx: ccc.Transaction) => Promise<ccc.Transaction>,
602
+ ): Promise<{ candidate: T; tx: ccc.Transaction }> {
603
+ let error: Error | undefined;
604
+ for (const candidate of candidates) {
605
+ try {
606
+ return { candidate, tx: await complete(build(candidate)) };
607
+ } catch (failure) {
608
+ if (!isFundabilityFailure(failure)) {
609
+ throw failure;
610
+ }
611
+ error = failure;
612
+ }
613
+ }
614
+ throw error ?? new Error("No candidate could be completed");
503
615
  }
504
616
 
505
617
  /**
506
- * Represents a cumulative CKB maturing entry.
618
+ * A failure the walk advances past: the candidate costs more than the account funds, or
619
+ * needs more DAO outputs or deposit-header slots than one transaction has.
507
620
  */
508
- export interface CkbCumulative {
509
- /** The cumulative CKB value (as FixedPoint) up to this maturity. */
510
- ckbCumulative: ccc.FixedPoint;
511
- /** The maturity timestamp (as ccc.Num). */
512
- maturity: ccc.Num;
621
+ export function isFundabilityFailure(error: unknown): error is Error {
622
+ return (
623
+ error instanceof IckbError ||
624
+ error instanceof ccc.ErrorTransactionInsufficientCapacity ||
625
+ error instanceof DaoOutputLimitError ||
626
+ error instanceof DaoHeaderIndexError
627
+ );
513
628
  }