forked from SmartDropLabs/smartdrop-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcoingecko.js
More file actions
95 lines (83 loc) · 2.71 KB
/
Copy pathcoingecko.js
File metadata and controls
95 lines (83 loc) · 2.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const axios = require('axios');
const config = require('../../config');
const logger = require('../../logger');
const { createCircuitBreaker } = require('./circuitBreaker');
const STELLAR_COINGECKO_MAP = {
XLM: 'stellar',
};
const circuit = createCircuitBreaker({
sourceName: 'coingecko',
cooldownMs: config.priceSources.circuitCooldownMs,
reminderIntervalMs: config.priceSources.circuitReminderIntervalMs,
});
let apiClient = null;
function getClient() {
if (!apiClient) {
const headers = { Accept: 'application/json' };
if (config.coingecko.apiKey) {
headers['x-cg-demo-api-key'] = config.coingecko.apiKey;
}
apiClient = axios.create({
baseURL: config.coingecko.baseUrl,
headers,
timeout: 10000,
});
}
return apiClient;
}
/**
* Whether this source can serve the given asset at all — distinct from a
* transient fetch failure. Callers (priceOracle's fetchFromAllSources)
* check this before ever invoking the circuit-breaker-wrapped fetchPrice,
* so a permanently-unsupported asset never counts toward this source's
* failure threshold (#130).
*/
function isSupported(assetCode) {
return Boolean(STELLAR_COINGECKO_MAP[assetCode]);
}
async function fetchPrice(assetCode) {
const coinId = STELLAR_COINGECKO_MAP[assetCode];
if (!coinId) {
logger.debug('Asset not supported by CoinGecko', { assetCode });
return null;
}
if (circuit.isOpen()) {
circuit.noteSkipped({ assetCode });
return null;
}
try {
const client = getClient();
const response = await client.get('/simple/price', {
params: {
ids: coinId,
vs_currencies: 'usd',
},
});
// A successful HTTP round-trip means any configured API key is valid,
// regardless of whether this particular coin had usable price data.
circuit.close();
const price = response.data[coinId]?.usd;
if (price === undefined || price === null) {
return null;
}
return price;
} catch (err) {
if (err.response?.status === 401) {
// Per CoinGecko's docs, 401 means a missing/invalid API key — a
// permanent misconfiguration, not something that self-heals on
// retry. Distinct from 403 (CDN/firewall block) and 429 (rate
// limit), neither of which indicate a bad key.
err.nonRetryable = true;
circuit.open({ assetCode });
logger.warn('CoinGecko authentication failed', { assetCode });
throw err;
}
if (err.response?.status === 429) {
logger.warn('CoinGecko rate limit hit', { assetCode });
} else {
logger.warn('CoinGecko price fetch failed', { assetCode, error: err.message });
}
return null;
}
}
module.exports = { fetchPrice, isSupported, getCircuitState: circuit.getState };