Skip to content

Commit 6b1ae51

Browse files
authored
Merge pull request #151 from nafiuishaaq/feat/network
Feat/network
2 parents 8bf42ae + 6d9b88b commit 6b1ae51

13 files changed

Lines changed: 411 additions & 28 deletions

apps/api-service/src/gas-estimation/__tests__/gas-estimation.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
66
import { getRepositoryToken } from '@nestjs/typeorm';
77
import { Repository } from 'typeorm';
88
import { DynamicPricingService } from '../services/dynamic-pricing.service';
9+
import { NetworkConfigService } from '../config/network-config.service';
910
import { NetworkMonitorService } from '../services/network-monitor.service';
1011
import { GasPriceHistoryService } from '../services/gas-price-history.service';
1112
import { GasEstimationController } from '../gas-estimation.controller';
@@ -23,6 +24,7 @@ describe('Dynamic Gas Estimation Engine', () => {
2324
controllers: [GasEstimationController],
2425
providers: [
2526
DynamicPricingService,
27+
NetworkConfigService,
2628
NetworkMonitorService,
2729
GasPriceHistoryService,
2830
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NetworkConfigService } from '../config/network-config.service';
2+
3+
describe('NetworkConfigService', () => {
4+
let service: NetworkConfigService;
5+
6+
beforeEach(() => {
7+
service = new NetworkConfigService();
8+
});
9+
10+
it('returns supported networks from a single source of truth', () => {
11+
const networks = service.getSupportedNetworks();
12+
13+
expect(networks.map((network) => network.chainId)).toEqual([
14+
'soroban-mainnet',
15+
'soroban-testnet',
16+
]);
17+
expect(networks[0]).toHaveProperty('chainName');
18+
expect(networks[0]).toHaveProperty('baseFeePerInstruction');
19+
});
20+
21+
it('resolves network metadata for a known chain', () => {
22+
expect(service.getNetworkConfig('soroban-mainnet')).toMatchObject({
23+
chainId: 'soroban-mainnet',
24+
chainName: 'Soroban Mainnet',
25+
});
26+
});
27+
28+
it('rejects unknown chains', () => {
29+
expect(() => service.getNetworkConfig('unknown-chain')).toThrow(
30+
'Unsupported chainId: unknown-chain',
31+
);
32+
});
33+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
export interface GasEstimationNetworkConfig {
4+
chainId: string;
5+
chainName: string;
6+
rpcUrl?: string;
7+
baseFeePerInstruction: number;
8+
historicalAverageGasPrice: number;
9+
defaultBlockGasLimit: number;
10+
baselineLoad: number;
11+
averageBlockTimeMs: number;
12+
}
13+
14+
@Injectable()
15+
export class NetworkConfigService {
16+
private readonly networks: GasEstimationNetworkConfig[] = [
17+
{
18+
chainId: 'soroban-mainnet',
19+
chainName: 'Soroban Mainnet',
20+
rpcUrl:
21+
process.env.GAS_ESTIMATION_SOROBAN_MAINNET_RPC_URL ||
22+
process.env.GAS_ESTIMATION_SOROBAN_RPC_URL,
23+
baseFeePerInstruction: 1000,
24+
historicalAverageGasPrice: 1000,
25+
defaultBlockGasLimit: 100000000,
26+
baselineLoad: 40,
27+
averageBlockTimeMs: 4000,
28+
},
29+
{
30+
chainId: 'soroban-testnet',
31+
chainName: 'Soroban Testnet',
32+
rpcUrl:
33+
process.env.GAS_ESTIMATION_SOROBAN_TESTNET_RPC_URL ||
34+
process.env.GAS_ESTIMATION_SOROBAN_RPC_URL,
35+
baseFeePerInstruction: 1000,
36+
historicalAverageGasPrice: 1000,
37+
defaultBlockGasLimit: 100000000,
38+
baselineLoad: 30,
39+
averageBlockTimeMs: 4500,
40+
},
41+
];
42+
43+
getSupportedNetworks(): GasEstimationNetworkConfig[] {
44+
return [...this.networks];
45+
}
46+
47+
getSupportedChainIds(): string[] {
48+
return this.networks.map((network) => network.chainId);
49+
}
50+
51+
getNetworkConfig(chainId: string): GasEstimationNetworkConfig {
52+
const network = this.networks.find((candidate) => candidate.chainId === chainId);
53+
54+
if (!network) {
55+
throw new Error(`Unsupported chainId: ${chainId}`);
56+
}
57+
58+
return network;
59+
}
60+
}

apps/api-service/src/gas-estimation/gas-estimation.controller.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
1313
import { DynamicPricingService } from './services/dynamic-pricing.service';
1414
import { GasPriceHistoryService } from './services/gas-price-history.service';
1515
import { NetworkMonitorService } from './services/network-monitor.service';
16+
import { NetworkConfigService } from './config/network-config.service';
1617
import {
1718
GetGasEstimateDto,
1819
GasEstimateResponseDto,
@@ -29,6 +30,7 @@ export class GasEstimationController {
2930
private dynamicPricingService: DynamicPricingService,
3031
private gasPriceHistoryService: GasPriceHistoryService,
3132
private networkMonitorService: NetworkMonitorService,
33+
private networkConfigService: NetworkConfigService,
3234
) {}
3335

3436
/**
@@ -290,7 +292,7 @@ export class GasEstimationController {
290292
status: 'healthy',
291293
timestamp: new Date().toISOString(),
292294
version: '1.0.0',
293-
supportedChains: ['soroban-mainnet', 'soroban-testnet'],
295+
supportedChains: this.networkConfigService.getSupportedChainIds(),
294296
};
295297
}
296298

apps/api-service/src/gas-estimation/gas-estimation.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { ScheduleModule } from '@nestjs/schedule';
44
import { GasEstimationController } from './gas-estimation.controller';
5+
import { NetworkConfigService } from './config/network-config.service';
56
import { NetworkMonitorService } from './services/network-monitor.service';
67
import { DynamicPricingService } from './services/dynamic-pricing.service';
78
import { GasPriceHistoryService } from './services/gas-price-history.service';
@@ -14,11 +15,13 @@ import { GasPriceHistory } from './entities/gas-price-history.entity';
1415
],
1516
controllers: [GasEstimationController],
1617
providers: [
18+
NetworkConfigService,
1719
NetworkMonitorService,
1820
DynamicPricingService,
1921
GasPriceHistoryService,
2022
],
2123
exports: [
24+
NetworkConfigService,
2225
NetworkMonitorService,
2326
DynamicPricingService,
2427
GasPriceHistoryService,

apps/api-service/src/gas-estimation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { GasEstimationModule } from './gas-estimation.module';
22
export { GasEstimationController } from './gas-estimation.controller';
3+
export * from './config/network-config.service';
34
export * from './services/network-monitor.service';
45
export * from './services/dynamic-pricing.service';
56
export * from './services/gas-price-history.service';

apps/api-service/src/gas-estimation/services/network-monitor.service.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable, Logger } from '@nestjs/common';
22
import { NetworkMetrics, GasPriceSnapshot } from '../interfaces/gas-price.interface';
33
import { Cron, CronExpression } from '@nestjs/schedule';
4+
import { NetworkConfigService } from '../config/network-config.service';
45

56
/**
67
* NetworkMonitorService
@@ -15,6 +16,8 @@ export class NetworkMonitorService {
1516
private metricsCache: Map<string, NetworkMetrics> = new Map();
1617
private priceSnapshotCache: Map<string, GasPriceSnapshot> = new Map();
1718

19+
constructor(private readonly networkConfigService: NetworkConfigService) {}
20+
1821
/**
1922
* Get current network metrics for a chain
2023
*/
@@ -51,8 +54,7 @@ export class NetworkMonitorService {
5154
@Cron(CronExpression.EVERY_10_SECONDS)
5255
async updateNetworkMetrics(): Promise<void> {
5356
try {
54-
// Update for all monitored chains
55-
const chainIds = ['soroban-testnet', 'soroban-mainnet'];
57+
const chainIds = this.networkConfigService.getSupportedChainIds();
5658

5759
for (const chainId of chainIds) {
5860
const metrics = await this.fetchNetworkMetricsFromRpc(chainId);
@@ -75,19 +77,22 @@ export class NetworkMonitorService {
7577
private async fetchNetworkMetricsFromRpc(chainId: string): Promise<NetworkMetrics> {
7678
// This would connect to actual Soroban RPC in production
7779
// For now, return mock data with some randomization to simulate network conditions
80+
const network = this.networkConfigService.getNetworkConfig(chainId);
7881

79-
const baseLoad = 35; // baseline network load
82+
const baseLoad = network.baselineLoad;
8083
const randomFluctuation = Math.random() * 30 - 15; // -15 to +15
8184
const congestionLevel = Math.max(0, Math.min(100, baseLoad + randomFluctuation));
8285

8386
return {
8487
congestionLevel,
8588
gasPoolUtilization: congestionLevel * 0.8,
86-
averageTransactionTime: 4000 + Math.random() * 2000, // 4-6 seconds
89+
averageTransactionTime: network.averageBlockTimeMs + Math.random() * 2000,
8790
pendingTransactionCount: Math.floor(congestionLevel * 10),
88-
lastBlockGasUsed: 75000000 + Math.floor(Math.random() * 25000000),
89-
lastBlockGasLimit: 100000000,
90-
historicalAverageGasPrice: 1000, // stroops per instruction
91+
lastBlockGasUsed:
92+
network.defaultBlockGasLimit * 0.75 +
93+
Math.floor(Math.random() * (network.defaultBlockGasLimit * 0.25)),
94+
lastBlockGasLimit: network.defaultBlockGasLimit,
95+
historicalAverageGasPrice: network.historicalAverageGasPrice,
9196
priceVolatility: congestionLevel * 0.5, // volatility increases with congestion
9297
};
9398
}
@@ -96,13 +101,14 @@ export class NetworkMonitorService {
96101
* Create a gas price snapshot based on current metrics
97102
*/
98103
private async createGasPriceSnapshot(chainId: string): Promise<GasPriceSnapshot> {
104+
const network = this.networkConfigService.getNetworkConfig(chainId);
99105
const metrics = await this.getNetworkMetrics(chainId);
100106

101107
// Calculate surge multiplier based on congestion
102108
const surgeMultiplier = this.calculateSurgeMultiplier(metrics.congestionLevel);
103109

104110
// Base price (stroops per instruction) - Soroban default
105-
const basePrice = 1000;
111+
const basePrice = network.baseFeePerInstruction;
106112
const recommendedFeeRate = basePrice * surgeMultiplier;
107113

108114
// Estimate price confidence (higher during stable, lower during volatile periods)
@@ -111,7 +117,7 @@ export class NetworkMonitorService {
111117
return {
112118
id: `snapshot-${chainId}-${Date.now()}`,
113119
chainId,
114-
chainName: chainId === 'soroban-mainnet' ? 'Soroban Mainnet' : 'Soroban Testnet',
120+
chainName: network.chainName,
115121
timestamp: new Date(),
116122
baseFeePerInstruction: basePrice,
117123
surgePriceMultiplier: surgeMultiplier,
@@ -153,6 +159,7 @@ export class NetworkMonitorService {
153159
chainId: string,
154160
hoursBack: number = 24,
155161
): Promise<GasPriceSnapshot[]> {
162+
const network = this.networkConfigService.getNetworkConfig(chainId);
156163
// In production, query database for historical snapshots
157164
const snapshots: GasPriceSnapshot[] = [];
158165
const now = Date.now();
@@ -164,15 +171,17 @@ export class NetworkMonitorService {
164171
snapshots.push({
165172
id: `hist-${chainId}-${i}`,
166173
chainId,
167-
chainName: chainId === 'soroban-mainnet' ? 'Soroban Mainnet' : 'Soroban Testnet',
174+
chainName: network.chainName,
168175
timestamp,
169-
baseFeePerInstruction: 1000,
176+
baseFeePerInstruction: network.baseFeePerInstruction,
170177
surgePriceMultiplier: this.calculateSurgeMultiplier(congestionAtTime),
171-
recommendedFeeRate: 1000 * this.calculateSurgeMultiplier(congestionAtTime),
178+
recommendedFeeRate:
179+
network.baseFeePerInstruction *
180+
this.calculateSurgeMultiplier(congestionAtTime),
172181
networkLoad: congestionAtTime,
173182
memoryPoolSize: 0,
174183
transactionCount: Math.floor(congestionAtTime * 10),
175-
averageBlockTime: 4000 + Math.random() * 2000,
184+
averageBlockTime: network.averageBlockTimeMs + Math.random() * 2000,
176185
volatilityIndex: congestionAtTime * 0.4,
177186
priceConfidence: Math.max(40, 100 - congestionAtTime),
178187
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { MonitoringHooksService } from '../services/monitoring-hooks.service';
2+
3+
describe('MonitoringHooksService', () => {
4+
let service: MonitoringHooksService;
5+
6+
beforeEach(() => {
7+
service = new MonitoringHooksService();
8+
});
9+
10+
it('increments counters using label-insensitive ordering', () => {
11+
service.incrementCounter('http_requests_total', 1, {
12+
statusCode: 200,
13+
method: 'GET',
14+
});
15+
service.incrementCounter('http_requests_total', 2, {
16+
method: 'GET',
17+
statusCode: 200,
18+
});
19+
20+
const snapshot = service.getSnapshot();
21+
22+
expect(snapshot.counters).toEqual([
23+
{
24+
name: 'http_requests_total',
25+
labels: { method: 'GET', statusCode: '200' },
26+
value: 3,
27+
},
28+
]);
29+
});
30+
31+
it('tracks gauges and histogram summaries', () => {
32+
service.setGauge('http_requests_in_flight', 2, { method: 'POST' });
33+
service.observeHistogram('http_request_duration_ms', 100, {
34+
endpoint: '/api/scanner',
35+
});
36+
service.observeHistogram('http_request_duration_ms', 300, {
37+
endpoint: '/api/scanner',
38+
});
39+
40+
const snapshot = service.getSnapshot();
41+
42+
expect(snapshot.gauges).toEqual([
43+
{
44+
name: 'http_requests_in_flight',
45+
labels: { method: 'POST' },
46+
value: 2,
47+
},
48+
]);
49+
expect(snapshot.histograms).toEqual([
50+
{
51+
name: 'http_request_duration_ms',
52+
labels: { endpoint: '/api/scanner' },
53+
count: 2,
54+
sum: 400,
55+
min: 100,
56+
max: 300,
57+
average: 200,
58+
},
59+
]);
60+
});
61+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Controller, Get } from '@nestjs/common';
2+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
3+
import { MonitoringHooksService } from '../services/monitoring-hooks.service';
4+
5+
@ApiTags('Monitoring')
6+
@Controller('metrics')
7+
export class MetricsController {
8+
constructor(private readonly monitoringHooksService: MonitoringHooksService) {}
9+
10+
@Get()
11+
@ApiOperation({ summary: 'Expose in-memory monitoring metrics snapshot' })
12+
getMetrics() {
13+
return this.monitoringHooksService.getSnapshot();
14+
}
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './performance-monitoring.module';
2+
export * from './services/performance-metric.service';
3+
export * from './services/monitoring-hooks.service';
4+
export * from './middleware/performance-logging.middleware';
5+
export * from './entities/api-performance-metric.entity';

0 commit comments

Comments
 (0)