@ckb-ccc/core 2.0.0-next.0 → 2.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +133 -1
  2. package/README.md +3 -3
  3. package/dist/advanced.d.mts +2 -2
  4. package/dist/advanced.mjs +1 -1
  5. package/{dist.commonjs/advancedBarrel-DNdhwiU0.d.ts → dist/advancedBarrel-DfFMFhdl.d.mts} +2790 -2237
  6. package/dist/advancedBarrel-DfFMFhdl.d.mts.map +1 -0
  7. package/dist/advancedBarrel.d.mts +2 -2
  8. package/dist/advancedBarrel.mjs +1 -1
  9. package/dist/advancedBarrel.mjs.map +1 -1
  10. package/dist/barrel-ONYEMVg5.mjs +7 -0
  11. package/dist/barrel-ONYEMVg5.mjs.map +1 -0
  12. package/dist/barrel.d.mts +2 -2
  13. package/dist/barrel.mjs +1 -1
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.mjs +1 -1
  16. package/dist/transformers-CduZ1Js5.mjs +2 -0
  17. package/dist/transformers-CduZ1Js5.mjs.map +1 -0
  18. package/dist.commonjs/advanced.d.ts +2 -2
  19. package/dist.commonjs/advanced.js +1 -1
  20. package/{dist/advancedBarrel-BHwnMJqR.d.mts → dist.commonjs/advancedBarrel-Dl0mM9JO.d.ts} +2791 -2236
  21. package/dist.commonjs/advancedBarrel-Dl0mM9JO.d.ts.map +1 -0
  22. package/dist.commonjs/advancedBarrel.d.ts +2 -2
  23. package/dist.commonjs/advancedBarrel.js +1 -1
  24. package/dist.commonjs/advancedBarrel.js.map +1 -1
  25. package/dist.commonjs/barrel-HfgYMxP_.js +23 -0
  26. package/dist.commonjs/barrel-HfgYMxP_.js.map +1 -0
  27. package/dist.commonjs/barrel.d.ts +2 -2
  28. package/dist.commonjs/barrel.js +1 -1
  29. package/dist.commonjs/index.d.ts +2 -2
  30. package/dist.commonjs/index.js +1 -1
  31. package/dist.commonjs/{rolldown-runtime-DWNI8pZO.js → rolldown-runtime-Chgba0Kb.js} +1 -1
  32. package/dist.commonjs/transformers-BUlLwiyt.js +2 -0
  33. package/dist.commonjs/transformers-BUlLwiyt.js.map +1 -0
  34. package/package.json +8 -7
  35. package/src/advancedBarrel.ts +1 -0
  36. package/src/bytes/advanced.ts +1 -6
  37. package/src/bytes/index.ts +37 -10
  38. package/src/ckb/transaction.ts +7 -5
  39. package/src/client/client.ts +83 -21
  40. package/src/client/clientPublicMainnet.advanced.ts +9 -0
  41. package/src/client/clientPublicMainnet.ts +75 -36
  42. package/src/client/clientPublicTestnet.advanced.ts +9 -0
  43. package/src/client/clientPublicTestnet.ts +75 -36
  44. package/src/client/jsonRpc/client.ts +77 -30
  45. package/src/client/jsonRpc/transformers.ts +4 -4
  46. package/src/jsonRpc/index.ts +1 -0
  47. package/src/jsonRpc/requestor.ts +106 -29
  48. package/src/jsonRpc/transports/advanced.ts +0 -4
  49. package/src/jsonRpc/transports/factory.ts +12 -5
  50. package/src/jsonRpc/transports/fallback.ts +14 -6
  51. package/src/jsonRpc/transports/http.ts +19 -12
  52. package/src/jsonRpc/transports/index.ts +4 -0
  53. package/src/jsonRpc/transports/transport.ts +42 -6
  54. package/src/jsonRpc/transports/webSocket.ts +145 -40
  55. package/src/keystore/index.ts +3 -3
  56. package/src/molecule/codec.ts +20 -17
  57. package/src/signer/advanced.ts +1 -0
  58. package/src/signer/btc/psbt.ts +4 -2
  59. package/src/signer/btc/verify.ts +2 -2
  60. package/src/signer/ckb/secp256k1Signing.ts +3 -3
  61. package/src/signer/doge/signerDoge.ts +1 -1
  62. package/src/signer/doge/signerDogePrivateKey.ts +1 -1
  63. package/src/signer/index.ts +1 -0
  64. package/src/signer/jsonRpc/handler.ts +478 -0
  65. package/src/signer/jsonRpc/index.ts +2 -0
  66. package/src/signer/jsonRpc/network.ts +15 -0
  67. package/src/signer/jsonRpc/signer.ts +378 -0
  68. package/src/signer/jsonRpc/transformers.ts +71 -0
  69. package/src/signer/signer/index.ts +5 -13
  70. package/src/utils/abortSignal.ts +56 -0
  71. package/src/utils/constructor.ts +11 -0
  72. package/src/utils/index.ts +172 -5
  73. package/src/utils/owner/aggregated.ts +56 -0
  74. package/src/utils/owner/index.ts +4 -0
  75. package/src/utils/owner/owner.ts +141 -0
  76. package/src/utils/owner/refCount.ts +78 -0
  77. package/src/utils/owner/unique.ts +30 -0
  78. package/src/utils/proxy.ts +165 -0
  79. package/tsdown.config.mts +1 -0
  80. package/dist/address.advanced-LxntWbev.mjs +0 -2
  81. package/dist/address.advanced-LxntWbev.mjs.map +0 -1
  82. package/dist/advancedBarrel-BHwnMJqR.d.mts.map +0 -1
  83. package/dist/barrel-C-BaV9Pe.mjs +0 -7
  84. package/dist/barrel-C-BaV9Pe.mjs.map +0 -1
  85. package/dist.commonjs/address.advanced-BqcYOpZn.js +0 -2
  86. package/dist.commonjs/address.advanced-BqcYOpZn.js.map +0 -1
  87. package/dist.commonjs/advancedBarrel-DNdhwiU0.d.ts.map +0 -1
  88. package/dist.commonjs/barrel-BBOmj4RJ.js +0 -21
  89. package/dist.commonjs/barrel-BBOmj4RJ.js.map +0 -1
