@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
@@ -1,6 +1,7 @@
1
1
  import { ccc } from "@ckb-ccc/core";
2
- import { cccA } from "@ckb-ccc/core/advanced";
3
- import { unique } from "../utils/utils.ts";
2
+ import { assertFresh, type BroadcastDeadline } from "../dao.ts";
3
+ import { jsonRpcRequestor, unique } from "../utils/utils.ts";
4
+ import { verifiedClient } from "../utils/verified_client.ts";
4
5
 
5
6
  /** Error from a broadcast whose outcome is unknown; the local transaction hash is retained. */
6
7
  export class TransactionBroadcastError extends Error {
@@ -32,49 +33,59 @@ export class TransactionExpiredError extends Error {
32
33
  }
33
34
 
34
35
  /**
35
- * Signs locally, records chain identity before RPC, then broadcasts without
36
- * CCC's cache wrapper.
36
+ * Verifies the inputs against the chain, signs locally, records chain identity before
37
+ * RPC, then broadcasts without CCC's cache wrapper.
37
38
  *
38
- * @remarks The connector must not alter the economic body it was handed: the
39
- * ordered inputs, outputs and outputs data are compared against a digest taken
40
- * before signing, so in-place mutation cannot evade the check. Witnesses and
41
- * cell/header dependency preparation stay signer-owned. The fee ceiling values
42
- * inputs from the pre-sign transaction rather than connector-supplied metadata,
43
- * while measuring the signed bytes. When `broadcastBefore` is given, one fresh tip
44
- * is read after the signature and the fee check, and the send is refused with
45
- * `TransactionExpiredError` once the tip's epoch has reached it: a withdrawal
46
- * request that commits after its claim locks the deposit for another cycle, and a
47
- * wallet may hold the signature for a while.
48
- * `recordTxHash` is called after these checks and before the send RPC starts. A
49
- * node that already holds this exact transaction is an acceptance; any other send
50
- * failure remains ambiguous and throws `TransactionBroadcastError`. The client
51
- * cache is never marked: later attempts rebuild from exact committed reads.
39
+ * @remarks Nothing is signed unless every cell attached to an input equals the output
40
+ * its creating transaction made, a bare `OutPoint` being valued from that output, and the
41
+ * signer kept the ordered inputs, outputs and outputs data.
42
+ * Nothing is sent when the chain would take more fee than `plannedFee`, the fee completion
43
+ * priced, `tx.estimateFee(feeRate)` at its rate. That fee is measured from verified values,
44
+ * a phase-2 DAO input proven in its request block with `get_transaction_proof`, so the
45
+ * client must be JSON-RPC. When `broadcastBefore` is given, the send is refused with
46
+ * `TransactionExpiredError` once a fresh tip reaches it.
47
+ * `recordTxHash` runs before the send RPC, for a caller that must record the hash before a
48
+ * send that may never answer; every later failure carries it as `TransactionBroadcastError`. A node already holding this exact transaction
49
+ * is an acceptance; any other send failure throws `TransactionBroadcastError`. Why each
50
+ * check exists: SDK README, Signing and broadcast.
52
51
  */
53
52
  export async function signAndSendTransaction(
54
53
  signer: ccc.Signer,
55
54
  tx: ccc.TransactionLike,
55
+ plannedFee: ccc.Num,
56
56
  recordTxHash?: (txHash: ccc.Hex) => void,
57
- broadcastBefore?: ccc.Epoch,
57
+ broadcastBefore?: BroadcastDeadline,
58
58
  ): Promise<ccc.Hex> {
59
59
  // Transaction.from reuses Transaction instances, which a signer may mutate in place.
60
60
  const requested = ccc.Transaction.from(tx).clone();
61
61
  const requestedBody = economicBody(requested);
62
+ // The checks read through a verified client, whatever client the signer holds.
63
+ const client = verifiedClient(signer.client);
64
+ const inputs = await verifiedInputs(client, requested.inputs);
62
65
  const signed = await signer.signTransaction(tx);
63
66
  if (economicBody(signed) !== requestedBody) {
64
67
  throw new Error("Signer altered the transaction inputs, outputs or outputs data");
65
68
  }
66
69
  const txHash = signed.hash();
67
- // CCC trusts input cell metadata already attached to the connector-controlled result.
68
- const feeTransaction = signed.clone();
69
- feeTransaction.inputs = requested.inputs;
70
- const feeRate = await feeTransaction.getFeeRate(signer.client);
71
- if (feeRate > cccA.DEFAULT_MAX_FEE_RATE) {
72
- throw new ccc.ErrorClientMaxFeeRateExceeded(cccA.DEFAULT_MAX_FEE_RATE, feeRate);
70
+ const fee = await verifiedFee(client, signed, inputs);
71
+ // A plain JavaScript caller of the earlier signature passes undefined or its recordTxHash
72
+ // here: numFrom throws on undefined, and on a function either throws or reads 0n. A bare
73
+ // comparison of a bigint with either is false and would skip the check.
74
+ const planned = ccc.numFrom(plannedFee);
75
+ if (fee > planned) {
76
+ throw new Error(
77
+ `Transaction ${txHash} pays ${String(fee)} shannons of fee, more than the ${String(planned)} planned`,
78
+ );
73
79
  }
74
80
  if (broadcastBefore !== undefined) {
75
- const { epoch } = await signer.client.getTipHeader();
76
- if (broadcastBefore.compare(epoch) <= 0) {
77
- throw new TransactionExpiredError({ broadcastBefore, tip: epoch });
81
+ const tip = await client.getTipHeader();
82
+ assertFresh(tip, broadcastBefore.tipMaxAgeMs);
83
+ const { epoch } = tip;
84
+ if (broadcastBefore.epoch.compare(epoch) <= 0) {
85
+ throw new TransactionExpiredError({
86
+ broadcastBefore: broadcastBefore.epoch,
87
+ tip: epoch,
88
+ });
78
89
  }
79
90
  }
80
91
  recordTxHash?.(txHash);
@@ -96,6 +107,131 @@ export async function signAndSendTransaction(
96
107
  return txHash;
97
108
  }
98
109
 
110
+ // The default lock signs each input as its OutPoint only, never the cell behind it, so
111
+ // the capacity, lock, type and data attached to an input are a node's claim: a node
112
+ // understating a plain cell's capacity or a UDT amount would turn the difference into
113
+ // fee or a burn on a transaction that passes every other check. The verified client reads
114
+ // the cell from its creating transaction, whose hash certifies its body.
115
+ async function verifiedInputs(
116
+ client: ccc.Client,
117
+ inputs: readonly ccc.CellInput[],
118
+ ): Promise<ccc.Cell[]> {
119
+ return Promise.all(
120
+ inputs.map(async (input) => {
121
+ const outPoint = input.previousOutput.toHex();
122
+ const cell = await client.getCell(input.previousOutput);
123
+ if (cell === undefined) {
124
+ throw new Error(`Input ${outPoint} is not an output of a known transaction`);
125
+ }
126
+ if (
127
+ (input.cellOutput !== undefined && !cell.cellOutput.eq(input.cellOutput)) ||
128
+ (input.outputData !== undefined && input.outputData !== cell.outputData)
129
+ ) {
130
+ throw new Error(
131
+ `Input ${outPoint} differs from the cell its transaction created`,
132
+ );
133
+ }
134
+ return cell;
135
+ }),
136
+ );
137
+ }
138
+
139
+ // The fee the chain will take: the verified inputs' capacity, plus the interest a phase-2
140
+ // DAO input collects, less the outputs. Completion priced that interest through the
141
+ // signer's client at whatever header the node named, so a lower rate there shortened the
142
+ // change; the miner would keep the difference under any fee-rate ceiling. A node names
143
+ // which block created a cell; the chain values it at headers the transaction names in its
144
+ // deps, the deposit header at the number the cell's data records and the request header
145
+ // at the block that committed the request, which the node must prove
146
+ // (assertCommittedIn), once per request transaction. The deposit header is then the one
147
+ // the cell's data numbers and the request header the real request block, so dao.c's own
148
+ // rules settle the rest on chain.
149
+ async function verifiedFee(
150
+ client: ccc.Client,
151
+ signed: ccc.Transaction,
152
+ inputs: readonly ccc.Cell[],
153
+ ): Promise<ccc.Num> {
154
+ const deps = new Set(signed.headerDeps);
155
+ const proven = new Set<string>();
156
+ let capacity = ccc.Zero;
157
+ for (const cell of inputs) {
158
+ capacity += cell.cellOutput.capacity;
159
+ const { depositHeader, withdrawHeader } = await cell.getNervosDaoInfo(client);
160
+ if (withdrawHeader === undefined) {
161
+ continue;
162
+ }
163
+ if (!deps.has(depositHeader.hash) || !deps.has(withdrawHeader.hash)) {
164
+ throw new Error(
165
+ `Input ${cell.outPoint.toHex()} is valued at a header the transaction does not name`,
166
+ );
167
+ }
168
+ const { txHash } = cell.outPoint;
169
+ if (!proven.has(`${txHash}${withdrawHeader.hash}`)) {
170
+ await assertCommittedIn(client, txHash, withdrawHeader);
171
+ proven.add(`${txHash}${withdrawHeader.hash}`);
172
+ }
173
+ capacity += ccc.calcDaoProfit(cell.capacityFree, depositHeader, withdrawHeader);
174
+ }
175
+ return capacity - signed.getOutputsCapacity();
176
+ }
177
+
178
+ /**
179
+ * Refuses unless the node proves `txHash` is among the transactions of `header`'s block.
180
+ * An outpoint names its transaction, not the block that committed it, so a phase-2 input
181
+ * valued at the request header the node names could otherwise be valued at another
182
+ * request block of the same transaction. The proof is a Merkle path from the transaction
183
+ * hash to the header's transactions root, which the header's own hash binds.
184
+ */
185
+ async function assertCommittedIn(
186
+ client: ccc.Client,
187
+ txHash: ccc.Hex,
188
+ header: ccc.ClientBlockHeader,
189
+ ): Promise<void> {
190
+ const requestor = jsonRpcRequestor(client);
191
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, no-restricted-syntax -- the node's answer, checked field by field in isCommittedIn.
192
+ const proof = (await requestor.request("get_transaction_proof", [
193
+ [txHash],
194
+ header.hash,
195
+ ])) as TransactionProof;
196
+ if (!isCommittedIn(txHash, header, proof)) {
197
+ throw new Error(`Transaction ${txHash} is not proven in block ${header.hash}`);
198
+ }
199
+ }
200
+
201
+ /** The node's `get_transaction_proof` answer for one transaction, the fields checked. */
202
+ export interface TransactionProof {
203
+ witnesses_root: ccc.Hex;
204
+ proof: { indices: readonly ccc.Hex[]; lemmas: readonly ccc.Hex[] };
205
+ }
206
+
207
+ /**
208
+ * Whether `proof` is a path from `txHash` to `header`'s transactions root. CKB builds that
209
+ * root over two subtrees, `blake2b(raw transactions root || witnesses root)`, each a
210
+ * complete binary Merkle tree whose node at position `i` has its parent at `(i - 1) / 2`
211
+ * and is a left child when `i` is odd; the index is the leaf's position and the lemmas are
212
+ * its siblings bottom up.
213
+ */
214
+ export function isCommittedIn(
215
+ txHash: ccc.Hex,
216
+ header: ccc.ClientBlockHeader,
217
+ { witnesses_root: witnessesRoot, proof }: TransactionProof,
218
+ ): boolean {
219
+ const [index, ...extra] = proof.indices;
220
+ if (index === undefined || extra.length > 0) {
221
+ return false;
222
+ }
223
+ let node = txHash;
224
+ let position = Number(index);
225
+ for (const lemma of proof.lemmas) {
226
+ if (position === 0) {
227
+ return false;
228
+ }
229
+ node = position % 2 === 1 ? ccc.hashCkb(node, lemma) : ccc.hashCkb(lemma, node);
230
+ position = (position - 1) >> 1;
231
+ }
232
+ return position === 0 && ccc.hashCkb(node, witnessesRoot) === header.transactionsRoot;
233
+ }
234
+
99
235
  // Hashing a body-only transaction reuses CCC serialization to freeze the ordered
100
236
  // inputs, outputs and outputs data, while ignoring signer-owned witnesses and
101
237
  // the cell/header deps a connector may still have to prepare.
@@ -1,5 +1,5 @@
1
1
  import { ccc } from "@ckb-ccc/core";
2
- import { jsonRpcRequestor, rawTransactionStatus } from "../utils/utils.ts";
2
+ import { jsonRpcRequestor } from "../utils/utils.ts";
3
3
 
4
4
  const MAX_TIMEOUT_MS = 2_147_483_647;
5
5
  const DEFAULT_TIMEOUT_MS = 60_000;
@@ -40,15 +40,16 @@ export interface WaitTransactionOptions {
40
40
  }
41
41
 
42
42
  /**
43
- * Observes one already-broadcast transaction for a single bounded window and returns
44
- * it once the node reports it committed.
43
+ * Observes one already-broadcast transaction for a single bounded window and resolves
44
+ * once the node reports it committed.
45
45
  *
46
46
  * @remarks Stack waits at depth zero. Actors rebuild next turn; the app can reopen a
47
47
  * window on the same hash without signing again. Callers needing deeper confirmation
48
48
  * must observe it separately.
49
- * Clients with a JSON-RPC requestor, the connector's composed client included, poll
50
- * `get_transaction` verbosity 1 directly because CCC's typed response discards a null
51
- * transaction body, including its rejection status. Body reads separately bypass the cache.
49
+ * The status is polled with raw `get_transaction` verbosity 1, since CCC's typed response
50
+ * discards a null transaction body and its rejection status; the client must be JSON-RPC,
51
+ * the connector's composed client included. The body is never read: no caller uses it,
52
+ * and the same node's word on it would verify nothing.
52
53
  * Timeout and abort also apply while awaiting client
53
54
  * operations, but the in-flight client operation receives no abort signal and may
54
55
  * finish after this function rejects. Nothing here mutates the client cache: later attempts rebuild
@@ -58,7 +59,7 @@ export async function waitTransaction(
58
59
  client: ccc.Client,
59
60
  txHashLike: ccc.HexLike,
60
61
  options: WaitTransactionOptions = {},
61
- ): Promise<ccc.ClientTransactionResponse> {
62
+ ): Promise<void> {
62
63
  const {
63
64
  timeout = DEFAULT_TIMEOUT_MS,
64
65
  interval = DEFAULT_INTERVAL_MS,
@@ -71,15 +72,13 @@ export async function waitTransaction(
71
72
  signal?.throwIfAborted();
72
73
 
73
74
  const budget = openWaitWindow(timeout, signal);
74
- const poll = { client, txHash, budget };
75
75
  try {
76
76
  for (;;) {
77
- const committed = await readCommittedTransaction(poll);
78
- if (committed !== undefined) {
77
+ if (await isCommitted(client, txHash, budget)) {
79
78
  // An abort synchronized into the gap between the last read and this
80
79
  // return must not resolve the wait successfully.
81
80
  budget.throwIfStopped();
82
- return committed;
81
+ return;
83
82
  }
84
83
  if (Date.now() + interval >= budget.deadline) {
85
84
  throw budget.timeoutError;
@@ -99,18 +98,6 @@ interface WaitWindow {
99
98
  close: () => void;
100
99
  }
101
100
 
102
- interface WaitPoll {
103
- client: ccc.Client;
104
- txHash: ccc.Hex;
105
- budget: WaitWindow;
106
- }
107
-
108
- interface TransactionStatus {
109
- status: string | undefined;
110
- reason: string | undefined;
111
- response?: ccc.ClientTransactionResponse;
112
- }
113
-
114
101
  function validateWaitOptions(timeout: number, interval: number): void {
115
102
  assertCount(interval, "interval");
116
103
  assertCount(timeout, "timeout");
@@ -198,74 +185,43 @@ async function pollingSleep(interval: number, budget: WaitWindow): Promise<void>
198
185
  }
199
186
  }
200
187
 
201
- async function readCommittedTransaction(
202
- poll: WaitPoll,
203
- ): Promise<ccc.ClientTransactionResponse | undefined> {
204
- const status = await readTransactionStatus(poll);
205
- assertNotRejected(poll.txHash, status);
206
- if (status.status !== "committed") {
207
- return undefined;
208
- }
209
-
210
- const response = status.response ?? (await readTransactionBody(poll));
211
- if (response === undefined) {
212
- return undefined;
213
- }
214
- assertNotRejected(poll.txHash, {
215
- status: response.status,
216
- reason: response.reason,
217
- });
218
- // Only a body that itself reports commitment counts: an uncached read may lag
219
- // or disagree with the status poll, and any other status is not commitment.
220
- return response.status === "committed" && response.blockNumber !== undefined
221
- ? response
222
- : undefined;
223
- }
224
-
225
- async function readTransactionStatus(poll: WaitPoll): Promise<TransactionStatus> {
226
- const { client, txHash, budget } = poll;
188
+ async function isCommitted(
189
+ client: ccc.Client,
190
+ txHash: ccc.Hex,
191
+ budget: WaitWindow,
192
+ ): Promise<boolean> {
227
193
  const requestor = jsonRpcRequestor(client);
228
- if (requestor !== undefined) {
229
- // Raw verbosity-1 polling keeps a status-only rejection that CCC's typed
230
- // transaction response discards (its body is null, so CCC returns nothing).
231
- return rawTransactionStatus(
232
- await within(
233
- async () => requestor.request("get_transaction", [txHash, "0x1"]),
234
- budget,
235
- ),
236
- );
237
- }
238
-
239
- const response = await readTransactionBody(poll);
240
- return response === undefined
241
- ? { status: undefined, reason: undefined }
242
- : { status: response.status, reason: response.reason, response };
243
- }
244
-
245
- async function readTransactionBody(
246
- poll: WaitPoll,
247
- ): Promise<ccc.ClientTransactionResponse | undefined> {
248
- const response = await within(
249
- async () => poll.client.getTransactionNoCache(poll.txHash),
250
- poll.budget,
194
+ // Raw verbosity-1 polling keeps a status-only rejection that CCC's typed transaction
195
+ // response discards (its body is null, so CCC returns nothing).
196
+ const { status, reason } = rawTransactionStatus(
197
+ await within(
198
+ async () => requestor.request("get_transaction", [txHash, "0x1"]),
199
+ budget,
200
+ ),
251
201
  );
252
- if (response === undefined) {
253
- return undefined;
254
- }
255
- try {
256
- // A generic client may return an unvalidated shape, and a body that cannot
257
- // be normalized is not evidence of commitment: keep the window polling.
258
- return ccc.ClientTransactionResponse.from(response);
259
- } catch {
260
- return undefined;
202
+ if (status === "rejected") {
203
+ throw new TransactionWaitError(txHash, { status, reason });
261
204
  }
205
+ return status === "committed";
262
206
  }
263
207
 
264
- function assertNotRejected(txHash: ccc.Hex, status: TransactionStatus): void {
265
- if (status.status === "rejected") {
266
- throw new TransactionWaitError(txHash, {
267
- status: status.status,
268
- reason: status.reason,
269
- });
270
- }
208
+ /** A node's raw `get_transaction` status record (verbosity 1), the fields read here. */
209
+ function rawTransactionStatus(response: unknown): {
210
+ status: string | undefined;
211
+ reason: string | undefined;
212
+ } {
213
+ const record =
214
+ typeof response === "object" &&
215
+ response !== null &&
216
+ "tx_status" in response &&
217
+ typeof response.tx_status === "object" &&
218
+ response.tx_status !== null
219
+ ? response.tx_status
220
+ : {};
221
+ return {
222
+ status:
223
+ "status" in record && typeof record.status === "string" ? record.status : undefined,
224
+ reason:
225
+ "reason" in record && typeof record.reason === "string" ? record.reason : undefined,
226
+ };
271
227
  }
package/src/udt.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { ccc, mol } from "@ckb-ccc/core";
2
2
  import { isDaoDeposit } from "./dao.ts";
3
3
  import { CheckedUint128LE, CheckedUint32LE, CheckedUint64LE } from "./utils/codec.ts";
4
- import { transactionHeaders } from "./utils/transaction_header.ts";
5
4
  import type { ExchangeRatio } from "./utils/utils.ts";
6
5
 
7
6
  const ickbXudtTypeOccupiedSize = 69;
@@ -57,11 +56,8 @@ export class IckbUdt {
57
56
  /** The iCKB xUDT type script. */
58
57
  public readonly script: ccc.Script;
59
58
 
60
- /** Out point of the xUDT code cell used by this iCKB token. */
61
- public readonly udtCode: ccc.OutPoint;
62
-
63
- /** Out point of the iCKB Logic code cell required by this token. */
64
- public readonly logicCode: ccc.OutPoint;
59
+ /** The iCKB dep group, which also carries the xUDT and iCKB Logic code cells. */
60
+ public readonly cellDeps: ccc.CellDep[];
65
61
 
66
62
  /** Logic script whose hash is embedded in this iCKB xUDT type script. */
67
63
  public readonly logicScript: ccc.Script;
@@ -71,21 +67,18 @@ export class IckbUdt {
71
67
 
72
68
  /** Creates an instance of IckbUdt from its code and script references. */
73
69
  constructor({
74
- code,
75
70
  script,
76
- logicCode,
71
+ cellDeps,
77
72
  logicScript,
78
73
  daoScript,
79
74
  }: {
80
- code: ccc.OutPointLike;
81
75
  script: ccc.ScriptLike;
82
- logicCode: ccc.OutPointLike;
76
+ cellDeps: ccc.CellDepLike[];
83
77
  logicScript: ccc.ScriptLike;
84
78
  daoScript: ccc.ScriptLike;
85
79
  }) {
86
80
  this.script = ccc.Script.from(script);
87
- this.udtCode = ccc.OutPoint.from(code);
88
- this.logicCode = ccc.OutPoint.from(logicCode);
81
+ this.cellDeps = cellDeps.map((cellDep) => ccc.CellDep.from(cellDep));
89
82
  this.logicScript = ccc.Script.from(logicScript);
90
83
  this.daoScript = ccc.Script.from(daoScript);
91
84
  }
@@ -132,101 +125,48 @@ export class IckbUdt {
132
125
  }
133
126
 
134
127
  /**
135
- * Adds iCKB-specific cell dependencies to a transaction.
136
- *
137
- * Adds individual code deps (not dep group) for:
138
- * - xUDT code cell (this.udtCode)
139
- * - iCKB Logic code cell (this.logicCode)
140
- *
141
- * @param txLike - The transaction to add cell deps to.
142
- * @returns The transaction with cell deps added.
128
+ * iCKB carried by the transaction's inputs: xUDT balances plus receipt value, minus the
129
+ * first-phase deposits it re-mints. Receipts and deposits take the value the state read
130
+ * gave them at their deposit headers; a protocol input missing from `protocolCells` is
131
+ * a builder bug and throws, since iCKB Logic would refuse whatever it was valued at.
143
132
  */
144
- public addCellDeps(txLike: ccc.TransactionLike): ccc.Transaction {
145
- const tx = ccc.Transaction.from(txLike);
146
- addCodeDep(tx, this.udtCode);
147
- addCodeDep(tx, this.logicCode);
148
- return tx;
149
- }
150
-
151
- /**
152
- * iCKB carried by the transaction's inputs: xUDT balances plus receipt value,
153
- * minus the first-phase deposits it re-mints. Final withdrawal inputs carry none.
154
- * The protocol cells are valued at their deposit headers, each read once.
155
- */
156
- public async inputBalance(tx: ccc.Transaction, client: ccc.Client): Promise<ccc.Num> {
157
- const cells = await Promise.all(
158
- tx.inputs.map(async (input) => {
159
- try {
160
- return await input.getCell(client);
161
- } catch (error) {
162
- throw new Error(`Failed to load input cell ${input.previousOutput.toHex()}`, {
163
- cause: error,
164
- });
165
- }
166
- }),
167
- );
168
- const valued = cells.map((cell) => this.protocolValue(cell));
169
- const headers = await transactionHeaders(
170
- client,
171
- valued.flatMap((value) => (value === undefined ? [] : [value.txHash])),
133
+ public async inputBalance(
134
+ tx: ccc.Transaction,
135
+ client: ccc.Client,
136
+ protocolCells: readonly ProtocolCell[],
137
+ ): Promise<ccc.Num> {
138
+ const values = new Map(
139
+ protocolCells.map(({ cell, udtValue }) => [cell.outPoint.toHex(), udtValue]),
172
140
  );
173
141
  let balance = ccc.Zero;
174
- for (const [index, cell] of cells.entries()) {
175
- const value = valued[index];
142
+ for (const input of tx.inputs) {
143
+ const cell = await input.getCell(client);
176
144
  if (this.isUdt(cell)) {
177
- balance += decodeUdtBalance(cell.outputData);
178
- } else if (value !== undefined) {
179
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- every protocol cell's hash was read above.
180
- const header = headers.get(value.txHash)!;
181
- balance += value.sign * ickbValue(value.amount, header) * value.quantity;
145
+ balance += ccc.udtBalanceFrom(cell.outputData);
146
+ continue;
147
+ }
148
+ const sign = this.protocolSign(cell);
149
+ if (sign === 0n) {
150
+ continue;
182
151
  }
152
+ const value = values.get(cell.outPoint.toHex());
153
+ if (value === undefined) {
154
+ throw new Error(
155
+ `iCKB input ${cell.outPoint.toHex()} has no value from the state read`,
156
+ );
157
+ }
158
+ balance += sign * value;
183
159
  }
184
160
  return balance;
185
161
  }
186
162
 
187
- /** What a protocol input (receipt or first-phase deposit) is worth once its header is known. */
188
- private protocolValue(
189
- cell: ccc.CellAny,
190
- ):
191
- | { txHash: ccc.Hex; amount: ccc.FixedPoint; quantity: bigint; sign: bigint }
192
- | undefined {
193
- if (this.isUdt(cell) || cell.outPoint === undefined) {
194
- return undefined;
195
- }
163
+ /** 1 for a receipt, -1 for a first-phase iCKB deposit, 0 for any other cell. */
164
+ private protocolSign(cell: ccc.Cell): bigint {
196
165
  const { type, lock } = cell.cellOutput;
197
166
  if (type !== undefined && this.logicScript.eq(type)) {
198
- let receipt: ReturnType<typeof decodeReceiptData>;
199
- try {
200
- receipt = decodeReceiptData(cell.outputData);
201
- } catch (error) {
202
- throw new Error(
203
- `Invalid iCKB receipt payload at ${cell.outPoint.toHex()}: ${cell.outputData}`,
204
- { cause: error },
205
- );
206
- }
207
- return {
208
- txHash: cell.outPoint.txHash,
209
- amount: receipt.depositAmount,
210
- quantity: receipt.depositQuantity,
211
- sign: 1n,
212
- };
213
- }
214
- if (this.logicScript.eq(lock) && isDaoDeposit(cell, this.daoScript)) {
215
- return {
216
- txHash: cell.outPoint.txHash,
217
- amount: cell.capacityFree,
218
- quantity: 1n,
219
- sign: -1n,
220
- };
167
+ return 1n;
221
168
  }
222
- return undefined;
223
- }
224
-
225
- /** iCKB the transaction's outputs require. */
226
- public outputBalance(tx: ccc.Transaction): ccc.Num {
227
- return Array.from(tx.outputCells).reduce((required, cell) => {
228
- return this.isUdt(cell) ? required + decodeUdtBalance(cell.outputData) : required;
229
- }, ccc.Zero);
169
+ return this.logicScript.eq(lock) && isDaoDeposit(cell, this.daoScript) ? -1n : 0n;
230
170
  }
231
171
 
232
172
  /** Adds the iCKB change output when the surplus is positive; zero needs no output. */
@@ -238,17 +178,6 @@ export class IckbUdt {
238
178
  }
239
179
  }
240
180
 
241
- function decodeUdtBalance(data: ccc.BytesLike): ccc.Num {
242
- return CheckedUint128LE.decode(ccc.bytesFrom(data).slice(0, udtDataSize));
243
- }
244
-
245
- function addCodeDep(tx: ccc.Transaction, outPoint: ccc.OutPoint): void {
246
- if (tx.cellDeps.some((dep) => dep.depType === "code" && dep.outPoint.eq(outPoint))) {
247
- return;
248
- }
249
- tx.addCellDeps({ outPoint, depType: "code" });
250
- }
251
-
252
181
  /**
253
182
  * Calculates iCKB value for unoccupied CKB capacity at a deposit header.
254
183
  *
@@ -260,7 +189,7 @@ export function ickbValue(
260
189
  ckbUnoccupiedCapacity: ccc.FixedPoint,
261
190
  header: ccc.ClientBlockHeader,
262
191
  ): ccc.FixedPoint {
263
- let ickbAmount = convert(true, ckbUnoccupiedCapacity, ickbAccountingRatio(header));
192
+ let ickbAmount = udtFromCkb(ckbUnoccupiedCapacity, ickbAccountingRatio(header));
264
193
  if (ICKB_DEPOSIT_CAP < ickbAmount) {
265
194
  // Apply a 10% discount for the amount exceeding the soft iCKB cap per deposit.
266
195
  ickbAmount -= (ickbAmount - ICKB_DEPOSIT_CAP) / 10n;
@@ -269,25 +198,22 @@ export function ickbValue(
269
198
  return ickbAmount;
270
199
  }
271
200
 
272
- /**
273
- * Converts between CKB and iCKB based on an explicit ratio.
274
- *
275
- * @param isCkb2Udt - A boolean indicating the direction of conversion (CKB to iCKB or vice versa).
276
- * @param amount - The amount to convert.
277
- * @param ratio - The CKB and iCKB scales to use.
278
- * @returns The converted amount in the target unit as a `ccc.FixedPoint`.
279
- */
280
- export function convert(
281
- isCkb2Udt: boolean,
282
- amount: ccc.FixedPoint,
283
- ratio: ExchangeRatio,
284
- ): ccc.FixedPoint {
285
- if (ratio.ckbScale <= 0n || ratio.udtScale <= 0n) {
201
+ /** The iCKB worth `amount` CKB at `ratio`, rounded down. */
202
+ export function udtFromCkb(amount: ccc.FixedPoint, ratio: ExchangeRatio): ccc.FixedPoint {
203
+ assertPositive(ratio);
204
+ return (amount * ratio.ckbScale) / ratio.udtScale;
205
+ }
206
+
207
+ /** The CKB worth `amount` iCKB at `ratio`, rounded down. */
208
+ export function ckbFromUdt(amount: ccc.FixedPoint, ratio: ExchangeRatio): ccc.FixedPoint {
209
+ assertPositive(ratio);
210
+ return (amount * ratio.udtScale) / ratio.ckbScale;
211
+ }
212
+
213
+ function assertPositive({ ckbScale, udtScale }: ExchangeRatio): void {
214
+ if (ckbScale <= 0n || udtScale <= 0n) {
286
215
  throw new Error("Exchange ratio scales must be positive");
287
216
  }
288
- return isCkb2Udt
289
- ? (amount * ratio.ckbScale) / ratio.udtScale
290
- : (amount * ratio.udtScale) / ratio.ckbScale;
291
217
  }
292
218
 
293
219
  /**
@@ -315,3 +241,9 @@ export function ickbExchangeRatio(header: ccc.ClientBlockHeader): ExchangeRatio
315
241
  udtScale: header.dao.ar + depositCapacityDelta,
316
242
  };
317
243
  }
244
+
245
+ /** A receipt or iCKB deposit with the iCKB value the state read gave it. */
246
+ export interface ProtocolCell {
247
+ cell: ccc.Cell;
248
+ udtValue: ccc.Num;
249
+ }