@bananapus/nana-sdk-core 2.30.0 → 2.32.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/index.d.ts +1 -0
  2. package/dist/cjs/review/index.d.ts.map +1 -1
  3. package/dist/cjs/review/index.js +1 -0
  4. package/dist/cjs/review/index.js.map +1 -1
  5. package/dist/cjs/review/messages.d.ts +16 -0
  6. package/dist/cjs/review/messages.d.ts.map +1 -1
  7. package/dist/cjs/review/messages.js +17 -1
  8. package/dist/cjs/review/messages.js.map +1 -1
  9. package/dist/cjs/review/relayr.d.ts +43 -7
  10. package/dist/cjs/review/relayr.d.ts.map +1 -1
  11. package/dist/cjs/review/relayr.js +118 -7
  12. package/dist/cjs/review/relayr.js.map +1 -1
  13. package/dist/cjs/review/writeHold.d.ts +197 -0
  14. package/dist/cjs/review/writeHold.d.ts.map +1 -0
  15. package/dist/cjs/review/writeHold.js +578 -0
  16. package/dist/cjs/review/writeHold.js.map +1 -0
  17. package/dist/cjs/review/writeRecovery.d.ts +102 -8
  18. package/dist/cjs/review/writeRecovery.d.ts.map +1 -1
  19. package/dist/cjs/review/writeRecovery.js +341 -46
  20. package/dist/cjs/review/writeRecovery.js.map +1 -1
  21. package/dist/cjs/safeService.d.ts +40 -0
  22. package/dist/cjs/safeService.d.ts.map +1 -1
  23. package/dist/cjs/safeService.js +69 -12
  24. package/dist/cjs/safeService.js.map +1 -1
  25. package/dist/esm/review/index.d.ts +1 -0
  26. package/dist/esm/review/index.d.ts.map +1 -1
  27. package/dist/esm/review/index.js +1 -0
  28. package/dist/esm/review/index.js.map +1 -1
  29. package/dist/esm/review/messages.d.ts +16 -0
  30. package/dist/esm/review/messages.d.ts.map +1 -1
  31. package/dist/esm/review/messages.js +16 -0
  32. package/dist/esm/review/messages.js.map +1 -1
  33. package/dist/esm/review/relayr.d.ts +43 -7
  34. package/dist/esm/review/relayr.d.ts.map +1 -1
  35. package/dist/esm/review/relayr.js +116 -6
  36. package/dist/esm/review/relayr.js.map +1 -1
  37. package/dist/esm/review/writeHold.d.ts +197 -0
  38. package/dist/esm/review/writeHold.d.ts.map +1 -0
  39. package/dist/esm/review/writeHold.js +571 -0
  40. package/dist/esm/review/writeHold.js.map +1 -0
  41. package/dist/esm/review/writeRecovery.d.ts +102 -8
  42. package/dist/esm/review/writeRecovery.d.ts.map +1 -1
  43. package/dist/esm/review/writeRecovery.js +339 -46
  44. package/dist/esm/review/writeRecovery.js.map +1 -1
  45. package/dist/esm/safeService.d.ts +40 -0
  46. package/dist/esm/safeService.d.ts.map +1 -1
  47. package/dist/esm/safeService.js +66 -12
  48. package/dist/esm/safeService.js.map +1 -1
  49. 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,16 @@ 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.sentFrom !== undefined &&
80
+ !(0, viem_1.isAddress)(record.sentFrom, { strict: false })) ||
81
+ (record.executed !== undefined && record.executed !== true) ||
82
+ (record.executionHash !== undefined &&
83
+ (record.executed !== true || !(0, untrusted_js_1.isBytes32)(record.executionHash))))
70
84
  throw new Error("Invalid saved write recovery record. Keep this action pending.");
