Skip to content

Commit e835fac

Browse files
committed
CR andrey 1
1 parent d5cc001 commit e835fac

6 files changed

Lines changed: 101 additions & 11 deletions

File tree

typescript/keyfunder/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,18 @@ chainsToSkip: []
7575
| `chains.<chain>.sweep.enabled` | Enable sweep functionality |
7676
| `chains.<chain>.sweep.address` | Address to sweep funds to (required when enabled) |
7777
| `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) |
78+
| `chains.<chain>.sweep.targetMultiplier` | Multiplier for target balance (default: 1.5; 2 decimal precision, floored) |
79+
| `chains.<chain>.sweep.triggerMultiplier` | Multiplier for trigger threshold (default: 2.0; 2 decimal precision, floored) |
8080
| `metrics.pushGateway` | Prometheus push gateway URL |
8181
| `metrics.jobName` | Job name for metrics |
8282
| `metrics.labels` | Additional labels for metrics |
8383
| `chainsToSkip` | Array of chain names to skip |
8484

85+
### Precision Notes
86+
87+
- **Balance strings**: Support up to 18 decimal places (standard ETH precision). Must include leading digit (e.g., `"0.5"` not `".5"`).
88+
- **Multipliers**: Calculated with 2 decimal precision using floor (e.g., `1.555` is treated as `1.55`, not `1.56`).
89+
8590
## Environment Variables
8691

8792
| Variable | Description | Required |

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { expect } from 'chai';
2+
import { BigNumber } from 'ethers';
3+
4+
import { calculateMultipliedBalance } from '../core/KeyFunder.js';
25

36
import {
47
ChainConfigSchema,
@@ -181,6 +184,60 @@ describe('KeyFunderConfig Schemas', () => {
181184
const result = ChainConfigSchema.safeParse(config);
182185
expect(result.success).to.be.false;
183186
});
187+
188+
it('should reject balance without leading digit (.5)', () => {
189+
const config = {
190+
balances: {
191+
'hyperlane-relayer': '.5',
192+
},
193+
};
194+
const result = ChainConfigSchema.safeParse(config);
195+
expect(result.success).to.be.false;
196+
});
197+
198+
it('should accept balance with leading zero (0.5)', () => {
199+
const config = {
200+
balances: {
201+
'hyperlane-relayer': '0.5',
202+
},
203+
};
204+
const result = ChainConfigSchema.safeParse(config);
205+
expect(result.success).to.be.true;
206+
});
207+
208+
it('should accept high precision balances (up to 18 decimals)', () => {
209+
const config = {
210+
balances: {
211+
'hyperlane-relayer': '0.000000000000000001',
212+
},
213+
};
214+
const result = ChainConfigSchema.safeParse(config);
215+
expect(result.success).to.be.true;
216+
});
217+
});
218+
219+
describe('Multiplier precision (calculateMultipliedBalance)', () => {
220+
const oneEther = BigNumber.from('1000000000000000000');
221+
222+
it('should calculate 1.5x correctly (1 ETH * 1.5 = 1.5 ETH)', () => {
223+
const result = calculateMultipliedBalance(oneEther, 1.5);
224+
expect(result.toString()).to.equal('1500000000000000000');
225+
});
226+
227+
it('should calculate 2.0x correctly (1 ETH * 2.0 = 2 ETH)', () => {
228+
const result = calculateMultipliedBalance(oneEther, 2.0);
229+
expect(result.toString()).to.equal('2000000000000000000');
230+
});
231+
232+
it('should floor third decimal (1 ETH * 1.555 = 1.55 ETH, not 1.56 ETH)', () => {
233+
const result = calculateMultipliedBalance(oneEther, 1.555);
234+
expect(result.toString()).to.equal('1550000000000000000');
235+
});
236+
237+
it('should floor (1 ETH * 1.999 = 1.99 ETH, not 2 ETH)', () => {
238+
const result = calculateMultipliedBalance(oneEther, 1.999);
239+
expect(result.toString()).to.equal('1990000000000000000');
240+
});
184241
});
185242

186243
describe('KeyFunderConfigSchema', () => {

typescript/keyfunder/src/config/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ const AddressSchema = z
77
'Must be a valid Ethereum address (0x-prefixed, 40 hex characters)',
88
);
99

10+
// Requires leading digit (e.g., "0.5" not ".5") for YAML readability
1011
const BalanceStringSchema = z
1112
.string()
1213
.regex(
1314
/^(?:\d+)(?:\.\d{1,18})?$/,
14-
'Must be a valid non-negative decimal string (up to 18 decimals)',
15+
'Must be a valid non-negative decimal string with leading digit (e.g., "0.5" not ".5", up to 18 decimals)',
1516
);
1617

1718
export const RoleConfigSchema = z.object({

typescript/keyfunder/src/core/KeyFunder.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,14 @@ export class KeyFunder {
285285
}
286286

287287
const threshold = ethers.utils.parseEther(sweepConfig.threshold);
288-
const targetBalance = threshold
289-
.mul(Math.round(sweepConfig.targetMultiplier * 100))
290-
.div(100);
291-
const triggerThreshold = threshold
292-
.mul(Math.round(sweepConfig.triggerMultiplier * 100))
293-
.div(100);
288+
const targetBalance = calculateMultipliedBalance(
289+
threshold,
290+
sweepConfig.targetMultiplier,
291+
);
292+
const triggerThreshold = calculateMultipliedBalance(
293+
threshold,
294+
sweepConfig.triggerMultiplier,
295+
);
294296

295297
const funderBalance = await this.multiProvider
296298
.getSigner(chain)
@@ -341,6 +343,17 @@ export class KeyFunder {
341343
}
342344
}
343345

346+
/**
347+
* Multiplies a BigNumber by a decimal multiplier with 2 decimal precision (floored).
348+
* e.g., 1 ETH * 1.555 = 1.55 ETH (not 1.56 ETH)
349+
*/
350+
export function calculateMultipliedBalance(
351+
base: BigNumber,
352+
multiplier: number,
353+
): BigNumber {
354+
return base.mul(Math.floor(multiplier * 100)).div(100);
355+
}
356+
344357
function createTimeoutPromise(
345358
timeoutMs: number,
346359
errorMessage: string,

typescript/keyfunder/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@ export type {
1818
ResolvedKeyConfig,
1919
} from './config/types.js';
2020

21-
export { KeyFunder, type KeyFunderOptions } from './core/KeyFunder.js';
21+
export {
22+
KeyFunder,
23+
calculateMultipliedBalance,
24+
type KeyFunderOptions,
25+
} from './core/KeyFunder.js';
2226
export { KeyFunderMetrics } from './metrics/Metrics.js';

typescript/keyfunder/src/service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,21 @@ async function main(): Promise<void> {
121121
igp,
122122
});
123123

124-
await funder.fundAllChains();
124+
let fundingError: Error | undefined;
125+
try {
126+
await funder.fundAllChains();
127+
} catch (error) {
128+
fundingError = error as Error;
129+
}
125130

131+
// Always push metrics, even on failure (matches original fund-keys-from-deployer.ts behavior)
126132
await metrics.push();
127133
logger.info('Metrics pushed to gateway');
128134

135+
if (fundingError) {
136+
throw fundingError;
137+
}
138+
129139
logger.info('KeyFunder completed successfully');
130140
process.exit(0);
131141
} catch (error) {

0 commit comments

Comments
 (0)