Skip to content

Commit f534e5f

Browse files
committed
feat: add token price service and cron job for price updates
1 parent 3b8293b commit f534e5f

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import cron from 'node-cron';
2+
import { TokenPriceService } from '../tokenPriceService';
3+
import { logger } from '../../utils/logger';
4+
5+
// Run every hour
6+
const CRON_SCHEDULE = '0 * * * *';
7+
8+
export const startTokenPriceCron = () => {
9+
logger.info('Starting token price cron job');
10+
11+
cron.schedule(CRON_SCHEDULE, async () => {
12+
logger.info('Running token price update cron job');
13+
try {
14+
await TokenPriceService.updateTokenPrices();
15+
logger.info('Token price update cron job completed successfully');
16+
} catch (error) {
17+
logger.error('Error in token price update cron job', {
18+
error: error.message,
19+
});
20+
}
21+
});
22+
};

src/services/tokenPriceService.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import axios from 'axios';
2+
import { TokenPriceHistory } from '../entities/tokenPriceHistory';
3+
import { logger } from '../utils/logger';
4+
import { QACC_DONATION_TOKEN_ADDRESS } from '../constants/qacc';
5+
import { Project } from '../entities/project';
6+
7+
interface GeckoTerminalResponse {
8+
data: {
9+
attributes: {
10+
price_usd: string;
11+
fdv_usd: string | null;
12+
};
13+
};
14+
}
15+
16+
export class TokenPriceService {
17+
private static readonly GECKO_TERMINAL_BASE_URL =
18+
'https://api.geckoterminal.com/api/v2/networks/polygon_pos/tokens';
19+
20+
static async fetchTokenPrice(tokenAddress: string): Promise<{
21+
price: number;
22+
priceUSD: number;
23+
marketCap: number | null;
24+
} | null> {
25+
try {
26+
const response = await axios.get<GeckoTerminalResponse>(
27+
`${this.GECKO_TERMINAL_BASE_URL}/${tokenAddress.toLowerCase()}/`,
28+
);
29+
30+
const priceUSD = parseFloat(response.data.data.attributes.price_usd);
31+
if (isNaN(priceUSD) || priceUSD === 0) {
32+
return null;
33+
}
34+
35+
// Calculate token price in POL by dividing USD price by POL price
36+
const polResponse = await axios.get<GeckoTerminalResponse>(
37+
`${this.GECKO_TERMINAL_BASE_URL}/${QACC_DONATION_TOKEN_ADDRESS}/`,
38+
);
39+
const polPriceUSD = parseFloat(
40+
polResponse.data.data.attributes.price_usd,
41+
);
42+
43+
if (isNaN(polPriceUSD) || polPriceUSD === 0) {
44+
return null;
45+
}
46+
47+
const marketCap = response.data.data.attributes.fdv_usd
48+
? parseFloat(response.data.data.attributes.fdv_usd)
49+
: null;
50+
51+
return {
52+
price: priceUSD / polPriceUSD, // Price in terms of POL tokens
53+
priceUSD,
54+
marketCap,
55+
};
56+
} catch (error) {
57+
logger.error('Error fetching token price from GeckoTerminal', {
58+
tokenAddress,
59+
error: error.message,
60+
});
61+
return null;
62+
}
63+
}
64+
65+
static async updateTokenPrices(): Promise<void> {
66+
try {
67+
// Get all tokens from the database
68+
const projects = await Project.find();
69+
const tokens = projects.flatMap(project => {
70+
const tokenAddress = project.abc.issuanceTokenAddress;
71+
const tokenTicker = project.abc.tokenTicker;
72+
return { tokenAddress, tokenTicker };
73+
});
74+
75+
for (const token of tokens) {
76+
const priceData = await this.fetchTokenPrice(token.tokenAddress);
77+
78+
if (priceData) {
79+
// Create new price history entry
80+
const priceHistory = new TokenPriceHistory();
81+
priceHistory.token = token.tokenTicker;
82+
priceHistory.tokenAddress = token.tokenAddress.toLowerCase();
83+
priceHistory.price = priceData.price;
84+
priceHistory.priceUSD = priceData.priceUSD;
85+
priceHistory.marketCap = priceData.marketCap ?? 0;
86+
87+
await priceHistory.save();
88+
logger.info('Token price updated successfully', {
89+
tokenTicker: token.tokenTicker,
90+
tokenAddress: token.tokenAddress,
91+
price: priceData.price,
92+
marketCap: priceData.marketCap,
93+
});
94+
} else {
95+
logger.info('Skipping token as no price data available', {
96+
token,
97+
});
98+
}
99+
}
100+
} catch (error) {
101+
logger.error('Error updating token prices', {
102+
error: error.message,
103+
});
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)