@bananapus/nana-sdk-core 4.0.0 → 5.0.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 (138) hide show
  1. package/bin/wallet-check.mjs +7 -1
  2. package/dist/cjs/review/confirm.d.ts +4 -0
  3. package/dist/cjs/review/confirm.d.ts.map +1 -1
  4. package/dist/cjs/review/confirm.js +14 -3
  5. package/dist/cjs/review/confirm.js.map +1 -1
  6. package/dist/cjs/review/contractWrite.d.ts.map +1 -1
  7. package/dist/cjs/review/contractWrite.js +2 -1
  8. package/dist/cjs/review/contractWrite.js.map +1 -1
  9. package/dist/cjs/review/heldWrite.d.ts +4 -0
  10. package/dist/cjs/review/heldWrite.d.ts.map +1 -1
  11. package/dist/cjs/review/heldWrite.js +31 -28
  12. package/dist/cjs/review/heldWrite.js.map +1 -1
  13. package/dist/cjs/review/index.d.ts +1 -1
  14. package/dist/cjs/review/index.d.ts.map +1 -1
  15. package/dist/cjs/review/index.js +6 -2
  16. package/dist/cjs/review/index.js.map +1 -1
  17. package/dist/cjs/review/lineError.d.ts +38 -0
  18. package/dist/cjs/review/lineError.d.ts.map +1 -1
  19. package/dist/cjs/review/lineError.js +35 -1
  20. package/dist/cjs/review/lineError.js.map +1 -1
  21. package/dist/cjs/review/plan.d.ts +0 -2
  22. package/dist/cjs/review/plan.d.ts.map +1 -1
  23. package/dist/cjs/review/plan.js +0 -6
  24. package/dist/cjs/review/plan.js.map +1 -1
  25. package/dist/cjs/review/react/useHeldSend.js +3 -4
  26. package/dist/cjs/review/react/useHeldSend.js.map +1 -1
  27. package/dist/cjs/review/react/useHeldWrite.d.ts.map +1 -1
  28. package/dist/cjs/review/react/useHeldWrite.js +2 -1
  29. package/dist/cjs/review/react/useHeldWrite.js.map +1 -1
  30. package/dist/cjs/review/react/useReviewedRun.d.ts +29 -3
  31. package/dist/cjs/review/react/useReviewedRun.d.ts.map +1 -1
  32. package/dist/cjs/review/react/useReviewedRun.js +219 -37
  33. package/dist/cjs/review/react/useReviewedRun.js.map +1 -1
  34. package/dist/cjs/review/receipt.d.ts +33 -0
  35. package/dist/cjs/review/receipt.d.ts.map +1 -1
  36. package/dist/cjs/review/receipt.js +69 -0
  37. package/dist/cjs/review/receipt.js.map +1 -1
  38. package/dist/cjs/review/relayr.d.ts.map +1 -1
  39. package/dist/cjs/review/relayr.js +1 -0
  40. package/dist/cjs/review/relayr.js.map +1 -1
  41. package/dist/cjs/review/relayrSend.d.ts +3 -32
  42. package/dist/cjs/review/relayrSend.d.ts.map +1 -1
  43. package/dist/cjs/review/relayrSend.js +78 -226
  44. package/dist/cjs/review/relayrSend.js.map +1 -1
  45. package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
  46. package/dist/cjs/review/safeRelayr.js +4 -8
  47. package/dist/cjs/review/safeRelayr.js.map +1 -1
  48. package/dist/cjs/review/safeSend.js +3 -5
  49. package/dist/cjs/review/safeSend.js.map +1 -1
  50. package/dist/cjs/review/send.d.ts +38 -7
  51. package/dist/cjs/review/send.d.ts.map +1 -1
  52. package/dist/cjs/review/send.js +267 -121
  53. package/dist/cjs/review/send.js.map +1 -1
  54. package/dist/cjs/review/sign.d.ts +6 -4
  55. package/dist/cjs/review/sign.d.ts.map +1 -1
  56. package/dist/cjs/review/sign.js +22 -18
  57. package/dist/cjs/review/sign.js.map +1 -1
  58. package/dist/cjs/review/wallet.d.ts +18 -2
  59. package/dist/cjs/review/wallet.d.ts.map +1 -1
  60. package/dist/cjs/review/wallet.js +127 -50
  61. package/dist/cjs/review/wallet.js.map +1 -1
  62. package/dist/cjs/review/writeHold.d.ts +38 -1
  63. package/dist/cjs/review/writeHold.d.ts.map +1 -1
  64. package/dist/cjs/review/writeHold.js +37 -5
  65. package/dist/cjs/review/writeHold.js.map +1 -1
  66. package/dist/cjs/review/writeRecovery.d.ts +11 -0
  67. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  68. package/dist/cjs/review/writeRecovery.js +38 -5
  69. package/dist/cjs/review/writeRecovery.js.map +1 -1
  70. package/dist/esm/review/confirm.d.ts +4 -0
  71. package/dist/esm/review/confirm.d.ts.map +1 -1
  72. package/dist/esm/review/confirm.js +13 -3
  73. package/dist/esm/review/confirm.js.map +1 -1
  74. package/dist/esm/review/contractWrite.d.ts.map +1 -1
  75. package/dist/esm/review/contractWrite.js +3 -2
  76. package/dist/esm/review/contractWrite.js.map +1 -1
  77. package/dist/esm/review/heldWrite.d.ts +4 -0
  78. package/dist/esm/review/heldWrite.d.ts.map +1 -1
  79. package/dist/esm/review/heldWrite.js +30 -27
  80. package/dist/esm/review/heldWrite.js.map +1 -1
  81. package/dist/esm/review/index.d.ts +1 -1
  82. package/dist/esm/review/index.d.ts.map +1 -1
  83. package/dist/esm/review/index.js +1 -1
  84. package/dist/esm/review/index.js.map +1 -1
  85. package/dist/esm/review/lineError.d.ts +38 -0
  86. package/dist/esm/review/lineError.d.ts.map +1 -1
  87. package/dist/esm/review/lineError.js +33 -1
  88. package/dist/esm/review/lineError.js.map +1 -1
  89. package/dist/esm/review/plan.d.ts +0 -2
  90. package/dist/esm/review/plan.d.ts.map +1 -1
  91. package/dist/esm/review/plan.js +0 -5
  92. package/dist/esm/review/plan.js.map +1 -1
  93. package/dist/esm/review/react/useHeldSend.js +3 -4
  94. package/dist/esm/review/react/useHeldSend.js.map +1 -1
  95. package/dist/esm/review/react/useHeldWrite.d.ts.map +1 -1
  96. package/dist/esm/review/react/useHeldWrite.js +2 -1
  97. package/dist/esm/review/react/useHeldWrite.js.map +1 -1
  98. package/dist/esm/review/react/useReviewedRun.d.ts +29 -3
  99. package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -1
  100. package/dist/esm/review/react/useReviewedRun.js +220 -38
  101. package/dist/esm/review/react/useReviewedRun.js.map +1 -1
  102. package/dist/esm/review/receipt.d.ts +33 -0
  103. package/dist/esm/review/receipt.d.ts.map +1 -1
  104. package/dist/esm/review/receipt.js +66 -0
  105. package/dist/esm/review/receipt.js.map +1 -1
  106. package/dist/esm/review/relayr.d.ts.map +1 -1
  107. package/dist/esm/review/relayr.js +2 -1
  108. package/dist/esm/review/relayr.js.map +1 -1
  109. package/dist/esm/review/relayrSend.d.ts +3 -32
  110. package/dist/esm/review/relayrSend.d.ts.map +1 -1
  111. package/dist/esm/review/relayrSend.js +85 -232
  112. package/dist/esm/review/relayrSend.js.map +1 -1
  113. package/dist/esm/review/safeRelayr.d.ts.map +1 -1
  114. package/dist/esm/review/safeRelayr.js +4 -8
  115. package/dist/esm/review/safeRelayr.js.map +1 -1
  116. package/dist/esm/review/safeSend.js +4 -6
  117. package/dist/esm/review/safeSend.js.map +1 -1
  118. package/dist/esm/review/send.d.ts +38 -7
  119. package/dist/esm/review/send.d.ts.map +1 -1
  120. package/dist/esm/review/send.js +270 -125
  121. package/dist/esm/review/send.js.map +1 -1
  122. package/dist/esm/review/sign.d.ts +6 -4
  123. package/dist/esm/review/sign.d.ts.map +1 -1
  124. package/dist/esm/review/sign.js +24 -20
  125. package/dist/esm/review/sign.js.map +1 -1
  126. package/dist/esm/review/wallet.d.ts +18 -2
  127. package/dist/esm/review/wallet.d.ts.map +1 -1
  128. package/dist/esm/review/wallet.js +128 -51
  129. package/dist/esm/review/wallet.js.map +1 -1
  130. package/dist/esm/review/writeHold.d.ts +38 -1
  131. package/dist/esm/review/writeHold.d.ts.map +1 -1
  132. package/dist/esm/review/writeHold.js +37 -6
  133. package/dist/esm/review/writeHold.js.map +1 -1
  134. package/dist/esm/review/writeRecovery.d.ts +11 -0
  135. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  136. package/dist/esm/review/writeRecovery.js +38 -5
  137. package/dist/esm/review/writeRecovery.js.map +1 -1
  138. package/package.json +1 -1
