@parity/product-sdk-tx 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1650 @@
1
+ // src/submit.ts
2
+ import { createLogger } from "@parity/product-sdk-logger";
3
+
4
+ // src/errors.ts
5
+ var TxError = class extends Error {
6
+ constructor(message, options) {
7
+ super(message, options);
8
+ this.name = "TxError";
9
+ }
10
+ };
11
+ var TxTimeoutError = class extends TxError {
12
+ timeoutMs;
13
+ constructor(timeoutMs) {
14
+ super(
15
+ `Transaction timed out after ${timeoutMs / 1e3}s. The transaction may still be processing on-chain.`
16
+ );
17
+ this.name = "TxTimeoutError";
18
+ this.timeoutMs = timeoutMs;
19
+ }
20
+ };
21
+ var TxDispatchError = class extends TxError {
22
+ /** Raw dispatch error from polkadot-api. */
23
+ dispatchError;
24
+ /** Human-readable error string (e.g., "Revive.ContractReverted"). */
25
+ formatted;
26
+ constructor(dispatchError, formatted) {
27
+ super(`Transaction dispatch failed: ${formatted}`);
28
+ this.name = "TxDispatchError";
29
+ this.dispatchError = dispatchError;
30
+ this.formatted = formatted;
31
+ }
32
+ };
33
+ var TxSigningRejectedError = class extends TxError {
34
+ constructor() {
35
+ super("Transaction signing was rejected.");
36
+ this.name = "TxSigningRejectedError";
37
+ }
38
+ };
39
+ function formatDispatchError(result) {
40
+ if (result.ok) return "";
41
+ try {
42
+ const err = result.dispatchError;
43
+ if (!err) return "unknown error";
44
+ if (err.type === "Module" && err.value && typeof err.value === "object") {
45
+ const palletErr = err.value;
46
+ const palletName = palletErr.type ?? "Unknown";
47
+ if (palletErr.value && typeof palletErr.value === "object") {
48
+ const innerErr = palletErr.value;
49
+ if (innerErr.type) {
50
+ return `${palletName}.${innerErr.type}`;
51
+ }
52
+ }
53
+ return palletName;
54
+ }
55
+ return err.type ?? "unknown error";
56
+ } catch {
57
+ return "unknown error";
58
+ }
59
+ }
60
+ var TxBatchError = class extends TxError {
61
+ constructor(message) {
62
+ super(message);
63
+ this.name = "TxBatchError";
64
+ }
65
+ };
66
+ var TxDryRunError = class extends TxError {
67
+ /** The raw dry-run result for programmatic inspection. */
68
+ raw;
69
+ /** Human-readable error string derived from the dry-run result. */
70
+ formatted;
71
+ /** Solidity revert reason, if the contract provided one. */
72
+ revertReason;
73
+ constructor(raw, formatted, revertReason) {
74
+ super(revertReason ? `Dry run failed: ${revertReason}` : `Dry run failed: ${formatted}`);
75
+ this.name = "TxDryRunError";
76
+ this.raw = raw;
77
+ this.formatted = formatted;
78
+ this.revertReason = revertReason;
79
+ }
80
+ };
81
+ function formatDryRunError(result) {
82
+ if (result.success) return "";
83
+ const formatted = extractErrorFromValue(result.value);
84
+ if (formatted) return formatted;
85
+ if (result.error != null && typeof result.error === "object") {
86
+ const err = result.error;
87
+ if (typeof err.type === "string") return err.type;
88
+ if (typeof err.name === "string") return err.name;
89
+ }
90
+ return "unknown error";
91
+ }
92
+ function extractErrorFromValue(value) {
93
+ if (value == null || typeof value !== "object") return void 0;
94
+ const v = value;
95
+ if (typeof v.revertReason === "string" && v.revertReason) {
96
+ return v.revertReason;
97
+ }
98
+ if (typeof v.type === "string") {
99
+ if (v.type === "Module") {
100
+ const asDispatch = formatDispatchError({ ok: false, dispatchError: value });
101
+ if (asDispatch !== "unknown error") return asDispatch;
102
+ }
103
+ if (v.type === "Message" && typeof v.value === "string") {
104
+ return v.value;
105
+ }
106
+ if (v.type === "Data") {
107
+ const hex = v.value != null && typeof v.value === "object" && typeof v.value.asHex === "function" ? String(v.value.asHex()) : typeof v.value === "string" ? v.value : void 0;
108
+ return hex ? `contract reverted with data: ${hex}` : "contract reverted";
109
+ }
110
+ return v.type;
111
+ }
112
+ if ("raw" in v && v.raw != null && typeof v.raw === "object") {
113
+ return extractErrorFromValue(v.raw);
114
+ }
115
+ return void 0;
116
+ }
117
+ function isSigningRejection(error) {
118
+ if (!(error instanceof Error)) return false;
119
+ const msg = error.message.toLowerCase();
120
+ return msg.includes("cancelled") || msg.includes("rejected") || msg.includes("denied") || msg.includes("user refused");
121
+ }
122
+ if (void 0) {
123
+ const { describe, test, expect } = void 0;
124
+ describe("TxError hierarchy", () => {
125
+ test("TxTimeoutError", () => {
126
+ const err = new TxTimeoutError(3e5);
127
+ expect(err).toBeInstanceOf(TxError);
128
+ expect(err).toBeInstanceOf(Error);
129
+ expect(err.name).toBe("TxTimeoutError");
130
+ expect(err.timeoutMs).toBe(3e5);
131
+ expect(err.message).toContain("300s");
132
+ });
133
+ test("TxDispatchError", () => {
134
+ const raw = {
135
+ type: "Module",
136
+ value: { type: "Balances", value: { type: "InsufficientBalance" } }
137
+ };
138
+ const err = new TxDispatchError(raw, "Balances.InsufficientBalance");
139
+ expect(err).toBeInstanceOf(TxError);
140
+ expect(err.name).toBe("TxDispatchError");
141
+ expect(err.dispatchError).toBe(raw);
142
+ expect(err.formatted).toBe("Balances.InsufficientBalance");
143
+ expect(err.message).toContain("Balances.InsufficientBalance");
144
+ });
145
+ test("TxSigningRejectedError", () => {
146
+ const err = new TxSigningRejectedError();
147
+ expect(err).toBeInstanceOf(TxError);
148
+ expect(err.name).toBe("TxSigningRejectedError");
149
+ });
150
+ test("TxBatchError", () => {
151
+ const err = new TxBatchError("Cannot batch zero calls");
152
+ expect(err).toBeInstanceOf(TxError);
153
+ expect(err).toBeInstanceOf(Error);
154
+ expect(err.name).toBe("TxBatchError");
155
+ expect(err.message).toBe("Cannot batch zero calls");
156
+ });
157
+ });
158
+ describe("formatDispatchError", () => {
159
+ test("returns empty string for ok result", () => {
160
+ expect(formatDispatchError({ ok: true })).toBe("");
161
+ });
162
+ test("walks Module.Pallet.Error chain", () => {
163
+ const result = {
164
+ ok: false,
165
+ dispatchError: {
166
+ type: "Module",
167
+ value: { type: "Revive", value: { type: "ContractReverted" } }
168
+ }
169
+ };
170
+ expect(formatDispatchError(result)).toBe("Revive.ContractReverted");
171
+ });
172
+ test("returns pallet name when inner error has no type", () => {
173
+ const result = {
174
+ ok: false,
175
+ dispatchError: {
176
+ type: "Module",
177
+ value: { type: "Balances", value: {} }
178
+ }
179
+ };
180
+ expect(formatDispatchError(result)).toBe("Balances");
181
+ });
182
+ test("returns error type for non-Module errors", () => {
183
+ const result = {
184
+ ok: false,
185
+ dispatchError: { type: "BadOrigin" }
186
+ };
187
+ expect(formatDispatchError(result)).toBe("BadOrigin");
188
+ });
189
+ test("returns unknown error when dispatchError is missing", () => {
190
+ expect(formatDispatchError({ ok: false })).toBe("unknown error");
191
+ });
192
+ test("returns unknown error when dispatchError has no type", () => {
193
+ expect(formatDispatchError({ ok: false, dispatchError: {} })).toBe("unknown error");
194
+ });
195
+ });
196
+ describe("TxDryRunError", () => {
197
+ test("with revert reason", () => {
198
+ const err = new TxDryRunError(
199
+ { success: false },
200
+ "Module.Error",
201
+ "InsufficientBalance"
202
+ );
203
+ expect(err).toBeInstanceOf(TxError);
204
+ expect(err.name).toBe("TxDryRunError");
205
+ expect(err.formatted).toBe("Module.Error");
206
+ expect(err.revertReason).toBe("InsufficientBalance");
207
+ expect(err.message).toContain("InsufficientBalance");
208
+ });
209
+ test("without revert reason uses formatted", () => {
210
+ const err = new TxDryRunError({ success: false }, "BadOrigin");
211
+ expect(err.message).toContain("BadOrigin");
212
+ expect(err.revertReason).toBeUndefined();
213
+ });
214
+ test("preserves raw result for inspection", () => {
215
+ const raw = { success: false, value: { type: "Module" } };
216
+ const err = new TxDryRunError(raw, "Module");
217
+ expect(err.raw).toBe(raw);
218
+ });
219
+ });
220
+ describe("formatDryRunError", () => {
221
+ test("returns empty string for successful result", () => {
222
+ expect(formatDryRunError({ success: true })).toBe("");
223
+ });
224
+ test("extracts revert reason", () => {
225
+ expect(
226
+ formatDryRunError({
227
+ success: false,
228
+ value: { revertReason: "InsufficientBalance" }
229
+ })
230
+ ).toBe("InsufficientBalance");
231
+ });
232
+ test("walks Module.Pallet.Error chain", () => {
233
+ expect(
234
+ formatDryRunError({
235
+ success: false,
236
+ value: {
237
+ type: "Module",
238
+ value: { type: "Revive", value: { type: "StorageDepositNotEnoughFunds" } }
239
+ }
240
+ })
241
+ ).toBe("Revive.StorageDepositNotEnoughFunds");
242
+ });
243
+ test("returns pallet name when inner error has no type", () => {
244
+ expect(
245
+ formatDryRunError({
246
+ success: false,
247
+ value: { type: "Module", value: { type: "Balances", value: {} } }
248
+ })
249
+ ).toBe("Balances");
250
+ });
251
+ test("extracts ReviveApi Message string", () => {
252
+ expect(
253
+ formatDryRunError({
254
+ success: false,
255
+ value: {
256
+ type: "Message",
257
+ value: "Insufficient balance for gas * price + value"
258
+ }
259
+ })
260
+ ).toBe("Insufficient balance for gas * price + value");
261
+ });
262
+ test("handles ReviveApi Data with string hex", () => {
263
+ expect(
264
+ formatDryRunError({
265
+ success: false,
266
+ value: { type: "Data", value: "0x08c379a0" }
267
+ })
268
+ ).toBe("contract reverted with data: 0x08c379a0");
269
+ });
270
+ test("handles ReviveApi Data with Binary-like object", () => {
271
+ const binary = { asHex: () => "0xdeadbeef" };
272
+ expect(
273
+ formatDryRunError({ success: false, value: { type: "Data", value: binary } })
274
+ ).toBe("contract reverted with data: 0xdeadbeef");
275
+ });
276
+ test("handles ReviveApi Data with no extractable hex", () => {
277
+ expect(formatDryRunError({ success: false, value: { type: "Data", value: 42 } })).toBe(
278
+ "contract reverted"
279
+ );
280
+ });
281
+ test("returns non-Module/Message type directly", () => {
282
+ expect(formatDryRunError({ success: false, value: { type: "BadOrigin" } })).toBe(
283
+ "BadOrigin"
284
+ );
285
+ });
286
+ test("extracts from nested raw field (patched SDK)", () => {
287
+ expect(
288
+ formatDryRunError({
289
+ success: false,
290
+ value: { raw: { type: "Message", value: "out of gas" } }
291
+ })
292
+ ).toBe("out of gas");
293
+ });
294
+ test("extracts revertReason from nested raw", () => {
295
+ expect(
296
+ formatDryRunError({
297
+ success: false,
298
+ value: { raw: { revertReason: "Unauthorized" } }
299
+ })
300
+ ).toBe("Unauthorized");
301
+ });
302
+ test("falls back to error.type", () => {
303
+ expect(formatDryRunError({ success: false, error: { type: "ContractTrapped" } })).toBe(
304
+ "ContractTrapped"
305
+ );
306
+ });
307
+ test("falls back to error.name", () => {
308
+ expect(formatDryRunError({ success: false, error: { name: "ExecutionFailed" } })).toBe(
309
+ "ExecutionFailed"
310
+ );
311
+ });
312
+ test("returns unknown error when nothing is extractable", () => {
313
+ expect(formatDryRunError({ success: false })).toBe("unknown error");
314
+ });
315
+ test("returns unknown error for empty value and error", () => {
316
+ expect(formatDryRunError({ success: false, value: {}, error: {} })).toBe(
317
+ "unknown error"
318
+ );
319
+ });
320
+ test("returns unknown error for null value", () => {
321
+ expect(formatDryRunError({ success: false, value: null })).toBe("unknown error");
322
+ });
323
+ test("prefers revertReason over Module error", () => {
324
+ expect(
325
+ formatDryRunError({
326
+ success: false,
327
+ value: {
328
+ revertReason: "OwnableUnauthorizedAccount",
329
+ type: "Module",
330
+ value: {}
331
+ }
332
+ })
333
+ ).toBe("OwnableUnauthorizedAccount");
334
+ });
335
+ });
336
+ describe("isSigningRejection", () => {
337
+ test("detects common rejection messages", () => {
338
+ expect(isSigningRejection(new Error("Cancelled"))).toBe(true);
339
+ expect(isSigningRejection(new Error("User rejected the request"))).toBe(true);
340
+ expect(isSigningRejection(new Error("Transaction was rejected by user"))).toBe(true);
341
+ expect(isSigningRejection(new Error("User denied"))).toBe(true);
342
+ expect(isSigningRejection(new Error("Signing was denied by user"))).toBe(true);
343
+ expect(isSigningRejection(new Error("User refused to sign"))).toBe(true);
344
+ });
345
+ test("returns false for non-rejection errors", () => {
346
+ expect(isSigningRejection(new Error("Network timeout"))).toBe(false);
347
+ expect(isSigningRejection(new Error("Insufficient balance"))).toBe(false);
348
+ });
349
+ test("returns false for non-Error values", () => {
350
+ expect(isSigningRejection("cancelled")).toBe(false);
351
+ expect(isSigningRejection(null)).toBe(false);
352
+ });
353
+ });
354
+ }
355
+
356
+ // src/submit.ts
357
+ var DEFAULT_TIMEOUT_MS = 3e5;
358
+ var DEFAULT_MORTALITY_PERIOD = 256;
359
+ var log = createLogger("tx");
360
+ async function resolveTransaction(tx) {
361
+ if (tx.waited && typeof tx.waited.then === "function") {
362
+ log.debug("Resolving Ink SDK AsyncTransaction");
363
+ return tx.waited;
364
+ }
365
+ return tx;
366
+ }
367
+ function buildTxResult(event) {
368
+ return {
369
+ txHash: event.txHash,
370
+ ok: event.ok,
371
+ block: event.block,
372
+ events: event.events,
373
+ dispatchError: "dispatchError" in event ? event.dispatchError : void 0
374
+ };
375
+ }
376
+ async function submitAndWatch(tx, signer, options) {
377
+ const waitFor = options?.waitFor ?? "best-block";
378
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
379
+ const mortalityPeriod = options?.mortalityPeriod ?? DEFAULT_MORTALITY_PERIOD;
380
+ const onStatus = options?.onStatus;
381
+ const resolvedTx = await resolveTransaction(tx);
382
+ return new Promise((resolve, reject) => {
383
+ let settled = false;
384
+ let subscription = null;
385
+ const timer = setTimeout(() => {
386
+ subscription?.unsubscribe();
387
+ if (!settled) {
388
+ settled = true;
389
+ onStatus?.("error");
390
+ reject(new TxTimeoutError(timeoutMs));
391
+ }
392
+ }, timeoutMs);
393
+ function teardown() {
394
+ clearTimeout(timer);
395
+ subscription?.unsubscribe();
396
+ }
397
+ function settleReject(error) {
398
+ if (settled) return;
399
+ settled = true;
400
+ teardown();
401
+ onStatus?.("error");
402
+ reject(error);
403
+ }
404
+ try {
405
+ const observable = resolvedTx.signSubmitAndWatch(signer, {
406
+ mortality: { mortal: true, period: mortalityPeriod }
407
+ });
408
+ subscription = observable.subscribe({
409
+ next: (event) => {
410
+ switch (event.type) {
411
+ case "signed": {
412
+ log.info("Transaction signed", { txHash: event.txHash });
413
+ onStatus?.("signing");
414
+ break;
415
+ }
416
+ case "broadcasted": {
417
+ log.info("Transaction broadcasted", { txHash: event.txHash });
418
+ onStatus?.("broadcasting");
419
+ break;
420
+ }
421
+ case "txBestBlocksState": {
422
+ if (!event.found) break;
423
+ if (event.ok === false) {
424
+ const formatted = formatDispatchError({
425
+ ok: false,
426
+ dispatchError: event.dispatchError
427
+ });
428
+ log.error("Transaction failed in best block", {
429
+ formatted,
430
+ block: event.block
431
+ });
432
+ settleReject(new TxDispatchError(event.dispatchError, formatted));
433
+ return;
434
+ }
435
+ log.info("Transaction in best block", { block: event.block });
436
+ onStatus?.("in-block");
437
+ if (waitFor === "best-block" && event.ok === true && event.block && event.events) {
438
+ settled = true;
439
+ clearTimeout(timer);
440
+ resolve(
441
+ buildTxResult(
442
+ event
443
+ )
444
+ );
445
+ }
446
+ break;
447
+ }
448
+ case "finalized": {
449
+ log.info("Transaction finalized", { ok: event.ok, block: event.block });
450
+ if (!event.ok) {
451
+ const formatted = formatDispatchError({
452
+ ok: false,
453
+ dispatchError: event.dispatchError
454
+ });
455
+ if (settled) {
456
+ log.warn(
457
+ "Transaction failed after being in best block (reorg). The consumer received a success result that is no longer valid.",
458
+ { formatted, block: event.block }
459
+ );
460
+ } else {
461
+ settleReject(
462
+ new TxDispatchError(event.dispatchError, formatted)
463
+ );
464
+ }
465
+ subscription?.unsubscribe();
466
+ return;
467
+ }
468
+ onStatus?.("finalized");
469
+ if (!settled) {
470
+ settled = true;
471
+ teardown();
472
+ resolve(buildTxResult(event));
473
+ } else {
474
+ subscription?.unsubscribe();
475
+ }
476
+ break;
477
+ }
478
+ }
479
+ },
480
+ error: (err) => {
481
+ log.error("Transaction subscription error", { error: err.message });
482
+ if (isSigningRejection(err)) {
483
+ settleReject(new TxSigningRejectedError());
484
+ } else {
485
+ settleReject(err);
486
+ }
487
+ }
488
+ });
489
+ } catch (err) {
490
+ log.error("Failed to start transaction", { error: err.message });
491
+ teardown();
492
+ if (isSigningRejection(err)) {
493
+ settleReject(new TxSigningRejectedError());
494
+ } else {
495
+ settleReject(err);
496
+ }
497
+ }
498
+ });
499
+ }
500
+ if (void 0) {
501
+ let createMockTx = function(emitFn) {
502
+ return {
503
+ signSubmitAndWatch: (_signer, _options) => ({
504
+ subscribe: (handlers) => {
505
+ const unsub = vi.fn();
506
+ queueMicrotask(() => emitFn(handlers));
507
+ return { unsubscribe: unsub };
508
+ }
509
+ })
510
+ };
511
+ };
512
+ createMockTx2 = createMockTx;
513
+ const { describe, test, expect, vi, beforeEach } = void 0;
514
+ const { configure } = await null;
515
+ beforeEach(() => {
516
+ configure({ handler: () => {
517
+ } });
518
+ });
519
+ const mockSigner = {};
520
+ const signedEvent = { type: "signed", txHash: "0xabc" };
521
+ const broadcastedEvent = { type: "broadcasted", txHash: "0xabc" };
522
+ const bestBlockOk = {
523
+ type: "txBestBlocksState",
524
+ txHash: "0xabc",
525
+ found: true,
526
+ ok: true,
527
+ events: [{ id: 1 }],
528
+ block: { hash: "0xblock1", number: 100, index: 0 }
529
+ };
530
+ const bestBlockFail = {
531
+ type: "txBestBlocksState",
532
+ txHash: "0xabc",
533
+ found: true,
534
+ ok: false,
535
+ events: [],
536
+ block: { hash: "0xblock1", number: 100, index: 0 },
537
+ dispatchError: {
538
+ type: "Module",
539
+ value: { type: "Balances", value: { type: "InsufficientBalance" } }
540
+ }
541
+ };
542
+ const finalizedOk = {
543
+ type: "finalized",
544
+ txHash: "0xabc",
545
+ ok: true,
546
+ events: [{ id: 1 }],
547
+ block: { hash: "0xblock2", number: 101, index: 0 }
548
+ };
549
+ const finalizedFail = {
550
+ type: "finalized",
551
+ txHash: "0xabc",
552
+ ok: false,
553
+ events: [],
554
+ block: { hash: "0xblock2", number: 101, index: 0 },
555
+ dispatchError: { type: "BadOrigin" }
556
+ };
557
+ describe("submitAndWatch", () => {
558
+ test("resolves at best-block by default", async () => {
559
+ const tx = createMockTx((h) => {
560
+ h.next(signedEvent);
561
+ h.next(broadcastedEvent);
562
+ h.next(bestBlockOk);
563
+ h.next(finalizedOk);
564
+ });
565
+ const result = await submitAndWatch(tx, mockSigner);
566
+ expect(result.ok).toBe(true);
567
+ expect(result.block.number).toBe(100);
568
+ });
569
+ test("resolves at finalized when configured", async () => {
570
+ const tx = createMockTx((h) => {
571
+ h.next(signedEvent);
572
+ h.next(bestBlockOk);
573
+ h.next(finalizedOk);
574
+ });
575
+ const result = await submitAndWatch(tx, mockSigner, { waitFor: "finalized" });
576
+ expect(result.ok).toBe(true);
577
+ expect(result.block.number).toBe(101);
578
+ });
579
+ test("rejects with TxDispatchError on best-block failure", async () => {
580
+ const tx = createMockTx((h) => {
581
+ h.next(signedEvent);
582
+ h.next(bestBlockFail);
583
+ });
584
+ await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(TxDispatchError);
585
+ });
586
+ test("rejects with TxDispatchError on finalized failure", async () => {
587
+ const tx = createMockTx((h) => {
588
+ h.next(signedEvent);
589
+ h.next(finalizedFail);
590
+ });
591
+ await expect(submitAndWatch(tx, mockSigner, { waitFor: "finalized" })).rejects.toThrow(
592
+ TxDispatchError
593
+ );
594
+ });
595
+ test("rejects with TxTimeoutError after timeout", async () => {
596
+ const tx = createMockTx(() => {
597
+ });
598
+ const error = await submitAndWatch(tx, mockSigner, { timeoutMs: 50 }).catch(
599
+ (e) => e
600
+ );
601
+ expect(error).toBeInstanceOf(TxTimeoutError);
602
+ expect(error.timeoutMs).toBe(50);
603
+ });
604
+ test("calls onStatus callbacks in order", async () => {
605
+ const statuses = [];
606
+ const tx = createMockTx((h) => {
607
+ h.next(signedEvent);
608
+ h.next(broadcastedEvent);
609
+ h.next(bestBlockOk);
610
+ });
611
+ await submitAndWatch(tx, mockSigner, {
612
+ onStatus: (s) => statuses.push(s)
613
+ });
614
+ expect(statuses).toEqual(["signing", "broadcasting", "in-block"]);
615
+ });
616
+ test("resolves Ink SDK AsyncTransaction", async () => {
617
+ const innerTx = createMockTx((h) => {
618
+ h.next(signedEvent);
619
+ h.next(bestBlockOk);
620
+ });
621
+ const wrappedTx = {
622
+ signSubmitAndWatch: () => {
623
+ throw new Error("Should not be called on outer tx");
624
+ },
625
+ waited: Promise.resolve(innerTx)
626
+ };
627
+ const result = await submitAndWatch(wrappedTx, mockSigner);
628
+ expect(result.ok).toBe(true);
629
+ });
630
+ test("passes mortality options", async () => {
631
+ let capturedOptions;
632
+ const tx = {
633
+ signSubmitAndWatch: (_signer, options) => {
634
+ capturedOptions = options;
635
+ return {
636
+ subscribe: (handlers) => {
637
+ queueMicrotask(() => {
638
+ handlers.next(signedEvent);
639
+ handlers.next(bestBlockOk);
640
+ });
641
+ return { unsubscribe: vi.fn() };
642
+ }
643
+ };
644
+ }
645
+ };
646
+ await submitAndWatch(tx, mockSigner, { mortalityPeriod: 512 });
647
+ expect(capturedOptions).toEqual({ mortality: { mortal: true, period: 512 } });
648
+ });
649
+ test("wraps signing rejection in TxSigningRejectedError", async () => {
650
+ const tx = createMockTx((h) => {
651
+ h.error(new Error("User rejected the request"));
652
+ });
653
+ await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(TxSigningRejectedError);
654
+ });
655
+ test("skips txBestBlocksState with found=false", async () => {
656
+ const tx = createMockTx((h) => {
657
+ h.next(signedEvent);
658
+ h.next({
659
+ type: "txBestBlocksState",
660
+ txHash: "0xabc",
661
+ found: false
662
+ });
663
+ h.next(bestBlockOk);
664
+ });
665
+ const result = await submitAndWatch(tx, mockSigner);
666
+ expect(result.ok).toBe(true);
667
+ });
668
+ test("rejects with original error for non-rejection Observable errors", async () => {
669
+ const tx = createMockTx((h) => {
670
+ h.error(new Error("WebSocket disconnected"));
671
+ });
672
+ const err = await submitAndWatch(tx, mockSigner).catch((e) => e);
673
+ expect(err.message).toBe("WebSocket disconnected");
674
+ expect(err).not.toBeInstanceOf(TxSigningRejectedError);
675
+ });
676
+ test("handles synchronous throw from signSubmitAndWatch", async () => {
677
+ const tx = {
678
+ signSubmitAndWatch: () => {
679
+ throw new Error("Signer not available");
680
+ }
681
+ };
682
+ await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow("Signer not available");
683
+ });
684
+ test("calls onStatus error on dispatch failure", async () => {
685
+ const statuses = [];
686
+ const tx = createMockTx((h) => {
687
+ h.next(bestBlockFail);
688
+ });
689
+ await submitAndWatch(tx, mockSigner, {
690
+ onStatus: (s) => statuses.push(s)
691
+ }).catch(() => {
692
+ });
693
+ expect(statuses).toContain("error");
694
+ });
695
+ test("logs warning on reorg (best-block ok, finalized fail)", async () => {
696
+ const warnings = [];
697
+ const { configure: configureLogs } = await null;
698
+ configureLogs({
699
+ level: "debug",
700
+ handler: (entry) => {
701
+ if (entry.level === "warn") warnings.push(entry.message);
702
+ }
703
+ });
704
+ const tx = createMockTx((h) => {
705
+ h.next(signedEvent);
706
+ h.next(bestBlockOk);
707
+ h.next(finalizedFail);
708
+ });
709
+ const result = await submitAndWatch(tx, mockSigner);
710
+ expect(result.ok).toBe(true);
711
+ await new Promise((r) => setTimeout(r, 10));
712
+ expect(warnings.some((w) => typeof w === "string" && w.includes("reorg"))).toBe(true);
713
+ configureLogs({ handler: () => {
714
+ } });
715
+ });
716
+ test("does not resolve when txBestBlocksState ok is undefined", async () => {
717
+ const tx = createMockTx((h) => {
718
+ h.next(signedEvent);
719
+ h.next({
720
+ type: "txBestBlocksState",
721
+ txHash: "0xabc",
722
+ found: true,
723
+ events: [{ id: 1 }],
724
+ block: { hash: "0xblock1", number: 100, index: 0 }
725
+ // ok intentionally omitted
726
+ });
727
+ h.next(finalizedOk);
728
+ });
729
+ const result = await submitAndWatch(tx, mockSigner);
730
+ expect(result.block.number).toBe(101);
731
+ });
732
+ });
733
+ }
734
+ var createMockTx2;
735
+
736
+ // src/batch.ts
737
+ import { createLogger as createLogger2 } from "@parity/product-sdk-logger";
738
+ var log2 = createLogger2("tx:batch");
739
+ async function resolveDecodedCall(call) {
740
+ if (call != null && typeof call === "object") {
741
+ const obj = call;
742
+ if ("waited" in obj && obj.waited && typeof obj.waited.then === "function") {
743
+ log2.debug("Resolving Ink SDK AsyncTransaction in batch");
744
+ const resolved = await obj.waited;
745
+ if (resolved.decodedCall !== void 0) return resolved.decodedCall;
746
+ throw new TxBatchError("Resolved AsyncTransaction has no decodedCall property");
747
+ }
748
+ if ("decodedCall" in obj && obj.decodedCall !== void 0) {
749
+ return obj.decodedCall;
750
+ }
751
+ }
752
+ if (call == null || typeof call !== "object") {
753
+ throw new TxBatchError(
754
+ `Invalid batch call: expected a transaction or decoded call object, got ${call === null ? "null" : typeof call}`
755
+ );
756
+ }
757
+ return call;
758
+ }
759
+ async function batchSubmitAndWatch(calls, api, signer, options) {
760
+ if (calls.length === 0) {
761
+ throw new TxBatchError("Cannot batch zero calls");
762
+ }
763
+ const mode = options?.mode ?? "batch_all";
764
+ log2.info("Resolving batch calls", { count: calls.length, mode });
765
+ const decodedCalls = await Promise.all(calls.map(resolveDecodedCall));
766
+ log2.info("Constructing batch transaction", { mode, callCount: decodedCalls.length });
767
+ const batchTx = api.tx.Utility[mode]({ calls: decodedCalls });
768
+ return submitAndWatch(batchTx, signer, options);
769
+ }
770
+ if (void 0) {
771
+ let createMockTx = function(emitFn, decodedCall) {
772
+ return {
773
+ signSubmitAndWatch: (_signer, _options) => ({
774
+ subscribe: (handlers) => {
775
+ const unsub = vi.fn();
776
+ queueMicrotask(() => emitFn(handlers));
777
+ return { unsubscribe: unsub };
778
+ }
779
+ }),
780
+ decodedCall
781
+ };
782
+ }, createMockBatchApi = function(emitFn) {
783
+ const capturedCalls = [];
784
+ const api = {
785
+ tx: {
786
+ Utility: {
787
+ batch: vi.fn((args) => {
788
+ capturedCalls.push(args.calls);
789
+ return createMockTx(emitFn);
790
+ }),
791
+ batch_all: vi.fn((args) => {
792
+ capturedCalls.push(args.calls);
793
+ return createMockTx(emitFn);
794
+ }),
795
+ force_batch: vi.fn((args) => {
796
+ capturedCalls.push(args.calls);
797
+ return createMockTx(emitFn);
798
+ })
799
+ }
800
+ }
801
+ };
802
+ return { api, getCapturedCalls: () => capturedCalls };
803
+ };
804
+ createMockTx2 = createMockTx, createMockBatchApi2 = createMockBatchApi;
805
+ const { describe, test, expect, vi, beforeEach } = void 0;
806
+ const { configure } = await null;
807
+ const { TxDispatchError: TxDispatchError2, TxSigningRejectedError: TxSigningRejectedError2 } = await null;
808
+ beforeEach(() => {
809
+ configure({ handler: () => {
810
+ } });
811
+ });
812
+ const mockSigner = {};
813
+ const signedEvent = { type: "signed", txHash: "0xbatch" };
814
+ const bestBlockOk = {
815
+ type: "txBestBlocksState",
816
+ txHash: "0xbatch",
817
+ found: true,
818
+ ok: true,
819
+ events: [{ id: 1 }],
820
+ block: { hash: "0xblock1", number: 100, index: 0 }
821
+ };
822
+ const successEmit = (h) => {
823
+ h.next(signedEvent);
824
+ h.next(bestBlockOk);
825
+ };
826
+ describe("batchSubmitAndWatch", () => {
827
+ test("batches multiple transactions with decodedCall", async () => {
828
+ const { api, getCapturedCalls } = createMockBatchApi(successEmit);
829
+ const calls = [
830
+ { decodedCall: { pallet: "Balances", method: "transfer", args: { value: 1 } } },
831
+ { decodedCall: { pallet: "Balances", method: "transfer", args: { value: 2 } } }
832
+ ];
833
+ const result = await batchSubmitAndWatch(calls, api, mockSigner);
834
+ expect(result.ok).toBe(true);
835
+ expect(getCapturedCalls()).toHaveLength(1);
836
+ expect(getCapturedCalls()[0]).toEqual([
837
+ { pallet: "Balances", method: "transfer", args: { value: 1 } },
838
+ { pallet: "Balances", method: "transfer", args: { value: 2 } }
839
+ ]);
840
+ expect(api.tx.Utility.batch_all).toHaveBeenCalledOnce();
841
+ });
842
+ test("handles Ink SDK AsyncTransaction wrappers", async () => {
843
+ const { api, getCapturedCalls } = createMockBatchApi(successEmit);
844
+ const asyncCall = {
845
+ waited: Promise.resolve({ decodedCall: { pallet: "Contracts", method: "call" } }),
846
+ signSubmitAndWatch: () => {
847
+ throw new Error("Should not be called");
848
+ }
849
+ };
850
+ const result = await batchSubmitAndWatch([asyncCall], api, mockSigner);
851
+ expect(result.ok).toBe(true);
852
+ expect(getCapturedCalls()[0]).toEqual([{ pallet: "Contracts", method: "call" }]);
853
+ });
854
+ test("accepts raw decoded calls (pass-through)", async () => {
855
+ const { api, getCapturedCalls } = createMockBatchApi(successEmit);
856
+ const rawCall = { pallet: "System", method: "remark" };
857
+ const result = await batchSubmitAndWatch([rawCall], api, mockSigner);
858
+ expect(result.ok).toBe(true);
859
+ expect(getCapturedCalls()[0]).toEqual([{ pallet: "System", method: "remark" }]);
860
+ });
861
+ test("mixes transaction types in a single batch", async () => {
862
+ const { api, getCapturedCalls } = createMockBatchApi(successEmit);
863
+ const txWithDecoded = { decodedCall: "call1" };
864
+ const asyncTx = {
865
+ waited: Promise.resolve({ decodedCall: "call2" }),
866
+ signSubmitAndWatch: () => {
867
+ throw new Error("Should not be called");
868
+ }
869
+ };
870
+ const rawCall = { pallet: "System", method: "remark" };
871
+ const result = await batchSubmitAndWatch(
872
+ [txWithDecoded, asyncTx, rawCall],
873
+ api,
874
+ mockSigner
875
+ );
876
+ expect(result.ok).toBe(true);
877
+ expect(getCapturedCalls()[0]).toEqual([
878
+ "call1",
879
+ "call2",
880
+ { pallet: "System", method: "remark" }
881
+ ]);
882
+ });
883
+ test("throws TxBatchError for empty calls array", async () => {
884
+ const { api } = createMockBatchApi(successEmit);
885
+ await expect(batchSubmitAndWatch([], api, mockSigner)).rejects.toThrow(TxBatchError);
886
+ await expect(batchSubmitAndWatch([], api, mockSigner)).rejects.toThrow(
887
+ "Cannot batch zero calls"
888
+ );
889
+ });
890
+ test("throws TxBatchError when AsyncTransaction resolves without decodedCall", async () => {
891
+ const { api } = createMockBatchApi(successEmit);
892
+ const badAsync = {
893
+ waited: Promise.resolve({ noDecodedCall: true }),
894
+ signSubmitAndWatch: () => {
895
+ throw new Error("Should not be called");
896
+ }
897
+ };
898
+ await expect(
899
+ batchSubmitAndWatch([badAsync], api, mockSigner)
900
+ ).rejects.toThrow(TxBatchError);
901
+ });
902
+ test("throws TxBatchError for null call", async () => {
903
+ const { api } = createMockBatchApi(successEmit);
904
+ await expect(
905
+ batchSubmitAndWatch([null], api, mockSigner)
906
+ ).rejects.toThrow(TxBatchError);
907
+ await expect(
908
+ batchSubmitAndWatch([null], api, mockSigner)
909
+ ).rejects.toThrow("Invalid batch call");
910
+ });
911
+ test("throws TxBatchError for primitive call", async () => {
912
+ const { api } = createMockBatchApi(successEmit);
913
+ await expect(
914
+ batchSubmitAndWatch([42], api, mockSigner)
915
+ ).rejects.toThrow(TxBatchError);
916
+ await expect(
917
+ batchSubmitAndWatch(["oops"], api, mockSigner)
918
+ ).rejects.toThrow("Invalid batch call");
919
+ });
920
+ test("treats { decodedCall: undefined } as raw pass-through object", async () => {
921
+ const { api, getCapturedCalls } = createMockBatchApi(successEmit);
922
+ const edgeCase = { decodedCall: void 0, other: "data" };
923
+ const result = await batchSubmitAndWatch([edgeCase], api, mockSigner);
924
+ expect(result.ok).toBe(true);
925
+ expect(getCapturedCalls()[0]).toEqual([{ decodedCall: void 0, other: "data" }]);
926
+ });
927
+ test("defaults to batch_all mode", async () => {
928
+ const { api } = createMockBatchApi(successEmit);
929
+ await batchSubmitAndWatch([{ decodedCall: "call1" }], api, mockSigner);
930
+ expect(api.tx.Utility.batch_all).toHaveBeenCalledOnce();
931
+ expect(api.tx.Utility.batch).not.toHaveBeenCalled();
932
+ expect(api.tx.Utility.force_batch).not.toHaveBeenCalled();
933
+ });
934
+ test("respects mode: batch", async () => {
935
+ const { api } = createMockBatchApi(successEmit);
936
+ await batchSubmitAndWatch([{ decodedCall: "call1" }], api, mockSigner, {
937
+ mode: "batch"
938
+ });
939
+ expect(api.tx.Utility.batch).toHaveBeenCalledOnce();
940
+ expect(api.tx.Utility.batch_all).not.toHaveBeenCalled();
941
+ });
942
+ test("respects mode: force_batch", async () => {
943
+ const { api } = createMockBatchApi(successEmit);
944
+ await batchSubmitAndWatch([{ decodedCall: "call1" }], api, mockSigner, {
945
+ mode: "force_batch"
946
+ });
947
+ expect(api.tx.Utility.force_batch).toHaveBeenCalledOnce();
948
+ expect(api.tx.Utility.batch_all).not.toHaveBeenCalled();
949
+ });
950
+ test("forwards SubmitOptions to submitAndWatch", async () => {
951
+ const statuses = [];
952
+ const { api } = createMockBatchApi(successEmit);
953
+ await batchSubmitAndWatch([{ decodedCall: "call1" }], api, mockSigner, {
954
+ onStatus: (s) => statuses.push(s)
955
+ });
956
+ expect(statuses).toContain("signing");
957
+ expect(statuses).toContain("in-block");
958
+ });
959
+ test("propagates TxDispatchError", async () => {
960
+ const { api } = createMockBatchApi((h) => {
961
+ h.next(signedEvent);
962
+ h.next({
963
+ type: "txBestBlocksState",
964
+ txHash: "0xbatch",
965
+ found: true,
966
+ ok: false,
967
+ events: [],
968
+ block: { hash: "0xblock1", number: 100, index: 0 },
969
+ dispatchError: {
970
+ type: "Module",
971
+ value: { type: "Utility", value: { type: "TooManyCalls" } }
972
+ }
973
+ });
974
+ });
975
+ await expect(
976
+ batchSubmitAndWatch([{ decodedCall: "call1" }], api, mockSigner)
977
+ ).rejects.toThrow(TxDispatchError2);
978
+ });
979
+ test("propagates TxSigningRejectedError", async () => {
980
+ const { api } = createMockBatchApi((h) => {
981
+ h.error(new Error("User rejected the request"));
982
+ });
983
+ await expect(
984
+ batchSubmitAndWatch([{ decodedCall: "call1" }], api, mockSigner)
985
+ ).rejects.toThrow(TxSigningRejectedError2);
986
+ });
987
+ test("resolves all calls in parallel", async () => {
988
+ const { api } = createMockBatchApi(successEmit);
989
+ const resolveOrder = [];
990
+ const asyncCall1 = {
991
+ waited: new Promise((resolve) => {
992
+ setTimeout(() => {
993
+ resolveOrder.push(1);
994
+ resolve({ decodedCall: "call1" });
995
+ }, 10);
996
+ }),
997
+ signSubmitAndWatch: () => {
998
+ throw new Error("Should not be called");
999
+ }
1000
+ };
1001
+ const asyncCall2 = {
1002
+ waited: new Promise((resolve) => {
1003
+ setTimeout(() => {
1004
+ resolveOrder.push(2);
1005
+ resolve({ decodedCall: "call2" });
1006
+ }, 5);
1007
+ }),
1008
+ signSubmitAndWatch: () => {
1009
+ throw new Error("Should not be called");
1010
+ }
1011
+ };
1012
+ await batchSubmitAndWatch([asyncCall1, asyncCall2], api, mockSigner);
1013
+ expect(resolveOrder).toContain(1);
1014
+ expect(resolveOrder).toContain(2);
1015
+ });
1016
+ });
1017
+ }
1018
+ var createMockTx2;
1019
+ var createMockBatchApi2;
1020
+
1021
+ // src/retry.ts
1022
+ import { createLogger as createLogger3 } from "@parity/product-sdk-logger";
1023
+ var log3 = createLogger3("tx:retry");
1024
+ function sleep(ms) {
1025
+ return new Promise((resolve) => setTimeout(resolve, ms));
1026
+ }
1027
+ function isNonRetryable(error) {
1028
+ return error instanceof TxBatchError || error instanceof TxDispatchError || error instanceof TxSigningRejectedError || error instanceof TxTimeoutError;
1029
+ }
1030
+ function calculateDelay(attempt, baseDelayMs, maxDelayMs) {
1031
+ const exponential = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
1032
+ const jitter = 0.5 + Math.random() * 0.5;
1033
+ return Math.round(exponential * jitter);
1034
+ }
1035
+ async function withRetry(fn, options) {
1036
+ const maxAttempts = options?.maxAttempts ?? 3;
1037
+ const baseDelayMs = options?.baseDelayMs ?? 1e3;
1038
+ const maxDelayMs = options?.maxDelayMs ?? 15e3;
1039
+ let lastError;
1040
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1041
+ try {
1042
+ return await fn();
1043
+ } catch (error) {
1044
+ lastError = error;
1045
+ if (isNonRetryable(error)) {
1046
+ throw error;
1047
+ }
1048
+ if (attempt + 1 >= maxAttempts) {
1049
+ break;
1050
+ }
1051
+ const delay = calculateDelay(attempt, baseDelayMs, maxDelayMs);
1052
+ log3.warn(`Attempt ${attempt + 1}/${maxAttempts} failed, retrying in ${delay}ms`, {
1053
+ error: error instanceof Error ? error.message : String(error)
1054
+ });
1055
+ await sleep(delay);
1056
+ }
1057
+ }
1058
+ throw lastError;
1059
+ }
1060
+ if (void 0) {
1061
+ const { describe, test, expect, vi, beforeEach } = void 0;
1062
+ const { configure } = await null;
1063
+ beforeEach(() => {
1064
+ configure({ handler: () => {
1065
+ } });
1066
+ vi.useRealTimers();
1067
+ });
1068
+ describe("withRetry", () => {
1069
+ test("returns on first success", async () => {
1070
+ const result = await withRetry(() => Promise.resolve("ok"));
1071
+ expect(result).toBe("ok");
1072
+ });
1073
+ test("retries transient error then succeeds", async () => {
1074
+ let calls = 0;
1075
+ const result = await withRetry(
1076
+ () => {
1077
+ calls++;
1078
+ if (calls < 2) return Promise.reject(new Error("Network error"));
1079
+ return Promise.resolve("recovered");
1080
+ },
1081
+ { baseDelayMs: 1 }
1082
+ );
1083
+ expect(result).toBe("recovered");
1084
+ expect(calls).toBe(2);
1085
+ });
1086
+ test("gives up after maxAttempts", async () => {
1087
+ let calls = 0;
1088
+ await expect(
1089
+ withRetry(
1090
+ () => {
1091
+ calls++;
1092
+ return Promise.reject(new Error("Persistent failure"));
1093
+ },
1094
+ { maxAttempts: 3, baseDelayMs: 1 }
1095
+ )
1096
+ ).rejects.toThrow("Persistent failure");
1097
+ expect(calls).toBe(3);
1098
+ });
1099
+ test("does NOT retry TxDispatchError", async () => {
1100
+ let calls = 0;
1101
+ await expect(
1102
+ withRetry(
1103
+ () => {
1104
+ calls++;
1105
+ return Promise.reject(
1106
+ new TxDispatchError({}, "Balances.InsufficientBalance")
1107
+ );
1108
+ },
1109
+ { maxAttempts: 3, baseDelayMs: 1 }
1110
+ )
1111
+ ).rejects.toThrow(TxDispatchError);
1112
+ expect(calls).toBe(1);
1113
+ });
1114
+ test("does NOT retry TxSigningRejectedError", async () => {
1115
+ let calls = 0;
1116
+ await expect(
1117
+ withRetry(
1118
+ () => {
1119
+ calls++;
1120
+ return Promise.reject(new TxSigningRejectedError());
1121
+ },
1122
+ { maxAttempts: 3, baseDelayMs: 1 }
1123
+ )
1124
+ ).rejects.toThrow(TxSigningRejectedError);
1125
+ expect(calls).toBe(1);
1126
+ });
1127
+ test("does NOT retry TxBatchError", async () => {
1128
+ let calls = 0;
1129
+ await expect(
1130
+ withRetry(
1131
+ () => {
1132
+ calls++;
1133
+ return Promise.reject(new TxBatchError("Cannot batch zero calls"));
1134
+ },
1135
+ { maxAttempts: 3, baseDelayMs: 1 }
1136
+ )
1137
+ ).rejects.toThrow(TxBatchError);
1138
+ expect(calls).toBe(1);
1139
+ });
1140
+ test("does NOT retry TxTimeoutError", async () => {
1141
+ let calls = 0;
1142
+ await expect(
1143
+ withRetry(
1144
+ () => {
1145
+ calls++;
1146
+ return Promise.reject(new TxTimeoutError(3e5));
1147
+ },
1148
+ { maxAttempts: 3, baseDelayMs: 1 }
1149
+ )
1150
+ ).rejects.toThrow(TxTimeoutError);
1151
+ expect(calls).toBe(1);
1152
+ });
1153
+ test("respects maxDelayMs cap", () => {
1154
+ const delay = calculateDelay(10, 1e3, 15e3);
1155
+ expect(delay).toBeLessThanOrEqual(15e3);
1156
+ expect(delay).toBeGreaterThan(0);
1157
+ });
1158
+ test("applies jitter (delay varies between calls)", () => {
1159
+ const delays = Array.from({ length: 20 }, () => calculateDelay(2, 1e3, 15e3));
1160
+ const unique = new Set(delays);
1161
+ expect(unique.size).toBeGreaterThan(1);
1162
+ });
1163
+ test("exponential backoff increases delay", () => {
1164
+ const base = 1e3;
1165
+ const minDelay2 = base * 4 * 0.5;
1166
+ const maxDelay0 = base * 1;
1167
+ expect(minDelay2).toBeGreaterThan(maxDelay0);
1168
+ });
1169
+ });
1170
+ }
1171
+
1172
+ // src/dev-signers.ts
1173
+ import { DEV_PHRASE } from "@polkadot-labs/hdkd-helpers";
1174
+ import { seedToAccount } from "@parity/product-sdk-keys";
1175
+ function createDevSigner(name) {
1176
+ return seedToAccount(DEV_PHRASE, `//${name}`).signer;
1177
+ }
1178
+ function getDevPublicKey(name) {
1179
+ return seedToAccount(DEV_PHRASE, `//${name}`).publicKey;
1180
+ }
1181
+ if (void 0) {
1182
+ const { describe, test, expect } = void 0;
1183
+ const ALICE_PUBKEY = new Uint8Array([
1184
+ 212,
1185
+ 53,
1186
+ 147,
1187
+ 199,
1188
+ 21,
1189
+ 253,
1190
+ 211,
1191
+ 28,
1192
+ 97,
1193
+ 20,
1194
+ 26,
1195
+ 189,
1196
+ 4,
1197
+ 169,
1198
+ 159,
1199
+ 214,
1200
+ 130,
1201
+ 44,
1202
+ 133,
1203
+ 88,
1204
+ 133,
1205
+ 76,
1206
+ 205,
1207
+ 227,
1208
+ 154,
1209
+ 86,
1210
+ 132,
1211
+ 231,
1212
+ 165,
1213
+ 109,
1214
+ 162,
1215
+ 125
1216
+ ]);
1217
+ describe("createDevSigner", () => {
1218
+ test("creates a signer for Alice with known public key", () => {
1219
+ const signer = createDevSigner("Alice");
1220
+ expect(signer).toBeDefined();
1221
+ expect(signer.publicKey).toEqual(ALICE_PUBKEY);
1222
+ });
1223
+ test("different names produce different signers", () => {
1224
+ const alice = createDevSigner("Alice");
1225
+ const bob = createDevSigner("Bob");
1226
+ expect(alice.publicKey).not.toEqual(bob.publicKey);
1227
+ });
1228
+ test("all dev account names produce valid signers", () => {
1229
+ const names = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie"];
1230
+ const keys = /* @__PURE__ */ new Set();
1231
+ for (const name of names) {
1232
+ const signer = createDevSigner(name);
1233
+ expect(signer).toBeDefined();
1234
+ expect(signer.publicKey).toBeInstanceOf(Uint8Array);
1235
+ expect(signer.publicKey.length).toBe(32);
1236
+ const hex = Array.from(signer.publicKey).map((b) => b.toString(16).padStart(2, "0")).join("");
1237
+ expect(keys.has(hex)).toBe(false);
1238
+ keys.add(hex);
1239
+ }
1240
+ });
1241
+ });
1242
+ describe("getDevPublicKey", () => {
1243
+ test("returns Alice's known public key", () => {
1244
+ expect(getDevPublicKey("Alice")).toEqual(ALICE_PUBKEY);
1245
+ });
1246
+ test("matches the signer's public key", () => {
1247
+ const signer = createDevSigner("Bob");
1248
+ const pubkey = getDevPublicKey("Bob");
1249
+ expect(pubkey).toEqual(signer.publicKey);
1250
+ });
1251
+ });
1252
+ }
1253
+
1254
+ // src/dry-run.ts
1255
+ function extractTransaction(result) {
1256
+ if (!result.success) {
1257
+ const formatted = formatDryRunError(result);
1258
+ const revertReason = extractRevertReason(result.value);
1259
+ throw new TxDryRunError(result, formatted, revertReason);
1260
+ }
1261
+ const value = result.value;
1262
+ if (value == null || typeof value !== "object") {
1263
+ throw new TxDryRunError(result, "dry run returned no value");
1264
+ }
1265
+ const v = value;
1266
+ if (typeof v.send !== "function") {
1267
+ throw new TxDryRunError(result, "not a write query (no send())");
1268
+ }
1269
+ return v.send();
1270
+ }
1271
+ function extractRevertReason(value) {
1272
+ if (value == null || typeof value !== "object") return void 0;
1273
+ const v = value;
1274
+ if (typeof v.revertReason === "string" && v.revertReason) {
1275
+ return v.revertReason;
1276
+ }
1277
+ if ("raw" in v && v.raw != null && typeof v.raw === "object") {
1278
+ return extractRevertReason(v.raw);
1279
+ }
1280
+ return void 0;
1281
+ }
1282
+ function applyWeightBuffer(weight, options) {
1283
+ const percent = options?.percent ?? 25;
1284
+ const multiplier = 100n + BigInt(percent);
1285
+ return {
1286
+ ref_time: weight.ref_time * multiplier / 100n,
1287
+ proof_size: weight.proof_size * multiplier / 100n
1288
+ };
1289
+ }
1290
+ if (void 0) {
1291
+ const { describe, test, expect } = void 0;
1292
+ describe("extractTransaction", () => {
1293
+ test("returns tx from successful dry-run with send()", () => {
1294
+ const mockTx = {
1295
+ signSubmitAndWatch: () => ({ subscribe: () => ({ unsubscribe: () => {
1296
+ } }) })
1297
+ };
1298
+ const result = {
1299
+ success: true,
1300
+ value: { response: "ok", send: () => mockTx }
1301
+ };
1302
+ expect(extractTransaction(result)).toBe(mockTx);
1303
+ });
1304
+ test("throws TxDryRunError on failed dry-run", () => {
1305
+ const result = {
1306
+ success: false,
1307
+ value: { revertReason: "InsufficientBalance" }
1308
+ };
1309
+ try {
1310
+ extractTransaction(result);
1311
+ expect.unreachable("should have thrown");
1312
+ } catch (e) {
1313
+ expect(e).toBeInstanceOf(TxDryRunError);
1314
+ const err = e;
1315
+ expect(err.revertReason).toBe("InsufficientBalance");
1316
+ expect(err.formatted).toBe("InsufficientBalance");
1317
+ expect(err.message).toContain("InsufficientBalance");
1318
+ expect(err.raw).toBe(result);
1319
+ }
1320
+ });
1321
+ test("throws TxDryRunError with Module error formatting", () => {
1322
+ const result = {
1323
+ success: false,
1324
+ value: {
1325
+ type: "Module",
1326
+ value: { type: "Revive", value: { type: "StorageDepositNotEnoughFunds" } }
1327
+ }
1328
+ };
1329
+ try {
1330
+ extractTransaction(result);
1331
+ expect.unreachable("should have thrown");
1332
+ } catch (e) {
1333
+ const err = e;
1334
+ expect(err.formatted).toBe("Revive.StorageDepositNotEnoughFunds");
1335
+ expect(err.revertReason).toBeUndefined();
1336
+ }
1337
+ });
1338
+ test("throws TxDryRunError with error field", () => {
1339
+ const result = {
1340
+ success: false,
1341
+ value: {},
1342
+ error: { type: "ContractTrapped" }
1343
+ };
1344
+ try {
1345
+ extractTransaction(result);
1346
+ expect.unreachable("should have thrown");
1347
+ } catch (e) {
1348
+ const err = e;
1349
+ expect(err.formatted).toBe("ContractTrapped");
1350
+ }
1351
+ });
1352
+ test("throws when value is missing", () => {
1353
+ const result = { success: true };
1354
+ expect(() => extractTransaction(result)).toThrow(TxDryRunError);
1355
+ });
1356
+ test("throws when send is not a function", () => {
1357
+ const result = { success: true, value: { response: "ok" } };
1358
+ expect(() => extractTransaction(result)).toThrow("not a write query");
1359
+ });
1360
+ test("throws with revertReason from nested raw (patched SDK)", () => {
1361
+ const result = {
1362
+ success: false,
1363
+ value: { raw: { revertReason: "Unauthorized" } }
1364
+ };
1365
+ try {
1366
+ extractTransaction(result);
1367
+ expect.unreachable("should have thrown");
1368
+ } catch (e) {
1369
+ const err = e;
1370
+ expect(err.revertReason).toBe("Unauthorized");
1371
+ }
1372
+ });
1373
+ test("throws with ReviveApi Message error", () => {
1374
+ const result = {
1375
+ success: false,
1376
+ value: { type: "Message", value: "Insufficient balance for gas * price + value" }
1377
+ };
1378
+ try {
1379
+ extractTransaction(result);
1380
+ expect.unreachable("should have thrown");
1381
+ } catch (e) {
1382
+ const err = e;
1383
+ expect(err.formatted).toBe("Insufficient balance for gas * price + value");
1384
+ }
1385
+ });
1386
+ });
1387
+ describe("applyWeightBuffer", () => {
1388
+ test("applies default 25% buffer", () => {
1389
+ const weight = { ref_time: 1000n, proof_size: 500n };
1390
+ const buffered = applyWeightBuffer(weight);
1391
+ expect(buffered.ref_time).toBe(1250n);
1392
+ expect(buffered.proof_size).toBe(625n);
1393
+ });
1394
+ test("applies custom buffer percentage", () => {
1395
+ const weight = { ref_time: 1000n, proof_size: 1000n };
1396
+ const buffered = applyWeightBuffer(weight, { percent: 50 });
1397
+ expect(buffered.ref_time).toBe(1500n);
1398
+ expect(buffered.proof_size).toBe(1500n);
1399
+ });
1400
+ test("zero buffer returns same values", () => {
1401
+ const weight = { ref_time: 1000n, proof_size: 500n };
1402
+ const buffered = applyWeightBuffer(weight, { percent: 0 });
1403
+ expect(buffered.ref_time).toBe(1000n);
1404
+ expect(buffered.proof_size).toBe(500n);
1405
+ });
1406
+ test("does not mutate original weight", () => {
1407
+ const weight = { ref_time: 1000n, proof_size: 500n };
1408
+ const buffered = applyWeightBuffer(weight);
1409
+ expect(weight.ref_time).toBe(1000n);
1410
+ expect(weight.proof_size).toBe(500n);
1411
+ expect(buffered).not.toBe(weight);
1412
+ });
1413
+ test("works with realistic weight values", () => {
1414
+ const weight = { ref_time: 4500000000n, proof_size: 1000000n };
1415
+ const buffered = applyWeightBuffer(weight);
1416
+ expect(buffered.ref_time).toBe(5625000000n);
1417
+ expect(buffered.proof_size).toBe(1250000n);
1418
+ });
1419
+ });
1420
+ }
1421
+
1422
+ // src/account-mapping.ts
1423
+ import { createLogger as createLogger4 } from "@parity/product-sdk-logger";
1424
+ var log4 = createLogger4("tx:mapping");
1425
+ var TxAccountMappingError = class extends Error {
1426
+ constructor(message, options) {
1427
+ super(message, options);
1428
+ this.name = "TxAccountMappingError";
1429
+ }
1430
+ };
1431
+ async function ensureAccountMapped(address, signer, checker, api, options) {
1432
+ const timeoutMs = options?.timeoutMs ?? 6e4;
1433
+ const onStatus = options?.onStatus;
1434
+ onStatus?.("checking");
1435
+ let isMapped;
1436
+ try {
1437
+ isMapped = await checker.addressIsMapped(address);
1438
+ } catch (cause) {
1439
+ throw new TxAccountMappingError(`Failed to check mapping status for ${address}`, { cause });
1440
+ }
1441
+ if (isMapped) {
1442
+ log4.debug("account already mapped", { address });
1443
+ onStatus?.("already-mapped");
1444
+ return null;
1445
+ }
1446
+ log4.info("mapping account", { address });
1447
+ onStatus?.("mapping");
1448
+ const tx = api.tx.Revive.map_account();
1449
+ const result = await submitAndWatch(tx, signer, {
1450
+ waitFor: "best-block",
1451
+ timeoutMs
1452
+ });
1453
+ log4.info("account mapped successfully", { address, block: result.block });
1454
+ onStatus?.("mapped");
1455
+ return result;
1456
+ }
1457
+ async function isAccountMapped(address, checker) {
1458
+ try {
1459
+ return await checker.addressIsMapped(address);
1460
+ } catch (cause) {
1461
+ throw new TxAccountMappingError(`Failed to check mapping status for ${address}`, { cause });
1462
+ }
1463
+ }
1464
+ if (void 0) {
1465
+ const { describe, test, expect, vi } = void 0;
1466
+ describe("ensureAccountMapped", () => {
1467
+ const mockSigner = {};
1468
+ test("returns null when already mapped", async () => {
1469
+ const checker = {
1470
+ addressIsMapped: vi.fn().mockResolvedValue(true)
1471
+ };
1472
+ const api = {};
1473
+ const result = await ensureAccountMapped("5Alice", mockSigner, checker, api);
1474
+ expect(result).toBeNull();
1475
+ expect(checker.addressIsMapped).toHaveBeenCalledWith("5Alice");
1476
+ });
1477
+ test("calls onStatus with already-mapped when mapped", async () => {
1478
+ const statuses = [];
1479
+ const checker = {
1480
+ addressIsMapped: vi.fn().mockResolvedValue(true)
1481
+ };
1482
+ await ensureAccountMapped("5Alice", mockSigner, checker, {}, {
1483
+ onStatus: (s) => statuses.push(s)
1484
+ });
1485
+ expect(statuses).toEqual(["checking", "already-mapped"]);
1486
+ });
1487
+ test("throws TxAccountMappingError when check fails", async () => {
1488
+ const checker = {
1489
+ addressIsMapped: vi.fn().mockRejectedValue(new Error("network error"))
1490
+ };
1491
+ await expect(
1492
+ ensureAccountMapped("5Alice", mockSigner, checker, {})
1493
+ ).rejects.toThrow(TxAccountMappingError);
1494
+ });
1495
+ test("submits map_account when not mapped", async () => {
1496
+ const checker = {
1497
+ addressIsMapped: vi.fn().mockResolvedValue(false)
1498
+ };
1499
+ const mockTx = {
1500
+ signSubmitAndWatch: (_signer, _options) => ({
1501
+ subscribe: (handlers) => {
1502
+ queueMicrotask(() => {
1503
+ handlers.next({ type: "signed", txHash: "0xabc" });
1504
+ handlers.next({
1505
+ type: "txBestBlocksState",
1506
+ txHash: "0xabc",
1507
+ found: true,
1508
+ ok: true,
1509
+ events: [],
1510
+ block: { hash: "0xblock", number: 1, index: 0 }
1511
+ });
1512
+ });
1513
+ return { unsubscribe: () => {
1514
+ } };
1515
+ }
1516
+ })
1517
+ };
1518
+ const api = {
1519
+ tx: { Revive: { map_account: () => mockTx } }
1520
+ };
1521
+ const result = await ensureAccountMapped("5Alice", mockSigner, checker, api);
1522
+ expect(result).not.toBeNull();
1523
+ expect(result.ok).toBe(true);
1524
+ });
1525
+ test("calls onStatus through full mapping flow", async () => {
1526
+ const statuses = [];
1527
+ const checker = {
1528
+ addressIsMapped: vi.fn().mockResolvedValue(false)
1529
+ };
1530
+ const mockTx = {
1531
+ signSubmitAndWatch: (_signer, _options) => ({
1532
+ subscribe: (handlers) => {
1533
+ queueMicrotask(() => {
1534
+ handlers.next({ type: "signed", txHash: "0xabc" });
1535
+ handlers.next({
1536
+ type: "txBestBlocksState",
1537
+ txHash: "0xabc",
1538
+ found: true,
1539
+ ok: true,
1540
+ events: [],
1541
+ block: { hash: "0xblock", number: 1, index: 0 }
1542
+ });
1543
+ });
1544
+ return { unsubscribe: () => {
1545
+ } };
1546
+ }
1547
+ })
1548
+ };
1549
+ const api = {
1550
+ tx: { Revive: { map_account: () => mockTx } }
1551
+ };
1552
+ await ensureAccountMapped("5Alice", mockSigner, checker, api, {
1553
+ onStatus: (s) => statuses.push(s)
1554
+ });
1555
+ expect(statuses).toEqual(["checking", "mapping", "mapped"]);
1556
+ });
1557
+ test("propagates TxDispatchError from submitAndWatch", async () => {
1558
+ const { TxDispatchError: TxDispatchError2 } = await null;
1559
+ const checker = {
1560
+ addressIsMapped: vi.fn().mockResolvedValue(false)
1561
+ };
1562
+ const mockTx = {
1563
+ signSubmitAndWatch: (_signer, _options) => ({
1564
+ subscribe: (handlers) => {
1565
+ queueMicrotask(() => {
1566
+ handlers.next({
1567
+ type: "txBestBlocksState",
1568
+ txHash: "0xabc",
1569
+ found: true,
1570
+ ok: false,
1571
+ events: [],
1572
+ block: { hash: "0xblock", number: 1, index: 0 },
1573
+ dispatchError: { type: "BadOrigin" }
1574
+ });
1575
+ });
1576
+ return { unsubscribe: () => {
1577
+ } };
1578
+ }
1579
+ })
1580
+ };
1581
+ const api = {
1582
+ tx: { Revive: { map_account: () => mockTx } }
1583
+ };
1584
+ await expect(ensureAccountMapped("5Alice", mockSigner, checker, api)).rejects.toThrow(
1585
+ TxDispatchError2
1586
+ );
1587
+ });
1588
+ test("propagates TxTimeoutError from submitAndWatch", async () => {
1589
+ const { TxTimeoutError: TxTimeoutError2 } = await null;
1590
+ const checker = {
1591
+ addressIsMapped: vi.fn().mockResolvedValue(false)
1592
+ };
1593
+ const mockTx = {
1594
+ signSubmitAndWatch: (_signer, _options) => ({
1595
+ subscribe: () => ({ unsubscribe: () => {
1596
+ } })
1597
+ })
1598
+ };
1599
+ const api = {
1600
+ tx: { Revive: { map_account: () => mockTx } }
1601
+ };
1602
+ await expect(
1603
+ ensureAccountMapped("5Alice", mockSigner, checker, api, { timeoutMs: 50 })
1604
+ ).rejects.toThrow(TxTimeoutError2);
1605
+ });
1606
+ });
1607
+ describe("isAccountMapped", () => {
1608
+ test("returns true when mapped", async () => {
1609
+ const checker = {
1610
+ addressIsMapped: vi.fn().mockResolvedValue(true)
1611
+ };
1612
+ expect(await isAccountMapped("5Alice", checker)).toBe(true);
1613
+ });
1614
+ test("returns false when not mapped", async () => {
1615
+ const checker = {
1616
+ addressIsMapped: vi.fn().mockResolvedValue(false)
1617
+ };
1618
+ expect(await isAccountMapped("5Alice", checker)).toBe(false);
1619
+ });
1620
+ test("throws TxAccountMappingError on failure", async () => {
1621
+ const checker = {
1622
+ addressIsMapped: vi.fn().mockRejectedValue(new Error("timeout"))
1623
+ };
1624
+ await expect(isAccountMapped("5Alice", checker)).rejects.toThrow(TxAccountMappingError);
1625
+ });
1626
+ });
1627
+ }
1628
+ export {
1629
+ TxAccountMappingError,
1630
+ TxBatchError,
1631
+ TxDispatchError,
1632
+ TxDryRunError,
1633
+ TxError,
1634
+ TxSigningRejectedError,
1635
+ TxTimeoutError,
1636
+ applyWeightBuffer,
1637
+ batchSubmitAndWatch,
1638
+ calculateDelay,
1639
+ createDevSigner,
1640
+ ensureAccountMapped,
1641
+ extractTransaction,
1642
+ formatDispatchError,
1643
+ formatDryRunError,
1644
+ getDevPublicKey,
1645
+ isAccountMapped,
1646
+ isSigningRejection,
1647
+ submitAndWatch,
1648
+ withRetry
1649
+ };
1650
+ //# sourceMappingURL=index.js.map