@ickb/sdk 9000.0.1 → 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 (138) hide show
  1. package/README.md +14 -11
  2. package/dist/constants.d.ts +0 -4
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +6 -24
  5. package/dist/constants.js.map +1 -1
  6. package/dist/conversion/estimate.d.ts +0 -6
  7. package/dist/conversion/estimate.d.ts.map +1 -1
  8. package/dist/conversion/estimate.js +1 -1
  9. package/dist/conversion/estimate.js.map +1 -1
  10. package/dist/conversion/maturity.d.ts +1 -1
  11. package/dist/conversion/maturity.js +13 -12
  12. package/dist/conversion/maturity.js.map +1 -1
  13. package/dist/conversion/plans.d.ts.map +1 -1
  14. package/dist/conversion/plans.js +6 -6
  15. package/dist/conversion/plans.js.map +1 -1
  16. package/dist/conversion/projection.d.ts +5 -6
  17. package/dist/conversion/projection.d.ts.map +1 -1
  18. package/dist/conversion/projection.js +11 -8
  19. package/dist/conversion/projection.js.map +1 -1
  20. package/dist/conversion/types.d.ts +29 -17
  21. package/dist/conversion/types.d.ts.map +1 -1
  22. package/dist/conversion/withdrawal_ring.d.ts +1 -3
  23. package/dist/conversion/withdrawal_ring.d.ts.map +1 -1
  24. package/dist/conversion/withdrawal_ring.js +4 -4
  25. package/dist/conversion/withdrawal_ring.js.map +1 -1
  26. package/dist/dao.d.ts +19 -5
  27. package/dist/dao.d.ts.map +1 -1
  28. package/dist/dao.js +24 -12
  29. package/dist/dao.js.map +1 -1
  30. package/dist/index.d.ts +9 -11
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/logic.d.ts +10 -14
  35. package/dist/logic.d.ts.map +1 -1
  36. package/dist/logic.js +28 -39
  37. package/dist/logic.js.map +1 -1
  38. package/dist/order/cells.d.ts +10 -49
  39. package/dist/order/cells.d.ts.map +1 -1
  40. package/dist/order/cells.js +28 -146
  41. package/dist/order/cells.js.map +1 -1
  42. package/dist/order/conversion.d.ts +3 -9
  43. package/dist/order/conversion.d.ts.map +1 -1
  44. package/dist/order/conversion.js +8 -11
  45. package/dist/order/conversion.js.map +1 -1
  46. package/dist/order/fill.d.ts +2 -2
  47. package/dist/order/fill.js +3 -3
  48. package/dist/order/fill.js.map +1 -1
  49. package/dist/order/info.d.ts.map +1 -1
  50. package/dist/order/info.js +3 -0
  51. package/dist/order/info.js.map +1 -1
  52. package/dist/order/matcher.d.ts +1 -1
  53. package/dist/order/matcher.d.ts.map +1 -1
  54. package/dist/order/matcher.js +3 -2
  55. package/dist/order/matcher.js.map +1 -1
  56. package/dist/order/order.d.ts +6 -2
  57. package/dist/order/order.d.ts.map +1 -1
  58. package/dist/order/order.js +32 -37
  59. package/dist/order/order.js.map +1 -1
  60. package/dist/order/order_data.d.ts.map +1 -1
  61. package/dist/order/order_data.js +3 -0
  62. package/dist/order/order_data.js.map +1 -1
  63. package/dist/order/ratio.d.ts +0 -2
  64. package/dist/order/ratio.d.ts.map +1 -1
  65. package/dist/order/ratio.js +3 -11
  66. package/dist/order/ratio.js.map +1 -1
  67. package/dist/order/relative.d.ts.map +1 -1
  68. package/dist/order/relative.js +3 -0
  69. package/dist/order/relative.js.map +1 -1
  70. package/dist/owned_owner.d.ts +10 -15
  71. package/dist/owned_owner.d.ts.map +1 -1
  72. package/dist/owned_owner.js +27 -47
  73. package/dist/owned_owner.js.map +1 -1
  74. package/dist/sdk.d.ts +74 -31
  75. package/dist/sdk.d.ts.map +1 -1
  76. package/dist/sdk.js +81 -59
  77. package/dist/sdk.js.map +1 -1
  78. package/dist/send/sign_and_send_transaction.d.ts +32 -17
  79. package/dist/send/sign_and_send_transaction.d.ts.map +1 -1
  80. package/dist/send/sign_and_send_transaction.js +128 -28
  81. package/dist/send/sign_and_send_transaction.js.map +1 -1
  82. package/dist/send/wait_transaction.d.ts +7 -6
  83. package/dist/send/wait_transaction.d.ts.map +1 -1
  84. package/dist/send/wait_transaction.js +29 -61
  85. package/dist/send/wait_transaction.js.map +1 -1
  86. package/dist/udt.d.ts +20 -35
  87. package/dist/udt.d.ts.map +1 -1
  88. package/dist/udt.js +39 -103
  89. package/dist/udt.js.map +1 -1
  90. package/dist/utils/chain.d.ts +0 -35
  91. package/dist/utils/chain.d.ts.map +1 -1
  92. package/dist/utils/chain.js +1 -19
  93. package/dist/utils/chain.js.map +1 -1
  94. package/dist/utils/codec.d.ts.map +1 -1
  95. package/dist/utils/codec.js +3 -0
  96. package/dist/utils/codec.js.map +1 -1
  97. package/dist/utils/utils.d.ts +19 -46
  98. package/dist/utils/utils.d.ts.map +1 -1
  99. package/dist/utils/utils.js +53 -74
  100. package/dist/utils/utils.js.map +1 -1
  101. package/dist/utils/verified_client.d.ts +19 -0
  102. package/dist/utils/verified_client.d.ts.map +1 -0
  103. package/dist/utils/verified_client.js +92 -0
  104. package/dist/utils/verified_client.js.map +1 -0
  105. package/package.json +2 -2
  106. package/src/constants.ts +6 -25
  107. package/src/conversion/estimate.ts +1 -3
  108. package/src/conversion/maturity.ts +13 -14
  109. package/src/conversion/plans.ts +6 -6
  110. package/src/conversion/projection.ts +15 -7
  111. package/src/conversion/types.ts +32 -17
  112. package/src/conversion/withdrawal_ring.ts +4 -8
  113. package/src/dao.ts +38 -14
  114. package/src/index.ts +8 -8
  115. package/src/logic.ts +40 -52
  116. package/src/order/cells.ts +37 -174
  117. package/src/order/conversion.ts +8 -19
  118. package/src/order/fill.ts +3 -3
  119. package/src/order/info.ts +3 -0
  120. package/src/order/matcher.ts +3 -3
  121. package/src/order/order.ts +38 -52
  122. package/src/order/order_data.ts +3 -0
  123. package/src/order/ratio.ts +3 -20
  124. package/src/order/relative.ts +3 -0
  125. package/src/owned_owner.ts +47 -77
  126. package/src/sdk.ts +95 -108
  127. package/src/send/sign_and_send_transaction.ts +164 -28
  128. package/src/send/wait_transaction.ts +44 -88
  129. package/src/udt.ts +56 -124
  130. package/src/utils/chain.ts +0 -41
  131. package/src/utils/codec.ts +4 -0
  132. package/src/utils/utils.ts +76 -105
  133. package/src/utils/verified_client.ts +136 -0
  134. package/dist/utils/transaction_header.d.ts +0 -17
  135. package/dist/utils/transaction_header.d.ts.map +0 -1
  136. package/dist/utils/transaction_header.js +0 -44
  137. package/dist/utils/transaction_header.js.map +0 -1
  138. package/src/utils/transaction_header.ts +0 -66
