@bananapus/nana-sdk-core 6.0.0 → 6.2.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 (213) hide show
  1. package/dist/cjs/jbcenter/ensureDeployed.d.ts +2 -2
  2. package/dist/cjs/jbcenter/ensureDeployed.d.ts.map +1 -1
  3. package/dist/cjs/jbcenter/ensureDeployed.js.map +1 -1
  4. package/dist/cjs/jbcenter.d.ts +4 -1
  5. package/dist/cjs/jbcenter.d.ts.map +1 -1
  6. package/dist/cjs/jbcenter.js +6 -6
  7. package/dist/cjs/jbcenter.js.map +1 -1
  8. package/dist/cjs/review/gas.d.ts +5 -2
  9. package/dist/cjs/review/gas.d.ts.map +1 -1
  10. package/dist/cjs/review/gas.js +8 -3
  11. package/dist/cjs/review/gas.js.map +1 -1
  12. package/dist/cjs/review/heldWrite.d.ts +6 -1
  13. package/dist/cjs/review/heldWrite.d.ts.map +1 -1
  14. package/dist/cjs/review/heldWrite.js +21 -16
  15. package/dist/cjs/review/heldWrite.js.map +1 -1
  16. package/dist/cjs/review/index.d.ts +2 -0
  17. package/dist/cjs/review/index.d.ts.map +1 -1
  18. package/dist/cjs/review/index.js +2 -0
  19. package/dist/cjs/review/index.js.map +1 -1
  20. package/dist/cjs/review/locks.d.ts +69 -0
  21. package/dist/cjs/review/locks.d.ts.map +1 -0
  22. package/dist/cjs/review/locks.js +232 -0
  23. package/dist/cjs/review/locks.js.map +1 -0
  24. package/dist/cjs/review/machines.d.ts +9 -0
  25. package/dist/cjs/review/machines.d.ts.map +1 -0
  26. package/dist/cjs/review/machines.js +25 -0
  27. package/dist/cjs/review/machines.js.map +1 -0
  28. package/dist/cjs/review/react/index.d.ts +3 -0
  29. package/dist/cjs/review/react/index.d.ts.map +1 -1
  30. package/dist/cjs/review/react/index.js +3 -0
  31. package/dist/cjs/review/react/index.js.map +1 -1
  32. package/dist/cjs/review/react/useFeeBuybackReview.d.ts +6 -0
  33. package/dist/cjs/review/react/useFeeBuybackReview.d.ts.map +1 -0
  34. package/dist/cjs/review/react/useFeeBuybackReview.js +53 -0
  35. package/dist/cjs/review/react/useFeeBuybackReview.js.map +1 -0
  36. package/dist/cjs/review/react/useMobileWallet.d.ts +15 -0
  37. package/dist/cjs/review/react/useMobileWallet.d.ts.map +1 -0
  38. package/dist/cjs/review/react/useMobileWallet.js +50 -0
  39. package/dist/cjs/review/react/useMobileWallet.js.map +1 -0
  40. package/dist/cjs/review/react/useReviewedRun.d.ts +28 -1
  41. package/dist/cjs/review/react/useReviewedRun.d.ts.map +1 -1
  42. package/dist/cjs/review/react/useReviewedRun.js +251 -39
  43. package/dist/cjs/review/react/useReviewedRun.js.map +1 -1
  44. package/dist/cjs/review/react/useSavedAttempt.d.ts +23 -0
  45. package/dist/cjs/review/react/useSavedAttempt.d.ts.map +1 -1
  46. package/dist/cjs/review/react/useSavedAttempt.js +101 -11
  47. package/dist/cjs/review/react/useSavedAttempt.js.map +1 -1
  48. package/dist/cjs/review/react/useStartOnce.d.ts +8 -0
  49. package/dist/cjs/review/react/useStartOnce.d.ts.map +1 -0
  50. package/dist/cjs/review/react/useStartOnce.js +27 -0
  51. package/dist/cjs/review/react/useStartOnce.js.map +1 -0
  52. package/dist/cjs/review/relayr.d.ts +1 -1
  53. package/dist/cjs/review/relayr.d.ts.map +1 -1
  54. package/dist/cjs/review/relayr.js +15 -0
  55. package/dist/cjs/review/relayr.js.map +1 -1
  56. package/dist/cjs/review/relayrLaunch.d.ts +90 -0
  57. package/dist/cjs/review/relayrLaunch.d.ts.map +1 -0
  58. package/dist/cjs/review/relayrLaunch.js +106 -0
  59. package/dist/cjs/review/relayrLaunch.js.map +1 -0
  60. package/dist/cjs/review/relayrSend.d.ts +5 -0
  61. package/dist/cjs/review/relayrSend.d.ts.map +1 -1
  62. package/dist/cjs/review/relayrSend.js +133 -107
  63. package/dist/cjs/review/relayrSend.js.map +1 -1
  64. package/dist/cjs/review/reviewDialogQueue.d.ts +47 -0
  65. package/dist/cjs/review/reviewDialogQueue.d.ts.map +1 -0
  66. package/dist/cjs/review/reviewDialogQueue.js +120 -0
  67. package/dist/cjs/review/reviewDialogQueue.js.map +1 -0
  68. package/dist/cjs/review/runStatus.d.ts +51 -0
  69. package/dist/cjs/review/runStatus.d.ts.map +1 -0
  70. package/dist/cjs/review/runStatus.js +54 -0
  71. package/dist/cjs/review/runStatus.js.map +1 -0
  72. package/dist/cjs/review/safeRelayr.d.ts +60 -16
  73. package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
  74. package/dist/cjs/review/safeRelayr.js +358 -161
  75. package/dist/cjs/review/safeRelayr.js.map +1 -1
  76. package/dist/cjs/review/savedAttempt.d.ts.map +1 -1
  77. package/dist/cjs/review/savedAttempt.js +7 -2
  78. package/dist/cjs/review/savedAttempt.js.map +1 -1
  79. package/dist/cjs/review/send.d.ts +27 -5
  80. package/dist/cjs/review/send.d.ts.map +1 -1
  81. package/dist/cjs/review/send.js +67 -24
  82. package/dist/cjs/review/send.js.map +1 -1
  83. package/dist/cjs/review/sign.d.ts +6 -1
  84. package/dist/cjs/review/sign.d.ts.map +1 -1
  85. package/dist/cjs/review/sign.js +10 -4
  86. package/dist/cjs/review/sign.js.map +1 -1
  87. package/dist/cjs/review/wallet.d.ts +16 -4
  88. package/dist/cjs/review/wallet.d.ts.map +1 -1
  89. package/dist/cjs/review/wallet.js +60 -17
  90. package/dist/cjs/review/wallet.js.map +1 -1
  91. package/dist/cjs/review/writeHold.d.ts +18 -3
  92. package/dist/cjs/review/writeHold.d.ts.map +1 -1
  93. package/dist/cjs/review/writeHold.js +37 -10
  94. package/dist/cjs/review/writeHold.js.map +1 -1
  95. package/dist/cjs/review/writeRecovery.d.ts +13 -8
  96. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  97. package/dist/cjs/review/writeRecovery.js +67 -144
  98. package/dist/cjs/review/writeRecovery.js.map +1 -1
  99. package/dist/cjs/v6/feeBuyback.d.ts +49 -0
  100. package/dist/cjs/v6/feeBuyback.d.ts.map +1 -1
  101. package/dist/cjs/v6/feeBuyback.js +151 -0
  102. package/dist/cjs/v6/feeBuyback.js.map +1 -1
  103. package/dist/cjs/v6/suckers.d.ts +24 -2
  104. package/dist/cjs/v6/suckers.d.ts.map +1 -1
  105. package/dist/cjs/v6/suckers.js +5 -5
  106. package/dist/cjs/v6/suckers.js.map +1 -1
  107. package/dist/esm/jbcenter/ensureDeployed.d.ts +2 -2
  108. package/dist/esm/jbcenter/ensureDeployed.d.ts.map +1 -1
  109. package/dist/esm/jbcenter/ensureDeployed.js.map +1 -1
  110. package/dist/esm/jbcenter.d.ts +4 -1
  111. package/dist/esm/jbcenter.d.ts.map +1 -1
  112. package/dist/esm/jbcenter.js +6 -6
  113. package/dist/esm/jbcenter.js.map +1 -1
  114. package/dist/esm/review/gas.d.ts +5 -2
  115. package/dist/esm/review/gas.d.ts.map +1 -1
  116. package/dist/esm/review/gas.js +7 -3
  117. package/dist/esm/review/gas.js.map +1 -1
  118. package/dist/esm/review/heldWrite.d.ts +6 -1
  119. package/dist/esm/review/heldWrite.d.ts.map +1 -1
  120. package/dist/esm/review/heldWrite.js +21 -16
  121. package/dist/esm/review/heldWrite.js.map +1 -1
  122. package/dist/esm/review/index.d.ts +2 -0
  123. package/dist/esm/review/index.d.ts.map +1 -1
  124. package/dist/esm/review/index.js +2 -0
  125. package/dist/esm/review/index.js.map +1 -1
  126. package/dist/esm/review/locks.d.ts +69 -0
  127. package/dist/esm/review/locks.d.ts.map +1 -0
  128. package/dist/esm/review/locks.js +225 -0
  129. package/dist/esm/review/locks.js.map +1 -0
  130. package/dist/esm/review/machines.d.ts +9 -0
  131. package/dist/esm/review/machines.d.ts.map +1 -0
  132. package/dist/esm/review/machines.js +9 -0
  133. package/dist/esm/review/machines.js.map +1 -0
  134. package/dist/esm/review/react/index.d.ts +3 -0
  135. package/dist/esm/review/react/index.d.ts.map +1 -1
  136. package/dist/esm/review/react/index.js +3 -0
  137. package/dist/esm/review/react/index.js.map +1 -1
  138. package/dist/esm/review/react/useFeeBuybackReview.d.ts +6 -0
  139. package/dist/esm/review/react/useFeeBuybackReview.d.ts.map +1 -0
  140. package/dist/esm/review/react/useFeeBuybackReview.js +50 -0
  141. package/dist/esm/review/react/useFeeBuybackReview.js.map +1 -0
  142. package/dist/esm/review/react/useMobileWallet.d.ts +15 -0
  143. package/dist/esm/review/react/useMobileWallet.d.ts.map +1 -0
  144. package/dist/esm/review/react/useMobileWallet.js +46 -0
  145. package/dist/esm/review/react/useMobileWallet.js.map +1 -0
  146. package/dist/esm/review/react/useReviewedRun.d.ts +28 -1
  147. package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -1
  148. package/dist/esm/review/react/useReviewedRun.js +252 -40
  149. package/dist/esm/review/react/useReviewedRun.js.map +1 -1
  150. package/dist/esm/review/react/useSavedAttempt.d.ts +23 -0
  151. package/dist/esm/review/react/useSavedAttempt.d.ts.map +1 -1
  152. package/dist/esm/review/react/useSavedAttempt.js +101 -11
  153. package/dist/esm/review/react/useSavedAttempt.js.map +1 -1
  154. package/dist/esm/review/react/useStartOnce.d.ts +8 -0
  155. package/dist/esm/review/react/useStartOnce.d.ts.map +1 -0
  156. package/dist/esm/review/react/useStartOnce.js +24 -0
  157. package/dist/esm/review/react/useStartOnce.js.map +1 -0
  158. package/dist/esm/review/relayr.d.ts +1 -1
  159. package/dist/esm/review/relayr.d.ts.map +1 -1
  160. package/dist/esm/review/relayr.js +1 -0
  161. package/dist/esm/review/relayr.js.map +1 -1
  162. package/dist/esm/review/relayrLaunch.d.ts +90 -0
  163. package/dist/esm/review/relayrLaunch.d.ts.map +1 -0
  164. package/dist/esm/review/relayrLaunch.js +97 -0
  165. package/dist/esm/review/relayrLaunch.js.map +1 -0
  166. package/dist/esm/review/relayrSend.d.ts +5 -0
  167. package/dist/esm/review/relayrSend.d.ts.map +1 -1
  168. package/dist/esm/review/relayrSend.js +134 -108
  169. package/dist/esm/review/relayrSend.js.map +1 -1
  170. package/dist/esm/review/reviewDialogQueue.d.ts +47 -0
  171. package/dist/esm/review/reviewDialogQueue.d.ts.map +1 -0
  172. package/dist/esm/review/reviewDialogQueue.js +117 -0
  173. package/dist/esm/review/reviewDialogQueue.js.map +1 -0
  174. package/dist/esm/review/runStatus.d.ts +51 -0
  175. package/dist/esm/review/runStatus.d.ts.map +1 -0
  176. package/dist/esm/review/runStatus.js +51 -0
  177. package/dist/esm/review/runStatus.js.map +1 -0
  178. package/dist/esm/review/safeRelayr.d.ts +60 -16
  179. package/dist/esm/review/safeRelayr.d.ts.map +1 -1
  180. package/dist/esm/review/safeRelayr.js +358 -161
  181. package/dist/esm/review/safeRelayr.js.map +1 -1
  182. package/dist/esm/review/savedAttempt.d.ts.map +1 -1
  183. package/dist/esm/review/savedAttempt.js +7 -2
  184. package/dist/esm/review/savedAttempt.js.map +1 -1
  185. package/dist/esm/review/send.d.ts +27 -5
  186. package/dist/esm/review/send.d.ts.map +1 -1
  187. package/dist/esm/review/send.js +67 -24
  188. package/dist/esm/review/send.js.map +1 -1
  189. package/dist/esm/review/sign.d.ts +6 -1
  190. package/dist/esm/review/sign.d.ts.map +1 -1
  191. package/dist/esm/review/sign.js +10 -4
  192. package/dist/esm/review/sign.js.map +1 -1
  193. package/dist/esm/review/wallet.d.ts +16 -4
  194. package/dist/esm/review/wallet.d.ts.map +1 -1
  195. package/dist/esm/review/wallet.js +60 -17
  196. package/dist/esm/review/wallet.js.map +1 -1
  197. package/dist/esm/review/writeHold.d.ts +18 -3
  198. package/dist/esm/review/writeHold.d.ts.map +1 -1
  199. package/dist/esm/review/writeHold.js +37 -10
  200. package/dist/esm/review/writeHold.js.map +1 -1
  201. package/dist/esm/review/writeRecovery.d.ts +13 -8
  202. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  203. package/dist/esm/review/writeRecovery.js +63 -142
  204. package/dist/esm/review/writeRecovery.js.map +1 -1
  205. package/dist/esm/v6/feeBuyback.d.ts +49 -0
  206. package/dist/esm/v6/feeBuyback.d.ts.map +1 -1
  207. package/dist/esm/v6/feeBuyback.js +150 -0
  208. package/dist/esm/v6/feeBuyback.js.map +1 -1
  209. package/dist/esm/v6/suckers.d.ts +24 -2
  210. package/dist/esm/v6/suckers.d.ts.map +1 -1
  211. package/dist/esm/v6/suckers.js +5 -5
  212. package/dist/esm/v6/suckers.js.map +1 -1
  213. package/package.json +11 -1
