@bananapus/nana-sdk-core 6.2.0 → 6.3.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 (109) 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.map +1 -1
  18. package/dist/cjs/review/react/useReviewedRun.js +394 -45
  19. package/dist/cjs/review/react/useReviewedRun.js.map +1 -1
  20. package/dist/cjs/review/relayrSend.d.ts +16 -1
  21. package/dist/cjs/review/relayrSend.d.ts.map +1 -1
  22. package/dist/cjs/review/relayrSend.js +25 -2
  23. package/dist/cjs/review/relayrSend.js.map +1 -1
  24. package/dist/cjs/review/safeRelayr.d.ts +2 -1
  25. package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
  26. package/dist/cjs/review/safeRelayr.js +23 -29
  27. package/dist/cjs/review/safeRelayr.js.map +1 -1
  28. package/dist/cjs/review/send.d.ts +31 -12
  29. package/dist/cjs/review/send.d.ts.map +1 -1
  30. package/dist/cjs/review/send.js +191 -41
  31. package/dist/cjs/review/send.js.map +1 -1
  32. package/dist/cjs/review/sequence.d.ts +25 -0
  33. package/dist/cjs/review/sequence.d.ts.map +1 -1
  34. package/dist/cjs/review/sequence.js +45 -7
  35. package/dist/cjs/review/sequence.js.map +1 -1
  36. package/dist/cjs/review/tabState.d.ts +4 -0
  37. package/dist/cjs/review/tabState.d.ts.map +1 -1
  38. package/dist/cjs/review/tabState.js +4 -0
  39. package/dist/cjs/review/tabState.js.map +1 -1
  40. package/dist/cjs/review/wallet.d.ts.map +1 -1
  41. package/dist/cjs/review/wallet.js +3 -1
  42. package/dist/cjs/review/wallet.js.map +1 -1
  43. package/dist/cjs/review/writeHold.d.ts +74 -11
  44. package/dist/cjs/review/writeHold.d.ts.map +1 -1
  45. package/dist/cjs/review/writeHold.js +65 -16
  46. package/dist/cjs/review/writeHold.js.map +1 -1
  47. package/dist/cjs/review/writeRecovery.d.ts +31 -6
  48. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  49. package/dist/cjs/review/writeRecovery.js +120 -7
  50. package/dist/cjs/review/writeRecovery.js.map +1 -1
  51. package/dist/cjs/testing.d.ts +13 -0
  52. package/dist/cjs/testing.d.ts.map +1 -1
  53. package/dist/cjs/testing.js +21 -0
  54. package/dist/cjs/testing.js.map +1 -1
  55. package/dist/esm/review/confirming.d.ts +8 -0
  56. package/dist/esm/review/confirming.d.ts.map +1 -0
  57. package/dist/esm/review/confirming.js +8 -0
  58. package/dist/esm/review/confirming.js.map +1 -0
  59. package/dist/esm/review/locks.d.ts +87 -7
  60. package/dist/esm/review/locks.d.ts.map +1 -1
  61. package/dist/esm/review/locks.js +321 -78
  62. package/dist/esm/review/locks.js.map +1 -1
  63. package/dist/esm/review/messages.d.ts +16 -0
  64. package/dist/esm/review/messages.d.ts.map +1 -1
  65. package/dist/esm/review/messages.js +16 -0
  66. package/dist/esm/review/messages.js.map +1 -1
  67. package/dist/esm/review/react/ownership.d.ts +30 -0
  68. package/dist/esm/review/react/ownership.d.ts.map +1 -0
  69. package/dist/esm/review/react/ownership.js +24 -0
  70. package/dist/esm/review/react/ownership.js.map +1 -0
  71. package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -1
  72. package/dist/esm/review/react/useReviewedRun.js +395 -46
  73. package/dist/esm/review/react/useReviewedRun.js.map +1 -1
  74. package/dist/esm/review/relayrSend.d.ts +16 -1
  75. package/dist/esm/review/relayrSend.d.ts.map +1 -1
  76. package/dist/esm/review/relayrSend.js +28 -5
  77. package/dist/esm/review/relayrSend.js.map +1 -1
  78. package/dist/esm/review/safeRelayr.d.ts +2 -1
  79. package/dist/esm/review/safeRelayr.d.ts.map +1 -1
  80. package/dist/esm/review/safeRelayr.js +24 -30
  81. package/dist/esm/review/safeRelayr.js.map +1 -1
  82. package/dist/esm/review/send.d.ts +31 -12
  83. package/dist/esm/review/send.d.ts.map +1 -1
  84. package/dist/esm/review/send.js +193 -43
  85. package/dist/esm/review/send.js.map +1 -1
  86. package/dist/esm/review/sequence.d.ts +25 -0
  87. package/dist/esm/review/sequence.d.ts.map +1 -1
  88. package/dist/esm/review/sequence.js +43 -7
  89. package/dist/esm/review/sequence.js.map +1 -1
  90. package/dist/esm/review/tabState.d.ts +4 -0
  91. package/dist/esm/review/tabState.d.ts.map +1 -1
  92. package/dist/esm/review/tabState.js +4 -0
  93. package/dist/esm/review/tabState.js.map +1 -1
  94. package/dist/esm/review/wallet.d.ts.map +1 -1
  95. package/dist/esm/review/wallet.js +3 -1
  96. package/dist/esm/review/wallet.js.map +1 -1
  97. package/dist/esm/review/writeHold.d.ts +74 -11
  98. package/dist/esm/review/writeHold.d.ts.map +1 -1
  99. package/dist/esm/review/writeHold.js +65 -16
  100. package/dist/esm/review/writeHold.js.map +1 -1
  101. package/dist/esm/review/writeRecovery.d.ts +31 -6
  102. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  103. package/dist/esm/review/writeRecovery.js +118 -7
  104. package/dist/esm/review/writeRecovery.js.map +1 -1
  105. package/dist/esm/testing.d.ts +13 -0
  106. package/dist/esm/testing.d.ts.map +1 -1
  107. package/dist/esm/testing.js +20 -0
  108. package/dist/esm/testing.js.map +1 -1
  109. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