@@ -1,21 +1,24 @@
1
- import { ReviewedCallRevertedError, ReviewLineError } from "./lineError.js";
1
+ import { ReviewedCallRevertedError, ReviewedReceiptUnverifiedError, ReviewLineError, WalletNotInvokedError, } from "./lineError.js";
2
2
  import { encodeFunctionData, formatEther, } from "viem";
3
- import { promptReview } from "./confirm.js";
3
+ import { failedSimulation, promptReview, } from "./confirm.js";
4
4
  import { isDefiniteWalletRejection, SubmittedContractWriteError, submitReviewedContractWrite, } from "./contractWrite.js";
5
5
  import { JB_CHAINS } from "../constants.js";
6
6
  import { gasWithHeadroom } from "./gas.js";
7
+ import { FOLLOW_MS, followHeldWrite, RECEIPT_RETRY_MS } from "./heldWrite.js";
8
+ import { TransactionReviewCancelledError } from "./reviewQueue.js";
9
+ import { pause } from "../pause.js";
7
10
  import { findPendingSafeAppProposal, hasSafeService, isSafeExecTransaction, } from "../safeService.js";
8
11
  import { REVIEWED_ACCOUNT_CHANGED_LINE, WRITE_HOLD_CALL_FAILED_LINE, WRITE_HOLD_SAFE_UNCONFIRMED_LINE, } from "./messages.js";
