ox 1.7.1 → 1.7.2

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 (121) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/Hash.js +1 -1
  3. package/dist/core/Hash.js.map +1 -1
  4. package/dist/tempo/KeyAuthorization.d.ts +1 -1
  5. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  6. package/dist/tempo/MultisigConfig.d.ts +151 -83
  7. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  8. package/dist/tempo/MultisigConfig.js +164 -86
  9. package/dist/tempo/MultisigConfig.js.map +1 -1
  10. package/dist/tempo/MultisigOperation.d.ts +324 -0
  11. package/dist/tempo/MultisigOperation.d.ts.map +1 -0
  12. package/dist/tempo/MultisigOperation.js +809 -0
  13. package/dist/tempo/MultisigOperation.js.map +1 -0
  14. package/dist/tempo/MultisigWitness.d.ts +106 -0
  15. package/dist/tempo/MultisigWitness.d.ts.map +1 -0
  16. package/dist/tempo/MultisigWitness.js +118 -0
  17. package/dist/tempo/MultisigWitness.js.map +1 -0
  18. package/dist/tempo/RpcSchemaTempo.d.ts +35 -0
  19. package/dist/tempo/RpcSchemaTempo.d.ts.map +1 -1
  20. package/dist/tempo/SignatureEnvelope.d.ts +60 -80
  21. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  22. package/dist/tempo/SignatureEnvelope.js +87 -146
  23. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  24. package/dist/tempo/TransactionRequest.d.ts +9 -17
  25. package/dist/tempo/TransactionRequest.d.ts.map +1 -1
  26. package/dist/tempo/TransactionRequest.js +7 -7
  27. package/dist/tempo/TransactionRequest.js.map +1 -1
  28. package/dist/tempo/TxEnvelopeTempo.d.ts +3 -2
  29. package/dist/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  30. package/dist/tempo/TxEnvelopeTempo.js +3 -2
  31. package/dist/tempo/TxEnvelopeTempo.js.map +1 -1
  32. package/dist/tempo/index.d.ts +22 -2
  33. package/dist/tempo/index.d.ts.map +1 -1
  34. package/dist/tempo/index.js +22 -2
  35. package/dist/tempo/index.js.map +1 -1
  36. package/dist/zod/tempo/AuthorizationTempo.d.ts +110 -110
  37. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  38. package/dist/zod/tempo/KeyAuthorization.d.ts +90 -83
  39. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  40. package/dist/zod/tempo/MultisigConfig.d.ts +31 -2
  41. package/dist/zod/tempo/MultisigConfig.d.ts.map +1 -1
  42. package/dist/zod/tempo/MultisigConfig.js +26 -3
  43. package/dist/zod/tempo/MultisigConfig.js.map +1 -1
  44. package/dist/zod/tempo/MultisigOperation.d.ts +168 -0
  45. package/dist/zod/tempo/MultisigOperation.d.ts.map +1 -0
  46. package/dist/zod/tempo/MultisigOperation.js +65 -0
  47. package/dist/zod/tempo/MultisigOperation.js.map +1 -0
  48. package/dist/zod/tempo/MultisigWitness.d.ts +152 -0
  49. package/dist/zod/tempo/MultisigWitness.d.ts.map +1 -0
  50. package/dist/zod/tempo/MultisigWitness.js +76 -0
  51. package/dist/zod/tempo/MultisigWitness.js.map +1 -0
  52. package/dist/zod/tempo/RpcSchemaTempo.d.ts +607 -102
  53. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  54. package/dist/zod/tempo/RpcSchemaTempo.js +38 -0
  55. package/dist/zod/tempo/RpcSchemaTempo.js.map +1 -1
  56. package/dist/zod/tempo/SignatureEnvelope.d.ts +33 -33
  57. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  58. package/dist/zod/tempo/SignatureEnvelope.js +9 -16
  59. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  60. package/dist/zod/tempo/Transaction.d.ts +228 -222
  61. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  62. package/dist/zod/tempo/TransactionRequest.d.ts +379 -176
  63. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  64. package/dist/zod/tempo/TransactionRequest.js +11 -22
  65. package/dist/zod/tempo/TransactionRequest.js.map +1 -1
  66. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +60 -48
  67. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  68. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts +15 -12
  69. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts.map +1 -1
  70. package/dist/zod/tempo/z.d.ts +2 -0
  71. package/dist/zod/tempo/z.d.ts.map +1 -1
  72. package/dist/zod/tempo/z.js +2 -0
  73. package/dist/zod/tempo/z.js.map +1 -1
  74. package/package.json +21 -1
  75. package/src/core/Hash.ts +1 -1
  76. package/src/core/_test/Hash.test.ts +15 -0
  77. package/src/tempo/KeyAuthorization.test-d.ts +13 -1
  78. package/src/tempo/KeyAuthorization.test.ts +65 -0
  79. package/src/tempo/KeyAuthorization.ts +1 -1
  80. package/src/tempo/MultisigConfig.test-d.ts +52 -0
  81. package/src/tempo/MultisigConfig.test.ts +288 -227
  82. package/src/tempo/MultisigConfig.ts +275 -104
  83. package/src/tempo/MultisigOperation.test-d.ts +77 -0
  84. package/src/tempo/MultisigOperation.test.ts +1883 -0
  85. package/src/tempo/MultisigOperation.ts +1254 -0
  86. package/src/tempo/MultisigWitness.test-d.ts +42 -0
  87. package/src/tempo/MultisigWitness.test.ts +288 -0
  88. package/src/tempo/MultisigWitness.ts +210 -0
  89. package/src/tempo/RpcSchemaTempo.test-d.ts +56 -0
  90. package/src/tempo/RpcSchemaTempo.ts +37 -0
  91. package/src/tempo/SignatureEnvelope.test-d.ts +63 -46
  92. package/src/tempo/SignatureEnvelope.test.ts +523 -500
  93. package/src/tempo/SignatureEnvelope.ts +172 -242
  94. package/src/tempo/TransactionRequest.test-d.ts +11 -0
  95. package/src/tempo/TransactionRequest.test.ts +285 -33
  96. package/src/tempo/TransactionRequest.ts +15 -20
  97. package/src/tempo/TxEnvelopeTempo.test-d.ts +7 -0
  98. package/src/tempo/TxEnvelopeTempo.test.ts +25 -0
  99. package/src/tempo/TxEnvelopeTempo.ts +5 -3
  100. package/src/tempo/index.ts +22 -2
  101. package/src/tempo/multisig.e2e.test.ts +113 -86
  102. package/src/version.ts +1 -1
  103. package/src/zod/tempo/MultisigConfig.ts +29 -3
  104. package/src/zod/tempo/MultisigOperation.ts +69 -0
  105. package/src/zod/tempo/MultisigWitness.ts +101 -0
  106. package/src/zod/tempo/RpcSchemaTempo.ts +43 -0
  107. package/src/zod/tempo/SignatureEnvelope.ts +20 -38
  108. package/src/zod/tempo/TransactionRequest.ts +17 -26
  109. package/src/zod/tempo/_test/KeyAuthorization.test.ts +11 -0
  110. package/src/zod/tempo/_test/MultisigConfig.test-d.ts +13 -0
  111. package/src/zod/tempo/_test/MultisigConfig.test.ts +13 -2
  112. package/src/zod/tempo/_test/MultisigOperation.test-d.ts +27 -0
  113. package/src/zod/tempo/_test/MultisigOperation.test.ts +68 -0
  114. package/src/zod/tempo/_test/MultisigWitness.test-d.ts +13 -0
  115. package/src/zod/tempo/_test/MultisigWitness.test.ts +149 -0
  116. package/src/zod/tempo/_test/RpcSchemaTempo.test-d.ts +31 -0
  117. package/src/zod/tempo/_test/RpcSchemaTempo.test.ts +41 -0
  118. package/src/zod/tempo/_test/SignatureEnvelope.test-d.ts +1 -1
  119. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +25 -34
  120. package/src/zod/tempo/_test/TransactionRequest.test.ts +176 -8
  121. package/src/zod/tempo/z.ts +2 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  Address,
