ox 0.14.29 → 0.14.31

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 (54) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/_cjs/core/AbiParameters.js +2 -1
  3. package/_cjs/core/AbiParameters.js.map +1 -1
  4. package/_cjs/core/internal/abiParameters.js +10 -2
  5. package/_cjs/core/internal/abiParameters.js.map +1 -1
  6. package/_cjs/tempo/KeyAuthorization.js +39 -10
  7. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  8. package/_cjs/tempo/MultisigConfig.js +26 -26
  9. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  10. package/_cjs/tempo/SignatureEnvelope.js +189 -55
  11. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  12. package/_cjs/version.js +1 -1
  13. package/_esm/core/AbiParameters.js +4 -1
  14. package/_esm/core/AbiParameters.js.map +1 -1
  15. package/_esm/core/internal/abiParameters.js +17 -2
  16. package/_esm/core/internal/abiParameters.js.map +1 -1
  17. package/_esm/tempo/KeyAuthorization.js +41 -9
  18. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  19. package/_esm/tempo/MultisigConfig.js +53 -81
  20. package/_esm/tempo/MultisigConfig.js.map +1 -1
  21. package/_esm/tempo/SignatureEnvelope.js +202 -75
  22. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  23. package/_esm/tempo/index.js +2 -2
  24. package/_esm/version.js +1 -1
  25. package/_types/core/AbiParameters.d.ts.map +1 -1
  26. package/_types/core/internal/abiParameters.d.ts.map +1 -1
  27. package/_types/core/internal/rpcSchemas/eth.d.ts +16 -0
  28. package/_types/core/internal/rpcSchemas/eth.d.ts.map +1 -1
  29. package/_types/tempo/KeyAuthorization.d.ts +22 -16
  30. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  31. package/_types/tempo/MultisigConfig.d.ts +43 -83
  32. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  33. package/_types/tempo/SignatureEnvelope.d.ts +62 -40
  34. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  35. package/_types/tempo/index.d.ts +2 -2
  36. package/_types/version.d.ts +1 -1
  37. package/core/AbiParameters.ts +3 -1
  38. package/core/internal/abiParameters.ts +17 -2
  39. package/core/internal/rpcSchemas/eth.ts +16 -0
  40. package/package.json +11 -1
  41. package/tempo/KeyAuthorization.test-d/package.json +6 -0
  42. package/tempo/KeyAuthorization.test-d.ts +62 -0
  43. package/tempo/KeyAuthorization.test.ts +132 -0
  44. package/tempo/KeyAuthorization.ts +76 -29
  45. package/tempo/MultisigConfig.test.ts +86 -37
  46. package/tempo/MultisigConfig.ts +75 -132
  47. package/tempo/SignatureEnvelope.test-d/package.json +6 -0
  48. package/tempo/SignatureEnvelope.test-d.ts +97 -0
  49. package/tempo/SignatureEnvelope.test.ts +322 -30
  50. package/tempo/SignatureEnvelope.ts +314 -118
  51. package/tempo/e2e.test.ts +22 -207
  52. package/tempo/index.ts +2 -2
  53. package/tempo/multisig.e2e.test.ts +582 -0
  54. package/version.ts +1 -1
@@ -6,9 +6,21 @@ import * as Hex from '../core/Hex.js'
6
6
  import type { Compute, OneOf } from '../core/internal/types.js'
7
7
 
8
8
  /** Maximum number of owners allowed in a native multisig config. */
9
- export const maxOwners = 10
9
+ export const maxOwners = 255
10
10
 
11
- /** Maximum encoded byte length for one primitive owner approval. */
11
+ /** Maximum threshold accepted by a native multisig config. */
12
+ export const maxThreshold = 8
13
+
14
+ /** Maximum number of owner approvals in a native multisig signature. */
15
+ export const maxSignatures = maxThreshold
16
+
17
+ /**
18
+ * Maximum number of native multisig signatures in one nested authorization
19
+ * path, including the top-level transaction signature.
20
+ */
21
+ export const maxNestingDepth = 2
22
+
23
+ /** Maximum encoded byte length for one owner approval. */
12
24
  export const maxOwnerSignatureBytes = 2049
