@openzeppelin/miden-multisig-client 0.14.8 → 0.14.9
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 +47 -0
- package/dist/multisig/proposal/execution.js +3 -2
- package/dist/multisig/proposal/execution.js.map +1 -1
- package/dist/multisig.d.ts +28 -5
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +173 -5
- package/dist/multisig.js.map +1 -1
- package/dist/multisig.test.js +110 -0
- package/dist/multisig.test.js.map +1 -1
- package/dist/proposal/metadata.d.ts.map +1 -1
- package/dist/proposal/metadata.js +14 -5
- package/dist/proposal/metadata.js.map +1 -1
- package/dist/proposal/metadata.test.js +44 -0
- package/dist/proposal/metadata.test.js.map +1 -1
- package/dist/types/proposal.d.ts +16 -5
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js.map +1 -1
- package/package.json +2 -2
- package/src/multisig.test.ts +139 -0
- package/src/multisig.ts +260 -5
- package/src/proposal/metadata.test.ts +53 -1
- package/src/proposal/metadata.ts +15 -6
- package/src/types/proposal.ts +24 -9
- package/src/multisig/proposal/execution.ts +0 -385
- package/src/multisig/proposal/parser.ts +0 -191
|
@@ -40,4 +40,48 @@ describe('ProposalMetadataCodec consume_notes v2 round-trip (issue #229)', () =>
|
|
|
40
40
|
expect(back.notes).toBeUndefined();
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
describe('ProposalMetadataCodec custom proposal types (issue #266)', () => {
|
|
44
|
+
it('fromGuardian collapses an unmodeled type to the custom bucket, keeping the raw label', () => {
|
|
45
|
+
const wire = {
|
|
46
|
+
proposalType: 'b2agg',
|
|
47
|
+
description: 'agglayer bridge note',
|
|
48
|
+
};
|
|
49
|
+
const md = ProposalMetadataCodec.fromGuardian(wire);
|
|
50
|
+
expect(md.proposalType).toBe('custom');
|
|
51
|
+
expect(md.rawProposalType).toBe('b2agg');
|
|
52
|
+
});
|
|
53
|
+
it('toGuardian round-trips the raw label, not the custom bucket', () => {
|
|
54
|
+
const md = {
|
|
55
|
+
proposalType: 'custom',
|
|
56
|
+
description: 'agglayer bridge note',
|
|
57
|
+
rawProposalType: 'b2agg',
|
|
58
|
+
};
|
|
59
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
60
|
+
expect(wire.proposalType).toBe('b2agg');
|
|
61
|
+
const back = ProposalMetadataCodec.fromGuardian(wire);
|
|
62
|
+
expect(back.proposalType).toBe('custom');
|
|
63
|
+
expect(back.rawProposalType).toBe('b2agg');
|
|
64
|
+
});
|
|
65
|
+
it('validate accepts a custom proposal', () => {
|
|
66
|
+
const md = {
|
|
67
|
+
proposalType: 'custom',
|
|
68
|
+
description: 'opaque',
|
|
69
|
+
rawProposalType: 'b2agg',
|
|
70
|
+
};
|
|
71
|
+
expect(ProposalMetadataCodec.validate(md)).toBe(md);
|
|
72
|
+
});
|
|
73
|
+
it('round-trips update_procedure_threshold through the codec', () => {
|
|
74
|
+
const wire = {
|
|
75
|
+
proposalType: 'update_procedure_threshold',
|
|
76
|
+
targetProcedure: 'send_asset',
|
|
77
|
+
targetThreshold: 2,
|
|
78
|
+
};
|
|
79
|
+
const md = ProposalMetadataCodec.fromGuardian(wire);
|
|
80
|
+
expect(md.proposalType).toBe('update_procedure_threshold');
|
|
81
|
+
const back = ProposalMetadataCodec.toGuardian(md);
|
|
82
|
+
expect(back.proposalType).toBe('update_procedure_threshold');
|
|
83
|
+
expect(back.targetProcedure).toBe('send_asset');
|
|
84
|
+
expect(back.targetThreshold).toBe(2);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
43
87
|
//# sourceMappingURL=metadata.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.test.js","sourceRoot":"","sources":["../../src/proposal/metadata.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"metadata.test.js","sourceRoot":"","sources":["../../src/proposal/metadata.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAMtD,QAAQ,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC9E,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,EAAE,GAAiC;YACvC,YAAY,EAAE,eAAe;YAC7B,WAAW,EAAE,kBAAkB;YAC/B,OAAO,EAAE,CAAC,OAAO,CAAC;YAClB,eAAe,EAAE,CAAC;YAClB,KAAK,EAAE,CAAC,kBAAkB,CAAC;SAC5B,CAAC;QACF,MAAM,IAAI,GAAG,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,IAAI,GAA6B;YACrC,YAAY,EAAE,eAAe;YAC7B,OAAO,EAAE,CAAC,OAAO,CAAC;YAClB,2BAA2B,EAAE,CAAC;YAC9B,iBAAiB,EAAE,CAAC,kBAAkB,CAAC;SACxC,CAAC;QACF,MAAM,EAAE,GAAG,qBAAqB,CAAC,YAAY,CAAC,IAAI,CAAiC,CAAC;QACpF,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC9C,MAAM,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,EAAE,GAAiC;YACvC,YAAY,EAAE,eAAe;YAC7B,WAAW,EAAE,QAAQ;YACrB,OAAO,EAAE,CAAC,OAAO,CAAC;SACnB,CAAC;QACF,MAAM,IAAI,GAAG,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,aAAa,EAAE,CAAC;QACzD,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,aAAa,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,qBAAqB,CAAC,YAAY,CAAC,IAAI,CAAiC,CAAC;QACtF,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,aAAa,EAAE,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0DAA0D,EAAE,GAAG,EAAE;IACxE,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;QAC9F,MAAM,IAAI,GAA6B;YACrC,YAAY,EAAE,OAAO;YACrB,WAAW,EAAE,sBAAsB;SACpC,CAAC;QACF,MAAM,EAAE,GAAG,qBAAqB,CAAC,YAAY,CAAC,IAAI,CAA2B,CAAC;QAC9E,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,EAAE,GAA2B;YACjC,YAAY,EAAE,QAAQ;YACtB,WAAW,EAAE,sBAAsB;YACnC,eAAe,EAAE,OAAO;SACzB,CAAC;QACF,MAAM,IAAI,GAAG,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAExC,MAAM,IAAI,GAAG,qBAAqB,CAAC,YAAY,CAAC,IAAI,CAA2B,CAAC;QAChF,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,EAAE,GAA2B;YACjC,YAAY,EAAE,QAAQ;YACtB,WAAW,EAAE,QAAQ;YACrB,eAAe,EAAE,OAAO;SACzB,CAAC;QACF,MAAM,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,IAAI,GAA6B;YACrC,YAAY,EAAE,4BAA4B;YAC1C,eAAe,EAAE,YAAY;YAC7B,eAAe,EAAE,CAAC;SACnB,CAAC;QACF,MAAM,EAAE,GAAG,qBAAqB,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;QAC7D,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAChD,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/types/proposal.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type { ProposalSignature,
|
|
1
|
+
import type { ProposalSignature, SignatureScheme } from '@openzeppelin/guardian-client';
|
|
2
2
|
import type { ProcedureName } from '../procedures.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Closed set of proposal types the multisig SDK models behaviorally, plus the
|
|
5
|
+
* `'custom'` bucket for any server-defined type the SDK does not model (issue
|
|
6
|
+
* #266). Defined explicitly (not derived from the now-arbitrary guardian-client
|
|
7
|
+
* wire union) so the exhaustive switches in the metadata codec stay sound.
|
|
8
|
+
*/
|
|
9
|
+
export type ProposalType = 'add_signer' | 'remove_signer' | 'change_threshold' | 'update_procedure_threshold' | 'switch_guardian' | 'consume_notes' | 'p2id' | 'custom';
|
|
4
10
|
export type ProposalStatus = 'pending' | 'ready' | 'finalized';
|
|
5
11
|
export type TransactionProposalStatus = {
|
|
6
12
|
type: 'pending';
|
|
@@ -61,10 +67,15 @@ export interface P2IdProposalMetadata extends BaseProposalMetadata {
|
|
|
61
67
|
faucetId: string;
|
|
62
68
|
amount: string;
|
|
63
69
|
}
|
|
64
|
-
export interface
|
|
65
|
-
proposalType: '
|
|
70
|
+
export interface CustomProposalMetadata extends BaseProposalMetadata {
|
|
71
|
+
proposalType: 'custom';
|
|
72
|
+
/** Original server-defined proposal label, e.g. "b2agg" (issue #266). Mirrors
|
|
73
|
+
* Rust `ProposalMetadata.proposal_type`; it is what lets a custom proposal
|
|
74
|
+
* round-trip back to GUARDIAN/export, so it is required in the domain model.
|
|
75
|
+
* Any wire-level optionality is resolved in the parser/codec boundary. */
|
|
76
|
+
rawProposalType: string;
|
|
66
77
|
}
|
|
67
|
-
export type ProposalMetadata = UpdateSignersProposalMetadata | SwitchGuardianProposalMetadata | UpdateProcedureThresholdProposalMetadata | ConsumeNotesProposalMetadata | P2IdProposalMetadata |
|
|
78
|
+
export type ProposalMetadata = UpdateSignersProposalMetadata | SwitchGuardianProposalMetadata | UpdateProcedureThresholdProposalMetadata | ConsumeNotesProposalMetadata | P2IdProposalMetadata | CustomProposalMetadata;
|
|
68
79
|
export interface Proposal {
|
|
69
80
|
id: string;
|
|
70
81
|
accountId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposal.d.ts","sourceRoot":"","sources":["../../src/types/proposal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"proposal.d.ts","sourceRoot":"","sources":["../../src/types/proposal.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../src/types/proposal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../src/types/proposal.ts"],"names":[],"mappings":"AA6DA,gFAAgF;AAChF,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAU,CAAC;AAE5D,iEAAiE;AACjE,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,GAAG,IAAI,CAAC;AAW3D,MAAM,UAAU,gBAAgB,CAAC,EAAgC;IAC/D,OAAO,EAAE,CAAC,eAAe,KAAK,iCAAiC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAgC;IAC/D,OAAO,EAAE,CAAC,eAAe,KAAK,SAAS,IAAI,EAAE,CAAC,eAAe,KAAK,CAAC,CAAC;AACtE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openzeppelin/miden-multisig-client",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
4
4
|
"description": "TypeScript SDK for Miden multisig accounts with GUARDIAN integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@noble/curves": "^1.9.7",
|
|
31
31
|
"@miden-sdk/miden-sdk": "0.14.5",
|
|
32
32
|
"@noble/hashes": "^2.0.1",
|
|
33
|
-
"@openzeppelin/guardian-client": "^0.14.
|
|
33
|
+
"@openzeppelin/guardian-client": "^0.14.9"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"typescript": "^5.7.2",
|
package/src/multisig.test.ts
CHANGED
|
@@ -44,6 +44,9 @@ vi.mock('@miden-sdk/miden-sdk', () => ({
|
|
|
44
44
|
toPreparedSignature: () => [1, 2, 3],
|
|
45
45
|
}),
|
|
46
46
|
},
|
|
47
|
+
TransactionRequest: {
|
|
48
|
+
deserialize: vi.fn().mockReturnValue({}),
|
|
49
|
+
},
|
|
47
50
|
AdviceMap: vi.fn().mockImplementation(() => ({
|
|
48
51
|
insert: vi.fn(),
|
|
49
52
|
})),
|
|
@@ -3144,6 +3147,142 @@ describe('Multisig', () => {
|
|
|
3144
3147
|
});
|
|
3145
3148
|
});
|
|
3146
3149
|
|
|
3150
|
+
describe('prepareCustomExecution', () => {
|
|
3151
|
+
const requestBytes = new Uint8Array([9, 8, 7]);
|
|
3152
|
+
|
|
3153
|
+
function customDelta(
|
|
3154
|
+
proposalType: string,
|
|
3155
|
+
cosignerSigs: any[],
|
|
3156
|
+
): any {
|
|
3157
|
+
return {
|
|
3158
|
+
account_id: '0x' + 'a'.repeat(30),
|
|
3159
|
+
nonce: 1,
|
|
3160
|
+
prev_commitment: '0x' + 'b'.repeat(64),
|
|
3161
|
+
delta_payload: {
|
|
3162
|
+
tx_summary: { data: 'AQID' },
|
|
3163
|
+
signatures: [],
|
|
3164
|
+
metadata: {
|
|
3165
|
+
proposal_type: proposalType,
|
|
3166
|
+
description: '',
|
|
3167
|
+
},
|
|
3168
|
+
},
|
|
3169
|
+
status: {
|
|
3170
|
+
status: 'pending',
|
|
3171
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
3172
|
+
proposer_id: '0x' + 'c'.repeat(64),
|
|
3173
|
+
cosigner_sigs: cosignerSigs,
|
|
3174
|
+
},
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
function falconSig(signerId: string): any {
|
|
3179
|
+
return {
|
|
3180
|
+
signer_id: signerId,
|
|
3181
|
+
signature: { scheme: 'falcon', signature: '0x' + 'e'.repeat(128) },
|
|
3182
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
it('rejects a built-in proposal type', async () => {
|
|
3187
|
+
const config = {
|
|
3188
|
+
threshold: 1,
|
|
3189
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3190
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3191
|
+
};
|
|
3192
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
3193
|
+
|
|
3194
|
+
const builtinDelta = {
|
|
3195
|
+
...customDelta('change_threshold', [falconSig('0x' + 'a'.repeat(64))]),
|
|
3196
|
+
};
|
|
3197
|
+
builtinDelta.delta_payload.metadata = {
|
|
3198
|
+
proposal_type: 'change_threshold',
|
|
3199
|
+
description: '',
|
|
3200
|
+
target_threshold: 1,
|
|
3201
|
+
signer_commitments: ['0x' + 'a'.repeat(64)],
|
|
3202
|
+
} as any;
|
|
3203
|
+
|
|
3204
|
+
mockFetch.mockResolvedValueOnce({
|
|
3205
|
+
ok: true,
|
|
3206
|
+
json: async () => builtinDelta,
|
|
3207
|
+
});
|
|
3208
|
+
|
|
3209
|
+
await expect(
|
|
3210
|
+
multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
|
|
3211
|
+
).rejects.toThrow('prepareCustomExecution is only for custom proposals');
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
it('rejects a proposal that is below its signature threshold', async () => {
|
|
3215
|
+
const config = {
|
|
3216
|
+
threshold: 2,
|
|
3217
|
+
signerCommitments: ['0x' + 'a'.repeat(64), '0x' + 'b'.repeat(64)],
|
|
3218
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3219
|
+
};
|
|
3220
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
3221
|
+
|
|
3222
|
+
mockFetch.mockResolvedValueOnce({
|
|
3223
|
+
ok: true,
|
|
3224
|
+
json: async () => customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]),
|
|
3225
|
+
});
|
|
3226
|
+
|
|
3227
|
+
await expect(
|
|
3228
|
+
multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
|
|
3229
|
+
).rejects.toThrow('have 1 of 2 required signatures');
|
|
3230
|
+
});
|
|
3231
|
+
|
|
3232
|
+
it('rejects when the rebuilt request does not reproduce the signed commitment', async () => {
|
|
3233
|
+
const config = {
|
|
3234
|
+
threshold: 1,
|
|
3235
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3236
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3237
|
+
};
|
|
3238
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
3239
|
+
|
|
3240
|
+
// Signed commitment comes from TransactionSummary.deserialize -> 'c' * 64.
|
|
3241
|
+
// Make the binding request derive a different commitment so the check fails.
|
|
3242
|
+
vi.mocked(executeForSummary).mockResolvedValueOnce({
|
|
3243
|
+
toCommitment: () => ({
|
|
3244
|
+
toHex: () => '0x' + '9'.repeat(64),
|
|
3245
|
+
}),
|
|
3246
|
+
serialize: () => new Uint8Array([1, 2, 3]),
|
|
3247
|
+
} as any);
|
|
3248
|
+
|
|
3249
|
+
mockFetch.mockResolvedValueOnce({
|
|
3250
|
+
ok: true,
|
|
3251
|
+
json: async () => customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]),
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
await expect(
|
|
3255
|
+
multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
|
|
3256
|
+
).rejects.toThrow('Custom proposal binding mismatch');
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
it('fails when GUARDIAN does not return an acknowledgment signature', async () => {
|
|
3260
|
+
const config = {
|
|
3261
|
+
threshold: 1,
|
|
3262
|
+
signerCommitments: ['0x' + 'a'.repeat(64)],
|
|
3263
|
+
guardianCommitment: '0x' + 'c'.repeat(64),
|
|
3264
|
+
};
|
|
3265
|
+
const multisig = new Multisig(mockAccount, config, guardian, mockSigner, mockWebClient);
|
|
3266
|
+
|
|
3267
|
+
const ready = customDelta('b2agg', [falconSig('0x' + 'a'.repeat(64))]);
|
|
3268
|
+
|
|
3269
|
+
// getDeltaProposal
|
|
3270
|
+
mockFetch.mockResolvedValueOnce({
|
|
3271
|
+
ok: true,
|
|
3272
|
+
json: async () => ready,
|
|
3273
|
+
});
|
|
3274
|
+
// pushDelta returns no ack_sig
|
|
3275
|
+
mockFetch.mockResolvedValueOnce({
|
|
3276
|
+
ok: true,
|
|
3277
|
+
json: async () => ({ ...ready, ack_sig: null }),
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
await expect(
|
|
3281
|
+
multisig.prepareCustomExecution('0x' + 'c'.repeat(64), requestBytes),
|
|
3282
|
+
).rejects.toThrow('GUARDIAN did not return acknowledgment signature');
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
|
|
3147
3286
|
describe('proposal metadata preservation', () => {
|
|
3148
3287
|
it('should preserve local metadata when syncing proposals', async () => {
|
|
3149
3288
|
const config = {
|
package/src/multisig.ts
CHANGED
|
@@ -13,13 +13,13 @@ import type {
|
|
|
13
13
|
NoteAsset,
|
|
14
14
|
Proposal,
|
|
15
15
|
ProposalMetadata,
|
|
16
|
+
ProposalSignatureEntry,
|
|
16
17
|
ProposalType,
|
|
17
18
|
} from './types.js';
|
|
18
19
|
import type { ProcedureName } from './procedures.js';
|
|
19
20
|
import type {
|
|
20
21
|
MidenClient,
|
|
21
22
|
TransactionProver,
|
|
22
|
-
TransactionRequest,
|
|
23
23
|
WasmWebClient,
|
|
24
24
|
} from '@miden-sdk/miden-sdk';
|
|
25
25
|
import {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
Note,
|
|
32
32
|
RpcClient,
|
|
33
33
|
Signature,
|
|
34
|
+
TransactionRequest,
|
|
34
35
|
TransactionSummary,
|
|
35
36
|
Word,
|
|
36
37
|
} from '@miden-sdk/miden-sdk';
|
|
@@ -97,6 +98,32 @@ export interface AccountStateVerificationResult {
|
|
|
97
98
|
/**
|
|
98
99
|
* Represents a multisig account with GUARDIAN integration.
|
|
99
100
|
*/
|
|
101
|
+
const BUILTIN_PROPOSAL_TYPES = new Set<string>([
|
|
102
|
+
'add_signer',
|
|
103
|
+
'remove_signer',
|
|
104
|
+
'change_threshold',
|
|
105
|
+
'update_procedure_threshold',
|
|
106
|
+
'switch_guardian',
|
|
107
|
+
'consume_notes',
|
|
108
|
+
'p2id',
|
|
109
|
+
// Reserved: the SDK's internal bucket name for unmodeled types. A producer
|
|
110
|
+
// must not use it as a custom label, or it would collide with the bucket.
|
|
111
|
+
'custom',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Deserialize producer-supplied transaction request bytes, wrapping any failure
|
|
116
|
+
* in a stable message that mirrors the Rust SDK's `deserialize_transaction_request`.
|
|
117
|
+
*/
|
|
118
|
+
function deserializeTransactionRequest(bytes: Uint8Array): TransactionRequest {
|
|
119
|
+
try {
|
|
120
|
+
return TransactionRequest.deserialize(bytes);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
123
|
+
throw new Error(`failed to decode transaction request: ${detail}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
export class Multisig {
|
|
101
128
|
account: Account;
|
|
102
129
|
threshold: number;
|
|
@@ -977,6 +1004,220 @@ export class Multisig {
|
|
|
977
1004
|
proposal.status = 'finalized';
|
|
978
1005
|
}
|
|
979
1006
|
|
|
1007
|
+
/**
|
|
1008
|
+
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
1009
|
+
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
1010
|
+
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
1011
|
+
*/
|
|
1012
|
+
async submitTransaction(request: TransactionRequest): Promise<void> {
|
|
1013
|
+
await this.midenClient.transactions.submit(AccountId.fromHex(this._accountId), request);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Create a proposal from a producer-built transaction the SDK does not model
|
|
1018
|
+
* (issue #266 producer API). `transactionRequestBytes` is a serialized TransactionRequest;
|
|
1019
|
+
* `proposalType` is a free-form, non-empty label that must not collide with a
|
|
1020
|
+
* built-in type. The integration keeps its own recipe to execute later via
|
|
1021
|
+
* `prepareCustomExecution`.
|
|
1022
|
+
*/
|
|
1023
|
+
async createCustomProposal(
|
|
1024
|
+
transactionRequestBytes: Uint8Array,
|
|
1025
|
+
proposalType: string,
|
|
1026
|
+
nonce?: number,
|
|
1027
|
+
): Promise<Proposal> {
|
|
1028
|
+
const label = proposalType.trim().toLowerCase();
|
|
1029
|
+
if (label.length === 0) {
|
|
1030
|
+
throw new Error('proposalType must not be empty');
|
|
1031
|
+
}
|
|
1032
|
+
if (!/^[a-z0-9_]+$/.test(label)) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`proposalType '${label}' must be lowercase snake_case ([a-z0-9_]): no spaces, hyphens, or other characters`,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (BUILTIN_PROPOSAL_TYPES.has(label)) {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
`'${label}' is a built-in proposal type; use the typed proposal API instead`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const webClient = await this.getRawClient();
|
|
1044
|
+
const request = deserializeTransactionRequest(transactionRequestBytes);
|
|
1045
|
+
const summary = await executeForSummary(webClient, this._accountId, request);
|
|
1046
|
+
const summaryBase64 = uint8ArrayToBase64(summary.serialize());
|
|
1047
|
+
const proposalNonce = nonce ?? Date.now();
|
|
1048
|
+
|
|
1049
|
+
const metadata: ProposalMetadata = {
|
|
1050
|
+
proposalType: 'custom',
|
|
1051
|
+
description: '',
|
|
1052
|
+
rawProposalType: label,
|
|
1053
|
+
requiredSignatures: this.getEffectiveThreshold('custom'),
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
return this.createProposal(proposalNonce, summaryBase64, metadata);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Assemble the validated execution advice (cosigner signatures + GUARDIAN
|
|
1061
|
+
* acknowledgment) for a ready custom proposal, so an integration can rebuild
|
|
1062
|
+
* its transaction with its own recipe and submit (issue #266 producer API).
|
|
1063
|
+
*
|
|
1064
|
+
* `transactionRequestBytes` is the serialized transaction request; it is used only to verify
|
|
1065
|
+
* (binding check) that it reproduces the signed commitment, before the
|
|
1066
|
+
* acknowledgment is requested. Returns the advice the integration folds into
|
|
1067
|
+
* its rebuilt transaction (`builder.extendAdviceMap(advice)`).
|
|
1068
|
+
*/
|
|
1069
|
+
async prepareCustomExecution(
|
|
1070
|
+
proposalId: string,
|
|
1071
|
+
transactionRequestBytes: Uint8Array,
|
|
1072
|
+
): Promise<AdviceMap> {
|
|
1073
|
+
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1074
|
+
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1075
|
+
const existing = this.getLocalProposal(proposalId);
|
|
1076
|
+
const proposal = this.proposalFactory().fromDelta(
|
|
1077
|
+
delta,
|
|
1078
|
+
normalizedProposalId,
|
|
1079
|
+
existing?.metadata,
|
|
1080
|
+
existing?.signatures ?? [],
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
if (proposal.metadata.proposalType !== 'custom') {
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
'prepareCustomExecution is only for custom proposals; use executeProposal for built-in types',
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const effectiveThreshold = this.getEffectiveThreshold('custom');
|
|
1090
|
+
const signaturesForExecution = new ProposalSignatures(
|
|
1091
|
+
proposal.signatures,
|
|
1092
|
+
this.signerCommitments,
|
|
1093
|
+
`Invalid proposal signatures for ${proposalId}`,
|
|
1094
|
+
).entries();
|
|
1095
|
+
if (signaturesForExecution.length < effectiveThreshold) {
|
|
1096
|
+
throw new Error(
|
|
1097
|
+
`Proposal is not ready for execution: have ${signaturesForExecution.length} of ${effectiveThreshold} required signatures.`,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const txSummary = TransactionSummary.deserialize(
|
|
1102
|
+
base64ToUint8Array(delta.deltaPayload.txSummary.data),
|
|
1103
|
+
);
|
|
1104
|
+
const signedCommitmentHex = normalizeHexWord(txSummary.toCommitment().toHex());
|
|
1105
|
+
|
|
1106
|
+
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
1107
|
+
|
|
1108
|
+
const webClient = await this.getRawClient();
|
|
1109
|
+
const derived = await executeForSummary(webClient, this._accountId, bindingRequest);
|
|
1110
|
+
const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1111
|
+
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return this.assembleCustomAdvice(
|
|
1118
|
+
proposalId,
|
|
1119
|
+
signaturesForExecution,
|
|
1120
|
+
signedCommitmentHex,
|
|
1121
|
+
delta,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private async assembleCustomAdvice(
|
|
1126
|
+
proposalId: string,
|
|
1127
|
+
signaturesForExecution: ProposalSignatureEntry[],
|
|
1128
|
+
normalizedTxCommitmentHex: string,
|
|
1129
|
+
delta: DeltaObject,
|
|
1130
|
+
): Promise<AdviceMap> {
|
|
1131
|
+
const normalizedSignerCommitments = new Set(
|
|
1132
|
+
this.signerCommitments.map((commitment) => normalizeHexWord(commitment)),
|
|
1133
|
+
);
|
|
1134
|
+
const adviceMap = new AdviceMap();
|
|
1135
|
+
const adviceMapKeys = new Set<string>();
|
|
1136
|
+
const createTxCommitmentWord = (): Word => Word.fromHex(normalizedTxCommitmentHex);
|
|
1137
|
+
|
|
1138
|
+
for (const cosignerSig of signaturesForExecution) {
|
|
1139
|
+
let signerCommitmentHex = normalizeHexWord(cosignerSig.signerId);
|
|
1140
|
+
const ecdsaPublicKey =
|
|
1141
|
+
cosignerSig.signature.scheme === 'ecdsa' ? cosignerSig.signature.publicKey : undefined;
|
|
1142
|
+
|
|
1143
|
+
if (cosignerSig.signature.scheme === 'ecdsa') {
|
|
1144
|
+
if (!ecdsaPublicKey) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
`ECDSA proposal signature for ${signerCommitmentHex} is missing publicKey`,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
const derivedCommitment = tryComputeEcdsaCommitmentHex(ecdsaPublicKey);
|
|
1150
|
+
if (derivedCommitment && derivedCommitment !== signerCommitmentHex) {
|
|
1151
|
+
if (!normalizedSignerCommitments.has(derivedCommitment)) {
|
|
1152
|
+
throw new Error(
|
|
1153
|
+
`ECDSA public key commitment mismatch: derived commitment ${derivedCommitment} is not in signerCommitments.`,
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
signerCommitmentHex = derivedCommitment;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const signerCommitment = Word.fromHex(signerCommitmentHex);
|
|
1161
|
+
const sigBytes = signatureHexToBytes(
|
|
1162
|
+
cosignerSig.signature.signature,
|
|
1163
|
+
cosignerSig.signature.scheme,
|
|
1164
|
+
);
|
|
1165
|
+
const signature = Signature.deserialize(sigBytes);
|
|
1166
|
+
const { key, values } = buildSignatureAdviceEntry(
|
|
1167
|
+
signerCommitment,
|
|
1168
|
+
createTxCommitmentWord(),
|
|
1169
|
+
signature,
|
|
1170
|
+
ecdsaPublicKey,
|
|
1171
|
+
cosignerSig.signature.scheme === 'ecdsa' ? cosignerSig.signature.signature : undefined,
|
|
1172
|
+
);
|
|
1173
|
+
const keyHex = normalizeHexWord(key.toHex());
|
|
1174
|
+
if (adviceMapKeys.has(keyHex)) {
|
|
1175
|
+
throw new Error(`Duplicate advice-map key detected for proposal ${proposalId}`);
|
|
1176
|
+
}
|
|
1177
|
+
adviceMapKeys.add(keyHex);
|
|
1178
|
+
adviceMap.insert(key, new FeltArray(values));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const executionDelta = { ...delta, deltaPayload: delta.deltaPayload.txSummary };
|
|
1182
|
+
const pushResult = await this.guardian.pushDelta(executionDelta);
|
|
1183
|
+
const ackSigHex = pushResult.ackSig;
|
|
1184
|
+
if (!ackSigHex) {
|
|
1185
|
+
throw new Error('GUARDIAN did not return acknowledgment signature');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const guardianCommitment = Word.fromHex(normalizeHexWord(this.guardianCommitment));
|
|
1189
|
+
const ackScheme = (pushResult.ackScheme as 'ecdsa' | 'falcon') || this.signer.scheme;
|
|
1190
|
+
const ackPubkey = pushResult.ackPubkey || this.guardianPublicKey;
|
|
1191
|
+
if (ackScheme === 'ecdsa' && !ackPubkey) {
|
|
1192
|
+
throw new Error('GUARDIAN acknowledgment is missing ECDSA public key');
|
|
1193
|
+
}
|
|
1194
|
+
if (ackScheme === 'ecdsa' && ackPubkey) {
|
|
1195
|
+
const derivedCommitment = tryComputeEcdsaCommitmentHex(ackPubkey);
|
|
1196
|
+
if (derivedCommitment && derivedCommitment !== normalizeHexWord(this.guardianCommitment)) {
|
|
1197
|
+
throw new Error('GUARDIAN public key commitment mismatch');
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
const ackSigBytes = signatureHexToBytes(ackSigHex, ackScheme);
|
|
1201
|
+
const ackSignature = Signature.deserialize(ackSigBytes);
|
|
1202
|
+
const { key: ackKey, values: ackValues } = buildSignatureAdviceEntry(
|
|
1203
|
+
guardianCommitment,
|
|
1204
|
+
createTxCommitmentWord(),
|
|
1205
|
+
ackSignature,
|
|
1206
|
+
ackScheme === 'ecdsa' ? ackPubkey : undefined,
|
|
1207
|
+
ackScheme === 'ecdsa' ? ackSigHex : undefined,
|
|
1208
|
+
);
|
|
1209
|
+
const ackKeyHex = normalizeHexWord(ackKey.toHex());
|
|
1210
|
+
if (adviceMapKeys.has(ackKeyHex)) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`Duplicate advice-map key detected for GUARDIAN acknowledgment in proposal ${proposalId}`,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
adviceMapKeys.add(ackKeyHex);
|
|
1216
|
+
adviceMap.insert(ackKey, new FeltArray(ackValues));
|
|
1217
|
+
|
|
1218
|
+
return adviceMap;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
980
1221
|
private getLocalProposal(proposalId: string): Proposal | undefined {
|
|
981
1222
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
982
1223
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
@@ -994,6 +1235,15 @@ export class Multisig {
|
|
|
994
1235
|
await this.verifyProposalMetadataBinding(proposal);
|
|
995
1236
|
|
|
996
1237
|
const metadata = proposal.metadata;
|
|
1238
|
+
// Reject custom proposals before any advice assembly or GUARDIAN ack push:
|
|
1239
|
+
// the SDK cannot rebuild an opaque custom transaction, and the rejection
|
|
1240
|
+
// must stay side-effect free (mirrors the Rust early guard in execute_proposal).
|
|
1241
|
+
if (metadata.proposalType === 'custom') {
|
|
1242
|
+
throw new Error(
|
|
1243
|
+
'Cannot execute a custom proposal via executeProposal; use prepareCustomExecution to ' +
|
|
1244
|
+
'get the cosigner + GUARDIAN advice, then submitTransaction with your rebuilt request (issue #266).',
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
997
1247
|
const effectiveThreshold = this.getEffectiveThreshold(metadata.proposalType);
|
|
998
1248
|
const signatureContext = `Invalid proposal signatures for ${proposalId}`;
|
|
999
1249
|
const signaturesForExecution = new ProposalSignatures(
|
|
@@ -1299,8 +1549,11 @@ export class Multisig {
|
|
|
1299
1549
|
|
|
1300
1550
|
private async verifyProposalMetadataBinding(proposal: Proposal): Promise<string> {
|
|
1301
1551
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
1302
|
-
if (proposal.metadata.proposalType === '
|
|
1303
|
-
|
|
1552
|
+
if (proposal.metadata.proposalType === 'custom') {
|
|
1553
|
+
// Custom proposals (issue #266) have no per-type reconstruction recipe;
|
|
1554
|
+
// the id ↔ tx_summary commitment match above is the only available
|
|
1555
|
+
// integrity guarantee for an opaque proposal.
|
|
1556
|
+
return txSummaryCommitment;
|
|
1304
1557
|
}
|
|
1305
1558
|
|
|
1306
1559
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
@@ -1410,8 +1663,10 @@ export class Multisig {
|
|
|
1410
1663
|
);
|
|
1411
1664
|
return request;
|
|
1412
1665
|
}
|
|
1413
|
-
case '
|
|
1414
|
-
throw new Error(
|
|
1666
|
+
case 'custom':
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`Cannot build a transaction for a custom proposal type: ${metadata.rawProposalType ?? 'custom'}`,
|
|
1669
|
+
);
|
|
1415
1670
|
}
|
|
1416
1671
|
}
|
|
1417
1672
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { ProposalMetadata as GuardianProposalMetadata } from '@openzeppelin/guardian-client';
|
|
3
3
|
import { ProposalMetadataCodec } from './metadata.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ConsumeNotesProposalMetadata,
|
|
6
|
+
CustomProposalMetadata,
|
|
7
|
+
} from '../types/proposal.js';
|
|
5
8
|
|
|
6
9
|
describe('ProposalMetadataCodec consume_notes v2 round-trip (issue #229)', () => {
|
|
7
10
|
it('toGuardian threads metadataVersion and notes to the wire', () => {
|
|
@@ -45,3 +48,52 @@ describe('ProposalMetadataCodec consume_notes v2 round-trip (issue #229)', () =>
|
|
|
45
48
|
expect(back.notes).toBeUndefined();
|
|
46
49
|
});
|
|
47
50
|
});
|
|
51
|
+
|
|
52
|
+
describe('ProposalMetadataCodec custom proposal types (issue #266)', () => {
|
|
53
|
+
it('fromGuardian collapses an unmodeled type to the custom bucket, keeping the raw label', () => {
|
|
54
|
+
const wire: GuardianProposalMetadata = {
|
|
55
|
+
proposalType: 'b2agg',
|
|
56
|
+
description: 'agglayer bridge note',
|
|
57
|
+
};
|
|
58
|
+
const md = ProposalMetadataCodec.fromGuardian(wire) as CustomProposalMetadata;
|
|
59
|
+
expect(md.proposalType).toBe('custom');
|
|
60
|
+
expect(md.rawProposalType).toBe('b2agg');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('toGuardian round-trips the raw label, not the custom bucket', () => {
|
|
64
|
+
const md: CustomProposalMetadata = {
|
|
65
|
+
proposalType: 'custom',
|
|
66
|
+
description: 'agglayer bridge note',
|
|
67
|
+
rawProposalType: 'b2agg',
|
|
68
|
+
};
|
|
69
|
+
const wire = ProposalMetadataCodec.toGuardian(md);
|
|
70
|
+
expect(wire.proposalType).toBe('b2agg');
|
|
71
|
+
|
|
72
|
+
const back = ProposalMetadataCodec.fromGuardian(wire) as CustomProposalMetadata;
|
|
73
|
+
expect(back.proposalType).toBe('custom');
|
|
74
|
+
expect(back.rawProposalType).toBe('b2agg');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('validate accepts a custom proposal', () => {
|
|
78
|
+
const md: CustomProposalMetadata = {
|
|
79
|
+
proposalType: 'custom',
|
|
80
|
+
description: 'opaque',
|
|
81
|
+
rawProposalType: 'b2agg',
|
|
82
|
+
};
|
|
83
|
+
expect(ProposalMetadataCodec.validate(md)).toBe(md);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('round-trips update_procedure_threshold through the codec', () => {
|
|
87
|
+
const wire: GuardianProposalMetadata = {
|
|
88
|
+
proposalType: 'update_procedure_threshold',
|
|
89
|
+
targetProcedure: 'send_asset',
|
|
90
|
+
targetThreshold: 2,
|
|
91
|
+
};
|
|
92
|
+
const md = ProposalMetadataCodec.fromGuardian(wire);
|
|
93
|
+
expect(md.proposalType).toBe('update_procedure_threshold');
|
|
94
|
+
const back = ProposalMetadataCodec.toGuardian(md);
|
|
95
|
+
expect(back.proposalType).toBe('update_procedure_threshold');
|
|
96
|
+
expect(back.targetProcedure).toBe('send_asset');
|
|
97
|
+
expect(back.targetThreshold).toBe(2);
|
|
98
|
+
});
|
|
99
|
+
});
|