ox 1.7.1 → 1.7.2

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