@ckb-ccc/core 1.19.0 → 1.20.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 (83) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/advanced.d.mts +2 -2
  3. package/dist/advanced.mjs +1 -1
  4. package/{dist.commonjs/advancedBarrel-B565UuhW.d.ts → dist/advancedBarrel-CZxkR1dk.d.mts} +2501 -2121
  5. package/dist/advancedBarrel-CZxkR1dk.d.mts.map +1 -0
  6. package/dist/advancedBarrel.d.mts +2 -2
  7. package/dist/advancedBarrel.mjs +1 -1
  8. package/dist/advancedBarrel.mjs.map +1 -1
  9. package/dist/barrel-CqiwnGqc.mjs +7 -0
  10. package/dist/barrel-CqiwnGqc.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-BJyj6caX.mjs +2 -0
  16. package/dist/transformers-BJyj6caX.mjs.map +1 -0
  17. package/dist.commonjs/advanced.d.ts +2 -2
  18. package/dist.commonjs/advanced.js +1 -1
  19. package/{dist/advancedBarrel-mPY1Rl-S.d.mts → dist.commonjs/advancedBarrel-Czsr2VlG.d.ts} +2502 -2120
  20. package/dist.commonjs/advancedBarrel-Czsr2VlG.d.ts.map +1 -0
  21. package/dist.commonjs/advancedBarrel.d.ts +2 -2
  22. package/dist.commonjs/advancedBarrel.js +1 -1
  23. package/dist.commonjs/advancedBarrel.js.map +1 -1
  24. package/dist.commonjs/barrel-BCS94_-c.js +23 -0
  25. package/dist.commonjs/barrel-BCS94_-c.js.map +1 -0
  26. package/dist.commonjs/barrel.d.ts +2 -2
  27. package/dist.commonjs/barrel.js +1 -1
  28. package/dist.commonjs/index.d.ts +2 -2
  29. package/dist.commonjs/index.js +1 -1
  30. package/dist.commonjs/{rolldown-runtime-DWNI8pZO.js → rolldown-runtime-Chgba0Kb.js} +1 -1
  31. package/dist.commonjs/transformers-Cue1Z8Up.js +2 -0
  32. package/dist.commonjs/transformers-Cue1Z8Up.js.map +1 -0
  33. package/package.json +8 -7
  34. package/src/advancedBarrel.ts +1 -0
  35. package/src/bytes/advanced.ts +1 -6
  36. package/src/bytes/index.ts +37 -10
  37. package/src/ckb/transaction.ts +5 -5
  38. package/src/client/client.ts +75 -19
  39. package/src/client/clientPublicMainnet.ts +75 -36
  40. package/src/client/clientPublicTestnet.ts +75 -36
  41. package/src/client/jsonRpc/client.ts +77 -30
  42. package/src/client/jsonRpc/transformers.ts +4 -4
  43. package/src/jsonRpc/index.ts +1 -0
  44. package/src/jsonRpc/requestor.ts +74 -21
  45. package/src/jsonRpc/transports/advanced.ts +0 -4
  46. package/src/jsonRpc/transports/factory.ts +12 -5
  47. package/src/jsonRpc/transports/fallback.ts +9 -5
  48. package/src/jsonRpc/transports/http.ts +21 -17
  49. package/src/jsonRpc/transports/index.ts +4 -0
  50. package/src/jsonRpc/transports/transport.ts +32 -6
  51. package/src/jsonRpc/transports/webSocket.ts +67 -20
  52. package/src/keystore/index.ts +3 -3
  53. package/src/molecule/codec.ts +20 -17
  54. package/src/signer/advanced.ts +1 -0
  55. package/src/signer/btc/verify.ts +2 -2
  56. package/src/signer/ckb/secp256k1Signing.ts +3 -3
  57. package/src/signer/doge/signerDoge.ts +1 -1
  58. package/src/signer/doge/signerDogePrivateKey.ts +1 -1
  59. package/src/signer/index.ts +1 -0
  60. package/src/signer/jsonRpc/handler.ts +217 -0
  61. package/src/signer/jsonRpc/index.ts +2 -0
  62. package/src/signer/jsonRpc/network.ts +15 -0
  63. package/src/signer/jsonRpc/signer.ts +181 -0
  64. package/src/signer/jsonRpc/transformers.ts +67 -0
  65. package/src/signer/signer/index.ts +5 -13
  66. package/src/utils/abortSignal.ts +42 -0
  67. package/src/utils/index.ts +2 -0
  68. package/src/utils/owner/aggregated.ts +56 -0
  69. package/src/utils/owner/index.ts +4 -0
  70. package/src/utils/owner/owner.ts +141 -0
  71. package/src/utils/owner/refCount.ts +78 -0
  72. package/src/utils/owner/unique.ts +30 -0
  73. package/tsdown.config.mts +1 -0
  74. package/dist/address.advanced-Cka_9tcR.mjs +0 -2
  75. package/dist/address.advanced-Cka_9tcR.mjs.map +0 -1
  76. package/dist/advancedBarrel-mPY1Rl-S.d.mts.map +0 -1
  77. package/dist/barrel-DsK2p-Rh.mjs +0 -7
  78. package/dist/barrel-DsK2p-Rh.mjs.map +0 -1
  79. package/dist.commonjs/address.advanced-BTrmYDic.js +0 -2
  80. package/dist.commonjs/address.advanced-BTrmYDic.js.map +0 -1
  81. package/dist.commonjs/advancedBarrel-B565UuhW.d.ts.map +0 -1
  82. package/dist.commonjs/barrel-D8_wJLfr.js +0 -23
  83. package/dist.commonjs/barrel-D8_wJLfr.js.map +0 -1
