ox 0.14.32 → 0.14.34
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 +24 -0
- package/_cjs/tempo/KeyAuthorization.js +6 -6
- package/_cjs/tempo/KeyAuthorization.js.map +1 -1
- package/_cjs/tempo/MultisigConfig.js +4 -4
- package/_cjs/tempo/MultisigConfig.js.map +1 -1
- package/_cjs/tempo/SignatureEnvelope.js +23 -15
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/ZoneId.js +38 -6
- package/_cjs/tempo/ZoneId.js.map +1 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/KeyAuthorization.js +7 -13
- package/_esm/tempo/KeyAuthorization.js.map +1 -1
- package/_esm/tempo/MultisigConfig.js +17 -16
- package/_esm/tempo/MultisigConfig.js.map +1 -1
- package/_esm/tempo/SignatureEnvelope.js +42 -28
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/ZoneId.js +46 -16
- package/_esm/tempo/ZoneId.js.map +1 -1
- package/_esm/tempo/index.js +7 -8
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/tempo/KeyAuthorization.d.ts +16 -12
- package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
- package/_types/tempo/MultisigConfig.d.ts +20 -17
- package/_types/tempo/MultisigConfig.d.ts.map +1 -1
- package/_types/tempo/SignatureEnvelope.d.ts +26 -18
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/ZoneId.d.ts +35 -17
- package/_types/tempo/ZoneId.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +7 -8
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/package.json +6 -1
- package/tempo/KeyAuthorization.test-d.ts +41 -6
- package/tempo/KeyAuthorization.test.ts +106 -60
- package/tempo/KeyAuthorization.ts +36 -38
- package/tempo/MultisigConfig.test.ts +28 -8
- package/tempo/MultisigConfig.ts +28 -19
- package/tempo/SignatureEnvelope.test.ts +64 -19
- package/tempo/SignatureEnvelope.ts +52 -36
- package/tempo/ZoneId.test-d/package.json +6 -0
- package/tempo/ZoneId.test-d.ts +16 -0
- package/tempo/ZoneId.test.ts +48 -13
- package/tempo/ZoneId.ts +55 -19
- package/tempo/index.ts +7 -8
- package/tempo/multisig.e2e.test.ts +402 -145
- package/version.ts +1 -1
package/tempo/MultisigConfig.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as Hex from '../core/Hex.js'
|
|
|
6
6
|
import type { Compute, OneOf } from '../core/internal/types.js'
|
|
7
7
|
|
|
8
8
|
/** Maximum number of owners allowed in a native multisig config. */
|
|
9
|
-
export const maxOwners =
|
|
9
|
+
export const maxOwners = 50
|
|
10
10
|
|
|
11
11
|
/** Maximum threshold accepted by a native multisig config. */
|
|
12
12
|
export const maxThreshold = 8
|
|
@@ -20,7 +20,7 @@ export const maxSignatures = maxThreshold
|
|
|
20
20
|
*/
|
|
21
21
|
export const maxNestingDepth = 2
|
|
22
22
|
|
|
23
|
-
/** Maximum encoded byte length for one owner approval. */
|
|
23
|
+
/** Maximum encoded byte length for one primitive owner approval. */
|
|
24
24
|
export const maxOwnerSignatureBytes = 2049
|
|
25
25
|
|
|
26
26
|
/** Tempo signature type byte for native multisig signatures. */
|
|
@@ -224,14 +224,14 @@ export function fromTuple(tuple: Tuple): Config {
|
|
|
224
224
|
* ```ts twoslash
|
|
225
225
|
* import { MultisigConfig } from 'ox/tempo'
|
|
226
226
|
*
|
|
227
|
-
* const
|
|
227
|
+
* const initialConfig = MultisigConfig.from({
|
|
228
228
|
* threshold: 1,
|
|
229
229
|
* owners: [
|
|
230
230
|
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
231
231
|
* ],
|
|
232
232
|
* })
|
|
233
233
|
*
|
|
234
|
-
* const address = MultisigConfig.getAddress(
|
|
234
|
+
* const address = MultisigConfig.getAddress(initialConfig)
|
|
235
235
|
* ```
|
|
236
236
|
*
|
|
237
237
|
* @param config - The initial (bootstrap) multisig config.
|
|
@@ -272,13 +272,13 @@ export declare namespace getAddress {
|
|
|
272
272
|
/**
|
|
273
273
|
* Computes the digest a native multisig owner approves (signs).
|
|
274
274
|
*
|
|
275
|
-
* `keccak256("tempo:multisig:signature" || inner_digest || account)`,
|
|
275
|
+
* `keccak256("tempo:multisig:signature" || inner_digest || account || uint64be(version))`,
|
|
276
276
|
* where `inner_digest` is the transaction sign payload
|
|
277
277
|
* ({@link ox#TxEnvelopeTempo.(getSignPayload:function)}).
|
|
278
278
|
*
|
|
279
|
-
* The digest is keyed on the permanent `account` derived from the
|
|
280
|
-
* (bootstrap) config
|
|
281
|
-
* is the
|
|
279
|
+
* The digest is keyed on the permanent `account` derived from the initial
|
|
280
|
+
* (bootstrap) config and the current config `version`. Bootstrap approvals use
|
|
281
|
+
* version `0n`, which is also the default; each config update increments it.
|
|
282
282
|
*
|
|
283
283
|
* For a nested multisig owner approval, the parent digest becomes the nested
|
|
284
284
|
* approval's `payload`, with the nested multisig `account`.
|
|
@@ -287,7 +287,7 @@ export declare namespace getAddress {
|
|
|
287
287
|
* ```ts twoslash
|
|
288
288
|
* import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
|
|
289
289
|
*
|
|
290
|
-
* const
|
|
290
|
+
* const initialConfig = MultisigConfig.from({
|
|
291
291
|
* threshold: 1,
|
|
292
292
|
* owners: [
|
|
293
293
|
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
@@ -301,7 +301,7 @@ export declare namespace getAddress {
|
|
|
301
301
|
*
|
|
302
302
|
* const digest = MultisigConfig.getSignPayload({
|
|
303
303
|
* payload: TxEnvelopeTempo.getSignPayload(envelope),
|
|
304
|
-
*
|
|
304
|
+
* initialConfig,
|
|
305
305
|
* })
|
|
306
306
|
* ```
|
|
307
307
|
*
|
|
@@ -314,19 +314,20 @@ export declare namespace getAddress {
|
|
|
314
314
|
* ```ts twoslash
|
|
315
315
|
* import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
|
|
316
316
|
*
|
|
317
|
-
* const
|
|
317
|
+
* const initialConfig = MultisigConfig.from({
|
|
318
318
|
* threshold: 1,
|
|
319
319
|
* owners: [
|
|
320
320
|
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
321
321
|
* ],
|
|
322
322
|
* })
|
|
323
|
-
* const account = MultisigConfig.getAddress(
|
|
323
|
+
* const account = MultisigConfig.getAddress(initialConfig)
|
|
324
324
|
*
|
|
325
325
|
* const envelope = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
|
|
326
326
|
*
|
|
327
327
|
* const digest = MultisigConfig.getSignPayload({
|
|
328
328
|
* payload: TxEnvelopeTempo.getSignPayload(envelope),
|
|
329
|
-
* account
|
|
329
|
+
* account,
|
|
330
|
+
* version: 1n
|
|
330
331
|
* })
|
|
331
332
|
* ```
|
|
332
333
|
*
|
|
@@ -334,13 +335,18 @@ export declare namespace getAddress {
|
|
|
334
335
|
* @returns The owner approval digest.
|
|
335
336
|
*/
|
|
336
337
|
export function getSignPayload(value: getSignPayload.Value): Hex.Hex {
|
|
337
|
-
const { payload } = value
|
|
338
|
+
const { payload, version = 0n } = value
|
|
338
339
|
const account =
|
|
339
340
|
'account' in value && value.account
|
|
340
341
|
? value.account
|
|
341
|
-
: getAddress((value as {
|
|
342
|
+
: getAddress((value as { initialConfig: Config }).initialConfig)
|
|
342
343
|
return Hash.keccak256(
|
|
343
|
-
Hex.concat(
|
|
344
|
+
Hex.concat(
|
|
345
|
+
Hex.fromString(signatureDomain),
|
|
346
|
+
Hex.from(payload),
|
|
347
|
+
account,
|
|
348
|
+
Hex.fromNumber(version, { size: 8 }),
|
|
349
|
+
),
|
|
344
350
|
)
|
|
345
351
|
}
|
|
346
352
|
|
|
@@ -348,6 +354,8 @@ export declare namespace getSignPayload {
|
|
|
348
354
|
type Value = {
|
|
349
355
|
/** The inner transaction sign payload (`tx.signature_hash()`). */
|
|
350
356
|
payload: Hex.Hex | Bytes.Bytes
|
|
357
|
+
/** Current multisig config version. Defaults to `0n`. */
|
|
358
|
+
version?: bigint | undefined
|
|
351
359
|
} & OneOf<
|
|
352
360
|
| {
|
|
353
361
|
/** The native multisig account address. */
|
|
@@ -357,10 +365,10 @@ export declare namespace getSignPayload {
|
|
|
357
365
|
/**
|
|
358
366
|
* The initial multisig config (the bootstrap config that derived the
|
|
359
367
|
* permanent `account`). Used to derive the account automatically.
|
|
360
|
-
* Config updates never change `account`, so the
|
|
361
|
-
*
|
|
368
|
+
* Config updates never change `account`, so the initial config can
|
|
369
|
+
* continue deriving the account for post-update transactions.
|
|
362
370
|
*/
|
|
363
|
-
|
|
371
|
+
initialConfig: Config
|
|
364
372
|
}
|
|
365
373
|
>
|
|
366
374
|
|
|
@@ -369,6 +377,7 @@ export declare namespace getSignPayload {
|
|
|
369
377
|
| Hash.keccak256.ErrorType
|
|
370
378
|
| Hex.concat.ErrorType
|
|
371
379
|
| Hex.from.ErrorType
|
|
380
|
+
| Hex.fromNumber.ErrorType
|
|
372
381
|
| Errors.GlobalErrorType
|
|
373
382
|
}
|
|
374
383
|
|
|
@@ -1100,7 +1100,7 @@ describe('from', () => {
|
|
|
1100
1100
|
})
|
|
1101
1101
|
|
|
1102
1102
|
describe('multisig', () => {
|
|
1103
|
-
const
|
|
1103
|
+
const initialConfig = MultisigConfig.from({
|
|
1104
1104
|
threshold: 1,
|
|
1105
1105
|
owners: [
|
|
1106
1106
|
{
|
|
@@ -1110,29 +1110,29 @@ describe('from', () => {
|
|
|
1110
1110
|
],
|
|
1111
1111
|
})
|
|
1112
1112
|
|
|
1113
|
-
test('behavior: derives `account` from `
|
|
1113
|
+
test('behavior: derives `account` from `initialConfig`', () => {
|
|
1114
1114
|
const envelope = SignatureEnvelope.from({
|
|
1115
|
-
|
|
1115
|
+
initialConfig,
|
|
1116
1116
|
signatures: [SignatureEnvelope.from(signature_secp256k1)],
|
|
1117
1117
|
})
|
|
1118
1118
|
|
|
1119
1119
|
expect(envelope).toMatchObject({
|
|
1120
1120
|
type: 'multisig',
|
|
1121
|
-
account: MultisigConfig.getAddress(
|
|
1121
|
+
account: MultisigConfig.getAddress(initialConfig),
|
|
1122
1122
|
})
|
|
1123
|
-
expect('
|
|
1123
|
+
expect('initialConfig' in envelope).toBe(false)
|
|
1124
1124
|
expect((envelope as SignatureEnvelope.Multisig).init).toBeUndefined()
|
|
1125
1125
|
})
|
|
1126
1126
|
|
|
1127
|
-
test('behavior: `init: true` opts into bootstrap (uses `
|
|
1127
|
+
test('behavior: `init: true` opts into bootstrap (uses `initialConfig` as `init`)', () => {
|
|
1128
1128
|
const envelope = SignatureEnvelope.from({
|
|
1129
|
-
|
|
1129
|
+
initialConfig,
|
|
1130
1130
|
signatures: [SignatureEnvelope.from(signature_secp256k1)],
|
|
1131
1131
|
init: true,
|
|
1132
1132
|
})
|
|
1133
1133
|
|
|
1134
1134
|
expect((envelope as SignatureEnvelope.Multisig).init).toEqual(
|
|
1135
|
-
|
|
1135
|
+
initialConfig,
|
|
1136
1136
|
)
|
|
1137
1137
|
})
|
|
1138
1138
|
|
|
@@ -1147,7 +1147,7 @@ describe('from', () => {
|
|
|
1147
1147
|
],
|
|
1148
1148
|
})
|
|
1149
1149
|
const envelope = SignatureEnvelope.from({
|
|
1150
|
-
|
|
1150
|
+
initialConfig,
|
|
1151
1151
|
signatures: [SignatureEnvelope.from(signature_secp256k1)],
|
|
1152
1152
|
init: otherConfig,
|
|
1153
1153
|
})
|
|
@@ -1156,7 +1156,7 @@ describe('from', () => {
|
|
|
1156
1156
|
})
|
|
1157
1157
|
|
|
1158
1158
|
test('behavior: `{ account }` form still works', () => {
|
|
1159
|
-
const account = MultisigConfig.getAddress(
|
|
1159
|
+
const account = MultisigConfig.getAddress(initialConfig)
|
|
1160
1160
|
const envelope = SignatureEnvelope.from({
|
|
1161
1161
|
account,
|
|
1162
1162
|
signatures: [SignatureEnvelope.from(signature_secp256k1)],
|
|
@@ -1769,7 +1769,7 @@ describe('serialize', () => {
|
|
|
1769
1769
|
})
|
|
1770
1770
|
|
|
1771
1771
|
describe('sortMultisigApprovals', () => {
|
|
1772
|
-
// Build owner key pairs first so we can construct a real
|
|
1772
|
+
// Build owner key pairs first so we can construct a real initial config
|
|
1773
1773
|
// whose owner set matches the keys that produce the approvals below.
|
|
1774
1774
|
const ownerKeys = Array.from({ length: 3 }, () => {
|
|
1775
1775
|
const privateKey = Secp256k1.randomPrivateKey()
|
|
@@ -1782,12 +1782,15 @@ describe('sortMultisigApprovals', () => {
|
|
|
1782
1782
|
Hex.toBigInt(a.address) < Hex.toBigInt(b.address) ? -1 : 1,
|
|
1783
1783
|
)
|
|
1784
1784
|
|
|
1785
|
-
const
|
|
1785
|
+
const initialConfig = MultisigConfig.from({
|
|
1786
1786
|
threshold: 2,
|
|
1787
1787
|
owners: ascendingOwners.map((o) => ({ owner: o.address, weight: 1 })),
|
|
1788
1788
|
})
|
|
1789
1789
|
const payload = `0x${'42'.repeat(32)}` as const
|
|
1790
|
-
const digest = MultisigConfig.getSignPayload({
|
|
1790
|
+
const digest = MultisigConfig.getSignPayload({
|
|
1791
|
+
payload,
|
|
1792
|
+
initialConfig,
|
|
1793
|
+
})
|
|
1791
1794
|
|
|
1792
1795
|
const owners = ownerKeys.map((owner) => ({
|
|
1793
1796
|
address: owner.address,
|
|
@@ -1802,7 +1805,7 @@ describe('sortMultisigApprovals', () => {
|
|
|
1802
1805
|
|
|
1803
1806
|
test('behavior: orders approvals ascending by recovered owner address', () => {
|
|
1804
1807
|
const ordered = SignatureEnvelope.sortMultisigApprovals({
|
|
1805
|
-
|
|
1808
|
+
initialConfig,
|
|
1806
1809
|
payload,
|
|
1807
1810
|
// Provide approvals in reverse of the canonical order.
|
|
1808
1811
|
signatures: [...ascending].reverse().map((owner) => owner.signature),
|
|
@@ -1814,7 +1817,7 @@ describe('sortMultisigApprovals', () => {
|
|
|
1814
1817
|
const signatures = ascending.map((owner) => owner.signature)
|
|
1815
1818
|
expect(
|
|
1816
1819
|
SignatureEnvelope.sortMultisigApprovals({
|
|
1817
|
-
|
|
1820
|
+
initialConfig,
|
|
1818
1821
|
payload,
|
|
1819
1822
|
signatures,
|
|
1820
1823
|
}),
|
|
@@ -1823,7 +1826,7 @@ describe('sortMultisigApprovals', () => {
|
|
|
1823
1826
|
|
|
1824
1827
|
test('behavior: recovered order matches the config owner order', () => {
|
|
1825
1828
|
const ordered = SignatureEnvelope.sortMultisigApprovals({
|
|
1826
|
-
|
|
1829
|
+
initialConfig,
|
|
1827
1830
|
payload,
|
|
1828
1831
|
signatures: owners.map((owner) => owner.signature),
|
|
1829
1832
|
})
|
|
@@ -1833,12 +1836,12 @@ describe('sortMultisigApprovals', () => {
|
|
|
1833
1836
|
expect(recovered).toEqual(ascending.map((owner) => owner.address))
|
|
1834
1837
|
})
|
|
1835
1838
|
|
|
1836
|
-
test('behavior: `
|
|
1837
|
-
const account = MultisigConfig.getAddress(
|
|
1839
|
+
test('behavior: `initialConfig` and `{ account }` produce identical ordering', () => {
|
|
1840
|
+
const account = MultisigConfig.getAddress(initialConfig)
|
|
1838
1841
|
const signatures = owners.map((owner) => owner.signature)
|
|
1839
1842
|
|
|
1840
1843
|
const fromConfig = SignatureEnvelope.sortMultisigApprovals({
|
|
1841
|
-
|
|
1844
|
+
initialConfig,
|
|
1842
1845
|
payload,
|
|
1843
1846
|
signatures,
|
|
1844
1847
|
})
|
|
@@ -3013,6 +3016,14 @@ describe('multisig', () => {
|
|
|
3013
3016
|
).toThrowErrorMatchingInlineSnapshot(
|
|
3014
3017
|
`[SignatureEnvelope.InvalidMultisigApprovalError: Invalid native multisig owner approval: multisig owner signature exceeds 2049 bytes.]`,
|
|
3015
3018
|
)
|
|
3019
|
+
|
|
3020
|
+
const serialized = Hex.concat(
|
|
3021
|
+
'0x05',
|
|
3022
|
+
Rlp.fromHex([account, [SignatureEnvelope.serialize(oversized)]]),
|
|
3023
|
+
)
|
|
3024
|
+
expect(() => SignatureEnvelope.deserialize(serialized)).toThrowError(
|
|
3025
|
+
SignatureEnvelope.InvalidSerializedError,
|
|
3026
|
+
)
|
|
3016
3027
|
})
|
|
3017
3028
|
|
|
3018
3029
|
test('assert: rejects keychain owner approvals', () => {
|
|
@@ -3054,6 +3065,40 @@ describe('multisig', () => {
|
|
|
3054
3065
|
expect(() => SignatureEnvelope.assert(depth2)).not.toThrowError()
|
|
3055
3066
|
})
|
|
3056
3067
|
|
|
3068
|
+
test('accepts a nested approval above the primitive byte limit', () => {
|
|
3069
|
+
const primitive = SignatureEnvelope.from({
|
|
3070
|
+
...signature_webauthn,
|
|
3071
|
+
metadata: {
|
|
3072
|
+
...signature_webauthn.metadata,
|
|
3073
|
+
clientDataJSON: JSON.stringify({ value: 'x'.repeat(1_050) }),
|
|
3074
|
+
},
|
|
3075
|
+
signature: {
|
|
3076
|
+
r: signature_webauthn.signature.r,
|
|
3077
|
+
s: signature_webauthn.signature.s,
|
|
3078
|
+
},
|
|
3079
|
+
})
|
|
3080
|
+
const largeNested = SignatureEnvelope.from({
|
|
3081
|
+
type: 'multisig',
|
|
3082
|
+
account: nestedAccount,
|
|
3083
|
+
signatures: [primitive, primitive],
|
|
3084
|
+
})
|
|
3085
|
+
expect(Hex.size(SignatureEnvelope.serialize(primitive))).toBeLessThan(
|
|
3086
|
+
MultisigConfig.maxOwnerSignatureBytes,
|
|
3087
|
+
)
|
|
3088
|
+
expect(
|
|
3089
|
+
Hex.size(SignatureEnvelope.serialize(largeNested)),
|
|
3090
|
+
).toBeGreaterThan(MultisigConfig.maxOwnerSignatureBytes)
|
|
3091
|
+
|
|
3092
|
+
const parent = SignatureEnvelope.from({
|
|
3093
|
+
type: 'multisig',
|
|
3094
|
+
account,
|
|
3095
|
+
signatures: [largeNested],
|
|
3096
|
+
})
|
|
3097
|
+
expect(
|
|
3098
|
+
SignatureEnvelope.deserialize(SignatureEnvelope.serialize(parent)),
|
|
3099
|
+
).toEqual(parent)
|
|
3100
|
+
})
|
|
3101
|
+
|
|
3057
3102
|
test('assert: rejects nesting deeper than the maximum', () => {
|
|
3058
3103
|
const depth3 = SignatureEnvelope.from({
|
|
3059
3104
|
type: 'multisig',
|
|
@@ -424,12 +424,13 @@ function assertMultisig(envelope: Multisig, depth: number): void {
|
|
|
424
424
|
reason: 'nested multisig owner approvals cannot carry `init`',
|
|
425
425
|
})
|
|
426
426
|
assertMultisig(multisig, depth + 1)
|
|
427
|
-
} else
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
427
|
+
} else {
|
|
428
|
+
assert(inner)
|
|
429
|
+
if (Hex.size(serialize(inner)) > MultisigConfig.maxOwnerSignatureBytes)
|
|
430
|
+
throw new InvalidMultisigApprovalError({
|
|
431
|
+
reason: `multisig owner signature exceeds ${MultisigConfig.maxOwnerSignatureBytes} bytes`,
|
|
432
|
+
})
|
|
433
|
+
}
|
|
433
434
|
}
|
|
434
435
|
}
|
|
435
436
|
|
|
@@ -740,7 +741,10 @@ function deserialize_(
|
|
|
740
741
|
})
|
|
741
742
|
for (const signature of signatures)
|
|
742
743
|
if (
|
|
743
|
-
Hex.size(signature as Hex.Hex) >
|
|
744
|
+
Hex.size(signature as Hex.Hex) >
|
|
745
|
+
MultisigConfig.maxOwnerSignatureBytes &&
|
|
746
|
+
Hex.slice(signature as Hex.Hex, 0, 1) !==
|
|
747
|
+
MultisigConfig.signatureTypeByte
|
|
744
748
|
)
|
|
745
749
|
throw new InvalidSerializedError({
|
|
746
750
|
reason: `multisig owner signature exceeds ${MultisigConfig.maxOwnerSignatureBytes} bytes`,
|
|
@@ -912,17 +916,17 @@ function deserialize_(
|
|
|
912
916
|
* ```
|
|
913
917
|
*
|
|
914
918
|
* @example
|
|
915
|
-
* ### Multisig (from
|
|
919
|
+
* ### Multisig (from initial config)
|
|
916
920
|
*
|
|
917
|
-
* Pass `
|
|
918
|
-
* opt into bootstrap (uses `
|
|
921
|
+
* Pass `initialConfig` to derive `account` automatically. Set `init: true` to
|
|
922
|
+
* opt into bootstrap (uses `initialConfig` as the bootstrap `init`); omit
|
|
919
923
|
* `init` for subsequent (non-bootstrap) transactions.
|
|
920
924
|
*
|
|
921
925
|
* ```ts twoslash
|
|
922
926
|
* import { Secp256k1 } from 'ox'
|
|
923
927
|
* import { MultisigConfig, SignatureEnvelope } from 'ox/tempo'
|
|
924
928
|
*
|
|
925
|
-
* const
|
|
929
|
+
* const initialConfig = MultisigConfig.from({
|
|
926
930
|
* threshold: 1,
|
|
927
931
|
* owners: [
|
|
928
932
|
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
@@ -936,14 +940,14 @@ function deserialize_(
|
|
|
936
940
|
*
|
|
937
941
|
* // Bootstrap transaction
|
|
938
942
|
* const bootstrap = SignatureEnvelope.from({
|
|
939
|
-
*
|
|
943
|
+
* initialConfig,
|
|
940
944
|
* signatures: [signature],
|
|
941
945
|
* init: true,
|
|
942
946
|
* })
|
|
943
947
|
*
|
|
944
948
|
* // Subsequent (non-bootstrap) transactions
|
|
945
949
|
* const subsequent = SignatureEnvelope.from({
|
|
946
|
-
*
|
|
950
|
+
* initialConfig,
|
|
947
951
|
* signatures: [signature],
|
|
948
952
|
* })
|
|
949
953
|
* ```
|
|
@@ -970,20 +974,20 @@ export function from<const value extends from.Value>(
|
|
|
970
974
|
|
|
971
975
|
if (type === 'multisig') {
|
|
972
976
|
const multisig = value as Multisig & {
|
|
973
|
-
|
|
977
|
+
initialConfig?: MultisigConfig.Config | undefined
|
|
974
978
|
init?: MultisigConfig.Config | boolean | undefined
|
|
975
979
|
}
|
|
976
|
-
const {
|
|
977
|
-
// Derive `account` from `
|
|
980
|
+
const { initialConfig, init, ...rest } = multisig
|
|
981
|
+
// Derive `account` from `initialConfig` when not provided explicitly.
|
|
978
982
|
const account = (() => {
|
|
979
983
|
if (rest.account) return rest.account
|
|
980
|
-
if (
|
|
984
|
+
if (initialConfig) return MultisigConfig.getAddress(initialConfig)
|
|
981
985
|
return rest.account
|
|
982
986
|
})()
|
|
983
|
-
// `init: true` opts into bootstrap using the supplied `
|
|
987
|
+
// `init: true` opts into bootstrap using the supplied `initialConfig`.
|
|
984
988
|
// Otherwise, `init` is treated as the explicit bootstrap config (or
|
|
985
989
|
// omitted).
|
|
986
|
-
const initSource = init === true ?
|
|
990
|
+
const initSource = init === true ? initialConfig : init || undefined
|
|
987
991
|
return {
|
|
988
992
|
...rest,
|
|
989
993
|
account,
|
|
@@ -1039,13 +1043,13 @@ export declare namespace from {
|
|
|
1039
1043
|
|
|
1040
1044
|
/**
|
|
1041
1045
|
* Multisig envelope input variant where `account` is derived from the
|
|
1042
|
-
* supplied `
|
|
1043
|
-
* `
|
|
1046
|
+
* supplied `initialConfig`. Pass `init: true` to opt into bootstrap (uses
|
|
1047
|
+
* `initialConfig` as the bootstrap `init`); omit `init` for subsequent
|
|
1044
1048
|
* (non-bootstrap) transactions.
|
|
1045
1049
|
*/
|
|
1046
|
-
type
|
|
1050
|
+
type MultisigFromInitialConfig = {
|
|
1047
1051
|
type?: 'multisig' | undefined
|
|
1048
|
-
|
|
1052
|
+
initialConfig: MultisigConfig.Config
|
|
1049
1053
|
signatures: readonly SignatureEnvelope[]
|
|
1050
1054
|
init?: MultisigConfig.Config | boolean | undefined
|
|
1051
1055
|
}
|
|
@@ -1054,7 +1058,7 @@ export declare namespace from {
|
|
|
1054
1058
|
| UnionPartialBy<SignatureEnvelope, 'prehash' | 'type'>
|
|
1055
1059
|
| Secp256k1Flat
|
|
1056
1060
|
| Serialized
|
|
1057
|
-
|
|
|
1061
|
+
| MultisigFromInitialConfig
|
|
1058
1062
|
|
|
1059
1063
|
type ReturnValue<value extends Value> = Compute<
|
|
1060
1064
|
OneOf<
|
|
@@ -1062,7 +1066,7 @@ export declare namespace from {
|
|
|
1062
1066
|
? SignatureEnvelope
|
|
1063
1067
|
: value extends Secp256k1Flat
|
|
1064
1068
|
? Secp256k1
|
|
1065
|
-
: value extends
|
|
1069
|
+
: value extends MultisigFromInitialConfig
|
|
1066
1070
|
? Multisig
|
|
1067
1071
|
: IsNarrowable<value, SignatureEnvelope> extends true
|
|
1068
1072
|
? SignatureEnvelope
|
|
@@ -1285,7 +1289,7 @@ export function getType<
|
|
|
1285
1289
|
// Detect Multisig signature
|
|
1286
1290
|
if (
|
|
1287
1291
|
('account' in envelope ||
|
|
1288
|
-
'
|
|
1292
|
+
'initialConfig' in envelope ||
|
|
1289
1293
|
'init' in envelope) &&
|
|
1290
1294
|
'signatures' in envelope
|
|
1291
1295
|
)
|
|
@@ -1436,7 +1440,7 @@ export declare namespace serialize {
|
|
|
1436
1440
|
* recovered owner address. Works for any owner key type (secp256k1, p256,
|
|
1437
1441
|
* webAuthn).
|
|
1438
1442
|
*
|
|
1439
|
-
* Config updates never change `account`, so the
|
|
1443
|
+
* Config updates never change `account`, so the initial config is the correct
|
|
1440
1444
|
* input even for post-update transactions.
|
|
1441
1445
|
*
|
|
1442
1446
|
* @example
|
|
@@ -1444,7 +1448,7 @@ export declare namespace serialize {
|
|
|
1444
1448
|
* import { Secp256k1 } from 'ox'
|
|
1445
1449
|
* import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
|
|
1446
1450
|
*
|
|
1447
|
-
* const
|
|
1451
|
+
* const initialConfig = MultisigConfig.from({
|
|
1448
1452
|
* threshold: 2,
|
|
1449
1453
|
* owners: [
|
|
1450
1454
|
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
@@ -1455,14 +1459,20 @@ export declare namespace serialize {
|
|
|
1455
1459
|
* const tx = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
|
|
1456
1460
|
* const payload = TxEnvelopeTempo.getSignPayload(tx)
|
|
1457
1461
|
*
|
|
1458
|
-
* const privateKeys = [
|
|
1459
|
-
*
|
|
1462
|
+
* const privateKeys = [
|
|
1463
|
+
* Secp256k1.randomPrivateKey(),
|
|
1464
|
+
* Secp256k1.randomPrivateKey(),
|
|
1465
|
+
* ]
|
|
1466
|
+
* const digest = MultisigConfig.getSignPayload({
|
|
1467
|
+
* payload,
|
|
1468
|
+
* initialConfig,
|
|
1469
|
+
* })
|
|
1460
1470
|
* const signatures = privateKeys.map((privateKey) =>
|
|
1461
1471
|
* SignatureEnvelope.from(Secp256k1.sign({ payload: digest, privateKey })),
|
|
1462
1472
|
* )
|
|
1463
1473
|
*
|
|
1464
1474
|
* const ordered = SignatureEnvelope.sortMultisigApprovals({ // [!code focus]
|
|
1465
|
-
*
|
|
1475
|
+
* initialConfig, // [!code focus]
|
|
1466
1476
|
* payload, // [!code focus]
|
|
1467
1477
|
* signatures, // [!code focus]
|
|
1468
1478
|
* }) // [!code focus]
|
|
@@ -1474,11 +1484,15 @@ export declare namespace serialize {
|
|
|
1474
1484
|
export function sortMultisigApprovals(
|
|
1475
1485
|
value: sortMultisigApprovals.Value,
|
|
1476
1486
|
): readonly SignatureEnvelope[] {
|
|
1477
|
-
const { payload, signatures } = value
|
|
1487
|
+
const { payload, signatures, version = 0n } = value
|
|
1478
1488
|
const digest = MultisigConfig.getSignPayload(
|
|
1479
|
-
'
|
|
1480
|
-
? { payload,
|
|
1481
|
-
: {
|
|
1489
|
+
'initialConfig' in value && value.initialConfig
|
|
1490
|
+
? { payload, initialConfig: value.initialConfig, version }
|
|
1491
|
+
: {
|
|
1492
|
+
payload,
|
|
1493
|
+
account: (value as { account: Address.Address }).account,
|
|
1494
|
+
version,
|
|
1495
|
+
},
|
|
1482
1496
|
)
|
|
1483
1497
|
// Recover each signer once (decorate–sort–undecorate) rather than inside the
|
|
1484
1498
|
// comparator.
|
|
@@ -1497,6 +1511,8 @@ export declare namespace sortMultisigApprovals {
|
|
|
1497
1511
|
payload: Hex.Hex | Bytes.Bytes
|
|
1498
1512
|
/** The owner approvals to order. */
|
|
1499
1513
|
signatures: readonly SignatureEnvelope[]
|
|
1514
|
+
/** Current multisig config version. Defaults to `0n`. */
|
|
1515
|
+
version?: bigint | undefined
|
|
1500
1516
|
} & OneOf<
|
|
1501
1517
|
| {
|
|
1502
1518
|
/** The native multisig account address. */
|
|
@@ -1507,7 +1523,7 @@ export declare namespace sortMultisigApprovals {
|
|
|
1507
1523
|
* The initial multisig config (the bootstrap config that derived the
|
|
1508
1524
|
* permanent `account`). Used to derive the account automatically.
|
|
1509
1525
|
*/
|
|
1510
|
-
|
|
1526
|
+
initialConfig: MultisigConfig.Config
|
|
1511
1527
|
}
|
|
1512
1528
|
>
|
|
1513
1529
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expectTypeOf, test } from 'vitest'
|
|
2
|
+
import * as ZoneId from './ZoneId.js'
|
|
3
|
+
|
|
4
|
+
test('accepts supported source IDs', () => {
|
|
5
|
+
expectTypeOf(ZoneId.fromChainId(421_700_001)).toEqualTypeOf<number>()
|
|
6
|
+
expectTypeOf(
|
|
7
|
+
ZoneId.fromChainId(1_424_310_001, 42_431),
|
|
8
|
+
).toEqualTypeOf<number>()
|
|
9
|
+
expectTypeOf(ZoneId.toChainId(1)).toEqualTypeOf<number>()
|
|
10
|
+
expectTypeOf(ZoneId.toChainId(1, 42_431)).toEqualTypeOf<number>()
|
|
11
|
+
|
|
12
|
+
// @ts-expect-error Unsupported source ID.
|
|
13
|
+
ZoneId.fromChainId(421_700_001, 1)
|
|
14
|
+
// @ts-expect-error Unsupported source ID.
|
|
15
|
+
ZoneId.toChainId(1, 1)
|
|
16
|
+
})
|
package/tempo/ZoneId.test.ts
CHANGED
|
@@ -3,40 +3,75 @@ import * as ZoneId from './ZoneId.js'
|
|
|
3
3
|
|
|
4
4
|
describe('fromChainId', () => {
|
|
5
5
|
test('default', () => {
|
|
6
|
-
expect(ZoneId.
|
|
6
|
+
expect(ZoneId.chainIdBase).toMatchInlineSnapshot(`421700000`)
|
|
7
|
+
expect(ZoneId.fromChainId(421_700_001)).toMatchInlineSnapshot(`1`)
|
|
7
8
|
})
|
|
8
9
|
|
|
9
|
-
test('
|
|
10
|
-
expect(ZoneId.fromChainId(
|
|
10
|
+
test('Moderato', () => {
|
|
11
|
+
expect(ZoneId.fromChainId(1_424_310_028, 42_431)).toMatchInlineSnapshot(
|
|
12
|
+
`28`,
|
|
13
|
+
)
|
|
11
14
|
})
|
|
12
15
|
|
|
13
|
-
test('
|
|
14
|
-
expect(
|
|
16
|
+
test('unsupported source id', () => {
|
|
17
|
+
expect(() =>
|
|
18
|
+
// @ts-expect-error Unsupported source ID.
|
|
19
|
+
ZoneId.fromChainId(421_700_001, 1),
|
|
20
|
+
).toThrowErrorMatchingInlineSnapshot(`
|
|
21
|
+
[ZoneId.UnsupportedSourceIdError: Source chain ID "1" is not supported.
|
|
22
|
+
|
|
23
|
+
Supported source chain IDs: 4217, 42431.]
|
|
24
|
+
`)
|
|
15
25
|
})
|
|
16
26
|
})
|
|
17
27
|
|
|
18
28
|
describe('toChainId', () => {
|
|
19
29
|
test('default', () => {
|
|
20
|
-
expect(ZoneId.toChainId(
|
|
30
|
+
expect(ZoneId.toChainId(1)).toMatchInlineSnapshot(`421700001`)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('Moderato', () => {
|
|
34
|
+
expect(ZoneId.toChainId(28, 42_431)).toMatchInlineSnapshot(`1424310028`)
|
|
21
35
|
})
|
|
22
36
|
|
|
23
|
-
test('
|
|
24
|
-
expect(ZoneId.toChainId(
|
|
37
|
+
test('wraps within the source chain range', () => {
|
|
38
|
+
expect(ZoneId.toChainId(1_002_610_001)).toMatchInlineSnapshot(`421700001`)
|
|
39
|
+
expect(ZoneId.toChainId(723_173_649, 42_431)).toMatchInlineSnapshot(
|
|
40
|
+
`1424310001`,
|
|
41
|
+
)
|
|
25
42
|
})
|
|
26
43
|
|
|
27
|
-
test('
|
|
28
|
-
expect(
|
|
44
|
+
test('unsupported source id', () => {
|
|
45
|
+
expect(() =>
|
|
46
|
+
// @ts-expect-error Unsupported source ID.
|
|
47
|
+
ZoneId.toChainId(1, 1),
|
|
48
|
+
).toThrowErrorMatchingInlineSnapshot(`
|
|
49
|
+
[ZoneId.UnsupportedSourceIdError: Source chain ID "1" is not supported.
|
|
50
|
+
|
|
51
|
+
Supported source chain IDs: 4217, 42431.]
|
|
52
|
+
`)
|
|
29
53
|
})
|
|
30
54
|
})
|
|
31
55
|
|
|
32
56
|
describe('roundtrip', () => {
|
|
33
57
|
test('fromChainId → toChainId', () => {
|
|
34
|
-
const chainId =
|
|
35
|
-
expect(ZoneId.toChainId(ZoneId.fromChainId(chainId))).
|
|
58
|
+
const chainId = 421_700_006
|
|
59
|
+
expect(ZoneId.toChainId(ZoneId.fromChainId(chainId))).toMatchInlineSnapshot(
|
|
60
|
+
`421700006`,
|
|
61
|
+
)
|
|
36
62
|
})
|
|
37
63
|
|
|
38
64
|
test('toChainId → fromChainId', () => {
|
|
39
65
|
const zoneId = 42
|
|
40
|
-
expect(ZoneId.fromChainId(ZoneId.toChainId(zoneId))).
|
|
66
|
+
expect(ZoneId.fromChainId(ZoneId.toChainId(zoneId))).toMatchInlineSnapshot(
|
|
67
|
+
`42`,
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('Moderato', () => {
|
|
72
|
+
const zoneId = 42
|
|
73
|
+
const sourceId = 42_431
|
|
74
|
+
const chainId = ZoneId.toChainId(zoneId, sourceId)
|
|
75
|
+
expect(ZoneId.fromChainId(chainId, sourceId)).toMatchInlineSnapshot(`42`)
|
|
41
76
|
})
|
|
42
77
|
})
|