@@ -5,6 +5,7 @@ import { SAFE_EXEC_ABI, canonicalSafeTxHash, requireSafeExecutionSuccess, safeEx
5
5
  import { RELAYR_API, RELAYR_UUID_RE, MAX_RELAYR_SENT_PAYMENTS, atCanonicalFinalizedBlock, bindRelayrQuote, readRelayrBundle, relayrBundleRequest, relayrPaymentOptions, relayrPaymentDetails, relayrPaymentAttemptOutcome, relayrDestinationHash, relayrDestinationHashes, relayrRetryOption, relayrSentPaymentsSnapshot, relayrStateIsPending, relayrStateIsFailed, relayrDeadlinePassed, relayrQuotedOptions, relayrQuoteUnfundable, RelayrPaymentRetryError, requireRelayrBundleUnpaid, requireRelayrRetry, revertedRelayrQuote, verifyRelayrDestinations, verifyRelayrDestination, verifyRelayrPayment, RelayrPaymentRevertedError, RelayrProofUnavailableError, } from "./relayr.js";
6
6
  import { ReviewFixedLineError } from "./lineError.js";
7
7
  import { TAB, tabShared } from "./tabState.js";
8
+ import { assertLocksHeld, LockLostError } from "./locks.js";
8
9
  function hasFundingRecords(records) {
9
10
  if (records === undefined)
10
11
  return false;
@@ -64,6 +65,15 @@ export function canReplaceSafeRelayrQuote(session) {
64
65
  return false;
65
66
  }
66
67
  }
68
+ const SAFE_RELAYR_VIEW = {
69
+ phase: null,
70
+ checks: {},
71
+ executions: {},
72
+ result: null,
73
+ running: null,
74
+ error: null,
75
+ session: null,
76
+ };
67
77
  /** The line a Safe action shows when its payment's status couldn't be read. */
68
78
  export const SAFE_RELAYR_STATUS_UNKNOWN_LINE = "Couldn't check this payment's status. Try again in a moment.";
69
79
  /** The line a Safe action shows when its quote can no longer be paid but hasn't expired onchain. */
@@ -322,14 +332,28 @@ export function createSafeRelayrController(options) {
322
332
  * `fundingObservedAt` another flow saved. Nothing ever clears them; an
323
333
  * exit keeps them for reconciliation.
324
334
  */
335
+ /** The locks each account's `withLock` holds, while it runs. */
336
+ const storeLocks = new Map();
325
337
  const store = {
326
338
  get scope() {
327
339
  return options.store.scope;
328
340
  },
329
341
  list: (account) => options.store.list(account),
330
- withLock: (account, run) => options.store.withLock(account, run),
342
+ withLock: (account, run) => options.store.withLock(account, async (locks) => {
343
+ const key = account.toLowerCase();
344
+ const outer = storeLocks.get(key);
345
+ storeLocks.set(key, locks);
346
+ try {
347
+ return await run(locks);
348
+ }
349
+ finally {
350
+ storeLocks.set(key, outer);
351
+ }
352
+ }),
331
353
  async save(next) {
332
354
  const saved = (await options.store.list(next.account)).find((session) => session.id === next.id);
355
+ // Under the store's lock: still this tab's after every read, right before the write.
356
+ await assertLocksHeld(storeLocks.get(next.account.toLowerCase()));
333
357
  const at = [saved?.fundingObservedAt, next.fundingObservedAt].filter((value) => typeof value === "number");
334
358
  // Once seen, funding stays, and its clock only moves forward.
335
359
  return options.store.save(saved?.fundingObserved || next.fundingObserved
@@ -343,7 +367,23 @@ export function createSafeRelayrController(options) {
343
367
  };
344
368
  const fetchRelayr = options.fetch ?? globalThis.fetch;
345
369
  const displayedExecutions = new Map();
346
- const notify = (progress, rechecking = false) => {
370
+ let view = SAFE_RELAYR_VIEW;
371
+ /** The calls in flight with what each runs, and the last call started before the last reset. */
372
+ const flying = new Map();
373
+ let since = 0;
374
+ /** Each call's own signal, to that call: its progress names the call it came from. */
375
+ const callOf = new WeakMap();
376
+ const listeners = new Set();
377
+ const show = (patch) => {
378
+ view = { ...view, ...patch };
379
+ for (const listener of listeners)
380
+ void tell(listener);
381
+ };
382
+ const notify = (progress, rechecking, signal) => {
383
+ // A call's progress shows once it started since the last reset: a call
384
+ // from before it changes nothing shown. Overlapping calls of one run all show.
385
+ const call = signal && callOf.get(signal);
386
+ const shows = call !== undefined && call > since;
347
387
  if (progress.type === "execution") {
348
388
  const key = `${progress.session.id}:${progress.index}`;
349
389
  const display = JSON.stringify([
@@ -358,15 +398,35 @@ export function createSafeRelayrController(options) {
358
398
  (rechecking &&
359
399
  previous === JSON.stringify(["executed", progress.hash, undefined])))
360
400
  return;
361
- displayedExecutions.set(key, display);
401
+ // Only a shown row is a repeat, so a held one shows from the next call.
402
+ if (shows)
403
+ displayedExecutions.set(key, display);
362
404
  }
405
+ if (shows)
406
+ show({
407
+ ...(progress.session
408
+ ? { session: structuredClone(progress.session) }
409
+ : {}),
410
+ ...(progress.type === "phase"
411
+ ? { phase: progress.phase }
412
+ : {
413
+ executions: {
414
+ ...view.executions,
415
+ [progress.index]: {
416
+ status: progress.status,
417
+ ...(progress.hash ? { hash: progress.hash } : {}),
418
+ ...(progress.message ? { message: progress.message } : {}),
419
+ },
420
+ },
421
+ }),
422
+ });
363
423
  // A view must not mutate frozen calls or turn a submitted payment into a
364
424
  // rejected operation: it gets a copy, and its failure is only logged.
365
425
  void tell((shown) => options.onProgress?.(structuredClone(shown)), progress);
366
426
  };
367
427
  const phase = (phase, session, signal) => {
368
428
  if (!signal?.aborted)
369
- notify({ type: "phase", phase, session });
429
+ notify({ type: "phase", phase, session }, false, signal);
370
430
  };
371
431
  const savedPhase = (session, signal) => {
372
432
  if (session.state === "released")
@@ -744,7 +804,7 @@ export function createSafeRelayrController(options) {
744
804
  async function inspect(session, signal) {
745
805
  const report = (progress, rechecking = false) => {
746
806
  if (!signal?.aborted)
747
- notify(progress, rechecking);
807
+ notify(progress, rechecking, signal);
748
808
  };
749
809
  try {
750
810
  savedPhase(session, signal);
@@ -1159,179 +1219,240 @@ export function createSafeRelayrController(options) {
1159
1219
  });
1160
1220
  }
1161
1221
  async function fund({ account, sessionId, paymentChainId, signal, onStatus, }) {
1162
- return store.withLock(account, async () => {
1163
- checkAccount(account, signal);
1164
- let session = await load(account, sessionId);
1165
- const checked = await inspect(session, signal);
1166
- session = checked.session;
1167
- if (checked.state !== "ready")
1168
- throw new SafeRelayrRecoveryError(session, checked.recovery?.message, checked.recovery);
1169
- if (session.payments.length >= MAX_RELAYR_SENT_PAYMENTS)
1170
- throw new SafeRelayrRecoveryError(session, "This quote has reached its saved payment attempt limit. Check the existing bundle.");
1171
- const payment = checked.payments.find((option) => option.chain === paymentChainId);
1172
- if (!payment)
1173
- throw new Error("Choose a current payment option from this saved quote.");
1174
- await assertUnreserved(account, session.executions, session.id);
1175
- if (!reviewedIntents.has(reviewKey(session))) {
1176
- phase("reviewing", session, signal);
1177
- await options.review(session.executions, { resumed: true });
1178
- checkAccount(account, signal);
1179
- rememberReview(session);
1222
+ // Once this attempt saved "sending", every later save goes through
1223
+ // `keep`: one refused for a lost lock leaves what it would have saved
1224
+ // (the attempt's outcome) to be applied under a fresh lock, to the saved
1225
+ // record only while it is still this attempt's.
1226
+ let late;
1227
+ const keep = async (next, onLost) => {
1228
+ try {
1229
+ await store.save(next);
1180
1230
  }
1181
- let verified = false;
1182
- let sending = false;
1183
- let paymentPersisted = false;
1184
- const requireOpenPayment = () => relayrPaymentDetails(payment, {
1185
- bundleUuid: session.bundleUuid,
1186
- destinationChainIds: session.executions.map(({ entry }) => entry.chain),
1187
- });
1188
- const beforeSend = async () => {
1189
- phase("payment-checking", session, signal);
1190
- await revalidate(session.executions, account, onStatus, true, signal);
1231
+ catch (error) {
1232
+ if (error instanceof LockLostError)
1233
+ late = onLost;
1234
+ throw error;
1235
+ }
1236
+ };
1237
+ try {
1238
+ return await funding();
1239
+ }
1240
+ catch (error) {
1241
+ const settle = late;
1242
+ if (settle)
1243
+ await store
1244
+ .withLock(account, async () => {
1245
+ const next = settle(await load(account, sessionId));
1246
+ if (next)
1247
+ await store.save(next);
1248
+ })
1249
+ // Unreconciled, the record stays as the lost lock left it.
1250
+ .catch(() => undefined);
1251
+ throw error;
1252
+ }
1253
+ async function funding() {
1254
+ return store.withLock(account, async () => {
1255
+ checkAccount(account, signal);
1256
+ let session = await load(account, sessionId);
1257
+ const checked = await inspect(session, signal);
1258
+ session = checked.session;
1259
+ if (checked.state !== "ready")
1260
+ throw new SafeRelayrRecoveryError(session, checked.recovery?.message, checked.recovery);
1261
+ if (session.payments.length >= MAX_RELAYR_SENT_PAYMENTS)
1262
+ throw new SafeRelayrRecoveryError(session, "This quote has reached its saved payment attempt limit. Check the existing bundle.");
1263
+ const payment = checked.payments.find((option) => option.chain === paymentChainId);
1264
+ if (!payment)
1265
+ throw new Error("Choose a current payment option from this saved quote.");
1191
1266
  await assertUnreserved(account, session.executions, session.id);
1192
- await statusRead(() => session.payments.length
1193
- ? requireRelayrRetry(options.clientFor, {
1194
- payments: session.payments,
1195
- from: account,
1196
- bundleUuid: session.bundleUuid,
1197
- }, { fetch: fetchRelayr })
1198
- : requireRelayrBundleUnpaid(session.bundleUuid, {
1199
- fetch: fetchRelayr,
1200
- })).catch(async (error) => {
1201
- if (statusUnknown(error))
1202
- throw unknownStatus(session);
1203
- if (fundingReported(error))
1204
- session = await observed(session);
1205
- throw error;
1267
+ if (!reviewedIntents.has(reviewKey(session))) {
1268
+ phase("reviewing", session, signal);
1269
+ await options.review(session.executions, { resumed: true });
1270
+ checkAccount(account, signal);
1271
+ rememberReview(session);
1272
+ }
1273
+ let verified = false;
1274
+ let sending = false;
1275
+ let paymentPersisted = false;
1276
+ const requireOpenPayment = () => relayrPaymentDetails(payment, {
1277
+ bundleUuid: session.bundleUuid,
1278
+ destinationChainIds: session.executions.map(({ entry }) => entry.chain),
1206
1279
  });
1207
- checkAccount(account, signal);
1208
- requireOpenPayment();
1209
- verified = true;
1210
- };
1211
- const onSending = async () => {
1212
- checkAccount(account, signal);
1213
- if (!verified || sending)
1214
- throw new Error("Recheck the Safe execution before funding it.");
1215
- // The saved record, read again just before the wallet opens: funding
1216
- // another flow reported, or any change to its payment, refuses.
1217
- const stored = await load(account, session.id);
1218
- if (hasObservedFunding(stored) ||
1219
- stored.state !== session.state ||
1220
- stored.paymentStatus !== session.paymentStatus ||
1221
- stored.bundleUuid !== session.bundleUuid ||
1222
- JSON.stringify(stored.payments) !== JSON.stringify(session.payments))
1223
- throw new SafeRelayrRecoveryError(stored, hasObservedFunding(stored)
1224
- ? SAFE_RELAYR_FUNDING_EXIT_LINE
1225
- : "This saved selection changed while it was being paid. Check it again before paying.");
1226
- session = { ...session, paymentStatus: "sending" };
1227
- await store.save(session);
1228
- try {
1280
+ const beforeSend = async () => {
1281
+ phase("payment-checking", session, signal);
1282
+ await revalidate(session.executions, account, onStatus, true, signal);
1283
+ await assertUnreserved(account, session.executions, session.id);
1284
+ await statusRead(() => session.payments.length
1285
+ ? requireRelayrRetry(options.clientFor, {
1286
+ payments: session.payments,
1287
+ from: account,
1288
+ bundleUuid: session.bundleUuid,
1289
+ }, { fetch: fetchRelayr })
1290
+ : requireRelayrBundleUnpaid(session.bundleUuid, {
1291
+ fetch: fetchRelayr,
1292
+ })).catch(async (error) => {
1293
+ if (statusUnknown(error))
1294
+ throw unknownStatus(session);
1295
+ if (fundingReported(error))
1296
+ session = await observed(session);
1297
+ throw error;
1298
+ });
1229
1299
  checkAccount(account, signal);
1230
1300
  requireOpenPayment();
1231
- }
1232
- catch (error) {
1301
+ verified = true;
1302
+ };
1303
+ /**
1304
+ * Nothing was sent after "sending" was saved: the status before this
1305
+ * attempt again. A lost lock leaves it to the fresh-lock reconcile,
1306
+ * and the caller still gets the reason nothing was sent.
1307
+ */
1308
+ const restoreUnsent = async () => {
1233
1309
  session = {
1234
1310
  ...session,
1235
1311
  paymentStatus: checked.session.paymentStatus,
1236
1312
  };
1313
+ const restored = session;
1314
+ await keep(restored, (current) => current.paymentStatus === "sending" &&
1315
+ current.bundleUuid === restored.bundleUuid &&
1316
+ JSON.stringify(current.payments) ===
1317
+ JSON.stringify(restored.payments)
1318
+ ? { ...current, paymentStatus: restored.paymentStatus }
1319
+ : undefined).catch((saveError) => {
1320
+ if (!(saveError instanceof LockLostError))
1321
+ throw saveError;
1322
+ });
1323
+ };
1324
+ const onSending = async () => {
1325
+ checkAccount(account, signal);
1326
+ if (!verified || sending)
1327
+ throw new Error("Recheck the Safe execution before funding it.");
1328
+ // The saved record, read again just before the wallet opens: funding
1329
+ // another flow reported, or any change to its payment, refuses.
1330
+ const stored = await load(account, session.id);
1331
+ if (hasObservedFunding(stored) ||
1332
+ stored.state !== session.state ||
1333
+ stored.paymentStatus !== session.paymentStatus ||
1334
+ stored.bundleUuid !== session.bundleUuid ||
1335
+ JSON.stringify(stored.payments) !== JSON.stringify(session.payments))
1336
+ throw new SafeRelayrRecoveryError(stored, hasObservedFunding(stored)
1337
+ ? SAFE_RELAYR_FUNDING_EXIT_LINE
1338
+ : "This saved selection changed while it was being paid. Check it again before paying.");
1339
+ session = { ...session, paymentStatus: "sending" };
1237
1340
  await store.save(session);
1238
- throw error;
1239
- }
1240
- sending = true;
1241
- phase("payment-submitting", session, signal);
1242
- };
1243
- const onSent = async (payments) => {
1244
- const snapshot = relayrSentPaymentsSnapshot(payments);
1245
- if (!sending ||
1246
- !snapshot?.length ||
1247
- snapshot.some((item) => item.bundleUuid !== session.bundleUuid))
1248
- throw new Error("The payment does not match the saved Safe bundle.");
1249
- if (!snapshot.some((item) => !checked.session.payments.some((prior) => prior.hash.toLowerCase() === item.hash.toLowerCase())))
1250
- throw new Error("The new payment attempt has no new transaction hash.");
1251
- for (const prior of session.payments) {
1252
- if (!snapshot.some((item) => item.hash.toLowerCase() === prior.hash.toLowerCase() &&
1253
- item.chainId === prior.chainId &&
1254
- item.calldata.toLowerCase() === prior.calldata.toLowerCase() &&
1255
- item.amount === prior.amount &&
1256
- item.deadline === prior.deadline &&
1257
- item.target.toLowerCase() === prior.target.toLowerCase()))
1258
- throw new Error("The saved payment history must retain every known payment.");
1259
- }
1260
- const details = relayrPaymentDetails(payment, {
1261
- bundleUuid: session.bundleUuid,
1262
- destinationChainIds: session.executions.map(({ entry }) => entry.chain),
1263
- nowSeconds: 0,
1264
- });
1265
- for (const item of snapshot.filter((item) => !session.payments.some((prior) => prior.hash === item.hash))) {
1266
- if (item.chainId !== details.chainId ||
1267
- BigInt(item.amount) !== details.amount ||
1268
- BigInt(item.deadline) !== details.deadline ||
1269
- item.calldata.toLowerCase() !== details.calldata.toLowerCase() ||
1270
- item.target.toLowerCase() !== details.target.toLowerCase())
1271
- throw new Error("The funding payment differs from the selected Relayr option.");
1272
- }
1273
- paymentPersisted = false;
1274
- session = {
1275
- ...session,
1276
- payments: structuredClone(payments),
1277
- paymentStatus: "submitted",
1341
+ try {
1342
+ checkAccount(account, signal);
1343
+ requireOpenPayment();
1344
+ }
1345
+ catch (error) {
1346
+ await restoreUnsent();
1347
+ throw error;
1348
+ }
1349
+ sending = true;
1350
+ phase("payment-submitting", session, signal);
1278
1351
  };
1279
- await store.save(session);
1280
- paymentPersisted = true;
1281
- phase("payment-confirming", session, signal);
1282
- };
1283
- try {
1284
- phase("payment-review", session, signal);
1285
- const sent = await options.sendPayment({
1286
- session: structuredClone(session),
1287
- payment,
1288
- beforeSend,
1289
- onSending,
1290
- onSent,
1291
- });
1292
- if (!sent.payments.some((item) => item.hash.toLowerCase() === sent.hash.toLowerCase()))
1293
- throw new Error("The funding result omitted its transaction hash.");
1294
- await onSent(sent.payments);
1295
- }
1296
- catch (error) {
1297
- if (sending &&
1298
- // Classify only this attempt; restoring the previous status below
1299
- // preserves the quote's earlier payments and their retry policy.
1300
- relayrPaymentAttemptOutcome(error, { sending, paid: false }) ===
1301
- "unpaid" &&
1302
- session.payments.length === checked.session.payments.length) {
1352
+ const onSent = async (payments) => {
1353
+ const snapshot = relayrSentPaymentsSnapshot(payments);
1354
+ if (!sending ||
1355
+ !snapshot?.length ||
1356
+ snapshot.some((item) => item.bundleUuid !== session.bundleUuid))
1357
+ throw new Error("The payment does not match the saved Safe bundle.");
1358
+ if (!snapshot.some((item) => !checked.session.payments.some((prior) => prior.hash.toLowerCase() === item.hash.toLowerCase())))
1359
+ throw new Error("The new payment attempt has no new transaction hash.");
1360
+ for (const prior of session.payments) {
1361
+ if (!snapshot.some((item) => item.hash.toLowerCase() === prior.hash.toLowerCase() &&
1362
+ item.chainId === prior.chainId &&
1363
+ item.calldata.toLowerCase() ===
1364
+ prior.calldata.toLowerCase() &&
1365
+ item.amount === prior.amount &&
1366
+ item.deadline === prior.deadline &&
1367
+ item.target.toLowerCase() === prior.target.toLowerCase()))
1368
+ throw new Error("The saved payment history must retain every known payment.");
1369
+ }
1370
+ const details = relayrPaymentDetails(payment, {
1371
+ bundleUuid: session.bundleUuid,
1372
+ destinationChainIds: session.executions.map(({ entry }) => entry.chain),
1373
+ nowSeconds: 0,
1374
+ });
1375
+ for (const item of snapshot.filter((item) => !session.payments.some((prior) => prior.hash === item.hash))) {
1376
+ if (item.chainId !== details.chainId ||
1377
+ BigInt(item.amount) !== details.amount ||
1378
+ BigInt(item.deadline) !== details.deadline ||
1379
+ item.calldata.toLowerCase() !== details.calldata.toLowerCase() ||
1380
+ item.target.toLowerCase() !== details.target.toLowerCase())
1381
+ throw new Error("The funding payment differs from the selected Relayr option.");
1382
+ }
1383
+ paymentPersisted = false;
1303
1384
  session = {
1304
1385
  ...session,
1305
- paymentStatus: checked.session.paymentStatus,
1386
+ payments: structuredClone(payments),
1387
+ paymentStatus: "submitted",
1306
1388
  };
1307
- await store.save(session);
1308
- throw error;
1389
+ const sent = session.payments;
1390
+ await keep(session, (current) => {
1391
+ const missing = sent.filter((item) => !current.payments.some((prior) => prior.hash.toLowerCase() === item.hash.toLowerCase()));
1392
+ return current.bundleUuid === session.bundleUuid &&
1393
+ (missing.length || current.paymentStatus === "sending")
1394
+ ? {
1395
+ ...current,
1396
+ payments: [...current.payments, ...missing],
1397
+ paymentStatus: current.paymentStatus === "sending" ||
1398
+ current.paymentStatus === "unfunded"
1399
+ ? "submitted"
1400
+ : current.paymentStatus,
1401
+ }
1402
+ : undefined;
1403
+ });
1404
+ paymentPersisted = true;
1405
+ phase("payment-confirming", session, signal);
1406
+ };
1407
+ try {
1408
+ phase("payment-review", session, signal);
1409
+ const sent = await options.sendPayment({
1410
+ session: structuredClone(session),
1411
+ payment,
1412
+ beforeSend,
1413
+ onSending,
1414
+ onSent,
1415
+ });
1416
+ if (!sent.payments.some((item) => item.hash.toLowerCase() === sent.hash.toLowerCase()))
1417
+ throw new Error("The funding result omitted its transaction hash.");
1418
+ await onSent(sent.payments);
1309
1419
  }
1310
- if (sending ||
1311
- session.payments.length > checked.session.payments.length) {
1312
- // A saved hash may already have funded or completed the bundle even
1313
- // when the wallet adapter could not prove it. Inspect once, read-only;
1314
- // never turn an uncertain or unsaved attempt into a retry invitation.
1315
- if (paymentPersisted) {
1316
- try {
1317
- const inspected = await inspect(session, signal);
1318
- session = inspected.session;
1319
- if (inspected.state === "complete" ||
1320
- (inspected.state === "pending" &&
1321
- session.paymentStatus === "confirmed"))
1322
- return inspected;
1323
- }
1324
- catch (inspectionError) {
1325
- if (inspectionError instanceof SafeRelayrRecoveryError)
1326
- session = inspectionError.session;
1420
+ catch (error) {
1421
+ if (sending &&
1422
+ // Classify only this attempt; restoring the previous status below
1423
+ // preserves the quote's earlier payments and their retry policy.
1424
+ relayrPaymentAttemptOutcome(error, { sending, paid: false }) ===
1425
+ "unpaid" &&
1426
+ session.payments.length === checked.session.payments.length) {
1427
+ await restoreUnsent();
1428
+ throw error;
1429
+ }
1430
+ if (sending ||
1431
+ session.payments.length > checked.session.payments.length) {
1432
+ // A saved hash may already have funded or completed the bundle even
1433
+ // when the wallet adapter could not prove it. Inspect once, read-only;
1434
+ // never turn an uncertain or unsaved attempt into a retry invitation.
1435
+ if (paymentPersisted) {
1436
+ try {
1437
+ const inspected = await inspect(session, signal);
1438
+ session = inspected.session;
1439
+ if (inspected.state === "complete" ||
1440
+ (inspected.state === "pending" &&
1441
+ session.paymentStatus === "confirmed"))
1442
+ return inspected;
1443
+ }
1444
+ catch (inspectionError) {
1445
+ if (inspectionError instanceof SafeRelayrRecoveryError)
1446
+ session = inspectionError.session;
1447
+ }
1327
1448
  }
1449
+ throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
1328
1450
  }
1329
- throw new SafeRelayrRecoveryError(session, error instanceof Error ? error.message : undefined);
1451
+ throw error;
1330
1452
  }
1331
- throw error;
1332
- }
1333
- return inspect(session, signal);
1334
- });
1453
+ return inspect(session, signal);
1454
+ });
1455
+ }
1335
1456
  }
1336
1457
  /** Read-only progress polling; each read releases its lock before the wait. */
1337
1458
  async function watch({ account, sessionId, signal, onUpdate, intervalMs = 2_500, timeoutMs = 300_000, }) {
@@ -1350,6 +1471,10 @@ export function createSafeRelayrController(options) {
1350
1471
  if (signal?.aborted)
1351
1472
  stop();
1352
1473
  const halted = halt.signal;
1474
+ // Its checks' progress is the watch's own.
1475
+ const call = signal && callOf.get(signal);
1476
+ if (call !== undefined)
1477
+ callOf.set(halted, call);
1353
1478
  try {
1354
1479
  const started = Date.now();
1355
1480
  while (true) {
@@ -1400,6 +1525,78 @@ export function createSafeRelayrController(options) {
1400
1525
  return result("released", session);
1401
1526
  });
1402
1527
  }
1403
- return { prepare, check, fund, watch, exit };
1528
+ let calls = 0;
1529
+ /**
1530
+ * Runs one call with its result, error and execution checks shown in the
1531
+ * view: the latest call since the last reset shows, and so does a watch of
1532
+ * the current run throughout, since the run waits on its polls.
1533
+ */
1534
+ const shown = (running, run) => async (args) => {
1535
+ const given = args;
1536
+ const call = ++calls;
1537
+ const shows = () => call === calls || (running === "watch" && call > since);
1538
+ // What still runs once this call ends: the latest call, else a watch of the current run.
1539
+ const still = () => flying.get(calls) ??
1540
+ ([...flying].some(([id, kind]) => kind === "watch" && id > since)
1541
+ ? "watch"
1542
+ : null);
1543
+ flying.set(call, running);
1544
+ // A signal of its own, even when the caller shares one between calls.
1545
+ const signal = given.signal
1546
+ ? AbortSignal.any([given.signal])
1547
+ : new AbortController().signal;
1548
+ callOf.set(signal, call);
1549
+ show({ running, error: null });
1550
+ try {
1551
+ const result = await run({
1552
+ ...args,
1553
+ signal,
1554
+ onStatus: (index, state, execution) => {
1555
+ if (shows())
1556
+ show({ checks: { ...view.checks, [index]: state } });
1557
+ given.onStatus?.(index, state, execution);
1558
+ },
1559
+ ...(running === "watch"
1560
+ ? {
1561
+ onUpdate: (result) => {
1562
+ if (shows())
1563
+ show({ result, error: null });
1564
+ return given.onUpdate?.(result);
1565
+ },
1566
+ }
1567
+ : {}),
1568
+ });
1569
+ flying.delete(call);
1570
+ if (shows())
1571
+ show({ result, error: null, running: still() });
1572
+ return result;
1573
+ }
1574
+ catch (error) {
1575
+ flying.delete(call);
1576
+ if (shows())
1577
+ show({ error, running: still() });
1578
+ throw error;
1579
+ }
1580
+ };
1581
+ return {
1582
+ prepare: shown("prepare", prepare),
1583
+ check: shown("check", check),
1584
+ fund: shown("fund", fund),
1585
+ watch: shown("watch", watch),
1586
+ exit: shown("exit", exit),
1587
+ getState: () => view,
1588
+ subscribe(listener) {
1589
+ listeners.add(listener);
1590
+ return () => {
1591
+ listeners.delete(listener);
1592
+ };
1593
+ },
1594
+ /** Clears the view, as a new run starts. */
1595
+ reset() {
1596
+ since = calls += 1;
1597
+ displayedExecutions.clear();
1598
+ show(SAFE_RELAYR_VIEW);
1599
+ },
1600
+ };
1404
1601
  }
1405
1602
  //# sourceMappingURL=safeRelayr.js.map