Skip to content

Commit 07f5bf3

Browse files
authored
Merge pull request #14 from withtally/component/batch-executor
Component/batch executor
2 parents bb83d26 + 0b5ba59 commit 07f5bf3

File tree

28 files changed

+1428
-26
lines changed

28 files changed

+1428
-26
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,10 @@ OPERATOR_PRIVATE_KEY=
2323

2424
# API Keys
2525
ETHERSCAN_API_KEY=
26+
27+
# CoinMarketCap API
28+
COINMARKETCAP_API_KEY=your-api-key-here
29+
30+
# Profitability Settings
31+
TIP_RECEIVER_ADDRESS=0x...
32+
PRICE_FEED_TOKEN_ADDRESS=0x...

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Staker Profitability Monitor
2+
3+
A service that monitors staking deposits and executes profitable earning power bump transactions.
4+
5+
## Setup
6+
7+
1. Install dependencies:
8+
9+
```bash
10+
npm install
11+
```
12+
13+
2. Configure environment variables:
14+
Copy `.env.example` to `.env` and fill in the required values:
15+
16+
- `RPC_URL`: Your Ethereum RPC URL (e.g. from Alchemy or Infura)
17+
- `STAKER_CONTRACT_ADDRESS`: The address of the Staker contract
18+
- `PRIVATE_KEY`: Your wallet's private key (without 0x prefix)
19+
20+
## Running the Service
21+
22+
1. Build the TypeScript code:
23+
24+
```bash
25+
npm run build
26+
```
27+
28+
2. Start the service:
29+
30+
```bash
31+
npm start
32+
```
33+
34+
The service will:
35+
36+
- Monitor deposits in the database
37+
- Analyze profitability of earning power bumps
38+
- Execute profitable transactions automatically
39+
- Log all activities to the console
40+
41+
To stop the service gracefully, press Ctrl+C.
42+
43+
## Configuration
44+
45+
The service can be configured through the following parameters in `main.ts`:
46+
47+
- Poll interval: How often to check for profitable opportunities (default: 15s)
48+
- Minimum profit margin: Minimum expected profit to execute a transaction (default: 0.001 ETH)
49+
- Gas price buffer: Additional buffer on gas price estimates (default: 20%)
50+
- Wallet minimum balance: Minimum wallet balance to maintain (default: 0.1 ETH)
51+
- Maximum pending transactions: Maximum number of pending transactions (default: 5)
52+
- Gas boost percentage: Percentage to boost gas price by (default: 10%)
53+
- Concurrent transactions: Number of transactions to execute concurrently (default: 3)
54+
55+
## Database
56+
57+
The service uses a JSON file database by default (`staker-monitor-db.json`). This can be changed to use Supabase by modifying the database configuration in `main.ts`.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
"license": "ISC",
2222
"dependencies": {
2323
"@supabase/supabase-js": "^2.48.1",
24+
"axios": "^1.7.9",
2425
"dotenv": "^16.4.5",
25-
"ethers": "^6.11.1"
26+
"ethers": "^6.11.1",
27+
"uuid": "^11.0.5"
2628
},
2729
"devDependencies": {
2830
"@types/node": "^20.17.17",
31+
"@types/uuid": "^10.0.0",
2932
"@typescript-eslint/eslint-plugin": "^7.1.0",
3033
"@typescript-eslint/parser": "^7.1.0",
3134
"eslint": "^8.57.0",

src/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ export const CONFIG = {
4343
confirmations: parseInt(process.env.CONFIRMATIONS || '20'),
4444
healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '60'),
4545
},
46+
priceFeed: {
47+
coinmarketcap: {
48+
apiKey: process.env.COINMARKETCAP_API_KEY || '',
49+
baseUrl: 'https://pro-api.coinmarketcap.com/v2',
50+
timeout: 5000,
51+
retries: 3,
52+
},
53+
},
54+
profitability: {
55+
minProfitMargin: ethers.parseEther('0.001'), // 0.001 tokens minimum profit
56+
gasPriceBuffer: 20, // 20% buffer for gas price volatility
57+
maxBatchSize: 10,
58+
defaultTipReceiver: process.env.TIP_RECEIVER_ADDRESS || '',
59+
priceFeed: {
60+
tokenAddress: process.env.PRICE_FEED_TOKEN_ADDRESS || '',
61+
cacheDuration: 10 * 60 * 1000, // 10 minutes
62+
},
63+
},
4664
} as const;
4765

4866
// Helper to create provider

