Skip to content

Commit 2103db3

Browse files
paulbalajiclaude
andauthored
feat(keyfunder): add standalone keyfunder package (#7720)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 22b9e14 commit 2103db3

21 files changed

Lines changed: 1835 additions & 0 deletions

.github/actions/docker-image-comment/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ runs:
5050
declare -A SERVICE_EMOJI=(
5151
["rebalancer"]="♻️"
5252
["warp-monitor"]="🕵️"
53+
["key-funder"]="🔑"
5354
["offchain-lookup-server"]="🔍"
5455
["monorepo"]="📦"
5556
)

.github/workflows/node-services-docker.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
- 'typescript/rebalancer/**'
1111
- 'typescript/warp-monitor/**'
1212
- 'typescript/ccip-server/**'
13+
- 'typescript/keyfunder/**'
1314
- 'typescript/Dockerfile.node-service'
1415
- 'pnpm-lock.yaml'
1516
- '.github/workflows/node-services-docker.yml'
@@ -125,6 +126,7 @@ jobs:
125126
TAGS=$(cat << EOF
126127
${REGISTRY}/hyperlane-rebalancer:${TAG_SHA_DATE}
127128
${REGISTRY}/hyperlane-warp-monitor:${TAG_SHA_DATE}
129+
${REGISTRY}/hyperlane-key-funder:${TAG_SHA_DATE}
128130
${REGISTRY}/hyperlane-offchain-lookup-server:${TAG_SHA_DATE}
129131
EOF
130132
)
@@ -140,6 +142,7 @@ jobs:
140142
|---------|-----|
141143
| ♻️ rebalancer | \`${TAG_SHA_DATE}\` |
142144
| 🕵️ warp-monitor | \`${TAG_SHA_DATE}\` |
145+
| 🔑 key-funder | \`${TAG_SHA_DATE}\` |
143146
| 🔍 offchain-lookup-server | \`${TAG_SHA_DATE}\` |
144147
145148
**Full image paths:**

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ COPY typescript/github-proxy/package.json ./typescript/github-proxy/
3535
COPY typescript/helloworld/package.json ./typescript/helloworld/
3636
COPY typescript/http-registry-server/package.json ./typescript/http-registry-server/
3737
COPY typescript/infra/package.json ./typescript/infra/
38+
COPY typescript/keyfunder/package.json ./typescript/keyfunder/
3839
COPY typescript/provider-sdk/package.json ./typescript/provider-sdk/
3940
COPY typescript/radix-sdk/package.json ./typescript/radix-sdk/
4041
COPY typescript/rebalancer/package.json ./typescript/rebalancer/

pnpm-lock.yaml

Lines changed: 88 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript/docker-bake.hcl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ target "ncc-services" {
4343
{ name = "rebalancer", dir = "rebalancer", package = "@hyperlane-xyz/rebalancer", image = "hyperlane-rebalancer", port = "" },
4444
{ name = "warp-monitor", dir = "warp-monitor", package = "@hyperlane-xyz/warp-monitor", image = "hyperlane-warp-monitor", port = "" },
4545
{ name = "ccip-server", dir = "ccip-server", package = "@hyperlane-xyz/ccip-server", image = "hyperlane-offchain-lookup-server", port = "3000" },
46+
{ name = "keyfunder", dir = "keyfunder", package = "@hyperlane-xyz/keyfunder", image = "hyperlane-key-funder", port=""},
4647
]
4748
}
4849

typescript/keyfunder/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.env*
2+
/dist
3+
/bundle
4+
/cache
5+
6+
# allow check-in of .env.example
7+
!.env.example

typescript/keyfunder/.mocharc.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"import": ["tsx"]
3+
}

typescript/keyfunder/README.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# @hyperlane-xyz/keyfunder
2+
3+
Standalone service for funding Hyperlane agent keys with native tokens across multiple chains.
4+
5+
## Overview
6+
7+
The KeyFunder service:
8+
9+
- Funds agent keys (relayers, kathy, rebalancer) to maintain desired balances
10+
- Claims accumulated fees from InterchainGasPaymaster (IGP) contracts
11+
- Sweeps excess funds from the funder wallet to a safe address
12+
13+
## Configuration
14+
15+
The service reads configuration from a YAML file. The file path is specified via the `KEYFUNDER_CONFIG_FILE` environment variable.
16+
17+
### Example Configuration
18+
19+
```yaml
20+
version: '1'
21+
22+
# Roles define WHO gets funded (address defined once, reused across chains)
23+
roles:
24+
hyperlane-relayer:
25+
address: '0x74cae0ecc47b02ed9b9d32e000fd70b9417970c5'
26+
hyperlane-kathy:
27+
address: '0x5fb02f40f56d15f0442a39d11a23f73747095b20'
28+
hyperlane-rebalancer:
29+
address: '0xdef456...'
30+
31+
# Chains define HOW MUCH each role gets (balances reference role names)
32+
chains:
33+
ethereum:
34+
balances:
35+
hyperlane-relayer: '0.5'
36+
hyperlane-kathy: '0.4'
37+
igp:
38+
address: '0x6cA0B6D43F8e45C82e57eC5a5F2Bce4bF2b6F1f7'
39+
claimThreshold: '0.2'
40+
sweep:
41+
enabled: true
42+
address: '0x478be6076f31E9666123B9721D0B6631baD944AF'
43+
threshold: '0.3'
44+
targetMultiplier: 1.5
45+
triggerMultiplier: 2.0
46+
arbitrum:
47+
balances:
48+
hyperlane-relayer: '0.1'
49+
igp:
50+
address: '0x3b6044acd6767f017e99318AA6Ef93b7B06A5a22'
51+
claimThreshold: '0.1'
52+
53+
metrics:
54+
jobName: 'keyfunder-mainnet3'
55+
labels:
56+
environment: 'mainnet3'
57+
chainsToSkip: []
58+
```
59+
60+
### Configuration Options
61+
62+
| Field | Description |
63+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
64+
| `version` | Config version, must be "1" |
65+
| `roles` | Role definitions (address per role) |
66+
| `roles.<role>.address` | Ethereum address for this role |
67+
| `chains` | Per-chain configuration |
68+
| `chains.<chain>.balances` | Map of role name to desired balance |
69+
| `chains.<chain>.balances.<role>` | Target balance decimal string (e.g., "0.5" ETH; up to 18 decimals) |
70+
| `chains.<chain>.igp` | IGP claim configuration |
71+
| `chains.<chain>.igp.address` | IGP contract address (required when `igp` is specified) |
72+
| `chains.<chain>.igp.claimThreshold` | Minimum IGP balance before claiming (decimal string; up to 18 decimals) |
73+
| `chains.<chain>.sweep` | Sweep excess funds configuration |
74+
| `chains.<chain>.sweep.enabled` | Enable sweep functionality |
75+
| `chains.<chain>.sweep.address` | Address to sweep funds to (required when enabled) |
76+
| `chains.<chain>.sweep.threshold` | Base threshold for sweep calculations (required when enabled; decimal string; up to 18 decimals) |
77+
| `chains.<chain>.sweep.targetMultiplier` | Multiplier for target balance (default: 1.5; 2 decimal precision, floored) |
78+
| `chains.<chain>.sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0; 2 decimal precision, floored) |
79+
| `metrics.jobName` | Job name for metrics |
80+
| `metrics.labels` | Additional labels for metrics |
81+
| `chainsToSkip` | Array of chain names to skip |
82+
83+
### Precision Notes
84+
85+
- **Balance strings**: Support up to 18 decimal places (standard ETH precision). Must include leading digit (e.g., `"0.5"` not `".5"`).
86+
- **Multipliers**: Calculated with 2 decimal precision using floor (e.g., `1.555` is treated as `1.55`, not `1.56`).
87+
88+
## Environment Variables
89+
90+
| Variable | Description | Required |
91+
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
92+
| `KEYFUNDER_CONFIG_FILE` | Path to config YAML file | Yes |
93+
| `HYP_KEY` | Private key for funding wallet | Yes |
94+
| `RPC_URL_<CHAIN>` | RPC URL per chain (e.g., `RPC_URL_ETHEREUM`, `RPC_URL_ARBITRUM`). Falls back to registry defaults if not set. | No |
95+
| `REGISTRY_URI` | Hyperlane registry URI (default: GitHub registry). Supports commit pinning (e.g., `github://hyperlane-xyz/hyperlane-registry/commit/abc123`) | No |
96+
| `SKIP_IGP_CLAIM` | Set to "true" to skip IGP claims | No |
97+
| `PROMETHEUS_PUSH_GATEWAY` | Prometheus push gateway URL (e.g., `http://prometheus-pushgateway:9091`) | No |
98+
| `SERVICE_VERSION` | Version identifier for logging (default: "dev") | No |
99+
| `LOG_LEVEL` | Log level: DEBUG, INFO, WARN, ERROR | No |
100+
| `LOG_FORMAT` | Log format: JSON, PRETTY | No |
101+
102+
In Kubernetes deployments, `HYP_KEY` and `RPC_URL_*` are injected via ExternalSecrets from GCP Secret Manager.
103+
104+
## Usage
105+
106+
### Docker
107+
108+
```bash
109+
docker run -v /path/to/config.yaml:/config/keyfunder.yaml \
110+
-e KEYFUNDER_CONFIG_FILE=/config/keyfunder.yaml \
111+
-e HYP_KEY=0x... \
112+
-e RPC_URL_ETHEREUM=https://... \
113+
gcr.io/abacus-labs-dev/hyperlane-key-funder:latest
114+
```
115+
116+
### Local Development
117+
118+
```bash
119+
# Build
120+
pnpm build
121+
122+
# Run locally
123+
KEYFUNDER_CONFIG_FILE=./config.yaml HYP_KEY=0x... RPC_URL_ETHEREUM=https://... pnpm start:dev
124+
```
125+
126+
### Bundle
127+
128+
The service can be bundled into a single file using ncc:
129+
130+
```bash
131+
pnpm bundle
132+
# Output: ./bundle/index.js
133+
```
134+
135+
## Funding Logic
136+
137+
### Key Funding
138+
139+
Keys are funded when their balance drops below 40% of the desired balance. The funding amount brings the balance up to the full desired balance.
140+
141+
**Example**: If `desiredBalance` is `1.0 ETH` and current balance is `0.39 ETH` (39%), funding is triggered. The key receives `0.61 ETH` to reach the full `1.0 ETH`.
142+
143+
### IGP Claims
144+
145+
When the IGP contract balance exceeds the claim threshold, accumulated fees are claimed to the funder wallet.
146+
147+
### Sweep
148+
149+
When the funder wallet balance exceeds `threshold * triggerMultiplier`, excess funds are swept to the safe address, leaving `threshold * targetMultiplier` in the wallet.
150+
151+
**Example**: With `threshold: '1.0'`, `triggerMultiplier: 2.0`, `targetMultiplier: 1.5`:
152+
153+
- If funder balance > 2.0 ETH, sweep is triggered
154+
- After sweep, funder balance = 1.5 ETH
155+
156+
### Timeouts
157+
158+
Each chain is processed with a 60-second timeout. If funding operations for a chain exceed this limit, the chain is marked as failed and processing continues with remaining chains.
159+
160+
## Metrics
161+
162+
The service exposes Prometheus metrics:
163+
164+
| Metric | Description |
165+
| ------------------------------------------------ | ---------------------------- |
166+
| `hyperlane_keyfunder_wallet_balance` | Current wallet balance |
167+
| `hyperlane_keyfunder_funding_amount` | Amount funded to a key |
168+
| `hyperlane_keyfunder_igp_balance` | IGP contract balance |
169+
| `hyperlane_keyfunder_sweep_amount` | Amount swept to safe address |
170+
| `hyperlane_keyfunder_operation_duration_seconds` | Duration of operations |
171+
172+
## Deployment
173+
174+
The service is typically deployed as a Kubernetes CronJob. See `typescript/infra/helm/key-funder/` for the Helm chart.
175+
176+
## License
177+
178+
Apache-2.0
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { jsRules, typescriptRules } from '@hyperlane-xyz/eslint-config';
2+
3+
export default [...jsRules, ...typescriptRules];

0 commit comments

Comments
 (0)