@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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet-facing orchestration of the note-recovery primitives.
|
|
3
|
+
*
|
|
4
|
+
* After key-based recovery the local Miden store starts empty (or, in a
|
|
5
|
+
* shared store, its global cursors may already be past this account's
|
|
6
|
+
* notes). Each recovery primitive rescans one source normal forward sync
|
|
7
|
+
* would skip: the private-note transport backlog, the notes embedded in
|
|
8
|
+
* pending consume-notes proposals, and the chain's historical public notes
|
|
9
|
+
* for the account's tag. `Multisig.recoverNotes` runs them as a single flow
|
|
10
|
+
* and finishes with a normal sync so recovered notes are verified and ready
|
|
11
|
+
* to consume.
|
|
12
|
+
*
|
|
13
|
+
* The TS counterpart of the Rust SDK's `MultisigClient::recover_notes`,
|
|
14
|
+
* with matching semantics and report shape.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { errorMessage } from '../connectivity.js';
|
|
18
|
+
import { RecoveryCancelledError } from './proposalNoteImport.js';
|
|
19
|
+
import type { NoteImportOutcome } from './proposalNoteImport.js';
|
|
20
|
+
import { BackfillRangeError, type PublicBackfillReport } from './publicNoteBackfill.js';
|
|
21
|
+
import type { TransportRecoveryReport } from './transportDrain.js';
|
|
22
|
+
|
|
23
|
+
/** One step of the note-recovery flow, used to attribute problems. */
|
|
24
|
+
export type RecoveryStep =
|
|
25
|
+
| 'transport-drain'
|
|
26
|
+
| 'proposal-import'
|
|
27
|
+
| 'public-backfill'
|
|
28
|
+
| 'sync';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A step of the recovery flow that could not run (or, for the final sync,
|
|
32
|
+
* did not finish). The flow always continues with the remaining steps; a
|
|
33
|
+
* problem here means the corresponding report field is absent (or `synced`
|
|
34
|
+
* is `false`) and rerunning the flow may recover more.
|
|
35
|
+
*/
|
|
36
|
+
export interface RecoveryStepProblem {
|
|
37
|
+
/** The step that failed. */
|
|
38
|
+
step: RecoveryStep;
|
|
39
|
+
/** Human-readable cause. */
|
|
40
|
+
reason: string;
|
|
41
|
+
/**
|
|
42
|
+
* Whether rerunning the flow can plausibly make this step succeed.
|
|
43
|
+
* Failures reaching this level are I/O failures against GUARDIAN, the
|
|
44
|
+
* node, or the local store; all but local-store failures are marked
|
|
45
|
+
* retryable. The flow is idempotent, so retrying is always safe.
|
|
46
|
+
*/
|
|
47
|
+
retryable: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Result of `Multisig.recoverNotes`.
|
|
52
|
+
*
|
|
53
|
+
* Each strategy's field holds its primitive's own report and is present
|
|
54
|
+
* exactly when the strategy was enabled and ran; an enabled strategy that
|
|
55
|
+
* could not run at all is a {@link RecoveryStepProblem} instead. Step
|
|
56
|
+
* problems never abort the flow.
|
|
57
|
+
*/
|
|
58
|
+
export interface NoteRecoveryReport {
|
|
59
|
+
/** Report of the transport backlog drain, when that strategy ran. */
|
|
60
|
+
transport?: TransportRecoveryReport;
|
|
61
|
+
/**
|
|
62
|
+
* Per-note outcomes of the proposal-embedded import, when that strategy
|
|
63
|
+
* ran.
|
|
64
|
+
*/
|
|
65
|
+
proposalImport?: NoteImportOutcome[];
|
|
66
|
+
/** Report of the historical public-note backfill, when that strategy ran. */
|
|
67
|
+
backfill?: PublicBackfillReport;
|
|
68
|
+
/** Steps that could not run or finish. Empty on a fully clean flow. */
|
|
69
|
+
problems: RecoveryStepProblem[];
|
|
70
|
+
/** Whether the final verifying sync completed. */
|
|
71
|
+
synced: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Total note records newly added to the local store by this flow: the
|
|
74
|
+
* drain's imports plus every `imported` outcome from the proposal import
|
|
75
|
+
* and the backfill.
|
|
76
|
+
*/
|
|
77
|
+
imported: number;
|
|
78
|
+
/**
|
|
79
|
+
* Whether rerunning the flow can plausibly recover more: any step
|
|
80
|
+
* problem, per-note outcome, or strategy report marked retryable.
|
|
81
|
+
*/
|
|
82
|
+
retryable: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Options for `Multisig.recoverNotes`. The default runs every strategy over
|
|
87
|
+
* the full chain history and syncs afterwards.
|
|
88
|
+
*/
|
|
89
|
+
export interface RecoverNotesOptions {
|
|
90
|
+
/**
|
|
91
|
+
* Rescan the private-note transport backlog (the standalone
|
|
92
|
+
* `drainPrivateNoteBacklog`). Default `true`.
|
|
93
|
+
*/
|
|
94
|
+
transportDrain?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Import the notes embedded in pending consume-notes proposals
|
|
97
|
+
* (`Multisig.importNotesFromProposals`). Default `true`.
|
|
98
|
+
*/
|
|
99
|
+
proposalImport?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Scan chain history for public notes addressed at the account's tag
|
|
102
|
+
* (`Multisig.backfillPublicNotesByTag`). Default `true`.
|
|
103
|
+
*/
|
|
104
|
+
publicBackfill?: boolean;
|
|
105
|
+
/** First block of the backfill scan; defaults to genesis. */
|
|
106
|
+
fromBlock?: number;
|
|
107
|
+
/** Last block of the backfill scan; defaults to the current chain tip. */
|
|
108
|
+
toBlock?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Run a normal sync after the strategies so imported notes are verified
|
|
111
|
+
* and show up as consumable. Default `true`.
|
|
112
|
+
*/
|
|
113
|
+
syncAfter?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The guardian-switch slice of {@link RecoverNotesOptions}: only the
|
|
118
|
+
* proposal-embedded note import runs. Internal — the public entry point is
|
|
119
|
+
* `Multisig.preservePreSwitchProposalNotes`, which adds the switch-specific
|
|
120
|
+
* safety contract (timeout, cancellation, warnings) around this slice. The
|
|
121
|
+
* `satisfies` clause forces every non-range option to be listed, so adding
|
|
122
|
+
* a recovery strategy fails to compile until the switch path decides on it.
|
|
123
|
+
*/
|
|
124
|
+
export const GUARDIAN_SWITCH_RECOVERY_OPTIONS = {
|
|
125
|
+
transportDrain: false,
|
|
126
|
+
proposalImport: true,
|
|
127
|
+
publicBackfill: false,
|
|
128
|
+
syncAfter: false,
|
|
129
|
+
} satisfies Required<Omit<RecoverNotesOptions, 'fromBlock' | 'toBlock'>>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The strategy implementations `runNoteRecovery` orchestrates. `Multisig`
|
|
133
|
+
* wires these to the SDK primitives; tests can substitute stubs.
|
|
134
|
+
*/
|
|
135
|
+
export interface NoteRecoverySteps {
|
|
136
|
+
/** Drain the private-note transport backlog. */
|
|
137
|
+
transportDrain: () => Promise<TransportRecoveryReport>;
|
|
138
|
+
/** Import notes embedded in pending consume-notes proposals. */
|
|
139
|
+
proposalImport: () => Promise<NoteImportOutcome[]>;
|
|
140
|
+
/** Backfill historical public notes by tag. */
|
|
141
|
+
publicBackfill: () => Promise<PublicBackfillReport>;
|
|
142
|
+
/** Run the final verifying sync. */
|
|
143
|
+
sync: () => Promise<void>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function countImported(outcomes: readonly NoteImportOutcome[]): number {
|
|
147
|
+
return outcomes.filter((o) => o.status === 'imported').length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Runs the enabled recovery strategies in order — transport drain, proposal
|
|
152
|
+
* import, public backfill, final sync — folding each strategy-level throw
|
|
153
|
+
* into a {@link RecoveryStepProblem} so no step failure aborts the flow.
|
|
154
|
+
*
|
|
155
|
+
* The optional `cancelled` token makes the run cooperatively cancellable:
|
|
156
|
+
* the orchestrator checks it before each step, and a
|
|
157
|
+
* {@link RecoveryCancelledError} from inside a step is recorded as one
|
|
158
|
+
* non-retryable problem on the interrupted step, after which no further
|
|
159
|
+
* step runs — cancellation never masquerades as a GUARDIAN or node outage.
|
|
160
|
+
*
|
|
161
|
+
* Throws only for an inverted backfill range (a caller error). See
|
|
162
|
+
* `Multisig.recoverNotes` for the wallet-facing entry point.
|
|
163
|
+
*/
|
|
164
|
+
export async function runNoteRecovery(
|
|
165
|
+
options: RecoverNotesOptions,
|
|
166
|
+
steps: NoteRecoverySteps,
|
|
167
|
+
cancelled?: () => boolean,
|
|
168
|
+
): Promise<NoteRecoveryReport> {
|
|
169
|
+
if (
|
|
170
|
+
options.fromBlock !== undefined &&
|
|
171
|
+
options.toBlock !== undefined &&
|
|
172
|
+
options.fromBlock > options.toBlock
|
|
173
|
+
) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`backfill range is inverted: fromBlock ${options.fromBlock} > toBlock ${options.toBlock}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const report: NoteRecoveryReport = {
|
|
180
|
+
problems: [],
|
|
181
|
+
synced: false,
|
|
182
|
+
imported: 0,
|
|
183
|
+
retryable: false,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Set once a cancellation is observed (token flipped, or a
|
|
187
|
+
// RecoveryCancelledError surfaced from a step): the remaining steps are
|
|
188
|
+
// skipped and exactly one problem records the interruption point.
|
|
189
|
+
let stopped = false;
|
|
190
|
+
const interrupted = (step: RecoveryStep, err: unknown): boolean => {
|
|
191
|
+
if (!(err instanceof RecoveryCancelledError)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
report.problems.push({ step, reason: errorMessage(err), retryable: false });
|
|
195
|
+
stopped = true;
|
|
196
|
+
return true;
|
|
197
|
+
};
|
|
198
|
+
const startStep = (step: RecoveryStep): boolean => {
|
|
199
|
+
if (stopped) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (cancelled?.()) {
|
|
203
|
+
report.problems.push({
|
|
204
|
+
step,
|
|
205
|
+
reason: 'note recovery stopped before completion by its caller',
|
|
206
|
+
retryable: false,
|
|
207
|
+
});
|
|
208
|
+
stopped = true;
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (options.transportDrain !== false && startStep('transport-drain')) {
|
|
215
|
+
try {
|
|
216
|
+
report.transport = await steps.transportDrain();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (!interrupted('transport-drain', err)) {
|
|
219
|
+
// The drain's contract: a throw means the local store itself failed,
|
|
220
|
+
// which a rerun is unlikely to fix.
|
|
221
|
+
report.problems.push({
|
|
222
|
+
step: 'transport-drain',
|
|
223
|
+
reason: errorMessage(err),
|
|
224
|
+
retryable: false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (options.proposalImport !== false && startStep('proposal-import')) {
|
|
231
|
+
try {
|
|
232
|
+
report.proposalImport = await steps.proposalImport();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (!interrupted('proposal-import', err)) {
|
|
235
|
+
report.problems.push({
|
|
236
|
+
step: 'proposal-import',
|
|
237
|
+
reason: `failed to list pending proposals: ${errorMessage(err)}`,
|
|
238
|
+
retryable: true,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (options.publicBackfill !== false && startStep('public-backfill')) {
|
|
245
|
+
try {
|
|
246
|
+
report.backfill = await steps.publicBackfill();
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (!interrupted('public-backfill', err)) {
|
|
249
|
+
// With the fully-explicit range validated above, a throw here is
|
|
250
|
+
// normally the chain-tip lookup (a node RPC failure, worth retrying);
|
|
251
|
+
// an invalid range against the resolved tip is a caller error and is
|
|
252
|
+
// not — mirroring the Rust orchestrator's `InvalidConfig` check.
|
|
253
|
+
report.problems.push({
|
|
254
|
+
step: 'public-backfill',
|
|
255
|
+
reason: errorMessage(err),
|
|
256
|
+
retryable: !(err instanceof BackfillRangeError),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (options.syncAfter !== false && startStep('sync')) {
|
|
263
|
+
try {
|
|
264
|
+
await steps.sync();
|
|
265
|
+
report.synced = true;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (!interrupted('sync', err)) {
|
|
268
|
+
report.problems.push({
|
|
269
|
+
step: 'sync',
|
|
270
|
+
reason: `recovery imports succeeded but the verifying sync failed; rerun a sync: ${errorMessage(err)}`,
|
|
271
|
+
retryable: true,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
report.imported =
|
|
278
|
+
(report.transport?.imported ?? 0) +
|
|
279
|
+
countImported(report.proposalImport ?? []) +
|
|
280
|
+
countImported(report.backfill?.outcomes ?? []);
|
|
281
|
+
report.retryable =
|
|
282
|
+
report.problems.some((p) => p.retryable) ||
|
|
283
|
+
report.transport?.retryable === true ||
|
|
284
|
+
(report.proposalImport ?? []).some((o) => o.retryable) ||
|
|
285
|
+
report.backfill?.retryable === true;
|
|
286
|
+
|
|
287
|
+
return report;
|
|
288
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery primitives for restoring note state after device loss.
|
|
3
|
+
*
|
|
4
|
+
* After recovery on a fresh device the local store has no note-transport
|
|
5
|
+
* cursor — and in a shared dirty store another account's sync may have
|
|
6
|
+
* advanced the cursor past notes belonging to the newly recovered account.
|
|
7
|
+
* The primitives here rescan sources that normal forward sync would skip.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MidenClient } from '@miden-sdk/miden-sdk';
|
|
11
|
+
import { errorMessage, isLikelyNetworkError } from '../connectivity.js';
|
|
12
|
+
import { isTransientRpcError } from '../rpc/errors.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Outcome class of a private-note transport backlog drain:
|
|
16
|
+
* - `completed` — the full transport backlog was scanned; every note the
|
|
17
|
+
* transport still holds for the tracked tags is now in the local store.
|
|
18
|
+
* - `unavailable` — the transport could not be consulted at all: it is
|
|
19
|
+
* disabled for the injected `MidenClient` (no `noteTransportUrl`
|
|
20
|
+
* configured) or unreachable before anything was imported (`imported` is
|
|
21
|
+
* always 0 — a connection lost mid-drain after partial progress reports
|
|
22
|
+
* `failed` instead). The rest of a recovery flow should proceed without
|
|
23
|
+
* transport notes.
|
|
24
|
+
* - `failed` — the drain started but did not finish; the backlog may be
|
|
25
|
+
* partially imported. `retryable` distinguishes transient failures (rerun
|
|
26
|
+
* the drain) from permanent ones.
|
|
27
|
+
*/
|
|
28
|
+
export type TransportRecoveryStatus = 'completed' | 'unavailable' | 'failed';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result of {@link drainPrivateNoteBacklog}.
|
|
32
|
+
*
|
|
33
|
+
* Transport problems are reported here rather than thrown so a transport
|
|
34
|
+
* failure never aborts the rest of a recovery flow.
|
|
35
|
+
*/
|
|
36
|
+
export interface TransportRecoveryReport {
|
|
37
|
+
/** Outcome class of the drain. */
|
|
38
|
+
status: TransportRecoveryStatus;
|
|
39
|
+
/**
|
|
40
|
+
* Number of note records newly imported into the local store by this
|
|
41
|
+
* drain. Can be non-zero even on `failed`: batches imported before the
|
|
42
|
+
* failure stay imported.
|
|
43
|
+
*/
|
|
44
|
+
imported: number;
|
|
45
|
+
/**
|
|
46
|
+
* Whether rerunning the drain can plausibly succeed (transient
|
|
47
|
+
* connectivity failures, the upstream pagination convergence guard).
|
|
48
|
+
* Always `false` on `completed`.
|
|
49
|
+
*/
|
|
50
|
+
retryable: boolean;
|
|
51
|
+
/** Human-readable cause when the drain did not complete. */
|
|
52
|
+
reason?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The WASM client surfaces the upstream transport errors as plain messages,
|
|
57
|
+
* so the message text is the only stable join key (same approach as
|
|
58
|
+
* `connectivity.ts`). These fragments come from miden-client's
|
|
59
|
+
* `NoteTransportError` `Display` impls.
|
|
60
|
+
*/
|
|
61
|
+
/** Exported for the drift-guard test, which pins them against the shipped WASM binary. */
|
|
62
|
+
export const TRANSPORT_DISABLED_FRAGMENT = 'note transport is disabled';
|
|
63
|
+
/** Exported for the drift-guard test, which pins them against the shipped WASM binary. */
|
|
64
|
+
export const PAGINATION_GUARD_FRAGMENT = 'did not converge';
|
|
65
|
+
/**
|
|
66
|
+
* `NoteTransportError::Network`'s Display prefix — the transport service
|
|
67
|
+
* answered with an error. Exported for the drift-guard test.
|
|
68
|
+
*/
|
|
69
|
+
export const TRANSPORT_NETWORK_FRAGMENT = 'note transport network error';
|
|
70
|
+
/**
|
|
71
|
+
* `NoteTransportError::Connection`'s Display prefix — endpoint parsing, TLS
|
|
72
|
+
* configuration, and actual connect failures, indiscriminately. Exported for
|
|
73
|
+
* the drift-guard test.
|
|
74
|
+
*/
|
|
75
|
+
export const TRANSPORT_CONNECTION_FRAGMENT = 'connection error';
|
|
76
|
+
/**
|
|
77
|
+
* `RpcError::RequestError`'s Display prefix — a NODE RPC failure (each
|
|
78
|
+
* fetched batch is imported through the node, so this is a mid-drain import
|
|
79
|
+
* failure, not a transport outage). Exported for the drift-guard test.
|
|
80
|
+
*/
|
|
81
|
+
export const NODE_RPC_FRAGMENT = 'grpc request failed';
|
|
82
|
+
/**
|
|
83
|
+
* `RpcError::ConnectionError`'s Display text — the NODE could not be
|
|
84
|
+
* reached at all, which mid-drain is the same interrupted-import class as
|
|
85
|
+
* {@link NODE_RPC_FRAGMENT}. Exported for the drift-guard test.
|
|
86
|
+
*/
|
|
87
|
+
export const NODE_CONNECT_FRAGMENT = 'failed to connect to the Miden node';
|
|
88
|
+
/**
|
|
89
|
+
* The covered-tags bookkeeping key, mirror of miden-client's
|
|
90
|
+
* `NOTE_TRANSPORT_COVERED_TAGS_KEY` (the JS surface does not re-export it).
|
|
91
|
+
* Exported for the drift-guard test, which pins it against the shipped WASM
|
|
92
|
+
* binary so a silent upstream rename cannot degrade the drain to a no-op.
|
|
93
|
+
*/
|
|
94
|
+
export const NOTE_TRANSPORT_COVERED_TAGS_KEY = 'note_transport_covered_tags';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Mirror of miden-client's `Client::MAX_BACKFILL_TAGS_PER_SYNC`: upstream
|
|
98
|
+
* backfills at most this many uncovered tags per transport sync, deferring
|
|
99
|
+
* the remainder to the next sync.
|
|
100
|
+
*/
|
|
101
|
+
const MAX_BACKFILL_TAGS_PER_SYNC = 64;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mirror of the Rust SDK's `CONNECT_PERMANENT_SIGNALS`: connection-failure
|
|
105
|
+
* wording a retry cannot fix (misconfigured endpoint, TLS/certificate
|
|
106
|
+
* problems). Everything else connection-shaped is the peer-still-booting
|
|
107
|
+
* case and stays retryable.
|
|
108
|
+
*/
|
|
109
|
+
const CONNECT_PERMANENT_SIGNALS = ['certificate', 'tls', 'invalid uri', 'unsupported scheme'];
|
|
110
|
+
/**
|
|
111
|
+
* `ClientError::StoreError`'s Display prefix in the WASM error chain.
|
|
112
|
+
* Exported for the drift-guard test, which pins it against the shipped WASM
|
|
113
|
+
* binary.
|
|
114
|
+
*/
|
|
115
|
+
export const STORE_ERROR_FRAGMENT = 'storage error';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* IndexedDB/Dexie error names that mean the local store itself failed —
|
|
119
|
+
* these must reject (matching the Rust `StoreError` branch), never be folded
|
|
120
|
+
* into a transport report.
|
|
121
|
+
*/
|
|
122
|
+
const STORE_ERROR_NAMES = ['QuotaExceededError', 'DatabaseClosedError', 'ReadOnlyError', 'TransactionInactiveError'];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Does this failure mean the local store is broken (as opposed to a
|
|
126
|
+
* transport/node problem)? Kept deliberately narrow: `AbortError` counts
|
|
127
|
+
* only when its message points at an IndexedDB transaction/database abort —
|
|
128
|
+
* network requests also abort, and those stay transport-classified.
|
|
129
|
+
*/
|
|
130
|
+
function isLocalStoreError(err: unknown): boolean {
|
|
131
|
+
const message = errorMessage(err);
|
|
132
|
+
const lower = message.toLowerCase();
|
|
133
|
+
if (lower.includes(STORE_ERROR_FRAGMENT)) return true;
|
|
134
|
+
const name =
|
|
135
|
+
typeof err === 'object' && err !== null && 'name' in err && typeof err.name === 'string'
|
|
136
|
+
? err.name
|
|
137
|
+
: undefined;
|
|
138
|
+
if (name !== undefined && STORE_ERROR_NAMES.includes(name)) return true;
|
|
139
|
+
// The WASM bridge may stringify the underlying store error into the
|
|
140
|
+
// message rather than preserving the name.
|
|
141
|
+
if (STORE_ERROR_NAMES.some((storeName) => message.includes(storeName))) return true;
|
|
142
|
+
if (name === 'AbortError' && (lower.includes('transaction') || lower.includes('database'))) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface DrainFailure {
|
|
149
|
+
status: TransportRecoveryStatus;
|
|
150
|
+
retryable: boolean;
|
|
151
|
+
reason: string;
|
|
152
|
+
/**
|
|
153
|
+
* `false` when the failure proves nothing was fetched (disabled transport
|
|
154
|
+
* throws before any scan), so the store does not need re-counting.
|
|
155
|
+
*/
|
|
156
|
+
scanned: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function classifyDrainFailure(err: unknown): DrainFailure {
|
|
160
|
+
const reason = errorMessage(err);
|
|
161
|
+
const lower = reason.toLowerCase();
|
|
162
|
+
// No transport configured — retrying cannot help until the MidenClient is
|
|
163
|
+
// rebuilt with a `noteTransportUrl`. Thrown before anything is fetched.
|
|
164
|
+
if (lower.includes(TRANSPORT_DISABLED_FRAGMENT)) {
|
|
165
|
+
return { status: 'unavailable', retryable: false, reason, scanned: false };
|
|
166
|
+
}
|
|
167
|
+
// The upstream convergence guard tripped (the server cursor kept advancing
|
|
168
|
+
// for 1000 iterations without an empty batch) — a server-side bug, not an
|
|
169
|
+
// honest backlog. Retryable in the sense that a rerun is safe (imports are
|
|
170
|
+
// idempotent) and succeeds once the server recovers.
|
|
171
|
+
if (lower.includes(PAGINATION_GUARD_FRAGMENT)) {
|
|
172
|
+
return { status: 'failed', retryable: true, reason, scanned: true };
|
|
173
|
+
}
|
|
174
|
+
// A NODE RPC failure: each fetched batch is imported through the node
|
|
175
|
+
// (inclusion-proof lookup), so this interrupted the drain mid-way — the
|
|
176
|
+
// transport itself was reachable. Mirrors the Rust `ClientError::RpcError`
|
|
177
|
+
// arm: report `failed`, with the shared RPC classifier deciding whether a
|
|
178
|
+
// rerun can help.
|
|
179
|
+
if (lower.includes(NODE_RPC_FRAGMENT) || lower.includes(NODE_CONNECT_FRAGMENT.toLowerCase())) {
|
|
180
|
+
return { status: 'failed', retryable: isTransientRpcError(err), reason, scanned: true };
|
|
181
|
+
}
|
|
182
|
+
// The transport answered with an error — worth retrying once the service
|
|
183
|
+
// recovers.
|
|
184
|
+
if (lower.includes(TRANSPORT_NETWORK_FRAGMENT)) {
|
|
185
|
+
return { status: 'unavailable', retryable: true, reason, scanned: true };
|
|
186
|
+
}
|
|
187
|
+
// `Connection` wraps endpoint parsing, TLS configuration, and actual
|
|
188
|
+
// connect failures indiscriminately; permanent wording (mirroring the Rust
|
|
189
|
+
// SDK's cause-chain classifier) must not tell a recovery flow to loop
|
|
190
|
+
// retrying a client that can never connect.
|
|
191
|
+
if (lower.includes(TRANSPORT_CONNECTION_FRAGMENT)) {
|
|
192
|
+
const permanent = CONNECT_PERMANENT_SIGNALS.some((signal) => lower.includes(signal));
|
|
193
|
+
return { status: 'unavailable', retryable: !permanent, reason, scanned: true };
|
|
194
|
+
}
|
|
195
|
+
// Remaining connectivity-shaped wording (e.g. a raw fetch failure): the
|
|
196
|
+
// transport could not be reached; worth retrying once connectivity
|
|
197
|
+
// returns.
|
|
198
|
+
if (isLikelyNetworkError(err)) {
|
|
199
|
+
return { status: 'unavailable', retryable: true, reason, scanned: true };
|
|
200
|
+
}
|
|
201
|
+
return { status: 'failed', retryable: false, reason, scanned: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Restores the covered-tags value after a failed drain: returning it to its
|
|
206
|
+
* pre-drain state keeps a client that synced fine before the attempt
|
|
207
|
+
* syncing fine after it. Unlike the Rust twin — which merges the snapshot
|
|
208
|
+
* with whatever the interrupted backfill re-covered — this restores the
|
|
209
|
+
* snapshot verbatim (the WASM surface exposes the set only as an opaque
|
|
210
|
+
* value); at worst the next successful drain re-covers tags the failed
|
|
211
|
+
* attempt already handled, which is idempotent. A failed restoring write
|
|
212
|
+
* throws: the store is left with its bookkeeping cleared and subsequent
|
|
213
|
+
* normal syncs may re-drain (and re-fail on) old history — a local-store
|
|
214
|
+
* environment failure the caller must not fold into a transport report.
|
|
215
|
+
*/
|
|
216
|
+
async function restoreCoveredTags(
|
|
217
|
+
midenClient: MidenClient,
|
|
218
|
+
snapshot: unknown,
|
|
219
|
+
drainReason: string,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
if (snapshot === null || snapshot === undefined) return;
|
|
222
|
+
try {
|
|
223
|
+
await midenClient.settings.set(NOTE_TRANSPORT_COVERED_TAGS_KEY, snapshot);
|
|
224
|
+
} catch (restoreError) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`the transport drain failed (${drainReason}) and restoring the covered-tags bookkeeping also failed (${errorMessage(restoreError)}); subsequent syncs may re-drain old transport history`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Rescans the full private-note transport backlog for every tracked note tag
|
|
233
|
+
* and imports what it finds, regardless of the stored transport
|
|
234
|
+
* cursor. Counterpart of `MultisigClient::drain_private_note_backlog`
|
|
235
|
+
* in the Rust SDK; the WASM boundary exposes only error message text, so
|
|
236
|
+
* failure classification here is message-based, keyed on the upstream
|
|
237
|
+
* Display prefixes pinned by the drift-guard test.
|
|
238
|
+
*
|
|
239
|
+
* Use after account recovery, passing the **same** `MidenClient` instance
|
|
240
|
+
* that was injected into `MultisigClient`: a fresh store has no transport
|
|
241
|
+
* cursor, and in a shared store another account's sync may have advanced the
|
|
242
|
+
* cursor past this account's notes. The drain is idempotent, tag-scoped (the
|
|
243
|
+
* recovered account must already be in the store so its note tag is tracked
|
|
244
|
+
* — `MultisigClient.load` does this), and never regresses an
|
|
245
|
+
* already-advanced cursor.
|
|
246
|
+
*
|
|
247
|
+
* Transport recovery is bounded by the transport service's retention:
|
|
248
|
+
* senders may bypass the transport entirely and relayed blobs are pruned
|
|
249
|
+
* after the retention window, so this is a best-effort rescan, **not** a
|
|
250
|
+
* backup. Transport-disabled and transport-unreachable outcomes are reported
|
|
251
|
+
* in the {@link TransportRecoveryReport} rather than thrown; this function
|
|
252
|
+
* only throws when the local store itself fails.
|
|
253
|
+
*
|
|
254
|
+
* The rescan runs as many transport syncs as the upstream per-sync tag
|
|
255
|
+
* backfill cap requires to cover every tracked tag, and a failed drain
|
|
256
|
+
* restores the pre-drain covered-tags bookkeeping so normal sync keeps
|
|
257
|
+
* working exactly as it did before the attempt.
|
|
258
|
+
*/
|
|
259
|
+
export async function drainPrivateNoteBacklog(
|
|
260
|
+
midenClient: MidenClient,
|
|
261
|
+
): Promise<TransportRecoveryReport> {
|
|
262
|
+
const before = (await midenClient.notes.list()).length;
|
|
263
|
+
// Records are never removed by a drain, so the length delta is the count
|
|
264
|
+
// of newly imported records. Caveat: it is a store delta — notes imported
|
|
265
|
+
// by concurrent activity on the same store (a background sync, another
|
|
266
|
+
// tab) during the drain are attributed to it.
|
|
267
|
+
const importedSince = async (): Promise<number> =>
|
|
268
|
+
Math.max((await midenClient.notes.list()).length - before, 0);
|
|
269
|
+
|
|
270
|
+
// Snapshot the covered-tags value before clearing it: the clear is
|
|
271
|
+
// durable, and upstream re-marks a tag covered only after its backfill
|
|
272
|
+
// succeeds — so without a restore, a drain that fails on a tag with a
|
|
273
|
+
// permanently bad relay blob would leave every tag uncovered and make
|
|
274
|
+
// every subsequent normal sync re-attempt (and fail) the same backfill.
|
|
275
|
+
// Store errors here propagate like the count above.
|
|
276
|
+
const coveredSnapshot = await midenClient.settings.get(NOTE_TRANSPORT_COVERED_TAGS_KEY);
|
|
277
|
+
let cleared = false;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// The incremental fetch doubles as the transport probe: miden-sdk 0.16's
|
|
281
|
+
// syncNoteTransport silently no-ops when the transport is disabled, but
|
|
282
|
+
// this drain must report `unavailable`, and fetchPrivate still throws
|
|
283
|
+
// the upstream disabled error.
|
|
284
|
+
await midenClient.notes.fetchPrivate();
|
|
285
|
+
// miden-sdk 0.16 replaced the explicit full drain with covered-tag
|
|
286
|
+
// bookkeeping inside syncNoteTransport: every tag not yet marked covered
|
|
287
|
+
// is drained from the start with a local cursor (the global cursor is
|
|
288
|
+
// never regressed), then the steady-state fetch runs. Clearing the
|
|
289
|
+
// covered-tags marker first forces that full per-tag re-drain — exactly
|
|
290
|
+
// the recovery semantic this primitive promises. Imports dedupe, so
|
|
291
|
+
// re-draining already-seen history is harmless.
|
|
292
|
+
await midenClient.settings.remove(NOTE_TRANSPORT_COVERED_TAGS_KEY);
|
|
293
|
+
cleared = true;
|
|
294
|
+
// Upstream backfills at most `MAX_BACKFILL_TAGS_PER_SYNC` uncovered tags
|
|
295
|
+
// per sync, so run enough passes to cover every tracked tag before
|
|
296
|
+
// reporting the backlog fully scanned.
|
|
297
|
+
const tagCount = (await midenClient.tags.list()).length;
|
|
298
|
+
const passes = Math.max(1, Math.ceil(tagCount / MAX_BACKFILL_TAGS_PER_SYNC));
|
|
299
|
+
for (let pass = 0; pass < passes; pass += 1) {
|
|
300
|
+
await midenClient.syncNoteTransport();
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
// A broken local store is an environment failure, not a transport
|
|
304
|
+
// outcome: the whole recovery flow needs to know, so it propagates
|
|
305
|
+
// (matching the Rust `StoreError` branch) instead of being folded into
|
|
306
|
+
// the report.
|
|
307
|
+
if (isLocalStoreError(err)) {
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
const { status, retryable, reason, scanned } = classifyDrainFailure(err);
|
|
311
|
+
if (cleared) {
|
|
312
|
+
await restoreCoveredTags(midenClient, coveredSnapshot, reason);
|
|
313
|
+
}
|
|
314
|
+
// Count even when the drain failed: each fetched batch is imported as it
|
|
315
|
+
// arrives, so notes recovered before the failure stay in the store. A
|
|
316
|
+
// disabled transport throws before fetching anything, so skip the
|
|
317
|
+
// re-count entirely.
|
|
318
|
+
const imported = scanned ? await importedSince() : 0;
|
|
319
|
+
if (status === 'unavailable' && imported > 0) {
|
|
320
|
+
// `unavailable` promises "nothing was imported"; a connection lost
|
|
321
|
+
// mid-drain after partial progress is an interrupted drain, so report
|
|
322
|
+
// it as a failure — keeping the classified retryability, like the
|
|
323
|
+
// Rust twin.
|
|
324
|
+
return { status: 'failed', imported, retryable, reason };
|
|
325
|
+
}
|
|
326
|
+
return { status, imported, retryable, reason };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { status: 'completed', imported: await importedSince(), retryable: false };
|
|
330
|
+
}
|
|
@@ -39,7 +39,10 @@ export function buildConsumeNotesTransactionRequestFromNotes(
|
|
|
39
39
|
|
|
40
40
|
let txBuilder = new TransactionRequestBuilder();
|
|
41
41
|
txBuilder = txBuilder.withInputNotes(noteAndArgsArray);
|
|
42
|
-
txBuilder = txBuilder.
|
|
42
|
+
txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
|
|
43
|
+
// Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
|
|
44
|
+
// so the handle stays ours to release once the builder has read it.
|
|
45
|
+
authSaltForBuilder.free?.();
|
|
43
46
|
|
|
44
47
|
if (options.signatureAdviceMap) {
|
|
45
48
|
txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
|
package/src/transaction/p2id.ts
CHANGED
|
@@ -166,7 +166,10 @@ export function buildP2idTransactionRequest(
|
|
|
166
166
|
|
|
167
167
|
let txBuilder = new TransactionRequestBuilder();
|
|
168
168
|
txBuilder = txBuilder.withOwnOutputNotes(outputNotes);
|
|
169
|
-
txBuilder = txBuilder.
|
|
169
|
+
txBuilder = txBuilder.withFeeConversionSalt(authSaltForBuilder);
|
|
170
|
+
// Borrows rather than consumes: the glue passes `__wbg_ptr` without taking it,
|
|
171
|
+
// so the handle stays ours to release once the builder has read it.
|
|
172
|
+
authSaltForBuilder.free?.();
|
|
170
173
|
|
|
171
174
|
if (options.signatureAdviceMap) {
|
|
172
175
|
txBuilder = txBuilder.extendAdviceMap(options.signatureAdviceMap);
|
|
@@ -9,11 +9,11 @@ import { getRawMidenClient } from '../raw-client.js';
|
|
|
9
9
|
import { base64ToUint8Array, uint8ArrayToBase64 } from '../utils/encoding.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Index of the first user param carrying the auth
|
|
12
|
+
* Index of the first user param carrying the auth args. The guarded-multisig
|
|
13
13
|
* auth component zeroes user params 0-2 and fills 3-6 with the auth args, matching
|
|
14
14
|
* `push.0.0.0` ahead of `multisig::auth_tx` in `guarded_multisig.masm`.
|
|
15
15
|
*/
|
|
16
|
-
const
|
|
16
|
+
const AUTH_ARG_USER_PARAM_OFFSET = 3;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Captures a `ChainAnchor` for the request at the current sync height and
|
|
@@ -98,13 +98,19 @@ export function chainAnchorFromBase64(anchorBase64: string): ChainAnchor {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Reads the auth
|
|
101
|
+
* Reads the auth args back out of a transaction summary.
|
|
102
102
|
*
|
|
103
103
|
* Since miden-protocol 0.16-rc the summary binds seven user-defined elements
|
|
104
104
|
* instead of a dedicated salt word. The guarded-multisig auth component zeroes
|
|
105
|
-
* the leading three and passes the auth args as the trailing four, so the
|
|
106
|
-
*
|
|
105
|
+
* the leading three and passes the auth args as the trailing four, so the auth
|
|
106
|
+
* args are the tail of `userParams()`.
|
|
107
|
+
*
|
|
108
|
+
* This is the auth-arg word, *not* the proposal salt. When the request declares
|
|
109
|
+
* a fee conversion salt, miden-client uses it to commit the native conversion
|
|
110
|
+
* info under `hash(CONVERSION_INFO || SALT)`. That commitment is not invertible
|
|
111
|
+
* to the salt. Keep the salt alongside the proposal — `ProposalMetadata.saltHex`
|
|
112
|
+
* — rather than trying to recover it from the summary.
|
|
107
113
|
*/
|
|
108
|
-
export function
|
|
109
|
-
return Word.newFromFelts(summary.userParams().slice(
|
|
114
|
+
export function summaryAuthArg(summary: TransactionSummary): Word {
|
|
115
|
+
return Word.newFromFelts(summary.userParams().slice(AUTH_ARG_USER_PARAM_OFFSET));
|
|
110
116
|
}
|