@openzeppelin/miden-multisig-client 0.15.2 → 0.16.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 +25 -1
- package/dist/account/builder.d.ts +2 -1
- package/dist/account/builder.d.ts.map +1 -1
- package/dist/account/builder.js +1 -0
- package/dist/account/builder.js.map +1 -1
- package/dist/account/builder.test.js +2 -2
- package/dist/account/builder.test.js.map +1 -1
- package/dist/client.d.ts +9 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +5 -5
- package/dist/client.js.map +1 -1
- package/dist/client.test.js +46 -15
- package/dist/client.test.js.map +1 -1
- package/dist/connectivity.d.ts +38 -0
- package/dist/connectivity.d.ts.map +1 -0
- package/dist/connectivity.js +97 -0
- package/dist/connectivity.js.map +1 -0
- package/dist/connectivity.test.d.ts +2 -0
- package/dist/connectivity.test.d.ts.map +1 -0
- package/dist/connectivity.test.js +61 -0
- package/dist/connectivity.test.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/multisig.d.ts +68 -7
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +90 -18
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +214 -79
- package/dist/multisig.test.js.map +1 -1
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +12 -0
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/metadata.test.js +49 -0
- package/dist/proposal/metadata.test.js.map +1 -1
- package/dist/raw-client.d.ts +2 -2
- package/dist/raw-client.d.ts.map +1 -1
- package/dist/raw-client.js +14 -4
- package/dist/raw-client.js.map +1 -1
- package/dist/raw-client.test.js +25 -3
- package/dist/raw-client.test.js.map +1 -1
- package/dist/transaction/consumeNotes.d.ts +6 -2
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +0 -4
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/transaction/options.d.ts +3 -0
- package/dist/transaction/options.d.ts.map +1 -1
- package/dist/transaction/p2id.d.ts +20 -2
- package/dist/transaction/p2id.d.ts.map +1 -1
- package/dist/transaction/p2id.js +46 -3
- package/dist/transaction/p2id.js.map +1 -1
- package/dist/transaction/p2id.test.js +66 -6
- package/dist/transaction/p2id.test.js.map +1 -1
- package/dist/transaction/summary.d.ts +2 -1
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction/updateGuardian.d.ts +6 -2
- package/dist/transaction/updateGuardian.d.ts.map +1 -1
- package/dist/transaction/updateGuardian.js.map +1 -1
- package/dist/transaction/updateProcedureThreshold.d.ts +7 -2
- package/dist/transaction/updateProcedureThreshold.d.ts.map +1 -1
- package/dist/transaction/updateProcedureThreshold.js.map +1 -1
- package/dist/transaction/updateSigners.d.ts +7 -2
- package/dist/transaction/updateSigners.d.ts.map +1 -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/dist/types/proposal.d.ts +5 -0
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js +3 -0
- package/dist/types/proposal.js.map +1 -1
- package/package.json +3 -3
- package/src/account/builder.test.ts +19 -11
- package/src/account/builder.ts +2 -1
- package/src/client.test.ts +67 -15
- package/src/client.ts +18 -10
- package/src/connectivity.test.ts +67 -0
- package/src/connectivity.ts +111 -0
- package/src/index.ts +20 -1
- package/src/multisig.test.ts +267 -80
- package/src/multisig.ts +109 -20
- package/src/proposal/metadata.test.ts +60 -0
- package/src/proposal/metadata.ts +14 -0
- package/src/raw-client.test.ts +41 -3
- package/src/raw-client.ts +15 -5
- package/src/transaction/consumeNotes.ts +11 -1
- package/src/transaction/options.ts +4 -0
- package/src/transaction/p2id.test.ts +98 -8
- package/src/transaction/p2id.ts +64 -4
- package/src/transaction/summary.ts +12 -0
- package/src/transaction/updateGuardian.ts +11 -1
- package/src/transaction/updateProcedureThreshold.ts +13 -1
- package/src/transaction/updateSigners.ts +13 -1
- package/src/transaction.ts +3 -0
- package/src/types/proposal.ts +9 -0
package/src/multisig.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* for proposal management.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { GuardianHttpClient, type DeltaObject, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
|
|
8
|
+
import { GuardianHttpClient, type AbandonCandidateResponse, type AbandonStatus, type DeltaObject, type ProposalSignature, type Signer, type AuthConfig, type StateObject } from '@openzeppelin/guardian-client';
|
|
9
9
|
import type {
|
|
10
10
|
ConsumableNote,
|
|
11
11
|
ExportedProposal,
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
Endpoint,
|
|
30
30
|
FeltArray,
|
|
31
31
|
Note,
|
|
32
|
+
NoteType,
|
|
32
33
|
RpcClient,
|
|
33
34
|
Signature,
|
|
34
35
|
TransactionRequest,
|
|
@@ -42,6 +43,8 @@ import {
|
|
|
42
43
|
buildUpdateGuardianTransactionRequest,
|
|
43
44
|
buildConsumeNotesTransactionRequest,
|
|
44
45
|
buildP2idTransactionRequest,
|
|
46
|
+
parseP2idNoteType,
|
|
47
|
+
p2idNoteTypeToMetadata,
|
|
45
48
|
} from './transaction.js';
|
|
46
49
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
47
50
|
import {
|
|
@@ -73,7 +76,11 @@ import { AccountInspector } from './inspector.js';
|
|
|
73
76
|
import { ProposalFactory } from './proposal/factory.js';
|
|
74
77
|
import { ProposalMetadataCodec } from './proposal/metadata.js';
|
|
75
78
|
import { ProposalSignatures } from './proposal/signatures.js';
|
|
76
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
getRawMidenClient,
|
|
81
|
+
getTransactionProver,
|
|
82
|
+
requireMidenRpcEndpoint,
|
|
83
|
+
} from './raw-client.js';
|
|
77
84
|
|
|
78
85
|
/**
|
|
79
86
|
* Result of fetching account state from GUARDIAN.
|
|
@@ -138,7 +145,7 @@ export class Multisig {
|
|
|
138
145
|
private readonly rawClientPromise: Promise<WasmWebClient>;
|
|
139
146
|
private readonly transactionProver: TransactionProver | null;
|
|
140
147
|
private readonly _accountId: string;
|
|
141
|
-
private readonly midenRpcEndpoint
|
|
148
|
+
private readonly midenRpcEndpoint: string;
|
|
142
149
|
private proposals: Map<string, Proposal> = new Map();
|
|
143
150
|
|
|
144
151
|
constructor(
|
|
@@ -147,8 +154,8 @@ export class Multisig {
|
|
|
147
154
|
guardian: GuardianHttpClient,
|
|
148
155
|
signer: Signer,
|
|
149
156
|
midenClient: MidenClient,
|
|
150
|
-
accountId
|
|
151
|
-
midenRpcEndpoint
|
|
157
|
+
accountId: string | undefined,
|
|
158
|
+
midenRpcEndpoint: string
|
|
152
159
|
) {
|
|
153
160
|
this.account = account;
|
|
154
161
|
this.threshold = config.threshold;
|
|
@@ -162,15 +169,12 @@ export class Multisig {
|
|
|
162
169
|
this.signer = signer;
|
|
163
170
|
this.midenClient = midenClient;
|
|
164
171
|
this._accountId = accountId ?? (account ? accountIdToHex(account) : '');
|
|
165
|
-
this.midenRpcEndpoint = midenRpcEndpoint;
|
|
166
|
-
this.rawClientPromise = getRawMidenClient(midenClient, midenRpcEndpoint);
|
|
172
|
+
this.midenRpcEndpoint = requireMidenRpcEndpoint(midenRpcEndpoint);
|
|
167
173
|
this.transactionProver = getTransactionProver(midenClient);
|
|
174
|
+
this.rawClientPromise = getRawMidenClient(midenClient, this.midenRpcEndpoint);
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
private getMidenRpcEndpoint(): string {
|
|
171
|
-
if (!this.midenRpcEndpoint) {
|
|
172
|
-
throw new Error('Missing Miden RPC endpoint in MultisigClient configuration');
|
|
173
|
-
}
|
|
174
178
|
return this.midenRpcEndpoint;
|
|
175
179
|
}
|
|
176
180
|
|
|
@@ -213,6 +217,19 @@ export class Multisig {
|
|
|
213
217
|
return this.signer.commitment;
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Resolve the account from the web client's store, falling back to the
|
|
222
|
+
* `account` snapshot when the store has no record.
|
|
223
|
+
*
|
|
224
|
+
* Transaction execution reads the store, and other flows (e.g. consume-notes
|
|
225
|
+
* finalize) update it without refreshing the snapshot, so vault lookups must
|
|
226
|
+
* source from the store to see the same state execution will.
|
|
227
|
+
*/
|
|
228
|
+
async getStoreAccount(): Promise<Account> {
|
|
229
|
+
const webClient = await this.getRawClient();
|
|
230
|
+
return (await webClient.getAccount(AccountId.fromHex(this._accountId))) ?? this.account;
|
|
231
|
+
}
|
|
232
|
+
|
|
216
233
|
/**
|
|
217
234
|
* Maps a proposal type to the procedure that determines its threshold.
|
|
218
235
|
*/
|
|
@@ -286,7 +303,13 @@ export class Multisig {
|
|
|
286
303
|
* Sync account state from GUARDIAN into the local Miden client store.
|
|
287
304
|
*
|
|
288
305
|
* If the GUARDIAN commitment differs from the local commitment (or the account
|
|
289
|
-
* is missing locally)
|
|
306
|
+
* is missing locally) and the GUARDIAN state is safe to import, the local store
|
|
307
|
+
* is overwritten with the GUARDIAN state. When the GUARDIAN is merely *behind*
|
|
308
|
+
* local — e.g. the pushed execution delta has not been canonicalized yet
|
|
309
|
+
* (see OpenZeppelin/guardian#316) — the local state is already ahead and
|
|
310
|
+
* on-chain-verifiable, so it is kept as authoritative. Either way, config is
|
|
311
|
+
* refreshed from the resulting account so callers reading `Multisig.account`
|
|
312
|
+
* (e.g. the UI) observe the current state instead of a stale snapshot.
|
|
290
313
|
*/
|
|
291
314
|
async syncState(): Promise<AccountState> {
|
|
292
315
|
const state = await this.fetchState();
|
|
@@ -303,9 +326,10 @@ export class Multisig {
|
|
|
303
326
|
if (!localAccount || localCommitment !== guardianCommitment) {
|
|
304
327
|
const accountBytes = base64ToUint8Array(state.stateDataBase64);
|
|
305
328
|
const incomingAccount = Account.deserialize(accountBytes);
|
|
306
|
-
await this.
|
|
307
|
-
|
|
308
|
-
|
|
329
|
+
if (await this.isSafeToOverwriteLocalState(incomingAccount, localAccount)) {
|
|
330
|
+
await webClient.newAccount(incomingAccount, true);
|
|
331
|
+
accountForConfigRefresh = incomingAccount;
|
|
332
|
+
}
|
|
309
333
|
}
|
|
310
334
|
|
|
311
335
|
this.refreshConfigFromAccount(accountForConfigRefresh);
|
|
@@ -344,17 +368,37 @@ export class Multisig {
|
|
|
344
368
|
};
|
|
345
369
|
}
|
|
346
370
|
|
|
347
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Decide whether GUARDIAN-provided state may overwrite the local store.
|
|
373
|
+
*
|
|
374
|
+
* Returns `false` — rather than throwing — when the GUARDIAN state is simply
|
|
375
|
+
* *behind* local (lower nonce). That happens whenever the execution delta the
|
|
376
|
+
* client pushed has not been canonicalized by the GUARDIAN's background worker
|
|
377
|
+
* yet (see OpenZeppelin/guardian#316), or permanently if that candidate was
|
|
378
|
+
* discarded (#312 / #319). In that case the local account is already ahead and
|
|
379
|
+
* is independently verifiable against chain (`verifyStateCommitment`), so it is
|
|
380
|
+
* authoritative and must be kept, not clobbered; the caller keeps local and
|
|
381
|
+
* refreshes config from it.
|
|
382
|
+
*
|
|
383
|
+
* Still throws for genuine divergence: an incoming state at the *same* nonce as
|
|
384
|
+
* local but a different commitment, or an incoming state whose commitment does
|
|
385
|
+
* not match the on-chain commitment.
|
|
386
|
+
*/
|
|
387
|
+
private async isSafeToOverwriteLocalState(
|
|
348
388
|
incomingAccount: Account,
|
|
349
389
|
localAccount?: Account,
|
|
350
|
-
): Promise<
|
|
390
|
+
): Promise<boolean> {
|
|
351
391
|
if (localAccount) {
|
|
352
392
|
const localNonce = localAccount.nonce().asInt();
|
|
353
393
|
const incomingNonce = incomingAccount.nonce().asInt();
|
|
354
394
|
|
|
355
|
-
if (incomingNonce
|
|
395
|
+
if (incomingNonce < localNonce) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (incomingNonce === localNonce) {
|
|
356
400
|
throw new Error(
|
|
357
|
-
`Refusing to overwrite local state: incoming nonce ${incomingNonce.toString()}
|
|
401
|
+
`Refusing to overwrite local state: incoming nonce ${incomingNonce.toString()} equals local nonce ${localNonce.toString()} but commitments differ for account ${this._accountId}`
|
|
358
402
|
);
|
|
359
403
|
}
|
|
360
404
|
}
|
|
@@ -362,7 +406,7 @@ export class Multisig {
|
|
|
362
406
|
const accountId = AccountId.fromHex(this._accountId);
|
|
363
407
|
const onChainCommitment = await this.getOnChainCommitment(accountId);
|
|
364
408
|
if (!onChainCommitment) {
|
|
365
|
-
return;
|
|
409
|
+
return true;
|
|
366
410
|
}
|
|
367
411
|
|
|
368
412
|
const incomingCommitment = normalizeHexWord(incomingAccount.to_commitment().toHex());
|
|
@@ -371,6 +415,8 @@ export class Multisig {
|
|
|
371
415
|
`Refusing to overwrite local state: incoming commitment does not match on-chain commitment for account ${this._accountId}`
|
|
372
416
|
);
|
|
373
417
|
}
|
|
418
|
+
|
|
419
|
+
return true;
|
|
374
420
|
}
|
|
375
421
|
|
|
376
422
|
private async getOnChainCommitment(accountId: AccountId): Promise<string | null> {
|
|
@@ -807,23 +853,30 @@ export class Multisig {
|
|
|
807
853
|
* @param faucetId - Faucet/token account ID (hex string)
|
|
808
854
|
* @param amount - Amount to send
|
|
809
855
|
* @param nonce - Optional proposal nonce (defaults to Date.now())
|
|
856
|
+
* @param options - Optional settings; `noteType` selects the created note's
|
|
857
|
+
* visibility (defaults to `NoteType.Public`, issue #322)
|
|
810
858
|
*/
|
|
811
859
|
async createP2idProposal(
|
|
812
860
|
recipientId: string,
|
|
813
861
|
faucetId: string,
|
|
814
862
|
amount: bigint,
|
|
815
863
|
nonce?: number,
|
|
864
|
+
options: { noteType?: NoteType } = {},
|
|
816
865
|
): Promise<Proposal> {
|
|
817
866
|
const webClient = await this.getRawClient();
|
|
818
867
|
if (amount <= 0n) {
|
|
819
868
|
throw new Error('Amount must be greater than 0');
|
|
820
869
|
}
|
|
821
870
|
|
|
871
|
+
const account = await this.getStoreAccount();
|
|
872
|
+
|
|
822
873
|
const { request, salt } = buildP2idTransactionRequest(
|
|
823
874
|
this._accountId,
|
|
824
875
|
recipientId,
|
|
825
876
|
faucetId,
|
|
826
877
|
amount,
|
|
878
|
+
account,
|
|
879
|
+
{ noteType: options.noteType },
|
|
827
880
|
);
|
|
828
881
|
|
|
829
882
|
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
@@ -837,6 +890,8 @@ export class Multisig {
|
|
|
837
890
|
recipientId,
|
|
838
891
|
faucetId,
|
|
839
892
|
amount: amount.toString(),
|
|
893
|
+
// Omitted for public notes so the wire shape matches pre-#322 proposals.
|
|
894
|
+
noteType: p2idNoteTypeToMetadata(options.noteType),
|
|
840
895
|
description: `Send ${amount} of asset ${faucetId.slice(0, 10)}... to ${recipientId.slice(0, 10)}...`,
|
|
841
896
|
};
|
|
842
897
|
|
|
@@ -902,6 +957,38 @@ export class Multisig {
|
|
|
902
957
|
*
|
|
903
958
|
* @param proposalId - The proposal commitment/ID (this is also what gets signed)
|
|
904
959
|
*/
|
|
960
|
+
/**
|
|
961
|
+
* Request abandonment of a pending canonicalization candidate whose
|
|
962
|
+
* transaction will never land on-chain (issue #319) — e.g. after an
|
|
963
|
+
* approved transaction died client-side (RPC submit failure, prover
|
|
964
|
+
* timeout, crash).
|
|
965
|
+
*
|
|
966
|
+
* Records an abandon *intent* on GUARDIAN: the account stays locked
|
|
967
|
+
* until the guardian's canonicalization worker confirms over a short
|
|
968
|
+
* quarantine (typically well under a minute) that the transaction did
|
|
969
|
+
* not land, then releases the account. Poll {@link abandonStatus} for
|
|
970
|
+
* the resolution.
|
|
971
|
+
*
|
|
972
|
+
* `nonce` pins the exact candidate to release; it is the nonce the
|
|
973
|
+
* proposal was pushed with. Retries are idempotent and preserve the
|
|
974
|
+
* original request timestamp. Refused with `GUARDIAN_CANDIDATE_LANDED`
|
|
975
|
+
* (409) when the transaction actually landed.
|
|
976
|
+
*/
|
|
977
|
+
async abandonCandidate(nonce: number): Promise<AbandonCandidateResponse> {
|
|
978
|
+
return this.guardian.abandonCandidate(this._accountId, nonce);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Poll the resolution of an abandon request made with
|
|
983
|
+
* {@link abandonCandidate}: `'waiting'` while the quarantine runs,
|
|
984
|
+
* `'landed'` if the transaction landed after all, `'abandoned'` once
|
|
985
|
+
* the account is released, `'unexpected'` for any state no abandon
|
|
986
|
+
* flow produces.
|
|
987
|
+
*/
|
|
988
|
+
async abandonStatus(nonce: number): Promise<AbandonStatus> {
|
|
989
|
+
return this.guardian.abandonStatus(this._accountId, nonce);
|
|
990
|
+
}
|
|
991
|
+
|
|
905
992
|
async signProposal(proposalId: string): Promise<Proposal> {
|
|
906
993
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
907
994
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
@@ -1678,12 +1765,14 @@ export class Multisig {
|
|
|
1678
1765
|
throw new UnsupportedMetadataVersionError(version);
|
|
1679
1766
|
}
|
|
1680
1767
|
case 'p2id': {
|
|
1768
|
+
const account = await this.getStoreAccount();
|
|
1681
1769
|
const { request } = buildP2idTransactionRequest(
|
|
1682
1770
|
this._accountId,
|
|
1683
1771
|
metadata.recipientId,
|
|
1684
1772
|
metadata.faucetId,
|
|
1685
1773
|
BigInt(metadata.amount),
|
|
1686
|
-
|
|
1774
|
+
account,
|
|
1775
|
+
{ salt, signatureAdviceMap, noteType: parseP2idNoteType(metadata.noteType) }
|
|
1687
1776
|
);
|
|
1688
1777
|
return request;
|
|
1689
1778
|
}
|
|
@@ -4,6 +4,7 @@ import { ProposalMetadataCodec } from './metadata.js';
|
|
|
4
4
|
import type {
|
|
5
5
|
ConsumeNotesProposalMetadata,
|
|
6
6
|
CustomProposalMetadata,
|
|
7
|
+
P2IdProposalMetadata,
|
|
7
8
|
} from '../types/proposal.js';
|
|
8
9
|
|
|
9
10
|
describe('ProposalMetadataCodec consume_notes v2 round-trip (issue #229)', () => {
|
|
@@ -97,3 +98,62 @@ describe('ProposalMetadataCodec custom proposal types (issue #266)', () => {
|
|
|
97
98
|
expect(back.targetThreshold).toBe(2);
|
|
98
99
|
});
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
describe('ProposalMetadataCodec p2id noteType (issue #322)', () => {
|
|
103
|
+
const baseWire: GuardianProposalMetadata = {
|
|
104
|
+
proposalType: 'p2id',
|
|
105
|
+
recipientId: '0xrecipient',
|
|
106
|
+
faucetId: '0xfaucet',
|
|
107
|
+
amount: '1000',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
it('round-trips a private noteType through the codec', () => {
|
|
111
|
+
const md = ProposalMetadataCodec.fromGuardian({
|
|
112
|
+
...baseWire,
|
|
113
|
+
noteType: 'private',
|
|
114
|
+
}) as P2IdProposalMetadata;
|
|
115
|
+
expect(md.noteType).toBe('private');
|
|
116
|
+
|
|
117
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
118
|
+
expect(wire.noteType).toBe('private');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('leaves noteType absent for legacy proposals (=> public)', () => {
|
|
122
|
+
const md = ProposalMetadataCodec.fromGuardian(baseWire) as P2IdProposalMetadata;
|
|
123
|
+
expect(md.noteType).toBeUndefined();
|
|
124
|
+
expect(ProposalMetadataCodec.toGuardian(md).noteType).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('canonicalizes an explicit public noteType to absent on encode', () => {
|
|
128
|
+
const md = {
|
|
129
|
+
proposalType: 'p2id',
|
|
130
|
+
description: '',
|
|
131
|
+
recipientId: '0xrecipient',
|
|
132
|
+
faucetId: '0xfaucet',
|
|
133
|
+
amount: '1000',
|
|
134
|
+
noteType: 'public',
|
|
135
|
+
} as P2IdProposalMetadata;
|
|
136
|
+
|
|
137
|
+
// toGuardian omits the field so a public note keeps the pre-#322 wire
|
|
138
|
+
// shape and matches the Rust encoder, even if handed an explicit 'public'.
|
|
139
|
+
expect(ProposalMetadataCodec.toGuardian(md).noteType).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('fromGuardian rejects an unsupported noteType', () => {
|
|
143
|
+
expect(() =>
|
|
144
|
+
ProposalMetadataCodec.fromGuardian({ ...baseWire, noteType: 'encrypted' }),
|
|
145
|
+
).toThrow(/unsupported noteType/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('validate rejects an unsupported noteType', () => {
|
|
149
|
+
const md = {
|
|
150
|
+
proposalType: 'p2id',
|
|
151
|
+
description: '',
|
|
152
|
+
recipientId: '0xrecipient',
|
|
153
|
+
faucetId: '0xfaucet',
|
|
154
|
+
amount: '1000',
|
|
155
|
+
noteType: 'encrypted',
|
|
156
|
+
} as unknown as P2IdProposalMetadata;
|
|
157
|
+
expect(() => ProposalMetadataCodec.validate(md)).toThrow(/unsupported noteType/);
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/proposal/metadata.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ProposalMetadata as GuardianProposalMetadata } from '@openzeppelin/guardian-client';
|
|
2
2
|
import type { ProposalMetadata } from '../types.js';
|
|
3
3
|
import { isProcedureName } from '../procedures.js';
|
|
4
|
+
import { isP2idNoteVisibility } from '../types/proposal.js';
|
|
4
5
|
|
|
5
6
|
export class ProposalMetadataCodec {
|
|
6
7
|
static toGuardian(metadata: ProposalMetadata): GuardianProposalMetadata {
|
|
@@ -25,6 +26,10 @@ export class ProposalMetadataCodec {
|
|
|
25
26
|
recipientId: metadata.recipientId,
|
|
26
27
|
faucetId: metadata.faucetId,
|
|
27
28
|
amount: metadata.amount,
|
|
29
|
+
// Canonicalize: emit note_type only when private, so a public note
|
|
30
|
+
// keeps the pre-#322 wire shape and matches the Rust encoder (which
|
|
31
|
+
// round-trips through the NoteType enum). Absent => public.
|
|
32
|
+
noteType: metadata.noteType === 'private' ? 'private' : undefined,
|
|
28
33
|
};
|
|
29
34
|
case 'switch_guardian':
|
|
30
35
|
return {
|
|
@@ -70,12 +75,18 @@ export class ProposalMetadataCodec {
|
|
|
70
75
|
if (!guardian.recipientId || !guardian.faucetId || !guardian.amount) {
|
|
71
76
|
throw new Error('p2id proposal is missing required metadata fields');
|
|
72
77
|
}
|
|
78
|
+
if (guardian.noteType !== undefined && !isP2idNoteVisibility(guardian.noteType)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`p2id proposal has unsupported noteType '${guardian.noteType}': expected 'public' or 'private'`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
73
83
|
return {
|
|
74
84
|
...base,
|
|
75
85
|
proposalType: 'p2id',
|
|
76
86
|
recipientId: guardian.recipientId,
|
|
77
87
|
faucetId: guardian.faucetId,
|
|
78
88
|
amount: guardian.amount,
|
|
89
|
+
noteType: guardian.noteType,
|
|
79
90
|
};
|
|
80
91
|
case 'consume_notes':
|
|
81
92
|
if (!guardian.noteIds || guardian.noteIds.length === 0) {
|
|
@@ -168,6 +179,9 @@ export class ProposalMetadataCodec {
|
|
|
168
179
|
if (!metadata.recipientId || !metadata.faucetId || !metadata.amount) {
|
|
169
180
|
throw new Error('p2id proposal metadata is incomplete');
|
|
170
181
|
}
|
|
182
|
+
if (metadata.noteType !== undefined && !isP2idNoteVisibility(metadata.noteType)) {
|
|
183
|
+
throw new Error(`p2id proposal has unsupported noteType '${metadata.noteType}'`);
|
|
184
|
+
}
|
|
171
185
|
return metadata;
|
|
172
186
|
case 'custom':
|
|
173
187
|
// Custom proposals are opaque to the SDK; nothing to validate beyond
|
package/src/raw-client.test.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
compileTxScript,
|
|
15
15
|
getRawMidenClient,
|
|
16
16
|
getTransactionProver,
|
|
17
|
-
|
|
17
|
+
requireConfigValue,
|
|
18
18
|
} from './raw-client.js';
|
|
19
19
|
|
|
20
20
|
describe('raw-client', () => {
|
|
@@ -22,8 +22,46 @@ describe('raw-client', () => {
|
|
|
22
22
|
mockCreateClient.mockReset();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it(
|
|
26
|
-
|
|
25
|
+
it.each([undefined, null, 42, {}, []])(
|
|
26
|
+
'rejects non-string configuration value %j consistently',
|
|
27
|
+
(value) => {
|
|
28
|
+
expect(() => requireConfigValue('guardianEndpoint', value)).toThrow(
|
|
29
|
+
'missing required configuration: guardianEndpoint',
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
it('trims surrounding whitespace from configuration values', () => {
|
|
35
|
+
expect(requireConfigValue('guardianEndpoint', ' http://localhost:3000\n')).toBe(
|
|
36
|
+
'http://localhost:3000',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects shadow client creation without an RPC endpoint', async () => {
|
|
41
|
+
const client = {
|
|
42
|
+
accounts: {},
|
|
43
|
+
sync: vi.fn(),
|
|
44
|
+
defaultProver: null,
|
|
45
|
+
storeIdentifier: vi.fn(() => 'browser-db'),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await expect(getRawMidenClient(client as any)).rejects.toThrow(
|
|
49
|
+
'missing required configuration: midenRpcEndpoint',
|
|
50
|
+
);
|
|
51
|
+
await expect(getRawMidenClient(client as any, ' ')).rejects.toThrow(
|
|
52
|
+
'missing required configuration: midenRpcEndpoint',
|
|
53
|
+
);
|
|
54
|
+
expect(mockCreateClient).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns an injected raw web client without needing an endpoint', async () => {
|
|
58
|
+
const rawClient = {
|
|
59
|
+
executeTransaction: vi.fn(),
|
|
60
|
+
proveTransaction: vi.fn(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await expect(getRawMidenClient(rawClient as any)).resolves.toBe(rawClient);
|
|
64
|
+
expect(mockCreateClient).not.toHaveBeenCalled();
|
|
27
65
|
});
|
|
28
66
|
|
|
29
67
|
it('returns the default prover from a public MidenClient', () => {
|
package/src/raw-client.ts
CHANGED
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
WasmWebClient,
|
|
6
6
|
} from '@miden-sdk/miden-sdk';
|
|
7
7
|
|
|
8
|
-
export const DEFAULT_MIDEN_RPC_URL = 'https://rpc.devnet.miden.io';
|
|
9
|
-
|
|
10
8
|
export type RawClientSource = MidenClient | WasmWebClient;
|
|
11
9
|
export interface ScriptLibrarySource {
|
|
12
10
|
namespace: string;
|
|
@@ -16,8 +14,19 @@ export interface ScriptLibrarySource {
|
|
|
16
14
|
|
|
17
15
|
const rawClientCache = new WeakMap<MidenClient, Promise<WasmWebClient>>();
|
|
18
16
|
|
|
19
|
-
export function
|
|
20
|
-
|
|
17
|
+
export function requireConfigValue(field: string, value?: unknown): string {
|
|
18
|
+
if (typeof value !== 'string') {
|
|
19
|
+
throw new Error(`missing required configuration: ${field}`);
|
|
20
|
+
}
|
|
21
|
+
const normalizedValue = value.trim();
|
|
22
|
+
if (normalizedValue === '') {
|
|
23
|
+
throw new Error(`missing required configuration: ${field}`);
|
|
24
|
+
}
|
|
25
|
+
return normalizedValue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireMidenRpcEndpoint(endpoint?: string): string {
|
|
29
|
+
return requireConfigValue('midenRpcEndpoint', endpoint);
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
function isPublicMidenClient(client: RawClientSource): client is MidenClient {
|
|
@@ -37,8 +46,9 @@ export async function getRawMidenClient(
|
|
|
37
46
|
return cached;
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
const endpoint = requireMidenRpcEndpoint(rpcUrl);
|
|
40
50
|
const rawClient = WasmWebClient.createClient(
|
|
41
|
-
|
|
51
|
+
endpoint,
|
|
42
52
|
undefined,
|
|
43
53
|
undefined,
|
|
44
54
|
await client.storeIdentifier(),
|
|
@@ -15,7 +15,7 @@ import { LegacyConsumeNotesNoteMissingError } from '../multisig/consumeNotesErro
|
|
|
15
15
|
import { getRawMidenClient } from '../raw-client.js';
|
|
16
16
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
17
17
|
import { randomWord } from '../utils/random.js';
|
|
18
|
-
import type { SignatureOptions } from './options.js';
|
|
18
|
+
import type { MidenClientSignatureOptions, SignatureOptions } from './options.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Build a consume-notes request from loaded `Note` objects (no local-store
|
|
@@ -57,6 +57,16 @@ export function buildConsumeNotesTransactionRequestFromNotes(
|
|
|
57
57
|
* Legacy/creation adapter: fetches notes from the local store and delegates
|
|
58
58
|
* to the from-notes variant. v2 verification MUST NOT call this.
|
|
59
59
|
*/
|
|
60
|
+
export function buildConsumeNotesTransactionRequest(
|
|
61
|
+
client: MidenClient,
|
|
62
|
+
noteIds: string[],
|
|
63
|
+
options: MidenClientSignatureOptions,
|
|
64
|
+
): Promise<{ request: TransactionRequest; salt: Word }>;
|
|
65
|
+
export function buildConsumeNotesTransactionRequest(
|
|
66
|
+
client: WasmWebClient,
|
|
67
|
+
noteIds: string[],
|
|
68
|
+
options?: SignatureOptions,
|
|
69
|
+
): Promise<{ request: TransactionRequest; salt: Word }>;
|
|
60
70
|
export async function buildConsumeNotesTransactionRequest(
|
|
61
71
|
client: MidenClient | WasmWebClient,
|
|
62
72
|
noteIds: string[],
|