@bananapus/nana-sdk-core 6.1.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.
- 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/heldWrite.d.ts +6 -1
- package/dist/cjs/review/heldWrite.d.ts.map +1 -1
- package/dist/cjs/review/heldWrite.js +13 -6
- package/dist/cjs/review/heldWrite.js.map +1 -1
- package/dist/cjs/review/locks.d.ts +141 -2
- package/dist/cjs/review/locks.d.ts.map +1 -1
- package/dist/cjs/review/locks.js +406 -58
- 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 +17 -3
- package/dist/cjs/review/react/useReviewedRun.d.ts.map +1 -1
- package/dist/cjs/review/react/useReviewedRun.js +528 -68
- package/dist/cjs/review/react/useReviewedRun.js.map +1 -1
- package/dist/cjs/review/react/useSavedAttempt.d.ts.map +1 -1
- package/dist/cjs/review/react/useSavedAttempt.js +12 -0
- package/dist/cjs/review/react/useSavedAttempt.js.map +1 -1
- package/dist/cjs/review/relayrSend.d.ts +20 -0
- package/dist/cjs/review/relayrSend.d.ts.map +1 -1
- package/dist/cjs/review/relayrSend.js +150 -105
- package/dist/cjs/review/relayrSend.js.map +1 -1
- package/dist/cjs/review/runStatus.d.ts +6 -2
- package/dist/cjs/review/runStatus.d.ts.map +1 -1
- package/dist/cjs/review/runStatus.js +3 -1
- package/dist/cjs/review/runStatus.js.map +1 -1
- package/dist/cjs/review/safeRelayr.d.ts +12 -2
- package/dist/cjs/review/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +226 -156
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/cjs/review/savedAttempt.d.ts.map +1 -1
- package/dist/cjs/review/savedAttempt.js +7 -2
- package/dist/cjs/review/savedAttempt.js.map +1 -1
- package/dist/cjs/review/send.d.ts +46 -5
- package/dist/cjs/review/send.d.ts.map +1 -1
- package/dist/cjs/review/send.js +251 -58
- 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/sign.d.ts +6 -1
- package/dist/cjs/review/sign.d.ts.map +1 -1
- package/dist/cjs/review/sign.js +10 -4
- package/dist/cjs/review/sign.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 +16 -4
- package/dist/cjs/review/wallet.d.ts.map +1 -1
- package/dist/cjs/review/wallet.js +62 -17
- package/dist/cjs/review/wallet.js.map +1 -1
- package/dist/cjs/review/writeHold.d.ts +92 -14
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +102 -26
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +42 -7
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +183 -21
- 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/heldWrite.d.ts +6 -1
- package/dist/esm/review/heldWrite.d.ts.map +1 -1
- package/dist/esm/review/heldWrite.js +13 -6
- package/dist/esm/review/heldWrite.js.map +1 -1
- package/dist/esm/review/locks.d.ts +141 -2
- package/dist/esm/review/locks.d.ts.map +1 -1
- package/dist/esm/review/locks.js +396 -58
- 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 +17 -3
- package/dist/esm/review/react/useReviewedRun.d.ts.map +1 -1
- package/dist/esm/review/react/useReviewedRun.js +529 -69
- package/dist/esm/review/react/useReviewedRun.js.map +1 -1
- package/dist/esm/review/react/useSavedAttempt.d.ts.map +1 -1
- package/dist/esm/review/react/useSavedAttempt.js +12 -0
- package/dist/esm/review/react/useSavedAttempt.js.map +1 -1
- package/dist/esm/review/relayrSend.d.ts +20 -0
- package/dist/esm/review/relayrSend.d.ts.map +1 -1
- package/dist/esm/review/relayrSend.js +154 -109
- package/dist/esm/review/relayrSend.js.map +1 -1
- package/dist/esm/review/runStatus.d.ts +6 -2
- package/dist/esm/review/runStatus.d.ts.map +1 -1
- package/dist/esm/review/runStatus.js +3 -1
- package/dist/esm/review/runStatus.js.map +1 -1
- package/dist/esm/review/safeRelayr.d.ts +12 -2
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +226 -156
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/dist/esm/review/savedAttempt.d.ts.map +1 -1
- package/dist/esm/review/savedAttempt.js +7 -2
- package/dist/esm/review/savedAttempt.js.map +1 -1
- package/dist/esm/review/send.d.ts +46 -5
- package/dist/esm/review/send.d.ts.map +1 -1
- package/dist/esm/review/send.js +252 -59
- 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/sign.d.ts +6 -1
- package/dist/esm/review/sign.d.ts.map +1 -1
- package/dist/esm/review/sign.js +10 -4
- package/dist/esm/review/sign.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 +16 -4
- package/dist/esm/review/wallet.d.ts.map +1 -1
- package/dist/esm/review/wallet.js +62 -17
- package/dist/esm/review/wallet.js.map +1 -1
- package/dist/esm/review/writeHold.d.ts +92 -14
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +102 -26
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +42 -7
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +180 -22
- 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,10 +1,14 @@
|
|
|
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, 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,
|
|
10
|
+
stopped: false,
|
|
11
|
+
following: false,
|
|
8
12
|
lastSubmittedStep: null,
|
|
9
13
|
phase: "idle",
|
|
10
14
|
step: 0,
|
|
@@ -15,6 +19,7 @@ const IDLE = {
|
|
|
15
19
|
safeTxHash: null,
|
|
16
20
|
receipt: null,
|
|
17
21
|
settled: null,
|
|
22
|
+
acknowledge: null,
|
|
18
23
|
};
|
|
19
24
|
/** The bytes a call sends, for comparing two actions. */
|
|
20
25
|
const sent = ({ chainId, to, data, value, gas }) => [
|
|
@@ -24,6 +29,28 @@ const sent = ({ chainId, to, data, value, gas }) => [
|
|
|
24
29
|
value ?? 0n,
|
|
25
30
|
gas ?? null,
|
|
26
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
|
+
}
|
|
27
54
|
export const REVIEWED_RUN_SCOPE_LINE = "This action has more than one step and no scope to resume it by. Nothing was sent.";
|
|
28
55
|
export const REVIEWED_RUN_BUSY_LINE = "This action is still running. Wait for it to finish, then try again.";
|
|
29
56
|
/**
|
|
@@ -33,18 +60,46 @@ export const REVIEWED_RUN_BUSY_LINE = "This action is still running. Wait for it
|
|
|
33
60
|
*/
|
|
34
61
|
export function useReviewedRun(deps) {
|
|
35
62
|
const [state, setState] = useState(IDLE);
|
|
36
|
-
/** A cancel to tell once
|
|
63
|
+
/** A cancel or stop to tell once rendered. */
|
|
37
64
|
const [cancelling, setCancelling] = useState(null);
|
|
38
65
|
useEffect(() => {
|
|
39
66
|
if (cancelling)
|
|
40
|
-
void tell(cancelling.
|
|
67
|
+
void tell(cancelling.told);
|
|
41
68
|
}, [cancelling]);
|
|
69
|
+
/** Every shown success's acknowledgement: the next run waits for all. */
|
|
70
|
+
const acknowledged = useRef(Promise.resolve());
|
|
71
|
+
// A success is acknowledged only once its done rendered: a reset after
|
|
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.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (state.acknowledge)
|
|
76
|
+
acknowledged.current = Promise.all([
|
|
77
|
+
acknowledged.current,
|
|
78
|
+
state.acknowledge(),
|
|
79
|
+
]).then(() => undefined);
|
|
80
|
+
}, [state.acknowledge]);
|
|
42
81
|
/** The hold followed after a run, and its chain's client: its view is `held`. */
|
|
43
|
-
const [watched,
|
|
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
|
+
};
|
|
44
92
|
/** The latest run: an earlier one's late answers change nothing. */
|
|
45
93
|
const latest = useRef(0);
|
|
94
|
+
/** The sent step whose hold the latest run's `ended()` awaits. */
|
|
95
|
+
const awaiting = useRef(null);
|
|
46
96
|
/** How many runs are open: a discard waits for none. */
|
|
47
97
|
const open = useRef(0);
|
|
98
|
+
/**
|
|
99
|
+
* The latest run's life: aborted when it is replaced, reset, dismissed or
|
|
100
|
+
* unmounted, so its completion acknowledges nothing it can no longer show.
|
|
101
|
+
*/
|
|
102
|
+
const life = useRef(new AbortController());
|
|
48
103
|
const mounted = useRef(false);
|
|
49
104
|
/** This render's `run`: a run's end that continues the action runs it. */
|
|
50
105
|
const runLatest = useRef(async () => null);
|
|
@@ -54,8 +109,106 @@ export function useReviewedRun(deps) {
|
|
|
54
109
|
* hold keeps the sent step.
|
|
55
110
|
*/
|
|
56
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
|
+
};
|
|
57
207
|
const replaced = () => {
|
|
58
208
|
latest.current += 1;
|
|
209
|
+
life.current.abort();
|
|
210
|
+
life.current = new AbortController();
|
|
211
|
+
awaiting.current = null;
|
|
59
212
|
const left = leave.current;
|
|
60
213
|
leave.current = () => { };
|
|
61
214
|
left();
|
|
@@ -69,9 +222,12 @@ export function useReviewedRun(deps) {
|
|
|
69
222
|
};
|
|
70
223
|
}, []);
|
|
71
224
|
/** How a followed step ended, as the run that followed it shows it. */
|
|
72
|
-
const showSettled = (run, step, total, settled, receipt) => {
|
|
225
|
+
const showSettled = (run, step, total, settled, receipt, acknowledge, account, reservation) => {
|
|
73
226
|
if (latest.current !== run)
|
|
74
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 });
|
|
75
231
|
const finished = step === total - 1;
|
|
76
232
|
setState((now) => settled.result !== "success"
|
|
77
233
|
? {
|
|
@@ -83,12 +239,14 @@ export function useReviewedRun(deps) {
|
|
|
83
239
|
: finished
|
|
84
240
|
? {
|
|
85
241
|
...now,
|
|
242
|
+
following: false,
|
|
86
243
|
phase: "done",
|
|
87
244
|
hash: settled.hash,
|
|
88
245
|
receipt,
|
|
89
246
|
error: null,
|
|
90
247
|
failure: null,
|
|
91
248
|
settled: { step, finished },
|
|
249
|
+
acknowledge,
|
|
92
250
|
}
|
|
93
251
|
: {
|
|
94
252
|
...IDLE,
|
|
@@ -109,6 +267,11 @@ export function useReviewedRun(deps) {
|
|
|
109
267
|
// never stays proposed, and never shows done unverified.
|
|
110
268
|
onSettled: (settled, receipt) => watched.finish(settled, receipt),
|
|
111
269
|
onReleased: () => {
|
|
270
|
+
// A later reservation's release (another tab's) never ends a
|
|
271
|
+
// write that settled: its settlement completes it.
|
|
272
|
+
if (watched.reservation &&
|
|
273
|
+
watched.handle.settlementOf(watched.reservation))
|
|
274
|
+
return;
|
|
112
275
|
watched.released?.();
|
|
113
276
|
if (latest.current === watched.run)
|
|
114
277
|
setState((now) => ({
|
|
@@ -142,16 +305,68 @@ export function useReviewedRun(deps) {
|
|
|
142
305
|
shown.phase === "settled"
|
|
143
306
|
? null
|
|
144
307
|
: shown;
|
|
145
|
-
const { cancelled: _cancelled, ...shownState } = state;
|
|
308
|
+
const { cancelled: _cancelled, stopped: _stopped, acknowledge: _acknowledge, ...shownState } = state;
|
|
309
|
+
const status = reviewedRunStatus({ ...state, held });
|
|
310
|
+
const reset = () => {
|
|
311
|
+
replaced();
|
|
312
|
+
setState(IDLE);
|
|
313
|
+
setWatched(null);
|
|
314
|
+
};
|
|
315
|
+
const detach = (target) => {
|
|
316
|
+
// ponytail: unbounded; it ends once the hold is clear.
|
|
317
|
+
if (target)
|
|
318
|
+
void followHeldWrite(target.handle, target.client, {
|
|
319
|
+
signal: new AbortController().signal,
|
|
320
|
+
...(target.reservation ? { reservation: target.reservation } : {}),
|
|
321
|
+
}).then((end) => end?.status === "clear" &&
|
|
322
|
+
end.settled?.result === "success" &&
|
|
323
|
+
tell(deps?.onDismissedSettled, target.call), () => undefined);
|
|
324
|
+
};
|
|
146
325
|
const api = {
|
|
147
326
|
...shownState,
|
|
148
|
-
...
|
|
327
|
+
...status,
|
|
328
|
+
busy: status.busy && !state.following,
|
|
149
329
|
held,
|
|
150
330
|
...actionsOf(shown, held ? view : undefined),
|
|
151
|
-
reset
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
331
|
+
reset,
|
|
332
|
+
dismiss: () => {
|
|
333
|
+
const waiting = awaiting.current;
|
|
334
|
+
if (open.current && !waiting)
|
|
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
|
+
};
|
|
350
|
+
if (waiting?.last) {
|
|
351
|
+
awaiting.current = null;
|
|
352
|
+
waiting.end();
|
|
353
|
+
closing();
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
detach(waiting
|
|
357
|
+
? waiting.detached
|
|
358
|
+
: watched && {
|
|
359
|
+
handle: watched.handle,
|
|
360
|
+
client: watched.client,
|
|
361
|
+
call: watched.call,
|
|
362
|
+
account: watched.account,
|
|
363
|
+
reservation: watched.reservation,
|
|
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.
|
|
367
|
+
reset();
|
|
368
|
+
closing();
|
|
369
|
+
return true;
|
|
155
370
|
},
|
|
156
371
|
preload,
|
|
157
372
|
discard: async (scope, account = deps?.account) => {
|
|
@@ -169,9 +384,106 @@ export function useReviewedRun(deps) {
|
|
|
169
384
|
setState(IDLE);
|
|
170
385
|
setWatched(null);
|
|
171
386
|
},
|
|
172
|
-
run: async ({ onCancelled, ...action }) => {
|
|
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
|
+
}
|
|
173
484
|
replaced();
|
|
174
485
|
const id = latest.current;
|
|
486
|
+
const { signal: alive } = life.current;
|
|
175
487
|
// A step still confirming is followed in place, in the same review,
|
|
176
488
|
// until a reset or a later run ends it.
|
|
177
489
|
const following = new AbortController();
|
|
@@ -182,6 +494,21 @@ export function useReviewedRun(deps) {
|
|
|
182
494
|
setState((now) => ({ ...now, ...next }));
|
|
183
495
|
};
|
|
184
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));
|
|
185
512
|
let loaded;
|
|
186
513
|
/** A step of the action was sent before this run, or by it: never a cancel. */
|
|
187
514
|
let sentAny = false;
|
|
@@ -193,36 +520,65 @@ export function useReviewedRun(deps) {
|
|
|
193
520
|
update({ lastSubmittedStep: step });
|
|
194
521
|
};
|
|
195
522
|
setWatched(null);
|
|
523
|
+
/** Acknowledges a success this run showed, unless the run is gone. */
|
|
524
|
+
const acknowledging = (handle, settled) => async () => {
|
|
525
|
+
if (!alive.aborted)
|
|
526
|
+
await handle.acknowledge(settled).catch(() => undefined);
|
|
527
|
+
};
|
|
196
528
|
/**
|
|
197
529
|
* Completes a sent step once its hold settled it, as the run would
|
|
198
530
|
* have (`completeReviewedStep`: the journal, `onConfirmed` once with
|
|
199
|
-
* the receipt its hold verified
|
|
200
|
-
*
|
|
531
|
+
* the receipt its hold verified), then shows how it ended, once. A
|
|
532
|
+
* last step's success is acknowledged once that rendered; an earlier
|
|
533
|
+
* step's at once, its sequence's journal keeping it from sending again. Until it can (the receipt unread, or the hold not yet
|
|
201
534
|
* settled for this send), it is tried again, never shown.
|
|
202
535
|
*/
|
|
203
536
|
const completing = (target, total, end) => {
|
|
204
537
|
let completed = Promise.resolve(null);
|
|
205
538
|
let shown = false;
|
|
206
539
|
const action$ = {
|
|
207
|
-
account:
|
|
540
|
+
account: runAccount,
|
|
208
541
|
scope: action.scope,
|
|
209
542
|
onConfirmed: action.onConfirmed,
|
|
543
|
+
locks: action.locks,
|
|
544
|
+
attemptScoped: action.attemptScoped,
|
|
210
545
|
};
|
|
211
546
|
const finish = (seen, receipt = null) => {
|
|
547
|
+
if (shown || alive.aborted)
|
|
548
|
+
return;
|
|
212
549
|
let unread = false;
|
|
213
550
|
completed = completed.then((prior) => prior ??
|
|
214
551
|
(loaded ? Promise.resolve(loaded) : load(deps.engine))
|
|
215
|
-
.then((engine) => engine.completeReviewedStep(target, action$, deps, receipt))
|
|
552
|
+
.then((engine) => engine.completeReviewedStep(target, action$, deps, receipt, alive, target.step < total - 1))
|
|
216
553
|
.catch(() => {
|
|
217
554
|
unread = true;
|
|
218
555
|
return null;
|
|
219
556
|
}));
|
|
220
557
|
void completed.then((ended) => {
|
|
221
|
-
|
|
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
|
+
});
|
|
565
|
+
if (shown || alive.aborted)
|
|
222
566
|
return;
|
|
223
|
-
// Nothing advanced (its receipt unread,
|
|
224
|
-
// for this send yet): it is
|
|
225
|
-
|
|
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
|
+
}
|
|
226
582
|
if (mounted.current)
|
|
227
583
|
setTimeout(() => finish(seen, receipt), RECEIPT_RETRY_MS);
|
|
228
584
|
return;
|
|
@@ -231,7 +587,9 @@ export function useReviewedRun(deps) {
|
|
|
231
587
|
return;
|
|
232
588
|
shown = true;
|
|
233
589
|
// Shown with the receipt completion checked, a failure's too.
|
|
234
|
-
showSettled(id, target.step, total, ended.settled, ended.receipt
|
|
590
|
+
showSettled(id, target.step, total, ended.settled, ended.receipt, ended.settled.result === "success" && target.step === total - 1
|
|
591
|
+
? acknowledging(openStepHold(deps.hold, action$.account, false, target.held.call), ended.settled)
|
|
592
|
+
: null, action$.account, target.held?.reservation);
|
|
235
593
|
setWatched(null);
|
|
236
594
|
end?.(ended);
|
|
237
595
|
});
|
|
@@ -243,9 +601,10 @@ export function useReviewedRun(deps) {
|
|
|
243
601
|
* found one, or null when the wallet could not say (its Safe mode unknown).
|
|
244
602
|
*/
|
|
245
603
|
const follow = async (finish, released) => {
|
|
246
|
-
if (latest.current !== id || !deps
|
|
604
|
+
if (latest.current !== id || !deps || !runAccount)
|
|
247
605
|
return false;
|
|
248
|
-
const {
|
|
606
|
+
const { adapter, hold, storage } = deps;
|
|
607
|
+
const account = runAccount;
|
|
249
608
|
// Its Safe mode is read only once the wallet says it is known.
|
|
250
609
|
try {
|
|
251
610
|
await walletReady(adapter);
|
|
@@ -256,36 +615,48 @@ export function useReviewedRun(deps) {
|
|
|
256
615
|
try {
|
|
257
616
|
if (latest.current !== id)
|
|
258
617
|
return false;
|
|
259
|
-
const safe =
|
|
618
|
+
const safe = runSafe();
|
|
619
|
+
if (safe === undefined)
|
|
620
|
+
return null;
|
|
260
621
|
const saved = !action.scope
|
|
261
622
|
? null
|
|
262
623
|
: findReviewedSequence(action.scope, account, { storage });
|
|
263
624
|
const total = saved?.calls.length ?? action.calls.length;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
{
|
|
268
|
-
call: saved.calls[saved.sending.step],
|
|
269
|
-
step: saved.sending.step,
|
|
270
|
-
},
|
|
271
|
-
]
|
|
272
|
-
: !saved && action.batch && safe && action.calls.length > 1
|
|
273
|
-
? [
|
|
274
|
-
{
|
|
275
|
-
call: safeBatchCall(account, action.calls),
|
|
276
|
-
step: total - 1,
|
|
277
|
-
},
|
|
278
|
-
]
|
|
279
|
-
: action.calls.map((call, step) => ({ call, step }));
|
|
280
|
-
// 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.
|
|
281
628
|
const found = steps
|
|
282
629
|
.map(({ call, step }) => ({
|
|
283
630
|
call,
|
|
284
631
|
step,
|
|
285
632
|
handle: openStepHold(hold, account, safe, call),
|
|
286
633
|
}))
|
|
287
|
-
.find(({ handle, step }) =>
|
|
288
|
-
|
|
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
|
+
});
|
|
289
660
|
// Its settlement completes it as a send's would: a proposal too.
|
|
290
661
|
const reservation = found?.handle.peek().reservation ??
|
|
291
662
|
found?.handle.unacknowledgedSuccess()?.reservation;
|
|
@@ -294,6 +665,9 @@ export function useReviewedRun(deps) {
|
|
|
294
665
|
handle: found.handle,
|
|
295
666
|
client: deps.clientFor(found.call.chainId),
|
|
296
667
|
run: id,
|
|
668
|
+
account,
|
|
669
|
+
call: found.call,
|
|
670
|
+
reservation,
|
|
297
671
|
step: found.step,
|
|
298
672
|
total,
|
|
299
673
|
finish: finish ??
|
|
@@ -304,6 +678,11 @@ export function useReviewedRun(deps) {
|
|
|
304
678
|
...(released ? { released } : {}),
|
|
305
679
|
}
|
|
306
680
|
: null);
|
|
681
|
+
// Only its hold is followed now: the confirm can close.
|
|
682
|
+
if (reservation)
|
|
683
|
+
mine.current.add(reservation);
|
|
684
|
+
if (found)
|
|
685
|
+
update({ following: true });
|
|
307
686
|
return !!found;
|
|
308
687
|
}
|
|
309
688
|
catch {
|
|
@@ -311,11 +690,31 @@ export function useReviewedRun(deps) {
|
|
|
311
690
|
return false;
|
|
312
691
|
}
|
|
313
692
|
};
|
|
693
|
+
const heldStep = (target) => {
|
|
694
|
+
const call = target.held?.call ?? action.calls[target.step];
|
|
695
|
+
if (!deps || !runAccount || !call)
|
|
696
|
+
return null;
|
|
697
|
+
try {
|
|
698
|
+
const safe = runSafe();
|
|
699
|
+
if (safe === undefined)
|
|
700
|
+
return null;
|
|
701
|
+
return {
|
|
702
|
+
handle: openStepHold(deps.hold, runAccount, safe, call),
|
|
703
|
+
client: deps.clientFor(call.chainId),
|
|
704
|
+
call,
|
|
705
|
+
account: runAccount,
|
|
706
|
+
reservation: target.held?.reservation,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
314
713
|
try {
|
|
315
|
-
if (!deps
|
|
714
|
+
if (!deps || !runAccount)
|
|
316
715
|
throw new ReviewLineError("Connect a wallet first.");
|
|
317
716
|
if (action.account &&
|
|
318
|
-
action.account.toLowerCase() !==
|
|
717
|
+
action.account.toLowerCase() !== runAccount.toLowerCase())
|
|
319
718
|
throw new ReviewLineError(REVIEWED_ACCOUNT_CHANGED_LINE);
|
|
320
719
|
if (action.calls.length > 1 && action.scope === undefined)
|
|
321
720
|
throw new ReviewLineError(REVIEWED_RUN_SCOPE_LINE);
|
|
@@ -334,7 +733,16 @@ export function useReviewedRun(deps) {
|
|
|
334
733
|
}),
|
|
335
734
|
]).finally(() => signal?.removeEventListener("abort", withdraw));
|
|
336
735
|
const { resumeReviewed, sendReviewed } = loaded;
|
|
337
|
-
|
|
736
|
+
// A success already shown is acknowledged before this run checks it.
|
|
737
|
+
await acknowledged.current;
|
|
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;
|
|
338
746
|
const tracked = {
|
|
339
747
|
...deps,
|
|
340
748
|
onProgress: (progress) => {
|
|
@@ -391,30 +799,46 @@ export function useReviewedRun(deps) {
|
|
|
391
799
|
tell(action.follow?.onConfirming, step);
|
|
392
800
|
},
|
|
393
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
|
+
};
|
|
394
813
|
const result = saved
|
|
395
|
-
? await resumeReviewed(saved,
|
|
396
|
-
: await sendReviewed({ ...
|
|
814
|
+
? await resumeReviewed(saved, gated, tracked)
|
|
815
|
+
: await sendReviewed({ ...gated, account }, tracked);
|
|
397
816
|
// A wallet that reviews on its own page reports no progress: the
|
|
398
817
|
// result is the run's last word.
|
|
399
|
-
|
|
818
|
+
const calls = saved?.calls ?? action.calls;
|
|
819
|
+
const last = result.status === "done"
|
|
820
|
+
? openStepHold(deps.hold, account, false, calls[calls.length - 1])
|
|
821
|
+
: null;
|
|
822
|
+
const success = last?.unacknowledgedSuccess();
|
|
823
|
+
update({
|
|
824
|
+
phase: result.status,
|
|
825
|
+
// Shown, its success blocks no later run.
|
|
826
|
+
acknowledge: last &&
|
|
827
|
+
success &&
|
|
828
|
+
result.status === "done" &&
|
|
829
|
+
result.hashes.some((h) => h.toLowerCase() === success.hash)
|
|
830
|
+
? acknowledging(last, success)
|
|
831
|
+
: null,
|
|
832
|
+
});
|
|
400
833
|
// Every step of a done run has its hash, a resumed one's too.
|
|
401
|
-
submit(result.status === "done"
|
|
402
|
-
? (saved?.calls ?? action.calls).length - 1
|
|
403
|
-
: result.step);
|
|
834
|
+
submit(result.status === "done" ? calls.length - 1 : result.step);
|
|
404
835
|
if (result.status === "proposed")
|
|
405
836
|
void follow();
|
|
406
|
-
else if (mounted.current && latest.current === id) {
|
|
407
|
-
// Shown: its success blocks no later run.
|
|
408
|
-
const calls = saved?.calls ?? action.calls;
|
|
409
|
-
const handle = openStepHold(deps.hold, account, false, calls[calls.length - 1]);
|
|
410
|
-
const success = handle.unacknowledgedSuccess();
|
|
411
|
-
if (success &&
|
|
412
|
-
result.hashes.some((h) => h.toLowerCase() === success.hash))
|
|
413
|
-
await handle.acknowledge(success).catch(() => undefined);
|
|
414
|
-
}
|
|
415
837
|
return result;
|
|
416
838
|
}
|
|
417
839
|
catch (error) {
|
|
840
|
+
if (error instanceof OwnSucceeded)
|
|
841
|
+
return close(error.found);
|
|
418
842
|
if (error instanceof ReviewedReceiptUnverifiedError) {
|
|
419
843
|
// Still confirming: followed until its hold settles it.
|
|
420
844
|
update({
|
|
@@ -423,12 +847,15 @@ export function useReviewedRun(deps) {
|
|
|
423
847
|
hash: error.hash,
|
|
424
848
|
error: null,
|
|
425
849
|
failure: null,
|
|
850
|
+
following: true,
|
|
426
851
|
});
|
|
427
852
|
let end;
|
|
428
853
|
const cancelled = action.follow?.signal;
|
|
429
854
|
const abandon = () => end(null);
|
|
430
855
|
const outcome = new Promise((resolve) => (end = (ran) => {
|
|
431
856
|
cancelled?.removeEventListener("abort", abandon);
|
|
857
|
+
if (awaiting.current?.end === abandon)
|
|
858
|
+
awaiting.current = null;
|
|
432
859
|
resolve(ran);
|
|
433
860
|
}));
|
|
434
861
|
// Replaced, or the caller's follow ended (a cancel, unlike its
|
|
@@ -439,11 +866,20 @@ export function useReviewedRun(deps) {
|
|
|
439
866
|
else {
|
|
440
867
|
leave.current = abandon;
|
|
441
868
|
cancelled?.addEventListener("abort", abandon, { once: true });
|
|
869
|
+
awaiting.current = {
|
|
870
|
+
run: id,
|
|
871
|
+
last: error.step === action.calls.length - 1,
|
|
872
|
+
owner: {
|
|
873
|
+
account: runAccount,
|
|
874
|
+
reservation: error.held?.reservation,
|
|
875
|
+
},
|
|
876
|
+
detached: heldStep(error),
|
|
877
|
+
end: abandon,
|
|
878
|
+
};
|
|
442
879
|
}
|
|
443
880
|
const finish = completing(error, action.calls.length, end);
|
|
444
881
|
// Followed for a later settlement; one already there is read once it attaches.
|
|
445
882
|
void follow(finish, () => end(null)).then(() => finish());
|
|
446
|
-
const before = error.held?.hashes ?? [];
|
|
447
883
|
return {
|
|
448
884
|
status: "receipt",
|
|
449
885
|
step: error.step,
|
|
@@ -454,6 +890,11 @@ export function useReviewedRun(deps) {
|
|
|
454
890
|
ended: () => outcome.then(async (ran) => {
|
|
455
891
|
if (!ran)
|
|
456
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
|
+
}
|
|
457
898
|
const { settled, receipt } = ran;
|
|
458
899
|
if (settled.result !== "success") {
|
|
459
900
|
const line = settledLine(settled);
|
|
@@ -463,11 +904,13 @@ export function useReviewedRun(deps) {
|
|
|
463
904
|
failure: new ReviewLineError(line),
|
|
464
905
|
};
|
|
465
906
|
}
|
|
466
|
-
|
|
907
|
+
// The run's own, as its hold saved them with the step.
|
|
908
|
+
const { signatures } = ran;
|
|
467
909
|
if (error.step < action.calls.length - 1) {
|
|
468
910
|
const started = await runLatest.current({
|
|
469
911
|
...action,
|
|
470
912
|
...(onCancelled ? { onCancelled } : {}),
|
|
913
|
+
...(onStopped ? { onStopped } : {}),
|
|
471
914
|
});
|
|
472
915
|
const next = started?.status === "receipt"
|
|
473
916
|
? await started.ended()
|
|
@@ -480,7 +923,7 @@ export function useReviewedRun(deps) {
|
|
|
480
923
|
}
|
|
481
924
|
return {
|
|
482
925
|
status: "done",
|
|
483
|
-
hashes:
|
|
926
|
+
hashes: ran.hashes,
|
|
484
927
|
receipts: receipt ? [receipt] : [],
|
|
485
928
|
...(signatures ? { signatures } : {}),
|
|
486
929
|
};
|
|
@@ -489,9 +932,9 @@ export function useReviewedRun(deps) {
|
|
|
489
932
|
}
|
|
490
933
|
// A resume can complete a step the wallet sent before the reload.
|
|
491
934
|
try {
|
|
492
|
-
const after = action.scope === undefined || !
|
|
935
|
+
const after = action.scope === undefined || !runAccount
|
|
493
936
|
? null
|
|
494
|
-
: findReviewedSequence(action.scope,
|
|
937
|
+
: findReviewedSequence(action.scope, runAccount, {
|
|
495
938
|
storage: deps.storage,
|
|
496
939
|
});
|
|
497
940
|
if (after?.completed.length &&
|
|
@@ -503,6 +946,19 @@ export function useReviewedRun(deps) {
|
|
|
503
946
|
catch {
|
|
504
947
|
/* An unreadable sequence adds nothing to what this run saw. */
|
|
505
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" });
|
|
506
962
|
const cancelled = error instanceof TransactionReviewCancelledError ||
|
|
507
963
|
isDefiniteWalletRejection(error);
|
|
508
964
|
const line = cancelled ? null : errorLine(error);
|
|
@@ -519,9 +975,13 @@ export function useReviewedRun(deps) {
|
|
|
519
975
|
return;
|
|
520
976
|
if (found === null)
|
|
521
977
|
setTimeout(settle, RECEIPT_RETRY_MS);
|
|
522
|
-
else if (cancelled && !
|
|
523
|
-
|
|
524
|
-
|
|
978
|
+
else if (cancelled && !found) {
|
|
979
|
+
const step = submitted ?? 0;
|
|
980
|
+
update(sentAny ? { stopped: true } : { cancelled: true });
|
|
981
|
+
const told = sentAny
|
|
982
|
+
? onStopped && (() => onStopped(step))
|
|
983
|
+
: onCancelled;
|
|
984
|
+
setCancelling(told ? { told } : null);
|
|
525
985
|
}
|
|
526
986
|
});
|
|
527
987
|
settle();
|