ox 1.7.2 → 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 (103) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/tempo/KeyAuthorization.d.ts +1 -1
  3. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  4. package/dist/tempo/MultisigOperation.d.ts +42 -26
  5. package/dist/tempo/MultisigOperation.d.ts.map +1 -1
  6. package/dist/tempo/MultisigOperation.js +78 -48
  7. package/dist/tempo/MultisigOperation.js.map +1 -1
  8. package/dist/tempo/MultisigSimulation.d.ts +132 -0
  9. package/dist/tempo/MultisigSimulation.d.ts.map +1 -0
  10. package/dist/tempo/MultisigSimulation.js +140 -0
  11. package/dist/tempo/MultisigSimulation.js.map +1 -0
  12. package/dist/tempo/RpcSchemaTempo.d.ts +20 -10
  13. package/dist/tempo/RpcSchemaTempo.d.ts.map +1 -1
  14. package/dist/tempo/SignatureEnvelope.d.ts +9 -20
  15. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  16. package/dist/tempo/SignatureEnvelope.js +12 -18
  17. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  18. package/dist/tempo/TransactionRequest.d.ts +6 -6
  19. package/dist/tempo/TransactionRequest.d.ts.map +1 -1
  20. package/dist/tempo/TransactionRequest.js +7 -7
  21. package/dist/tempo/TransactionRequest.js.map +1 -1
  22. package/dist/tempo/index.d.ts +4 -4
  23. package/dist/tempo/index.d.ts.map +1 -1
  24. package/dist/tempo/index.js +4 -4
  25. package/dist/tempo/index.js.map +1 -1
  26. package/dist/zod/tempo/AuthorizationTempo.d.ts +10 -130
  27. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  28. package/dist/zod/tempo/KeyAuthorization.d.ts +5 -65
  29. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  30. package/dist/zod/tempo/MultisigOperation.d.ts +0 -10
  31. package/dist/zod/tempo/MultisigOperation.d.ts.map +1 -1
  32. package/dist/zod/tempo/MultisigOperation.js +0 -3
  33. package/dist/zod/tempo/MultisigOperation.js.map +1 -1
  34. package/dist/zod/tempo/{MultisigWitness.d.ts → MultisigSimulation.d.ts} +17 -49
  35. package/dist/zod/tempo/MultisigSimulation.d.ts.map +1 -0
  36. package/dist/zod/tempo/{MultisigWitness.js → MultisigSimulation.js} +26 -24
  37. package/dist/zod/tempo/MultisigSimulation.js.map +1 -0
  38. package/dist/zod/tempo/RpcSchemaTempo.d.ts +83 -204
  39. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  40. package/dist/zod/tempo/RpcSchemaTempo.js +19 -10
  41. package/dist/zod/tempo/RpcSchemaTempo.js.map +1 -1
  42. package/dist/zod/tempo/SignatureEnvelope.d.ts +3 -39
  43. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  44. package/dist/zod/tempo/SignatureEnvelope.js +4 -14
  45. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  46. package/dist/zod/tempo/Transaction.d.ts +18 -234
  47. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  48. package/dist/zod/tempo/TransactionRequest.d.ts +36 -192
  49. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  50. package/dist/zod/tempo/TransactionRequest.js +10 -10
  51. package/dist/zod/tempo/TransactionRequest.js.map +1 -1
  52. package/dist/zod/tempo/z.d.ts +1 -1
  53. package/dist/zod/tempo/z.d.ts.map +1 -1
  54. package/dist/zod/tempo/z.js +1 -1
  55. package/dist/zod/tempo/z.js.map +1 -1
  56. package/package.json +9 -9
  57. package/src/tempo/AuthorizationTempo.test.ts +4 -0
  58. package/src/tempo/KeyAuthorization.test-d.ts +1 -12
  59. package/src/tempo/KeyAuthorization.test.ts +44 -3
  60. package/src/tempo/KeyAuthorization.ts +3 -3
  61. package/src/tempo/MultisigOperation.test-d.ts +8 -6
  62. package/src/tempo/MultisigOperation.test.ts +236 -169
  63. package/src/tempo/MultisigOperation.ts +122 -83
  64. package/src/tempo/MultisigSimulation.test-d.ts +33 -0
  65. package/src/tempo/MultisigSimulation.test.ts +283 -0
  66. package/src/tempo/MultisigSimulation.ts +266 -0
  67. package/src/tempo/RpcSchemaTempo.test-d.ts +45 -10
  68. package/src/tempo/RpcSchemaTempo.ts +15 -6
  69. package/src/tempo/SignatureEnvelope.test-d.ts +3 -59
  70. package/src/tempo/SignatureEnvelope.test.ts +61 -51
  71. package/src/tempo/SignatureEnvelope.ts +27 -44
  72. package/src/tempo/Transaction.test.ts +61 -0
  73. package/src/tempo/TransactionRequest.test-d.ts +7 -7
  74. package/src/tempo/TransactionRequest.test.ts +129 -208
  75. package/src/tempo/TransactionRequest.ts +16 -12
  76. package/src/tempo/index.ts +4 -4
  77. package/src/tempo/multisig.e2e.test.ts +51 -1
  78. package/src/version.ts +1 -1
  79. package/src/zod/tempo/MultisigOperation.ts +0 -3
  80. package/src/zod/tempo/{MultisigWitness.ts → MultisigSimulation.ts} +39 -42
  81. package/src/zod/tempo/RpcSchemaTempo.ts +20 -10
  82. package/src/zod/tempo/SignatureEnvelope.ts +11 -35
  83. package/src/zod/tempo/TransactionRequest.ts +14 -14
  84. package/src/zod/tempo/_test/KeyAuthorization.test.ts +2 -15
  85. package/src/zod/tempo/_test/MultisigOperation.test.ts +0 -2
  86. package/src/zod/tempo/_test/MultisigSimulation.test-d.ts +13 -0
  87. package/src/zod/tempo/_test/MultisigSimulation.test.ts +78 -0
  88. package/src/zod/tempo/_test/RpcSchemaTempo.test-d.ts +19 -2
  89. package/src/zod/tempo/_test/RpcSchemaTempo.test.ts +46 -4
  90. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +58 -112
  91. package/src/zod/tempo/_test/TransactionRequest.test.ts +19 -160
  92. package/src/zod/tempo/z.ts +1 -1
  93. package/dist/tempo/MultisigWitness.d.ts +0 -106
  94. package/dist/tempo/MultisigWitness.d.ts.map +0 -1
  95. package/dist/tempo/MultisigWitness.js +0 -118
  96. package/dist/tempo/MultisigWitness.js.map +0 -1
  97. package/dist/zod/tempo/MultisigWitness.d.ts.map +0 -1
  98. package/dist/zod/tempo/MultisigWitness.js.map +0 -1
  99. package/src/tempo/MultisigWitness.test-d.ts +0 -42
  100. package/src/tempo/MultisigWitness.test.ts +0 -288
  101. package/src/tempo/MultisigWitness.ts +0 -210
  102. package/src/zod/tempo/_test/MultisigWitness.test-d.ts +0 -13
  103. package/src/zod/tempo/_test/MultisigWitness.test.ts +0 -149
