ox 0.14.37 → 0.14.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/_cjs/tempo/MultisigConfig.js +71 -15
  3. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  4. package/_cjs/tempo/MultisigOperation.js +241 -19
  5. package/_cjs/tempo/MultisigOperation.js.map +1 -1
  6. package/_cjs/tempo/SignatureEnvelope.js +71 -108
  7. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  8. package/_cjs/version.js +1 -1
  9. package/_esm/tempo/MultisigConfig.js +140 -71
  10. package/_esm/tempo/MultisigConfig.js.map +1 -1
  11. package/_esm/tempo/MultisigOperation.js +331 -19
  12. package/_esm/tempo/MultisigOperation.js.map +1 -1
  13. package/_esm/tempo/SignatureEnvelope.js +84 -143
  14. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  15. package/_esm/tempo/index.js +2 -2
  16. package/_esm/version.js +1 -1
  17. package/_types/tempo/KeyAuthorization.d.ts +1 -1
  18. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  19. package/_types/tempo/MultisigConfig.d.ts +137 -73
  20. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  21. package/_types/tempo/MultisigOperation.d.ts +169 -1
  22. package/_types/tempo/MultisigOperation.d.ts.map +1 -1
  23. package/_types/tempo/SignatureEnvelope.d.ts +81 -75
  24. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  25. package/_types/tempo/index.d.ts +2 -2
  26. package/_types/version.d.ts +1 -1
  27. package/package.json +6 -1
  28. package/tempo/KeyAuthorization.test-d.ts +12 -0
  29. package/tempo/KeyAuthorization.test.ts +54 -0
  30. package/tempo/KeyAuthorization.ts +1 -1
  31. package/tempo/MultisigConfig.test-d/package.json +6 -0
  32. package/tempo/MultisigConfig.test-d.ts +52 -0
  33. package/tempo/MultisigConfig.test.ts +291 -263
  34. package/tempo/MultisigConfig.ts +250 -95
  35. package/tempo/MultisigOperation.test-d.ts +25 -0
  36. package/tempo/MultisigOperation.test.ts +754 -22
  37. package/tempo/MultisigOperation.ts +548 -17
  38. package/tempo/SignatureEnvelope.test-d.ts +57 -40
  39. package/tempo/SignatureEnvelope.test.ts +523 -480
  40. package/tempo/SignatureEnvelope.ts +250 -257
  41. package/tempo/index.ts +2 -2
  42. package/tempo/multisig.e2e.test.ts +113 -86
  43. package/version.ts +1 -1
@@ -3,16 +3,7 @@ import type * as Bytes from '../core/Bytes.js'
3
3
  import * as Errors from '../core/Errors.js'
4
4
  import * as Hash from '../core/Hash.js'
5
5
  import * as Hex from '../core/Hex.js'
6
- import type { Compute, OneOf } from '../core/internal/types.js'
7
-
8
- /** Maximum number of owners allowed in a native multisig config. */
9
- export const maxOwners = 48
10
-
11
- /** Maximum threshold accepted by a native multisig config. */
12
- export const maxThreshold = 0xff
13
-
14
- /** Maximum number of owner approvals in a native multisig signature. */
15
- export const maxSignatures = 8
6
+ import type { Compute } from '../core/internal/types.js'
16
7
 
17
8
  /**
18
9
  * Maximum number of native multisig signatures in one nested authorization
@@ -23,6 +14,18 @@ export const maxNestingDepth = 2
23
14
  /** Maximum encoded byte length for one primitive owner approval. */
24
15
  export const maxOwnerSignatureBytes = 2049
25
16
 
17
+ /** Maximum number of owners allowed in a native multisig config. */
18
+ export const maxOwners = 48
19
+
20
+ /** Maximum number of owner approvals in a native multisig signature. */
21
+ export const maxSignatures = 8
22
+
23
+ /** Maximum threshold accepted by a native multisig config. */
24
+ export const maxThreshold = 0xff
25
+
26
+ /** Maximum version accepted by a native multisig config. */
27
+ export const maxVersion = 2n ** 64n - 1n
28
+
26
29
  /** Tempo signature type byte for native multisig signatures. */
27
30
  export const signatureTypeByte = '0x05' as const
28
31
 
@@ -32,14 +35,33 @@ export const zeroSalt = `0x${'00'.repeat(32)}` as const
32
35
  /** Domain prefix for the native multisig account address derivation. */