@@ -10,8 +10,8 @@ import {
10
10
  RequestorJsonRpcConfig,
11
11
  } from "../../jsonRpc/requestor.js";
12
12
  import { Num, NumLike, numFrom, numToHex } from "../../num/index.js";
13
- import { apply } from "../../utils/index.js";
14
- import { ClientCache } from "../cache/index.js";
13
+ import { Owner, apply } from "../../utils/index.js";
14
+ import type { ClientConfig } from "../client.js";
15
15
  import { Client } from "../client.js";
16
16
  import { DEFAULT_MIN_FEE_RATE } from "../clientTypes.advanced.js";
17
17
  import {
@@ -68,10 +68,35 @@ const ERROR_PARSERS: [
68
68
  ],
69
69
  ];
70
70
 
71
- export type ClientJsonRpcConfig = RequestorJsonRpcConfig & {
72
- cache?: ClientCache;
73
- requestor?: RequestorJsonRpc;
74
- };
71
+ function handleJsonRpcError(errAny: unknown): never {
72
+ if (
73
+ typeof errAny !== "object" ||
74
+ errAny === null ||
75
+ !("data" in errAny) ||
76
+ typeof errAny.data !== "string"
77
+ ) {
78
+ throw errAny;
79
+ }
80
+ const err = errAny as ErrorClientBaseLike;
81
+
82
+ for (const [regexp, builder] of ERROR_PARSERS) {
83
+ const match = err.data.match(regexp);
84
+ if (match) {
85
+ throw builder(err, match);
86
+ }
87
+ }
88
+
89
+ throw new ErrorClientBase(err);
90
+ }
91
+
92
+ export type ClientJsonRpcConfig = RequestorJsonRpcConfig &
93
+ ClientConfig & {
94
+ /**
95
+ * @deprecated Requestor injection is supported only by legacy constructors.
96
+ * Use a borrowed Transport with `Client.new` or let `Client.open` create one.
97
+ */
98
+ requestor?: RequestorJsonRpc;
99
+ };
75
100
 
