@ckb-ccc/core 1.21.0 → 1.22.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 (55) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +3 -3
  3. package/dist/advanced.d.mts +1 -1
  4. package/dist/advanced.mjs +1 -1
  5. package/dist/{advancedBarrel-B_9PyL2D.d.mts → advancedBarrel-Daj_LJkz.d.mts} +139 -19
  6. package/dist/advancedBarrel-Daj_LJkz.d.mts.map +1 -0
  7. package/dist/advancedBarrel.d.mts +1 -1
  8. package/dist/advancedBarrel.mjs +1 -1
  9. package/dist/barrel-CFOWSjcM.mjs +7 -0
  10. package/dist/barrel-CFOWSjcM.mjs.map +1 -0
  11. package/dist/barrel.d.mts +2 -2
  12. package/dist/barrel.mjs +1 -1
  13. package/dist/index.d.mts +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/transformers-DAMi6ZOq.mjs +2 -0
  16. package/dist/transformers-DAMi6ZOq.mjs.map +1 -0
  17. package/dist.commonjs/advanced.d.ts +1 -1
  18. package/dist.commonjs/advanced.js +1 -1
  19. package/dist.commonjs/{advancedBarrel-Cll2j9KP.d.ts → advancedBarrel-CJL3rfuW.d.ts} +139 -19
  20. package/dist.commonjs/advancedBarrel-CJL3rfuW.d.ts.map +1 -0
  21. package/dist.commonjs/advancedBarrel.d.ts +1 -1
  22. package/dist.commonjs/advancedBarrel.js +1 -1
  23. package/dist.commonjs/barrel-DZkApbAR.js +23 -0
  24. package/dist.commonjs/barrel-DZkApbAR.js.map +1 -0
  25. package/dist.commonjs/barrel.d.ts +2 -2
  26. package/dist.commonjs/barrel.js +1 -1
  27. package/dist.commonjs/index.d.ts +2 -2
  28. package/dist.commonjs/index.js +1 -1
  29. package/dist.commonjs/transformers-C6jbFO7W.js +2 -0
  30. package/dist.commonjs/transformers-C6jbFO7W.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/client/client.ts +8 -2
  33. package/src/client/clientPublicMainnet.advanced.ts +9 -0
  34. package/src/client/clientPublicTestnet.advanced.ts +9 -0
  35. package/src/jsonRpc/requestor.ts +32 -8
  36. package/src/jsonRpc/transports/fallback.ts +6 -2
  37. package/src/jsonRpc/transports/http.ts +20 -17
  38. package/src/jsonRpc/transports/transport.ts +11 -1
  39. package/src/jsonRpc/transports/webSocket.ts +84 -26
  40. package/src/signer/btc/psbt.ts +4 -2
  41. package/src/signer/jsonRpc/handler.ts +394 -133
  42. package/src/signer/jsonRpc/signer.ts +178 -34
  43. package/src/signer/jsonRpc/transformers.ts +4 -0
  44. package/src/utils/abortSignal.ts +14 -0
  45. package/src/utils/index.ts +148 -0
  46. package/dist/advancedBarrel-B_9PyL2D.d.mts.map +0 -1
  47. package/dist/barrel-CrN8Ibc4.mjs +0 -7
  48. package/dist/barrel-CrN8Ibc4.mjs.map +0 -1
  49. package/dist/transformers-7aume1sD.mjs +0 -2
  50. package/dist/transformers-7aume1sD.mjs.map +0 -1
  51. package/dist.commonjs/advancedBarrel-Cll2j9KP.d.ts.map +0 -1
  52. package/dist.commonjs/barrel-BL6-tNI3.js +0 -23
  53. package/dist.commonjs/barrel-BL6-tNI3.js.map +0 -1
  54. package/dist.commonjs/transformers-1XBitiyh.js +0 -2
  55. package/dist.commonjs/transformers-1XBitiyh.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckb-ccc/core",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Core of CCC - CKBer's Codebase",
5
5
  "author": "Hanssen0 <hanssen0@hanssen0.com>",
6
6
  "license": "MIT",
@@ -450,7 +450,9 @@ export abstract class Client {
450
450
  after?: string,
451
451
  ): Promise<ClientFindCellsResponse> {
452
452
  const res = await this.findCellsPagedNoCache(key, order, limit, after);
453
- await this.cache.recordCells(res.cells);
453
+ if (key.withData !== false) {
454
+ await this.cache.recordCells(res.cells);
455
+ }
454
456
  return res;
455
457
  }
456
458
 
