Skip to content

Commit f342405

Browse files
authored
Merge pull request #118 from OsejiFabian/feature/implement-four-issues
feat: Implement four issues - leaderboard, token verification, networ…
2 parents 6992d90 + 412e4b4 commit f342405

9 files changed

Lines changed: 239 additions & 2 deletions

File tree

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,67 @@ curl http://localhost:3000/api/v1/stats/tvl?format=short
9090
"lastUpdated": "2026-03-19T00:00:00Z"
9191
}
9292
```
93+
94+
### Analytics Endpoints
95+
96+
#### `GET /api/v1/analytics/leaderboard`
97+
98+
Get the top traders leaderboard sorted by 7-day trading volume.
99+
100+
**Response Format:**
101+
```json
102+
[
103+
{
104+
"rank": 1,
105+
"walletAddress": "0x742d...8b4c",
106+
"volumeUSD": 850000
107+
},
108+
{
109+
"rank": 2,
110+
"walletAddress": "0x8f3a...2d1e",
111+
"volumeUSD": 720000
112+
}
113+
]
114+
```
115+
116+
### Token Verification
117+
118+
#### `GET /api/v1/tokens/verify/:address`
119+
120+
Check if a Stellar contract address is officially verified and safe to trade.
121+
122+
**Response Format:**
123+
```json
124+
{
125+
"isVerified": true,
126+
"riskLevel": "LOW"
127+
}
128+
```
129+
130+
**Parameters:**
131+
- `address` (path): Stellar contract address to verify
132+
133+
### Network Fees
134+
135+
#### `GET /api/v1/network/fees`
136+
137+
Get current Stellar network fee estimates.
138+
139+
**Response Format:**
140+
```json
141+
{
142+
"success": true,
143+
"data": {
144+
"baseFee": 100,
145+
"priorityFee": 500,
146+
"estimatedTotal": 600
147+
},
148+
"timestamp": "2024-01-15T10:30:00.000Z"
149+
}
150+
```
151+
152+
**Note:** This endpoint includes a 30-second cache header for performance optimization.
153+
154+
### Background Jobs
155+
156+
The API includes a background indexer job that runs every 5 minutes to sync blockchain data. This is automatically initialized when the server starts and logs "Syncing Blockchain Data..." during each run.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"express-rate-limit": "^8.3.1",
4747
"ioredis": "^5.10.1",
4848
"jsonwebtoken": "^9.0.3",
49+
"node-cron": "^3.0.3",
4950
"pg": "^8.11.0",
5051
"prisma": "^5.19.1",
5152
"rate-limit-redis": "^4.3.1",
@@ -60,6 +61,7 @@
6061
"@types/express": "^5.0.6",
6162
"@types/jest": "^30.0.0",
6263
"@types/node": "^25.2.3",
64+
"@types/node-cron": "^3.0.11",
6365
"@types/pdf-parse": "^1.1.5",
6466
"jest": "^30.2.0",
6567
"ts-jest": "^29.4.6",

src/analytics/analytics.controller.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Controller, Get, HttpStatus, Query } from '@nestjs/common';
22
import { ApiOperation, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger';
3-
import { AnalyticsService, VolumeData, ImpermanentLossData } from './analytics.service';
3+
import { AnalyticsService, VolumeData, ImpermanentLossData, LeaderboardEntry } from './analytics.service';
44

55
@ApiTags('Analytics')
66
@Controller('api/v1/analytics')
@@ -77,4 +77,25 @@ export class AnalyticsController {
7777
timestamp: new Date().toISOString(),
7878
};
7979
}
80+
81+
@Get('leaderboard')
82+
@ApiOperation({ summary: 'Get top volume traders leaderboard' })
83+
@ApiResponse({
84+
status: 200,
85+
description: 'Successfully retrieved leaderboard data',
86+
schema: {
87+
type: 'array',
88+
items: {
89+
type: 'object',
90+
properties: {
91+
rank: { type: 'number', example: 1 },
92+
walletAddress: { type: 'string', example: '0x742d...8b4c' },
93+
volumeUSD: { type: 'number', example: 850000 },
94+
},
95+
},
96+
},
97+
})
98+
getLeaderboard(): LeaderboardEntry[] {
99+
return this.analyticsService.generateLeaderboard();
100+
}
80101
}

src/analytics/analytics.service.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export interface ImpermanentLossData {
1111
impermanentLossPercentage: number;
1212
}
1313

14+
export interface LeaderboardEntry {
15+
walletAddress: string;
16+
volumeUSD: number;
17+
rank: number;
18+
}
19+
1420
@Injectable()
1521
export class AnalyticsService {
1622
generateMockVolumeData(): VolumeData[] {
@@ -56,4 +62,31 @@ export class AnalyticsService {
5662
impermanentLossPercentage,
5763
};
5864
}
65+
66+
generateLeaderboard(): LeaderboardEntry[] {
67+
// Generate dummy wallet addresses (truncated for privacy)
68+
const dummyWallets = [
69+
'0x742d...8b4c',
70+
'0x8f3a...2d1e',
71+
'0x1a9c...5f7b',
72+
'0x6e2d...9a3c',
73+
'0x4b8f...1e2d',
74+
'0x9c3a...7f5b',
75+
'0x2d8f...4c1e',
76+
'0x5a7b...9d2f',
77+
'0x8e1c...3a6b',
78+
'0x3f9d...2e8c'
79+
];
80+
81+
// Generate realistic trading volumes (sorted in descending order)
82+
const baseVolumes = [850000, 720000, 650000, 580000, 490000, 420000, 380000, 310000, 270000, 220000];
83+
84+
const leaderboard: LeaderboardEntry[] = dummyWallets.map((wallet, index) => ({
85+
walletAddress,
86+
volumeUSD: baseVolumes[index] + Math.round(Math.random() * 50000 - 25000),
87+
rank: index + 1
88+
}));
89+
90+
return leaderboard;
91+
}
5992
}

src/jobs/indexer.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as cron from 'node-cron';
2+
3+
export class IndexerJob {
4+
constructor() {
5+
this.initializeJobs();
6+
}
7+
8+
private initializeJobs() {
9+
// Schedule a job to run every 5 minutes
10+
cron.schedule('*/5 * * * *', () => {
11+
console.log('Syncing Blockchain Data...');
12+
this.syncBlockchainData();
13+
});
14+
15+
console.log('Background indexer jobs initialized');
16+
}
17+
18+
private syncBlockchainData() {
19+
// Simulate blockchain data syncing
20+
console.log(`[${new Date().toISOString()}] Starting blockchain data sync...`);
21+
22+
// Simulate some work
23+
setTimeout(() => {
24+
console.log(`[${new Date().toISOString()}] Blockchain data sync completed`);
25+
}, 2000);
26+
}
27+
}

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ async function bootstrap() {
7676

7777
await app.listen(3000);
7878
console.log('Application is running on: http://localhost:3000');
79+
80+
// Initialize background jobs
81+
new IndexerJob();
7982
}
8083

8184
bootstrap();

src/network/network.controller.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Controller, Get, Res, HttpStatus } from '@nestjs/common';
2+
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
3+
import { Response } from 'express';
4+
5+
@ApiTags('Network')
6+
@Controller('api/v1/network')
7+
export class NetworkController {
8+
@Get('fees')
9+
@ApiOperation({ summary: 'Get estimated network gas fees', description: 'Get current Stellar network fee estimates in stroops' })
10+
@ApiResponse({
11+
status: 200,
12+
description: 'Successfully retrieved network fee estimates',
13+
schema: {
14+
type: 'object',
15+
properties: {
16+
success: { type: 'boolean', example: true },
17+
data: {
18+
type: 'object',
19+
properties: {
20+
baseFee: { type: 'number', example: 100 },
21+
priorityFee: { type: 'number', example: 500 },
22+
estimatedTotal: { type: 'number', example: 600 }
23+
}
24+
},
25+
timestamp: { type: 'string', example: '2024-01-15T10:30:00.000Z' }
26+
}
27+
}
28+
})
29+
getNetworkFees(@Res() res: Response) {
30+
// Generate realistic mock Stellar network fees
31+
const fees = {
32+
baseFee: 100, // Base fee in stroops (0.00001 XLM)
33+
priorityFee: Math.floor(Math.random() * 1000) + 200, // Priority fee between 200-1200 stroops
34+
estimatedTotal: 0 // Will be calculated
35+
};
36+
37+
fees.estimatedTotal = fees.baseFee + fees.priorityFee;
38+
39+
const response = {
40+
success: true,
41+
data: fees,
42+
timestamp: new Date().toISOString()
43+
};
44+
45+
// Set cache header for 30 seconds
46+
res.set('Cache-Control', 'public, max-age=30');
47+
res.status(HttpStatus.OK).json(response);
48+
}
49+
}

src/network/network.module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Module } from '@nestjs/common';
2+
import { NetworkController } from './network.controller';
3+
4+
@Module({
5+
controllers: [NetworkController],
6+
})
7+
export class NetworkModule {}

src/tokens/tokens.controller.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
33
import { Response } from 'express';
44

55
@ApiTags('Tokens')
6-
@Controller('tokens')
6+
@Controller('api/v1/tokens')
77
export class TokensController {
88
private cachedTokens: string[] | null = null;
99
private cacheTimestamp: number = 0;
1010
private readonly CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
1111

12+
// Hardcoded verified Stellar contract addresses
13+
private readonly verifiedAddresses = [
14+
'GBBDN5LQJ3FOJJNGZAKDRJMBJN5P6LIXZ6NFUGZU6J3E3MNJNGHG35TV',
15+
'GDTNJZK5MHCJKGV6O7G7LXKXVYJU2R2H5N5JQ6LQ5J3E3MNJNGHG35TV',
16+
'GD5DJQD6K3XV5FJ2RQ7J7LXKXVYJU2R2H5N5JQ6LQ5J3E3MNJNGHG35TV',
17+
'GA6HCMBLTZS5VYYBCATRBRZB5J2J2F2ZQ5JQ6LQ5J3E3MNJNGHG35TV'
18+
];
19+
1220
@Get()
1321
@ApiOperation({ summary: 'Search for tokens by symbol', description: 'Search for tokens using a symbol query parameter' })
1422
@ApiQuery({ name: 'search', required: false, description: 'Token symbol to search for' })
@@ -80,4 +88,27 @@ export class TokensController {
8088
timestamp: new Date().toISOString()
8189
};
8290
}
91+
92+
@Get('verify/:address')
93+
@ApiOperation({ summary: 'Verify if a token contract address is whitelisted', description: 'Check if a Stellar contract address is officially verified and safe to trade' })
94+
@ApiResponse({
95+
status: 200,
96+
description: 'Token verification result',
97+
schema: {
98+
type: 'object',
99+
properties: {
100+
isVerified: { type: 'boolean', example: true },
101+
riskLevel: { type: 'string', example: 'LOW' }
102+
}
103+
}
104+
})
105+
verifyToken(@Param('address') address: string) {
106+
const isVerified = this.verifiedAddresses.includes(address);
107+
const riskLevel = isVerified ? 'LOW' : 'HIGH';
108+
109+
return {
110+
isVerified,
111+
riskLevel
112+
};
113+
}
83114
}

0 commit comments

Comments
 (0)