ox 1.7.1 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/core/Hash.js +1 -1
  3. package/dist/core/Hash.js.map +1 -1
  4. package/dist/tempo/KeyAuthorization.d.ts +2 -2
  5. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  6. package/dist/tempo/MultisigConfig.d.ts +151 -83
  7. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  8. package/dist/tempo/MultisigConfig.js +164 -86
  9. package/dist/tempo/MultisigConfig.js.map +1 -1
  10. package/dist/tempo/MultisigOperation.d.ts +340 -0
  11. package/dist/tempo/MultisigOperation.d.ts.map +1 -0
  12. package/dist/tempo/MultisigOperation.js +839 -0
  13. package/dist/tempo/MultisigOperation.js.map +1 -0
  14. package/dist/tempo/MultisigSimulation.d.ts +132 -0
  15. package/dist/tempo/MultisigSimulation.d.ts.map +1 -0
  16. package/dist/tempo/MultisigSimulation.js +140 -0
  17. package/dist/tempo/MultisigSimulation.js.map +1 -0
  18. package/dist/tempo/RpcSchemaTempo.d.ts +45 -0
  19. package/dist/tempo/RpcSchemaTempo.d.ts.map +1 -1
  20. package/dist/tempo/SignatureEnvelope.d.ts +55 -86
  21. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  22. package/dist/tempo/SignatureEnvelope.js +93 -158
  23. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  24. package/dist/tempo/TransactionRequest.d.ts +9 -17
  25. package/dist/tempo/TransactionRequest.d.ts.map +1 -1
  26. package/dist/tempo/TransactionRequest.js +7 -7
  27. package/dist/tempo/TransactionRequest.js.map +1 -1
  28. package/dist/tempo/TxEnvelopeTempo.d.ts +3 -2
  29. package/dist/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  30. package/dist/tempo/TxEnvelopeTempo.js +3 -2
  31. package/dist/tempo/TxEnvelopeTempo.js.map +1 -1
  32. package/dist/tempo/index.d.ts +22 -2
  33. package/dist/tempo/index.d.ts.map +1 -1
  34. package/dist/tempo/index.js +22 -2
  35. package/dist/tempo/index.js.map +1 -1
  36. package/dist/zod/tempo/AuthorizationTempo.d.ts +60 -180
  37. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  38. package/dist/zod/tempo/KeyAuthorization.d.ts +65 -118
  39. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  40. package/dist/zod/tempo/MultisigConfig.d.ts +31 -2
  41. package/dist/zod/tempo/MultisigConfig.d.ts.map +1 -1
  42. package/dist/zod/tempo/MultisigConfig.js +26 -3
  43. package/dist/zod/tempo/MultisigConfig.js.map +1 -1
  44. package/dist/zod/tempo/MultisigOperation.d.ts +158 -0
  45. package/dist/zod/tempo/MultisigOperation.d.ts.map +1 -0
  46. package/dist/zod/tempo/MultisigOperation.js +62 -0
  47. package/dist/zod/tempo/MultisigOperation.js.map +1 -0
  48. package/dist/zod/tempo/MultisigSimulation.d.ts +120 -0
  49. package/dist/zod/tempo/MultisigSimulation.d.ts.map +1 -0
  50. package/dist/zod/tempo/MultisigSimulation.js +78 -0
  51. package/dist/zod/tempo/MultisigSimulation.js.map +1 -0
  52. package/dist/zod/tempo/RpcSchemaTempo.d.ts +528 -144
  53. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  54. package/dist/zod/tempo/RpcSchemaTempo.js +47 -0
  55. package/dist/zod/tempo/RpcSchemaTempo.js.map +1 -1
  56. package/dist/zod/tempo/SignatureEnvelope.d.ts +18 -54
  57. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  58. package/dist/zod/tempo/SignatureEnvelope.js +5 -22
  59. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  60. package/dist/zod/tempo/Transaction.d.ts +138 -348
  61. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  62. package/dist/zod/tempo/TransactionRequest.d.ts +286 -239
  63. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  64. package/dist/zod/tempo/TransactionRequest.js +11 -22
  65. package/dist/zod/tempo/TransactionRequest.js.map +1 -1
  66. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +60 -48
  67. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  68. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts +15 -12
  69. package/dist/zod/tempo/ZoneRpcAuthentication.d.ts.map +1 -1
  70. package/dist/zod/tempo/z.d.ts +2 -0
  71. package/dist/zod/tempo/z.d.ts.map +1 -1
  72. package/dist/zod/tempo/z.js +2 -0
  73. package/dist/zod/tempo/z.js.map +1 -1
  74. package/package.json +21 -1
  75. package/src/core/Hash.ts +1 -1
  76. package/src/core/_test/Hash.test.ts +15 -0
  77. package/src/tempo/AuthorizationTempo.test.ts +4 -0
  78. package/src/tempo/KeyAuthorization.test-d.ts +13 -12
  79. package/src/tempo/KeyAuthorization.test.ts +109 -3
  80. package/src/tempo/KeyAuthorization.ts +4 -4
  81. package/src/tempo/MultisigConfig.test-d.ts +52 -0
  82. package/src/tempo/MultisigConfig.test.ts +288 -227
  83. package/src/tempo/MultisigConfig.ts +275 -104
  84. package/src/tempo/MultisigOperation.test-d.ts +79 -0
  85. package/src/tempo/MultisigOperation.test.ts +1950 -0
  86. package/src/tempo/MultisigOperation.ts +1293 -0
  87. package/src/tempo/MultisigSimulation.test-d.ts +33 -0
  88. package/src/tempo/MultisigSimulation.test.ts +283 -0
  89. package/src/tempo/MultisigSimulation.ts +266 -0
  90. package/src/tempo/RpcSchemaTempo.test-d.ts +91 -0
  91. package/src/tempo/RpcSchemaTempo.ts +46 -0
  92. package/src/tempo/SignatureEnvelope.test-d.ts +33 -72
  93. package/src/tempo/SignatureEnvelope.test.ts +533 -500
  94. package/src/tempo/SignatureEnvelope.ts +171 -258
  95. package/src/tempo/Transaction.test.ts +61 -0
  96. package/src/tempo/TransactionRequest.test-d.ts +11 -0
  97. package/src/tempo/TransactionRequest.test.ts +214 -41
  98. package/src/tempo/TransactionRequest.ts +19 -20
  99. package/src/tempo/TxEnvelopeTempo.test-d.ts +7 -0
  100. package/src/tempo/TxEnvelopeTempo.test.ts +25 -0
  101. package/src/tempo/TxEnvelopeTempo.ts +5 -3
  102. package/src/tempo/index.ts +22 -2
  103. package/src/tempo/multisig.e2e.test.ts +164 -87
  104. package/src/version.ts +1 -1
  105. package/src/zod/tempo/MultisigConfig.ts +29 -3
  106. package/src/zod/tempo/MultisigOperation.ts +66 -0
  107. package/src/zod/tempo/MultisigSimulation.ts +98 -0
  108. package/src/zod/tempo/RpcSchemaTempo.ts +53 -0
  109. package/src/zod/tempo/SignatureEnvelope.ts +12 -54
  110. package/src/zod/tempo/TransactionRequest.ts +17 -26
  111. package/src/zod/tempo/_test/KeyAuthorization.test.ts +2 -4
  112. package/src/zod/tempo/_test/MultisigConfig.test-d.ts +13 -0
  113. package/src/zod/tempo/_test/MultisigConfig.test.ts +13 -2
  114. package/src/zod/tempo/_test/MultisigOperation.test-d.ts +27 -0
  115. package/src/zod/tempo/_test/MultisigOperation.test.ts +66 -0
  116. package/src/zod/tempo/_test/MultisigSimulation.test-d.ts +13 -0
  117. package/src/zod/tempo/_test/MultisigSimulation.test.ts +78 -0
  118. package/src/zod/tempo/_test/RpcSchemaTempo.test-d.ts +48 -0
  119. package/src/zod/tempo/_test/RpcSchemaTempo.test.ts +83 -0
  120. package/src/zod/tempo/_test/SignatureEnvelope.test-d.ts +1 -1
  121. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +57 -120
  122. package/src/zod/tempo/_test/TransactionRequest.test.ts +43 -16
  123. package/src/zod/tempo/z.ts +2 -0