@@ -645,7 +647,11 @@ export abstract class Client {
645
647
 
646
648
  /**
647
649
  * Resolve cell dependency info into concrete CellDep objects.
648
- * If a CellDepInfo specifies a type script, the actual deployed cell is located on-chain.
650
+ *
651
+ * If a CellDepInfo specifies a type script, the live cell carrying it is looked up
652
+ * on-chain and, when found, its outpoint is used instead of the configured one. When
653
+ * the lookup finds nothing, the configured outpoint is used as written. A CellDepInfo
654
+ * without a type script is always used as written.
649
655
  *
650
656
  * @param cellDepsInfoLike - One or more CellDepInfo or arrays of CellDepInfo.
651
657
  * @returns Resolved CellDep array ready to be added to a transaction.
@@ -1,6 +1,15 @@
1
1
  import { ScriptInfoLike } from "./clientTypes.js";
2
2
  import { KnownScript } from "./knownScript.js";
3
3
 
4
+ /**
5
+ * The known scripts, with a cell dep outpoint recorded for each.
6
+ *
7
+ * An outpoint here is a starting point. A dep with a `type` is resolved on-chain by
8
+ * {@link Client.getCellDeps} before use, so its outpoint keeps working after the code
9
+ * cell moves and may look spent when checked directly. Only a dep without a `type`
10
+ * needs updating when its cell moves. See #531.
11
+ */
12
+
4
13
  export const MAINNET_SCRIPTS: Record<KnownScript, ScriptInfoLike | undefined> =
5
14
  Object.freeze({
6
15
  [KnownScript.NervosDao]: {
@@ -1,6 +1,15 @@
1
1
  import { ScriptInfoLike } from "./clientTypes.js";
2
2
  import { KnownScript } from "./knownScript.js";
3
3
 
4
+ /**
5
+ * The known scripts, with a cell dep outpoint recorded for each.
6
+ *
7
+ * An outpoint here is a starting point. A dep with a `type` is resolved on-chain by
8
+ * {@link Client.getCellDeps} before use, so its outpoint keeps working after the code
9
+ * cell moves and may look spent when checked directly. Only a dep without a `type`
10
+ * needs updating when its cell moves. See #531.
11
+ */
12
+
4
13
  export const TESTNET_SCRIPTS: Record<KnownScript, ScriptInfoLike> =
5
14
  Object.freeze({
6
15
  [KnownScript.NervosDao]: {
@@ -1,11 +1,14 @@
1
+ import { abortSignalToPromise } from "../utils/abortSignal.js";
1
2
  import { OwnerAggregated } from "../utils/owner/aggregated.js";
2
3
  import { Owner } from "../utils/owner/owner.js";
3
4
  import { jsonRpcTransportFromUri } from "./transports/factory.js";
4
5
  import { JsonRpcTransportFallback } from "./transports/fallback.js";
5
6
  import {
7
+ JsonRpcError,
6
8
  JsonRpcPayload,
7
9
  JsonRpcResponse,
8
10
  JsonRpcTransport,
11
+ JsonRpcTransportRequestOptions,
9
12
  } from "./transports/index.js";
10
13
 
11
14
  function openTransports(
@@ -146,6 +149,7 @@ export class RequestorJsonRpc {
146
149
  inTransformers?: (((_: any) => unknown) | undefined)[],
147
150
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
151
  outTransformer?: (_: any) => unknown,
152
+ options?: JsonRpcTransportRequestOptions,
149
153
  ): Promise<unknown> {
150
154
  const payload = this.buildPayload(
151
155
  rpcMethod,
@@ -164,7 +168,7 @@ export class RequestorJsonRpc {
164
168
 
165
169
  try {
166
170
  return await transform(
167
- await this.requestPayload(payload),
171
+ await this.requestPayload(payload, options),
168
172
  outTransformer,
169
173
  );
170
174
  } catch (err: unknown) {
@@ -175,21 +179,41 @@ export class RequestorJsonRpc {
175
179
  }
176
180
  }
177
181
 
178
- async requestPayload(payload: JsonRpcPayload): Promise<unknown> {
182
+ async requestPayload(
183
+ payload: JsonRpcPayload,
184
+ options?: JsonRpcTransportRequestOptions,
185
+ ): Promise<unknown> {
179
186
  if (
180
187
  this.maxConcurrent !== undefined &&
181
188
  this.concurrent >= this.maxConcurrent
182
189
  ) {
183
- const pending = new Promise<void>((resolve) =>
184
- this.pending.push(resolve),
185
- );
186
- await pending;
190
+ options?.signal?.throwIfAborted();
191
+ let resolvePending: (() => void) | undefined;
192
+ const pending = new Promise<void>((resolve) => {
193
+ resolvePending = resolve;
194
+ this.pending.push(resolve);
195
+ });
196
+ try {
197
+ await (options?.signal
198
+ ? Promise.race([pending, abortSignalToPromise(options.signal)])
199
+ : pending);
200
+ } catch (cause) {
201
+ const index = resolvePending
202
+ ? this.pending.indexOf(resolvePending)
203
+ : -1;
204
+ if (index !== -1) {
205
+ this.pending.splice(index, 1);
206
+ } else {
207
+ this.pending.shift()?.();
208
+ }
209
+ throw cause;
210
+ }
187
211
  }
188
212
 
189
213
  const res: JsonRpcResponse = await (async () => {
190
214
  this.concurrent += 1;
191
215
  try {
192
- return await this.transport.request(payload);
216
+ return await this.transport.request(payload, options);
193
217
  } finally {
194
218
  this.concurrent -= 1;
195
219
  this.pending.shift()?.();
@@ -200,7 +224,7 @@ export class RequestorJsonRpc {
200
224
  throw new Error(`Id mismatched, got ${res.id}, expected ${payload.id}`);
201
225
  }
202
226
  if (res.error != null) {
203
- throw res.error as unknown;
227
+ throw new JsonRpcError(res.error);
204
228
  }
205
229
  return res.result;
206
230
  }
@@ -2,6 +2,7 @@ import {
2
2
  JsonRpcPayload,
3
3
  JsonRpcResponse,
4
4
  JsonRpcTransport,
5
+ JsonRpcTransportRequestOptions,
5
6
  } from "./transport.js";
6
7
 
7
8
  export class JsonRpcTransportFallback implements JsonRpcTransport {
@@ -10,7 +11,10 @@ export class JsonRpcTransportFallback implements JsonRpcTransport {
10
11
 
11
12
  constructor(private readonly transports: JsonRpcTransport[]) {}
12
13
 
13
- async request(data: JsonRpcPayload): Promise<JsonRpcResponse> {
14
+ async request(
15
+ data: JsonRpcPayload,
16
+ options?: JsonRpcTransportRequestOptions,
17
+ ): Promise<JsonRpcResponse> {
14
18
  const startI = this.i;
15
19
  let lastErr: unknown = new Error(
16
20
  "JsonRpcTransportFallback requires at least one transport",
@@ -20,7 +24,7 @@ export class JsonRpcTransportFallback implements JsonRpcTransport {
20
24
  const i = (startI + tried) % this.transports.length;
21
25
 
22
26
  try {
23
- const res = await this.transports[i].request(data);
27
+ const res = await this.transports[i].request(data, options);
24
28
 
25
29
  this.i = i;
26
30
  return res;
@@ -1,7 +1,9 @@
1
+ import { abortSignalAny } from "../../utils/abortSignal.js";
1
2
  import {
2
3
  JsonRpcPayload,
3
4
  JsonRpcResponse,
4
5
  JsonRpcTransport,
6
+ JsonRpcTransportRequestOptions,
5
7
  } from "./transport.js";
6
8
 
7
9
  export class JsonRpcTransportHttp implements JsonRpcTransport {
@@ -10,23 +12,24 @@ export class JsonRpcTransportHttp implements JsonRpcTransport {
10
12
  private readonly timeout = 30000,
11
13
  ) {}
12
14
 
13
- async request(payload: JsonRpcPayload): Promise<JsonRpcResponse> {
14
- const aborter = new AbortController();
15
- const abortTimer = setTimeout(() => aborter.abort(), this.timeout);
15
+ async request(
16
+ payload: JsonRpcPayload,
17
+ options?: JsonRpcTransportRequestOptions,
18
+ ): Promise<JsonRpcResponse> {
19
+ const timeoutSignal = AbortSignal.timeout(options?.timeout ?? this.timeout);
20
+ const signal = options?.signal
21
+ ? abortSignalAny([options.signal, timeoutSignal])
22
+ : timeoutSignal;
16
23
 
17
- try {
18
- return (await (
19
- await fetch(this.url, {
20
- method: "POST",
21
- headers: {
22
- "content-type": "application/json",
23
- },
24
- body: JSON.stringify(payload),
25
- signal: aborter.signal,
26
- })
27
- ).json()) as JsonRpcResponse;
28
- } finally {
29
- clearTimeout(abortTimer);
30
- }
24
+ return (await (
25
+ await fetch(this.url, {
26
+ method: "POST",
27
+ headers: {
28
+ "content-type": "application/json",
29
+ },
30
+ body: JSON.stringify(payload),
31
+ signal,
32
+ })
33
+ ).json()) as JsonRpcResponse;
31
34
  }
32
35
  }
@@ -33,6 +33,13 @@ export type JsonRpcResponse<Result = unknown, ErrorData = unknown> = {
33
33
  | { result?: never; error: JsonRpcErrorLike<ErrorData> }
34
34
  );
35
35
 
36
+ export type JsonRpcTransportRequestOptions = {
37
+ /** Cancels this individual request without disposing the transport. */
38
+ signal?: AbortSignal;
39
+ /** Overrides the transport's default timeout for this request. */
40
+ timeout?: number;
41
+ };
42
+
36
43
  export interface JsonRpcTransport {
37
44
  /**
38
45
  * Sends a JSON-RPC request to the server.
@@ -40,5 +47,8 @@ export interface JsonRpcTransport {
40
47
  * @param payload - The JSON-RPC payload to send.
41
48
  * @returns The JSON-RPC response.
42
49
  */
43
- request(payload: JsonRpcPayload): Promise<JsonRpcResponse>;
50
+ request(
51
+ payload: JsonRpcPayload,
52
+ options?: JsonRpcTransportRequestOptions,
53
+ ): Promise<JsonRpcResponse>;
44
54
  }
@@ -5,17 +5,21 @@ import {
5
5
  JsonRpcPayload,
6
6
  JsonRpcResponse,
7
7
  JsonRpcTransport,
8
+ JsonRpcTransportRequestOptions,
8
9
  } from "./transport.js";
9
10
 
11
+ const SOCKET_TIMEOUT_GRACE_PERIOD = 10_000;
12
+
10
13
  export class JsonRpcTransportWebSocket implements JsonRpcTransport {
11
14
  private ongoing: Map<
12
15
  JsonRpcId,
13
16
  [
14
17
  (response: JsonRpcResponse) => unknown,
15
18
  (error: unknown) => unknown,
16
- ReturnType<typeof setTimeout>,
19
+ WebSocket,
17
20
  ]
18
21
  > = new Map();
22
+ private firstTimeout?: { socket: WebSocket; at: number };
19
23
  private disposed = false;
20
24
  private socket?: WebSocket;
21
25
  private openSocket?: Promise<WebSocket>;
@@ -39,12 +43,20 @@ export class JsonRpcTransportWebSocket implements JsonRpcTransport {
39
43
  return new OwnerUnique(transport, (transport) => transport.dispose());
40
44
  }
41
45
 
42
- request(data: JsonRpcPayload): Promise<JsonRpcResponse> {
46
+ request(
47
+ data: JsonRpcPayload,
48
+ options?: JsonRpcTransportRequestOptions,
49
+ ): Promise<JsonRpcResponse> {
43
50
  if (this.disposed) {
44
51
  return Promise.reject(
45
52
  new Error("Cannot use a disposed JsonRpcTransportWebSocket"),
46
53
  );
47
54
  }
55
+ if (options?.signal?.aborted) {
56
+ return Promise.reject(
57
+ options.signal.reason ?? new Error("Request aborted"),
58
+ );
59
+ }
48
60
 
49
61
  const [socketUnsafe, socket] = (() => {
50
62
  if (
@@ -71,23 +83,26 @@ export class JsonRpcTransportWebSocket implements JsonRpcTransport {
71
83
  return;
72
84
  }
73
85
  const id = res.id;
86
+ if (this.firstTimeout?.socket === socket) {
87
+ this.firstTimeout = undefined;
88
+ }
74
89
 
75
90
  const req = this.ongoing.get(id);
76
- if (!req) {
91
+ if (!req || req[2] !== socket) {
77
92
  return;
78
93
  }
79
- const [resolve, _, timeout] = req;
80
- clearTimeout(timeout);
81
- this.ongoing.delete(id);
82
-
94
+ const [resolve] = req;
83
95
  resolve(res);
84
96
  };
85
97
  const onClose = () => {
86
- this.ongoing.forEach(([_, reject, timeout]) => {
87
- clearTimeout(timeout);
88
- reject(new Error("Connection closed"));
98
+ if (this.firstTimeout?.socket === socket) {
99
+ this.firstTimeout = undefined;
100
+ }
101
+ this.ongoing.forEach(([_, reject, requestSocket]) => {
102
+ if (requestSocket === socket) {
103
+ reject(new Error("Connection closed"));
104
+ }
89
105
  });
90
- this.ongoing.clear();
91
106
  };
92
107
 
93
108
  socket.onclose = onClose;
@@ -108,41 +123,84 @@ export class JsonRpcTransportWebSocket implements JsonRpcTransport {
108
123
  })();
109
124
 
110
125
  return new Promise<JsonRpcResponse>((resolve, reject) => {
126
+ const abortSignal = options?.signal;
127
+ let timeout: ReturnType<typeof setTimeout>;
111
128
  const req: [
112
129
  (res: JsonRpcResponse) => unknown,
113
130
  (err: unknown) => unknown,
114
- ReturnType<typeof setTimeout>,
131
+ WebSocket,
115
132
  ] = [
116
- resolve,
117
- reject,
118
- setTimeout(() => {
119
- this.ongoing.delete(data.id);
120
- socketUnsafe.close();
121
- reject(new Error("Request timeout"));
122
- }, this.timeout),
133
+ (response) => {
134
+ if (cleanup()) {
135
+ resolve(response);
136
+ }
137
+ },
138
+ (error) => {
139
+ if (cleanup()) {
140
+ reject(error);
141
+ }
142
+ },
143
+ socketUnsafe,
123
144
  ];
145
+ const cleanup = () => {
146
+ if (this.ongoing.get(data.id) !== req) {
147
+ return false;
148
+ }
149
+ this.ongoing.delete(data.id);
150
+ clearTimeout(timeout);
151
+ abortSignal?.removeEventListener("abort", onAbort);
152
+ return true;
153
+ };
154
+ const onAbort = () => {
155
+ req[1](abortSignal?.reason ?? new Error("Request aborted"));
156
+ };
157
+
158
+ timeout = setTimeout(() => {
159
+ if (this.ongoing.get(data.id) === req) {
160
+ req[1](new Error("Request timeout"));
161
+ const hasPending = [...this.ongoing.values()].some(
162
+ ([, , requestSocket]) => requestSocket === socketUnsafe,
163
+ );
164
+ if (!hasPending) {
165
+ socketUnsafe.close();
166
+ return;
167
+ }
168
+
169
+ const firstTimeout = this.firstTimeout;
170
+ if (firstTimeout?.socket !== socketUnsafe) {
171
+ this.firstTimeout = { socket: socketUnsafe, at: Date.now() };
172
+ } else if (
173
+ Date.now() - firstTimeout.at >=
174
+ SOCKET_TIMEOUT_GRACE_PERIOD
175
+ ) {
176
+ socketUnsafe.close();
177
+ }
178
+ }
179
+ }, options?.timeout ?? this.timeout);
124
180
  this.ongoing.set(data.id, req);
125
181
 
182
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
183
+ if (abortSignal?.aborted) {
184
+ onAbort();
185
+ return;
186
+ }
187
+
126
188
  void socket
127
189
  .then((socket) => {
128
- if (!this.ongoing.has(data.id)) {
190
+ if (this.ongoing.get(data.id) !== req) {
129
191
  return;
130
192
  }
131
193
  if (
132
194
  socket.readyState === socket.CLOSED ||
133
195
  socket.readyState === socket.CLOSING
134
196
  ) {
135
- clearTimeout(req[2]);
136
- this.ongoing.delete(data.id);
137
- reject(new Error("Connection closed"));
197
+ req[1](new Error("Connection closed"));
138
198
  } else {
139
199
  socket.send(JSON.stringify(data));
140
200
  }
141
201
  })
142
202
  .catch((err) => {
143
- clearTimeout(req[2]);
144
- this.ongoing.delete(data.id);
145
- reject(err);
203
+ req[1](err);
146
204
  });
147
205
  });
148
206
  }
@@ -5,7 +5,7 @@ import { Hex, HexLike, hexFrom } from "../../hex/index.js";
5
5
  */
6
6
  export type SignPsbtOptionsLike = {
7
7
  /**
8
- * Whether to finalize the PSBT after signing.
8
+ * Whether to attempt to finalize the signed inputs after signing.
9
9
  * Default is true.
10
10
  */
11
11
  autoFinalized?: boolean;
@@ -21,7 +21,9 @@ export class SignPsbtOptions {
21
21
  public inputsToSign: InputToSign[],
22
22
  ) {}
23
23
 
24
- static from(options?: SignPsbtOptionsLike): SignPsbtOptions {
24
+ static from(
25
+ options?: SignPsbtOptionsLike | SignPsbtOptions,
26
+ ): SignPsbtOptions {
25
27
  if (options instanceof SignPsbtOptions) {
26
28
  return options;
27
29
  }