ox 0.14.37 → 0.14.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/_cjs/tempo/MultisigConfig.js +71 -15
- package/_cjs/tempo/MultisigConfig.js.map +1 -1
- package/_cjs/tempo/MultisigOperation.js +241 -19
- package/_cjs/tempo/MultisigOperation.js.map +1 -1
- package/_cjs/tempo/SignatureEnvelope.js +71 -108
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigConfig.js +140 -71
- package/_esm/tempo/MultisigConfig.js.map +1 -1
- package/_esm/tempo/MultisigOperation.js +331 -19
- package/_esm/tempo/MultisigOperation.js.map +1 -1
- package/_esm/tempo/SignatureEnvelope.js +84 -143
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/index.js +2 -2
- package/_esm/version.js +1 -1
- package/_types/tempo/KeyAuthorization.d.ts +1 -1
- package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
- package/_types/tempo/MultisigConfig.d.ts +137 -73
- package/_types/tempo/MultisigConfig.d.ts.map +1 -1
- package/_types/tempo/MultisigOperation.d.ts +169 -1
- package/_types/tempo/MultisigOperation.d.ts.map +1 -1
- package/_types/tempo/SignatureEnvelope.d.ts +81 -75
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +2 -2
- package/_types/version.d.ts +1 -1
- package/package.json +6 -1
- package/tempo/KeyAuthorization.test-d.ts +12 -0
- package/tempo/KeyAuthorization.test.ts +54 -0
- package/tempo/KeyAuthorization.ts +1 -1
- package/tempo/MultisigConfig.test-d/package.json +6 -0
- package/tempo/MultisigConfig.test-d.ts +52 -0
- package/tempo/MultisigConfig.test.ts +291 -263
- package/tempo/MultisigConfig.ts +250 -95
- package/tempo/MultisigOperation.test-d.ts +25 -0
- package/tempo/MultisigOperation.test.ts +754 -22
- package/tempo/MultisigOperation.ts +548 -17
- package/tempo/SignatureEnvelope.test-d.ts +57 -40
- package/tempo/SignatureEnvelope.test.ts +523 -480
- package/tempo/SignatureEnvelope.ts +250 -257
- package/tempo/index.ts +2 -2
- package/tempo/multisig.e2e.test.ts +113 -86
- package/version.ts +1 -1
|
@@ -14,7 +14,7 @@ export type Base<quantity = bigint> = {
|
|
|
14
14
|
/** Every retained serialized owner approval. */
|
|
15
15
|
approvals: readonly Hex.Hex[]
|
|
16
16
|
/** Root configuration used to verify approvals. */
|
|
17
|
-
config: MultisigConfig.Config
|
|
17
|
+
config: MultisigConfig.Config<quantity>
|
|
18
18
|
/** Root configuration version. */
|
|
19
19
|
configVersion: quantity
|
|
20
20
|
/** Unix creation time in milliseconds. */
|
|
@@ -76,6 +76,250 @@ export type Rpc = Operation<Hex.Hex>
|
|
|
76
76
|
/** Maximum supported multisig configuration version. */
|
|
77
77
|
const maxConfigVersion = 2n ** 64n - 1n
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Derives the deterministic hash for a multisig operation.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts twoslash
|
|
84
|
+
* // @noErrors
|
|
85
|
+
* import { MultisigOperation } from 'ox/tempo'
|
|
86
|
+
*
|
|
87
|
+
* const hash = MultisigOperation.getHash({
|
|
88
|
+
* account,
|
|
89
|
+
* configVersion: 1n,
|
|
90
|
+
* transaction,
|
|
91
|
+
* type: 'transaction',
|
|
92
|
+
* })
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* @param options - Operation payload and multisig identity.
|
|
96
|
+
* @returns The operation hash signed by each owner.
|
|
97
|
+
*/
|
|
98
|
+
export function getHash(options: getHash.Options): Hex.Hex {
|
|
99
|
+
const { account, configVersion } = options
|
|
100
|
+
const payload =
|
|
101
|
+
options.type === 'transaction'
|
|
102
|
+
? TxEnvelopeTempo.getSignPayload(
|
|
103
|
+
TxEnvelopeTempo.deserialize(
|
|
104
|
+
options.transaction as TxEnvelopeTempo.Serialized,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
: KeyAuthorization_.getSignPayload(
|
|
108
|
+
KeyAuthorization_.deserialize(options.keyAuthorization),
|
|
109
|
+
)
|
|
110
|
+
return MultisigConfig.getSignPayload({
|
|
111
|
+
account,
|
|
112
|
+
config: { version: configVersion },
|
|
113
|
+
payload,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export declare namespace getHash {
|
|
118
|
+
/** Parameters for `getHash`. */
|
|
119
|
+
export type Options = {
|
|
120
|
+
/** Root multisig account. */
|
|
121
|
+
account: Address.Address
|
|
122
|
+
/** Root configuration version. */
|
|
123
|
+
configVersion: bigint
|
|
124
|
+
} & (
|
|
125
|
+
| {
|
|
126
|
+
/** Canonical serialized key authorization. */
|
|
127
|
+
keyAuthorization: Hex.Hex
|
|
128
|
+
/** Operation kind. */
|
|
129
|
+
type: 'keyAuthorization'
|
|
130
|
+
}
|
|
131
|
+
| {
|
|
132
|
+
/** Canonical serialized Tempo envelope without its outer sender signature. */
|
|
133
|
+
transaction: Hex.Hex
|
|
134
|
+
/** Operation kind. */
|
|
135
|
+
type: 'transaction'
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
/** Error type for `getHash`. */
|
|
140
|
+
export type ErrorType =
|
|
141
|
+
| KeyAuthorization_.deserialize.ErrorType
|
|
142
|
+
| KeyAuthorization_.getSignPayload.ErrorType
|
|
143
|
+
| MultisigConfig.getSignPayload.ErrorType
|
|
144
|
+
| TxEnvelopeTempo.deserialize.ErrorType
|
|
145
|
+
| TxEnvelopeTempo.getSignPayload.ErrorType
|
|
146
|
+
| Errors.GlobalErrorType
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validates, deduplicates, and selects owner approvals for an operation.
|
|
151
|
+
*
|
|
152
|
+
* The function retains one canonical approval per owner. It selects the
|
|
153
|
+
* smallest deterministic quorum by owner weight, then orders the selected
|
|
154
|
+
* approvals by owner address for serialization.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts twoslash
|
|
158
|
+
* // @noErrors
|
|
159
|
+
* import { MultisigOperation } from 'ox/tempo'
|
|
160
|
+
*
|
|
161
|
+
* const selection = await MultisigOperation.selectApprovals({
|
|
162
|
+
* account,
|
|
163
|
+
* approvals,
|
|
164
|
+
* config,
|
|
165
|
+
* hash,
|
|
166
|
+
* resolveConfig,
|
|
167
|
+
* })
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @param options - Approval selection parameters.
|
|
171
|
+
* @returns The retained approvals and deterministic quorum selection.
|
|
172
|
+
*/
|
|
173
|
+
export async function selectApprovals(
|
|
174
|
+
options: selectApprovals.Options,
|
|
175
|
+
): Promise<selectApprovals.ReturnValue> {
|
|
176
|
+
const { account, approvals, hash, resolveConfig } = options
|
|
177
|
+
if (!Address.validate(account) || Hex.toBigInt(account) === 0n)
|
|
178
|
+
throw new InvalidApprovalError({ reason: 'account is invalid' })
|
|
179
|
+
if (!Hash.validate(hash))
|
|
180
|
+
throw new InvalidApprovalError({ reason: 'hash is invalid' })
|
|
181
|
+
return selectApprovals_internal(
|
|
182
|
+
{
|
|
183
|
+
account,
|
|
184
|
+
approvals,
|
|
185
|
+
config: MultisigConfig.from(options.config),
|
|
186
|
+
hash,
|
|
187
|
+
resolveConfig,
|
|
188
|
+
},
|
|
189
|
+
[account.toLowerCase()],
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export declare namespace selectApprovals {
|
|
194
|
+
/** Parameters for `selectApprovals`. */
|
|
195
|
+
export type Options = {
|
|
196
|
+
/** Root multisig account. */
|
|
197
|
+
account: Address.Address
|
|
198
|
+
/** Serialized primitive or nested owner approvals. */
|
|
199
|
+
approvals: readonly SignatureEnvelope.Serialized[]
|
|
200
|
+
/** Current root multisig configuration. */
|
|
201
|
+
config: MultisigConfig.Config
|
|
202
|
+
/** Deterministic operation hash approved by root owners. */
|
|
203
|
+
hash: Hex.Hex
|
|
204
|
+
/** Resolves the current configuration of a nested multisig owner. */
|
|
205
|
+
resolveConfig?: ResolveConfig | undefined
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Resolves an initialized nested multisig configuration. */
|
|
209
|
+
export type ResolveConfig = (
|
|
210
|
+
options: ResolveConfigOptions,
|
|
211
|
+
) => ResolvedConfig | Promise<ResolvedConfig>
|
|
212
|
+
|
|
213
|
+
/** Nested multisig configuration lookup parameters. */
|
|
214
|
+
export type ResolveConfigOptions = {
|
|
215
|
+
/** Nested multisig account. */
|
|
216
|
+
account: Address.Address
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Resolved nested multisig configuration. */
|
|
220
|
+
export type ResolvedConfig = {
|
|
221
|
+
/** Current nested multisig configuration. */
|
|
222
|
+
config: MultisigConfig.Config
|
|
223
|
+
/** Current nested multisig configuration version. */
|
|
224
|
+
version: bigint
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Result of validating and selecting approvals. */
|
|
228
|
+
export type ReturnValue = {
|
|
229
|
+
/** Every retained approval, ordered by owner address. */
|
|
230
|
+
approvals: readonly SignatureEnvelope.Serialized[]
|
|
231
|
+
/** Number of approvals selected for quorum evaluation. */
|
|
232
|
+
signatureCount: number
|
|
233
|
+
/** Approvals selected for serialization, ordered by owner address. */
|
|
234
|
+
selectedApprovals: readonly SignatureEnvelope.Serialized[]
|
|
235
|
+
/** Required owner weight. */
|
|
236
|
+
threshold: number
|
|
237
|
+
/** Owner weight reached by the selected approvals. */
|
|
238
|
+
weight: number
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Error type for `selectApprovals`. */
|
|
242
|
+
export type ErrorType =
|
|
243
|
+
| InvalidApprovalError
|
|
244
|
+
| MultisigConfig.assert.ErrorType
|
|
245
|
+
| MultisigConfig.getSignPayload.ErrorType
|
|
246
|
+
| SignatureEnvelope.CoercionError
|
|
247
|
+
| SignatureEnvelope.extractAddress.ErrorType
|
|
248
|
+
| SignatureEnvelope.serialize.ErrorType
|
|
249
|
+
| SignatureEnvelope.VerificationError
|
|
250
|
+
| Errors.GlobalErrorType
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Serializes a multisig transaction operation with selected owner approvals.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts twoslash
|
|
258
|
+
* // @noErrors
|
|
259
|
+
* import { MultisigOperation } from 'ox/tempo'
|
|
260
|
+
*
|
|
261
|
+
* const transaction = MultisigOperation.serializeTransaction(operation, {
|
|
262
|
+
* approvals: selection.selectedApprovals,
|
|
263
|
+
* })
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
266
|
+
* @param operation - Multisig transaction operation.
|
|
267
|
+
* @param options - Transaction serialization options.
|
|
268
|
+
* @returns The signed serialized Tempo transaction.
|
|
269
|
+
*/
|
|
270
|
+
export function serializeTransaction(
|
|
271
|
+
operation: TransactionOperation,
|
|
272
|
+
options: serializeTransaction.Options,
|
|
273
|
+
): TxEnvelopeTempo.Serialized {
|
|
274
|
+
const value = from(operation)
|
|
275
|
+
const envelope = TxEnvelopeTempo.deserialize(
|
|
276
|
+
value.transaction as TxEnvelopeTempo.Serialized,
|
|
277
|
+
)
|
|
278
|
+
const approvals = options.approvals.map((approval) =>
|
|
279
|
+
SignatureEnvelope.from(approval),
|
|
280
|
+
)
|
|
281
|
+
assertRetainedApprovals(value, approvals)
|
|
282
|
+
const signatures = SignatureEnvelope.sortMultisigApprovals({
|
|
283
|
+
account: value.account,
|
|
284
|
+
config: value.config,
|
|
285
|
+
payload: TxEnvelopeTempo.getSignPayload(envelope),
|
|
286
|
+
signatures: approvals,
|
|
287
|
+
})
|
|
288
|
+
const signature = SignatureEnvelope.from({
|
|
289
|
+
account: value.account,
|
|
290
|
+
config: value.config,
|
|
291
|
+
signatures,
|
|
292
|
+
})
|
|
293
|
+
return TxEnvelopeTempo.serialize(
|
|
294
|
+
envelope,
|
|
295
|
+
value.transaction.startsWith(TxEnvelopeTempo.feePayerMagic)
|
|
296
|
+
? {
|
|
297
|
+
format: 'feePayer',
|
|
298
|
+
sender: envelope.from,
|
|
299
|
+
signature,
|
|
300
|
+
}
|
|
301
|
+
: { signature },
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export declare namespace serializeTransaction {
|
|
306
|
+
/** Options for `serializeTransaction`. */
|
|
307
|
+
export type Options = {
|
|
308
|
+
/** Selected retained approvals to attach to the transaction. */
|
|
309
|
+
approvals: readonly SignatureEnvelope.Serialized[]
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Error type for `serializeTransaction`. */
|
|
313
|
+
export type ErrorType =
|
|
314
|
+
| from.ErrorType
|
|
315
|
+
| InvalidOperationError
|
|
316
|
+
| SignatureEnvelope.sortMultisigApprovals.ErrorType
|
|
317
|
+
| TxEnvelopeTempo.deserialize.ErrorType
|
|
318
|
+
| TxEnvelopeTempo.getSignPayload.ErrorType
|
|
319
|
+
| TxEnvelopeTempo.serialize.ErrorType
|
|
320
|
+
| Errors.GlobalErrorType
|
|
321
|
+
}
|
|
322
|
+
|
|
79
323
|
/**
|
|
80
324
|
* Validates and normalizes a multisig operation.
|
|
81
325
|
*
|
|
@@ -94,7 +338,10 @@ export function from<const operation extends Operation>(
|
|
|
94
338
|
operation: operation,
|
|
95
339
|
): from.ReturnValue<operation> {
|
|
96
340
|
try {
|
|
97
|
-
const config = MultisigConfig.from(
|
|
341
|
+
const config = MultisigConfig.from({
|
|
342
|
+
...operation.config,
|
|
343
|
+
version: operation.configVersion,
|
|
344
|
+
})
|
|
98
345
|
if (
|
|
99
346
|
typeof config.threshold !== 'number' ||
|
|
100
347
|
config.owners.some((owner) => typeof owner.weight !== 'number')
|
|
@@ -102,12 +349,13 @@ export function from<const operation extends Operation>(
|
|
|
102
349
|
throw new InvalidOperationError({
|
|
103
350
|
reason: 'config threshold and owner weights must be numbers',
|
|
104
351
|
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
352
|
+
const value = { ...operation, config } as Operation
|
|
353
|
+
assertBase(value, config)
|
|
354
|
+
if (value.type === 'transaction') assertTransaction(value)
|
|
355
|
+
else if (value.type === 'keyAuthorization')
|
|
356
|
+
assertKeyAuthorization(value, config)
|
|
109
357
|
else throw new InvalidOperationError({ reason: 'unknown operation type' })
|
|
110
|
-
return
|
|
358
|
+
return value as never
|
|
111
359
|
} catch (cause) {
|
|
112
360
|
if (cause instanceof InvalidOperationError) throw cause
|
|
113
361
|
throw new InvalidOperationError({ cause })
|
|
@@ -155,7 +403,11 @@ export function fromRpc<const operation extends Rpc>(
|
|
|
155
403
|
throw new InvalidOperationError({
|
|
156
404
|
reason: 'configVersion must use canonical quantity encoding',
|
|
157
405
|
})
|
|
158
|
-
return from({
|
|
406
|
+
return from({
|
|
407
|
+
...operation,
|
|
408
|
+
config: MultisigConfig.fromRpc(operation.config),
|
|
409
|
+
configVersion,
|
|
410
|
+
} as Operation) as never
|
|
159
411
|
} catch (cause) {
|
|
160
412
|
if (cause instanceof InvalidOperationError) throw cause
|
|
161
413
|
throw new InvalidOperationError({ cause })
|
|
@@ -193,6 +445,7 @@ export function toRpc<const operation extends Operation>(
|
|
|
193
445
|
const value = from(operation)
|
|
194
446
|
return {
|
|
195
447
|
...value,
|
|
448
|
+
config: MultisigConfig.toRpc(value.config),
|
|
196
449
|
configVersion: Hex.fromNumber(value.configVersion),
|
|
197
450
|
} as never
|
|
198
451
|
}
|
|
@@ -208,6 +461,222 @@ export declare namespace toRpc {
|
|
|
208
461
|
export type ErrorType = from.ErrorType | Hex.fromNumber.ErrorType
|
|
209
462
|
}
|
|
210
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Validates and selects approvals recursively.
|
|
466
|
+
*
|
|
467
|
+
* @internal
|
|
468
|
+
*/
|
|
469
|
+
async function selectApprovals_internal(
|
|
470
|
+
options: selectApprovals.Options,
|
|
471
|
+
path: readonly string[],
|
|
472
|
+
): Promise<selectApprovals.ReturnValue> {
|
|
473
|
+
const owners = new Map(
|
|
474
|
+
options.config.owners.map((owner) => [
|
|
475
|
+
owner.owner.toLowerCase(),
|
|
476
|
+
{ address: owner.owner, weight: Number(owner.weight) },
|
|
477
|
+
]),
|
|
478
|
+
)
|
|
479
|
+
const groups = new Map<string, ApprovalGroup>()
|
|
480
|
+
for (const serialized of options.approvals) {
|
|
481
|
+
const signature = SignatureEnvelope.from(serialized)
|
|
482
|
+
if (signature.type === 'keychain')
|
|
483
|
+
throw new InvalidApprovalError({
|
|
484
|
+
reason: 'keychain signatures cannot approve a multisig operation',
|
|
485
|
+
})
|
|
486
|
+
const address =
|
|
487
|
+
signature.type === 'multisig'
|
|
488
|
+
? signature.account
|
|
489
|
+
: SignatureEnvelope.extractAddress({
|
|
490
|
+
payload: options.hash,
|
|
491
|
+
signature,
|
|
492
|
+
})
|
|
493
|
+
const owner = owners.get(address.toLowerCase())
|
|
494
|
+
if (!owner)
|
|
495
|
+
throw new InvalidApprovalError({
|
|
496
|
+
reason: `signature is from non-owner ${address}`,
|
|
497
|
+
})
|
|
498
|
+
const key = address.toLowerCase()
|
|
499
|
+
const group = groups.get(key)
|
|
500
|
+
if (group) group.signatures.push(signature)
|
|
501
|
+
else
|
|
502
|
+
groups.set(key, {
|
|
503
|
+
address: owner.address,
|
|
504
|
+
signatures: [signature],
|
|
505
|
+
weight: owner.weight,
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const valid: SelectedApproval[] = []
|
|
510
|
+
const retained: RetainedApproval[] = []
|
|
511
|
+
for (const group of groups.values()) {
|
|
512
|
+
const nested = group.signatures.filter(
|
|
513
|
+
(signature) => signature.type === 'multisig',
|
|
514
|
+
)
|
|
515
|
+
if (nested.length > 0) {
|
|
516
|
+
if (nested.length !== group.signatures.length)
|
|
517
|
+
throw new InvalidApprovalError({
|
|
518
|
+
reason: `owner ${group.address} has conflicting signature types`,
|
|
519
|
+
})
|
|
520
|
+
if (
|
|
521
|
+
path.length >= MultisigConfig.maxNestingDepth ||
|
|
522
|
+
path.includes(group.address.toLowerCase())
|
|
523
|
+
)
|
|
524
|
+
throw new InvalidApprovalError({
|
|
525
|
+
reason: `nested multisig owner ${group.address} is invalid`,
|
|
526
|
+
})
|
|
527
|
+
if (!options.resolveConfig)
|
|
528
|
+
throw new InvalidApprovalError({
|
|
529
|
+
reason: `nested multisig owner ${group.address} requires a config resolver`,
|
|
530
|
+
})
|
|
531
|
+
const resolved = await options.resolveConfig({ account: group.address })
|
|
532
|
+
const config = MultisigConfig.from({
|
|
533
|
+
...resolved.config,
|
|
534
|
+
version: resolved.version,
|
|
535
|
+
})
|
|
536
|
+
const selected = await selectApprovals_internal(
|
|
537
|
+
{
|
|
538
|
+
account: group.address,
|
|
539
|
+
approvals: nested.flatMap((signature) =>
|
|
540
|
+
signature.signatures.map((approval) =>
|
|
541
|
+
SignatureEnvelope.serialize(approval),
|
|
542
|
+
),
|
|
543
|
+
),
|
|
544
|
+
config,
|
|
545
|
+
hash: MultisigConfig.getSignPayload({
|
|
546
|
+
account: group.address,
|
|
547
|
+
config,
|
|
548
|
+
payload: options.hash,
|
|
549
|
+
}),
|
|
550
|
+
resolveConfig: options.resolveConfig,
|
|
551
|
+
},
|
|
552
|
+
[...path, group.address.toLowerCase()],
|
|
553
|
+
)
|
|
554
|
+
retained.push({
|
|
555
|
+
address: group.address,
|
|
556
|
+
signature: SignatureEnvelope.serialize(
|
|
557
|
+
SignatureEnvelope.from({
|
|
558
|
+
account: group.address,
|
|
559
|
+
config,
|
|
560
|
+
signatures: selected.approvals.map((approval) =>
|
|
561
|
+
SignatureEnvelope.from(approval),
|
|
562
|
+
),
|
|
563
|
+
}),
|
|
564
|
+
),
|
|
565
|
+
})
|
|
566
|
+
if (selected.weight >= selected.threshold)
|
|
567
|
+
valid.push({
|
|
568
|
+
address: group.address,
|
|
569
|
+
signature: SignatureEnvelope.serialize(
|
|
570
|
+
SignatureEnvelope.from({
|
|
571
|
+
account: group.address,
|
|
572
|
+
config,
|
|
573
|
+
signatures: selected.selectedApprovals.map((approval) =>
|
|
574
|
+
SignatureEnvelope.from(approval),
|
|
575
|
+
),
|
|
576
|
+
}),
|
|
577
|
+
),
|
|
578
|
+
weight: group.weight,
|
|
579
|
+
})
|
|
580
|
+
continue
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const signatures = group.signatures.map((signature) => {
|
|
584
|
+
if (
|
|
585
|
+
!SignatureEnvelope.verify(signature, {
|
|
586
|
+
address: group.address,
|
|
587
|
+
payload: options.hash,
|
|
588
|
+
})
|
|
589
|
+
)
|
|
590
|
+
throw new InvalidApprovalError({
|
|
591
|
+
reason: `signature from owner ${group.address} is invalid`,
|
|
592
|
+
})
|
|
593
|
+
return SignatureEnvelope.serialize(signature)
|
|
594
|
+
})
|
|
595
|
+
const signature = signatures.sort(compareHex)[0]!
|
|
596
|
+
valid.push({
|
|
597
|
+
address: group.address,
|
|
598
|
+
signature,
|
|
599
|
+
weight: group.weight,
|
|
600
|
+
})
|
|
601
|
+
retained.push({ address: group.address, signature })
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const ranked = valid.sort(
|
|
605
|
+
(a, b) => b.weight - a.weight || compareApprovalAddress(a, b),
|
|
606
|
+
)
|
|
607
|
+
const selected: typeof ranked = []
|
|
608
|
+
let weight = 0
|
|
609
|
+
for (const approval of ranked.slice(0, MultisigConfig.maxSignatures)) {
|
|
610
|
+
if (weight >= Number(options.config.threshold)) break
|
|
611
|
+
selected.push(approval)
|
|
612
|
+
weight += approval.weight
|
|
613
|
+
}
|
|
614
|
+
selected.sort(compareApprovalAddress)
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
approvals: retained
|
|
618
|
+
.sort(compareApprovalAddress)
|
|
619
|
+
.map((approval) => approval.signature),
|
|
620
|
+
selectedApprovals: selected.map((approval) => approval.signature),
|
|
621
|
+
signatureCount: selected.length,
|
|
622
|
+
threshold: Number(options.config.threshold),
|
|
623
|
+
weight,
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Approval selected for quorum evaluation. @internal */
|
|
628
|
+
type SelectedApproval = {
|
|
629
|
+
/** Configured owner address. */
|
|
630
|
+
address: Address.Address
|
|
631
|
+
/** Serialized owner signature. */
|
|
632
|
+
signature: SignatureEnvelope.Serialized
|
|
633
|
+
/** Configured owner weight. */
|
|
634
|
+
weight: number
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Approvals submitted for one configured owner. @internal */
|
|
638
|
+
type ApprovalGroup = {
|
|
639
|
+
/** Configured owner address. */
|
|
640
|
+
address: Address.Address
|
|
641
|
+
/** Submitted signatures that resolve to the owner. */
|
|
642
|
+
signatures: SignatureEnvelope.SignatureEnvelope[]
|
|
643
|
+
/** Configured owner weight. */
|
|
644
|
+
weight: number
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Approval retained in operation storage. @internal */
|
|
648
|
+
type RetainedApproval = {
|
|
649
|
+
/** Configured owner address. */
|
|
650
|
+
address: Address.Address
|
|
651
|
+
/** Serialized primitive or normalized nested approval. */
|
|
652
|
+
signature: SignatureEnvelope.Serialized
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Orders approval records by owner address.
|
|
657
|
+
*
|
|
658
|
+
* @internal
|
|
659
|
+
*/
|
|
660
|
+
function compareApprovalAddress(
|
|
661
|
+
a: SelectedApproval | RetainedApproval,
|
|
662
|
+
b: SelectedApproval | RetainedApproval,
|
|
663
|
+
) {
|
|
664
|
+
const addressA = Hex.toBigInt(a.address)
|
|
665
|
+
const addressB = Hex.toBigInt(b.address)
|
|
666
|
+
return addressA < addressB ? -1 : addressA > addressB ? 1 : 0
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Orders hexadecimal data bytewise.
|
|
671
|
+
*
|
|
672
|
+
* @internal
|
|
673
|
+
*/
|
|
674
|
+
function compareHex(a: Hex.Hex, b: Hex.Hex) {
|
|
675
|
+
const hexA = a.toLowerCase()
|
|
676
|
+
const hexB = b.toLowerCase()
|
|
677
|
+
return hexA < hexB ? -1 : hexA > hexB ? 1 : 0
|
|
678
|
+
}
|
|
679
|
+
|
|
211
680
|
/**
|
|
212
681
|
* Validates fields shared by every operation.
|
|
213
682
|
*
|
|
@@ -276,6 +745,7 @@ function assertBase(operation: Operation, config: MultisigConfig.Config): void {
|
|
|
276
745
|
const signature = assertApproval(
|
|
277
746
|
operation.account,
|
|
278
747
|
approval as SignatureEnvelope.Serialized,
|
|
748
|
+
config,
|
|
279
749
|
)
|
|
280
750
|
const address = SignatureEnvelope.extractAddress({
|
|
281
751
|
payload: operation.hash,
|
|
@@ -481,13 +951,9 @@ function assertKeyAuthorization(
|
|
|
481
951
|
reason: 'key authorization signatureCount does not match its signature',
|
|
482
952
|
})
|
|
483
953
|
assertSelectedApprovals(operation, signature.signatures, authorization)
|
|
484
|
-
if (
|
|
954
|
+
if (!sameConfig(signature.config, config))
|
|
485
955
|
throw new InvalidOperationError({
|
|
486
|
-
reason: 'key authorization
|
|
487
|
-
})
|
|
488
|
-
if (signature.init && !sameConfig(signature.init, config))
|
|
489
|
-
throw new InvalidOperationError({
|
|
490
|
-
reason: 'key authorization bootstrap config does not match',
|
|
956
|
+
reason: 'key authorization config does not match',
|
|
491
957
|
})
|
|
492
958
|
}
|
|
493
959
|
assertOperationHash(
|
|
@@ -511,10 +977,12 @@ function assertKeyAuthorization(
|
|
|
511
977
|
function assertApproval(
|
|
512
978
|
account: Address.Address,
|
|
513
979
|
serialized: SignatureEnvelope.Serialized,
|
|
980
|
+
config: MultisigConfig.Config,
|
|
514
981
|
): SignatureEnvelope.SignatureEnvelope {
|
|
515
982
|
const approval = SignatureEnvelope.deserialize(serialized)
|
|
516
983
|
SignatureEnvelope.assert({
|
|
517
984
|
account,
|
|
985
|
+
config,
|
|
518
986
|
signatures: [approval],
|
|
519
987
|
type: 'multisig',
|
|
520
988
|
})
|
|
@@ -526,6 +994,30 @@ function assertApproval(
|
|
|
526
994
|
return approval
|
|
527
995
|
}
|
|
528
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Checks that selected transaction approvals are retained by the operation.
|
|
999
|
+
*
|
|
1000
|
+
* @internal
|
|
1001
|
+
*/
|
|
1002
|
+
function assertRetainedApprovals(
|
|
1003
|
+
operation: TransactionOperation,
|
|
1004
|
+
selected: readonly SignatureEnvelope.SignatureEnvelope[],
|
|
1005
|
+
): void {
|
|
1006
|
+
const retained = operation.approvals.map((approval) =>
|
|
1007
|
+
SignatureEnvelope.deserialize(approval),
|
|
1008
|
+
)
|
|
1009
|
+
for (const approval of selected) {
|
|
1010
|
+
const index = retained.findIndex((candidate) =>
|
|
1011
|
+
includesApproval(candidate, approval),
|
|
1012
|
+
)
|
|
1013
|
+
if (index === -1)
|
|
1014
|
+
throw new InvalidOperationError({
|
|
1015
|
+
reason: 'transaction signature is not a retained approval',
|
|
1016
|
+
})
|
|
1017
|
+
retained.splice(index, 1)
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
529
1021
|
/**
|
|
530
1022
|
* Checks that a successful key authorization uses retained approvals in canonical order.
|
|
531
1023
|
*
|
|
@@ -552,8 +1044,8 @@ function assertSelectedApprovals(
|
|
|
552
1044
|
|
|
553
1045
|
const digest = MultisigConfig.getSignPayload({
|
|
554
1046
|
account: operation.account,
|
|
1047
|
+
config: operation.config,
|
|
555
1048
|
payload: KeyAuthorization_.getSignPayload(authorization),
|
|
556
|
-
version: operation.configVersion,
|
|
557
1049
|
})
|
|
558
1050
|
const addresses = selected.map((signature) =>
|
|
559
1051
|
SignatureEnvelope.extractAddress({ payload: digest, signature }),
|
|
@@ -588,7 +1080,7 @@ function includesApproval(
|
|
|
588
1080
|
)
|
|
589
1081
|
if (retained.account.toLowerCase() !== selected.account.toLowerCase())
|
|
590
1082
|
return false
|
|
591
|
-
|
|
1083
|
+
if (!sameConfig(retained.config, selected.config)) return false
|
|
592
1084
|
let index = 0
|
|
593
1085
|
for (const approval of selected.signatures) {
|
|
594
1086
|
while (
|
|
@@ -632,8 +1124,8 @@ function isWeightReachable(
|
|
|
632
1124
|
function assertOperationHash(operation: Operation, payload: Hex.Hex): void {
|
|
633
1125
|
const hash = MultisigConfig.getSignPayload({
|
|
634
1126
|
account: operation.account,
|
|
1127
|
+
config: operation.config,
|
|
635
1128
|
payload,
|
|
636
|
-
version: operation.configVersion,
|
|
637
1129
|
})
|
|
638
1130
|
if (hash.toLowerCase() !== operation.hash.toLowerCase())
|
|
639
1131
|
throw new InvalidOperationError({
|
|
@@ -670,6 +1162,7 @@ function sameConfig(
|
|
|
670
1162
|
configB.salt ?? MultisigConfig.zeroSalt,
|
|
671
1163
|
) &&
|
|
672
1164
|
configA.threshold === configB.threshold &&
|
|
1165
|
+
configA.version === configB.version &&
|
|
673
1166
|
configA.owners.length === configB.owners.length &&
|
|
674
1167
|
configA.owners.every((owner, index) => {
|
|
675
1168
|
const other = configB.owners[index]!
|
|
@@ -681,6 +1174,44 @@ function sameConfig(
|
|
|
681
1174
|
)
|
|
682
1175
|
}
|
|
683
1176
|
|
|
1177
|
+
/** Thrown when a multisig owner approval is invalid. */
|
|
1178
|
+
export class InvalidApprovalError extends Errors.BaseError<Error | undefined> {
|
|
1179
|
+
override readonly name = 'MultisigOperation.InvalidApprovalError'
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Creates an invalid multisig approval error.
|
|
1183
|
+
*
|
|
1184
|
+
* @example
|
|
1185
|
+
* ```ts twoslash
|
|
1186
|
+
* import { MultisigOperation } from 'ox/tempo'
|
|
1187
|
+
*
|
|
1188
|
+
* throw new MultisigOperation.InvalidApprovalError({
|
|
1189
|
+
* reason: 'signature is from a non-owner',
|
|
1190
|
+
* })
|
|
1191
|
+
* ```
|
|
1192
|
+
*
|
|
1193
|
+
* @param options - Error options.
|
|
1194
|
+
*/
|
|
1195
|
+
constructor(options: InvalidApprovalError.Options = {}) {
|
|
1196
|
+
super(
|
|
1197
|
+
options.reason
|
|
1198
|
+
? `Invalid multisig approval: ${options.reason}.`
|
|
1199
|
+
: 'Invalid multisig approval.',
|
|
1200
|
+
{ cause: options.cause as Error | undefined },
|
|
1201
|
+
)
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export declare namespace InvalidApprovalError {
|
|
1206
|
+
/** Error construction options. */
|
|
1207
|
+
export type Options = {
|
|
1208
|
+
/** Underlying error. */
|
|
1209
|
+
cause?: unknown | undefined
|
|
1210
|
+
/** Validation failure. */
|
|
1211
|
+
reason?: string | undefined
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
684
1215
|
/** Thrown when a multisig operation is malformed or internally inconsistent. */
|
|
685
1216
|
export class InvalidOperationError extends Errors.BaseError<Error | undefined> {
|
|
686
1217
|
override readonly name = 'MultisigOperation.InvalidOperationError'
|