@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,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery primitives.
|
|
3
|
+
*
|
|
4
|
+
* After key-based recovery the local Miden store starts empty, so notes the
|
|
5
|
+
* account was in the middle of consuming are gone. v2 `consume_notes`
|
|
6
|
+
* proposals embed the serialized notes they consume, which makes
|
|
7
|
+
* pending proposals opportunistic recovery material: this module rebuilds
|
|
8
|
+
* importable notes from those embedded bytes plus a node-fetched inclusion
|
|
9
|
+
* proof, without needing the node to hold the note body — so it works for
|
|
10
|
+
* private notes too.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Endpoint,
|
|
15
|
+
InputNote,
|
|
16
|
+
type InputNoteRecord,
|
|
17
|
+
InputNoteState,
|
|
18
|
+
Note,
|
|
19
|
+
type NoteAssets,
|
|
20
|
+
NoteDetails,
|
|
21
|
+
NoteFile,
|
|
22
|
+
NoteFilter,
|
|
23
|
+
NoteFilterTypes,
|
|
24
|
+
type NoteInclusionProof,
|
|
25
|
+
RpcClient,
|
|
26
|
+
} from '@miden-sdk/miden-sdk';
|
|
27
|
+
|
|
28
|
+
import { getRawMidenClient, requireMidenRpcEndpoint, type RawClientSource } from '../raw-client.js';
|
|
29
|
+
import { resolveRpcConfig, type RpcConfig } from '../rpc/config.js';
|
|
30
|
+
import { isTransientRpcError } from '../rpc/errors.js';
|
|
31
|
+
import { retryRpcRead } from '../rpc/retry.js';
|
|
32
|
+
import { isConsumeNotesV2 } from '../types/proposal.js';
|
|
33
|
+
import type { Proposal } from '../types/proposal.js';
|
|
34
|
+
import { noteFromBase64, normalizeHexWord } from '../utils/encoding.js';
|
|
35
|
+
|
|
36
|
+
/** Where a recovered note's bytes came from. */
|
|
37
|
+
export type NoteImportSource =
|
|
38
|
+
/** Embedded in a v2 `consume_notes` proposal. */
|
|
39
|
+
| 'proposal'
|
|
40
|
+
/** Discovered on chain by a tag-scoped historical scan
|
|
41
|
+
* (`backfillPublicNotesByTag`). */
|
|
42
|
+
| 'backfill';
|
|
43
|
+
|
|
44
|
+
/** Per-note result of a recovery import attempt. */
|
|
45
|
+
export type NoteImportStatus =
|
|
46
|
+
/** Note imported with its on-chain inclusion proof; it lands in the store's
|
|
47
|
+
* unverified state and the next sync verifies it. */
|
|
48
|
+
| 'imported'
|
|
49
|
+
/** The local store already tracks this note (not yet consumed). */
|
|
50
|
+
| 'already-present'
|
|
51
|
+
/** The note is already consumed — either the local store tracked it as
|
|
52
|
+
* consumed, or the chain had nullified it and the import recorded it as
|
|
53
|
+
* consumption history rather than a consumable note. */
|
|
54
|
+
| 'already-consumed'
|
|
55
|
+
/** The chain does not know the note yet. Its details were recorded as
|
|
56
|
+
* expected with its tag tracked, so a later sync picks it up once it
|
|
57
|
+
* commits. */
|
|
58
|
+
| 'not-committed'
|
|
59
|
+
/** The embedded bytes could not be decoded into a note. */
|
|
60
|
+
| 'invalid'
|
|
61
|
+
/** The import attempt failed (store or RPC error). */
|
|
62
|
+
| 'failed';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Outcome of one unique embedded note's recovery import. A batch of outcomes
|
|
66
|
+
* is the full report of {@link importNotesFromProposals}; no per-note problem
|
|
67
|
+
* aborts the batch. A note embedded by several proposals is deduplicated into
|
|
68
|
+
* a single outcome (its first occurrence).
|
|
69
|
+
*/
|
|
70
|
+
export interface NoteImportOutcome {
|
|
71
|
+
/** The note ID hex when the bytes decoded, otherwise a positional reference
|
|
72
|
+
* into the proposal (`proposal <id> notes[<i>]`). */
|
|
73
|
+
identifier: string;
|
|
74
|
+
/** Where the note bytes came from. */
|
|
75
|
+
source: NoteImportSource;
|
|
76
|
+
/** What happened to this note. */
|
|
77
|
+
status: NoteImportStatus;
|
|
78
|
+
/** Whether retrying the import later can change the status (transient RPC
|
|
79
|
+
* failures, notes not yet committed). Absent means not retryable. */
|
|
80
|
+
retryable?: boolean;
|
|
81
|
+
/** Human-readable detail for non-success statuses — or, on an `imported`
|
|
82
|
+
* outcome, a warning that the post-import consumed-state check failed and a
|
|
83
|
+
* sync should confirm the note's status. */
|
|
84
|
+
reason?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ImportNotesFromProposalsOptions {
|
|
88
|
+
/** Miden node RPC endpoint used to fetch inclusion proofs. Must point at
|
|
89
|
+
* the same network as the injected Miden client. */
|
|
90
|
+
midenRpcEndpoint: string;
|
|
91
|
+
/** Node RPC read-retry configuration (defaults match the rest of the SDK). */
|
|
92
|
+
rpc?: RpcConfig;
|
|
93
|
+
/**
|
|
94
|
+
* Cooperative cancellation, checked before each network attempt and store
|
|
95
|
+
* write: once `true`, the import throws {@link RecoveryCancelledError}
|
|
96
|
+
* instead of starting further work.
|
|
97
|
+
*/
|
|
98
|
+
cancelled?: () => boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Thrown by the recovery cancellation checkpoints; `runNoteRecovery` stops
|
|
103
|
+
* on it instead of misreporting a step failure. The message avoids
|
|
104
|
+
* 'cancelled'/'timeout' wording on purpose — the RPC retry classifier
|
|
105
|
+
* treats those fragments as transient, and cancellation must not retry.
|
|
106
|
+
*/
|
|
107
|
+
export class RecoveryCancelledError extends Error {
|
|
108
|
+
constructor() {
|
|
109
|
+
super('note recovery stopped before completion by its caller');
|
|
110
|
+
this.name = 'RecoveryCancelledError';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Throws {@link RecoveryCancelledError} when the token reports cancelled. */
|
|
115
|
+
export function throwIfCancelled(cancelled?: () => boolean): void {
|
|
116
|
+
if (cancelled?.()) {
|
|
117
|
+
throw new RecoveryCancelledError();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Shared by the recovery primitives (proposal import and backfill). */
|
|
122
|
+
export function errorDetail(error: unknown): string {
|
|
123
|
+
return error instanceof Error ? error.message : String(error);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface DecodedCandidate {
|
|
127
|
+
note: Note;
|
|
128
|
+
idHex: string;
|
|
129
|
+
/** Metadata-independent identifier: metadata-less store records (details
|
|
130
|
+
* imports in expected state, chain-consumed history) expose neither a note
|
|
131
|
+
* ID nor a nullifier, so records are matched by their details — recipient
|
|
132
|
+
* digest plus asset fingerprint (see {@link detailsKeyOf} for the
|
|
133
|
+
* fingerprint's coverage and its limits). */
|
|
134
|
+
detailsKey: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Details key: recipient digest + canonical FUNGIBLE asset list. This
|
|
138
|
+
* approximates the details commitment — which the WASM record surface does
|
|
139
|
+
* not expose — as closely as the surface allows: `fungibleAssets()` silently
|
|
140
|
+
* omits non-fungible assets and no complete accessor or commitment exists,
|
|
141
|
+
* so two notes sharing a recipient digest and fungible assets but differing
|
|
142
|
+
* only in non-fungible assets collide (the Rust SDK, keyed on the real
|
|
143
|
+
* `NoteDetailsCommitment`, does not). Latent until NFA-bearing notes reach
|
|
144
|
+
* these flows; closing it needs an upstream `NoteAssets` commitment/NFA
|
|
145
|
+
* accessor. Recipient digest alone would be worse: distinct notes can share
|
|
146
|
+
* a recipient while carrying different fungible assets. */
|
|
147
|
+
export function detailsKeyOf(recipientDigestHex: string, assets: NoteAssets): string {
|
|
148
|
+
const fingerprint = assets
|
|
149
|
+
.fungibleAssets()
|
|
150
|
+
.map((asset) => `${normalizeHexWord(asset.faucetId().toString())}:${asset.amount()}`)
|
|
151
|
+
.sort()
|
|
152
|
+
.join(',');
|
|
153
|
+
return `${recipientDigestHex}|${fingerprint}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function recordKeys(record: InputNoteRecord): string[] {
|
|
157
|
+
const keys: string[] = [];
|
|
158
|
+
const recordId = record.id();
|
|
159
|
+
if (recordId) {
|
|
160
|
+
keys.push(normalizeHexWord(recordId.toString()));
|
|
161
|
+
}
|
|
162
|
+
const details = record.details();
|
|
163
|
+
keys.push(
|
|
164
|
+
detailsKeyOf(normalizeHexWord(details.recipient().digest().toHex()), details.assets()),
|
|
165
|
+
);
|
|
166
|
+
return keys;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Scans the store once and keys every record by note ID *and* details key:
|
|
171
|
+
* records the store keeps without metadata (a note details import in
|
|
172
|
+
* expected state, or a note observed as consumed on chain) expose neither a
|
|
173
|
+
* note ID nor a nullifier, and an ID-only lookup would keep re-importing
|
|
174
|
+
* them forever. (The WASM NoteFilter has no details-commitment variant,
|
|
175
|
+
* unlike the Rust SDK, so the store is scanned once and keyed both ways.)
|
|
176
|
+
* Shared by the recovery primitives (proposal import and backfill).
|
|
177
|
+
*/
|
|
178
|
+
export async function collectExistingRecords(
|
|
179
|
+
webClient: Awaited<ReturnType<typeof getRawMidenClient>>,
|
|
180
|
+
): Promise<Map<string, InputNoteRecord>> {
|
|
181
|
+
const existing = new Map<string, InputNoteRecord>();
|
|
182
|
+
const records = await webClient.getInputNotes(new NoteFilter(NoteFilterTypes.All));
|
|
183
|
+
for (const record of records) {
|
|
184
|
+
for (const key of recordKeys(record)) {
|
|
185
|
+
existing.set(key, record);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return existing;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Imports one note with its inclusion proof and classifies the result.
|
|
193
|
+
* Upstream note-import batches are atomic, which is why callers import
|
|
194
|
+
* individually — one bad note must not sink the rest. Returns the outcome
|
|
195
|
+
* and whether the import succeeded (input for the batched consumed-state
|
|
196
|
+
* re-check).
|
|
197
|
+
*/
|
|
198
|
+
export async function importNoteWithProof(
|
|
199
|
+
webClient: Awaited<ReturnType<typeof getRawMidenClient>>,
|
|
200
|
+
source: NoteImportSource,
|
|
201
|
+
idHex: string,
|
|
202
|
+
note: Note,
|
|
203
|
+
proof: NoteInclusionProof,
|
|
204
|
+
): Promise<{ outcome: NoteImportOutcome; wasImported: boolean }> {
|
|
205
|
+
try {
|
|
206
|
+
const inputNote = InputNote.authenticated(note, proof);
|
|
207
|
+
await webClient.importNoteFile(NoteFile.fromInputNote(inputNote));
|
|
208
|
+
return {
|
|
209
|
+
outcome: { identifier: idHex, source, status: 'imported' },
|
|
210
|
+
wasImported: true,
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
outcome: {
|
|
215
|
+
identifier: idHex,
|
|
216
|
+
source,
|
|
217
|
+
status: 'failed',
|
|
218
|
+
retryable: isTransientRpcError(error),
|
|
219
|
+
reason: `failed to import note: ${errorDetail(error)}`,
|
|
220
|
+
},
|
|
221
|
+
wasImported: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Re-classifies provisionally `imported` outcomes from the records the
|
|
228
|
+
* import actually left behind. A note the chain had already nullified is
|
|
229
|
+
* stored as consumption history, not as a consumable note — reported as
|
|
230
|
+
* `already-consumed`. A note whose inclusion proof failed verification
|
|
231
|
+
* against the authenticated block header is stored in `Invalid` state by
|
|
232
|
+
* upstream while the import still resolves — reported as `failed`, because
|
|
233
|
+
* "recovered" notes that can never be consumed must not count as recovered.
|
|
234
|
+
* Records are matched by note ID when they expose one, and by the (lossy,
|
|
235
|
+
* fungible-assets-only) details key otherwise — a chain-consumed record is
|
|
236
|
+
* stored without metadata, so the approximation is the only join available;
|
|
237
|
+
* it can only misstate the status here, never skip an import. One batched
|
|
238
|
+
* store read covers every imported note. A failed check downgrades nothing;
|
|
239
|
+
* it flags the outcome's classification as unconfirmed instead.
|
|
240
|
+
*/
|
|
241
|
+
export async function reclassifyConsumedImports(
|
|
242
|
+
webClient: Awaited<ReturnType<typeof getRawMidenClient>>,
|
|
243
|
+
imported: Array<{ index: number; idHex: string; detailsKey: string }>,
|
|
244
|
+
outcomes: NoteImportOutcome[],
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
if (imported.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const records = await webClient.getInputNotes(new NoteFilter(NoteFilterTypes.All));
|
|
251
|
+
const byId = new Map<string, InputNoteRecord>();
|
|
252
|
+
const byDetailsKey = new Map<string, InputNoteRecord>();
|
|
253
|
+
for (const record of records) {
|
|
254
|
+
const recordId = record.id();
|
|
255
|
+
if (recordId) {
|
|
256
|
+
byId.set(normalizeHexWord(recordId.toString()), record);
|
|
257
|
+
} else {
|
|
258
|
+
const details = record.details();
|
|
259
|
+
byDetailsKey.set(
|
|
260
|
+
detailsKeyOf(normalizeHexWord(details.recipient().digest().toHex()), details.assets()),
|
|
261
|
+
record,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const entry of imported) {
|
|
266
|
+
const record = byId.get(entry.idHex) ?? byDetailsKey.get(entry.detailsKey);
|
|
267
|
+
if (!record) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (record.isConsumed()) {
|
|
271
|
+
outcomes[entry.index] = {
|
|
272
|
+
...outcomes[entry.index],
|
|
273
|
+
status: 'already-consumed',
|
|
274
|
+
reason: 'note was already consumed on chain; recorded as consumption history',
|
|
275
|
+
};
|
|
276
|
+
} else if (record.state() === InputNoteState.Invalid) {
|
|
277
|
+
outcomes[entry.index] = {
|
|
278
|
+
...outcomes[entry.index],
|
|
279
|
+
status: 'failed',
|
|
280
|
+
retryable: false,
|
|
281
|
+
reason:
|
|
282
|
+
"the note's inclusion proof failed verification against the authenticated block header; the record is stored as invalid and the note is not consumable",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// The imports themselves succeeded; stay `imported` but flag that the
|
|
288
|
+
// post-import state check is unknown.
|
|
289
|
+
for (const entry of imported) {
|
|
290
|
+
outcomes[entry.index] = {
|
|
291
|
+
...outcomes[entry.index],
|
|
292
|
+
reason: `imported, but the post-import state check failed (${errorDetail(
|
|
293
|
+
error,
|
|
294
|
+
)}); run sync to confirm the note's status`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Imports the notes embedded in v2 `consume_notes` proposals into the local
|
|
302
|
+
* Miden store, typically after key-based recovery rebuilt the
|
|
303
|
+
* proposal list (`syncProposals`) but left the note store empty.
|
|
304
|
+
*
|
|
305
|
+
* Proposals are opportunistic recovery material, not a backup: v1 proposals
|
|
306
|
+
* carry no note bytes, and proposals disappear once canonicalized, so only
|
|
307
|
+
* notes still mid-consumption are recoverable this way.
|
|
308
|
+
*
|
|
309
|
+
* Per note: decode the embedded bytes, skip notes the store already tracks,
|
|
310
|
+
* fetch the on-chain inclusion proof, and import the note individually
|
|
311
|
+
* (upstream note-import batches are atomic, so one bad note must not sink the
|
|
312
|
+
* rest). A note the chain does not know yet is recorded as expected with its
|
|
313
|
+
* tag tracked so a later sync picks it up, and is reported as
|
|
314
|
+
* `not-committed`/retryable. A note the chain has already nullified is
|
|
315
|
+
* recorded as consumption history and reported `already-consumed`.
|
|
316
|
+
*
|
|
317
|
+
* The returned outcomes cover every unique embedded note — a note embedded by
|
|
318
|
+
* several proposals yields one outcome, not one per embedding — and this
|
|
319
|
+
* function does not throw for per-note problems.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* const proposals = await multisig.syncProposals();
|
|
324
|
+
* const outcomes = await importNotesFromProposals(midenClient, proposals, {
|
|
325
|
+
* midenRpcEndpoint: 'https://rpc.testnet.miden.io',
|
|
326
|
+
* });
|
|
327
|
+
* await multisig.syncState();
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
export async function importNotesFromProposals(
|
|
331
|
+
midenClient: RawClientSource,
|
|
332
|
+
proposals: ReadonlyArray<Pick<Proposal, 'id' | 'metadata'>>,
|
|
333
|
+
options: ImportNotesFromProposalsOptions,
|
|
334
|
+
): Promise<NoteImportOutcome[]> {
|
|
335
|
+
const midenRpcEndpoint = requireMidenRpcEndpoint(options.midenRpcEndpoint);
|
|
336
|
+
const rpcConfig = resolveRpcConfig(options.rpc);
|
|
337
|
+
throwIfCancelled(options.cancelled);
|
|
338
|
+
const webClient = await getRawMidenClient(midenClient, midenRpcEndpoint);
|
|
339
|
+
|
|
340
|
+
const outcomes: NoteImportOutcome[] = [];
|
|
341
|
+
|
|
342
|
+
// Decode, validate, and deduplicate embedded notes (the same note may be
|
|
343
|
+
// embedded by several proposals). Undecodable entries, embeddings past the
|
|
344
|
+
// declared note-id list, and embeddings whose decoded ID disagrees with
|
|
345
|
+
// the declared one become isolated `invalid` outcomes with a positional
|
|
346
|
+
// identifier: recovery runs automatically over synced proposals, so the
|
|
347
|
+
// per-index ID binding is what keeps a malformed or adversarial proposal
|
|
348
|
+
// from smuggling arbitrary notes (and, for uncommitted ones, persistent
|
|
349
|
+
// expected records and tag registrations) into the local store.
|
|
350
|
+
const decoded: DecodedCandidate[] = [];
|
|
351
|
+
const seen = new Set<string>();
|
|
352
|
+
for (const proposal of proposals) {
|
|
353
|
+
const metadata = proposal.metadata;
|
|
354
|
+
if (metadata.proposalType !== 'consume_notes' || !isConsumeNotesV2(metadata)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const embedded = metadata.notes ?? [];
|
|
358
|
+
const declaredIds = metadata.noteIds ?? [];
|
|
359
|
+
for (let index = 0; index < embedded.length; index += 1) {
|
|
360
|
+
const identifier = `proposal ${proposal.id} notes[${index}]`;
|
|
361
|
+
const declaredId = declaredIds[index];
|
|
362
|
+
if (declaredId === undefined) {
|
|
363
|
+
outcomes.push({
|
|
364
|
+
identifier,
|
|
365
|
+
source: 'proposal',
|
|
366
|
+
status: 'invalid',
|
|
367
|
+
reason: "embedded note has no matching entry in the proposal's declared note ids",
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
// The try covers every per-note WASM accessor, so a payload that
|
|
372
|
+
// deserializes but traps on use is isolated like any other bad note.
|
|
373
|
+
let candidate: DecodedCandidate;
|
|
374
|
+
try {
|
|
375
|
+
const note = noteFromBase64(embedded[index], Note);
|
|
376
|
+
candidate = {
|
|
377
|
+
note,
|
|
378
|
+
idHex: normalizeHexWord(note.id().toString()),
|
|
379
|
+
detailsKey: detailsKeyOf(
|
|
380
|
+
normalizeHexWord(note.recipient().digest().toHex()),
|
|
381
|
+
note.assets(),
|
|
382
|
+
),
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
outcomes.push({
|
|
386
|
+
identifier,
|
|
387
|
+
source: 'proposal',
|
|
388
|
+
status: 'invalid',
|
|
389
|
+
reason: `failed to decode embedded note: ${errorDetail(error)}`,
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (candidate.idHex !== normalizeHexWord(declaredId)) {
|
|
394
|
+
outcomes.push({
|
|
395
|
+
identifier,
|
|
396
|
+
source: 'proposal',
|
|
397
|
+
status: 'invalid',
|
|
398
|
+
reason: `embedded note decodes to ${candidate.idHex} but the proposal declares ${declaredId}`,
|
|
399
|
+
});
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (seen.has(candidate.idHex)) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
seen.add(candidate.idHex);
|
|
406
|
+
decoded.push(candidate);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (decoded.length === 0) {
|
|
411
|
+
return outcomes;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let existing: Map<string, InputNoteRecord>;
|
|
415
|
+
try {
|
|
416
|
+
existing = await collectExistingRecords(webClient);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
const reason = `failed to read local store: ${errorDetail(error)}`;
|
|
419
|
+
for (const candidate of decoded) {
|
|
420
|
+
outcomes.push({
|
|
421
|
+
identifier: candidate.idHex,
|
|
422
|
+
source: 'proposal',
|
|
423
|
+
status: 'failed',
|
|
424
|
+
reason,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return outcomes;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Skip notes the store already tracks — but only on an exact note-ID
|
|
431
|
+
// match. A details-key match is a lossy approximation (see
|
|
432
|
+
// {@link detailsKeyOf}), so a candidate that only matches a metadata-less
|
|
433
|
+
// record proceeds to import: the upstream import dedupes exactly by the
|
|
434
|
+
// real details commitment, upgrading or no-oping in place, so importing
|
|
435
|
+
// "again" is safe while pre-skipping on the approximation could silently
|
|
436
|
+
// drop a genuinely new note.
|
|
437
|
+
const pending: DecodedCandidate[] = [];
|
|
438
|
+
for (const candidate of decoded) {
|
|
439
|
+
const record = existing.get(candidate.idHex);
|
|
440
|
+
if (record) {
|
|
441
|
+
outcomes.push({
|
|
442
|
+
identifier: candidate.idHex,
|
|
443
|
+
source: 'proposal',
|
|
444
|
+
status: record.isConsumed() ? 'already-consumed' : 'already-present',
|
|
445
|
+
});
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
pending.push(candidate);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (pending.length === 0) {
|
|
452
|
+
return outcomes;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// One round trip for all missing notes; only the import itself is per-note.
|
|
456
|
+
// The node returns proofs for private notes too, so the locally-held bytes
|
|
457
|
+
// are the only body this path ever needs.
|
|
458
|
+
const proofs = new Map<string, NoteInclusionProof>();
|
|
459
|
+
throwIfCancelled(options.cancelled);
|
|
460
|
+
try {
|
|
461
|
+
const rpcClient = new RpcClient(new Endpoint(midenRpcEndpoint));
|
|
462
|
+
// Inside the retried closure, so a token flip between attempts stops
|
|
463
|
+
// the retry loop instead of letting backoff attempts outlive the
|
|
464
|
+
// deadline.
|
|
465
|
+
const fetchedNotes = await retryRpcRead(() => {
|
|
466
|
+
throwIfCancelled(options.cancelled);
|
|
467
|
+
return rpcClient.getNotesById(pending.map((candidate) => candidate.note.id()));
|
|
468
|
+
}, rpcConfig);
|
|
469
|
+
for (const fetched of fetchedNotes) {
|
|
470
|
+
proofs.set(normalizeHexWord(fetched.noteId.toString()), fetched.inclusionProof);
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error instanceof RecoveryCancelledError) {
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
const retryable = isTransientRpcError(error);
|
|
477
|
+
const reason = `failed to fetch inclusion proofs: ${errorDetail(error)}`;
|
|
478
|
+
for (const candidate of pending) {
|
|
479
|
+
outcomes.push({
|
|
480
|
+
identifier: candidate.idHex,
|
|
481
|
+
source: 'proposal',
|
|
482
|
+
status: 'failed',
|
|
483
|
+
retryable,
|
|
484
|
+
reason,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return outcomes;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Provisionally `imported` outcomes, re-classified in one batched
|
|
491
|
+
// post-import state check below.
|
|
492
|
+
const imported: Array<{ index: number; idHex: string; detailsKey: string }> = [];
|
|
493
|
+
|
|
494
|
+
for (const candidate of pending) {
|
|
495
|
+
throwIfCancelled(options.cancelled);
|
|
496
|
+
const proof = proofs.get(candidate.idHex);
|
|
497
|
+
if (proof) {
|
|
498
|
+
const { outcome, wasImported } = await importNoteWithProof(
|
|
499
|
+
webClient,
|
|
500
|
+
'proposal',
|
|
501
|
+
candidate.idHex,
|
|
502
|
+
candidate.note,
|
|
503
|
+
proof,
|
|
504
|
+
);
|
|
505
|
+
if (wasImported) {
|
|
506
|
+
imported.push({
|
|
507
|
+
index: outcomes.length,
|
|
508
|
+
idHex: candidate.idHex,
|
|
509
|
+
detailsKey: candidate.detailsKey,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
outcomes.push(outcome);
|
|
513
|
+
} else {
|
|
514
|
+
try {
|
|
515
|
+
// Mirror the Rust SDK's `NoteFile::ExpectedNote` + sync-hint import:
|
|
516
|
+
// the tag rides in the note file itself, so upstream registers a
|
|
517
|
+
// note-source tag that sync uses to discover the commitment and
|
|
518
|
+
// removes once the note commits. (An explicit `addTag` would instead
|
|
519
|
+
// create a permanent user-source tag that the transport backfill
|
|
520
|
+
// also re-drains, and that would outlive even a failed import.)
|
|
521
|
+
const details = new NoteDetails(candidate.note.assets(), candidate.note.recipient());
|
|
522
|
+
await webClient.importNoteFile(
|
|
523
|
+
NoteFile.fromExpectedNote(details, candidate.note.metadata().tag(), 0),
|
|
524
|
+
);
|
|
525
|
+
outcomes.push({
|
|
526
|
+
identifier: candidate.idHex,
|
|
527
|
+
source: 'proposal',
|
|
528
|
+
status: 'not-committed',
|
|
529
|
+
retryable: true,
|
|
530
|
+
reason: 'note not yet committed on chain; recorded as expected so a later sync picks it up',
|
|
531
|
+
});
|
|
532
|
+
} catch (error) {
|
|
533
|
+
outcomes.push({
|
|
534
|
+
identifier: candidate.idHex,
|
|
535
|
+
source: 'proposal',
|
|
536
|
+
status: 'failed',
|
|
537
|
+
retryable: isTransientRpcError(error),
|
|
538
|
+
reason: `failed to record expected note: ${errorDetail(error)}`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
throwIfCancelled(options.cancelled);
|
|
545
|
+
await reclassifyConsumedImports(webClient, imported, outcomes);
|
|
546
|
+
|
|
547
|
+
return outcomes;
|
|
548
|
+
}
|