@openzeppelin/miden-multisig-client 0.14.6 → 0.14.8
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 +67 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/multisig/config.d.ts +8 -0
- package/dist/multisig/config.d.ts.map +1 -0
- package/dist/multisig/config.js +8 -0
- package/dist/multisig/config.js.map +1 -0
- package/dist/multisig/consumeNotesErrors.d.ts +48 -0
- package/dist/multisig/consumeNotesErrors.d.ts.map +1 -0
- package/dist/multisig/consumeNotesErrors.js +58 -0
- package/dist/multisig/consumeNotesErrors.js.map +1 -0
- package/dist/multisig/consumeNotesErrors.test.d.ts +2 -0
- package/dist/multisig/consumeNotesErrors.test.d.ts.map +1 -0
- package/dist/multisig/consumeNotesErrors.test.js +28 -0
- package/dist/multisig/consumeNotesErrors.test.js.map +1 -0
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +63 -4
- package/dist/multisig.js.map +1 -1
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +4 -0
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/metadata.test.d.ts +2 -0
- package/dist/proposal/metadata.test.d.ts.map +1 -0
- package/dist/proposal/metadata.test.js +43 -0
- package/dist/proposal/metadata.test.js.map +1 -0
- package/dist/transaction/consumeNotes.d.ts +13 -1
- package/dist/transaction/consumeNotes.d.ts.map +1 -1
- package/dist/transaction/consumeNotes.js +30 -13
- package/dist/transaction/consumeNotes.js.map +1 -1
- package/dist/types/proposal.d.ts +10 -0
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js +10 -1
- package/dist/types/proposal.js.map +1 -1
- package/dist/types/proposal.test.d.ts +2 -0
- package/dist/types/proposal.test.d.ts.map +1 -0
- package/dist/types/proposal.test.js +56 -0
- package/dist/types/proposal.test.js.map +1 -0
- package/dist/utils/encoding.d.ts +8 -0
- package/dist/utils/encoding.d.ts.map +1 -1
- package/dist/utils/encoding.js +8 -0
- package/dist/utils/encoding.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +24 -0
- package/src/multisig/config.ts +7 -0
- package/src/multisig/consumeNotesErrors.test.ts +43 -0
- package/src/multisig/consumeNotesErrors.ts +74 -0
- package/src/multisig.ts +81 -7
- package/src/proposal/metadata.test.ts +47 -0
- package/src/proposal/metadata.ts +4 -0
- package/src/transaction/consumeNotes.ts +40 -17
- package/src/types/proposal.test.ts +68 -0
- package/src/types/proposal.ts +18 -0
- package/src/utils/encoding.ts +13 -0
package/src/multisig.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
AdviceMap,
|
|
29
29
|
Endpoint,
|
|
30
30
|
FeltArray,
|
|
31
|
+
Note,
|
|
31
32
|
RpcClient,
|
|
32
33
|
Signature,
|
|
33
34
|
TransactionSummary,
|
|
@@ -41,6 +42,19 @@ import {
|
|
|
41
42
|
buildConsumeNotesTransactionRequest,
|
|
42
43
|
buildP2idTransactionRequest,
|
|
43
44
|
} from './transaction.js';
|
|
45
|
+
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
46
|
+
import {
|
|
47
|
+
CONSUME_NOTES_METADATA_VERSION_V2,
|
|
48
|
+
MAX_CONSUME_NOTES_METADATA_BYTES,
|
|
49
|
+
} from './types/proposal.js';
|
|
50
|
+
import { LEGACY_CONSUME_NOTES_ENABLED } from './multisig/config.js';
|
|
51
|
+
import {
|
|
52
|
+
ConsumeNotesMetadataOversizeError,
|
|
53
|
+
LegacyConsumeNotesNoteMissingError,
|
|
54
|
+
NoteBindingMismatchError,
|
|
55
|
+
UnsupportedMetadataVersionError,
|
|
56
|
+
} from './multisig/consumeNotesErrors.js';
|
|
57
|
+
import { noteFromBase64, noteToBase64 } from './utils/encoding.js';
|
|
44
58
|
import {
|
|
45
59
|
base64ToUint8Array,
|
|
46
60
|
uint8ArrayToBase64,
|
|
@@ -728,7 +742,19 @@ export class Multisig {
|
|
|
728
742
|
throw new Error('At least one note ID is required');
|
|
729
743
|
}
|
|
730
744
|
|
|
731
|
-
|
|
745
|
+
// Fetch notes locally (proposer has them per FR-012); embed for v2 verification.
|
|
746
|
+
const rawClient = await getRawMidenClient(webClient);
|
|
747
|
+
const fetchedNotes: Note[] = [];
|
|
748
|
+
for (const noteIdHex of noteIds) {
|
|
749
|
+
const inputNoteRecord = await rawClient.getInputNote(noteIdHex);
|
|
750
|
+
if (!inputNoteRecord) {
|
|
751
|
+
throw new LegacyConsumeNotesNoteMissingError(noteIdHex);
|
|
752
|
+
}
|
|
753
|
+
fetchedNotes.push(inputNoteRecord.toNote());
|
|
754
|
+
}
|
|
755
|
+
const embeddedNotes = fetchedNotes.map((n) => noteToBase64(n));
|
|
756
|
+
|
|
757
|
+
const { request, salt } = buildConsumeNotesTransactionRequestFromNotes(fetchedNotes);
|
|
732
758
|
|
|
733
759
|
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
734
760
|
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
@@ -737,11 +763,23 @@ export class Multisig {
|
|
|
737
763
|
const metadata: ProposalMetadata = {
|
|
738
764
|
proposalType: 'consume_notes',
|
|
739
765
|
noteIds,
|
|
766
|
+
metadataVersion: CONSUME_NOTES_METADATA_VERSION_V2,
|
|
767
|
+
notes: embeddedNotes,
|
|
740
768
|
saltHex: salt.toHex(),
|
|
741
769
|
requiredSignatures: this.getEffectiveThreshold('consume_notes'),
|
|
742
770
|
description: `Consume ${noteIds.length} note(s)`,
|
|
743
771
|
};
|
|
744
772
|
|
|
773
|
+
// FR-011: enforce metadata size cap on the wire-encoded form (what GUARDIAN
|
|
774
|
+
// actually persists), matching the Rust side which measures
|
|
775
|
+
// `ProposalMetadataPayload`. Sizing the local in-memory `metadata` would
|
|
776
|
+
// miss codec divergence.
|
|
777
|
+
const encoded = ProposalMetadataCodec.toGuardian(metadata);
|
|
778
|
+
const metadataSize = new TextEncoder().encode(JSON.stringify(encoded)).length;
|
|
779
|
+
if (metadataSize > MAX_CONSUME_NOTES_METADATA_BYTES) {
|
|
780
|
+
throw new ConsumeNotesMetadataOversizeError(MAX_CONSUME_NOTES_METADATA_BYTES, metadataSize);
|
|
781
|
+
}
|
|
782
|
+
|
|
745
783
|
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
746
784
|
}
|
|
747
785
|
|
|
@@ -1319,12 +1357,48 @@ export class Multisig {
|
|
|
1319
1357
|
return request;
|
|
1320
1358
|
}
|
|
1321
1359
|
case 'consume_notes': {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1360
|
+
// v1/v2 dispatch for issue #229 / FR-009.
|
|
1361
|
+
const version = metadata.metadataVersion;
|
|
1362
|
+
if (version === CONSUME_NOTES_METADATA_VERSION_V2) {
|
|
1363
|
+
const embedded = metadata.notes ?? [];
|
|
1364
|
+
if (embedded.length !== metadata.noteIds.length) {
|
|
1365
|
+
throw new NoteBindingMismatchError(
|
|
1366
|
+
`consume_notes v2: notes.length=${embedded.length} does not match noteIds.length=${metadata.noteIds.length}`,
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
const decoded: Note[] = [];
|
|
1370
|
+
for (let i = 0; i < embedded.length; i++) {
|
|
1371
|
+
const note = noteFromBase64(embedded[i], Note);
|
|
1372
|
+
// Normalize both sides; matches the file's other hex comparisons.
|
|
1373
|
+
const embeddedId = normalizeHexWord(note.id().toString());
|
|
1374
|
+
const declaredId = normalizeHexWord(metadata.noteIds[i]);
|
|
1375
|
+
if (embeddedId !== declaredId) {
|
|
1376
|
+
throw new NoteBindingMismatchError(
|
|
1377
|
+
`consume_notes v2: notes[${i}] id ${embeddedId} != noteIds[${i}] ${declaredId}`,
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
decoded.push(note);
|
|
1381
|
+
}
|
|
1382
|
+
const { request } = buildConsumeNotesTransactionRequestFromNotes(decoded, {
|
|
1383
|
+
salt,
|
|
1384
|
+
signatureAdviceMap,
|
|
1385
|
+
});
|
|
1386
|
+
return request;
|
|
1387
|
+
}
|
|
1388
|
+
if (version === undefined || version === 1) {
|
|
1389
|
+
if (!LEGACY_CONSUME_NOTES_ENABLED) {
|
|
1390
|
+
// Preserve explicit `1` vs absent so the error tells the
|
|
1391
|
+
// operator which legacy shape was rejected.
|
|
1392
|
+
throw new UnsupportedMetadataVersionError(version);
|
|
1393
|
+
}
|
|
1394
|
+
const { request } = await buildConsumeNotesTransactionRequest(
|
|
1395
|
+
webClient,
|
|
1396
|
+
metadata.noteIds,
|
|
1397
|
+
{ salt, signatureAdviceMap },
|
|
1398
|
+
);
|
|
1399
|
+
return request;
|
|
1400
|
+
}
|
|
1401
|
+
throw new UnsupportedMetadataVersionError(version);
|
|
1328
1402
|
}
|
|
1329
1403
|
case 'p2id': {
|
|
1330
1404
|
const { request } = buildP2idTransactionRequest(
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ProposalMetadata as GuardianProposalMetadata } from '@openzeppelin/guardian-client';
|
|
3
|
+
import { ProposalMetadataCodec } from './metadata.js';
|
|
4
|
+
import type { ConsumeNotesProposalMetadata } from '../types/proposal.js';
|
|
5
|
+
|
|
6
|
+
describe('ProposalMetadataCodec consume_notes v2 round-trip (issue #229)', () => {
|
|
7
|
+
it('toGuardian threads metadataVersion and notes to the wire', () => {
|
|
8
|
+
const md: ConsumeNotesProposalMetadata = {
|
|
9
|
+
proposalType: 'consume_notes',
|
|
10
|
+
description: 'consume one note',
|
|
11
|
+
noteIds: ['0xabc'],
|
|
12
|
+
metadataVersion: 2,
|
|
13
|
+
notes: ['YmFzZTY0Tm90ZQ=='],
|
|
14
|
+
};
|
|
15
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
16
|
+
expect(wire.noteIds).toEqual(['0xabc']);
|
|
17
|
+
expect(wire.consumeNotesMetadataVersion).toBe(2);
|
|
18
|
+
expect(wire.consumeNotesNotes).toEqual(['YmFzZTY0Tm90ZQ==']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('fromGuardian reconstructs the v2 fields', () => {
|
|
22
|
+
const wire: GuardianProposalMetadata = {
|
|
23
|
+
proposalType: 'consume_notes',
|
|
24
|
+
noteIds: ['0xabc'],
|
|
25
|
+
consumeNotesMetadataVersion: 2,
|
|
26
|
+
consumeNotesNotes: ['YmFzZTY0Tm90ZQ=='],
|
|
27
|
+
};
|
|
28
|
+
const md = ProposalMetadataCodec.fromGuardian(wire) as ConsumeNotesProposalMetadata;
|
|
29
|
+
expect(md.proposalType).toBe('consume_notes');
|
|
30
|
+
expect(md.metadataVersion).toBe(2);
|
|
31
|
+
expect(md.notes).toEqual(['YmFzZTY0Tm90ZQ==']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('round-trips a v1 (legacy) proposal without spurious v2 fields', () => {
|
|
35
|
+
const md: ConsumeNotesProposalMetadata = {
|
|
36
|
+
proposalType: 'consume_notes',
|
|
37
|
+
description: 'legacy',
|
|
38
|
+
noteIds: ['0xabc'],
|
|
39
|
+
};
|
|
40
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
41
|
+
expect(wire.consumeNotesMetadataVersion).toBeUndefined();
|
|
42
|
+
expect(wire.consumeNotesNotes).toBeUndefined();
|
|
43
|
+
const back = ProposalMetadataCodec.fromGuardian(wire) as ConsumeNotesProposalMetadata;
|
|
44
|
+
expect(back.metadataVersion).toBeUndefined();
|
|
45
|
+
expect(back.notes).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/proposal/metadata.ts
CHANGED
|
@@ -16,6 +16,8 @@ export class ProposalMetadataCodec {
|
|
|
16
16
|
return {
|
|
17
17
|
...base,
|
|
18
18
|
noteIds: metadata.noteIds,
|
|
19
|
+
consumeNotesMetadataVersion: metadata.metadataVersion,
|
|
20
|
+
consumeNotesNotes: metadata.notes,
|
|
19
21
|
};
|
|
20
22
|
case 'p2id':
|
|
21
23
|
return {
|
|
@@ -82,6 +84,8 @@ export class ProposalMetadataCodec {
|
|
|
82
84
|
...base,
|
|
83
85
|
proposalType: 'consume_notes',
|
|
84
86
|
noteIds: guardian.noteIds,
|
|
87
|
+
metadataVersion: guardian.consumeNotesMetadataVersion as 1 | 2 | undefined,
|
|
88
|
+
notes: guardian.consumeNotesNotes,
|
|
85
89
|
};
|
|
86
90
|
case 'switch_guardian':
|
|
87
91
|
if (!guardian.newGuardianPubkey || !guardian.newGuardianEndpoint) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
MidenClient,
|
|
3
|
+
Note,
|
|
3
4
|
TransactionRequest,
|
|
4
5
|
WasmWebClient,
|
|
5
6
|
Word,
|
|
@@ -10,34 +11,30 @@ import {
|
|
|
10
11
|
TransactionRequestBuilder,
|
|
11
12
|
Word as WordType,
|
|
12
13
|
} from '@miden-sdk/miden-sdk';
|
|
14
|
+
import { LegacyConsumeNotesNoteMissingError } from '../multisig/consumeNotesErrors.js';
|
|
13
15
|
import { getRawMidenClient } from '../raw-client.js';
|
|
14
|
-
import { randomWord } from '../utils/random.js';
|
|
15
16
|
import { normalizeHexWord } from '../utils/encoding.js';
|
|
17
|
+
import { randomWord } from '../utils/random.js';
|
|
16
18
|
import type { SignatureOptions } from './options.js';
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Build a consume-notes request from loaded `Note` objects (no local-store
|
|
22
|
+
* read). v2 verification path for issue #229.
|
|
23
|
+
*/
|
|
24
|
+
export function buildConsumeNotesTransactionRequestFromNotes(
|
|
25
|
+
notes: Note[],
|
|
21
26
|
options: SignatureOptions = {},
|
|
22
|
-
):
|
|
23
|
-
if (
|
|
24
|
-
throw new Error('At least one note
|
|
27
|
+
): { request: TransactionRequest; salt: Word } {
|
|
28
|
+
if (notes.length === 0) {
|
|
29
|
+
throw new Error('At least one note is required');
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
const rawClient = await getRawMidenClient(client, options.midenRpcEndpoint);
|
|
28
32
|
const noteAndArgsArray = new NoteAndArgsArray();
|
|
29
|
-
for (const
|
|
30
|
-
|
|
31
|
-
if (!inputNoteRecord) {
|
|
32
|
-
throw new Error(`Note not found in local store: ${noteIdHex}`);
|
|
33
|
-
}
|
|
34
|
-
const note = inputNoteRecord.toNote();
|
|
35
|
-
const noteAndArgs = new NoteAndArgs(note, null);
|
|
36
|
-
noteAndArgsArray.push(noteAndArgs);
|
|
33
|
+
for (const note of notes) {
|
|
34
|
+
noteAndArgsArray.push(new NoteAndArgs(note, null));
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
const authSaltHex = options.salt ? options.salt.toHex() : randomWord().toHex();
|
|
40
|
-
|
|
41
38
|
const authSaltForBuilder = WordType.fromHex(normalizeHexWord(authSaltHex));
|
|
42
39
|
|
|
43
40
|
let txBuilder = new TransactionRequestBuilder();
|
|
@@ -55,3 +52,29 @@ export async function buildConsumeNotesTransactionRequest(
|
|
|
55
52
|
salt: authSaltForReturn,
|
|
56
53
|
};
|
|
57
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Legacy/creation adapter: fetches notes from the local store and delegates
|
|
58
|
+
* to the from-notes variant. v2 verification MUST NOT call this.
|
|
59
|
+
*/
|
|
60
|
+
export async function buildConsumeNotesTransactionRequest(
|
|
61
|
+
client: MidenClient | WasmWebClient,
|
|
62
|
+
noteIds: string[],
|
|
63
|
+
options: SignatureOptions = {},
|
|
64
|
+
): Promise<{ request: TransactionRequest; salt: Word }> {
|
|
65
|
+
if (noteIds.length === 0) {
|
|
66
|
+
throw new Error('At least one note ID is required');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rawClient = await getRawMidenClient(client, options.midenRpcEndpoint);
|
|
70
|
+
const notes: Note[] = [];
|
|
71
|
+
for (const noteIdHex of noteIds) {
|
|
72
|
+
const inputNoteRecord = await rawClient.getInputNote(noteIdHex);
|
|
73
|
+
if (!inputNoteRecord) {
|
|
74
|
+
throw new LegacyConsumeNotesNoteMissingError(noteIdHex);
|
|
75
|
+
}
|
|
76
|
+
notes.push(inputNoteRecord.toNote());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return buildConsumeNotesTransactionRequestFromNotes(notes, options);
|
|
80
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
CONSUME_NOTES_METADATA_VERSION_V2,
|
|
4
|
+
type ConsumeNotesProposalMetadata,
|
|
5
|
+
MAX_CONSUME_NOTES_METADATA_BYTES,
|
|
6
|
+
isConsumeNotesV1,
|
|
7
|
+
isConsumeNotesV2,
|
|
8
|
+
} from './proposal.js';
|
|
9
|
+
|
|
10
|
+
describe('consume_notes metadata v1/v2 discriminator (issue #229)', () => {
|
|
11
|
+
it('v1 metadata omits metadataVersion and notes on the wire', () => {
|
|
12
|
+
const md: ConsumeNotesProposalMetadata = {
|
|
13
|
+
proposalType: 'consume_notes',
|
|
14
|
+
description: '',
|
|
15
|
+
noteIds: ['0xabc', '0xdef'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const json = JSON.parse(JSON.stringify(md));
|
|
19
|
+
expect(json.metadataVersion).toBeUndefined();
|
|
20
|
+
expect(json.notes).toBeUndefined();
|
|
21
|
+
expect(isConsumeNotesV1(md)).toBe(true);
|
|
22
|
+
expect(isConsumeNotesV2(md)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('v2 metadata round-trips with discriminator and embedded notes', () => {
|
|
26
|
+
const md: ConsumeNotesProposalMetadata = {
|
|
27
|
+
proposalType: 'consume_notes',
|
|
28
|
+
description: '',
|
|
29
|
+
noteIds: ['0xabc'],
|
|
30
|
+
metadataVersion: CONSUME_NOTES_METADATA_VERSION_V2,
|
|
31
|
+
notes: ['YmFzZTY0Tm90ZQ=='],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const json = JSON.stringify(md);
|
|
35
|
+
const parsed = JSON.parse(json) as ConsumeNotesProposalMetadata;
|
|
36
|
+
expect(parsed.metadataVersion).toBe(2);
|
|
37
|
+
expect(parsed.notes).toEqual(['YmFzZTY0Tm90ZQ==']);
|
|
38
|
+
expect(isConsumeNotesV2(parsed)).toBe(true);
|
|
39
|
+
expect(isConsumeNotesV1(parsed)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('explicit metadataVersion === 1 is treated as v1', () => {
|
|
43
|
+
const md = {
|
|
44
|
+
proposalType: 'consume_notes',
|
|
45
|
+
description: '',
|
|
46
|
+
noteIds: ['0xabc'],
|
|
47
|
+
metadataVersion: 1 as unknown as 2, // legacy explicit-v1 case
|
|
48
|
+
} as ConsumeNotesProposalMetadata;
|
|
49
|
+
expect(isConsumeNotesV1(md)).toBe(true);
|
|
50
|
+
expect(isConsumeNotesV2(md)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('unknown future version is neither v1 nor v2; dispatch handles rejection', () => {
|
|
54
|
+
const md = {
|
|
55
|
+
proposalType: 'consume_notes',
|
|
56
|
+
description: '',
|
|
57
|
+
noteIds: ['0xabc'],
|
|
58
|
+
metadataVersion: 99 as unknown as 2,
|
|
59
|
+
} as ConsumeNotesProposalMetadata;
|
|
60
|
+
expect(isConsumeNotesV1(md)).toBe(false);
|
|
61
|
+
expect(isConsumeNotesV2(md)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('pins MAX_CONSUME_NOTES_METADATA_BYTES at 256 KiB', () => {
|
|
65
|
+
expect(MAX_CONSUME_NOTES_METADATA_BYTES).toBe(256 * 1024);
|
|
66
|
+
expect(MAX_CONSUME_NOTES_METADATA_BYTES).toBe(262_144);
|
|
67
|
+
});
|
|
68
|
+
});
|
package/src/types/proposal.ts
CHANGED
|
@@ -49,9 +49,27 @@ export interface UpdateProcedureThresholdProposalMetadata extends BaseProposalMe
|
|
|
49
49
|
targetThreshold: number;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** `consume_notes` metadata version. Absence on the wire => v1 (issue #229). */
|
|
53
|
+
export const CONSUME_NOTES_METADATA_VERSION_V2 = 2 as const;
|
|
54
|
+
|
|
55
|
+
/** Max serialized v2 metadata, enforced at creation (FR-011). */
|
|
56
|
+
export const MAX_CONSUME_NOTES_METADATA_BYTES = 256 * 1024;
|
|
57
|
+
|
|
52
58
|
export interface ConsumeNotesProposalMetadata extends BaseProposalMetadata {
|
|
53
59
|
proposalType: 'consume_notes';
|
|
54
60
|
noteIds: string[];
|
|
61
|
+
/** Absent or `1` => v1 (legacy), `2` => v2 (issue #229). */
|
|
62
|
+
metadataVersion?: 1 | 2;
|
|
63
|
+
/** v2: base64-encoded `note.serialize()` output, index-aligned with `noteIds`. */
|
|
64
|
+
notes?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isConsumeNotesV2(md: ConsumeNotesProposalMetadata): boolean {
|
|
68
|
+
return md.metadataVersion === CONSUME_NOTES_METADATA_VERSION_V2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isConsumeNotesV1(md: ConsumeNotesProposalMetadata): boolean {
|
|
72
|
+
return md.metadataVersion === undefined || md.metadataVersion === 1;
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
export interface P2IdProposalMetadata extends BaseProposalMetadata {
|
package/src/utils/encoding.ts
CHANGED
|
@@ -40,6 +40,19 @@ export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
|
40
40
|
throw new Error('No base64 encoder available in this environment');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Serialize a Miden `Note` to base64 (v2 `consume_notes` metadata, issue #229). */
|
|
44
|
+
export function noteToBase64(note: { serialize(): Uint8Array }): string {
|
|
45
|
+
return uint8ArrayToBase64(note.serialize());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Decode a base64 note. `noteCtor` is `Note` from `@miden-sdk/miden-sdk`. */
|
|
49
|
+
export function noteFromBase64<TNote>(
|
|
50
|
+
base64: string,
|
|
51
|
+
noteCtor: { deserialize(bytes: Uint8Array): TNote },
|
|
52
|
+
): TNote {
|
|
53
|
+
return noteCtor.deserialize(base64ToUint8Array(base64));
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
export function base64ToUint8Array(base64: string): Uint8Array {
|
|
44
57
|
const binary =
|
|
45
58
|
typeof atob === 'function'
|