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/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.27'
2
+ export const version = '0.14.29'