ox 1.7.1 → 1.7.4

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 (123) hide show
  1. package/CHANGELOG.md +42 -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 +2 -2
  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 +340 -0
  11. package/dist/tempo/MultisigOperation.d.ts.map +1 -0
  12. package/dist/tempo/MultisigOperation.js +839 -0
  13. package/dist/tempo/MultisigOperation.js.map +1 -0
  14. package/dist/tempo/MultisigSimulation.d.ts +132 -0
  15. package/dist/tempo/MultisigSimulation.d.ts.map +1 -0
  16. package/dist/tempo/MultisigSimulation.js +140 -0
  17. package/dist/tempo/MultisigSimulation.js.map +1 -0
  18. package/dist/tempo/RpcSchemaTempo.d.ts +45 -0
  19. package/dist/tempo/RpcSchemaTempo.d.ts.map +1 -1
  20. package/dist/tempo/SignatureEnvelope.d.ts +55 -86
  21. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  22. package/dist/tempo/SignatureEnvelope.js +93 -158
  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 +60 -180
  37. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  38. package/dist/zod/tempo/KeyAuthorization.d.ts +65 -118
  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 +158 -0
  45. package/dist/zod/tempo/MultisigOperation.d.ts.map +1 -0
  46. package/dist/zod/tempo/MultisigOperation.js +62 -0
  47. package/dist/zod/tempo/MultisigOperation.js.map +1 -0
  48. package/dist/zod/tempo/MultisigSimulation.d.ts +120 -0
  49. package/dist/zod/tempo/MultisigSimulation.d.ts.map +1 -0
  50. package/dist/zod/tempo/MultisigSimulation.js +78 -0
  51. package/dist/zod/tempo/MultisigSimulation.js.map +1 -0
  52. package/dist/zod/tempo/RpcSchemaTempo.d.ts +528 -144
  53. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  54. package/dist/zod/tempo/RpcSchemaTempo.js +47 -0
  55. package/dist/zod/tempo/RpcSchemaTempo.js.map +1 -1
  56. package/dist/zod/tempo/SignatureEnvelope.d.ts +18 -54
  57. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  58. package/dist/zod/tempo/SignatureEnvelope.js +5 -22
  59. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  60. package/dist/zod/tempo/Transaction.d.ts +138 -348
  61. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  62. package/dist/zod/tempo/TransactionRequest.d.ts +286 -239
  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/AuthorizationTempo.test.ts +4 -0
  78. package/src/tempo/KeyAuthorization.test-d.ts +13 -12
  79. package/src/tempo/KeyAuthorization.test.ts +109 -3
  80. package/src/tempo/KeyAuthorization.ts +4 -4
  81. package/src/tempo/MultisigConfig.test-d.ts +52 -0
  82. package/src/tempo/MultisigConfig.test.ts +288 -227
  83. package/src/tempo/MultisigConfig.ts +275 -104
  84. package/src/tempo/MultisigOperation.test-d.ts +79 -0
  85. package/src/tempo/MultisigOperation.test.ts +1950 -0
  86. package/src/tempo/MultisigOperation.ts +1293 -0
  87. package/src/tempo/MultisigSimulation.test-d.ts +33 -0
  88. package/src/tempo/MultisigSimulation.test.ts +283 -0
  89. package/src/tempo/MultisigSimulation.ts +266 -0
  90. package/src/tempo/RpcSchemaTempo.test-d.ts +91 -0
  91. package/src/tempo/RpcSchemaTempo.ts +46 -0
  92. package/src/tempo/SignatureEnvelope.test-d.ts +33 -72
  93. package/src/tempo/SignatureEnvelope.test.ts +533 -500
  94. package/src/tempo/SignatureEnvelope.ts +171 -258
  95. package/src/tempo/Transaction.test.ts +61 -0
  96. package/src/tempo/TransactionRequest.test-d.ts +11 -0
  97. package/src/tempo/TransactionRequest.test.ts +214 -41
  98. package/src/tempo/TransactionRequest.ts +19 -20
  99. package/src/tempo/TxEnvelopeTempo.test-d.ts +7 -0
  100. package/src/tempo/TxEnvelopeTempo.test.ts +25 -0
  101. package/src/tempo/TxEnvelopeTempo.ts +5 -3
  102. package/src/tempo/index.ts +22 -2
  103. package/src/tempo/multisig.e2e.test.ts +164 -87
  104. package/src/version.ts +1 -1
  105. package/src/zod/tempo/MultisigConfig.ts +29 -3
  106. package/src/zod/tempo/MultisigOperation.ts +66 -0
  107. package/src/zod/tempo/MultisigSimulation.ts +98 -0
  108. package/src/zod/tempo/RpcSchemaTempo.ts +53 -0
  109. package/src/zod/tempo/SignatureEnvelope.ts +12 -54
  110. package/src/zod/tempo/TransactionRequest.ts +17 -26
  111. package/src/zod/tempo/_test/KeyAuthorization.test.ts +2 -4
  112. package/src/zod/tempo/_test/MultisigConfig.test-d.ts +13 -0
  113. package/src/zod/tempo/_test/MultisigConfig.test.ts +13 -2
  114. package/src/zod/tempo/_test/MultisigOperation.test-d.ts +27 -0
  115. package/src/zod/tempo/_test/MultisigOperation.test.ts +66 -0
  116. package/src/zod/tempo/_test/MultisigSimulation.test-d.ts +13 -0
  117. package/src/zod/tempo/_test/MultisigSimulation.test.ts +78 -0
  118. package/src/zod/tempo/_test/RpcSchemaTempo.test-d.ts +48 -0
  119. package/src/zod/tempo/_test/RpcSchemaTempo.test.ts +83 -0
  120. package/src/zod/tempo/_test/SignatureEnvelope.test-d.ts +1 -1
  121. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +57 -120
  122. package/src/zod/tempo/_test/TransactionRequest.test.ts +43 -16
  123. 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,
