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 }} 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/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/ 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/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..fd554f34794 100644 --- a/typescript/infra/scripts/funding/deploy-key-funder.ts +++ b/typescript/infra/scripts/funding/deploy-key-funder.ts @@ -1,12 +1,21 @@ +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 { checkMonorepoImageExists } from '../../src/utils/gcloud.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) @@ -16,23 +25,32 @@ 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 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):', + }); } - const manager = KeyFunderHelmManager.forEnvironment(environment); + await validateRegistryCommit(registryCommit); + + 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..e36efa6e754 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,217 @@ 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 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 balances = this.getBalancesForChain(chain, roleAddressMap); + if (Object.keys(balances).length > 0) { + chainConfig.balances = balances; + } + + 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, + roles, + 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 buildRoleAddressMap( + environment: DeployEnvironment, + ): Record { + const roleAddressMap: 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 address = this.getAddressForRole(environment, context, role); + if (address) { + const roleName = `${context}-${role}`; + roleAddressMap[roleName] = address; + } + } + } + + return roleAddressMap; + } + + 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 balances; } + + 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 RoleYamlConfig { + address: string; +} + +interface ChainYamlConfig { + balances?: Record; + 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..6509b2af499 --- /dev/null +++ b/typescript/keyfunder/README.md @@ -0,0 +1,165 @@ +# @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' + +# 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: + balances: + hyperlane-relayer: '0.5' + hyperlane-kathy: '0.4' + igp: + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7' + claimThreshold: '0.2' + sweep: + enabled: true + address: '0x478be6076f31E9666123B9721D0B6631baD944AF' + threshold: '0.3' + targetMultiplier: 1.5 + triggerMultiplier: 2.0 + arbitrum: + balances: + hyperlane-relayer: '0.1' + igp: + address: '0x...' + claimThreshold: '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" | +| `roles` | Role definitions (address per role) | +| `roles..address` | Ethereum address for this role | +| `chains` | Per-chain configuration | +| `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 | +| `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..a4647f87df2 --- /dev/null +++ b/typescript/keyfunder/src/config/KeyFunderConfig.test.ts @@ -0,0 +1,188 @@ +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" +roles: + hyperlane-relayer: + address: "0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5" +chains: + ethereum: + balances: + hyperlane-relayer: "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.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', () => { + fsExistsStub.returns(false); + + expect(() => KeyFunderConfigLoader.load('/nonexistent.yaml')).to.throw( + 'Config file not found', + ); + }); + + it('should throw on invalid config', () => { + const invalidYaml = ` +version: "2" +roles: {} +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, + roles: { + 'hyperlane-relayer': { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }, + }, + chains: { + ethereum: { + balances: { + 'hyperlane-relayer': '0.5', + }, + }, + }, + }; + + const loader = KeyFunderConfigLoader.fromObject(config); + 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 never)).to.throw( + 'Invalid keyfunder config', + ); + }); + }); + + describe('getConfiguredChains', () => { + it('should return all chain names', () => { + const config = { + version: '1' as const, + roles: {}, + chains: { + ethereum: {}, + arbitrum: {}, + polygon: {}, + }, + }; + + 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, + roles: {}, + chains: { + ethereum: {}, + arbitrum: {}, + polygon: {}, + }, + 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, + roles: {}, + chains: { + ethereum: {}, + arbitrum: {}, + }, + }; + + 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, + roles: {}, + 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, + roles: {}, + 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..4d1d5f54b07 --- /dev/null +++ b/typescript/keyfunder/src/config/types.test.ts @@ -0,0 +1,291 @@ +import { expect } from 'chai'; + +import { + ChainConfigSchema, + KeyFunderConfigSchema, + RoleConfigSchema, + SweepConfigSchema, +} from './types.js'; + +describe('KeyFunderConfig Schemas', () => { + describe('RoleConfigSchema', () => { + it('should validate a valid role config', () => { + const config = { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }; + const result = RoleConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + + it('should reject invalid address', () => { + const config = { + address: 'invalid-address', + }; + const result = RoleConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + + it('should reject missing address', () => { + const config = {}; + const result = RoleConfigSchema.safeParse(config); + expect(result.success).to.be.false; + }); + }); + + 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, + }; + 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, + }; + const result = SweepConfigSchema.safeParse(config); + expect(result.success).to.be.true; + }); + }); + + describe('ChainConfigSchema', () => { + it('should validate chain config with balances only', () => { + const config = { + balances: { + 'hyperlane-relayer': '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 = { + balances: { + 'hyperlane-relayer': '0.5', + 'hyperlane-kathy': '0.3', + }, + igp: { + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', + claimThreshold: '0.2', + }, + sweep: { + enabled: true, + threshold: '0.3', + }, + }; + 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); + expect(result.success).to.be.true; + }); + + 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); + expect(result.success).to.be.false; + }); + + it('should validate complete config', () => { + const config = { + version: '1', + roles: { + 'hyperlane-relayer': { + address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5', + }, + 'hyperlane-kathy': { + address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20', + }, + }, + chains: { + ethereum: { + balances: { + 'hyperlane-relayer': '0.5', + 'hyperlane-kathy': '0.4', + }, + igp: { + address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7', + claimThreshold: '0.2', + }, + sweep: { + enabled: true, + address: '0x478be6076f31E9666123B9721D0B6631baD944AF', + threshold: '0.3', + }, + }, + arbitrum: { + balances: { + 'hyperlane-relayer': '0.1', + }, + }, + }, + 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 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: {}, + }; + 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..a334f33a79e --- /dev/null +++ b/typescript/keyfunder/src/config/types.ts @@ -0,0 +1,118 @@ +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 RoleConfigSchema = z.object({ + address: AddressSchema, +}); + +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({ + balances: z.record(z.string(), BalanceStringSchema).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'), + 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 RoleConfig = 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; + +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 new file mode 100644 index 00000000000..6a604f0c63f --- /dev/null +++ b/typescript/keyfunder/src/core/KeyFunder.ts @@ -0,0 +1,360 @@ +import { BigNumber, ethers } from 'ethers'; +import type { Logger } from 'pino'; + +import { HyperlaneIgp, MultiProvider } from '@hyperlane-xyz/sdk'; + +import type { + ChainConfig, + KeyFunderConfig, + ResolvedKeyConfig, +} 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); + } + + const resolvedKeys = this.resolveKeysForChain(chain, chainConfig); + if (resolvedKeys.length > 0) { + await this.fundKeys(chain, resolvedKeys); + } + + 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 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, + ): 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: ResolvedKeyConfig[], + ): Promise { + for (const key of keys) { + await this.fundKey(chain, key); + } + } + + private async fundKey(chain: string, key: ResolvedKeyConfig): 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, + 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, + 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..01920fd2ac1 --- /dev/null +++ b/typescript/keyfunder/src/index.ts @@ -0,0 +1,24 @@ +export { KeyFunderConfigLoader } from './config/KeyFunderConfig.js'; +export { + KeyFunderConfigSchema, + RoleConfigSchema, + IgpConfigSchema, + SweepConfigSchema, + ChainConfigSchema, + FunderConfigSchema, + MetricsConfigSchema, +} from './config/types.js'; +export type { + KeyFunderConfig, + KeyFunderConfigInput, + RoleConfig, + IgpConfig, + SweepConfig, + ChainConfig, + FunderConfig, + MetricsConfig, + ResolvedKeyConfig, +} 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/**/*"] +} 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/**"] + } + } +}