1
  import { ReviewLineError } from "../lineError.js";
2
+ import { ConfirmationQueuedError, LockLostError, lockWorkRunning, } from "../locks.js";
2
3
  import { tell } from "../receipt.js";
3
- import { createHeldWriteView, discardReviewedSequence, errorLine, findReviewedSequence, followHeldWrite, isDefiniteWalletRejection, listReviewedWrites, openStepHold, RECEIPT_RETRY_MS, REVIEWED_ACCOUNT_CHANGED_LINE, REVIEWED_ENGINE_LOAD_LINE, ReviewedReceiptUnverifiedError, reviewedRunStatus, safeBatchCall, settledLine, TransactionReviewCancelledError, walletReady, } from "../index.js";
4
+ import { createHeldWriteView, discardReviewedSequence, errorLine, findReviewedSequence, followHeldWrite, isDefiniteWalletRejection, listReviewedWrites, openStepHold, RECEIPT_RETRY_MS, REVIEWED_ACCOUNT_CHANGED_LINE, REVIEWED_ENGINE_LOAD_LINE, ReviewedReceiptUnverifiedError, reviewedRunStatus, safeBatchCall, settledLine, TransactionReviewCancelledError, walletReady, WriteHeldError, WriteHoldUnacknowledgedSuccessError, } from "../index.js";
4
5
  import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
5
6
  import { actionsOf, idle, keyOf, none, } from "./heldView.js";