13
25
 
14
26
  /** Tempo signature type byte for native multisig signatures. */
@@ -20,20 +32,17 @@ export const zeroSalt = `0x${'00'.repeat(32)}` as const
20
32
  /** Domain prefix for the native multisig account address derivation. */
21
33
  const accountDomain = 'tempo:multisig:account'
22
34
 
23
- /** Domain prefix for the native multisig config ID derivation. */
24
- const configDomain = 'tempo:multisig:config'
25
-
26
35
  /** Domain prefix for native multisig owner approvals. */
27
36
  const signatureDomain = 'tempo:multisig:signature'
28
37
 
29
38
  /**
30
- * Native multisig configuration. Determines the permanent config ID and the
31
- * stable multisig account address.
39
+ * Native multisig configuration. Determines the stable multisig account
40
+ * address.
32
41
  */
33
42
  export type Config<numberType = number> = Compute<{
34
43
  /**
35
- * Caller-chosen 32-byte salt mixed into the permanent config ID. Defaults to
36
- * the zero salt (`MultisigConfig.zeroSalt`) when omitted.
44
+ * Caller-chosen 32-byte salt mixed into the derived account address.
45
+ * Defaults to the zero salt (`MultisigConfig.zeroSalt`) when omitted.
37
46
  */
38
47
  salt?: Hex.Hex | undefined
39
48
  /** Minimum total owner weight required to authorize a transaction. */
@@ -44,7 +53,7 @@ export type Config<numberType = number> = Compute<{
44
53
 
45
54
  /** Native multisig owner entry. */
46
55
  export type Owner<numberType = number> = {
47
- /** Owner address (recovered from the owner's primitive signature). */
56
+ /** Owner address (recovered from the owner's approval). */
48
57
  owner: Address.Address
49
58
  /** Nonzero owner weight. */
50
59
  weight: numberType
@@ -60,10 +69,10 @@ export type Tuple = readonly [
60
69
  /**
61
70
  * Asserts that a native multisig {@link ox#MultisigConfig.Config} is valid.
62
71
  *
63
- * Mirrors the Tempo `validate_multisig_config` rules: owners non-empty and
72
+ * Mirrors the Tempo `InitMultisig::validate` rules: owners non-empty and
64
73
  * `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
65
- * owner weights, `threshold >= 1`, total weight `<= u32::MAX`, and
66
- * `threshold <= total weight`.
74
+ * integer owner weights, integer `threshold` between `1` and `maxThreshold`,
75
+ * total weight `<= 255` (u8 max), and `threshold <= total weight`.
67
76
  *
68
77
  * @example
69
78
  * ```ts twoslash
@@ -88,14 +97,22 @@ export function assert<numberType = number>(config: Config<numberType>): void {
88
97
  throw new InvalidConfigError({ reason: 'owners cannot be empty' })
89
98
  if (owners.length > maxOwners)
90
99
  throw new InvalidConfigError({ reason: 'too many owners' })
100
+ if (!Number.isInteger(Number(threshold)))
101
+ throw new InvalidConfigError({ reason: 'threshold must be an integer' })
91
102
  if (Number(threshold) < 1)
92
103
  throw new InvalidConfigError({ reason: 'threshold cannot be zero' })
104
+ if (Number(threshold) > maxThreshold)
105
+ throw new InvalidConfigError({ reason: 'threshold exceeds max threshold' })
93
106
 
94
107
  let totalWeight = 0
95
108
  let previous: bigint | undefined
96
109
  for (const owner of owners) {
97
110
  if (!Address.validate(owner.owner) || Hex.toBigInt(owner.owner) === 0n)
98
111
  throw new InvalidConfigError({ reason: 'owner cannot be zero' })
112
+ if (!Number.isInteger(Number(owner.weight)))
113
+ throw new InvalidConfigError({
114
+ reason: 'owner weight must be an integer',
115
+ })
99
116
  if (Number(owner.weight) < 1)
100
117
  throw new InvalidConfigError({ reason: 'owner weight cannot be zero' })
101
118
 
@@ -109,9 +126,9 @@ export function assert<numberType = number>(config: Config<numberType>): void {
109
126
  totalWeight += Number(owner.weight)
110
127
  }
111
128
 
112
- if (totalWeight > 0xffffffff)
129
+ if (totalWeight > 0xff)
113
130
  throw new InvalidConfigError({
114
- reason: 'total owner weight exceeds u32 max',
131
+ reason: 'total owner weight exceeds u8 max',
115
132
  })
116
133
  if (Number(threshold) > totalWeight)
117
134
  throw new InvalidConfigError({
@@ -127,7 +144,7 @@ export declare namespace assert {
127
144
  * Normalizes a native multisig {@link ox#MultisigConfig.Config}.
128
145
  *
129
146
  * Sorts owners into strictly ascending `owner` address order (the canonical
130
- * form required for config ID derivation) and asserts the config is valid.
147
+ * form required for account derivation) and asserts the config is valid.
131
148
  *
132
149
  * @example
133
150
  * ```ts twoslash
@@ -197,7 +214,11 @@ export function fromTuple(tuple: Tuple): Config {
197
214
  /**
198
215
  * Derives the stable native multisig account address.
199
216
  *
200
- * `keccak256("tempo:multisig:account" || config_id)[12:32]`.
217
+ * Preimage (fixed-width big-endian, **not** RLP):
218
+ * `keccak256("tempo:multisig:account" || salt || u8(threshold) || u8(owners.length) || (owner || u8(weight)) for each owner)[12:32]`.
219
+ *
220
+ * The address is derived once from the initial (bootstrap) config and never
221
+ * changes — config updates do not affect it.
201
222
  *
202
223
  * @example
203
224
  * ```ts twoslash
@@ -213,49 +234,37 @@ export function fromTuple(tuple: Tuple): Config {
213
234
  * const address = MultisigConfig.getAddress(genesisConfig)
214
235
  * ```
215
236
  *
216
- * @example
217
- * ### From an genesis config ID
218
- *
219
- * If you already have the permanent `genesisConfigId`, pass it directly:
220
- *
221
- * ```ts twoslash
222
- * import { MultisigConfig } from 'ox/tempo'
223
- *
224
- * const genesisConfigId =
225
- * `0x${'00'.repeat(32)}` as const
226
- *
227
- * const address = MultisigConfig.getAddress({ genesisConfigId })
228
- * ```
229
- *
230
- * @param value - The genesis config (positional) or `{ genesisConfigId }`.
237
+ * @param config - The initial (bootstrap) multisig config.
231
238
  * @returns The multisig account address.
232
239
  */
233
- export function getAddress(value: getAddress.Value): Address.Address {
234
- const id =
235
- typeof value === 'object' && 'genesisConfigId' in value
236
- ? value.genesisConfigId
237
- : toId(value)
238
- const hash = Hash.keccak256(Hex.concat(Hex.fromString(accountDomain), id))
239
- return Address.from(Hex.slice(hash, 12, 32))
240
+ export function getAddress(config: Config): Address.Address {
241
+ assert(config)
242
+ const hash = Hash.keccak256(
243
+ Hex.concat(
244
+ Hex.fromString(accountDomain),
245
+ Hex.padLeft(config.salt ?? zeroSalt, 32),
246
+ Hex.fromNumber(config.threshold, { size: 1 }),
247
+ Hex.fromNumber(config.owners.length, { size: 1 }),
248
+ ...config.owners.flatMap((owner) => [
249
+ owner.owner,
250
+ Hex.fromNumber(owner.weight, { size: 1 }),
251
+ ]),
252
+ ),
253
+ )
254
+ const account = Address.from(Hex.slice(hash, 12, 32))
255
+ if (Hex.toBigInt(account) === 0n)
256
+ throw new InvalidConfigError({ reason: 'derived account cannot be zero' })
257
+ return account
240
258
  }
241
259
 
242
260
  export declare namespace getAddress {
243
- type Value =
244
- | Config
245
- | {
246
- /**
247
- * The permanent genesis config ID (`MultisigConfig.toId(genesisConfig)`).
248
- * Config updates never change this value, so it identifies the
249
- * account permanently.
250
- */
251
- genesisConfigId: Hex.Hex
252
- }
253
-
254
261
  type ErrorType =
255
- | toId.ErrorType
262
+ | assert.ErrorType
256
263
  | Address.from.ErrorType
257
264
  | Hash.keccak256.ErrorType
258
265
  | Hex.concat.ErrorType
266
+ | Hex.fromNumber.ErrorType
267
+ | Hex.fromString.ErrorType
259
268
  | Hex.slice.ErrorType
260
269
  | Errors.GlobalErrorType
261
270
  }
@@ -263,14 +272,16 @@ export declare namespace getAddress {
263
272
  /**
264
273
  * Computes the digest a native multisig owner approves (signs).
265
274
  *
266
- * `keccak256("tempo:multisig:signature" || inner_digest || account || config_id)`,
275
+ * `keccak256("tempo:multisig:signature" || inner_digest || account)`,
267
276
  * where `inner_digest` is the transaction sign payload
268
277
  * ({@link ox#TxEnvelopeTempo.(getSignPayload:function)}).
269
278
  *
270
- * The digest is always keyed on the permanent `account`/`genesisConfigId`
271
- * derived from the genesis (bootstrap) config — config updates never change
272
- * these values, so the genesis config is the correct input even for
273
- * post-update transactions.
279
+ * The digest is keyed on the permanent `account` derived from the genesis
280
+ * (bootstrap) config — config updates never change it, so the genesis config
281
+ * is the correct input even for post-update transactions.
282
+ *
283
+ * For a nested multisig owner approval, the parent digest becomes the nested
284
+ * approval's `payload`, with the nested multisig `account`.
274
285
  *
275
286
  * @example
276
287
  * ```ts twoslash
@@ -295,10 +306,10 @@ export declare namespace getAddress {
295
306
  * ```
296
307
  *
297
308
  * @example
298
- * ### From `account` + `genesisConfigId`
309
+ * ### From `account`
299
310
  *
300
- * If you already have the permanent `account` and `genesisConfigId` (for
301
- * example, recovered from a stored envelope), pass them directly:
311
+ * If you already have the permanent `account` (for example, recovered from a
312
+ * stored envelope), pass it directly:
302
313
  *
303
314
  * ```ts twoslash
304
315
  * import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
@@ -309,15 +320,13 @@ export declare namespace getAddress {
309
320
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
310
321
  * ],
311
322
  * })
312
- * const genesisConfigId = MultisigConfig.toId(genesisConfig)
313
323
  * const account = MultisigConfig.getAddress(genesisConfig)
314
324
  *
315
325
  * const envelope = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
316
326
  *
317
327
  * const digest = MultisigConfig.getSignPayload({
318
328
  * payload: TxEnvelopeTempo.getSignPayload(envelope),
319
- * account,
320
- * genesisConfigId,
329
+ * account
321
330
  * })
322
331
  * ```
323
332
  *
@@ -330,17 +339,8 @@ export function getSignPayload(value: getSignPayload.Value): Hex.Hex {
330
339
  'account' in value && value.account
331
340
  ? value.account
332
341
  : getAddress((value as { genesisConfig: Config }).genesisConfig)
333
- const genesisConfigId =
334
- 'genesisConfigId' in value && value.genesisConfigId
335
- ? value.genesisConfigId
336
- : toId((value as { genesisConfig: Config }).genesisConfig)
337
342
  return Hash.keccak256(
338
- Hex.concat(
339
- Hex.fromString(signatureDomain),
340
- Hex.from(payload),
341
- account,
342
- genesisConfigId,
343
- ),
343
+ Hex.concat(Hex.fromString(signatureDomain), Hex.from(payload), account),
344
344
  )
345
345
  }
346
346
 
@@ -352,16 +352,13 @@ export declare namespace getSignPayload {
352
352
  | {
353
353
  /** The native multisig account address. */
354
354
  account: Address.Address
355
- /** The permanent config ID. */
356
- genesisConfigId: Hex.Hex
357
355
  }
358
356
  | {
359
357
  /**
360
358
  * The initial multisig config (the bootstrap config that derived the
361
- * permanent `account` and `genesisConfigId`). Used to derive both values
362
- * automatically. Config updates never change `account`/`genesisConfigId`,
363
- * so the genesis config is also the correct input for post-update
364
- * transactions.
359
+ * permanent `account`). Used to derive the account automatically.
360
+ * Config updates never change `account`, so the genesis config is
361
+ * also the correct input for post-update transactions.
365
362
  */
366
363
  genesisConfig: Config
367
364
  }
@@ -369,66 +366,12 @@ export declare namespace getSignPayload {
369
366
 
370
367
  type ErrorType =
371
368
  | getAddress.ErrorType
372
- | toId.ErrorType
373
369
  | Hash.keccak256.ErrorType
374
370
  | Hex.concat.ErrorType
375
371
  | Hex.from.ErrorType
376
372
  | Errors.GlobalErrorType
377
373
  }
378
374
 
379
- /**
380
- * Derives the permanent config ID for a native multisig
381
- * {@link ox#MultisigConfig.Config}.
382
- *
383
- * Preimage (fixed-width big-endian, **not** RLP):
384
- * `keccak256("tempo:multisig:config" || salt || be_u32(threshold) || be_u32(owners.length) || (owner || be_u32(weight)) for each owner)`.
385
- *
386
- * @example
387
- * ```ts twoslash
388
- * import { MultisigConfig } from 'ox/tempo'
389
- *
390
- * const config = MultisigConfig.from({
391
- * threshold: 1,
392
- * owners: [
393
- * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
394
- * ],
395
- * })
396
- *
397
- * const genesisConfigId = MultisigConfig.toId(config)
398
- * ```
399
- *
400
- * @param config - The multisig config.
401
- * @returns The 32-byte config ID.
402
- */
403
- export function toId(config: Config): Hex.Hex {
404
- assert(config)
405
- const id = Hash.keccak256(
406
- Hex.concat(
407
- Hex.fromString(configDomain),
408
- Hex.padLeft(config.salt ?? zeroSalt, 32),
409
- Hex.fromNumber(config.threshold, { size: 4 }),
410
- Hex.fromNumber(config.owners.length, { size: 4 }),
411
- ...config.owners.flatMap((owner) => [
412
- owner.owner,
413
- Hex.fromNumber(owner.weight, { size: 4 }),
414
- ]),
415
- ),
416
- )
417
- if (Hex.toBigInt(id) === 0n)
418
- throw new InvalidConfigError({ reason: 'config ID cannot be zero' })
419
- return id
420
- }
421
-
422
- export declare namespace toId {
423
- type ErrorType =
424
- | assert.ErrorType
425
- | Hash.keccak256.ErrorType
426
- | Hex.concat.ErrorType
427
- | Hex.fromNumber.ErrorType
428
- | Hex.fromString.ErrorType
429
- | Errors.GlobalErrorType
430
- }
431
-
432
375
  /**
433
376
  * Converts a {@link ox#MultisigConfig.Config} to its RLP tuple form (carried
434
377
  * by the multisig signature `init`).
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "module",
3
+ "types": "../../_types/tempo/SignatureEnvelope.test-d.d.ts",
4
+ "main": "../../_cjs/tempo/SignatureEnvelope.test-d.js",
5
+ "module": "../../_esm/tempo/SignatureEnvelope.test-d.js"
6
+ }
@@ -0,0 +1,97 @@
1
+ import { expectTypeOf, test } from 'vitest'
2
+ import * as SignatureEnvelope from './SignatureEnvelope.js'
3
+
4
+ const signatureRpc = {
5
+ r: '0x01',
6
+ s: '0x02',
7
+ type: 'secp256k1',
8
+ yParity: '0x0',
9
+ } as const satisfies SignatureEnvelope.SignatureEnvelopeRpc
10
+
11
+ const signature = {
12
+ signature: {
13
+ r: 1n,
14
+ s: 2n,
15
+ yParity: 0,
16
+ },
17
+ type: 'secp256k1',
18
+ } as const satisfies SignatureEnvelope.Secp256k1
19
+
20
+ test('toRpc preserves the signature type', () => {
21
+ expectTypeOf(
22
+ SignatureEnvelope.toRpc(signature),
23
+ ).toEqualTypeOf<SignatureEnvelope.Secp256k1Rpc>()
24
+ })
25
+
26
+ test('MultisigRpc uses static initialized and bootstrap shapes', () => {
27
+ const initialized = {
28
+ account: '0x1111111111111111111111111111111111111111',
29
+ signatures: [signatureRpc],
30
+ } as const satisfies SignatureEnvelope.MultisigRpc
31
+ const bootstrap = {
32
+ init: {
33
+ owners: [
34
+ {
35
+ owner: '0x1111111111111111111111111111111111111111',
36
+ weight: 1,
37
+ },
38
+ ],
39
+ threshold: 1,
40
+ },
41
+ signatures: [signatureRpc],
42
+ } as const satisfies SignatureEnvelope.MultisigRpc
43
+ const bootstrapWithAccountUndefined: SignatureEnvelope.MultisigRpc = {
44
+ ...bootstrap,
45
+ account: undefined,
46
+ }
47
+
48
+ expectTypeOf(initialized.signatures).toMatchTypeOf<
49
+ readonly SignatureEnvelope.SignatureEnvelopeRpc[]
50
+ >()
51
+ expectTypeOf(bootstrap.signatures).toMatchTypeOf<
52
+ readonly SignatureEnvelope.SignatureEnvelopeRpc[]
53
+ >()
54
+ expectTypeOf(
55
+ bootstrapWithAccountUndefined,
56
+ ).toMatchTypeOf<SignatureEnvelope.MultisigRpc>()
57
+ expectTypeOf<
58
+ SignatureEnvelope.GetType<typeof bootstrap>
59
+ >().toEqualTypeOf<'multisig'>()
60
+ })
61
+
62
+ test('MultisigRpc rejects legacy shapes', () => {
63
+ const combined = {
64
+ account: '0x1111111111111111111111111111111111111111',
65
+ init: {
66
+ owners: [
67
+ {
68
+ owner: '0x1111111111111111111111111111111111111111',
69
+ weight: 1,
70
+ },
71
+ ],
72
+ threshold: 1,
73
+ },
74
+ signatures: [signatureRpc],
75
+ } as const
76
+ // @ts-expect-error Bootstrap RPC signatures omit `account`.
77
+ const combinedRpc: SignatureEnvelope.MultisigRpc = combined
78
+
79
+ const serialized = {
80
+ account: '0x1111111111111111111111111111111111111111',
81
+ signatures: ['0x1234'],
82
+ } as const
83
+ // @ts-expect-error Owner approvals use structured RPC envelopes.
84
+ const serializedRpc: SignatureEnvelope.MultisigRpc = serialized
85
+
86
+ const tagged = {
87
+ account: '0x1111111111111111111111111111111111111111',
88
+ signatures: [signatureRpc],
89
+ type: 'multisig',
90
+ } as const
91
+ // @ts-expect-error Multisig RPC signatures are untagged.
92
+ const taggedRpc: SignatureEnvelope.MultisigRpc = tagged
93
+
94
+ expectTypeOf(combinedRpc).toEqualTypeOf<SignatureEnvelope.MultisigRpc>()
95
+ expectTypeOf(serializedRpc).toEqualTypeOf<SignatureEnvelope.MultisigRpc>()
96
+ expectTypeOf(taggedRpc).toEqualTypeOf<SignatureEnvelope.MultisigRpc>()
97
+ })