76
101
  /**
77
102
  * An abstract class implementing JSON-RPC client functionality for a specific URL and timeout.
@@ -85,42 +110,64 @@ export abstract class ClientJsonRpc extends Client {
85
110
  *
86
111
  * @param url_ - The URL of the JSON-RPC server.
87
112
  * @param timeout - The timeout for requests in milliseconds
113
+ * @deprecated Use the concrete Client's `new` or `open` method.
88
114
  */
89
115
 
90
- constructor(url_: string, config?: ClientJsonRpcConfig) {
116
+ constructor(
117
+ private readonly url_: string,
118
+ config?: ClientJsonRpcConfig,
119
+ ) {
91
120
  super(config);
92
121
 
93
- this.requestor =
94
- config?.requestor ??
95
- new RequestorJsonRpc(url_, config, (errAny) => {
96
- if (
97
- typeof errAny !== "object" ||
98
- errAny === null ||
99
- !("data" in errAny) ||
100
- typeof errAny.data !== "string"
101
- ) {
102
- throw errAny;
103
- }
104
- const err = errAny as ErrorClientBaseLike;
105
-
106
- for (const [regexp, builder] of ERROR_PARSERS) {
107
- const match = err.data.match(regexp);
108
- if (match) {
109
- throw builder(err, match);
110
- }
111
- }
112
-
113
- throw new ErrorClientBase(err);
122
+ const requestor = config?.requestor;
123
+ if (requestor) {
124
+ this.requestor = requestor;
125
+ } else if (config?.transport) {
126
+ this.requestor = RequestorJsonRpc.new({
127
+ transport: config.transport,
128
+ maxConcurrent: config.maxConcurrent,
129
+ onError: handleJsonRpcError,
114
130
  });
131
+ } else {
132
+ // Legacy constructor intentionally discards ownership.
133
+ this.requestor = RequestorJsonRpc.open({
134
+ urls: [url_, ...(config?.fallbacks ?? [])],
135
+ timeout: config?.timeout,
136
+ maxConcurrent: config?.maxConcurrent,
137
+ onError: handleJsonRpcError,
138
+ }).value;
139
+ }
140
+ }
141
+
142
+ /** Creates a Requestor that borrows an existing Transport. */
143
+ protected static newRequestor(
144
+ config: Omit<Parameters<typeof RequestorJsonRpc.new>[0], "onError">,
145
+ ): RequestorJsonRpc {
146
+ return RequestorJsonRpc.new({
147
+ ...config,
148
+ onError: handleJsonRpcError,
149
+ });
150
+ }
151
+
152
+ /** Opens a Requestor with ownership of its default Transport. */
153
+ protected static openRequestor(
154
+ config: Omit<Parameters<typeof RequestorJsonRpc.open>[0], "onError">,
155
+ ): Owner<RequestorJsonRpc> {
156
+ return RequestorJsonRpc.open({
157
+ ...config,
158
+ onError: handleJsonRpcError,
159
+ });
115
160
  }
116
161
 
117
162
  /**
118
- * Returns the URL of the JSON-RPC server.
163
+ * Returns the legacy primary URL of the JSON-RPC server.
119
164
  *
120
165
  * @returns The URL of the JSON-RPC server.
166
+ * @deprecated A Client may use multiple endpoints or a Transport without a
167
+ * URL, so this value does not reliably identify its connection.
121
168
  */
122
169
  get url(): string {
123
- return this.requestor.url;
170
+ return this.url_;
124
171
  }
125
172
 
126
173
  /**
@@ -212,10 +212,10 @@ export class JsonRpcTransformers {
212
212
  return {
213
213
  compactTarget: numFrom(header.compact_target),
214
214
  dao: {
215
- c: numLeFromBytes(dao.slice(0, 8)),
216
- ar: numLeFromBytes(dao.slice(8, 16)),
217
- s: numLeFromBytes(dao.slice(16, 24)),
218
- u: numLeFromBytes(dao.slice(24, 32)),
215
+ c: numLeFromBytes(dao.subarray(0, 8)),
216
+ ar: numLeFromBytes(dao.subarray(8, 16)),
217
+ s: numLeFromBytes(dao.subarray(16, 24)),
218
+ u: numLeFromBytes(dao.subarray(24, 32)),
219
219
  },
220
220
  epoch: Epoch.fromNum(header.epoch),
221
221
  extraHash: header.extra_hash,
@@ -1 +1,2 @@
1
1
  export * from "./requestor.js";
2
+ export * from "./transports/index.js";
@@ -1,9 +1,24 @@
1
+ import { OwnerAggregated } from "../utils/owner/aggregated.js";
2
+ import { Owner } from "../utils/owner/owner.js";
3
+ import { jsonRpcTransportFromUri } from "./transports/factory.js";
4
+ import { JsonRpcTransportFallback } from "./transports/fallback.js";
1
5
  import {
2
6
  JsonRpcPayload,
3
- Transport,
4
- TransportFallback,
5
- transportFromUri,
6
- } from "./transports/advanced.js";
7
+ JsonRpcResponse,
8
+ JsonRpcTransport,
9
+ } from "./transports/index.js";
10
+
11
+ function openTransports(
12
+ urls: readonly [string, ...string[]],
13
+ config?: { timeout?: number },
14
+ ): Owner<JsonRpcTransportFallback> {
15
+ const transportOwners = Array.from(new Set(urls), (url) =>
16
+ jsonRpcTransportFromUri(url, config),
17
+ );
18
+ return OwnerAggregated.from(transportOwners).map(
19
+ (transports) => new JsonRpcTransportFallback([...transports]),
20
+ );
21
+ }
7
22
 
8
23
  /**
9
24
  * Applies a transformation function to a value if the transformer is provided.
@@ -25,11 +40,15 @@ function transform(value: unknown, transformer?: (i: unknown) => unknown) {
25
40
  return value;
26
41
  }
27
42
 
43
+ /**
44
+ * @deprecated Used only by the legacy positional constructor. Use the static
45
+ * {@link RequestorJsonRpc.new} or {@link RequestorJsonRpc.open} methods.
46
+ */
28
47
  export type RequestorJsonRpcConfig = {
29
48
  fallbacks?: string[];
30
49
  timeout?: number;
31
50
  maxConcurrent?: number;
32
- transport?: Transport;
51
+ transport?: JsonRpcTransport;
33
52
  };
34
53
 
35
54
  export class RequestorJsonRpc {
@@ -37,15 +56,17 @@ export class RequestorJsonRpc {
37
56
  private concurrent = 0;
38
57
  private readonly pending: (() => void)[] = [];
39
58
 
40
- public readonly transport: Transport;
59
+ public readonly transport: JsonRpcTransport;
41
60
 
42
61
  private id = 0;
43
62
 
44
63
  /**
45
- * Creates an instance of ClientJsonRpc.
64
+ * Creates a Requestor using legacy positional arguments.
46
65
  *
47
66
  * @param url_ - The URL of the JSON-RPC server.
48
67
  * @param timeout - The timeout for requests in milliseconds
68
+ * @deprecated Use {@link RequestorJsonRpc.new} with a borrowed Transport or
69
+ * {@link RequestorJsonRpc.open} when creating Transports.
49
70
  */
50
71
  constructor(
51
72
  private readonly url_: string,
@@ -53,20 +74,56 @@ export class RequestorJsonRpc {
53
74
  private readonly onError?: (err: unknown) => Promise<void> | void,
54
75
  ) {
55
76
  this.maxConcurrent = config?.maxConcurrent;
56
- this.transport =
57
- config?.transport ??
58
- new TransportFallback(
59
- Array.from(
60
- new Set([url_, ...(config?.fallbacks ?? [])]).values(),
61
- (url) => transportFromUri(url, config),
62
- ),
63
- );
77
+ if (config?.transport) {
78
+ this.transport = config.transport;
79
+ } else {
80
+ this.transport = openTransports(
81
+ [url_, ...(config?.fallbacks ?? [])],
82
+ config,
83
+ ).value;
84
+ }
85
+ }
86
+
87
+ /** Creates a Requestor that borrows an existing Transport. */
88
+ static new({
89
+ transport,
90
+ maxConcurrent,
91
+ onError,
92
+ }: {
93
+ transport: JsonRpcTransport;
94
+ maxConcurrent?: number;
95
+ onError?: (err: unknown) => Promise<void> | void;
96
+ }): RequestorJsonRpc {
97
+ return new RequestorJsonRpc("", { transport, maxConcurrent }, onError);
98
+ }
99
+
100
+ /** Opens a Requestor with ownership of its default transports. */
101
+ static open({
102
+ urls: [url, ...fallbacks],
103
+ onError,
104
+ ...config
105
+ }: {
106
+ urls: readonly [string, ...string[]];
107
+ timeout?: number;
108
+ maxConcurrent?: number;
109
+ onError?: (err: unknown) => Promise<void> | void;
110
+ }): Owner<RequestorJsonRpc> {
111
+ const transportOwner = openTransports([url, ...fallbacks], config);
112
+ return transportOwner.map((transport) =>
113
+ RequestorJsonRpc.new({
114
+ transport,
115
+ ...config,
116
+ onError,
117
+ }),
118
+ );
64
119
  }
65
120
 
66
121
  /**
67
122
  * Returns the URL of the JSON-RPC server.
68
123
  *
69
124
  * @returns The URL of the JSON-RPC server.
125
+ * @deprecated URL belongs to Transport construction and is unavailable for
126
+ * Requestors created with {@link RequestorJsonRpc.new}.
70
127
  */
71
128
 
72
129
  get url(): string {
@@ -129,7 +186,7 @@ export class RequestorJsonRpc {
129
186
  await pending;
130
187
  }
131
188
 
132
- const res = (await (async () => {
189
+ const res: JsonRpcResponse = await (async () => {
133
190
  this.concurrent += 1;
134
191
  try {
135
192
  return await this.transport.request(payload);
@@ -137,11 +194,7 @@ export class RequestorJsonRpc {
137
194
  this.concurrent -= 1;
138
195
  this.pending.shift()?.();
139
196
  }
140
- })()) as {
141
- id: number;
142
- error: unknown;
143
- result: unknown;
144
- };
197
+ })();
145
198
 
146
199
  if (res.id !== payload.id) {
147
200
  throw new Error(`Id mismatched, got ${res.id}, expected ${payload.id}`);
@@ -1,5 +1 @@
1
1
  export * from "./factory.js";
2
- export * from "./fallback.js";
3
- export * from "./http.js";
4
- export * from "./transport.js";
5
- export * from "./webSocket.js";
@@ -1,10 +1,17 @@
1
- import { TransportHttp } from "./http.js";
2
- import { TransportWebSocket } from "./webSocket.js";
1
+ import { OwnerUnique } from "../../utils/owner/unique.js";
2
+ import { JsonRpcTransportHttp } from "./http.js";
3
+ import { JsonRpcTransportWebSocket } from "./webSocket.js";
3
4
 
4
- export function transportFromUri(uri: string, config?: { timeout?: number }) {
5
+ export function jsonRpcTransportFromUri(
6
+ uri: string,
7
+ config?: { timeout?: number },
8
+ ) {
5
9
  if (uri.startsWith("wss://") || uri.startsWith("ws://")) {
6
- return new TransportWebSocket(uri, config?.timeout);
10
+ return JsonRpcTransportWebSocket.open(uri, config?.timeout);
7
11
  }
8
12
 
9
- return new TransportHttp(uri, config?.timeout);
13
+ return new OwnerUnique(
14
+ new JsonRpcTransportHttp(uri, config?.timeout),
15
+ () => {},
16
+ );
10
17
  }
@@ -1,15 +1,19 @@
1
- import { JsonRpcPayload, Transport } from "./transport.js";
1
+ import {
2
+ JsonRpcPayload,
3
+ JsonRpcResponse,
4
+ JsonRpcTransport,
5
+ } from "./transport.js";
2
6
 
3
- export class TransportFallback implements Transport {
7
+ export class JsonRpcTransportFallback implements JsonRpcTransport {
4
8
  // Current transport index
5
9
  private i = 0;
6
10
 
7
- constructor(private readonly transports: Transport[]) {}
11
+ constructor(private readonly transports: JsonRpcTransport[]) {}
8
12
 
9
- async request(data: JsonRpcPayload): Promise<unknown> {
13
+ async request(data: JsonRpcPayload): Promise<JsonRpcResponse> {
10
14
  const startI = this.i;
11
15
  let lastErr: unknown = new Error(
12
- "TransportFallback requires at least one transport",
16
+ "JsonRpcTransportFallback requires at least one transport",
13
17
  );
14
18
 
15
19
  for (let tried = 0; tried < this.transports.length; tried += 1) {
@@ -1,28 +1,32 @@
1
- import { JsonRpcPayload, Transport } from "./transport.js";
1
+ import {
2
+ JsonRpcPayload,
3
+ JsonRpcResponse,
4
+ JsonRpcTransport,
5
+ } from "./transport.js";
2
6
 
3
- export class TransportHttp implements Transport {
7
+ export class JsonRpcTransportHttp implements JsonRpcTransport {
4
8
  constructor(
5
9
  private readonly url: string,
6
10
  private readonly timeout = 30000,
7
11
  ) {}
8
12
 
9
- async request(payload: JsonRpcPayload) {
13
+ async request(payload: JsonRpcPayload): Promise<JsonRpcResponse> {
10
14
  const aborter = new AbortController();
11
15
  const abortTimer = setTimeout(() => aborter.abort(), this.timeout);
12
16
 
13
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
14
- const raw = await (
15
- await fetch(this.url, {
16
- method: "POST",
17
- headers: {
18
- "content-type": "application/json",
19
- },
20
- body: JSON.stringify(payload),
21
- signal: aborter.signal,
22
- })
23
- ).json();
24
- clearTimeout(abortTimer);
25
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
26
- return raw;
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
+ }
27
31
  }
28
32
  }
@@ -0,0 +1,4 @@
1
+ export * from "./fallback.js";
2
+ export * from "./http.js";
3
+ export * from "./transport.js";
4
+ export * from "./webSocket.js";
@@ -1,18 +1,44 @@
1
+ export type JsonRpcId = string | number;
2
+
1
3
  export type JsonRpcPayload = {
2
- id: number;
4
+ id: JsonRpcId;
3
5
  jsonrpc: "2.0";
4
6
  method: string;
5
7
  params: unknown[] | Record<string, unknown>;
6
8
  };
7
9
 
8
- export interface Transport {
10
+ export type JsonRpcErrorLike<Data = unknown> = {
11
+ code: number;
12
+ message: string;
13
+ data?: Data;
14
+ };
15
+
16
+ export class JsonRpcError<Data = unknown> extends Error {
17
+ readonly code: number;
18
+ readonly data?: Data;
19
+
20
+ constructor(error: JsonRpcErrorLike<Data>) {
21
+ super(error.message);
22
+ this.name = "JsonRpcError";
23
+ this.code = error.code;
24
+ this.data = error.data;
25
+ }
26
+ }
27
+
28
+ export type JsonRpcResponse<Result = unknown, ErrorData = unknown> = {
29
+ id: JsonRpcId;
30
+ jsonrpc: "2.0";
31
+ } & (
32
+ | { result: Result; error?: never }
33
+ | { result?: never; error: JsonRpcErrorLike<ErrorData> }
34
+ );
35
+
36
+ export interface JsonRpcTransport {
9
37
  /**
10
38
  * Sends a JSON-RPC request to the server.
11
39
  *
12
40
  * @param payload - The JSON-RPC payload to send.
13
- * @returns The result of the JSON-RPC request.
14
- *
15
- * @throws Will throw an error if the response ID does not match the request ID, or if the response contains an error.
41
+ * @returns The JSON-RPC response.
16
42
  */
17
- request(data: JsonRpcPayload): Promise<unknown>;
43
+ request(payload: JsonRpcPayload): Promise<JsonRpcResponse>;
18
44
  }
@@ -1,46 +1,76 @@
1
1
  import WebSocket from "isomorphic-ws";
2
- import { JsonRpcPayload, Transport } from "./transport.js";
2
+ import { OwnerUnique } from "../../utils/owner/unique.js";
3
+ import {
4
+ JsonRpcId,
5
+ JsonRpcPayload,
6
+ JsonRpcResponse,
7
+ JsonRpcTransport,
8
+ } from "./transport.js";
3
9
 
4
- export class TransportWebSocket implements Transport {
10
+ export class JsonRpcTransportWebSocket implements JsonRpcTransport {
5
11
  private ongoing: Map<
6
- number,
12
+ JsonRpcId,
7
13
  [
8
- (response: unknown) => unknown,
14
+ (response: JsonRpcResponse) => unknown,
9
15
  (error: unknown) => unknown,
10
16
  ReturnType<typeof setTimeout>,
11
17
  ]
12
18
  > = new Map();
19
+ private disposed = false;
13
20
  private socket?: WebSocket;
14
21
  private openSocket?: Promise<WebSocket>;
15
22
 
23
+ /**
24
+ * @deprecated Use {@link JsonRpcTransportWebSocket.open} to make lifecycle
25
+ * ownership explicit. This constructor will become private in a future
26
+ * release.
27
+ */
16
28
  constructor(
17
29
  private readonly url: string,
18
30
  private readonly timeout = 30000,
19
31
  ) {}
20
32
 
21
- request(data: JsonRpcPayload) {
22
- const socket = (() => {
33
+ /** Opens an owned WebSocket transport. */
34
+ static open(
35
+ url: string,
36
+ timeout = 30000,
37
+ ): OwnerUnique<JsonRpcTransportWebSocket> {
38
+ const transport = new JsonRpcTransportWebSocket(url, timeout);
39
+ return new OwnerUnique(transport, (transport) => transport.dispose());
40
+ }
41
+
42
+ request(data: JsonRpcPayload): Promise<JsonRpcResponse> {
43
+ if (this.disposed) {
44
+ return Promise.reject(
45
+ new Error("Cannot use a disposed JsonRpcTransportWebSocket"),
46
+ );
47
+ }
48
+
49
+ const [socketUnsafe, socket] = (() => {
23
50
  if (
24
51
  this.socket &&
25
52
  this.socket.readyState !== this.socket.CLOSING &&
26
53
  this.socket.readyState !== this.socket.CLOSED &&
27
54
  this.openSocket
28
55
  ) {
29
- return this.openSocket;
56
+ return [this.socket, this.openSocket] as const;
30
57
  }
31
58
  const socket = new WebSocket(this.url);
32
59
  const onMessage = ({ data }: WebSocket.MessageEvent) => {
33
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
34
- const res = JSON.parse(data as string);
60
+ let res: JsonRpcResponse;
61
+ try {
62
+ res = JSON.parse(data as string) as JsonRpcResponse;
63
+ } catch (_) {
64
+ return;
65
+ }
35
66
  if (
36
67
  typeof res !== "object" ||
37
68
  res === null ||
38
- typeof res.id !== "number"
69
+ (typeof res.id !== "number" && typeof res.id !== "string")
39
70
  ) {
40
- throw new Error(`Unknown response ${data as string}`);
71
+ return;
41
72
  }
42
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
43
- const id: number = res.id;
73
+ const id = res.id;
44
74
 
45
75
  const req = this.ongoing.get(id);
46
76
  if (!req) {
@@ -74,12 +104,12 @@ export class TransportWebSocket implements Transport {
74
104
  };
75
105
  }
76
106
  });
77
- return this.openSocket;
107
+ return [socket, this.openSocket] as const;
78
108
  })();
79
109
 
80
- return new Promise((resolve, reject) => {
110
+ return new Promise<JsonRpcResponse>((resolve, reject) => {
81
111
  const req: [
82
- (res: unknown) => unknown,
112
+ (res: JsonRpcResponse) => unknown,
83
113
  (err: unknown) => unknown,
84
114
  ReturnType<typeof setTimeout>,
85
115
  ] = [
@@ -87,9 +117,7 @@ export class TransportWebSocket implements Transport {
87
117
  reject,
88
118
  setTimeout(() => {
89
119
  this.ongoing.delete(data.id);
90
- void socket
91
- .then((socket) => socket.close())
92
- .catch((err) => reject(err));
120
+ socketUnsafe.close();
93
121
  reject(new Error("Request timeout"));
94
122
  }, this.timeout),
95
123
  ];
@@ -97,6 +125,9 @@ export class TransportWebSocket implements Transport {
97
125
 
98
126
  void socket
99
127
  .then((socket) => {
128
+ if (!this.ongoing.has(data.id)) {
129
+ return;
130
+ }
100
131
  if (
101
132
  socket.readyState === socket.CLOSED ||
102
133
  socket.readyState === socket.CLOSING
@@ -108,7 +139,23 @@ export class TransportWebSocket implements Transport {
108
139
  socket.send(JSON.stringify(data));
109
140
  }
110
141
  })
111
- .catch((err) => reject(err));
142
+ .catch((err) => {
143
+ clearTimeout(req[2]);
144
+ this.ongoing.delete(data.id);
145
+ reject(err);
146
+ });
147
+ });
148
+ }
149
+
150
+ private async dispose(): Promise<void> {
151
+ this.disposed = true;
152
+ const socket = this.socket;
153
+
154
+ if (!socket || socket.readyState === socket.CLOSED) return;
155
+
156
+ await new Promise<void>((resolve) => {
157
+ socket.addEventListener("close", () => resolve(), { once: true });
158
+ socket.close();
112
159
  });
113
160
  }
114
161
  }
@@ -14,7 +14,7 @@ const DEFAULT_SCRYPT_PARAM_N = 262144;
14
14
 
15
15
  function mac(derivedKey: Bytes, cipherText: Bytes) {
16
16
  return hexFrom(
17
- keccak_256(bytesConcat(derivedKey.slice(16, 32), cipherText)),
17
+ keccak_256(bytesConcat(derivedKey.subarray(16, 32), cipherText)),
18
18
  ).slice(2);
19
19
  }
20
20
 
@@ -60,7 +60,7 @@ export async function keystoreEncrypt(
60
60
  dkLen: kdfparams.dklen,
61
61
  });
62
62
  const cipher = ctr(
63
- derivedKey.slice(0, 16),
63
+ derivedKey.subarray(0, 16),
64
64
  iv.map((v) => v),
65
65
  );
66
66
  const ciphertext = cipher.encrypt(
@@ -151,7 +151,7 @@ export async function keystoreDecrypt(
151
151
  throw Error("Invalid password");
152
152
  }
153
153
  const cipher = ctr(
154
- derivedKey.slice(0, 16),
154
+ derivedKey.subarray(0, 16),
155
155
  bytesFrom(crypto.cipherparams.iv),
156
156
  );
157
157
  const result = cipher.decrypt(ciphertext);