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