@@ -0,0 +1,1293 @@
1
+ import * as Address from '../core/Address.js'
2
+ import * as Errors from '../core/Errors.js'
3
+ import * as Hash from '../core/Hash.js'
4
+ import * as Hex from '../core/Hex.js'
5
+ import * as KeyAuthorization_ from './KeyAuthorization.js'
6
+ import * as MultisigConfig from './MultisigConfig.js'
7
+ import * as SignatureEnvelope from './SignatureEnvelope.js'
8
+ import * as TxEnvelopeTempo from './TxEnvelopeTempo.js'
9
+
10
+ /** Fields shared by every multisig operation. */
11
+ export type Base<quantity = bigint> = {
12
+ /** Root multisig account. */
13
+ account: Address.Address
14
+ /** Every retained serialized owner approval. */
15
+ approvals: readonly Hex.Hex[]
16
+ /** Root configuration used to verify approvals. */
17
+ config: MultisigConfig.Config<quantity>
18
+ /** Unix creation time in milliseconds. */
19
+ createdAt: number
20
+ /** Deterministic multisig operation hash. */
21
+ hash: Hex.Hex
22
+ /** Number of approvals selected for quorum evaluation. */
23
+ signatureCount: number
24
+ /** Required root owner weight. */
25
+ threshold: number
26
+ /** Unix time of the last update in milliseconds. */
27
+ updatedAt: number
28
+ /** Root owner weight reached by the selected approvals. */
29
+ weight: number
30
+ }
31
+
32
+ /** Multisig transaction approval operation. */
33
+ export type TransactionOperation<quantity = bigint> = Base<quantity> & {
34
+ /** Time when another relay may reclaim the submission lease. */
35
+ expiresAt?: number | undefined
36
+ /** Current operation state. */
37
+ status: 'pending' | 'submitting' | 'success'
38
+ /** Fencing token owned by the current submitter. */
39
+ submissionId?: Hex.Hex | undefined
40
+ /** Canonical serialized Tempo envelope without its outer sender signature. */
41
+ transaction: Hex.Hex
42
+ /** Hash returned after the downstream submitter accepts the transaction. */
43
+ transactionHash?: Hex.Hex | undefined
44
+ /** Operation kind. */
45
+ type: 'transaction'
46
+ }
47
+
48
+ /** Multisig key authorization approval operation. */
49
+ export type KeyAuthorizationOperation<quantity = bigint> = Base<quantity> & {
50
+ /** Canonical serialized key authorization. */
51
+ keyAuthorization: Hex.Hex
52
+ /** Current operation state. */
53
+ status: 'pending' | 'success'
54
+ /** Operation kind. */
55
+ type: 'keyAuthorization'
56
+ }
57
+
58
+ /** Transaction or key authorization multisig operation. */
59
+ export type Operation<quantity = bigint> =
60
+ | TransactionOperation<quantity>
61
+ | KeyAuthorizationOperation<quantity>
62
+
63
+ /** JSON-RPC multisig transaction operation. */
64
+ export type TransactionRpc = TransactionOperation<Hex.Hex>
65
+
66
+ /** JSON-RPC multisig key authorization operation. */
67
+ export type KeyAuthorizationRpc = KeyAuthorizationOperation<Hex.Hex>
68
+
69
+ /** JSON-RPC multisig operation. */
70
+ export type Rpc = Operation<Hex.Hex>
71
+
72
+ /**
73
+ * Derives the deterministic hash for a multisig operation.
74
+ *
75
+ * @example
76
+ * ```ts twoslash
77
+ * // @noErrors
78
+ * import { MultisigOperation } from 'ox/tempo'
79
+ *
80
+ * const hash = MultisigOperation.getHash({
81
+ * account,
82
+ * config,
83
+ * transaction,
84
+ * type: 'transaction'
85
+ * })
86
+ * ```
87
+ *
88
+ * @param options - Operation payload and multisig identity.
89
+ * @returns The operation hash signed by each owner.
90
+ */
91
+ export function getHash(options: getHash.Options): Hex.Hex {
92
+ const { account, config } = options
93
+ const payload =
94
+ options.type === 'transaction'
95
+ ? TxEnvelopeTempo.getSignPayload(
96
+ TxEnvelopeTempo.deserialize(options.transaction),
97
+ )
98
+ : KeyAuthorization_.getSignPayload(
99
+ KeyAuthorization_.deserialize(options.keyAuthorization),
100
+ )
101
+ return MultisigConfig.getSignPayload({
102
+ account,
103
+ config,
104
+ payload,
105
+ })
106
+ }
107
+
108
+ export declare namespace getHash {
109
+ /** Parameters for `getHash`. */
110
+ export type Options = {
111
+ /** Root multisig account. */
112
+ account: Address.Address
113
+ /** Complete root multisig configuration witness. */
114
+ config: MultisigConfig.Config
115
+ } & (
116
+ | {
117
+ /** Canonical serialized key authorization. */
118
+ keyAuthorization: Hex.Hex
119
+ /** Operation kind. */
120
+ type: 'keyAuthorization'
121
+ }
122
+ | {
123
+ /** Canonical serialized Tempo envelope without its outer sender signature. */
124
+ transaction: TxEnvelopeTempo.Serialized
125
+ /** Operation kind. */
126
+ type: 'transaction'
127
+ }
128
+ )
129
+
130
+ /** Error type for `getHash`. */
131
+ export type ErrorType =
132
+ | KeyAuthorization_.deserialize.ErrorType
133
+ | KeyAuthorization_.getSignPayload.ErrorType
134
+ | MultisigConfig.getSignPayload.ErrorType
135
+ | TxEnvelopeTempo.deserialize.ErrorType
136
+ | TxEnvelopeTempo.getSignPayload.ErrorType
137
+ | Errors.GlobalErrorType
138
+ }
139
+
140
+ /**
141
+ * Validates, deduplicates, and selects owner approvals for an operation.
142
+ *
143
+ * The function retains one canonical approval per owner. It selects the
144
+ * smallest deterministic quorum by owner weight, then orders the selected
145
+ * approvals by owner address for serialization.
146
+ *
147
+ * @example
148
+ * ```ts twoslash
149
+ * // @noErrors
150
+ * import { MultisigOperation } from 'ox/tempo'
151
+ *
152
+ * const selection = await MultisigOperation.selectApprovals({
153
+ * account,
154
+ * approvals,
155
+ * config,
156
+ * hash
157
+ * })
158
+ * ```
159
+ *
160
+ * @param options - Approval selection parameters.
161
+ * @returns The retained approvals and deterministic quorum selection.
162
+ */
163
+ export async function selectApprovals(
164
+ options: selectApprovals.Options,
165
+ ): Promise<selectApprovals.ReturnValue> {
166
+ const { account, approvals, hash } = options
167
+ if (!Address.validate(account) || Hex.toBigInt(account) === 0n)
168
+ throw new InvalidApprovalError({ reason: 'account is invalid' })
169
+ if (!Hash.validate(hash))
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
+ return selectApprovals_internal(
180
+ {
181
+ account,
182
+ approvals,
183
+ config,
184
+ hash,
185
+ },
186
+ [account.toLowerCase()],
187
+ )
188
+ }
189
+
190
+ export declare namespace selectApprovals {
191
+ /** Parameters for `selectApprovals`. */
192
+ export type Options = {
193
+ /** Root multisig account. */
194
+ account: Address.Address
195
+ /** Serialized primitive or nested owner approvals. */
196
+ approvals: readonly SignatureEnvelope.Serialized[]
197
+ /** Current root multisig configuration. */
198
+ config: MultisigConfig.Config
199
+ /** Deterministic operation hash approved by root owners. */
200
+ hash: Hex.Hex
201
+ }
202
+
203
+ /** Result of validating and selecting approvals. */
204
+ export type ReturnValue = {
205
+ /** Every retained approval, ordered by owner address. */
206
+ approvals: readonly SignatureEnvelope.Serialized[]
207
+ /** Number of approvals selected for quorum evaluation. */
208
+ signatureCount: number
209
+ /** Approvals selected for serialization, ordered by owner address. */
210
+ selectedApprovals: readonly SignatureEnvelope.Serialized[]
211
+ /** Required owner weight. */
212
+ threshold: number
213
+ /** Owner weight reached by the selected approvals. */
214
+ weight: number
215
+ }
216
+
217
+ /** Error type for `selectApprovals`. */
218
+ export type ErrorType =
219
+ | InvalidApprovalError
220
+ | MultisigConfig.assert.ErrorType
221
+ | MultisigConfig.getSignPayload.ErrorType
222
+ | SignatureEnvelope.CoercionError
223
+ | SignatureEnvelope.extractAddress.ErrorType
224
+ | SignatureEnvelope.serialize.ErrorType
225
+ | SignatureEnvelope.VerificationError
226
+ | Errors.GlobalErrorType
227
+ }
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
+
313
+ /**
314
+ * Serializes a multisig transaction operation with selected owner approvals.
315
+ *
316
+ * @example
317
+ * ```ts twoslash
318
+ * // @noErrors
319
+ * import { MultisigOperation } from 'ox/tempo'
320
+ *
321
+ * const transaction = MultisigOperation.serializeTransaction(
322
+ * operation,
323
+ * {
324
+ * approvals: selection.selectedApprovals
325
+ * }
326
+ * )
327
+ * ```
328
+ *
329
+ * @param operation - Multisig transaction operation.
330
+ * @param options - Transaction serialization options.
331
+ * @returns The signed serialized Tempo transaction.
332
+ */
333
+ export function serializeTransaction(
334
+ operation: TransactionOperation,
335
+ options: serializeTransaction.Options,
336
+ ): TxEnvelopeTempo.Serialized {
337
+ const value = from(operation)
338
+ const envelope = TxEnvelopeTempo.deserialize(
339
+ value.transaction as TxEnvelopeTempo.Serialized,
340
+ )
341
+ const approvals = options.approvals.map((approval) =>
342
+ SignatureEnvelope.from(approval),
343
+ )
344
+ assertRetainedApprovals(value, approvals)
345
+ const signatures = SignatureEnvelope.sortMultisigApprovals({
346
+ account: value.account,
347
+ config: value.config,
348
+ payload: TxEnvelopeTempo.getSignPayload(envelope),
349
+ signatures: approvals,
350
+ })
351
+ const signature = SignatureEnvelope.from({
352
+ account: value.account,
353
+ config: value.config,
354
+ signatures,
355
+ })
356
+ return TxEnvelopeTempo.serialize(
357
+ envelope,
358
+ value.transaction.startsWith(TxEnvelopeTempo.feePayerMagic)
359
+ ? {
360
+ format: 'feePayer',
361
+ sender: envelope.from,
362
+ signature,
363
+ }
364
+ : { signature },
365
+ )
366
+ }
367
+
368
+ export declare namespace serializeTransaction {
369
+ /** Options for `serializeTransaction`. */
370
+ export type Options = {
371
+ /** Selected retained approvals to attach to the transaction. */
372
+ approvals: readonly SignatureEnvelope.Serialized[]
373
+ }
374
+
375
+ /** Error type for `serializeTransaction`. */
376
+ export type ErrorType =
377
+ | from.ErrorType
378
+ | InvalidOperationError
379
+ | SignatureEnvelope.sortMultisigApprovals.ErrorType
380
+ | TxEnvelopeTempo.deserialize.ErrorType
381
+ | TxEnvelopeTempo.getSignPayload.ErrorType
382
+ | TxEnvelopeTempo.serialize.ErrorType
383
+ | Errors.GlobalErrorType
384
+ }
385
+
386
+ /**
387
+ * Validates and normalizes a multisig operation.
388
+ *
389
+ * @example
390
+ * ```ts twoslash
391
+ * // @noErrors
392
+ * import { MultisigOperation } from 'ox/tempo'
393
+ *
394
+ * const operation = MultisigOperation.from(value)
395
+ * ```
396
+ *
397
+ * @param operation - Multisig operation.
398
+ * @returns The validated operation.
399
+ */
400
+ export function from<const operation extends Operation>(
401
+ operation: operation,
402
+ ): from.ReturnValue<operation> {
403
+ try {
404
+ const config = MultisigConfig.from(operation.config)
405
+ if (
406
+ typeof config.threshold !== 'number' ||
407
+ config.owners.some((owner) => typeof owner.weight !== 'number')
408
+ )
409
+ throw new InvalidOperationError({
410
+ reason: 'config threshold and owner weights must be numbers',
411
+ })
412
+ const value = { ...operation, config } as Operation
413
+ assertBase(value, config)
414
+ if (value.type === 'transaction') assertTransaction(value)
415
+ else if (value.type === 'keyAuthorization')
416
+ assertKeyAuthorization(value, config)
417
+ else throw new InvalidOperationError({ reason: 'unknown operation type' })
418
+ return value as never
419
+ } catch (cause) {
420
+ if (cause instanceof InvalidOperationError) throw cause
421
+ throw new InvalidOperationError({ cause })
422
+ }
423
+ }
424
+
425
+ export declare namespace from {
426
+ /** Return type for `from`. */
427
+ export type ReturnValue<operation extends Operation> =
428
+ operation extends TransactionOperation
429
+ ? TransactionOperation
430
+ : KeyAuthorizationOperation
431
+
432
+ /** Error type for `from`. */
433
+ export type ErrorType = InvalidOperationError | Errors.GlobalErrorType
434
+ }
435
+
436
+ /**
437
+ * Converts a JSON-RPC multisig operation to its domain representation.
438
+ *
439
+ * @example
440
+ * ```ts twoslash
441
+ * // @noErrors
442
+ * import { MultisigOperation } from 'ox/tempo'
443
+ *
444
+ * const operation = MultisigOperation.fromRpc(value)
445
+ * ```
446
+ *
447
+ * @param operation - JSON-RPC multisig operation.
448
+ * @returns The validated operation.
449
+ */
450
+ export function fromRpc<const operation extends Rpc>(
451
+ operation: operation,
452
+ ): fromRpc.ReturnValue<operation> {
453
+ try {
454
+ const version = operation.config?.version
455
+ if (typeof version !== 'string' || !Hex.validate(version))
456
+ throw new InvalidOperationError({
457
+ reason: 'config.version must be a hexadecimal quantity',
458
+ })
459
+ const version_ = Hex.toBigInt(version)
460
+ if (Hex.fromNumber(version_) !== version)
461
+ throw new InvalidOperationError({
462
+ reason: 'config.version must use canonical quantity encoding',
463
+ })
464
+ return from({
465
+ ...operation,
466
+ config: MultisigConfig.fromRpc(operation.config),
467
+ } as Operation) as never
468
+ } catch (cause) {
469
+ if (cause instanceof InvalidOperationError) throw cause
470
+ throw new InvalidOperationError({ cause })
471
+ }
472
+ }
473
+
474
+ export declare namespace fromRpc {
475
+ /** Return type for `fromRpc`. */
476
+ export type ReturnValue<operation extends Rpc> =
477
+ operation extends TransactionRpc
478
+ ? TransactionOperation
479
+ : KeyAuthorizationOperation
480
+
481
+ /** Error type for `fromRpc`. */
482
+ export type ErrorType = InvalidOperationError | Errors.GlobalErrorType
483
+ }
484
+
485
+ /**
486
+ * Converts a multisig operation to its JSON-RPC representation.
487
+ *
488
+ * @example
489
+ * ```ts twoslash
490
+ * // @noErrors
491
+ * import { MultisigOperation } from 'ox/tempo'
492
+ *
493
+ * const operationRpc = MultisigOperation.toRpc(operation)
494
+ * ```
495
+ *
496
+ * @param operation - Multisig operation.
497
+ * @returns The JSON-RPC operation.
498
+ */
499
+ export function toRpc<const operation extends Operation>(
500
+ operation: operation,
501
+ ): toRpc.ReturnValue<operation> {
502
+ const value = from(operation)
503
+ return {
504
+ ...value,
505
+ config: MultisigConfig.toRpc(value.config),
506
+ } as never
507
+ }
508
+
509
+ export declare namespace toRpc {
510
+ /** Return type for `toRpc`. */
511
+ export type ReturnValue<operation extends Operation> =
512
+ operation extends TransactionOperation
513
+ ? TransactionRpc
514
+ : KeyAuthorizationRpc
515
+
516
+ /** Error type for `toRpc`. */
517
+ export type ErrorType = from.ErrorType | MultisigConfig.toRpc.ErrorType
518
+ }
519
+
520
+ /**
521
+ * Validates and selects approvals recursively.
522
+ *
523
+ * @internal
524
+ */
525
+ async function selectApprovals_internal(
526
+ options: selectApprovals.Options,
527
+ path: readonly string[],
528
+ ): Promise<selectApprovals.ReturnValue> {
529
+ const owners = new Map(
530
+ options.config.owners.map((owner) => [
531
+ owner.owner.toLowerCase(),
532
+ { address: owner.owner, weight: Number(owner.weight) },
533
+ ]),
534
+ )
535
+ const groups = new Map<string, ApprovalGroup>()
536
+ for (const serialized of options.approvals) {
537
+ const signature = SignatureEnvelope.from(serialized)
538
+ if (signature.type === 'keychain')
539
+ throw new InvalidApprovalError({
540
+ reason: 'keychain signatures cannot approve a multisig operation',
541
+ })
542
+ const address =
543
+ signature.type === 'multisig'
544
+ ? signature.account
545
+ : SignatureEnvelope.extractAddress({
546
+ payload: options.hash,
547
+ signature,
548
+ })
549
+ const owner = owners.get(address.toLowerCase())
550
+ if (!owner)
551
+ throw new InvalidApprovalError({
552
+ reason: `signature is from non-owner ${address}`,
553
+ })
554
+ const key = address.toLowerCase()
555
+ const group = groups.get(key)
556
+ if (group) group.signatures.push(signature)
557
+ else
558
+ groups.set(key, {
559
+ address: owner.address,
560
+ signatures: [signature],
561
+ weight: owner.weight,
562
+ })
563
+ }
564
+
565
+ const valid: SelectedApproval[] = []
566
+ const retained: RetainedApproval[] = []
567
+ for (const group of groups.values()) {
568
+ const nested = group.signatures.filter(
569
+ (signature) => signature.type === 'multisig',
570
+ )
571
+ if (nested.length > 0) {
572
+ if (nested.length !== group.signatures.length)
573
+ throw new InvalidApprovalError({
574
+ reason: `owner ${group.address} has conflicting signature types`,
575
+ })
576
+ if (
577
+ path.length >= MultisigConfig.maxNestingDepth ||
578
+ path.includes(group.address.toLowerCase())
579
+ )
580
+ throw new InvalidApprovalError({
581
+ reason: `nested multisig owner ${group.address} is invalid`,
582
+ })
583
+ const config = MultisigConfig.from(nested[0]!.config)
584
+ if (nested.some((signature) => !sameConfig(signature.config, config)))
585
+ throw new InvalidApprovalError({
586
+ reason: `nested multisig owner ${group.address} has conflicting config witnesses`,
587
+ })
588
+ if (
589
+ config.version === 0n &&
590
+ !Address.isEqual(MultisigConfig.getAddress(config), group.address)
591
+ )
592
+ throw new InvalidApprovalError({
593
+ reason: `initial config does not derive nested multisig owner ${group.address}`,
594
+ })
595
+ const selected = await selectApprovals_internal(
596
+ {
597
+ account: group.address,
598
+ approvals: nested.flatMap((signature) =>
599
+ signature.signatures.map((approval) =>
600
+ SignatureEnvelope.serialize(approval),
601
+ ),
602
+ ),
603
+ config,
604
+ hash: MultisigConfig.getSignPayload({
605
+ account: group.address,
606
+ config,
607
+ payload: options.hash,
608
+ }),
609
+ },
610
+ [...path, group.address.toLowerCase()],
611
+ )
612
+ retained.push({
613
+ address: group.address,
614
+ signature: SignatureEnvelope.serialize(
615
+ SignatureEnvelope.from({
616
+ account: group.address,
617
+ config,
618
+ signatures: selected.approvals.map((approval) =>
619
+ SignatureEnvelope.from(approval),
620
+ ),
621
+ }),
622
+ ),
623
+ })
624
+ if (selected.weight >= selected.threshold)
625
+ valid.push({
626
+ address: group.address,
627
+ signature: SignatureEnvelope.serialize(
628
+ SignatureEnvelope.from({
629
+ account: group.address,
630
+ config,
631
+ signatures: selected.selectedApprovals.map((approval) =>
632
+ SignatureEnvelope.from(approval),
633
+ ),
634
+ }),
635
+ ),
636
+ weight: group.weight,
637
+ })
638
+ continue
639
+ }
640
+
641
+ const signatures = group.signatures.map((signature) => {
642
+ if (
643
+ !SignatureEnvelope.verify(signature, {
644
+ address: group.address,
645
+ payload: options.hash,
646
+ })
647
+ )
648
+ throw new InvalidApprovalError({
649
+ reason: `signature from owner ${group.address} is invalid`,
650
+ })
651
+ return SignatureEnvelope.serialize(signature)
652
+ })
653
+ const signature = signatures.sort(compareHex)[0]!
654
+ valid.push({
655
+ address: group.address,
656
+ signature,
657
+ weight: group.weight,
658
+ })
659
+ retained.push({ address: group.address, signature })
660
+ }
661
+
662
+ const ranked = valid.sort(
663
+ (a, b) => b.weight - a.weight || compareApprovalAddress(a, b),
664
+ )
665
+ const selected: typeof ranked = []
666
+ let weight = 0
667
+ for (const approval of ranked.slice(0, MultisigConfig.maxSignatures)) {
668
+ if (weight >= Number(options.config.threshold)) break
669
+ selected.push(approval)
670
+ weight += approval.weight
671
+ }
672
+ selected.sort(compareApprovalAddress)
673
+
674
+ return {
675
+ approvals: retained
676
+ .sort(compareApprovalAddress)
677
+ .map((approval) => approval.signature),
678
+ selectedApprovals: selected.map((approval) => approval.signature),
679
+ signatureCount: selected.length,
680
+ threshold: Number(options.config.threshold),
681
+ weight,
682
+ }
683
+ }
684
+
685
+ /** Approval selected for quorum evaluation. @internal */
686
+ type SelectedApproval = {
687
+ /** Configured owner address. */
688
+ address: Address.Address
689
+ /** Serialized owner signature. */
690
+ signature: SignatureEnvelope.Serialized
691
+ /** Configured owner weight. */
692
+ weight: number
693
+ }
694
+
695
+ /** Approvals submitted for one configured owner. @internal */
696
+ type ApprovalGroup = {
697
+ /** Configured owner address. */
698
+ address: Address.Address
699
+ /** Submitted signatures that resolve to the owner. */
700
+ signatures: SignatureEnvelope.SignatureEnvelope[]
701
+ /** Configured owner weight. */
702
+ weight: number
703
+ }
704
+
705
+ /** Approval retained in operation storage. @internal */
706
+ type RetainedApproval = {
707
+ /** Configured owner address. */
708
+ address: Address.Address
709
+ /** Serialized primitive or normalized nested approval. */
710
+ signature: SignatureEnvelope.Serialized
711
+ }
712
+
713
+ /**
714
+ * Orders approval records by owner address.
715
+ *
716
+ * @internal
717
+ */
718
+ function compareApprovalAddress(
719
+ a: SelectedApproval | RetainedApproval,
720
+ b: SelectedApproval | RetainedApproval,
721
+ ) {
722
+ const addressA = Hex.toBigInt(a.address)
723
+ const addressB = Hex.toBigInt(b.address)
724
+ return addressA < addressB ? -1 : addressA > addressB ? 1 : 0
725
+ }
726
+
727
+ /**
728
+ * Orders hexadecimal data bytewise.
729
+ *
730
+ * @internal
731
+ */
732
+ function compareHex(a: Hex.Hex, b: Hex.Hex) {
733
+ const hexA = a.toLowerCase()
734
+ const hexB = b.toLowerCase()
735
+ return hexA < hexB ? -1 : hexA > hexB ? 1 : 0
736
+ }
737
+
738
+ /**
739
+ * Validates fields shared by every operation.
740
+ *
741
+ * @internal
742
+ */
743
+ function assertBase(operation: Operation, config: MultisigConfig.Config): void {
744
+ if (!Address.validate(operation.account))
745
+ throw new InvalidOperationError({ reason: 'account is invalid' })
746
+ if (Hex.toBigInt(operation.account) === 0n)
747
+ throw new InvalidOperationError({ reason: 'account cannot be zero' })
748
+ if (!Hash.validate(operation.hash))
749
+ throw new InvalidOperationError({ reason: 'hash is invalid' })
750
+ assertInteger(operation.createdAt, 'createdAt')
751
+ assertInteger(operation.updatedAt, 'updatedAt')
752
+ if (operation.updatedAt < operation.createdAt)
753
+ throw new InvalidOperationError({
754
+ reason: 'updatedAt cannot precede createdAt',
755
+ })
756
+ assertInteger(operation.signatureCount, 'signatureCount')
757
+ assertInteger(operation.threshold, 'threshold')
758
+ assertInteger(operation.weight, 'weight')
759
+ if (operation.threshold !== Number(config.threshold))
760
+ throw new InvalidOperationError({
761
+ reason: 'threshold must equal config.threshold',
762
+ })
763
+ if (operation.weight > 0xff)
764
+ throw new InvalidOperationError({ reason: 'weight exceeds u8 max' })
765
+ if (operation.signatureCount > MultisigConfig.maxSignatures)
766
+ throw new InvalidOperationError({ reason: 'too many selected signatures' })
767
+ if (!Array.isArray(operation.approvals))
768
+ throw new InvalidOperationError({ reason: 'approvals must be an array' })
769
+ if (operation.approvals.length > config.owners.length)
770
+ throw new InvalidOperationError({ reason: 'too many retained approvals' })
771
+ if (operation.signatureCount > operation.approvals.length)
772
+ throw new InvalidOperationError({
773
+ reason: 'signatureCount exceeds retained approvals',
774
+ })
775
+ if ((operation.signatureCount === 0) !== (operation.weight === 0))
776
+ throw new InvalidOperationError({
777
+ reason: 'signatureCount and weight must both be zero or nonzero',
778
+ })
779
+ const owners = new Map(
780
+ config.owners.map((owner) => [
781
+ owner.owner.toLowerCase(),
782
+ Number(owner.weight),
783
+ ]),
784
+ )
785
+ const approvalWeights: number[] = []
786
+ const seen = new Set<string>()
787
+ for (const approval of operation.approvals) {
788
+ if (
789
+ typeof approval !== 'string' ||
790
+ !Hex.validate(approval, { strict: true })
791
+ )
792
+ throw new InvalidOperationError({ reason: 'approval is invalid' })
793
+ const signature = assertApproval(
794
+ operation.account,
795
+ approval as SignatureEnvelope.Serialized,
796
+ config,
797
+ )
798
+ const address = SignatureEnvelope.extractAddress({
799
+ payload: operation.hash,
800
+ signature,
801
+ })
802
+ const key = address.toLowerCase()
803
+ const weight = owners.get(key)
804
+ if (weight === undefined)
805
+ throw new InvalidOperationError({
806
+ reason: 'approval is from a non-owner',
807
+ })
808
+ if (seen.has(key))
809
+ throw new InvalidOperationError({
810
+ reason:
811
+ operation.type === 'keyAuthorization'
812
+ ? 'key authorization contains duplicate owner approvals'
813
+ : 'duplicate owner approval',
814
+ })
815
+ seen.add(key)
816
+ approvalWeights.push(weight)
817
+ }
818
+ if (
819
+ !isWeightReachable(
820
+ approvalWeights,
821
+ operation.signatureCount,
822
+ operation.weight,
823
+ )
824
+ )
825
+ throw new InvalidOperationError({
826
+ reason:
827
+ 'weight is not reachable by signatureCount retained owner approvals',
828
+ })
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
+ })
836
+ }
837
+
838
+ /**
839
+ * Validates a transaction operation and its state-specific fields.
840
+ *
841
+ * @internal
842
+ */
843
+ function assertTransaction(operation: TransactionOperation): void {
844
+ if (
845
+ 'keyAuthorization' in operation &&
846
+ operation.keyAuthorization !== undefined
847
+ )
848
+ throw new InvalidOperationError({
849
+ reason: 'transaction operations cannot contain keyAuthorization',
850
+ })
851
+ const expiresAt = operation.expiresAt
852
+ const submissionId = operation.submissionId
853
+ const transactionHash = operation.transactionHash
854
+ if (operation.status === 'pending') {
855
+ if (
856
+ expiresAt !== undefined ||
857
+ submissionId !== undefined ||
858
+ transactionHash !== undefined
859
+ )
860
+ throw new InvalidOperationError({
861
+ reason: 'pending transactions cannot contain submission fields',
862
+ })
863
+ } else if (operation.status === 'submitting') {
864
+ assertInteger(expiresAt, 'expiresAt')
865
+ if (!Hash.validate(submissionId ?? ''))
866
+ throw new InvalidOperationError({ reason: 'submissionId is invalid' })
867
+ if (submissionId!.toLowerCase() === operation.hash.toLowerCase())
868
+ throw new InvalidOperationError({
869
+ reason: 'submissionId must differ from the operation hash',
870
+ })
871
+ if (transactionHash !== undefined)
872
+ throw new InvalidOperationError({
873
+ reason: 'submitting transactions cannot contain transactionHash',
874
+ })
875
+ } else if (operation.status === 'success') {
876
+ if (!Hash.validate(transactionHash ?? ''))
877
+ throw new InvalidOperationError({ reason: 'transactionHash is invalid' })
878
+ if (expiresAt !== undefined || submissionId !== undefined)
879
+ throw new InvalidOperationError({
880
+ reason: 'successful transactions cannot contain submission fields',
881
+ })
882
+ } else
883
+ throw new InvalidOperationError({ reason: 'invalid transaction status' })
884
+ if (
885
+ operation.status !== 'pending' &&
886
+ (operation.weight < operation.threshold || operation.signatureCount === 0)
887
+ )
888
+ throw new InvalidOperationError({
889
+ reason: 'submitted transactions must have quorum',
890
+ })
891
+
892
+ if (typeof operation.transaction !== 'string')
893
+ throw new InvalidOperationError({ reason: 'transaction is invalid' })
894
+ const feePayer = operation.transaction.startsWith(
895
+ TxEnvelopeTempo.feePayerMagic,
896
+ )
897
+ const transaction = TxEnvelopeTempo.deserialize(
898
+ operation.transaction as TxEnvelopeTempo.Serialized,
899
+ )
900
+ if (transaction.signature)
901
+ throw new InvalidOperationError({
902
+ reason: 'transaction must not contain an outer sender signature',
903
+ })
904
+ if (
905
+ transaction.from &&
906
+ transaction.from.toLowerCase() !== operation.account.toLowerCase()
907
+ )
908
+ throw new InvalidOperationError({
909
+ reason: 'transaction sender does not match the operation account',
910
+ })
911
+ assertOperationHash(operation, TxEnvelopeTempo.getSignPayload(transaction))
912
+ const normalized = TxEnvelopeTempo.serialize(
913
+ transaction,
914
+ feePayer
915
+ ? transaction.from
916
+ ? { format: 'feePayer', sender: transaction.from }
917
+ : { format: 'feePayer' }
918
+ : {},
919
+ )
920
+ if (normalized.toLowerCase() !== operation.transaction.toLowerCase())
921
+ throw new InvalidOperationError({
922
+ reason: 'transaction is not canonically serialized',
923
+ })
924
+ }
925
+
926
+ /**
927
+ * Validates a key authorization operation and its serialized payload.
928
+ *
929
+ * @internal
930
+ */
931
+ function assertKeyAuthorization(
932
+ operation: KeyAuthorizationOperation,
933
+ config: MultisigConfig.Config,
934
+ ): void {
935
+ const transactionFields = operation as KeyAuthorizationOperation & {
936
+ expiresAt?: unknown
937
+ submissionId?: unknown
938
+ transaction?: unknown
939
+ transactionHash?: unknown
940
+ }
941
+ if (
942
+ transactionFields.expiresAt !== undefined ||
943
+ transactionFields.submissionId !== undefined ||
944
+ transactionFields.transaction !== undefined ||
945
+ transactionFields.transactionHash !== undefined
946
+ )
947
+ throw new InvalidOperationError({
948
+ reason: 'key authorization operations cannot contain transaction fields',
949
+ })
950
+ if (operation.status !== 'pending' && operation.status !== 'success')
951
+ throw new InvalidOperationError({
952
+ reason: 'invalid key authorization status',
953
+ })
954
+ if (
955
+ operation.status === 'success' &&
956
+ (operation.weight < operation.threshold || operation.signatureCount === 0)
957
+ )
958
+ throw new InvalidOperationError({
959
+ reason: 'successful key authorizations must have quorum',
960
+ })
961
+ if (operation.status === 'pending' && operation.weight >= operation.threshold)
962
+ throw new InvalidOperationError({
963
+ reason: 'pending key authorizations cannot have quorum',
964
+ })
965
+ if (typeof operation.keyAuthorization !== 'string')
966
+ throw new InvalidOperationError({ reason: 'keyAuthorization is invalid' })
967
+ const authorization = KeyAuthorization_.deserialize(
968
+ operation.keyAuthorization,
969
+ )
970
+ if (
971
+ !authorization.account ||
972
+ authorization.account.toLowerCase() !== operation.account.toLowerCase()
973
+ )
974
+ throw new InvalidOperationError({
975
+ reason: 'key authorization account does not match the operation account',
976
+ })
977
+ const signature = authorization.signature
978
+ if (operation.status === 'pending' && signature)
979
+ throw new InvalidOperationError({
980
+ reason: 'pending key authorizations must be unsigned',
981
+ })
982
+ if (operation.status === 'success') {
983
+ if (signature?.type !== 'multisig')
984
+ throw new InvalidOperationError({
985
+ reason: 'successful key authorizations require a multisig signature',
986
+ })
987
+ if (signature.account.toLowerCase() !== operation.account.toLowerCase())
988
+ throw new InvalidOperationError({
989
+ reason: 'key authorization signature account does not match',
990
+ })
991
+ if (signature.signatures.length !== operation.signatureCount)
992
+ throw new InvalidOperationError({
993
+ reason: 'key authorization signatureCount does not match its signature',
994
+ })
995
+ assertSelectedApprovals(operation, signature.signatures, authorization)
996
+ if (!sameConfig(signature.config, config))
997
+ throw new InvalidOperationError({
998
+ reason: 'key authorization config does not match',
999
+ })
1000
+ }
1001
+ assertOperationHash(
1002
+ operation,
1003
+ KeyAuthorization_.getSignPayload(authorization),
1004
+ )
1005
+ if (
1006
+ KeyAuthorization_.serialize(authorization).toLowerCase() !==
1007
+ operation.keyAuthorization.toLowerCase()
1008
+ )
1009
+ throw new InvalidOperationError({
1010
+ reason: 'keyAuthorization is not canonically serialized',
1011
+ })
1012
+ }
1013
+
1014
+ /**
1015
+ * Validates a retained signature in the root owner's approval context.
1016
+ *
1017
+ * @internal
1018
+ */
1019
+ function assertApproval(
1020
+ account: Address.Address,
1021
+ serialized: SignatureEnvelope.Serialized,
1022
+ config: MultisigConfig.Config,
1023
+ ): SignatureEnvelope.SignatureEnvelope {
1024
+ const approval = SignatureEnvelope.deserialize(serialized)
1025
+ SignatureEnvelope.assert({
1026
+ account,
1027
+ config,
1028
+ signatures: [approval],
1029
+ type: 'multisig',
1030
+ })
1031
+ if (
1032
+ SignatureEnvelope.serialize(approval).toLowerCase() !==
1033
+ serialized.toLowerCase()
1034
+ )
1035
+ throw new InvalidOperationError({ reason: 'approval is not canonical' })
1036
+ return approval
1037
+ }
1038
+
1039
+ /**
1040
+ * Checks that selected transaction approvals are retained by the operation.
1041
+ *
1042
+ * @internal
1043
+ */
1044
+ function assertRetainedApprovals(
1045
+ operation: TransactionOperation,
1046
+ selected: readonly SignatureEnvelope.SignatureEnvelope[],
1047
+ ): void {
1048
+ const retained = operation.approvals.map((approval) =>
1049
+ SignatureEnvelope.deserialize(approval),
1050
+ )
1051
+ for (const approval of selected) {
1052
+ const index = retained.findIndex((candidate) =>
1053
+ includesApproval(candidate, approval),
1054
+ )
1055
+ if (index === -1)
1056
+ throw new InvalidOperationError({
1057
+ reason: 'transaction signature is not a retained approval',
1058
+ })
1059
+ retained.splice(index, 1)
1060
+ }
1061
+ }
1062
+
1063
+ /**
1064
+ * Checks that a successful key authorization uses retained approvals in canonical order.
1065
+ *
1066
+ * @internal
1067
+ */
1068
+ function assertSelectedApprovals(
1069
+ operation: KeyAuthorizationOperation,
1070
+ selected: readonly SignatureEnvelope.SignatureEnvelope[],
1071
+ authorization: KeyAuthorization_.KeyAuthorization,
1072
+ ): void {
1073
+ const retained = operation.approvals.map((approval) =>
1074
+ SignatureEnvelope.deserialize(approval),
1075
+ )
1076
+ for (const approval of selected) {
1077
+ const index = retained.findIndex((candidate) =>
1078
+ includesApproval(candidate, approval),
1079
+ )
1080
+ if (index === -1)
1081
+ throw new InvalidOperationError({
1082
+ reason: 'key authorization signature is not a retained approval',
1083
+ })
1084
+ retained.splice(index, 1)
1085
+ }
1086
+
1087
+ const digest = MultisigConfig.getSignPayload({
1088
+ account: operation.account,
1089
+ config: operation.config,
1090
+ payload: KeyAuthorization_.getSignPayload(authorization),
1091
+ })
1092
+ const addresses = selected.map((signature) =>
1093
+ SignatureEnvelope.extractAddress({ payload: digest, signature }),
1094
+ )
1095
+ for (let index = 1; index < addresses.length; index++) {
1096
+ const previous = Hex.toBigInt(addresses[index - 1]!)
1097
+ const current = Hex.toBigInt(addresses[index]!)
1098
+ if (previous === current)
1099
+ throw new InvalidOperationError({
1100
+ reason: 'key authorization contains duplicate owner approvals',
1101
+ })
1102
+ if (previous > current)
1103
+ throw new InvalidOperationError({
1104
+ reason: 'key authorization approvals are not canonically ordered',
1105
+ })
1106
+ }
1107
+ }
1108
+
1109
+ /**
1110
+ * Checks whether a selected approval is contained in a retained approval tree.
1111
+ *
1112
+ * @internal
1113
+ */
1114
+ function includesApproval(
1115
+ retained: SignatureEnvelope.SignatureEnvelope,
1116
+ selected: SignatureEnvelope.SignatureEnvelope,
1117
+ ): boolean {
1118
+ if (retained.type !== 'multisig' || selected.type !== 'multisig')
1119
+ return (
1120
+ SignatureEnvelope.serialize(retained).toLowerCase() ===
1121
+ SignatureEnvelope.serialize(selected).toLowerCase()
1122
+ )
1123
+ if (retained.account.toLowerCase() !== selected.account.toLowerCase())
1124
+ return false
1125
+ if (!sameConfig(retained.config, selected.config)) return false
1126
+ let index = 0
1127
+ for (const approval of selected.signatures) {
1128
+ while (
1129
+ index < retained.signatures.length &&
1130
+ !includesApproval(retained.signatures[index]!, approval)
1131
+ )
1132
+ index++
1133
+ if (index === retained.signatures.length) return false
1134
+ index++
1135
+ }
1136
+ return true
1137
+ }
1138
+
1139
+ /**
1140
+ * Checks whether exactly `signatureCount` retained owners can produce `weight`.
1141
+ *
1142
+ * @internal
1143
+ */
1144
+ function isWeightReachable(
1145
+ weights: readonly number[],
1146
+ signatureCount: number,
1147
+ weight: number,
1148
+ ): boolean {
1149
+ const reachable = Array.from(
1150
+ { length: signatureCount + 1 },
1151
+ () => new Set<number>(),
1152
+ )
1153
+ reachable[0]!.add(0)
1154
+ for (const ownerWeight of weights)
1155
+ for (let count = signatureCount; count > 0; count--)
1156
+ for (const current of reachable[count - 1]!)
1157
+ reachable[count]!.add(current + ownerWeight)
1158
+ return reachable[signatureCount]!.has(weight)
1159
+ }
1160
+
1161
+ /**
1162
+ * Validates the deterministic operation hash.
1163
+ *
1164
+ * @internal
1165
+ */
1166
+ function assertOperationHash(operation: Operation, payload: Hex.Hex): void {
1167
+ const hash = MultisigConfig.getSignPayload({
1168
+ account: operation.account,
1169
+ config: operation.config,
1170
+ payload,
1171
+ })
1172
+ if (hash.toLowerCase() !== operation.hash.toLowerCase())
1173
+ throw new InvalidOperationError({
1174
+ reason: 'hash does not match the operation payload',
1175
+ })
1176
+ }
1177
+
1178
+ /**
1179
+ * Validates a nonnegative safe integer field.
1180
+ *
1181
+ * @internal
1182
+ */
1183
+ function assertInteger(value: unknown, field: string): asserts value is number {
1184
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
1185
+ throw new InvalidOperationError({
1186
+ reason: `${field} must be a nonnegative safe integer`,
1187
+ })
1188
+ }
1189
+
1190
+ /**
1191
+ * Compares normalized multisig configurations.
1192
+ *
1193
+ * @internal
1194
+ */
1195
+ function sameConfig(
1196
+ a: MultisigConfig.Config,
1197
+ b: MultisigConfig.Config,
1198
+ ): boolean {
1199
+ const configA = MultisigConfig.from(a)
1200
+ const configB = MultisigConfig.from(b)
1201
+ return (
1202
+ Hex.isEqual(
1203
+ configA.salt ?? MultisigConfig.zeroSalt,
1204
+ configB.salt ?? MultisigConfig.zeroSalt,
1205
+ ) &&
1206
+ configA.threshold === configB.threshold &&
1207
+ configA.version === configB.version &&
1208
+ configA.owners.length === configB.owners.length &&
1209
+ configA.owners.every((owner, index) => {
1210
+ const other = configB.owners[index]!
1211
+ return (
1212
+ Address.isEqual(owner.owner, other.owner) &&
1213
+ owner.weight === other.weight
1214
+ )
1215
+ })
1216
+ )
1217
+ }
1218
+
1219
+ /** Thrown when a multisig owner approval is invalid. */
1220
+ export class InvalidApprovalError extends Errors.BaseError<Error | undefined> {
1221
+ override readonly name = 'MultisigOperation.InvalidApprovalError'
1222
+
1223
+ /**
1224
+ * Creates an invalid multisig approval error.
1225
+ *
1226
+ * @example
1227
+ * ```ts twoslash
1228
+ * import { MultisigOperation } from 'ox/tempo'
1229
+ *
1230
+ * throw new MultisigOperation.InvalidApprovalError({
1231
+ * reason: 'signature is from a non-owner'
1232
+ * })
1233
+ * ```
1234
+ *
1235
+ * @param options - Error options.
1236
+ */
1237
+ constructor(options: InvalidApprovalError.Options = {}) {
1238
+ super(
1239
+ options.reason
1240
+ ? `Invalid multisig approval: ${options.reason}.`
1241
+ : 'Invalid multisig approval.',
1242
+ { cause: options.cause as Error | undefined },
1243
+ )
1244
+ }
1245
+ }
1246
+
1247
+ export declare namespace InvalidApprovalError {
1248
+ /** Error construction options. */
1249
+ export type Options = {
1250
+ /** Underlying error. */
1251
+ cause?: unknown | undefined
1252
+ /** Validation failure. */
1253
+ reason?: string | undefined
1254
+ }
1255
+ }
1256
+
1257
+ /** Thrown when a multisig operation is malformed or internally inconsistent. */
1258
+ export class InvalidOperationError extends Errors.BaseError<Error | undefined> {
1259
+ override readonly name = 'MultisigOperation.InvalidOperationError'
1260
+
1261
+ /**
1262
+ * Creates an invalid multisig operation error.
1263
+ *
1264
+ * @example
1265
+ * ```ts twoslash
1266
+ * import { MultisigOperation } from 'ox/tempo'
1267
+ *
1268
+ * throw new MultisigOperation.InvalidOperationError({
1269
+ * reason: 'hash does not match the operation payload'
1270
+ * })
1271
+ * ```
1272
+ *
1273
+ * @param options - Error options.
1274
+ */
1275
+ constructor(options: InvalidOperationError.Options = {}) {
1276
+ super(
1277
+ options.reason
1278
+ ? `Invalid multisig operation: ${options.reason}.`
1279
+ : 'Invalid multisig operation.',
1280
+ { cause: options.cause as Error | undefined },
1281
+ )
1282
+ }
1283
+ }
1284
+
1285
+ export declare namespace InvalidOperationError {
1286
+ /** Error construction options. */
1287
+ export type Options = {
1288
+ /** Underlying error. */
1289
+ cause?: unknown | undefined
1290
+ /** Validation failure. */
1291
+ reason?: string | undefined
1292
+ }
1293
+ }