@bananapus/nana-sdk-core 2.35.0 → 2.36.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/safeRelayr.d.ts.map +1 -1
- package/dist/cjs/review/safeRelayr.js +42 -24
- package/dist/cjs/review/safeRelayr.js.map +1 -1
- package/dist/cjs/review/tabState.d.ts +23 -0
- package/dist/cjs/review/tabState.d.ts.map +1 -0
- package/dist/cjs/review/tabState.js +30 -0
- package/dist/cjs/review/tabState.js.map +1 -0
- package/dist/cjs/review/writeHold.d.ts +31 -5
- package/dist/cjs/review/writeHold.d.ts.map +1 -1
- package/dist/cjs/review/writeHold.js +260 -42
- package/dist/cjs/review/writeHold.js.map +1 -1
- package/dist/cjs/review/writeRecovery.d.ts +42 -3
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +168 -14
- package/dist/cjs/review/writeRecovery.js.map +1 -1
- package/dist/cjs/testing.d.ts +7 -0
- package/dist/cjs/testing.d.ts.map +1 -0
- package/dist/cjs/testing.js +30 -0
- package/dist/cjs/testing.js.map +1 -0
- package/dist/esm/review/safeRelayr.d.ts.map +1 -1
- package/dist/esm/review/safeRelayr.js +42 -24
- package/dist/esm/review/safeRelayr.js.map +1 -1
- package/dist/esm/review/tabState.d.ts +23 -0
- package/dist/esm/review/tabState.d.ts.map +1 -0
- package/dist/esm/review/tabState.js +26 -0
- package/dist/esm/review/tabState.js.map +1 -0
- package/dist/esm/review/writeHold.d.ts +31 -5
- package/dist/esm/review/writeHold.d.ts.map +1 -1
- package/dist/esm/review/writeHold.js +261 -43
- package/dist/esm/review/writeHold.js.map +1 -1
- package/dist/esm/review/writeRecovery.d.ts +42 -3
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +167 -16
- package/dist/esm/review/writeRecovery.js.map +1 -1
- package/dist/esm/testing.d.ts +7 -0
- package/dist/esm/testing.d.ts.map +1 -0
- package/dist/esm/testing.js +25 -0
- package/dist/esm/testing.js.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this tab's shared write state lives on `globalThis`, by symbol, so
|
|
3
|
+
* every copy of the SDK loaded in the tab shares it, and the test-only entry
|
|
4
|
+
* (`@bananapus/nana-sdk-core/testing`) can reset it without a production
|
|
5
|
+
* export. Not part of the public surface.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TAB: {
|
|
8
|
+
/** Held writes kept in memory where storage is refused. */
|
|
9
|
+
readonly memory: symbol;
|
|
10
|
+
/** Without Web Locks, one queue per action. */
|
|
11
|
+
readonly queues: symbol;
|
|
12
|
+
/** What each tab memory last saw in durable storage. */
|
|
13
|
+
readonly seen: symbol;
|
|
14
|
+
/** The stop of every write hold controller with a follower or listener running. */
|
|
15
|
+
readonly holds: symbol;
|
|
16
|
+
/** The stop of every running Safe Relayr watch. */
|
|
17
|
+
readonly watches: symbol;
|
|
18
|
+
/** Safe Relayr reviews remembered this session. */
|
|
19
|
+
readonly reviews: symbol;
|
|
20
|
+
};
|
|
21
|
+
/** The tab's shared `T` at `key`, made by `make` the first time. */
|
|
22
|
+
export declare function tabShared<T>(key: symbol, make: () => T): T;
|
|
23
|
+
//# sourceMappingURL=tabState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabState.d.ts","sourceRoot":"","sources":["../../../src/review/tabState.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,GAAG;IACd,2DAA2D;;IAE3D,+CAA+C;;IAE/C,wDAAwD;;IAExD,mFAAmF;;IAEnF,mDAAmD;;IAEnD,mDAAmD;;CAE3C,CAAC;AAEX,oEAAoE;AACpE,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAG1D"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this tab's shared write state lives on `globalThis`, by symbol, so
|
|
3
|
+
* every copy of the SDK loaded in the tab shares it, and the test-only entry
|
|
4
|
+
* (`@bananapus/nana-sdk-core/testing`) can reset it without a production
|
|
5
|
+
* export. Not part of the public surface.
|
|
6
|
+
*/
|
|
7
|
+
export const TAB = {
|
|
8
|
+
/** Held writes kept in memory where storage is refused. */
|
|
9
|
+
memory: Symbol.for("nana-sdk:held-writes"),
|
|
10
|
+
/** Without Web Locks, one queue per action. */
|
|
11
|
+
queues: Symbol.for("nana-sdk:write-lock-queues"),
|
|
12
|
+
/** What each tab memory last saw in durable storage. */
|
|
13
|
+
seen: Symbol.for("nana-sdk:held-writes-seen"),
|
|
14
|
+
/** The stop of every write hold controller with a follower or listener running. */
|
|
15
|
+
holds: Symbol.for("nana-sdk:write-hold-stops"),
|
|
16
|
+
/** The stop of every running Safe Relayr watch. */
|
|
17
|
+
watches: Symbol.for("nana-sdk:safe-relayr-watches"),
|
|
18
|
+
/** Safe Relayr reviews remembered this session. */
|
|
19
|
+
reviews: Symbol.for("nana-sdk:safe-relayr-reviews"),
|
|
20
|
+
};
|
|
21
|
+
/** The tab's shared `T` at `key`, made by `make` the first time. */
|
|
22
|
+
export function tabShared(key, make) {
|
|
23
|
+
const tab = globalThis;
|
|
24
|
+
return (tab[key] ??= make());
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=tabState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabState.js","sourceRoot":"","sources":["../../../src/review/tabState.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAC1C,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC;IAChD,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC;IAC7C,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC;IAC9C,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC;IACnD,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC;CAC3C,CAAC;AAEX,oEAAoE;AACpE,MAAM,UAAU,SAAS,CAAI,GAAW,EAAE,IAAa;IACrD,MAAM,GAAG,GAAG,UAAqC,CAAC;IAClD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAM,CAAC;AACpC,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Hex, type PublicClient, type TransactionReceipt } from "viem";
|
|
1
|
+
import { type Address, type Hex, type PublicClient, type TransactionReceipt } from "viem";
|
|
2
2
|
import { type SafeQueuedTransaction, type SafeServiceOptions } from "../safeService.js";
|
|
3
3
|
import { type HeldWriteStorage, type ReviewedWriteRecoveryInput, type ReviewedWriteRecoveryRecord } from "./writeRecovery.js";
|
|
4
4
|
/** The reads a write hold makes on the call's chain; a viem PublicClient satisfies it. */
|
|
@@ -41,9 +41,9 @@ type Held<S extends string, Extra> = {
|
|
|
41
41
|
* actually sent at (`sentNonce`, from its authenticated transaction; the
|
|
42
42
|
* nonce read before the wallet opened only locates the call). The exit is
|
|
43
43
|
* offered;
|
|
44
|
-
* - `held-unchecked`: a saved hash the node doesn't know
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* - `held-unchecked`: a saved hash the node doesn't know 30 minutes after
|
|
45
|
+
* the wallet returned it (or, for a record saved before that time was
|
|
46
|
+
* kept or adopted without it, after it was first read). The
|
|
47
47
|
* exit is offered, with a line that says so; a hash that shows up later is
|
|
48
48
|
* followed as usual. A hash seen with its sent nonce that the node has not
|
|
49
49
|
* known for 30 minutes, with that nonce unused at a canonical finalized
|
|
@@ -72,6 +72,13 @@ export type WriteHoldState = WriteHoldStateOf & {
|
|
|
72
72
|
* so an observer can ignore a newer reservation it didn't start.
|
|
73
73
|
*/
|
|
74
74
|
reservation?: string;
|
|
75
|
+
/**
|
|
76
|
+
* On a result of state() or exit(): true when the gate read the chain
|
|
77
|
+
* under the call's lock; false when the lock was busy and this is only the
|
|
78
|
+
* saved record (its exit hidden). Never let a false one hide an exit
|
|
79
|
+
* already shown.
|
|
80
|
+
*/
|
|
81
|
+
checked?: boolean;
|
|
75
82
|
};
|
|
76
83
|
type WriteHoldStateOf = {
|
|
77
84
|
status: "clear";
|
|
@@ -185,6 +192,11 @@ export declare function createWriteHold(options?: {
|
|
|
185
192
|
open: (call: ReviewedWriteRecoveryInput, { legacy, }?: {
|
|
186
193
|
legacy?: (string | {
|
|
187
194
|
call: ReviewedWriteRecoveryInput["call"];
|
|
195
|
+
} | {
|
|
196
|
+
match: {
|
|
197
|
+
to: Address;
|
|
198
|
+
selector: Hex;
|
|
199
|
+
};
|
|
188
200
|
})[];
|
|
189
201
|
}) => {
|
|
190
202
|
peek: () => WriteHoldState;
|
|
@@ -205,6 +217,20 @@ export declare function createWriteHold(options?: {
|
|
|
205
217
|
onUnproven: (client: WriteHoldClient, proposalHash?: `0x${string}` | undefined) => Promise<WriteHoldState>;
|
|
206
218
|
onExecuted: (client: WriteHoldClient, executionHash?: `0x${string}` | undefined) => Promise<WriteHoldState>;
|
|
207
219
|
attachHash: (client: WriteHoldClient, hash: `0x${string}`) => Promise<WriteHoldState>;
|
|
220
|
+
attachSafeNonce: (client: WriteHoldClient, args_1: {
|
|
221
|
+
safeTxHash: Hex;
|
|
222
|
+
nonce: number;
|
|
223
|
+
}) => Promise<WriteHoldState>;
|
|
224
|
+
adopt: (snapshot: WriteHoldState, evidence?: {
|
|
225
|
+
nonce?: number | bigint;
|
|
226
|
+
sentNonce?: number | bigint;
|
|
227
|
+
fromBlock?: bigint;
|
|
228
|
+
submittedAt?: number;
|
|
229
|
+
hash?: Hex;
|
|
230
|
+
safe?: {
|
|
231
|
+
safeTxHash: Hex;
|
|
232
|
+
};
|
|
233
|
+
} | undefined, client?: WriteHoldClient | undefined) => Promise<WriteHoldState>;
|
|
208
234
|
/**
|
|
209
235
|
* Every state this controller reads or makes for the call, and other
|
|
210
236
|
* tabs' changes. The latest one, if any, is replayed at once.
|
|
@@ -216,7 +242,7 @@ export declare function createWriteHold(options?: {
|
|
|
216
242
|
/** The review's line while holds last only in this tab, or null. */
|
|
217
243
|
reviewLine: () => "This browser can't remember pending transactions. Check your wallet before repeating one." | null;
|
|
218
244
|
/** Stop every settle poll and the tab listener (tests, teardown). */
|
|
219
|
-
dispose()
|
|
245
|
+
dispose: () => void;
|
|
220
246
|
};
|
|
221
247
|
export type WriteHold = ReturnType<typeof createWriteHold>;
|
|
222
248
|
export type WriteHoldHandle = ReturnType<WriteHold["open"]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeHold.d.ts","sourceRoot":"","sources":["../../../src/review/writeHold.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,GAAG,EACR,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACxB,MAAM,MAAM,CAAC;AACd,OAAO,EAOL,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAgB3B,OAAO,
|
|
1
|
+
{"version":3,"file":"writeHold.d.ts","sourceRoot":"","sources":["../../../src/review/writeHold.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { isAddress, toEventSelector, } from "viem";
|
|
2
2
|
import { canonicalSafeTxHash, lookAtSafeProposalEnd, provenSafeProposalEnd, readSafeTransaction, SAFE_PROPOSAL_AWAITING, safeTransactionMessage, } from "../safeService.js";
|
|
3
3
|
import { SAFE_PROPOSAL_EXECUTED_LINE, WRITE_HOLD_HASHED_EXIT_LINE, WRITE_HOLD_IN_TAB_LINE, WRITE_HOLD_LOOKS_DONE_LINE, WRITE_HOLD_MINED_LINE, WRITE_HOLD_NONCE_LINE, WRITE_HOLD_PENDING_LINE, WRITE_HOLD_UNCHECKED_LINE, WRITE_HOLD_REVERTED_LINE, WRITE_HOLD_SAFE_REPLACED_LINE, WRITE_HOLD_SAFE_UNPROVEN_LINE, WRITE_HOLD_STALE_REVIEW_LINE, writeHeldMessage, } from "./messages.js";
|
|
4
|
-
import { createBrowserWriteRecovery, createHeldWriteStorage, findTransactionAtNonce, readReviewedWriteNonce, SubmittedWritePersistenceError, verifyReviewedWriteExpiry, verifyReviewedWriteReceipt, } from "./writeRecovery.js";
|
|
4
|
+
import { createBrowserWriteRecovery, createHeldWriteStorage, findReviewedWrites, heldSafeProposal, findTransactionAtNonce, readReviewedWriteNonce, safeProposalIsHeldCall, SubmittedWritePersistenceError, verifyReviewedWriteExpiry, verifyReviewedWriteReceipt, } from "./writeRecovery.js";
|
|
5
5
|
import { isBytes32 } from "../untrusted.js";
|
|
6
6
|
import { atCanonicalFinalizedBlock } from "./relayr.js";
|
|
7
|
+
import { TAB, tabShared } from "./tabState.js";
|
|
8
|
+
/** `sent` is the transaction at `hash`, exactly `record`'s call, from its account, on its chain. */
|
|
9
|
+
function sentIsCall(sent, hash, record) {
|
|
10
|
+
return (sent.hash?.toLowerCase() === hash.toLowerCase() &&
|
|
11
|
+
sent.from?.toLowerCase() === record.account &&
|
|
12
|
+
(sent.chainId === undefined || sent.chainId === record.chainId) &&
|
|
13
|
+
sent.to?.toLowerCase() === record.call.to &&
|
|
14
|
+
sent.input?.toLowerCase() === record.call.data &&
|
|
15
|
+
BigInt(sent.value) === BigInt(record.call.value));
|
|
16
|
+
}
|
|
17
|
+
/** The stop of every controller with a follower or tab listener running, for the test-only reset. */
|
|
18
|
+
const liveHolds = tabShared(TAB.holds, () => new Set());
|
|
7
19
|
/**
|
|
8
20
|
* A state read without the chain (peek, a replay, another tab's change, a
|
|
9
21
|
* send's progress): its status and line, never the exit. Only the gate in
|
|
@@ -59,6 +71,8 @@ const SAFE_EXECUTION_TOPICS = [
|
|
|
59
71
|
];
|
|
60
72
|
/** A hash the network still hasn't seen this long after the wallet returned it was almost certainly never broadcast. */
|
|
61
73
|
const UNSEEN_HASH_MS = 30 * 60_000;
|
|
74
|
+
/** The longest one request to Safe's service may take inside the hold. */
|
|
75
|
+
const SAFE_SERVICE_READ_MS = 8_000;
|
|
62
76
|
/** The 30 minutes have passed since the wallet returned the hash, or since it was first read. A clock that couldn't be saved (NaN) never runs out. */
|
|
63
77
|
function unseenTooLong(record) {
|
|
64
78
|
return (Date.now() - Number(record.submittedAt ?? record.firstSeenAt) >=
|
|
@@ -175,6 +189,30 @@ function classify(record) {
|
|
|
175
189
|
*/
|
|
176
190
|
export function createWriteHold(options = {}) {
|
|
177
191
|
const storage = options.storage ?? createHeldWriteStorage();
|
|
192
|
+
/**
|
|
193
|
+
* Safe's service, every request and its body bounded together to
|
|
194
|
+
* {@link SAFE_SERVICE_READ_MS} whatever its fetch does with the signal: a read that times out is
|
|
195
|
+
* unknown, which never releases or settles, so a hung service can't hold
|
|
196
|
+
* a send or the call's lock.
|
|
197
|
+
*/
|
|
198
|
+
const service = {
|
|
199
|
+
...options.safeService,
|
|
200
|
+
fetch: (url, init) => {
|
|
201
|
+
const request = options.safeService?.fetch ?? globalThis.fetch;
|
|
202
|
+
let timer;
|
|
203
|
+
return Promise.race([
|
|
204
|
+
// The whole read, its body included, under the one deadline.
|
|
205
|
+
request(url, init).then(async (response) => new Response(await response.arrayBuffer(), {
|
|
206
|
+
status: response.status,
|
|
207
|
+
statusText: response.statusText,
|
|
208
|
+
headers: response.headers,
|
|
209
|
+
})),
|
|
210
|
+
new Promise((_, reject) => {
|
|
211
|
+
timer = setTimeout(() => reject(new Error("Safe's service did not answer in time.")), SAFE_SERVICE_READ_MS);
|
|
212
|
+
}),
|
|
213
|
+
]).finally(() => clearTimeout(timer));
|
|
214
|
+
},
|
|
215
|
+
};
|
|
178
216
|
const interval = options.settle?.intervalMs ?? SETTLE_POLL_MS;
|
|
179
217
|
const attempts = options.settle?.attempts ?? SETTLE_POLL_ATTEMPTS;
|
|
180
218
|
const listeners = new Map();
|
|
@@ -218,11 +256,25 @@ export function createWriteHold(options = {}) {
|
|
|
218
256
|
}
|
|
219
257
|
};
|
|
220
258
|
globalThis.addEventListener("storage", onStorage);
|
|
259
|
+
liveHolds.add(dispose);
|
|
260
|
+
}
|
|
261
|
+
/** Stop every settle poll and the tab listener. */
|
|
262
|
+
function dispose() {
|
|
263
|
+
for (const timer of timers.values())
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
timers.clear();
|
|
266
|
+
if (onStorage)
|
|
267
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
268
|
+
onStorage = undefined;
|
|
269
|
+
liveHolds.delete(dispose);
|
|
221
270
|
}
|
|
222
|
-
function openOne(call, hooks = {}
|
|
271
|
+
function openOne(call, hooks = {},
|
|
272
|
+
/** The key a found write was saved under, when it isn't its call's own. */
|
|
273
|
+
at) {
|
|
223
274
|
const recovery = createBrowserWriteRecovery(call, {
|
|
224
275
|
storage,
|
|
225
276
|
locks: options.locks,
|
|
277
|
+
at,
|
|
226
278
|
});
|
|
227
279
|
// The canonical recovery scope: a namespace and an exact call never share it.
|
|
228
280
|
const key = recovery.scopeKey;
|
|
@@ -279,6 +331,7 @@ export function createWriteHold(options = {}) {
|
|
|
279
331
|
function follow(client, id, left = attempts) {
|
|
280
332
|
if (timers.has(id) || left <= 0)
|
|
281
333
|
return;
|
|
334
|
+
liveHolds.add(dispose);
|
|
282
335
|
timers.set(id, setTimeout(() => {
|
|
283
336
|
void state(client)
|
|
284
337
|
.catch(() => null)
|
|
@@ -320,10 +373,37 @@ export function createWriteHold(options = {}) {
|
|
|
320
373
|
follow(client, record.id);
|
|
321
374
|
return classify(recovery.read());
|
|
322
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Whether a previous deploy's `hash` may be this call's: a valid hash
|
|
378
|
+
* read on the call's chain that isn't shown to be another write. A
|
|
379
|
+
* transaction must be the exact call from the account; a Safe proposal,
|
|
380
|
+
* exactly this call. One that can't be read is unknown, not refused.
|
|
381
|
+
*/
|
|
382
|
+
async function adoptable(hash, client) {
|
|
383
|
+
if (!isBytes32(hash) ||
|
|
384
|
+
!client ||
|
|
385
|
+
(await client.getChainId().catch(() => null)) !== call.chainId)
|
|
386
|
+
return false;
|
|
387
|
+
const reviewed = {
|
|
388
|
+
account: call.account.toLowerCase(),
|
|
389
|
+
chainId: call.chainId,
|
|
390
|
+
call: {
|
|
391
|
+
to: call.call.to.toLowerCase(),
|
|
392
|
+
data: call.call.data.toLowerCase(),
|
|
393
|
+
value: String(call.call.value ?? 0),
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
if (call.safe) {
|
|
397
|
+
const proposal = await readSafeTransaction(call.chainId, call.account, hash, service).catch(() => null);
|
|
398
|
+
return !proposal || safeProposalIsHeldCall(proposal, call);
|
|
399
|
+
}
|
|
400
|
+
const sent = await client.getTransaction({ hash }).catch(() => null);
|
|
401
|
+
return !sent || sentIsCall(sent, hash, reviewed);
|
|
402
|
+
}
|
|
323
403
|
/** The authenticated nonce of a Safe proposal, from Safe's service. */
|
|
324
404
|
async function proposalNonceOf(record) {
|
|
325
405
|
try {
|
|
326
|
-
const proposal = await readSafeTransaction(record.chainId, record.account, record.hash,
|
|
406
|
+
const proposal = await readSafeTransaction(record.chainId, record.account, record.hash, service);
|
|
327
407
|
return safeTransactionMessage(proposal).nonce;
|
|
328
408
|
}
|
|
329
409
|
catch {
|
|
@@ -528,13 +608,13 @@ export function createWriteHold(options = {}) {
|
|
|
528
608
|
}
|
|
529
609
|
const end = (record.safeNonce !== undefined &&
|
|
530
610
|
(await provenSafeProposalEnd(client, record.account, hash, BigInt(record.safeNonce)))) ||
|
|
531
|
-
(await lookAtSafeProposalEnd(client, record.chainId, record.account, hash,
|
|
611
|
+
(await lookAtSafeProposalEnd(client, record.chainId, record.account, hash, service));
|
|
532
612
|
// Another Safe transaction took its nonce, even after a saved execution
|
|
533
613
|
// was reorganized away. It may have run the call too: the exit, never a release.
|
|
534
614
|
if (end.look === "replaced")
|
|
535
615
|
return safeReplaced(recovery.replacedOnChain(record));
|
|
536
616
|
if (end.look === "expired" && !record.executed) {
|
|
537
|
-
const expired = await verifyReviewedWriteExpiry(client, record,
|
|
617
|
+
const expired = await verifyReviewedWriteExpiry(client, record, service).catch(() => false);
|
|
538
618
|
if (expired && release(record))
|
|
539
619
|
return clear({ result: "expired" }, record.id);
|
|
540
620
|
}
|
|
@@ -665,9 +745,7 @@ export function createWriteHold(options = {}) {
|
|
|
665
745
|
}
|
|
666
746
|
if (record.hash &&
|
|
667
747
|
record.sentNonce === undefined &&
|
|
668
|
-
(record
|
|
669
|
-
record.fromBlock === undefined ||
|
|
670
|
-
unseenTooLong(record)) &&
|
|
748
|
+
unseenTooLong(record) &&
|
|
671
749
|
(await client.getTransaction({ hash: record.hash }).then(() => false, (error) => error?.name === "TransactionNotFoundError")))
|
|
672
750
|
return unchecked(record);
|
|
673
751
|
return classify(record);
|
|
@@ -679,7 +757,7 @@ export function createWriteHold(options = {}) {
|
|
|
679
757
|
async function state(client, checks = {}) {
|
|
680
758
|
lastEffect = checks.effect;
|
|
681
759
|
const result = await locked(() => inspect(client, checks.effect), false);
|
|
682
|
-
return shown(result ? result.value : peek());
|
|
760
|
+
return { ...shown(result ? result.value : peek()), checked: !!result };
|
|
683
761
|
}
|
|
684
762
|
return {
|
|
685
763
|
state,
|
|
@@ -865,7 +943,10 @@ export function createWriteHold(options = {}) {
|
|
|
865
943
|
// what the saved record says, with no exit.
|
|
866
944
|
}, false);
|
|
867
945
|
const ended = result?.value ?? { released: false, state: peek() };
|
|
868
|
-
return {
|
|
946
|
+
return {
|
|
947
|
+
...ended,
|
|
948
|
+
state: { ...shown(ended.state), checked: !!result },
|
|
949
|
+
};
|
|
869
950
|
},
|
|
870
951
|
/**
|
|
871
952
|
* A receipt for the held write: an ordinary write's own hash, or a Safe
|
|
@@ -909,6 +990,94 @@ export function createWriteHold(options = {}) {
|
|
|
909
990
|
* one lock, without releasing it. The hold then follows that hash: no
|
|
910
991
|
* exit while it is pending, and its receipt settles it.
|
|
911
992
|
*/
|
|
993
|
+
/**
|
|
994
|
+
* Move a write a previous deploy kept (outside these holds) into this
|
|
995
|
+
* hold. `snapshot` is the state the app read before deciding. Under
|
|
996
|
+
* the call's lock, it reserves a held record with `evidence` (the
|
|
997
|
+
* account's next nonce, or the nonce it was sent at, with the block it
|
|
998
|
+
* was read at; when it was sent; and its `hash`, or for a Safe write
|
|
999
|
+
* `safe.safeTxHash`) only while nothing is held and no reservation
|
|
1000
|
+
* came since `snapshot`: another tab's hold or settlement wins, and
|
|
1001
|
+
* the current state is returned unchanged. A hash is checked first
|
|
1002
|
+
* with `client`: a transaction that is another call, from another
|
|
1003
|
+
* account or on another chain (or a Safe proposal of another call) is
|
|
1004
|
+
* refused, adopting nothing. One the chain can't show is held, with
|
|
1005
|
+
* the warned exit 30 minutes after it was sent (or adopted). Then the
|
|
1006
|
+
* gate reads it: a mined one settles on its verified receipt. It never
|
|
1007
|
+
* sends or releases; adopting twice holds once.
|
|
1008
|
+
*/
|
|
1009
|
+
async adopt(snapshot, evidence = {}, client) {
|
|
1010
|
+
const nonce = evidence.nonce ?? evidence.sentNonce;
|
|
1011
|
+
const hash = (call.safe ? evidence.safe?.safeTxHash : evidence.hash)?.toLowerCase();
|
|
1012
|
+
/** Nothing held, and no reservation since the app's read. */
|
|
1013
|
+
const free = () => snapshot.status === "clear" &&
|
|
1014
|
+
!recovery.read() &&
|
|
1015
|
+
recovery.latestReservation() === snapshot.reservation;
|
|
1016
|
+
const reserve = () => recovery.reserve(undefined, nonce !== undefined && evidence.fromBlock !== undefined
|
|
1017
|
+
? { nonce, blockNumber: evidence.fromBlock }
|
|
1018
|
+
: undefined, {
|
|
1019
|
+
hash,
|
|
1020
|
+
submittedAt: evidence.submittedAt === undefined
|
|
1021
|
+
? undefined
|
|
1022
|
+
: String(evidence.submittedAt),
|
|
1023
|
+
});
|
|
1024
|
+
if (hooks.across) {
|
|
1025
|
+
// Under an intent: the same atomic reserve as a send, under every
|
|
1026
|
+
// legacy key's lock, mirrored at each so older versions see it.
|
|
1027
|
+
if (!free() ||
|
|
1028
|
+
(hash !== undefined && !(await adoptable(hash, client))))
|
|
1029
|
+
return shown(peek());
|
|
1030
|
+
let adopted = false;
|
|
1031
|
+
try {
|
|
1032
|
+
const { released } = await hooks.across((work) => recovery.withLock(work), () => {
|
|
1033
|
+
if (!free())
|
|
1034
|
+
throw new Error("Held since the app's read.");
|
|
1035
|
+
reserve();
|
|
1036
|
+
adopted = true;
|
|
1037
|
+
}, recovery.rejected, Promise.resolve(), hooks.generations());
|
|
1038
|
+
await released;
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
/* Held elsewhere, busy, or no longer free: nothing adopted. */
|
|
1042
|
+
}
|
|
1043
|
+
// A mined hash settles on its verified receipt, through the gate.
|
|
1044
|
+
return adopted && hash && client ? state(client) : shown(peek());
|
|
1045
|
+
}
|
|
1046
|
+
const result = await locked(async () => {
|
|
1047
|
+
if (!free())
|
|
1048
|
+
return peek();
|
|
1049
|
+
if (hash !== undefined && !(await adoptable(hash, client)))
|
|
1050
|
+
return peek();
|
|
1051
|
+
reserve();
|
|
1052
|
+
return hash && client ? inspect(client) : peek();
|
|
1053
|
+
}, true);
|
|
1054
|
+
return shown(result?.value ?? peek());
|
|
1055
|
+
},
|
|
1056
|
+
/**
|
|
1057
|
+
* Save the Safe nonce of a held Safe proposal adopted before it was
|
|
1058
|
+
* known, so a replacement can be proven later. Under the call's lock,
|
|
1059
|
+
* only while the held proposal is `safeTxHash` with no nonce yet, and
|
|
1060
|
+
* only when the call's proposal at `nonce` hashes to it: the hash is
|
|
1061
|
+
* the proof. Then the gate reads it. Never sends or releases.
|
|
1062
|
+
*/
|
|
1063
|
+
async attachSafeNonce(client, { safeTxHash, nonce }) {
|
|
1064
|
+
const result = await locked(async () => {
|
|
1065
|
+
const record = recovery.read();
|
|
1066
|
+
if (record?.safe &&
|
|
1067
|
+
!record.mirrorOf &&
|
|
1068
|
+
record.safeNonce === undefined &&
|
|
1069
|
+
record.hash === safeTxHash.toLowerCase())
|
|
1070
|
+
try {
|
|
1071
|
+
canonicalSafeTxHash(call.chainId, call.account, heldSafeProposal(call, nonce), safeTxHash);
|
|
1072
|
+
recovery.proposalNonce(record, BigInt(nonce));
|
|
1073
|
+
}
|
|
1074
|
+
catch {
|
|
1075
|
+
/* Not this proposal at that nonce: nothing saved. */
|
|
1076
|
+
}
|
|
1077
|
+
return inspect(client);
|
|
1078
|
+
}, true);
|
|
1079
|
+
return shown(result?.value ?? peek());
|
|
1080
|
+
},
|
|
912
1081
|
async attachHash(client, hash) {
|
|
913
1082
|
const result = await locked(async () => {
|
|
914
1083
|
const record = recovery.read();
|
|
@@ -921,12 +1090,8 @@ export function createWriteHold(options = {}) {
|
|
|
921
1090
|
const sent = await client
|
|
922
1091
|
.getTransaction({ hash })
|
|
923
1092
|
.catch(() => null);
|
|
924
|
-
if (sent
|
|
925
|
-
sent
|
|
926
|
-
(sent.chainId === undefined || sent.chainId === record.chainId) &&
|
|
927
|
-
sent.to?.toLowerCase() === record.call.to &&
|
|
928
|
-
sent.input?.toLowerCase() === record.call.data &&
|
|
929
|
-
BigInt(sent.value) === BigInt(record.call.value) &&
|
|
1093
|
+
if (sent &&
|
|
1094
|
+
sentIsCall(sent, hash, record) &&
|
|
930
1095
|
// This attempt, not an earlier identical call: never below the
|
|
931
1096
|
// account's next nonce or mined before it was reserved.
|
|
932
1097
|
// With no such evidence, an identical earlier call can't be
|
|
@@ -982,7 +1147,13 @@ export function createWriteHold(options = {}) {
|
|
|
982
1147
|
/**
|
|
983
1148
|
* `legacy`: for a write held under an intent namespace, the keys an earlier
|
|
984
1149
|
* version held it under (an old namespace, or `{ call }` for an exact
|
|
985
|
-
* call); the current exact call is always checked.
|
|
1150
|
+
* call); the current exact call is always checked. `{ match: { to,
|
|
1151
|
+
* selector } }` finds them instead: every exact-call write this account
|
|
1152
|
+
* saved on this chain to `to` with that 4-byte selector, for a hold saved
|
|
1153
|
+
* before the intent key existed under calldata the retry can't rebuild (an
|
|
1154
|
+
* old minimum, expiration or deadline). Pass one only for a write whose
|
|
1155
|
+
* calldata is volatile: it holds every such call. It is found when the
|
|
1156
|
+
* handle opens, and again right before a send reserves. While any of them holds
|
|
986
1157
|
* an unresolved write, the intent handle *is* that hold: its state, its
|
|
987
1158
|
* refusal and its own exit, in place. Each is checked alone, under its own
|
|
988
1159
|
* lock only, never two at once. Only when all are clear does the intent's
|
|
@@ -992,6 +1163,9 @@ export function createWriteHold(options = {}) {
|
|
|
992
1163
|
function open(call, { legacy = [], } = {}) {
|
|
993
1164
|
if (call.namespace === undefined)
|
|
994
1165
|
return openExact(call);
|
|
1166
|
+
const matches = legacy.flatMap((source) => typeof source === "object" && "match" in source ? [source.match] : []);
|
|
1167
|
+
/** Every exact-call write the matches find in storage now. */
|
|
1168
|
+
const matched = () => matches.flatMap((match) => findReviewedWrites({ chainId: call.chainId, account: call.account, ...match }, { storage }));
|
|
995
1169
|
const namespace = call.namespace;
|
|
996
1170
|
const own = openOne(call, {
|
|
997
1171
|
across: reserveAcross,
|
|
@@ -1003,19 +1177,54 @@ export function createWriteHold(options = {}) {
|
|
|
1003
1177
|
const { namespace: _, ...exact } = call;
|
|
1004
1178
|
const scopeOf = (input) => createBrowserWriteRecovery(input, { storage }).scopeKey;
|
|
1005
1179
|
const scopes = new Set([scopeOf(call)]);
|
|
1006
|
-
const inputs = [{ call: call.call }, ...legacy]
|
|
1007
|
-
.map((source) => typeof source === "string"
|
|
1008
|
-
? { ...call, namespace: source }
|
|
1009
|
-
: { ...exact, call: source.call })
|
|
1010
|
-
.filter((input) => {
|
|
1011
|
-
const scope = scopeOf(input);
|
|
1012
|
-
return !scopes.has(scope) && !!scopes.add(scope);
|
|
1013
|
-
})
|
|
1014
|
-
// One canonical order, by scope, for every writer's locks.
|
|
1015
|
-
.sort((a, b) => Number(scopeOf(a) > scopeOf(b)) - Number(scopeOf(a) < scopeOf(b)));
|
|
1016
1180
|
// Mirror-aware: evidence and exits always act on the source attempt.
|
|
1017
|
-
const delegates =
|
|
1018
|
-
const journals =
|
|
1181
|
+
const delegates = [];
|
|
1182
|
+
const journals = [];
|
|
1183
|
+
/** Hold `inputs` too, each once, in one canonical order by scope for every writer's locks. */
|
|
1184
|
+
function include(inputs) {
|
|
1185
|
+
const added = inputs.filter(({ input, at }) => {
|
|
1186
|
+
const scope = at ?? scopeOf(input);
|
|
1187
|
+
return !scopes.has(scope) && !!scopes.add(scope);
|
|
1188
|
+
});
|
|
1189
|
+
if (!added.length)
|
|
1190
|
+
return;
|
|
1191
|
+
const keyed = [
|
|
1192
|
+
...delegates.map((delegate, index) => ({
|
|
1193
|
+
delegate,
|
|
1194
|
+
journal: journals[index],
|
|
1195
|
+
})),
|
|
1196
|
+
...added.map(({ input, at }) => ({
|
|
1197
|
+
delegate: openExact(input, at),
|
|
1198
|
+
journal: createBrowserWriteRecovery(input, {
|
|
1199
|
+
storage,
|
|
1200
|
+
locks: options.locks,
|
|
1201
|
+
at,
|
|
1202
|
+
}),
|
|
1203
|
+
})),
|
|
1204
|
+
].sort((a, b) => Number(a.journal.scopeKey > b.journal.scopeKey) -
|
|
1205
|
+
Number(a.journal.scopeKey < b.journal.scopeKey));
|
|
1206
|
+
delegates.splice(0, delegates.length, ...keyed.map((k) => k.delegate));
|
|
1207
|
+
journals.splice(0, journals.length, ...keyed.map((k) => k.journal));
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Look for matching writes again (on every state(), send() and exit());
|
|
1211
|
+
* those found since are held too, each at the exact key it was found
|
|
1212
|
+
* under, with its own Safe flag. Returns the ones that are new.
|
|
1213
|
+
*/
|
|
1214
|
+
function rescan() {
|
|
1215
|
+
const found = matched().filter(({ key }) => !scopes.has(key));
|
|
1216
|
+
include(found.map(({ key, record }) => ({
|
|
1217
|
+
input: { ...exact, safe: record.safe, call: record.call },
|
|
1218
|
+
at: key,
|
|
1219
|
+
})));
|
|
1220
|
+
return found.map(({ record }) => record);
|
|
1221
|
+
}
|
|
1222
|
+
include([{ call: call.call }, ...legacy].flatMap((source) => typeof source === "string"
|
|
1223
|
+
? [{ input: { ...call, namespace: source } }]
|
|
1224
|
+
: "call" in source
|
|
1225
|
+
? [{ input: { ...exact, call: source.call } }]
|
|
1226
|
+
: []));
|
|
1227
|
+
rescan();
|
|
1019
1228
|
/** A legacy key holding anything but this intent's own mirror. */
|
|
1020
1229
|
const foreign = (index) => journals[index].read()?.mirrorOf !== namespace &&
|
|
1021
1230
|
delegates[index].peek().status !== "clear";
|
|
@@ -1023,6 +1232,7 @@ export function createWriteHold(options = {}) {
|
|
|
1023
1232
|
const active = () => delegates.find((_, index) => foreign(index));
|
|
1024
1233
|
/** The first legacy key holding an unresolved write, through its own gate. */
|
|
1025
1234
|
async function delegated(client) {
|
|
1235
|
+
rescan();
|
|
1026
1236
|
for (const [index, handle] of delegates.entries()) {
|
|
1027
1237
|
if (!foreign(index))
|
|
1028
1238
|
continue;
|
|
@@ -1058,14 +1268,20 @@ export function createWriteHold(options = {}) {
|
|
|
1058
1268
|
entered += 1;
|
|
1059
1269
|
return acquire(index + 1);
|
|
1060
1270
|
});
|
|
1271
|
+
const refuse = (held) => new WriteHeldError(noticed({
|
|
1272
|
+
...ungated(classify(held)),
|
|
1273
|
+
reservation: held.id,
|
|
1274
|
+
}));
|
|
1061
1275
|
for (const journal of journals) {
|
|
1062
1276
|
const held = journal.read();
|
|
1063
1277
|
if (held && held.mirrorOf !== namespace)
|
|
1064
|
-
throw
|
|
1065
|
-
...ungated(classify(held)),
|
|
1066
|
-
reservation: held.id,
|
|
1067
|
-
}));
|
|
1278
|
+
throw refuse(held);
|
|
1068
1279
|
}
|
|
1280
|
+
// A matching write saved since the last look holds it too, and
|
|
1281
|
+
// its own state() and exit() are reached from now on.
|
|
1282
|
+
const [late] = rescan();
|
|
1283
|
+
if (late)
|
|
1284
|
+
throw refuse(late);
|
|
1069
1285
|
// Another attempt since this review opened: review it again.
|
|
1070
1286
|
if (generations() !== reviewed)
|
|
1071
1287
|
throw new WriteHoldStaleReviewError(ownJournal.settled());
|
|
@@ -1153,6 +1369,7 @@ export function createWriteHold(options = {}) {
|
|
|
1153
1369
|
return own.send(client, run);
|
|
1154
1370
|
},
|
|
1155
1371
|
async exit(snapshot, acknowledgement, client) {
|
|
1372
|
+
rescan();
|
|
1156
1373
|
const owner = snapshot.status === "clear"
|
|
1157
1374
|
? undefined
|
|
1158
1375
|
: delegates.find((handle, index) => {
|
|
@@ -1174,6 +1391,10 @@ export function createWriteHold(options = {}) {
|
|
|
1174
1391
|
onUnproven: async (...args) => active()?.onUnproven(...args) ?? after(await own.onUnproven(...args)),
|
|
1175
1392
|
onExecuted: async (...args) => active()?.onExecuted(...args) ?? after(await own.onExecuted(...args)),
|
|
1176
1393
|
attachHash: async (...args) => active()?.attachHash(...args) ?? after(await own.attachHash(...args)),
|
|
1394
|
+
attachSafeNonce: async (...args) => active()?.attachSafeNonce(...args) ??
|
|
1395
|
+
after(await own.attachSafeNonce(...args)),
|
|
1396
|
+
// A legacy hold already holds it: nothing is adopted.
|
|
1397
|
+
adopt: async (...args) => active()?.peek() ?? after(await own.adopt(...args)),
|
|
1177
1398
|
};
|
|
1178
1399
|
}
|
|
1179
1400
|
/**
|
|
@@ -1181,11 +1402,12 @@ export function createWriteHold(options = {}) {
|
|
|
1181
1402
|
* it is that intent's hold: it never sends or releases on its own, and its
|
|
1182
1403
|
* exit is the intent's. A mirror left after the intent ended is removed.
|
|
1183
1404
|
*/
|
|
1184
|
-
function openExact(call) {
|
|
1185
|
-
const own = openOne(call);
|
|
1405
|
+
function openExact(call, at) {
|
|
1406
|
+
const own = openOne(call, {}, at);
|
|
1186
1407
|
const journal = createBrowserWriteRecovery(call, {
|
|
1187
1408
|
storage,
|
|
1188
1409
|
locks: options.locks,
|
|
1410
|
+
at,
|
|
1189
1411
|
});
|
|
1190
1412
|
/** The intent this key mirrors, from storage alone. */
|
|
1191
1413
|
const mirrored = () => {
|
|
@@ -1249,6 +1471,9 @@ export function createWriteHold(options = {}) {
|
|
|
1249
1471
|
onUnproven: (...args) => (route() ?? own).onUnproven(...args),
|
|
1250
1472
|
onExecuted: (...args) => (route() ?? own).onExecuted(...args),
|
|
1251
1473
|
attachHash: (...args) => (route() ?? own).attachHash(...args),
|
|
1474
|
+
attachSafeNonce: (...args) => (route() ?? own).attachSafeNonce(...args),
|
|
1475
|
+
// An intent's mirror already holds it: nothing is adopted.
|
|
1476
|
+
adopt: async (...args) => route()?.peek() ?? own.adopt(...args),
|
|
1252
1477
|
};
|
|
1253
1478
|
}
|
|
1254
1479
|
return {
|
|
@@ -1258,14 +1483,7 @@ export function createWriteHold(options = {}) {
|
|
|
1258
1483
|
/** The review's line while holds last only in this tab, or null. */
|
|
1259
1484
|
reviewLine: () => (storage.durable() ? null : WRITE_HOLD_IN_TAB_LINE),
|
|
1260
1485
|
/** Stop every settle poll and the tab listener (tests, teardown). */
|
|
1261
|
-
dispose
|
|
1262
|
-
for (const timer of timers.values())
|
|
1263
|
-
clearTimeout(timer);
|
|
1264
|
-
timers.clear();
|
|
1265
|
-
if (onStorage)
|
|
1266
|
-
globalThis.removeEventListener?.("storage", onStorage);
|
|
1267
|
-
onStorage = undefined;
|
|
1268
|
-
},
|
|
1486
|
+
dispose,
|
|
1269
1487
|
};
|
|
1270
1488
|
}
|
|
1271
1489
|
//# sourceMappingURL=writeHold.js.map
|