@bananapus/nana-sdk-core 6.2.0 → 6.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/cjs/review/confirming.d.ts +8 -0
  2. package/dist/cjs/review/confirming.d.ts.map +1 -0
  3. package/dist/cjs/review/confirming.js +11 -0
  4. package/dist/cjs/review/confirming.js.map +1 -0
  5. package/dist/cjs/review/locks.d.ts +87 -7
  6. package/dist/cjs/review/locks.d.ts.map +1 -1
  7. package/dist/cjs/review/locks.js +328 -79
  8. package/dist/cjs/review/locks.js.map +1 -1
  9. package/dist/cjs/review/messages.d.ts +16 -0
  10. package/dist/cjs/review/messages.d.ts.map +1 -1
  11. package/dist/cjs/review/messages.js +17 -1
  12. package/dist/cjs/review/messages.js.map +1 -1
  13. package/dist/cjs/review/react/ownership.d.ts +30 -0
  14. package/dist/cjs/review/react/ownership.d.ts.map +1 -0
  15. package/dist/cjs/review/react/ownership.js +27 -0
  16. package/dist/cjs/review/react/ownership.js.map +1 -0
  17. package/dist/cjs/review/react/useReviewedRun.d.ts +3 -1
  18. package/dist/cjs/review/react/useReviewedRun.d.ts.map +1 -1
  19. package/dist/cjs/review/react/useReviewedRun.js +428 -50
  20. package/dist/cjs/review/react/useReviewedRun.js.map +1 -1
  21. package/dist/cjs/review/relayrSend.d.ts +16 -1
  22. package/dist/cjs/review/relayrSend.d.ts.map +1 -1
  23. package/dist/cjs/review/relayrSend.js +25 -2
  24. package/dist/cjs/review/relayrSend.js.map +1 -1
  25. package/dist/cjs/review/safeRelayr.d.ts +2 -1
  26. package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
  27. package/dist/cjs/review/safeRelayr.js +23 -29
  28. package/dist/cjs/review/safeRelayr.js.map +1 -1
  29. package/dist/cjs/review/send.d.ts +31 -12
  30. package/dist/cjs/review/send.d.ts.map +1 -1
  31. package/dist/cjs/review/send.js +191 -41
  32. package/dist/cjs/review/send.js.map +1 -1
  33. package/dist/cjs/review/sequence.d.ts +25 -0
  34. package/dist/cjs/review/sequence.d.ts.map +1 -1
  35. package/dist/cjs/review/sequence.js +45 -7
  36. package/dist/cjs/review/sequence.js.map +1 -1
  37. package/dist/cjs/review/tabState.d.ts +4 -0
  38. package/dist/cjs/review/tabState.d.ts.map +1 -1
  39. package/dist/cjs/review/tabState.js +4 -0
  40. package/dist/cjs/review/tabState.js.map +1 -1
  41. package/dist/cjs/review/wallet.d.ts.map +1 -1
  42. package/dist/cjs/review/wallet.js +3 -1
  43. package/dist/cjs/review/wallet.js.map +1 -1
  44. package/dist/cjs/review/writeHold.d.ts +74 -11
  45. package/dist/cjs/review/writeHold.d.ts.map +1 -1
  46. package/dist/cjs/review/writeHold.js +65 -16
  47. package/dist/cjs/review/writeHold.js.map +1 -1
  48. package/dist/cjs/review/writeRecovery.d.ts +31 -6
  49. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  50. package/dist/cjs/review/writeRecovery.js +120 -7
  51. package/dist/cjs/review/writeRecovery.js.map +1 -1
  52. package/dist/cjs/testing.d.ts +13 -0
  53. package/dist/cjs/testing.d.ts.map +1 -1
  54. package/dist/cjs/testing.js +21 -0
  55. package/dist/cjs/testing.js.map +1 -1
  56. package/dist/esm/review/confirming.d.ts +8 -0
  57. package/dist/esm/review/confirming.d.ts.map +1 -0
  58. package/dist/esm/review/confirming.js +8 -0
  59. package/dist/esm/review/confirming.js.map +1 -0
  60. package/dist/esm/review/locks.d.ts +87 -7
  61. package/dist/esm/review/locks.d.ts.map +1 -1
  62. package/dist/esm/review/locks.js +321 -78
  63. package/dist/esm/review/locks.js.map +1 -1
  64. package/dist/esm/review/messages.d.ts +16 -0
  65. package/dist/esm/review/messages.d.ts.map +1 -1
  66. package/dist/esm/review/messages.js +16 -0
  67. package/dist/esm/review/messages.js.map +1 -1
  68. package/dist/esm/review/react/ownership.d.ts +30 -0
  69. package/dist/esm/review/react/ownership.d.ts.map +1 -0
  70. package/dist/esm/review/react/ownership.js +24 -0
  71. package/dist/esm/review/react/ownership.js.map +1 -0
  72. package/dist/esm/review/react/useReviewedRun.d.ts +3 -1
  73. package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -1
  74. package/dist/esm/review/react/useReviewedRun.js +429 -51
  75. package/dist/esm/review/react/useReviewedRun.js.map +1 -1
  76. package/dist/esm/review/relayrSend.d.ts +16 -1
  77. package/dist/esm/review/relayrSend.d.ts.map +1 -1
  78. package/dist/esm/review/relayrSend.js +28 -5
  79. package/dist/esm/review/relayrSend.js.map +1 -1
  80. package/dist/esm/review/safeRelayr.d.ts +2 -1
  81. package/dist/esm/review/safeRelayr.d.ts.map +1 -1
  82. package/dist/esm/review/safeRelayr.js +24 -30
  83. package/dist/esm/review/safeRelayr.js.map +1 -1
  84. package/dist/esm/review/send.d.ts +31 -12
  85. package/dist/esm/review/send.d.ts.map +1 -1
  86. package/dist/esm/review/send.js +193 -43
  87. package/dist/esm/review/send.js.map +1 -1
  88. package/dist/esm/review/sequence.d.ts +25 -0
  89. package/dist/esm/review/sequence.d.ts.map +1 -1
  90. package/dist/esm/review/sequence.js +43 -7
  91. package/dist/esm/review/sequence.js.map +1 -1
  92. package/dist/esm/review/tabState.d.ts +4 -0
  93. package/dist/esm/review/tabState.d.ts.map +1 -1
  94. package/dist/esm/review/tabState.js +4 -0
  95. package/dist/esm/review/tabState.js.map +1 -1
  96. package/dist/esm/review/wallet.d.ts.map +1 -1
  97. package/dist/esm/review/wallet.js +3 -1
  98. package/dist/esm/review/wallet.js.map +1 -1
  99. package/dist/esm/review/writeHold.d.ts +74 -11
  100. package/dist/esm/review/writeHold.d.ts.map +1 -1
  101. package/dist/esm/review/writeHold.js +65 -16
  102. package/dist/esm/review/writeHold.js.map +1 -1
  103. package/dist/esm/review/writeRecovery.d.ts +31 -6
  104. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  105. package/dist/esm/review/writeRecovery.js +118 -7
  106. package/dist/esm/review/writeRecovery.js.map +1 -1
  107. package/dist/esm/testing.d.ts +13 -0
  108. package/dist/esm/testing.d.ts.map +1 -1
  109. package/dist/esm/testing.js +20 -0
  110. package/dist/esm/testing.js.map +1 -1
  111. package/package.json +1 -1
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.REVIEWED_RUN_BUSY_LINE = exports.REVIEWED_RUN_SCOPE_LINE = void 0;
4
4
  exports.useReviewedRun = useReviewedRun;
