@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.
Files changed (49) hide show
  1. package/dist/cjs/review/heldWrite.d.ts +113 -0
  2. package/dist/cjs/review/heldWrite.d.ts.map +1 -0
  3. package/dist/cjs/review/heldWrite.js +551 -0
  4. package/dist/cjs/review/heldWrite.js.map +1 -0
  5. package/dist/cjs/review/index.d.ts +2 -0
  6. package/dist/cjs/review/index.d.ts.map +1 -1
  7. package/dist/cjs/review/index.js +2 -0
  8. package/dist/cjs/review/index.js.map +1 -1
  9. package/dist/cjs/review/messages.d.ts +30 -0
  10. package/dist/cjs/review/messages.d.ts.map +1 -1
  11. package/dist/cjs/review/messages.js +34 -1
  12. package/dist/cjs/review/messages.js.map +1 -1
  13. package/dist/cjs/review/savedAttempt.d.ts +180 -0
  14. package/dist/cjs/review/savedAttempt.d.ts.map +1 -0
  15. package/dist/cjs/review/savedAttempt.js +527 -0
  16. package/dist/cjs/review/savedAttempt.js.map +1 -0
  17. package/dist/cjs/review/writeHold.d.ts +94 -2
  18. package/dist/cjs/review/writeHold.d.ts.map +1 -1
  19. package/dist/cjs/review/writeHold.js +284 -43
  20. package/dist/cjs/review/writeHold.js.map +1 -1
  21. package/dist/cjs/review/writeRecovery.d.ts +58 -0
  22. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  23. package/dist/cjs/review/writeRecovery.js +159 -2
  24. package/dist/cjs/review/writeRecovery.js.map +1 -1
  25. package/dist/esm/review/heldWrite.d.ts +113 -0
  26. package/dist/esm/review/heldWrite.d.ts.map +1 -0
  27. package/dist/esm/review/heldWrite.js +544 -0
  28. package/dist/esm/review/heldWrite.js.map +1 -0
  29. package/dist/esm/review/index.d.ts +2 -0
  30. package/dist/esm/review/index.d.ts.map +1 -1
  31. package/dist/esm/review/index.js +2 -0
  32. package/dist/esm/review/index.js.map +1 -1
  33. package/dist/esm/review/messages.d.ts +30 -0
  34. package/dist/esm/review/messages.d.ts.map +1 -1
  35. package/dist/esm/review/messages.js +32 -0
  36. package/dist/esm/review/messages.js.map +1 -1
  37. package/dist/esm/review/savedAttempt.d.ts +180 -0
  38. package/dist/esm/review/savedAttempt.d.ts.map +1 -0
  39. package/dist/esm/review/savedAttempt.js +524 -0
  40. package/dist/esm/review/savedAttempt.js.map +1 -0
  41. package/dist/esm/review/writeHold.d.ts +94 -2
  42. package/dist/esm/review/writeHold.d.ts.map +1 -1
  43. package/dist/esm/review/writeHold.js +285 -44
  44. package/dist/esm/review/writeHold.js.map +1 -1
  45. package/dist/esm/review/writeRecovery.d.ts +58 -0
  46. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  47. package/dist/esm/review/writeRecovery.js +158 -2
  48. package/dist/esm/review/writeRecovery.js.map +1 -1
  49. 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