Skip to content

Commit d5cc001

Browse files
committed
fix(keyfunder): align config validation and IGP init
1 parent 93cea4e commit d5cc001

6 files changed

Lines changed: 124 additions & 45 deletions

File tree

typescript/keyfunder/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
2222

2323
COPY patches ./patches
2424

25+
COPY typescript/aleo-sdk/package.json ./typescript/aleo-sdk/
2526
COPY typescript/keyfunder/package.json ./typescript/keyfunder/
2627
COPY typescript/sdk/package.json ./typescript/sdk/
2728
COPY typescript/provider-sdk/package.json ./typescript/provider-sdk/
@@ -39,6 +40,7 @@ COPY starknet/package.json ./starknet/
3940
RUN pnpm install --frozen-lockfile
4041

4142
COPY turbo.json ./
43+
COPY typescript/aleo-sdk ./typescript/aleo-sdk
4244
COPY typescript/keyfunder ./typescript/keyfunder
4345
COPY typescript/sdk ./typescript/sdk
4446
COPY typescript/provider-sdk ./typescript/provider-sdk

typescript/keyfunder/README.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -60,27 +60,27 @@ chainsToSkip: []
6060
6161
### Configuration Options
6262
63-
| Field | Description |
64-
| ---------------------------------------- | ------------------------------------------------ |
65-
| `version` | Config version, must be "1" |
66-
| `roles` | Role definitions (address per role) |
67-
| `roles.<role>.address` | Ethereum address for this role |
68-
| `chains` | Per-chain configuration |
69-
| `chains.<chain>.balances` | Map of role name to desired balance |
70-
| `chains.<chain>.balances.<role>` | Target balance in native token (e.g., "0.5" ETH) |
71-
| `chains.<chain>.igp` | IGP claim configuration |
72-
| `chains.<chain>.igp.address` | IGP contract address |
73-
| `chains.<chain>.igp.claimThreshold` | Minimum IGP balance before claiming |
74-
| `chains.<chain>.sweep` | Sweep excess funds configuration |
75-
| `chains.<chain>.sweep.enabled` | Enable sweep functionality |
76-
| `chains.<chain>.sweep.address` | Address to sweep funds to |
77-
| `chains.<chain>.sweep.threshold` | Base threshold for sweep calculations |
78-
| `chains.<chain>.sweep.targetMultiplier` | Multiplier for target balance (default: 1.5) |
79-
| `chains.<chain>.sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0) |
80-
| `metrics.pushGateway` | Prometheus push gateway URL |
81-
| `metrics.jobName` | Job name for metrics |
82-
| `metrics.labels` | Additional labels for metrics |
83-
| `chainsToSkip` | Array of chain names to skip |
63+
| Field | Description |
64+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
65+
| `version` | Config version, must be "1" |
66+
| `roles` | Role definitions (address per role) |
67+
| `roles.<role>.address` | Ethereum address for this role |
68+
| `chains` | Per-chain configuration |
69+
| `chains.<chain>.balances` | Map of role name to desired balance |
70+
| `chains.<chain>.balances.<role>` | Target balance decimal string (e.g., "0.5" ETH; up to 18 decimals) |
71+
| `chains.<chain>.igp` | IGP claim configuration |
72+
| `chains.<chain>.igp.address` | IGP contract address |
73+
| `chains.<chain>.igp.claimThreshold` | Minimum IGP balance before claiming (decimal string; up to 18 decimals) |
74+
| `chains.<chain>.sweep` | Sweep excess funds configuration |
75+
| `chains.<chain>.sweep.enabled` | Enable sweep functionality |
76+
| `chains.<chain>.sweep.address` | Address to sweep funds to (required when enabled) |
77+
| `chains.<chain>.sweep.threshold` | Base threshold for sweep calculations (required when enabled; decimal string; up to 18 decimals) |
78+
| `chains.<chain>.sweep.targetMultiplier` | Multiplier for target balance (default: 1.5) |
79+
| `chains.<chain>.sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0) |
80+
| `metrics.pushGateway` | Prometheus push gateway URL |
81+
| `metrics.jobName` | Job name for metrics |
82+
| `metrics.labels` | Additional labels for metrics |
83+
| `chainsToSkip` | Array of chain names to skip |
8484

8585
## Environment Variables
8686

@@ -132,9 +132,9 @@ pnpm bundle
132132