9
12
  import { buildReviewModel, enrichReviewModel, } from "./model.js";
10
13
  import { mintPlan, relayrStandInSender, REVIEWED_PLAN_REFUSED_LINE, spendPlan, } from "./plan.js";
11
- import { waitForTrackedReceipt } from "./receipt.js";
14
+ import { heldBlock, heldReceipt, tell, waitForTrackedReceipt, } from "./receipt.js";
12
15
  import { relayrPaymentChains } from "./relayr.js";
13
16
  import { relayrFeeCap } from "./relayrSend.js";
14
17
  import { assertSavableCalls, readReviewedSequence, removeReviewedSequence, REVIEWED_SEQUENCE_MOVED_LINE, REVIEWED_SEQUENCE_UNCERTAIN_LINE, REVIEWED_SEQUENCE_UNFINISHED_LINE, saveReviewedSequence, } from "./sequence.js";
15
18
  import { plannedCallsOf, prepareSignature, signPrepared, } from "./sign.js";
16
19
  import { isRevert, REVERT_UNKNOWN_LINE, revertDetail, revertReason, simulateCallSequence, simulateStateChangingTransaction, simulationFailure, } from "./simulation.js";
17
20
  import { unsupportedWalletRequest, walletReady, } from "./wallet.js";
18
- import { openStepHold, safeBatchCall, WriteHeldError, WriteHoldStaleReviewError, } from "./writeHold.js";
21
+ import { openStepHold, safeBatchCall, WriteHeldError, WriteHoldStaleReviewError, WriteHoldUnacknowledgedSuccessError, } from "./writeHold.js";
19
22
  import { createHeldWriteStorage, } from "./writeRecovery.js";
20
23
  const CHANGED = "Transaction data changed after review. Nothing was sent; review it again.";
21
24
  const MISMATCHED = "This call's data does not match its arguments. Nothing was sent.";
@@ -188,14 +191,15 @@ funded) {
188
191
  if (!isRevert(error))
189
192
  return { status: "unavailable", message: SIMULATION_UNAVAILABLE };
190
193
  const reason = revertReason(error);
191
- return {
194
+ // A sequence's failure is the SDK's line naming its call.
195
+ const failure = error instanceof ReviewLineError
196
+ ? error
197
+ : simulationFailure(`${labelOf(calls[steps[0]], steps[0])} cannot run on ${chainName}: ${reason ?? REVERT_UNKNOWN_LINE}`, error);
198
+ return failedSimulation({
192
199
  status: "failed",
193
- // A sequence's failure is the SDK's line naming its call.
194
- message: error instanceof ReviewLineError
195
- ? error.message
196
- : `${labelOf(calls[steps[0]], steps[0])} cannot run on ${chainName}: ${reason ?? REVERT_UNKNOWN_LINE}`,
200
+ message: failure.message,
197
201
  explained: reason !== null,
198
- };
202
+ }, failure);
199
203
  }
200
204
  });
201
205
  // A revert on one chain is reported at once, whatever the others still wait on.