7
+ import { mergeOwn } from "./ownership.js";
6
8
  const IDLE = {
7
9
  cancelled: false,
8
10
  stopped: false,
@@ -27,6 +29,28 @@ const sent = ({ chainId, to, data, value, gas }) => [
27
29
  value ?? 0n,
28
30
  gas ?? null,
29
31
  ];
32
+ /** This form's earlier write of the same calls succeeded: the run closes on it, sending nothing. */
33
+ /**
34
+ * The holds `action`'s steps are held in, each with its step: a resumed
35
+ * sequence's sending step; a Safe app's batch as its one MultiSend
36
+ * proposal, its last step; or each call. The one home for that mapping.
37
+ */
38
+ function heldSteps(account, safe, action, saved) {
39
+ if (saved?.sending)
40
+ return [
41
+ { call: saved.calls[saved.sending.step], step: saved.sending.step },
42
+ ];
43
+ const total = saved?.calls.length ?? action.calls.length;
44
+ return !saved && action.batch && safe && action.calls.length > 1
45
+ ? [{ call: safeBatchCall(account, action.calls), step: total - 1 }]
46
+ : action.calls.map((call, step) => ({ call, step }));
47
+ }
48
+ class OwnSucceeded extends Error {
49
+ constructor(found) {
50
+ super("succeeded");
51
+ this.found = found;
52
+ }
53
+ }
30
54
  export const REVIEWED_RUN_SCOPE_LINE = "This action has more than one step and no scope to resume it by. Nothing was sent.";
31
55
  export const REVIEWED_RUN_BUSY_LINE = "This action is still running. Wait for it to finish, then try again.";
32
56
  /**
@@ -42,16 +66,29 @@ export function useReviewedRun(deps) {
42
66
  if (cancelling)
43
67
  void tell(cancelling.told);
44
68
  }, [cancelling]);
45
- /** The last shown success's acknowledgement: the next run waits for it. */
69
+ /** Every shown success's acknowledgement: the next run waits for all. */
46
70
  const acknowledged = useRef(Promise.resolve());
47
71
  // A success is acknowledged only once its done rendered: a reset after
48
- // that hides nothing, and one before it leaves the success warning.
72
+ // that hides nothing, and one before it leaves the success warning. A
73
+ // later one never drops an earlier one still running from the wait.
49
74
  useEffect(() => {
50
75
  if (state.acknowledge)
51
- acknowledged.current = state.acknowledge();
76
+ acknowledged.current = Promise.all([
77
+ acknowledged.current,
78
+ state.acknowledge(),
79
+ ]).then(() => undefined);
52
80
  }, [state.acknowledge]);
53
81
  /** The hold followed after a run, and its chain's client: its view is `held`. */
54
- const [watched, setWatched] = useState(null);
82
+ const [watched, setWatchedState] = useState(null);
83
+ /**
84
+ * `watched` as last set, for a handler that resets and runs from one
85
+ * render's handle (a "Try again"): never that render's stale copy.
86
+ */
87
+ const watchedNow = useRef(null);
88
+ const setWatched = (next) => {
89
+ watchedNow.current = next;
90
+ setWatchedState(next);
91
+ };
55
92
  /** The latest run: an earlier one's late answers change nothing. */
56
93
  const latest = useRef(0);
57
94
  /** The sent step whose hold the latest run's `ended()` awaits. */
@@ -72,6 +109,101 @@ export function useReviewedRun(deps) {
72
109
  * hold keeps the sent step.
73
110
  */
74
111
  const leave = useRef(() => { });
112
+ /** Every reservation this runner's runs followed: its own, whatever its calls now. */
113
+ const mine = useRef(new Set());
114
+ /**
115
+ * The reservations `calls`' holds hold now (a held write, or a success
116
+ * nothing showed), each with its hold.
117
+ */
118
+ const holdsOf = (account, calls) => calls.flatMap((call) => {
119
+ if (!deps || !account)
120
+ return [];
121
+ try {
122
+ // A hold's key is its call's, whatever the Safe mode.
123
+ const handle = openStepHold(deps.hold, account, false, call);
124
+ const held = handle.peek();
125
+ const reservation = held.status !== "clear"
126
+ ? held.reservation
127
+ : handle.unacknowledgedSuccess()?.reservation;
128
+ return reservation ? [{ handle, reservation }] : [];
129
+ }
130
+ catch {
131
+ return [];
132
+ }
133
+ });
134
+ /**
135
+ * The reservations this form's last run left unshown, or the last step
136
+ * `dismiss` closed while it was followed (`run`, whose later showing the
137
+ * user closed, never counts as seen), and the account that sent them.
138
+ * Their success, whoever acknowledged it, closes a later run under the
139
+ * same hold (its intent's, though the calls were refreshed) from the same
140
+ * account. Still pending, the hold refuses that run, which follows it again.
141
+ * One entry per account and reservation: keeping or showing one never
142
+ * touches another's.
143
+ */
144
+ const own = useRef(new Map());
145
+ /**
146
+ * Every change to a form's write, under the account that sent it, goes
147
+ * through {@link mergeOwn}: forward only. A showing (`shown`) changes
148
+ * the reservation's entry whichever account it is under.
149
+ */
150
+ const changeOwn = (account, reservations, change) => {
151
+ for (const reservation of reservations) {
152
+ if (!reservation)
153
+ continue;
154
+ const keys = [...own.current]
155
+ .filter(([, entry]) => change.kind === "shown" && entry.reservation === reservation)
156
+ .map(([key]) => key);
157
+ const mine = `${account.toLowerCase()}:${reservation}`;
158
+ for (const key of keys.length ? keys : [mine]) {
159
+ const entry = own.current.get(key);
160
+ own.current.set(key, mergeOwn(entry, entry?.account ?? account, reservation, change));
161
+ }
162
+ }
163
+ };
164
+ const ownSuccess = (action) => {
165
+ if (!deps?.account ||
166
+ (action.account &&
167
+ action.account.toLowerCase() !== deps.account.toLowerCase()))
168
+ return undefined;
169
+ const kept = [...own.current.values()]
170
+ .filter((entry) => entry.state === "owned" &&
171
+ entry.account.toLowerCase() === deps.account.toLowerCase())
172
+ .map((entry) => entry.reservation);
173
+ if (!kept.length)
174
+ return undefined;
175
+ // Only the action's last step's hold, as `follow` maps it: a success
176
+ // of an earlier step never completes the action. Its Safe mode may not
177
+ // be known yet; a reservation is only ever in its own mode's hold.
178
+ const account = deps.account;
179
+ const lasts = [false, true].map((safe) => {
180
+ const steps = heldSteps(account, safe, action);
181
+ return steps[steps.length - 1];
182
+ });
183
+ for (const { call, step } of lasts) {
184
+ try {
185
+ // A reservation is read only through its own hold: another hold
186
+ // (another intent, or another exact call) never answers for it.
187
+ const handle = openStepHold(deps.hold, deps.account, false, call);
188
+ for (const reservation of kept) {
189
+ const settled = handle.settlementOf(reservation);
190
+ if (settled?.result === "success")
191
+ return {
192
+ settled: settled,
193
+ handle,
194
+ account,
195
+ call: call,
196
+ step,
197
+ reservation,
198
+ };
199
+ }
200
+ }
201
+ catch {
202
+ /* A call the hold can't open holds nothing of this form's. */
203
+ }
204
+ }
205
+ return undefined;
206
+ };
75
207
  const replaced = () => {
76
208
  latest.current += 1;
77
209
  life.current.abort();
@@ -90,9 +222,12 @@ export function useReviewedRun(deps) {
90
222
  };
91
223
  }, []);
92
224
  /** How a followed step ended, as the run that followed it shows it. */
93
- const showSettled = (run, step, total, settled, receipt, acknowledge) => {
225
+ const showSettled = (run, step, total, settled, receipt, acknowledge, account, reservation) => {
94
226
  if (latest.current !== run)
95
227
  return;
228
+ // Shown: its own entry is released, unless `dismiss` closed it while
229
+ // this run followed it (its showing the user closed never counts).
230
+ changeOwn(account, [reservation], { kind: "shown", run });
96
231
  const finished = step === total - 1;
97
232
  setState((now) => settled.result !== "success"
98
233
  ? {
@@ -198,9 +333,24 @@ export function useReviewedRun(deps) {
198
333
  const waiting = awaiting.current;
199
334
  if (open.current && !waiting)
200
335
  return false;
336
+ /**
337
+ * The followed last step this closes, under the account that sent it,
338
+ * and the run that followed it: a retry of it never sends it again.
339
+ */
340
+ const last = waiting
341
+ ? waiting.last && { ...waiting.owner, run: waiting.run }
342
+ : watched?.step === watched?.total - 1 && watched;
343
+ const closing = () => {
344
+ if (last)
345
+ changeOwn(last.account, [last.reservation], {
346
+ kind: "dismiss",
347
+ run: last.run,
348
+ });
349
+ };
201
350
  if (waiting?.last) {
202
351
  awaiting.current = null;
203
352
  waiting.end();
353
+ closing();
204
354
  return true;
205
355
  }
206
356
  detach(waiting
@@ -209,9 +359,13 @@ export function useReviewedRun(deps) {
209
359
  handle: watched.handle,
210
360
  client: watched.client,
211
361
  call: watched.call,
362
+ account: watched.account,
212
363
  reservation: watched.reservation,
213
364
  });
365
+ // Closing the view keeps what this form sent: a reset forgets no
366
+ // entry, so a later dismiss of the idle view never forgets one.
214
367
  reset();
368
+ closing();
215
369
  return true;
216
370
  },
217
371
  preload,
@@ -231,6 +385,102 @@ export function useReviewedRun(deps) {
231
385
  setWatched(null);
232
386
  },
233
387
  run: async ({ onCancelled, onStopped, ...action }) => {
388
+ /**
389
+ * The account this run sends from, read once: every continuation of
390
+ * it uses this one, whatever account the app's deps hold by then.
391
+ */
392
+ const runAccount = deps?.account;
393
+ /**
394
+ * The wallet's Safe mode, only while it still speaks for the run's
395
+ * account: another account's mode never names this run's holds.
396
+ */
397
+ const runSafe = () => deps && deps.account?.toLowerCase() === runAccount?.toLowerCase()
398
+ ? deps.adapter.isSafe()
399
+ : undefined;
400
+ /** This form's unshown write of these calls succeeded: the form closes on it. */
401
+ const close = async (found) => {
402
+ const { settled, handle } = found;
403
+ // Every exit holds the run open, and this form's ownership stays
404
+ // until its success is shown and acknowledged: until then a dismiss
405
+ // is refused and a later run closes on the same success again.
406
+ open.current += 1;
407
+ try {
408
+ // Completed as its run would have, under this run's locks: told to
409
+ // onConfirmed once (unless another completion did), and done only
410
+ // once delivered.
411
+ let failure;
412
+ const told = await load(deps.engine)
413
+ .then((engine) => engine.completeReviewedStep({
414
+ step: found.step,
415
+ held: { call: found.call, reservation: found.reservation },
416
+ }, {
417
+ account: found.account,
418
+ scope: action.scope,
419
+ onConfirmed: action.onConfirmed,
420
+ locks: action.locks,
421
+ attemptScoped: action.attemptScoped,
422
+ }, deps, null, alive, false))
423
+ .catch((error) => {
424
+ failure = error;
425
+ return null;
426
+ });
427
+ if (!told || told.undelivered) {
428
+ failure ??= new LockLostError(action.locks?.[0]?.name ?? "site");
429
+ const error = errorLine(failure);
430
+ update({ ...IDLE, error, failure });
431
+ return { status: "failed", error, failure };
432
+ }
433
+ const step = action.calls.length - 1;
434
+ update({
435
+ ...IDLE,
436
+ // Shown now: acknowledged once rendered, so it blocks no new send.
437
+ // Rendered: the form's own is released, and a later run waits
438
+ // for this acknowledgement before it sends.
439
+ acknowledge: async () => {
440
+ if (alive.aborted)
441
+ return;
442
+ changeOwn(found.account, [found.reservation], {
443
+ kind: "shown",
444
+ });
445
+ await handle.acknowledge(settled).catch(() => undefined);
446
+ },
447
+ phase: "done",
448
+ step,
449
+ total: action.calls.length,
450
+ hash: settled.hash,
451
+ settled: { step, finished: true },
452
+ });
453
+ return {
454
+ status: "done",
455
+ hashes: told.hashes,
456
+ receipts: [],
457
+ ...(told.signatures ? { signatures: told.signatures } : {}),
458
+ };
459
+ }
460
+ finally {
461
+ open.current -= 1;
462
+ }
463
+ };
464
+ // This form's own write is still followed: a retry of it is refused
465
+ // with the hold's line, and the run following it goes on.
466
+ const now = watchedNow.current;
467
+ // Identity, never bytes: the followed reservation is one of these
468
+ // calls' holds'. A scoped sequence's journal answers for its own.
469
+ const followed = now?.reservation &&
470
+ runAccount &&
471
+ (action.scope === undefined || action.calls.length === 1)
472
+ ? now
473
+ : null;
474
+ const ownState = followed?.handle.peek();
475
+ if (followed &&
476
+ ownState?.status !== "clear" &&
477
+ ownState?.reservation === followed.reservation &&
478
+ holdsOf(runAccount, action.calls).some(({ reservation }) => reservation === followed.reservation)) {
479
+ const failure = new WriteHeldError(ownState);
480
+ const error = errorLine(failure);
481
+ setState((now) => ({ ...now, error, failure }));
482
+ return { status: "failed", error, failure };
483
+ }
234
484
  replaced();
235
485
  const id = latest.current;
236
486
  const { signal: alive } = life.current;
@@ -244,6 +494,21 @@ export function useReviewedRun(deps) {
244
494
  setState((now) => ({ ...now, ...next }));
245
495
  };
246
496
  setState({ ...IDLE, phase: "review", total: action.calls.length });
497
+ const succeeded = ownSuccess(action);
498
+ if (succeeded) {
499
+ setWatched(null);
500
+ // close holds the run open itself, from here until it ends.
501
+ open.current -= 1;
502
+ return close(succeeded);
503
+ }
504
+ /**
505
+ * Reservations the action's holds had before this run: another run's
506
+ * (another form's, sharing a hold). This run never follows one.
507
+ */
508
+ const holds = () => holdsOf(runAccount, runAccount && action.batch && action.calls.length > 1
509
+ ? [...action.calls, safeBatchCall(runAccount, action.calls)]
510
+ : action.calls);
511
+ const before = new Set(holds().map(({ reservation }) => reservation));
247
512
  let loaded;
248
513
  /** A step of the action was sent before this run, or by it: never a cancel. */
249
514
  let sentAny = false;
@@ -272,9 +537,11 @@ export function useReviewedRun(deps) {
272
537
  let completed = Promise.resolve(null);
273
538
  let shown = false;
274
539
  const action$ = {
275
- account: deps.account,
540
+ account: runAccount,
276
541
  scope: action.scope,
277
542
  onConfirmed: action.onConfirmed,
543
+ locks: action.locks,
544
+ attemptScoped: action.attemptScoped,
278
545
  };
279
546
  const finish = (seen, receipt = null) => {
280
547
  if (shown || alive.aborted)
@@ -288,11 +555,30 @@ export function useReviewedRun(deps) {
288
555
  return null;
289
556
  }));
290
557
  void completed.then((ended) => {
558
+ // Its notice undelivered: the form's own, whichever run shows
559
+ // now (a retry that replaced this one included), so a later
560
+ // run closes on it, telling it then.
561
+ if (ended?.undelivered && target.step === total - 1)
562
+ changeOwn(action$.account, [target.held?.reservation], {
563
+ kind: "keep",
564
+ });
291
565
  if (shown || alive.aborted)
292
566
  return;
293
- // Nothing advanced (its receipt unread, or its hold not settled
294
- // for this send yet): it is tried again.
295
- if (unread || (!ended && seen)) {
567
+ // Nothing advanced (its receipt unread, its hold not settled
568
+ // for this send yet, or its onConfirmed not delivered): it is
569
+ // tried again, still followed and acknowledged by nothing.
570
+ // ponytail: retried at the receipt's pace while mounted.
571
+ if (unread || (!ended && seen) || ended?.undelivered) {
572
+ if (ended?.undelivered) {
573
+ completed = Promise.resolve(null);
574
+ // While the site's work under its locks runs, its notice may
575
+ // wait for it (kept for a lock this run lost, or another
576
+ // completion's claim queued behind one it holds): the wait
577
+ // ends now, retryable, and is never what holds that work up.
578
+ // Once that work ended, nothing of the caller's is in its way.
579
+ if (action.locks?.some(lockWorkRunning))
580
+ end?.("queued");
581
+ }
296
582
  if (mounted.current)
297
583
  setTimeout(() => finish(seen, receipt), RECEIPT_RETRY_MS);
298
584
  return;
@@ -302,8 +588,8 @@ export function useReviewedRun(deps) {
302
588
  shown = true;
303
589
  // Shown with the receipt completion checked, a failure's too.
304
590
  showSettled(id, target.step, total, ended.settled, ended.receipt, ended.settled.result === "success" && target.step === total - 1
305
- ? acknowledging(openStepHold(deps.hold, deps.account, false, target.held.call), ended.settled)
306
- : null);
591
+ ? acknowledging(openStepHold(deps.hold, action$.account, false, target.held.call), ended.settled)
592
+ : null, action$.account, target.held?.reservation);
307
593
  setWatched(null);
308
594
  end?.(ended);
309
595
  });
@@ -315,9 +601,10 @@ export function useReviewedRun(deps) {
315
601
  * found one, or null when the wallet could not say (its Safe mode unknown).
316
602
  */
317
603
  const follow = async (finish, released) => {
318
- if (latest.current !== id || !deps?.account)
604
+ if (latest.current !== id || !deps || !runAccount)
319
605
  return false;
320
- const { account, adapter, hold, storage } = deps;
606
+ const { adapter, hold, storage } = deps;
607
+ const account = runAccount;
321
608
  // Its Safe mode is read only once the wallet says it is known.
322
609
  try {
323
610
  await walletReady(adapter);
@@ -328,36 +615,48 @@ export function useReviewedRun(deps) {
328
615
  try {
329
616
  if (latest.current !== id)
330
617
  return false;
331
- const safe = adapter.isSafe();
618
+ const safe = runSafe();
619
+ if (safe === undefined)
620
+ return null;
332
621
  const saved = !action.scope
333
622
  ? null
334
623
  : findReviewedSequence(action.scope, account, { storage });
335
624
  const total = saved?.calls.length ?? action.calls.length;
336
- // A Safe app's batch is held as its one MultiSend proposal, its last step.
337
- const steps = saved?.sending
338
- ? [
339
- {
340
- call: saved.calls[saved.sending.step],
341
- step: saved.sending.step,
342
- },
343
- ]
344
- : !saved && action.batch && safe && action.calls.length > 1
345
- ? [
346
- {
347
- call: safeBatchCall(account, action.calls),
348
- step: total - 1,
349
- },
350
- ]
351
- : action.calls.map((call, step) => ({ call, step }));
352
- // A held step, or the last step's success nothing showed.
625
+ const steps = heldSteps(account, safe, action, saved);
626
+ // A held step, or the last step's success nothing showed, of this
627
+ // run (or of the saved sequence it resumes), never another run's.
353
628
  const found = steps
354
629
  .map(({ call, step }) => ({
355
630
  call,
356
631
  step,
357
632
  handle: openStepHold(hold, account, safe, call),
358
633
  }))
359
- .find(({ handle, step }) => handle.peek().status !== "clear" ||
360
- (step === total - 1 && handle.unacknowledgedSuccess()));
634
+ .find(({ handle, step, call }) => {
635
+ const held = handle.peek();
636
+ const success = step === total - 1 ? handle.unacknowledgedSuccess() : undefined;
637
+ if (held.status === "clear" && !success)
638
+ return false;
639
+ const reservation = held.status !== "clear"
640
+ ? held.reservation
641
+ : success?.reservation;
642
+ // A write held before this run, under an intent other forms
643
+ // share, is followed only when this runner followed it before,
644
+ // or it is this exact call (or this saved sequence's): another
645
+ // form's write is never adopted.
646
+ if (!reservation ||
647
+ !before.has(reservation) ||
648
+ mine.current.has(reservation) ||
649
+ reservation === saved?.sending?.reservation ||
650
+ !call.intent)
651
+ return true;
652
+ if (held.status === "clear")
653
+ return false;
654
+ const record = listReviewedWrites({ chainId: call.chainId, account }, { storage }).find(({ record }) => record.id === reservation)?.record;
655
+ return (!!record &&
656
+ record.call.to.toLowerCase() === call.to.toLowerCase() &&
657
+ record.call.data.toLowerCase() === call.data.toLowerCase() &&
658
+ record.call.value === String(call.value ?? 0n));
659
+ });
361
660
  // Its settlement completes it as a send's would: a proposal too.
362
661
  const reservation = found?.handle.peek().reservation ??
363
662
  found?.handle.unacknowledgedSuccess()?.reservation;
@@ -366,6 +665,7 @@ export function useReviewedRun(deps) {
366
665
  handle: found.handle,
367
666
  client: deps.clientFor(found.call.chainId),
368
667
  run: id,
668
+ account,
369
669
  call: found.call,
370
670
  reservation,
371
671
  step: found.step,
@@ -379,6 +679,8 @@ export function useReviewedRun(deps) {
379
679
  }
380
680
  : null);
381
681
  // Only its hold is followed now: the confirm can close.
682
+ if (reservation)
683
+ mine.current.add(reservation);
382
684
  if (found)
383
685
  update({ following: true });
384
686
  return !!found;
@@ -390,13 +692,17 @@ export function useReviewedRun(deps) {
390
692
  };
391
693
  const heldStep = (target) => {
392
694
  const call = target.held?.call ?? action.calls[target.step];
393
- if (!deps?.account || !call)
695
+ if (!deps || !runAccount || !call)
394
696
  return null;
395
697
  try {
698
+ const safe = runSafe();
699
+ if (safe === undefined)
700
+ return null;
396
701
  return {
397
- handle: openStepHold(deps.hold, deps.account, deps.adapter.isSafe(), call),
702
+ handle: openStepHold(deps.hold, runAccount, safe, call),
398
703
  client: deps.clientFor(call.chainId),
399
704
  call,
705
+ account: runAccount,
400
706
  reservation: target.held?.reservation,
401
707
  };
402
708
  }
@@ -405,10 +711,10 @@ export function useReviewedRun(deps) {
405
711
  }
406
712
  };
407
713
  try {
408
- if (!deps?.account)
714
+ if (!deps || !runAccount)
409
715
  throw new ReviewLineError("Connect a wallet first.");
410
716
  if (action.account &&
411
- action.account.toLowerCase() !== deps.account.toLowerCase())
717
+ action.account.toLowerCase() !== runAccount.toLowerCase())
412
718
  throw new ReviewLineError(REVIEWED_ACCOUNT_CHANGED_LINE);
413
719
  if (action.calls.length > 1 && action.scope === undefined)
414
720
  throw new ReviewLineError(REVIEWED_RUN_SCOPE_LINE);
@@ -429,7 +735,14 @@ export function useReviewedRun(deps) {
429
735
  const { resumeReviewed, sendReviewed } = loaded;
430
736
  // A success already shown is acknowledged before this run checks it.
431
737
  await acknowledged.current;
432
- const { account, guard } = deps;
738
+ const settledOwn = () => {
739
+ const settled = ownSuccess(action);
740
+ if (settled)
741
+ throw new OwnSucceeded(settled);
742
+ };
743
+ settledOwn();
744
+ const { guard } = deps;
745
+ const account = runAccount;
433
746
  const tracked = {
434
747
  ...deps,
435
748
  onProgress: (progress) => {
@@ -486,9 +799,20 @@ export function useReviewedRun(deps) {
486
799
  tell(action.follow?.onConfirming, step);
487
800
  },
488
801
  };
802
+ const assertWriteState = () => {
803
+ settledOwn();
804
+ action.assertWriteState?.();
805
+ };
806
+ const gated = {
807
+ ...action,
808
+ guard,
809
+ follow: inPlace,
810
+ onSubmitted,
811
+ assertWriteState,
812
+ };
489
813
  const result = saved
490
- ? await resumeReviewed(saved, { ...action, guard, follow: inPlace, onSubmitted }, tracked)
491
- : await sendReviewed({ ...action, account, guard, follow: inPlace, onSubmitted }, tracked);
814
+ ? await resumeReviewed(saved, gated, tracked)
815
+ : await sendReviewed({ ...gated, account }, tracked);
492
816
  // A wallet that reviews on its own page reports no progress: the
493
817
  // result is the run's last word.
494
818
  const calls = saved?.calls ?? action.calls;
@@ -513,6 +837,8 @@ export function useReviewedRun(deps) {
513
837
  return result;
514
838
  }
515
839
  catch (error) {
840
+ if (error instanceof OwnSucceeded)
841
+ return close(error.found);
516
842
  if (error instanceof ReviewedReceiptUnverifiedError) {
517
843
  // Still confirming: followed until its hold settles it.
518
844
  update({
@@ -541,7 +867,12 @@ export function useReviewedRun(deps) {
541
867
  leave.current = abandon;
542
868
  cancelled?.addEventListener("abort", abandon, { once: true });
543
869
  awaiting.current = {
870
+ run: id,
544
871
  last: error.step === action.calls.length - 1,
872
+ owner: {
873
+ account: runAccount,
874
+ reservation: error.held?.reservation,
875
+ },
545
876
  detached: heldStep(error),
546
877
  end: abandon,
547
878
  };
@@ -549,7 +880,6 @@ export function useReviewedRun(deps) {
549
880
  const finish = completing(error, action.calls.length, end);
550
881
  // Followed for a later settlement; one already there is read once it attaches.
551
882
  void follow(finish, () => end(null)).then(() => finish());
552
- const before = error.held?.hashes ?? [];
553
883
  return {
554
884
  status: "receipt",
555
885
  step: error.step,
@@ -560,6 +890,11 @@ export function useReviewedRun(deps) {
560
890
  ended: () => outcome.then(async (ran) => {
561
891
  if (!ran)
562
892
  return null;
893
+ if (ran === "queued") {
894
+ const failure = new ConfirmationQueuedError(error.step);
895
+ const line = errorLine(failure);
896
+ return { status: "failed", error: line, failure };
897
+ }
563
898
  const { settled, receipt } = ran;
564
899
  if (settled.result !== "success") {
565
900
  const line = settledLine(settled);
@@ -569,7 +904,8 @@ export function useReviewedRun(deps) {
569
904
  failure: new ReviewLineError(line),
570
905
  };
571
906
  }
572
- const signatures = error.held?.signatures;
907
+ // The run's own, as its hold saved them with the step.
908
+ const { signatures } = ran;
573
909
  if (error.step < action.calls.length - 1) {
574
910
  const started = await runLatest.current({
575
911
  ...action,
@@ -587,7 +923,7 @@ export function useReviewedRun(deps) {
587
923
  }
588
924
  return {
589
925
  status: "done",
590
- hashes: [...before, settled.hash],
926
+ hashes: ran.hashes,
591
927
  receipts: receipt ? [receipt] : [],
592
928
  ...(signatures ? { signatures } : {}),
593
929
  };
@@ -596,9 +932,9 @@ export function useReviewedRun(deps) {
596
932
  }
597
933
  // A resume can complete a step the wallet sent before the reload.
598
934
  try {
599
- const after = action.scope === undefined || !deps?.account
935
+ const after = action.scope === undefined || !runAccount
600
936
  ? null
601
- : findReviewedSequence(action.scope, deps.account, {
937
+ : findReviewedSequence(action.scope, runAccount, {
602
938
  storage: deps.storage,
603
939
  });
604
940
  if (after?.completed.length &&
@@ -610,6 +946,19 @@ export function useReviewedRun(deps) {
610
946
  catch {
611
947
  /* An unreadable sequence adds nothing to what this run saw. */
612
948
  }
949
+ // Refused by a write already held (another form's, sharing the
950
+ // hold): this run follows it only as its own exact call.
951
+ const refused = error instanceof WriteHeldError
952
+ ? error.state.reservation
953
+ : error instanceof WriteHoldUnacknowledgedSuccessError
954
+ ? error.success.reservation
955
+ : undefined;
956
+ if (refused)
957
+ before.add(refused);
958
+ // ponytail: one call only; a sequence's journal keeps its steps.
959
+ const left = holds().filter(({ reservation }) => !before.has(reservation));
960
+ if (action.calls.length === 1 && left.length && runAccount)
961
+ changeOwn(runAccount, left.map(({ reservation }) => reservation), { kind: "keep" });
613
962
  const cancelled = error instanceof TransactionReviewCancelledError ||
614
963
  isDefiniteWalletRejection(error);
615
964
  const line = cancelled ? null : errorLine(error);