133133
### Key Funding
134134

135-
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.
135+
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.
136136

137-
**Example**: If `desiredBalance` is `1.0 ETH` and current balance is `0.55 ETH` (55%), funding is triggered. The key receives `0.45 ETH` to reach the full `1.0 ETH`.
137+
**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`.
138138

139139
### IGP Claims
140140

typescript/keyfunder/src/config/types.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ describe('KeyFunderConfig Schemas', () => {
4545
expect(result.success).to.be.true;
4646
});
4747

48+
it('should reject enabled sweep config without address', () => {
49+
const config = {
50+
enabled: true,
51+
threshold: '0.5',
52+
};
53+
const result = SweepConfigSchema.safeParse(config);
54+
expect(result.success).to.be.false;
55+
});
56+
57+
it('should reject enabled sweep config without threshold', () => {
58+
const config = {
59+
enabled: true,
60+
address: '0x478be6076f31E9666123B9721D0B6631baD944AF',
61+
};
62+
const result = SweepConfigSchema.safeParse(config);
63+
expect(result.success).to.be.false;
64+
});
65+
4866
it('should reject trigger multiplier less than target + 0.05', () => {
4967
const config = {
5068
enabled: true,
@@ -60,6 +78,7 @@ describe('KeyFunderConfig Schemas', () => {
6078
it('should use default multipliers', () => {
6179
const config = {
6280
enabled: true,
81+
address: '0x478be6076f31E9666123B9721D0B6631baD944AF',
6382
threshold: '0.5',
6483
};
6584
const result = SweepConfigSchema.safeParse(config);
@@ -115,6 +134,7 @@ describe('KeyFunderConfig Schemas', () => {
115134
},
116135
sweep: {
117136
enabled: true,
137+
address: '0x478be6076f31E9666123B9721D0B6631baD944AF',
118138
threshold: '0.3',
119139
},
120140
};
@@ -132,6 +152,26 @@ describe('KeyFunderConfig Schemas', () => {
132152
expect(result.success).to.be.false;
133153
});
134154

155+
it('should reject scientific notation in balances', () => {
156+
const config = {
157+
balances: {
158+
'hyperlane-relayer': '1e3',
159+
},
160+
};
161+
const result = ChainConfigSchema.safeParse(config);
162+
expect(result.success).to.be.false;
163+
});
164+
165+
it('should reject balances with too many decimals', () => {
166+
const config = {
167+
balances: {
168+
'hyperlane-relayer': '1.1234567890123456789',
169+
},
170+
};
171+
const result = ChainConfigSchema.safeParse(config);
172+
expect(result.success).to.be.false;
173+
});
174+
135175
it('should reject negative balance', () => {
136176
const config = {
137177
balances: {

typescript/keyfunder/src/config/types.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ const AddressSchema = z
99

1010
const BalanceStringSchema = z
1111
.string()
12-
.refine(
13-
(val) => !isNaN(parseFloat(val)) && parseFloat(val) >= 0,
14-
'Must be a valid non-negative number string',
12+
.regex(
13+
/^(?:\d+)(?:\.\d{1,18})?$/,
14+
'Must be a valid non-negative decimal string (up to 18 decimals)',
1515
);
1616

1717
export const RoleConfigSchema = z.object({
@@ -45,6 +45,14 @@ export const SweepConfigSchema = z
4545
.default(2.0),
4646
threshold: BalanceStringSchema.optional(),
4747
})
48+
.refine((data) => !data.enabled || !!data.address, {
49+
message: 'Sweep address is required when sweep is enabled',
50+
path: ['address'],
51+
})
52+
.refine((data) => !data.enabled || !!data.threshold, {
53+
message: 'Sweep threshold is required when sweep is enabled',
54+
path: ['threshold'],
55+
})
4856
.refine(
4957
(data) => {
5058
if (!data.enabled) return true;

typescript/keyfunder/src/core/KeyFunder.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ import type { KeyFunderMetrics } from '../metrics/Metrics.js';
1313
const MIN_DELTA_NUMERATOR = BigNumber.from(6);
1414
const MIN_DELTA_DENOMINATOR = BigNumber.from(10);
1515

16-
const DEFAULT_SWEEP_ADDRESS = '0x478be6076f31E9666123B9721D0B6631baD944AF';
17-
const DEFAULT_TARGET_MULTIPLIER = 1.5;
18-
const DEFAULT_TRIGGER_MULTIPLIER = 2.0;
19-
2016
const CHAIN_FUNDING_TIMEOUT_MS = 60_000;
2117

2218
export interface KeyFunderOptions {
@@ -276,24 +272,24 @@ export class KeyFunder {
276272
chainConfig: ChainConfig,
277273
): Promise<void> {
278274
const sweepConfig = chainConfig.sweep;
279-
if (!sweepConfig?.enabled || !sweepConfig.threshold) {
275+
if (!sweepConfig?.enabled) {
280276
return;
281277
}
282278

283279
const logger = this.options.logger.child({ chain, operation: 'sweep' });
284280

285-
const sweepAddress = sweepConfig.address ?? DEFAULT_SWEEP_ADDRESS;
286-
const targetMultiplier =
287-
sweepConfig.targetMultiplier ?? DEFAULT_TARGET_MULTIPLIER;
288-
const triggerMultiplier =
289-
sweepConfig.triggerMultiplier ?? DEFAULT_TRIGGER_MULTIPLIER;
281+
if (!sweepConfig.address || !sweepConfig.threshold) {
282+
throw new Error(
283+
`Sweep config is invalid for chain ${chain}: address and threshold are required when sweep is enabled`,
284+
);
285+
}
290286

291287
const threshold = ethers.utils.parseEther(sweepConfig.threshold);
292288
const targetBalance = threshold
293-
.mul(Math.floor(targetMultiplier * 100))
289+
.mul(Math.round(sweepConfig.targetMultiplier * 100))
294290
.div(100);
295291
const triggerThreshold = threshold
296-
.mul(Math.floor(triggerMultiplier * 100))
292+
.mul(Math.round(sweepConfig.triggerMultiplier * 100))
297293
.div(100);
298294

299295
const funderBalance = await this.multiProvider
@@ -315,13 +311,13 @@ export class KeyFunder {
315311
logger.info(
316312
{
317313
sweepAmount: ethers.utils.formatEther(sweepAmount),
318-
sweepAddress,
314+
sweepAddress: sweepConfig.address,
319315
},
320316
'Sweeping excess funds',
321317
);
322318

323319
const tx = await this.multiProvider.sendTransaction(chain, {
324-
to: sweepAddress,
320+
to: sweepConfig.address,
325321
value: sweepAmount,
326322
});
327323

typescript/keyfunder/src/service.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,45 @@ async function main(): Promise<void> {
6868
if (chainsWithIgp.length > 0) {
6969
const addresses = await registry.getAddresses();
7070
const igpAddresses = Object.fromEntries(
71-
chainsWithIgp
72-
.filter((chain) => addresses[chain])
73-
.map((chain) => [chain, addresses[chain]]),
71+
chainsWithIgp.flatMap((chain) => {
72+
const igpConfig = config.chains[chain]?.igp;
73+
if (!igpConfig) {
74+
return [];
75+
}
76+
77+
const registryChainAddresses = addresses[chain] ?? {};
78+
const merged = {
79+
...registryChainAddresses,
80+
// Prefer config-provided IGP address, fall back to registry if needed.
81+
interchainGasPaymaster:
82+
igpConfig.address ??
83+
registryChainAddresses.interchainGasPaymaster,
84+
};
85+
86+
if (!merged.interchainGasPaymaster) {
87+
logger.warn(
88+
{ chain },
89+
'No IGP address found (config or registry), skipping IGP initialization for chain',
90+
);
91+
return [];
92+
}
93+
94+
return [[chain, merged]];
95+
}),
7496
);
75-
igp = HyperlaneIgp.fromAddressesMap(igpAddresses, multiProvider);
76-
logger.info({ chains: chainsWithIgp }, 'Initialized IGP contracts');
97+
98+
if (Object.keys(igpAddresses).length === 0) {
99+
logger.warn(
100+
{ chainsWithIgp },
101+
'IGP configured but no usable IGP addresses were found; skipping IGP initialization',
102+
);
103+
} else {
104+
igp = HyperlaneIgp.fromAddressesMap(igpAddresses, multiProvider);
105+
logger.info(
106+
{ chains: Object.keys(igpAddresses) },
107+
'Initialized IGP contracts',
108+
);
109+
}
77110
}
78111

79112
const metrics = new KeyFunderMetrics(

0 commit comments

Comments
 (0)