ox 0.14.38 → 0.14.39

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/_cjs/tempo/MultisigConfig.js +71 -15
  3. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  4. package/_cjs/tempo/MultisigOperation.js +42 -36
  5. package/_cjs/tempo/MultisigOperation.js.map +1 -1
  6. package/_cjs/tempo/SignatureEnvelope.js +71 -108
  7. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  8. package/_cjs/version.js +1 -1
  9. package/_esm/tempo/MultisigConfig.js +140 -71
  10. package/_esm/tempo/MultisigConfig.js.map +1 -1
  11. package/_esm/tempo/MultisigOperation.js +42 -37
  12. package/_esm/tempo/MultisigOperation.js.map +1 -1
  13. package/_esm/tempo/SignatureEnvelope.js +84 -143
  14. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  15. package/_esm/version.js +1 -1
  16. package/_types/tempo/KeyAuthorization.d.ts +1 -1
  17. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  18. package/_types/tempo/MultisigConfig.d.ts +137 -73
  19. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  20. package/_types/tempo/MultisigOperation.d.ts +1 -1
  21. package/_types/tempo/MultisigOperation.d.ts.map +1 -1
  22. package/_types/tempo/SignatureEnvelope.d.ts +81 -75
  23. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  24. package/_types/version.d.ts +1 -1
  25. package/package.json +6 -1
  26. package/tempo/KeyAuthorization.test-d.ts +12 -0
  27. package/tempo/KeyAuthorization.test.ts +54 -0
  28. package/tempo/KeyAuthorization.ts +1 -1
  29. package/tempo/MultisigConfig.test-d/package.json +6 -0
  30. package/tempo/MultisigConfig.test-d.ts +52 -0
  31. package/tempo/MultisigConfig.test.ts +291 -263
  32. package/tempo/MultisigConfig.ts +250 -95
  33. package/tempo/MultisigOperation.test-d.ts +6 -0
  34. package/tempo/MultisigOperation.test.ts +113 -72
  35. package/tempo/MultisigOperation.ts +41 -35
  36. package/tempo/SignatureEnvelope.test-d.ts +57 -40
  37. package/tempo/SignatureEnvelope.test.ts +523 -480
  38. package/tempo/SignatureEnvelope.ts +250 -257
  39. package/tempo/multisig.e2e.test.ts +113 -86
  40. package/version.ts +1 -1
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  Address,
3
+ Hash,
3
4
  Hex,
4
5
  P256,
5
6
  PublicKey,
@@ -1100,72 +1101,86 @@ describe('from', () => {
1100
1101
  })
1101
1102
 