@@ -237,8 +241,8 @@ async function promptSimulated(model, fromOf, calls, run, blocks, clientFor, sig
237
241
  * One review of calls an app then sends one by one, across chains and
238
242
  * paths: core's prompt over exactly `action.calls`, simulated as their
239
243
  * senders, and nothing else. The plan it returns lets {@link sendReviewed},
240
- * `signReviewed`, `proposeReviewedSafeTransaction`, `payRelayrReviewed` and
241
- * `runRelayrReviewed` skip their prompt for a call identical to one of its
244
+ * `signReviewed`, `proposeReviewedSafeTransaction` and `payRelayrReviewed`
245
+ * skip their prompt for a call identical to one of its
242
246
  * calls (account, chain, sender, target, data, value and gas: a call's
243
247
  * exact gas limit, or a forward request's signed gas, is set before the
244
248
  * review), each spent once, for {@link REVIEWED_PLAN_TTL_MS} after confirm.
@@ -375,38 +379,21 @@ export async function resumeReviewed(record, action, deps) {
375
379
  // wallet") or nothing was sent: then the step is reviewed and sent again.
376
380
  if (!state.settled && !handle.released(reservation))
377
381
  throw new ReviewLineError(REVIEWED_SEQUENCE_UNCERTAIN_LINE);
378
- delete saved.sending;
379
- delete saved.proposed;
380
382
  const { settled } = state;
381
- if (settled?.result === "success") {
382
- // This sequence's own step: it is recorded, never offered again.
383
+ // Read before anything advances: a failed read leaves the step to resume again.
384
+ const receipt = await confirmedReceipt(settled, deps.clientFor(call.chainId));
385
+ // This sequence's own step: it is recorded, told, and only then
386
+ // acknowledged, so it is never offered again.
387
+ const kept = await recordSending(saved, settled, storage, expected);
388
+ await tellConfirmed(action, handle, call, step, settled, receipt);
389
+ if (settled?.result === "success")
383
390
  await handle.acknowledge(settled);
384
- saved.completed.push(settled.hash);
385
- }
386
- /** Tells the site of the step that went through while away, once saved and when its receipt reads. */
387
- const told = async () => {
388
- if (settled?.result !== "success" || !action.onConfirmed)
389
- return;
390
- try {
391
- const receipt = await deps
392
- .clientFor(call.chainId)
393
- .getTransactionReceipt({ hash: settled.hash });
394
- if (receipt.status === "success")
395
- await action.onConfirmed(call, step, receipt);
396
- }
397
- catch {
398
- /* The step went through whatever the read or the site's callback does. */
399
- }
400
- };
401
- if (saved.completed.length === saved.calls.length) {
402
- await removeReviewedSequence(saved, storage, expected);
403
- await told();
391
+ if (kept === null) {
404
392
  const total = saved.calls.length;
405
- deps.onProgress?.({ step: total - 1, total, phase: "done" });
393
+ tell(deps.onProgress, { step: total - 1, total, phase: "done" });
406
394
  return { status: "done", hashes: saved.completed, receipts: [] };
407
395
  }
408
- expected = await saveReviewedSequence(saved, storage, expected);
409
- await told();
396
+ expected = kept;
410
397
  }
411
398
  return send({
412
399
  ...action,
@@ -415,8 +402,133 @@ export async function resumeReviewed(record, action, deps) {
415
402
  scope: saved.scope,
416
403
  }, deps, { record: saved, storage, raw: expected });
417
404
  }
405
+ /**
406
+ * Ends `saved`'s step in flight as its hold settled it: a success is
407
+ * recorded as completed. Removes a finished sequence (null) or saves it over
408
+ * `expected`, returning the value written.
409
+ */
410
+ async function recordSending(saved, settled, storage, expected) {
411
+ delete saved.sending;
412
+ delete saved.proposed;
413
+ if (settled?.result === "success")
414
+ saved.completed.push(settled.hash);
415
+ if (saved.completed.length < saved.calls.length)
416
+ return saveReviewedSequence(saved, storage, expected);
417
+ await removeReviewedSequence(saved, storage, expected);
418
+ return null;
419
+ }
420
+ /**
421
+ * The receipt of a step its hold settled ({@link heldReceipt}), checked
422
+ * before anything advances: a read that fails or disagrees with the hold
423
+ * throws. Null when the settlement has no transaction (an expiry).
424
+ */
425
+ async function confirmedReceipt(settled, client, given) {
426
+ return settled && "hash" in settled
427
+ ? heldReceipt(settled, client, given)
428
+ : null;
429
+ }
430
+ /**
431
+ * Tells `onConfirmed` of a step's verified success, once per settlement:
432
+ * only the completion (a run, `completeReviewedStep`, `resumeReviewed`, in
433
+ * any tab) that claims its notice on the hold tells it.
434
+ */
435
+ async function tellConfirmed(action, handle, call, step, settled, receipt) {
436
+ if (settled?.result === "success" &&
437
+ receipt &&
438
+ (await handle.claimConfirmed(settled)))
439
+ await tell(action.onConfirmed, call, step, receipt);
440
+ }
441
+ /**
442
+ * A sent step's settlement by its own reservation, and the receipt its hold
443
+ * verified ({@link heldReceipt}), read again until it reads. Null on `follow`'s
444
+ * timeout or signal, or when the hold cleared without settling it (its exit).
445
+ */
446
+ async function followSent(handle, client, reserved, follow) {
447
+ const stop = new AbortController();
448
+ const timer = setTimeout(() => stop.abort(), follow.timeoutMs ?? FOLLOW_MS);
449
+ const signal = follow.signal
450
+ ? AbortSignal.any([stop.signal, follow.signal])
451
+ : stop.signal;
452
+ // Every read races the follow's end: a read that answers after it is ignored.
453
+ const ended = new Promise((resolve) => {
454
+ signal.addEventListener("abort", () => resolve(null), { once: true });
455
+ });
456
+ const read = (work) => Promise.race([work, ended]);
457
+ try {
458
+ while (!signal.aborted) {
459
+ const state = await read(followHeldWrite(handle, client, { signal }));
460
+ if (signal.aborted)
461
+ return null;
462
+ if (state?.status !== "clear" ||
463
+ state.reservation !== reserved ||
464
+ !state.settled ||
465
+ !("hash" in state.settled))
466
+ return null;
467
+ const { settled } = state;
468
+ try {
469
+ const receipt = await read(heldReceipt(settled, client));
470
+ return signal.aborted || !receipt ? null : { settled, receipt };
471
+ }
472
+ catch {
473
+ await pause(RECEIPT_RETRY_MS, signal, () => null).catch(() => null);
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ finally {
479
+ clearTimeout(timer);
480
+ }
481
+ }
482
+ /**
483
+ * Completes a sent step (one a run left at its receipt,
484
+ * {@link ReviewedReceiptUnverifiedError}, or a proposal: its step and its
485
+ * `held` call and reservation) once its hold settled that send, as the run
486
+ * itself would have: the saved sequence under `scope` records it, a success
487
+ * is told to `onConfirmed` with the receipt its hold verified (`receipt`,
488
+ * the hold view's, say, when it is that one) and only then acknowledged, the
489
+ * one acknowledgement of a followed step. Returns the settlement and that
490
+ * checked receipt (a failure's too); null until then. A read that fails or disagrees with the hold throws with
491
+ * nothing advanced: call it again. Call it once per settlement:
492
+ * `onConfirmed` is told each time.
493
+ */
494
+ export async function completeReviewedStep(unverified, action, deps, receipt) {
495
+ const { step, held } = unverified;
496
+ if (!held)
497
+ return null;
498
+ const { call, reservation } = held;
499
+ const client = deps.clientFor(call.chainId);
500
+ const handle = openStepHold(deps.hold, action.account, false, call);
501
+ const state = await handle.state(client);
502
+ if (state.status !== "clear" ||
503
+ state.reservation !== reservation ||
504
+ !state.settled)
505
+ return null;
506
+ const { settled } = state;
507
+ const confirmed = await confirmedReceipt(settled, client, receipt);
508
+ if (action.scope !== undefined) {
509
+ const storage = deps.storage ?? createHeldWriteStorage();
510
+ const { raw, record } = readReviewedSequence(action.scope, action.account, storage);
511
+ if (record?.sending?.step === step &&
512
+ record.sending.reservation === reservation)
513
+ await recordSending(record, settled, storage, raw);
514
+ }
515
+ await tellConfirmed(action, handle, call, step, settled, confirmed);
516
+ if (settled.result === "success")
517
+ await handle.acknowledge(settled);
518
+ return { settled, receipt: confirmed };
519
+ }
520
+ /**
521
+ * The hash a wallet returned, even when the site's record of it failed
522
+ * afterwards: the step was sent.
523
+ */
524
+ function sentHash(error) {
525
+ if (error instanceof SubmittedContractWriteError)
526
+ return error.hash;
527
+ throw error;
528
+ }
418
529
  async function send(action, deps, resuming) {
419
- const { adapter, clientFor, hold, onProgress } = deps;
530
+ const { adapter, clientFor, hold } = deps;
531
+ const onProgress = (progress) => tell(deps.onProgress, progress);
420
532
  action.guard?.();
421
533
  const { account } = action;
422
534
  const calls = [...action.calls];
@@ -427,14 +539,7 @@ async function send(action, deps, resuming) {
427
539
  /** Each step's simulation block, read once; a resumed run's prerequisites are long confirmed. */
428
540
  const blocks = calls.map((call) => resuming ? undefined : call.simulationBlockNumber);
429
541
  /** A site's release callback never stops core's own release. */
430
- const tellUnsent = async (call, step) => {
431
- try {
432
- await action.onUnsent?.(call, step);
433
- }
434
- catch {
435
- /* The step is released whatever the site's callback does. */
436
- }
437
- };
542
+ const tellUnsent = (call, step) => tell(action.onUnsent, call, step);
438
543
  const connected = adapter.account();
439
544
  if (!connected)
440
545
  throw new ReviewLineError("Connect a wallet first.");
@@ -489,20 +594,12 @@ async function send(action, deps, resuming) {
489
594
  await tellUnsent(call, step);
490
595
  throw error;
491
596
  }
492
- // Every step is told; a site's failure to record it never reads as unsent.
493
- const failures = [];
597
+ // Every step is told; a site's failure to record it never changes what was sent.
494
598
  for (const [step, call] of calls.entries())
495
- try {
496
- await action.onSubmitted?.(call, step, hash, {
497
- safe: false,
498
- adopted: false,
499
- });
500
- }
501
- catch (error) {
502
- failures.push(error);
503
- }
504
- if (failures.length)
505
- throw new SubmittedContractWriteError(hash, failures[0]);
599
+ await tell(action.onSubmitted, call, step, hash, {
600
+ safe: false,
601
+ adopted: false,
602
+ });
506
603
  return { status: "done", hashes: [hash], receipts: [] };
507
604
  }
508
605
  const { scope } = action;
@@ -543,6 +640,10 @@ async function send(action, deps, resuming) {
543
640
  if (state.status !== "clear")
544
641
  throw new WriteHeldError(state);
545
642
  }
643
+ // So is one whose last success nothing showed: a resend may pay twice.
644
+ const away = handles[handles.length - 1].unacknowledgedSuccess();
645
+ if (away)
646
+ throw new WriteHoldUnacknowledgedSuccessError(away);
546
647
  /**
547
648
  * Each step's latest reservation as the review opened. Another prompt of
548
649
  * the same action that reserved meanwhile, whether or not it has settled,
@@ -615,7 +716,7 @@ async function send(action, deps, resuming) {
615
716
  if (!sameCall(now, sent[step]))
616
717
  throw new ReviewLineError(CHANGED);
617
718
  });
618
- const progress = (phase, hash) => onProgress?.({
719
+ const progress = (phase, hash) => onProgress({
619
720
  step: 0,
620
721
  total: calls.length,
621
722
  phase,
@@ -623,7 +724,8 @@ async function send(action, deps, resuming) {
623
724
  });
624
725
  /** The steps whose `beforeWallet` began. */
625
726
  let began = 0;
626
- const hash = await handles[0].send(client, (gate) => {
727
+ const hash = await handles[0]
728
+ .send(client, (gate) => {
627
729
  if (generation(handles[0]) !== reviewedGenerations[0]) {
628
730
  throw new WriteHoldStaleReviewError();
629
731
  }
@@ -686,19 +788,11 @@ async function send(action, deps, resuming) {
686
788
  onWriteSubmitted: async (submitted) => {
687
789
  gate.afterSubmit(submitted);
688
790
  // Every step went: each is told, whatever an earlier callback does.
689
- const failures = [];
690
791
  for (const [step, call] of calls.entries())
691
- try {
692
- await action.onSubmitted?.(call, step, submitted, {
693
- safe: true,
694
- adopted: false,
695
- });
696
- }
697
- catch (error) {
698
- failures.push(error);
699
- }
700
- if (failures.length)
701
- throw failures[0];
792
+ await tell(action.onSubmitted, call, step, submitted, {
793
+ safe: true,
794
+ adopted: false,
795
+ });
702
796
  },
703
797
  write: async () => {
704
798
  const id = await adapter.sendCalls(batched.chainId, sent.map(({ to, data, value }) => ({ to, data, value })));
@@ -710,7 +804,8 @@ async function send(action, deps, resuming) {
710
804
  onPhase: (phase) => progress(phase),
711
805
  guard: action.guard,
712
806
  });
713
- });
807
+ })
808
+ .catch(sentHash);
714
809
  progress("proposed", hash);
715
810
  return { status: "proposed", step: 0, safeTxHash: hash, ...signed };
716
811
  }
@@ -759,7 +854,7 @@ async function send(action, deps, resuming) {
759
854
  const reviewed = sent[step];
760
855
  /** This step's index in the whole action. */
761
856
  const at = offset + step;
762
- const progress = (phase, hash) => onProgress?.({ step: at, total, phase, ...(hash ? { hash } : {}) });
857
+ const progress = (phase, hash) => onProgress({ step: at, total, phase, ...(hash ? { hash } : {}) });
763
858
  const unchanged = () => {
764
859
  let now;
765
860
  try {
@@ -786,20 +881,20 @@ async function send(action, deps, resuming) {
786
881
  const done = resuming?.record;
787
882
  if (step > 0 || !done?.completed.length)
788
883
  return undefined;
789
- const { chainId } = done.calls[done.completed.length - 1];
790
- // One read: a receipt that can't be read now (a repriced original) is no block.
791
- const receipt = await clientFor(chainId)
792
- .getTransactionReceipt({
793
- hash: done.completed[done.completed.length - 1],
794
- })
795
- .catch(() => undefined);
796
- return receipt
797
- ? {
798
- chainId,
799
- blockNumber: receipt.blockNumber,
800
- blockHash: receipt.blockHash,
801
- }
802
- : undefined;
884
+ const last = done.calls[done.completed.length - 1];
885
+ const hash = done.completed[done.completed.length - 1];
886
+ // Its hold's settlement, while it is still the call's latest, kept
887
+ // the block it verified; else the hash's receipt, canonical and final.
888
+ const held = openStepHold(hold, account, adapter.isSafe(), last).peek();
889
+ const settled = held.status === "clear" &&
890
+ held.settled &&
891
+ "hash" in held.settled &&
892
+ held.settled.hash.toLowerCase() === hash.toLowerCase()
893
+ ? held.settled
894
+ : { result: "success", hash };
895
+ // One try: a block that can't be read or verified now is no block.
896
+ const block = await heldBlock(settled, clientFor(last.chainId)).catch(() => undefined);
897
+ return block && { chainId: last.chainId, ...block };
803
898
  };
804
899
  const priorBlock = call.dependsOnPrior &&
805
900
  prior &&
@@ -810,7 +905,12 @@ async function send(action, deps, resuming) {
810
905
  let reserved;
811
906
  /** A proposal of exactly this call its Safe already queued, sent in its place. */
812
907
  let queued;
813
- const hash = await handles[step].send(client, (gate) => {
908
+ /** The caller's end of the run, once this run sent a step. */
909
+ const runEnd = hashes.length ? action.follow?.signal : undefined;
910
+ /** The site's failed record of this step's hash: the step was sent, and is held unverified. */
911
+ let unrecorded;
912
+ const hash = await handles[step]
913
+ .send(client, (gate) => {
814
914
  // Under the hold's own check of the call, before anything reserves.
815
915
  if (generation(handles[step]) !== reviewedGenerations[step]) {
816
916
  throw new WriteHoldStaleReviewError();
@@ -901,6 +1001,10 @@ async function send(action, deps, resuming) {
901
1001
  }
902
1002
  },
903
1003
  beforeSend: () => {
1004
+ // The caller ended the run (a reset, an unmount) after it sent a
1005
+ // step: nothing more is sent.
1006
+ if (hashes.length && action.follow?.signal?.aborted)
1007
+ throw new TransactionReviewCancelledError();
904
1008
  action.assertWriteState?.();
905
1009
  unchanged();
906
1010
  if (adapter.isSafe() !== safe) {
@@ -919,16 +1023,32 @@ async function send(action, deps, resuming) {
919
1023
  // An adopted proposal's nonce lets the hold prove its execution onchain.
920
1024
  onWriteSubmitted: async (submitted) => {
921
1025
  gate.afterSubmit(submitted, queued?.tx);
922
- await action.onSubmitted?.(call, at, submitted, {
1026
+ await tell(action.onSubmitted, call, at, submitted, {
923
1027
  safe,
924
1028
  adopted: !!queued,
925
1029
  });
926
1030
  },
927
1031
  write: async ({ chainId, to, data, value, gas }) => queued?.proposalHash ??
928
- adapter.sendTransaction({ chainId, to, data, value, gas }),
1032
+ adapter.sendTransaction({
1033
+ chainId,
1034
+ to,
1035
+ data,
1036
+ value,
1037
+ gas,
1038
+ // After a step was sent, the run's end refuses this one, unsent,
1039
+ // as late as the wallet's own last check.
1040
+ ...(runEnd ? { signal: runEnd } : {}),
1041
+ }),
929
1042
  onPhase: (phase) => progress(phase),
930
1043
  guard: action.guard,
931
1044
  });
1045
+ })
1046
+ .catch((error) => {
1047
+ // Refused unsent because the caller ended the run: a cancel, not a failure.
1048
+ if (runEnd?.aborted && error instanceof WalletNotInvokedError)
1049
+ throw new TransactionReviewCancelledError();
1050
+ unrecorded = error;
1051
+ return sentHash(error);
932
1052
  });
933
1053
  hashes.push(hash);
934
1054
  // A later step held under this step's key (the same call, or the same
@@ -950,21 +1070,56 @@ async function send(action, deps, resuming) {
950
1070
  progress("proposed", hash);
951
1071
  return { status: "proposed", step: at, safeTxHash: hash, ...signed };
952
1072
  }
953
- progress("receipt", hash);
954
- const receipt = await waitForTrackedReceipt(client, hash);
955
- // The hold checks the receipt itself and settles, so the same call (or
956
- // intent) can be reviewed and sent again without waiting on a view.
957
- // Only this send's own reservation's settlement speaks for it: a later
958
- // send of the same call or intent may have settled since.
959
- const settled = await handles[step]
960
- .state(client)
961
- .then((state) => state.status === "clear" &&
962
- reserved !== undefined &&
963
- state.reservation === reserved
964
- ? state.settled
965
- : undefined)
966
- .catch(() => undefined);
967
- if (receipt.status === "success" && settled?.result === "failed") {
1073
+ // From here the step was sent: it ends done, failed as its hold
1074
+ // verified, or unverified with what resumes it, never any other way.
1075
+ const unverified = (cause) => new ReviewedReceiptUnverifiedError(at, hash, isSafeExecTransaction(reviewed.data)
1076
+ ? WRITE_HOLD_SAFE_UNCONFIRMED_LINE
1077
+ : undefined, {
1078
+ call,
1079
+ reservation: reserved,
1080
+ // This run's sends, its own last: only the steps before it completed.
1081
+ hashes: journal ? [...journal.completed] : hashes.slice(0, -1),
1082
+ ...signed,
1083
+ }, cause === undefined ? {} : { cause });
1084
+ // The site could not record the hash: held as sent, for it to resume.
1085
+ if (unrecorded !== undefined)
1086
+ throw unverified(unrecorded);
1087
+ // Its hold's verified settlement is the only word on how it ended: its
1088
+ // result, its transaction (a speed-up's, when the wallet replaced the
1089
+ // send) and that transaction's receipt in the block the hold verified.
1090
+ // A watcher's receipt only says when to ask.
1091
+ let receipt;
1092
+ let settled;
1093
+ try {
1094
+ progress("receipt", hash);
1095
+ const seen = await waitForTrackedReceipt(client, hash);
1096
+ // Only this send's own reservation's settlement speaks for it: a later
1097
+ // send of the same call or intent may have settled since.
1098
+ settled = await handles[step]
1099
+ .state(client)
1100
+ .then((state) => state.status === "clear" &&
1101
+ reserved !== undefined &&
1102
+ state.reservation === reserved &&
1103
+ state.settled &&
1104
+ "hash" in state.settled
1105
+ ? state.settled
1106
+ : undefined)
1107
+ .catch(() => undefined);
1108
+ receipt = settled
1109
+ ? await heldReceipt(settled, client, seen).catch(() => undefined)
1110
+ : undefined;
1111
+ if (!receipt && action.follow) {
1112
+ tell(action.follow.onConfirming, at);
1113
+ const ended = await followSent(handles[step], client, reserved, action.follow);
1114
+ ({ settled, receipt } = ended ?? { settled, receipt });
1115
+ }
1116
+ }
1117
+ catch (error) {
1118
+ throw unverified(error);
1119
+ }
1120
+ if (!settled || !receipt)
1121
+ throw unverified();
1122
+ if (settled.result === "failed" && receipt.status === "success") {
968
1123
  // It ran, and the hold proved it failed: a Safe's call that failed.
969
1124
  if (journal) {
970
1125
  delete journal.sending;
@@ -972,35 +1127,25 @@ async function send(action, deps, resuming) {
972
1127
  }
973
1128
  throw new ReviewLineError(WRITE_HOLD_CALL_FAILED_LINE);
974
1129
  }
975
- // A Safe's execTransaction is done only once its hold proved the Safe's
976
- // own success; until then the step stays in flight, held and journaled.
977
- if (receipt.status === "success" &&
978
- isSafeExecTransaction(reviewed.data) &&
979
- settled?.result !== "success") {
980
- throw new ReviewLineError(WRITE_HOLD_SAFE_UNCONFIRMED_LINE);
981
- }
1130
+ const mined = settled.hash;
1131
+ hashes[hashes.length - 1] = mined;
982
1132
  if (receipt.status !== "success") {
983
1133
  // It ran and reverted: nothing of it to resume.
984
1134
  if (journal) {
985
1135
  delete journal.sending;
986
1136
  await keep();
987
1137
  }
988
- throw new ReviewedCallRevertedError(at, hash, receipt);
1138
+ throw new ReviewedCallRevertedError(at, mined, receipt);
989
1139
  }
990
1140
  receipts.push(receipt);
991
1141
  if (journal) {
992
1142
  delete journal.sending;
993
- journal.completed.push(hash);
1143
+ journal.completed.push(mined);
994
1144
  await keep();
995
1145
  }
996
- try {
997
- await action.onConfirmed?.(call, at, receipt);
998
- }
999
- catch {
1000
- /* The step went through whatever the site's callback does. */
1001
- }
1146
+ await tellConfirmed(action, handles[step], call, at, settled, receipt);
1002
1147
  }
1003
- onProgress?.({ step: total - 1, total, phase: "done" });
1148
+ onProgress({ step: total - 1, total, phase: "done" });
1004
1149
  return {
1005
1150
  status: "done",
1006
1151
  hashes: journal ? [...journal.completed] : hashes,