@openzeppelin/miden-multisig-client 0.17.0-rc.2 → 0.17.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/README.md +172 -47
- package/dist/account/builder.d.ts +2 -2
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +49 -24
- package/dist/account/builder.js.map +1 -1
- package/dist/connectivity.d.ts +2 -0
- package/dist/connectivity.d.ts.map +1 -1
- package/dist/connectivity.js +7 -4
- package/dist/connectivity.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +59 -0
- package/dist/multisig/authArgErrors.d.ts.map +1 -0
- package/dist/multisig/authArgErrors.js +115 -0
- package/dist/multisig/authArgErrors.js.map +1 -0
- package/dist/multisig/helpers.d.ts +5 -0
- package/dist/multisig/helpers.d.ts.map +1 -1
- package/dist/multisig/helpers.js +7 -2
- package/dist/multisig/helpers.js.map +1 -1
- package/dist/multisig.d.ts +140 -0
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +419 -12
- package/dist/multisig.js.map +1 -1
- package/dist/procedures.d.ts +1 -1
- package/dist/procedures.js +1 -1
- package/dist/recovery/proposalNoteImport.d.ts +173 -0
- package/dist/recovery/proposalNoteImport.d.ts.map +1 -0
- package/dist/recovery/proposalNoteImport.js +411 -0
- package/dist/recovery/proposalNoteImport.js.map +1 -0
- package/dist/recovery/publicNoteBackfill.d.ts +168 -0
- package/dist/recovery/publicNoteBackfill.d.ts.map +1 -0
- package/dist/recovery/publicNoteBackfill.js +368 -0
- package/dist/recovery/publicNoteBackfill.js.map +1 -0
- package/dist/recovery/recoverNotes.d.ts +147 -0
- package/dist/recovery/recoverNotes.d.ts.map +1 -0
- package/dist/recovery/recoverNotes.js +163 -0
- package/dist/recovery/recoverNotes.js.map +1 -0
- package/dist/recovery/transportDrain.d.ts +124 -0
- package/dist/recovery/transportDrain.d.ts.map +1 -0
- package/dist/recovery/transportDrain.js +262 -0
- package/dist/recovery/transportDrain.js.map +1 -0
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +4 -1
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +4 -1
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/summary.d.ts +10 -4
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +13 -7
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.js +4 -1
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js +4 -1
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.js +4 -1
- package/dist/transaction/updateSigners.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/package.json +14 -10
- package/src/account/builder.ts +87 -35
- package/src/connectivity.ts +9 -4
- package/src/index.ts +34 -1
- package/src/multisig/authArgErrors.ts +138 -0
- package/src/multisig/helpers.ts +7 -2
- package/src/multisig.ts +513 -20
- package/src/procedures.ts +1 -1
- package/src/recovery/proposalNoteImport.ts +548 -0
- package/src/recovery/publicNoteBackfill.ts +502 -0
- package/src/recovery/recoverNotes.ts +288 -0
- package/src/recovery/transportDrain.ts +330 -0
- package/src/transaction/consumeNotes.ts +4 -1
- package/src/transaction/p2id.ts +4 -1
- package/src/transaction/summary.ts +13 -7
- package/src/transaction/updateGuardian.ts +4 -1
- package/src/transaction/updateProcedureThreshold.ts +4 -1
- package/src/transaction/updateSigners.ts +4 -1
- package/src/transaction.ts +1 -1
- package/dist/account/builder.test.d.ts +0 -2
- package/dist/account/builder.test.d.ts.map +0 -1
- package/dist/account/builder.test.js +0 -166
- package/dist/account/builder.test.js.map +0 -1
- package/dist/account/masm/account-components/auth.d.ts +0 -2
- package/dist/account/masm/account-components/auth.d.ts.map +0 -1
- package/dist/account/masm/account-components/auth.js +0 -46
- package/dist/account/masm/account-components/auth.js.map +0 -1
- package/dist/account/masm/index.d.ts +0 -2
- package/dist/account/masm/index.d.ts.map +0 -1
- package/dist/account/masm/index.js +0 -4
- package/dist/account/masm/index.js.map +0 -1
- package/dist/account/masm.d.ts +0 -2
- package/dist/account/masm.d.ts.map +0 -1
- package/dist/account/masm.js +0 -4
- package/dist/account/masm.js.map +0 -1
- package/dist/account/storage.test.d.ts +0 -2
- package/dist/account/storage.test.d.ts.map +0 -1
- package/dist/account/storage.test.js +0 -73
- package/dist/account/storage.test.js.map +0 -1
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.d.ts.map +0 -1
- package/dist/client.test.js +0 -380
- package/dist/client.test.js.map +0 -1
- package/dist/connectivity.test.d.ts +0 -2
- package/dist/connectivity.test.d.ts.map +0 -1
- package/dist/connectivity.test.js +0 -61
- package/dist/connectivity.test.js.map +0 -1
- package/dist/inspector.test.d.ts +0 -2
- package/dist/inspector.test.d.ts.map +0 -1
- package/dist/inspector.test.js +0 -425
- package/dist/inspector.test.js.map +0 -1
- package/dist/lookupAuth.test.d.ts +0 -2
- package/dist/lookupAuth.test.d.ts.map +0 -1
- package/dist/lookupAuth.test.js +0 -138
- package/dist/lookupAuth.test.js.map +0 -1
- package/dist/multisig/consumeNotesErrors.test.d.ts +0 -2
- package/dist/multisig/consumeNotesErrors.test.d.ts.map +0 -1
- package/dist/multisig/consumeNotesErrors.test.js +0 -28
- package/dist/multisig/consumeNotesErrors.test.js.map +0 -1
- package/dist/multisig/helpers.test.d.ts +0 -2
- package/dist/multisig/helpers.test.d.ts.map +0 -1
- package/dist/multisig/helpers.test.js +0 -94
- package/dist/multisig/helpers.test.js.map +0 -1
- package/dist/multisig.test.d.ts +0 -2
- package/dist/multisig.test.d.ts.map +0 -1
- package/dist/multisig.test.js +0 -4112
- package/dist/multisig.test.js.map +0 -1
- package/dist/proposal/factory.test.d.ts +0 -2
- package/dist/proposal/factory.test.d.ts.map +0 -1
- package/dist/proposal/factory.test.js +0 -32
- package/dist/proposal/factory.test.js.map +0 -1
- package/dist/proposal/metadata.test.d.ts +0 -2
- package/dist/proposal/metadata.test.d.ts.map +0 -1
- package/dist/proposal/metadata.test.js +0 -193
- package/dist/proposal/metadata.test.js.map +0 -1
- package/dist/prover/config.test.d.ts +0 -2
- package/dist/prover/config.test.d.ts.map +0 -1
- package/dist/prover/config.test.js +0 -54
- package/dist/prover/config.test.js.map +0 -1
- package/dist/prover/errors.test.d.ts +0 -2
- package/dist/prover/errors.test.d.ts.map +0 -1
- package/dist/prover/errors.test.js +0 -29
- package/dist/prover/errors.test.js.map +0 -1
- package/dist/prover/retry.test.d.ts +0 -2
- package/dist/prover/retry.test.d.ts.map +0 -1
- package/dist/prover/retry.test.js +0 -20
- package/dist/prover/retry.test.js.map +0 -1
- package/dist/prover/workflow.test.d.ts +0 -2
- package/dist/prover/workflow.test.d.ts.map +0 -1
- package/dist/prover/workflow.test.js +0 -105
- package/dist/prover/workflow.test.js.map +0 -1
- package/dist/raw-client.test.d.ts +0 -2
- package/dist/raw-client.test.d.ts.map +0 -1
- package/dist/raw-client.test.js +0 -111
- package/dist/raw-client.test.js.map +0 -1
- package/dist/rpc/config.test.d.ts +0 -2
- package/dist/rpc/config.test.d.ts.map +0 -1
- package/dist/rpc/config.test.js +0 -24
- package/dist/rpc/config.test.js.map +0 -1
- package/dist/rpc/errors.test.d.ts +0 -2
- package/dist/rpc/errors.test.d.ts.map +0 -1
- package/dist/rpc/errors.test.js +0 -34
- package/dist/rpc/errors.test.js.map +0 -1
- package/dist/rpc/retry.test.d.ts +0 -2
- package/dist/rpc/retry.test.d.ts.map +0 -1
- package/dist/rpc/retry.test.js +0 -98
- package/dist/rpc/retry.test.js.map +0 -1
- package/dist/signers/ecdsa.test.d.ts +0 -2
- package/dist/signers/ecdsa.test.d.ts.map +0 -1
- package/dist/signers/ecdsa.test.js +0 -88
- package/dist/signers/ecdsa.test.js.map +0 -1
- package/dist/signers/falcon.test.d.ts +0 -2
- package/dist/signers/falcon.test.d.ts.map +0 -1
- package/dist/signers/falcon.test.js +0 -161
- package/dist/signers/falcon.test.js.map +0 -1
- package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts +0 -2
- package/dist/signers/miden-wallet.ecdsa-recovery.test.d.ts.map +0 -1
- package/dist/signers/miden-wallet.ecdsa-recovery.test.js +0 -148
- package/dist/signers/miden-wallet.ecdsa-recovery.test.js.map +0 -1
- package/dist/signers/miden-wallet.test.d.ts +0 -2
- package/dist/signers/miden-wallet.test.d.ts.map +0 -1
- package/dist/signers/miden-wallet.test.js +0 -164
- package/dist/signers/miden-wallet.test.js.map +0 -1
- package/dist/signers/para.test.d.ts +0 -2
- package/dist/signers/para.test.d.ts.map +0 -1
- package/dist/signers/para.test.js +0 -146
- package/dist/signers/para.test.js.map +0 -1
- package/dist/transaction/index.d.ts +0 -8
- package/dist/transaction/index.d.ts.map +0 -1
- package/dist/transaction/index.js +0 -7
- package/dist/transaction/index.js.map +0 -1
- package/dist/transaction/p2id.test.d.ts +0 -2
- package/dist/transaction/p2id.test.d.ts.map +0 -1
- package/dist/transaction/p2id.test.js +0 -246
- package/dist/transaction/p2id.test.js.map +0 -1
- package/dist/transaction/rpoRandomCoin.test.d.ts +0 -2
- package/dist/transaction/rpoRandomCoin.test.d.ts.map +0 -1
- package/dist/transaction/rpoRandomCoin.test.js +0 -52
- package/dist/transaction/rpoRandomCoin.test.js.map +0 -1
- package/dist/transaction/summary.test.d.ts +0 -2
- package/dist/transaction/summary.test.d.ts.map +0 -1
- package/dist/transaction/summary.test.js +0 -26
- package/dist/transaction/summary.test.js.map +0 -1
- package/dist/transaction.test.d.ts +0 -2
- package/dist/transaction.test.d.ts.map +0 -1
- package/dist/transaction.test.js +0 -122
- package/dist/transaction.test.js.map +0 -1
- package/dist/types/proposal.test.d.ts +0 -2
- package/dist/types/proposal.test.d.ts.map +0 -1
- package/dist/types/proposal.test.js +0 -56
- package/dist/types/proposal.test.js.map +0 -1
- package/dist/utils/digest.test.d.ts +0 -2
- package/dist/utils/digest.test.d.ts.map +0 -1
- package/dist/utils/digest.test.js +0 -48
- package/dist/utils/digest.test.js.map +0 -1
- package/dist/utils/ecdsa.test.d.ts +0 -2
- package/dist/utils/ecdsa.test.d.ts.map +0 -1
- package/dist/utils/ecdsa.test.js +0 -131
- package/dist/utils/ecdsa.test.js.map +0 -1
- package/dist/utils/encoding.test.d.ts +0 -2
- package/dist/utils/encoding.test.d.ts.map +0 -1
- package/dist/utils/encoding.test.js +0 -185
- package/dist/utils/encoding.test.js.map +0 -1
- package/dist/utils/key.test.d.ts +0 -2
- package/dist/utils/key.test.d.ts.map +0 -1
- package/dist/utils/key.test.js +0 -82
- package/dist/utils/key.test.js.map +0 -1
- package/dist/utils/signature.test.d.ts +0 -2
- package/dist/utils/signature.test.d.ts.map +0 -1
- package/dist/utils/signature.test.js +0 -164
- package/dist/utils/signature.test.js.map +0 -1
- package/dist/utils/word.test.d.ts +0 -2
- package/dist/utils/word.test.d.ts.map +0 -1
- package/dist/utils/word.test.js +0 -54
- package/dist/utils/word.test.js.map +0 -1
- package/masm/account_components/auth/guarded_multisig.masm +0 -42
- package/src/account/builder.test.ts +0 -238
- package/src/account/masm/account-components/auth.ts +0 -46
- package/src/account/masm/index.ts +0 -4
- package/src/account/masm.ts +0 -4
- package/src/account/storage.test.ts +0 -90
- package/src/client.test.ts +0 -473
- package/src/connectivity.test.ts +0 -67
- package/src/inspector.test.ts +0 -542
- package/src/lookupAuth.test.ts +0 -185
- package/src/multisig/consumeNotesErrors.test.ts +0 -43
- package/src/multisig/helpers.test.ts +0 -114
- package/src/multisig.test.ts +0 -5008
- package/src/proposal/factory.test.ts +0 -40
- package/src/proposal/metadata.test.ts +0 -235
- package/src/prover/config.test.ts +0 -90
- package/src/prover/errors.test.ts +0 -61
- package/src/prover/retry.test.ts +0 -38
- package/src/prover/workflow.test.ts +0 -145
- package/src/raw-client.test.ts +0 -171
- package/src/rpc/config.test.ts +0 -45
- package/src/rpc/errors.test.ts +0 -69
- package/src/rpc/retry.test.ts +0 -144
- package/src/signers/ecdsa.test.ts +0 -111
- package/src/signers/falcon.test.ts +0 -197
- package/src/signers/miden-wallet.ecdsa-recovery.test.ts +0 -201
- package/src/signers/miden-wallet.test.ts +0 -206
- package/src/signers/para.test.ts +0 -186
- package/src/transaction/index.ts +0 -13
- package/src/transaction/p2id.test.ts +0 -371
- package/src/transaction/rpoRandomCoin.test.ts +0 -64
- package/src/transaction/summary.test.ts +0 -32
- package/src/transaction.test.ts +0 -142
- package/src/types/proposal.test.ts +0 -68
- package/src/utils/digest.test.ts +0 -55
- package/src/utils/ecdsa.test.ts +0 -164
- package/src/utils/encoding.test.ts +0 -233
- package/src/utils/key.test.ts +0 -91
- package/src/utils/signature.test.ts +0 -210
- package/src/utils/word.test.ts +0 -64
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historical public-note backfill by tag.
|
|
3
|
+
*
|
|
4
|
+
* Public notes addressed to an account are on chain, but normal forward sync
|
|
5
|
+
* starts from the store's **global** cursor: in a shared dirty store the
|
|
6
|
+
* cursor may already be past blocks containing a recovered account's notes,
|
|
7
|
+
* and a fresh store has no efficient path to them at all. This module
|
|
8
|
+
* rescans a historical block range with the account's standard note tag and
|
|
9
|
+
* imports what it finds with on-chain inclusion proofs, without ever
|
|
10
|
+
* touching the global sync height.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
AccountId,
|
|
15
|
+
type CommittedNote,
|
|
16
|
+
Endpoint,
|
|
17
|
+
type InputNoteRecord,
|
|
18
|
+
type Note,
|
|
19
|
+
type NoteInclusionProof,
|
|
20
|
+
NoteScript,
|
|
21
|
+
NoteTag,
|
|
22
|
+
NoteType,
|
|
23
|
+
RpcClient,
|
|
24
|
+
} from '@miden-sdk/miden-sdk';
|
|
25
|
+
|
|
26
|
+
import { errorMessage } from '../connectivity.js';
|
|
27
|
+
import {
|
|
28
|
+
collectExistingRecords,
|
|
29
|
+
detailsKeyOf,
|
|
30
|
+
errorDetail,
|
|
31
|
+
importNoteWithProof,
|
|
32
|
+
type NoteImportOutcome,
|
|
33
|
+
reclassifyConsumedImports,
|
|
34
|
+
} from './proposalNoteImport.js';
|
|
35
|
+
import { getRawMidenClient, requireMidenRpcEndpoint, type RawClientSource } from '../raw-client.js';
|
|
36
|
+
import { resolveRpcConfig, type RpcConfig } from '../rpc/config.js';
|
|
37
|
+
import { isTransientRpcError } from '../rpc/errors.js';
|
|
38
|
+
import { retryRpcRead } from '../rpc/retry.js';
|
|
39
|
+
import { normalizeHexWord } from '../utils/encoding.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `RpcError::PaginationError`'s Display prefix in the WASM error chain: the
|
|
43
|
+
* node caps internal pagination per `syncNotes` request, and the scan splits
|
|
44
|
+
* the range client-side when it trips. Exported for the drift-guard test,
|
|
45
|
+
* which pins it against the shipped WASM binary.
|
|
46
|
+
*/
|
|
47
|
+
export const RPC_PAGINATION_FRAGMENT = 'rpc pagination error';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Upper bound on `syncNotes` requests per backfill. Splitting around the
|
|
51
|
+
* node's pagination cap halves ranges, so the budget is only approachable
|
|
52
|
+
* when nearly every sub-range is dense enough to trip the cap; exhausting it
|
|
53
|
+
* reports the remaining ranges as uncovered instead of scanning forever.
|
|
54
|
+
*/
|
|
55
|
+
const MAX_SCAN_REQUESTS = 128;
|
|
56
|
+
|
|
57
|
+
/** Block numbers are u32 on chain; out-of-range JS numbers would silently
|
|
58
|
+
* wrap modulo 2^32 at the WASM boundary and scan the wrong range. */
|
|
59
|
+
const MAX_BLOCK_NUMBER = 4_294_967_295;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The requested scan range is invalid (out-of-range bound, or inverted
|
|
63
|
+
* against the resolved chain tip) — a caller error that retrying cannot fix,
|
|
64
|
+
* as opposed to the transient chain-tip-lookup failures this function also
|
|
65
|
+
* throws. `Multisig.recoverNotes` keys its problem retryability on this,
|
|
66
|
+
* mirroring the Rust orchestrator's `InvalidConfig` check.
|
|
67
|
+
*/
|
|
68
|
+
export class BackfillRangeError extends Error {}
|
|
69
|
+
|
|
70
|
+
function requireBlockNumber(name: string, value: number): void {
|
|
71
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_BLOCK_NUMBER) {
|
|
72
|
+
throw new BackfillRangeError(
|
|
73
|
+
`${name} must be an integer in [0, ${MAX_BLOCK_NUMBER}], got ${value}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Verdict of the static relevance screen. */
|
|
79
|
+
export type ScreenVerdict = 'relevant' | 'irrelevant' | 'unscreenable';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Static relevance screen for a discovered public note. The Rust SDK screens
|
|
83
|
+
* with the execution-based `NoteScreener` normal sync uses; the WASM surface
|
|
84
|
+
* does not expose it, so this mirrors its verdict for the well-known note
|
|
85
|
+
* scripts: a note is `relevant` when it is a P2ID/P2IDE note whose target
|
|
86
|
+
* (or P2IDE reclaimer) is the scanned account, and `irrelevant` when it is
|
|
87
|
+
* one of those scripts addressed at someone else. Notes with other scripts
|
|
88
|
+
* are `unscreenable` — this screen cannot judge them, and they are
|
|
89
|
+
* conservatively not imported (tags are shared, truncated filters, and
|
|
90
|
+
* importing unscreened tag matches would let anyone pollute the store), but
|
|
91
|
+
* the report counts them separately so "screened out" and "not screenable"
|
|
92
|
+
* stay distinguishable. Exported for the drift-guard test, which pins the
|
|
93
|
+
* root and storage-layout assumptions against real WASM-built notes.
|
|
94
|
+
*/
|
|
95
|
+
export function screenNoteForAccount(note: Note, account: AccountId): ScreenVerdict {
|
|
96
|
+
const root = normalizeHexWord(note.script().root().toHex());
|
|
97
|
+
const items = note.recipient().storage().items();
|
|
98
|
+
const prefix = account.prefix().asInt();
|
|
99
|
+
const suffix = account.suffix().asInt();
|
|
100
|
+
const accountAt = (index: number): boolean =>
|
|
101
|
+
items.length > index + 1 &&
|
|
102
|
+
items[index].asInt() === suffix &&
|
|
103
|
+
items[index + 1].asInt() === prefix;
|
|
104
|
+
if (root === normalizeHexWord(NoteScript.p2id().root().toHex())) {
|
|
105
|
+
// P2ID note storage: [target.suffix, target.prefix].
|
|
106
|
+
return accountAt(0) ? 'relevant' : 'irrelevant';
|
|
107
|
+
}
|
|
108
|
+
if (root === normalizeHexWord(NoteScript.p2ide().root().toHex())) {
|
|
109
|
+
// P2IDE note storage: [reclaimer.suffix, reclaimer.prefix,
|
|
110
|
+
// target.suffix, target.prefix, reclaim, timelock].
|
|
111
|
+
return accountAt(2) || accountAt(0) ? 'relevant' : 'irrelevant';
|
|
112
|
+
}
|
|
113
|
+
return 'unscreenable';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A contiguous block range, inclusive on both ends. */
|
|
117
|
+
export interface BlockRange {
|
|
118
|
+
/** First block of the range. */
|
|
119
|
+
from: number;
|
|
120
|
+
/** Last block of the range. */
|
|
121
|
+
to: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Result of {@link backfillPublicNotesByTag}.
|
|
126
|
+
*
|
|
127
|
+
* Scan problems are reported here rather than thrown so a partially failing
|
|
128
|
+
* scan never aborts the rest of a recovery flow: notes discovered in the
|
|
129
|
+
* covered ranges are imported regardless.
|
|
130
|
+
*/
|
|
131
|
+
export interface PublicBackfillReport {
|
|
132
|
+
/** First block of the requested scan range. */
|
|
133
|
+
scannedFrom: number;
|
|
134
|
+
/** Last block of the requested scan range. */
|
|
135
|
+
scannedTo: number;
|
|
136
|
+
/** Unique tag-matching notes the scan discovered, of every visibility. */
|
|
137
|
+
discovered: number;
|
|
138
|
+
/** Unique non-public matches skipped: the chain does not hold their
|
|
139
|
+
* bodies, so they cannot be rebuilt from a scan. Private notes are covered
|
|
140
|
+
* by the transport drain and proposal-import primitives instead. */
|
|
141
|
+
skippedPrivate: number;
|
|
142
|
+
/** Unique public matches the relevance screen rejected: tags are
|
|
143
|
+
* best-effort, truncated filters, so unrelated notes can carry this
|
|
144
|
+
* account's tag. Like normal sync, only notes the account could actually
|
|
145
|
+
* consume are imported; the rest are counted here. (This SDK screens
|
|
146
|
+
* statically against the well-known P2ID/P2IDE scripts; the Rust SDK uses
|
|
147
|
+
* the execution-based screener.) */
|
|
148
|
+
skippedIrrelevant: number;
|
|
149
|
+
/** Unique public matches this SDK's static screen could not judge (custom
|
|
150
|
+
* note scripts). They are conservatively not imported, but counted apart
|
|
151
|
+
* from `skippedIrrelevant` so callers can tell "screened out" from "not
|
|
152
|
+
* screenable". Always `0` in the Rust SDK, whose execution-based screener
|
|
153
|
+
* judges every note. */
|
|
154
|
+
skippedUnscreenable: number;
|
|
155
|
+
/** One outcome per unique public note that passed the relevance screen —
|
|
156
|
+
* `outcomes.length === discovered - skippedPrivate - skippedIrrelevant -
|
|
157
|
+
* skippedUnscreenable`. Screened-out, unscreenable, and private matches
|
|
158
|
+
* get no outcome, only their counters. */
|
|
159
|
+
outcomes: NoteImportOutcome[];
|
|
160
|
+
/** Sub-ranges of `[scannedFrom, scannedTo]` the scan could not cover (RPC
|
|
161
|
+
* failures, or the scan budget ran out while splitting around the node's
|
|
162
|
+
* pagination cap). Empty when the whole range was scanned. Notes committed
|
|
163
|
+
* in these ranges may be missing from `outcomes`. */
|
|
164
|
+
uncovered: BlockRange[];
|
|
165
|
+
/** Whether rerunning the backfill can plausibly improve the result: cover
|
|
166
|
+
* `uncovered` ranges, or retry outcomes whose own `retryable` flag is set.
|
|
167
|
+
* Always `false` when the scan fully covered the range and no outcome is
|
|
168
|
+
* retryable. */
|
|
169
|
+
retryable: boolean;
|
|
170
|
+
/** Human-readable cause when the scan did not cover the whole range. */
|
|
171
|
+
reason?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface BackfillPublicNotesOptions {
|
|
175
|
+
/** Hex ID of the account whose standard note tag should be scanned. */
|
|
176
|
+
accountId: string;
|
|
177
|
+
/** Miden node RPC endpoint used for the scan and body fetches. Must point
|
|
178
|
+
* at the same network as the injected Miden client. */
|
|
179
|
+
midenRpcEndpoint: string;
|
|
180
|
+
/** First block of the scan range (default: genesis). */
|
|
181
|
+
fromBlock?: number;
|
|
182
|
+
/** Last block of the scan range (default: the current chain tip). */
|
|
183
|
+
toBlock?: number;
|
|
184
|
+
/** Node RPC read-retry configuration (defaults match the rest of the SDK). */
|
|
185
|
+
rpc?: RpcConfig;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Scans a historical block range for public notes addressed at an account's
|
|
190
|
+
* standard note tag and imports what it finds with their on-chain inclusion
|
|
191
|
+
* proofs. Counterpart of
|
|
192
|
+
* `MultisigClient::backfill_public_notes_by_tag` in the Rust SDK.
|
|
193
|
+
*
|
|
194
|
+
* Use after account recovery: normal forward sync starts from the store's
|
|
195
|
+
* **global** cursor, so in a shared dirty store the cursor may already be
|
|
196
|
+
* past blocks containing the recovered account's notes, and a fresh store
|
|
197
|
+
* would need to replay the whole chain state to see them. The scan is
|
|
198
|
+
* tag-scoped and its cost grows with the number of matching notes, not the
|
|
199
|
+
* range length, which makes genesis an acceptable default lower
|
|
200
|
+
* bound. The global sync height is never touched — run normal sync
|
|
201
|
+
* afterwards to verify the imported notes. The store must have synced at
|
|
202
|
+
* least once (`Multisig.recoverNotes` syncs the chain before this strategy
|
|
203
|
+
* runs): importing a proof into a store that has never seen the chain
|
|
204
|
+
* fails, and such failures surface as `failed` outcomes.
|
|
205
|
+
*
|
|
206
|
+
* Notes are discovered by tag only — a best-effort filter: notes sent with
|
|
207
|
+
* unrelated custom tags are outside this scan's guarantee, and, like normal
|
|
208
|
+
* sync, every new discovery is screened for relevance before import —
|
|
209
|
+
* tag-colliding notes the account cannot consume are counted as
|
|
210
|
+
* `skippedIrrelevant` instead of polluting the store. This SDK screens
|
|
211
|
+
* statically against the well-known P2ID/P2IDE scripts (the WASM surface
|
|
212
|
+
* does not expose the execution-based screener the Rust SDK uses), so notes
|
|
213
|
+
* with custom scripts are conservatively not imported and counted as
|
|
214
|
+
* `skippedUnscreenable`. Only public notes can be
|
|
215
|
+
* rebuilt from chain data; private matches are counted as `skippedPrivate`
|
|
216
|
+
* and are covered by the transport drain and proposal-import primitives
|
|
217
|
+
* instead.
|
|
218
|
+
*
|
|
219
|
+
* A range dense enough to trip the node's internal pagination cap is split
|
|
220
|
+
* client-side and rescanned as narrower requests; ranges that still cannot
|
|
221
|
+
* be covered are reported in {@link PublicBackfillReport.uncovered} rather
|
|
222
|
+
* than failing the recovery flow. This function throws only when the scan
|
|
223
|
+
* range itself cannot be established (chain-tip lookup failed, an invalid
|
|
224
|
+
* account ID, a block bound that is not a u32 integer, or
|
|
225
|
+
* `fromBlock > toBlock`).
|
|
226
|
+
*
|
|
227
|
+
* Prefer the `Multisig.backfillPublicNotesByTag` convenience method, which
|
|
228
|
+
* reuses the client's endpoint and retry configuration.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const report = await multisig.backfillPublicNotesByTag();
|
|
233
|
+
* console.log(report.discovered, 'discovered,', report.outcomes.length, 'public');
|
|
234
|
+
* await multisig.syncState(); // verifies the imported notes
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export async function backfillPublicNotesByTag(
|
|
238
|
+
midenClient: RawClientSource,
|
|
239
|
+
options: BackfillPublicNotesOptions,
|
|
240
|
+
): Promise<PublicBackfillReport> {
|
|
241
|
+
const midenRpcEndpoint = requireMidenRpcEndpoint(options.midenRpcEndpoint);
|
|
242
|
+
const rpcConfig = resolveRpcConfig(options.rpc);
|
|
243
|
+
const webClient = await getRawMidenClient(midenClient, midenRpcEndpoint);
|
|
244
|
+
const rpcClient = new RpcClient(new Endpoint(midenRpcEndpoint));
|
|
245
|
+
// Parse eagerly so a malformed account ID throws before any network work.
|
|
246
|
+
AccountId.fromHex(options.accountId);
|
|
247
|
+
|
|
248
|
+
const from = options.fromBlock ?? 0;
|
|
249
|
+
requireBlockNumber('fromBlock', from);
|
|
250
|
+
let to: number;
|
|
251
|
+
if (options.toBlock !== undefined) {
|
|
252
|
+
requireBlockNumber('toBlock', options.toBlock);
|
|
253
|
+
to = options.toBlock;
|
|
254
|
+
} else {
|
|
255
|
+
try {
|
|
256
|
+
const tip = await retryRpcRead(() => rpcClient.getBlockHeaderByNumber(), rpcConfig);
|
|
257
|
+
to = tip.blockNum();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`failed to resolve the chain tip for the backfill scan: ${errorDetail(error)}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (from > to) {
|
|
265
|
+
throw new BackfillRangeError(`backfill range is inverted: fromBlock ${from} > toBlock ${to}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Work queue of inclusive sub-ranges, split in half whenever the node
|
|
269
|
+
// reports its pagination cap for one of them. WASM call arguments are
|
|
270
|
+
// consumed by the bridge, so the tag is rebuilt per request.
|
|
271
|
+
const scanTag = (): NoteTag => NoteTag.withAccountTarget(AccountId.fromHex(options.accountId));
|
|
272
|
+
const queue: Array<[number, number]> = [[from, to]];
|
|
273
|
+
const discovered = new Map<string, CommittedNote>();
|
|
274
|
+
const uncovered: BlockRange[] = [];
|
|
275
|
+
const scanReasons: string[] = [];
|
|
276
|
+
let retryable = false;
|
|
277
|
+
let requests = 0;
|
|
278
|
+
let budgetExhausted = false;
|
|
279
|
+
|
|
280
|
+
while (queue.length > 0) {
|
|
281
|
+
const [lo, hi] = queue.shift() as [number, number];
|
|
282
|
+
if (requests >= MAX_SCAN_REQUESTS) {
|
|
283
|
+
budgetExhausted = true;
|
|
284
|
+
uncovered.push({ from: lo, to: hi });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
requests += 1;
|
|
288
|
+
try {
|
|
289
|
+
const info = await retryRpcRead(() => rpcClient.syncNotes(lo, hi, [scanTag()]), rpcConfig);
|
|
290
|
+
for (const committed of info.notes()) {
|
|
291
|
+
const idHex = normalizeHexWord(committed.noteId().toString());
|
|
292
|
+
if (!discovered.has(idHex)) {
|
|
293
|
+
// The wrapper is kept (not a one-shot accessor result) so fresh
|
|
294
|
+
// NoteId handles can be minted per body-fetch attempt below.
|
|
295
|
+
discovered.set(idHex, committed);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// The node caps internal pagination per request rather than
|
|
300
|
+
// truncating; a single-block range cannot be split further (and
|
|
301
|
+
// cannot realistically hold that many pages), so only splittable
|
|
302
|
+
// ranges take this branch.
|
|
303
|
+
if (errorMessage(error).toLowerCase().includes(RPC_PAGINATION_FRAGMENT) && lo < hi) {
|
|
304
|
+
const mid = lo + Math.floor((hi - lo) / 2);
|
|
305
|
+
queue.unshift([lo, mid], [mid + 1, hi]);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
retryable ||= isTransientRpcError(error);
|
|
309
|
+
scanReasons.push(`blocks [${lo}, ${hi}]: ${errorDetail(error)}`);
|
|
310
|
+
uncovered.push({ from: lo, to: hi });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (budgetExhausted) {
|
|
314
|
+
retryable = true;
|
|
315
|
+
scanReasons.push(
|
|
316
|
+
`scan budget of ${MAX_SCAN_REQUESTS} requests exhausted while splitting around the node's pagination cap; rerun the backfill over the uncovered ranges`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const publicNotes: Array<{ idHex: string; committed: CommittedNote }> = [];
|
|
321
|
+
for (const [idHex, committed] of discovered) {
|
|
322
|
+
if (committed.noteType() === NoteType.Public) {
|
|
323
|
+
publicNotes.push({ idHex, committed });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const skippedPrivate = discovered.size - publicNotes.length;
|
|
327
|
+
let skippedIrrelevant = 0;
|
|
328
|
+
let skippedUnscreenable = 0;
|
|
329
|
+
|
|
330
|
+
const outcomes: NoteImportOutcome[] = [];
|
|
331
|
+
const buildReport = (): PublicBackfillReport => {
|
|
332
|
+
let reason: string | undefined;
|
|
333
|
+
if (scanReasons.length > 0) {
|
|
334
|
+
reason =
|
|
335
|
+
scanReasons.length <= 3
|
|
336
|
+
? scanReasons.join('; ')
|
|
337
|
+
: `${scanReasons.slice(0, 3).join('; ')}; …and ${scanReasons.length - 3} more`;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
scannedFrom: from,
|
|
341
|
+
scannedTo: to,
|
|
342
|
+
discovered: discovered.size,
|
|
343
|
+
skippedPrivate,
|
|
344
|
+
skippedIrrelevant,
|
|
345
|
+
skippedUnscreenable,
|
|
346
|
+
outcomes,
|
|
347
|
+
uncovered,
|
|
348
|
+
// Rerunning can help when scan ranges were left uncovered OR when any
|
|
349
|
+
// per-note outcome is itself retryable — surface both at report level
|
|
350
|
+
// so orchestration keyed on the report alone reruns when it should.
|
|
351
|
+
retryable: retryable || outcomes.some((outcome) => outcome.retryable === true),
|
|
352
|
+
...(reason === undefined ? {} : { reason }),
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
interface BackfillCandidate {
|
|
357
|
+
idHex: string;
|
|
358
|
+
note: Note;
|
|
359
|
+
proof: NoteInclusionProof;
|
|
360
|
+
detailsKey: string;
|
|
361
|
+
}
|
|
362
|
+
const pending: BackfillCandidate[] = [];
|
|
363
|
+
if (publicNotes.length > 0) {
|
|
364
|
+
try {
|
|
365
|
+
// One batched body fetch — the upstream client chunks internally by
|
|
366
|
+
// the node's negotiated note-ids limit, and the node returns full
|
|
367
|
+
// bodies for public notes, so the scan's ID + proof is all this path
|
|
368
|
+
// needs. The WASM bridge consumes call arguments, so fresh NoteId
|
|
369
|
+
// handles are minted from the kept wrappers on every retry attempt.
|
|
370
|
+
const fetchedNotes = await retryRpcRead(
|
|
371
|
+
() =>
|
|
372
|
+
rpcClient.getNotesById(publicNotes.map((candidate) => candidate.committed.noteId())),
|
|
373
|
+
rpcConfig,
|
|
374
|
+
);
|
|
375
|
+
const bodies = new Map<string, { note: Note; proof: NoteInclusionProof }>();
|
|
376
|
+
for (const fetched of fetchedNotes) {
|
|
377
|
+
if (fetched.note) {
|
|
378
|
+
bodies.set(normalizeHexWord(fetched.noteId.toString()), {
|
|
379
|
+
note: fetched.note,
|
|
380
|
+
proof: fetched.inclusionProof,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const { idHex } of publicNotes) {
|
|
385
|
+
const body = bodies.get(idHex);
|
|
386
|
+
if (body) {
|
|
387
|
+
pending.push({
|
|
388
|
+
idHex,
|
|
389
|
+
note: body.note,
|
|
390
|
+
proof: body.proof,
|
|
391
|
+
detailsKey: detailsKeyOf(
|
|
392
|
+
normalizeHexWord(body.note.recipient().digest().toHex()),
|
|
393
|
+
body.note.assets(),
|
|
394
|
+
),
|
|
395
|
+
});
|
|
396
|
+
} else {
|
|
397
|
+
// Discovered as public by the scan but returned without a body —
|
|
398
|
+
// not expected for a committed public note.
|
|
399
|
+
outcomes.push({
|
|
400
|
+
identifier: idHex,
|
|
401
|
+
source: 'backfill',
|
|
402
|
+
status: 'failed',
|
|
403
|
+
retryable: true,
|
|
404
|
+
reason: 'the node did not return a body for this public note',
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
const fetchRetryable = isTransientRpcError(error);
|
|
410
|
+
const reason = `failed to fetch note bodies: ${errorDetail(error)}`;
|
|
411
|
+
for (const { idHex } of publicNotes) {
|
|
412
|
+
outcomes.push({
|
|
413
|
+
identifier: idHex,
|
|
414
|
+
source: 'backfill',
|
|
415
|
+
status: 'failed',
|
|
416
|
+
retryable: fetchRetryable,
|
|
417
|
+
reason,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (pending.length === 0) {
|
|
424
|
+
return buildReport();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let existing: Map<string, InputNoteRecord>;
|
|
428
|
+
try {
|
|
429
|
+
existing = await collectExistingRecords(webClient);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const reason = `failed to read local store: ${errorDetail(error)}`;
|
|
432
|
+
for (const candidate of pending) {
|
|
433
|
+
outcomes.push({
|
|
434
|
+
identifier: candidate.idHex,
|
|
435
|
+
source: 'backfill',
|
|
436
|
+
status: 'failed',
|
|
437
|
+
reason,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return buildReport();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Provisionally `imported` outcomes, re-classified in one batched
|
|
444
|
+
// post-import state check below.
|
|
445
|
+
const screenAccount = AccountId.fromHex(options.accountId);
|
|
446
|
+
const imported: Array<{ index: number; idHex: string; detailsKey: string }> = [];
|
|
447
|
+
for (const candidate of pending) {
|
|
448
|
+
// Skip decisions key on an exact note-ID match only: a details-key
|
|
449
|
+
// match is a lossy approximation (see {@link detailsKeyOf} in the
|
|
450
|
+
// proposal-import module), and the upstream import dedupes exactly by
|
|
451
|
+
// the real details commitment, so importing "again" is safe while
|
|
452
|
+
// pre-skipping on the approximation could silently drop a genuinely
|
|
453
|
+
// new note. Unlike the proposal import, a proof-less (expected) record
|
|
454
|
+
// is NOT skipped here even on an ID match: this primitive exists
|
|
455
|
+
// because forward sync will never revisit the note's block, so the
|
|
456
|
+
// freshly fetched proof is applied to upgrade the record in place (the
|
|
457
|
+
// WASM import handles existing records).
|
|
458
|
+
const record = existing.get(candidate.idHex);
|
|
459
|
+
if (record && (record.isConsumed() || record.inclusionProof() !== undefined)) {
|
|
460
|
+
outcomes.push({
|
|
461
|
+
identifier: candidate.idHex,
|
|
462
|
+
source: 'backfill',
|
|
463
|
+
status: record.isConsumed() ? 'already-consumed' : 'already-present',
|
|
464
|
+
});
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
// Screen genuinely new discoveries for relevance, exactly like normal
|
|
468
|
+
// sync does before it stores a tag match. Records the store already
|
|
469
|
+
// tracks (by ID, or a metadata-less record matching on details) are
|
|
470
|
+
// material the user chose to track and skip the screen.
|
|
471
|
+
if (!record && !existing.has(candidate.detailsKey)) {
|
|
472
|
+
const verdict = screenNoteForAccount(candidate.note, screenAccount);
|
|
473
|
+
if (verdict === 'irrelevant') {
|
|
474
|
+
skippedIrrelevant += 1;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (verdict === 'unscreenable') {
|
|
478
|
+
skippedUnscreenable += 1;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const { outcome, wasImported } = await importNoteWithProof(
|
|
483
|
+
webClient,
|
|
484
|
+
'backfill',
|
|
485
|
+
candidate.idHex,
|
|
486
|
+
candidate.note,
|
|
487
|
+
candidate.proof,
|
|
488
|
+
);
|
|
489
|
+
if (wasImported) {
|
|
490
|
+
imported.push({
|
|
491
|
+
index: outcomes.length,
|
|
492
|
+
idHex: candidate.idHex,
|
|
493
|
+
detailsKey: candidate.detailsKey,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
outcomes.push(outcome);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await reclassifyConsumedImports(webClient, imported, outcomes);
|
|
500
|
+
|
|
501
|
+
return buildReport();
|
|
502
|
+
}
|