@ckb-ccc/core 1.21.0 → 1.22.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 (58) hide show
  1. package/CHANGELOG.md +50 -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-pRBtHOAW.mjs +7 -0
  10. package/dist/barrel-pRBtHOAW.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-lavDTtAW.mjs +2 -0
  16. package/dist/transformers-lavDTtAW.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--PaV2IZx.js +23 -0
  24. package/dist.commonjs/barrel--PaV2IZx.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-VBXQGQKe.js +2 -0
  30. package/dist.commonjs/transformers-VBXQGQKe.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/ckb/transaction.ts +20 -4
  33. package/src/client/client.ts +8 -2
  34. package/src/client/clientPublicMainnet.advanced.ts +9 -0
  35. package/src/client/clientPublicTestnet.advanced.ts +9 -0
  36. package/src/client/jsonRpc/client.ts +1 -1
  37. package/src/fixedPoint/index.ts +17 -8
  38. package/src/jsonRpc/requestor.ts +32 -8
  39. package/src/jsonRpc/transports/fallback.ts +6 -2
  40. package/src/jsonRpc/transports/http.ts +20 -17
  41. package/src/jsonRpc/transports/transport.ts +11 -1
  42. package/src/jsonRpc/transports/webSocket.ts +87 -27
  43. package/src/signer/btc/psbt.ts +4 -2
  44. package/src/signer/jsonRpc/handler.ts +394 -133
  45. package/src/signer/jsonRpc/signer.ts +178 -34
  46. package/src/signer/jsonRpc/transformers.ts +4 -0
  47. package/src/utils/abortSignal.ts +14 -0
  48. package/src/utils/index.ts +150 -0
  49. package/dist/advancedBarrel-B_9PyL2D.d.mts.map +0 -1
  50. package/dist/barrel-CrN8Ibc4.mjs +0 -7
  51. package/dist/barrel-CrN8Ibc4.mjs.map +0 -1
  52. package/dist/transformers-7aume1sD.mjs +0 -2
  53. package/dist/transformers-7aume1sD.mjs.map +0 -1
  54. package/dist.commonjs/advancedBarrel-Cll2j9KP.d.ts.map +0 -1
  55. package/dist.commonjs/barrel-BL6-tNI3.js +0 -23
  56. package/dist.commonjs/barrel-BL6-tNI3.js.map +0 -1
  57. package/dist.commonjs/transformers-1XBitiyh.js +0 -2
  58. 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.1",
4
4
  "description": "Core of CCC - CKBer's Codebase",
5
5
  "author": "Hanssen0 <hanssen0@hanssen0.com>",
6
6
  "license": "MIT",