3
+ Hash,
3
4
  Hex,
4
5
  P256,
5
6
  PublicKey,
@@ -1116,72 +1117,86 @@ describe('from', () => {
1116
1117
  })
1117
1118
 
1118
1119
  describe('multisig', () => {
1119
- const initialConfig = MultisigConfig.from({
1120
- threshold: 1,
1120
+ const config = MultisigConfig.from({
1121
1121
  owners: [
1122
1122
  {
1123
1123
  owner: '0x1111111111111111111111111111111111111111',
1124
1124
  weight: 1,
1125
1125
  },
1126
1126
  ],
1127
+ threshold: 1,
1127
1128
  })
1128
1129
 
1129
- test('behavior: derives `account` from `initialConfig`', () => {
1130
- const envelope = SignatureEnvelope.from({
1131
- initialConfig,
1132
- signatures: [SignatureEnvelope.from(signature_secp256k1)],
1133
- })
1134
-
1135
- expect(envelope).toMatchObject({
1136
- type: 'multisig',
1137
- account: MultisigConfig.getAddress(initialConfig),
1138
- })
1139
- expect('initialConfig' in envelope).toBe(false)
1140
- expect((envelope as SignatureEnvelope.Multisig).init).toBeUndefined()
1141
- })
1142
-
1143
- test('behavior: `init: true` opts into bootstrap (uses `initialConfig` as `init`)', () => {
1130
+ test('behavior: derives an initial account from config', () => {
1144
1131
  const envelope = SignatureEnvelope.from({
1145
- initialConfig,
1132
+ config,
1146
1133
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
1147
- init: true,
1148
1134
  })
1149
1135
 
1150
- expect((envelope as SignatureEnvelope.Multisig).init).toEqual(
1151
- initialConfig,
1136
+ expect(envelope).toMatchInlineSnapshot(
1137
+ {
1138
+ signatures: [
1139
+ {
1140
+ signature: {
1141
+ r: expect.anything(),
1142
+ s: expect.anything(),
1143
+ yParity: expect.any(Number),
1144
+ },
1145
+ },
1146
+ ],
1147
+ },
1148
+ `
1149
+ {
1150
+ "account": "0x8820d1497eeaf4f68e00b2cfc00a2f3b1dbb00da",
1151
+ "config": {
1152
+ "owners": [
1153
+ {
1154
+ "owner": "0x1111111111111111111111111111111111111111",
1155
+ "weight": 1,
1156
+ },
1157
+ ],
1158
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1159
+ "threshold": 1,
1160
+ "version": 0n,
1161
+ },
1162
+ "signatures": [
1163
+ {
1164
+ "signature": {
1165
+ "r": Anything,
1166
+ "s": Anything,
1167
+ "yParity": Any<Number>,
1168
+ },
1169
+ "type": "secp256k1",
1170
+ },
1171
+ ],
1172
+ "type": "multisig",
1173
+ }
1174
+ `,
1152
1175
  )
1153
1176
  })
1154
1177
 
1155
- test('behavior: `init` accepts an explicit config', () => {
1156
- const otherConfig = MultisigConfig.from({
1157
- threshold: 1,
1158
- owners: [
1159
- {
1160
- owner: '0x2222222222222222222222222222222222222222',
1161
- weight: 1,
1162
- },
1163
- ],
1164
- })
1178
+ test('behavior: accepts an explicit account for a current config', () => {
1165
1179
  const envelope = SignatureEnvelope.from({
1166
- initialConfig,
1180
+ account: '0x2222222222222222222222222222222222222222',
1181
+ config: { ...config, version: 1 },
1167
1182
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
1168
- init: otherConfig,
1169
1183
  })
1170
1184
 
1171
- expect((envelope as SignatureEnvelope.Multisig).init).toEqual(otherConfig)
1185
+ expect(envelope.account).toBe(
1186
+ '0x2222222222222222222222222222222222222222',
1187
+ )
1188
+ expect(envelope.config.version).toBe(1n)
1172
1189
  })
1173
1190
 
1174
- test('behavior: `{ account }` form still works', () => {
1175
- const account = MultisigConfig.getAddress(initialConfig)
1176
- const envelope = SignatureEnvelope.from({
1177
- account,
1178
- signatures: [SignatureEnvelope.from(signature_secp256k1)],
1179
- })
1180
-
1181
- expect(envelope).toMatchObject({
1182
- type: 'multisig',
1183
- account,
1184
- })
1191
+ test('error: requires an account for a current config', () => {
1192
+ expect(() =>
1193
+ SignatureEnvelope.from({
1194
+ config: { ...config, version: 1n },
1195
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1196
+ } as never),
1197
+ ).toThrowErrorMatchingInlineSnapshot(
1198
+ `[MultisigConfig.InvalidConfigError: Invalid native multisig config: account address requires version zero.]`,
1199
+ )
1185
1200
  })
1186
1201
  })
1187
1202
  })
@@ -1793,8 +1808,6 @@ describe('serialize', () => {
1793
1808
  })
1794
1809
 
1795
1810
  describe('sortMultisigApprovals', () => {
1796
- // Build owner key pairs first so we can construct a real initial config
1797
- // whose owner set matches the keys that produce the approvals below.
1798
1811
  const ownerKeys = Array.from({ length: 3 }, () => {
1799
1812
  const privateKey = Secp256k1.randomPrivateKey()
1800
1813
  const address = Address.fromPublicKey(
@@ -1806,14 +1819,16 @@ describe('sortMultisigApprovals', () => {
1806
1819
  Hex.toBigInt(a.address) < Hex.toBigInt(b.address) ? -1 : 1,
1807
1820
  )
1808
1821
 
1809
- const initialConfig = MultisigConfig.from({
1810
- threshold: 2,
1822
+ const config = MultisigConfig.from({
1811
1823
  owners: ascendingOwners.map((o) => ({ owner: o.address, weight: 1 })),
1824
+ threshold: 2,
1812
1825
  })
1826
+ const account = MultisigConfig.getAddress(config)
1813
1827
  const payload = `0x${'42'.repeat(32)}` as const
1814
1828
  const digest = MultisigConfig.getSignPayload({
1829
+ account,
1830
+ config,
1815
1831
  payload,
1816
- initialConfig,
1817
1832
  })
1818
1833
 
1819
1834
  const owners = ownerKeys.map((owner) => ({
@@ -1829,7 +1844,8 @@ describe('sortMultisigApprovals', () => {
1829
1844
 
1830
1845
  test('behavior: orders approvals ascending by recovered owner address', () => {
1831
1846
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1832
- initialConfig,
1847
+ account,
1848
+ config,
1833
1849
  payload,
1834
1850
  // Provide approvals in reverse of the canonical order.
1835
1851
  signatures: [...ascending].reverse().map((owner) => owner.signature),
@@ -1841,7 +1857,8 @@ describe('sortMultisigApprovals', () => {
1841
1857
  const signatures = ascending.map((owner) => owner.signature)
1842
1858
  expect(
1843
1859
  SignatureEnvelope.sortMultisigApprovals({
1844
- initialConfig,
1860
+ account,
1861
+ config,
1845
1862
  payload,
1846
1863
  signatures,
1847
1864
  }),
@@ -1850,7 +1867,8 @@ describe('sortMultisigApprovals', () => {
1850
1867
 
1851
1868
  test('behavior: recovered order matches the config owner order', () => {
1852
1869
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1853
- initialConfig,
1870
+ account,
1871
+ config,
1854
1872
  payload,
1855
1873
  signatures: owners.map((owner) => owner.signature),
1856
1874
  })
@@ -1859,23 +1877,6 @@ describe('sortMultisigApprovals', () => {
1859
1877
  )
1860
1878
  expect(recovered).toEqual(ascending.map((owner) => owner.address))
1861
1879
  })
1862
-
1863
- test('behavior: `initialConfig` and `{ account }` produce identical ordering', () => {
1864
- const account = MultisigConfig.getAddress(initialConfig)
1865
- const signatures = owners.map((owner) => owner.signature)
1866
-
1867
- const fromConfig = SignatureEnvelope.sortMultisigApprovals({
1868
- initialConfig,
1869
- payload,
1870
- signatures,
1871
- })
1872
- const fromAccount = SignatureEnvelope.sortMultisigApprovals({
1873
- account,
1874
- payload,
1875
- signatures,
1876
- })
1877
- expect(fromConfig).toEqual(fromAccount)
1878
- })
1879
1880
  })
1880
1881
 
1881
1882
  describe('validate', () => {
@@ -2880,146 +2881,371 @@ describe('CoercionError', () => {
2880
2881
  })
2881
2882
 
2882
2883
  describe('multisig', () => {
2883
- const account = '0x8ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7ae'
2884
-
2885
- // P256 signatures do not carry `yParity` in the wire format, so use a clean
2886
- // inner signature for round-trip equality checks.
2887
- const innerP256 = SignatureEnvelope.from({
2888
- signature: { r: p256Signature.r, s: p256Signature.s },
2889
- publicKey,
2890
- prehash: true,
2884
+ const initial =
2885
+ '0x05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b841869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b' as const
2886
+ const current =
2887
+ '0x05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000000101d7d6942b5ad5c4795c026514f8317c7a215e218dccd6cf01f843b8414a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e0504a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf51b' as const
2888
+ const nested =
2889
+ '0x05f8f0949969e2243075b27a8eab61009c59b77ab13b83f6f83ba033333333333333333333333333333333333333333333333333333333333333338001d7d6944f9f5b162a7464bcc260ce44d7ae0d935f9c583701f89cb89a05f897944f9f5b162a7464bcc260ce44d7ae0d935f9c5837f83ba022222222222222222222222222222222222222222222222222222222222222228001d7d6946813eb9362372eef6200f3b1dbc3f819671cba6901f843b841032aa6f3ea7b0b7069720d0f3891983c493d149326c5c957d864ed7371b8475e3e95325079b24491f4b6d68920e4b3bacd7c6094df6ed88b8674864ab559c8fb1b' as const
2890
+
2891
+ test('example: decodes and re-encodes the frozen initial witness', () => {
2892
+ const envelope = SignatureEnvelope.deserialize(initial)
2893
+
2894
+ expect(envelope).toMatchInlineSnapshot(`
2895
+ {
2896
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2897
+ "config": {
2898
+ "owners": [
2899
+ {
2900
+ "owner": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
2901
+ "weight": 1,
2902
+ },
2903
+ ],
2904
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
2905
+ "threshold": 1,
2906
+ "version": 0n,
2907
+ },
2908
+ "signatures": [
2909
+ {
2910
+ "signature": {
2911
+ "r": "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f985",
2912
+ "s": "0x35dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f4",
2913
+ "yParity": 0,
2914
+ },
2915
+ "type": "secp256k1",
2916
+ },
2917
+ ],
2918
+ "type": "multisig",
2919
+ }
2920
+ `)
2921
+ expect(SignatureEnvelope.serialize(envelope)).toBe(initial)
2891
2922
  })
2892
2923
 
2893
- const envelope = SignatureEnvelope.from({
2894
- type: 'multisig',
2895
- account,
2896
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
2924
+ test('example: decodes and re-encodes the frozen current witness', () => {
2925
+ const envelope = SignatureEnvelope.deserialize(current)
2926
+
2927
+ expect(envelope).toMatchInlineSnapshot(`
2928
+ {
2929
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2930
+ "config": {
2931
+ "owners": [
2932
+ {
2933
+ "owner": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
2934
+ "weight": 1,
2935
+ },
2936
+ ],
2937
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
2938
+ "threshold": 1,
2939
+ "version": 1n,
2940
+ },
2941
+ "signatures": [
2942
+ {
2943
+ "signature": {
2944
+ "r": "0x4a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e050",
2945
+ "s": "0x4a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf5",
2946
+ "yParity": 0,
2947
+ },
2948
+ "type": "secp256k1",
2949
+ },
2950
+ ],
2951
+ "type": "multisig",
2952
+ }
2953
+ `)
2954
+ expect(SignatureEnvelope.serialize(envelope)).toBe(current)
2897
2955
  })
2898
2956
 
2899
- test('serialize: type byte 0x05 prefix', () => {
2900
- const serialized = SignatureEnvelope.serialize(envelope)
2901
- expect(serialized.startsWith('0x05')).toBe(true)
2957
+ test('example: decodes and re-encodes the frozen nested witness', () => {
2958
+ const envelope = SignatureEnvelope.deserialize(nested)
2959
+
2960
+ expect(envelope).toMatchInlineSnapshot(`
2961
+ {
2962
+ "account": "0x9969e2243075b27a8eab61009c59b77ab13b83f6",
2963
+ "config": {
2964
+ "owners": [
2965
+ {
2966
+ "owner": "0x4f9f5b162a7464bcc260ce44d7ae0d935f9c5837",
2967
+ "weight": 1,
2968
+ },
2969
+ ],
2970
+ "salt": "0x3333333333333333333333333333333333333333333333333333333333333333",
2971
+ "threshold": 1,
2972
+ "version": 0n,
2973
+ },
2974
+ "signatures": [
2975
+ {
2976
+ "account": "0x4f9f5b162a7464bcc260ce44d7ae0d935f9c5837",
2977
+ "config": {
2978
+ "owners": [
2979
+ {
2980
+ "owner": "0x6813eb9362372eef6200f3b1dbc3f819671cba69",
2981
+ "weight": 1,
2982
+ },
2983
+ ],
2984
+ "salt": "0x2222222222222222222222222222222222222222222222222222222222222222",
2985
+ "threshold": 1,
2986
+ "version": 0n,
2987
+ },
2988
+ "signatures": [
2989
+ {
2990
+ "signature": {
2991
+ "r": "0x032aa6f3ea7b0b7069720d0f3891983c493d149326c5c957d864ed7371b8475e",
2992
+ "s": "0x3e95325079b24491f4b6d68920e4b3bacd7c6094df6ed88b8674864ab559c8fb",
2993
+ "yParity": 0,
2994
+ },
2995
+ "type": "secp256k1",
2996
+ },
2997
+ ],
2998
+ "type": "multisig",
2999
+ },
3000
+ ],
3001
+ "type": "multisig",
3002
+ }
3003
+ `)
3004
+ expect(SignatureEnvelope.serialize(envelope)).toBe(nested)
2902
3005
  })
2903
3006
 
2904
- test('serialize: wire shape is `0x05 || rlp([account, signatures])`', () => {
2905
- const deterministic = SignatureEnvelope.from({
2906
- type: 'multisig',
2907
- account,
2908
- signatures: [innerP256],
2909
- })
2910
- expect(SignatureEnvelope.serialize(deterministic)).toMatchInlineSnapshot(
2911
- `"0x05f89b948ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7aef884b88201ccbb3485d4726235f13cb15ef394fb7158179fb7b1925eccec0147671090c52e77c3c53373cc1e3b05e7c23f609deb17cea8fe097300c45411237e9fe4166b35ad8ac16e167d6992c3e120d7f17d2376bc1cbcf30c46ba6dd00ce07303e742f511edf6ce1c32de66846f56afa7be1cbd729bc35750b6d0cdcf3ec9d75461aba001"`,
2912
- )
3007
+ test('behavior: preserves exact 65-byte secp256k1 precedence', () => {
3008
+ const serialized = ('0x05' +
3009
+ '00'.repeat(31) +
3010
+ '00'.repeat(31) +
3011
+ '01' +
3012
+ '00') as Hex.Hex
3013
+
3014
+ expect(Hex.size(serialized)).toBe(65)
3015
+ expect(SignatureEnvelope.deserialize(serialized).type).toBe('secp256k1')
2913
3016
  })
2914
3017
 
2915
- test('serialize/deserialize round-trip', () => {
3018
+ test('behavior: serializes account, config, and approvals in wire order', () => {
3019
+ const envelope = SignatureEnvelope.deserialize(initial)
3020
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
2916
3021
  const serialized = SignatureEnvelope.serialize(envelope)
2917
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(envelope)
3022
+ const decoded = Rlp.toHex(Hex.slice(serialized, 1))
3023
+
3024
+ expect(decoded).toMatchInlineSnapshot(`
3025
+ [
3026
+ "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
3027
+ [
3028
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
3029
+ "0x",
3030
+ "0x01",
3031
+ [
3032
+ [
3033
+ "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
3034
+ "0x01",
3035
+ ],
3036
+ ],
3037
+ ],
3038
+ [
3039
+ "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b",
3040
+ ],
3041
+ ]
3042
+ `)
2918
3043
  })
2919
3044
 
2920
- test('deserialize: rejects the legacy three-field wire shape', () => {
2921
- const init = MultisigConfig.toTuple({
2922
- threshold: 1,
2923
- owners: [
2924
- { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
2925
- ],
2926
- })
2927
- const legacy = Hex.concat(
2928
- '0x05',
2929
- Rlp.fromHex([account, [SignatureEnvelope.serialize(innerP256)], init]),
2930
- )
2931
- expect(() => SignatureEnvelope.deserialize(legacy)).toThrowError(
2932
- SignatureEnvelope.InvalidSerializedError,
2933
- )
2934
- })
3045
+ test('behavior: round trips initial and current RPC witnesses', () => {
3046
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3047
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3048
+ const rpc = {
3049
+ current: SignatureEnvelope.toRpc(currentEnvelope),
3050
+ initial: SignatureEnvelope.toRpc(initialEnvelope),
3051
+ }
2935
3052
 
2936
- test('deserialize: rejects an invalid initialized account', () => {
2937
- const malformed = Hex.concat(
2938
- '0x05',
2939
- Rlp.fromHex(['0x11', [SignatureEnvelope.serialize(innerP256)]]),
3053
+ expect(rpc).toMatchInlineSnapshot(`
3054
+ {
3055
+ "current": {
3056
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
3057
+ "config": {
3058
+ "owners": [
3059
+ {
3060
+ "owner": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
3061
+ "weight": 1,
3062
+ },
3063
+ ],
3064
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
3065
+ "threshold": 1,
3066
+ "version": "0x1",
3067
+ },
3068
+ "signatures": [
3069
+ {
3070
+ "r": "0x4a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e050",
3071
+ "s": "0x4a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf5",
3072
+ "type": "secp256k1",
3073
+ "yParity": "0x0",
3074
+ },
3075
+ ],
3076
+ },
3077
+ "initial": {
3078
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
3079
+ "config": {
3080
+ "owners": [
3081
+ {
3082
+ "owner": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
3083
+ "weight": 1,
3084
+ },
3085
+ ],
3086
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
3087
+ "threshold": 1,
3088
+ "version": "0x0",
3089
+ },
3090
+ "signatures": [
3091
+ {
3092
+ "r": "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f985",
3093
+ "s": "0x35dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f4",
3094
+ "type": "secp256k1",
3095
+ "yParity": "0x0",
3096
+ },
3097
+ ],
3098
+ },
3099
+ }
3100
+ `)
3101
+ expect(SignatureEnvelope.fromRpc(rpc.initial)).toStrictEqual(
3102
+ initialEnvelope,
2940
3103
  )
2941
- expect(() => SignatureEnvelope.deserialize(malformed)).toThrowError(
2942
- SignatureEnvelope.InvalidSerializedError,
3104
+ expect(SignatureEnvelope.fromRpc(rpc.current)).toStrictEqual(
3105
+ currentEnvelope,
2943
3106
  )
2944
3107
  })
2945
3108
 
2946
- test('getType', () => {
2947
- expect(SignatureEnvelope.getType(envelope)).toBe('multisig')
2948
- expect(
2949
- SignatureEnvelope.getType({
2950
- init: {
2951
- threshold: 1,
2952
- owners: [
2953
- { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
2954
- ],
2955
- },
2956
- signatures: [innerP256],
3109
+ test('behavior: derives the account only for an initial witness', () => {
3110
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3111
+ if (initialEnvelope.type !== 'multisig') throw new Error('unreachable')
3112
+ if (initialEnvelope.config.version !== 0n) throw new Error('unreachable')
3113
+ const derived = SignatureEnvelope.from({
3114
+ config: initialEnvelope.config as MultisigConfig.Config<0n>,
3115
+ signatures: initialEnvelope.signatures,
3116
+ })
3117
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3118
+ if (currentEnvelope.type !== 'multisig') throw new Error('unreachable')
3119
+
3120
+ expect(derived.account).toBe(initialEnvelope.account)
3121
+ expect(() =>
3122
+ SignatureEnvelope.from({
3123
+ config: currentEnvelope.config,
3124
+ signatures: currentEnvelope.signatures,
2957
3125
  } as never),
2958
- ).toBe('multisig')
3126
+ ).toThrowErrorMatchingInlineSnapshot(
3127
+ `[MultisigConfig.InvalidConfigError: Invalid native multisig config: account address requires version zero.]`,
3128
+ )
2959
3129
  })
2960
3130
 
2961
- test('extractAddress returns the multisig account', () => {
3131
+ test('behavior: extracts the multisig account', () => {
3132
+ const envelope = SignatureEnvelope.deserialize(initial)
3133
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3134
+
2962
3135
  expect(
2963
3136
  SignatureEnvelope.extractAddress({
2964
3137
  payload: '0xdeadbeef',
2965
3138
  signature: envelope,
2966
3139
  }),
2967
- ).toBe(account)
2968
- })
2969
-
2970
- test('toRpc/fromRpc round-trip', () => {
2971
- const rpc = SignatureEnvelope.toRpc(
2972
- envelope,
2973
- ) as SignatureEnvelope.MultisigRpc
2974
- expect(rpc).toMatchObject({
2975
- account,
2976
- signatures: [{ type: 'secp256k1' }, { type: 'p256' }],
3140
+ ).toBe(envelope.account)
3141
+ })
3142
+
3143
+ test('example: matches the frozen eight-approval boundary vector', () => {
3144
+ const config = MultisigConfig.from({
3145
+ owners: Array.from({ length: 8 }, (_, index) => ({
3146
+ owner:
3147
+ `0x${(index + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
3148
+ weight: 1,
3149
+ })),
3150
+ salt: `0x${'66'.repeat(32)}`,
3151
+ threshold: 8,
3152
+ })
3153
+ const approval = SignatureEnvelope.from({
3154
+ r: '0x840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565',
3155
+ s: '0x25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1',
3156
+ yParity: 0,
3157
+ })
3158
+ const envelope = SignatureEnvelope.from({
3159
+ config,
3160
+ signatures: Array.from(
3161
+ { length: MultisigConfig.maxSignatures },
3162
+ () => approval,
3163
+ ),
2977
3164
  })
2978
- expect(rpc.init).toBeUndefined()
2979
- expect('type' in rpc).toBe(false)
2980
- expect(SignatureEnvelope.fromRpc(rpc)).toEqual(envelope)
2981
- expect(
2982
- SignatureEnvelope.fromRpc({ ...rpc, type: 'multisig' } as never),
2983
- ).toEqual(envelope)
3165
+ const serialized = SignatureEnvelope.serialize(envelope)
3166
+
3167
+ expect({
3168
+ hash: Hash.keccak256(serialized),
3169
+ length: Hex.size(serialized),
3170
+ signatureCount: envelope.signatures.length,
3171
+ }).toMatchInlineSnapshot(`
3172
+ {
3173
+ "hash": "0x1cb47fcf31fb1ebb3177ec1744eeda195e4d789920d1214af32baaa6150ad5e2",
3174
+ "length": 787,
3175
+ "signatureCount": 8,
3176
+ }
3177
+ `)
2984
3178
  })
2985
3179
 
2986
- test('assert: missing properties', () => {
2987
- expect(() =>
2988
- SignatureEnvelope.assert({ type: 'multisig', account } as never),
2989
- ).toThrowError()
3180
+ test('behavior: accepts initial and current nested witnesses', () => {
3181
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3182
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3183
+ if (
3184
+ initialEnvelope.type !== 'multisig' ||
3185
+ currentEnvelope.type !== 'multisig'
3186
+ )
3187
+ throw new Error('unreachable')
3188
+
3189
+ for (const signature of [initialEnvelope, currentEnvelope]) {
3190
+ const config = MultisigConfig.from({
3191
+ owners: [{ owner: signature.account, weight: 1 }],
3192
+ salt: ('0x' + 'ff'.repeat(32)) as Hex.Hex,
3193
+ threshold: 1,
3194
+ version: 1n,
3195
+ })
3196
+ const envelope = SignatureEnvelope.from({
3197
+ account: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
3198
+ config,
3199
+ signatures: [signature],
3200
+ })
3201
+
3202
+ expect(
3203
+ SignatureEnvelope.deserialize(SignatureEnvelope.serialize(envelope)),
3204
+ ).toStrictEqual(envelope)
3205
+ }
2990
3206
  })
2991
3207
 
2992
- test('assert: rejects empty owner approvals', () => {
3208
+ test('error: rejects an initial account mismatch', () => {
3209
+ const envelope = SignatureEnvelope.deserialize(initial)
3210
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3211
+
2993
3212
  expect(() =>
2994
3213
  SignatureEnvelope.assert({
2995
- type: 'multisig',
2996
- account,
2997
- signatures: [],
3214
+ ...envelope,
3215
+ account: '0x1111111111111111111111111111111111111111',
2998
3216
  }),
2999
3217
  ).toThrowErrorMatchingInlineSnapshot(
3000
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig signatures cannot be empty.]`,
3218
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: initial multisig config does not derive account.]`,
3001
3219
  )
3002
3220
  })
3003
3221
 
3004
- test('assert: accepts at most eight owner approvals', () => {
3222
+ test('error: rejects a self-owned current witness', () => {
3223
+ const envelope = SignatureEnvelope.deserialize(current)
3224
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3225
+ const account = envelope.config.owners[0]!.owner
3226
+
3005
3227
  expect(() =>
3006
- SignatureEnvelope.assert({
3007
- type: 'multisig',
3008
- account,
3009
- signatures: Array.from(
3010
- { length: MultisigConfig.maxSignatures },
3011
- () => innerP256,
3012
- ),
3013
- }),
3014
- ).not.toThrow()
3228
+ SignatureEnvelope.assert({ ...envelope, account }),
3229
+ ).toThrowErrorMatchingInlineSnapshot(
3230
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig account cannot be an owner.]`,
3231
+ )
3232
+ })
3015
3233
 
3234
+ test('error: rejects empty and excess approvals', () => {
3235
+ const envelope = SignatureEnvelope.deserialize(current)
3236
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3237
+
3238
+ expect(() =>
3239
+ SignatureEnvelope.assert({ ...envelope, signatures: [] }),
3240
+ ).toThrowErrorMatchingInlineSnapshot(
3241
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig signatures cannot be empty.]`,
3242
+ )
3016
3243
  expect(() =>
3017
3244
  SignatureEnvelope.assert({
3018
- type: 'multisig',
3019
- account,
3245
+ ...envelope,
3020
3246
  signatures: Array.from(
3021
3247
  { length: MultisigConfig.maxSignatures + 1 },
3022
- () => innerP256,
3248
+ () => envelope.signatures[0]!,
3023
3249
  ),
3024
3250
  }),
3025
3251
  ).toThrowErrorMatchingInlineSnapshot(
@@ -3027,7 +3253,23 @@ describe('multisig', () => {
3027
3253
  )
3028
3254
  })
3029
3255
 
3030
- test('assert: rejects oversized owner approvals', () => {
3256
+ test('error: rejects keychain owner approvals', () => {
3257
+ const envelope = SignatureEnvelope.deserialize(current)
3258
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3259
+
3260
+ expect(() =>
3261
+ SignatureEnvelope.assert({
3262
+ ...envelope,
3263
+ signatures: [signature_keychain_secp256k1],
3264
+ } as never),
3265
+ ).toThrowErrorMatchingInlineSnapshot(
3266
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
3267
+ )
3268
+ })
3269
+
3270
+ test('error: rejects oversized primitive approvals', () => {
3271
+ const envelope = SignatureEnvelope.deserialize(current)
3272
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3031
3273
  const oversized = {
3032
3274
  ...signature_webauthn,
3033
3275
  metadata: {
@@ -3035,354 +3277,135 @@ describe('multisig', () => {
3035
3277
  clientDataJSON: JSON.stringify({ value: 'x'.repeat(2048) }),
3036
3278
  },
3037
3279
  }
3280
+
3038
3281
  expect(() =>
3039
3282
  SignatureEnvelope.assert({
3040
- type: 'multisig',
3041
- account,
3283
+ ...envelope,
3042
3284
  signatures: [oversized],
3043
3285
  }),
3044
3286
  ).toThrowErrorMatchingInlineSnapshot(
3045
3287
  `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig owner signature exceeds 2049 bytes.]`,
3046
3288
  )
3047
-
3048
- const serialized = Hex.concat(
3049
- '0x05',
3050
- Rlp.fromHex([account, [SignatureEnvelope.serialize(oversized)]]),
3051
- )
3052
- expect(() => SignatureEnvelope.deserialize(serialized)).toThrowError(
3053
- SignatureEnvelope.InvalidSerializedError,
3054
- )
3055
3289
  })
3056
3290
 
3057
- test('assert: rejects keychain owner approvals', () => {
3291
+ test('error: rejects nesting deeper than two nodes', () => {
3292
+ const envelope = SignatureEnvelope.deserialize(initial)
3293
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3294
+ const depth2 = SignatureEnvelope.from({
3295
+ ...envelope,
3296
+ signatures: [envelope],
3297
+ })
3298
+
3058
3299
  expect(() =>
3059
3300
  SignatureEnvelope.assert({
3060
- type: 'multisig',
3061
- account,
3062
- signatures: [signature_keychain_secp256k1],
3063
- } as never),
3301
+ ...envelope,
3302
+ signatures: [depth2],
3303
+ }),
3064
3304
  ).toThrowErrorMatchingInlineSnapshot(
3065
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
3305
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig nesting depth exceeds 2.]`,
3066
3306
  )
3067
3307
  })
3068
3308
 
3069
- describe('nested approvals', () => {
3070
- const nestedAccount = '0x1111111111111111111111111111111111111111'
3071
- const nested = SignatureEnvelope.from({
3072
- type: 'multisig',
3073
- account: nestedAccount,
3074
- signatures: [innerP256],
3075
- })
3076
-
3077
- test('serialize/deserialize round-trip with a nested multisig approval', () => {
3078
- const parent = SignatureEnvelope.from({
3079
- type: 'multisig',
3080
- account,
3081
- signatures: [innerP256, nested],
3082
- })
3083
- const serialized = SignatureEnvelope.serialize(parent)
3084
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(parent)
3085
- })
3086
-
3087
- test('assert: accepts the maximum nesting depth', () => {
3088
- const depth2 = SignatureEnvelope.from({
3089
- type: 'multisig',
3090
- account,
3091
- signatures: [nested],
3092
- })
3093
- expect(() => SignatureEnvelope.assert(depth2)).not.toThrowError()
3094
- })
3095
-
3096
- test('accepts a nested approval above the primitive byte limit', () => {
3097
- const primitive = SignatureEnvelope.from({
3098
- ...signature_webauthn,
3099
- metadata: {
3100
- ...signature_webauthn.metadata,
3101
- clientDataJSON: JSON.stringify({ value: 'x'.repeat(1_050) }),
3102
- },
3103
- signature: {
3104
- r: signature_webauthn.signature.r,
3105
- s: signature_webauthn.signature.s,
3106
- },
3107
- })
3108
- const largeNested = SignatureEnvelope.from({
3109
- type: 'multisig',
3110
- account: nestedAccount,
3111
- signatures: [primitive, primitive],
3112
- })
3113
- expect(Hex.size(SignatureEnvelope.serialize(primitive))).toBeLessThan(
3114
- MultisigConfig.maxOwnerSignatureBytes,
3115
- )
3116
- expect(
3117
- Hex.size(SignatureEnvelope.serialize(largeNested)),
3118
- ).toBeGreaterThan(MultisigConfig.maxOwnerSignatureBytes)
3119
-
3120
- const parent = SignatureEnvelope.from({
3121
- type: 'multisig',
3122
- account,
3123
- signatures: [largeNested],
3124
- })
3125
- expect(
3126
- SignatureEnvelope.deserialize(SignatureEnvelope.serialize(parent)),
3127
- ).toEqual(parent)
3128
- })
3129
-
3130
- test('assert: rejects nesting deeper than the maximum', () => {
3131
- const depth3 = SignatureEnvelope.from({
3132
- type: 'multisig',
3133
- account,
3134
- signatures: [
3135
- SignatureEnvelope.from({
3136
- type: 'multisig',
3137
- account: nestedAccount,
3138
- signatures: [nested],
3139
- }),
3140
- ],
3141
- })
3142
- expect(() =>
3143
- SignatureEnvelope.assert(depth3),
3144
- ).toThrowErrorMatchingInlineSnapshot(
3145
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig nesting depth exceeds 2.]`,
3146
- )
3147
- })
3148
-
3149
- test('deserialize: rejects nesting deeper than the maximum during decode', () => {
3150
- const primitive = SignatureEnvelope.serialize(innerP256)
3151
- const depth1 = Hex.concat(
3152
- '0x05',
3153
- Rlp.fromHex([nestedAccount, [primitive]]),
3154
- )
3155
- const depth2 = Hex.concat('0x05', Rlp.fromHex([nestedAccount, [depth1]]))
3156
- const depth3 = Hex.concat('0x05', Rlp.fromHex([account, [depth2]]))
3157
-
3158
- expect(() => SignatureEnvelope.deserialize(depth3)).toThrowError(
3159
- SignatureEnvelope.InvalidSerializedError,
3160
- )
3161
- })
3162
-
3163
- test('assert: rejects nested approvals carrying `init`', () => {
3164
- expect(() =>
3165
- SignatureEnvelope.assert({
3166
- type: 'multisig',
3167
- account,
3168
- signatures: [
3169
- {
3170
- ...nested,
3171
- init: {
3172
- threshold: 1,
3173
- owners: [{ owner: nestedAccount, weight: 1 }],
3309
+ test.each([
3310
+ {
3311
+ name: 'two fields',
3312
+ value: Rlp.fromHex([
3313
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3314
+ [Hex.slice(initial, -65)],
3315
+ ]),
3316
+ },
3317
+ {
3318
+ name: 'trailing field',
3319
+ value: Rlp.fromHex([
3320
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3321
+ MultisigConfig.toTuple(
3322
+ MultisigConfig.from({
3323
+ owners: [
3324
+ {
3325
+ owner: '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf',
3326
+ weight: 1,
3174
3327
  },
3175
- },
3176
- ],
3177
- } as never),
3178
- ).toThrowErrorMatchingInlineSnapshot(
3179
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: nested multisig owner approvals cannot carry \`init\`.]`,
3180
- )
3181
- })
3182
- })
3183
-
3184
- describe('init (bootstrap)', () => {
3185
- const init = {
3186
- salt: `0x${'00'.repeat(32)}` as const,
3187
- threshold: 1,
3188
- owners: [
3189
- {
3190
- owner: '0x1111111111111111111111111111111111111111' as const,
3191
- weight: 1,
3192
- },
3193
- ],
3194
- }
3195
- const bootstrapAccount = MultisigConfig.getAddress(init)
3196
-
3197
- const bootstrapEnvelope = SignatureEnvelope.from({
3198
- type: 'multisig',
3199
- account: bootstrapAccount,
3200
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
3201
- init,
3202
- })
3203
-
3204
- test('serialize/deserialize round-trip with init', () => {
3205
- const serialized = SignatureEnvelope.serialize(bootstrapEnvelope)
3206
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(
3207
- bootstrapEnvelope,
3208
- )
3209
- })
3210
-
3211
- test('deserialize: validates bootstrap init semantics', () => {
3212
- const invalid = Hex.concat(
3213
- '0x05',
3214
- Rlp.fromHex([
3215
- [MultisigConfig.zeroSalt, '0x01', []],
3216
- [SignatureEnvelope.serialize(innerP256)],
3217
- ]),
3218
- )
3219
- expect(() => SignatureEnvelope.deserialize(invalid)).toThrowError(
3220
- MultisigConfig.InvalidConfigError,
3221
- )
3222
- })
3223
-
3224
- test('deserialize: rejects noncanonical bootstrap field widths', () => {
3225
- const signatures = [SignatureEnvelope.serialize(innerP256)]
3226
- const shortSalt = Hex.concat(
3227
- '0x05',
3228
- Rlp.fromHex([
3229
- [`0x${'00'.repeat(31)}`, '0x01', [[init.owners[0]!.owner, '0x01']]],
3230
- signatures,
3231
- ]),
3232
- )
3233
- const wideThreshold = Hex.concat(
3234
- '0x05',
3235
- Rlp.fromHex([
3236
- [init.salt, '0x0001', [[init.owners[0]!.owner, '0x01']]],
3237
- signatures,
3238
- ]),
3239
- )
3240
- const wideWeight = Hex.concat(
3241
- '0x05',
3242
- Rlp.fromHex([
3243
- [init.salt, '0x01', [[init.owners[0]!.owner, '0x0001']]],
3244
- signatures,
3245
- ]),
3246
- )
3247
-
3248
- for (const serialized of [shortSalt, wideThreshold, wideWeight])
3249
- expect(() => SignatureEnvelope.deserialize(serialized)).toThrowError(
3250
- SignatureEnvelope.InvalidSerializedError,
3251
- )
3252
- })
3253
-
3254
- test('serialize: wire shape is `0x05 || rlp([init, signatures])`', () => {
3255
- const serialized = SignatureEnvelope.serialize(bootstrapEnvelope)
3256
- const [address] = Rlp.toHex(Hex.slice(serialized, 1)) as readonly [
3257
- MultisigConfig.Tuple,
3258
- readonly Hex.Hex[],
3259
- ]
3260
- expect(MultisigConfig.fromTuple(address)).toEqual(init)
3261
- })
3262
-
3263
- test('serialize/deserialize round-trip preserves non-zero salt', () => {
3264
- const saltedInit = {
3265
- ...init,
3266
- salt: `0x${'42'.repeat(32)}` as const,
3267
- }
3268
- const salted = SignatureEnvelope.from({
3269
- type: 'multisig',
3270
- account: MultisigConfig.getAddress(saltedInit),
3271
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
3272
- init: saltedInit,
3273
- })
3274
- const serialized = SignatureEnvelope.serialize(salted)
3275
- const deserialized = SignatureEnvelope.deserialize(
3276
- serialized,
3277
- ) as SignatureEnvelope.Multisig
3278
- expect(deserialized.init?.salt).toBe(`0x${'42'.repeat(32)}`)
3279
- expect(deserialized).toEqual(salted)
3280
- })
3281
-
3282
- test('absent init has no `init` key after deserialize', () => {
3283
- const serialized = SignatureEnvelope.serialize(envelope)
3284
- const deserialized = SignatureEnvelope.deserialize(serialized)
3285
- expect('init' in deserialized).toBe(false)
3286
- })
3287
-
3288
- test('init absent vs present produce different serializations', () => {
3289
- expect(SignatureEnvelope.serialize(envelope)).not.toBe(
3290
- SignatureEnvelope.serialize(bootstrapEnvelope),
3291
- )
3292
- })
3293
-
3294
- test('toRpc/fromRpc round-trip with init', () => {
3295
- const rpc = SignatureEnvelope.toRpc(
3296
- bootstrapEnvelope,
3297
- ) as SignatureEnvelope.MultisigRpc
3298
- expect(rpc.init).toEqual(init)
3299
- expect(rpc.account).toBeUndefined()
3300
- expect('type' in rpc).toBe(false)
3301
- expect(SignatureEnvelope.fromRpc(rpc)).toEqual(bootstrapEnvelope)
3302
- })
3303
-
3304
- test('toRpc normalizes numberish init fields and the default salt', () => {
3305
- const rpc = SignatureEnvelope.toRpc({
3306
- ...bootstrapEnvelope,
3307
- init: {
3308
- threshold: '0x1',
3309
- owners: init.owners.map((owner) => ({
3310
- ...owner,
3311
- weight: '0x1' as const,
3312
- })),
3313
- },
3314
- } as SignatureEnvelope.Multisig<
3315
- number | Hex.Hex
3316
- >) as SignatureEnvelope.MultisigRpc
3317
- expect(rpc.init).toEqual({
3318
- salt: MultisigConfig.zeroSalt,
3319
- threshold: 1,
3320
- owners: init.owners,
3321
- })
3322
- })
3323
-
3324
- test('toRpc encodes owner approvals as structured RPC envelopes', () => {
3325
- const multisig = bootstrapEnvelope as SignatureEnvelope.Multisig
3326
- const rpc = SignatureEnvelope.toRpc(
3327
- multisig,
3328
- ) as SignatureEnvelope.MultisigRpc
3329
- expect(rpc.signatures).toEqual(
3330
- multisig.signatures.map((signature) =>
3331
- SignatureEnvelope.toRpc(signature),
3328
+ ],
3329
+ threshold: 1,
3330
+ }),
3332
3331
  ),
3333
- )
3334
- })
3335
-
3336
- test('fromRpc detects multisig by shape (no `type` field)', () => {
3337
- const rpc = SignatureEnvelope.toRpc(bootstrapEnvelope)
3338
- expect(SignatureEnvelope.fromRpc(rpc)).toEqual(bootstrapEnvelope)
3339
- })
3340
-
3341
- test('fromRpc accepts the opposite static property as undefined', () => {
3342
- const rpc = SignatureEnvelope.toRpc(
3343
- bootstrapEnvelope,
3344
- ) as SignatureEnvelope.MultisigRpc
3345
- expect(
3346
- SignatureEnvelope.fromRpc({ ...rpc, account: undefined } as never),
3347
- ).toEqual(bootstrapEnvelope)
3348
- })
3349
-
3350
- test('fromRpc rejects the legacy combined account and init shape', () => {
3351
- const rpc = SignatureEnvelope.toRpc(
3352
- bootstrapEnvelope,
3353
- ) as SignatureEnvelope.MultisigRpc
3354
- expect(() =>
3355
- SignatureEnvelope.fromRpc({
3356
- ...rpc,
3357
- account: bootstrapAccount,
3358
- } as never),
3359
- ).toThrowErrorMatchingInlineSnapshot(
3360
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: RPC multisig must contain exactly one of \`account\` or \`init\`.]`,
3361
- )
3362
- })
3363
-
3364
- test('assert: invalid init config throws', () => {
3365
- expect(() =>
3366
- SignatureEnvelope.assert({
3367
- type: 'multisig',
3368
- account: bootstrapAccount,
3369
- signatures: [innerP256],
3370
- init: { threshold: 1, owners: [] },
3371
- } as never),
3372
- ).toThrowError()
3373
- })
3332
+ [Hex.slice(initial, -65)],
3333
+ '0x',
3334
+ ]),
3335
+ },
3336
+ ])('error: rejects malformed $name wire shape', ({ value }) => {
3337
+ expect(() =>
3338
+ SignatureEnvelope.deserialize(Hex.concat('0x05', value)),
3339
+ ).toThrowError(SignatureEnvelope.InvalidSerializedError)
3340
+ })
3341
+
3342
+ test.each([
3343
+ {
3344
+ field: 'salt',
3345
+ value: [
3346
+ ('0x' + '00'.repeat(31)) as Hex.Hex,
3347
+ '0x',
3348
+ '0x01',
3349
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3350
+ ],
3351
+ },
3352
+ {
3353
+ field: 'oversized version',
3354
+ value: [
3355
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3356
+ ('0x' + '01'.repeat(9)) as Hex.Hex,
3357
+ '0x01',
3358
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3359
+ ],
3360
+ },
3361
+ {
3362
+ field: 'zero version',
3363
+ value: [
3364
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3365
+ '0x00',
3366
+ '0x01',
3367
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3368
+ ],
3369
+ },
3370
+ {
3371
+ field: 'zero-prefixed version',
3372
+ value: [
3373
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3374
+ '0x0001',
3375
+ '0x01',
3376
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3377
+ ],
3378
+ },
3379
+ {
3380
+ field: 'threshold',
3381
+ value: [
3382
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3383
+ '0x',
3384
+ '0x0001',
3385
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3386
+ ],
3387
+ },
3388
+ {
3389
+ field: 'weight',
3390
+ value: [
3391
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3392
+ '0x',
3393
+ '0x01',
3394
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x0001']],
3395
+ ],
3396
+ },
3397
+ ])('error: rejects a noncanonical $field', ({ value }) => {
3398
+ const malformed = Hex.concat(
3399
+ '0x05',
3400
+ Rlp.fromHex([
3401
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3402
+ value as never,
3403
+ [Hex.slice(initial, -65)],
3404
+ ]),
3405
+ )
3374
3406
 
3375
- test('assert: init must derive the supplied account', () => {
3376
- expect(() =>
3377
- SignatureEnvelope.assert({
3378
- type: 'multisig',
3379
- account,
3380
- signatures: [innerP256],
3381
- init,
3382
- }),
3383
- ).toThrowErrorMatchingInlineSnapshot(
3384
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig init does not derive account.]`,
3385
- )
3386
- })
3407
+ expect(() => SignatureEnvelope.deserialize(malformed)).toThrowError(
3408
+ SignatureEnvelope.InvalidSerializedError,
3409
+ )
3387
3410
  })
3388
3411
  })