5
5
  const lineError_js_1 = require("../lineError.js");
6
+ const locks_js_1 = require("../locks.js");
6
7
  const receipt_js_1 = require("../receipt.js");
7
8
  const index_js_1 = require("../index.js");
8
9
  const react_1 = require("react");
9
10
  const heldView_js_1 = require("./heldView.js");
11
+ const ownership_js_1 = require("./ownership.js");
10
12
  const IDLE = {
11
13
  cancelled: false,
12
14
  stopped: false,
@@ -22,6 +24,7 @@ const IDLE = {
22
24
  receipt: null,
23
25
  settled: null,
24
26
  acknowledge: null,
27
+ away: null,
25
28
  };
26
29
  /** The bytes a call sends, for comparing two actions. */
27
30
  const sent = ({ chainId, to, data, value, gas }) => [
@@ -31,6 +34,28 @@ const sent = ({ chainId, to, data, value, gas }) => [
31
34
  value ?? 0n,
32
35
  gas ?? null,
33
36
  ];
37
+ /** This form's earlier write of the same calls succeeded: the run closes on it, sending nothing. */
38
+ /**
39
+ * The holds `action`'s steps are held in, each with its step: a resumed
40
+ * sequence's sending step; a Safe app's batch as its one MultiSend
41
+ * proposal, its last step; or each call. The one home for that mapping.
42
+ */
43
+ function heldSteps(account, safe, action, saved) {
44
+ if (saved?.sending)
45
+ return [
46
+ { call: saved.calls[saved.sending.step], step: saved.sending.step },
47
+ ];
48
+ const total = saved?.calls.length ?? action.calls.length;
49
+ return !saved && action.batch && safe && action.calls.length > 1
50
+ ? [{ call: (0, index_js_1.safeBatchCall)(account, action.calls), step: total - 1 }]
51
+ : action.calls.map((call, step) => ({ call, step }));
52
+ }
53
+ class OwnSucceeded extends Error {
54
+ constructor(found) {
55
+ super("succeeded");
56
+ this.found = found;
57
+ }
58
+ }
34
59
  exports.REVIEWED_RUN_SCOPE_LINE = "This action has more than one step and no scope to resume it by. Nothing was sent.";
35
60
  exports.REVIEWED_RUN_BUSY_LINE = "This action is still running. Wait for it to finish, then try again.";
36
61
  /**
@@ -46,16 +71,29 @@ function useReviewedRun(deps) {
46
71
  if (cancelling)
47
72
  void (0, receipt_js_1.tell)(cancelling.told);
48
73
  }, [cancelling]);
49
- /** The last shown success's acknowledgement: the next run waits for it. */
74
+ /** Every shown success's acknowledgement: the next run waits for all. */
50
75
  const acknowledged = (0, react_1.useRef)(Promise.resolve());
51
76
  // A success is acknowledged only once its done rendered: a reset after
52
- // that hides nothing, and one before it leaves the success warning.
77
+ // that hides nothing, and one before it leaves the success warning. A
78
+ // later one never drops an earlier one still running from the wait.
53
79
  (0, react_1.useEffect)(() => {
54
80
  if (state.acknowledge)
55
- acknowledged.current = state.acknowledge();
81
+ acknowledged.current = Promise.all([
82
+ acknowledged.current,
83
+ state.acknowledge(),
84
+ ]).then(() => undefined);
56
85
  }, [state.acknowledge]);
57
86
  /** The hold followed after a run, and its chain's client: its view is `held`. */
58
- const [watched, setWatched] = (0, react_1.useState)(null);
87
+ const [watched, setWatchedState] = (0, react_1.useState)(null);
88
+ /**
89
+ * `watched` as last set, for a handler that resets and runs from one
90
+ * render's handle (a "Try again"): never that render's stale copy.
91
+ */
92
+ const watchedNow = (0, react_1.useRef)(null);
93
+ const setWatched = (next) => {
94
+ watchedNow.current = next;
95
+ setWatchedState(next);
96
+ };
59
97
  /** The latest run: an earlier one's late answers change nothing. */
60
98
  const latest = (0, react_1.useRef)(0);
61
99
  /** The sent step whose hold the latest run's `ended()` awaits. */
@@ -76,6 +114,101 @@ function useReviewedRun(deps) {
76
114
  * hold keeps the sent step.
77
115
  */
78
116
  const leave = (0, react_1.useRef)(() => { });
117
+ /** Every reservation this runner's runs followed: its own, whatever its calls now. */
118
+ const mine = (0, react_1.useRef)(new Set());
119
+ /**
120
+ * The reservations `calls`' holds hold now (a held write, or a success
121
+ * nothing showed), each with its hold.
122
+ */
123
+ const holdsOf = (account, calls) => calls.flatMap((call) => {
124
+ if (!deps || !account)
125
+ return [];
126
+ try {
127
+ // A hold's key is its call's, whatever the Safe mode.
128
+ const handle = (0, index_js_1.openStepHold)(deps.hold, account, false, call);
129
+ const held = handle.peek();
130
+ const reservation = held.status !== "clear"
131
+ ? held.reservation
132
+ : handle.unacknowledgedSuccess()?.reservation;
133
+ return reservation ? [{ handle, reservation }] : [];
134
+ }
135
+ catch {
136
+ return [];
137
+ }
138
+ });
139
+ /**
140
+ * The reservations this form's last run left unshown, or the last step
141
+ * `dismiss` closed while it was followed (`run`, whose later showing the
142
+ * user closed, never counts as seen), and the account that sent them.
143
+ * Their success, whoever acknowledged it, closes a later run under the
144
+ * same hold (its intent's, though the calls were refreshed) from the same
145
+ * account. Still pending, the hold refuses that run, which follows it again.
146
+ * One entry per account and reservation: keeping or showing one never
147
+ * touches another's.
148
+ */
149
+ const own = (0, react_1.useRef)(new Map());
150
+ /**
151
+ * Every change to a form's write, under the account that sent it, goes
152
+ * through {@link mergeOwn}: forward only. A showing (`shown`) changes
153
+ * the reservation's entry whichever account it is under.
154
+ */
155
+ const changeOwn = (account, reservations, change) => {
156
+ for (const reservation of reservations) {
157
+ if (!reservation)
158
+ continue;
159
+ const keys = [...own.current]
160
+ .filter(([, entry]) => change.kind === "shown" && entry.reservation === reservation)
161
+ .map(([key]) => key);
162
+ const mine = `${account.toLowerCase()}:${reservation}`;
163
+ for (const key of keys.length ? keys : [mine]) {
164
+ const entry = own.current.get(key);
165
+ own.current.set(key, (0, ownership_js_1.mergeOwn)(entry, entry?.account ?? account, reservation, change));
166
+ }
167
+ }
168
+ };
169
+ const ownSuccess = (action) => {
170
+ if (!deps?.account ||
171
+ (action.account &&
172
+ action.account.toLowerCase() !== deps.account.toLowerCase()))
173
+ return undefined;
174
+ const kept = [...own.current.values()]
175
+ .filter((entry) => entry.state === "owned" &&
176
+ entry.account.toLowerCase() === deps.account.toLowerCase())
177
+ .map((entry) => entry.reservation);
178
+ if (!kept.length)
179
+ return undefined;
180
+ // Only the action's last step's hold, as `follow` maps it: a success
181
+ // of an earlier step never completes the action. Its Safe mode may not
182
+ // be known yet; a reservation is only ever in its own mode's hold.
183
+ const account = deps.account;
184
+ const lasts = [false, true].map((safe) => {
185
+ const steps = heldSteps(account, safe, action);
186
+ return steps[steps.length - 1];
187
+ });
188
+ for (const { call, step } of lasts) {
189
+ try {
190
+ // A reservation is read only through its own hold: another hold
191
+ // (another intent, or another exact call) never answers for it.
192
+ const handle = (0, index_js_1.openStepHold)(deps.hold, deps.account, false, call);
193
+ for (const reservation of kept) {
194
+ const settled = handle.settlementOf(reservation);
195
+ if (settled?.result === "success")
196
+ return {
197
+ settled: settled,
198
+ handle,
199
+ account,
200
+ call: call,
201
+ step,
202
+ reservation,
203
+ };
204
+ }
205
+ }
206
+ catch {
207
+ /* A call the hold can't open holds nothing of this form's. */
208
+ }
209
+ }
210
+ return undefined;
211
+ };
79
212
  const replaced = () => {
80
213
  latest.current += 1;
81
214
  life.current.abort();
@@ -94,9 +227,12 @@ function useReviewedRun(deps) {
94
227
  };
95
228
  }, []);
96
229
  /** How a followed step ended, as the run that followed it shows it. */
97
- const showSettled = (run, step, total, settled, receipt, acknowledge) => {
230
+ const showSettled = (run, step, total, settled, receipt, acknowledge, account, reservation) => {
98
231
  if (latest.current !== run)
99
232
  return;
233
+ // Shown: its own entry is released, unless `dismiss` closed it while
234
+ // this run followed it (its showing the user closed never counts).
235
+ changeOwn(account, [reservation], { kind: "shown", run });
100
236
  const finished = step === total - 1;
101
237
  setState((now) => settled.result !== "success"
102
238
  ? {
@@ -174,8 +310,18 @@ function useReviewedRun(deps) {
174
310
  shown.phase === "settled"
175
311
  ? null
176
312
  : shown;
177
- const { cancelled: _cancelled, stopped: _stopped, acknowledge: _acknowledge, ...shownState } = state;
178
- const status = (0, index_js_1.reviewedRunStatus)({ ...state, held });
313
+ const { cancelled: _cancelled, stopped: _stopped, acknowledge: _acknowledge, away, ...shownState } = state;
314
+ const awayShown = away && !held
315
+ ? {
316
+ phase: "succeeded",
317
+ state: null,
318
+ line: index_js_1.HELD_AWAY_SUCCESS_LINE,
319
+ exit: false,
320
+ settled: { result: "success", hash: away.hash },
321
+ adopting: false,
322
+ }
323
+ : null;
324
+ const status = (0, index_js_1.reviewedRunStatus)({ ...state, held: held ?? awayShown });
179
325
  const reset = () => {
180
326
  replaced();
181
327
  setState(IDLE);
@@ -195,16 +341,43 @@ function useReviewedRun(deps) {
195
341
  ...shownState,
196
342
  ...status,
197
343
  busy: status.busy && !state.following,
198
- held,
344
+ held: held ?? awayShown,
199
345
  ...(0, heldView_js_1.actionsOf)(shown, held ? view : undefined),
346
+ ...(awayShown && {
347
+ sendAgainAction: {
348
+ label: index_js_1.HELD_SEND_AGAIN_LABEL,
349
+ run: async () => {
350
+ await away.handle.acknowledge({
351
+ result: "success",
352
+ hash: away.hash,
353
+ });
354
+ setState((now) => (now.away === away ? { ...now, away: null } : now));
355
+ },
356
+ },
357
+ }),
200
358
  reset,
201
359
  dismiss: () => {
202
360
  const waiting = awaiting.current;
203
361
  if (open.current && !waiting)
204
362
  return false;
363
+ /**
364
+ * The followed last step this closes, under the account that sent it,
365
+ * and the run that followed it: a retry of it never sends it again.
366
+ */
367
+ const last = waiting
368
+ ? waiting.last && { ...waiting.owner, run: waiting.run }
369
+ : watched?.step === watched?.total - 1 && watched;
370
+ const closing = () => {
371
+ if (last)
372
+ changeOwn(last.account, [last.reservation], {
373
+ kind: "dismiss",
374
+ run: last.run,
375
+ });
376
+ };
205
377
  if (waiting?.last) {
206
378
  awaiting.current = null;
207
379
  waiting.end();
380
+ closing();
208
381
  return true;
209
382
  }
210
383
  detach(waiting
@@ -213,9 +386,13 @@ function useReviewedRun(deps) {
213
386
  handle: watched.handle,
214
387
  client: watched.client,
215
388
  call: watched.call,
389
+ account: watched.account,
216
390
  reservation: watched.reservation,
217
391
  });
392
+ // Closing the view keeps what this form sent: a reset forgets no
393
+ // entry, so a later dismiss of the idle view never forgets one.
218
394
  reset();
395
+ closing();
219
396
  return true;
220
397
  },
221
398
  preload,
@@ -235,6 +412,102 @@ function useReviewedRun(deps) {
235
412
  setWatched(null);
236
413
  },
237
414
  run: async ({ onCancelled, onStopped, ...action }) => {
415
+ /**
416
+ * The account this run sends from, read once: every continuation of
417
+ * it uses this one, whatever account the app's deps hold by then.
418
+ */
419
+ const runAccount = deps?.account;
420
+ /**
421
+ * The wallet's Safe mode, only while it still speaks for the run's
422
+ * account: another account's mode never names this run's holds.
423
+ */
424
+ const runSafe = () => deps && deps.account?.toLowerCase() === runAccount?.toLowerCase()
425
+ ? deps.adapter.isSafe()
426
+ : undefined;
427
+ /** This form's unshown write of these calls succeeded: the form closes on it. */
428
+ const close = async (found) => {
429
+ const { settled, handle } = found;
430
+ // Every exit holds the run open, and this form's ownership stays
431
+ // until its success is shown and acknowledged: until then a dismiss
432
+ // is refused and a later run closes on the same success again.
433
+ open.current += 1;
434
+ try {
435
+ // Completed as its run would have, under this run's locks: told to
436
+ // onConfirmed once (unless another completion did), and done only
437
+ // once delivered.
438
+ let failure;
439
+ const told = await load(deps.engine)
440
+ .then((engine) => engine.completeReviewedStep({
441
+ step: found.step,
442
+ held: { call: found.call, reservation: found.reservation },
443
+ }, {
444
+ account: found.account,
445
+ scope: action.scope,
446
+ onConfirmed: action.onConfirmed,
447
+ locks: action.locks,
448
+ attemptScoped: action.attemptScoped,
449
+ }, deps, null, alive, false))
450
+ .catch((error) => {
451
+ failure = error;
452
+ return null;
453
+ });
454
+ if (!told || told.undelivered) {
455
+ failure ??= new locks_js_1.LockLostError(action.locks?.[0]?.name ?? "site");
456
+ const error = (0, index_js_1.errorLine)(failure);
457
+ update({ ...IDLE, error, failure });
458
+ return { status: "failed", error, failure };
459
+ }
460
+ const step = action.calls.length - 1;
461
+ update({
462
+ ...IDLE,
463
+ // Shown now: acknowledged once rendered, so it blocks no new send.
464
+ // Rendered: the form's own is released, and a later run waits
465
+ // for this acknowledgement before it sends.
466
+ acknowledge: async () => {
467
+ if (alive.aborted)
468
+ return;
469
+ changeOwn(found.account, [found.reservation], {
470
+ kind: "shown",
471
+ });
472
+ await handle.acknowledge(settled).catch(() => undefined);
473
+ },
474
+ phase: "done",
475
+ step,
476
+ total: action.calls.length,
477
+ hash: settled.hash,
478
+ settled: { step, finished: true },
479
+ });
480
+ return {
481
+ status: "done",
482
+ hashes: told.hashes,
483
+ receipts: [],
484
+ ...(told.signatures ? { signatures: told.signatures } : {}),
485
+ };
486
+ }
487
+ finally {
488
+ open.current -= 1;
489
+ }
490
+ };
491
+ // This form's own write is still followed: a retry of it is refused
492
+ // with the hold's line, and the run following it goes on.
493
+ const now = watchedNow.current;
494
+ // Identity, never bytes: the followed reservation is one of these
495
+ // calls' holds'. A scoped sequence's journal answers for its own.
496
+ const followed = now?.reservation &&
497
+ runAccount &&
498
+ (action.scope === undefined || action.calls.length === 1)
499
+ ? now
500
+ : null;
501
+ const ownState = followed?.handle.peek();
502
+ if (followed &&
503
+ ownState?.status !== "clear" &&
504
+ ownState?.reservation === followed.reservation &&
505
+ holdsOf(runAccount, action.calls).some(({ reservation }) => reservation === followed.reservation)) {
506
+ const failure = new index_js_1.WriteHeldError(ownState);
507
+ const error = (0, index_js_1.errorLine)(failure);
508
+ setState((now) => ({ ...now, error, failure }));
509
+ return { status: "failed", error, failure };
510
+ }
238
511
  replaced();
239
512
  const id = latest.current;
240
513
  const { signal: alive } = life.current;
@@ -248,6 +521,21 @@ function useReviewedRun(deps) {
248
521
  setState((now) => ({ ...now, ...next }));
249
522
  };
250
523
  setState({ ...IDLE, phase: "review", total: action.calls.length });
524
+ const succeeded = ownSuccess(action);
525
+ if (succeeded) {
526
+ setWatched(null);
527
+ // close holds the run open itself, from here until it ends.
528
+ open.current -= 1;
529
+ return close(succeeded);
530
+ }
531
+ /**
532
+ * Reservations the action's holds had before this run: another run's
533
+ * (another form's, sharing a hold). This run never follows one.
534
+ */
535
+ const holds = () => holdsOf(runAccount, runAccount && action.batch && action.calls.length > 1
536
+ ? [...action.calls, (0, index_js_1.safeBatchCall)(runAccount, action.calls)]
537
+ : action.calls);
538
+ const before = new Set(holds().map(({ reservation }) => reservation));
251
539
  let loaded;
252
540
  /** A step of the action was sent before this run, or by it: never a cancel. */
253
541
  let sentAny = false;
@@ -276,9 +564,11 @@ function useReviewedRun(deps) {
276
564
  let completed = Promise.resolve(null);
277
565
  let shown = false;
278
566
  const action$ = {
279
- account: deps.account,
567
+ account: runAccount,
280
568
  scope: action.scope,
281
569
  onConfirmed: action.onConfirmed,
570
+ locks: action.locks,
571
+ attemptScoped: action.attemptScoped,
282
572
  };
283
573
  const finish = (seen, receipt = null) => {
284
574
  if (shown || alive.aborted)
@@ -292,11 +582,30 @@ function useReviewedRun(deps) {
292
582
  return null;
293
583
  }));
294
584
  void completed.then((ended) => {
585
+ // Its notice undelivered: the form's own, whichever run shows
586
+ // now (a retry that replaced this one included), so a later
587
+ // run closes on it, telling it then.
588
+ if (ended?.undelivered && target.step === total - 1)
589
+ changeOwn(action$.account, [target.held?.reservation], {
590
+ kind: "keep",
591
+ });
295
592
  if (shown || alive.aborted)
296
593
  return;
297
- // Nothing advanced (its receipt unread, or its hold not settled
298
- // for this send yet): it is tried again.
299
- if (unread || (!ended && seen)) {
594
+ // Nothing advanced (its receipt unread, its hold not settled
595
+ // for this send yet, or its onConfirmed not delivered): it is
596
+ // tried again, still followed and acknowledged by nothing.
597
+ // ponytail: retried at the receipt's pace while mounted.
598
+ if (unread || (!ended && seen) || ended?.undelivered) {
599
+ if (ended?.undelivered) {
600
+ completed = Promise.resolve(null);
601
+ // While the site's work under its locks runs, its notice may
602
+ // wait for it (kept for a lock this run lost, or another
603
+ // completion's claim queued behind one it holds): the wait
604
+ // ends now, retryable, and is never what holds that work up.
605
+ // Once that work ended, nothing of the caller's is in its way.
606
+ if (action.locks?.some(locks_js_1.lockWorkRunning))
607
+ end?.("queued");
608
+ }
300
609
  if (mounted.current)
301
610
  setTimeout(() => finish(seen, receipt), index_js_1.RECEIPT_RETRY_MS);
302
611
  return;
@@ -306,8 +615,8 @@ function useReviewedRun(deps) {
306
615
  shown = true;
307
616
  // Shown with the receipt completion checked, a failure's too.
308
617
  showSettled(id, target.step, total, ended.settled, ended.receipt, ended.settled.result === "success" && target.step === total - 1
309
- ? acknowledging((0, index_js_1.openStepHold)(deps.hold, deps.account, false, target.held.call), ended.settled)
310
- : null);
618
+ ? acknowledging((0, index_js_1.openStepHold)(deps.hold, action$.account, false, target.held.call), ended.settled)
619
+ : null, action$.account, target.held?.reservation);
311
620
  setWatched(null);
312
621
  end?.(ended);
313
622
  });
@@ -319,9 +628,10 @@ function useReviewedRun(deps) {
319
628
  * found one, or null when the wallet could not say (its Safe mode unknown).
320
629
  */
321
630
  const follow = async (finish, released) => {
322
- if (latest.current !== id || !deps?.account)
631
+ if (latest.current !== id || !deps || !runAccount)
323
632
  return false;
324
- const { account, adapter, hold, storage } = deps;
633
+ const { adapter, hold, storage } = deps;
634
+ const account = runAccount;
325
635
  // Its Safe mode is read only once the wallet says it is known.
326
636
  try {
327
637
  await (0, index_js_1.walletReady)(adapter);
@@ -332,36 +642,55 @@ function useReviewedRun(deps) {
332
642
  try {
333
643
  if (latest.current !== id)
334
644
  return false;
335
- const safe = adapter.isSafe();
645
+ const safe = runSafe();
646
+ if (safe === undefined)
647
+ return null;
336
648
  const saved = !action.scope
337
649
  ? null
338
650
  : (0, index_js_1.findReviewedSequence)(action.scope, account, { storage });
339
651
  const total = saved?.calls.length ?? action.calls.length;
340
- // A Safe app's batch is held as its one MultiSend proposal, its last step.
341
- const steps = saved?.sending
342
- ? [
343
- {
344
- call: saved.calls[saved.sending.step],
345
- step: saved.sending.step,
346
- },
347
- ]
348
- : !saved && action.batch && safe && action.calls.length > 1
349
- ? [
350
- {
351
- call: (0, index_js_1.safeBatchCall)(account, action.calls),
352
- step: total - 1,
353
- },
354
- ]
355
- : action.calls.map((call, step) => ({ call, step }));
356
- // A held step, or the last step's success nothing showed.
652
+ const steps = heldSteps(account, safe, action, saved);
653
+ // A held step, or the last step's success nothing showed, of this
654
+ // run (or of the saved sequence it resumes), never another run's.
655
+ let away = null;
357
656
  const found = steps
358
657
  .map(({ call, step }) => ({
359
658
  call,
360
659
  step,
361
660
  handle: (0, index_js_1.openStepHold)(hold, account, safe, call),
362
661
  }))
363
- .find(({ handle, step }) => handle.peek().status !== "clear" ||
364
- (step === total - 1 && handle.unacknowledgedSuccess()));
662
+ .find(({ handle, step, call }) => {
663
+ const held = handle.peek();
664
+ const success = step === total - 1 ? handle.unacknowledgedSuccess() : undefined;
665
+ if (held.status === "clear" && !success)
666
+ return false;
667
+ const reservation = held.status !== "clear"
668
+ ? held.reservation
669
+ : success?.reservation;
670
+ // A write held before this run, under an intent other forms
671
+ // share, is followed only when this runner followed it before,
672
+ // or it is this exact call (or this saved sequence's): another
673
+ // form's write is never adopted.
674
+ if (!reservation ||
675
+ !before.has(reservation) ||
676
+ mine.current.has(reservation) ||
677
+ reservation === saved?.sending?.reservation ||
678
+ !call.intent)
679
+ return true;
680
+ // A success another runner left settled while away, of this
681
+ // call or another form's: only shown, with "Send it again", so
682
+ // the refused form is never stuck. Never followed, so no
683
+ // dismissal makes it this form's to close a changed call on.
684
+ if (held.status === "clear") {
685
+ away = { handle, hash: success.hash };
686
+ return false;
687
+ }
688
+ const record = (0, index_js_1.listReviewedWrites)({ chainId: call.chainId, account }, { storage }).find(({ record }) => record.id === reservation)?.record;
689
+ return (!!record &&
690
+ record.call.to.toLowerCase() === call.to.toLowerCase() &&
691
+ record.call.data.toLowerCase() === call.data.toLowerCase() &&
692
+ record.call.value === String(call.value ?? 0n));
693
+ });
365
694
  // Its settlement completes it as a send's would: a proposal too.
366
695
  const reservation = found?.handle.peek().reservation ??
367
696
  found?.handle.unacknowledgedSuccess()?.reservation;
@@ -370,6 +699,7 @@ function useReviewedRun(deps) {
370
699
  handle: found.handle,
371
700
  client: deps.clientFor(found.call.chainId),
372
701
  run: id,
702
+ account,
373
703
  call: found.call,
374
704
  reservation,
375
705
  step: found.step,
@@ -383,8 +713,9 @@ function useReviewedRun(deps) {
383
713
  }
384
714
  : null);
385
715
  // Only its hold is followed now: the confirm can close.
386
- if (found)
387
- update({ following: true });
716
+ if (reservation)
717
+ mine.current.add(reservation);
718
+ update(found ? { following: true, away: null } : { away });
388
719
  return !!found;
389
720
  }
390
721
  catch {
@@ -394,13 +725,17 @@ function useReviewedRun(deps) {
394
725
  };
395
726
  const heldStep = (target) => {
396
727
  const call = target.held?.call ?? action.calls[target.step];
397
- if (!deps?.account || !call)
728
+ if (!deps || !runAccount || !call)
398
729
  return null;
399
730
  try {
731
+ const safe = runSafe();
732
+ if (safe === undefined)
733
+ return null;
400
734
  return {
401
- handle: (0, index_js_1.openStepHold)(deps.hold, deps.account, deps.adapter.isSafe(), call),
735
+ handle: (0, index_js_1.openStepHold)(deps.hold, runAccount, safe, call),
402
736
  client: deps.clientFor(call.chainId),
403
737
  call,
738
+ account: runAccount,
404
739
  reservation: target.held?.reservation,
405
740
  };
406
741
  }
@@ -409,10 +744,10 @@ function useReviewedRun(deps) {
409
744
  }
410
745
  };
411
746
  try {
412
- if (!deps?.account)
747
+ if (!deps || !runAccount)
413
748
  throw new lineError_js_1.ReviewLineError("Connect a wallet first.");
414
749
  if (action.account &&
415
- action.account.toLowerCase() !== deps.account.toLowerCase())
750
+ action.account.toLowerCase() !== runAccount.toLowerCase())
416
751
  throw new lineError_js_1.ReviewLineError(index_js_1.REVIEWED_ACCOUNT_CHANGED_LINE);
417
752
  if (action.calls.length > 1 && action.scope === undefined)
418
753
  throw new lineError_js_1.ReviewLineError(exports.REVIEWED_RUN_SCOPE_LINE);
@@ -433,7 +768,14 @@ function useReviewedRun(deps) {
433
768
  const { resumeReviewed, sendReviewed } = loaded;
434
769
  // A success already shown is acknowledged before this run checks it.
435
770
  await acknowledged.current;
436
- const { account, guard } = deps;
771
+ const settledOwn = () => {
772
+ const settled = ownSuccess(action);
773
+ if (settled)
774
+ throw new OwnSucceeded(settled);
775
+ };
776
+ settledOwn();
777
+ const { guard } = deps;
778
+ const account = runAccount;
437
779
  const tracked = {
438
780
  ...deps,
439
781
  onProgress: (progress) => {
@@ -490,9 +832,20 @@ function useReviewedRun(deps) {
490
832
  (0, receipt_js_1.tell)(action.follow?.onConfirming, step);
491
833
  },
492
834
  };
835
+ const assertWriteState = () => {
836
+ settledOwn();
837
+ action.assertWriteState?.();
838
+ };
839
+ const gated = {
840
+ ...action,
841
+ guard,
842
+ follow: inPlace,
843
+ onSubmitted,
844
+ assertWriteState,
845
+ };
493
846
  const result = saved
494
- ? await resumeReviewed(saved, { ...action, guard, follow: inPlace, onSubmitted }, tracked)
495
- : await sendReviewed({ ...action, account, guard, follow: inPlace, onSubmitted }, tracked);
847
+ ? await resumeReviewed(saved, gated, tracked)
848
+ : await sendReviewed({ ...gated, account }, tracked);
496
849
  // A wallet that reviews on its own page reports no progress: the
497
850
  // result is the run's last word.
498
851
  const calls = saved?.calls ?? action.calls;
@@ -517,6 +870,8 @@ function useReviewedRun(deps) {
517
870
  return result;
518
871
  }
519
872
  catch (error) {
873
+ if (error instanceof OwnSucceeded)
874
+ return close(error.found);
520
875
  if (error instanceof index_js_1.ReviewedReceiptUnverifiedError) {
521
876
  // Still confirming: followed until its hold settles it.
522
877
  update({
@@ -545,7 +900,12 @@ function useReviewedRun(deps) {
545
900
  leave.current = abandon;
546
901
  cancelled?.addEventListener("abort", abandon, { once: true });
547
902
  awaiting.current = {
903
+ run: id,
548
904
  last: error.step === action.calls.length - 1,
905
+ owner: {
906
+ account: runAccount,
907
+ reservation: error.held?.reservation,
908
+ },
549
909
  detached: heldStep(error),
550
910
  end: abandon,
551
911
  };
@@ -553,7 +913,6 @@ function useReviewedRun(deps) {
553
913
  const finish = completing(error, action.calls.length, end);
554
914
  // Followed for a later settlement; one already there is read once it attaches.
555
915
  void follow(finish, () => end(null)).then(() => finish());
556
- const before = error.held?.hashes ?? [];
557
916
  return {
558
917
  status: "receipt",
559
918
  step: error.step,
@@ -564,6 +923,11 @@ function useReviewedRun(deps) {
564
923
  ended: () => outcome.then(async (ran) => {
565
924
  if (!ran)
566
925
  return null;
926
+ if (ran === "queued") {
927
+ const failure = new locks_js_1.ConfirmationQueuedError(error.step);
928
+ const line = (0, index_js_1.errorLine)(failure);
929
+ return { status: "failed", error: line, failure };
930
+ }
567
931
  const { settled, receipt } = ran;
568
932
  if (settled.result !== "success") {
569
933
  const line = (0, index_js_1.settledLine)(settled);
@@ -573,7 +937,8 @@ function useReviewedRun(deps) {
573
937
  failure: new lineError_js_1.ReviewLineError(line),
574
938
  };
575
939
  }
576
- const signatures = error.held?.signatures;
940
+ // The run's own, as its hold saved them with the step.
941
+ const { signatures } = ran;
577
942
  if (error.step < action.calls.length - 1) {
578
943
  const started = await runLatest.current({
579
944
  ...action,
@@ -591,7 +956,7 @@ function useReviewedRun(deps) {
591
956
  }
592
957
  return {
593
958
  status: "done",
594
- hashes: [...before, settled.hash],
959
+ hashes: ran.hashes,
595
960
  receipts: receipt ? [receipt] : [],
596
961
  ...(signatures ? { signatures } : {}),
597
962
  };
@@ -600,9 +965,9 @@ function useReviewedRun(deps) {
600
965
  }
601
966
  // A resume can complete a step the wallet sent before the reload.
602
967
  try {
603
- const after = action.scope === undefined || !deps?.account
968
+ const after = action.scope === undefined || !runAccount
604
969
  ? null
605
- : (0, index_js_1.findReviewedSequence)(action.scope, deps.account, {
970
+ : (0, index_js_1.findReviewedSequence)(action.scope, runAccount, {
606
971
  storage: deps.storage,
607
972
  });
608
973
  if (after?.completed.length &&
@@ -614,6 +979,19 @@ function useReviewedRun(deps) {
614
979
  catch {
615
980
  /* An unreadable sequence adds nothing to what this run saw. */
616
981
  }
982
+ // Refused by a write already held (another form's, sharing the
983
+ // hold): this run follows it only as its own exact call.
984
+ const refused = error instanceof index_js_1.WriteHeldError
985
+ ? error.state.reservation
986
+ : error instanceof index_js_1.WriteHoldUnacknowledgedSuccessError
987
+ ? error.success.reservation
988
+ : undefined;
989
+ if (refused)
990
+ before.add(refused);
991
+ // ponytail: one call only; a sequence's journal keeps its steps.
992
+ const left = holds().filter(({ reservation }) => !before.has(reservation));
993
+ if (action.calls.length === 1 && left.length && runAccount)
994
+ changeOwn(runAccount, left.map(({ reservation }) => reservation), { kind: "keep" });
617
995
  const cancelled = error instanceof index_js_1.TransactionReviewCancelledError ||
618
996
  (0, index_js_1.isDefiniteWalletRejection)(error);
619
997
  const line = cancelled ? null : (0, index_js_1.errorLine)(error);