@@ -2497,13 +2497,22 @@ export class Transaction extends Entity.Base<TransactionLike, Transaction>() {
2497
2497
  const needed = numFrom(
2498
2498
  await Promise.resolve(change(changedTx, fee - leastFee)),
2499
2499
  );
2500
+
2500
2501
  if (needed > Zero) {
2501
2502
  // No enough extra capacity to create new cells for change, collect inputs again
2502
2503
  leastExtraCapacity = needed;
2503
2504
  continue;
2504
2505
  }
2505
2506
 
2506
- if ((await changedTx.getFee(from.client)) !== leastFee) {
2507
+ const changedActualFee = await changedTx.getFee(from.client);
2508
+ if (changedActualFee < leastFee && fee < leastFee) {
2509
+ // Fee is not fully paid yet because the initial transaction had insufficient inputs.
2510
+ // Collect inputs again to pay leastFee.
2511
+ leastExtraCapacity = Zero;
2512
+ continue;
2513
+ }
2514
+
2515
+ if (changedActualFee !== leastFee) {
2507
2516
  throw new Error(
2508
2517
  "The change function doesn't use all available capacity",
2509
2518
  );
@@ -2671,14 +2680,21 @@ export class Transaction extends Entity.Base<TransactionLike, Transaction>() {
2671
2680
  shouldAddInputs?: boolean;
2672
2681
  },
2673
2682
  ): Promise<[number, boolean]> {
2674
- const change = Number(numFrom(index));
2675
- if (!this.outputs[change]) {
2683
+ const change = transactionIndexFrom(index);
2684
+ if (change === undefined || !this.outputs[change]) {
2676
2685
  throw new Error("Non-existed output to change");
2677
2686
  }
2678
2687
  return this.completeFee(
2679
2688
  from,
2680
2689
  (tx, capacity) => {
2681
- tx.outputs[change].capacity += capacity;
2690
+ tx.setOutput(
2691
+ change,
2692
+ {
2693
+ ...tx.outputs[change],
2694
+ capacity: tx.outputs[change].capacity + capacity,
2695
+ },
2696
+ tx.outputsData[change],
2697
+ );
2682
2698
  return 0;
2683
2699
  },
2684
2700
  feeRate,
@@ -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]: {
@@ -178,7 +178,7 @@ export abstract class ClientJsonRpc extends Client {
178
178
 
179
179
  getFeeRateStatistics = this.buildSender(
180
180
  "get_fee_rate_statistics",
181
- [(n: NumLike) => apply(numFrom, n)],
181
+ [(n?: NumLike) => apply(numToHex, n)],
182
182
  // TODO: This is a temporary workaround for the fact that the node may return null for fee rate statistics.
183
183
  // We should change the method signature to return null instead of falling back to the default value,
184
184
  // but this would be a breaking change.
@@ -30,7 +30,11 @@ export type FixedPointLike = bigint | string | number;
30
30
  */
31
31
 
32
32
  export function fixedPointToString(val: FixedPointLike, decimals = 8): string {
33
- const str = fixedPointFrom(val).toString();
33
+ const fp = fixedPointFrom(val, decimals);
34
+ if (fp < 0n) {
35
+ return `-${fixedPointToString(-fp, decimals)}`;
36
+ }
37
+ const str = fp.toString();
34
38
  if (decimals === 0) {
35
39
  return str;
36
40
  }
@@ -65,15 +69,20 @@ export function fixedPointFrom(val: FixedPointLike, decimals = 8): FixedPoint {
65
69
  return val;
66
70
  }
67
71
 
68
- const [l, r] = (
69
- typeof val === "number" ? val.toFixed(decimals) : val.toString()
70
- ).split(".");
71
- const lVal = BigInt(l.padEnd(l.length + decimals, "0"));
72
- if (r === undefined) {
73
- return lVal;
72
+ let str = typeof val === "number" ? val.toFixed(decimals) : val.toString();
73
+ const isNegative = str.startsWith("-");
74
+ if (isNegative || str.startsWith("+")) {
75
+ str = str.slice(1);
76
+ }
77
+
78
+ const [l, r] = str.split(".");
79
+ const lVal = l === "" ? 0n : BigInt(l.padEnd(l.length + decimals, "0"));
80
+ if (r === undefined || decimals === 0) {
81
+ return isNegative ? -lVal : lVal;
74
82
  }
75
83
 
76
- return lVal + BigInt(r.slice(0, decimals).padEnd(decimals, "0"));
84
+ const unsignedVal = lVal + BigInt(r.slice(0, decimals).padEnd(decimals, "0"));
85
+ return isNegative ? -unsignedVal : unsignedVal;
77
86
  }
78
87
 
79
88
  /**
@@ -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
  }
@@ -151,7 +209,9 @@ export class JsonRpcTransportWebSocket implements JsonRpcTransport {
151
209
  this.disposed = true;
152
210
  const socket = this.socket;
153
211
 
154
- if (!socket || socket.readyState === socket.CLOSED) return;
212
+ if (!socket || socket.readyState === socket.CLOSED) {
213
+ return;
214
+ }
155
215
 
156
216
  await new Promise<void>((resolve) => {
157
217
  socket.addEventListener("close", () => resolve(), { once: true });
@@ -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
  }