@@ -0,0 +1,478 @@
1
+ import { randomBytes } from "@noble/hashes/utils.js";
2
+ import type { Transaction } from "../../ckb/index.js";
3
+ import { JsonRpcTransformers } from "../../client/jsonRpc/advanced.js";
4
+ import { hexFrom } from "../../hex/index.js";
5
+ import { JsonRpcError, type JsonRpcPayload } from "../../jsonRpc/index.js";
6
+ import { abortSignalAny, sleep } from "../../utils/index.js";
7
+ import { OwnerUnique } from "../../utils/owner/unique.js";
8
+ import type { Signer } from "../signer/index.js";
9
+ import { signerJsonRpcNetworkIdFromAddressPrefix } from "./network.js";
10
+ import {
11
+ SignerJsonRpcTransformers,
12
+ type SignerJsonRpcInfo,
13
+ type SignerJsonRpcInfoPayload,
14
+ type SignerJsonRpcMessageToSign,
15
+ } from "./transformers.js";
16
+
17
+ export type SignerJsonRpcConfirmation =
18
+ | { method: "connect"; networkId: string }
19
+ | { method: "sign_message"; message: SignerJsonRpcMessageToSign }
20
+ | { method: "sign_transaction"; transaction: Transaction };
21
+
22
+ const GET_RESULT_CACHE_MS = 120_000;
23
+ const GET_RESULT_HISTORY_CAPACITY = 128;
24
+ const GET_RESULT_WAIT_MS = 10_000;
25
+ const GET_RESULT_PENDING_RESULT = Symbol("pending");
26
+
27
+ type SignerJsonRpcResultEntry = {
28
+ completion: Promise<unknown>;
29
+ retrieved: boolean;
30
+ };
31
+
32
+ export enum SignerJsonRpcErrorCode {
33
+ MethodNotFound = -32601,
34
+ InvalidParams = -32602,
35
+ ServerError = -32000,
36
+ InvalidState = -32001,
37
+ NetworkMismatch = -32002,
38
+ UserRejected = -32003,
39
+ InvalidSession = -32004,
40
+ DuplicateRequestId = -32005,
41
+ }
42
+
43
+ export type SignerJsonRpcProviderSessionOptions = {
44
+ /** Aborts when the provider session is disposed. */
45
+ signal?: AbortSignal;
46
+ };
47
+
48
+ export type SignerJsonRpcResultRecord =
49
+ | { status: "not_found" }
50
+ | { status: "pending" }
51
+ | { status: "completed"; result: unknown };
52
+
53
+ export type SignerJsonRpcProviderSessionConfig = {
54
+ getSigner: () => Signer | undefined;
55
+ getSignerMetadata?: () => Pick<SignerJsonRpcInfo, "name" | "icon">;
56
+ /** Connects the requested network and returns its connected Signer. */
57
+ connect: (
58
+ networkId: string,
59
+ options?: SignerJsonRpcProviderSessionOptions,
60
+ ) => Promise<Signer>;
61
+ confirmRequest: (
62
+ request: SignerJsonRpcConfirmation,
63
+ options?: SignerJsonRpcProviderSessionOptions,
64
+ ) => Promise<boolean>;
65
+ };
66
+
67
+ export class SignerJsonRpcProviderSession {
68
+ private readonly abortController = new AbortController();
69
+ private readonly id = hexFrom(randomBytes(16));
70
+ private readonly resultRecords = new Map<string, SignerJsonRpcResultEntry>();
71
+ private readonly resultHistory = new Map<string, SignerJsonRpcResultEntry>();
72
+ private readonly handlers;
73
+ private info?: SignerJsonRpcInfoPayload;
74
+ private connected = false;
75
+
76
+ private constructor(
77
+ private readonly config: SignerJsonRpcProviderSessionConfig,
78
+ ) {
79
+ this.handlers = new Map<
80
+ string,
81
+ readonly [
82
+ number,
83
+ (
84
+ params: unknown[],
85
+ options?: SignerJsonRpcProviderSessionOptions,
86
+ ) => unknown,
87
+ ]
88
+ >([
89
+ [
90
+ "get_info",
91
+ [
92
+ 0,
93
+ () =>
94
+ (this.info ??= buildSignerInfo(
95
+ this.id,
96
+ this.requireSigner(),
97
+ this.config.getSignerMetadata?.() ?? {},
98
+ )),
99
+ ],
100
+ ],
101
+ [
102
+ "connect",
103
+ [
104
+ 1,
105
+ async ([networkId], options) => {
106
+ this.connected = false;
107
+ if (typeof networkId !== "string" || !networkId) {
108
+ throw new JsonRpcError({
109
+ code: SignerJsonRpcErrorCode.InvalidParams,
110
+ message: "Invalid network ID",
111
+ });
112
+ }
113
+
114
+ await this.requireConfirmation(
115
+ { method: "connect", networkId },
116
+ options,
117
+ );
118
+
119
+ const signer = await this.config.connect(networkId, options);
120
+ options?.signal?.throwIfAborted();
121
+ const actualNetworkId = signerJsonRpcNetworkIdFromAddressPrefix(
122
+ signer.client.addressPrefix,
123
+ );
124
+ if (actualNetworkId !== networkId) {
125
+ throw new JsonRpcError({
126
+ code: SignerJsonRpcErrorCode.NetworkMismatch,
127
+ message: `Signer uses ${actualNetworkId}, expected ${networkId}`,
128
+ });
129
+ }
130
+
131
+ this.connected = true;
132
+ return null;
133
+ },
134
+ ],
135
+ ],
136
+ [
137
+ "get_scripts",
138
+ [
139
+ 0,
140
+ async () =>
141
+ (await this.requireSigner().getAddressObjs()).map(({ script }) =>
142
+ JsonRpcTransformers.scriptFrom(script),
143
+ ),
144
+ ],
145
+ ],
146
+ [
147
+ "get_native_address",
148
+ [0, () => this.requireSigner().getInternalAddress()],
149
+ ],
150
+ ["get_identity", [0, () => this.requireSigner().getIdentity()]],
151
+ [
152
+ "sign_message",
153
+ [
154
+ 1,
155
+ async ([message], options) => {
156
+ const parsedMessage = parseMessageParam(message);
157
+ await this.requireConfirmation(
158
+ {
159
+ method: "sign_message",
160
+ message: SignerJsonRpcTransformers.messageFrom(parsedMessage),
161
+ },
162
+ options,
163
+ );
164
+ return this.requireSigner().signMessageRaw(parsedMessage);
165
+ },
166
+ ],
167
+ ],
168
+ [
169
+ "prepare_transaction",
170
+ [
171
+ 1,
172
+ async ([transaction]) =>
173
+ JsonRpcTransformers.transactionFrom(
174
+ await this.requireSigner().prepareTransaction(
175
+ parseTransactionParam(transaction),
176
+ ),
177
+ ),
178
+ ],
179
+ ],
180
+ [
181
+ "sign_transaction",
182
+ [
183
+ 1,
184
+ async ([transaction], options) => {
185
+ const parsedTransaction = parseTransactionParam(transaction);
186
+ await this.requireConfirmation(
187
+ { method: "sign_transaction", transaction: parsedTransaction },
188
+ options,
189
+ );
190
+ return JsonRpcTransformers.transactionFrom(
191
+ await this.requireSigner().signOnlyTransaction(parsedTransaction),
192
+ );
193
+ },
194
+ ],
195
+ ],
196
+ ]);
197
+ }
198
+
199
+ static open(
200
+ config: SignerJsonRpcProviderSessionConfig,
201
+ ): OwnerUnique<SignerJsonRpcProviderSession> {
202
+ const session = new SignerJsonRpcProviderSession(config);
203
+ return new OwnerUnique(session, (session) => session.dispose());
204
+ }
205
+
206
+ get isConnected() {
207
+ return this.connected;
208
+ }
209
+
210
+ handle = (payload: JsonRpcPayload) => {
211
+ const signal = this.abortController.signal;
212
+ signal.throwIfAborted();
213
+
214
+ const [metadata, ...params] = requireParams(payload);
215
+ const { requestId, sessionId } = parseRequestMetadata(metadata);
216
+
217
+ if (payload.method === "get_result") {
218
+ const [targetRequestId] = params;
219
+ requireParams(payload, 2);
220
+ requireRequestId(targetRequestId);
221
+ return this.getResult(
222
+ this.resultRecords.get(targetRequestId) ??
223
+ this.resultHistory.get(targetRequestId),
224
+ signal,
225
+ );
226
+ }
227
+
228
+ requireRequestId(requestId);
229
+ if (
230
+ this.resultRecords.has(requestId) ||
231
+ this.resultHistory.has(requestId)
232
+ ) {
233
+ throw new JsonRpcError({
234
+ code: SignerJsonRpcErrorCode.DuplicateRequestId,
235
+ message: "Request ID already exists",
236
+ });
237
+ }
238
+
239
+ return this.executeRequest(requestId, async () => {
240
+ signal.throwIfAborted();
241
+ const isGetInfo = payload.method === "get_info";
242
+ if (isGetInfo) {
243
+ if (sessionId !== undefined) {
244
+ throw new JsonRpcError({
245
+ code: SignerJsonRpcErrorCode.InvalidParams,
246
+ message: "get_info does not accept a session ID",
247
+ });
248
+ }
249
+ } else {
250
+ this.requireSession(sessionId);
251
+ }
252
+
253
+ const entry = this.handlers.get(payload.method);
254
+ if (!entry) {
255
+ throw new JsonRpcError({
256
+ code: SignerJsonRpcErrorCode.MethodNotFound,
257
+ message: `Unsupported method: ${payload.method}`,
258
+ });
259
+ }
260
+ const [paramCount, handler] = entry;
261
+ requireParams(payload, paramCount + 1);
262
+
263
+ if (!isGetInfo && payload.method !== "connect" && !this.connected) {
264
+ throw new JsonRpcError({
265
+ code: SignerJsonRpcErrorCode.InvalidState,
266
+ message: "Connect must be approved before this request",
267
+ });
268
+ }
269
+ const result = await handler(params, { signal });
270
+ signal.throwIfAborted();
271
+ return result;
272
+ });
273
+ };
274
+
275
+ private dispose() {
276
+ this.connected = false;
277
+ this.resultRecords.clear();
278
+ this.resultHistory.clear();
279
+ this.abortController.abort(
280
+ new JsonRpcError({
281
+ code: SignerJsonRpcErrorCode.InvalidSession,
282
+ message: "Invalid or expired session",
283
+ }),
284
+ );
285
+ }
286
+
287
+ private requireSession(value: unknown) {
288
+ if (value === this.id) {
289
+ return;
290
+ }
291
+ throw new JsonRpcError({
292
+ code: SignerJsonRpcErrorCode.InvalidSession,
293
+ message: "Invalid or expired session",
294
+ });
295
+ }
296
+
297
+ private requireSigner() {
298
+ const signer = this.config.getSigner();
299
+ if (!signer) {
300
+ throw new JsonRpcError({
301
+ code: SignerJsonRpcErrorCode.ServerError,
302
+ message: "No signer is connected",
303
+ });
304
+ }
305
+
306
+ return signer;
307
+ }
308
+
309
+ private async requireConfirmation(
310
+ request: SignerJsonRpcConfirmation,
311
+ options?: SignerJsonRpcProviderSessionOptions,
312
+ ) {
313
+ const confirmed = await this.config.confirmRequest(request, options);
314
+ options?.signal?.throwIfAborted();
315
+ if (!confirmed) {
316
+ throw new JsonRpcError({
317
+ code: SignerJsonRpcErrorCode.UserRejected,
318
+ message: "User rejected request",
319
+ });
320
+ }
321
+ }
322
+
323
+ private executeRequest(requestId: string, request: () => unknown) {
324
+ const completion = Promise.resolve().then(request);
325
+ const entry = { completion, retrieved: false };
326
+ this.resultRecords.set(requestId, entry);
327
+ const expire = () => {
328
+ setTimeout(() => {
329
+ if (this.resultRecords.get(requestId) === entry) {
330
+ this.resultRecords.delete(requestId);
331
+ this.resultHistory.set(requestId, entry);
332
+ this.trimResultHistory();
333
+ }
334
+ }, GET_RESULT_CACHE_MS);
335
+ };
336
+ void completion.then(expire, expire);
337
+ return completion;
338
+ }
339
+
340
+ private trimResultHistory() {
341
+ while (this.resultHistory.size > GET_RESULT_HISTORY_CAPACITY) {
342
+ let oldestRetrieved: string | undefined;
343
+ for (const [requestId, entry] of this.resultHistory) {
344
+ if (entry.retrieved) {
345
+ oldestRetrieved = requestId;
346
+ break;
347
+ }
348
+ }
349
+
350
+ this.resultHistory.delete(
351
+ oldestRetrieved ?? this.resultHistory.keys().next().value!,
352
+ );
353
+ }
354
+ }
355
+
356
+ private async getResult(
357
+ entry: SignerJsonRpcResultEntry | undefined,
358
+ signal?: AbortSignal,
359
+ ): Promise<SignerJsonRpcResultRecord> {
360
+ if (!entry) {
361
+ return { status: "not_found" };
362
+ }
363
+
364
+ const controller = new AbortController();
365
+ try {
366
+ const result = await Promise.race([
367
+ entry.completion.then(
368
+ (value) => ({ status: "fulfilled" as const, value }),
369
+ (reason: unknown) => ({ status: "rejected" as const, reason }),
370
+ ),
371
+ sleep(
372
+ GET_RESULT_WAIT_MS,
373
+ signal
374
+ ? abortSignalAny([signal, controller.signal])
375
+ : controller.signal,
376
+ ).then(
377
+ (): typeof GET_RESULT_PENDING_RESULT => GET_RESULT_PENDING_RESULT,
378
+ ),
379
+ ]);
380
+ if (result === GET_RESULT_PENDING_RESULT) {
381
+ return { status: "pending" };
382
+ }
383
+
384
+ entry.retrieved = true;
385
+ if (result.status === "rejected") {
386
+ throw result.reason;
387
+ }
388
+ return { status: "completed", result: result.value };
389
+ } finally {
390
+ controller.abort();
391
+ }
392
+ }
393
+ }
394
+
395
+ function parseMessageParam(message: unknown) {
396
+ try {
397
+ return SignerJsonRpcTransformers.messageTo(message);
398
+ } catch (cause) {
399
+ throw new JsonRpcError({
400
+ code: SignerJsonRpcErrorCode.InvalidParams,
401
+ message:
402
+ cause instanceof Error ? cause.message : "Invalid signer message",
403
+ });
404
+ }
405
+ }
406
+
407
+ function parseTransactionParam(transaction: unknown) {
408
+ try {
409
+ return JsonRpcTransformers.transactionTo(
410
+ transaction as Parameters<typeof JsonRpcTransformers.transactionTo>[0],
411
+ );
412
+ } catch (cause) {
413
+ throw new JsonRpcError({
414
+ code: SignerJsonRpcErrorCode.InvalidParams,
415
+ message:
416
+ cause instanceof Error ? cause.message : "Invalid signer transaction",
417
+ });
418
+ }
419
+ }
420
+
421
+ function requireParams(payload: JsonRpcPayload, count?: number) {
422
+ if (!Array.isArray(payload.params)) {
423
+ throw new JsonRpcError({
424
+ code: SignerJsonRpcErrorCode.InvalidParams,
425
+ message: `${payload.method} expects positional parameters`,
426
+ });
427
+ }
428
+ if (count !== undefined && payload.params.length !== count) {
429
+ throw new JsonRpcError({
430
+ code: SignerJsonRpcErrorCode.InvalidParams,
431
+ message: `${payload.method} expects ${count} parameter${count === 1 ? "" : "s"}`,
432
+ });
433
+ }
434
+
435
+ return payload.params;
436
+ }
437
+
438
+ function parseRequestMetadata(value: unknown) {
439
+ if (
440
+ typeof value !== "object" ||
441
+ value === null ||
442
+ ("request_id" in value && typeof value.request_id !== "string") ||
443
+ ("session_id" in value && typeof value.session_id !== "string")
444
+ ) {
445
+ throw new JsonRpcError({
446
+ code: SignerJsonRpcErrorCode.InvalidParams,
447
+ message: "Invalid request metadata",
448
+ });
449
+ }
450
+
451
+ return {
452
+ requestId: "request_id" in value ? value.request_id : undefined,
453
+ sessionId: "session_id" in value ? value.session_id : undefined,
454
+ };
455
+ }
456
+
457
+ function requireRequestId(value: unknown): asserts value is string {
458
+ if (typeof value !== "string" || !value) {
459
+ throw new JsonRpcError({
460
+ code: SignerJsonRpcErrorCode.InvalidParams,
461
+ message: "Invalid request ID",
462
+ });
463
+ }
464
+ }
465
+
466
+ function buildSignerInfo(
467
+ sessionId: string,
468
+ signer: Signer,
469
+ metadata: Pick<SignerJsonRpcInfo, "name" | "icon">,
470
+ ) {
471
+ return SignerJsonRpcTransformers.infoFrom({
472
+ sessionId,
473
+ type: signer.type,
474
+ signType: signer.signType,
475
+ name: metadata.name,
476
+ icon: metadata.icon,
477
+ });
478
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./handler.js";
2
+ export * from "./signer.js";
@@ -0,0 +1,15 @@
1
+ const CKB_MAINNET_NETWORK_ID = "ckb-mainnet";
2
+ const CKB_TESTNET_NETWORK_ID = "ckb-testnet";
3
+
4
+ // TODO: In the next major version, read the network ID directly from Client
5
+ // instead of inferring it from addressPrefix.
6
+ export function signerJsonRpcNetworkIdFromAddressPrefix(addressPrefix: string) {
7
+ if (addressPrefix === "ckb") {
8
+ return CKB_MAINNET_NETWORK_ID;
9
+ }
10
+ if (addressPrefix === "ckt") {
11
+ return CKB_TESTNET_NETWORK_ID;
12
+ }
13
+
14
+ throw new Error(`Unsupported address prefix: ${addressPrefix}`);
15
+ }