@bananapus/nana-sdk-core 2.38.2 → 2.39.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/heldWrite.d.ts +113 -0
- package/dist/cjs/review/heldWrite.d.ts.map +1 -0
- package/dist/cjs/review/heldWrite.js +551 -0
- package/dist/cjs/review/heldWrite.js.map +1 -0
- package/dist/cjs/review/index.d.ts +2 -0
- package/dist/cjs/review/index.d.ts.map +1 -1
- package/dist/cjs/review/index.js +2 -0
- package/dist/cjs/review/index.js.map +1 -1
- package/dist/cjs/review/messages.d.ts +30 -0
- package/dist/cjs/review/messages.d.ts.map +1 -1
- package/dist/cjs/review/messages.js +34 -1
- package/dist/cjs/review/messages.js.map +1 -1
- package/dist/cjs/review/savedAttempt.d.ts +180 -0
- package/dist/cjs/review/savedAttempt.d.ts.map +1 -0
- package/dist/cjs/review/savedAttempt.js +527 -0
- package/dist/cjs/review/savedAttempt.js.map +1 -0
- package/dist/cjs/review/writeHold.d.ts +94 -2
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +284 -43
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +58 -0
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +159 -2
- package/dist/cjs/review/writeRecovery.js.map +1 -1
- package/dist/esm/review/heldWrite.d.ts +113 -0
- package/dist/esm/review/heldWrite.d.ts.map +1 -0
- package/dist/esm/review/heldWrite.js +544 -0
- package/dist/esm/review/heldWrite.js.map +1 -0
- package/dist/esm/review/index.d.ts +2 -0
- package/dist/esm/review/index.d.ts.map +1 -1
- package/dist/esm/review/index.js +2 -0
- package/dist/esm/review/index.js.map +1 -1
- package/dist/esm/review/messages.d.ts +30 -0
- package/dist/esm/review/messages.d.ts.map +1 -1
- package/dist/esm/review/messages.js +32 -0
- package/dist/esm/review/messages.js.map +1 -1
- package/dist/esm/review/savedAttempt.d.ts +180 -0
- package/dist/esm/review/savedAttempt.d.ts.map +1 -0
- package/dist/esm/review/savedAttempt.js +524 -0
- package/dist/esm/review/savedAttempt.js.map +1 -0
- package/dist/esm/review/writeHold.d.ts +94 -2
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +285 -44
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +58 -0
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +158 -2
- package/dist/esm/review/writeRecovery.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Hex, TransactionReceipt } from "viem";
|
|
2
|
+
import { type WriteHoldClient, type WriteHoldGate, type WriteHoldHandle, type WriteHoldSettled, type WriteHoldState } from "./writeHold.js";
|
|
3
|
+
/**
|
|
4
|
+
* Where one held write stands, for the one way every app shows it:
|
|
5
|
+
*
|
|
6
|
+
* - `clear`: nothing held (a failure or expiry from an earlier visit is in
|
|
7
|
+
* `settled`);
|
|
8
|
+
* - `succeeded`: a success the hold settled while the user was away: every
|
|
9
|
+
* send refuses until they acknowledge it (`acknowledge`, "Send it again"),
|
|
10
|
+
* since sending again may pay twice;
|
|
11
|
+
* - `awaiting`: a Safe proposal waiting for its signers;
|
|
12
|
+
* - `confirming`: sent, being confirmed; once a write is confirming it never
|
|
13
|
+
* goes back to awaiting;
|
|
14
|
+
* - `held`: held with the hold's own line (its exit when `exit`);
|
|
15
|
+
* - `settled`: the hold settled what this view saw held, on authenticated
|
|
16
|
+
* evidence;
|
|
17
|
+
* - `released`: the user's exit released it (`releasedBy: "user"`), or another
|
|
18
|
+
* window did (`"other-tab"`, neutral copy).
|
|
19
|
+
*/
|
|
20
|
+
export type HeldWritePhase = "clear" | "succeeded" | "awaiting" | "confirming" | "held" | "settled" | "released";
|
|
21
|
+
export type HeldWriteView = {
|
|
22
|
+
phase: HeldWritePhase;
|
|
23
|
+
/** The last hold state this view accepted. */
|
|
24
|
+
state: WriteHoldState | null;
|
|
25
|
+
/** The one line to show, or null. */
|
|
26
|
+
line: string | null;
|
|
27
|
+
/** A second line for an exit that can run the write twice. */
|
|
28
|
+
warning?: string;
|
|
29
|
+
/** While holds last only in this tab. */
|
|
30
|
+
notice?: string;
|
|
31
|
+
/** Whether the gated hold offers its exit right now. */
|
|
32
|
+
exit: boolean;
|
|
33
|
+
settled?: WriteHoldSettled;
|
|
34
|
+
releasedBy?: "user" | "other-tab";
|
|
35
|
+
/** A previous deploy's record is being moved into the hold: sends wait. */
|
|
36
|
+
adopting: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** What a previous deploy saved of a write: the evidence `hold.adopt` takes. */
|
|
39
|
+
export type HeldWriteEvidence = NonNullable<Parameters<WriteHoldHandle["adopt"]>[1]>;
|
|
40
|
+
/** How often a held write is asked again: 30 s by default, kept between 15 s and 60 s. */
|
|
41
|
+
export declare const HELD_POLL_MS = 30000;
|
|
42
|
+
/**
|
|
43
|
+
* Follow one held write to its end, whatever becomes of the flow that sent
|
|
44
|
+
* it: a direct transaction by its hold's own reads, a Safe proposal also by
|
|
45
|
+
* Safe's service, whose execution goes to the hold (`onExecuted`) and whose
|
|
46
|
+
* loss ends it unproven (`onUnproven`). It ends only when the hold is clear
|
|
47
|
+
* (an authenticated settlement or an exit) or `signal` aborts: never on its
|
|
48
|
+
* own reading of a receipt, a replacement or an expiry. The hold decides.
|
|
49
|
+
*/
|
|
50
|
+
export declare function followHeldWrite(handle: WriteHoldHandle, client: WriteHoldClient, { signal, pollMs, fastMs, }: {
|
|
51
|
+
signal: AbortSignal;
|
|
52
|
+
pollMs?: number;
|
|
53
|
+
fastMs?: number;
|
|
54
|
+
}): Promise<WriteHoldState | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Whether `next` would hide an exit `shown` offers without the chain having
|
|
57
|
+
* been checked: an emission, a replay or a contended read
|
|
58
|
+
* (`checked: false`). Only a checked answer removes it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hidesShownExit(shown: WriteHoldState | null | undefined, next: WriteHoldState): boolean;
|
|
61
|
+
/** The line for a settlement. */
|
|
62
|
+
export declare function settledLine(settled: WriteHoldSettled): string;
|
|
63
|
+
/**
|
|
64
|
+
* One held write's view, framework-free: its phase and line, the watch
|
|
65
|
+
* (the hold's own events, and a checked read on start, on focus and every
|
|
66
|
+
* poll while held), a shared follower for its hash, its exit, its send and
|
|
67
|
+
* the adoption of a previous deploy's record. Every app binds the same one
|
|
68
|
+
* (useHeldWrite in React), so the same action behaves the same everywhere.
|
|
69
|
+
*
|
|
70
|
+
* - A read the chain didn't check never hides an exit already shown.
|
|
71
|
+
* - Once confirming, a write never goes back to awaiting.
|
|
72
|
+
* - `onSettled` fires once, only for a settlement of what this view saw held.
|
|
73
|
+
* - A hold released without a settlement is `released`: by the user's exit
|
|
74
|
+
* here, or neutrally by another window.
|
|
75
|
+
*/
|
|
76
|
+
export declare function createHeldWriteView(handle: WriteHoldHandle, options: {
|
|
77
|
+
client: WriteHoldClient;
|
|
78
|
+
pollMs?: number;
|
|
79
|
+
/** Follow a held hash to its end (default true). */
|
|
80
|
+
follow?: boolean;
|
|
81
|
+
onSettled?: (settled: WriteHoldSettled, receipt: TransactionReceipt | null) => void;
|
|
82
|
+
onReleased?: (by: "user" | "other-tab") => void;
|
|
83
|
+
/** A previous deploy's saved evidence of this write, if any: adopted before any send. */
|
|
84
|
+
adopt?: () => Promise<HeldWriteEvidence | null | undefined>;
|
|
85
|
+
/** The state the hold took the adopted record into (held, or settled). */
|
|
86
|
+
onAdopted?: (state: WriteHoldState) => void;
|
|
87
|
+
}): {
|
|
88
|
+
get: () => HeldWriteView;
|
|
89
|
+
subscribe(listener: () => void): () => void;
|
|
90
|
+
check: () => Promise<HeldWriteView>;
|
|
91
|
+
/**
|
|
92
|
+
* Start watching: the hold's events, a checked read now (after adoption),
|
|
93
|
+
* on focus and every poll while held. Returns the stop.
|
|
94
|
+
*/
|
|
95
|
+
start(): () => void;
|
|
96
|
+
/**
|
|
97
|
+
* The user checked their wallet: the exit the shown state offers, asked
|
|
98
|
+
* of the chain once more. True when released: the app reopens its review.
|
|
99
|
+
*/
|
|
100
|
+
exit(): Promise<boolean>;
|
|
101
|
+
/**
|
|
102
|
+
* Send under the hold, once any adoption is done. A held write refuses
|
|
103
|
+
* with the hold's checked state shown.
|
|
104
|
+
*/
|
|
105
|
+
send(run: (gate: WriteHoldGate) => Promise<Hex>): Promise<Hex>;
|
|
106
|
+
/**
|
|
107
|
+
* The user saw a success that settled while away and chose to send again:
|
|
108
|
+
* the hold marks it acknowledged, for every reader, and sends are allowed.
|
|
109
|
+
*/
|
|
110
|
+
acknowledge(): Promise<void>;
|
|
111
|
+
};
|
|
112
|
+
export type HeldWriteViewController = ReturnType<typeof createHeldWriteView>;
|
|
113
|
+
//# sourceMappingURL=heldWrite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heldWrite.d.ts","sourceRoot":"","sources":["../../../src/review/heldWrite.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELD_POLL_MS = void 0;
|
|
4
|
+
exports.followHeldWrite = followHeldWrite;
|
|
5
|
+
exports.hidesShownExit = hidesShownExit;
|
|
6
|
+
exports.settledLine = settledLine;
|
|
7
|
+
exports.createHeldWriteView = createHeldWriteView;
|
|
8
|
+
const safeService_js_1 = require("../safeService.js");
|
|
9
|
+
const messages_js_1 = require("./messages.js");
|
|
10
|
+
const writeHold_js_1 = require("./writeHold.js");
|
|
11
|
+
/** How often a held write is asked again: 30 s by default, kept between 15 s and 60 s. */
|
|
12
|
+
exports.HELD_POLL_MS = 30_000;
|
|
13
|
+
const MIN_POLL_MS = 15_000;
|
|
14
|
+
const MAX_POLL_MS = 60_000;
|
|
15
|
+
/** A follower's first looks, a few seconds apart, before it slows to the poll. */
|
|
16
|
+
const FAST_MS = 4_000;
|
|
17
|
+
const FAST_LOOKS = 15;
|
|
18
|
+
/** A direct write's confirming line gives way to the wallet's pending line after this long. */
|
|
19
|
+
const CONFIRMING_MS = 120_000;
|
|
20
|
+
const pollOf = (ms) => Math.min(MAX_POLL_MS, Math.max(MIN_POLL_MS, ms ?? exports.HELD_POLL_MS));
|
|
21
|
+
function pause(ms, signal) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const timer = setTimeout(done, ms);
|
|
24
|
+
signal.addEventListener("abort", done, { once: true });
|
|
25
|
+
function done() {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
signal.removeEventListener("abort", done);
|
|
28
|
+
resolve();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Follow one held write to its end, whatever becomes of the flow that sent
|
|
34
|
+
* it: a direct transaction by its hold's own reads, a Safe proposal also by
|
|
35
|
+
* Safe's service, whose execution goes to the hold (`onExecuted`) and whose
|
|
36
|
+
* loss ends it unproven (`onUnproven`). It ends only when the hold is clear
|
|
37
|
+
* (an authenticated settlement or an exit) or `signal` aborts: never on its
|
|
38
|
+
* own reading of a receipt, a replacement or an expiry. The hold decides.
|
|
39
|
+
*/
|
|
40
|
+
async function followHeldWrite(handle, client, { signal, pollMs, fastMs = FAST_MS, }) {
|
|
41
|
+
const now = handle.peek();
|
|
42
|
+
if (now.status !== "clear" &&
|
|
43
|
+
now.record.safe &&
|
|
44
|
+
now.record.hash &&
|
|
45
|
+
!now.record.executed &&
|
|
46
|
+
(0, safeService_js_1.hasSafeService)(now.record.chainId)) {
|
|
47
|
+
const proposal = now.record.hash;
|
|
48
|
+
void (0, safeService_js_1.waitForSafeExecutionHash)(now.record.chainId, proposal, {
|
|
49
|
+
signal,
|
|
50
|
+
client: client,
|
|
51
|
+
})
|
|
52
|
+
.then((execution) => handle.onExecuted(client, execution), () => (signal.aborted ? null : handle.onUnproven(client, proposal)))
|
|
53
|
+
.catch(() => null);
|
|
54
|
+
}
|
|
55
|
+
for (let look = 0; !signal.aborted; look += 1) {
|
|
56
|
+
const state = await handle.state(client).catch(() => null);
|
|
57
|
+
if (state?.status === "clear")
|
|
58
|
+
return state;
|
|
59
|
+
await pause(look < FAST_LOOKS ? fastMs : pollOf(pollMs), signal);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Reservations seen confirming in this tab: a new view of one never shows it awaiting. */
|
|
64
|
+
const confirmed = new Set();
|
|
65
|
+
/** One follower per reservation in this tab, however many views show it. */
|
|
66
|
+
const followers = new Map();
|
|
67
|
+
function followShared(reservation, handle, client, pollMs) {
|
|
68
|
+
let shared = followers.get(reservation);
|
|
69
|
+
// A follower stopped by its last view, still ending, is never rejoined.
|
|
70
|
+
if (!shared || shared.stop.signal.aborted) {
|
|
71
|
+
const stop = new AbortController();
|
|
72
|
+
const ended = followHeldWrite(handle, client, {
|
|
73
|
+
signal: stop.signal,
|
|
74
|
+
pollMs,
|
|
75
|
+
}).finally(() => {
|
|
76
|
+
if (followers.get(reservation)?.stop === stop)
|
|
77
|
+
followers.delete(reservation);
|
|
78
|
+
});
|
|
79
|
+
shared = { views: 0, stop, ended };
|
|
80
|
+
followers.set(reservation, shared);
|
|
81
|
+
}
|
|
82
|
+
shared.views += 1;
|
|
83
|
+
const joined = shared;
|
|
84
|
+
return {
|
|
85
|
+
ended: joined.ended,
|
|
86
|
+
leave() {
|
|
87
|
+
joined.views -= 1;
|
|
88
|
+
if (joined.views === 0)
|
|
89
|
+
joined.stop.abort();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether `next` would hide an exit `shown` offers without the chain having
|
|
95
|
+
* been checked: an emission, a replay or a contended read
|
|
96
|
+
* (`checked: false`). Only a checked answer removes it.
|
|
97
|
+
*/
|
|
98
|
+
function hidesShownExit(shown, next) {
|
|
99
|
+
return (!!shown &&
|
|
100
|
+
shown.status !== "clear" &&
|
|
101
|
+
shown.exit &&
|
|
102
|
+
next.status !== "clear" &&
|
|
103
|
+
!next.exit &&
|
|
104
|
+
next.checked !== true);
|
|
105
|
+
}
|
|
106
|
+
/** The line for a settlement. */
|
|
107
|
+
function settledLine(settled) {
|
|
108
|
+
return settled.result === "success"
|
|
109
|
+
? messages_js_1.HELD_SUCCESS_LINE
|
|
110
|
+
: settled.result === "expired"
|
|
111
|
+
? messages_js_1.HELD_EXPIRED_LINE
|
|
112
|
+
: messages_js_1.WRITE_HOLD_REVERTED_LINE;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* One held write's view, framework-free: its phase and line, the watch
|
|
116
|
+
* (the hold's own events, and a checked read on start, on focus and every
|
|
117
|
+
* poll while held), a shared follower for its hash, its exit, its send and
|
|
118
|
+
* the adoption of a previous deploy's record. Every app binds the same one
|
|
119
|
+
* (useHeldWrite in React), so the same action behaves the same everywhere.
|
|
120
|
+
*
|
|
121
|
+
* - A read the chain didn't check never hides an exit already shown.
|
|
122
|
+
* - Once confirming, a write never goes back to awaiting.
|
|
123
|
+
* - `onSettled` fires once, only for a settlement of what this view saw held.
|
|
124
|
+
* - A hold released without a settlement is `released`: by the user's exit
|
|
125
|
+
* here, or neutrally by another window.
|
|
126
|
+
*/
|
|
127
|
+
function createHeldWriteView(handle, options) {
|
|
128
|
+
const { client } = options;
|
|
129
|
+
let view = {
|
|
130
|
+
phase: "clear",
|
|
131
|
+
state: null,
|
|
132
|
+
line: null,
|
|
133
|
+
exit: false,
|
|
134
|
+
adopting: !!options.adopt,
|
|
135
|
+
};
|
|
136
|
+
const listeners = new Set();
|
|
137
|
+
/** The reservation this view saw held, and whether its exit here released it. */
|
|
138
|
+
let observed;
|
|
139
|
+
let wasHeld = false;
|
|
140
|
+
let exitedHere;
|
|
141
|
+
const delivered = new Set();
|
|
142
|
+
/** Every reservation this view saw held and hasn't delivered: each is delivered once, whatever is shown. */
|
|
143
|
+
const seen = new Set();
|
|
144
|
+
let following;
|
|
145
|
+
let running = false;
|
|
146
|
+
/** Each start and stop begins a new lifecycle: a read or receipt from an earlier one is dropped. */
|
|
147
|
+
let epoch = 0;
|
|
148
|
+
let live = true;
|
|
149
|
+
let adoption;
|
|
150
|
+
function set(next) {
|
|
151
|
+
view = { ...view, ...next };
|
|
152
|
+
for (const listener of listeners)
|
|
153
|
+
listener();
|
|
154
|
+
}
|
|
155
|
+
function stopFollowing() {
|
|
156
|
+
following?.leave();
|
|
157
|
+
following = undefined;
|
|
158
|
+
}
|
|
159
|
+
function follow(reservation) {
|
|
160
|
+
if (options.follow === false ||
|
|
161
|
+
!running ||
|
|
162
|
+
following?.reservation === reservation)
|
|
163
|
+
return;
|
|
164
|
+
stopFollowing();
|
|
165
|
+
const shared = followShared(reservation, handle, client, options.pollMs);
|
|
166
|
+
const mine = { reservation, leave: shared.leave };
|
|
167
|
+
following = mine;
|
|
168
|
+
const at = epoch;
|
|
169
|
+
void shared.ended.then((end) => {
|
|
170
|
+
if (following === mine)
|
|
171
|
+
following = undefined;
|
|
172
|
+
if (end && at === epoch)
|
|
173
|
+
apply(end);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Settlements this view saw, still to tell the app, by reservation: kept across a restart. */
|
|
177
|
+
const undelivered = new Map();
|
|
178
|
+
/**
|
|
179
|
+
* Tell the app of a settlement it saw held, once, while live. Only once its
|
|
180
|
+
* callback ran is it done, and a success acknowledged: a lifecycle change
|
|
181
|
+
* before then leaves it to the next start.
|
|
182
|
+
*/
|
|
183
|
+
function deliver(settled, key) {
|
|
184
|
+
undelivered.set(key, settled);
|
|
185
|
+
if (!live)
|
|
186
|
+
return;
|
|
187
|
+
const at = epoch;
|
|
188
|
+
const done = (receipt) => {
|
|
189
|
+
if (at !== epoch || delivered.has(key))
|
|
190
|
+
return;
|
|
191
|
+
delivered.add(key);
|
|
192
|
+
undelivered.delete(key);
|
|
193
|
+
if ("hash" in settled)
|
|
194
|
+
told.add(settled.hash.toLowerCase());
|
|
195
|
+
options.onSettled?.(settled, receipt);
|
|
196
|
+
present();
|
|
197
|
+
// Told: the hold marks it acknowledged, so no later visit asks again.
|
|
198
|
+
if ("hash" in settled &&
|
|
199
|
+
settled.result === "success" &&
|
|
200
|
+
!settled.acknowledged)
|
|
201
|
+
void handle.acknowledge(settled).catch(() => null);
|
|
202
|
+
};
|
|
203
|
+
if (!options.onSettled || !("hash" in settled)) {
|
|
204
|
+
done(null);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
void client.getTransactionReceipt({ hash: settled.hash }).then((receipt) => done(receipt), () => done(null));
|
|
208
|
+
}
|
|
209
|
+
/** The phase and line a held state shows, monotonic once confirming. */
|
|
210
|
+
function heldView(next) {
|
|
211
|
+
// A held state's reservation is its record's id.
|
|
212
|
+
const reservation = next.record.id;
|
|
213
|
+
const record = next.record;
|
|
214
|
+
if (next.status === "ran-executed" ||
|
|
215
|
+
(record.safe &&
|
|
216
|
+
(record.executed ||
|
|
217
|
+
record.executionHash !== undefined ||
|
|
218
|
+
(record.candidates?.length ?? 0) > 0)))
|
|
219
|
+
confirmed.add(reservation);
|
|
220
|
+
const base = {
|
|
221
|
+
state: next,
|
|
222
|
+
exit: next.exit,
|
|
223
|
+
warning: "warning" in next ? next.warning : undefined,
|
|
224
|
+
notice: next.notice,
|
|
225
|
+
settled: undefined,
|
|
226
|
+
releasedBy: undefined,
|
|
227
|
+
};
|
|
228
|
+
if (next.exit)
|
|
229
|
+
return { ...base, phase: "held", line: next.line };
|
|
230
|
+
if (next.status === "ran-executed")
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
phase: "confirming",
|
|
234
|
+
line: messages_js_1.HELD_CONFIRMING_LINE,
|
|
235
|
+
};
|
|
236
|
+
if (next.status === "pending-known-hash") {
|
|
237
|
+
if (record.safe)
|
|
238
|
+
return confirmed.has(reservation)
|
|
239
|
+
? {
|
|
240
|
+
...base,
|
|
241
|
+
phase: "confirming",
|
|
242
|
+
line: messages_js_1.HELD_CONFIRMING_LINE,
|
|
243
|
+
}
|
|
244
|
+
: {
|
|
245
|
+
...base,
|
|
246
|
+
phase: "awaiting",
|
|
247
|
+
line: safeService_js_1.SAFE_PROPOSAL_AWAITING,
|
|
248
|
+
};
|
|
249
|
+
// A direct write: confirming, until the wallet has held it a while.
|
|
250
|
+
const since = Number(record.submittedAt ?? record.firstSeenAt);
|
|
251
|
+
const line = next.line === messages_js_1.WRITE_HOLD_PENDING_LINE &&
|
|
252
|
+
!(Date.now() - since >= CONFIRMING_MS)
|
|
253
|
+
? messages_js_1.HELD_CONFIRMING_LINE
|
|
254
|
+
: next.line;
|
|
255
|
+
return { ...base, phase: "confirming", line };
|
|
256
|
+
}
|
|
257
|
+
return { ...base, phase: "held", line: next.line };
|
|
258
|
+
}
|
|
259
|
+
/** Take a hold state, through the one guard every source goes through. */
|
|
260
|
+
function apply(next) {
|
|
261
|
+
if (!next || hidesShownExit(view.state, next))
|
|
262
|
+
return;
|
|
263
|
+
if (next.status === "clear") {
|
|
264
|
+
const reservation = next.reservation;
|
|
265
|
+
const mine = wasHeld && (!reservation || reservation === observed);
|
|
266
|
+
stopFollowing();
|
|
267
|
+
if (next.settled && mine) {
|
|
268
|
+
seen.delete(observed);
|
|
269
|
+
deliver(next.settled, observed);
|
|
270
|
+
set({
|
|
271
|
+
phase: "settled",
|
|
272
|
+
state: next,
|
|
273
|
+
line: settledLine(next.settled),
|
|
274
|
+
warning: undefined,
|
|
275
|
+
notice: undefined,
|
|
276
|
+
exit: false,
|
|
277
|
+
settled: next.settled,
|
|
278
|
+
releasedBy: undefined,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else if (mine) {
|
|
282
|
+
const by = exitedHere === observed ? "user" : "other-tab";
|
|
283
|
+
set({
|
|
284
|
+
phase: "released",
|
|
285
|
+
state: next,
|
|
286
|
+
line: by === "user" ? messages_js_1.HELD_RELEASED_LINE : messages_js_1.HELD_RELEASED_ELSEWHERE_LINE,
|
|
287
|
+
warning: undefined,
|
|
288
|
+
notice: undefined,
|
|
289
|
+
exit: false,
|
|
290
|
+
settled: undefined,
|
|
291
|
+
releasedBy: by,
|
|
292
|
+
});
|
|
293
|
+
options.onReleased?.(by);
|
|
294
|
+
}
|
|
295
|
+
else if (view.phase === "settled" || view.phase === "released") {
|
|
296
|
+
// Already shown: kept, unless the gate below has another success.
|
|
297
|
+
}
|
|
298
|
+
else
|
|
299
|
+
set({
|
|
300
|
+
phase: "clear",
|
|
301
|
+
state: next,
|
|
302
|
+
line: null,
|
|
303
|
+
warning: undefined,
|
|
304
|
+
notice: undefined,
|
|
305
|
+
exit: false,
|
|
306
|
+
settled: next.settled,
|
|
307
|
+
});
|
|
308
|
+
if (mine)
|
|
309
|
+
seen.delete(observed);
|
|
310
|
+
wasHeld = false;
|
|
311
|
+
observed = undefined;
|
|
312
|
+
exitedHere = undefined;
|
|
313
|
+
collect();
|
|
314
|
+
present();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const reservation = next.record.id;
|
|
318
|
+
if (observed !== reservation) {
|
|
319
|
+
observed = reservation;
|
|
320
|
+
exitedHere = undefined;
|
|
321
|
+
}
|
|
322
|
+
wasHeld = true;
|
|
323
|
+
seen.add(reservation);
|
|
324
|
+
set(heldView(next));
|
|
325
|
+
if (next.record.hash)
|
|
326
|
+
follow(reservation);
|
|
327
|
+
collect();
|
|
328
|
+
}
|
|
329
|
+
/** Deliver the settlement of every other reservation this view saw held. */
|
|
330
|
+
function collect() {
|
|
331
|
+
for (const reservation of [...seen]) {
|
|
332
|
+
if (reservation === observed)
|
|
333
|
+
continue;
|
|
334
|
+
const settled = handle.settlementOf(reservation);
|
|
335
|
+
if (!settled)
|
|
336
|
+
continue;
|
|
337
|
+
seen.delete(reservation);
|
|
338
|
+
deliver(settled, reservation);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The send gate, from current evidence alone: any unacknowledged success
|
|
343
|
+
* of the intent or an eligible legacy hold. It holds whatever is shown.
|
|
344
|
+
*/
|
|
345
|
+
function unacknowledged() {
|
|
346
|
+
if (gate())
|
|
347
|
+
throw new Error(messages_js_1.HELD_AWAY_SUCCESS_LINE);
|
|
348
|
+
}
|
|
349
|
+
/** Successes this view told the app of: their acknowledgement is under way. */
|
|
350
|
+
const told = new Set();
|
|
351
|
+
/** The gate's success, but for one this view already told the app of. */
|
|
352
|
+
function gate() {
|
|
353
|
+
return handle.unacknowledgedSuccess((hash) => told.has(hash.toLowerCase()));
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Show the gate's success as `succeeded` ("Send it again") once nothing is
|
|
357
|
+
* left to deliver, unless a hold is shown.
|
|
358
|
+
*/
|
|
359
|
+
function present() {
|
|
360
|
+
const success = gate();
|
|
361
|
+
if (!success || undelivered.size)
|
|
362
|
+
return;
|
|
363
|
+
if (view.phase === "awaiting" ||
|
|
364
|
+
view.phase === "confirming" ||
|
|
365
|
+
view.phase === "held")
|
|
366
|
+
return;
|
|
367
|
+
// A success this view saw settle was told, so the gate never names it.
|
|
368
|
+
set({
|
|
369
|
+
phase: "succeeded",
|
|
370
|
+
state: view.state,
|
|
371
|
+
line: messages_js_1.HELD_AWAY_SUCCESS_LINE,
|
|
372
|
+
warning: undefined,
|
|
373
|
+
notice: undefined,
|
|
374
|
+
exit: false,
|
|
375
|
+
settled: { result: "success", hash: success.hash },
|
|
376
|
+
releasedBy: undefined,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** The gated answer, now. */
|
|
380
|
+
async function check() {
|
|
381
|
+
const at = epoch;
|
|
382
|
+
const next = await handle.state(client).catch(() => null);
|
|
383
|
+
if (at === epoch)
|
|
384
|
+
apply(next);
|
|
385
|
+
return view;
|
|
386
|
+
}
|
|
387
|
+
/** Move a previous deploy's record into the hold, once, before any send. */
|
|
388
|
+
function adopt() {
|
|
389
|
+
const provider = options.adopt;
|
|
390
|
+
if (!provider)
|
|
391
|
+
return Promise.resolve(true);
|
|
392
|
+
adoption ??= (async () => {
|
|
393
|
+
set({ adopting: true });
|
|
394
|
+
// Each answer is this lifecycle's only: a stopped view's late read
|
|
395
|
+
// applies nothing and acknowledges nothing.
|
|
396
|
+
const at = epoch;
|
|
397
|
+
const current = () => {
|
|
398
|
+
if (at !== epoch)
|
|
399
|
+
throw new Error("Stopped while adopting.");
|
|
400
|
+
};
|
|
401
|
+
const snapshot = await handle.state(client);
|
|
402
|
+
current();
|
|
403
|
+
apply(snapshot);
|
|
404
|
+
if (snapshot.status === "clear") {
|
|
405
|
+
const evidence = await provider();
|
|
406
|
+
current();
|
|
407
|
+
if (evidence) {
|
|
408
|
+
const state = await handle.adopt(snapshot, evidence, client);
|
|
409
|
+
current();
|
|
410
|
+
// Unread (the chain, or a lock another writer held): no send until it is.
|
|
411
|
+
if (state.declined === "unreadable")
|
|
412
|
+
throw new Error("The saved write couldn't be checked.");
|
|
413
|
+
apply(state);
|
|
414
|
+
if (state.status !== "clear" || state.settled)
|
|
415
|
+
options.onAdopted?.(state);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
set({ adopting: false });
|
|
419
|
+
return true;
|
|
420
|
+
})().catch(() => {
|
|
421
|
+
// Unchecked: asked again on the next send.
|
|
422
|
+
adoption = undefined;
|
|
423
|
+
set({ adopting: false });
|
|
424
|
+
return false;
|
|
425
|
+
});
|
|
426
|
+
return adoption;
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
get: () => view,
|
|
430
|
+
subscribe(listener) {
|
|
431
|
+
listeners.add(listener);
|
|
432
|
+
return () => void listeners.delete(listener);
|
|
433
|
+
},
|
|
434
|
+
check,
|
|
435
|
+
/**
|
|
436
|
+
* Start watching: the hold's events, a checked read now (after adoption),
|
|
437
|
+
* on focus and every poll while held. Returns the stop.
|
|
438
|
+
*/
|
|
439
|
+
start() {
|
|
440
|
+
running = true;
|
|
441
|
+
live = true;
|
|
442
|
+
epoch += 1;
|
|
443
|
+
// Every settlement a stopped lifecycle never told is told now.
|
|
444
|
+
for (const [key, settled] of [...undelivered])
|
|
445
|
+
deliver(settled, key);
|
|
446
|
+
const unsubscribe = handle.subscribe(apply);
|
|
447
|
+
// An adoption a stopped lifecycle left unfinished runs again here.
|
|
448
|
+
void adopt()
|
|
449
|
+
.then((done) => (done || !running ? done : adopt()))
|
|
450
|
+
.then(() => (running ? check() : view));
|
|
451
|
+
const timer = setInterval(() => {
|
|
452
|
+
const held = view.phase === "awaiting" ||
|
|
453
|
+
view.phase === "confirming" ||
|
|
454
|
+
view.phase === "held";
|
|
455
|
+
const hidden = globalThis.document?.visibilityState === "hidden";
|
|
456
|
+
if (held && !hidden)
|
|
457
|
+
void check();
|
|
458
|
+
}, pollOf(options.pollMs));
|
|
459
|
+
const onFocus = () => void check();
|
|
460
|
+
globalThis.addEventListener?.("focus", onFocus);
|
|
461
|
+
return () => {
|
|
462
|
+
running = false;
|
|
463
|
+
live = false;
|
|
464
|
+
epoch += 1;
|
|
465
|
+
unsubscribe();
|
|
466
|
+
clearInterval(timer);
|
|
467
|
+
globalThis.removeEventListener?.("focus", onFocus);
|
|
468
|
+
stopFollowing();
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
/**
|
|
472
|
+
* The user checked their wallet: the exit the shown state offers, asked
|
|
473
|
+
* of the chain once more. True when released: the app reopens its review.
|
|
474
|
+
*/
|
|
475
|
+
async exit() {
|
|
476
|
+
const shown = view.state;
|
|
477
|
+
if (!shown || shown.status === "clear" || !view.exit)
|
|
478
|
+
return false;
|
|
479
|
+
exitedHere = shown.record.id;
|
|
480
|
+
const ended = await handle.exit(shown, { checkedWallet: true }, client);
|
|
481
|
+
if (!ended.released)
|
|
482
|
+
exitedHere = undefined;
|
|
483
|
+
apply(ended.state);
|
|
484
|
+
return ended.released;
|
|
485
|
+
},
|
|
486
|
+
/**
|
|
487
|
+
* Send under the hold, once any adoption is done. A held write refuses
|
|
488
|
+
* with the hold's checked state shown.
|
|
489
|
+
*/
|
|
490
|
+
async send(run) {
|
|
491
|
+
if (!(await adopt()))
|
|
492
|
+
throw new Error("Saved transactions are still being checked. Try again in a moment.");
|
|
493
|
+
unacknowledged();
|
|
494
|
+
// The send's own check may find one too: it says so before the wallet.
|
|
495
|
+
const unsubscribe = handle.subscribe(apply);
|
|
496
|
+
try {
|
|
497
|
+
const hash = await handle.send(client, async (gate) => {
|
|
498
|
+
unacknowledged();
|
|
499
|
+
return run({
|
|
500
|
+
...gate,
|
|
501
|
+
// Asked again at the reservation itself, after any lock retries.
|
|
502
|
+
async beforeSend(actualCall) {
|
|
503
|
+
unacknowledged();
|
|
504
|
+
await gate.beforeSend(actualCall);
|
|
505
|
+
try {
|
|
506
|
+
unacknowledged();
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
gate.onRejected();
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
unsubscribe();
|
|
516
|
+
apply(handle.peek());
|
|
517
|
+
return hash;
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
unsubscribe();
|
|
521
|
+
// Its own evidence, checked or not: a contended refusal hides no exit.
|
|
522
|
+
if (error instanceof writeHold_js_1.WriteHeldError)
|
|
523
|
+
apply(error.state);
|
|
524
|
+
else if (view.phase !== "succeeded")
|
|
525
|
+
await check();
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
/**
|
|
530
|
+
* The user saw a success that settled while away and chose to send again:
|
|
531
|
+
* the hold marks it acknowledged, for every reader, and sends are allowed.
|
|
532
|
+
*/
|
|
533
|
+
async acknowledge() {
|
|
534
|
+
const success = gate();
|
|
535
|
+
if (view.phase !== "succeeded" || !success)
|
|
536
|
+
return;
|
|
537
|
+
const at = epoch;
|
|
538
|
+
await handle.acknowledge({ result: success.result, hash: success.hash });
|
|
539
|
+
// Only that success's presentation ends: never a newer hold or its exit.
|
|
540
|
+
if (at === epoch &&
|
|
541
|
+
view.phase === "succeeded" &&
|
|
542
|
+
view.settled !== undefined &&
|
|
543
|
+
"hash" in view.settled &&
|
|
544
|
+
view.settled.hash === success.hash)
|
|
545
|
+
set({ phase: "clear", line: null, settled: undefined, state: null });
|
|
546
|
+
// Another success still unacknowledged is shown next.
|
|
547
|
+
present();
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
//# sourceMappingURL=heldWrite.js.map
|