@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.
@@ -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;AAGtD,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"}
1
+ {"version":3,"file":"metadata.test.js","sourceRoot":"","sources":["../../src/proposal/metadata.test.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,12 @@
1
- import type { ProposalSignature, ProposalType as GuardianProposalType, SignatureScheme } from '@openzeppelin/guardian-client';
1
+ import type { ProposalSignature, SignatureScheme } from '@openzeppelin/guardian-client';
2
2
  import type { ProcedureName } from '../procedures.js';
3
- export type ProposalType = Exclude<GuardianProposalType, 'custom'>;
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 UnknownProposalMetadata extends BaseProposalMetadata {
65
- proposalType: 'unknown';
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 | UnknownProposalMetadata;
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,EACV,iBAAiB,EACjB,YAAY,IAAI,oBAAoB,EACpC,eAAe,EAChB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,oBAAoB,EAAE,QAAQ,CAAC,CAAC;AAEnE,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;AAE/D,MAAM,MAAM,yBAAyB,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/F;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAE1B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,4BAA4B,GAAG,sBAAsB,CAAC;AAElE,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,6BAA8B,SAAQ,oBAAoB;IACzE,YAAY,EAAE,YAAY,GAAG,eAAe,GAAG,kBAAkB,CAAC;IAClE,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,8BAA+B,SAAQ,oBAAoB;IAC1E,YAAY,EAAE,iBAAiB,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,wCAAyC,SAAQ,oBAAoB;IACpF,YAAY,EAAE,4BAA4B,CAAC;IAC3C,eAAe,EAAE,aAAa,CAAC;IAC/B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,gFAAgF;AAChF,eAAO,MAAM,iCAAiC,EAAG,CAAU,CAAC;AAE5D,iEAAiE;AACjE,eAAO,MAAM,gCAAgC,QAAa,CAAC;AAE3D,MAAM,WAAW,4BAA6B,SAAQ,oBAAoB;IACxE,YAAY,EAAE,eAAe,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4DAA4D;IAC5D,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACxB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,4BAA4B,GAAG,OAAO,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,4BAA4B,GAAG,OAAO,CAE1E;AAED,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAwB,SAAQ,oBAAoB;IACnE,YAAY,EAAE,SAAS,CAAC;CACzB;AAED,MAAM,MAAM,gBAAgB,GACxB,6BAA6B,GAC7B,8BAA8B,GAC9B,wCAAwC,GACxC,4BAA4B,GAC5B,oBAAoB,GACpB,uBAAuB,CAAC;AAE5B,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,yBAAyB,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,4BAA4B,EAAE,CAAC;IAC3C,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,KAAK,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,eAAe,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,MAAM,2BAA2B,GAAG,gBAAgB,CAAC;AAE3D,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B"}
1
+ {"version":3,"file":"proposal.d.ts","sourceRoot":"","sources":["../../src/types/proposal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACpB,YAAY,GACZ,eAAe,GACf,kBAAkB,GAClB,4BAA4B,GAC5B,iBAAiB,GACjB,eAAe,GACf,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;AAE/D,MAAM,MAAM,yBAAyB,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAC/F;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAE1B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,4BAA4B,GAAG,sBAAsB,CAAC;AAElE,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,6BAA8B,SAAQ,oBAAoB;IACzE,YAAY,EAAE,YAAY,GAAG,eAAe,GAAG,kBAAkB,CAAC;IAClE,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,8BAA+B,SAAQ,oBAAoB;IAC1E,YAAY,EAAE,iBAAiB,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;CACpC;AAED,MAAM,WAAW,wCAAyC,SAAQ,oBAAoB;IACpF,YAAY,EAAE,4BAA4B,CAAC;IAC3C,eAAe,EAAE,aAAa,CAAC;IAC/B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,gFAAgF;AAChF,eAAO,MAAM,iCAAiC,EAAG,CAAU,CAAC;AAE5D,iEAAiE;AACjE,eAAO,MAAM,gCAAgC,QAAa,CAAC;AAE3D,MAAM,WAAW,4BAA6B,SAAQ,oBAAoB;IACxE,YAAY,EAAE,eAAe,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,4DAA4D;IAC5D,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACxB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,4BAA4B,GAAG,OAAO,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,4BAA4B,GAAG,OAAO,CAE1E;AAED,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;IAClE,YAAY,EAAE,QAAQ,CAAC;IACvB;;;8EAG0E;IAC1E,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,gBAAgB,GACxB,6BAA6B,GAC7B,8BAA8B,GAC9B,wCAAwC,GACxC,4BAA4B,GAC5B,oBAAoB,GACpB,sBAAsB,CAAC;AAE3B,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,yBAAyB,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,4BAA4B,EAAE,CAAC;IAC3C,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,KAAK,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,eAAe,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,QAAQ,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,MAAM,2BAA2B,GAAG,gBAAgB,CAAC;AAE3D,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../../src/types/proposal.ts"],"names":[],"mappings":"AAmDA,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"}
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.8",
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.8"
33
+ "@openzeppelin/guardian-client": "^0.14.9"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "^5.7.2",
@@ -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 === 'unknown') {
1303
- throw new Error(`Cannot verify proposal metadata for unknown proposal type: ${proposal.id}`);
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 'unknown':
1414
- throw new Error('Unsupported proposal type: unknown');
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 { ConsumeNotesProposalMetadata } from '../types/proposal.js';
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
+ });