From a128253b0d214b6460998f1ca194621209034e09 Mon Sep 17 00:00:00 2001 From: pbio <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:44:15 +0000 Subject: [PATCH 1/6] feat(infra): migrate keyfunder to standalone Docker package Migrates the keyfunder from embedded infra code to a standalone package at typescript/keyfunder/ with its own Docker image, following the pattern established by ccip-server, rebalancer, and warp-monitor. Key changes: - New standalone package with config-driven YAML configuration - Removed L2 bridging - keyfunder is now a pure fund dispersal tool - No GCP SDK dependency - secrets injected via K8s ExternalSecrets - Uses Prometheus push gateway for metrics (CronJob pattern) - Registry URI with commit pinning via REGISTRY_URI env var - RPC overrides via RPC_URL_ env vars Helm chart updates: - ConfigMap for keyfunder.yaml config - Updated CronJob to run standalone Docker image - Simplified ExternalSecret for funder key and RPC URLs Deleted: - fund-keys-from-deployer.ts (37KB legacy script) - addresses-external-secret.yaml (addresses now in config YAML) --- pnpm-lock.yaml | 507 +++---- turbo.json | 2 +- .../templates/addresses-external-secret.yaml | 31 - .../helm/key-funder/templates/configmap.yaml | 9 + .../helm/key-funder/templates/cron-job.yaml | 67 +- .../templates/env-var-external-secret.yaml | 17 +- typescript/infra/helm/key-funder/values.yaml | 17 +- .../scripts/funding/deploy-key-funder.ts | 28 +- .../funding/fund-keys-from-deployer.ts | 1207 ----------------- .../infra/scripts/funding/reclaim-from-igp.ts | 2 +- typescript/infra/src/funding/key-funder.ts | 214 ++- typescript/infra/src/utils/rpcUrls.ts | 3 +- typescript/keyfunder/.gitignore | 7 + typescript/keyfunder/.mocharc.json | 3 + typescript/keyfunder/Dockerfile | 73 + typescript/keyfunder/README.md | 156 +++ typescript/keyfunder/eslint.config.mjs | 13 + typescript/keyfunder/package.json | 63 + .../keyfunder/scripts/ncc.post-bundle.mjs | 37 + .../src/config/KeyFunderConfig.test.ts | 176 +++ .../keyfunder/src/config/KeyFunderConfig.ts | 55 + typescript/keyfunder/src/config/types.test.ts | 229 ++++ typescript/keyfunder/src/config/types.ts | 94 ++ typescript/keyfunder/src/core/KeyFunder.ts | 325 +++++ typescript/keyfunder/src/index.ts | 23 + .../keyfunder/src/metrics/Metrics.test.ts | 125 ++ typescript/keyfunder/src/metrics/Metrics.ts | 118 ++ typescript/keyfunder/src/service.ts | 121 ++ typescript/keyfunder/tsconfig.json | 8 + 29 files changed, 2082 insertions(+), 1648 deletions(-) delete mode 100644 typescript/infra/helm/key-funder/templates/addresses-external-secret.yaml create mode 100644 typescript/infra/helm/key-funder/templates/configmap.yaml delete mode 100644 typescript/infra/scripts/funding/fund-keys-from-deployer.ts create mode 100644 typescript/keyfunder/.gitignore create mode 100644 typescript/keyfunder/.mocharc.json create mode 100644 typescript/keyfunder/Dockerfile create mode 100644 typescript/keyfunder/README.md create mode 100644 typescript/keyfunder/eslint.config.mjs create mode 100644 typescript/keyfunder/package.json create mode 100644 typescript/keyfunder/scripts/ncc.post-bundle.mjs create mode 100644 typescript/keyfunder/src/config/KeyFunderConfig.test.ts create mode 100644 typescript/keyfunder/src/config/KeyFunderConfig.ts create mode 100644 typescript/keyfunder/src/config/types.test.ts create mode 100644 typescript/keyfunder/src/config/types.ts create mode 100644 typescript/keyfunder/src/core/KeyFunder.ts create mode 100644 typescript/keyfunder/src/index.ts create mode 100644 typescript/keyfunder/src/metrics/Metrics.test.ts create mode 100644 typescript/keyfunder/src/metrics/Metrics.ts create mode 100644 typescript/keyfunder/src/service.ts create mode 100644 typescript/keyfunder/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef571968580..fafa5be4b6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1596,6 +1596,88 @@ importers: specifier: 'catalog:' version: 5.8.3 + typescript/keyfunder: + dependencies: + '@google-cloud/pino-logging-gcp-config': + specifier: 'catalog:' + version: 1.3.0 + '@hyperlane-xyz/core': + specifier: workspace:* + version: link:../../solidity + '@hyperlane-xyz/registry': + specifier: 'catalog:' + version: 23.7.0 + '@hyperlane-xyz/sdk': + specifier: workspace:* + version: link:../sdk + '@hyperlane-xyz/utils': + specifier: workspace:* + version: link:../utils + ethers: + specifier: 'catalog:' + version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + pino: + specifier: 'catalog:' + version: 8.21.0 + pino-pretty: + specifier: 'catalog:' + version: 13.1.2 + prom-client: + specifier: 'catalog:' + version: 14.2.0 + yaml: + specifier: 'catalog:' + version: 2.4.5 + zod: + specifier: 'catalog:' + version: 3.25.76 + zod-validation-error: + specifier: 'catalog:' + version: 3.5.4(zod@3.25.76) + devDependencies: + '@hyperlane-xyz/tsconfig': + specifier: workspace:^ + version: link:../tsconfig + '@types/chai-as-promised': + specifier: 'catalog:' + version: 8.0.2 + '@types/mocha': + specifier: 'catalog:' + version: 10.0.10 + '@types/node': + specifier: 'catalog:' + version: 20.19.25 + '@types/sinon': + specifier: 'catalog:' + version: 17.0.4 + '@vercel/ncc': + specifier: 'catalog:' + version: 0.38.4 + chai: + specifier: 'catalog:' + version: 4.5.0 + chai-as-promised: + specifier: 'catalog:' + version: 8.0.2(chai@4.5.0) + eslint: + specifier: 'catalog:' + version: 9.31.0(jiti@2.6.1) + mocha: + specifier: 'catalog:' + version: 11.7.5 + prettier: + specifier: 'catalog:' + version: 3.5.3 + sinon: + specifier: 'catalog:' + version: 13.0.2 + tsx: + specifier: 'catalog:' + version: 4.19.1 + typescript: + specifier: 'catalog:' + version: 5.8.3 + typescript/provider-sdk: dependencies: '@hyperlane-xyz/utils': @@ -2229,7 +2311,7 @@ importers: version: 2.2.1(typescript@5.8.3) '@rainbow-me/rainbowkit': specifier: ^2.2.0 - version: 2.2.9(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(wagmi@2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12)) + version: 2.2.9(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) '@solana/wallet-adapter-react': specifier: ^0.15.32 version: 0.15.39(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(bs58@6.0.0)(fastestsmallesttextencoderdecoder@1.0.22)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3) @@ -2247,7 +2329,7 @@ importers: version: 3.7.4(bufferutil@4.0.9)(get-starknet-core@4.0.0)(react@18.3.1)(starknet@7.6.4)(typescript@5.8.3)(utf-8-validate@5.0.10) '@wagmi/core': specifier: ^2.12.26 - version: 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + version: 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -2265,13 +2347,13 @@ importers: version: 7.6.4 starknetkit: specifier: 2.6.1 - version: 2.6.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(starknet@7.6.4)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + version: 2.6.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(starknet@7.6.4)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) viem: specifier: 'catalog:' - version: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + version: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.12.26 - version: 2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12) + version: 2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) yaml: specifier: 'catalog:' version: 2.4.5 @@ -14040,10 +14122,6 @@ packages: resolution: {integrity: sha512-Yxz2kRwT90aPiWEMHVYnEf4+rhwF1tBmmZ4KepCP+Wkium9JxtWnUm1nqGwpiAHr/tnTSeHqr3wb++jgSkXjhA==} engines: {node: '>=6'} - punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -18041,16 +18119,16 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@base-org/account@2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12)': + '@base-org/account@2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)': dependencies: '@coinbase/cdp-sdk': 1.38.6(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.8.3)(zod@4.1.12) + ox: 0.6.9(typescript@5.8.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' @@ -18404,15 +18482,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@18.3.27)(bufferutil@4.0.9)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.1.12)': + '@coinbase/wallet-sdk@4.3.6(@types/react@18.3.27)(bufferutil@4.0.9)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.8.3)(zod@4.1.12) + ox: 0.6.9(typescript@5.8.3)(zod@3.25.76) preact: 10.24.2 - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.3(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) transitivePeerDependencies: - '@types/react' @@ -19822,11 +19900,11 @@ snapshots: optionalDependencies: '@trufflesuite/bigint-buffer': 1.1.9 - '@gemini-wallet/core@0.3.2(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))': + '@gemini-wallet/core@0.3.2(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color @@ -21726,7 +21804,7 @@ snapshots: reflect-metadata: 0.1.13 secp256k1: 5.0.0 - '@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(wagmi@2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12))': + '@rainbow-me/rainbowkit@2.2.9(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))': dependencies: '@tanstack/react-query': 5.90.10(react@18.3.1) '@vanilla-extract/css': 1.17.3(babel-plugin-macros@3.1.0) @@ -21738,8 +21816,8 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.6.2(@types/react@18.3.27)(react@18.3.1) ua-parser-js: 1.0.41 - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - wagmi: 2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - babel-plugin-macros @@ -22796,24 +22874,24 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@reown/appkit-controllers@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -22842,12 +22920,12 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@reown/appkit-pay@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@4.1.12) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@3.25.76) lit: 3.3.0 valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) transitivePeerDependencies: @@ -22882,12 +22960,12 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@4.1.12)': + '@reown/appkit-scaffold-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@4.1.12) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: @@ -22919,10 +22997,10 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@reown/appkit-ui@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 @@ -22954,16 +23032,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@4.1.12)': + '@reown/appkit-utils@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23003,21 +23081,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@reown/appkit@1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-pay': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@4.1.12) - '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@4.1.12) + '@reown/appkit-scaffold-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@18.3.27)(react@18.3.1))(zod@3.25.76) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/universal-provider': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 valtio: 1.13.2(@types/react@18.3.27)(react@18.3.1) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23185,9 +23263,9 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -23195,10 +23273,10 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -25231,7 +25309,7 @@ snapshots: '@turnkey/webauthn-stamper': 0.6.0 '@wallet-standard/app': 1.1.0 '@wallet-standard/base': 1.1.0 - '@walletconnect/sign-client': 2.23.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) cross-fetch: 3.2.0 ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -25484,7 +25562,7 @@ snapshots: '@types/cors@2.8.19': dependencies: - '@types/node': 20.19.25 + '@types/node': 22.19.1 '@types/cross-spawn@6.0.6': dependencies: @@ -25707,7 +25785,7 @@ snapshots: '@types/prompts@2.4.9': dependencies: - '@types/node': 20.19.25 + '@types/node': 22.19.1 kleur: 3.0.3 '@types/prop-types@15.7.15': {} @@ -25814,7 +25892,7 @@ snapshots: '@types/unzipper@0.10.11': dependencies: - '@types/node': 20.19.25 + '@types/node': 22.19.1 '@types/uuid@8.3.4': {} @@ -26249,19 +26327,19 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 - '@wagmi/connectors@6.1.4(0412c5480cb372a861c205176362b8d1)': + '@wagmi/connectors@6.1.4(3015cd4f3cc533a2a82d169692de7690)': dependencies: - '@base-org/account': 2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12) - '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.27)(bufferutil@4.0.9)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@4.1.12) - '@gemini-wallet/core': 0.3.2(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + '@base-org/account': 2.4.0(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@18.3.27)(bufferutil@4.0.9)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)) - '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(52953fff89e02f75760b90db6005bc45) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + porto: 0.2.35(f8a5cd75b5ca2acd10c494cdcd5e46bd) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -26303,11 +26381,11 @@ snapshots: - ws - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.8.3) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) zustand: 5.0.0(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: '@tanstack/query-core': 5.90.10 @@ -26345,7 +26423,7 @@ snapshots: dependencies: '@wallet-standard/base': 1.1.0 - '@walletconnect/core@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/core@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -26359,7 +26437,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -26389,7 +26467,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/core@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -26403,7 +26481,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -26433,7 +26511,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/core@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 @@ -26447,51 +26525,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(typescript@5.8.3)(zod@4.1.12) - '@walletconnect/window-getters': 1.0.1 - es-toolkit: 1.39.3 - events: 3.3.0 - uint8arrays: 3.1.1 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - ioredis - - typescript - - uploadthing - - utf-8-validate - - zod - - '@walletconnect/core@2.23.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@walletconnect/heartbeat': 1.2.2 - '@walletconnect/jsonrpc-provider': 1.0.14 - '@walletconnect/jsonrpc-types': 1.0.4 - '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/logger': 3.0.0 - '@walletconnect/relay-api': 1.0.11 - '@walletconnect/relay-auth': 1.1.0 - '@walletconnect/safe-json': 1.0.2 - '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.23.0(typescript@5.8.3)(zod@3.25.76) + '@walletconnect/utils': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(typescript@5.8.3)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.39.3 events: 3.3.0 @@ -26525,18 +26559,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/ethereum-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@reown/appkit': 1.7.8(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/universal-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/universal-provider': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -26682,16 +26716,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/sign-client@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/core': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -26718,16 +26752,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/sign-client@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/core': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -26754,52 +26788,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/sign-client@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/core': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 3.0.0 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(typescript@5.8.3)(zod@4.1.12) - events: 3.3.0 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bufferutil - - db0 - - ioredis - - typescript - - uploadthing - - utf-8-validate - - zod - - '@walletconnect/sign-client@2.23.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@walletconnect/core': 2.23.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/events': 1.0.1 - '@walletconnect/heartbeat': 1.2.2 - '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/logger': 3.0.0 - '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.23.0(typescript@5.8.3)(zod@3.25.76) + '@walletconnect/utils': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(typescript@5.8.3)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -26946,7 +26944,7 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/universal-provider@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -26955,9 +26953,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/sign-client': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/utils': 2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -26986,7 +26984,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/universal-provider@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -26995,9 +26993,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/sign-client': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/utils': 2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -27026,7 +27024,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/utils@2.21.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -27044,7 +27042,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -27070,7 +27068,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)': + '@walletconnect/utils@2.21.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -27088,7 +27086,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -27114,52 +27112,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(typescript@5.8.3)(zod@4.1.12)': - dependencies: - '@msgpack/msgpack': 3.1.2 - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.7 - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 - '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/keyvaluestorage': 1.1.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/logger': 3.0.0 - '@walletconnect/relay-api': 1.0.11 - '@walletconnect/relay-auth': 1.1.0 - '@walletconnect/safe-json': 1.0.2 - '@walletconnect/time': 1.0.2 - '@walletconnect/types': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))) - '@walletconnect/window-getters': 1.0.1 - '@walletconnect/window-metadata': 1.0.1 - blakejs: 1.2.1 - bs58: 6.0.0 - detect-browser: 5.3.0 - ox: 0.9.3(typescript@5.8.3)(zod@4.1.12) - uint8arrays: 3.1.1 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@react-native-async-storage/async-storage' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - ioredis - - typescript - - uploadthing - - zod - - '@walletconnect/utils@2.23.0(typescript@5.8.3)(zod@3.25.76)': + '@walletconnect/utils@2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(typescript@5.8.3)(zod@3.25.76)': dependencies: '@msgpack/msgpack': 3.1.2 '@noble/ciphers': 1.3.0 @@ -27335,10 +27288,10 @@ snapshots: typescript: 5.8.3 zod: 3.25.76 - abitype@1.0.8(typescript@5.8.3)(zod@4.1.12): + abitype@1.0.8(typescript@5.8.3)(zod@3.25.76): optionalDependencies: typescript: 5.8.3 - zod: 4.1.12 + zod: 3.25.76 abitype@1.1.0(typescript@5.8.3)(zod@3.22.4): optionalDependencies: @@ -27350,11 +27303,6 @@ snapshots: typescript: 5.8.3 zod: 3.25.76 - abitype@1.1.0(typescript@5.8.3)(zod@4.1.12): - optionalDependencies: - typescript: 5.8.3 - zod: 4.1.12 - abitype@1.1.2(typescript@5.8.3)(zod@3.22.4): optionalDependencies: typescript: 5.8.3 @@ -33365,28 +33313,28 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - ox@0.6.7(typescript@5.8.3)(zod@4.1.12): + ox@0.6.7(typescript@5.8.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.2(typescript@5.8.3)(zod@4.1.12) + abitype: 1.1.2(typescript@5.8.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: - zod - ox@0.6.9(typescript@5.8.3)(zod@4.1.12): + ox@0.6.9(typescript@5.8.3)(zod@3.25.76): dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.1.2(typescript@5.8.3)(zod@4.1.12) + abitype: 1.1.2(typescript@5.8.3)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.8.3 @@ -33423,21 +33371,6 @@ snapshots: transitivePeerDependencies: - zod - ox@0.9.3(typescript@5.8.3)(zod@4.1.12): - dependencies: - '@adraffy/ens-normalize': 1.11.1 - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - abitype: 1.1.2(typescript@5.8.3)(zod@4.1.12) - eventemitter3: 5.0.1 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - zod - ox@0.9.6(typescript@5.8.3)(zod@3.22.4): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -33468,21 +33401,6 @@ snapshots: transitivePeerDependencies: - zod - ox@0.9.6(typescript@5.8.3)(zod@4.1.12): - dependencies: - '@adraffy/ens-normalize': 1.11.1 - '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.1 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - abitype: 1.1.2(typescript@5.8.3)(zod@4.1.12) - eventemitter3: 5.0.1 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - zod - p-cancelable@3.0.0: {} p-filter@2.1.0: @@ -33804,14 +33722,14 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.35(52953fff89e02f75760b90db6005bc45): + porto@0.2.35(f8a5cd75b5ca2acd10c494cdcd5e46bd): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) hono: 4.10.6 idb-keyval: 6.2.2 mipd: 0.0.7(typescript@5.8.3) ox: 0.9.14(typescript@5.8.3)(zod@4.1.12) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 4.1.12 zustand: 5.0.8(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) optionalDependencies: @@ -33819,7 +33737,7 @@ snapshots: react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10) typescript: 5.8.3 - wagmi: 2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12) + wagmi: 2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -34032,7 +33950,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.19.25 + '@types/node': 22.19.1 long: 5.3.2 proxy-addr@2.0.7: @@ -34083,8 +34001,6 @@ snapshots: punycode@2.1.0: {} - punycode@2.1.1: {} - punycode@2.3.1: {} puppeteer-core@2.1.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): @@ -35508,14 +35424,14 @@ snapshots: pako: 2.1.0 ts-mixer: 6.0.4 - starknetkit@2.6.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(starknet@7.6.4)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12): + starknetkit@2.6.1(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(starknet@7.6.4)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@starknet-io/get-starknet': 4.0.8 '@starknet-io/get-starknet-core': 4.0.8 '@starknet-io/types-js': 0.7.10 '@trpc/client': 10.45.2(@trpc/server@10.45.2) '@trpc/server': 10.45.2 - '@walletconnect/sign-client': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + '@walletconnect/sign-client': 2.23.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) bowser: 2.12.1 detect-browser: 5.3.0 eventemitter3: 5.0.1 @@ -36583,7 +36499,7 @@ snapshots: uri-js@4.4.1: dependencies: - punycode: 2.1.1 + punycode: 2.3.1 url@0.11.4: dependencies: @@ -36698,15 +36614,15 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - viem@2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12): + viem@2.23.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.8.3)(zod@4.1.12) + abitype: 1.0.8(typescript@5.8.3)(zod@3.25.76) isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.6.7(typescript@5.8.3)(zod@4.1.12) + ox: 0.6.7(typescript@5.8.3)(zod@3.25.76) ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: typescript: 5.8.3 @@ -36749,23 +36665,6 @@ snapshots: - utf-8-validate - zod - viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12): - dependencies: - '@noble/curves': 1.9.1 - '@noble/hashes': 1.8.0 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - abitype: 1.1.0(typescript@5.8.3)(zod@4.1.12) - isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - ox: 0.9.6(typescript@5.8.3)(zod@4.1.12) - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - zod - vite-node@1.4.0(@types/node@22.19.1)(terser@5.44.1): dependencies: cac: 6.7.14 @@ -36840,14 +36739,14 @@ snapshots: vlq@1.0.1: {} - wagmi@2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.12): + wagmi@2.19.4(@react-native-async-storage/async-storage@1.24.0(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.90.10)(@tanstack/react-query@5.90.10(react@18.3.1))(@types/react@18.3.27)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.2.0)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76): dependencies: '@tanstack/react-query': 5.90.10(react@18.3.1) - '@wagmi/connectors': 6.1.4(0412c5480cb372a861c205176362b8d1) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12)) + '@wagmi/connectors': 6.1.4(3015cd4f3cc533a2a82d169692de7690) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.10)(@types/react@18.3.27)(immer@10.2.0)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) - viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@4.1.12) + viem: 2.39.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: diff --git a/turbo.json b/turbo.json index 5b5a79cbdda..2928816dde7 100644 --- a/turbo.json +++ b/turbo.json @@ -29,7 +29,7 @@ }, "bundle": { "dependsOn": ["^build"], - "outputs": ["*-bundle/**"] + "outputs": ["*-bundle/**", "bundle/**"] } } } diff --git a/typescript/infra/helm/key-funder/templates/addresses-external-secret.yaml b/typescript/infra/helm/key-funder/templates/addresses-external-secret.yaml deleted file mode 100644 index e0c12c63fcf..00000000000 --- a/typescript/infra/helm/key-funder/templates/addresses-external-secret.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: external-secrets.io/v1beta1 -kind: ExternalSecret -metadata: - name: key-funder-addresses-external-secret - labels: - {{- include "hyperlane.labels" . | nindent 4 }} -spec: - secretStoreRef: - name: {{ include "hyperlane.cluster-secret-store.name" . }} - kind: ClusterSecretStore - refreshInterval: "1h" - # The secret that will be created - target: - name: key-funder-addresses-secret - template: - type: Opaque - metadata: - labels: - {{- include "hyperlane.labels" . | nindent 10 }} - annotations: - update-on-redeploy: "{{ now }}" - data: -{{- range $context, $roles := .Values.hyperlane.contextsAndRolesToFund }} - {{ $context }}-addresses.json: {{ printf "'{{ .%s_addresses | toString }}'" $context }} -{{- end }} - data: -{{- range $context, $roles := .Values.hyperlane.contextsAndRolesToFund }} - - secretKey: {{ $context }}_addresses - remoteRef: - key: {{ printf "%s-%s-key-addresses" $context $.Values.hyperlane.runEnv }} -{{- end }} diff --git a/typescript/infra/helm/key-funder/templates/configmap.yaml b/typescript/infra/helm/key-funder/templates/configmap.yaml new file mode 100644 index 00000000000..31f31d3ce7c --- /dev/null +++ b/typescript/infra/helm/key-funder/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: key-funder-config + labels: + {{- include "hyperlane.labels" . | nindent 4 }} +data: + keyfunder.yaml: | +{{ .Values.hyperlane.keyfunderConfig | indent 4 }} diff --git a/typescript/infra/helm/key-funder/templates/cron-job.yaml b/typescript/infra/helm/key-funder/templates/cron-job.yaml index e25484c5f9e..1e8e765ee21 100644 --- a/typescript/infra/helm/key-funder/templates/cron-job.yaml +++ b/typescript/infra/helm/key-funder/templates/cron-job.yaml @@ -10,7 +10,7 @@ spec: jobTemplate: spec: backoffLimit: 0 - activeDeadlineSeconds: 14400 # 60 * 60 * 4 seconds = 4 hours + activeDeadlineSeconds: 14400 template: metadata: labels: @@ -21,52 +21,31 @@ spec: - name: key-funder image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: IfNotPresent - command: - - pnpm - - exec - - tsx - - ./typescript/infra/scripts/funding/fund-keys-from-deployer.ts - - -e - - {{ .Values.hyperlane.runEnv }} - - --context - - {{ .Values.hyperlane.contextFundingFrom }} -{{- range $context, $roles := .Values.hyperlane.contextsAndRolesToFund }} - - --contexts-and-roles - - {{ $context }}={{ join "," $roles }} -{{- end }} -{{- range $chain, $balance := .Values.hyperlane.desiredBalancePerChain }} - - --desired-balance-per-chain - - {{ $chain }}={{ $balance }} -{{- end }} -{{- range $chain, $balance := .Values.hyperlane.desiredKathyBalancePerChain }} - - --desired-kathy-balance-per-chain - - {{ $chain }}={{ $balance }} -{{- end }} -{{- range $chain, $balance := .Values.hyperlane.desiredRebalancerBalancePerChain }} - - --desired-rebalancer-balance-per-chain - - {{ $chain }}={{ $balance }} -{{- end }} -{{- range $chain, $balance := .Values.hyperlane.igpClaimThresholdPerChain }} - - --igp-claim-threshold-per-chain - - {{ $chain }}={{ $balance }} -{{- end }} -{{- if .Values.hyperlane.chainsToSkip }} - - --chain-skip-override -{{- range $index, $chain := .Values.hyperlane.chainsToSkip }} - - {{ $chain }} -{{- end }} -{{- end }} env: - - name: PROMETHEUS_PUSH_GATEWAY - value: {{ .Values.infra.prometheusPushGateway }} + - name: LOG_FORMAT + value: json + - name: LOG_LEVEL + value: info + - name: KEYFUNDER_CONFIG_FILE + value: /config/keyfunder.yaml + {{- if .Values.hyperlane.registryUri }} + - name: REGISTRY_URI + value: {{ .Values.hyperlane.registryUri }} + {{- end }} + - name: FUNDER_PRIVATE_KEY + value: $(FUNDER_PRIVATE_KEY) + {{- if .Values.hyperlane.skipIgpClaim }} + - name: SKIP_IGP_CLAIM + value: "true" + {{- end }} envFrom: - secretRef: name: key-funder-env-var-secret volumeMounts: - - name: key-funder-addresses-secret - mountPath: /addresses-secret + - name: config + mountPath: /config + readOnly: true volumes: - - name: key-funder-addresses-secret - secret: - secretName: key-funder-addresses-secret - defaultMode: 0400 + - name: config + configMap: + name: key-funder-config diff --git a/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml b/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml index 2a95d627e23..3cef9b5e9e8 100644 --- a/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml +++ b/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml @@ -9,7 +9,6 @@ spec: name: {{ include "hyperlane.cluster-secret-store.name" . }} kind: ClusterSecretStore refreshInterval: "1h" - # The secret that will be created target: name: key-funder-env-var-secret template: @@ -20,24 +19,14 @@ spec: annotations: update-on-redeploy: "{{ now }}" data: - GCP_SECRET_OVERRIDES_ENABLED: "true" - GCP_SECRET_OVERRIDE_HYPERLANE_{{ .Values.hyperlane.runEnv | upper }}_KEY_DEPLOYER: {{ print "'{{ .deployer_key | toString }}'" }} -{{/* - * For each network, create an environment variable with the RPC endpoint. - * The templating of external-secrets will use the data section below to know how - * to replace the correct value in the created secret. - */}} + FUNDER_PRIVATE_KEY: {{ print "'{{ $json := .funder_key | fromJson }}{{ $json.privateKey }}'" }} {{- range .Values.hyperlane.chains }} - GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} + RPC_URL_{{ . | upper | replace "-" "_" }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} {{- end }} data: - - secretKey: deployer_key + - secretKey: funder_key remoteRef: key: {{ printf "hyperlane-%s-key-deployer" .Values.hyperlane.runEnv }} -{{/* - * For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network, - * and associate it with the secret key networkname_rpc. - */}} {{- range .Values.hyperlane.chains }} - secretKey: {{ printf "%s_rpcs" . }} remoteRef: diff --git a/typescript/infra/helm/key-funder/values.yaml b/typescript/infra/helm/key-funder/values.yaml index 7da578484ef..6d60ece7a76 100644 --- a/typescript/infra/helm/key-funder/values.yaml +++ b/typescript/infra/helm/key-funder/values.yaml @@ -1,18 +1,15 @@ image: - repository: gcr.io/hyperlane-labs-dev/hyperlane-monorepo - tag: + repository: gcr.io/abacus-labs-dev/hyperlane-keyfunder + tag: latest hyperlane: - runEnv: testnet2 - # Used for fetching secrets + runEnv: testnet4 chains: [] chainsToSkip: [] - contextFundingFrom: hyperlane - # key = context, value = array of roles to fund - contextsAndRolesToFund: - hyperlane: - - relayer + registryUri: '' + keyfunderConfig: '' + skipIgpClaim: false cronjob: - schedule: '*/10 * * * *' # Every 10 minutes + schedule: '*/10 * * * *' successfulJobsHistoryLimit: 6 failedJobsHistoryLimit: 10 externalSecrets: diff --git a/typescript/infra/scripts/funding/deploy-key-funder.ts b/typescript/infra/scripts/funding/deploy-key-funder.ts index 5376de025ef..1cf12090808 100644 --- a/typescript/infra/scripts/funding/deploy-key-funder.ts +++ b/typescript/infra/scripts/funding/deploy-key-funder.ts @@ -1,8 +1,9 @@ +import { input } from '@inquirer/prompts'; import chalk from 'chalk'; import { Contexts } from '../../config/contexts.js'; import { KeyFunderHelmManager } from '../../src/funding/key-funder.js'; -import { checkMonorepoImageExists } from '../../src/utils/gcloud.js'; +import { validateRegistryCommit } from '../../src/utils/git.js'; import { HelmCommand } from '../../src/utils/helm.js'; import { assertCorrectKubeContext } from '../agent-utils.js'; import { getConfigsBasedOnArgs } from '../core-utils.js'; @@ -16,23 +17,16 @@ async function main() { await assertCorrectKubeContext(envConfig); - if (envConfig.keyFunderConfig?.docker.tag) { - const exists = await checkMonorepoImageExists( - envConfig.keyFunderConfig.docker.tag, - ); - if (!exists) { - console.log( - chalk.red( - `Attempted to deploy key funder with image tag ${chalk.bold( - envConfig.keyFunderConfig.docker.tag, - )}, but it has not been published to GCR.`, - ), - ); - process.exit(1); - } - } + const registryCommit = await input({ + message: + 'Enter the registry version to use (can be a commit, branch or tag):', + }); + await validateRegistryCommit(registryCommit); - const manager = KeyFunderHelmManager.forEnvironment(environment); + const manager = KeyFunderHelmManager.forEnvironment( + environment, + registryCommit, + ); await manager.runHelmCommand(HelmCommand.InstallOrUpgrade); } diff --git a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts deleted file mode 100644 index 55bab1793c9..00000000000 --- a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts +++ /dev/null @@ -1,1207 +0,0 @@ -import { EthBridger, getArbitrumNetwork } from '@arbitrum/sdk'; -import { CrossChainMessenger } from '@eth-optimism/sdk'; -import { BigNumber, ethers } from 'ethers'; -import { Registry } from 'prom-client'; -import { format } from 'util'; - -import { - ChainMap, - ChainName, - HyperlaneIgp, - MultiProvider, - defaultMultisigConfigs, -} from '@hyperlane-xyz/sdk'; -import { Address, objFilter, objMap, rootLogger } from '@hyperlane-xyz/utils'; -import { readJson } from '@hyperlane-xyz/utils/fs'; - -import { Contexts } from '../../config/contexts.js'; -import { getEnvAddresses } from '../../config/registry.js'; -import { - KeyAsAddress, - fetchLocalKeyAddresses, - getRoleKeysPerChain, -} from '../../src/agents/key-utils.js'; -import { - BaseAgentKey, - LocalAgentKey, - ReadOnlyCloudAgentKey, -} from '../../src/agents/keys.js'; -import { DeployEnvironment } from '../../src/config/environment.js'; -import { - ContextAndRoles, - ContextAndRolesMap, - KeyFunderConfig, - SweepOverrideConfig, - validateSweepConfig, -} from '../../src/config/funding.js'; -import { FundableRole, Role } from '../../src/roles.js'; -import { - getWalletBalanceGauge, - submitMetrics, -} from '../../src/utils/metrics.js'; -import { - assertContext, - assertFundableRole, - assertRole, - isEthereumProtocolChain, -} from '../../src/utils/utils.js'; -import { getAgentConfig, getArgs } from '../agent-utils.js'; -import { getEnvironmentConfig } from '../core-utils.js'; - -import L1ETHGateway from './utils/L1ETHGateway.json' with { type: 'json' }; -import L1MessageQueue from './utils/L1MessageQueue.json' with { type: 'json' }; -import L1ScrollMessenger from './utils/L1ScrollMessenger.json' with { type: 'json' }; - -const logger = rootLogger.child({ module: 'fund-keys' }); - -// Default sweep configuration -const DEFAULT_SWEEP_ADDRESS = '0x478be6076f31E9666123B9721D0B6631baD944AF'; -const DEFAULT_TARGET_MULTIPLIER = 1.5; // Leave 1.5x threshold after sweep -const DEFAULT_TRIGGER_MULTIPLIER = 2.0; // Sweep when balance > 2x threshold - -const nativeBridges = { - scrollsepolia: { - l1ETHGateway: '0x8A54A2347Da2562917304141ab67324615e9866d', - l1Messenger: '0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A', - }, -}; - -const L2Chains: ChainName[] = ['optimism', 'arbitrum', 'base']; - -const L2ToL1: ChainMap = { - optimism: 'ethereum', - arbitrum: 'ethereum', - base: 'ethereum', -}; - -// Manually adding these labels as we are using a push gateway, -// and ordinarily these labels would be added via K8s annotations -const constMetricLabels = { - hyperlane_deployment: '', - hyperlane_context: 'hyperlane', -}; - -const metricsRegister = new Registry(); - -const walletBalanceGauge = getWalletBalanceGauge( - metricsRegister, - Object.keys(constMetricLabels), -); - -// Min delta is 60% of the desired balance -const MIN_DELTA_NUMERATOR = ethers.BigNumber.from(6); -const MIN_DELTA_DENOMINATOR = ethers.BigNumber.from(10); - -// Don't send the full amount over to RC keys -const RC_FUNDING_DISCOUNT_NUMERATOR = ethers.BigNumber.from(2); -const RC_FUNDING_DISCOUNT_DENOMINATOR = ethers.BigNumber.from(10); - -const CONTEXT_FUNDING_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes -const CHAIN_FUNDING_TIMEOUT_MS = 1 * 60 * 1000; // 1 minute - -// Funds key addresses for multiple contexts from the deployer key of the context -// specified via the `--context` flag. -// The --contexts-and-roles flag is used to specify the contexts and the key roles -// for each context to fund. -// There are two ways to configure this script so that key addresses are known. -// You can pass in files using `-f`, which are expected to each be JSON arrays of objects -// of the form { identifier: '..', address: '..' }, where the keys described in one file -// are all for the same context. This will avoid requiring any sort of GCP/AWS credentials for -// fetching addresses from the keys themselves. A file for each context specified in --contexts-and-roles -// must be provided -// If the -f flag is not provided, addresses will be read directly from GCP/AWS for each -// context provided in --contexts-and-roles, which requires the appropriate credentials. -// -// Example usage: -// tsx ./scripts/funding/fund-keys-from-deployer.ts -e testnet4 --context hyperlane --contexts-and-roles rc=relayer -async function main() { - const { environment, ...argv } = await getArgs() - .string('f') - .array('f') - .alias('f', 'address-files') - .describe( - 'f', - 'Files each containing JSON arrays of identifier and address objects for a single context. If not specified, key addresses are fetched from GCP/AWS and require sufficient credentials.', - ) - - .string('contexts-and-roles') - .array('contexts-and-roles') - .describe( - 'contexts-and-roles', - 'Array indicating contexts and the roles to fund for each context. Each element is expected as =,,...', - ) - .coerce('contexts-and-roles', parseContextAndRolesMap) - .demandOption('contexts-and-roles') - - .string('desired-balance-per-chain') - .array('desired-balance-per-chain') - .describe( - 'desired-balance-per-chain', - 'Array indicating target balance to fund for each chain. Each element is expected as =', - ) - .coerce('desired-balance-per-chain', parseBalancePerChain) - .demandOption('desired-balance-per-chain') - - .string('desired-kathy-balance-per-chain') - .array('desired-kathy-balance-per-chain') - .describe( - 'desired-kathy-balance-per-chain', - 'Array indicating target balance to fund Kathy for each chain. Each element is expected as =', - ) - .coerce('desired-kathy-balance-per-chain', parseBalancePerChain) - - .string('desired-rebalancer-balance-per-chain') - .array('desired-rebalancer-balance-per-chain') - .describe( - 'desired-rebalancer-balance-per-chain', - 'Array indicating target balance to fund Rebalancer for each chain. Each element is expected as =', - ) - .coerce('desired-rebalancer-balance-per-chain', parseBalancePerChain) - - .string('igp-claim-threshold-per-chain') - .array('igp-claim-threshold-per-chain') - .describe( - 'igp-claim-threshold-per-chain', - 'Array indicating threshold to claim IGP balance for each chain. Each element is expected as =', - ) - .coerce('igp-claim-threshold-per-chain', parseBalancePerChain) - - .boolean('skip-igp-claim') - .describe('skip-igp-claim', 'If true, never claims funds from the IGP') - .default('skip-igp-claim', false) - - .array('chain-skip-override') - .describe('chain-skip-override', 'Array of chains to skip funding for') - .default('chain-skip-override', []).argv; - - constMetricLabels.hyperlane_deployment = environment; - const config = getEnvironmentConfig(environment); - const multiProvider = await config.getMultiProvider( - Contexts.Hyperlane, // Always fund from the hyperlane context - Role.Deployer, // Always fund from the deployer - ); - - // Load sweep overrides and low urgency balances from the environment config - const keyFunderConfig = config.keyFunderConfig; - const sweepOverrides = keyFunderConfig?.sweepOverrides; - const lowUrgencyKeyFunderBalances = - keyFunderConfig?.lowUrgencyKeyFunderBalances ?? {}; - - let contextFunders: ContextFunder[]; - - if (argv.f) { - contextFunders = argv.f.map((path) => - ContextFunder.fromSerializedAddressFile( - environment, - multiProvider, - argv.contextsAndRoles, - argv.skipIgpClaim, - argv.chainSkipOverride, - argv.desiredBalancePerChain, - argv.desiredKathyBalancePerChain ?? {}, - argv.desiredRebalancerBalancePerChain ?? {}, - argv.igpClaimThresholdPerChain ?? {}, - sweepOverrides, - lowUrgencyKeyFunderBalances, - path, - ), - ); - } else { - const contexts = Object.keys(argv.contextsAndRoles) as Contexts[]; - contextFunders = await Promise.all( - contexts.map((context) => - ContextFunder.fromLocal( - environment, - multiProvider, - context, - argv.contextsAndRoles[context]!, - argv.skipIgpClaim, - argv.chainSkipOverride, - argv.desiredBalancePerChain, - argv.desiredKathyBalancePerChain ?? {}, - argv.desiredRebalancerBalancePerChain ?? {}, - argv.igpClaimThresholdPerChain ?? {}, - sweepOverrides, - lowUrgencyKeyFunderBalances, - ), - ), - ); - } - - let failureOccurred = false; - for (const funder of contextFunders) { - const { promise, cleanup } = createTimeoutPromise( - CONTEXT_FUNDING_TIMEOUT_MS, - `Funding timed out for context ${funder.context} after ${ - CONTEXT_FUNDING_TIMEOUT_MS / 1000 - }s`, - ); - - try { - await Promise.race([funder.fund(), promise]); - } catch (error) { - logger.error('Error funding context', { - error: format(error), - context: funder.context, - timeoutMs: CONTEXT_FUNDING_TIMEOUT_MS, - }); - failureOccurred = true; - } finally { - cleanup(); - } - } - - await submitMetrics(metricsRegister, `key-funder-${environment}`); - - if (failureOccurred) { - logger.error('At least one failure occurred when funding'); - process.exit(1); - } -} - -// Funds keys for a single context -class ContextFunder { - igp: HyperlaneIgp; - - keysToFundPerChain: ChainMap; - - constructor( - public readonly environment: DeployEnvironment, - public readonly multiProvider: MultiProvider, - roleKeysPerChain: ChainMap>, - public readonly context: Contexts, - public readonly rolesToFund: FundableRole[], - public readonly skipIgpClaim: boolean, - public readonly chainSkipOverride: ChainName[], - public readonly desiredBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredBalancePerChain'], - public readonly desiredKathyBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredKathyBalancePerChain'], - public readonly desiredRebalancerBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredRebalancerBalancePerChain'], - public readonly igpClaimThresholdPerChain: KeyFunderConfig< - ChainName[] - >['igpClaimThresholdPerChain'], - public readonly sweepOverrides: ChainMap | undefined, - public readonly lowUrgencyKeyFunderBalances: ChainMap, - ) { - // At the moment, only blessed EVM chains are supported - roleKeysPerChain = objFilter( - roleKeysPerChain, - (chain, _roleKeys): _roleKeys is Record => { - const valid = - isEthereumProtocolChain(chain) && - multiProvider.tryGetChainName(chain) !== null; - if (!valid) { - logger.warn( - { chain }, - 'Skipping funding for non-blessed or non-Ethereum chain', - ); - } - return valid; - }, - ); - - this.igp = HyperlaneIgp.fromAddressesMap( - getEnvAddresses(this.environment), - multiProvider, - ); - this.keysToFundPerChain = objMap(roleKeysPerChain, (_chain, roleKeys) => { - return Object.keys(roleKeys).reduce((agg, roleStr) => { - const role = roleStr as FundableRole; - if (this.rolesToFund.includes(role)) { - return [...agg, ...roleKeys[role]]; - } - return agg; - }, [] as BaseAgentKey[]); - }); - } - - static fromSerializedAddressFile( - environment: DeployEnvironment, - multiProvider: MultiProvider, - contextsAndRolesToFund: ContextAndRolesMap, - skipIgpClaim: boolean, - chainSkipOverride: ChainName[], - desiredBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredBalancePerChain'], - desiredKathyBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredKathyBalancePerChain'], - desiredRebalancerBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredRebalancerBalancePerChain'], - igpClaimThresholdPerChain: KeyFunderConfig< - ChainName[] - >['igpClaimThresholdPerChain'], - sweepOverrides: ChainMap | undefined, - lowUrgencyKeyFunderBalances: ChainMap, - filePath: string, - ) { - logger.info({ filePath }, 'Reading identifiers and addresses from file'); - // A big array of KeyAsAddress, including keys that we may not care about. - const allIdsAndAddresses: KeyAsAddress[] = - readJson(filePath); - if (!allIdsAndAddresses.length) { - throw Error(`Expected at least one key in file ${filePath}`); - } - - // Arbitrarily pick the first key to get the context - const firstKey = allIdsAndAddresses[0]; - const context = ReadOnlyCloudAgentKey.fromSerializedAddress( - firstKey.identifier, - firstKey.address, - ).context; - - // Indexed by the identifier for quicker lookup - const idsAndAddresses: Record = - allIdsAndAddresses.reduce( - (agg, idAndAddress) => { - agg[idAndAddress.identifier] = idAndAddress; - return agg; - }, - {} as Record, - ); - - const agentConfig = getAgentConfig(context, environment); - // Unfetched keys per chain and role, so we know which keys - // we need. We'll use this to create a corresponding object - // of ReadOnlyCloudAgentKeys using addresses found in the - // serialized address file. - const roleKeysPerChain = getRoleKeysPerChain(agentConfig); - - const readOnlyKeysPerChain = objMap( - roleKeysPerChain, - (_chain, roleKeys) => { - return objMap(roleKeys, (_role, keys) => { - return keys.map((key) => { - const idAndAddress = idsAndAddresses[key.identifier]; - if (!idAndAddress) { - throw Error( - `Expected key identifier ${key.identifier} to be in file ${filePath}`, - ); - } - return ReadOnlyCloudAgentKey.fromSerializedAddress( - idAndAddress.identifier, - idAndAddress.address, - ); - }); - }); - }, - ); - - logger.info( - { - filePath, - readOnlyKeysPerChain, - context, - }, - 'Successfully read keys for context from file', - ); - - return new ContextFunder( - environment, - multiProvider, - readOnlyKeysPerChain, - context, - contextsAndRolesToFund[context]!, - skipIgpClaim, - chainSkipOverride, - desiredBalancePerChain, - desiredKathyBalancePerChain, - desiredRebalancerBalancePerChain, - igpClaimThresholdPerChain, - sweepOverrides, - lowUrgencyKeyFunderBalances, - ); - } - - // the keys are retrieved from the local artifacts in the infra/config/relayer.json or infra/config/kathy.json - static async fromLocal( - environment: DeployEnvironment, - multiProvider: MultiProvider, - context: Contexts, - rolesToFund: FundableRole[], - skipIgpClaim: boolean, - chainSkipOverride: ChainName[], - desiredBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredBalancePerChain'], - desiredKathyBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredKathyBalancePerChain'], - desiredRebalancerBalancePerChain: KeyFunderConfig< - ChainName[] - >['desiredRebalancerBalancePerChain'], - igpClaimThresholdPerChain: KeyFunderConfig< - ChainName[] - >['igpClaimThresholdPerChain'], - sweepOverrides: ChainMap | undefined, - lowUrgencyKeyFunderBalances: ChainMap, - ) { - // only roles that are fundable keys ie. relayer and kathy - const fundableRoleKeys: Record = { - [Role.Relayer]: '', - [Role.Kathy]: '', - [Role.Rebalancer]: '', - }; - const roleKeysPerChain: ChainMap> = {}; - const { supportedChainNames } = getEnvironmentConfig(environment); - for (const role of rolesToFund) { - assertFundableRole(role); // only the relayer and kathy are fundable keys - const roleAddress = fetchLocalKeyAddresses(role)[environment][context]; - if (!roleAddress) { - throw Error( - `Could not find address for ${role} in ${environment} ${context}`, - ); - } - fundableRoleKeys[role] = roleAddress; - - for (const chain of supportedChainNames) { - if (!roleKeysPerChain[chain as ChainName]) { - roleKeysPerChain[chain as ChainName] = { - [Role.Relayer]: [], - [Role.Kathy]: [], - [Role.Rebalancer]: [], - }; - } - roleKeysPerChain[chain][role] = [ - new LocalAgentKey( - environment, - context, - role, - fundableRoleKeys[role as FundableRole], - chain, - ), - ]; - } - } - return new ContextFunder( - environment, - multiProvider, - roleKeysPerChain, - context, - rolesToFund, - skipIgpClaim, - chainSkipOverride, - desiredBalancePerChain, - desiredKathyBalancePerChain, - desiredRebalancerBalancePerChain, - igpClaimThresholdPerChain, - sweepOverrides, - lowUrgencyKeyFunderBalances, - ); - } - - // Funds all the roles in this.keysToFundPerChain. - // Throws if any funding operations fail. - async fund(): Promise { - const chainKeyEntries = Object.entries(this.keysToFundPerChain); - const results = await Promise.allSettled( - chainKeyEntries.map(([chain, keys]) => this.fundChain(chain, keys)), - ); - - if (results.some((result) => result.status === 'rejected')) { - logger.error('One or more chains failed to fund'); - throw new Error('One or more chains failed to fund'); - } - } - - private async fundChain(chain: string, keys: BaseAgentKey[]): Promise { - if (this.chainSkipOverride.includes(chain)) { - logger.warn( - { chain }, - `Configured to skip funding operations for chain ${chain}, skipping`, - ); - return; - } - - const { promise, cleanup } = createTimeoutPromise( - CHAIN_FUNDING_TIMEOUT_MS, - `Timed out funding chain ${chain} after ${ - CHAIN_FUNDING_TIMEOUT_MS / 1000 - }s`, - ); - - try { - await Promise.race([this.executeFundingOperations(chain, keys), promise]); - } catch (error) { - logger.error( - { - chain, - error: format(error), - timeoutMs: CHAIN_FUNDING_TIMEOUT_MS, - keysCount: keys.length, - }, - `Funding operations failed for chain ${chain}.`, - ); - throw error; - } finally { - cleanup(); - } - } - - private async executeFundingOperations( - chain: string, - keys: BaseAgentKey[], - ): Promise { - if (keys.length === 0) { - return; - } - - if (!this.skipIgpClaim) { - try { - await this.attemptToClaimFromIgp(chain); - } catch (err) { - logger.error( - { - chain, - error: err, - }, - `Error claiming from IGP on chain ${chain}`, - ); - } - } - - try { - await this.bridgeIfL2(chain); - } catch (err) { - logger.error( - { - chain, - error: err, - }, - `Error bridging to L2 chain ${chain}`, - ); - throw err; - } - - const failedKeys: BaseAgentKey[] = []; - for (const key of keys) { - try { - await this.attemptToFundKey(key, chain); - } catch (err) { - logger.error( - { - chain, - key: await getKeyInfo( - key, - chain, - this.multiProvider.getProvider(chain), - ), - context: this.context, - error: err, - }, - `Error funding key ${key.address} on chain ${chain}`, - ); - failedKeys.push(key); - } - } - - // Attempt to sweep excess funds after all claim/funding operations are complete - // Only sweep when processing the Hyperlane context to avoid duplicate sweeps - if (this.context === Contexts.Hyperlane) { - try { - await this.attemptToSweepExcessFunds(chain); - } catch (err) { - logger.error( - { - chain, - error: err, - }, - `Error sweeping excess funds on chain ${chain}`, - ); - } - } - - if (failedKeys.length > 0) { - throw new Error( - `Failed to fund ${ - failedKeys.length - } keys on chain ${chain}: ${failedKeys - .map(({ address, role }) => `${address} (${role})`) - .join(', ')}`, - ); - } - } - - private async attemptToFundKey( - key: BaseAgentKey, - chain: ChainName, - ): Promise { - const provider = this.multiProvider.tryGetProvider(chain); - if (!provider) { - throw new Error(`Cannot get chain connection for ${chain}`); - } - - const desiredBalance = this.getDesiredBalanceForRole(chain, key.role); - await this.fundKeyIfRequired(chain, key, desiredBalance); - await this.updateWalletBalanceGauge(chain); - } - - private async bridgeIfL2(chain: ChainName): Promise { - if (L2Chains.includes(chain)) { - const funderAddress = await this.multiProvider.getSignerAddress(chain)!; - const desiredBalanceEther = ethers.utils.parseUnits( - this.desiredBalancePerChain[chain], - 'ether', - ); - // Optionally bridge ETH to L2 before funding the desired key. - // By bridging the funder with 10x the desired balance we save - // on L1 gas. - const bridgeAmount = await this.getFundingAmount( - chain, - funderAddress, - desiredBalanceEther.mul(5), - ); - if (bridgeAmount.gt(0)) { - await this.bridgeToL2(chain, funderAddress, bridgeAmount); - } - } - } - - // Attempts to sweep excess funds to a given address when balance exceeds threshold. - // To avoid churning txs, only sweep when balance > triggerMultiplier * threshold, - // and leave targetMultiplier * threshold after sweep. - private async attemptToSweepExcessFunds(chain: ChainName): Promise { - // Skip if the chain isn't in production yet i.e. if the validator set size is still 1 - if (defaultMultisigConfigs[chain].validators.length === 1) { - logger.debug( - { chain }, - 'Chain is not in production yet, skipping sweep.', - ); - return; - } - - // Skip if we don't have a threshold configured for this chain - const lowUrgencyBalanceStr = this.lowUrgencyKeyFunderBalances[chain]; - if (!lowUrgencyBalanceStr) { - logger.debug( - { chain }, - 'No low urgency balance configured for chain, skipping sweep', - ); - return; - } - - const lowUrgencyBalance = ethers.utils.parseEther(lowUrgencyBalanceStr); - - // Skip if threshold is zero or negligible - if (lowUrgencyBalance.lte(0)) { - logger.debug({ chain }, 'Low urgency balance is zero, skipping sweep'); - return; - } - - // Get override config for this chain, if any - const override = this.sweepOverrides?.[chain]; - - // Use override or default sweep address - const sweepAddress = override?.sweepAddress ?? DEFAULT_SWEEP_ADDRESS; - - // Use override or default multipliers - const targetMultiplier = - override?.targetMultiplier ?? DEFAULT_TARGET_MULTIPLIER; - const triggerMultiplier = - override?.triggerMultiplier ?? DEFAULT_TRIGGER_MULTIPLIER; - - // If we have overrides, validate the full config with all overrides applied. - if (override) { - try { - validateSweepConfig({ - sweepAddress, - targetMultiplier, - triggerMultiplier, - }); - } catch (error) { - logger.error( - { - chain, - override, - error: format(error), - }, - 'Invalid sweep override configuration', - ); - throw new Error( - `Invalid sweep override configuration for chain ${chain}: ${error}`, - ); - } - } - - // Calculate threshold amounts - const targetBalance = lowUrgencyBalance - .mul(Math.floor(targetMultiplier * 100)) - .div(100); - const triggerThreshold = lowUrgencyBalance - .mul(Math.floor(triggerMultiplier * 100)) - .div(100); - - // Get current funder balance - const funderAddress = await this.multiProvider.getSignerAddress(chain); - const funderBalance = await this.multiProvider - .getSigner(chain) - .getBalance(); - - logger.info( - { - chain, - funderAddress, - funderBalance: ethers.utils.formatEther(funderBalance), - lowUrgencyBalance: ethers.utils.formatEther(lowUrgencyBalance), - targetBalance: ethers.utils.formatEther(targetBalance), - triggerThreshold: ethers.utils.formatEther(triggerThreshold), - targetMultiplier, - triggerMultiplier, - }, - 'Checking if sweep is needed', - ); - - // Only sweep if balance exceeds trigger threshold - if (funderBalance.gt(triggerThreshold)) { - const sweepAmount = funderBalance.sub(targetBalance); - - logger.info( - { - chain, - sweepAmount: ethers.utils.formatEther(sweepAmount), - sweepAddress, - funderBalance: ethers.utils.formatEther(funderBalance), - remainingBalance: ethers.utils.formatEther(targetBalance), - }, - 'Sweeping excess funds', - ); - - const tx = await this.multiProvider.sendTransaction(chain, { - to: sweepAddress, - value: sweepAmount, - }); - - logger.info( - { - chain, - tx: - this.multiProvider.tryGetExplorerTxUrl(chain, { - hash: tx.transactionHash, - }) ?? tx.transactionHash, - sweepAmount: ethers.utils.formatEther(sweepAmount), - sweepAddress, - }, - 'Successfully swept excess funds', - ); - } else { - logger.info( - { chain }, - 'Funder balance below trigger threshold, no sweep needed', - ); - } - } - - // Attempts to claim from the IGP if the balance exceeds the claim threshold. - // If no threshold is set, infer it by reading the desired balance and dividing that by 5. - private async attemptToClaimFromIgp(chain: ChainName): Promise { - // Determine the IGP claim threshold in Ether for the given chain. - // If a specific threshold is not set, use the desired balance for the chain. - const igpClaimThresholdEther = - this.igpClaimThresholdPerChain[chain] || - this.desiredBalancePerChain[chain]; - - // If neither the IGP claim threshold nor the desired balance is set, log a warning and skip the claim attempt. - if (!igpClaimThresholdEther) { - logger.warn( - { chain }, - `No IGP claim threshold or desired balance for chain ${chain}, skipping`, - ); - return; - } - - // Convert the IGP claim threshold from Ether to a BigNumber. - let igpClaimThreshold = ethers.utils.parseEther(igpClaimThresholdEther); - - // If the IGP claim threshold is not explicitly set, infer it from the desired balance by dividing it by 5. - if (!this.igpClaimThresholdPerChain[chain]) { - igpClaimThreshold = igpClaimThreshold.div(5); - logger.info( - { chain }, - 'Inferring IGP claim threshold from desired balance', - ); - } - - const provider = this.multiProvider.getProvider(chain); - const igp = this.igp.getContracts(chain).interchainGasPaymaster; - const igpBalance = await provider.getBalance(igp.address); - - logger.info( - { - chain, - igpBalance: ethers.utils.formatEther(igpBalance), - igpClaimThreshold: ethers.utils.formatEther(igpClaimThreshold), - }, - 'Checking IGP balance', - ); - - if (igpBalance.gt(igpClaimThreshold)) { - logger.info({ chain }, 'IGP balance exceeds claim threshold, claiming'); - await this.multiProvider.sendTransaction( - chain, - await igp.populateTransaction.claim(), - ); - } else { - logger.info( - { chain }, - 'IGP balance does not exceed claim threshold, skipping', - ); - } - } - - private async getFundingAmount( - chain: ChainName, - address: string, - desiredBalance: BigNumber, - ): Promise { - const currentBalance = await this.multiProvider - .getProvider(chain) - .getBalance(address); - const delta = desiredBalance.sub(currentBalance); - const minDelta = desiredBalance - .mul(MIN_DELTA_NUMERATOR) - .div(MIN_DELTA_DENOMINATOR); - return delta.gt(minDelta) ? delta : BigNumber.from(0); - } - - private getDesiredBalanceForRole(chain: ChainName, role: Role): BigNumber { - let desiredBalanceEther: string | undefined; - if (role === Role.Kathy) { - const desiredKathyBalance = this.desiredKathyBalancePerChain[chain]; - if (desiredKathyBalance === undefined) { - logger.warn({ chain }, 'No desired balance for Kathy, not funding'); - desiredBalanceEther = '0'; - } else { - desiredBalanceEther = this.desiredKathyBalancePerChain[chain]; - } - } else if (role === Role.Rebalancer) { - const desiredRebalancerBalance = - this.desiredRebalancerBalancePerChain[chain]; - if (desiredRebalancerBalance === undefined) { - logger.warn( - { chain }, - 'No desired balance for Rebalancer, not funding', - ); - desiredBalanceEther = '0'; - } else { - desiredBalanceEther = this.desiredRebalancerBalancePerChain[chain]; - } - } else { - desiredBalanceEther = this.desiredBalancePerChain[chain]; - } - let desiredBalance = ethers.utils.parseEther(desiredBalanceEther ?? '0'); - if (this.context === Contexts.ReleaseCandidate) { - desiredBalance = desiredBalance - .mul(RC_FUNDING_DISCOUNT_NUMERATOR) - .div(RC_FUNDING_DISCOUNT_DENOMINATOR); - } - return desiredBalance; - } - - // Tops up the key's balance to the desired balance if the current balance - // is lower than the desired balance by the min delta - private async fundKeyIfRequired( - chain: ChainName, - key: BaseAgentKey, - desiredBalance: BigNumber, - ) { - const fundingAmount = await this.getFundingAmount( - chain, - key.address, - desiredBalance, - ); - const keyInfo = await getKeyInfo( - key, - chain, - this.multiProvider.getProvider(chain), - ); - const funderAddress = await this.multiProvider.getSignerAddress(chain); - - if (fundingAmount.eq(0)) { - logger.info( - { - key: keyInfo, - context: this.context, - chain, - }, - 'Skipping funding for key', - ); - return; - } - - logger.info( - { - chain, - amount: ethers.utils.formatEther(fundingAmount), - key: keyInfo, - funder: { - address: funderAddress, - balance: ethers.utils.formatEther( - await this.multiProvider.getSigner(chain).getBalance(), - ), - }, - context: this.context, - }, - 'Funding key', - ); - - const tx = await this.multiProvider.sendTransaction(chain, { - to: key.address, - value: fundingAmount, - }); - logger.info( - { - key: keyInfo, - txUrl: this.multiProvider.tryGetExplorerTxUrl(chain, { - hash: tx.transactionHash, - }), - context: this.context, - chain, - }, - 'Sent transaction', - ); - logger.info( - { - key: keyInfo, - tx, - context: this.context, - chain, - }, - 'Got transaction receipt', - ); - } - - private async bridgeToL2(l2Chain: ChainName, to: string, amount: BigNumber) { - const l1Chain = L2ToL1[l2Chain]; - logger.info( - { - l1Chain, - l2Chain, - amount: ethers.utils.formatEther(amount), - l1Funder: await getAddressInfo( - await this.multiProvider.getSignerAddress(l1Chain), - l1Chain, - this.multiProvider.getProvider(l1Chain), - ), - l2Funder: await getAddressInfo( - to, - l2Chain, - this.multiProvider.getProvider(l2Chain), - ), - }, - 'Bridging ETH to L2', - ); - let tx; - if (l2Chain.includes('optimism') || l2Chain.includes('base')) { - tx = await this.bridgeToOptimism(l2Chain, amount, to); - } else if (l2Chain.includes('arbitrum')) { - tx = await this.bridgeToArbitrum(l2Chain, amount); - } else if (l2Chain.includes('scroll')) { - tx = await this.bridgeToScroll(l2Chain, amount, to); - } else { - throw new Error(`${l2Chain} is not an L2`); - } - await this.multiProvider.handleTx(l1Chain, tx); - } - - private async bridgeToOptimism( - l2Chain: ChainName, - amount: BigNumber, - to: string, - ) { - const l1Chain = L2ToL1[l2Chain]; - const crossChainMessenger = new CrossChainMessenger({ - l1ChainId: this.multiProvider.getEvmChainId(l1Chain), - l2ChainId: this.multiProvider.getEvmChainId(l2Chain), - l1SignerOrProvider: this.multiProvider.getSignerOrProvider(l1Chain), - l2SignerOrProvider: this.multiProvider.getSignerOrProvider(l2Chain), - }); - return crossChainMessenger.depositETH(amount, { - recipient: to, - overrides: this.multiProvider.getTransactionOverrides(l1Chain), - }); - } - - private async bridgeToArbitrum(l2Chain: ChainName, amount: BigNumber) { - const l1Chain = L2ToL1[l2Chain]; - const l2Network = await getArbitrumNetwork( - this.multiProvider.getEvmChainId(l2Chain), - ); - const ethBridger = new EthBridger(l2Network); - return ethBridger.deposit({ - amount, - parentSigner: this.multiProvider.getSigner(l1Chain), - overrides: this.multiProvider.getTransactionOverrides(l1Chain), - }); - } - - private async bridgeToScroll( - l2Chain: ChainName, - amount: BigNumber, - to: Address, - ) { - const l1Chain = L2ToL1[l2Chain]; - const l1ChainSigner = this.multiProvider.getSigner(l1Chain); - const l1EthGateway = new ethers.Contract( - nativeBridges.scrollsepolia.l1ETHGateway, - L1ETHGateway.abi, - l1ChainSigner, - ); - const l1ScrollMessenger = new ethers.Contract( - nativeBridges.scrollsepolia.l1Messenger, - L1ScrollMessenger.abi, - l1ChainSigner, - ); - const l2GasLimit = BigNumber.from('200000'); // l2 gas amount for the transfer and an empty callback calls - const l1MessageQueueAddress = await l1ScrollMessenger.messageQueue(); - const l1MessageQueue = new ethers.Contract( - l1MessageQueueAddress, - L1MessageQueue.abi, - l1ChainSigner, - ); - const gasQuote = - await l1MessageQueue.estimateCrossDomainMessageFee(l2GasLimit); - const totalAmount = amount.add(gasQuote); - return l1EthGateway['depositETH(address,uint256,uint256)']( - to, - amount, - l2GasLimit, - { - value: totalAmount, - }, - ); - } - - private async updateWalletBalanceGauge(chain: ChainName) { - const funderAddress = await this.multiProvider.getSignerAddress(chain); - walletBalanceGauge - .labels({ - chain, - wallet_address: funderAddress ?? 'unknown', - wallet_name: 'key-funder', - token_symbol: 'Native', - token_name: 'Native', - ...constMetricLabels, - }) - .set( - parseFloat( - ethers.utils.formatEther( - await this.multiProvider.getSigner(chain).getBalance(), - ), - ), - ); - } -} - -async function getAddressInfo( - address: string, - chain: ChainName, - provider: ethers.providers.Provider, -) { - return { - chain, - balance: ethers.utils.formatEther(await provider.getBalance(address)), - address, - }; -} - -async function getKeyInfo( - key: BaseAgentKey, - chain: ChainName, - provider: ethers.providers.Provider, -) { - return { - ...(await getAddressInfo(key.address, chain, provider)), - context: (key as LocalAgentKey).context, - originChain: key.chainName, - role: key.role, - }; -} - -function parseContextAndRolesMap(strs: string[]): ContextAndRolesMap { - const contextsAndRoles = strs.map(parseContextAndRoles); - return contextsAndRoles.reduce( - (prev, curr) => ({ - ...prev, - [curr.context]: curr.roles, - }), - {}, - ); -} - -// Parses strings of the form =,,... -// e.g.: -// hyperlane=relayer -// flowcarbon=relayer,kathy -function parseContextAndRoles(str: string): ContextAndRoles { - const [contextStr, rolesStr] = str.split('='); - const context = assertContext(contextStr); - - const roles = rolesStr.split(',').map(assertRole); - if (roles.length === 0) { - throw Error('Expected > 0 roles'); - } - - // For now, restrict the valid roles we think are reasonable to want to fund - const validRoles = new Set([Role.Relayer, Role.Kathy, Role.Rebalancer]); - for (const role of roles) { - if (!validRoles.has(role)) { - throw Error( - `Invalid fundable role ${role}, must be one of ${Array.from( - validRoles, - )}`, - ); - } - } - - return { - context, - roles, - }; -} - -function parseBalancePerChain(strs: string[]): ChainMap { - const balanceMap: ChainMap = {}; - strs.forEach((str) => { - const [chain, balance] = str.split('='); - if (!chain || !balance) { - throw new Error(`Invalid format for balance entry: ${str}`); - } - balanceMap[chain] = balance; - }); - return balanceMap; -} - -// Utility function to create a timeout promise -function createTimeoutPromise( - timeoutMs: number, - errorMessage: string, -): { promise: Promise; cleanup: () => void } { - let cleanup: () => void; - const promise = new Promise((_, reject) => { - const timeout = setTimeout( - () => reject(new Error(errorMessage)), - timeoutMs, - ); - cleanup = () => clearTimeout(timeout); - }); - return { promise, cleanup: cleanup! }; -} - -main().catch((err) => { - logger.error( - { - // JSON.stringifying an Error returns '{}'. - // This is a workaround from https://stackoverflow.com/a/60370781 - error: format(err), - }, - 'Error occurred in main', - ); - process.exit(1); -}); diff --git a/typescript/infra/scripts/funding/reclaim-from-igp.ts b/typescript/infra/scripts/funding/reclaim-from-igp.ts index 747278c41b0..b4a0e165618 100644 --- a/typescript/infra/scripts/funding/reclaim-from-igp.ts +++ b/typescript/infra/scripts/funding/reclaim-from-igp.ts @@ -114,7 +114,7 @@ async function main() { const formattedBalance = formatEther(balance); // Get the threshold for this chain from config, default to 0.1 ETH if not set - // Fallback to 1/5th of desired balance if no threshold configured, matching fund-keys-from-deployer.ts logic + // Fallback to 1/5th of desired balance if no threshold configured let threshold: bigint; const thresholdStr = igpClaimThresholds[chain]; if (thresholdStr) { diff --git a/typescript/infra/src/funding/key-funder.ts b/typescript/infra/src/funding/key-funder.ts index 0389a36f0ab..07836c7a60b 100644 --- a/typescript/infra/src/funding/key-funder.ts +++ b/typescript/infra/src/funding/key-funder.ts @@ -1,13 +1,24 @@ import { join } from 'path'; +import YAML from 'yaml'; + +import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; import { Contexts } from '../../config/contexts.js'; +import rebalancerAddresses from '../../config/rebalancer.json' with { type: 'json' }; +import { getEnvAddresses } from '../../config/registry.js'; import { getAgentConfig } from '../../scripts/agent-utils.js'; import { getEnvironmentConfig } from '../../scripts/core-utils.js'; +import { + fetchLocalKeyAddresses, + kathyAddresses, + relayerAddresses, +} from '../agents/key-utils.js'; import { AgentContextConfig } from '../config/agent/agent.js'; import { DeployEnvironment, EnvironmentConfig } from '../config/environment.js'; -import { KeyFunderConfig } from '../config/funding.js'; +import { ContextAndRolesMap, KeyFunderConfig } from '../config/funding.js'; +import { FundableRole, Role } from '../roles.js'; import { HelmManager } from '../utils/helm.js'; -import { getInfraPath } from '../utils/utils.js'; +import { getInfraPath, isEthereumProtocolChain } from '../utils/utils.js'; export class KeyFunderHelmManager extends HelmManager { readonly helmReleaseName: string = 'key-funder'; @@ -16,16 +27,23 @@ export class KeyFunderHelmManager extends HelmManager { constructor( readonly config: KeyFunderConfig, readonly agentConfig: AgentContextConfig, + readonly registryCommit: string, ) { super(); } - static forEnvironment(environment: DeployEnvironment): KeyFunderHelmManager { + static forEnvironment( + environment: DeployEnvironment, + registryCommit: string, + ): KeyFunderHelmManager { const envConfig = getEnvironmentConfig(environment); const keyFunderConfig = getKeyFunderConfig(envConfig); - // Always use Hyperlane context for key funder const agentConfig = getAgentConfig(Contexts.Hyperlane, environment); - return new KeyFunderHelmManager(keyFunderConfig, agentConfig); + return new KeyFunderHelmManager( + keyFunderConfig, + agentConfig, + registryCommit, + ); } get namespace() { @@ -33,32 +51,194 @@ export class KeyFunderHelmManager extends HelmManager { } async helmValues() { + const registryUri = `${DEFAULT_GITHUB_REGISTRY}/tree/${this.registryCommit}`; + const keyfunderConfig = this.generateKeyfunderYaml(); + return { cronjob: { schedule: this.config.cronSchedule, }, hyperlane: { runEnv: this.agentConfig.runEnv, - // Only used for fetching RPC urls as env vars - chains: this.agentConfig.environmentChainNames, - contextFundingFrom: this.config.contextFundingFrom, - contextsAndRolesToFund: this.config.contextsAndRolesToFund, - desiredBalancePerChain: this.config.desiredBalancePerChain, - desiredKathyBalancePerChain: this.config.desiredKathyBalancePerChain, - desiredRebalancerBalancePerChain: - this.config.desiredRebalancerBalancePerChain, - igpClaimThresholdPerChain: this.config.igpClaimThresholdPerChain, + chains: this.getEthereumChains(), + registryUri, + keyfunderConfig, chainsToSkip: this.config.chainsToSkip, + skipIgpClaim: false, }, image: { - repository: this.config.docker.repo, + repository: 'gcr.io/abacus-labs-dev/hyperlane-keyfunder', tag: this.config.docker.tag, }, - infra: { - prometheusPushGateway: this.config.prometheusPushGateway, + }; + } + + private getEthereumChains(): string[] { + return this.agentConfig.environmentChainNames.filter((chain) => + isEthereumProtocolChain(chain), + ); + } + + private generateKeyfunderYaml(): string { + const environment = this.agentConfig.runEnv; + const chains: Record = {}; + const envAddresses = getEnvAddresses(environment); + + for (const chain of this.getEthereumChains()) { + if (this.config.chainsToSkip?.includes(chain)) continue; + + const chainConfig: ChainYamlConfig = {}; + + const keys = this.getKeysForChain(chain, environment); + if (keys.length > 0) { + chainConfig.keys = keys; + } + + const igpAddress = envAddresses[chain]?.interchainGasPaymaster; + const igpThreshold = this.config.igpClaimThresholdPerChain?.[chain]; + if (igpAddress && igpThreshold) { + chainConfig.igp = { + address: igpAddress, + claimThreshold: igpThreshold, + }; + } + + const sweepThreshold = this.config.lowUrgencyKeyFunderBalances?.[chain]; + if (sweepThreshold && parseFloat(sweepThreshold) > 0) { + const override = this.config.sweepOverrides?.[chain]; + chainConfig.sweep = { + enabled: true, + address: + override?.sweepAddress ?? + '0x478be6076f31E9666123B9721D0B6631baD944AF', + threshold: sweepThreshold, + targetMultiplier: override?.targetMultiplier ?? 1.5, + triggerMultiplier: override?.triggerMultiplier ?? 2.0, + }; + } + + if (Object.keys(chainConfig).length > 0) { + chains[chain] = chainConfig; + } + } + + const config = { + version: '1' as const, + chains, + funder: { + privateKeyEnvVar: 'FUNDER_PRIVATE_KEY', + }, + metrics: { + pushGateway: this.config.prometheusPushGateway, + jobName: `keyfunder-${environment}`, + labels: { + environment, + }, }, + chainsToSkip: this.config.chainsToSkip, }; + + return YAML.stringify(config); } + + private getKeysForChain( + chain: string, + environment: DeployEnvironment, + ): KeyYamlConfig[] { + const keys: KeyYamlConfig[] = []; + const contextsAndRoles = this.config.contextsAndRolesToFund; + + for (const [contextStr, roles] of Object.entries(contextsAndRoles)) { + const context = contextStr as Contexts; + if (!roles) continue; + + for (const role of roles) { + const address = this.getAddressForRole(environment, context, role); + if (!address) continue; + + const desiredBalance = this.getDesiredBalanceForRole(chain, role); + if (!desiredBalance || desiredBalance === '0') continue; + + keys.push({ + address, + role: `${context}-${role}`, + desiredBalance, + }); + } + } + + return keys; + } + + private getAddressForRole( + environment: DeployEnvironment, + context: Contexts, + role: FundableRole, + ): string | undefined { + const envAddresses = this.getRoleAddresses(role); + return envAddresses?.[environment]?.[context]; + } + + private getRoleAddresses( + role: FundableRole, + ): Record> | undefined { + switch (role) { + case Role.Relayer: + return relayerAddresses as Record< + DeployEnvironment, + Record + >; + case Role.Kathy: + return kathyAddresses as Record< + DeployEnvironment, + Record + >; + case Role.Rebalancer: + return rebalancerAddresses as Record< + DeployEnvironment, + Record + >; + default: + return undefined; + } + } + + private getDesiredBalanceForRole( + chain: string, + role: FundableRole, + ): string | undefined { + switch (role) { + case Role.Relayer: + return this.config.desiredBalancePerChain[chain]; + case Role.Kathy: + return this.config.desiredKathyBalancePerChain?.[chain]; + case Role.Rebalancer: + return this.config.desiredRebalancerBalancePerChain?.[chain]; + default: + return undefined; + } + } +} + +interface KeyYamlConfig { + address: string; + role: string; + desiredBalance: string; +} + +interface ChainYamlConfig { + keys?: KeyYamlConfig[]; + igp?: { + address: string; + claimThreshold: string; + }; + sweep?: { + enabled: boolean; + address: string; + threshold: string; + targetMultiplier: number; + triggerMultiplier: number; + }; } export function getKeyFunderConfig( diff --git a/typescript/infra/src/utils/rpcUrls.ts b/typescript/infra/src/utils/rpcUrls.ts index e41da725dd4..b76239f4555 100644 --- a/typescript/infra/src/utils/rpcUrls.ts +++ b/typescript/infra/src/utils/rpcUrls.ts @@ -316,10 +316,9 @@ async function refreshDependentK8sResourcesInteractive( } if (context == Contexts.Hyperlane) { - // Key funder pushContextHelmManager( context, - KeyFunderHelmManager.forEnvironment(environment), + KeyFunderHelmManager.forEnvironment(environment, 'main'), ); // Kathy - only expected to be running as a long-running service in the diff --git a/typescript/keyfunder/.gitignore b/typescript/keyfunder/.gitignore new file mode 100644 index 00000000000..6e2fc577008 --- /dev/null +++ b/typescript/keyfunder/.gitignore @@ -0,0 +1,7 @@ +.env* +/dist +/bundle +/cache + +# allow check-in of .env.example +!.env.example diff --git a/typescript/keyfunder/.mocharc.json b/typescript/keyfunder/.mocharc.json new file mode 100644 index 00000000000..2a414eb587f --- /dev/null +++ b/typescript/keyfunder/.mocharc.json @@ -0,0 +1,3 @@ +{ + "import": ["tsx"] +} diff --git a/typescript/keyfunder/Dockerfile b/typescript/keyfunder/Dockerfile new file mode 100644 index 00000000000..5086bff5317 --- /dev/null +++ b/typescript/keyfunder/Dockerfile @@ -0,0 +1,73 @@ +FROM node:20-slim AS builder + +WORKDIR /hyperlane-monorepo + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git g++ make python3 python3-pip jq bash curl ca-certificates unzip \ + && rm -rf /var/lib/apt/lists/* + +ARG FOUNDRY_VERSION +ARG SERVICE_VERSION=dev +ARG TARGETARCH +SHELL ["/bin/bash", "-c"] +RUN set -o pipefail && \ + ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") && \ + curl --fail -L "https://github.com/foundry-rs/foundry/releases/download/${FOUNDRY_VERSION}/foundry_${FOUNDRY_VERSION}_linux_${ARCH}.tar.gz" | tar -xzC /usr/local/bin forge cast +SHELL ["/bin/sh", "-c"] + +COPY package.json ./ +RUN corepack enable && corepack install + +COPY pnpm-lock.yaml pnpm-workspace.yaml ./ + +COPY patches ./patches + +COPY typescript/keyfunder/package.json ./typescript/keyfunder/ +COPY typescript/sdk/package.json ./typescript/sdk/ +COPY typescript/provider-sdk/package.json ./typescript/provider-sdk/ +COPY typescript/utils/package.json ./typescript/utils/ +COPY typescript/cosmos-sdk/package.json ./typescript/cosmos-sdk/ +COPY typescript/cosmos-types/package.json ./typescript/cosmos-types/ +COPY typescript/radix-sdk/package.json ./typescript/radix-sdk/ +COPY typescript/tsconfig/package.json ./typescript/tsconfig/ +COPY typescript/eslint-config/package.json ./typescript/eslint-config/ +COPY typescript/deploy-sdk/package.json ./typescript/deploy-sdk/ +COPY solidity/package.json ./solidity/ +COPY solhint-plugin/package.json ./solhint-plugin/ +COPY starknet/package.json ./starknet/ + +RUN pnpm install --frozen-lockfile + +COPY turbo.json ./ +COPY typescript/keyfunder ./typescript/keyfunder +COPY typescript/sdk ./typescript/sdk +COPY typescript/provider-sdk ./typescript/provider-sdk +COPY typescript/utils ./typescript/utils +COPY typescript/cosmos-sdk ./typescript/cosmos-sdk +COPY typescript/cosmos-types ./typescript/cosmos-types +COPY typescript/radix-sdk ./typescript/radix-sdk +COPY typescript/tsconfig ./typescript/tsconfig +COPY typescript/eslint-config ./typescript/eslint-config +COPY typescript/deploy-sdk ./typescript/deploy-sdk +COPY solidity ./solidity +COPY solhint-plugin ./solhint-plugin +COPY starknet ./starknet + +RUN pnpm turbo run bundle --filter=@hyperlane-xyz/keyfunder + +FROM node:20-alpine AS runner + +WORKDIR /app + +RUN apk add --no-cache ca-certificates + +COPY --from=builder /hyperlane-monorepo/typescript/keyfunder/bundle ./bundle + +RUN npm install @google-cloud/pino-logging-gcp-config + +ARG SERVICE_VERSION=dev +ENV NODE_ENV=production +ENV LOG_LEVEL=info +ENV SERVICE_VERSION=${SERVICE_VERSION} + +CMD ["node", "bundle/index.js"] diff --git a/typescript/keyfunder/README.md b/typescript/keyfunder/README.md new file mode 100644 index 00000000000..f237c0649df --- /dev/null +++ b/typescript/keyfunder/README.md @@ -0,0 +1,156 @@ +# @hyperlane-xyz/keyfunder + +Standalone service for funding Hyperlane agent keys with native tokens across multiple chains. + +## Overview + +The KeyFunder service: + +- Funds agent keys (relayers, kathy, rebalancer) to maintain desired balances +- Claims accumulated fees from InterchainGasPaymaster (IGP) contracts +- Sweeps excess funds from the funder wallet to a safe address + +## Configuration + +The service reads configuration from a YAML file specified by `KEYFUNDER_CONFIG_FILE` environment variable. + +### Example Configuration + +```yaml +version: '1' +chains: + ethereum: + keys: + - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5' + role: 'hyperlane-relayer' + desiredBalance: '0.5' + - address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20' + role: 'hyperlane-kathy' + desiredBalance: '0.4' + igp: + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7' + claimThreshold: '0.2' + sweep: + enabled: true + address: '0x478be6076f31E9666123B9721D0B6631baD944AF' + threshold: '0.3' + targetMultiplier: 1.5 + triggerMultiplier: 2.0 + arbitrum: + keys: + - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5' + role: 'hyperlane-relayer' + desiredBalance: '0.1' +funder: + privateKeyEnvVar: 'FUNDER_PRIVATE_KEY' +metrics: + pushGateway: 'http://prometheus-pushgateway:9091' + jobName: 'keyfunder-mainnet3' + labels: + environment: 'mainnet3' +chainsToSkip: [] +``` + +### Configuration Options + +| Field | Description | +| ---------------------------------------- | ------------------------------------------------ | +| `version` | Config version, must be "1" | +| `chains` | Per-chain configuration | +| `chains..keys` | Array of keys to fund | +| `chains..keys[].address` | Ethereum address of the key | +| `chains..keys[].role` | Optional role identifier for metrics | +| `chains..keys[].desiredBalance` | Target balance in native token (e.g., "0.5" ETH) | +| `chains..igp` | IGP claim configuration | +| `chains..igp.address` | IGP contract address | +| `chains..igp.claimThreshold` | Minimum IGP balance before claiming | +| `chains..sweep` | Sweep excess funds configuration | +| `chains..sweep.enabled` | Enable sweep functionality | +| `chains..sweep.address` | Address to sweep funds to | +| `chains..sweep.threshold` | Base threshold for sweep calculations | +| `chains..sweep.targetMultiplier` | Multiplier for target balance (default: 1.5) | +| `chains..sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0) | +| `funder.privateKeyEnvVar` | Environment variable name for funder private key | +| `metrics.pushGateway` | Prometheus push gateway URL | +| `metrics.jobName` | Job name for metrics | +| `metrics.labels` | Additional labels for metrics | +| `chainsToSkip` | Array of chain names to skip | + +## Environment Variables + +| Variable | Description | Required | +| ----------------------- | ----------------------------------------------------- | -------- | +| `KEYFUNDER_CONFIG_FILE` | Path to config YAML file | Yes | +| `FUNDER_PRIVATE_KEY` | Private key for funding wallet (or custom via config) | Yes | +| `REGISTRY_URI` | Hyperlane registry URI (default: GitHub registry) | No | +| `RPC_URL_` | RPC override per chain (e.g., `RPC_URL_ETHEREUM`) | No | +| `SKIP_IGP_CLAIM` | Set to "true" to skip IGP claims | No | +| `LOG_LEVEL` | Log level: DEBUG, INFO, WARN, ERROR | No | +| `LOG_FORMAT` | Log format: JSON, PRETTY | No | + +## Usage + +### Docker + +```bash +docker run -v /path/to/config.yaml:/config/keyfunder.yaml \ + -e KEYFUNDER_CONFIG_FILE=/config/keyfunder.yaml \ + -e FUNDER_PRIVATE_KEY=0x... \ + gcr.io/abacus-labs-dev/hyperlane-keyfunder:latest +``` + +### Local Development + +```bash +# Build +pnpm build + +# Run with tsx +KEYFUNDER_CONFIG_FILE=./config.yaml FUNDER_PRIVATE_KEY=0x... pnpm start:dev + +# Run built version +KEYFUNDER_CONFIG_FILE=./config.yaml FUNDER_PRIVATE_KEY=0x... pnpm start +``` + +### Bundle + +The service can be bundled into a single file using ncc: + +```bash +pnpm bundle +# Output: ./bundle/index.js +``` + +## Funding Logic + +### Key Funding + +Keys are funded when their balance drops below 60% of the desired balance. The funding amount brings the balance up to the full desired balance. + +### IGP Claims + +When the IGP contract balance exceeds the claim threshold, accumulated fees are claimed to the funder wallet. + +### Sweep + +When the funder wallet balance exceeds `threshold * triggerMultiplier`, excess funds are swept to the safe address, leaving `threshold * targetMultiplier` in the wallet. + +## Metrics + +The service exposes Prometheus metrics: + +| Metric | Description | +| ------------------------------------------------ | ---------------------------- | +| `hyperlane_keyfunder_wallet_balance` | Current wallet balance | +| `hyperlane_keyfunder_funding_amount` | Amount funded to a key | +| `hyperlane_keyfunder_igp_balance` | IGP contract balance | +| `hyperlane_keyfunder_sweep_amount` | Amount swept to safe address | +| `hyperlane_keyfunder_operation_duration_seconds` | Duration of operations | + +## Deployment + +The service is typically deployed as a Kubernetes CronJob. See `typescript/infra/helm/key-funder/` for the Helm chart. + +## License + +Apache-2.0 diff --git a/typescript/keyfunder/eslint.config.mjs b/typescript/keyfunder/eslint.config.mjs new file mode 100644 index 00000000000..cfc34554685 --- /dev/null +++ b/typescript/keyfunder/eslint.config.mjs @@ -0,0 +1,13 @@ +import MonorepoDefaults from '../../eslint.config.mjs'; + +export default [ + ...MonorepoDefaults, + { + files: ['./src/**/*.ts'], + }, + { + rules: { + 'no-restricted-imports': ['off'], + }, + }, +]; diff --git a/typescript/keyfunder/package.json b/typescript/keyfunder/package.json new file mode 100644 index 00000000000..97f7e1b05d2 --- /dev/null +++ b/typescript/keyfunder/package.json @@ -0,0 +1,63 @@ +{ + "name": "@hyperlane-xyz/keyfunder", + "version": "0.0.0", + "private": true, + "description": "Hyperlane Key Funder Service - funds agent keys with native tokens", + "type": "module", + "scripts": { + "build": "tsc", + "bundle": "rm -rf ./bundle && ncc build ./dist/service.js -o bundle -e @google-cloud/pino-logging-gcp-config && node ./scripts/ncc.post-bundle.mjs", + "clean": "rm -rf dist cache bundle", + "dev": "tsc --watch", + "lint": "eslint -c ./eslint.config.mjs ./src", + "prettier": "prettier --write ./src", + "test": "mocha --config .mocharc.json './src/**/*.test.ts' --exit", + "test:ci": "pnpm test", + "start": "node dist/service.js", + "start:dev": "tsx src/service.ts" + }, + "dependencies": { + "@google-cloud/pino-logging-gcp-config": "catalog:", + "@hyperlane-xyz/core": "workspace:*", + "@hyperlane-xyz/registry": "catalog:", + "@hyperlane-xyz/sdk": "workspace:*", + "@hyperlane-xyz/utils": "workspace:*", + "ethers": "catalog:", + "pino": "catalog:", + "pino-pretty": "catalog:", + "prom-client": "catalog:", + "yaml": "catalog:", + "zod": "catalog:", + "zod-validation-error": "catalog:" + }, + "devDependencies": { + "@hyperlane-xyz/tsconfig": "workspace:^", + "@types/chai-as-promised": "catalog:", + "@types/mocha": "catalog:", + "@types/node": "catalog:", + "@types/sinon": "catalog:", + "@vercel/ncc": "catalog:", + "chai": "catalog:", + "chai-as-promised": "catalog:", + "eslint": "catalog:", + "mocha": "catalog:", + "prettier": "catalog:", + "sinon": "catalog:", + "tsx": "catalog:", + "typescript": "catalog:" + }, + "engines": { + "node": ">=18" + }, + "repository": "https://github.com/hyperlane-xyz/hyperlane-monorepo", + "keywords": [ + "hyperlane", + "keyfunder", + "funding", + "agent", + "blockchain", + "interchain" + ], + "author": "Abacus Works, Inc.", + "license": "Apache-2.0" +} diff --git a/typescript/keyfunder/scripts/ncc.post-bundle.mjs b/typescript/keyfunder/scripts/ncc.post-bundle.mjs new file mode 100644 index 00000000000..aa3f27cbcf0 --- /dev/null +++ b/typescript/keyfunder/scripts/ncc.post-bundle.mjs @@ -0,0 +1,37 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { readFile, writeFile } from 'fs/promises'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const outputFile = path.join(__dirname, '..', 'bundle', 'index.js'); + +const shebang = '#!/usr/bin/env node'; +const dirnameDef = `import { fileURLToPath } from 'url'; +import path from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename);`; + +async function prepend() { + try { + const content = await readFile(outputFile, 'utf8'); + + if (!content.startsWith(shebang)) { + throw new Error('Missing shebang from service entry point'); + } + + if (!content.includes(dirnameDef)) { + const [, executable] = content.split(shebang); + const newContent = `${shebang}\n${dirnameDef}\n${executable}`; + await writeFile(outputFile, newContent, 'utf8'); + console.log('Adding missing __dirname definition to service executable'); + } + } catch (err) { + console.error('Error processing output file:', err); + process.exit(1); + } +} + +await prepend(); diff --git a/typescript/keyfunder/src/config/KeyFunderConfig.test.ts b/typescript/keyfunder/src/config/KeyFunderConfig.test.ts new file mode 100644 index 00000000000..88018b0b4fc --- /dev/null +++ b/typescript/keyfunder/src/config/KeyFunderConfig.test.ts @@ -0,0 +1,176 @@ +import { expect } from 'chai'; +import fs from 'fs'; +import sinon from 'sinon'; + +import { KeyFunderConfigLoader } from './KeyFunderConfig.js'; + +describe('KeyFunderConfigLoader', () => { + let fsExistsStub: sinon.SinonStub; + let fsReadFileStub: sinon.SinonStub; + + beforeEach(() => { + fsExistsStub = sinon.stub(fs, 'existsSync'); + fsReadFileStub = sinon.stub(fs, 'readFileSync'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('load', () => { + it('should load valid config from file', () => { + const configYaml = ` +version: "1" +chains: + ethereum: + keys: + - address: "0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5" + role: "hyperlane-relayer" + desiredBalance: "0.5" +`; + fsExistsStub.returns(true); + fsReadFileStub.returns(configYaml); + + const loader = KeyFunderConfigLoader.load('/path/to/config.yaml'); + + expect(loader.config.version).to.equal('1'); + expect(loader.config.chains.ethereum.keys).to.have.lengthOf(1); + expect(loader.config.chains.ethereum.keys![0].address).to.equal( + '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + ); + }); + + it('should throw if file does not exist', () => { + fsExistsStub.returns(false); + + expect(() => KeyFunderConfigLoader.load('/nonexistent.yaml')).to.throw( + 'Config file not found', + ); + }); + + it('should throw on invalid config', () => { + const invalidYaml = ` +version: "2" +chains: {} +`; + fsExistsStub.returns(true); + fsReadFileStub.returns(invalidYaml); + + expect(() => KeyFunderConfigLoader.load('/path/to/config.yaml')).to.throw( + 'Invalid keyfunder config', + ); + }); + }); + + describe('fromObject', () => { + it('should create loader from valid object', () => { + const config = { + version: '1' as const, + chains: { + ethereum: { + keys: [ + { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + desiredBalance: '0.5', + }, + ], + }, + }, + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + expect(loader.config.chains.ethereum.keys).to.have.lengthOf(1); + }); + + it('should throw on invalid object', () => { + const config = { + version: '2', + chains: {}, + }; + + expect(() => + KeyFunderConfigLoader.fromObject( + config as unknown as { version: '1'; chains: Record }, + ), + ).to.throw('Invalid keyfunder config'); + }); + }); + + describe('getConfiguredChains', () => { + it('should return all chain names', () => { + const config = { + version: '1' as const, + chains: { + ethereum: { keys: [] }, + arbitrum: { keys: [] }, + polygon: { keys: [] }, + }, + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + const chains = loader.getConfiguredChains(); + + expect(chains).to.have.members(['ethereum', 'arbitrum', 'polygon']); + }); + }); + + describe('getChainsToProcess', () => { + it('should exclude skipped chains', () => { + const config = { + version: '1' as const, + chains: { + ethereum: { keys: [] }, + arbitrum: { keys: [] }, + polygon: { keys: [] }, + }, + chainsToSkip: ['polygon'], + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + const chains = loader.getChainsToProcess(); + + expect(chains).to.have.members(['ethereum', 'arbitrum']); + expect(chains).to.not.include('polygon'); + }); + + it('should return all chains when none skipped', () => { + const config = { + version: '1' as const, + chains: { + ethereum: { keys: [] }, + arbitrum: { keys: [] }, + }, + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + const chains = loader.getChainsToProcess(); + + expect(chains).to.have.members(['ethereum', 'arbitrum']); + }); + }); + + describe('getFunderPrivateKeyEnvVar', () => { + it('should return configured env var name', () => { + const config = { + version: '1' as const, + chains: {}, + funder: { + privateKeyEnvVar: 'CUSTOM_PRIVATE_KEY', + }, + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + expect(loader.getFunderPrivateKeyEnvVar()).to.equal('CUSTOM_PRIVATE_KEY'); + }); + + it('should return default when not configured', () => { + const config = { + version: '1' as const, + chains: {}, + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + expect(loader.getFunderPrivateKeyEnvVar()).to.equal('FUNDER_PRIVATE_KEY'); + }); + }); +}); diff --git a/typescript/keyfunder/src/config/KeyFunderConfig.ts b/typescript/keyfunder/src/config/KeyFunderConfig.ts new file mode 100644 index 00000000000..6aab92689dc --- /dev/null +++ b/typescript/keyfunder/src/config/KeyFunderConfig.ts @@ -0,0 +1,55 @@ +import fs from 'fs'; +import YAML from 'yaml'; +import { fromZodError } from 'zod-validation-error'; + +import { + KeyFunderConfig, + KeyFunderConfigInput, + KeyFunderConfigSchema, +} from './types.js'; + +export class KeyFunderConfigLoader { + private constructor(public readonly config: KeyFunderConfig) {} + + static load(filePath: string): KeyFunderConfigLoader { + if (!fs.existsSync(filePath)) { + throw new Error(`Config file not found: ${filePath}`); + } + + const content = fs.readFileSync(filePath, 'utf8'); + const rawConfig: KeyFunderConfigInput = YAML.parse(content); + + const validationResult = KeyFunderConfigSchema.safeParse(rawConfig); + if (!validationResult.success) { + throw new Error( + `Invalid keyfunder config: ${fromZodError(validationResult.error).message}`, + ); + } + + return new KeyFunderConfigLoader(validationResult.data); + } + + static fromObject(config: KeyFunderConfigInput): KeyFunderConfigLoader { + const validationResult = KeyFunderConfigSchema.safeParse(config); + if (!validationResult.success) { + throw new Error( + `Invalid keyfunder config: ${fromZodError(validationResult.error).message}`, + ); + } + return new KeyFunderConfigLoader(validationResult.data); + } + + getConfiguredChains(): string[] { + return Object.keys(this.config.chains); + } + + getChainsToProcess(): string[] { + const allChains = this.getConfiguredChains(); + const chainsToSkip = new Set(this.config.chainsToSkip ?? []); + return allChains.filter((chain) => !chainsToSkip.has(chain)); + } + + getFunderPrivateKeyEnvVar(): string { + return this.config.funder?.privateKeyEnvVar ?? 'FUNDER_PRIVATE_KEY'; + } +} diff --git a/typescript/keyfunder/src/config/types.test.ts b/typescript/keyfunder/src/config/types.test.ts new file mode 100644 index 00000000000..96386aa12a9 --- /dev/null +++ b/typescript/keyfunder/src/config/types.test.ts @@ -0,0 +1,229 @@ +import { expect } from 'chai'; + +import { + ChainConfigSchema, + KeyConfigSchema, + KeyFunderConfigSchema, + SweepConfigSchema, +} from './types.js'; + +describe('KeyFunderConfig Schemas', () => { + describe('KeyConfigSchema', () => { + it('should validate a valid key config', () => { + const config = { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + role: 'hyperlane-relayer', + desiredBalance: '0.5', + }; + const result = KeyConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should reject invalid address', () => { + const config = { + address: 'invalid-address', + desiredBalance: '0.5', + }; + const result = KeyConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should reject invalid balance', () => { + const config = { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + desiredBalance: 'not-a-number', + }; + const result = KeyConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should reject negative balance', () => { + const config = { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + desiredBalance: '-1', + }; + const result = KeyConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should allow optional role', () => { + const config = { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + desiredBalance: '0.5', + }; + const result = KeyConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + }); + + describe('SweepConfigSchema', () => { + it('should validate valid sweep config', () => { + const config = { + enabled: true, + address: '0x478be6076f31E9666123B9721D0B6631baD944AF', + threshold: '0.5', + targetMultiplier: 1.5, + triggerMultiplier: 2.0, + }; + const result = SweepConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should reject trigger multiplier less than target + 0.05', () => { + const config = { + enabled: true, + address: '0x478be6076f31E9666123B9721D0B6631baD944AF', + threshold: '0.5', + targetMultiplier: 1.5, + triggerMultiplier: 1.52, // Less than 1.5 + 0.05 + }; + const result = SweepConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should use default multipliers', () => { + const config = { + enabled: true, + threshold: '0.5', + }; + const result = SweepConfigSchema.safeParse(config); + expect(result.success).to.be.true; + if (result.success) { + expect(result.data.targetMultiplier).to.equal(1.5); + expect(result.data.triggerMultiplier).to.equal(2.0); + } + }); + + it('should skip validation when disabled', () => { + const config = { + enabled: false, + targetMultiplier: 1.5, + triggerMultiplier: 1.5, // Would fail if enabled + }; + const result = SweepConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + }); + + describe('ChainConfigSchema', () => { + it('should validate chain config with keys only', () => { + const config = { + keys: [ + { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + desiredBalance: '0.5', + }, + ], + }; + const result = ChainConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should validate chain config with igp', () => { + const config = { + igp: { + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', + claimThreshold: '0.2', + }, + }; + const result = ChainConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should validate complete chain config', () => { + const config = { + keys: [ + { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + role: 'relayer', + desiredBalance: '0.5', + }, + ], + igp: { + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', + claimThreshold: '0.2', + }, + sweep: { + enabled: true, + threshold: '0.3', + }, + }; + const result = ChainConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + }); + + describe('KeyFunderConfigSchema', () => { + it('should validate minimal config', () => { + const config = { + version: '1', + chains: {}, + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should reject invalid version', () => { + const config = { + version: '2', + chains: {}, + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should validate complete config', () => { + const config = { + version: '1', + chains: { + ethereum: { + keys: [ + { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + role: 'hyperlane-relayer', + desiredBalance: '0.5', + }, + ], + igp: { + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', + claimThreshold: '0.2', + }, + sweep: { + enabled: true, + address: '0x478be6076f31E9666123B9721D0B6631baD944AF', + threshold: '0.3', + }, + }, + }, + funder: { + privateKeyEnvVar: 'FUNDER_PRIVATE_KEY', + }, + metrics: { + pushGateway: 'http://prometheus:9091', + jobName: 'keyfunder', + labels: { + environment: 'mainnet3', + }, + }, + chainsToSkip: ['polygon'], + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should apply default funder privateKeyEnvVar', () => { + const config = { + version: '1', + chains: {}, + funder: {}, + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.true; + if (result.success) { + expect(result.data.funder?.privateKeyEnvVar).to.equal( + 'FUNDER_PRIVATE_KEY', + ); + } + }); + }); +}); diff --git a/typescript/keyfunder/src/config/types.ts b/typescript/keyfunder/src/config/types.ts new file mode 100644 index 00000000000..32acc1473d8 --- /dev/null +++ b/typescript/keyfunder/src/config/types.ts @@ -0,0 +1,94 @@ +import { z } from 'zod'; + +const AddressSchema = z + .string() + .regex( + /^0x[a-fA-F0-9]{40}$/, + 'Must be a valid Ethereum address (0x-prefixed, 40 hex characters)', + ); + +const BalanceStringSchema = z + .string() + .refine( + (val) => !isNaN(parseFloat(val)) && parseFloat(val) >= 0, + 'Must be a valid non-negative number string', + ); + +export const KeyConfigSchema = z.object({ + address: AddressSchema, + role: z.string().optional(), + desiredBalance: BalanceStringSchema, +}); + +export const IgpConfigSchema = z.object({ + address: AddressSchema, + claimThreshold: BalanceStringSchema, +}); + +const MIN_TRIGGER_DIFFERENCE = 0.05; +const MIN_TARGET = 1.05; +const MIN_TRIGGER = 1.1; +const MAX_TARGET = 10.0; +const MAX_TRIGGER = 200.0; + +export const SweepConfigSchema = z + .object({ + enabled: z.boolean().default(false), + address: AddressSchema.optional(), + targetMultiplier: z + .number() + .min(MIN_TARGET, `Target multiplier must be at least ${MIN_TARGET}`) + .max(MAX_TARGET, `Target multiplier must be at most ${MAX_TARGET}`) + .default(1.5), + triggerMultiplier: z + .number() + .min(MIN_TRIGGER, `Trigger multiplier must be at least ${MIN_TRIGGER}`) + .max(MAX_TRIGGER, `Trigger multiplier must be at most ${MAX_TRIGGER}`) + .default(2.0), + threshold: BalanceStringSchema.optional(), + }) + .refine( + (data) => { + if (!data.enabled) return true; + return ( + data.triggerMultiplier >= data.targetMultiplier + MIN_TRIGGER_DIFFERENCE + ); + }, + { + message: `Trigger multiplier must be at least ${MIN_TRIGGER_DIFFERENCE} greater than target multiplier`, + path: ['triggerMultiplier'], + }, + ); + +export const ChainConfigSchema = z.object({ + keys: z.array(KeyConfigSchema).optional(), + igp: IgpConfigSchema.optional(), + sweep: SweepConfigSchema.optional(), +}); + +export const FunderConfigSchema = z.object({ + privateKeyEnvVar: z.string().default('FUNDER_PRIVATE_KEY'), +}); + +export const MetricsConfigSchema = z.object({ + pushGateway: z.string().optional(), + jobName: z.string().default('keyfunder'), + labels: z.record(z.string(), z.string()).optional(), +}); + +export const KeyFunderConfigSchema = z.object({ + version: z.literal('1'), + chains: z.record(z.string(), ChainConfigSchema), + funder: FunderConfigSchema.optional(), + metrics: MetricsConfigSchema.optional(), + chainsToSkip: z.array(z.string()).optional(), +}); + +export type KeyConfig = z.infer; +export type IgpConfig = z.infer; +export type SweepConfig = z.infer; +export type ChainConfig = z.infer; +export type FunderConfig = z.infer; +export type MetricsConfig = z.infer; +export type KeyFunderConfig = z.infer; +export type KeyFunderConfigInput = z.input; diff --git a/typescript/keyfunder/src/core/KeyFunder.ts b/typescript/keyfunder/src/core/KeyFunder.ts new file mode 100644 index 00000000000..ebc5a875fc7 --- /dev/null +++ b/typescript/keyfunder/src/core/KeyFunder.ts @@ -0,0 +1,325 @@ +import { BigNumber, ethers } from 'ethers'; +import type { Logger } from 'pino'; + +import { HyperlaneIgp, MultiProvider } from '@hyperlane-xyz/sdk'; + +import type { + ChainConfig, + KeyConfig, + KeyFunderConfig, +} from '../config/types.js'; +import type { KeyFunderMetrics } from '../metrics/Metrics.js'; + +const MIN_DELTA_NUMERATOR = BigNumber.from(6); +const MIN_DELTA_DENOMINATOR = BigNumber.from(10); + +const DEFAULT_SWEEP_ADDRESS = '0x478be6076f31E9666123B9721D0B6631baD944AF'; +const DEFAULT_TARGET_MULTIPLIER = 1.5; +const DEFAULT_TRIGGER_MULTIPLIER = 2.0; + +const CHAIN_FUNDING_TIMEOUT_MS = 60_000; + +export interface KeyFunderOptions { + logger: Logger; + metrics?: KeyFunderMetrics; + skipIgpClaim?: boolean; + igp?: HyperlaneIgp; +} + +export class KeyFunder { + constructor( + private readonly multiProvider: MultiProvider, + private readonly config: KeyFunderConfig, + private readonly options: KeyFunderOptions, + ) {} + + async fundAllChains(): Promise { + const chainsToSkip = new Set(this.config.chainsToSkip ?? []); + const chains = Object.keys(this.config.chains).filter( + (chain) => !chainsToSkip.has(chain), + ); + + const results = await Promise.allSettled( + chains.map((chain) => this.fundChainWithTimeout(chain)), + ); + + const failures = results.filter((r) => r.status === 'rejected'); + if (failures.length > 0) { + this.options.logger.error( + { failureCount: failures.length, totalChains: chains.length }, + 'Some chains failed to fund', + ); + throw new Error( + `${failures.length}/${chains.length} chains failed to fund`, + ); + } + } + + private async fundChainWithTimeout(chain: string): Promise { + const { promise: timeoutPromise, cleanup } = createTimeoutPromise( + CHAIN_FUNDING_TIMEOUT_MS, + `Funding timed out for chain ${chain}`, + ); + + try { + await Promise.race([this.fundChain(chain), timeoutPromise]); + } finally { + cleanup(); + } + } + + async fundChain(chain: string): Promise { + const chainConfig = this.config.chains[chain]; + if (!chainConfig) { + this.options.logger.warn({ chain }, 'No config for chain, skipping'); + return; + } + + const startTime = Date.now(); + const logger = this.options.logger.child({ chain }); + + try { + if (!this.options.skipIgpClaim && chainConfig.igp) { + await this.claimFromIgp(chain, chainConfig); + } + + if (chainConfig.keys?.length) { + await this.fundKeys(chain, chainConfig.keys); + } + + if (chainConfig.sweep?.enabled) { + await this.sweepExcessFunds(chain, chainConfig); + } + + const durationSeconds = (Date.now() - startTime) / 1000; + this.options.metrics?.recordOperationDuration( + chain, + 'fund', + durationSeconds, + ); + logger.info({ durationSeconds }, 'Chain funding completed'); + } catch (error) { + logger.error({ error }, 'Chain funding failed'); + throw error; + } + } + + private async claimFromIgp( + chain: string, + chainConfig: ChainConfig, + ): Promise { + const igpConfig = chainConfig.igp; + if (!igpConfig || !this.options.igp) { + return; + } + + const logger = this.options.logger.child({ chain, operation: 'igp-claim' }); + const provider = this.multiProvider.getProvider(chain); + const igpContract = + this.options.igp.getContracts(chain).interchainGasPaymaster; + const igpBalance = await provider.getBalance(igpContract.address); + const claimThreshold = ethers.utils.parseEther(igpConfig.claimThreshold); + + this.options.metrics?.recordIgpBalance( + chain, + parseFloat(ethers.utils.formatEther(igpBalance)), + ); + + logger.info( + { + igpBalance: ethers.utils.formatEther(igpBalance), + claimThreshold: ethers.utils.formatEther(claimThreshold), + }, + 'Checking IGP balance', + ); + + if (igpBalance.gt(claimThreshold)) { + logger.info('IGP balance exceeds threshold, claiming'); + await this.multiProvider.sendTransaction( + chain, + await igpContract.populateTransaction.claim(), + ); + logger.info('IGP claim completed'); + } + } + + private async fundKeys(chain: string, keys: KeyConfig[]): Promise { + for (const key of keys) { + await this.fundKey(chain, key); + } + } + + private async fundKey(chain: string, key: KeyConfig): Promise { + const logger = this.options.logger.child({ + chain, + address: key.address, + role: key.role, + }); + + const desiredBalance = ethers.utils.parseEther(key.desiredBalance); + const fundingAmount = await this.calculateFundingAmount( + chain, + key.address, + desiredBalance, + ); + + const currentBalance = await this.multiProvider + .getProvider(chain) + .getBalance(key.address); + + this.options.metrics?.recordWalletBalance( + chain, + key.address, + key.role ?? 'unknown', + parseFloat(ethers.utils.formatEther(currentBalance)), + ); + + if (fundingAmount.eq(0)) { + logger.debug( + { currentBalance: ethers.utils.formatEther(currentBalance) }, + 'Key balance sufficient, skipping', + ); + return; + } + + const funderAddress = await this.multiProvider.getSignerAddress(chain); + const funderBalance = await this.multiProvider + .getSigner(chain) + .getBalance(); + + logger.info( + { + amount: ethers.utils.formatEther(fundingAmount), + currentBalance: ethers.utils.formatEther(currentBalance), + desiredBalance: ethers.utils.formatEther(desiredBalance), + funderAddress, + funderBalance: ethers.utils.formatEther(funderBalance), + }, + 'Funding key', + ); + + const tx = await this.multiProvider.sendTransaction(chain, { + to: key.address, + value: fundingAmount, + }); + + this.options.metrics?.recordFundingAmount( + chain, + key.address, + key.role ?? 'unknown', + parseFloat(ethers.utils.formatEther(fundingAmount)), + ); + + logger.info( + { + txHash: tx.transactionHash, + txUrl: this.multiProvider.tryGetExplorerTxUrl(chain, { + hash: tx.transactionHash, + }), + }, + 'Funding transaction completed', + ); + } + + private async calculateFundingAmount( + chain: string, + address: string, + desiredBalance: BigNumber, + ): Promise { + const currentBalance = await this.multiProvider + .getProvider(chain) + .getBalance(address); + const delta = desiredBalance.sub(currentBalance); + const minDelta = desiredBalance + .mul(MIN_DELTA_NUMERATOR) + .div(MIN_DELTA_DENOMINATOR); + return delta.gt(minDelta) ? delta : BigNumber.from(0); + } + + private async sweepExcessFunds( + chain: string, + chainConfig: ChainConfig, + ): Promise { + const sweepConfig = chainConfig.sweep; + if (!sweepConfig?.enabled || !sweepConfig.threshold) { + return; + } + + const logger = this.options.logger.child({ chain, operation: 'sweep' }); + + const sweepAddress = sweepConfig.address ?? DEFAULT_SWEEP_ADDRESS; + const targetMultiplier = + sweepConfig.targetMultiplier ?? DEFAULT_TARGET_MULTIPLIER; + const triggerMultiplier = + sweepConfig.triggerMultiplier ?? DEFAULT_TRIGGER_MULTIPLIER; + + const threshold = ethers.utils.parseEther(sweepConfig.threshold); + const targetBalance = threshold + .mul(Math.floor(targetMultiplier * 100)) + .div(100); + const triggerThreshold = threshold + .mul(Math.floor(triggerMultiplier * 100)) + .div(100); + + const funderBalance = await this.multiProvider + .getSigner(chain) + .getBalance(); + + logger.info( + { + funderBalance: ethers.utils.formatEther(funderBalance), + triggerThreshold: ethers.utils.formatEther(triggerThreshold), + targetBalance: ethers.utils.formatEther(targetBalance), + }, + 'Checking sweep conditions', + ); + + if (funderBalance.gt(triggerThreshold)) { + const sweepAmount = funderBalance.sub(targetBalance); + + logger.info( + { + sweepAmount: ethers.utils.formatEther(sweepAmount), + sweepAddress, + }, + 'Sweeping excess funds', + ); + + const tx = await this.multiProvider.sendTransaction(chain, { + to: sweepAddress, + value: sweepAmount, + }); + + this.options.metrics?.recordSweepAmount( + chain, + parseFloat(ethers.utils.formatEther(sweepAmount)), + ); + + logger.info( + { + txHash: tx.transactionHash, + txUrl: this.multiProvider.tryGetExplorerTxUrl(chain, { + hash: tx.transactionHash, + }), + }, + 'Sweep completed', + ); + } else { + logger.debug('Funder balance below trigger threshold, no sweep needed'); + } + } +} + +function createTimeoutPromise( + timeoutMs: number, + errorMessage: string, +): { promise: Promise; cleanup: () => void } { + let timeoutId: NodeJS.Timeout; + const promise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error(errorMessage)), timeoutMs); + }); + return { + promise, + cleanup: () => clearTimeout(timeoutId), + }; +} diff --git a/typescript/keyfunder/src/index.ts b/typescript/keyfunder/src/index.ts new file mode 100644 index 00000000000..14a92bfa72b --- /dev/null +++ b/typescript/keyfunder/src/index.ts @@ -0,0 +1,23 @@ +export { KeyFunderConfigLoader } from './config/KeyFunderConfig.js'; +export { + KeyFunderConfigSchema, + KeyConfigSchema, + IgpConfigSchema, + SweepConfigSchema, + ChainConfigSchema, + FunderConfigSchema, + MetricsConfigSchema, +} from './config/types.js'; +export type { + KeyFunderConfig, + KeyFunderConfigInput, + KeyConfig, + IgpConfig, + SweepConfig, + ChainConfig, + FunderConfig, + MetricsConfig, +} from './config/types.js'; + +export { KeyFunder, type KeyFunderOptions } from './core/KeyFunder.js'; +export { KeyFunderMetrics } from './metrics/Metrics.js'; diff --git a/typescript/keyfunder/src/metrics/Metrics.test.ts b/typescript/keyfunder/src/metrics/Metrics.test.ts new file mode 100644 index 00000000000..a1d71e5b537 --- /dev/null +++ b/typescript/keyfunder/src/metrics/Metrics.test.ts @@ -0,0 +1,125 @@ +import { expect } from 'chai'; + +import { KeyFunderMetrics } from './Metrics.js'; + +describe('KeyFunderMetrics', () => { + describe('constructor', () => { + it('should create metrics without push gateway', () => { + const metrics = new KeyFunderMetrics(undefined); + expect(metrics.getRegistry()).to.not.be.undefined; + }); + + it('should create metrics with push gateway', () => { + const metrics = new KeyFunderMetrics({ + pushGateway: 'http://localhost:9091', + jobName: 'test', + }); + expect(metrics.getRegistry()).to.not.be.undefined; + }); + + it('should include base labels in gauge configurations', () => { + const metrics = new KeyFunderMetrics( + { jobName: 'test' }, + { environment: 'testnet' }, + ); + expect(metrics.getRegistry()).to.not.be.undefined; + }); + }); + + describe('recordWalletBalance', () => { + it('should record wallet balance metric', async () => { + const metrics = new KeyFunderMetrics(undefined); + metrics.recordWalletBalance( + 'ethereum', + '0x1234567890123456789012345678901234567890', + 'relayer', + 1.5, + ); + + const metricsOutput = await metrics.getRegistry().metrics(); + expect(metricsOutput).to.include('hyperlane_keyfunder_wallet_balance'); + expect(metricsOutput).to.include('ethereum'); + expect(metricsOutput).to.include('relayer'); + }); + }); + + describe('recordFundingAmount', () => { + it('should record funding amount metric', async () => { + const metrics = new KeyFunderMetrics(undefined); + metrics.recordFundingAmount( + 'arbitrum', + '0x1234567890123456789012345678901234567890', + 'kathy', + 0.25, + ); + + const metricsOutput = await metrics.getRegistry().metrics(); + expect(metricsOutput).to.include('hyperlane_keyfunder_funding_amount'); + expect(metricsOutput).to.include('arbitrum'); + expect(metricsOutput).to.include('kathy'); + }); + }); + + describe('recordIgpBalance', () => { + it('should record IGP balance metric', async () => { + const metrics = new KeyFunderMetrics(undefined); + metrics.recordIgpBalance('polygon', 2.5); + + const metricsOutput = await metrics.getRegistry().metrics(); + expect(metricsOutput).to.include('hyperlane_keyfunder_igp_balance'); + expect(metricsOutput).to.include('polygon'); + }); + }); + + describe('recordSweepAmount', () => { + it('should record sweep amount metric', async () => { + const metrics = new KeyFunderMetrics(undefined); + metrics.recordSweepAmount('optimism', 5.0); + + const metricsOutput = await metrics.getRegistry().metrics(); + expect(metricsOutput).to.include('hyperlane_keyfunder_sweep_amount'); + expect(metricsOutput).to.include('optimism'); + }); + }); + + describe('recordOperationDuration', () => { + it('should record operation duration metric', async () => { + const metrics = new KeyFunderMetrics(undefined); + metrics.recordOperationDuration('base', 'fund', 3.14); + + const metricsOutput = await metrics.getRegistry().metrics(); + expect(metricsOutput).to.include( + 'hyperlane_keyfunder_operation_duration_seconds', + ); + expect(metricsOutput).to.include('base'); + expect(metricsOutput).to.include('fund'); + }); + }); + + describe('push', () => { + it('should not throw when no push gateway configured', async () => { + const metrics = new KeyFunderMetrics(undefined); + await metrics.push(); + }); + }); + + describe('with base labels', () => { + it('should include base labels in all metrics', async () => { + const metrics = new KeyFunderMetrics( + { jobName: 'keyfunder-test' }, + { environment: 'mainnet3', region: 'us-east' }, + ); + + metrics.recordWalletBalance( + 'ethereum', + '0x1234567890123456789012345678901234567890', + 'relayer', + 1.0, + ); + + const metricsOutput = await metrics.getRegistry().metrics(); + expect(metricsOutput).to.include('environment="mainnet3"'); + expect(metricsOutput).to.include('region="us-east"'); + }); + }); +}); diff --git a/typescript/keyfunder/src/metrics/Metrics.ts b/typescript/keyfunder/src/metrics/Metrics.ts new file mode 100644 index 00000000000..682a1bb1ed0 --- /dev/null +++ b/typescript/keyfunder/src/metrics/Metrics.ts @@ -0,0 +1,118 @@ +import { Gauge, Pushgateway, Registry } from 'prom-client'; + +import type { MetricsConfig } from '../config/types.js'; + +export class KeyFunderMetrics { + private registry: Registry; + private pushGateway: Pushgateway | null = null; + + readonly walletBalanceGauge: Gauge; + readonly fundingAmountGauge: Gauge; + readonly igpBalanceGauge: Gauge; + readonly sweepAmountGauge: Gauge; + readonly operationDurationGauge: Gauge; + + constructor( + private readonly config: MetricsConfig | undefined, + private readonly baseLabels: Record = {}, + ) { + this.registry = new Registry(); + + const labelNames = ['chain', 'address', 'role', ...Object.keys(baseLabels)]; + + this.walletBalanceGauge = new Gauge({ + name: 'hyperlane_keyfunder_wallet_balance', + help: 'Current wallet balance in native token', + labelNames, + registers: [this.registry], + }); + + this.fundingAmountGauge = new Gauge({ + name: 'hyperlane_keyfunder_funding_amount', + help: 'Amount funded to a key', + labelNames, + registers: [this.registry], + }); + + this.igpBalanceGauge = new Gauge({ + name: 'hyperlane_keyfunder_igp_balance', + help: 'IGP contract balance', + labelNames: ['chain', ...Object.keys(baseLabels)], + registers: [this.registry], + }); + + this.sweepAmountGauge = new Gauge({ + name: 'hyperlane_keyfunder_sweep_amount', + help: 'Amount swept to safe address', + labelNames: ['chain', ...Object.keys(baseLabels)], + registers: [this.registry], + }); + + this.operationDurationGauge = new Gauge({ + name: 'hyperlane_keyfunder_operation_duration_seconds', + help: 'Duration of funding operations', + labelNames: ['chain', 'operation', ...Object.keys(baseLabels)], + registers: [this.registry], + }); + + if (config?.pushGateway) { + this.pushGateway = new Pushgateway(config.pushGateway, [], this.registry); + } + } + + recordWalletBalance( + chain: string, + address: string, + role: string, + balance: number, + ): void { + this.walletBalanceGauge.set( + { chain, address, role, ...this.baseLabels }, + balance, + ); + } + + recordFundingAmount( + chain: string, + address: string, + role: string, + amount: number, + ): void { + this.fundingAmountGauge.set( + { chain, address, role, ...this.baseLabels }, + amount, + ); + } + + recordIgpBalance(chain: string, balance: number): void { + this.igpBalanceGauge.set({ chain, ...this.baseLabels }, balance); + } + + recordSweepAmount(chain: string, amount: number): void { + this.sweepAmountGauge.set({ chain, ...this.baseLabels }, amount); + } + + recordOperationDuration( + chain: string, + operation: string, + durationSeconds: number, + ): void { + this.operationDurationGauge.set( + { chain, operation, ...this.baseLabels }, + durationSeconds, + ); + } + + async push(): Promise { + if (!this.pushGateway) { + return; + } + + const jobName = this.config?.jobName ?? 'keyfunder'; + await this.pushGateway.push({ jobName }); + } + + getRegistry(): Registry { + return this.registry; + } +} diff --git a/typescript/keyfunder/src/service.ts b/typescript/keyfunder/src/service.ts new file mode 100644 index 00000000000..e61407c0f16 --- /dev/null +++ b/typescript/keyfunder/src/service.ts @@ -0,0 +1,121 @@ +#!/usr/bin/env node +import { Wallet } from 'ethers'; + +import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; +import { getRegistry } from '@hyperlane-xyz/registry/fs'; +import { HyperlaneIgp, MultiProvider } from '@hyperlane-xyz/sdk'; +import { createServiceLogger, rootLogger } from '@hyperlane-xyz/utils'; + +import { KeyFunderConfigLoader } from './config/KeyFunderConfig.js'; +import { KeyFunder } from './core/KeyFunder.js'; +import { KeyFunderMetrics } from './metrics/Metrics.js'; + +async function main(): Promise { + const VERSION = process.env.SERVICE_VERSION || 'dev'; + + const configFile = process.env.KEYFUNDER_CONFIG_FILE; + if (!configFile) { + rootLogger.error('KEYFUNDER_CONFIG_FILE environment variable is required'); + process.exit(1); + } + + const logger = await createServiceLogger({ + service: 'keyfunder', + version: VERSION, + }); + + logger.info( + { version: VERSION, configFile }, + 'Starting Hyperlane KeyFunder Service', + ); + + try { + const configLoader = KeyFunderConfigLoader.load(configFile); + const config = configLoader.config; + logger.info('Loaded keyfunder configuration'); + + const privateKeyEnvVar = configLoader.getFunderPrivateKeyEnvVar(); + const privateKey = process.env[privateKeyEnvVar]; + if (!privateKey) { + throw new Error(`${privateKeyEnvVar} environment variable is required`); + } + + const registryUri = process.env.REGISTRY_URI || DEFAULT_GITHUB_REGISTRY; + const registry = getRegistry({ + registryUris: [registryUri], + enableProxy: true, + logger: rootLogger, + }); + logger.info({ registryUri }, 'Initialized registry'); + + const chainMetadata = await registry.getMetadata(); + applyRpcOverrides(chainMetadata, configLoader.getConfiguredChains()); + logger.info( + `Loaded metadata for ${Object.keys(chainMetadata).length} chains`, + ); + + const multiProvider = new MultiProvider(chainMetadata); + const signer = new Wallet(privateKey); + multiProvider.setSharedSigner(signer); + logger.info('Initialized MultiProvider with signer'); + + let igp: HyperlaneIgp | undefined; + const chainsWithIgp = Object.entries(config.chains) + .filter(([, cfg]) => cfg.igp) + .map(([chain]) => chain); + + if (chainsWithIgp.length > 0) { + const addresses = await registry.getAddresses(); + const igpAddresses = Object.fromEntries( + chainsWithIgp + .filter((chain) => addresses[chain]) + .map((chain) => [chain, addresses[chain]]), + ); + igp = HyperlaneIgp.fromAddressesMap(igpAddresses, multiProvider); + logger.info({ chains: chainsWithIgp }, 'Initialized IGP contracts'); + } + + const metrics = new KeyFunderMetrics( + config.metrics, + config.metrics?.labels, + ); + + const funder = new KeyFunder(multiProvider, config, { + logger, + metrics, + skipIgpClaim: process.env.SKIP_IGP_CLAIM === 'true', + igp, + }); + + await funder.fundAllChains(); + + await metrics.push(); + logger.info('Metrics pushed to gateway'); + + logger.info('KeyFunder completed successfully'); + process.exit(0); + } catch (error) { + const err = error as Error; + logger.error({ error: err.message, stack: err.stack }, 'KeyFunder failed'); + process.exit(1); + } +} + +function applyRpcOverrides( + chainMetadata: Record }>, + configuredChains: string[], +): void { + for (const chain of configuredChains) { + const envVarName = `RPC_URL_${chain.toUpperCase().replace(/-/g, '_')}`; + const rpcOverride = process.env[envVarName]; + if (rpcOverride && chainMetadata[chain]) { + chainMetadata[chain].rpcUrls = [{ http: rpcOverride }]; + } + } +} + +main().catch((error) => { + const err = error as Error; + rootLogger.error({ error: err.message, stack: err.stack }, 'Fatal error'); + process.exit(1); +}); diff --git a/typescript/keyfunder/tsconfig.json b/typescript/keyfunder/tsconfig.json new file mode 100644 index 00000000000..9005365a9b7 --- /dev/null +++ b/typescript/keyfunder/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@hyperlane-xyz/tsconfig/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["./src/**/*"] +} From 21e0afa4f7cace64e90ea47bf1fbccb23f8c5ad3 Mon Sep 17 00:00:00 2001 From: pbio <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:57:31 +0000 Subject: [PATCH 2/6] refactor(keyfunder): use global roles with per-chain balances Config now uses a 'roles' section to define addresses once, and chains reference role names in their 'balances' map. This simplifies config management for roles that use the same address across all chains (relayer, kathy, rebalancer). Before (address repeated per chain): chains: ethereum: keys: - address: '0x...' role: 'relayer' desiredBalance: '0.5' arbitrum: keys: - address: '0x...' # same address repeated role: 'relayer' desiredBalance: '0.1' After (address defined once): roles: hyperlane-relayer: address: '0x...' chains: ethereum: balances: hyperlane-relayer: '0.5' arbitrum: balances: hyperlane-relayer: '0.1' --- typescript/infra/src/funding/key-funder.ts | 63 +++++-- typescript/keyfunder/README.md | 39 ++-- .../src/config/KeyFunderConfig.test.ts | 64 ++++--- typescript/keyfunder/src/config/types.test.ts | 172 ++++++++++++------ typescript/keyfunder/src/config/types.ts | 48 +++-- typescript/keyfunder/src/core/KeyFunder.ts | 49 ++++- typescript/keyfunder/src/index.ts | 5 +- 7 files changed, 303 insertions(+), 137 deletions(-) diff --git a/typescript/infra/src/funding/key-funder.ts b/typescript/infra/src/funding/key-funder.ts index 07836c7a60b..e36efa6e754 100644 --- a/typescript/infra/src/funding/key-funder.ts +++ b/typescript/infra/src/funding/key-funder.ts @@ -81,17 +81,23 @@ export class KeyFunderHelmManager extends HelmManager { private generateKeyfunderYaml(): string { const environment = this.agentConfig.runEnv; + const roles: Record = {}; const chains: Record = {}; const envAddresses = getEnvAddresses(environment); + const roleAddressMap = this.buildRoleAddressMap(environment); + for (const [roleName, address] of Object.entries(roleAddressMap)) { + roles[roleName] = { address }; + } + for (const chain of this.getEthereumChains()) { if (this.config.chainsToSkip?.includes(chain)) continue; const chainConfig: ChainYamlConfig = {}; - const keys = this.getKeysForChain(chain, environment); - if (keys.length > 0) { - chainConfig.keys = keys; + const balances = this.getBalancesForChain(chain, roleAddressMap); + if (Object.keys(balances).length > 0) { + chainConfig.balances = balances; } const igpAddress = envAddresses[chain]?.interchainGasPaymaster; @@ -124,6 +130,7 @@ export class KeyFunderHelmManager extends HelmManager { const config = { version: '1' as const, + roles, chains, funder: { privateKeyEnvVar: 'FUNDER_PRIVATE_KEY', @@ -141,11 +148,10 @@ export class KeyFunderHelmManager extends HelmManager { return YAML.stringify(config); } - private getKeysForChain( - chain: string, + private buildRoleAddressMap( environment: DeployEnvironment, - ): KeyYamlConfig[] { - const keys: KeyYamlConfig[] = []; + ): Record { + const roleAddressMap: Record = {}; const contextsAndRoles = this.config.contextsAndRolesToFund; for (const [contextStr, roles] of Object.entries(contextsAndRoles)) { @@ -154,20 +160,39 @@ export class KeyFunderHelmManager extends HelmManager { for (const role of roles) { const address = this.getAddressForRole(environment, context, role); - if (!address) continue; + if (address) { + const roleName = `${context}-${role}`; + roleAddressMap[roleName] = address; + } + } + } - const desiredBalance = this.getDesiredBalanceForRole(chain, role); - if (!desiredBalance || desiredBalance === '0') continue; + return roleAddressMap; + } - keys.push({ - address, - role: `${context}-${role}`, - desiredBalance, - }); + private getBalancesForChain( + chain: string, + roleAddressMap: Record, + ): Record { + const balances: Record = {}; + const contextsAndRoles = this.config.contextsAndRolesToFund; + + for (const [contextStr, roles] of Object.entries(contextsAndRoles)) { + const context = contextStr as Contexts; + if (!roles) continue; + + for (const role of roles) { + const roleName = `${context}-${role}`; + if (!roleAddressMap[roleName]) continue; + + const desiredBalance = this.getDesiredBalanceForRole(chain, role); + if (desiredBalance && desiredBalance !== '0') { + balances[roleName] = desiredBalance; + } } } - return keys; + return balances; } private getAddressForRole( @@ -220,14 +245,12 @@ export class KeyFunderHelmManager extends HelmManager { } } -interface KeyYamlConfig { +interface RoleYamlConfig { address: string; - role: string; - desiredBalance: string; } interface ChainYamlConfig { - keys?: KeyYamlConfig[]; + balances?: Record; igp?: { address: string; claimThreshold: string; diff --git a/typescript/keyfunder/README.md b/typescript/keyfunder/README.md index f237c0649df..6509b2af499 100644 --- a/typescript/keyfunder/README.md +++ b/typescript/keyfunder/README.md @@ -18,15 +18,22 @@ The service reads configuration from a YAML file specified by `KEYFUNDER_CONFIG_ ```yaml version: '1' + +# Roles define WHO gets funded (address defined once, reused across chains) +roles: + hyperlane-relayer: + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5' + hyperlane-kathy: + address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20' + hyperlane-rebalancer: + address: '0xdef456...' + +# Chains define HOW MUCH each role gets (balances reference role names) chains: ethereum: - keys: - - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5' - role: 'hyperlane-relayer' - desiredBalance: '0.5' - - address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20' - role: 'hyperlane-kathy' - desiredBalance: '0.4' + balances: + hyperlane-relayer: '0.5' + hyperlane-kathy: '0.4' igp: address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7' claimThreshold: '0.2' @@ -37,10 +44,12 @@ chains: targetMultiplier: 1.5 triggerMultiplier: 2.0 arbitrum: - keys: - - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5' - role: 'hyperlane-relayer' - desiredBalance: '0.1' + balances: + hyperlane-relayer: '0.1' + igp: + address: '0x...' + claimThreshold: '0.1' + funder: privateKeyEnvVar: 'FUNDER_PRIVATE_KEY' metrics: @@ -56,11 +65,11 @@ chainsToSkip: [] | Field | Description | | ---------------------------------------- | ------------------------------------------------ | | `version` | Config version, must be "1" | +| `roles` | Role definitions (address per role) | +| `roles..address` | Ethereum address for this role | | `chains` | Per-chain configuration | -| `chains..keys` | Array of keys to fund | -| `chains..keys[].address` | Ethereum address of the key | -| `chains..keys[].role` | Optional role identifier for metrics | -| `chains..keys[].desiredBalance` | Target balance in native token (e.g., "0.5" ETH) | +| `chains..balances` | Map of role name to desired balance | +| `chains..balances.` | Target balance in native token (e.g., "0.5" ETH) | | `chains..igp` | IGP claim configuration | | `chains..igp.address` | IGP contract address | | `chains..igp.claimThreshold` | Minimum IGP balance before claiming | diff --git a/typescript/keyfunder/src/config/KeyFunderConfig.test.ts b/typescript/keyfunder/src/config/KeyFunderConfig.test.ts index 88018b0b4fc..a4647f87df2 100644 --- a/typescript/keyfunder/src/config/KeyFunderConfig.test.ts +++ b/typescript/keyfunder/src/config/KeyFunderConfig.test.ts @@ -21,12 +21,13 @@ describe('KeyFunderConfigLoader', () => { it('should load valid config from file', () => { const configYaml = ` version: "1" +roles: + hyperlane-relayer: + address: "0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5" chains: ethereum: - keys: - - address: "0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5" - role: "hyperlane-relayer" - desiredBalance: "0.5" + balances: + hyperlane-relayer: "0.5" `; fsExistsStub.returns(true); fsReadFileStub.returns(configYaml); @@ -34,10 +35,12 @@ chains: const loader = KeyFunderConfigLoader.load('/path/to/config.yaml'); expect(loader.config.version).to.equal('1'); - expect(loader.config.chains.ethereum.keys).to.have.lengthOf(1); - expect(loader.config.chains.ethereum.keys![0].address).to.equal( + expect(loader.config.roles['hyperlane-relayer'].address).to.equal( '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', ); + expect(loader.config.chains.ethereum.balances).to.deep.equal({ + 'hyperlane-relayer': '0.5', + }); }); it('should throw if file does not exist', () => { @@ -51,6 +54,7 @@ chains: it('should throw on invalid config', () => { const invalidYaml = ` version: "2" +roles: {} chains: {} `; fsExistsStub.returns(true); @@ -66,33 +70,36 @@ chains: {} it('should create loader from valid object', () => { const config = { version: '1' as const, + roles: { + 'hyperlane-relayer': { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }, + }, chains: { ethereum: { - keys: [ - { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - desiredBalance: '0.5', - }, - ], + balances: { + 'hyperlane-relayer': '0.5', + }, }, }, }; const loader = KeyFunderConfigLoader.fromObject(config); - expect(loader.config.chains.ethereum.keys).to.have.lengthOf(1); + expect(loader.config.chains.ethereum.balances).to.deep.equal({ + 'hyperlane-relayer': '0.5', + }); }); it('should throw on invalid object', () => { const config = { version: '2', + roles: {}, chains: {}, }; - expect(() => - KeyFunderConfigLoader.fromObject( - config as unknown as { version: '1'; chains: Record }, - ), - ).to.throw('Invalid keyfunder config'); + expect(() => KeyFunderConfigLoader.fromObject(config as never)).to.throw( + 'Invalid keyfunder config', + ); }); }); @@ -100,10 +107,11 @@ chains: {} it('should return all chain names', () => { const config = { version: '1' as const, + roles: {}, chains: { - ethereum: { keys: [] }, - arbitrum: { keys: [] }, - polygon: { keys: [] }, + ethereum: {}, + arbitrum: {}, + polygon: {}, }, }; @@ -118,10 +126,11 @@ chains: {} it('should exclude skipped chains', () => { const config = { version: '1' as const, + roles: {}, chains: { - ethereum: { keys: [] }, - arbitrum: { keys: [] }, - polygon: { keys: [] }, + ethereum: {}, + arbitrum: {}, + polygon: {}, }, chainsToSkip: ['polygon'], }; @@ -136,9 +145,10 @@ chains: {} it('should return all chains when none skipped', () => { const config = { version: '1' as const, + roles: {}, chains: { - ethereum: { keys: [] }, - arbitrum: { keys: [] }, + ethereum: {}, + arbitrum: {}, }, }; @@ -153,6 +163,7 @@ chains: {} it('should return configured env var name', () => { const config = { version: '1' as const, + roles: {}, chains: {}, funder: { privateKeyEnvVar: 'CUSTOM_PRIVATE_KEY', @@ -166,6 +177,7 @@ chains: {} it('should return default when not configured', () => { const config = { version: '1' as const, + roles: {}, chains: {}, }; diff --git a/typescript/keyfunder/src/config/types.test.ts b/typescript/keyfunder/src/config/types.test.ts index 96386aa12a9..4d1d5f54b07 100644 --- a/typescript/keyfunder/src/config/types.test.ts +++ b/typescript/keyfunder/src/config/types.test.ts @@ -2,58 +2,34 @@ import { expect } from 'chai'; import { ChainConfigSchema, - KeyConfigSchema, KeyFunderConfigSchema, + RoleConfigSchema, SweepConfigSchema, } from './types.js'; describe('KeyFunderConfig Schemas', () => { - describe('KeyConfigSchema', () => { - it('should validate a valid key config', () => { + describe('RoleConfigSchema', () => { + it('should validate a valid role config', () => { const config = { address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - role: 'hyperlane-relayer', - desiredBalance: '0.5', }; - const result = KeyConfigSchema.safeParse(config); + const result = RoleConfigSchema.safeParse(config); expect(result.success).to.be.true; }); it('should reject invalid address', () => { const config = { address: 'invalid-address', - desiredBalance: '0.5', }; - const result = KeyConfigSchema.safeParse(config); + const result = RoleConfigSchema.safeParse(config); expect(result.success).to.be.false; }); - it('should reject invalid balance', () => { - const config = { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - desiredBalance: 'not-a-number', - }; - const result = KeyConfigSchema.safeParse(config); + it('should reject missing address', () => { + const config = {}; + const result = RoleConfigSchema.safeParse(config); expect(result.success).to.be.false; }); - - it('should reject negative balance', () => { - const config = { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - desiredBalance: '-1', - }; - const result = KeyConfigSchema.safeParse(config); - expect(result.success).to.be.false; - }); - - it('should allow optional role', () => { - const config = { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - desiredBalance: '0.5', - }; - const result = KeyConfigSchema.safeParse(config); - expect(result.success).to.be.true; - }); }); describe('SweepConfigSchema', () => { @@ -75,7 +51,7 @@ describe('KeyFunderConfig Schemas', () => { address: '0x478be6076f31E9666123B9721D0B6631baD944AF', threshold: '0.5', targetMultiplier: 1.5, - triggerMultiplier: 1.52, // Less than 1.5 + 0.05 + triggerMultiplier: 1.52, }; const result = SweepConfigSchema.safeParse(config); expect(result.success).to.be.false; @@ -98,7 +74,7 @@ describe('KeyFunderConfig Schemas', () => { const config = { enabled: false, targetMultiplier: 1.5, - triggerMultiplier: 1.5, // Would fail if enabled + triggerMultiplier: 1.5, }; const result = SweepConfigSchema.safeParse(config); expect(result.success).to.be.true; @@ -106,14 +82,11 @@ describe('KeyFunderConfig Schemas', () => { }); describe('ChainConfigSchema', () => { - it('should validate chain config with keys only', () => { + it('should validate chain config with balances only', () => { const config = { - keys: [ - { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - desiredBalance: '0.5', - }, - ], + balances: { + 'hyperlane-relayer': '0.5', + }, }; const result = ChainConfigSchema.safeParse(config); expect(result.success).to.be.true; @@ -132,13 +105,10 @@ describe('KeyFunderConfig Schemas', () => { it('should validate complete chain config', () => { const config = { - keys: [ - { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - role: 'relayer', - desiredBalance: '0.5', - }, - ], + balances: { + 'hyperlane-relayer': '0.5', + 'hyperlane-kathy': '0.3', + }, igp: { address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', claimThreshold: '0.2', @@ -151,12 +121,33 @@ describe('KeyFunderConfig Schemas', () => { const result = ChainConfigSchema.safeParse(config); expect(result.success).to.be.true; }); + + it('should reject invalid balance value', () => { + const config = { + balances: { + 'hyperlane-relayer': 'not-a-number', + }, + }; + const result = ChainConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should reject negative balance', () => { + const config = { + balances: { + 'hyperlane-relayer': '-1', + }, + }; + const result = ChainConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); }); describe('KeyFunderConfigSchema', () => { it('should validate minimal config', () => { const config = { version: '1', + roles: {}, chains: {}, }; const result = KeyFunderConfigSchema.safeParse(config); @@ -166,6 +157,16 @@ describe('KeyFunderConfig Schemas', () => { it('should reject invalid version', () => { const config = { version: '2', + roles: {}, + chains: {}, + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should reject missing roles', () => { + const config = { + version: '1', chains: {}, }; const result = KeyFunderConfigSchema.safeParse(config); @@ -175,15 +176,20 @@ describe('KeyFunderConfig Schemas', () => { it('should validate complete config', () => { const config = { version: '1', + roles: { + 'hyperlane-relayer': { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }, + 'hyperlane-kathy': { + address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20', + }, + }, chains: { ethereum: { - keys: [ - { - address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', - role: 'hyperlane-relayer', - desiredBalance: '0.5', - }, - ], + balances: { + 'hyperlane-relayer': '0.5', + 'hyperlane-kathy': '0.4', + }, igp: { address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', claimThreshold: '0.2', @@ -194,6 +200,11 @@ describe('KeyFunderConfig Schemas', () => { threshold: '0.3', }, }, + arbitrum: { + balances: { + 'hyperlane-relayer': '0.1', + }, + }, }, funder: { privateKeyEnvVar: 'FUNDER_PRIVATE_KEY', @@ -211,9 +222,60 @@ describe('KeyFunderConfig Schemas', () => { expect(result.success).to.be.true; }); + it('should reject undefined role reference in chain balances', () => { + const config = { + version: '1', + roles: { + 'hyperlane-relayer': { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }, + }, + chains: { + ethereum: { + balances: { + 'hyperlane-relayer': '0.5', + 'undefined-role': '0.3', + }, + }, + }, + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should allow chain balances that reference defined roles', () => { + const config = { + version: '1', + roles: { + 'hyperlane-relayer': { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }, + 'hyperlane-kathy': { + address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20', + }, + }, + chains: { + ethereum: { + balances: { + 'hyperlane-relayer': '0.5', + }, + }, + arbitrum: { + balances: { + 'hyperlane-relayer': '0.1', + 'hyperlane-kathy': '0.05', + }, + }, + }, + }; + const result = KeyFunderConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + it('should apply default funder privateKeyEnvVar', () => { const config = { version: '1', + roles: {}, chains: {}, funder: {}, }; diff --git a/typescript/keyfunder/src/config/types.ts b/typescript/keyfunder/src/config/types.ts index 32acc1473d8..a334f33a79e 100644 --- a/typescript/keyfunder/src/config/types.ts +++ b/typescript/keyfunder/src/config/types.ts @@ -14,10 +14,8 @@ const BalanceStringSchema = z 'Must be a valid non-negative number string', ); -export const KeyConfigSchema = z.object({ +export const RoleConfigSchema = z.object({ address: AddressSchema, - role: z.string().optional(), - desiredBalance: BalanceStringSchema, }); export const IgpConfigSchema = z.object({ @@ -61,7 +59,7 @@ export const SweepConfigSchema = z ); export const ChainConfigSchema = z.object({ - keys: z.array(KeyConfigSchema).optional(), + balances: z.record(z.string(), BalanceStringSchema).optional(), igp: IgpConfigSchema.optional(), sweep: SweepConfigSchema.optional(), }); @@ -76,15 +74,35 @@ export const MetricsConfigSchema = z.object({ labels: z.record(z.string(), z.string()).optional(), }); -export const KeyFunderConfigSchema = z.object({ - version: z.literal('1'), - chains: z.record(z.string(), ChainConfigSchema), - funder: FunderConfigSchema.optional(), - metrics: MetricsConfigSchema.optional(), - chainsToSkip: z.array(z.string()).optional(), -}); +export const KeyFunderConfigSchema = z + .object({ + version: z.literal('1'), + roles: z.record(z.string(), RoleConfigSchema), + chains: z.record(z.string(), ChainConfigSchema), + funder: FunderConfigSchema.optional(), + metrics: MetricsConfigSchema.optional(), + chainsToSkip: z.array(z.string()).optional(), + }) + .refine( + (data) => { + const definedRoles = new Set(Object.keys(data.roles)); + for (const chainConfig of Object.values(data.chains)) { + if (!chainConfig.balances) continue; + for (const roleName of Object.keys(chainConfig.balances)) { + if (!definedRoles.has(roleName)) { + return false; + } + } + } + return true; + }, + { + message: + 'Chain balances reference undefined roles. All roles must be defined in the roles section.', + }, + ); -export type KeyConfig = z.infer; +export type RoleConfig = z.infer; export type IgpConfig = z.infer; export type SweepConfig = z.infer; export type ChainConfig = z.infer; @@ -92,3 +110,9 @@ export type FunderConfig = z.infer; export type MetricsConfig = z.infer; export type KeyFunderConfig = z.infer; export type KeyFunderConfigInput = z.input; + +export interface ResolvedKeyConfig { + address: string; + role: string; + desiredBalance: string; +} diff --git a/typescript/keyfunder/src/core/KeyFunder.ts b/typescript/keyfunder/src/core/KeyFunder.ts index ebc5a875fc7..6a604f0c63f 100644 --- a/typescript/keyfunder/src/core/KeyFunder.ts +++ b/typescript/keyfunder/src/core/KeyFunder.ts @@ -5,8 +5,8 @@ import { HyperlaneIgp, MultiProvider } from '@hyperlane-xyz/sdk'; import type { ChainConfig, - KeyConfig, KeyFunderConfig, + ResolvedKeyConfig, } from '../config/types.js'; import type { KeyFunderMetrics } from '../metrics/Metrics.js'; @@ -83,8 +83,9 @@ export class KeyFunder { await this.claimFromIgp(chain, chainConfig); } - if (chainConfig.keys?.length) { - await this.fundKeys(chain, chainConfig.keys); + const resolvedKeys = this.resolveKeysForChain(chain, chainConfig); + if (resolvedKeys.length > 0) { + await this.fundKeys(chain, resolvedKeys); } if (chainConfig.sweep?.enabled) { @@ -104,6 +105,37 @@ export class KeyFunder { } } + private resolveKeysForChain( + chain: string, + chainConfig: ChainConfig, + ): ResolvedKeyConfig[] { + if (!chainConfig.balances) { + return []; + } + + const resolvedKeys: ResolvedKeyConfig[] = []; + for (const [roleName, desiredBalance] of Object.entries( + chainConfig.balances, + )) { + const roleConfig = this.config.roles[roleName]; + if (!roleConfig) { + this.options.logger.warn( + { chain, role: roleName }, + 'Role not found in config, skipping', + ); + continue; + } + + resolvedKeys.push({ + address: roleConfig.address, + role: roleName, + desiredBalance, + }); + } + + return resolvedKeys; + } + private async claimFromIgp( chain: string, chainConfig: ChainConfig, @@ -143,13 +175,16 @@ export class KeyFunder { } } - private async fundKeys(chain: string, keys: KeyConfig[]): Promise { + private async fundKeys( + chain: string, + keys: ResolvedKeyConfig[], + ): Promise { for (const key of keys) { await this.fundKey(chain, key); } } - private async fundKey(chain: string, key: KeyConfig): Promise { + private async fundKey(chain: string, key: ResolvedKeyConfig): Promise { const logger = this.options.logger.child({ chain, address: key.address, @@ -170,7 +205,7 @@ export class KeyFunder { this.options.metrics?.recordWalletBalance( chain, key.address, - key.role ?? 'unknown', + key.role, parseFloat(ethers.utils.formatEther(currentBalance)), ); @@ -206,7 +241,7 @@ export class KeyFunder { this.options.metrics?.recordFundingAmount( chain, key.address, - key.role ?? 'unknown', + key.role, parseFloat(ethers.utils.formatEther(fundingAmount)), ); diff --git a/typescript/keyfunder/src/index.ts b/typescript/keyfunder/src/index.ts index 14a92bfa72b..01920fd2ac1 100644 --- a/typescript/keyfunder/src/index.ts +++ b/typescript/keyfunder/src/index.ts @@ -1,7 +1,7 @@ export { KeyFunderConfigLoader } from './config/KeyFunderConfig.js'; export { KeyFunderConfigSchema, - KeyConfigSchema, + RoleConfigSchema, IgpConfigSchema, SweepConfigSchema, ChainConfigSchema, @@ -11,12 +11,13 @@ export { export type { KeyFunderConfig, KeyFunderConfigInput, - KeyConfig, + RoleConfig, IgpConfig, SweepConfig, ChainConfig, FunderConfig, MetricsConfig, + ResolvedKeyConfig, } from './config/types.js'; export { KeyFunder, type KeyFunderOptions } from './core/KeyFunder.js'; From bb53853c45f71631ab58024032cede76dda3a684 Mon Sep 17 00:00:00 2001 From: pbio <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:58:05 +0000 Subject: [PATCH 3/6] fix: add keyfunder package.json COPY to Dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 18c9ead4581..aa9db6e662c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,7 @@ COPY typescript/github-proxy/package.json ./typescript/github-proxy/ COPY typescript/helloworld/package.json ./typescript/helloworld/ COPY typescript/http-registry-server/package.json ./typescript/http-registry-server/ COPY typescript/infra/package.json ./typescript/infra/ +COPY typescript/keyfunder/package.json ./typescript/keyfunder/ COPY typescript/provider-sdk/package.json ./typescript/provider-sdk/ COPY typescript/radix-sdk/package.json ./typescript/radix-sdk/ COPY typescript/rebalancer/package.json ./typescript/rebalancer/ From a030115af096ced32a9192fa628f1e2c4e7718df Mon Sep 17 00:00:00 2001 From: pbio <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:03:46 +0000 Subject: [PATCH 4/6] ci: add keyfunder Docker build workflow --- .github/workflows/keyfunder-docker.yml | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 .github/workflows/keyfunder-docker.yml diff --git a/.github/workflows/keyfunder-docker.yml b/.github/workflows/keyfunder-docker.yml new file mode 100644 index 00000000000..f59fd82ba2f --- /dev/null +++ b/.github/workflows/keyfunder-docker.yml @@ -0,0 +1,125 @@ +name: Build and Push KeyFunder Image to GCR +on: + push: + branches: [main] + tags: + - '**' + pull_request: + paths: + - 'typescript/keyfunder/**' + - '.github/workflows/keyfunder-docker.yml' + workflow_dispatch: + inputs: + include_arm64: + description: 'Include arm64 in the build' + required: false + default: 'false' + +concurrency: + group: build-push-keyfunder-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-env: + runs-on: ubuntu-latest + outputs: + gcloud-service-key: ${{ steps.gcloud-service-key.outputs.defined }} + steps: + - id: gcloud-service-key + env: + GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }} + if: "${{ env.GCLOUD_SERVICE_KEY != '' }}" + run: echo "defined=true" >> $GITHUB_OUTPUT + + build-and-push-to-gcr: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + pull-requests: write + + needs: [check-env] + if: needs.check-env.outputs.gcloud-service-key == 'true' + + steps: + - name: Generate GitHub App Token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.HYPER_GONK_APP_ID }} + private-key: ${{ secrets.HYPER_GONK_PRIVATE_KEY }} + + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + submodules: recursive + fetch-depth: 0 + + - name: Generate tag data + id: taggen + run: | + echo "TAG_DATE=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT + echo "TAG_SHA=$(echo '${{ github.event.pull_request.head.sha || github.sha }}' | cut -b 1-7)" >> $GITHUB_OUTPUT + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + gcr.io/abacus-labs-dev/hyperlane-keyfunder + tags: | + type=ref,event=branch + type=ref,event=pr + type=raw,value=${{ steps.taggen.outputs.TAG_SHA }}-${{ steps.taggen.outputs.TAG_DATE }} + + - name: Set up Depot CLI + uses: depot/setup-action@v1 + + - name: Login to GCR + uses: docker/login-action@v3 + with: + registry: gcr.io + username: _json_key + password: ${{ secrets.GCLOUD_SERVICE_KEY }} + + - name: Determine platforms + id: determine-platforms + run: | + if [ "${{ github.event.inputs.include_arm64 }}" == "true" ]; then + echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT + else + echo "platforms=linux/amd64" >> $GITHUB_OUTPUT + fi + + - name: Get Foundry version + id: foundry-version + run: | + FOUNDRY_VERSION=$(cat solidity/.foundryrc) + echo "FOUNDRY_VERSION=$FOUNDRY_VERSION" >> $GITHUB_OUTPUT + + - name: Build and push + id: build + uses: depot/build-push-action@v1 + with: + project: 3cpjhx94qv + context: ./ + file: ./typescript/keyfunder/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ steps.determine-platforms.outputs.platforms }} + build-args: | + FOUNDRY_VERSION=${{ steps.foundry-version.outputs.FOUNDRY_VERSION }} + SERVICE_VERSION=${{ steps.taggen.outputs.TAG_SHA }}-${{ steps.taggen.outputs.TAG_DATE }} + + - name: Comment image tags on PR + if: github.event_name == 'pull_request' + uses: ./.github/actions/docker-image-comment + with: + comment_tag: keyfunder-docker-image + image_name: KeyFunder Docker Image + emoji: 🔑 + image_tags: ${{ steps.meta.outputs.tags }} + pr_number: ${{ github.event.pull_request.number }} + github_token: ${{ steps.generate-token.outputs.token }} + job_status: ${{ job.status }} From b8d2f02f3a0cd83622ef594a47f5d8f8503e66b5 Mon Sep 17 00:00:00 2001 From: pbio <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:21:19 +0000 Subject: [PATCH 5/6] fix(keyfunder): add package turbo.json to ensure build runs before bundle --- .gitignore | 5 ++++- typescript/keyfunder/turbo.json | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 typescript/keyfunder/turbo.json diff --git a/.gitignore b/.gitignore index 53d53897ccd..e1968d9c209 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ yalc.lock **/deployer-key.json # .yarn as we use pnpm now -.yarn \ No newline at end of file +.yarn + +# opencode +.opencode/ \ No newline at end of file diff --git a/typescript/keyfunder/turbo.json b/typescript/keyfunder/turbo.json new file mode 100644 index 00000000000..eda75172045 --- /dev/null +++ b/typescript/keyfunder/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "bundle": { + "dependsOn": ["build"], + "outputs": ["bundle/**"] + } + } +} From 532846beeb7492a258a736db105feab0a13f346d Mon Sep 17 00:00:00 2001 From: pbio <10051819+paulbalaji@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:49:26 +0000 Subject: [PATCH 6/6] feat(infra): use .registryrc as default for deploy-key-funder with override option --- turbo.json | 2 +- .../scripts/funding/deploy-key-funder.ts | 32 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/turbo.json b/turbo.json index 2928816dde7..5b5a79cbdda 100644 --- a/turbo.json +++ b/turbo.json @@ -29,7 +29,7 @@ }, "bundle": { "dependsOn": ["^build"], - "outputs": ["*-bundle/**", "bundle/**"] + "outputs": ["*-bundle/**"] } } } diff --git a/typescript/infra/scripts/funding/deploy-key-funder.ts b/typescript/infra/scripts/funding/deploy-key-funder.ts index 1cf12090808..fd554f34794 100644 --- a/typescript/infra/scripts/funding/deploy-key-funder.ts +++ b/typescript/infra/scripts/funding/deploy-key-funder.ts @@ -1,13 +1,21 @@ -import { input } from '@inquirer/prompts'; +import { confirm, input } from '@inquirer/prompts'; import chalk from 'chalk'; +import { readFileSync } from 'fs'; +import { join } from 'path'; import { Contexts } from '../../config/contexts.js'; import { KeyFunderHelmManager } from '../../src/funding/key-funder.js'; import { validateRegistryCommit } from '../../src/utils/git.js'; import { HelmCommand } from '../../src/utils/helm.js'; +import { getMonorepoRoot } from '../../src/utils/utils.js'; import { assertCorrectKubeContext } from '../agent-utils.js'; import { getConfigsBasedOnArgs } from '../core-utils.js'; +function readRegistryRc(): string { + const registryRcPath = join(getMonorepoRoot(), '.registryrc'); + return readFileSync(registryRcPath, 'utf-8').trim(); +} + async function main() { const { agentConfig, envConfig, environment } = await getConfigsBasedOnArgs(); if (agentConfig.context != Contexts.Hyperlane) @@ -17,10 +25,26 @@ async function main() { await assertCorrectKubeContext(envConfig); - const registryCommit = await input({ - message: - 'Enter the registry version to use (can be a commit, branch or tag):', + const defaultRegistryCommit = readRegistryRc(); + console.log( + chalk.gray( + `Using registry commit from .registryrc: ${defaultRegistryCommit}`, + ), + ); + + const shouldOverride = await confirm({ + message: 'Do you want to override the registry version?', + default: false, }); + + let registryCommit = defaultRegistryCommit; + if (shouldOverride) { + registryCommit = await input({ + message: + 'Enter the registry version to use (can be a commit, branch or tag):', + }); + } + await validateRegistryCommit(registryCommit); const manager = KeyFunderHelmManager.forEnvironment(