71
85
  return {
72
86
  version: 1,
@@ -78,6 +92,21 @@ function recordOf(value) {
78
92
  ...(record.nonce === undefined ? {} : { nonce: record.nonce }),
79
93
  ...(record.fromBlock === undefined ? {} : { fromBlock: record.fromBlock }),
80
94
  ...(record.sentNonce === undefined ? {} : { sentNonce: record.sentNonce }),
95
+ ...(record.submittedAt === undefined
96
+ ? {}
97
+ : { submittedAt: record.submittedAt }),
98
+ ...(record.firstSeenAt === undefined
99
+ ? {}
100
+ : { firstSeenAt: record.firstSeenAt }),
101
+ ...(record.safeNonce === undefined ? {} : { safeNonce: record.safeNonce }),
102
+ ...(record.sentFrom === undefined
103
+ ? {}
104
+ : { sentFrom: record.sentFrom.toLowerCase() }),
105
+ ...(record.replaced ? { replaced: true } : {}),
106
+ ...(record.executed ? { executed: true } : {}),
107
+ ...(record.executionHash === undefined
108
+ ? {}
109
+ : { executionHash: record.executionHash.toLowerCase() }),
81
110
  };
82
111
  }
83
112
  function optionalDecimal(value) {
@@ -117,6 +146,136 @@ function recoveryStorage(store) {
117
146
  throw new Error("Write recovery storage is unavailable.");
118
147
  return result;
119
148
  }
149
+ const PROBE_KEY = "nana-sdk:reviewed-write:probe";
150
+ function idOf(raw) {
151
+ try {
152
+ const id = JSON.parse(raw)?.id;
153
+ return typeof id === "string" ? id : undefined;
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
159
+ /**
160
+ * The browser's storage, shared by its tabs and kept across reloads, while it
161
+ * can be written; this tab's memory while it can't (private browsing, a
162
+ * sandboxed frame, some wallet webviews), so a write is held either way and
163
+ * never refused. Every read looks at both. A record kept in memory moves into
164
+ * the browser's storage as soon as it can be written there, after any other
165
+ * live record saved at the same key ends. While the browser's storage can't
166
+ * be read, what this tab last read or wrote there still holds.
167
+ *
168
+ * `memory` defaults to one map per tab, shared by every copy of this module
169
+ * loaded there.
170
+ */
171
+ function createHeldWriteStorage(options = {}) {
172
+ const durable = options.durable ??
173
+ (() => globalThis.localStorage);
174
+ const tab = globalThis;
175
+ const memory = options.memory ?? (tab[TAB_MEMORY] ??= new Map());
176
+ /**
177
+ * What this tab last saw in, or wrote to, durable storage (records,
178
+ * releases and deletions), shared by every controller and copy of this
179
+ * module using the same tab memory.
180
+ */
181
+ const registry = (globalThis[TAB_SEEN] ??= new WeakMap());
182
+ let seen = registry.get(memory);
183
+ if (!seen)
184
+ registry.set(memory, (seen = new Map()));
185
+ const read = (key) => {
186
+ try {
187
+ const store = durable();
188
+ if (!store)
189
+ return undefined;
190
+ const value = store.getItem(key);
191
+ if (value === null)
192
+ seen.delete(key);
193
+ else
194
+ seen.set(key, value);
195
+ return value;
196
+ }
197
+ catch {
198
+ return undefined;
199
+ }
200
+ };
201
+ const write = (key, value) => {
202
+ try {
203
+ const store = durable();
204
+ if (!store)
205
+ return false;
206
+ store.setItem(key, value);
207
+ if (store.getItem(key) !== value)
208
+ return false;
209
+ seen.set(key, value);
210
+ return true;
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ };
216
+ const getItem = (key) => {
217
+ const saved = read(key);
218
+ const kept = memory.get(key);
219
+ if (saved === undefined)
220
+ return kept ?? seen.get(key) ?? null;
221
+ if (kept === undefined)
222
+ return saved;
223
+ if (saved === kept) {
224
+ memory.delete(key);
225
+ return saved;
226
+ }
227
+ const savedId = saved === null ? undefined : idOf(saved);
228
+ // Another live record saved there first is held first; this one waits.
229
+ if (saved !== null &&
230
+ savedId !== idOf(kept) &&
231
+ !(savedId !== undefined && getItem(releasedKey(key, savedId)) !== null))
232
+ return saved;
233
+ if (write(key, kept))
234
+ memory.delete(key);
235
+ return kept;
236
+ };
237
+ return {
238
+ getItem,
239
+ setItem(key, value) {
240
+ const kept = memory.get(key);
241
+ if (write(key, value)) {
242
+ if (kept === value || (kept && idOf(kept) === idOf(value)))
243
+ memory.delete(key);
244
+ }
245
+ else
246
+ memory.set(key, value);
247
+ },
248
+ removeItem(key) {
249
+ const shown = getItem(key);
250
+ if (shown !== null && memory.get(key) === shown) {
251
+ memory.delete(key);
252
+ if (read(key) !== shown)
253
+ return;
254
+ }
255
+ try {
256
+ durable()?.removeItem(key);
257
+ }
258
+ catch {
259
+ /* The readback below decides. */
260
+ }
261
+ if (read(key) === null)
262
+ seen.delete(key);
263
+ },
264
+ durable() {
265
+ const probe = String(Date.now());
266
+ if (!write(PROBE_KEY, probe))
267
+ return false;
268
+ try {
269
+ durable().removeItem(PROBE_KEY);
270
+ }
271
+ catch {
272
+ /* A leftover probe names no write. */
273
+ }
274
+ seen.delete(PROBE_KEY);
275
+ return true;
276
+ },
277
+ };
278
+ }
120
279
  /**
121
280
  * Open the original namespaced action before fresh quotation or phase checks.
122
281
  * No call is invented to locate evidence. Legacy actions retain factory.read().
@@ -152,8 +311,8 @@ function readRecovery(store, key, raw = store.getItem(key), scopeOf = scope) {
152
311
  const memory = unpersistedSubmissions.get(store)?.get(key);
153
312
  if (!record ||
154
313
  !memory ||
155
- JSON.stringify({ ...record, hash: undefined }) !==
156
- JSON.stringify({ ...memory, hash: undefined }) ||
314
+ JSON.stringify({ ...record, hash: undefined, submittedAt: undefined }) !==
315
+ JSON.stringify({ ...memory, hash: undefined, submittedAt: undefined }) ||
157
316
  (record.hash && record.hash !== memory.hash)) {
158
317
  // A readable durable snapshot also proves whether cached evidence is stale.
159
318
  unpersistedSubmissions.get(store)?.delete(key);
@@ -167,8 +326,9 @@ function readRecovery(store, key, raw = store.getItem(key), scopeOf = scope) {
167
326
  function sameReviewedWrite(record, input) {
168
327
  return (JSON.stringify(normalized(record)) === JSON.stringify(normalized(input)));
169
328
  }
170
- // Without Web Locks, writes in this tab still run one at a time per action.
171
- const tabQueues = new Map();
329
+ // Without Web Locks, writes in this tab still run one at a time per action,
330
+ // across every copy of this module loaded in the tab.
331
+ const tabQueues = (globalThis[TAB_QUEUES] ??= new Map());
172
332
  let tabLocksNoted = false;
173
333
  const tabLocks = {
174
334
  async request(name, options, work) {
@@ -397,6 +557,23 @@ function createBrowserWriteRecovery(input, options = {}) {
397
557
  throw new Error("Write recovery storage did not clear the action.");
398
558
  unpersistedSubmissions.get(store)?.delete(at);
399
559
  }
560
+ /** Add `fields` to exactly `expected`, a hashed record `when` allows; extra evidence only. */
561
+ function amend(expected, when, fields) {
562
+ requireLock();
563
+ const current = read();
564
+ if (!current?.hash ||
565
+ !when(current) ||
566
+ JSON.stringify(current) !== JSON.stringify(recordOf(expected)))
567
+ return current ?? recordOf(expected);
568
+ const next = recordOf({ ...current, ...fields });
569
+ try {
570
+ persist(next, locate(storage()).at);
571
+ return next;
572
+ }
573
+ catch {
574
+ return current;
575
+ }
576
+ }
400
577
  function rejected() {
401
578
  requireLock();
402
579
  // A rejection callback from another attempt must never release this one.
@@ -439,7 +616,15 @@ function createBrowserWriteRecovery(input, options = {}) {
439
616
  (!replace && previous.hash && previous.hash !== submittedHash)) {
440
617
  throw new Error("Write recovery changed before submission was recorded.");
441
618
  }
442
- const submitted = { ...previous, hash: submittedHash };
619
+ // A hash the wallet returns now starts its clock; a speed-up found at its nonce keeps it.
620
+ const submittedAt = replace || previous.hash === submittedHash
621
+ ? previous.submittedAt
622
+ : String(Date.now());
623
+ const submitted = recordOf({
624
+ ...previous,
625
+ hash: submittedHash,
626
+ ...(submittedAt === undefined ? {} : { submittedAt }),
627
+ });
443
628
  let raw;
444
629
  let at = key;
445
630
  try {
@@ -453,8 +638,16 @@ function createBrowserWriteRecovery(input, options = {}) {
453
638
  // A record the user released keeps its marker: a late hash leaves it released.
454
639
  const current = readStored(raw, at, scopeAt(at));
455
640
  if (!current ||
456
- JSON.stringify({ ...current, hash: undefined }) !==
457
- JSON.stringify({ ...previous, hash: undefined }) ||
641
+ JSON.stringify({
642
+ ...current,
643
+ hash: undefined,
644
+ submittedAt: undefined,
645
+ }) !==
646
+ JSON.stringify({
647
+ ...previous,
648
+ hash: undefined,
649
+ submittedAt: undefined,
650
+ }) ||
458
651
  (!replace && current.hash && current.hash !== submittedHash)) {
459
652
  throw new Error("Write recovery changed before submission was recorded.");
460
653
  }
@@ -541,10 +734,83 @@ function createBrowserWriteRecovery(input, options = {}) {
541
734
  },
542
735
  rejected,
543
736
  clear,
737
+ /**
738
+ * Start the clock of a hash saved before `submittedAt` existed: save now
739
+ * as its `firstSeenAt`, once. Extra evidence: a stale snapshot or a failed
740
+ * save keeps the record as it was.
741
+ */
742
+ firstSeen(expected) {
743
+ return amend(expected, (current) => current.submittedAt === undefined &&
744
+ current.firstSeenAt === undefined, { firstSeenAt: String(Date.now()) });
745
+ },
746
+ /**
747
+ * Save who sent a Safe execution the wallet returned at once, and at which
748
+ * nonce, from its authenticated transaction, once: its canonical
749
+ * replacement can then be proven. Extra evidence only.
750
+ */
751
+ sentBy(expected, from, nonce) {
752
+ return amend(expected, (current) => current.safe && current.sentFrom === undefined, { sentFrom: from, sentNonce: nonce.toString() });
753
+ },
754
+ /**
755
+ * Drop the proof that `expected` ran, once its execution is gone from the
756
+ * chain: it is held again as an unproven write. Extra evidence only.
757
+ */
758
+ unexecuted(expected) {
759
+ return amend(expected, (current) => current.executed === true, {
760
+ executed: undefined,
761
+ executionHash: undefined,
762
+ });
763
+ },
764
+ /**
765
+ * Save that another Safe transaction used this Safe write's nonce. Any
766
+ * saved execution goes: the user's exit, not the chain, ends it.
767
+ */
768
+ replacedOnChain(expected) {
769
+ return amend(expected, (current) => current.safe && !current.replaced, {
770
+ executed: undefined,
771
+ executionHash: undefined,
772
+ replaced: true,
773
+ });
774
+ },
775
+ /**
776
+ * Save a Safe proposal's nonce from its authenticated record, once. Extra
777
+ * evidence: a stale snapshot or a failed save keeps the record as it was.
778
+ */
779
+ proposalNonce(expected, nonce) {
780
+ return amend(expected, (current) => current.safe && current.safeNonce === undefined, { safeNonce: nonce.toString() });
781
+ },
782
+ /**
783
+ * Save the chain's proof that `expected` ran, with the transaction it ran
784
+ * in when known. From then on the user's exit refuses it: only its result
785
+ * ({@link verifyReviewedWriteReceipt}) releases it.
786
+ */
787
+ executed(expected, executionHash) {
788
+ requireLock();
789
+ if (executionHash !== undefined && !(0, untrusted_js_1.isBytes32)(executionHash))
790
+ throw new Error("Invalid execution transaction identity.");
791
+ const store = storage();
792
+ const { at, raw } = locate(store);
793
+ const current = readRecovery(store, at, raw, scopeAt(at));
794
+ const wanted = recordOf(expected);
795
+ if (!current?.hash ||
796
+ current.id !== wanted.id ||
797
+ current.hash !== wanted.hash)
798
+ throw new Error("Write recovery changed. Keep the current action pending.");
799
+ const next = recordOf({
800
+ ...current,
801
+ executed: true,
802
+ ...((executionHash ?? current.executionHash)
803
+ ? { executionHash: executionHash ?? current.executionHash }
804
+ : {}),
805
+ });
806
+ persist(next, at);
807
+ unpersistedSubmissions.get(store)?.delete(at);
808
+ return next;
809
+ },
544
810
  /**
545
811
  * Check the saved write against the chain ({@link checkReviewedWrite}) and
546
- * apply the finding: `sent` records the hash to track, `ran` releases it.
547
- * Null when nothing is saved.
812
+ * apply the finding: `sent` records the hash to track. Nothing here
813
+ * releases a write; null when nothing is saved.
548
814
  */
549
815
  async resolve(client, checkOptions) {
550
816
  requireLock();
@@ -554,12 +820,8 @@ function createBrowserWriteRecovery(input, options = {}) {
554
820
  const { check, sentNonce } = await inspectWrite(client, saved, checkOptions?.effect);
555
821
  if (sentNonce !== undefined && saved.sentNonce !== sentNonce)
556
822
  saved = bind(saved, sentNonce);
557
- if (check.status === "sent") {
558
- if (check.hash !== saved.hash)
559
- record(check.hash, saved, true);
560
- }
561
- else if (check.status !== "pending")
562
- clear(saved);
823
+ if (check.status === "sent" && check.hash !== saved.hash)
824
+ record(check.hash, saved, true);
563
825
  return check;
564
826
  },
565
827
  /**
@@ -579,6 +841,8 @@ function createBrowserWriteRecovery(input, options = {}) {
579
841
  const durable = readStored(raw, at, scopeAt(at), store);
580
842
  if (!durable)
581
843
  return;
844
+ if (durable.executed)
845
+ throw new Error("This write ran onchain. Keep it held until its result is known.");
582
846
  const shown = readRecovery(store, at, raw, scopeAt(at));
583
847
  if (JSON.stringify(durable) !== wanted &&
584
848
  JSON.stringify(shown) !== wanted)
@@ -642,20 +906,29 @@ async function checkReviewedWrite(client, saved, { effect } = {}) {
642
906
  }
643
907
  async function inspectWrite(client, saved, effect) {
644
908
  const record = recordOf(saved);
909
+ // No proof, the app's effect read included, from another chain, and none
910
+ // here for a Safe write.
911
+ if (record.safe ||
912
+ (await client.getChainId().catch(() => null)) !== record.chainId)
913
+ return { check: { status: "pending" } };
914
+ const found = await inspectOrdinary(client, record);
915
+ if (found.check.status === "pending" &&
916
+ effect &&
917
+ (await effect(record.chainId).catch(() => false)))
918
+ return { ...found, check: { ...found.check, effect: true } };
919
+ return found;
920
+ }
921
+ async function inspectOrdinary(client, record) {
645
922
  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
923
  if (record.hash) {
654
924
  const sent = await client.getTransaction({ hash: record.hash }).then((tx) => tx, (error) => error?.name === "TransactionNotFoundError" ? null : undefined);
655
925
  if (sent === undefined)
656
926
  return pending;
657
927
  if (sent) {
658
- if (sent.from?.toLowerCase() !== record.account)
928
+ // Only the saved hash's own transaction, on the saved chain, is evidence.
929
+ if (sent.from?.toLowerCase() !== record.account ||
930
+ sent.hash?.toLowerCase() !== record.hash ||
931
+ (sent.chainId !== undefined && sent.chainId !== record.chainId))
659
932
  return pending;
660
933
  return {
661
934
  check: sent.blockNumber !== null && sent.blockNumber !== undefined
@@ -667,15 +940,39 @@ async function inspectWrite(client, saved, effect) {
667
940
  }
668
941
  const bound = record.hash !== undefined && record.sentNonce !== undefined;
669
942
  const target = bound ? record.sentNonce : record.nonce;
670
- if (target === undefined || record.fromBlock === undefined)
943
+ // The nonce a hash was sent at is authenticated by its own transaction, so
944
+ // a record saved without a starting block searches from genesis instead.
945
+ const from = record.fromBlock ?? (bound ? "0" : undefined);
946
+ if (target === undefined || from === undefined)
671
947
  return pending;
672
- const nonce = BigInt(target);
673
- const count = async (blockNumber) => BigInt(await client.getTransactionCount({
674
- address: record.account,
675
- blockNumber,
676
- }));
948
+ const used = await findTransactionAtNonce(client, record.account, BigInt(target), BigInt(from));
949
+ if (!used)
950
+ return pending;
951
+ if (used.hash === record.hash)
952
+ return { check: { status: "sent", hash: used.hash } };
953
+ const sameCall = used.to?.toLowerCase() === record.call.to &&
954
+ used.input?.toLowerCase() === record.call.data &&
955
+ BigInt(used.value ?? 0) === BigInt(record.call.value);
956
+ // Another hash is adopted only at the nonce the saved hash's own
957
+ // transaction was sent at; the pre-wallet nonce only locates the call.
958
+ if (sameCall)
959
+ return bound ? { check: { status: "sent", hash: used.hash } } : pending;
960
+ // Another transaction at that nonce may have run the call inside it
961
+ // (a wrapper, or an EIP-7702 wallet that ran code and then cleared its
962
+ // delegation), so it proves nothing. The user's exit ends it.
963
+ return { check: { status: "pending", usedBy: used.hash } };
964
+ }
965
+ /**
966
+ * The transaction `account` sent at `nonce`, once the account's nonce is past
967
+ * it at a canonical finalized block: a search of its nonce from `fromBlock`
968
+ * finds the block where it moved. Null while that is unproven or the node
969
+ * can't say (no history, a reorganized answer).
970
+ */
971
+ async function findTransactionAtNonce(client, account, nonce, fromBlock = 0n) {
972
+ const from = account.toLowerCase();
973
+ const count = async (blockNumber) => BigInt(await client.getTransactionCount({ address: account, blockNumber }));
677
974
  const proof = await (0, relayr_js_1.atCanonicalFinalizedBlock)(client, async (finalized) => {
678
- let low = BigInt(record.fromBlock);
975
+ let low = fromBlock;
679
976
  let high = finalized;
680
977
  if (high <= low ||
681
978
  (await count(high)) <= nonce ||
@@ -693,24 +990,13 @@ async function inspectWrite(client, saved, effect) {
693
990
  includeTransactions: true,
694
991
  });
695
992
  const used = block.number === high
696
- ? block.transactions.find((tx) => tx.from?.toLowerCase() === record.account &&
697
- BigInt(tx.nonce) === nonce)
993
+ ? block.transactions.find((tx) => tx.from?.toLowerCase() === from && BigInt(tx.nonce) === nonce)
698
994
  : undefined;
699
995
  if (!used || !(0, untrusted_js_1.isBytes32)(used.hash))
700
996
  return null;
701
- const hash = used.hash.toLowerCase();
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;
997
+ return { ...used, hash: used.hash.toLowerCase() };
712
998
  });
713
- return { check: proof?.value ?? pending.check };
999
+ return proof?.value ?? null;
714
1000
  }
715
1001
  /**
716
1002
  * Authenticate the saved wallet identity, canonical inclusion and Safe effect.
@@ -767,7 +1053,16 @@ async function verifyReviewedWriteReceipt(client, saved, receipt, proof) {
767
1053
  (canonical.status !== "success" && canonical.status !== "reverted"))
768
1054
  throw pending();
769
1055
  let status = canonical.status === "success" ? "success" : "failed";
770
- if (record.safe) {
1056
+ // An execution the wallet returned at once whose own transaction reverted
1057
+ // ran nothing on the Safe: a failure, once final, when it was sent to it.
1058
+ const atOnceReverted = record.safe &&
1059
+ canonical.status === "reverted" &&
1060
+ hash.toLowerCase() === record.hash;
1061
+ if (atOnceReverted) {
1062
+ if (transaction.to?.toLowerCase() !== record.account)
1063
+ throw pending();
1064
+ }
1065
+ else if (record.safe) {
771
1066
  if (!Array.isArray(canonical.logs) ||
772
1067
  canonical.logs.some((log) => log.removed !== false ||
773
1068
  log.transactionHash?.toLowerCase() !== hash.toLowerCase() ||