@@ -67,6 +68,9 @@ const signature_keychain_webauthn = SignatureEnvelope.from({
67
68
  version: 'v2',
68
69
  })
69
70
 
71
+ const signature_multisig_rpc =
72
+ '0xf897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b841869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b' as const
73
+
70
74
  describe('assert', () => {
71
75
  describe('secp256k1', () => {
72
76
  test('behavior: validates valid signature', () => {
@@ -1116,72 +1120,86 @@ describe('from', () => {
1116
1120
  })
1117
1121
 
1118
1122
  describe('multisig', () => {
1119
- const initialConfig = MultisigConfig.from({
1120
- threshold: 1,
1123
+ const config = MultisigConfig.from({
1121
1124
  owners: [
1122
1125
  {
1123
1126
  owner: '0x1111111111111111111111111111111111111111',
1124
1127
  weight: 1,
1125
1128
  },
1126
1129
  ],
1130
+ threshold: 1,
1127
1131
  })
1128
1132
 
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`)', () => {
1133
+ test('behavior: derives an initial account from config', () => {
1144
1134
  const envelope = SignatureEnvelope.from({
1145
- initialConfig,
1135
+ config,
1146
1136
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
1147
- init: true,
1148
1137
  })
1149
1138
 
1150
- expect((envelope as SignatureEnvelope.Multisig).init).toEqual(
1151
- initialConfig,
1139
+ expect(envelope).toMatchInlineSnapshot(
1140
+ {
1141
+ signatures: [
1142
+ {
1143
+ signature: {
1144
+ r: expect.anything(),
1145
+ s: expect.anything(),
1146
+ yParity: expect.any(Number),
1147
+ },
1148
+ },
1149
+ ],
1150
+ },
1151
+ `
1152
+ {
1153
+ "account": "0x8820d1497eeaf4f68e00b2cfc00a2f3b1dbb00da",
1154
+ "config": {
1155
+ "owners": [
1156
+ {
1157
+ "owner": "0x1111111111111111111111111111111111111111",
1158
+ "weight": 1,
1159
+ },
1160
+ ],
1161
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
1162
+ "threshold": 1,
1163
+ "version": 0n,
1164
+ },
1165
+ "signatures": [
1166
+ {
1167
+ "signature": {
1168
+ "r": Anything,
1169
+ "s": Anything,
1170
+ "yParity": Any<Number>,
1171
+ },
1172
+ "type": "secp256k1",
1173
+ },
1174
+ ],
1175
+ "type": "multisig",
1176
+ }
1177
+ `,
1152
1178
  )
1153
1179
  })
1154
1180
 
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
- })
1181
+ test('behavior: accepts an explicit account for a current config', () => {
1165
1182
  const envelope = SignatureEnvelope.from({
1166
- initialConfig,
1183
+ account: '0x2222222222222222222222222222222222222222',
1184
+ config: { ...config, version: 1 },
1167
1185
  signatures: [SignatureEnvelope.from(signature_secp256k1)],
1168
- init: otherConfig,
1169
1186
  })
1170
1187
 
1171
- expect((envelope as SignatureEnvelope.Multisig).init).toEqual(otherConfig)
1188
+ expect(envelope.account).toBe(
1189
+ '0x2222222222222222222222222222222222222222',
1190
+ )
1191
+ expect(envelope.config.version).toBe(1n)
1172
1192
  })
1173
1193
 
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
- })
1194
+ test('error: requires an account for a current config', () => {
1195
+ expect(() =>
1196
+ SignatureEnvelope.from({
1197
+ config: { ...config, version: 1n },
1198
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1199
+ } as never),
1200
+ ).toThrowErrorMatchingInlineSnapshot(
1201
+ `[MultisigConfig.InvalidConfigError: Invalid native multisig config: account address requires version zero.]`,
1202
+ )
1185
1203
  })
1186
1204
  })
1187
1205
  })
@@ -1793,8 +1811,6 @@ describe('serialize', () => {
1793
1811
  })
1794
1812
 
1795
1813
  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
1814
  const ownerKeys = Array.from({ length: 3 }, () => {
1799
1815
  const privateKey = Secp256k1.randomPrivateKey()
1800
1816
  const address = Address.fromPublicKey(
@@ -1806,14 +1822,16 @@ describe('sortMultisigApprovals', () => {
1806
1822
  Hex.toBigInt(a.address) < Hex.toBigInt(b.address) ? -1 : 1,
1807
1823
  )
1808
1824
 
1809
- const initialConfig = MultisigConfig.from({
1810
- threshold: 2,
1825
+ const config = MultisigConfig.from({
1811
1826
  owners: ascendingOwners.map((o) => ({ owner: o.address, weight: 1 })),
1827
+ threshold: 2,
1812
1828
  })
1829
+ const account = MultisigConfig.getAddress(config)
1813
1830
  const payload = `0x${'42'.repeat(32)}` as const
1814
1831
  const digest = MultisigConfig.getSignPayload({
1832
+ account,
1833
+ config,
1815
1834
  payload,
1816
- initialConfig,
1817
1835
  })
1818
1836
 
1819
1837
  const owners = ownerKeys.map((owner) => ({
@@ -1829,7 +1847,8 @@ describe('sortMultisigApprovals', () => {
1829
1847
 
1830
1848
  test('behavior: orders approvals ascending by recovered owner address', () => {
1831
1849
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1832
- initialConfig,
1850
+ account,
1851
+ config,
1833
1852
  payload,
1834
1853
  // Provide approvals in reverse of the canonical order.
1835
1854
  signatures: [...ascending].reverse().map((owner) => owner.signature),
@@ -1841,7 +1860,8 @@ describe('sortMultisigApprovals', () => {
1841
1860
  const signatures = ascending.map((owner) => owner.signature)
1842
1861
  expect(
1843
1862
  SignatureEnvelope.sortMultisigApprovals({
1844
- initialConfig,
1863
+ account,
1864
+ config,
1845
1865
  payload,
1846
1866
  signatures,
1847
1867
  }),
@@ -1850,7 +1870,8 @@ describe('sortMultisigApprovals', () => {
1850
1870
 
1851
1871
  test('behavior: recovered order matches the config owner order', () => {
1852
1872
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1853
- initialConfig,
1873
+ account,
1874
+ config,
1854
1875
  payload,
1855
1876
  signatures: owners.map((owner) => owner.signature),
1856
1877
  })
@@ -1859,23 +1880,6 @@ describe('sortMultisigApprovals', () => {
1859
1880
  )
1860
1881
  expect(recovered).toEqual(ascending.map((owner) => owner.address))
1861
1882
  })
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
1883
  })
1880
1884
 
1881
1885
  describe('validate', () => {
@@ -2491,6 +2495,41 @@ describe('fromRpc', () => {
2491
2495
  expect(envelope.keyId).toBe('0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c')
2492
2496
  })
2493
2497
  })
2498
+ describe('multisig', () => {
2499
+ test('behavior: converts an untagged serialized multisig signature', () => {
2500
+ expect(
2501
+ SignatureEnvelope.serialize(
2502
+ SignatureEnvelope.fromRpc(signature_multisig_rpc),
2503
+ ),
2504
+ ).toMatchInlineSnapshot(`"0x05${signature_multisig_rpc.slice(2)}"`)
2505
+ })
2506
+
2507
+ test('error: rejects a payload with the transaction type byte', () => {
2508
+ expect(() =>
2509
+ SignatureEnvelope.fromRpc(`0x05${signature_multisig_rpc.slice(2)}`),
2510
+ ).toThrowError()
2511
+ })
2512
+
2513
+ test('error: rejects trailing bytes', () => {
2514
+ expect(() =>
2515
+ SignatureEnvelope.fromRpc(`${signature_multisig_rpc}00`),
2516
+ ).toThrowErrorMatchingInlineSnapshot(
2517
+ `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`1\` trailing byte remains.]`,
2518
+ )
2519
+ })
2520
+
2521
+ test('error: rejects a structured multisig signature', () => {
2522
+ expect(() =>
2523
+ SignatureEnvelope.fromRpc({
2524
+ account: '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
2525
+ config: {},
2526
+ signatures: [],
2527
+ } as never),
2528
+ ).toThrowErrorMatchingInlineSnapshot(
2529
+ `[SignatureEnvelope.CoercionError: Unable to coerce value (\`{"account":"0x9dba7f426b711d4893c11611eacf7cc334e7146b","config":{},"signatures":[]}\`) to a valid signature envelope.]`,
2530
+ )
2531
+ })
2532
+ })
2494
2533
  })
2495
2534
 
2496
2535
  describe('toRpc', () => {
@@ -2588,6 +2627,7 @@ describe('toRpc', () => {
2588
2627
  const rpc = SignatureEnvelope.toRpc(
2589
2628
  signature_keychain_p256,
2590
2629
  ) as SignatureEnvelope.KeychainRpc
2630
+ if (typeof rpc.signature === 'string') throw new Error('unreachable')
2591
2631
 
2592
2632
  expect(rpc.type).toBe('keychain')
2593
2633
  expect(rpc.userAddress).toBe(signature_keychain_p256.userAddress)
@@ -2600,6 +2640,7 @@ describe('toRpc', () => {
2600
2640
  const rpc = SignatureEnvelope.toRpc(
2601
2641
  signature_keychain_webauthn,
2602
2642
  ) as SignatureEnvelope.KeychainRpc
2643
+ if (typeof rpc.signature === 'string') throw new Error('unreachable')
2603
2644
 
2604
2645
  expect(rpc.type).toBe('keychain')
2605
2646
  expect(rpc.userAddress).toBe(signature_keychain_webauthn.userAddress)
@@ -2643,6 +2684,18 @@ describe('toRpc', () => {
2643
2684
  expect(rpc.keyId).toBeUndefined()
2644
2685
  })
2645
2686
  })
2687
+
2688
+ describe('multisig', () => {
2689
+ test('behavior: removes the transaction type byte', () => {
2690
+ const envelope = SignatureEnvelope.deserialize(
2691
+ `0x05${signature_multisig_rpc.slice(2)}`,
2692
+ )
2693
+
2694
+ expect(SignatureEnvelope.toRpc(envelope)).toMatchInlineSnapshot(
2695
+ `"${signature_multisig_rpc}"`,
2696
+ )
2697
+ })
2698
+ })
2646
2699
  })
2647
2700
 
2648
2701
  describe('roundtrip: toRpc <-> fromRpc', () => {
@@ -2880,146 +2933,329 @@ describe('CoercionError', () => {
2880
2933
  })
2881
2934
 
2882
2935
  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,
2936
+ const initial =
2937
+ '0x05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b841869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b' as const
2938
+ const current =
2939
+ '0x05f897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000000101d7d6942b5ad5c4795c026514f8317c7a215e218dccd6cf01f843b8414a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e0504a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf51b' as const
2940
+ const nested =
2941
+ '0x05f8f0949969e2243075b27a8eab61009c59b77ab13b83f6f83ba033333333333333333333333333333333333333333333333333333333333333338001d7d6944f9f5b162a7464bcc260ce44d7ae0d935f9c583701f89cb89a05f897944f9f5b162a7464bcc260ce44d7ae0d935f9c5837f83ba022222222222222222222222222222222222222222222222222222222222222228001d7d6946813eb9362372eef6200f3b1dbc3f819671cba6901f843b841032aa6f3ea7b0b7069720d0f3891983c493d149326c5c957d864ed7371b8475e3e95325079b24491f4b6d68920e4b3bacd7c6094df6ed88b8674864ab559c8fb1b' as const
2942
+
2943
+ test('example: decodes and re-encodes the frozen initial signature', () => {
2944
+ const envelope = SignatureEnvelope.deserialize(initial)
2945
+
2946
+ expect(envelope).toMatchInlineSnapshot(`
2947
+ {
2948
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2949
+ "config": {
2950
+ "owners": [
2951
+ {
2952
+ "owner": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
2953
+ "weight": 1,
2954
+ },
2955
+ ],
2956
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
2957
+ "threshold": 1,
2958
+ "version": 0n,
2959
+ },
2960
+ "signatures": [
2961
+ {
2962
+ "signature": {
2963
+ "r": "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f985",
2964
+ "s": "0x35dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f4",
2965
+ "yParity": 0,
2966
+ },
2967
+ "type": "secp256k1",
2968
+ },
2969
+ ],
2970
+ "type": "multisig",
2971
+ }
2972
+ `)
2973
+ expect(SignatureEnvelope.serialize(envelope)).toBe(initial)
2891
2974
  })
2892
2975
 
2893
- const envelope = SignatureEnvelope.from({
2894
- type: 'multisig',
2895
- account,
2896
- signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
2976
+ test('example: decodes and re-encodes the frozen current signature', () => {
2977
+ const envelope = SignatureEnvelope.deserialize(current)
2978
+
2979
+ expect(envelope).toMatchInlineSnapshot(`
2980
+ {
2981
+ "account": "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
2982
+ "config": {
2983
+ "owners": [
2984
+ {
2985
+ "owner": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
2986
+ "weight": 1,
2987
+ },
2988
+ ],
2989
+ "salt": "0x0000000000000000000000000000000000000000000000000000000000000000",
2990
+ "threshold": 1,
2991
+ "version": 1n,
2992
+ },
2993
+ "signatures": [
2994
+ {
2995
+ "signature": {
2996
+ "r": "0x4a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e050",
2997
+ "s": "0x4a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf5",
2998
+ "yParity": 0,
2999
+ },
3000
+ "type": "secp256k1",
3001
+ },
3002
+ ],
3003
+ "type": "multisig",
3004
+ }
3005
+ `)
3006
+ expect(SignatureEnvelope.serialize(envelope)).toBe(current)
2897
3007
  })
2898
3008
 
2899
- test('serialize: type byte 0x05 prefix', () => {
2900
- const serialized = SignatureEnvelope.serialize(envelope)
2901
- expect(serialized.startsWith('0x05')).toBe(true)
3009
+ test('example: decodes and re-encodes the frozen nested signature', () => {
3010
+ const envelope = SignatureEnvelope.deserialize(nested)
3011
+
3012
+ expect(envelope).toMatchInlineSnapshot(`
3013
+ {
3014
+ "account": "0x9969e2243075b27a8eab61009c59b77ab13b83f6",
3015
+ "config": {
3016
+ "owners": [
3017
+ {
3018
+ "owner": "0x4f9f5b162a7464bcc260ce44d7ae0d935f9c5837",
3019
+ "weight": 1,
3020
+ },
3021
+ ],
3022
+ "salt": "0x3333333333333333333333333333333333333333333333333333333333333333",
3023
+ "threshold": 1,
3024
+ "version": 0n,
3025
+ },
3026
+ "signatures": [
3027
+ {
3028
+ "account": "0x4f9f5b162a7464bcc260ce44d7ae0d935f9c5837",
3029
+ "config": {
3030
+ "owners": [
3031
+ {
3032
+ "owner": "0x6813eb9362372eef6200f3b1dbc3f819671cba69",
3033
+ "weight": 1,
3034
+ },
3035
+ ],
3036
+ "salt": "0x2222222222222222222222222222222222222222222222222222222222222222",
3037
+ "threshold": 1,
3038
+ "version": 0n,
3039
+ },
3040
+ "signatures": [
3041
+ {
3042
+ "signature": {
3043
+ "r": "0x032aa6f3ea7b0b7069720d0f3891983c493d149326c5c957d864ed7371b8475e",
3044
+ "s": "0x3e95325079b24491f4b6d68920e4b3bacd7c6094df6ed88b8674864ab559c8fb",
3045
+ "yParity": 0,
3046
+ },
3047
+ "type": "secp256k1",
3048
+ },
3049
+ ],
3050
+ "type": "multisig",
3051
+ },
3052
+ ],
3053
+ "type": "multisig",
3054
+ }
3055
+ `)
3056
+ expect(SignatureEnvelope.serialize(envelope)).toBe(nested)
2902
3057
  })
2903
3058
 
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
- )
3059
+ test('behavior: preserves exact 65-byte secp256k1 precedence', () => {
3060
+ const serialized = ('0x05' +
3061
+ '00'.repeat(31) +
3062
+ '00'.repeat(31) +
3063
+ '01' +
3064
+ '00') as Hex.Hex
3065
+
3066
+ expect(Hex.size(serialized)).toBe(65)
3067
+ expect(SignatureEnvelope.deserialize(serialized).type).toBe('secp256k1')
2913
3068
  })
2914
3069
 
2915
- test('serialize/deserialize round-trip', () => {
3070
+ test('behavior: serializes account, config, and approvals in wire order', () => {
3071
+ const envelope = SignatureEnvelope.deserialize(initial)
3072
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
2916
3073
  const serialized = SignatureEnvelope.serialize(envelope)
2917
- expect(SignatureEnvelope.deserialize(serialized)).toEqual(envelope)
3074
+ const decoded = Rlp.toHex(Hex.slice(serialized, 1))
3075
+
3076
+ expect(decoded).toMatchInlineSnapshot(`
3077
+ [
3078
+ "0x9dba7f426b711d4893c11611eacf7cc334e7146b",
3079
+ [
3080
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
3081
+ "0x",
3082
+ "0x01",
3083
+ [
3084
+ [
3085
+ "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
3086
+ "0x01",
3087
+ ],
3088
+ ],
3089
+ ],
3090
+ [
3091
+ "0x869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b",
3092
+ ],
3093
+ ]
3094
+ `)
2918
3095
  })
2919
3096
 
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
- })
3097
+ test('behavior: round trips initial and current RPC signatures', () => {
3098
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3099
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3100
+ const rpc = {
3101
+ current: SignatureEnvelope.toRpc(currentEnvelope),
3102
+ initial: SignatureEnvelope.toRpc(initialEnvelope),
3103
+ }
2935
3104
 
2936
- test('deserialize: rejects an invalid initialized account', () => {
2937
- const malformed = Hex.concat(
2938
- '0x05',
2939
- Rlp.fromHex(['0x11', [SignatureEnvelope.serialize(innerP256)]]),
3105
+ expect(rpc).toMatchInlineSnapshot(`
3106
+ {
3107
+ "current": "0xf897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000000101d7d6942b5ad5c4795c026514f8317c7a215e218dccd6cf01f843b8414a0e5b5b4a90f08e6e8d676a73a22dc2cf022ffdcc9299512b5d9a6daf66e0504a395a2ef6f6c0f173910b875c45d01aa66d928e56ba6f49bbc8186f80268bf51b",
3108
+ "initial": "0xf897949dba7f426b711d4893c11611eacf7cc334e7146bf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d6947e5f4552091a69125d5dfcb7b8c2659029395bdf01f843b841869437e01f64bebeb78a8a6b30bfd3a993819c8cad82c807515d9b9e9b36f98535dfaa5eebc597715d05f6ce4927747f14fa4cd2acc717fdcd3877146437f8f41b",
3109
+ }
3110
+ `)
3111
+ expect(SignatureEnvelope.fromRpc(rpc.initial)).toStrictEqual(
3112
+ initialEnvelope,
2940
3113
  )
2941
- expect(() => SignatureEnvelope.deserialize(malformed)).toThrowError(
2942
- SignatureEnvelope.InvalidSerializedError,
3114
+ expect(SignatureEnvelope.fromRpc(rpc.current)).toStrictEqual(
3115
+ currentEnvelope,
2943
3116
  )
2944
3117
  })
2945
3118
 
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],
3119
+ test('behavior: derives the account only for an initial config', () => {
3120
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3121
+ if (initialEnvelope.type !== 'multisig') throw new Error('unreachable')
3122
+ if (initialEnvelope.config.version !== 0n) throw new Error('unreachable')
3123
+ const derived = SignatureEnvelope.from({
3124
+ config: initialEnvelope.config as MultisigConfig.Config<0n>,
3125
+ signatures: initialEnvelope.signatures,
3126
+ })
3127
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3128
+ if (currentEnvelope.type !== 'multisig') throw new Error('unreachable')
3129
+
3130
+ expect(derived.account).toBe(initialEnvelope.account)
3131
+ expect(() =>
3132
+ SignatureEnvelope.from({
3133
+ config: currentEnvelope.config,
3134
+ signatures: currentEnvelope.signatures,
2957
3135
  } as never),
2958
- ).toBe('multisig')
3136
+ ).toThrowErrorMatchingInlineSnapshot(
3137
+ `[MultisigConfig.InvalidConfigError: Invalid native multisig config: account address requires version zero.]`,
3138
+ )
2959
3139
  })
2960
3140
 
2961
- test('extractAddress returns the multisig account', () => {
3141
+ test('behavior: extracts the multisig account', () => {
3142
+ const envelope = SignatureEnvelope.deserialize(initial)
3143
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3144
+
2962
3145
  expect(
2963
3146
  SignatureEnvelope.extractAddress({
2964
3147
  payload: '0xdeadbeef',
2965
3148
  signature: envelope,
2966
3149
  }),
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' }],
3150
+ ).toBe(envelope.account)
3151
+ })
3152
+
3153
+ test('example: matches the frozen eight-approval boundary vector', () => {
3154
+ const config = MultisigConfig.from({
3155
+ owners: Array.from({ length: 8 }, (_, index) => ({
3156
+ owner:
3157
+ `0x${(index + 1).toString(16).padStart(40, '0')}` as `0x${string}`,
3158
+ weight: 1,
3159
+ })),
3160
+ salt: `0x${'66'.repeat(32)}`,
3161
+ threshold: 8,
3162
+ })
3163
+ const approval = SignatureEnvelope.from({
3164
+ r: '0x840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565',
3165
+ s: '0x25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1',
3166
+ yParity: 0,
3167
+ })
3168
+ const envelope = SignatureEnvelope.from({
3169
+ config,
3170
+ signatures: Array.from(
3171
+ { length: MultisigConfig.maxSignatures },
3172
+ () => approval,
3173
+ ),
2977
3174
  })
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)
3175
+ const serialized = SignatureEnvelope.serialize(envelope)
3176
+
3177
+ expect({
3178
+ hash: Hash.keccak256(serialized),
3179
+ length: Hex.size(serialized),
3180
+ signatureCount: envelope.signatures.length,
3181
+ }).toMatchInlineSnapshot(`
3182
+ {
3183
+ "hash": "0x1cb47fcf31fb1ebb3177ec1744eeda195e4d789920d1214af32baaa6150ad5e2",
3184
+ "length": 787,
3185
+ "signatureCount": 8,
3186
+ }
3187
+ `)
2984
3188
  })
2985
3189
 
2986
- test('assert: missing properties', () => {
2987
- expect(() =>
2988
- SignatureEnvelope.assert({ type: 'multisig', account } as never),
2989
- ).toThrowError()
3190
+ test('behavior: accepts initial and current nested signatures', () => {
3191
+ const initialEnvelope = SignatureEnvelope.deserialize(initial)
3192
+ const currentEnvelope = SignatureEnvelope.deserialize(current)
3193
+ if (
3194
+ initialEnvelope.type !== 'multisig' ||
3195
+ currentEnvelope.type !== 'multisig'
3196
+ )
3197
+ throw new Error('unreachable')
3198
+
3199
+ for (const signature of [initialEnvelope, currentEnvelope]) {
3200
+ const config = MultisigConfig.from({
3201
+ owners: [{ owner: signature.account, weight: 1 }],
3202
+ salt: ('0x' + 'ff'.repeat(32)) as Hex.Hex,
3203
+ threshold: 1,
3204
+ version: 1n,
3205
+ })
3206
+ const envelope = SignatureEnvelope.from({
3207
+ account: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
3208
+ config,
3209
+ signatures: [signature],
3210
+ })
3211
+
3212
+ expect(
3213
+ SignatureEnvelope.deserialize(SignatureEnvelope.serialize(envelope)),
3214
+ ).toStrictEqual(envelope)
3215
+ }
2990
3216
  })
2991
3217
 
2992
- test('assert: rejects empty owner approvals', () => {
3218
+ test('error: rejects an initial account mismatch', () => {
3219
+ const envelope = SignatureEnvelope.deserialize(initial)
3220
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3221
+
2993
3222
  expect(() =>
2994
3223
  SignatureEnvelope.assert({
2995
- type: 'multisig',
2996
- account,
2997
- signatures: [],
3224
+ ...envelope,
3225
+ account: '0x1111111111111111111111111111111111111111',
2998
3226
  }),
2999
3227
  ).toThrowErrorMatchingInlineSnapshot(
3000
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig signatures cannot be empty.]`,
3228
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: initial multisig config does not derive account.]`,
3001
3229
  )
3002
3230
  })
3003
3231
 
3004
- test('assert: accepts at most eight owner approvals', () => {
3232
+ test('error: rejects a self-owned current signature', () => {
3233
+ const envelope = SignatureEnvelope.deserialize(current)
3234
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3235
+ const account = envelope.config.owners[0]!.owner
3236
+
3005
3237
  expect(() =>
3006
- SignatureEnvelope.assert({
3007
- type: 'multisig',
3008
- account,
3009
- signatures: Array.from(
3010
- { length: MultisigConfig.maxSignatures },
3011
- () => innerP256,
3012
- ),
3013
- }),
3014
- ).not.toThrow()
3238
+ SignatureEnvelope.assert({ ...envelope, account }),
3239
+ ).toThrowErrorMatchingInlineSnapshot(
3240
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig account cannot be an owner.]`,
3241
+ )
3242
+ })
3243
+
3244
+ test('error: rejects empty and excess approvals', () => {
3245
+ const envelope = SignatureEnvelope.deserialize(current)
3246
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3015
3247
 
3248
+ expect(() =>
3249
+ SignatureEnvelope.assert({ ...envelope, signatures: [] }),
3250
+ ).toThrowErrorMatchingInlineSnapshot(
3251
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig signatures cannot be empty.]`,
3252
+ )
3016
3253
  expect(() =>
3017
3254
  SignatureEnvelope.assert({
3018
- type: 'multisig',
3019
- account,
3255
+ ...envelope,
3020
3256
  signatures: Array.from(
3021
3257
  { length: MultisigConfig.maxSignatures + 1 },
3022
- () => innerP256,
3258
+ () => envelope.signatures[0]!,
3023
3259
  ),
3024
3260
  }),
3025
3261
  ).toThrowErrorMatchingInlineSnapshot(
@@ -3027,7 +3263,23 @@ describe('multisig', () => {
3027
3263
  )
3028
3264
  })
3029
3265
 
3030
- test('assert: rejects oversized owner approvals', () => {
3266
+ test('error: rejects keychain owner approvals', () => {
3267
+ const envelope = SignatureEnvelope.deserialize(current)
3268
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3269
+
3270
+ expect(() =>
3271
+ SignatureEnvelope.assert({
3272
+ ...envelope,
3273
+ signatures: [signature_keychain_secp256k1],
3274
+ } as never),
3275
+ ).toThrowErrorMatchingInlineSnapshot(
3276
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
3277
+ )
3278
+ })
3279
+
3280
+ test('error: rejects oversized primitive approvals', () => {
3281
+ const envelope = SignatureEnvelope.deserialize(current)
3282
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3031
3283
  const oversized = {
3032
3284
  ...signature_webauthn,
3033
3285
  metadata: {
@@ -3035,354 +3287,135 @@ describe('multisig', () => {
3035
3287
  clientDataJSON: JSON.stringify({ value: 'x'.repeat(2048) }),
3036
3288
  },
3037
3289
  }
3290
+
3038
3291
  expect(() =>
3039
3292
  SignatureEnvelope.assert({
3040
- type: 'multisig',
3041
- account,
3293
+ ...envelope,
3042
3294
  signatures: [oversized],
3043
3295
  }),
3044
3296
  ).toThrowErrorMatchingInlineSnapshot(
3045
3297
  `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig owner signature exceeds 2049 bytes.]`,
3046
3298
  )
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
3299
  })
3056
3300
 
3057
- test('assert: rejects keychain owner approvals', () => {
3301
+ test('error: rejects nesting deeper than two nodes', () => {
3302
+ const envelope = SignatureEnvelope.deserialize(initial)
3303
+ if (envelope.type !== 'multisig') throw new Error('unreachable')
3304
+ const depth2 = SignatureEnvelope.from({
3305
+ ...envelope,
3306
+ signatures: [envelope],
3307
+ })
3308
+
3058
3309
  expect(() =>
3059
3310
  SignatureEnvelope.assert({
3060
- type: 'multisig',
3061
- account,
3062
- signatures: [signature_keychain_secp256k1],
3063
- } as never),
3311
+ ...envelope,
3312
+ signatures: [depth2],
3313
+ }),
3064
3314
  ).toThrowErrorMatchingInlineSnapshot(
3065
- `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: keychain owner approvals are not allowed.]`,
3315
+ `[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig nesting depth exceeds 2.]`,
3066
3316
  )
3067
3317
  })
3068
3318
 
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 }],
3319
+ test.each([
3320
+ {
3321
+ name: 'two fields',
3322
+ value: Rlp.fromHex([
3323
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3324
+ [Hex.slice(initial, -65)],
3325
+ ]),
3326
+ },
3327
+ {
3328
+ name: 'trailing field',
3329
+ value: Rlp.fromHex([
3330
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3331
+ MultisigConfig.toTuple(
3332
+ MultisigConfig.from({
3333
+ owners: [
3334
+ {
3335
+ owner: '0x7e5f4552091a69125d5dfcb7b8c2659029395bdf',
3336
+ weight: 1,
3174
3337
  },
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),
3338
+ ],
3339
+ threshold: 1,
3340
+ }),
3332
3341
  ),
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
- })
3342
+ [Hex.slice(initial, -65)],
3343
+ '0x',
3344
+ ]),
3345
+ },
3346
+ ])('error: rejects malformed $name wire shape', ({ value }) => {
3347
+ expect(() =>
3348
+ SignatureEnvelope.deserialize(Hex.concat('0x05', value)),
3349
+ ).toThrowError(SignatureEnvelope.InvalidSerializedError)
3350
+ })
3351
+
3352
+ test.each([
3353
+ {
3354
+ field: 'salt',
3355
+ value: [
3356
+ ('0x' + '00'.repeat(31)) as Hex.Hex,
3357
+ '0x',
3358
+ '0x01',
3359
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3360
+ ],
3361
+ },
3362
+ {
3363
+ field: 'oversized version',
3364
+ value: [
3365
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3366
+ ('0x' + '01'.repeat(9)) as Hex.Hex,
3367
+ '0x01',
3368
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3369
+ ],
3370
+ },
3371
+ {
3372
+ field: 'zero version',
3373
+ value: [
3374
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3375
+ '0x00',
3376
+ '0x01',
3377
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3378
+ ],
3379
+ },
3380
+ {
3381
+ field: 'zero-prefixed version',
3382
+ value: [
3383
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3384
+ '0x0001',
3385
+ '0x01',
3386
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3387
+ ],
3388
+ },
3389
+ {
3390
+ field: 'threshold',
3391
+ value: [
3392
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3393
+ '0x',
3394
+ '0x0001',
3395
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x01']],
3396
+ ],
3397
+ },
3398
+ {
3399
+ field: 'weight',
3400
+ value: [
3401
+ ('0x' + '00'.repeat(32)) as Hex.Hex,
3402
+ '0x',
3403
+ '0x01',
3404
+ [['0x7e5f4552091a69125d5dfcb7b8c2659029395bdf', '0x0001']],
3405
+ ],
3406
+ },
3407
+ ])('error: rejects a noncanonical $field', ({ value }) => {
3408
+ const malformed = Hex.concat(
3409
+ '0x05',
3410
+ Rlp.fromHex([
3411
+ '0x9dba7f426b711d4893c11611eacf7cc334e7146b',
3412
+ value as never,
3413
+ [Hex.slice(initial, -65)],
3414
+ ]),
3415
+ )
3374
3416
 
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
- })
3417
+ expect(() => SignatureEnvelope.deserialize(malformed)).toThrowError(
3418
+ SignatureEnvelope.InvalidSerializedError,
3419
+ )
3387
3420
  })
3388
3421
  })