@bananapus/nana-sdk-core 2.31.0 → 2.33.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/index.d.ts +1 -0
- package/dist/cjs/review/index.d.ts.map +1 -1
- package/dist/cjs/review/index.js +1 -0
- package/dist/cjs/review/index.js.map +1 -1
- package/dist/cjs/review/messages.d.ts +18 -0
- package/dist/cjs/review/messages.d.ts.map +1 -1
- package/dist/cjs/review/messages.js +19 -1
- package/dist/cjs/review/messages.js.map +1 -1
- package/dist/cjs/review/writeHold.d.ts +211 -0
- package/dist/cjs/review/writeHold.d.ts.map +1 -0
- package/dist/cjs/review/writeHold.js +620 -0
- package/dist/cjs/review/writeHold.js.map +1 -0
- package/dist/cjs/review/writeRecovery.d.ts +113 -9
- package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
- package/dist/cjs/review/writeRecovery.js +357 -49
- package/dist/cjs/review/writeRecovery.js.map +1 -1
- package/dist/cjs/safeService.d.ts +40 -0
- package/dist/cjs/safeService.d.ts.map +1 -1
- package/dist/cjs/safeService.js +69 -12
- package/dist/cjs/safeService.js.map +1 -1
- package/dist/esm/review/index.d.ts +1 -0
- package/dist/esm/review/index.d.ts.map +1 -1
- package/dist/esm/review/index.js +1 -0
- package/dist/esm/review/index.js.map +1 -1
- package/dist/esm/review/messages.d.ts +18 -0
- package/dist/esm/review/messages.d.ts.map +1 -1
- package/dist/esm/review/messages.js +18 -0
- package/dist/esm/review/messages.js.map +1 -1
- package/dist/esm/review/writeHold.d.ts +211 -0
- package/dist/esm/review/writeHold.d.ts.map +1 -0
- package/dist/esm/review/writeHold.js +613 -0
- package/dist/esm/review/writeHold.js.map +1 -0
- package/dist/esm/review/writeRecovery.d.ts +113 -9
- package/dist/esm/review/writeRecovery.d.ts.map +1 -1
- package/dist/esm/review/writeRecovery.js +355 -49
- package/dist/esm/review/writeRecovery.js.map +1 -1
- package/dist/esm/safeService.d.ts +40 -0
- package/dist/esm/safeService.d.ts.map +1 -1
- package/dist/esm/safeService.js +66 -12
- package/dist/esm/safeService.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SubmittedWritePersistenceError = void 0;
|
|
4
4
|
exports.parseReviewedWriteRecoveryRecord = recordOf;
|
|
5
|
+
exports.createHeldWriteStorage = createHeldWriteStorage;
|
|
5
6
|
exports.readBrowserWriteRecovery = readBrowserWriteRecovery;
|
|
6
7
|
exports.sameReviewedWrite = sameReviewedWrite;
|
|
7
8
|
exports.browserLocks = browserLocks;
|
|
8
9
|
exports.createBrowserWriteRecovery = createBrowserWriteRecovery;
|
|
9
10
|
exports.readReviewedWriteNonce = readReviewedWriteNonce;
|
|
10
11
|
exports.checkReviewedWrite = checkReviewedWrite;
|
|
12
|
+
exports.findTransactionAtNonce = findTransactionAtNonce;
|
|
11
13
|
exports.verifyReviewedWriteReceipt = verifyReviewedWriteReceipt;
|
|
12
14
|
exports.verifyReviewedWriteExpiry = verifyReviewedWriteExpiry;
|
|
13
15
|
const viem_1 = require("viem");
|
|
14
16
|
const safeService_js_1 = require("../safeService.js");
|
|
15
17
|
const untrusted_js_1 = require("../untrusted.js");
|
|
16
18
|
const relayr_js_1 = require("./relayr.js");
|
|
19
|
+
const TAB_MEMORY = Symbol.for("nana-sdk:held-writes");
|
|
20
|
+
const TAB_QUEUES = Symbol.for("nana-sdk:write-lock-queues");
|
|
21
|
+
const TAB_SEEN = Symbol.for("nana-sdk:held-writes-seen");
|
|
17
22
|
// A returned hash must survive a component remount even when its durable update
|
|
18
23
|
// failed. It is visible only while the exact durable reservation still exists.
|
|
19
24
|
const unpersistedSubmissions = new WeakMap();
|
|
@@ -66,7 +71,17 @@ function recordOf(value) {
|
|
|
66
71
|
(record.hash !== undefined && !(0, untrusted_js_1.isBytes32)(record.hash)) ||
|
|
67
72
|
!optionalDecimal(record.nonce) ||
|
|
68
73
|
!optionalDecimal(record.fromBlock) ||
|
|
69
|
-
!optionalDecimal(record.sentNonce)
|
|
74
|
+
!optionalDecimal(record.sentNonce) ||
|
|
75
|
+
!optionalDecimal(record.submittedAt) ||
|
|
76
|
+
!optionalDecimal(record.firstSeenAt) ||
|
|
77
|
+
!optionalDecimal(record.safeNonce) ||
|
|
78
|
+
(record.replaced !== undefined && record.replaced !== true) ||
|
|
79
|
+
(record.unproven !== undefined && record.unproven !== true) ||
|
|
80
|
+
(record.sentFrom !== undefined &&
|
|
81
|
+
!(0, viem_1.isAddress)(record.sentFrom, { strict: false })) ||
|
|
82
|
+
(record.executed !== undefined && record.executed !== true) ||
|
|
83
|
+
(record.executionHash !== undefined &&
|
|
84
|
+
(record.executed !== true || !(0, untrusted_js_1.isBytes32)(record.executionHash))))
|
|
70
85
|
throw new Error("Invalid saved write recovery record. Keep this action pending.");
|
|
71
86
|
return {
|
|
72
87
|
version: 1,
|
|
@@ -78,6 +93,22 @@ function recordOf(value) {
|
|
|
78
93
|
...(record.nonce === undefined ? {} : { nonce: record.nonce }),
|
|
79
94
|
...(record.fromBlock === undefined ? {} : { fromBlock: record.fromBlock }),
|
|
80
95
|
...(record.sentNonce === undefined ? {} : { sentNonce: record.sentNonce }),
|
|
96
|
+
...(record.submittedAt === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { submittedAt: record.submittedAt }),
|
|
99
|
+
...(record.firstSeenAt === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { firstSeenAt: record.firstSeenAt }),
|
|
102
|
+
...(record.safeNonce === undefined ? {} : { safeNonce: record.safeNonce }),
|
|
103
|
+
...(record.sentFrom === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { sentFrom: record.sentFrom.toLowerCase() }),
|
|
106
|
+
...(record.replaced ? { replaced: true } : {}),
|
|
107
|
+
...(record.unproven ? { unproven: true } : {}),
|
|
108
|
+
...(record.executed ? { executed: true } : {}),
|
|
109
|
+
...(record.executionHash === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: { executionHash: record.executionHash.toLowerCase() }),
|
|
81
112
|
};
|
|
82
113
|
}
|
|
83
114
|
function optionalDecimal(value) {
|
|
@@ -117,6 +148,136 @@ function recoveryStorage(store) {
|
|
|
117
148
|
throw new Error("Write recovery storage is unavailable.");
|
|
118
149
|
return result;
|
|
119
150
|
}
|
|
151
|
+
const PROBE_KEY = "nana-sdk:reviewed-write:probe";
|
|
152
|
+
function idOf(raw) {
|
|
153
|
+
try {
|
|
154
|
+
const id = JSON.parse(raw)?.id;
|
|
155
|
+
return typeof id === "string" ? id : undefined;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The browser's storage, shared by its tabs and kept across reloads, while it
|
|
163
|
+
* can be written; this tab's memory while it can't (private browsing, a
|
|
164
|
+
* sandboxed frame, some wallet webviews), so a write is held either way and
|
|
165
|
+
* never refused. Every read looks at both. A record kept in memory moves into
|
|
166
|
+
* the browser's storage as soon as it can be written there, after any other
|
|
167
|
+
* live record saved at the same key ends. While the browser's storage can't
|
|
168
|
+
* be read, what this tab last read or wrote there still holds.
|
|
169
|
+
*
|
|
170
|
+
* `memory` defaults to one map per tab, shared by every copy of this module
|
|
171
|
+
* loaded there.
|
|
172
|
+
*/
|
|
173
|
+
function createHeldWriteStorage(options = {}) {
|
|
174
|
+
const durable = options.durable ??
|
|
175
|
+
(() => globalThis.localStorage);
|
|
176
|
+
const tab = globalThis;
|
|
177
|
+
const memory = options.memory ?? (tab[TAB_MEMORY] ??= new Map());
|
|
178
|
+
/**
|
|
179
|
+
* What this tab last saw in, or wrote to, durable storage (records,
|
|
180
|
+
* releases and deletions), shared by every controller and copy of this
|
|
181
|
+
* module using the same tab memory.
|
|
182
|
+
*/
|
|
183
|
+
const registry = (globalThis[TAB_SEEN] ??= new WeakMap());
|
|
184
|
+
let seen = registry.get(memory);
|
|
185
|
+
if (!seen)
|
|
186
|
+
registry.set(memory, (seen = new Map()));
|
|
187
|
+
const read = (key) => {
|
|
188
|
+
try {
|
|
189
|
+
const store = durable();
|
|
190
|
+
if (!store)
|
|
191
|
+
return undefined;
|
|
192
|
+
const value = store.getItem(key);
|
|
193
|
+
if (value === null)
|
|
194
|
+
seen.delete(key);
|
|
195
|
+
else
|
|
196
|
+
seen.set(key, value);
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const write = (key, value) => {
|
|
204
|
+
try {
|
|
205
|
+
const store = durable();
|
|
206
|
+
if (!store)
|
|
207
|
+
return false;
|
|
208
|
+
store.setItem(key, value);
|
|
209
|
+
if (store.getItem(key) !== value)
|
|
210
|
+
return false;
|
|
211
|
+
seen.set(key, value);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const getItem = (key) => {
|
|
219
|
+
const saved = read(key);
|
|
220
|
+
const kept = memory.get(key);
|
|
221
|
+
if (saved === undefined)
|
|
222
|
+
return kept ?? seen.get(key) ?? null;
|
|
223
|
+
if (kept === undefined)
|
|
224
|
+
return saved;
|
|
225
|
+
if (saved === kept) {
|
|
226
|
+
memory.delete(key);
|
|
227
|
+
return saved;
|
|
228
|
+
}
|
|
229
|
+
const savedId = saved === null ? undefined : idOf(saved);
|
|
230
|
+
// Another live record saved there first is held first; this one waits.
|
|
231
|
+
if (saved !== null &&
|
|
232
|
+
savedId !== idOf(kept) &&
|
|
233
|
+
!(savedId !== undefined && getItem(releasedKey(key, savedId)) !== null))
|
|
234
|
+
return saved;
|
|
235
|
+
if (write(key, kept))
|
|
236
|
+
memory.delete(key);
|
|
237
|
+
return kept;
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
getItem,
|
|
241
|
+
setItem(key, value) {
|
|
242
|
+
const kept = memory.get(key);
|
|
243
|
+
if (write(key, value)) {
|
|
244
|
+
if (kept === value || (kept && idOf(kept) === idOf(value)))
|
|
245
|
+
memory.delete(key);
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
memory.set(key, value);
|
|
249
|
+
},
|
|
250
|
+
removeItem(key) {
|
|
251
|
+
const shown = getItem(key);
|
|
252
|
+
if (shown !== null && memory.get(key) === shown) {
|
|
253
|
+
memory.delete(key);
|
|
254
|
+
if (read(key) !== shown)
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
durable()?.removeItem(key);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* The readback below decides. */
|
|
262
|
+
}
|
|
263
|
+
if (read(key) === null)
|
|
264
|
+
seen.delete(key);
|
|
265
|
+
},
|
|
266
|
+
durable() {
|
|
267
|
+
const probe = String(Date.now());
|
|
268
|
+
if (!write(PROBE_KEY, probe))
|
|
269
|
+
return false;
|
|
270
|
+
try {
|
|
271
|
+
durable().removeItem(PROBE_KEY);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
/* A leftover probe names no write. */
|
|
275
|
+
}
|
|
276
|
+
seen.delete(PROBE_KEY);
|
|
277
|
+
return true;
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
120
281
|
/**
|
|
121
282
|
* Open the original namespaced action before fresh quotation or phase checks.
|
|
122
283
|
* No call is invented to locate evidence. Legacy actions retain factory.read().
|
|
@@ -152,8 +313,8 @@ function readRecovery(store, key, raw = store.getItem(key), scopeOf = scope) {
|
|
|
152
313
|
const memory = unpersistedSubmissions.get(store)?.get(key);
|
|
153
314
|
if (!record ||
|
|
154
315
|
!memory ||
|
|
155
|
-
JSON.stringify({ ...record, hash: undefined }) !==
|
|
156
|
-
JSON.stringify({ ...memory, hash: undefined }) ||
|
|
316
|
+
JSON.stringify({ ...record, hash: undefined, submittedAt: undefined }) !==
|
|
317
|
+
JSON.stringify({ ...memory, hash: undefined, submittedAt: undefined }) ||
|
|
157
318
|
(record.hash && record.hash !== memory.hash)) {
|
|
158
319
|
// A readable durable snapshot also proves whether cached evidence is stale.
|
|
159
320
|
unpersistedSubmissions.get(store)?.delete(key);
|
|
@@ -167,8 +328,9 @@ function readRecovery(store, key, raw = store.getItem(key), scopeOf = scope) {
|
|
|
167
328
|
function sameReviewedWrite(record, input) {
|
|
168
329
|
return (JSON.stringify(normalized(record)) === JSON.stringify(normalized(input)));
|
|
169
330
|
}
|
|
170
|
-
// Without Web Locks, writes in this tab still run one at a time per action
|
|
171
|
-
|
|
331
|
+
// Without Web Locks, writes in this tab still run one at a time per action,
|
|
332
|
+
// across every copy of this module loaded in the tab.
|
|
333
|
+
const tabQueues = (globalThis[TAB_QUEUES] ??= new Map());
|
|
172
334
|
let tabLocksNoted = false;
|
|
173
335
|
const tabLocks = {
|
|
174
336
|
async request(name, options, work) {
|
|
@@ -397,6 +559,23 @@ function createBrowserWriteRecovery(input, options = {}) {
|
|
|
397
559
|
throw new Error("Write recovery storage did not clear the action.");
|
|
398
560
|
unpersistedSubmissions.get(store)?.delete(at);
|
|
399
561
|
}
|
|
562
|
+
/** Add `fields` to exactly `expected`, a hashed record `when` allows; extra evidence only. */
|
|
563
|
+
function amend(expected, when, fields) {
|
|
564
|
+
requireLock();
|
|
565
|
+
const current = read();
|
|
566
|
+
if (!current?.hash ||
|
|
567
|
+
!when(current) ||
|
|
568
|
+
JSON.stringify(current) !== JSON.stringify(recordOf(expected)))
|
|
569
|
+
return current ?? recordOf(expected);
|
|
570
|
+
const next = recordOf({ ...current, ...fields });
|
|
571
|
+
try {
|
|
572
|
+
persist(next, locate(storage()).at);
|
|
573
|
+
return next;
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return current;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
400
579
|
function rejected() {
|
|
401
580
|
requireLock();
|
|
402
581
|
// A rejection callback from another attempt must never release this one.
|
|
@@ -439,7 +618,15 @@ function createBrowserWriteRecovery(input, options = {}) {
|
|
|
439
618
|
(!replace && previous.hash && previous.hash !== submittedHash)) {
|
|
440
619
|
throw new Error("Write recovery changed before submission was recorded.");
|
|
441
620
|
}
|
|
442
|
-
|
|
621
|
+
// A hash the wallet returns now starts its clock; a speed-up found at its nonce keeps it.
|
|
622
|
+
const submittedAt = replace || previous.hash === submittedHash
|
|
623
|
+
? previous.submittedAt
|
|
624
|
+
: String(Date.now());
|
|
625
|
+
const submitted = recordOf({
|
|
626
|
+
...previous,
|
|
627
|
+
hash: submittedHash,
|
|
628
|
+
...(submittedAt === undefined ? {} : { submittedAt }),
|
|
629
|
+
});
|
|
443
630
|
let raw;
|
|
444
631
|
let at = key;
|
|
445
632
|
try {
|
|
@@ -453,8 +640,16 @@ function createBrowserWriteRecovery(input, options = {}) {
|
|
|
453
640
|
// A record the user released keeps its marker: a late hash leaves it released.
|
|
454
641
|
const current = readStored(raw, at, scopeAt(at));
|
|
455
642
|
if (!current ||
|
|
456
|
-
JSON.stringify({
|
|
457
|
-
|
|
643
|
+
JSON.stringify({
|
|
644
|
+
...current,
|
|
645
|
+
hash: undefined,
|
|
646
|
+
submittedAt: undefined,
|
|
647
|
+
}) !==
|
|
648
|
+
JSON.stringify({
|
|
649
|
+
...previous,
|
|
650
|
+
hash: undefined,
|
|
651
|
+
submittedAt: undefined,
|
|
652
|
+
}) ||
|
|
458
653
|
(!replace && current.hash && current.hash !== submittedHash)) {
|
|
459
654
|
throw new Error("Write recovery changed before submission was recorded.");
|
|
460
655
|
}
|
|
@@ -541,10 +736,93 @@ function createBrowserWriteRecovery(input, options = {}) {
|
|
|
541
736
|
},
|
|
542
737
|
rejected,
|
|
543
738
|
clear,
|
|
739
|
+
/**
|
|
740
|
+
* Start the clock of a hash saved before `submittedAt` existed: save now
|
|
741
|
+
* as its `firstSeenAt`, once. Extra evidence: a stale snapshot or a failed
|
|
742
|
+
* save keeps the record as it was.
|
|
743
|
+
*/
|
|
744
|
+
firstSeen(expected) {
|
|
745
|
+
return amend(expected, (current) => current.submittedAt === undefined &&
|
|
746
|
+
current.firstSeenAt === undefined, { firstSeenAt: String(Date.now()) });
|
|
747
|
+
},
|
|
748
|
+
/**
|
|
749
|
+
* Save who sent a Safe execution the wallet returned at once, and at which
|
|
750
|
+
* nonce, from its authenticated transaction, once: its canonical
|
|
751
|
+
* replacement can then be proven. Extra evidence only.
|
|
752
|
+
*/
|
|
753
|
+
sentBy(expected, from, nonce) {
|
|
754
|
+
return amend(expected, (current) => current.safe && current.sentFrom === undefined, { sentFrom: from, sentNonce: nonce.toString() });
|
|
755
|
+
},
|
|
756
|
+
/**
|
|
757
|
+
* Drop the proof that `expected` ran, once its execution is gone from the
|
|
758
|
+
* chain: it is held again as an unproven write. Extra evidence only.
|
|
759
|
+
*/
|
|
760
|
+
unexecuted(expected) {
|
|
761
|
+
return amend(expected, (current) => current.executed === true, {
|
|
762
|
+
executed: undefined,
|
|
763
|
+
executionHash: undefined,
|
|
764
|
+
});
|
|
765
|
+
},
|
|
766
|
+
/**
|
|
767
|
+
* Save that another Safe transaction used this Safe write's nonce. Any
|
|
768
|
+
* saved execution goes: the user's exit, not the chain, ends it.
|
|
769
|
+
*/
|
|
770
|
+
replacedOnChain(expected) {
|
|
771
|
+
return amend(expected, (current) => current.safe && !current.replaced, {
|
|
772
|
+
executed: undefined,
|
|
773
|
+
executionHash: undefined,
|
|
774
|
+
replaced: true,
|
|
775
|
+
});
|
|
776
|
+
},
|
|
777
|
+
/**
|
|
778
|
+
* Save that the app's follower of this Safe write ended without proof.
|
|
779
|
+
* A proven execution or replacement keeps its own state.
|
|
780
|
+
*/
|
|
781
|
+
unprovenEnd(expected) {
|
|
782
|
+
return amend(expected, (current) => current.safe &&
|
|
783
|
+
!current.executed &&
|
|
784
|
+
!current.replaced &&
|
|
785
|
+
!current.unproven, { unproven: true });
|
|
786
|
+
},
|
|
787
|
+
/**
|
|
788
|
+
* Save a Safe proposal's nonce from its authenticated record, once. Extra
|
|
789
|
+
* evidence: a stale snapshot or a failed save keeps the record as it was.
|
|
790
|
+
*/
|
|
791
|
+
proposalNonce(expected, nonce) {
|
|
792
|
+
return amend(expected, (current) => current.safe && current.safeNonce === undefined, { safeNonce: nonce.toString() });
|
|
793
|
+
},
|
|
794
|
+
/**
|
|
795
|
+
* Save the chain's proof that `expected` ran, with the transaction it ran
|
|
796
|
+
* in when known. From then on the user's exit refuses it: only its result
|
|
797
|
+
* ({@link verifyReviewedWriteReceipt}) releases it.
|
|
798
|
+
*/
|
|
799
|
+
executed(expected, executionHash) {
|
|
800
|
+
requireLock();
|
|
801
|
+
if (executionHash !== undefined && !(0, untrusted_js_1.isBytes32)(executionHash))
|
|
802
|
+
throw new Error("Invalid execution transaction identity.");
|
|
803
|
+
const store = storage();
|
|
804
|
+
const { at, raw } = locate(store);
|
|
805
|
+
const current = readRecovery(store, at, raw, scopeAt(at));
|
|
806
|
+
const wanted = recordOf(expected);
|
|
807
|
+
if (!current?.hash ||
|
|
808
|
+
current.id !== wanted.id ||
|
|
809
|
+
current.hash !== wanted.hash)
|
|
810
|
+
throw new Error("Write recovery changed. Keep the current action pending.");
|
|
811
|
+
const next = recordOf({
|
|
812
|
+
...current,
|
|
813
|
+
executed: true,
|
|
814
|
+
...((executionHash ?? current.executionHash)
|
|
815
|
+
? { executionHash: executionHash ?? current.executionHash }
|
|
816
|
+
: {}),
|
|
817
|
+
});
|
|
818
|
+
persist(next, at);
|
|
819
|
+
unpersistedSubmissions.get(store)?.delete(at);
|
|
820
|
+
return next;
|
|
821
|
+
},
|
|
544
822
|
/**
|
|
545
823
|
* Check the saved write against the chain ({@link checkReviewedWrite}) and
|
|
546
|
-
* apply the finding: `sent` records the hash to track
|
|
547
|
-
*
|
|
824
|
+
* apply the finding: `sent` records the hash to track. Nothing here
|
|
825
|
+
* releases a write; null when nothing is saved.
|
|
548
826
|
*/
|
|
549
827
|
async resolve(client, checkOptions) {
|
|
550
828
|
requireLock();
|
|
@@ -554,12 +832,8 @@ function createBrowserWriteRecovery(input, options = {}) {
|
|
|
554
832
|
const { check, sentNonce } = await inspectWrite(client, saved, checkOptions?.effect);
|
|
555
833
|
if (sentNonce !== undefined && saved.sentNonce !== sentNonce)
|
|
556
834
|
saved = bind(saved, sentNonce);
|
|
557
|
-
if (check.status === "sent")
|
|
558
|
-
|
|
559
|
-
record(check.hash, saved, true);
|
|
560
|
-
}
|
|
561
|
-
else if (check.status !== "pending")
|
|
562
|
-
clear(saved);
|
|
835
|
+
if (check.status === "sent" && check.hash !== saved.hash)
|
|
836
|
+
record(check.hash, saved, true);
|
|
563
837
|
return check;
|
|
564
838
|
},
|
|
565
839
|
/**
|
|
@@ -579,6 +853,8 @@ function createBrowserWriteRecovery(input, options = {}) {
|
|
|
579
853
|
const durable = readStored(raw, at, scopeAt(at), store);
|
|
580
854
|
if (!durable)
|
|
581
855
|
return;
|
|
856
|
+
if (durable.executed)
|
|
857
|
+
throw new Error("This write ran onchain. Keep it held until its result is known.");
|
|
582
858
|
const shown = readRecovery(store, at, raw, scopeAt(at));
|
|
583
859
|
if (JSON.stringify(durable) !== wanted &&
|
|
584
860
|
JSON.stringify(shown) !== wanted)
|
|
@@ -642,20 +918,29 @@ async function checkReviewedWrite(client, saved, { effect } = {}) {
|
|
|
642
918
|
}
|
|
643
919
|
async function inspectWrite(client, saved, effect) {
|
|
644
920
|
const record = recordOf(saved);
|
|
921
|
+
// No proof, the app's effect read included, from another chain, and none
|
|
922
|
+
// here for a Safe write.
|
|
923
|
+
if (record.safe ||
|
|
924
|
+
(await client.getChainId().catch(() => null)) !== record.chainId)
|
|
925
|
+
return { check: { status: "pending" } };
|
|
926
|
+
const found = await inspectOrdinary(client, record);
|
|
927
|
+
if (found.check.status === "pending" &&
|
|
928
|
+
effect &&
|
|
929
|
+
(await effect(record.chainId).catch(() => false)))
|
|
930
|
+
return { ...found, check: { ...found.check, effect: true } };
|
|
931
|
+
return found;
|
|
932
|
+
}
|
|
933
|
+
async function inspectOrdinary(client, record) {
|
|
645
934
|
const pending = { check: { status: "pending" } };
|
|
646
|
-
// No proof, the app's effect read included, from another chain.
|
|
647
|
-
if ((await client.getChainId().catch(() => null)) !== record.chainId)
|
|
648
|
-
return pending;
|
|
649
|
-
if (effect && (await effect(record.chainId).catch(() => false)))
|
|
650
|
-
return { check: { status: "ran" } };
|
|
651
|
-
if (record.safe)
|
|
652
|
-
return pending;
|
|
653
935
|
if (record.hash) {
|
|
654
936
|
const sent = await client.getTransaction({ hash: record.hash }).then((tx) => tx, (error) => error?.name === "TransactionNotFoundError" ? null : undefined);
|
|
655
937
|
if (sent === undefined)
|
|
656
938
|
return pending;
|
|
657
939
|
if (sent) {
|
|
658
|
-
|
|
940
|
+
// Only the saved hash's own transaction, on the saved chain, is evidence.
|
|
941
|
+
if (sent.from?.toLowerCase() !== record.account ||
|
|
942
|
+
sent.hash?.toLowerCase() !== record.hash ||
|
|
943
|
+
(sent.chainId !== undefined && sent.chainId !== record.chainId))
|
|
659
944
|
return pending;
|
|
660
945
|
return {
|
|
661
946
|
check: sent.blockNumber !== null && sent.blockNumber !== undefined
|
|
@@ -667,15 +952,39 @@ async function inspectWrite(client, saved, effect) {
|
|
|
667
952
|
}
|
|
668
953
|
const bound = record.hash !== undefined && record.sentNonce !== undefined;
|
|
669
954
|
const target = bound ? record.sentNonce : record.nonce;
|
|
670
|
-
|
|
955
|
+
// The nonce a hash was sent at is authenticated by its own transaction, so
|
|
956
|
+
// a record saved without a starting block searches from genesis instead.
|
|
957
|
+
const from = record.fromBlock ?? (bound ? "0" : undefined);
|
|
958
|
+
if (target === undefined || from === undefined)
|
|
671
959
|
return pending;
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
960
|
+
const used = await findTransactionAtNonce(client, record.account, BigInt(target), BigInt(from));
|
|
961
|
+
if (!used)
|
|
962
|
+
return pending;
|
|
963
|
+
if (used.hash === record.hash)
|
|
964
|
+
return { check: { status: "sent", hash: used.hash } };
|
|
965
|
+
const sameCall = used.to?.toLowerCase() === record.call.to &&
|
|
966
|
+
used.input?.toLowerCase() === record.call.data &&
|
|
967
|
+
BigInt(used.value ?? 0) === BigInt(record.call.value);
|
|
968
|
+
// Another hash is adopted only at the nonce the saved hash's own
|
|
969
|
+
// transaction was sent at; the pre-wallet nonce only locates the call.
|
|
970
|
+
if (sameCall)
|
|
971
|
+
return bound ? { check: { status: "sent", hash: used.hash } } : pending;
|
|
972
|
+
// Another transaction at that nonce may have run the call inside it
|
|
973
|
+
// (a wrapper, or an EIP-7702 wallet that ran code and then cleared its
|
|
974
|
+
// delegation), so it proves nothing. The user's exit ends it.
|
|
975
|
+
return { check: { status: "pending", usedBy: used.hash } };
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* The transaction `account` sent at `nonce`, once the account's nonce is past
|
|
979
|
+
* it at a canonical finalized block: a search of its nonce from `fromBlock`
|
|
980
|
+
* finds the block where it moved. Null while that is unproven or the node
|
|
981
|
+
* can't say (no history, a reorganized answer).
|
|
982
|
+
*/
|
|
983
|
+
async function findTransactionAtNonce(client, account, nonce, fromBlock = 0n) {
|
|
984
|
+
const from = account.toLowerCase();
|
|
985
|
+
const count = async (blockNumber) => BigInt(await client.getTransactionCount({ address: account, blockNumber }));
|
|
677
986
|
const proof = await (0, relayr_js_1.atCanonicalFinalizedBlock)(client, async (finalized) => {
|
|
678
|
-
let low =
|
|
987
|
+
let low = fromBlock;
|
|
679
988
|
let high = finalized;
|
|
680
989
|
if (high <= low ||
|
|
681
990
|
(await count(high)) <= nonce ||
|
|
@@ -693,34 +1002,24 @@ async function inspectWrite(client, saved, effect) {
|
|
|
693
1002
|
includeTransactions: true,
|
|
694
1003
|
});
|
|
695
1004
|
const used = block.number === high
|
|
696
|
-
? block.transactions.find((tx) => tx.from?.toLowerCase() ===
|
|
697
|
-
BigInt(tx.nonce) === nonce)
|
|
1005
|
+
? block.transactions.find((tx) => tx.from?.toLowerCase() === from && BigInt(tx.nonce) === nonce)
|
|
698
1006
|
: undefined;
|
|
699
1007
|
if (!used || !(0, untrusted_js_1.isBytes32)(used.hash))
|
|
700
1008
|
return null;
|
|
701
|
-
|
|
702
|
-
if (hash === record.hash)
|
|
703
|
-
return { status: "sent", hash };
|
|
704
|
-
if (used.to?.toLowerCase() === record.call.to &&
|
|
705
|
-
used.input?.toLowerCase() === record.call.data &&
|
|
706
|
-
BigInt(used.value ?? 0) === BigInt(record.call.value))
|
|
707
|
-
return { status: "sent", hash };
|
|
708
|
-
// Another transaction at that nonce may have run the call inside it
|
|
709
|
-
// (a wrapper, or an EIP-7702 wallet that ran code and then cleared its
|
|
710
|
-
// delegation), so it proves nothing. The user's exit ends it.
|
|
711
|
-
return null;
|
|
1009
|
+
return { ...used, hash: used.hash.toLowerCase() };
|
|
712
1010
|
});
|
|
713
|
-
return
|
|
1011
|
+
return proof?.value ?? null;
|
|
714
1012
|
}
|
|
715
1013
|
/**
|
|
716
1014
|
* Authenticate the saved wallet identity, canonical inclusion and Safe effect.
|
|
717
1015
|
* Ordinary wallet wrappers are allowed: the wallet's returned hash binds their
|
|
718
1016
|
* execution. A wallet-returned Safe proposal hash authenticates its execution
|
|
719
1017
|
* event even through a wrapper; an immediate execution hash additionally needs
|
|
720
|
-
* exact reviewed calldata. A failure releases only after finality
|
|
1018
|
+
* exact reviewed calldata. A failure releases only after finality;
|
|
1019
|
+
* `finality: false` authenticates a canonical failure before it is final.
|
|
721
1020
|
* Throws on missing or inconsistent evidence; the caller retains its record.
|
|
722
1021
|
*/
|
|
723
|
-
async function verifyReviewedWriteReceipt(client, saved, receipt, proof) {
|
|
1022
|
+
async function verifyReviewedWriteReceipt(client, saved, receipt, proof, { finality = true } = {}) {
|
|
724
1023
|
const record = recordOf(saved);
|
|
725
1024
|
const pending = () => new Error("The saved wallet write is not yet proven. Keep this action pending.");
|
|
726
1025
|
let calls = [record.call];
|
|
@@ -767,7 +1066,16 @@ async function verifyReviewedWriteReceipt(client, saved, receipt, proof) {
|
|
|
767
1066
|
(canonical.status !== "success" && canonical.status !== "reverted"))
|
|
768
1067
|
throw pending();
|
|
769
1068
|
let status = canonical.status === "success" ? "success" : "failed";
|
|
770
|
-
|
|
1069
|
+
// An execution the wallet returned at once whose own transaction reverted
|
|
1070
|
+
// ran nothing on the Safe: a failure, once final, when it was sent to it.
|
|
1071
|
+
const atOnceReverted = record.safe &&
|
|
1072
|
+
canonical.status === "reverted" &&
|
|
1073
|
+
hash.toLowerCase() === record.hash;
|
|
1074
|
+
if (atOnceReverted) {
|
|
1075
|
+
if (transaction.to?.toLowerCase() !== record.account)
|
|
1076
|
+
throw pending();
|
|
1077
|
+
}
|
|
1078
|
+
else if (record.safe) {
|
|
771
1079
|
if (!Array.isArray(canonical.logs) ||
|
|
772
1080
|
canonical.logs.some((log) => log.removed !== false ||
|
|
773
1081
|
log.transactionHash?.toLowerCase() !== hash.toLowerCase() ||
|
|
@@ -793,7 +1101,7 @@ async function verifyReviewedWriteReceipt(client, saved, receipt, proof) {
|
|
|
793
1101
|
return (block.number === canonical.blockNumber &&
|
|
794
1102
|
block.hash?.toLowerCase() === canonical.blockHash.toLowerCase());
|
|
795
1103
|
};
|
|
796
|
-
if (status === "failed") {
|
|
1104
|
+
if (status === "failed" && finality) {
|
|
797
1105
|
const finalized = await (0, relayr_js_1.atCanonicalFinalizedBlock)(client, async (number) => number >= canonical.blockNumber && (await isCanonical()));
|
|
798
1106
|
if (!finalized?.value)
|
|
799
1107
|
throw pending();
|