ox 0.14.26 → 0.14.28
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 +12 -0
- package/_cjs/tempo/MultisigConfig.js +127 -0
- package/_cjs/tempo/MultisigConfig.js.map +1 -0
- package/_cjs/tempo/ReceivePolicyReceipt.js +80 -0
- package/_cjs/tempo/ReceivePolicyReceipt.js.map +1 -0
- package/_cjs/tempo/SignatureEnvelope.js +107 -6
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/index.js +3 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigConfig.js +312 -0
- package/_esm/tempo/MultisigConfig.js.map +1 -0
- package/_esm/tempo/ReceivePolicyReceipt.js +176 -0
- package/_esm/tempo/ReceivePolicyReceipt.js.map +1 -0
- package/_esm/tempo/SignatureEnvelope.js +170 -6
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/index.js +48 -0
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/tempo/MultisigConfig.d.ts +270 -0
- package/_types/tempo/MultisigConfig.d.ts.map +1 -0
- package/_types/tempo/ReceivePolicyReceipt.d.ts +168 -0
- package/_types/tempo/ReceivePolicyReceipt.d.ts.map +1 -0
- package/_types/tempo/SignatureEnvelope.d.ts +106 -6
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +48 -0
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/package.json +11 -1
- package/tempo/MultisigConfig/package.json +6 -0
- package/tempo/MultisigConfig.test.ts +227 -0
- package/tempo/MultisigConfig.ts +423 -0
- package/tempo/ReceivePolicyReceipt/package.json +6 -0
- package/tempo/ReceivePolicyReceipt.test.ts +198 -0
- package/tempo/ReceivePolicyReceipt.ts +263 -0
- package/tempo/SignatureEnvelope.test.ts +213 -2
- package/tempo/SignatureEnvelope.ts +257 -9
- package/tempo/e2e.test.ts +217 -0
- package/tempo/index.ts +48 -0
- package/version.ts +1 -1
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import * as Address from '../core/Address.js'
|
|
2
|
+
import type * as Bytes from '../core/Bytes.js'
|
|
3
|
+
import * as Errors from '../core/Errors.js'
|
|
4
|
+
import * as Hash from '../core/Hash.js'
|
|
5
|
+
import * as Hex from '../core/Hex.js'
|
|
6
|
+
import type { Compute } from '../core/internal/types.js'
|
|
7
|
+
|
|
8
|
+
/** Maximum number of owners allowed in a native multisig config. */
|
|
9
|
+
export const maxOwners = 10
|
|
10
|
+
|
|
11
|
+
/** Maximum encoded byte length for one primitive owner approval. */
|
|
12
|
+
export const maxOwnerSignatureBytes = 2049
|
|
13
|
+
|
|
14
|
+
/** Tempo signature type byte for native multisig signatures. */
|
|
15
|
+
export const signatureTypeByte = '0x05' as const
|
|
16
|
+
|
|
17
|
+
/** Zero 32-byte salt (the default when no salt is provided). */
|
|
18
|
+
export const zeroSalt = `0x${'00'.repeat(32)}` as const
|
|
19
|
+
|
|
20
|
+
/** Domain prefix for the native multisig account address derivation. */
|
|
21
|
+
const accountDomain = 'tempo:multisig:account'
|
|
22
|
+
|
|
23
|
+
/** Domain prefix for the native multisig config ID derivation. */
|
|
24
|
+
const configDomain = 'tempo:multisig:config'
|
|
25
|
+
|
|
26
|
+
/** Domain prefix for native multisig owner approvals. */
|
|
27
|
+
const signatureDomain = 'tempo:multisig:signature'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Native multisig configuration. Determines the permanent config ID and the
|
|
31
|
+
* stable multisig account address.
|
|
32
|
+
*/
|
|
33
|
+
export type Config<numberType = number> = Compute<{
|
|
34
|
+
/**
|
|
35
|
+
* Caller-chosen 32-byte salt mixed into the permanent config ID. Defaults to
|
|
36
|
+
* the zero salt (`MultisigConfig.zeroSalt`) when omitted.
|
|
37
|
+
*/
|
|
38
|
+
salt?: Hex.Hex | undefined
|
|
39
|
+
/** Minimum total owner weight required to authorize a transaction. */
|
|
40
|
+
threshold: numberType
|
|
41
|
+
/** Weighted owner list (strictly ascending by `owner` address). */
|
|
42
|
+
owners: readonly Owner<numberType>[]
|
|
43
|
+
}>
|
|
44
|
+
|
|
45
|
+
/** Native multisig owner entry. */
|
|
46
|
+
export type Owner<numberType = number> = {
|
|
47
|
+
/** Owner address (recovered from the owner's primitive signature). */
|
|
48
|
+
owner: Address.Address
|
|
49
|
+
/** Nonzero owner weight. */
|
|
50
|
+
weight: numberType
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** RLP tuple representation of a {@link ox#MultisigConfig.Config}. */
|
|
54
|
+
export type Tuple = readonly [
|
|
55
|
+
salt: Hex.Hex,
|
|
56
|
+
threshold: Hex.Hex,
|
|
57
|
+
owners: readonly Hex.Hex[][],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Asserts that a native multisig {@link ox#MultisigConfig.Config} is valid.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors the Tempo `validate_multisig_config` rules: owners non-empty and
|
|
64
|
+
* `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
|
|
65
|
+
* owner weights, `threshold >= 1`, total weight `<= u32::MAX`, and
|
|
66
|
+
* `threshold <= total weight`.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts twoslash
|
|
70
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
71
|
+
*
|
|
72
|
+
* MultisigConfig.assert({
|
|
73
|
+
* threshold: 1,
|
|
74
|
+
* owners: [
|
|
75
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
76
|
+
* ],
|
|
77
|
+
* })
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @param config - The multisig config.
|
|
81
|
+
*/
|
|
82
|
+
export function assert<numberType = number>(config: Config<numberType>): void {
|
|
83
|
+
const { salt, threshold, owners } = config
|
|
84
|
+
|
|
85
|
+
if (typeof salt !== 'undefined' && Hex.size(salt) !== 32)
|
|
86
|
+
throw new InvalidConfigError({ reason: 'salt must be 32 bytes' })
|
|
87
|
+
if (owners.length === 0)
|
|
88
|
+
throw new InvalidConfigError({ reason: 'owners cannot be empty' })
|
|
89
|
+
if (owners.length > maxOwners)
|
|
90
|
+
throw new InvalidConfigError({ reason: 'too many owners' })
|
|
91
|
+
if (Number(threshold) < 1)
|
|
92
|
+
throw new InvalidConfigError({ reason: 'threshold cannot be zero' })
|
|
93
|
+
|
|
94
|
+
let totalWeight = 0
|
|
95
|
+
let previous: bigint | undefined
|
|
96
|
+
for (const owner of owners) {
|
|
97
|
+
if (!Address.validate(owner.owner) || Hex.toBigInt(owner.owner) === 0n)
|
|
98
|
+
throw new InvalidConfigError({ reason: 'owner cannot be zero' })
|
|
99
|
+
if (Number(owner.weight) < 1)
|
|
100
|
+
throw new InvalidConfigError({ reason: 'owner weight cannot be zero' })
|
|
101
|
+
|
|
102
|
+
const current = Hex.toBigInt(owner.owner)
|
|
103
|
+
if (typeof previous !== 'undefined' && previous >= current)
|
|
104
|
+
throw new InvalidConfigError({
|
|
105
|
+
reason: 'owners must be strictly ascending',
|
|
106
|
+
})
|
|
107
|
+
previous = current
|
|
108
|
+
|
|
109
|
+
totalWeight += Number(owner.weight)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (totalWeight > 0xffffffff)
|
|
113
|
+
throw new InvalidConfigError({
|
|
114
|
+
reason: 'total owner weight exceeds u32 max',
|
|
115
|
+
})
|
|
116
|
+
if (Number(threshold) > totalWeight)
|
|
117
|
+
throw new InvalidConfigError({
|
|
118
|
+
reason: 'threshold exceeds total owner weight',
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export declare namespace assert {
|
|
123
|
+
type ErrorType = InvalidConfigError | Errors.GlobalErrorType
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Normalizes a native multisig {@link ox#MultisigConfig.Config}.
|
|
128
|
+
*
|
|
129
|
+
* Sorts owners into strictly ascending `owner` address order (the canonical
|
|
130
|
+
* form required for config ID derivation) and asserts the config is valid.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts twoslash
|
|
134
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
135
|
+
*
|
|
136
|
+
* const config = MultisigConfig.from({
|
|
137
|
+
* threshold: 2,
|
|
138
|
+
* owners: [
|
|
139
|
+
* { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
|
|
140
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
141
|
+
* ],
|
|
142
|
+
* })
|
|
143
|
+
* // owners are now sorted ascending by address
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* @param config - The multisig config.
|
|
147
|
+
* @returns The normalized multisig config.
|
|
148
|
+
*/
|
|
149
|
+
export function from<numberType = number>(
|
|
150
|
+
config: Config<numberType>,
|
|
151
|
+
): Config<numberType> {
|
|
152
|
+
const owners = [...config.owners].sort((a, b) =>
|
|
153
|
+
Hex.toBigInt(a.owner) < Hex.toBigInt(b.owner) ? -1 : 1,
|
|
154
|
+
)
|
|
155
|
+
const normalized = {
|
|
156
|
+
salt: config.salt ? Hex.padLeft(config.salt, 32) : zeroSalt,
|
|
157
|
+
threshold: config.threshold,
|
|
158
|
+
owners,
|
|
159
|
+
} as Config<numberType>
|
|
160
|
+
assert(normalized)
|
|
161
|
+
return normalized
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Converts an RLP {@link ox#MultisigConfig.Tuple} back to a
|
|
166
|
+
* {@link ox#MultisigConfig.Config}.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts twoslash
|
|
170
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
171
|
+
*
|
|
172
|
+
* const config = MultisigConfig.fromTuple([
|
|
173
|
+
* `0x${'00'.repeat(32)}`,
|
|
174
|
+
* '0x01',
|
|
175
|
+
* [['0x1111111111111111111111111111111111111111', '0x01']],
|
|
176
|
+
* ])
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* @param tuple - The RLP tuple.
|
|
180
|
+
* @returns The multisig config.
|
|
181
|
+
*/
|
|
182
|
+
export function fromTuple(tuple: Tuple): Config {
|
|
183
|
+
const [salt, threshold, owners] = tuple
|
|
184
|
+
return {
|
|
185
|
+
salt: salt && salt !== '0x' ? Hex.padLeft(salt, 32) : zeroSalt,
|
|
186
|
+
threshold: threshold === '0x' ? 0 : Hex.toNumber(threshold),
|
|
187
|
+
owners: owners.map((owner) => {
|
|
188
|
+
const [ownerAddress, weight] = owner as readonly Hex.Hex[]
|
|
189
|
+
return {
|
|
190
|
+
owner: ownerAddress as Address.Address,
|
|
191
|
+
weight: !weight || weight === '0x' ? 0 : Hex.toNumber(weight),
|
|
192
|
+
}
|
|
193
|
+
}),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Derives the stable native multisig account address.
|
|
199
|
+
*
|
|
200
|
+
* `keccak256("tempo:multisig:account" || config_id)[12:32]`.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts twoslash
|
|
204
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
205
|
+
*
|
|
206
|
+
* const config = MultisigConfig.from({
|
|
207
|
+
* threshold: 1,
|
|
208
|
+
* owners: [
|
|
209
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
210
|
+
* ],
|
|
211
|
+
* })
|
|
212
|
+
*
|
|
213
|
+
* const address = MultisigConfig.getAddress({ config })
|
|
214
|
+
* ```
|
|
215
|
+
*
|
|
216
|
+
* @param value - The config or config ID to derive the address from.
|
|
217
|
+
* @returns The multisig account address.
|
|
218
|
+
*/
|
|
219
|
+
export function getAddress(value: getAddress.Value): Address.Address {
|
|
220
|
+
const id = 'configId' in value ? value.configId : toId(value.config)
|
|
221
|
+
const hash = Hash.keccak256(Hex.concat(Hex.fromString(accountDomain), id))
|
|
222
|
+
return Address.from(Hex.slice(hash, 12, 32))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export declare namespace getAddress {
|
|
226
|
+
type Value = { config: Config } | { configId: Hex.Hex }
|
|
227
|
+
|
|
228
|
+
type ErrorType =
|
|
229
|
+
| toId.ErrorType
|
|
230
|
+
| Address.from.ErrorType
|
|
231
|
+
| Hash.keccak256.ErrorType
|
|
232
|
+
| Hex.concat.ErrorType
|
|
233
|
+
| Hex.slice.ErrorType
|
|
234
|
+
| Errors.GlobalErrorType
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Computes the digest a native multisig owner approves (signs).
|
|
239
|
+
*
|
|
240
|
+
* `keccak256("tempo:multisig:signature" || inner_digest || account || config_id)`,
|
|
241
|
+
* where `inner_digest` is the transaction sign payload
|
|
242
|
+
* ({@link ox#TxEnvelopeTempo.(getSignPayload:function)}).
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts twoslash
|
|
246
|
+
* import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
|
|
247
|
+
*
|
|
248
|
+
* const config = MultisigConfig.from({
|
|
249
|
+
* threshold: 1,
|
|
250
|
+
* owners: [
|
|
251
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
252
|
+
* ],
|
|
253
|
+
* })
|
|
254
|
+
* const configId = MultisigConfig.toId(config)
|
|
255
|
+
* const account = MultisigConfig.getAddress({ configId })
|
|
256
|
+
*
|
|
257
|
+
* const envelope = TxEnvelopeTempo.from({
|
|
258
|
+
* chainId: 1,
|
|
259
|
+
* calls: [],
|
|
260
|
+
* })
|
|
261
|
+
*
|
|
262
|
+
* const digest = MultisigConfig.getSignPayload({
|
|
263
|
+
* payload: TxEnvelopeTempo.getSignPayload(envelope),
|
|
264
|
+
* account,
|
|
265
|
+
* configId,
|
|
266
|
+
* })
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* @param value - The digest derivation parameters.
|
|
270
|
+
* @returns The owner approval digest.
|
|
271
|
+
*/
|
|
272
|
+
export function getSignPayload(value: getSignPayload.Value): Hex.Hex {
|
|
273
|
+
const { payload, account, configId } = value
|
|
274
|
+
return Hash.keccak256(
|
|
275
|
+
Hex.concat(
|
|
276
|
+
Hex.fromString(signatureDomain),
|
|
277
|
+
Hex.from(payload),
|
|
278
|
+
account,
|
|
279
|
+
configId,
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export declare namespace getSignPayload {
|
|
285
|
+
type Value = {
|
|
286
|
+
/** The inner transaction sign payload (`tx.signature_hash()`). */
|
|
287
|
+
payload: Hex.Hex | Bytes.Bytes
|
|
288
|
+
/** The native multisig account address. */
|
|
289
|
+
account: Address.Address
|
|
290
|
+
/** The permanent config ID. */
|
|
291
|
+
configId: Hex.Hex
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
type ErrorType =
|
|
295
|
+
| Hash.keccak256.ErrorType
|
|
296
|
+
| Hex.concat.ErrorType
|
|
297
|
+
| Hex.from.ErrorType
|
|
298
|
+
| Errors.GlobalErrorType
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Derives the permanent config ID for a native multisig
|
|
303
|
+
* {@link ox#MultisigConfig.Config}.
|
|
304
|
+
*
|
|
305
|
+
* Preimage (fixed-width big-endian, **not** RLP):
|
|
306
|
+
* `keccak256("tempo:multisig:config" || salt || be_u32(threshold) || be_u32(owners.length) || (owner || be_u32(weight)) for each owner)`.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts twoslash
|
|
310
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
311
|
+
*
|
|
312
|
+
* const config = MultisigConfig.from({
|
|
313
|
+
* threshold: 1,
|
|
314
|
+
* owners: [
|
|
315
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
316
|
+
* ],
|
|
317
|
+
* })
|
|
318
|
+
*
|
|
319
|
+
* const configId = MultisigConfig.toId(config)
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* @param config - The multisig config.
|
|
323
|
+
* @returns The 32-byte config ID.
|
|
324
|
+
*/
|
|
325
|
+
export function toId(config: Config): Hex.Hex {
|
|
326
|
+
assert(config)
|
|
327
|
+
const id = Hash.keccak256(
|
|
328
|
+
Hex.concat(
|
|
329
|
+
Hex.fromString(configDomain),
|
|
330
|
+
Hex.padLeft(config.salt ?? zeroSalt, 32),
|
|
331
|
+
Hex.fromNumber(config.threshold, { size: 4 }),
|
|
332
|
+
Hex.fromNumber(config.owners.length, { size: 4 }),
|
|
333
|
+
...config.owners.flatMap((owner) => [
|
|
334
|
+
owner.owner,
|
|
335
|
+
Hex.fromNumber(owner.weight, { size: 4 }),
|
|
336
|
+
]),
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
if (Hex.toBigInt(id) === 0n)
|
|
340
|
+
throw new InvalidConfigError({ reason: 'config ID cannot be zero' })
|
|
341
|
+
return id
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export declare namespace toId {
|
|
345
|
+
type ErrorType =
|
|
346
|
+
| assert.ErrorType
|
|
347
|
+
| Hash.keccak256.ErrorType
|
|
348
|
+
| Hex.concat.ErrorType
|
|
349
|
+
| Hex.fromNumber.ErrorType
|
|
350
|
+
| Hex.fromString.ErrorType
|
|
351
|
+
| Errors.GlobalErrorType
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Converts a {@link ox#MultisigConfig.Config} to its RLP tuple form (carried
|
|
356
|
+
* by the multisig signature `init`).
|
|
357
|
+
*
|
|
358
|
+
* Tuple shape: `[salt, threshold, [[owner, weight], ...]]`. The
|
|
359
|
+
* 32-byte `salt` encodes as a full fixed-width string; other integers use
|
|
360
|
+
* canonical RLP encoding (zero values encode as `0x`).
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```ts twoslash
|
|
364
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
365
|
+
*
|
|
366
|
+
* const tuple = MultisigConfig.toTuple({
|
|
367
|
+
* threshold: 1,
|
|
368
|
+
* owners: [
|
|
369
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
370
|
+
* ],
|
|
371
|
+
* })
|
|
372
|
+
* ```
|
|
373
|
+
*
|
|
374
|
+
* @param config - The multisig config.
|
|
375
|
+
* @returns The RLP tuple.
|
|
376
|
+
*/
|
|
377
|
+
export function toTuple(config: Config): Tuple {
|
|
378
|
+
assert(config)
|
|
379
|
+
const owners = config.owners.map(
|
|
380
|
+
(owner) => [owner.owner, Hex.fromNumber(owner.weight)] as Hex.Hex[],
|
|
381
|
+
)
|
|
382
|
+
// `salt` is a fixed 32-byte value: it RLP-encodes as a full 32-byte string
|
|
383
|
+
// (including the zero salt), never trimmed like an integer.
|
|
384
|
+
const salt = config.salt ? Hex.padLeft(config.salt, 32) : zeroSalt
|
|
385
|
+
return [salt, Hex.fromNumber(config.threshold), owners] as const
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Validates a native multisig {@link ox#MultisigConfig.Config}. Returns `true`
|
|
390
|
+
* if valid, `false` otherwise.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts twoslash
|
|
394
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
395
|
+
*
|
|
396
|
+
* const valid = MultisigConfig.validate({
|
|
397
|
+
* threshold: 1,
|
|
398
|
+
* owners: [
|
|
399
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
400
|
+
* ],
|
|
401
|
+
* })
|
|
402
|
+
* // @log: true
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* @param config - The multisig config.
|
|
406
|
+
* @returns Whether the config is valid.
|
|
407
|
+
*/
|
|
408
|
+
export function validate(config: Config): boolean {
|
|
409
|
+
try {
|
|
410
|
+
assert(config)
|
|
411
|
+
return true
|
|
412
|
+
} catch {
|
|
413
|
+
return false
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Thrown when a native multisig config is invalid. */
|
|
418
|
+
export class InvalidConfigError extends Errors.BaseError {
|
|
419
|
+
override readonly name = 'MultisigConfig.InvalidConfigError'
|
|
420
|
+
constructor({ reason }: { reason: string }) {
|
|
421
|
+
super(`Invalid native multisig config: ${reason}.`)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { AbiEvent, AbiParameters } from 'ox'
|
|
2
|
+
import { ReceivePolicyReceipt } from 'ox/tempo'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
const token = '0x20c0000000000000000000000000000000000001'
|
|
6
|
+
const originator = '0x0000000000000000000000000000000000000aaa'
|
|
7
|
+
const recipient = '0x0000000000000000000000000000000000000bbb'
|
|
8
|
+
const zeroAddress = '0x0000000000000000000000000000000000000000'
|
|
9
|
+
const zeroHash =
|
|
10
|
+
'0x0000000000000000000000000000000000000000000000000000000000000000'
|
|
11
|
+
|
|
12
|
+
const witnessParameters = [
|
|
13
|
+
{
|
|
14
|
+
type: 'tuple',
|
|
15
|
+
components: [
|
|
16
|
+
{ name: 'version', type: 'uint8' },
|
|
17
|
+
{ name: 'token', type: 'address' },
|
|
18
|
+
{ name: 'recoveryAuthority', type: 'address' },
|
|
19
|
+
{ name: 'originator', type: 'address' },
|
|
20
|
+
{ name: 'recipient', type: 'address' },
|
|
21
|
+
{ name: 'blockedAt', type: 'uint64' },
|
|
22
|
+
{ name: 'blockedNonce', type: 'uint64' },
|
|
23
|
+
{ name: 'blockedReason', type: 'uint8' },
|
|
24
|
+
{ name: 'kind', type: 'uint8' },
|
|
25
|
+
{ name: 'memo', type: 'bytes32' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
] as const
|
|
29
|
+
|
|
30
|
+
function encodeWitness(
|
|
31
|
+
overrides: Partial<{
|
|
32
|
+
blockedReason: number
|
|
33
|
+
kind: number
|
|
34
|
+
}> = {},
|
|
35
|
+
) {
|
|
36
|
+
return AbiParameters.encode(witnessParameters, [
|
|
37
|
+
{
|
|
38
|
+
version: 1,
|
|
39
|
+
token,
|
|
40
|
+
recoveryAuthority: zeroAddress,
|
|
41
|
+
originator,
|
|
42
|
+
recipient,
|
|
43
|
+
blockedAt: 1234n,
|
|
44
|
+
blockedNonce: 5n,
|
|
45
|
+
blockedReason: overrides.blockedReason ?? 2,
|
|
46
|
+
kind: overrides.kind ?? 0,
|
|
47
|
+
memo: zeroHash,
|
|
48
|
+
},
|
|
49
|
+
])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const transferBlocked = AbiEvent.from(
|
|
53
|
+
'event TransferBlocked(address indexed token, address indexed receiver, uint64 indexed blockedNonce, uint256 amount, uint8 receiptVersion, bytes receipt)',
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
function blockedLog(witness: `0x${string}`) {
|
|
57
|
+
const { topics } = AbiEvent.encode(transferBlocked, {
|
|
58
|
+
token,
|
|
59
|
+
receiver: recipient,
|
|
60
|
+
blockedNonce: 5n,
|
|
61
|
+
})
|
|
62
|
+
const data = AbiParameters.encode(
|
|
63
|
+
[{ type: 'uint256' }, { type: 'uint8' }, { type: 'bytes' }],
|
|
64
|
+
[10_000_000n, 1, witness],
|
|
65
|
+
)
|
|
66
|
+
return { data, topics: topics as readonly `0x${string}`[] }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('decode', () => {
|
|
70
|
+
test('default', () => {
|
|
71
|
+
expect(ReceivePolicyReceipt.decode(encodeWitness())).toMatchInlineSnapshot(`
|
|
72
|
+
{
|
|
73
|
+
"blockedAt": 1234n,
|
|
74
|
+
"blockedNonce": 5n,
|
|
75
|
+
"blockedReason": "receivePolicy",
|
|
76
|
+
"kind": "transfer",
|
|
77
|
+
"memo": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
78
|
+
"originator": "0x0000000000000000000000000000000000000aaa",
|
|
79
|
+
"recipient": "0x0000000000000000000000000000000000000bbb",
|
|
80
|
+
"recoveryAuthority": "0x0000000000000000000000000000000000000000",
|
|
81
|
+
"token": "0x20c0000000000000000000000000000000000001",
|
|
82
|
+
"version": 1,
|
|
83
|
+
}
|
|
84
|
+
`)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('behavior: kind mint', () => {
|
|
88
|
+
expect(ReceivePolicyReceipt.decode(encodeWitness({ kind: 1 })).kind).toBe(
|
|
89
|
+
'mint',
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('behavior: blocked reason token filter', () => {
|
|
94
|
+
expect(
|
|
95
|
+
ReceivePolicyReceipt.decode(encodeWitness({ blockedReason: 1 }))
|
|
96
|
+
.blockedReason,
|
|
97
|
+
).toBe('tokenFilter')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('encode', () => {
|
|
102
|
+
test('round-trips with decode', () => {
|
|
103
|
+
const witness = encodeWitness()
|
|
104
|
+
expect(
|
|
105
|
+
ReceivePolicyReceipt.encode(ReceivePolicyReceipt.decode(witness)),
|
|
106
|
+
).toBe(witness)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('behavior: maps enum strings back to indices', () => {
|
|
110
|
+
const receipt = ReceivePolicyReceipt.decode(
|
|
111
|
+
encodeWitness({ blockedReason: 1, kind: 1 }),
|
|
112
|
+
)
|
|
113
|
+
const reencoded = ReceivePolicyReceipt.encode(receipt)
|
|
114
|
+
expect(ReceivePolicyReceipt.decode(reencoded)).toEqual(receipt)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('fromLog', () => {
|
|
119
|
+
test('default', () => {
|
|
120
|
+
const witness = encodeWitness()
|
|
121
|
+
expect(ReceivePolicyReceipt.fromLog(blockedLog(witness))).toBe(witness)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('error: non-matching log', () => {
|
|
125
|
+
expect(() =>
|
|
126
|
+
ReceivePolicyReceipt.fromLog({ data: '0x', topics: [zeroHash] }),
|
|
127
|
+
).toThrow()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('from', () => {
|
|
132
|
+
test('passthrough encoded receipt', () => {
|
|
133
|
+
const witness = encodeWitness()
|
|
134
|
+
expect(ReceivePolicyReceipt.from(witness)).toBe(witness)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('from decoded fields', () => {
|
|
138
|
+
const witness = encodeWitness()
|
|
139
|
+
expect(
|
|
140
|
+
ReceivePolicyReceipt.from(ReceivePolicyReceipt.decode(witness)),
|
|
141
|
+
).toBe(witness)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('fromTransactionReceipt', () => {
|
|
146
|
+
test('single blocked transfer', () => {
|
|
147
|
+
const witness = encodeWitness()
|
|
148
|
+
const receipts = ReceivePolicyReceipt.fromTransactionReceipt({
|
|
149
|
+
logs: [blockedLog(witness)],
|
|
150
|
+
})
|
|
151
|
+
expect(receipts).toHaveLength(1)
|
|
152
|
+
expect(receipts[0]).toBe(witness)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('multiple blocked transfers', () => {
|
|
156
|
+
const receipts = ReceivePolicyReceipt.fromTransactionReceipt({
|
|
157
|
+
logs: [
|
|
158
|
+
blockedLog(encodeWitness()),
|
|
159
|
+
blockedLog(encodeWitness({ kind: 1 })),
|
|
160
|
+
],
|
|
161
|
+
})
|
|
162
|
+
expect(receipts).toHaveLength(2)
|
|
163
|
+
expect(ReceivePolicyReceipt.decode(receipts[0]!).kind).toBe('transfer')
|
|
164
|
+
expect(ReceivePolicyReceipt.decode(receipts[1]!).kind).toBe('mint')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('ignores unrelated logs', () => {
|
|
168
|
+
const receipts = ReceivePolicyReceipt.fromTransactionReceipt({
|
|
169
|
+
logs: [
|
|
170
|
+
{
|
|
171
|
+
data: '0x',
|
|
172
|
+
topics: [zeroHash],
|
|
173
|
+
},
|
|
174
|
+
blockedLog(encodeWitness()),
|
|
175
|
+
],
|
|
176
|
+
})
|
|
177
|
+
expect(receipts).toHaveLength(1)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('behavior: no logs', () => {
|
|
181
|
+
expect(ReceivePolicyReceipt.fromTransactionReceipt({})).toEqual([])
|
|
182
|
+
expect(ReceivePolicyReceipt.fromTransactionReceipt({ logs: [] })).toEqual(
|
|
183
|
+
[],
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('exports', () => {
|
|
189
|
+
expect(Object.keys(ReceivePolicyReceipt)).toMatchInlineSnapshot(`
|
|
190
|
+
[
|
|
191
|
+
"decode",
|
|
192
|
+
"encode",
|
|
193
|
+
"from",
|
|
194
|
+
"fromLog",
|
|
195
|
+
"fromTransactionReceipt",
|
|
196
|
+
]
|
|
197
|
+
`)
|
|
198
|
+
})
|