1102
1103
  describe('multisig', () => {
1103
- const initialConfig = MultisigConfig.from({
1104
- threshold: 1,
1104
+ const config = MultisigConfig.from({
1105
1105
  owners: [
1106
1106
  {
1107
1107
  owner: '0x1111111111111111111111111111111111111111',
1108
1108
  weight: 1,
1109
1109
  },
1110
1110
  ],
1111
+ threshold: 1,
1111
1112
  })
1112
1113
 
1113
- test('behavior: derives `account` from `initialConfig`', () => {
1114
- const envelope = SignatureEnvelope.from({
1115
- initialConfig,
1116
- signatures: [SignatureEnvelope.from(signature_secp256k1)],
1117
- })
1118
-
1119
- expect(envelope).toMatchObject({
1120
- type: 'multisig',
1121
- account: MultisigConfig.getAddress(initialConfig),
1122
- })
1123
- expect('initialConfig' in envelope).toBe(false)
1124
- expect((envelope as SignatureEnvelope.Multisig).init).toBeUndefined()
1125
- })
1126
-
1127
- test('behavior: `init: true` opts into bootstrap (uses `initialConfig` as `init`)', () => {
1114
+ test('behavior: derives an initial account from config', () => {
1128
1115
  const envelope = SignatureEnvelope.from({
1129
- initialConfig,
1116
+ config,
1130
1117
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
1131
- init: true,
1132
1118
  })
1133
1119
 
1134
- expect((envelope as SignatureEnvelope.Multisig).init).toEqual(
1135
- initialConfig,
1120
+ expect(envelope).toMatchInlineSnapshot(
1121
+ {
1122
+ signatures: [
1123
+ {
1124
+ signature: {
1125
+ r: expect.anything(),
1126
+ s: expect.anything(),
1127
+ yParity: expect.any(Number),
1128
+ },
1129
+ },
1130
+ ],
1131
+ },
1132
+ `
1133
+ {
1134
+ "account": "0x8820d1497eeaf4f68e00b2cfc00a2f3b1dbb00da",
1135
+ "config": {
1136
+ "owners": [
1137
+ {
1138
+ "owner": "0x1111111111111111111111111111111111111111",
1139
+ "weight": 1,
1140
+ },
1141
+ ],
1142
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1143
+ "threshold": 1,
1144
+ "version": 0n,
1145
+ },
1146
+ "signatures": [
1147
+ {
1148
+ "signature": {
1149
+ "r": Anything,
1150
+ "s": Anything,
1151
+ "yParity": Any<Number>,
1152
+ },
1153
+ "type": "secp256k1",
1154
+ },
1155
+ ],
1156
+ "type": "multisig",
1157
+ }
1158
+ `,
1136
1159
  )
1137
1160
  })
1138
1161
 
1139
- test('behavior: `init` accepts an explicit config', () => {
1140
- const otherConfig = MultisigConfig.from({
1141
- threshold: 1,
1142
- owners: [
1143
- {
1144
- owner: '0x2222222222222222222222222222222222222222',
1145
- weight: 1,
1146
- },
1147
- ],
1148
- })
1162
+ test('behavior: accepts an explicit account for a current config', () => {
1149
1163
  const envelope = SignatureEnvelope.from({
1150
- initialConfig,
1164
+ account: '0x2222222222222222222222222222222222222222',
1165
+ config: { ...config, version: 1 },
1151
1166
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
1152
- init: otherConfig,
1153
1167
  })
1154
1168
 
1155
- expect((envelope as SignatureEnvelope.Multisig).init).toEqual(otherConfig)
1169
+ expect(envelope.account).toBe(
1170
+ '0x2222222222222222222222222222222222222222',
1171
+ )
1172
+ expect(envelope.config.version).toBe(1n)
1156
1173
  })
1157
1174
 
1158
- test('behavior: `{ account }` form still works', () => {
1159
- const account = MultisigConfig.getAddress(initialConfig)
1160
- const envelope = SignatureEnvelope.from({
1161
- account,
1162
- signatures: [SignatureEnvelope.from(signature_secp256k1)],
1163
- })
1164
-
1165
- expect(envelope).toMatchObject({
1166
- type: 'multisig',
1167
- account,
1168
- })
1175
+ test('error: requires an account for a current config', () => {
1176
+ expect(() =>
1177
+ SignatureEnvelope.from({
1178
+ config: { ...config, version: 1n },
1179
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1180
+ } as never),
1181
+ ).toThrowErrorMatchingInlineSnapshot(
1182
+ `[MultisigConfig.InvalidConfigError: Invalid native multisig config: account address requires version zero.]`,
1183
+ )
1169
1184
  })
1170
1185
  })
1171
1186
  })
@@ -1769,8 +1784,6 @@ describe('serialize', () => {
1769
1784
  })
1770
1785
 
1771
1786
  describe('sortMultisigApprovals', () => {
1772
- // Build owner key pairs first so we can construct a real initial config
1773
- // whose owner set matches the keys that produce the approvals below.
1774
1787
  const ownerKeys = Array.from({ length: 3 }, () => {
1775
1788
  const privateKey = Secp256k1.randomPrivateKey()
1776
1789
  const address = Address.fromPublicKey(
@@ -1782,14 +1795,16 @@ describe('sortMultisigApprovals', () => {
1782
1795
  Hex.toBigInt(a.address) < Hex.toBigInt(b.address) ? -1 : 1,
1783
1796
  )
1784
1797
 
1785
- const initialConfig = MultisigConfig.from({
1786
- threshold: 2,
1798
+ const config = MultisigConfig.from({
1787
1799
  owners: ascendingOwners.map((o) => ({ owner: o.address, weight: 1 })),
1800
+ threshold: 2,
1788
1801
  })
1802
+ const account = MultisigConfig.getAddress(config)
1789
1803
  const payload = `0x${'42'.repeat(32)}` as const
1790
1804
  const digest = MultisigConfig.getSignPayload({
1805
+ account,
1806
+ config,
1791
1807
  payload,
1792
- initialConfig,
1793
1808
  })
1794
1809
 
1795
1810
  const owners = ownerKeys.map((owner) => ({
@@ -1805,7 +1820,8 @@ describe('sortMultisigApprovals', () => {
1805
1820
 
1806
1821
  test('behavior: orders approvals ascending by recovered owner address', () => {
1807
1822
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1808
- initialConfig,
1823
+ account,
1824
+ config,
1809
1825
  payload,
1810
1826
  // Provide approvals in reverse of the canonical order.
1811
1827
  signatures: [...ascending].reverse().map((owner) => owner.signature),
@@ -1817,7 +1833,8 @@ describe('sortMultisigApprovals', () => {
1817
1833
  const signatures = ascending.map((owner) => owner.signature)
1818
1834
  expect(
1819
1835
  SignatureEnvelope.sortMultisigApprovals({
1820
- initialConfig,
1836
+ account,
1837
+ config,
1821
1838
  payload,
1822
1839
  signatures,
1823
1840
  }),
@@ -1826,7 +1843,8 @@ describe('sortMultisigApprovals', () => {
1826
1843
 
1827
1844
  test('behavior: recovered order matches the config owner order', () => {
1828
1845
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1829
- initialConfig,
1846
+ account,
1847
+ config,
1830
1848
  payload,
1831
1849
  signatures: owners.map((owner) => owner.signature),
1832
1850
  })
@@ -1835,23 +1853,6 @@ describe('sortMultisigApprovals', () => {
1835
1853
  )
1836
1854
  expect(recovered).toEqual(ascending.map((owner) => owner.address))
1837
1855
  })
1838
-
1839
- test('behavior: `initialConfig` and `{ account }` produce identical ordering', () => {
1840
- const account = MultisigConfig.getAddress(initialConfig)
1841
- const signatures = owners.map((owner) => owner.signature)
1842
-
1843
- const fromConfig = SignatureEnvelope.sortMultisigApprovals({
1844
- initialConfig,
1845
- payload,
1846
- signatures,
1847
- })
1848
- const fromAccount = SignatureEnvelope.sortMultisigApprovals({
1849
- account,
1850
- payload,
1851
- signatures,
1852
- })
1853
- expect(fromConfig).toEqual(fromAccount)
1854
- })
1855
1856
  })
1856
1857
 
1857
1858
  describe('validate', () => {
@@ -2852,146 +2853,371 @@ describe('CoercionError', () => {
2852
2853
  })
2853
2854
 
2854
2855
  describe('multisig', () => {
2855
- const account = '0x8ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7ae'
2856
-
2857
- // P256 signatures do not carry `yParity` in the wire format, so use a clean
2858
- // inner signature for round-trip equality checks.
2859
- const innerP256 = SignatureEnvelope.from({
2860
- signature: { r: p256Signature.r, s: p256Signature.s },
2861
- publicKey,
2862
- prehash: true,
2856
+ const initial =
2857
+ '0x05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b841869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b' as const
2858
+ const current =
2859
+ '0x05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000000101d7d6942b5ad5c4795c026514f8317c7a215e218dccd6cf01f843b8414a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e0504a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf51b' as const
2860
+ const nested =
2861
+ '0x05f8f0949969e2243075b27a8eab61009c59b77ab13b83f6f83ba033333333333333333333333333333333333333333333333333333333333333338001d7d6944f9f5b162a7464bcc260ce44d7ae0d935f9c583701f89cb89a05f897944f9f5b162a7464bcc260ce44d7ae0d935f9c5837f83ba022222222222222222222222222222222222222222222222222222222222222228001d7d6946813eb9362372eef6200f3b1dbc3f819671cba6901f843b841032aa6f3ea7b0b7069720d0f3891983c493d149326c5c957d864ed7371b8475e3e95325079b24491f4b6d68920e4b3bacd7c6094df6ed88b8674864ab559c8fb1b' as const
2862
+
2863
+ test('example: decodes and re-encodes the frozen initial witness', () => {
2864
+ const envelope = SignatureEnvelope.deserialize(initial)
2865
+
2866
+ expect(envelope).toMatchInlineSnapshot(`
2867
+ {
2868
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2869
+ "config": {
2870
+ "owners": [
2871
+ {
2872
+ "owner": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
2873
+ "weight": 1,
2874
+ },
2875
+ ],
2876
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
2877
+ "threshold": 1,
2878
+ "version": 0n,
2879
+ },
2880
+ "signatures": [
2881
+ {
2882
+ "signature": {
2883
+ "r": 60871800714128149016591846789173983752390955456021923556141688418770269698437n,
2884
+ "s": 24367763726302312398084372528434045669788111037623467391175388828437596076276n,
2885
+ "yParity": 0,
2886
+ },
2887
+ "type": "secp256k1",
2888
+ },
2889
+ ],
2890
+ "type": "multisig",
2891
+ }
2892
+ `)
2893
+ expect(SignatureEnvelope.serialize(envelope)).toBe(initial)
2863
2894
  })
2864
2895
 
2865
- const envelope = SignatureEnvelope.from({
2866
- type: 'multisig',
2867
- account,
2868
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
2896
+ test('example: decodes and re-encodes the frozen current witness', () => {
2897
+ const envelope = SignatureEnvelope.deserialize(current)
2898
+
2899
+ expect(envelope).toMatchInlineSnapshot(`
2900
+ {
2901
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2902
+ "config": {
2903
+ "owners": [
2904
+ {
2905
+ "owner": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
2906
+ "weight": 1,
2907
+ },
2908
+ ],
2909
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
2910
+ "threshold": 1,
2911
+ "version": 1n,
2912
+ },
2913
+ "signatures": [
2914
+ {
2915
+ "signature": {
2916
+ "r": 33496517174194049078979631673636743206216482049680398378774155205635729711184n,
2917
+ "s": 33572483501191170814542557897380275621900207658854593170435098846326627404789n,
2918
+ "yParity": 0,
2919
+ },
2920
+ "type": "secp256k1",
2921
+ },
2922
+ ],
2923
+ "type": "multisig",
2924
+ }
2925
+ `)
2926
+ expect(SignatureEnvelope.serialize(envelope)).toBe(current)
2869
2927
  })
2870
2928
 
2871
- test('serialize: type byte 0x05 prefix', () => {
2872
- const serialized = SignatureEnvelope.serialize(envelope)
2873
- expect(serialized.startsWith('0x05')).toBe(true)
2929
+ test('example: decodes and re-encodes the frozen nested witness', () => {
2930
+ const envelope = SignatureEnvelope.deserialize(nested)
2931
+
2932
+ expect(envelope).toMatchInlineSnapshot(`
2933
+ {
2934
+ "account": "0x9969e2243075b27a8eab61009c59b77ab13b83f6",
2935
+ "config": {
2936
+ "owners": [
2937
+ {
2938
+ "owner": "0x4f9f5b162a7464bcc260ce44d7ae0d935f9c5837",
2939
+ "weight": 1,
2940
+ },
2941
+ ],
2942
+ "salt": "0x3333333333333333333333333333333333333333333333333333333333333333",
2943
+ "threshold": 1,
2944
+ "version": 0n,
2945
+ },
2946
+ "signatures": [
2947
+ {
2948
+ "account": "0x4f9f5b162a7464bcc260ce44d7ae0d935f9c5837",
2949
+ "config": {
2950
+ "owners": [
2951
+ {
2952
+ "owner": "0x6813eb9362372eef6200f3b1dbc3f819671cba69",
2953
+ "weight": 1,
2954
+ },
2955
+ ],
2956
+ "salt": "0x2222222222222222222222222222222222222222222222222222222222222222",
2957
+ "threshold": 1,
2958
+ "version": 0n,
2959
+ },
2960
+ "signatures": [
2961
+ {
2962
+ "signature": {
2963
+ "r": 1432298388324792555676061659976215111517531547814803085200940835913455912798n,
2964
+ "s": 28307004081743690677157914067925034813425317008577473150162358055484664695035n,
2965
+ "yParity": 0,
2966
+ },
2967
+ "type": "secp256k1",
2968
+ },
2969
+ ],
2970
+ "type": "multisig",
2971
+ },
2972
+ ],
2973
+ "type": "multisig",
2974
+ }
2975
+ `)
2976
+ expect(SignatureEnvelope.serialize(envelope)).toBe(nested)
2874
2977
  })
2875
2978
 
2876
- test('serialize: wire shape is `0x05 || rlp([account, signatures])`', () => {
2877
- const deterministic = SignatureEnvelope.from({
2878
- type: 'multisig',
2879
- account,
2880
- signatures: [innerP256],
2881
- })
2882
- expect(SignatureEnvelope.serialize(deterministic)).toMatchInlineSnapshot(
2883
- `"0x05f89b948ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7aef884b88201ccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba001"`,
2884
- )
2979
+ test('behavior: preserves exact 65-byte secp256k1 precedence', () => {
2980
+ const serialized = ('0x05' +
2981
+ '00'.repeat(31) +
2982
+ '00'.repeat(31) +
2983
+ '01' +
2984
+ '00') as Hex.Hex
2985
+
2986
+ expect(Hex.size(serialized)).toBe(65)
2987
+ expect(SignatureEnvelope.deserialize(serialized).type).toBe('secp256k1')
2885
2988
  })
2886
2989
 
2887
- test('serialize/deserialize round-trip', () => {
2990
+ test('behavior: serializes account, config, and approvals in wire order', () => {
2991
+ const envelope = SignatureEnvelope.deserialize(initial)
2992
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
2888
2993
  const serialized = SignatureEnvelope.serialize(envelope)
2889
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(envelope)
2994
+ const decoded = Rlp.toHex(Hex.slice(serialized, 1))
2995
+
2996
+ expect(decoded).toMatchInlineSnapshot(`
2997
+ [
2998
+ "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2999
+ [
3000
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
3001
+ "0x",
3002
+ "0x01",
3003
+ [
3004
+ [
3005
+ "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
3006
+ "0x01",
3007
+ ],
3008
+ ],
3009
+ ],
3010
+ [
3011
+ "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b",
3012
+ ],
3013
+ ]
3014
+ `)
2890
3015
  })
2891
3016
 
2892
- test('deserialize: rejects the legacy three-field wire shape', () => {
2893
- const init = MultisigConfig.toTuple({
2894
- threshold: 1,
2895
- owners: [
2896
- { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
2897
- ],
2898
- })
2899
- const legacy = Hex.concat(
2900
- '0x05',
2901
- Rlp.fromHex([account, [SignatureEnvelope.serialize(innerP256)], init]),
2902
- )
2903
- expect(() => SignatureEnvelope.deserialize(legacy)).toThrowError(
2904
- SignatureEnvelope.InvalidSerializedError,
2905
- )
2906
- })
3017
+ test('behavior: round trips initial and current RPC witnesses', () => {
3018
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3019
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3020
+ const rpc = {
3021
+ current: SignatureEnvelope.toRpc(currentEnvelope),
3022
+ initial: SignatureEnvelope.toRpc(initialEnvelope),
3023
+ }
2907
3024
 
2908
- test('deserialize: rejects an invalid initialized account', () => {
2909
- const malformed = Hex.concat(
2910
- '0x05',
2911
- Rlp.fromHex(['0x11', [SignatureEnvelope.serialize(innerP256)]]),
3025
+ expect(rpc).toMatchInlineSnapshot(`
3026
+ {
3027
+ "current": {
3028
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
3029
+ "config": {
3030
+ "owners": [
3031
+ {
3032
+ "owner": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
3033
+ "weight": 1,
3034
+ },
3035
+ ],
3036
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
3037
+ "threshold": 1,
3038
+ "version": "0x1",
3039
+ },
3040
+ "signatures": [
3041
+ {
3042
+ "r": "0x4a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e050",
3043
+ "s": "0x4a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf5",
3044
+ "type": "secp256k1",
3045
+ "yParity": "0x0",
3046
+ },
3047
+ ],
3048
+ },
3049
+ "initial": {
3050
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
3051
+ "config": {
3052
+ "owners": [
3053
+ {
3054
+ "owner": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
3055
+ "weight": 1,
3056
+ },
3057
+ ],
3058
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
3059
+ "threshold": 1,
3060
+ "version": "0x0",
3061
+ },
3062
+ "signatures": [
3063
+ {
3064
+ "r": "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f985",
3065
+ "s": "0x35dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f4",
3066
+ "type": "secp256k1",
3067
+ "yParity": "0x0",
3068
+ },
3069
+ ],
3070
+ },
3071
+ }
3072
+ `)
3073
+ expect(SignatureEnvelope.fromRpc(rpc.initial)).toStrictEqual(
3074
+ initialEnvelope,
2912
3075
  )
2913
- expect(() => SignatureEnvelope.deserialize(malformed)).toThrowError(
2914
- SignatureEnvelope.InvalidSerializedError,
3076
+ expect(SignatureEnvelope.fromRpc(rpc.current)).toStrictEqual(
3077
+ currentEnvelope,
2915
3078
  )
2916
3079
  })
2917
3080
 
2918
- test('getType', () => {
2919
- expect(SignatureEnvelope.getType(envelope)).toBe('multisig')
2920
- expect(
2921
- SignatureEnvelope.getType({
2922
- init: {
2923
- threshold: 1,
2924
- owners: [
2925
- { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
2926
- ],
2927
- },
2928
- signatures: [innerP256],
3081
+ test('behavior: derives the account only for an initial witness', () => {
3082
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3083
+ if (initialEnvelope.type !== 'multisig') throw new Error('unreachable')
3084
+ if (initialEnvelope.config.version !== 0n) throw new Error('unreachable')
3085
+ const derived = SignatureEnvelope.from({
3086
+ config: initialEnvelope.config as MultisigConfig.Config<0n>,
3087
+ signatures: initialEnvelope.signatures,
3088
+ })
3089
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3090
+ if (currentEnvelope.type !== 'multisig') throw new Error('unreachable')
3091
+
3092
+ expect(derived.account).toBe(initialEnvelope.account)
3093
+ expect(() =>
3094
+ SignatureEnvelope.from({
3095
+ config: currentEnvelope.config,
3096
+ signatures: currentEnvelope.signatures,
2929
3097
  } as never),
2930
- ).toBe('multisig')
3098
+ ).toThrowErrorMatchingInlineSnapshot(
3099
+ `[MultisigConfig.InvalidConfigError: Invalid native multisig config: account address requires version zero.]`,
3100
+ )
2931
3101
  })
2932
3102
 
2933
- test('extractAddress returns the multisig account', () => {
3103
+ test('behavior: extracts the multisig account', () => {
3104
+ const envelope = SignatureEnvelope.deserialize(initial)
3105
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3106
+
2934
3107
  expect(
2935
3108
  SignatureEnvelope.extractAddress({
2936
3109
  payload: '0xdeadbeef',
2937
3110
  signature: envelope,
2938
3111
  }),
2939
- ).toBe(account)
2940
- })
2941
-
2942
- test('toRpc/fromRpc round-trip', () => {
2943
- const rpc = SignatureEnvelope.toRpc(
2944
- envelope,
2945
- ) as SignatureEnvelope.MultisigRpc
2946
- expect(rpc).toMatchObject({
2947
- account,
2948
- signatures: [{ type: 'secp256k1' }, { type: 'p256' }],
3112
+ ).toBe(envelope.account)
3113
+ })
3114
+
3115
+ test('example: matches the frozen eight-approval boundary vector', () => {
3116
+ const config = MultisigConfig.from({
3117
+ owners: Array.from({ length: 8 }, (_, index) => ({
3118
+ owner:
3119
+ `0x${(index + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
3120
+ weight: 1,
3121
+ })),
3122
+ salt: `0x${'66'.repeat(32)}`,
3123
+ threshold: 8,
3124
+ })
3125
+ const approval = SignatureEnvelope.from({
3126
+ r: 0x840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565n,
3127
+ s: 0x25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1n,
3128
+ yParity: 0,
3129
+ })
3130
+ const envelope = SignatureEnvelope.from({
3131
+ config,
3132
+ signatures: Array.from(
3133
+ { length: MultisigConfig.maxSignatures },
3134
+ () => approval,
3135
+ ),
2949
3136
  })
2950
- expect(rpc.init).toBeUndefined()
2951
- expect('type' in rpc).toBe(false)
2952
- expect(SignatureEnvelope.fromRpc(rpc)).toEqual(envelope)
2953
- expect(
2954
- SignatureEnvelope.fromRpc({ ...rpc, type: 'multisig' } as never),
2955
- ).toEqual(envelope)
3137
+ const serialized = SignatureEnvelope.serialize(envelope)
3138
+
3139
+ expect({
3140
+ hash: Hash.keccak256(serialized),
3141
+ length: Hex.size(serialized),
3142
+ signatureCount: envelope.signatures.length,
3143
+ }).toMatchInlineSnapshot(`
3144
+ {
3145
+ "hash": "0x1cb47fcf31fb1ebb3177ec1744eeda195e4d789920d1214af32baaa6150ad5e2",
3146
+ "length": 787,
3147
+ "signatureCount": 8,
3148
+ }
3149
+ `)
2956
3150
  })
2957
3151
 
2958
- test('assert: missing properties', () => {
2959
- expect(() =>
2960
- SignatureEnvelope.assert({ type: 'multisig', account } as never),
2961
- ).toThrowError()
3152
+ test('behavior: accepts initial and current nested witnesses', () => {
3153
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3154
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3155
+ if (
3156
+ initialEnvelope.type !== 'multisig' ||
3157
+ currentEnvelope.type !== 'multisig'
3158
+ )
3159
+ throw new Error('unreachable')
3160
+
3161
+ for (const signature of [initialEnvelope, currentEnvelope]) {
3162
+ const config = MultisigConfig.from({
3163
+ owners: [{ owner: signature.account, weight: 1 }],
3164
+ salt: ('0x' + 'ff'.repeat(32)) as Hex.Hex,
3165
+ threshold: 1,
3166
+ version: 1n,
3167
+ })
3168
+ const envelope = SignatureEnvelope.from({
3169
+ account: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
3170
+ config,
3171
+ signatures: [signature],
3172
+ })
3173
+
3174
+ expect(
3175
+ SignatureEnvelope.deserialize(SignatureEnvelope.serialize(envelope)),
3176
+ ).toStrictEqual(envelope)
3177
+ }
2962
3178
  })
2963
3179
 
2964
- test('assert: rejects empty owner approvals', () => {
3180
+ test('error: rejects an initial account mismatch', () => {
3181
+ const envelope = SignatureEnvelope.deserialize(initial)
3182
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3183
+
2965
3184
  expect(() =>
2966
3185
  SignatureEnvelope.assert({
2967
- type: 'multisig',
2968
- account,
2969
- signatures: [],
3186
+ ...envelope,
3187
+ account: '0x1111111111111111111111111111111111111111',
2970
3188
  }),
2971
3189
  ).toThrowErrorMatchingInlineSnapshot(
2972
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig signatures cannot be empty.]`,
3190
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: initial multisig config does not derive account.]`,
2973
3191
  )
2974
3192
  })
2975
3193
 
2976
- test('assert: accepts at most eight owner approvals', () => {
3194
+ test('error: rejects a self-owned current witness', () => {
3195
+ const envelope = SignatureEnvelope.deserialize(current)
3196
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3197
+ const account = envelope.config.owners[0]!.owner
3198
+
2977
3199
  expect(() =>
2978
- SignatureEnvelope.assert({
2979
- type: 'multisig',
2980
- account,
2981
- signatures: Array.from(
2982
- { length: MultisigConfig.maxSignatures },
2983
- () => innerP256,
2984
- ),
2985
- }),
2986
- ).not.toThrow()
3200
+ SignatureEnvelope.assert({ ...envelope, account }),
3201
+ ).toThrowErrorMatchingInlineSnapshot(
3202
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig account cannot be an owner.]`,
3203
+ )
3204
+ })
3205
+
3206
+ test('error: rejects empty and excess approvals', () => {
3207
+ const envelope = SignatureEnvelope.deserialize(current)
3208
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
2987
3209
 
3210
+ expect(() =>
3211
+ SignatureEnvelope.assert({ ...envelope, signatures: [] }),
3212
+ ).toThrowErrorMatchingInlineSnapshot(
3213
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig signatures cannot be empty.]`,
3214
+ )
2988
3215
  expect(() =>
2989
3216
  SignatureEnvelope.assert({
2990
- type: 'multisig',
2991
- account,
3217
+ ...envelope,
2992
3218
  signatures: Array.from(
2993
3219
  { length: MultisigConfig.maxSignatures + 1 },
2994
- () => innerP256,
3220
+ () => envelope.signatures[0]!,
2995
3221
  ),
2996
3222
  }),
2997
3223
  ).toThrowErrorMatchingInlineSnapshot(
@@ -2999,7 +3225,23 @@ describe('multisig', () => {
2999
3225
  )
3000
3226
  })
3001
3227
 
3002
- test('assert: rejects oversized owner approvals', () => {
3228
+ test('error: rejects keychain owner approvals', () => {
3229
+ const envelope = SignatureEnvelope.deserialize(current)
3230
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3231
+
3232
+ expect(() =>
3233
+ SignatureEnvelope.assert({
3234
+ ...envelope,
3235
+ signatures: [signature_keychain_secp256k1],
3236
+ } as never),
3237
+ ).toThrowErrorMatchingInlineSnapshot(
3238
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
3239
+ )
3240
+ })
3241
+
3242
+ test('error: rejects oversized primitive approvals', () => {
3243
+ const envelope = SignatureEnvelope.deserialize(current)
3244
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3003
3245
  const oversized = {
3004
3246
  ...signature_webauthn,
3005
3247
  metadata: {
@@ -3007,334 +3249,135 @@ describe('multisig', () => {
3007
3249
  clientDataJSON: JSON.stringify({ value: 'x'.repeat(2048) }),
3008
3250
  },
3009
3251
  }
3252
+
3010
3253
  expect(() =>
3011
3254
  SignatureEnvelope.assert({
3012
- type: 'multisig',
3013
- account,
3255
+ ...envelope,
3014
3256
  signatures: [oversized],
3015
3257
  }),
3016
3258
  ).toThrowErrorMatchingInlineSnapshot(
3017
3259
  `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig owner signature exceeds 2049 bytes.]`,
3018
3260
  )
3019
-
3020
- const serialized = Hex.concat(
3021
- '0x05',
3022
- Rlp.fromHex([account, [SignatureEnvelope.serialize(oversized)]]),
3023
- )
3024
- expect(() => SignatureEnvelope.deserialize(serialized)).toThrowError(
3025
- SignatureEnvelope.InvalidSerializedError,
3026
- )
3027
3261
  })
3028
3262
 
3029
- test('assert: rejects keychain owner approvals', () => {
3263
+ test('error: rejects nesting deeper than two nodes', () => {
3264
+ const envelope = SignatureEnvelope.deserialize(initial)
3265
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3266
+ const depth2 = SignatureEnvelope.from({
3267
+ ...envelope,
3268
+ signatures: [envelope],
3269
+ })
3270
+
3030
3271
  expect(() =>
3031
3272
  SignatureEnvelope.assert({
3032
- type: 'multisig',
3033
- account,
3034
- signatures: [signature_keychain_secp256k1],
3035
- } as never),
3273
+ ...envelope,
3274
+ signatures: [depth2],
3275
+ }),
3036
3276
  ).toThrowErrorMatchingInlineSnapshot(
3037
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
3277
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig nesting depth exceeds 2.]`,
3038
3278
  )
3039
3279
  })
3040
3280
 
3041
- describe('nested approvals', () => {
3042
- const nestedAccount = '0x1111111111111111111111111111111111111111'
3043
- const nested = SignatureEnvelope.from({
3044
- type: 'multisig',
3045
- account: nestedAccount,
3046
- signatures: [innerP256],
3047
- })
3048
-
3049
- test('serialize/deserialize round-trip with a nested multisig approval', () => {
3050
- const parent = SignatureEnvelope.from({
3051
- type: 'multisig',
3052
- account,
3053
- signatures: [innerP256, nested],
3054
- })
3055
- const serialized = SignatureEnvelope.serialize(parent)
3056
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(parent)
3057
- })
3058
-
3059
- test('assert: accepts the maximum nesting depth', () => {
3060
- const depth2 = SignatureEnvelope.from({
3061
- type: 'multisig',
3062
- account,
3063
- signatures: [nested],
3064
- })
3065
- expect(() => SignatureEnvelope.assert(depth2)).not.toThrowError()
3066
- })
3067
-
3068
- test('accepts a nested approval above the primitive byte limit', () => {
3069
- const primitive = SignatureEnvelope.from({
3070
- ...signature_webauthn,
3071
- metadata: {
3072
- ...signature_webauthn.metadata,
3073
- clientDataJSON: JSON.stringify({ value: 'x'.repeat(1_050) }),
3074
- },
3075
- signature: {
3076
- r: signature_webauthn.signature.r,
3077
- s: signature_webauthn.signature.s,
3078
- },
3079
- })
3080
- const largeNested = SignatureEnvelope.from({
3081
- type: 'multisig',
3082
- account: nestedAccount,
3083
- signatures: [primitive, primitive],
3084
- })
3085
- expect(Hex.size(SignatureEnvelope.serialize(primitive))).toBeLessThan(
3086
- MultisigConfig.maxOwnerSignatureBytes,
3087
- )
3088
- expect(
3089
- Hex.size(SignatureEnvelope.serialize(largeNested)),
3090
- ).toBeGreaterThan(MultisigConfig.maxOwnerSignatureBytes)
3091
-
3092
- const parent = SignatureEnvelope.from({
3093
- type: 'multisig',
3094
- account,
3095
- signatures: [largeNested],
3096
- })
3097
- expect(
3098
- SignatureEnvelope.deserialize(SignatureEnvelope.serialize(parent)),
3099
- ).toEqual(parent)
3100
- })
3101
-
3102
- test('assert: rejects nesting deeper than the maximum', () => {
3103
- const depth3 = SignatureEnvelope.from({
3104
- type: 'multisig',
3105
- account,
3106
- signatures: [
3107
- SignatureEnvelope.from({
3108
- type: 'multisig',
3109
- account: nestedAccount,
3110
- signatures: [nested],
3111
- }),
3112
- ],
3113
- })
3114
- expect(() =>
3115
- SignatureEnvelope.assert(depth3),
3116
- ).toThrowErrorMatchingInlineSnapshot(
3117
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig nesting depth exceeds 2.]`,
3118
- )
3119
- })
3120
-
3121
- test('deserialize: rejects nesting deeper than the maximum during decode', () => {
3122
- const primitive = SignatureEnvelope.serialize(innerP256)
3123
- const depth1 = Hex.concat(
3124
- '0x05',
3125
- Rlp.fromHex([nestedAccount, [primitive]]),
3126
- )
3127
- const depth2 = Hex.concat('0x05', Rlp.fromHex([nestedAccount, [depth1]]))
3128
- const depth3 = Hex.concat('0x05', Rlp.fromHex([account, [depth2]]))
3129
-
3130
- expect(() => SignatureEnvelope.deserialize(depth3)).toThrowError(
3131
- SignatureEnvelope.InvalidSerializedError,
3132
- )
3133
- })
3134
-
3135
- test('assert: rejects nested approvals carrying `init`', () => {
3136
- expect(() =>
3137
- SignatureEnvelope.assert({
3138
- type: 'multisig',
3139
- account,
3140
- signatures: [
3141
- {
3142
- ...nested,
3143
- init: {
3144
- threshold: 1,
3145
- owners: [{ owner: nestedAccount, weight: 1 }],
3281
+ test.each([
3282
+ {
3283
+ name: 'two fields',
3284
+ value: Rlp.fromHex([
3285
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3286
+ [Hex.slice(initial, -65)],
3287
+ ]),
3288
+ },
3289
+ {
3290
+ name: 'trailing field',
3291
+ value: Rlp.fromHex([
3292
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3293
+ MultisigConfig.toTuple(
3294
+ MultisigConfig.from({
3295
+ owners: [
3296
+ {
3297
+ owner: '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf',
3298
+ weight: 1,
3146
3299
  },
3147
- },
3148
- ],
3149
- } as never),
3150
- ).toThrowErrorMatchingInlineSnapshot(
3151
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: nested multisig owner approvals cannot carry \`init\`.]`,
3152
- )
3153
- })
3154
- })
3155
-
3156
- describe('init (bootstrap)', () => {
3157
- const init = {
3158
- salt: `0x${'00'.repeat(32)}` as const,
3159
- threshold: 1,
3160
- owners: [
3161
- {
3162
- owner: '0x1111111111111111111111111111111111111111' as const,
3163
- weight: 1,
3164
- },
3165
- ],
3166
- }
3167
- const bootstrapAccount = MultisigConfig.getAddress(init)
3168
-
3169
- const bootstrapEnvelope = SignatureEnvelope.from({
3170
- type: 'multisig',
3171
- account: bootstrapAccount,
3172
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
3173
- init,
3174
- })
3175
-
3176
- test('serialize/deserialize round-trip with init', () => {
3177
- const serialized = SignatureEnvelope.serialize(bootstrapEnvelope)
3178
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(
3179
- bootstrapEnvelope,
3180
- )
3181
- })
3182
-
3183
- test('deserialize: validates bootstrap init semantics', () => {
3184
- const invalid = Hex.concat(
3185
- '0x05',
3186
- Rlp.fromHex([
3187
- [MultisigConfig.zeroSalt, '0x01', []],
3188
- [SignatureEnvelope.serialize(innerP256)],
3189
- ]),
3190
- )
3191
- expect(() => SignatureEnvelope.deserialize(invalid)).toThrowError(
3192
- MultisigConfig.InvalidConfigError,
3193
- )
3194
- })
3195
-
3196
- test('deserialize: rejects noncanonical bootstrap field widths', () => {
3197
- const signatures = [SignatureEnvelope.serialize(innerP256)]
3198
- const shortSalt = Hex.concat(
3199
- '0x05',
3200
- Rlp.fromHex([
3201
- [`0x${'00'.repeat(31)}`, '0x01', [[init.owners[0]!.owner, '0x01']]],
3202
- signatures,
3203
- ]),
3204
- )
3205
- const wideThreshold = Hex.concat(
3206
- '0x05',
3207
- Rlp.fromHex([
3208
- [init.salt, '0x0001', [[init.owners[0]!.owner, '0x01']]],
3209
- signatures,
3210
- ]),
3211
- )
3212
- const wideWeight = Hex.concat(
3213
- '0x05',
3214
- Rlp.fromHex([
3215
- [init.salt, '0x01', [[init.owners[0]!.owner, '0x0001']]],
3216
- signatures,
3217
- ]),
3218
- )
3219
-
3220
- for (const serialized of [shortSalt, wideThreshold, wideWeight])
3221
- expect(() => SignatureEnvelope.deserialize(serialized)).toThrowError(
3222
- SignatureEnvelope.InvalidSerializedError,
3223
- )
3224
- })
3225
-
3226
- test('serialize: wire shape is `0x05 || rlp([init, signatures])`', () => {
3227
- const serialized = SignatureEnvelope.serialize(bootstrapEnvelope)
3228
- const [address] = Rlp.toHex(Hex.slice(serialized, 1)) as readonly [
3229
- MultisigConfig.Tuple,
3230
- readonly Hex.Hex[],
3231
- ]
3232
- expect(MultisigConfig.fromTuple(address)).toEqual(init)
3233
- })
3234
-
3235
- test('serialize/deserialize round-trip preserves non-zero salt', () => {
3236
- const saltedInit = {
3237
- ...init,
3238
- salt: `0x${'42'.repeat(32)}` as const,
3239
- }
3240
- const salted = SignatureEnvelope.from({
3241
- type: 'multisig',
3242
- account: MultisigConfig.getAddress(saltedInit),
3243
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
3244
- init: saltedInit,
3245
- })
3246
- const serialized = SignatureEnvelope.serialize(salted)
3247
- const deserialized = SignatureEnvelope.deserialize(
3248
- serialized,
3249
- ) as SignatureEnvelope.Multisig
3250
- expect(deserialized.init?.salt).toBe(`0x${'42'.repeat(32)}`)
3251
- expect(deserialized).toEqual(salted)
3252
- })
3253
-
3254
- test('absent init has no `init` key after deserialize', () => {
3255
- const serialized = SignatureEnvelope.serialize(envelope)
3256
- const deserialized = SignatureEnvelope.deserialize(serialized)
3257
- expect('init' in deserialized).toBe(false)
3258
- })
3259
-
3260
- test('init absent vs present produce different serializations', () => {
3261
- expect(SignatureEnvelope.serialize(envelope)).not.toBe(
3262
- SignatureEnvelope.serialize(bootstrapEnvelope),
3263
- )
3264
- })
3265
-
3266
- test('toRpc/fromRpc round-trip with init', () => {
3267
- const rpc = SignatureEnvelope.toRpc(
3268
- bootstrapEnvelope,
3269
- ) as SignatureEnvelope.MultisigRpc
3270
- expect(rpc.init).toEqual(init)
3271
- expect(rpc.account).toBeUndefined()
3272
- expect('type' in rpc).toBe(false)
3273
- expect(SignatureEnvelope.fromRpc(rpc)).toEqual(bootstrapEnvelope)
3274
- })
3275
-
3276
- test('toRpc encodes owner approvals as structured RPC envelopes', () => {
3277
- const multisig = bootstrapEnvelope as SignatureEnvelope.Multisig
3278
- const rpc = SignatureEnvelope.toRpc(
3279
- multisig,
3280
- ) as SignatureEnvelope.MultisigRpc
3281
- expect(rpc.signatures).toEqual(
3282
- multisig.signatures.map((signature) =>
3283
- SignatureEnvelope.toRpc(signature),
3300
+ ],
3301
+ threshold: 1,
3302
+ }),
3284
3303
  ),
3285
- )
3286
- })
3287
-
3288
- test('fromRpc detects multisig by shape (no `type` field)', () => {
3289
- const rpc = SignatureEnvelope.toRpc(bootstrapEnvelope)
3290
- expect(SignatureEnvelope.fromRpc(rpc)).toEqual(bootstrapEnvelope)
3291
- })
3292
-
3293
- test('fromRpc accepts the opposite static property as undefined', () => {
3294
- const rpc = SignatureEnvelope.toRpc(
3295
- bootstrapEnvelope,
3296
- ) as SignatureEnvelope.MultisigRpc
3297
- expect(
3298
- SignatureEnvelope.fromRpc({ ...rpc, account: undefined } as never),
3299
- ).toEqual(bootstrapEnvelope)
3300
- })
3301
-
3302
- test('fromRpc rejects the legacy combined account and init shape', () => {
3303
- const rpc = SignatureEnvelope.toRpc(
3304
- bootstrapEnvelope,
3305
- ) as SignatureEnvelope.MultisigRpc
3306
- expect(() =>
3307
- SignatureEnvelope.fromRpc({
3308
- ...rpc,
3309
- account: bootstrapAccount,
3310
- } as never),
3311
- ).toThrowErrorMatchingInlineSnapshot(
3312
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: RPC multisig must contain exactly one of \`account\` or \`init\`.]`,
3313
- )
3314
- })
3315
-
3316
- test('assert: invalid init config throws', () => {
3317
- expect(() =>
3318
- SignatureEnvelope.assert({
3319
- type: 'multisig',
3320
- account: bootstrapAccount,
3321
- signatures: [innerP256],
3322
- init: { threshold: 1, owners: [] },
3323
- } as never),
3324
- ).toThrowError()
3325
- })
3304
+ [Hex.slice(initial, -65)],
3305
+ '0x',
3306
+ ]),
3307
+ },
3308
+ ])('error: rejects malformed $name wire shape', ({ value }) => {
3309
+ expect(() =>
3310
+ SignatureEnvelope.deserialize(Hex.concat('0x05', value)),
3311
+ ).toThrowError(SignatureEnvelope.InvalidSerializedError)
3312
+ })
3313
+
3314
+ test.each([
3315
+ {
3316
+ field: 'salt',
3317
+ value: [
3318
+ ('0x' + '00'.repeat(31)) as Hex.Hex,
3319
+ '0x',
3320
+ '0x01',
3321
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3322
+ ],
3323
+ },
3324
+ {
3325
+ field: 'oversized version',
3326
+ value: [
3327
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3328
+ ('0x' + '01'.repeat(9)) as Hex.Hex,
3329
+ '0x01',
3330
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3331
+ ],
3332
+ },
3333
+ {
3334
+ field: 'zero version',
3335
+ value: [
3336
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3337
+ '0x00',
3338
+ '0x01',
3339
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3340
+ ],
3341
+ },
3342
+ {
3343
+ field: 'zero-prefixed version',
3344
+ value: [
3345
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3346
+ '0x0001',
3347
+ '0x01',
3348
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3349
+ ],
3350
+ },
3351
+ {
3352
+ field: 'threshold',
3353
+ value: [
3354
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3355
+ '0x',
3356
+ '0x0001',
3357
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3358
+ ],
3359
+ },
3360
+ {
3361
+ field: 'weight',
3362
+ value: [
3363
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3364
+ '0x',
3365
+ '0x01',
3366
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x0001']],
3367
+ ],
3368
+ },
3369
+ ])('error: rejects a noncanonical $field', ({ value }) => {
3370
+ const malformed = Hex.concat(
3371
+ '0x05',
3372
+ Rlp.fromHex([
3373
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3374
+ value as never,
3375
+ [Hex.slice(initial, -65)],
3376
+ ]),
3377
+ )
3326
3378
 
3327
- test('assert: init must derive the supplied account', () => {
3328
- expect(() =>
3329
- SignatureEnvelope.assert({
3330
- type: 'multisig',
3331
- account,
3332
- signatures: [innerP256],
3333
- init,
3334
- }),
3335
- ).toThrowErrorMatchingInlineSnapshot(
3336
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig init does not derive account.]`,
3337
- )
3338
- })
3379
+ expect(() => SignatureEnvelope.deserialize(malformed)).toThrowError(
3380
+ SignatureEnvelope.InvalidSerializedError,
3381
+ )
3339
3382
  })
3340
3383
  })