src/database/DatabaseWrapper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class DatabaseWrapper implements IDatabase {
2323
deleteDeposit: supabaseDb.deleteDeposit,
2424
getDeposit: supabaseDb.getDeposit,
2525
getDepositsByDelegatee: supabaseDb.getDepositsByDelegatee,
26+
getAllDeposits: supabaseDb.getAllDeposits,
2627
updateCheckpoint: supabaseCheckpoints.updateCheckpoint,
2728
getCheckpoint: supabaseCheckpoints.getCheckpoint,
2829
createScoreEvent: supabaseScoreEvents.createScoreEvent,
@@ -62,6 +63,14 @@ export class DatabaseWrapper implements IDatabase {
6263
return this.db.getDepositsByDelegatee(delegateeAddress);
6364
}
6465

66+
async getAllDeposits(): Promise<Deposit[]> {
67+
if (this.db instanceof JsonDatabase) {
68+
return Object.values(this.db.data.deposits);
69+
} else {
70+
return await supabaseDb.getAllDeposits();
71+
}
72+
}
73+
6574
// Checkpoints
6675
async updateCheckpoint(checkpoint: ProcessingCheckpoint): Promise<void> {
6776
return this.db.updateCheckpoint(checkpoint);

src/database/interfaces/IDatabase.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface IDatabase {
55
createDeposit(deposit: Deposit): Promise<void>;
66
getDeposit(depositId: string): Promise<Deposit | null>;
77
getDepositsByDelegatee(delegateeAddress: string): Promise<Deposit[]>;
8+
getAllDeposits(): Promise<Deposit[]>;
89
updateDeposit(
910
depositId: string,
1011
update: Partial<Omit<Deposit, 'deposit_id'>>,

src/database/interfaces/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export interface Deposit {
22
deposit_id: string;
33
owner_address: string;
4-
amount: number;
5-
delegatee_address: string;
4+
amount: string;
5+
delegatee_address: string | null;
66
created_at?: string;
77
updated_at?: string;
88
}

src/database/json/JsonDatabase.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ConsoleLogger, Logger } from '@/monitor/logging';
77
export class JsonDatabase implements IDatabase {
88
private dbPath: string;
99
private logger: Logger;
10-
private data: {
10+
public data: {
1111
deposits: Record<string, Deposit>;
1212
checkpoints: Record<string, ProcessingCheckpoint>;
1313
score_events: Record<string, Record<number, ScoreEvent>>;
@@ -94,6 +94,10 @@ export class JsonDatabase implements IDatabase {
9494
);
9595
}
9696

97+
async getAllDeposits(): Promise<Deposit[]> {
98+
return Object.values(this.data.deposits);
99+
}
100+
97101
// Checkpoints
98102
async updateCheckpoint(checkpoint: ProcessingCheckpoint): Promise<void> {
99103
this.data.checkpoints[checkpoint.component_type] = {

src/database/supabase/deposits.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,9 @@ export async function getDepositsByDelegatee(
4747
if (error) throw error;
4848
return data;
4949
}
50+
51+
export async function getAllDeposits(): Promise<Deposit[]> {
52+
const { data, error } = await supabase.from('deposits').select();
53+
if (error) throw error;
54+
return data || [];
55+
}

src/executor/ExecutorWrapper.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { ethers } from 'ethers';
2+
import { BaseExecutor } from './strategies/BaseExecutor';
3+
import {
4+
ExecutorConfig,
5+
QueuedTransaction,
6+
QueueStats,
7+
TransactionReceipt,
8+
} from './interfaces/types';
9+
import { DEFAULT_EXECUTOR_CONFIG } from './constants';
10+
import { ProfitabilityCheck } from '@/profitability/interfaces/types';
11+
12+
export class ExecutorWrapper {
13+
private executor: BaseExecutor;
14+
15+
constructor(
16+
stakerContract: ethers.Contract,
17+
provider: ethers.Provider,
18+
config: Partial<ExecutorConfig> = {},
19+
) {
20+
// Merge provided config with defaults
21+
const fullConfig: ExecutorConfig = {
22+
...DEFAULT_EXECUTOR_CONFIG,
23+
...config,
24+
wallet: {
25+
...DEFAULT_EXECUTOR_CONFIG.wallet,
26+
...config.wallet,
27+
},
28+
};
29+
30+
this.executor = new BaseExecutor(stakerContract, provider, fullConfig);
31+
}
32+
33+
/**
34+
* Start the executor
35+
*/
36+
async start(): Promise<void> {
37+
await this.executor.start();
38+
}
39+
40+
/**
41+
* Stop the executor
42+
*/
43+
async stop(): Promise<void> {
44+
await this.executor.stop();
45+
}
46+
47+
/**
48+
* Get the current status of the executor
49+
*/
50+
async getStatus(): Promise<{
51+
isRunning: boolean;
52+
walletBalance: bigint;
53+
pendingTransactions: number;
54+
queueSize: number;
55+
}> {
56+
return this.executor.getStatus();
57+
}
58+
59+
/**
60+
* Queue a transaction for execution
61+
*/
62+
async queueTransaction(
63+
depositId: bigint,
64+
profitability: ProfitabilityCheck,
65+
): Promise<QueuedTransaction> {
66+
return this.executor.queueTransaction(depositId, profitability);
67+
}
68+
69+
/**
70+
* Get statistics about the transaction queue
71+
*/
72+
async getQueueStats(): Promise<QueueStats> {
73+
return this.executor.getQueueStats();
74+
}
75+
76+
/**
77+
* Get a specific transaction by ID
78+
*/
79+
async getTransaction(id: string): Promise<QueuedTransaction | null> {
80+
return this.executor.getTransaction(id);
81+
}
82+
83+
/**
84+
* Get transaction receipt
85+
*/
86+
async getTransactionReceipt(
87+
hash: string,
88+
): Promise<TransactionReceipt | null> {
89+
return this.executor.getTransactionReceipt(hash);
90+
}
91+
92+
/**
93+
* Transfer accumulated tips to the configured receiver
94+
*/
95+
async transferOutTips(): Promise<TransactionReceipt | null> {
96+
return this.executor.transferOutTips();
97+
}
98+
99+
/**
100+
* Clear the transaction queue
101+
*/
102+
async clearQueue(): Promise<void> {
103+
await this.executor.clearQueue();
104+
}
105+
}

0 commit comments

Comments
 (0)