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,839 @@
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
+ * Derives the deterministic hash for a multisig operation.
11
+ *
12
+ * @example
13
+ * ```ts twoslash
14
+ * // @noErrors
15
+ * import { MultisigOperation } from 'ox/tempo'
16
+ *
17
+ * const hash = MultisigOperation.getHash({
18
+ * account,
19
+ * config,
20
+ * transaction,
21
+ * type: 'transaction'
22
+ * })
23
+ * ```
24
+ *
25
+ * @param options - Operation payload and multisig identity.
26
+ * @returns The operation hash signed by each owner.
27
+ */
28
+ export function getHash(options) {
29
+ const { account, config } = options;
30
+ const payload = options.type === 'transaction'
31
+ ? TxEnvelopeTempo.getSignPayload(TxEnvelopeTempo.deserialize(options.transaction))
32
+ : KeyAuthorization_.getSignPayload(KeyAuthorization_.deserialize(options.keyAuthorization));
33
+ return MultisigConfig.getSignPayload({
34
+ account,
35
+ config,
36
+ payload,
37
+ });
38
+ }
39
+ /**
40
+ * Validates, deduplicates, and selects owner approvals for an operation.
41
+ *
42
+ * The function retains one canonical approval per owner. It selects the
43
+ * smallest deterministic quorum by owner weight, then orders the selected
44
+ * approvals by owner address for serialization.
45
+ *
46
+ * @example
47
+ * ```ts twoslash
48
+ * // @noErrors
49
+ * import { MultisigOperation } from 'ox/tempo'
50
+ *
51
+ * const selection = await MultisigOperation.selectApprovals({
52
+ * account,
53
+ * approvals,
54
+ * config,
55
+ * hash
56
+ * })
57
+ * ```
58
+ *
59
+ * @param options - Approval selection parameters.
60
+ * @returns The retained approvals and deterministic quorum selection.
61
+ */
62
+ export async function selectApprovals(options) {
63
+ const { account, approvals, hash } = options;
64
+ if (!Address.validate(account) || Hex.toBigInt(account) === 0n)
65
+ throw new InvalidApprovalError({ reason: 'account is invalid' });
66
+ if (!Hash.validate(hash))
67
+ throw new InvalidApprovalError({ reason: 'hash is invalid' });
68
+ const config = MultisigConfig.from(options.config);
69
+ if (config.version === 0n &&
70
+ !Address.isEqual(MultisigConfig.getAddress(config), account))
71
+ throw new InvalidApprovalError({
72
+ reason: 'initial config does not derive the root multisig account',
73
+ });
74
+ return selectApprovals_internal({
75
+ account,
76
+ approvals,
77
+ config,
78
+ hash,
79
+ }, [account.toLowerCase()]);
80
+ }
81
+ /**
82
+ * Serializes a key authorization with selected multisig owner approvals.
83
+ *
84
+ * @example
85
+ * ```ts twoslash
86
+ * // @noErrors
87
+ * import { MultisigOperation } from 'ox/tempo'
88
+ *
89
+ * const authorization =
90
+ * MultisigOperation.serializeKeyAuthorization(
91
+ * keyAuthorization,
92
+ * {
93
+ * account,
94
+ * approvals: selection.selectedApprovals,
95
+ * config
96
+ * }
97
+ * )
98
+ * ```
99
+ *
100
+ * @param keyAuthorization - Canonical serialized unsigned key authorization.
101
+ * @param options - Multisig account, config, and selected approvals.
102
+ * @returns The signed serialized key authorization.
103
+ */
104
+ export function serializeKeyAuthorization(keyAuthorization, options) {
105
+ const authorization = KeyAuthorization_.deserialize(keyAuthorization);
106
+ if (authorization.signature)
107
+ throw new InvalidOperationError({
108
+ reason: 'keyAuthorization must not contain a signature',
109
+ });
110
+ if (KeyAuthorization_.serialize(authorization).toLowerCase() !==
111
+ keyAuthorization.toLowerCase())
112
+ throw new InvalidOperationError({
113
+ reason: 'keyAuthorization is not canonically serialized',
114
+ });
115
+ const config = MultisigConfig.from(options.config);
116
+ const signatures = SignatureEnvelope.sortMultisigApprovals({
117
+ account: options.account,
118
+ config,
119
+ payload: KeyAuthorization_.getSignPayload(authorization),
120
+ signatures: options.approvals.map((approval) => SignatureEnvelope.deserialize(approval)),
121
+ });
122
+ return KeyAuthorization_.serialize(KeyAuthorization_.from(authorization, {
123
+ signature: SignatureEnvelope.from({
124
+ account: options.account,
125
+ config,
126
+ signatures,
127
+ }),
128
+ }));
129
+ }
130
+ /**
131
+ * Serializes a multisig transaction operation with selected owner approvals.
132
+ *
133
+ * @example
134
+ * ```ts twoslash
135
+ * // @noErrors
136
+ * import { MultisigOperation } from 'ox/tempo'
137
+ *
138
+ * const transaction = MultisigOperation.serializeTransaction(
139
+ * operation,
140
+ * {
141
+ * approvals: selection.selectedApprovals
142
+ * }
143
+ * )
144
+ * ```
145
+ *
146
+ * @param operation - Multisig transaction operation.
147
+ * @param options - Transaction serialization options.
148
+ * @returns The signed serialized Tempo transaction.
149
+ */
150
+ export function serializeTransaction(operation, options) {
151
+ const value = from(operation);
152
+ const envelope = TxEnvelopeTempo.deserialize(value.transaction);
153
+ const approvals = options.approvals.map((approval) => SignatureEnvelope.from(approval));
154
+ assertRetainedApprovals(value, approvals);
155
+ const signatures = SignatureEnvelope.sortMultisigApprovals({
156
+ account: value.account,
157
+ config: value.config,
158
+ payload: TxEnvelopeTempo.getSignPayload(envelope),
159
+ signatures: approvals,
160
+ });
161
+ const signature = SignatureEnvelope.from({
162
+ account: value.account,
163
+ config: value.config,
164
+ signatures,
165
+ });
166
+ return TxEnvelopeTempo.serialize(envelope, value.transaction.startsWith(TxEnvelopeTempo.feePayerMagic)
167
+ ? {
168
+ format: 'feePayer',
169
+ sender: envelope.from,
170
+ signature,
171
+ }
172
+ : { signature });
173
+ }
174
+ /**
175
+ * Validates and normalizes a multisig operation.
176
+ *
177
+ * @example
178
+ * ```ts twoslash
179
+ * // @noErrors
180
+ * import { MultisigOperation } from 'ox/tempo'
181
+ *
182
+ * const operation = MultisigOperation.from(value)
183
+ * ```
184
+ *
185
+ * @param operation - Multisig operation.
186
+ * @returns The validated operation.
187
+ */
188
+ export function from(operation) {
189
+ try {
190
+ const config = MultisigConfig.from(operation.config);
191
+ if (typeof config.threshold !== 'number' ||
192
+ config.owners.some((owner) => typeof owner.weight !== 'number'))
193
+ throw new InvalidOperationError({
194
+ reason: 'config threshold and owner weights must be numbers',
195
+ });
196
+ const value = { ...operation, config };
197
+ assertBase(value, config);
198
+ if (value.type === 'transaction')
199
+ assertTransaction(value);
200
+ else if (value.type === 'keyAuthorization')
201
+ assertKeyAuthorization(value, config);
202
+ else
203
+ throw new InvalidOperationError({ reason: 'unknown operation type' });
204
+ return value;
205
+ }
206
+ catch (cause) {
207
+ if (cause instanceof InvalidOperationError)
208
+ throw cause;
209
+ throw new InvalidOperationError({ cause });
210
+ }
211
+ }
212
+ /**
213
+ * Converts a JSON-RPC multisig operation to its domain representation.
214
+ *
215
+ * @example
216
+ * ```ts twoslash
217
+ * // @noErrors
218
+ * import { MultisigOperation } from 'ox/tempo'
219
+ *
220
+ * const operation = MultisigOperation.fromRpc(value)
221
+ * ```
222
+ *
223
+ * @param operation - JSON-RPC multisig operation.
224
+ * @returns The validated operation.
225
+ */
226
+ export function fromRpc(operation) {
227
+ try {
228
+ const version = operation.config?.version;
229
+ if (typeof version !== 'string' || !Hex.validate(version))
230
+ throw new InvalidOperationError({
231
+ reason: 'config.version must be a hexadecimal quantity',
232
+ });
233
+ const version_ = Hex.toBigInt(version);
234
+ if (Hex.fromNumber(version_) !== version)
235
+ throw new InvalidOperationError({
236
+ reason: 'config.version must use canonical quantity encoding',
237
+ });
238
+ return from({
239
+ ...operation,
240
+ config: MultisigConfig.fromRpc(operation.config),
241
+ });
242
+ }
243
+ catch (cause) {
244
+ if (cause instanceof InvalidOperationError)
245
+ throw cause;
246
+ throw new InvalidOperationError({ cause });
247
+ }
248
+ }
249
+ /**
250
+ * Converts a multisig operation to its JSON-RPC representation.
251
+ *
252
+ * @example
253
+ * ```ts twoslash
254
+ * // @noErrors
255
+ * import { MultisigOperation } from 'ox/tempo'
256
+ *
257
+ * const operationRpc = MultisigOperation.toRpc(operation)
258
+ * ```
259
+ *
260
+ * @param operation - Multisig operation.
261
+ * @returns The JSON-RPC operation.
262
+ */
263
+ export function toRpc(operation) {
264
+ const value = from(operation);
265
+ return {
266
+ ...value,
267
+ config: MultisigConfig.toRpc(value.config),
268
+ };
269
+ }
270
+ /**
271
+ * Validates and selects approvals recursively.
272
+ *
273
+ * @internal
274
+ */
275
+ async function selectApprovals_internal(options, path) {
276
+ const owners = new Map(options.config.owners.map((owner) => [
277
+ owner.owner.toLowerCase(),
278
+ { address: owner.owner, weight: Number(owner.weight) },
279
+ ]));
280
+ const groups = new Map();
281
+ for (const serialized of options.approvals) {
282
+ const signature = SignatureEnvelope.from(serialized);
283
+ if (signature.type === 'keychain')
284
+ throw new InvalidApprovalError({
285
+ reason: 'keychain signatures cannot approve a multisig operation',
286
+ });
287
+ const address = signature.type === 'multisig'
288
+ ? signature.account
289
+ : SignatureEnvelope.extractAddress({
290
+ payload: options.hash,
291
+ signature,
292
+ });
293
+ const owner = owners.get(address.toLowerCase());
294
+ if (!owner)
295
+ throw new InvalidApprovalError({
296
+ reason: `signature is from non-owner ${address}`,
297
+ });
298
+ const key = address.toLowerCase();
299
+ const group = groups.get(key);
300
+ if (group)
301
+ group.signatures.push(signature);
302
+ else
303
+ groups.set(key, {
304
+ address: owner.address,
305
+ signatures: [signature],
306
+ weight: owner.weight,
307
+ });
308
+ }
309
+ const valid = [];
310
+ const retained = [];
311
+ for (const group of groups.values()) {
312
+ const nested = group.signatures.filter((signature) => signature.type === 'multisig');
313
+ if (nested.length > 0) {
314
+ if (nested.length !== group.signatures.length)
315
+ throw new InvalidApprovalError({
316
+ reason: `owner ${group.address} has conflicting signature types`,
317
+ });
318
+ if (path.length >= MultisigConfig.maxNestingDepth ||
319
+ path.includes(group.address.toLowerCase()))
320
+ throw new InvalidApprovalError({
321
+ reason: `nested multisig owner ${group.address} is invalid`,
322
+ });
323
+ const config = MultisigConfig.from(nested[0].config);
324
+ if (nested.some((signature) => !sameConfig(signature.config, config)))
325
+ throw new InvalidApprovalError({
326
+ reason: `nested multisig owner ${group.address} has conflicting config witnesses`,
327
+ });
328
+ if (config.version === 0n &&
329
+ !Address.isEqual(MultisigConfig.getAddress(config), group.address))
330
+ throw new InvalidApprovalError({
331
+ reason: `initial config does not derive nested multisig owner ${group.address}`,
332
+ });
333
+ const selected = await selectApprovals_internal({
334
+ account: group.address,
335
+ approvals: nested.flatMap((signature) => signature.signatures.map((approval) => SignatureEnvelope.serialize(approval))),
336
+ config,
337
+ hash: MultisigConfig.getSignPayload({
338
+ account: group.address,
339
+ config,
340
+ payload: options.hash,
341
+ }),
342
+ }, [...path, group.address.toLowerCase()]);
343
+ retained.push({
344
+ address: group.address,
345
+ signature: SignatureEnvelope.serialize(SignatureEnvelope.from({
346
+ account: group.address,
347
+ config,
348
+ signatures: selected.approvals.map((approval) => SignatureEnvelope.from(approval)),
349
+ })),
350
+ });
351
+ if (selected.weight >= selected.threshold)
352
+ valid.push({
353
+ address: group.address,
354
+ signature: SignatureEnvelope.serialize(SignatureEnvelope.from({
355
+ account: group.address,
356
+ config,
357
+ signatures: selected.selectedApprovals.map((approval) => SignatureEnvelope.from(approval)),
358
+ })),
359
+ weight: group.weight,
360
+ });
361
+ continue;
362
+ }
363
+ const signatures = group.signatures.map((signature) => {
364
+ if (!SignatureEnvelope.verify(signature, {
365
+ address: group.address,
366
+ payload: options.hash,
367
+ }))
368
+ throw new InvalidApprovalError({
369
+ reason: `signature from owner ${group.address} is invalid`,
370
+ });
371
+ return SignatureEnvelope.serialize(signature);
372
+ });
373
+ const signature = signatures.sort(compareHex)[0];
374
+ valid.push({
375
+ address: group.address,
376
+ signature,
377
+ weight: group.weight,
378
+ });
379
+ retained.push({ address: group.address, signature });
380
+ }
381
+ const ranked = valid.sort((a, b) => b.weight - a.weight || compareApprovalAddress(a, b));
382
+ const selected = [];
383
+ let weight = 0;
384
+ for (const approval of ranked.slice(0, MultisigConfig.maxSignatures)) {
385
+ if (weight >= Number(options.config.threshold))
386
+ break;
387
+ selected.push(approval);
388
+ weight += approval.weight;
389
+ }
390
+ selected.sort(compareApprovalAddress);
391
+ return {
392
+ approvals: retained
393
+ .sort(compareApprovalAddress)
394
+ .map((approval) => approval.signature),
395
+ selectedApprovals: selected.map((approval) => approval.signature),
396
+ signatureCount: selected.length,
397
+ threshold: Number(options.config.threshold),
398
+ weight,
399
+ };
400
+ }
401
+ /**
402
+ * Orders approval records by owner address.
403
+ *
404
+ * @internal
405
+ */
406
+ function compareApprovalAddress(a, b) {
407
+ const addressA = Hex.toBigInt(a.address);
408
+ const addressB = Hex.toBigInt(b.address);
409
+ return addressA < addressB ? -1 : addressA > addressB ? 1 : 0;
410
+ }
411
+ /**
412
+ * Orders hexadecimal data bytewise.
413
+ *
414
+ * @internal
415
+ */
416
+ function compareHex(a, b) {
417
+ const hexA = a.toLowerCase();
418
+ const hexB = b.toLowerCase();
419
+ return hexA < hexB ? -1 : hexA > hexB ? 1 : 0;
420
+ }
421
+ /**
422
+ * Validates fields shared by every operation.
423
+ *
424
+ * @internal
425
+ */
426
+ function assertBase(operation, config) {
427
+ if (!Address.validate(operation.account))
428
+ throw new InvalidOperationError({ reason: 'account is invalid' });
429
+ if (Hex.toBigInt(operation.account) === 0n)
430
+ throw new InvalidOperationError({ reason: 'account cannot be zero' });
431
+ if (!Hash.validate(operation.hash))
432
+ throw new InvalidOperationError({ reason: 'hash is invalid' });
433
+ assertInteger(operation.createdAt, 'createdAt');
434
+ assertInteger(operation.updatedAt, 'updatedAt');
435
+ if (operation.updatedAt < operation.createdAt)
436
+ throw new InvalidOperationError({
437
+ reason: 'updatedAt cannot precede createdAt',
438
+ });
439
+ assertInteger(operation.signatureCount, 'signatureCount');
440
+ assertInteger(operation.threshold, 'threshold');
441
+ assertInteger(operation.weight, 'weight');
442
+ if (operation.threshold !== Number(config.threshold))
443
+ throw new InvalidOperationError({
444
+ reason: 'threshold must equal config.threshold',
445
+ });
446
+ if (operation.weight > 0xff)
447
+ throw new InvalidOperationError({ reason: 'weight exceeds u8 max' });
448
+ if (operation.signatureCount > MultisigConfig.maxSignatures)
449
+ throw new InvalidOperationError({ reason: 'too many selected signatures' });
450
+ if (!Array.isArray(operation.approvals))
451
+ throw new InvalidOperationError({ reason: 'approvals must be an array' });
452
+ if (operation.approvals.length > config.owners.length)
453
+ throw new InvalidOperationError({ reason: 'too many retained approvals' });
454
+ if (operation.signatureCount > operation.approvals.length)
455
+ throw new InvalidOperationError({
456
+ reason: 'signatureCount exceeds retained approvals',
457
+ });
458
+ if ((operation.signatureCount === 0) !== (operation.weight === 0))
459
+ throw new InvalidOperationError({
460
+ reason: 'signatureCount and weight must both be zero or nonzero',
461
+ });
462
+ const owners = new Map(config.owners.map((owner) => [
463
+ owner.owner.toLowerCase(),
464
+ Number(owner.weight),
465
+ ]));
466
+ const approvalWeights = [];
467
+ const seen = new Set();
468
+ for (const approval of operation.approvals) {
469
+ if (typeof approval !== 'string' ||
470
+ !Hex.validate(approval, { strict: true }))
471
+ throw new InvalidOperationError({ reason: 'approval is invalid' });
472
+ const signature = assertApproval(operation.account, approval, config);
473
+ const address = SignatureEnvelope.extractAddress({
474
+ payload: operation.hash,
475
+ signature,
476
+ });
477
+ const key = address.toLowerCase();
478
+ const weight = owners.get(key);
479
+ if (weight === undefined)
480
+ throw new InvalidOperationError({
481
+ reason: 'approval is from a non-owner',
482
+ });
483
+ if (seen.has(key))
484
+ throw new InvalidOperationError({
485
+ reason: operation.type === 'keyAuthorization'
486
+ ? 'key authorization contains duplicate owner approvals'
487
+ : 'duplicate owner approval',
488
+ });
489
+ seen.add(key);
490
+ approvalWeights.push(weight);
491
+ }
492
+ if (!isWeightReachable(approvalWeights, operation.signatureCount, operation.weight))
493
+ throw new InvalidOperationError({
494
+ reason: 'weight is not reachable by signatureCount retained owner approvals',
495
+ });
496
+ if (config.version === 0n &&
497
+ !Address.isEqual(MultisigConfig.getAddress(config), operation.account))
498
+ throw new InvalidOperationError({
499
+ reason: 'initial config does not derive the operation account',
500
+ });
501
+ }
502
+ /**
503
+ * Validates a transaction operation and its state-specific fields.
504
+ *
505
+ * @internal
506
+ */
507
+ function assertTransaction(operation) {
508
+ if ('keyAuthorization' in operation &&
509
+ operation.keyAuthorization !== undefined)
510
+ throw new InvalidOperationError({
511
+ reason: 'transaction operations cannot contain keyAuthorization',
512
+ });
513
+ const expiresAt = operation.expiresAt;
514
+ const submissionId = operation.submissionId;
515
+ const transactionHash = operation.transactionHash;
516
+ if (operation.status === 'pending') {
517
+ if (expiresAt !== undefined ||
518
+ submissionId !== undefined ||
519
+ transactionHash !== undefined)
520
+ throw new InvalidOperationError({
521
+ reason: 'pending transactions cannot contain submission fields',
522
+ });
523
+ }
524
+ else if (operation.status === 'submitting') {
525
+ assertInteger(expiresAt, 'expiresAt');
526
+ if (!Hash.validate(submissionId ?? ''))
527
+ throw new InvalidOperationError({ reason: 'submissionId is invalid' });
528
+ if (submissionId.toLowerCase() === operation.hash.toLowerCase())
529
+ throw new InvalidOperationError({
530
+ reason: 'submissionId must differ from the operation hash',
531
+ });
532
+ if (transactionHash !== undefined)
533
+ throw new InvalidOperationError({
534
+ reason: 'submitting transactions cannot contain transactionHash',
535
+ });
536
+ }
537
+ else if (operation.status === 'success') {
538
+ if (!Hash.validate(transactionHash ?? ''))
539
+ throw new InvalidOperationError({ reason: 'transactionHash is invalid' });
540
+ if (expiresAt !== undefined || submissionId !== undefined)
541
+ throw new InvalidOperationError({
542
+ reason: 'successful transactions cannot contain submission fields',
543
+ });
544
+ }
545
+ else
546
+ throw new InvalidOperationError({ reason: 'invalid transaction status' });
547
+ if (operation.status !== 'pending' &&
548
+ (operation.weight < operation.threshold || operation.signatureCount === 0))
549
+ throw new InvalidOperationError({
550
+ reason: 'submitted transactions must have quorum',
551
+ });
552
+ if (typeof operation.transaction !== 'string')
553
+ throw new InvalidOperationError({ reason: 'transaction is invalid' });
554
+ const feePayer = operation.transaction.startsWith(TxEnvelopeTempo.feePayerMagic);
555
+ const transaction = TxEnvelopeTempo.deserialize(operation.transaction);
556
+ if (transaction.signature)
557
+ throw new InvalidOperationError({
558
+ reason: 'transaction must not contain an outer sender signature',
559
+ });
560
+ if (transaction.from &&
561
+ transaction.from.toLowerCase() !== operation.account.toLowerCase())
562
+ throw new InvalidOperationError({
563
+ reason: 'transaction sender does not match the operation account',
564
+ });
565
+ assertOperationHash(operation, TxEnvelopeTempo.getSignPayload(transaction));
566
+ const normalized = TxEnvelopeTempo.serialize(transaction, feePayer
567
+ ? transaction.from
568
+ ? { format: 'feePayer', sender: transaction.from }
569
+ : { format: 'feePayer' }
570
+ : {});
571
+ if (normalized.toLowerCase() !== operation.transaction.toLowerCase())
572
+ throw new InvalidOperationError({
573
+ reason: 'transaction is not canonically serialized',
574
+ });
575
+ }
576
+ /**
577
+ * Validates a key authorization operation and its serialized payload.
578
+ *
579
+ * @internal
580
+ */
581
+ function assertKeyAuthorization(operation, config) {
582
+ const transactionFields = operation;
583
+ if (transactionFields.expiresAt !== undefined ||
584
+ transactionFields.submissionId !== undefined ||
585
+ transactionFields.transaction !== undefined ||
586
+ transactionFields.transactionHash !== undefined)
587
+ throw new InvalidOperationError({
588
+ reason: 'key authorization operations cannot contain transaction fields',
589
+ });
590
+ if (operation.status !== 'pending' && operation.status !== 'success')
591
+ throw new InvalidOperationError({
592
+ reason: 'invalid key authorization status',
593
+ });
594
+ if (operation.status === 'success' &&
595
+ (operation.weight < operation.threshold || operation.signatureCount === 0))
596
+ throw new InvalidOperationError({
597
+ reason: 'successful key authorizations must have quorum',
598
+ });
599
+ if (operation.status === 'pending' && operation.weight >= operation.threshold)
600
+ throw new InvalidOperationError({
601
+ reason: 'pending key authorizations cannot have quorum',
602
+ });
603
+ if (typeof operation.keyAuthorization !== 'string')
604
+ throw new InvalidOperationError({ reason: 'keyAuthorization is invalid' });
605
+ const authorization = KeyAuthorization_.deserialize(operation.keyAuthorization);
606
+ if (!authorization.account ||
607
+ authorization.account.toLowerCase() !== operation.account.toLowerCase())
608
+ throw new InvalidOperationError({
609
+ reason: 'key authorization account does not match the operation account',
610
+ });
611
+ const signature = authorization.signature;
612
+ if (operation.status === 'pending' && signature)
613
+ throw new InvalidOperationError({
614
+ reason: 'pending key authorizations must be unsigned',
615
+ });
616
+ if (operation.status === 'success') {
617
+ if (signature?.type !== 'multisig')
618
+ throw new InvalidOperationError({
619
+ reason: 'successful key authorizations require a multisig signature',
620
+ });
621
+ if (signature.account.toLowerCase() !== operation.account.toLowerCase())
622
+ throw new InvalidOperationError({
623
+ reason: 'key authorization signature account does not match',
624
+ });
625
+ if (signature.signatures.length !== operation.signatureCount)
626
+ throw new InvalidOperationError({
627
+ reason: 'key authorization signatureCount does not match its signature',
628
+ });
629
+ assertSelectedApprovals(operation, signature.signatures, authorization);
630
+ if (!sameConfig(signature.config, config))
631
+ throw new InvalidOperationError({
632
+ reason: 'key authorization config does not match',
633
+ });
634
+ }
635
+ assertOperationHash(operation, KeyAuthorization_.getSignPayload(authorization));
636
+ if (KeyAuthorization_.serialize(authorization).toLowerCase() !==
637
+ operation.keyAuthorization.toLowerCase())
638
+ throw new InvalidOperationError({
639
+ reason: 'keyAuthorization is not canonically serialized',
640
+ });
641
+ }
642
+ /**
643
+ * Validates a retained signature in the root owner's approval context.
644
+ *
645
+ * @internal
646
+ */
647
+ function assertApproval(account, serialized, config) {
648
+ const approval = SignatureEnvelope.deserialize(serialized);
649
+ SignatureEnvelope.assert({
650
+ account,
651
+ config,
652
+ signatures: [approval],
653
+ type: 'multisig',
654
+ });
655
+ if (SignatureEnvelope.serialize(approval).toLowerCase() !==
656
+ serialized.toLowerCase())
657
+ throw new InvalidOperationError({ reason: 'approval is not canonical' });
658
+ return approval;
659
+ }
660
+ /**
661
+ * Checks that selected transaction approvals are retained by the operation.
662
+ *
663
+ * @internal
664
+ */
665
+ function assertRetainedApprovals(operation, selected) {
666
+ const retained = operation.approvals.map((approval) => SignatureEnvelope.deserialize(approval));
667
+ for (const approval of selected) {
668
+ const index = retained.findIndex((candidate) => includesApproval(candidate, approval));
669
+ if (index === -1)
670
+ throw new InvalidOperationError({
671
+ reason: 'transaction signature is not a retained approval',
672
+ });
673
+ retained.splice(index, 1);
674
+ }
675
+ }
676
+ /**
677
+ * Checks that a successful key authorization uses retained approvals in canonical order.
678
+ *
679
+ * @internal
680
+ */
681
+ function assertSelectedApprovals(operation, selected, authorization) {
682
+ const retained = operation.approvals.map((approval) => SignatureEnvelope.deserialize(approval));
683
+ for (const approval of selected) {
684
+ const index = retained.findIndex((candidate) => includesApproval(candidate, approval));
685
+ if (index === -1)
686
+ throw new InvalidOperationError({
687
+ reason: 'key authorization signature is not a retained approval',
688
+ });
689
+ retained.splice(index, 1);
690
+ }
691
+ const digest = MultisigConfig.getSignPayload({
692
+ account: operation.account,
693
+ config: operation.config,
694
+ payload: KeyAuthorization_.getSignPayload(authorization),
695
+ });
696
+ const addresses = selected.map((signature) => SignatureEnvelope.extractAddress({ payload: digest, signature }));
697
+ for (let index = 1; index < addresses.length; index++) {
698
+ const previous = Hex.toBigInt(addresses[index - 1]);
699
+ const current = Hex.toBigInt(addresses[index]);
700
+ if (previous === current)
701
+ throw new InvalidOperationError({
702
+ reason: 'key authorization contains duplicate owner approvals',
703
+ });
704
+ if (previous > current)
705
+ throw new InvalidOperationError({
706
+ reason: 'key authorization approvals are not canonically ordered',
707
+ });
708
+ }
709
+ }
710
+ /**
711
+ * Checks whether a selected approval is contained in a retained approval tree.
712
+ *
713
+ * @internal
714
+ */
715
+ function includesApproval(retained, selected) {
716
+ if (retained.type !== 'multisig' || selected.type !== 'multisig')
717
+ return (SignatureEnvelope.serialize(retained).toLowerCase() ===
718
+ SignatureEnvelope.serialize(selected).toLowerCase());
719
+ if (retained.account.toLowerCase() !== selected.account.toLowerCase())
720
+ return false;
721
+ if (!sameConfig(retained.config, selected.config))
722
+ return false;
723
+ let index = 0;
724
+ for (const approval of selected.signatures) {
725
+ while (index < retained.signatures.length &&
726
+ !includesApproval(retained.signatures[index], approval))
727
+ index++;
728
+ if (index === retained.signatures.length)
729
+ return false;
730
+ index++;
731
+ }
732
+ return true;
733
+ }
734
+ /**
735
+ * Checks whether exactly `signatureCount` retained owners can produce `weight`.
736
+ *
737
+ * @internal
738
+ */
739
+ function isWeightReachable(weights, signatureCount, weight) {
740
+ const reachable = Array.from({ length: signatureCount + 1 }, () => new Set());
741
+ reachable[0].add(0);
742
+ for (const ownerWeight of weights)
743
+ for (let count = signatureCount; count > 0; count--)
744
+ for (const current of reachable[count - 1])
745
+ reachable[count].add(current + ownerWeight);
746
+ return reachable[signatureCount].has(weight);
747
+ }
748
+ /**
749
+ * Validates the deterministic operation hash.
750
+ *
751
+ * @internal
752
+ */
753
+ function assertOperationHash(operation, payload) {
754
+ const hash = MultisigConfig.getSignPayload({
755
+ account: operation.account,
756
+ config: operation.config,
757
+ payload,
758
+ });
759
+ if (hash.toLowerCase() !== operation.hash.toLowerCase())
760
+ throw new InvalidOperationError({
761
+ reason: 'hash does not match the operation payload',
762
+ });
763
+ }
764
+ /**
765
+ * Validates a nonnegative safe integer field.
766
+ *
767
+ * @internal
768
+ */
769
+ function assertInteger(value, field) {
770
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
771
+ throw new InvalidOperationError({
772
+ reason: `${field} must be a nonnegative safe integer`,
773
+ });
774
+ }
775
+ /**
776
+ * Compares normalized multisig configurations.
777
+ *
778
+ * @internal
779
+ */
780
+ function sameConfig(a, b) {
781
+ const configA = MultisigConfig.from(a);
782
+ const configB = MultisigConfig.from(b);
783
+ return (Hex.isEqual(configA.salt ?? MultisigConfig.zeroSalt, configB.salt ?? MultisigConfig.zeroSalt) &&
784
+ configA.threshold === configB.threshold &&
785
+ configA.version === configB.version &&
786
+ configA.owners.length === configB.owners.length &&
787
+ configA.owners.every((owner, index) => {
788
+ const other = configB.owners[index];
789
+ return (Address.isEqual(owner.owner, other.owner) &&
790
+ owner.weight === other.weight);
791
+ }));
792
+ }
793
+ /** Thrown when a multisig owner approval is invalid. */
794
+ export class InvalidApprovalError extends Errors.BaseError {
795
+ name = 'MultisigOperation.InvalidApprovalError';
796
+ /**
797
+ * Creates an invalid multisig approval error.
798
+ *
799
+ * @example
800
+ * ```ts twoslash
801
+ * import { MultisigOperation } from 'ox/tempo'
802
+ *
803
+ * throw new MultisigOperation.InvalidApprovalError({
804
+ * reason: 'signature is from a non-owner'
805
+ * })
806
+ * ```
807
+ *
808
+ * @param options - Error options.
809
+ */
810
+ constructor(options = {}) {
811
+ super(options.reason
812
+ ? `Invalid multisig approval: ${options.reason}.`
813
+ : 'Invalid multisig approval.', { cause: options.cause });
814
+ }
815
+ }
816
+ /** Thrown when a multisig operation is malformed or internally inconsistent. */
817
+ export class InvalidOperationError extends Errors.BaseError {
818
+ name = 'MultisigOperation.InvalidOperationError';
819
+ /**
820
+ * Creates an invalid multisig operation error.
821
+ *
822
+ * @example
823
+ * ```ts twoslash
824
+ * import { MultisigOperation } from 'ox/tempo'
825
+ *
826
+ * throw new MultisigOperation.InvalidOperationError({
827
+ * reason: 'hash does not match the operation payload'
828
+ * })
829
+ * ```
830
+ *
831
+ * @param options - Error options.
832
+ */
833
+ constructor(options = {}) {
834
+ super(options.reason
835
+ ? `Invalid multisig operation: ${options.reason}.`
836
+ : 'Invalid multisig operation.', { cause: options.cause });
837
+ }
838
+ }
839
+ //# sourceMappingURL=MultisigOperation.js.map