@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.
- package/README.md +14 -11
- package/dist/constants.d.ts +0 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -24
- package/dist/constants.js.map +1 -1
- package/dist/conversion/estimate.d.ts +0 -6
- package/dist/conversion/estimate.d.ts.map +1 -1
- package/dist/conversion/estimate.js +1 -1
- package/dist/conversion/estimate.js.map +1 -1
- package/dist/conversion/maturity.d.ts +1 -1
- package/dist/conversion/maturity.js +13 -12
- package/dist/conversion/maturity.js.map +1 -1
- package/dist/conversion/plans.d.ts.map +1 -1
- package/dist/conversion/plans.js +6 -6
- package/dist/conversion/plans.js.map +1 -1
- package/dist/conversion/projection.d.ts +5 -6
- package/dist/conversion/projection.d.ts.map +1 -1
- package/dist/conversion/projection.js +11 -8
- package/dist/conversion/projection.js.map +1 -1
- package/dist/conversion/types.d.ts +29 -17
- package/dist/conversion/types.d.ts.map +1 -1
- package/dist/conversion/withdrawal_ring.d.ts +1 -3
- package/dist/conversion/withdrawal_ring.d.ts.map +1 -1
- package/dist/conversion/withdrawal_ring.js +4 -4
- package/dist/conversion/withdrawal_ring.js.map +1 -1
- package/dist/dao.d.ts +19 -5
- package/dist/dao.d.ts.map +1 -1
- package/dist/dao.js +24 -12
- package/dist/dao.js.map +1 -1
- package/dist/index.d.ts +9 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/logic.d.ts +10 -14
- package/dist/logic.d.ts.map +1 -1
- package/dist/logic.js +28 -39
- package/dist/logic.js.map +1 -1
- package/dist/order/cells.d.ts +10 -49
- package/dist/order/cells.d.ts.map +1 -1
- package/dist/order/cells.js +28 -146
- package/dist/order/cells.js.map +1 -1
- package/dist/order/conversion.d.ts +3 -9
- package/dist/order/conversion.d.ts.map +1 -1
- package/dist/order/conversion.js +8 -11
- package/dist/order/conversion.js.map +1 -1
- package/dist/order/fill.d.ts +2 -2
- package/dist/order/fill.js +3 -3
- package/dist/order/fill.js.map +1 -1
- package/dist/order/info.d.ts.map +1 -1
- package/dist/order/info.js +3 -0
- package/dist/order/info.js.map +1 -1
- package/dist/order/matcher.d.ts +1 -1
- package/dist/order/matcher.d.ts.map +1 -1
- package/dist/order/matcher.js +3 -2
- package/dist/order/matcher.js.map +1 -1
- package/dist/order/order.d.ts +6 -2
- package/dist/order/order.d.ts.map +1 -1
- package/dist/order/order.js +32 -37
- package/dist/order/order.js.map +1 -1
- package/dist/order/order_data.d.ts.map +1 -1
- package/dist/order/order_data.js +3 -0
- package/dist/order/order_data.js.map +1 -1
- package/dist/order/ratio.d.ts +0 -2
- package/dist/order/ratio.d.ts.map +1 -1
- package/dist/order/ratio.js +3 -11
- package/dist/order/ratio.js.map +1 -1
- package/dist/order/relative.d.ts.map +1 -1
- package/dist/order/relative.js +3 -0
- package/dist/order/relative.js.map +1 -1
- package/dist/owned_owner.d.ts +10 -15
- package/dist/owned_owner.d.ts.map +1 -1
- package/dist/owned_owner.js +27 -47
- package/dist/owned_owner.js.map +1 -1
- package/dist/sdk.d.ts +74 -31
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +81 -59
- package/dist/sdk.js.map +1 -1
- package/dist/send/sign_and_send_transaction.d.ts +32 -17
- package/dist/send/sign_and_send_transaction.d.ts.map +1 -1
- package/dist/send/sign_and_send_transaction.js +128 -28
- package/dist/send/sign_and_send_transaction.js.map +1 -1
- package/dist/send/wait_transaction.d.ts +7 -6
- package/dist/send/wait_transaction.d.ts.map +1 -1
- package/dist/send/wait_transaction.js +29 -61
- package/dist/send/wait_transaction.js.map +1 -1
- package/dist/udt.d.ts +20 -35
- package/dist/udt.d.ts.map +1 -1
- package/dist/udt.js +39 -103
- package/dist/udt.js.map +1 -1
- package/dist/utils/chain.d.ts +0 -35
- package/dist/utils/chain.d.ts.map +1 -1
- package/dist/utils/chain.js +1 -19
- package/dist/utils/chain.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +3 -0
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/utils.d.ts +19 -46
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +53 -74
- package/dist/utils/utils.js.map +1 -1
- package/dist/utils/verified_client.d.ts +19 -0
- package/dist/utils/verified_client.d.ts.map +1 -0
- package/dist/utils/verified_client.js +92 -0
- package/dist/utils/verified_client.js.map +1 -0
- package/package.json +2 -2
- package/src/constants.ts +6 -25
- package/src/conversion/estimate.ts +1 -3
- package/src/conversion/maturity.ts +13 -14
- package/src/conversion/plans.ts +6 -6
- package/src/conversion/projection.ts +15 -7
- package/src/conversion/types.ts +32 -17
- package/src/conversion/withdrawal_ring.ts +4 -8
- package/src/dao.ts +38 -14
- package/src/index.ts +8 -8
- package/src/logic.ts +40 -52
- package/src/order/cells.ts +37 -174
- package/src/order/conversion.ts +8 -19
- package/src/order/fill.ts +3 -3
- package/src/order/info.ts +3 -0
- package/src/order/matcher.ts +3 -3
- package/src/order/order.ts +38 -52
- package/src/order/order_data.ts +3 -0
- package/src/order/ratio.ts +3 -20
- package/src/order/relative.ts +3 -0
- package/src/owned_owner.ts +47 -77
- package/src/sdk.ts +95 -108
- package/src/send/sign_and_send_transaction.ts +164 -28
- package/src/send/wait_transaction.ts +44 -88
- package/src/udt.ts +56 -124
- package/src/utils/chain.ts +0 -41
- package/src/utils/codec.ts +4 -0
- package/src/utils/utils.ts +76 -105
- package/src/utils/verified_client.ts +136 -0
- package/dist/utils/transaction_header.d.ts +0 -17
- package/dist/utils/transaction_header.d.ts.map +0 -1
- package/dist/utils/transaction_header.js +0 -44
- package/dist/utils/transaction_header.js.map +0 -1
- package/src/utils/transaction_header.ts +0 -66
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ccc } from "@ckb-ccc/core";
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `TransactionExpiredError` once
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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?:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
44
|
-
*
|
|
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
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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<
|
|
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
|
-
|
|
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
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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 (
|
|
253
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
/**
|
|
61
|
-
public readonly
|
|
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
|
-
|
|
71
|
+
cellDeps,
|
|
77
72
|
logicScript,
|
|
78
73
|
daoScript,
|
|
79
74
|
}: {
|
|
80
|
-
code: ccc.OutPointLike;
|
|
81
75
|
script: ccc.ScriptLike;
|
|
82
|
-
|
|
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.
|
|
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
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
175
|
-
const
|
|
142
|
+
for (const input of tx.inputs) {
|
|
143
|
+
const cell = await input.getCell(client);
|
|
176
144
|
if (this.isUdt(cell)) {
|
|
177
|
-
balance +=
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
/**
|
|
188
|
-
private
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
):
|
|
285
|
-
if (
|
|
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
|
+
}
|