ox 1.0.1 → 1.0.3

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/tempo/KeyAuthorization.d.ts +22 -16
  3. package/dist/tempo/KeyAuthorization.d.ts.map +1 -1
  4. package/dist/tempo/KeyAuthorization.js +31 -7
  5. package/dist/tempo/KeyAuthorization.js.map +1 -1
  6. package/dist/tempo/MultisigConfig.d.ts +8 -4
  7. package/dist/tempo/MultisigConfig.d.ts.map +1 -1
  8. package/dist/tempo/MultisigConfig.js +16 -4
  9. package/dist/tempo/MultisigConfig.js.map +1 -1
  10. package/dist/tempo/SignatureEnvelope.d.ts +31 -13
  11. package/dist/tempo/SignatureEnvelope.d.ts.map +1 -1
  12. package/dist/tempo/SignatureEnvelope.js +172 -55
  13. package/dist/tempo/SignatureEnvelope.js.map +1 -1
  14. package/dist/zod/tempo/AuthorizationTempo.d.ts +70 -60
  15. package/dist/zod/tempo/AuthorizationTempo.d.ts.map +1 -1
  16. package/dist/zod/tempo/KeyAuthorization.d.ts +0 -306
  17. package/dist/zod/tempo/KeyAuthorization.d.ts.map +1 -1
  18. package/dist/zod/tempo/KeyAuthorization.js +2 -2
  19. package/dist/zod/tempo/KeyAuthorization.js.map +1 -1
  20. package/dist/zod/tempo/MultisigConfig.d.ts.map +1 -1
  21. package/dist/zod/tempo/MultisigConfig.js +5 -2
  22. package/dist/zod/tempo/MultisigConfig.js.map +1 -1
  23. package/dist/zod/tempo/RpcSchemaTempo.d.ts +28 -132
  24. package/dist/zod/tempo/RpcSchemaTempo.d.ts.map +1 -1
  25. package/dist/zod/tempo/SignatureEnvelope.d.ts +79 -18
  26. package/dist/zod/tempo/SignatureEnvelope.d.ts.map +1 -1
  27. package/dist/zod/tempo/SignatureEnvelope.js +39 -11
  28. package/dist/zod/tempo/SignatureEnvelope.js.map +1 -1
  29. package/dist/zod/tempo/Transaction.d.ts +84 -396
  30. package/dist/zod/tempo/Transaction.d.ts.map +1 -1
  31. package/dist/zod/tempo/TransactionRequest.d.ts +42 -234
  32. package/dist/zod/tempo/TransactionRequest.d.ts.map +1 -1
  33. package/dist/zod/tempo/TxEnvelopeTempo.d.ts +0 -108
  34. package/dist/zod/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  35. package/package.json +2 -2
  36. package/src/tempo/KeyAuthorization.test-d.ts +62 -0
  37. package/src/tempo/KeyAuthorization.test.ts +53 -0
  38. package/src/tempo/KeyAuthorization.ts +69 -27
  39. package/src/tempo/MultisigConfig.test.ts +34 -2
  40. package/src/tempo/MultisigConfig.ts +18 -4
  41. package/src/tempo/SignatureEnvelope.test-d.ts +97 -0
  42. package/src/tempo/SignatureEnvelope.test.ts +247 -36
  43. package/src/tempo/SignatureEnvelope.ts +271 -85
  44. package/src/tempo/e2e.test.ts +0 -416
  45. package/src/tempo/multisig.e2e.test.ts +582 -0
  46. package/src/version.ts +1 -1
  47. package/src/zod/tempo/KeyAuthorization.ts +2 -2
  48. package/src/zod/tempo/MultisigConfig.ts +13 -5
  49. package/src/zod/tempo/SignatureEnvelope.ts +81 -18
  50. package/src/zod/tempo/_test/KeyAuthorization.test.ts +25 -0
  51. package/src/zod/tempo/_test/MultisigConfig.test.ts +44 -0
  52. package/src/zod/tempo/_test/SignatureEnvelope.test-d.ts +9 -0
  53. package/src/zod/tempo/_test/SignatureEnvelope.test.ts +142 -6
