ox 0.14.27 → 0.14.29
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 +135 -0
- package/_cjs/tempo/MultisigConfig.js.map +1 -0
- package/_cjs/tempo/SignatureEnvelope.js +128 -6
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/index.js +2 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigConfig.js +363 -0
- package/_esm/tempo/MultisigConfig.js.map +1 -0
- package/_esm/tempo/SignatureEnvelope.js +237 -6
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/index.js +26 -0
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/tempo/MultisigConfig.d.ts +326 -0
- package/_types/tempo/MultisigConfig.d.ts.map +1 -0
- package/_types/tempo/SignatureEnvelope.d.ts +170 -8
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +26 -0
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/package.json +6 -1
- package/tempo/MultisigConfig/package.json +6 -0
- package/tempo/MultisigConfig.test.ts +236 -0
- package/tempo/MultisigConfig.ts +501 -0
- package/tempo/SignatureEnvelope.test.ts +311 -2
- package/tempo/SignatureEnvelope.ts +368 -20
- package/tempo/e2e.test.ts +201 -0
- package/tempo/index.ts +26 -0
- package/version.ts +1 -1
package/tempo/e2e.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { chain, client, fundAddress, nodeEnv } from '../../test/tempo/config.js'
|
|
|
14
14
|
import {
|
|
15
15
|
AuthorizationTempo,
|
|
16
16
|
KeyAuthorization,
|
|
17
|
+
MultisigConfig,
|
|
17
18
|
Period,
|
|
18
19
|
SignatureEnvelope,
|
|
19
20
|
} from './index.js'
|
|
@@ -2826,6 +2827,11 @@ describe('behavior: keyAuthorization', () => {
|
|
|
2826
2827
|
},
|
|
2827
2828
|
)
|
|
2828
2829
|
|
|
2830
|
+
// TIP-1049 admin keys are not yet in any released tempo tag (only on
|
|
2831
|
+
// `main` via PR #4265). CI runs the localnet job against the `edge`
|
|
2832
|
+
// image which tracks main; devnet/testnet still ship the older
|
|
2833
|
+
// release and would reject the 9-field wire format with
|
|
2834
|
+
// `failed to decode signed transaction`.
|
|
2829
2835
|
// TODO: remove skipIf when devnet/testnet have T6 (TIP-1049).
|
|
2830
2836
|
test.skipIf(nodeEnv !== 'localnet')(
|
|
2831
2837
|
'behavior: TIP-1049 admin access key round-trips through registration',
|
|
@@ -2993,3 +2999,198 @@ describe('behavior: keyAuthorization', () => {
|
|
|
2993
2999
|
},
|
|
2994
3000
|
)
|
|
2995
3001
|
})
|
|
3002
|
+
|
|
3003
|
+
// TODO: unskip once TIP-1061 native multisig is deployed to the standard
|
|
3004
|
+
// localnet/testnet nodes. Until then these only pass against the dedicated
|
|
3005
|
+
// PR-5178 devnet (run with VITE_TEMPO_RPC_URL pointed at it).
|
|
3006
|
+
describe.skip('behavior: multisig (TIP-1061)', () => {
|
|
3007
|
+
// Helper: builds a fresh set of secp256k1 owners + the derived config.
|
|
3008
|
+
function setup(parameters: { count: number; threshold: number }) {
|
|
3009
|
+
const { count, threshold } = parameters
|
|
3010
|
+
const ownerKeys = Array.from({ length: count }, () => {
|
|
3011
|
+
const privateKey = Secp256k1.randomPrivateKey()
|
|
3012
|
+
const address = Address.fromPublicKey(
|
|
3013
|
+
Secp256k1.getPublicKey({ privateKey }),
|
|
3014
|
+
)
|
|
3015
|
+
return { address, privateKey } as const
|
|
3016
|
+
})
|
|
3017
|
+
|
|
3018
|
+
const genesisConfig = MultisigConfig.from({
|
|
3019
|
+
// A fresh random salt yields a distinct account each run, exercising the
|
|
3020
|
+
// salt-inclusive config-ID derivation against the node.
|
|
3021
|
+
salt: Hex.random(32),
|
|
3022
|
+
threshold,
|
|
3023
|
+
owners: ownerKeys.map((key) => ({
|
|
3024
|
+
owner: key.address,
|
|
3025
|
+
weight: 1,
|
|
3026
|
+
})),
|
|
3027
|
+
})
|
|
3028
|
+
const account = MultisigConfig.getAddress(genesisConfig)
|
|
3029
|
+
|
|
3030
|
+
return { account, genesisConfig, ownerKeys } as const
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// Signs the multisig owner digest with the provided owner keys, returning
|
|
3034
|
+
// primitive approval envelopes ordered strictly ascending by recovered owner
|
|
3035
|
+
// address (required by the node: "recovered owners must be strictly
|
|
3036
|
+
// ascending").
|
|
3037
|
+
function approve(parameters: {
|
|
3038
|
+
genesisConfig: MultisigConfig.Config
|
|
3039
|
+
payload: Hex.Hex
|
|
3040
|
+
signers: readonly { privateKey: Hex.Hex }[]
|
|
3041
|
+
}) {
|
|
3042
|
+
const { genesisConfig, payload, signers } = parameters
|
|
3043
|
+
const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
|
|
3044
|
+
const signatures = signers.map((signer) =>
|
|
3045
|
+
SignatureEnvelope.from(
|
|
3046
|
+
Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
|
|
3047
|
+
),
|
|
3048
|
+
)
|
|
3049
|
+
return SignatureEnvelope.sortMultisigApprovals({
|
|
3050
|
+
genesisConfig,
|
|
3051
|
+
payload,
|
|
3052
|
+
signatures,
|
|
3053
|
+
})
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
test('behavior: bootstrap + spend (2-of-3 secp256k1)', async () => {
|
|
3057
|
+
const { account, genesisConfig, ownerKeys } = setup({
|
|
3058
|
+
count: 3,
|
|
3059
|
+
threshold: 2,
|
|
3060
|
+
})
|
|
3061
|
+
|
|
3062
|
+
// The derived multisig account pays its own fees.
|
|
3063
|
+
await fundAddress(client, { address: account })
|
|
3064
|
+
|
|
3065
|
+
// Bootstrap (first transaction): the bootstrap config travels in the
|
|
3066
|
+
// multisig signature `init`, nonce 0.
|
|
3067
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
3068
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
3069
|
+
chainId,
|
|
3070
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
3071
|
+
nonce: 0n,
|
|
3072
|
+
gas: 2_000_000n,
|
|
3073
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
3074
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
3075
|
+
})
|
|
3076
|
+
|
|
3077
|
+
const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
|
|
3078
|
+
signature: SignatureEnvelope.from({
|
|
3079
|
+
genesisConfig,
|
|
3080
|
+
// Initialize multisig.
|
|
3081
|
+
init: true,
|
|
3082
|
+
// Approve with 2 of the 3 owners to satisfy the threshold.
|
|
3083
|
+
signatures: approve({
|
|
3084
|
+
genesisConfig,
|
|
3085
|
+
payload: TxEnvelopeTempo.getSignPayload(bootstrap),
|
|
3086
|
+
signers: [ownerKeys[0]!, ownerKeys[1]!],
|
|
3087
|
+
}),
|
|
3088
|
+
}),
|
|
3089
|
+
})
|
|
3090
|
+
|
|
3091
|
+
const bootstrap_receipt = (await client
|
|
3092
|
+
.request({
|
|
3093
|
+
method: 'eth_sendRawTransactionSync',
|
|
3094
|
+
params: [bootstrap_signed],
|
|
3095
|
+
})
|
|
3096
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
3097
|
+
expect(bootstrap_receipt).toBeDefined()
|
|
3098
|
+
expect(bootstrap_receipt.status).toBe('success')
|
|
3099
|
+
expect(bootstrap_receipt.from).toBe(account)
|
|
3100
|
+
|
|
3101
|
+
{
|
|
3102
|
+
const response = await client
|
|
3103
|
+
.request({
|
|
3104
|
+
method: 'eth_getTransactionByHash',
|
|
3105
|
+
params: [bootstrap_receipt.transactionHash],
|
|
3106
|
+
})
|
|
3107
|
+
.then((tx) => Transaction.fromRpc(tx as any))
|
|
3108
|
+
if (!response) throw new Error()
|
|
3109
|
+
expect(response.from).toBe(account)
|
|
3110
|
+
expect(response.signature?.type).toBe('multisig')
|
|
3111
|
+
// The bootstrap config is carried by the multisig signature `init`.
|
|
3112
|
+
expect(
|
|
3113
|
+
(response.signature as SignatureEnvelope.Multisig | undefined)?.init,
|
|
3114
|
+
).toEqual(genesisConfig)
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// Spend (subsequent transaction): no signature `init`, nonce 1, uses the
|
|
3118
|
+
// stored config loaded by the node.
|
|
3119
|
+
const nonce = await getTransactionCount(client, {
|
|
3120
|
+
address: account,
|
|
3121
|
+
blockTag: 'pending',
|
|
3122
|
+
})
|
|
3123
|
+
|
|
3124
|
+
const spend = TxEnvelopeTempo.from({
|
|
3125
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
3126
|
+
chainId,
|
|
3127
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
3128
|
+
nonce: BigInt(nonce),
|
|
3129
|
+
gas: 2_000_000n,
|
|
3130
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
3131
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
3132
|
+
})
|
|
3133
|
+
|
|
3134
|
+
const spend_signed = TxEnvelopeTempo.serialize(spend, {
|
|
3135
|
+
signature: SignatureEnvelope.from({
|
|
3136
|
+
genesisConfig,
|
|
3137
|
+
// A different 2-of-3 subset still authorizes the transaction.
|
|
3138
|
+
signatures: approve({
|
|
3139
|
+
genesisConfig,
|
|
3140
|
+
payload: TxEnvelopeTempo.getSignPayload(spend),
|
|
3141
|
+
signers: [ownerKeys[1]!, ownerKeys[2]!],
|
|
3142
|
+
}),
|
|
3143
|
+
}),
|
|
3144
|
+
})
|
|
3145
|
+
|
|
3146
|
+
const spend_receipt = (await client
|
|
3147
|
+
.request({
|
|
3148
|
+
method: 'eth_sendRawTransactionSync',
|
|
3149
|
+
params: [spend_signed],
|
|
3150
|
+
})
|
|
3151
|
+
.then((tx) => TransactionReceipt.fromRpc(tx as any)))!
|
|
3152
|
+
expect(spend_receipt).toBeDefined()
|
|
3153
|
+
expect(spend_receipt.status).toBe('success')
|
|
3154
|
+
expect(spend_receipt.from).toBe(account)
|
|
3155
|
+
})
|
|
3156
|
+
|
|
3157
|
+
test('behavior: rejects below-threshold approvals', async () => {
|
|
3158
|
+
const { account, genesisConfig, ownerKeys } = setup({
|
|
3159
|
+
count: 3,
|
|
3160
|
+
threshold: 2,
|
|
3161
|
+
})
|
|
3162
|
+
|
|
3163
|
+
await fundAddress(client, { address: account })
|
|
3164
|
+
|
|
3165
|
+
const bootstrap = TxEnvelopeTempo.from({
|
|
3166
|
+
calls: [{ to: '0x0000000000000000000000000000000000000000' }],
|
|
3167
|
+
chainId,
|
|
3168
|
+
feeToken: '0x20c0000000000000000000000000000000000001',
|
|
3169
|
+
nonce: 0n,
|
|
3170
|
+
gas: 2_000_000n,
|
|
3171
|
+
maxFeePerGas: Value.fromGwei('20'),
|
|
3172
|
+
maxPriorityFeePerGas: Value.fromGwei('10'),
|
|
3173
|
+
})
|
|
3174
|
+
|
|
3175
|
+
const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
|
|
3176
|
+
signature: SignatureEnvelope.from({
|
|
3177
|
+
genesisConfig,
|
|
3178
|
+
// Opt into bootstrap: writes `genesisConfig` into the signature `init`.
|
|
3179
|
+
init: true,
|
|
3180
|
+
// Only one approval — below the threshold of 2.
|
|
3181
|
+
signatures: approve({
|
|
3182
|
+
genesisConfig,
|
|
3183
|
+
payload: TxEnvelopeTempo.getSignPayload(bootstrap),
|
|
3184
|
+
signers: [ownerKeys[0]!],
|
|
3185
|
+
}),
|
|
3186
|
+
}),
|
|
3187
|
+
})
|
|
3188
|
+
|
|
3189
|
+
await expect(
|
|
3190
|
+
client.request({
|
|
3191
|
+
method: 'eth_sendRawTransactionSync',
|
|
3192
|
+
params: [serialized_signed],
|
|
3193
|
+
}),
|
|
3194
|
+
).rejects.toThrow()
|
|
3195
|
+
})
|
|
3196
|
+
})
|
package/tempo/index.ts
CHANGED
|
@@ -102,6 +102,32 @@ export * as Channel from './Channel.js'
|
|
|
102
102
|
* @category Reference
|
|
103
103
|
*/
|
|
104
104
|
export * as KeyAuthorization from './KeyAuthorization.js'
|
|
105
|
+
/**
|
|
106
|
+
* Native multisig account utilities (TIP-1061).
|
|
107
|
+
*
|
|
108
|
+
* Derives stable multisig account addresses and permanent config IDs from a weighted
|
|
109
|
+
* owner configuration, and computes the owner approval digest that owners sign.
|
|
110
|
+
*
|
|
111
|
+
* [TIP-1061](https://tips.sh/1061)
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts twoslash
|
|
115
|
+
* import { MultisigConfig } from 'ox/tempo'
|
|
116
|
+
*
|
|
117
|
+
* const genesisConfig = MultisigConfig.from({
|
|
118
|
+
* threshold: 2,
|
|
119
|
+
* owners: [
|
|
120
|
+
* { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
|
|
121
|
+
* { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
|
|
122
|
+
* ],
|
|
123
|
+
* })
|
|
124
|
+
*
|
|
125
|
+
* const account = MultisigConfig.getAddress(genesisConfig)
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @category Reference
|
|
129
|
+
*/
|
|
130
|
+
export * as MultisigConfig from './MultisigConfig.js'
|
|
105
131
|
/**
|
|
106
132
|
* Utilities for constructing period durations (in seconds) for recurring spending limits.
|
|
107
133
|
*
|
package/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export const version = '0.14.
|
|
2
|
+
export const version = '0.14.29'
|