diff --git a/.github/workflows/rebalancer-docker.yml b/.github/workflows/rebalancer-docker.yml new file mode 100644 index 00000000000..f7d7d0a0339 --- /dev/null +++ b/.github/workflows/rebalancer-docker.yml @@ -0,0 +1,134 @@ +name: Build and Push Rebalancer Image to GCR +on: + push: + branches: [main] + tags: + - '**' + paths: + - 'typescript/rebalancer/**' + - 'typescript/sdk/**' + - 'typescript/provider-sdk/**' + - 'typescript/utils/**' + - '.registryrc' + - '.github/workflows/rebalancer-docker.yml' + pull_request: + paths: + - 'typescript/rebalancer/**' + - 'typescript/sdk/**' + - 'typescript/provider-sdk/**' + - 'typescript/utils/**' + - '.registryrc' + - '.github/workflows/rebalancer-docker.yml' + workflow_dispatch: + inputs: + include_arm64: + description: 'Include arm64 in the build' + required: false + default: 'false' + +concurrency: + group: build-push-rebalancer-${{ 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 + + - 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-rebalancer + 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: Read .registryrc + shell: bash + run: | + REGISTRY_VERSION=$(cat .registryrc) + echo "REGISTRY_VERSION=$REGISTRY_VERSION" >> $GITHUB_ENV + + - 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: Build and push + id: build + uses: depot/build-push-action@v1 + with: + project: 3cpjhx94qv + context: ./ + file: ./typescript/rebalancer/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + REGISTRY_COMMIT=${{ env.REGISTRY_VERSION }} + platforms: ${{ steps.determine-platforms.outputs.platforms }} + + - name: Comment image tags on PR + if: github.event_name == 'pull_request' && always() + uses: ./.github/actions/docker-image-comment + with: + comment_tag: rebalancer-docker-image + image_name: Rebalancer 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/Dockerfile b/Dockerfile index f052c9c7b57..899f5582cb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY typescript/http-registry-server/package.json ./typescript/http-registry-ser COPY typescript/infra/package.json ./typescript/infra/ 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/ COPY typescript/sdk/package.json ./typescript/sdk/ COPY typescript/tsconfig/package.json ./typescript/tsconfig/ COPY typescript/utils/package.json ./typescript/utils/ diff --git a/package.json b/package.json index 3a621d3ad45..d9ca51dcb13 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "starknet" ], "resolutions": { + "zod": "3.21.2", "async": "^2.6.4", "fetch-ponyfill": "^7.1", "flat": "^5.0.2", diff --git a/solhint-plugin/package.json b/solhint-plugin/package.json index c0473462ec4..f5c4ffc9c35 100644 --- a/solhint-plugin/package.json +++ b/solhint-plugin/package.json @@ -1,7 +1,7 @@ { "name": "solhint-plugin-hyperlane", "private": true, - "version": "19.9.0", + "version": "19.10.0", "description": "", "license": "Apache-2.0", "type": "commonjs", diff --git a/typescript/ccip-server/package.json b/typescript/ccip-server/package.json index 6ca394f8b0b..638f0f2249b 100644 --- a/typescript/ccip-server/package.json +++ b/typescript/ccip-server/package.json @@ -48,7 +48,7 @@ "@eth-optimism/sdk": "^3.3.3", "@google-cloud/pino-logging-gcp-config": "^1.0.6", "@hyperlane-xyz/core": "10.0.4", - "@hyperlane-xyz/registry": "20.0.0", + "@hyperlane-xyz/registry": "23.6.0", "@hyperlane-xyz/sdk": "19.10.0", "@hyperlane-xyz/utils": "19.10.0", "@prisma/client": "^6.8.2", @@ -58,7 +58,7 @@ "express": "^4.17.1", "pino-http": "^10.2.0", "prisma": "^6.8.2", - "prom-client": "^14.0.1", + "prom-client": "^15.1.0", "zod": "^3.21.2" } } diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 01d43142f7f..1319a7ab420 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -15,7 +15,8 @@ "@hyperlane-xyz/http-registry-server": "19.10.0", "@hyperlane-xyz/provider-sdk": "0.2.0", "@hyperlane-xyz/radix-sdk": "19.10.0", - "@hyperlane-xyz/registry": "20.0.0", + "@hyperlane-xyz/rebalancer": "0.1.0", + "@hyperlane-xyz/registry": "23.6.0", "@hyperlane-xyz/sdk": "19.10.0", "@hyperlane-xyz/tsconfig": "workspace:^", "@hyperlane-xyz/utils": "19.10.0", @@ -49,7 +50,7 @@ "mocha": "^11.5.0", "pino": "^8.19.0", "prettier": "^3.5.3", - "prom-client": "^14.0.1", + "prom-client": "^15.1.0", "sinon": "^13.0.2", "terminal-link": "^3.0.0", "testcontainers": "11.7.0", diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index 2ee9d8ed90f..b732851a431 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -2,6 +2,7 @@ import util from 'util'; import { stringify as yamlStringify } from 'yaml'; import { CommandModule } from 'yargs'; +import { RebalancerConfig, RebalancerService } from '@hyperlane-xyz/rebalancer'; import { RawForkedChainConfigByChain, RawForkedChainConfigByChainSchema, @@ -15,6 +16,7 @@ import { difference, intersection, objFilter, + rootLogger, } from '@hyperlane-xyz/utils'; import { runWarpRouteCheck } from '../check/warp.js'; @@ -35,7 +37,6 @@ import { logGreen, } from '../logger.js'; import { getWarpRouteConfigsByCore, runWarpRouteRead } from '../read/warp.js'; -import { RebalancerRunner } from '../rebalancer/runner.js'; import { sendTestTransfer } from '../send/transfer.js'; import { ExtendedChainSubmissionStrategySchema } from '../submitters/types.js'; import { @@ -490,18 +491,63 @@ export const rebalancer: CommandModuleWithWriteContext<{ }, }, handler: async (args) => { - let runner: RebalancerRunner; - try { - const { context, ...rest } = args; - runner = await RebalancerRunner.create(rest, context); - } catch (e: any) { - // exit on startup errors - errorRed(`Rebalancer startup error: ${util.format(e)}`); - process.exit(1); - } + const { + context, + config: configPath, + checkFrequency, + withMetrics, + monitorOnly, + manual, + origin, + destination, + amount, + } = args; + + logCommandHeader('Hyperlane Warp Route Rebalancer'); try { - await runner.run(); + // Load rebalancer configuration + const rebalancerConfig = RebalancerConfig.load(configPath); + + // Determine execution mode + const mode = manual ? 'manual' : 'daemon'; + + // Create rebalancer service + const service = new RebalancerService( + context.multiProvider, + context.multiProtocolProvider, + context.registry, + rebalancerConfig, + { + mode, + checkFrequency, + withMetrics, + monitorOnly, + coingeckoApiKey: process.env.COINGECKO_API_KEY, + logger: rootLogger.child({ module: 'rebalancer' }), + }, + ); + + // Execute based on mode + if (manual) { + if (!origin || !destination || !amount) { + errorRed( + 'Origin, destination, and amount are required for manual rebalance', + ); + process.exit(1); + } + + await service.executeManual({ + origin, + destination, + amount, + }); + + logGreen('✅ Manual rebalance completed successfully'); + } else { + // Start daemon mode + await service.start(); + } } catch (e: any) { errorRed(`Rebalancer error: ${util.format(e)}`); process.exit(1); diff --git a/typescript/cli/src/context/strategies/chain/chainResolver.ts b/typescript/cli/src/context/strategies/chain/chainResolver.ts index 87ee90673d0..748a6d9225e 100644 --- a/typescript/cli/src/context/strategies/chain/chainResolver.ts +++ b/typescript/cli/src/context/strategies/chain/chainResolver.ts @@ -1,3 +1,4 @@ +import { RebalancerConfig } from '@hyperlane-xyz/rebalancer'; import { ChainName, DeployedCoreAddresses, @@ -9,7 +10,6 @@ import { ProtocolType, assert } from '@hyperlane-xyz/utils'; import { CommandType } from '../../../commands/signCommands.js'; import { readCoreDeployConfigs } from '../../../config/core.js'; import { getWarpRouteDeployConfig } from '../../../config/warp.js'; -import { RebalancerConfig } from '../../../rebalancer/config/RebalancerConfig.js'; import { runMultiChainSelectionStep, runSingleChainSelectionStep, diff --git a/typescript/cli/src/rebalancer/README.md b/typescript/cli/src/rebalancer/README.md deleted file mode 100644 index a375f41da95..00000000000 --- a/typescript/cli/src/rebalancer/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Hyperlane Warp Rebalancer - -The Hyperlane Warp Rebalancer is a tool that automatically manages the balance of collateral across chains in a Warp Route. It ensures that each chain maintains an optimal balance of tokens based on the configured strategy. - -## Configuration - -The rebalancer uses a configuration file that defines both global settings and chain-specific configurations. The configuration file can be in either YAML or JSON format. - -The basic structure of the configuration is as follows: - -```yaml -# Required: Unique identifier for the Warp Route. This is used to identify the -# HypERC20 token that is being rebalanced. -# The format is /