@@ -14,7 +14,6 @@ import { chain, client, fundAddress, nodeEnv } from '../../test/tempo/config.js'
14
14
  import {
15
15
  AuthorizationTempo,
16
16
  KeyAuthorization,
17
- MultisigConfig,
18
17
  Period,
19
18
  SignatureEnvelope,
20
19
  } from './index.js'
@@ -2968,418 +2967,3 @@ describe('behavior: keyAuthorization', () => {
2968
2967
  },
2969
2968
  )
2970
2969
  })
2971
-
2972
- // TODO: unskip once TIP-1061 native multisig is deployed to the standard
2973
- // localnet/testnet nodes. Until then these only pass against the dedicated
2974
- // PR-5178 devnet (run with VITE_TEMPO_RPC_URL pointed at it).
2975
- describe.skip('behavior: multisig (TIP-1061)', () => {
2976
- // Helper: builds a fresh set of secp256k1 owners + the derived config.
2977
- function setup(parameters: { count: number; threshold: number }) {
2978
- const { count, threshold } = parameters
2979
- const ownerKeys = Array.from({ length: count }, () => {
2980
- const privateKey = Secp256k1.randomPrivateKey()
2981
- const address = Address.fromPublicKey(
2982
- Secp256k1.getPublicKey({ privateKey }),
2983
- )
2984
- return { address, privateKey } as const
2985
- })
2986
-
2987
- const genesisConfig = MultisigConfig.from({
2988
- // A fresh random salt yields a distinct account each run, exercising the
2989
- // salt-inclusive account derivation against the node.
2990
- salt: Hex.random(32),
2991
- threshold,
2992
- owners: ownerKeys.map((key) => ({
2993
- owner: key.address,
2994
- weight: 1,
2995
- })),
2996
- })
2997
- const account = MultisigConfig.getAddress(genesisConfig)
2998
-
2999
- return { account, genesisConfig, ownerKeys } as const
3000
- }
3001
-
3002
- // Signs the multisig owner digest with the provided owner keys, returning
3003
- // primitive approval envelopes ordered strictly ascending by recovered owner
3004
- // address (required by the node: "recovered owners must be strictly
3005
- // ascending").
3006
- function approve(parameters: {
3007
- genesisConfig: MultisigConfig.Config
3008
- payload: Hex.Hex
3009
- signers: readonly { privateKey: Hex.Hex }[]
3010
- }) {
3011
- const { genesisConfig, payload, signers } = parameters
3012
- const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
3013
- const signatures = signers.map((signer) =>
3014
- SignatureEnvelope.from(
3015
- Secp256k1.sign({ payload: digest, privateKey: signer.privateKey }),
3016
- ),
3017
- )
3018
- return SignatureEnvelope.sortMultisigApprovals({
3019
- genesisConfig,
3020
- payload,
3021
- signatures,
3022
- })
3023
- }
3024
-
3025
- test('behavior: bootstrap + spend (2-of-3 secp256k1)', async () => {
3026
- const { account, genesisConfig, ownerKeys } = setup({
3027
- count: 3,
3028
- threshold: 2,
3029
- })
3030
-
3031
- // The derived multisig account pays its own fees.
3032
- await fundAddress(client, { address: account })
3033
-
3034
- // Bootstrap (first transaction): the bootstrap config travels in the
3035
- // multisig signature `init`, nonce 0.
3036
- const bootstrap = TxEnvelopeTempo.from({
3037
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
3038
- chainId,
3039
- feeToken: '0x20c0000000000000000000000000000000000001',
3040
- nonce: 0n,
3041
- gas: 2_000_000n,
3042
- maxFeePerGas: Value.fromGwei('20'),
3043
- maxPriorityFeePerGas: Value.fromGwei('10'),
3044
- })
3045
-
3046
- const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
3047
- signature: SignatureEnvelope.from({
3048
- genesisConfig,
3049
- // Initialize multisig.
3050
- init: true,
3051
- // Approve with 2 of the 3 owners to satisfy the threshold.
3052
- signatures: approve({
3053
- genesisConfig,
3054
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
3055
- signers: [ownerKeys[0]!, ownerKeys[1]!],
3056
- }),
3057
- }),
3058
- })
3059
-
3060
- const bootstrap_receipt = (await client
3061
- .request({
3062
- method: 'eth_sendRawTransactionSync',
3063
- params: [bootstrap_signed],
3064
- })
3065
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
3066
- expect(bootstrap_receipt).toBeDefined()
3067
- expect(bootstrap_receipt.status).toBe('success')
3068
- expect(bootstrap_receipt.from).toBe(account)
3069
-
3070
- {
3071
- const response = await client
3072
- .request({
3073
- method: 'eth_getTransactionByHash',
3074
- params: [bootstrap_receipt.transactionHash],
3075
- })
3076
- .then((tx) => Transaction.fromRpc(tx as any))
3077
- if (!response) throw new Error()
3078
- expect(response.from).toBe(account)
3079
- expect(response.signature?.type).toBe('multisig')
3080
- // The bootstrap config is carried by the multisig signature `init`.
3081
- expect(
3082
- (response.signature as SignatureEnvelope.Multisig | undefined)?.init,
3083
- ).toEqual(genesisConfig)
3084
- }
3085
-
3086
- // Spend (subsequent transaction): no signature `init`, nonce 1, uses the
3087
- // stored config loaded by the node.
3088
- const nonce = await getTransactionCount(client, {
3089
- address: account,
3090
- blockTag: 'pending',
3091
- })
3092
-
3093
- const spend = TxEnvelopeTempo.from({
3094
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
3095
- chainId,
3096
- feeToken: '0x20c0000000000000000000000000000000000001',
3097
- nonce: BigInt(nonce),
3098
- gas: 2_000_000n,
3099
- maxFeePerGas: Value.fromGwei('20'),
3100
- maxPriorityFeePerGas: Value.fromGwei('10'),
3101
- })
3102
-
3103
- const spend_signed = TxEnvelopeTempo.serialize(spend, {
3104
- signature: SignatureEnvelope.from({
3105
- genesisConfig,
3106
- // A different 2-of-3 subset still authorizes the transaction.
3107
- signatures: approve({
3108
- genesisConfig,
3109
- payload: TxEnvelopeTempo.getSignPayload(spend),
3110
- signers: [ownerKeys[1]!, ownerKeys[2]!],
3111
- }),
3112
- }),
3113
- })
3114
-
3115
- const spend_receipt = (await client
3116
- .request({
3117
- method: 'eth_sendRawTransactionSync',
3118
- params: [spend_signed],
3119
- })
3120
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
3121
- expect(spend_receipt).toBeDefined()
3122
- expect(spend_receipt.status).toBe('success')
3123
- expect(spend_receipt.from).toBe(account)
3124
- })
3125
-
3126
- test('behavior: rejects below-threshold approvals', async () => {
3127
- const { account, genesisConfig, ownerKeys } = setup({
3128
- count: 3,
3129
- threshold: 2,
3130
- })
3131
-
3132
- await fundAddress(client, { address: account })
3133
-
3134
- const bootstrap = TxEnvelopeTempo.from({
3135
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
3136
- chainId,
3137
- feeToken: '0x20c0000000000000000000000000000000000001',
3138
- nonce: 0n,
3139
- gas: 2_000_000n,
3140
- maxFeePerGas: Value.fromGwei('20'),
3141
- maxPriorityFeePerGas: Value.fromGwei('10'),
3142
- })
3143
-
3144
- const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
3145
- signature: SignatureEnvelope.from({
3146
- genesisConfig,
3147
- // Opt into bootstrap: writes `genesisConfig` into the signature `init`.
3148
- init: true,
3149
- // Only one approval — below the threshold of 2.
3150
- signatures: approve({
3151
- genesisConfig,
3152
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
3153
- signers: [ownerKeys[0]!],
3154
- }),
3155
- }),
3156
- })
3157
-
3158
- await expect(
3159
- client.request({
3160
- method: 'eth_sendRawTransactionSync',
3161
- params: [serialized_signed],
3162
- }),
3163
- ).rejects.toThrow()
3164
- })
3165
-
3166
- test('behavior: keychain access key (authorize via AccountKeychain + spend)', async () => {
3167
- const { account, genesisConfig, ownerKeys } = setup({
3168
- count: 2,
3169
- threshold: 2,
3170
- })
3171
-
3172
- await fundAddress(client, { address: account })
3173
-
3174
- // Fresh secp256k1 access key for the multisig account.
3175
- const access = (() => {
3176
- const privateKey = Secp256k1.randomPrivateKey()
3177
- const address = Address.fromPublicKey(
3178
- Secp256k1.getPublicKey({ privateKey }),
3179
- )
3180
- return { address, privateKey } as const
3181
- })()
3182
-
3183
- // Bootstrap the multisig (nonce 0).
3184
- const bootstrap = TxEnvelopeTempo.from({
3185
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
3186
- chainId,
3187
- feeToken: '0x20c0000000000000000000000000000000000001',
3188
- nonce: 0n,
3189
- gas: 2_000_000n,
3190
- maxFeePerGas: Value.fromGwei('20'),
3191
- maxPriorityFeePerGas: Value.fromGwei('10'),
3192
- })
3193
-
3194
- const bootstrap_signed = TxEnvelopeTempo.serialize(bootstrap, {
3195
- signature: SignatureEnvelope.from({
3196
- genesisConfig,
3197
- init: true,
3198
- signatures: approve({
3199
- genesisConfig,
3200
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
3201
- signers: ownerKeys,
3202
- }),
3203
- }),
3204
- })
3205
-
3206
- const bootstrap_receipt = (await client
3207
- .request({
3208
- method: 'eth_sendRawTransactionSync',
3209
- params: [bootstrap_signed],
3210
- })
3211
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
3212
- expect(bootstrap_receipt.status).toBe('success')
3213
-
3214
- // Authorize the access key (nonce 1): multisig transactions cannot carry
3215
- // a TIP-1049 `keyAuthorization`, so the multisig account calls the
3216
- // AccountKeychain precompile's `authorizeKey` directly.
3217
- const authorizeKey = AbiFunction.from(
3218
- 'function authorizeKey(address keyId, uint8 signatureType, (uint64 expiry, bool enforceLimits, (address token, uint256 amount, uint64 period)[] limits, bool allowAnyCalls, (address target, (bytes4 selector, address[] recipients)[] selectorRules)[] allowedCalls) config)',
3219
- )
3220
-
3221
- const authorize = TxEnvelopeTempo.from({
3222
- calls: [
3223
- {
3224
- to: '0xaaaaaaaa00000000000000000000000000000000',
3225
- data: AbiFunction.encodeData(authorizeKey, [
3226
- access.address,
3227
- 0, // secp256k1
3228
- {
3229
- expiry: 18446744073709551615n, // u64 max (never expires)
3230
- enforceLimits: false,
3231
- limits: [],
3232
- allowAnyCalls: true,
3233
- allowedCalls: [],
3234
- },
3235
- ]),
3236
- },
3237
- ],
3238
- chainId,
3239
- feeToken: '0x20c0000000000000000000000000000000000001',
3240
- nonce: 1n,
3241
- gas: 2_000_000n,
3242
- maxFeePerGas: Value.fromGwei('20'),
3243
- maxPriorityFeePerGas: Value.fromGwei('10'),
3244
- })
3245
-
3246
- const authorize_signed = TxEnvelopeTempo.serialize(authorize, {
3247
- signature: SignatureEnvelope.from({
3248
- genesisConfig,
3249
- signatures: approve({
3250
- genesisConfig,
3251
- payload: TxEnvelopeTempo.getSignPayload(authorize),
3252
- signers: ownerKeys,
3253
- }),
3254
- }),
3255
- })
3256
-
3257
- const authorize_receipt = (await client
3258
- .request({
3259
- method: 'eth_sendRawTransactionSync',
3260
- params: [authorize_signed],
3261
- })
3262
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
3263
- expect(authorize_receipt.status).toBe('success')
3264
- expect(authorize_receipt.from).toBe(account)
3265
-
3266
- // Spend with the access key (nonce 2): outer keychain signature with the
3267
- // multisig account as `userAddress` — no owner approvals involved.
3268
- const nonce = await getTransactionCount(client, {
3269
- address: account,
3270
- blockTag: 'pending',
3271
- })
3272
-
3273
- const spend = TxEnvelopeTempo.from({
3274
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
3275
- chainId,
3276
- feeToken: '0x20c0000000000000000000000000000000000001',
3277
- nonce: BigInt(nonce),
3278
- gas: 2_000_000n,
3279
- maxFeePerGas: Value.fromGwei('20'),
3280
- maxPriorityFeePerGas: Value.fromGwei('10'),
3281
- })
3282
-
3283
- const spend_signature = Secp256k1.sign({
3284
- payload: TxEnvelopeTempo.getSignPayload(spend, { from: account }),
3285
- privateKey: access.privateKey,
3286
- })
3287
-
3288
- const spend_signed = TxEnvelopeTempo.serialize(spend, {
3289
- signature: SignatureEnvelope.from({
3290
- userAddress: account,
3291
- inner: SignatureEnvelope.from(spend_signature),
3292
- type: 'keychain',
3293
- }),
3294
- })
3295
-
3296
- const spend_receipt = (await client
3297
- .request({
3298
- method: 'eth_sendRawTransactionSync',
3299
- params: [spend_signed],
3300
- })
3301
- .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
3302
- expect(spend_receipt.status).toBe('success')
3303
- expect(spend_receipt.from).toBe(account)
3304
-
3305
- {
3306
- const response = await client
3307
- .request({
3308
- method: 'eth_getTransactionByHash',
3309
- params: [spend_receipt.transactionHash],
3310
- })
3311
- .then((tx) => Transaction.fromRpc(tx as any))
3312
- if (!response) throw new Error()
3313
- expect(response.from).toBe(account)
3314
- expect(response.signature?.type).toBe('keychain')
3315
- expect(
3316
- (response.signature as SignatureEnvelope.Keychain | undefined)
3317
- ?.userAddress,
3318
- ).toBe(account)
3319
- }
3320
- })
3321
-
3322
- test('behavior: rejects `keyAuthorization` on multisig transactions', async () => {
3323
- const { account, genesisConfig, ownerKeys } = setup({
3324
- count: 1,
3325
- threshold: 1,
3326
- })
3327
-
3328
- await fundAddress(client, { address: account })
3329
-
3330
- const access = (() => {
3331
- const privateKey = Secp256k1.randomPrivateKey()
3332
- const address = Address.fromPublicKey(
3333
- Secp256k1.getPublicKey({ privateKey }),
3334
- )
3335
- return { address, privateKey } as const
3336
- })()
3337
-
3338
- // TIP-1049 key authorizations are only valid on primitive/keychain
3339
- // transactions; multisig accounts must call the AccountKeychain
3340
- // precompile instead.
3341
- const keyAuth = KeyAuthorization.from({
3342
- address: access.address,
3343
- chainId: BigInt(chainId),
3344
- type: 'secp256k1',
3345
- })
3346
- const keyAuth_signed = KeyAuthorization.from(keyAuth, {
3347
- signature: SignatureEnvelope.from(
3348
- Secp256k1.sign({
3349
- payload: KeyAuthorization.getSignPayload(keyAuth),
3350
- privateKey: ownerKeys[0]!.privateKey,
3351
- }),
3352
- ),
3353
- })
3354
-
3355
- const bootstrap = TxEnvelopeTempo.from({
3356
- calls: [{ to: '0x0000000000000000000000000000000000000000' }],
3357
- chainId,
3358
- feeToken: '0x20c0000000000000000000000000000000000001',
3359
- keyAuthorization: keyAuth_signed,
3360
- nonce: 0n,
3361
- gas: 2_000_000n,
3362
- maxFeePerGas: Value.fromGwei('20'),
3363
- maxPriorityFeePerGas: Value.fromGwei('10'),
3364
- })
3365
-
3366
- const serialized_signed = TxEnvelopeTempo.serialize(bootstrap, {
3367
- signature: SignatureEnvelope.from({
3368
- genesisConfig,
3369
- init: true,
3370
- signatures: approve({
3371
- genesisConfig,
3372
- payload: TxEnvelopeTempo.getSignPayload(bootstrap),
3373
- signers: ownerKeys,
3374
- }),
3375
- }),
3376
- })
3377
-
3378
- await expect(
3379
- client.request({
3380
- method: 'eth_sendRawTransactionSync',
3381
- params: [serialized_signed],
3382
- }),
3383
- ).rejects.toThrow()
3384
- })
3385
- })