@@ -9,8 +9,12 @@ import {
9
9
  import type { IckbDepositCell } from "./logic.ts";
10
10
  import { ickbValue } from "./udt.ts";
11
11
  import { CheckedInt32LE, CheckedUint64LE } from "./utils/codec.ts";
12
- import { headersByNumber, transactionHeaders } from "./utils/transaction_header.ts";
13
- import type { ScriptDeps, TransactionHeader, ValueComponents } from "./utils/utils.ts";
12
+ import {
13
+ headersByNumber,
14
+ type ScannedCell,
15
+ type ScriptDeps,
16
+ type ValueComponents,
17
+ } from "./utils/utils.ts";
14
18
 
15
19
  /**
16
20
  * A live Nervos DAO withdrawal request cell, valued at the two headers it spans.
@@ -19,9 +23,7 @@ export interface DaoWithdrawalRequestCell {
19
23
  /** The DAO withdrawal request cell. */
20
24
  cell: ccc.Cell;
21
25
  /** The deposit header and the request's own header. */
22
- headers: [TransactionHeader, TransactionHeader];
23
- /** The DAO interest accrued between the two headers. */
24
- interests: ccc.Num;
26
+ headers: [ccc.ClientBlockHeader, ccc.ClientBlockHeader];
25
27
  /** The DAO claim epoch of the request. */
26
28
  maturity: ccc.Epoch;
27
29
  /** Whether the claim epoch is at or before the tip: the request can be withdrawn. */
@@ -64,23 +66,15 @@ export function decodeOwnerData(outputData: ccc.Hex): OwnerData {
64
66
  /**
65
67
  * Wraps an owner marker cell that references an owned withdrawal request.
66
68
  */
67
- export class OwnerCell implements ValueComponents {
69
+ export class OwnerCell {
68
70
  /** The live owner marker cell whose output data points to the owned request. */
69
71
  public cell: ccc.Cell;
70
72
 
71
- /** Owner marker cells carry no UDT value. */
72
- public readonly udtValue = 0n;
73
-
74
73
  /** Creates an owner marker wrapper for a live cell. */
75
74
  constructor(cell: ccc.Cell) {
76
75
  this.cell = cell;
77
76
  }
78
77
 
79
- /** Returns the CKB capacity held by the owner marker cell. */
80
- public get ckbValue(): ccc.FixedPoint {
81
- return this.cell.cellOutput.capacity;
82
- }
83
-
84
78
  /**
85
79
  * Returns the owned withdrawal request out point referenced by this owner marker.
86
80
  *
@@ -131,7 +125,7 @@ export class WithdrawalGroup implements ValueComponents {
131
125
 
132
126
  /** Returns the iCKB amount represented by the owned withdrawal request. */
133
127
  public get udtValue(): ccc.FixedPoint {
134
- return ickbValue(this.owned.cell.capacityFree, this.owned.headers[0].header);
128
+ return ickbValue(this.owned.cell.capacityFree, this.owned.headers[0]);
135
129
  }
136
130
  }
137
131
 
@@ -180,8 +174,8 @@ export class OwnedOwnerManager implements ScriptDeps {
180
174
  * Spends the deposits into DAO withdrawal requests locked by the Owned Owner script,
181
175
  * each followed by an owner marker locked by `lock` that points back at it.
182
176
  *
183
- * @remarks Caller must ensure UDT cellDeps are added to the transaction, for example
184
- * via `ickbUdt.addCellDeps(tx)`.
177
+ * @remarks Completion (`IckbSdk.completeTransaction`) adds the dep group that also
178
+ * resolves the xUDT script.
185
179
  */
186
180
  public requestWithdrawal(
187
181
  txLike: ccc.TransactionLike,
@@ -202,21 +196,15 @@ export class OwnedOwnerManager implements ScriptDeps {
202
196
  tx.addCellDeps(this.cellDeps);
203
197
  tx.addCellDeps(this.dao.cellDeps);
204
198
  const requestStart = tx.outputs.length;
205
- for (const { cell, headers } of deposits) {
206
- const depositHeader = headers[0];
207
- if (depositHeader.txHash !== cell.outPoint.txHash) {
208
- throw new Error(
209
- `DAO deposit ${cell.outPoint.toHex()} header txHash ${String(depositHeader.txHash)} does not match cell txHash ${cell.outPoint.txHash}`,
210
- );
211
- }
199
+ for (const { cell, header } of deposits) {
212
200
  if (cell.cellOutput.lock.args.length !== this.script.args.length) {
213
201
  throw new Error("Withdrawal request lock args has different size from deposit");
214
202
  }
215
- pushHeaderDep(tx, depositHeader.header.hash);
203
+ pushHeaderDep(tx, header.hash);
216
204
  tx.addInput(cell);
217
205
  tx.addOutput(
218
206
  { capacity: cell.cellOutput.capacity, lock: this.script, type: this.dao.script },
219
- CheckedUint64LE.encode(depositHeader.header.number),
207
+ CheckedUint64LE.encode(header.number),
220
208
  );
221
209
  }
222
210
  for (let index = 0; index < deposits.length; index += 1) {
@@ -256,12 +244,6 @@ export class OwnedOwnerManager implements ScriptDeps {
256
244
  `Withdrawal owner ${group.owner.cell.outPoint.toHex()} points to ${linkedOutPoint.toHex()} but group owned cell is ${ownedOutPoint.toHex()}`,
257
245
  );
258
246
  }
259
- const { cell, headers } = group.owned;
260
- if (headers[1].txHash !== cell.outPoint.txHash) {
261
- throw new Error(
262
- `DAO withdrawal request ${cell.outPoint.toHex()} header txHash ${String(headers[1].txHash)} does not match cell txHash ${cell.outPoint.txHash}`,
263
- );
264
- }
265
247
  }
266
248
  tx.addCellDeps(this.cellDeps);
267
249
  tx.addCellDeps(this.dao.cellDeps);
@@ -270,17 +252,17 @@ export class OwnedOwnerManager implements ScriptDeps {
270
252
  // pushed: the limit then counts these alone. Safe because nothing else in the stack reads
271
253
  // a header by position and every built transaction passes here once.
272
254
  const depositHashes = [
273
- ...new Set(withdrawalGroups.map(({ owned }) => owned.headers[0].header.hash)),
255
+ ...new Set(withdrawalGroups.map(({ owned }) => owned.headers[0].hash)),
274
256
  ];
275
257
  tx.headerDeps = [
276
258
  ...depositHashes,
277
259
  ...tx.headerDeps.filter((hash) => !depositHashes.includes(hash)),
278
260
  ];
279
261
  for (const { owned } of withdrawalGroups) {
280
- pushHeaderDep(tx, owned.headers[1].header.hash);
262
+ pushHeaderDep(tx, owned.headers[1].hash);
281
263
  }
282
264
  for (const { owned } of withdrawalGroups) {
283
- const headerIndex = tx.headerDeps.indexOf(owned.headers[0].header.hash);
265
+ const headerIndex = tx.headerDeps.indexOf(owned.headers[0].hash);
284
266
  // The deployed dao.c reads one byte of the u64 index field (RFC 0023 erratum,
285
267
  // nervosnetwork/rfcs pull 456), so index 256 would be read as 0.
286
268
  if (headerIndex >= DAO_HEADER_INDEX_LIMIT) {
@@ -309,52 +291,45 @@ export class OwnedOwnerManager implements ScriptDeps {
309
291
 
310
292
  /**
311
293
  * The owner markers among `cells` paired with their owned withdrawal requests, which live
312
- * in the marker's own transaction at the encoded distance. Markers whose target is not an
313
- * owned withdrawal request are skipped. Each header is read once per batch.
294
+ * in the marker's own transaction at the encoded distance, so the marker's block is the
295
+ * request's. Markers whose target is not an owned withdrawal request are skipped. Each
296
+ * header is read once per batch.
314
297
  */
315
298
  public async withdrawalGroupsFrom(
316
299
  client: ccc.Client,
317
- cells: readonly ccc.Cell[],
300
+ cells: readonly ScannedCell[],
318
301
  tip: ccc.ClientBlockHeader,
319
302
  ): Promise<WithdrawalGroup[]> {
320
303
  const owners = cells
321
- .filter((cell) => this.isOwner(cell))
322
- .map((cell) => new OwnerCell(cell));
304
+ .filter(({ cell }) => this.isOwner(cell))
305
+ .map(({ cell, blockNumber }) => ({ owner: new OwnerCell(cell), blockNumber }));
323
306
  const ownedCells = await Promise.all(
324
- owners.map(async (owner) => client.getCell(owner.getOwned())),
307
+ owners.map(async ({ owner }) => client.getCell(owner.getOwned())),
325
308
  );
326
- const pairs = owners.flatMap((owner, index) => {
309
+ const pairs = owners.flatMap(({ owner, blockNumber }, index) => {
327
310
  const owned = ownedCells[index];
328
311
  return owned !== undefined && this.isOwned(owned)
329
- ? [{ owner, owned, depositBlockNumber: depositBlockNumberOf(owned) }]
312
+ ? [{ owner, owned, blockNumber, depositBlockNumber: depositBlockNumberOf(owned) }]
330
313
  : [];
331
314
  });
332
- const [depositHeaders, requestHeaders] = await Promise.all([
333
- headersByNumber(
334
- client,
335
- pairs.map((pair) => pair.depositBlockNumber),
336
- ),
337
- transactionHeaders(
338
- client,
339
- pairs.map((pair) => pair.owned.outPoint.txHash),
340
- ),
341
- ]);
342
- return pairs.map(({ owner, owned, depositBlockNumber }) => {
343
- const { txHash } = owned.outPoint;
344
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- both headers were read above.
345
- const depositHeader = depositHeaders.get(depositBlockNumber)!;
346
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- both headers were read above.
347
- const requestHeader = requestHeaders.get(txHash)!;
348
- return new WithdrawalGroup(
349
- withdrawalRequestCell(
350
- owned,
351
- depositHeader,
352
- { header: requestHeader, txHash },
353
- tip,
315
+ const headers = await headersByNumber(
316
+ client,
317
+ pairs.flatMap((pair) => [pair.depositBlockNumber, pair.blockNumber]),
318
+ );
319
+ return pairs.map(
320
+ ({ owner, owned, blockNumber, depositBlockNumber }) =>
321
+ new WithdrawalGroup(
322
+ withdrawalRequestCell(
323
+ owned,
324
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- both headers were read above.
325
+ headers.get(depositBlockNumber)!,
326
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- both headers were read above.
327
+ headers.get(blockNumber)!,
328
+ tip,
329
+ ),
330
+ owner,
354
331
  ),
355
- owner,
356
- );
357
- });
332
+ );
358
333
  }
359
334
  }
360
335
 
@@ -362,19 +337,14 @@ export class OwnedOwnerManager implements ScriptDeps {
362
337
  export function withdrawalRequestCell(
363
338
  cell: ccc.Cell,
364
339
  depositHeader: ccc.ClientBlockHeader,
365
- requestHeader: TransactionHeader,
340
+ requestHeader: ccc.ClientBlockHeader,
366
341
  tip: ccc.ClientBlockHeader,
367
342
  ): DaoWithdrawalRequestCell {
368
- const interests = ccc.calcDaoProfit(
369
- cell.capacityFree,
370
- depositHeader,
371
- requestHeader.header,
372
- );
373
- const maturity = ccc.calcDaoClaimEpoch(depositHeader, requestHeader.header);
343
+ const interests = ccc.calcDaoProfit(cell.capacityFree, depositHeader, requestHeader);
344
+ const maturity = ccc.calcDaoClaimEpoch(depositHeader, requestHeader);
374
345
  return {
375
346
  cell,
376
- headers: [{ header: depositHeader }, requestHeader],
377
- interests,
347
+ headers: [depositHeader, requestHeader],
378
348
  maturity,
379
349
  isReady: maturity.compare(tip.epoch) <= 0,
380
350
  ckbValue: cell.cellOutput.capacity + interests,
package/src/sdk.ts CHANGED
@@ -17,52 +17,23 @@ import type {
17
17
  } from "./conversion/types.ts";
18
18
  import {
19
19
  assertDaoOutputLimit,
20
+ assertFresh,
20
21
  broadcastDeadline,
21
22
  DaoHeaderIndexError,
22
23
  DaoOutputLimitError,
23
24
  type LockUpPolicy,
25
+ pushHeaderDep,
26
+ tipMaxAgeMs,
24
27
  } from "./dao.ts";
25
28
  import type { IckbDepositCell, LogicManager } from "./logic.ts";
26
29
  import type { OrderGroup } from "./order/cells.ts";
27
30
  import type { OrderManager } from "./order/order.ts";
28
31
  import { Ratio } from "./order/ratio.ts";
29
32
  import type { OwnedOwnerManager } from "./owned_owner.ts";
30
- import { ickbExchangeRatio, type IckbUdt } from "./udt.ts";
33
+ import { ickbExchangeRatio, type IckbUdt, type ProtocolCell } from "./udt.ts";
31
34
  import type { SupportedChain } from "./utils/chain.ts";
32
35
  import { compareBigInt, findCells, isPlainCapacityCell, unique } from "./utils/utils.ts";
33
-
34
- /**
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.
37
- */
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
- }
36
+ import { verifiedClient } from "./utils/verified_client.ts";
66
37
 
67
38
  /**
68
39
  * Input count up to which one own transaction sweeps liquid cells.
@@ -80,13 +51,16 @@ export const SWEEP_INPUT_LIMIT = 1000;
80
51
  type ResolvedConversionOptions = ConversionTransactionOptions & { lock: ccc.Script };
81
52
 
82
53
  /** What every own transaction collects along: the context's collectable positions. */
83
- export type CollectSteps = Pick<
54
+ type CollectSteps = Pick<
84
55
  ConversionTransactionContext,
85
56
  "availableOrders" | "receipts" | "readyWithdrawals"
86
57
  >;
87
58
 
88
- // eslint-disable-next-line @typescript-eslint/no-shadow -- Preserve the runtime constructor name.
89
- const IckbSdkImplementation = class IckbSdk {
59
+ /**
60
+ * The whole SDK, which the Node actors drive by relative import; the package barrel
61
+ * exposes the two conversion-workflow methods, and its assignment checks they agree.
62
+ */
63
+ export class IckbSdk {
90
64
  private readonly ickbUdt: IckbUdt;
91
65
  private readonly ownedOwner: OwnedOwnerManager;
92
66
  private readonly ickbLogic: LogicManager;
@@ -107,28 +81,49 @@ const IckbSdkImplementation = class IckbSdk {
107
81
  // ---- State reads -------------------------------------------------------------------
108
82
 
109
83
  /**
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`).
84
+ * Reads system, user-order, and account state at the sampled tip's parent, under the
85
+ * caller's withdrawal timing rules (`BOT_LOCK_UP` or `WALLET_LOCK_UP`) and, when given, a
86
+ * positive fee rate cap that replaces CCC's ceiling; a cap of zero or less throws.
112
87
  *
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.
88
+ * @remarks Every read goes through `verifiedClient` and the tip's parent is bounded by the
89
+ * policy's slack, so a caller passes a plain client. Every read is complete and uncapped: a large
90
+ * book or pool costs a slower read, never a partial or failed one. Each account lock is
91
+ * enumerated by one uncached unfiltered exact-lock scan and classified client-side.
116
92
  */
117
93
  public async getL1AccountState(
118
- client: ccc.Client,
94
+ plainClient: ccc.Client,
119
95
  locks: ccc.Script[],
120
96
  lockUp: LockUpPolicy,
97
+ maxFeeRate?: ccc.NumLike,
121
98
  ): Promise<{
122
99
  system: SystemState;
123
- user: { orders: OrderGroup[] };
100
+ userOrders: OrderGroup[];
124
101
  account: AccountState;
125
102
  }> {
126
- const tip = await client.getTipHeader();
103
+ // CCC reads a zero cap as no cap at all and would plan at a negative one.
104
+ if (maxFeeRate !== undefined && ccc.numFrom(maxFeeRate) <= ccc.Zero) {
105
+ throw new RangeError("maxFeeRate must be positive");
106
+ }
107
+ const client = verifiedClient(plainClient);
108
+ const live = await client.getTipHeader();
109
+ // The live tip can still lose a one-block race, and a transaction naming it as a header
110
+ // dep is then refused after signing; every recorded fork was one block deep.
111
+ const tip = await client.getHeaderByNumber(live.number - 1n);
112
+ if (tip?.hash !== live.parentHash) {
113
+ throw new Error(
114
+ `Block ${String(live.number - 1n)} is not the parent of tip ${live.hash}`,
115
+ );
116
+ }
117
+ // The age bound is on the header the read is priced at: a fresh tip a node invented can
118
+ // name any old block as its parent.
119
+ assertFresh(tip, tipMaxAgeMs(lockUp));
127
120
  const exchangeRatio = Ratio.from(ickbExchangeRatio(tip));
128
121
  const [poolDeposits, orders, feeRate, accounts] = await Promise.all([
129
122
  this.ickbLogic.findDeposits(client, tip),
130
123
  this.order.findOrders(client),
131
- getFeeRate(client),
124
+ // CCC clamps the sampled median at the cap, the connector's chosen rate too. A given cap
125
+ // replaces CCC's default ceiling, on purpose: the caller's cap is the ceiling.
126
+ client.getFeeRate(undefined, maxFeeRate === undefined ? undefined : { maxFeeRate }),
132
127
  Promise.all(
133
128
  [...unique(locks)].map(async (lock) => this.readLockCells(client, lock, tip)),
134
129
  ),
@@ -144,7 +139,7 @@ const IckbSdkImplementation = class IckbSdk {
144
139
  poolDeposits,
145
140
  lockUp,
146
141
  },
147
- user: { orders: orders.filter((group) => group.isOwner(...locks)) },
142
+ userOrders: orders.filter((group) => group.isOwner(...locks)),
148
143
  account: {
149
144
  capacityCells: accounts.flatMap((account) => account.capacityCells),
150
145
  nativeUdtCells: accounts.flatMap((account) => account.nativeUdtCells),
@@ -160,15 +155,16 @@ const IckbSdkImplementation = class IckbSdk {
160
155
  lock: ccc.Script,
161
156
  tip: ccc.ClientBlockHeader,
162
157
  ): Promise<AccountState> {
163
- const cells = await findCells(client, {
158
+ const scanned = await findCells(client, {
164
159
  script: lock,
165
160
  scriptType: "lock",
166
161
  scriptSearchMode: "exact",
167
162
  withData: true,
168
163
  });
164
+ const cells = scanned.map(({ cell }) => cell);
169
165
  const [receipts, withdrawalGroups] = await Promise.all([
170
- this.ickbLogic.receiptsFrom(client, cells),
171
- this.ownedOwner.withdrawalGroupsFrom(client, cells, tip),
166
+ this.ickbLogic.receiptsFrom(client, scanned),
167
+ this.ownedOwner.withdrawalGroupsFrom(client, scanned, tip),
172
168
  ]);
173
169
  return {
174
170
  capacityCells: cells.filter(isPlainCapacityCell),
@@ -192,7 +188,6 @@ const IckbSdkImplementation = class IckbSdk {
192
188
  * expected planning outcomes; when no plan can be funded the last completion error throws.
193
189
  */
194
190
  public async buildConversionTransaction(
195
- txLike: ccc.TransactionLike,
196
191
  options: ConversionTransactionOptions,
197
192
  ): Promise<ConversionTransactionResult> {
198
193
  const { amount, context, direction } = options;
@@ -206,25 +201,23 @@ const IckbSdkImplementation = class IckbSdk {
206
201
  return conversionFailure("insufficient-ickb", context.estimatedMaturity);
207
202
  }
208
203
 
209
- const baseTx = ccc.Transaction.from(txLike);
210
204
  const resolved = {
211
205
  ...options,
212
206
  lock: options.lock ?? (await options.signer.getRecommendedAddressObj()).script,
213
207
  };
214
208
  if (amount === 0n) {
215
- return this.buildCollectOnlyConversion(baseTx, resolved);
209
+ return this.buildCollectOnlyConversion(resolved);
216
210
  }
217
211
  return direction === "ckb-to-ickb"
218
- ? this.buildCkbToIckbConversion(baseTx, resolved)
219
- : this.buildIckbToCkbConversion(baseTx, resolved);
212
+ ? this.buildCkbToIckbConversion(resolved)
213
+ : this.buildIckbToCkbConversion(resolved);
220
214
  }
221
215
 
222
216
  private async buildCollectOnlyConversion(
223
- baseTx: ccc.Transaction,
224
217
  options: ResolvedConversionOptions,
225
218
  ): Promise<ConversionTransactionResult> {
226
219
  const { context, lock, signer } = options;
227
- const tx = this.buildBaseTransaction(baseTx, context);
220
+ const tx = this.buildBaseTransaction(ccc.Transaction.default(), context);
228
221
  // An empty collection to a lock of the signer's own would only compact the account;
229
222
  // to another lock it moves the liquid cells, which completion builds from the sweep.
230
223
  if (
@@ -238,29 +231,29 @@ const IckbSdkImplementation = class IckbSdk {
238
231
  ok: true,
239
232
  tx: completed,
240
233
  estimatedMaturity: context.estimatedMaturity,
241
- conversion: { kind: "collect-only" },
234
+ conversionKind: "collect-only",
242
235
  isSweepComplete: sweepsAll(completed, context.cells),
243
236
  };
244
237
  }
245
238
 
246
239
  private async buildCkbToIckbConversion(
247
- baseTx: ccc.Transaction,
248
240
  options: ResolvedConversionOptions,
249
241
  ): Promise<ConversionTransactionResult> {
250
242
  const { context, lock } = options;
251
243
  // A plan is dropped only for an unrepresentable remainder order.
252
244
  const plans = ckbToIckbConversionPlans(options);
253
245
  if (plans.length === 0) {
254
- return conversionFailure(
255
- "amount-too-small",
256
- context.estimatedMaturity,
257
- minimumOrderAmount(true, context.system),
258
- );
246
+ return {
247
+ ok: false,
248
+ reason: "amount-too-small",
249
+ estimatedMaturity: context.estimatedMaturity,
250
+ minimum: minimumOrderAmount(true, context.system),
251
+ };
259
252
  }
260
253
  const { candidate: plan, tx } = await completeFirstFundable(
261
254
  plans,
262
255
  (candidate) => {
263
- let partial = this.buildBaseTransaction(baseTx.clone(), context);
256
+ let partial = this.buildBaseTransaction(ccc.Transaction.default(), context);
264
257
  if (candidate.depositCount > 0) {
265
258
  partial = this.ickbLogic.deposit(
266
259
  partial,
@@ -285,30 +278,28 @@ const IckbSdkImplementation = class IckbSdk {
285
278
  ok: true,
286
279
  tx,
287
280
  estimatedMaturity: plan.estimatedMaturity,
288
- conversion: {
289
- kind: conversionKind(plan.depositCount > 0, plan.order !== undefined),
290
- },
281
+ conversionKind: conversionKind(plan.depositCount > 0, plan.order !== undefined),
291
282
  isSweepComplete: sweepsAll(tx, context.cells),
292
283
  };
293
284
  }
294
285
 
295
286
  private async buildIckbToCkbConversion(
296
- baseTx: ccc.Transaction,
297
287
  options: ResolvedConversionOptions,
298
288
  ): Promise<ConversionTransactionResult> {
299
289
  const { context, lock } = options;
300
290
  const plans = ickbToCkbConversionPlans(options, context.system.poolDeposits);
301
291
  if (plans.length === 0) {
302
- return conversionFailure(
303
- "amount-too-small",
304
- context.estimatedMaturity,
305
- minimumOrderAmount(false, context.system),
306
- );
292
+ return {
293
+ ok: false,
294
+ reason: "amount-too-small",
295
+ estimatedMaturity: context.estimatedMaturity,
296
+ minimum: minimumOrderAmount(false, context.system),
297
+ };
307
298
  }
308
299
  const { candidate: plan, tx } = await completeFirstFundable(
309
300
  plans,
310
301
  (candidate) => {
311
- let partial = this.buildBaseTransaction(baseTx.clone(), context, {
302
+ let partial = this.buildBaseTransaction(ccc.Transaction.default(), context, {
312
303
  deposits: candidate.selectedDeposits,
313
304
  lock,
314
305
  });
@@ -333,9 +324,10 @@ const IckbSdkImplementation = class IckbSdk {
333
324
  ok: true,
334
325
  tx,
335
326
  estimatedMaturity: plan.estimatedMaturity,
336
- conversion: {
337
- kind: conversionKind(plan.selectedDeposits.length > 0, plan.order !== undefined),
338
- },
327
+ conversionKind: conversionKind(
328
+ plan.selectedDeposits.length > 0,
329
+ plan.order !== undefined,
330
+ ),
339
331
  ...(notice === undefined ? {} : { conversionNotice: notice }),
340
332
  isSweepComplete: sweepsAll(tx, context.cells),
341
333
  ...(broadcastBefore === undefined ? {} : { broadcastBefore }),
@@ -351,6 +343,11 @@ const IckbSdkImplementation = class IckbSdk {
351
343
  lock: options.lock,
352
344
  feeRate: options.context.system.feeRate,
353
345
  cells: options.context.cells,
346
+ protocolCells: [
347
+ ...options.context.receipts,
348
+ ...options.context.system.poolDeposits,
349
+ ],
350
+ tip: options.context.system.tip,
354
351
  });
355
352
  }
356
353
 
@@ -405,7 +402,8 @@ const IckbSdkImplementation = class IckbSdk {
405
402
  options: CompleteIckbTransactionOptions,
406
403
  ): Promise<ccc.Transaction> {
407
404
  const { signer, feeRate } = options;
408
- const tx = this.ickbUdt.addCellDeps(ccc.Transaction.from(txLike).clone());
405
+ const tx = ccc.Transaction.from(txLike).clone();
406
+ tx.addCellDeps(this.ickbUdt.cellDeps);
409
407
  const changeLock = options.lock ?? (await signer.getRecommendedAddressObj()).script;
410
408
  // Inputs come from one state read, each cell once, and each action spends its own
411
409
  // cell kind, so a repeated out point is a builder bug: this is the one place that
@@ -428,9 +426,18 @@ const IckbSdkImplementation = class IckbSdk {
428
426
  compareBigInt(right.cellOutput.capacity, left.cellOutput.capacity),
429
427
  );
430
428
 
431
- await this.completeIckb(tx, signer.client, changeLock, ickbCells);
429
+ await this.completeIckb(
430
+ tx,
431
+ signer.client,
432
+ changeLock,
433
+ ickbCells,
434
+ options.protocolCells,
435
+ );
432
436
  // Plain CKB: the sweep first, then whatever the fee still needs beyond the limit.
433
437
  const swept = sweep(tx, plainCells);
438
+ // Last of the header deps, so no index another step names shifts, and before the fee so
439
+ // its 32 bytes are paid for.
440
+ pushHeaderDep(tx, options.tip.hash);
434
441
  await completeFee(tx, signer, changeLock, feeRate, plainCells.slice(swept));
435
442
  assertDaoOutputLimit(tx, this.ickbLogic.dao.script);
436
443
  return tx;
@@ -442,9 +449,10 @@ const IckbSdkImplementation = class IckbSdk {
442
449
  client: ccc.Client,
443
450
  changeLock: ccc.Script,
444
451
  ickbCells: readonly ccc.Cell[],
452
+ protocolCells: readonly ProtocolCell[],
445
453
  ): Promise<void> {
446
- const required = this.ickbUdt.outputBalance(tx);
447
- let balance = await this.ickbUdt.inputBalance(tx, client);
454
+ const required = tx.getOutputsUdtBalance(this.ickbUdt.script);
455
+ let balance = await this.ickbUdt.inputBalance(tx, client, protocolCells);
448
456
  for (const cell of ickbCells) {
449
457
  if (balance >= required && tx.inputs.length >= SWEEP_INPUT_LIMIT) {
450
458
  break;
@@ -459,17 +467,10 @@ const IckbSdkImplementation = class IckbSdk {
459
467
  }
460
468
  this.ickbUdt.addChange(tx, changeLock, balance - required);
461
469
  }
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
+ }
470
471
 
471
472
  /** Whether the transaction already carries any input or output. */
472
- export function hasTransactionActivity(tx: ccc.Transaction): boolean {
473
+ function hasTransactionActivity(tx: ccc.Transaction): boolean {
473
474
  return tx.inputs.length > 0 || tx.outputs.length > 0;
474
475
  }
475
476
 
@@ -484,16 +485,10 @@ function isPastPar(group: OrderGroup, exchangeRatio: Ratio): boolean {
484
485
  }
485
486
 
486
487
  function conversionFailure(
487
- reason: ConversionTransactionFailureReason,
488
+ reason: Exclude<ConversionTransactionFailureReason, "amount-too-small">,
488
489
  estimatedMaturity: bigint,
489
- minimum?: bigint,
490
490
  ): ConversionTransactionResult {
491
- return {
492
- ok: false,
493
- reason,
494
- estimatedMaturity,
495
- ...(minimum === undefined ? {} : { minimum }),
496
- };
491
+ return { ok: false, reason, estimatedMaturity };
497
492
  }
498
493
 
499
494
  /** A plan always carries a deposit or an order, else it would be a collection. */
@@ -517,14 +512,6 @@ async function isOwnLock(signer: ccc.Signer, lock: ccc.Script): Promise<boolean>
517
512
  return (await signer.getAddressObjs()).some(({ script }) => script.eq(lock));
518
513
  }
519
514
 
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
- }
527
-
528
515
  function udtBalance(cell: ccc.Cell): ccc.Num {
529
516
  return ccc.udtBalanceFrom(cell.outputData);
530
517
  }
@@ -569,7 +556,7 @@ async function completeFee(
569
556
  }
570
557
  }
571
558
 
572
- /** Stable machine-readable failures owned by the Phase-2 SDK. */
559
+ /** Stable machine-readable SDK failures. */
573
560
  export type IckbErrorCode = "insufficient_capacity" | "insufficient_ickb";
574
561
 
575
562
  /** Typed SDK failure with a stable machine-readable code. */