@@ -15,14 +15,10 @@ export type Base<quantity = bigint> = {
15
15
  approvals: readonly Hex.Hex[]
16
16
  /** Root configuration used to verify approvals. */
17
17
  config: MultisigConfig.Config<quantity>
18
- /** Root configuration version. */
19
- configVersion: quantity
20
18
  /** Unix creation time in milliseconds. */
21
19
  createdAt: number
22
20
  /** Deterministic multisig operation hash. */
23
21
  hash: Hex.Hex
24
- /** Whether the operation initializes the root multisig account. */
25
- init: boolean
26
22
  /** Number of approvals selected for quorum evaluation. */
27
23
  signatureCount: number
28
24
  /** Required root owner weight. */
@@ -73,9 +69,6 @@ export type KeyAuthorizationRpc = KeyAuthorizationOperation<Hex.Hex>
73
69
  /** JSON-RPC multisig operation. */
74
70
  export type Rpc = Operation<Hex.Hex>
75
71
 
76
- /** Maximum supported multisig configuration version. */
77
- const maxConfigVersion = 2n ** 64n - 1n
78
-
79
72
  /**
80
73
  * Derives the deterministic hash for a multisig operation.
81
74
  *
@@ -86,7 +79,7 @@ const maxConfigVersion = 2n ** 64n - 1n
86
79
  *
87
80
  * const hash = MultisigOperation.getHash({
88
81
  * account,
89
- * configVersion: 1n,
82
+ * config,
90
83
  * transaction,
91
84
  * type: 'transaction'
92
85
  * })
@@ -96,7 +89,7 @@ const maxConfigVersion = 2n ** 64n - 1n
96
89
  * @returns The operation hash signed by each owner.
97
90
  */
98
91
  export function getHash(options: getHash.Options): Hex.Hex {
99
- const { account, configVersion } = options
92
+ const { account, config } = options
100
93
  const payload =
101
94
  options.type === 'transaction'
102
95
  ? TxEnvelopeTempo.getSignPayload(
@@ -107,7 +100,7 @@ export function getHash(options: getHash.Options): Hex.Hex {
107
100
  )
108
101
  return MultisigConfig.getSignPayload({
109
102
  account,
110
- config: { version: configVersion },
103
+ config,
111
104
  payload,
112
105
  })
113
106
  }
@@ -117,8 +110,8 @@ export declare namespace getHash {
117
110
  export type Options = {
118
111
  /** Root multisig account. */
119
112
  account: Address.Address
120
- /** Root configuration version. */
121
- configVersion: bigint
113
+ /** Complete root multisig configuration witness. */
114
+ config: MultisigConfig.Config
122
115
  } & (
123
116
  | {
124
117
  /** Canonical serialized key authorization. */
@@ -160,8 +153,7 @@ export declare namespace getHash {
160
153
  * account,
161
154
  * approvals,
162
155
  * config,
163
- * hash,
164
- * resolveConfig
156
+ * hash
165
157
  * })
166
158
  * ```
167
159
  *
@@ -171,18 +163,25 @@ export declare namespace getHash {
171
163
  export async function selectApprovals(
172
164
  options: selectApprovals.Options,
173
165
  ): Promise<selectApprovals.ReturnValue> {
174
- const { account, approvals, hash, resolveConfig } = options
166
+ const { account, approvals, hash } = options
175
167
  if (!Address.validate(account) || Hex.toBigInt(account) === 0n)
176
168
  throw new InvalidApprovalError({ reason: 'account is invalid' })
177
169
  if (!Hash.validate(hash))
178
170
  throw new InvalidApprovalError({ reason: 'hash is invalid' })
171
+ const config = MultisigConfig.from(options.config)
172
+ if (
173
+ config.version === 0n &&
174
+ !Address.isEqual(MultisigConfig.getAddress(config), account)
175
+ )
176
+ throw new InvalidApprovalError({
177
+ reason: 'initial config does not derive the root multisig account',
178
+ })
179
179
  return selectApprovals_internal(
180
180
  {
181
181
  account,
182
182
  approvals,
183
- config: MultisigConfig.from(options.config),
183
+ config,
184
184
  hash,
185
- resolveConfig,
186
185
  },
187
186
  [account.toLowerCase()],
188
187
  )
@@ -199,27 +198,6 @@ export declare namespace selectApprovals {
199
198
  config: MultisigConfig.Config
200
199
  /** Deterministic operation hash approved by root owners. */
201
200
  hash: Hex.Hex
202
- /** Resolves the current configuration of a nested multisig owner. */
203
- resolveConfig?: ResolveConfig | undefined
204
- }
205
-
206
- /** Resolves an initialized nested multisig configuration. */
207
- export type ResolveConfig = (
208
- options: ResolveConfigOptions,
209
- ) => ResolvedConfig | Promise<ResolvedConfig>
210
-
211
- /** Nested multisig configuration lookup parameters. */
212
- export type ResolveConfigOptions = {
213
- /** Nested multisig account. */
214
- account: Address.Address
215
- }
216
-
217
- /** Resolved nested multisig configuration. */
218
- export type ResolvedConfig = {
219
- /** Current nested multisig configuration. */
220
- config: MultisigConfig.Config
221
- /** Current nested multisig configuration version. */
222
- version: bigint
223
201
  }
224
202
 
225
203
  /** Result of validating and selecting approvals. */
@@ -248,6 +226,90 @@ export declare namespace selectApprovals {
248
226
  | Errors.GlobalErrorType
249
227
  }
250
228
 
229
+ /**
230
+ * Serializes a key authorization with selected multisig owner approvals.
231
+ *
232
+ * @example
233
+ * ```ts twoslash
234
+ * // @noErrors
235
+ * import { MultisigOperation } from 'ox/tempo'
236
+ *
237
+ * const authorization =
238
+ * MultisigOperation.serializeKeyAuthorization(
239
+ * keyAuthorization,
240
+ * {
241
+ * account,
242
+ * approvals: selection.selectedApprovals,
243
+ * config
244
+ * }
245
+ * )
246
+ * ```
247
+ *
248
+ * @param keyAuthorization - Canonical serialized unsigned key authorization.
249
+ * @param options - Multisig account, config, and selected approvals.
250
+ * @returns The signed serialized key authorization.
251
+ */
252
+ export function serializeKeyAuthorization(
253
+ keyAuthorization: Hex.Hex,
254
+ options: serializeKeyAuthorization.Options,
255
+ ): Hex.Hex {
256
+ const authorization = KeyAuthorization_.deserialize(keyAuthorization)
257
+ if (authorization.signature)
258
+ throw new InvalidOperationError({
259
+ reason: 'keyAuthorization must not contain a signature',
260
+ })
261
+ if (
262
+ KeyAuthorization_.serialize(authorization).toLowerCase() !==
263
+ keyAuthorization.toLowerCase()
264
+ )
265
+ throw new InvalidOperationError({
266
+ reason: 'keyAuthorization is not canonically serialized',
267
+ })
268
+ const config = MultisigConfig.from(options.config)
269
+ const signatures = SignatureEnvelope.sortMultisigApprovals({
270
+ account: options.account,
271
+ config,
272
+ payload: KeyAuthorization_.getSignPayload(authorization),
273
+ signatures: options.approvals.map((approval) =>
274
+ SignatureEnvelope.deserialize(approval),
275
+ ),
276
+ })
277
+ return KeyAuthorization_.serialize(
278
+ KeyAuthorization_.from(authorization, {
279
+ signature: SignatureEnvelope.from({
280
+ account: options.account,
281
+ config,
282
+ signatures,
283
+ }),
284
+ }),
285
+ )
286
+ }
287
+
288
+ export declare namespace serializeKeyAuthorization {
289
+ /** Options for `serializeKeyAuthorization`. */
290
+ export type Options = {
291
+ /** Root multisig account. */
292
+ account: Address.Address
293
+ /** Selected serialized owner approvals. */
294
+ approvals: readonly SignatureEnvelope.Serialized[]
295
+ /** Complete applicable root multisig config. */
296
+ config: MultisigConfig.Config
297
+ }
298
+
299
+ /** Error type for `serializeKeyAuthorization`. */
300
+ export type ErrorType =
301
+ | InvalidOperationError
302
+ | KeyAuthorization_.deserialize.ErrorType
303
+ | KeyAuthorization_.from.ErrorType
304
+ | KeyAuthorization_.getSignPayload.ErrorType
305
+ | KeyAuthorization_.serialize.ErrorType
306
+ | MultisigConfig.assert.ErrorType
307
+ | SignatureEnvelope.assert.ErrorType
308
+ | SignatureEnvelope.InvalidSerializedError
309
+ | SignatureEnvelope.sortMultisigApprovals.ErrorType
310
+ | Errors.GlobalErrorType
311
+ }
312
+
251
313
  /**
252
314
  * Serializes a multisig transaction operation with selected owner approvals.
253
315
  *
@@ -340,10 +402,6 @@ export function from<const operation extends Operation>(
340
402
  ): from.ReturnValue<operation> {
341
403
  try {
342
404
  const config = MultisigConfig.from(operation.config)
343
- if (config.version !== operation.configVersion)
344
- throw new InvalidOperationError({
345
- reason: 'config.version must equal configVersion',
346
- })
347
405
  if (
348
406
  typeof config.threshold !== 'number' ||
349
407
  config.owners.some((owner) => typeof owner.weight !== 'number')
@@ -393,22 +451,19 @@ export function fromRpc<const operation extends Rpc>(
393
451
  operation: operation,
394
452
  ): fromRpc.ReturnValue<operation> {
395
453
  try {
396
- if (
397
- typeof operation.configVersion !== 'string' ||
398
- !Hex.validate(operation.configVersion)
399
- )
454
+ const version = operation.config?.version
455
+ if (typeof version !== 'string' || !Hex.validate(version))
400
456
  throw new InvalidOperationError({
401
- reason: 'configVersion must be a hexadecimal quantity',
457
+ reason: 'config.version must be a hexadecimal quantity',
402
458
  })
403
- const configVersion = Hex.toBigInt(operation.configVersion)
404
- if (Hex.fromNumber(configVersion) !== operation.configVersion)
459
+ const version_ = Hex.toBigInt(version)
460
+ if (Hex.fromNumber(version_) !== version)
405
461
  throw new InvalidOperationError({
406
- reason: 'configVersion must use canonical quantity encoding',
462
+ reason: 'config.version must use canonical quantity encoding',
407
463
  })
408
464
  return from({
409
465
  ...operation,
410
466
  config: MultisigConfig.fromRpc(operation.config),
411
- configVersion,
412
467
  } as Operation) as never
413
468
  } catch (cause) {
414
469
  if (cause instanceof InvalidOperationError) throw cause
@@ -448,7 +503,6 @@ export function toRpc<const operation extends Operation>(
448
503
  return {
449
504
  ...value,
450
505
  config: MultisigConfig.toRpc(value.config),
451
- configVersion: Hex.fromNumber(value.configVersion),
452
506
  } as never
453
507
  }
454
508
 
@@ -460,7 +514,7 @@ export declare namespace toRpc {
460
514
  : KeyAuthorizationRpc
461
515
 
462
516
  /** Error type for `toRpc`. */
463
- export type ErrorType = from.ErrorType | Hex.fromNumber.ErrorType
517
+ export type ErrorType = from.ErrorType | MultisigConfig.toRpc.ErrorType
464
518
  }
465
519
 
466
520
  /**
@@ -526,15 +580,17 @@ async function selectApprovals_internal(
526
580
  throw new InvalidApprovalError({
527
581
  reason: `nested multisig owner ${group.address} is invalid`,
528
582
  })
529
- if (!options.resolveConfig)
583
+ const config = MultisigConfig.from(nested[0]!.config)
584
+ if (nested.some((signature) => !sameConfig(signature.config, config)))
530
585
  throw new InvalidApprovalError({
531
- reason: `nested multisig owner ${group.address} requires a config resolver`,
586
+ reason: `nested multisig owner ${group.address} has conflicting config witnesses`,
532
587
  })
533
- const resolved = await options.resolveConfig({ account: group.address })
534
- const config = MultisigConfig.from(resolved.config)
535
- if (config.version !== resolved.version)
588
+ if (
589
+ config.version === 0n &&
590
+ !Address.isEqual(MultisigConfig.getAddress(config), group.address)
591
+ )
536
592
  throw new InvalidApprovalError({
537
- reason: `resolved config.version must equal version for nested multisig owner ${group.address}`,
593
+ reason: `initial config does not derive nested multisig owner ${group.address}`,
538
594
  })
539
595
  const selected = await selectApprovals_internal(
540
596
  {
@@ -550,7 +606,6 @@ async function selectApprovals_internal(
550
606
  config,
551
607
  payload: options.hash,
552
608
  }),
553
- resolveConfig: options.resolveConfig,
554
609
  },
555
610
  [...path, group.address.toLowerCase()],
556
611
  )
@@ -692,16 +747,6 @@ function assertBase(operation: Operation, config: MultisigConfig.Config): void {
692
747
  throw new InvalidOperationError({ reason: 'account cannot be zero' })
693
748
  if (!Hash.validate(operation.hash))
694
749
  throw new InvalidOperationError({ reason: 'hash is invalid' })
695
- if (typeof operation.init !== 'boolean')
696
- throw new InvalidOperationError({ reason: 'init must be a boolean' })
697
- if (
698
- typeof operation.configVersion !== 'bigint' ||
699
- operation.configVersion < 0n ||
700
- operation.configVersion > maxConfigVersion
701
- )
702
- throw new InvalidOperationError({
703
- reason: 'configVersion must be an unsigned 64-bit integer',
704
- })
705
750
  assertInteger(operation.createdAt, 'createdAt')
706
751
  assertInteger(operation.updatedAt, 'updatedAt')
707
752
  if (operation.updatedAt < operation.createdAt)
@@ -781,19 +826,13 @@ function assertBase(operation: Operation, config: MultisigConfig.Config): void {
781
826
  reason:
782
827
  'weight is not reachable by signatureCount retained owner approvals',
783
828
  })
784
- if (operation.init) {
785
- if (operation.configVersion !== 0n)
786
- throw new InvalidOperationError({
787
- reason: 'bootstrap operations must use config version zero',
788
- })
789
- if (
790
- MultisigConfig.getAddress(config).toLowerCase() !==
791
- operation.account.toLowerCase()
792
- )
793
- throw new InvalidOperationError({
794
- reason: 'bootstrap config does not derive the operation account',
795
- })
796
- }
829
+ if (
830
+ config.version === 0n &&
831
+ !Address.isEqual(MultisigConfig.getAddress(config), operation.account)
832
+ )
833
+ throw new InvalidOperationError({
834
+ reason: 'initial config does not derive the operation account',
835
+ })
797
836
  }
798
837
 
799
838
  /**
@@ -0,0 +1,33 @@
1
+ import { expectTypeOf, test } from 'vp/test'
2
+ import * as MultisigSimulation from './MultisigSimulation.js'
3
+
4
+ const rpc = {
5
+ account: '0x2222222222222222222222222222222222222222',
6
+ approvals: [
7
+ {
8
+ owner: '0x1111111111111111111111111111111111111111',
9
+ type: 'primitive',
10
+ },
11
+ ],
12
+ config:
13
+ '0xf83ba000000000000000000000000000000000000000000000000000000000000000008001d7d694111111111111111111111111111111111111111101',
14
+ } as const satisfies MultisigSimulation.Rpc
15
+
16
+ test('fromRpc returns a domain spec', () => {
17
+ const spec = MultisigSimulation.fromRpc(rpc)
18
+
19
+ expectTypeOf(spec).toEqualTypeOf<MultisigSimulation.Spec>()
20
+ expectTypeOf(spec.config.version).toEqualTypeOf<bigint>()
21
+ })
22
+
23
+ test('RPC specs use encoded configurations', () => {
24
+ expectTypeOf<
25
+ MultisigSimulation.Rpc['config']
26
+ >().toEqualTypeOf<`0x${string}`>()
27
+ })
28
+
29
+ test('toRpc returns an RPC spec', () => {
30
+ expectTypeOf(
31
+ MultisigSimulation.toRpc(MultisigSimulation.fromRpc(rpc)),
32
+ ).toEqualTypeOf<MultisigSimulation.Rpc>()
33
+ })
@@ -0,0 +1,283 @@
1
+ import { MultisigConfig, MultisigSimulation } from 'ox/tempo'
2
+ import { describe, expect, test } from 'vp/test'
3
+
4
+ const config =
5
+ '0xf852a011111111111111111111111111111111111111111111111111111111111111118002eed694111111111111111111111111111111111111111101d694bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb01' as const
6
+ const nestedConfig =
7
+ '0xf83ba022222222222222222222222222222222222222222222222222222222222222220101d7d694222222222222222222222222222222222222222201' as const
8
+ const rpc = {
9
+ account: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
10
+ approvals: [
11
+ {
12
+ keyData: '0x0102030405',
13
+ keyType: 'webAuthn',
14
+ owner: '0x1111111111111111111111111111111111111111',
15
+ type: 'primitive',
16
+ },
17
+ {
18
+ spec: {
19
+ account: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
20
+ approvals: [
21
+ {
22
+ keyData: '0x010203040506',
23
+ keyType: 'secp256k1',
24
+ owner: '0x2222222222222222222222222222222222222222',
25
+ },
26
+ ],
27
+ config: nestedConfig,
28
+ },
29
+ type: 'multisig',
30
+ },
31
+ ],
32
+ config,
33
+ } as const satisfies MultisigSimulation.Rpc
34
+
35
+ describe('fromRpc', () => {
36
+ test('behavior: decodes root and nested configurations', () => {
37
+ expect(MultisigSimulation.fromRpc(rpc)).toMatchInlineSnapshot(`
38
+ {
39
+ "account": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
40
+ "approvals": [
41
+ {
42
+ "keyData": "0x0102030405",
43
+ "keyType": "webAuthn",
44
+ "owner": "0x1111111111111111111111111111111111111111",
45
+ "type": "primitive",
46
+ },
47
+ {
48
+ "spec": {
49
+ "account": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
50
+ "approvals": [
51
+ {
52
+ "keyData": "0x010203040506",
53
+ "keyType": "secp256k1",
54
+ "owner": "0x2222222222222222222222222222222222222222",
55
+ },
56
+ ],
57
+ "config": {
58
+ "owners": [
59
+ {
60
+ "owner": "0x2222222222222222222222222222222222222222",
61
+ "weight": 1,
62
+ },
63
+ ],
64
+ "salt": "0x2222222222222222222222222222222222222222222222222222222222222222",
65
+ "threshold": 1,
66
+ "version": 1n,
67
+ },
68
+ },
69
+ "type": "multisig",
70
+ },
71
+ ],
72
+ "config": {
73
+ "owners": [
74
+ {
75
+ "owner": "0x1111111111111111111111111111111111111111",
76
+ "weight": 1,
77
+ },
78
+ {
79
+ "owner": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
80
+ "weight": 1,
81
+ },
82
+ ],
83
+ "salt": "0x1111111111111111111111111111111111111111111111111111111111111111",
84
+ "threshold": 2,
85
+ "version": 0n,
86
+ },
87
+ }
88
+ `)
89
+ })
90
+
91
+ test('behavior: preserves the maximum uint64 configuration version', () => {
92
+ const config =
93
+ '0xf843a0000000000000000000000000000000000000000000000000000000000000000088ffffffffffffffff01d7d694111111111111111111111111111111111111111101' as const
94
+
95
+ expect(
96
+ MultisigSimulation.fromRpc({ ...rpc, approvals: [], config }).config
97
+ .version,
98
+ ).toMatchInlineSnapshot(`18446744073709551615n`)
99
+ })
100
+
101
+ test('error: rejects excess root approvals', () => {
102
+ expect(() =>
103
+ MultisigSimulation.fromRpc({
104
+ ...rpc,
105
+ approvals: Array.from({ length: 9 }, () => rpc.approvals[0]),
106
+ }),
107
+ ).toThrowErrorMatchingInlineSnapshot(
108
+ `[MultisigSimulation.InvalidSimulationError: Invalid multisig simulation: approval count exceeds 8.]`,
109
+ )
110
+ })
111
+
112
+ test('error: rejects excess nested approvals', () => {
113
+ expect(() =>
114
+ MultisigSimulation.fromRpc({
115
+ ...rpc,
116
+ approvals: [
117
+ {
118
+ ...rpc.approvals[1],
119
+ spec: {
120
+ ...rpc.approvals[1].spec,
121
+ approvals: Array.from(
122
+ { length: 9 },
123
+ () => rpc.approvals[1].spec.approvals[0],
124
+ ),
125
+ },
126
+ },
127
+ ],
128
+ }),
129
+ ).toThrowErrorMatchingInlineSnapshot(
130
+ `[MultisigSimulation.InvalidSimulationError: Invalid multisig simulation: approval count exceeds 8.]`,
131
+ )
132
+ })
133
+
134
+ test('error: rejects trailing configuration bytes', () => {
135
+ expect(() =>
136
+ MultisigSimulation.fromRpc({ ...rpc, config: `${config}00` }),
137
+ ).toThrowErrorMatchingInlineSnapshot(
138
+ `[Rlp.TrailingBytesError: RLP payload encodes a single item, but \`1\` trailing byte remains.]`,
139
+ )
140
+ })
141
+
142
+ test('error: rejects malformed configuration RLP', () => {
143
+ expect(() =>
144
+ MultisigSimulation.fromRpc({ ...rpc, config: '0xf8' }),
145
+ ).toThrowErrorMatchingInlineSnapshot(
146
+ `[Cursor.PositionOutOfBoundsError: Position \`1\` is out of bounds (\`0 < position < 1\`).]`,
147
+ )
148
+ })
149
+
150
+ test('error: rejects a non-list configuration', () => {
151
+ expect(() =>
152
+ MultisigSimulation.fromRpc({ ...rpc, config: '0x01' }),
153
+ ).toThrowErrorMatchingInlineSnapshot(
154
+ `[MultisigSimulation.InvalidSimulationError: Invalid multisig simulation: invalid config encoding.]`,
155
+ )
156
+ })
157
+
158
+ test('error: rejects noncanonical configuration integers', () => {
159
+ const noncanonical = config
160
+ .replace('0xf852', '0xf853')
161
+ .replace('118002', '11810002') as `0x${string}`
162
+
163
+ expect(() =>
164
+ MultisigSimulation.fromRpc({ ...rpc, config: noncanonical }),
165
+ ).toThrowErrorMatchingInlineSnapshot(
166
+ `[MultisigSimulation.InvalidSimulationError: Invalid multisig simulation: invalid config encoding.]`,
167
+ )
168
+ })
169
+ })
170
+
171
+ describe('toRpc', () => {
172
+ test('behavior: encodes configurations and shims long key data', () => {
173
+ expect(MultisigSimulation.toRpc(MultisigSimulation.fromRpc(rpc)))
174
+ .toMatchInlineSnapshot(`
175
+ {
176
+ "account": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
177
+ "approvals": [
178
+ {
179
+ "keyData": "0x0005",
180
+ "keyType": "webAuthn",
181
+ "owner": "0x1111111111111111111111111111111111111111",
182
+ "type": "primitive",
183
+ },
184
+ {
185
+ "spec": {
186
+ "account": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
187
+ "approvals": [
188
+ {
189
+ "keyData": "0x0006",
190
+ "keyType": "secp256k1",
191
+ "owner": "0x2222222222222222222222222222222222222222",
192
+ },
193
+ ],
194
+ "config": "${nestedConfig}",
195
+ },
196
+ "type": "multisig",
197
+ },
198
+ ],
199
+ "config": "${config}",
200
+ }
201
+ `)
202
+ })
203
+
204
+ test('behavior: preserves short key data', () => {
205
+ const spec = MultisigSimulation.fromRpc({
206
+ ...rpc,
207
+ approvals: [{ ...rpc.approvals[0], keyData: '0x0102' }],
208
+ })
209
+
210
+ expect(MultisigSimulation.toRpc(spec).approvals).toMatchInlineSnapshot(`
211
+ [
212
+ {
213
+ "keyData": "0x0102",
214
+ "keyType": "webAuthn",
215
+ "owner": "0x1111111111111111111111111111111111111111",
216
+ "type": "primitive",
217
+ },
218
+ ]
219
+ `)
220
+ })
221
+
222
+ test('behavior: encodes the maximum uint64 configuration version', () => {
223
+ const spec = MultisigSimulation.fromRpc(rpc)
224
+
225
+ expect(
226
+ MultisigSimulation.toRpc({
227
+ ...spec,
228
+ approvals: [],
229
+ config: MultisigConfig.from({
230
+ owners: [
231
+ {
232
+ owner: '0x1111111111111111111111111111111111111111',
233
+ weight: 1,
234
+ },
235
+ ],
236
+ threshold: 1,
237
+ version: MultisigConfig.maxVersion,
238
+ }),
239
+ }).config,
240
+ ).toMatchInlineSnapshot(
241
+ `"0xf843a0000000000000000000000000000000000000000000000000000000000000000088ffffffffffffffff01d7d694111111111111111111111111111111111111111101"`,
242
+ )
243
+ })
244
+
245
+ test('error: rejects excess root approvals', () => {
246
+ const spec = MultisigSimulation.fromRpc(rpc)
247
+
248
+ expect(() =>
249
+ MultisigSimulation.toRpc({
250
+ ...spec,
251
+ approvals: Array.from({ length: 9 }, () => spec.approvals[0]!),
252
+ }),
253
+ ).toThrowErrorMatchingInlineSnapshot(
254
+ `[MultisigSimulation.InvalidSimulationError: Invalid multisig simulation: approval count exceeds 8.]`,
255
+ )
256
+ })
257
+
258
+ test('error: rejects excess nested approvals', () => {
259
+ const spec = MultisigSimulation.fromRpc(rpc)
260
+ const nested = spec.approvals[1]!
261
+ if (nested.type !== 'multisig') throw new Error('unreachable')
262
+
263
+ expect(() =>
264
+ MultisigSimulation.toRpc({
265
+ ...spec,
266
+ approvals: [
267
+ {
268
+ ...nested,
269
+ spec: {
270
+ ...nested.spec,
271
+ approvals: Array.from(
272
+ { length: 9 },
273
+ () => nested.spec.approvals[0]!,
274
+ ),
275
+ },
276
+ },
277
+ ],
278
+ }),
279
+ ).toThrowErrorMatchingInlineSnapshot(
280
+ `[MultisigSimulation.InvalidSimulationError: Invalid multisig simulation: approval count exceeds 8.]`,
281
+ )
282
+ })
283
+ })