Skip to content

Commit ccccbfb

Browse files
authored
Merge pull request #12 from withtally/component/calculator
Base Calculator Component
2 parents 5aad9cb + 23eafcc commit ccccbfb

File tree

20 files changed

+1170
-141
lines changed

20 files changed

+1170
-141
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
node_modules/
33
.pnpm-store/
44
pnpm-lock.yaml
5+
56
# Environment variables
67
.env
78
**/.env
89
.env.*
910
!.env.example
1011

12+
# Output files
13+
staker-monitor-db.json
14+
1115
# Build output
1216
dist/
1317
build/
@@ -20,6 +24,7 @@ yarn-debug.log*
2024
yarn-error.log*
2125
pnpm-debug.log*
2226

27+
2328
# IDE specific files
2429
.vscode/
2530
.idea/

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
"scripts": {
77
"start": "tsx src/index.ts",
88
"dev": "tsx watch src/index.ts",
9-
"monitor": "tsx src/monitor/index.ts",
10-
"monitor:dev": "tsx watch src/monitor/index.ts",
9+
"start:monitor": "COMPONENTS=monitor tsx src/index.ts",
10+
"start:calculator": "COMPONENTS=calculator tsx src/index.ts",
11+
"start:all": "COMPONENTS=monitor,calculator tsx src/index.ts",
12+
"dev:monitor": "COMPONENTS=monitor tsx watch src/index.ts",
13+
"dev:calculator": "COMPONENTS=calculator tsx watch src/index.ts",
14+
"dev:all": "COMPONENTS=monitor,calculator tsx watch src/index.ts",
1115
"lint": "eslint . --ext .ts",
1216
"format": "prettier --write .",
1317
"format:check": "prettier --check ."

src/calculator/Calculator.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { ethers } from 'ethers';
2+
import { IDatabase } from '@/database';
3+
import { ConsoleLogger, Logger } from '@/monitor/logging';
4+
import { CalculatorWrapper } from './CalculatorWrapper';
5+
import { MonitorConfig } from '@/monitor/types';
6+
7+
export class Calculator {
8+
private readonly db: IDatabase;
9+
private readonly provider: ethers.Provider;
10+
private readonly logger: Logger;
11+
private readonly calculator: CalculatorWrapper;
12+
private readonly config: MonitorConfig;
13+
private isRunning: boolean;
14+
private processingPromise?: Promise<void>;
15+
private lastProcessedBlock: number;
16+
17+
constructor(config: MonitorConfig) {
18+
this.config = config;
19+
this.db = config.database;
20+
this.provider = config.provider;
21+
this.logger = new ConsoleLogger(config.logLevel);
22+
this.calculator = new CalculatorWrapper(this.db, this.provider);
23+
this.isRunning = false;
24+
this.lastProcessedBlock = config.startBlock;
25+
}
26+
27+
async start(): Promise<void> {
28+
if (this.isRunning) {
29+
this.logger.warn('Calculator is already running');
30+
return;
31+
}
32+
33+
this.isRunning = true;
34+
this.logger.info('Starting Calculator', {
35+
network: this.config.networkName,
36+
chainId: this.config.chainId,
37+
rewardCalculatorAddress: this.config.rewardCalculatorAddress,
38+
});
39+
40+
// Check for existing checkpoint first
41+
const checkpoint = await this.db.getCheckpoint('calculator');
42+
43+
if (checkpoint) {
44+
this.lastProcessedBlock = checkpoint.last_block_number;
45+
this.logger.info('Resuming from checkpoint', {
46+
blockNumber: this.lastProcessedBlock,
47+
blockHash: checkpoint.block_hash,
48+
lastUpdate: checkpoint.last_update,
49+
});
50+
} else {
51+
// Initialize with start block if no checkpoint exists
52+
this.lastProcessedBlock = this.config.startBlock;
53+
await this.db.updateCheckpoint({
54+
component_type: 'calculator',
55+
last_block_number: this.config.startBlock,
56+
block_hash:
57+
'0x0000000000000000000000000000000000000000000000000000000000000000',
58+
last_update: new Date().toISOString(),
59+
});
60+
this.logger.info('Starting from initial block', {
61+
blockNumber: this.lastProcessedBlock,
62+
});
63+
}
64+
65+
await this.calculator.start();
66+
this.processingPromise = this.processLoop();
67+
}
68+
69+
async stop(): Promise<void> {
70+
this.logger.info('Stopping calculator...');
71+
this.isRunning = false;
72+
await this.calculator.stop();
73+
if (this.processingPromise) {
74+
await this.processingPromise;
75+
}
76+
this.logger.info('Calculator stopped');
77+
}
78+
79+
async getCalculatorStatus(): Promise<{
80+
isRunning: boolean;
81+
lastProcessedBlock: number;
82+
currentChainBlock: number;
83+
processingLag: number;
84+
}> {
85+
const currentBlock = await this.getCurrentBlock();
86+
return {
87+
isRunning: this.isRunning,
88+
lastProcessedBlock: this.lastProcessedBlock,
89+
currentChainBlock: currentBlock,
90+
processingLag: currentBlock - this.lastProcessedBlock,
91+
};
92+
}
93+
94+
private async processLoop(): Promise<void> {
95+
while (this.isRunning) {
96+
try {
97+
const currentBlock = await this.getCurrentBlock();
98+
const targetBlock = currentBlock - this.config.confirmations;
99+
100+
if (targetBlock <= this.lastProcessedBlock) {
101+
this.logger.debug('Waiting for new blocks', {
102+
currentBlock,
103+
targetBlock,
104+
lastProcessedBlock: this.lastProcessedBlock,
105+
});
106+
await new Promise((resolve) =>
107+
setTimeout(resolve, this.config.pollInterval * 1000),
108+
);
109+
continue;
110+
}
111+
112+
const fromBlock = this.lastProcessedBlock + 1;
113+
const toBlock = Math.min(
114+
targetBlock,
115+
fromBlock + this.config.maxBlockRange - 1,
116+
);
117+
118+
this.logger.info('Processing new blocks', {
119+
fromBlock,
120+
toBlock,
121+
currentBlock,
122+
blockRange: toBlock - fromBlock + 1,
123+
});
124+
125+
await this.calculator.processScoreEvents(fromBlock, toBlock);
126+
127+
const block = await this.provider.getBlock(toBlock);
128+
if (!block) throw new Error(`Block ${toBlock} not found`);
129+
130+
// Update checkpoint
131+
await this.db.updateCheckpoint({
132+
component_type: 'calculator',
133+
last_block_number: toBlock,
134+
block_hash: block.hash!,
135+
last_update: new Date().toISOString(),
136+
});
137+
138+
this.lastProcessedBlock = toBlock;
139+
this.logger.info('Blocks processed successfully', {
140+
fromBlock,
141+
toBlock,
142+
blockHash: block.hash,
143+
});
144+
} catch (error) {
145+
this.logger.error('Error in processing loop', {
146+
error,
147+
lastProcessedBlock: this.lastProcessedBlock,
148+
});
149+
await new Promise((resolve) =>
150+
setTimeout(resolve, this.config.pollInterval * 1000),
151+
);
152+
}
153+
}
154+
}
155+
156+
private async getCurrentBlock(): Promise<number> {
157+
return this.provider.getBlockNumber();
158+
}
159+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ICalculatorStrategy } from './interfaces/ICalculatorStrategy';
2+
import { BinaryEligibilityOracleEarningPowerCalculator } from './strategies/BinaryEligibilityOracleEarningPowerCalculator';
3+
import { CalculatorConfig, CalculatorStatus } from './interfaces/types';
4+
import { IDatabase } from '@/database';
5+
import { ethers } from 'ethers';
6+
7+
export class CalculatorWrapper implements ICalculatorStrategy {
8+
private strategy: ICalculatorStrategy;
9+
private isRunning: boolean;
10+
private lastProcessedBlock: number;
11+
12+
constructor(
13+
db: IDatabase,
14+
provider: ethers.Provider,
15+
config: CalculatorConfig = { type: 'binary' },
16+
) {
17+
// Initialize with BinaryEligibilityOracleEarningPowerCalculator strategy by default
18+
this.strategy = new BinaryEligibilityOracleEarningPowerCalculator(
19+
db,
20+
provider,
21+
);
22+
this.isRunning = false;
23+
this.lastProcessedBlock = 0;
24+
25+
// Can extend here to support other calculator types
26+
if (config.type !== 'binary') {
27+
throw new Error(`Calculator type ${config.type} not supported`);
28+
}
29+
}
30+
31+
async start(): Promise<void> {
32+
this.isRunning = true;
33+
}
34+
35+
async stop(): Promise<void> {
36+
this.isRunning = false;
37+
}
38+
39+
async getStatus(): Promise<CalculatorStatus> {
40+
return {
41+
isRunning: this.isRunning,
42+
lastProcessedBlock: this.lastProcessedBlock,
43+
};
44+
}
45+
46+
async getEarningPower(
47+
amountStaked: bigint,
48+
staker: string,
49+
delegatee: string,
50+
): Promise<bigint> {
51+
return this.strategy.getEarningPower(amountStaked, staker, delegatee);
52+
}
53+
54+
async getNewEarningPower(
55+
amountStaked: bigint,
56+
staker: string,
57+
delegatee: string,
58+
oldEarningPower: bigint,
59+
): Promise<[bigint, boolean]> {
60+
return this.strategy.getNewEarningPower(
61+
amountStaked,
62+
staker,
63+
delegatee,
64+
oldEarningPower,
65+
);
66+
}
67+
68+
async processScoreEvents(fromBlock: number, toBlock: number): Promise<void> {
69+
await this.strategy.processScoreEvents(fromBlock, toBlock);
70+
this.lastProcessedBlock = toBlock;
71+
}
72+
}

src/calculator/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Calculator Component
2+
3+
The Calculator component is responsible for monitoring and processing delegatee score updates from the Reward Calculator contract.
4+
5+
## Overview
6+
7+
The calculator monitors the `DelegateeScoreUpdated` events emitted by the Reward Calculator contract at `${CONFIG.monitor.rewardCalculatorAddress}`. These events are emitted whenever a delegatee's score is updated.
8+
9+
## Components
10+
11+
### CalculatorWrapper
12+
13+
- Main entry point for the calculator functionality
14+
- Implements strategy pattern to support different calculation methods
15+
- Manages calculator state (running/stopped)
16+
- Tracks last processed block
17+
18+
### BinaryEligibilityOracleEarningPowerCalculator
19+
20+
- Default calculator strategy implementation
21+
- Monitors `DelegateeScoreUpdated` events
22+
- Calculates earning power based on stake amount and delegatee score
23+
- Maintains a score cache for quick lookups
24+
25+
## Event Structure
26+
27+
```solidity
28+
event DelegateeScoreUpdated(
29+
address indexed delegatee, // The delegatee whose score was updated
30+
uint256 oldScore, // Previous score
31+
uint256 newScore // New score
32+
);
33+
```
34+
35+
## Database Schema
36+
37+
Score events are stored in the database with the following structure:
38+
39+
```typescript
40+
type ScoreEvent = {
41+
delegatee: string; // Delegatee address
42+
score: string; // Current score (stored as string due to bigint)
43+
block_number: number; // Block where the event occurred
44+
created_at?: string; // Timestamp of database entry
45+
updated_at?: string; // Last update timestamp
46+
};
47+
```
48+
49+
## Usage
50+
51+
### Running the Calculator
52+
53+
```bash
54+
# Run only the calculator
55+
npm run start:calculator
56+
57+
# Run with monitor
58+
npm run start:all
59+
```
60+
61+
### Environment Variables
62+
63+
- `REWARD_CALCULATOR_ADDRESS`: Address of the reward calculator contract
64+
- `START_BLOCK`: Starting block number for event processing
65+
- `POLL_INTERVAL`: How often to check for new events (in seconds)
66+
- `MAX_BLOCK_RANGE`: Maximum number of blocks to process in one batch
67+
- `CONFIRMATIONS`: Number of block confirmations to wait before processing
68+
69+
### Health Checks
70+
71+
The calculator includes built-in health monitoring:
72+
73+
- Processing status (running/stopped)
74+
- Last processed block
75+
- Processing lag (current chain height - last processed block)
76+
- Event processing statistics
77+
78+
## Error Handling
79+
80+
- Automatic retries for failed event processing
81+
- Checkpoint system to resume from last processed block
82+
- Detailed error logging for debugging
83+
- Score caching to reduce contract calls

src/calculator/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const REWARD_CALCULATOR_ABI = [
2+
'function getEarningPower(uint256 amountStaked, address staker, address delegatee) view returns (uint256)',
3+
'function getNewEarningPower(uint256 amountStaked, address staker, address delegatee, uint256 oldEarningPower) view returns (uint256, bool)',
4+
'event DelegateeScoreUpdated(address indexed delegatee, uint256 oldScore, uint256 newScore)',
5+
];

src/calculator/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './CalculatorWrapper';
2+
export * from './interfaces/ICalculatorStrategy';
3+
export * from './interfaces/types';
4+
export * from './strategies/BinaryEligibilityOracleEarningPowerCalculator';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface ICalculatorStrategy {
2+
getEarningPower(
3+
amountStaked: bigint,
4+
staker: string,
5+
delegatee: string,
6+
): Promise<bigint>;
7+
8+
getNewEarningPower(
9+
amountStaked: bigint,
10+
staker: string,
11+
delegatee: string,
12+
oldEarningPower: bigint,
13+
): Promise<[bigint, boolean]>;
14+
15+
processScoreEvents(fromBlock: number, toBlock: number): Promise<void>;
16+
}

0 commit comments

Comments
 (0)