33
36
  const accountDomain = 'tempo:multisig:account'
34
37
 
38
+ /** Domain prefix for native multisig configuration commitments. */
39
+ const configDomain = 'tempo:multisig:config'
40
+
35
41
  /** Domain prefix for native multisig owner approvals. */
36
42
  const signatureDomain = 'tempo:multisig:signature'
37
43
 
38
44
  /**
39
- * Native multisig configuration. Determines the stable multisig account
40
- * address.
45
+ * Complete native multisig configuration witness.
41
46
  */
42
- export type Config<numberType = number> = Compute<{
47
+ export type Config<bigintType = bigint, numberType = number> = Compute<{
48
+ /** Weighted owner list, strictly ascending by owner address. */
49
+ owners: readonly Owner<numberType>[]
50
+ /** Caller-chosen 32-byte salt. */
51
+ salt: Hex.Hex
52
+ /** Minimum total owner weight required for authorization. */
53
+ threshold: numberType
54
+ /** Configuration version. Zero identifies the initial configuration. */
55
+ version: bigintType
56
+ }>
57
+
58
+ /** Input accepted when constructing a native multisig configuration. */
59
+ export type Input<
60
+ versionType extends bigint | number = bigint | number,
61
+ numberType = number,
62
+ > = Compute<{
63
+ /** Weighted owner list (strictly ascending by `owner` address). */
64
+ owners: readonly Owner<numberType>[]
43
65
  /**
44
66
  * Caller-chosen 32-byte salt mixed into the derived account address.
45
67
  * Defaults to the zero salt (`MultisigConfig.zeroSalt`) when omitted.
@@ -47,8 +69,8 @@ export type Config<numberType = number> = Compute<{
47
69
  salt?: Hex.Hex | undefined
48
70
  /** Minimum total owner weight required to authorize a transaction. */
49
71
  threshold: numberType
50
- /** Weighted owner list (strictly ascending by `owner` address). */
51
- owners: readonly Owner<numberType>[]
72
+ /** Configuration version as a safe integer or bigint. Defaults to `0n`. */
73
+ version?: versionType | undefined
52
74
  }>
53
75
 
54
76
  /** Native multisig owner entry. */
@@ -59,9 +81,13 @@ export type Owner<numberType = number> = {
59
81
  weight: numberType
60
82
  }
61
83
 
84
+ /** JSON-RPC representation of a native multisig configuration. */
85
+ export type Rpc = Config<Hex.Hex, number>
86
+
62
87
  /** RLP tuple representation of a {@link ox#MultisigConfig.Config}. */
63
88
  export type Tuple = readonly [
64
89
  salt: Hex.Hex,
90
+ version: Hex.Hex,
65
91
  threshold: Hex.Hex,
66
92
  owners: readonly Hex.Hex[][],
67
93
  ]
@@ -69,7 +95,7 @@ export type Tuple = readonly [
69
95
  /**
70
96
  * Asserts that a native multisig {@link ox#MultisigConfig.Config} is valid.
71
97
  *
72
- * Mirrors the Tempo `InitMultisig::validate` rules: owners non-empty and
98
+ * Mirrors the Tempo configuration rules: owners non-empty and
73
99
  * `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
74
100
  * integer owner weights, integer `threshold` between `1` and `maxThreshold`,
75
101
  * total weight `<= 255` (u8 max), and a threshold reachable by at most
@@ -89,11 +115,15 @@ export type Tuple = readonly [
89
115
  *
90
116
  * @param config - The multisig config.
91
117
  */
92
- export function assert<numberType = number>(config: Config<numberType>): void {
93
- const { salt, threshold, owners } = config
118
+ export function assert<
119
+ versionType extends bigint | number = bigint | number,
120
+ numberType = number,
121
+ >(config: Input<versionType, numberType>): void {
122
+ const { owners, salt, threshold, version = 0n } = config
94
123
 
95
124
  if (typeof salt !== 'undefined' && Hex.size(salt) !== 32)
96
125
  throw new InvalidConfigError({ reason: 'salt must be 32 bytes' })
126
+ assertVersion(version)
97
127
  if (owners.length === 0)
98
128
  throw new InvalidConfigError({ reason: 'owners cannot be empty' })
99
129
  if (owners.length > maxOwners)
@@ -164,11 +194,11 @@ export declare namespace assert {
164
194
  * import { MultisigConfig } from 'ox/tempo'
165
195
  *
166
196
  * const config = MultisigConfig.from({
167
- * threshold: 2,
168
197
  * owners: [
169
198
  * { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
170
199
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
171
200
  * ],
201
+ * threshold: 2,
172
202
  * })
173
203
  * // owners are now sorted ascending by address
174
204
  * ```
@@ -177,20 +207,70 @@ export declare namespace assert {
177
207
  * @returns The normalized multisig config.
178
208
  */
179
209
  export function from<numberType = number>(
180
- config: Config<numberType>,
181
- ): Config<numberType> {
210
+ config: Input<0 | 0n, numberType> & { version?: 0 | 0n | undefined },
211
+ ): Config<0n, numberType>
212
+ export function from<bigintType extends bigint, numberType = number>(
213
+ config: Input<bigintType, numberType> & { version: bigintType },
214
+ ): Config<bigintType, numberType>
215
+ export function from<numberType = number>(
216
+ config: Input<number, numberType> & { version: number },
217
+ ): Config<bigint, numberType>
218
+ export function from<numberType = number>(
219
+ config: Input<bigint | number, numberType>,
220
+ ): Config<bigint, numberType>
221
+ // eslint-disable-next-line jsdoc/require-jsdoc
222
+ export function from<numberType = number>(
223
+ config: Input<bigint | number, numberType>,
224
+ ): Config<bigint, numberType> {
225
+ const version = config.version ?? 0n
226
+ assertVersion(version)
182
227
  const owners = [...config.owners].sort((a, b) =>
183
228
  Hex.toBigInt(a.owner) < Hex.toBigInt(b.owner) ? -1 : 1,
184
229
  )
185
230
  const normalized = {
231
+ owners,
186
232
  salt: config.salt ? Hex.padLeft(config.salt, 32) : zeroSalt,
187
233
  threshold: config.threshold,
188
- owners,
189
- } as Config<numberType>
234
+ version: BigInt(version),
235
+ } as Config<bigint, numberType>
190
236
  assert(normalized)
191
237
  return normalized
192
238
  }
193
239
 
240
+ /**
241
+ * Converts a JSON-RPC multisig configuration to its domain representation.
242
+ *
243
+ * @example
244
+ * ```ts twoslash
245
+ * import { MultisigConfig } from 'ox/tempo'
246
+ *
247
+ * const config = MultisigConfig.fromRpc({
248
+ * owners: [
249
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
250
+ * ],
251
+ * salt: `0x${'00'.repeat(32)}`,
252
+ * threshold: 1,
253
+ * version: '0x0',
254
+ * })
255
+ * ```
256
+ *
257
+ * @param config - The JSON-RPC multisig configuration.
258
+ * @returns The normalized multisig configuration.
259
+ */
260
+ export function fromRpc(config: Rpc): Config {
261
+ return from({
262
+ ...config,
263
+ version: Hex.toBigInt(config.version),
264
+ })
265
+ }
266
+
267
+ export declare namespace fromRpc {
268
+ type ErrorType =
269
+ | assert.ErrorType
270
+ | Hex.toBigInt.ErrorType
271
+ | Errors.GlobalErrorType
272
+ }
273
+
194
274
  /**
195
275
  * Converts an RLP {@link ox#MultisigConfig.Tuple} back to a
196
276
  * {@link ox#MultisigConfig.Config}.
@@ -201,6 +281,7 @@ export function from<numberType = number>(
201
281
  *
202
282
  * const config = MultisigConfig.fromTuple([
203
283
  * `0x${'00'.repeat(32)}`,
284
+ * '0x',
204
285
  * '0x01',
205
286
  * [['0x1111111111111111111111111111111111111111', '0x01']],
206
287
  * ])
@@ -210,10 +291,8 @@ export function from<numberType = number>(
210
291
  * @returns The multisig config.
211
292
  */
212
293
  export function fromTuple(tuple: Tuple): Config {
213
- const [salt, threshold, owners] = tuple
294
+ const [salt, version, threshold, owners] = tuple
214
295
  return {
215
- salt: salt && salt !== '0x' ? Hex.padLeft(salt, 32) : zeroSalt,
216
- threshold: threshold === '0x' ? 0 : Hex.toNumber(threshold),
217
296
  owners: owners.map((owner) => {
218
297
  const [ownerAddress, weight] = owner as readonly Hex.Hex[]
219
298
  return {
@@ -221,6 +300,9 @@ export function fromTuple(tuple: Tuple): Config {
221
300
  weight: !weight || weight === '0x' ? 0 : Hex.toNumber(weight),
222
301
  }
223
302
  }),
303
+ salt: salt && salt !== '0x' ? Hex.padLeft(salt, 32) : zeroSalt,
304
+ threshold: threshold === '0x' ? 0 : Hex.toNumber(threshold),
305
+ version: version === '0x' ? 0n : Hex.toBigInt(version),
224
306
  }
225
307
  }
226
308
 
@@ -230,28 +312,32 @@ export function fromTuple(tuple: Tuple): Config {
230
312
  * Preimage (fixed-width big-endian, **not** RLP):
231
313
  * `keccak256("tempo:multisig:account" || salt || u8(threshold) || u8(owners.length) || (owner || u8(weight)) for each owner)[12:32]`.
232
314
  *
233
- * The address is derived once from the initial (bootstrap) config and never
234
- * changes — config updates do not affect it.
315
+ * The address is derived once from the initial version-0 config. Config
316
+ * updates do not change it.
235
317
  *
236
318
  * @example
237
319
  * ```ts twoslash
238
320
  * import { MultisigConfig } from 'ox/tempo'
239
321
  *
240
322
  * const initialConfig = MultisigConfig.from({
241
- * threshold: 1,
242
323
  * owners: [
243
324
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
244
325
  * ],
326
+ * threshold: 1,
245
327
  * })
246
328
  *
247
329
  * const address = MultisigConfig.getAddress(initialConfig)
248
330
  * ```
249
331
  *
250
- * @param config - The initial (bootstrap) multisig config.
332
+ * @param config - The initial multisig config.
251
333
  * @returns The multisig account address.
252
334
  */
253
- export function getAddress(config: Config): Address.Address {
335
+ export function getAddress(config: Input): Address.Address {
254
336
  assert(config)
337
+ if (BigInt(config.version ?? 0) !== 0n)
338
+ throw new InvalidConfigError({
339
+ reason: 'account address requires version zero',
340
+ })
255
341
  const hash = Hash.keccak256(
256
342
  Hex.concat(
257
343
  Hex.fromString(accountDomain),
@@ -267,6 +353,10 @@ export function getAddress(config: Config): Address.Address {
267
353
  const account = Address.from(Hex.slice(hash, 12, 32))
268
354
  if (Hex.toBigInt(account) === 0n)
269
355
  throw new InvalidConfigError({ reason: 'derived account cannot be zero' })
356
+ if (config.owners.some((owner) => Address.isEqual(owner.owner, account)))
357
+ throw new InvalidConfigError({
358
+ reason: 'derived account cannot be an owner',
359
+ })
270
360
  return account
271
361
  }
272
362
 
@@ -282,6 +372,55 @@ export declare namespace getAddress {
282
372
  | Errors.GlobalErrorType
283
373
  }
284
374
 
375
+ /**
376
+ * Computes the commitment for a native multisig configuration.
377
+ *
378
+ * The commitment uses raw fixed-width fields, not RLP or ABI encoding:
379
+ * `keccak256("tempo:multisig:config" || salt || uint64be(version) || uint8(threshold) || uint8(owners.length) || owners)`.
380
+ *
381
+ * @example
382
+ * ```ts twoslash
383
+ * import { MultisigConfig } from 'ox/tempo'
384
+ *
385
+ * const commitment = MultisigConfig.getCommitment({
386
+ * owners: [
387
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
388
+ * ],
389
+ * threshold: 1,
390
+ * version: 1n,
391
+ * })
392
+ * ```
393
+ *
394
+ * @param config - The complete multisig configuration.
395
+ * @returns The configuration commitment.
396
+ */
397
+ export function getCommitment(config: Input): Hex.Hex {
398
+ assert(config)
399
+ return Hash.keccak256(
400
+ Hex.concat(
401
+ Hex.fromString(configDomain),
402
+ Hex.padLeft(config.salt ?? zeroSalt, 32),
403
+ Hex.fromNumber(config.version ?? 0n, { size: 8 }),
404
+ Hex.fromNumber(config.threshold, { size: 1 }),
405
+ Hex.fromNumber(config.owners.length, { size: 1 }),
406
+ ...config.owners.flatMap((owner) => [
407
+ owner.owner,
408
+ Hex.fromNumber(owner.weight, { size: 1 }),
409
+ ]),
410
+ ),
411
+ )
412
+ }
413
+
414
+ export declare namespace getCommitment {
415
+ type ErrorType =
416
+ | assert.ErrorType
417
+ | Hash.keccak256.ErrorType
418
+ | Hex.concat.ErrorType
419
+ | Hex.fromNumber.ErrorType
420
+ | Hex.fromString.ErrorType
421
+ | Errors.GlobalErrorType
422
+ }
423
+
285
424
  /**
286
425
  * Computes the digest a native multisig owner approves (signs).
287
426
  *
@@ -289,9 +428,9 @@ export declare namespace getAddress {
289
428
  * where `inner_digest` is the transaction sign payload
290
429
  * ({@link ox#TxEnvelopeTempo.(getSignPayload:function)}).
291
430
  *
292
- * The digest is keyed on the permanent `account` derived from the initial
293
- * (bootstrap) config and the current config `version`. Bootstrap approvals use
294
- * version `0n`, which is also the default; each config update increments it.
431
+ * The digest is keyed on the permanent `account` and the supplied config
432
+ * version. Initial approvals use version `0n`; each config update increments
433
+ * it.
295
434
  *
296
435
  * For a nested multisig owner approval, the parent digest becomes the nested
297
436
  * approval's `payload`, with the nested multisig `account`.
@@ -300,11 +439,11 @@ export declare namespace getAddress {
300
439
  * ```ts twoslash
301
440
  * import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
302
441
  *
303
- * const initialConfig = MultisigConfig.from({
304
- * threshold: 1,
442
+ * const config = MultisigConfig.from({
305
443
  * owners: [
306
444
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
307
445
  * ],
446
+ * threshold: 1,
308
447
  * })
309
448
  *
310
449
  * const envelope = TxEnvelopeTempo.from({
@@ -313,34 +452,9 @@ export declare namespace getAddress {
313
452
  * })
314
453
  *
315
454
  * const digest = MultisigConfig.getSignPayload({
455
+ * account: MultisigConfig.getAddress(config),
456
+ * config,
316
457
  * payload: TxEnvelopeTempo.getSignPayload(envelope),
317
- * initialConfig,
318
- * })
319
- * ```
320
- *
321
- * @example
322
- * ### From `account`
323
- *
324
- * If you already have the permanent `account` (for example, recovered from a
325
- * stored envelope), pass it directly:
326
- *
327
- * ```ts twoslash
328
- * import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
329
- *
330
- * const initialConfig = MultisigConfig.from({
331
- * threshold: 1,
332
- * owners: [
333
- * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
334
- * ],
335
- * })
336
- * const account = MultisigConfig.getAddress(initialConfig)
337
- *
338
- * const envelope = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
339
- *
340
- * const digest = MultisigConfig.getSignPayload({
341
- * payload: TxEnvelopeTempo.getSignPayload(envelope),
342
- * account,
343
- * version: 1n
344
458
  * })
345
459
  * ```
346
460
  *
@@ -348,45 +462,30 @@ export declare namespace getAddress {
348
462
  * @returns The owner approval digest.
349
463
  */
350
464
  export function getSignPayload(value: getSignPayload.Value): Hex.Hex {
351
- const { payload, version = 0n } = value
352
- const account =
353
- 'account' in value && value.account
354
- ? value.account
355
- : getAddress((value as { initialConfig: Config }).initialConfig)
465
+ const { account, config, payload } = value
466
+ assertVersion(config.version)
356
467
  return Hash.keccak256(
357
468
  Hex.concat(
358
469
  Hex.fromString(signatureDomain),
359
470
  Hex.from(payload),
360
471
  account,
361
- Hex.fromNumber(version, { size: 8 }),
472
+ Hex.fromNumber(config.version ?? 0n, { size: 8 }),
362
473
  ),
363
474
  )
364
475
  }
365
476
 
366
477
  export declare namespace getSignPayload {
367
478
  type Value = {
479
+ /** The native multisig account address. */
480
+ account: Address.Address
481
+ /** Configuration whose version applies to the approval. */
482
+ config: Pick<Config<bigint | number>, 'version'>
368
483
  /** The inner transaction sign payload (`tx.signature_hash()`). */
369
484
  payload: Hex.Hex | Bytes.Bytes
370
- /** Current multisig config version. Defaults to `0n`. */
371
- version?: bigint | undefined
372
- } & OneOf<
373
- | {
374
- /** The native multisig account address. */
375
- account: Address.Address
376
- }
377
- | {
378
- /**
379
- * The initial multisig config (the bootstrap config that derived the
380
- * permanent `account`). Used to derive the account automatically.
381
- * Config updates never change `account`, so the initial config can
382
- * continue deriving the account for post-update transactions.
383
- */
384
- initialConfig: Config
385
- }
386
- >
485
+ }
387
486
 
388
487
  type ErrorType =
389
- | getAddress.ErrorType
488
+ | assert.ErrorType
390
489
  | Hash.keccak256.ErrorType
391
490
  | Hex.concat.ErrorType
392
491
  | Hex.from.ErrorType
@@ -395,10 +494,47 @@ export declare namespace getSignPayload {
395
494
  }
396
495
 
397
496
  /**
398
- * Converts a {@link ox#MultisigConfig.Config} to its RLP tuple form (carried
399
- * by the multisig signature `init`).
497
+ * Converts a multisig configuration to its JSON-RPC representation.
498
+ *
499
+ * @example
500
+ * ```ts twoslash
501
+ * import { MultisigConfig } from 'ox/tempo'
400
502
  *
401
- * Tuple shape: `[salt, threshold, [[owner, weight], ...]]`. The
503
+ * const config = MultisigConfig.toRpc({
504
+ * owners: [
505
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
506
+ * ],
507
+ * threshold: 1,
508
+ * })
509
+ * ```
510
+ *
511
+ * @param config - The multisig configuration.
512
+ * @returns The JSON-RPC multisig configuration.
513
+ */
514
+ export function toRpc(config: Input): Rpc {
515
+ const value = from(config)
516
+ return {
517
+ owners: value.owners.map((owner) => ({
518
+ owner: owner.owner,
519
+ weight: Number(owner.weight),
520
+ })),
521
+ salt: value.salt,
522
+ threshold: Number(value.threshold),
523
+ version: Hex.fromNumber(value.version),
524
+ }
525
+ }
526
+
527
+ export declare namespace toRpc {
528
+ type ErrorType =
529
+ | assert.ErrorType
530
+ | Hex.fromNumber.ErrorType
531
+ | Errors.GlobalErrorType
532
+ }
533
+
534
+ /**
535
+ * Converts a {@link ox#MultisigConfig.Config} to its RLP tuple form.
536
+ *
537
+ * Tuple shape: `[salt, version, threshold, [[owner, weight], ...]]`. The
402
538
  * 32-byte `salt` encodes as a full fixed-width string; other integers use
403
539
  * canonical RLP encoding (zero values encode as `0x`).
404
540
  *
@@ -407,17 +543,17 @@ export declare namespace getSignPayload {
407
543
  * import { MultisigConfig } from 'ox/tempo'
408
544
  *
409
545
  * const tuple = MultisigConfig.toTuple({
410
- * threshold: 1,
411
546
  * owners: [
412
547
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
413
548
  * ],
549
+ * threshold: 1,
414
550
  * })
415
551
  * ```
416
552
  *
417
553
  * @param config - The multisig config.
418
554
  * @returns The RLP tuple.
419
555
  */
420
- export function toTuple(config: Config): Tuple {
556
+ export function toTuple(config: Input): Tuple {
421
557
  assert(config)
422
558
  const owners = config.owners.map(
423
559
  (owner) => [owner.owner, Hex.fromNumber(owner.weight)] as Hex.Hex[],
@@ -425,7 +561,13 @@ export function toTuple(config: Config): Tuple {
425
561
  // `salt` is a fixed 32-byte value: it RLP-encodes as a full 32-byte string
426
562
  // (including the zero salt), never trimmed like an integer.
427
563
  const salt = config.salt ? Hex.padLeft(config.salt, 32) : zeroSalt
428
- return [salt, Hex.fromNumber(config.threshold), owners] as const
564
+ const version = BigInt(config.version ?? 0)
565
+ return [
566
+ salt,
567
+ version === 0n ? '0x' : Hex.fromNumber(version),
568
+ Hex.fromNumber(config.threshold),
569
+ owners,
570
+ ] as const
429
571
  }
430
572
 
431
573
  /**
@@ -437,10 +579,10 @@ export function toTuple(config: Config): Tuple {
437
579
  * import { MultisigConfig } from 'ox/tempo'
438
580
  *
439
581
  * const valid = MultisigConfig.validate({
440
- * threshold: 1,
441
582
  * owners: [
442
583
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
443
584
  * ],
585
+ * threshold: 1,
444
586
  * })
445
587
  * // @log: true
446
588
  * ```
@@ -448,7 +590,7 @@ export function toTuple(config: Config): Tuple {
448
590
  * @param config - The multisig config.
449
591
  * @returns Whether the config is valid.
450
592
  */
451
- export function validate(config: Config): boolean {
593
+ export function validate(config: Input): boolean {
452
594
  try {
453
595
  assert(config)
454
596
  return true
@@ -464,3 +606,16 @@ export class InvalidConfigError extends Errors.BaseError {
464
606
  super(`Invalid native multisig config: ${reason}.`)
465
607
  }
466
608
  }
609
+
610
+ /** @internal */
611
+ function assertVersion(version: unknown): asserts version is bigint | number {
612
+ if (
613
+ (typeof version !== 'bigint' && typeof version !== 'number') ||
614
+ (typeof version === 'number' && !Number.isSafeInteger(version)) ||
615
+ BigInt(version) < 0n ||
616
+ BigInt(version) > maxVersion
617
+ )
618
+ throw new InvalidConfigError({
619
+ reason: 'version must be an unsigned 64-bit integer',
620
+ })
621
+ }
@@ -1,6 +1,7 @@
1
1
  import { expectTypeOf, test } from 'vitest'
2
2
  import type * as Hex from '../core/Hex.js'
3
3
  import * as MultisigOperation from './MultisigOperation.js'
4
+ import type * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
4
5
 
5
6
  declare const transaction: MultisigOperation.TransactionOperation
6
7
  declare const transactionRpc: MultisigOperation.TransactionRpc
@@ -8,6 +9,9 @@ declare const keyAuthorization: MultisigOperation.KeyAuthorizationOperation
8
9
  declare const keyAuthorizationRpc: MultisigOperation.KeyAuthorizationRpc
9
10
  declare const operation: MultisigOperation.Operation
10
11
  declare const operationRpc: MultisigOperation.Rpc
12
+ declare const getHashOptions: MultisigOperation.getHash.Options
13
+ declare const selectApprovalsOptions: MultisigOperation.selectApprovals.Options
14
+ declare const serializeTransactionOptions: MultisigOperation.serializeTransaction.Options
11
15
 
12
16
  test('preserves operation kinds during validation', () => {
13
17
  expectTypeOf(
@@ -43,10 +47,31 @@ test('preserves operation kinds during RPC conversion', () => {
43
47
  })
44
48
 
45
49
  test('uses JSON-RPC quantities only in RPC operations', () => {
50
+ expectTypeOf<
51
+ MultisigOperation.Operation['config']['version']
52
+ >().toEqualTypeOf<bigint>()
46
53
  expectTypeOf<
47
54
  MultisigOperation.Operation['configVersion']
48
55
  >().toEqualTypeOf<bigint>()
56
+ expectTypeOf<
57
+ MultisigOperation.Rpc['config']['version']
58
+ >().toEqualTypeOf<Hex.Hex>()
49
59
  expectTypeOf<
50
60
  MultisigOperation.Rpc['configVersion']
51
61
  >().toEqualTypeOf<Hex.Hex>()
52
62
  })
63
+
64
+ test('operation helpers return narrow types', async () => {
65
+ expectTypeOf(
66
+ MultisigOperation.getHash(getHashOptions),
67
+ ).toEqualTypeOf<Hex.Hex>()
68
+ expectTypeOf(
69
+ await MultisigOperation.selectApprovals(selectApprovalsOptions),
70
+ ).toEqualTypeOf<MultisigOperation.selectApprovals.ReturnValue>()
71
+ expectTypeOf(
72
+ MultisigOperation.serializeTransaction(
73
+ transaction,
74
+ serializeTransactionOptions,
75
+ ),
76
+ ).toEqualTypeOf<TxEnvelopeTempo.Serialized>()
77
+ })