From 6228de45af2d2349c40be8c84e933ad957b6b92c Mon Sep 17 00:00:00 2001 From: Troy Date: Wed, 20 Nov 2024 15:51:11 -0700 Subject: [PATCH] add new endpoints --- src/routes/stats/perps-volume.js | 137 +++++++++++++++++++++++++++ src/routes/v3/top-asset.js | 155 +++++++++++++++++++++++++++++++ src/routes/v3/tvl.js | 118 +++++++++++++++++++++++ src/server.js | 10 ++ 4 files changed, 420 insertions(+) create mode 100644 src/routes/stats/perps-volume.js create mode 100644 src/routes/v3/top-asset.js create mode 100644 src/routes/v3/tvl.js diff --git a/src/routes/stats/perps-volume.js b/src/routes/stats/perps-volume.js new file mode 100644 index 0000000..3c8eb5f --- /dev/null +++ b/src/routes/stats/perps-volume.js @@ -0,0 +1,137 @@ +const express = require('express'); +const router = express.Router(); +const { log, postgresClient, getCache, setCache } = require('../../utils'); + +const cacheKey = 'perps-volume'; + +fetchDataFromPostgres(); +const cacheTime = + ((process.env.CACHE_TIME = + typeof process.env.CACHE_TIME === 'string' + ? parseInt(process.env.CACHE_TIME) + : process.env.CACHE_TIME) - + 30) * + 1000; +setInterval(fetchDataFromPostgres, cacheTime < 30000 ? 30000 : cacheTime); + +/** + * @openapi + * /stats/perps-volume: + * get: + * tags: + * - stats + * description: Returns total volume figures across all perps deployments. + * responses: + * 200: + * description: Successful response. + * content: + * application/json: + * schema: + * type: object + * properties: + * timestamp: + * type: string + * format: date-time + * example: '2024-05-23T14:00:00.000Z' + * volume_7d: + * type: number + * example: 123456789.123456789 + * volume_24h: + * type: number + * example: 12345678.123456789 + * 401: + * description: Unauthorized. + * 403: + * description: You have been banned by WAF. + * 429: + * description: Too many requests, you're being rate-limited. + * 5XX: + * description: Service unavailable. + * default: + * description: Unexpected error. + */ +router.get('/', async (req, res, next) => { + try { + log.debug('Checking cache..'); + const cachedResponse = await getCache(cacheKey); + if (cachedResponse) { + log.debug('Cache found'); + res.json(cachedResponse); + } else { + log.debug('Cache not found, executing..'); + const responseData = await fetchDataFromPostgres(); + res.json(responseData); + } + } catch (error) { + log.error(`[statsPerpsVolume] Error: ${error.message}`); + next(error); + } +}); + +module.exports = router; + +async function fetchDataFromPostgres() { + log.debug('[statsPerpsVolume] Fetching data from postgres..'); + const queryResult = await postgresClient.query( + `WITH volume AS ( + SELECT ts, + 'volume_24h' AS label, + volume + FROM prod_base_mainnet.fct_perp_stats_hourly_base_mainnet + WHERE ts >= NOW() - INTERVAL '24 HOURS' + UNION ALL + SELECT ts, + 'volume_24h' AS label, + volume + FROM prod_arbitrum_mainnet.fct_perp_stats_hourly_arbitrum_mainnet + WHERE ts >= NOW() - INTERVAL '24 HOURS' + UNION ALL + select ts, + 'volume_24h' AS label, + volume + from prod_optimism_mainnet.fct_v2_stats_hourly_optimism_mainnet + where ts >= NOW() - INTERVAL '24 HOURS' + UNION ALL + SELECT ts, + 'volume_7d' AS label, + volume + FROM prod_base_mainnet.fct_perp_stats_hourly_base_mainnet + WHERE ts >= NOW() - INTERVAL '7 DAYS' + UNION ALL + SELECT ts, + 'volume_7d' AS label, + volume + FROM prod_arbitrum_mainnet.fct_perp_stats_hourly_arbitrum_mainnet + WHERE ts >= NOW() - INTERVAL '7 DAYS' + UNION ALL + select ts, + 'volume_7d' AS label, + volume + from prod_optimism_mainnet.fct_v2_stats_hourly_optimism_mainnet + where ts >= NOW() - INTERVAL '7 DAYS' + ) + SELECT label, + round(SUM(volume), 2) AS volume + FROM volume + GROUP BY label;`, + ); + + const volume24h = queryResult.rows.find( + (row) => row.label === 'volume_24h', + ).volume; + const volume7d = queryResult.rows.find( + (row) => row.label === 'volume_7d', + ).volume; + + const volume24hUsd = parseFloat(volume24h); + const volume7dUsd = parseFloat(volume7d); + + const responseData = { + timestamp: new Date().toISOString(), + volume_24h: volume24hUsd, + volume_7d: volume7dUsd, + }; + log.debug('[statsPerpsVolume] Setting cache..'); + await setCache(cacheKey, responseData, 60); + return responseData; +} diff --git a/src/routes/v3/top-asset.js b/src/routes/v3/top-asset.js new file mode 100644 index 0000000..768acaa --- /dev/null +++ b/src/routes/v3/top-asset.js @@ -0,0 +1,155 @@ +const express = require('express'); +const router = express.Router(); +const { log, postgresClient, getCache, setCache } = require('../../utils'); + +const cacheKey = 'v3-top-asset'; + +fetchDataFromPostgres(); +const cacheTime = + ((process.env.CACHE_TIME = + typeof process.env.CACHE_TIME === 'string' + ? parseInt(process.env.CACHE_TIME) + : process.env.CACHE_TIME) - + 30) * + 1000; +setInterval(fetchDataFromPostgres, cacheTime < 30000 ? 30000 : cacheTime); + +/** + * @openapi + * /v3/top-asset: + * get: + * tags: + * - v3 + * description: Returns the current top performing asset and it's APR and APY estimated from past 7 day performance. + * responses: + * 200: + * description: Successful response. + * content: + * application/json: + * schema: + * type: object + * properties: + * timestamp: + * type: string + * format: date-time + * example: '2024-05-23T14:00:00.000Z' + * chain: + * type: string + * example: 'base' + * token_symbol: + * type: string + * example: 'USDC' + * apr: + * type: number + * example: 0.123456789 + * apy: + * type: number + * example: 0.123456789 + * 401: + * description: Unauthorized. + * 403: + * description: You have been banned by WAF. + * 429: + * description: Too many requests, you're being rate-limited. + * 5XX: + * description: Service unavailable. + * default: + * description: Unexpected error. + */ +router.get('/', async (req, res, next) => { + try { + log.debug('Checking cache..'); + const cachedResponse = await getCache(cacheKey); + if (cachedResponse) { + log.debug('Cache found'); + res.json(cachedResponse); + } else { + log.debug('Cache not found, executing..'); + const responseData = await fetchDataFromPostgres(); + res.json(responseData); + } + } catch (error) { + log.error(`[v3TopAsset] Error: ${error.message}`); + next(error); + } +}); + +module.exports = router; + +async function fetchDataFromPostgres() { + log.debug('[v3TopAsset] Fetching data from postgres..'); + const queryResult = await postgresClient.query( + `with base as ( + SELECT 'base' as chain, + t.token_symbol, + apr.apy_7d, + apr.apr_7d, + ROW_NUMBER() OVER ( + PARTITION BY collateral_type + ORDER BY ts DESC + ) AS rn + FROM prod_base_mainnet.fct_core_apr_base_mainnet apr + join prod_seeds.base_mainnet_tokens t on lower(apr.collateral_type) = lower(t.token_address) + ), + arb as ( + SELECT 'arbitrum' as chain, + t.token_symbol, + apr.apy_7d, + apr.apr_7d, + ROW_NUMBER() OVER ( + PARTITION BY collateral_type + ORDER BY ts DESC + ) AS rn + FROM prod_arbitrum_mainnet.fct_core_apr_arbitrum_mainnet apr + join prod_seeds.arbitrum_mainnet_tokens t on lower(apr.collateral_type) = lower(t.token_address) + ), + eth as ( + SELECT 'ethereum' as chain, + t.token_symbol, + apr.apy_7d, + apr.apr_7d, + ROW_NUMBER() OVER ( + PARTITION BY collateral_type + ORDER BY ts DESC + ) AS rn + FROM prod_eth_mainnet.fct_core_apr_eth_mainnet apr + join prod_seeds.eth_mainnet_tokens t on lower(apr.collateral_type) = lower(t.token_address) + ), + combined as ( + select * + from base + where rn = 1 + union all + select * + from arb + where rn = 1 + union all + select * + from eth + where rn = 1 + ) + select chain, + token_symbol, + round(apy_7d, 8) as apy, + round(apr_7d, 8) as apr + from combined + order by apy_7d desc + limit 1;`, + ); + + const chain = queryResult.rows[0].chain; + const tokenSymbol = queryResult.rows[0].token_symbol; + const apy = parseFloat(queryResult.rows[0].apy); + const apr = parseFloat(queryResult.rows[0].apr); + + const responseData = { + timestamp: new Date().toISOString(), + chain, + token_symbol: tokenSymbol, + apr, + apy, + }; + log.debug('[v3TopAsset] Setting cache..'); + await setCache(cacheKey, responseData, 60); + return responseData; +} diff --git a/src/routes/v3/tvl.js b/src/routes/v3/tvl.js new file mode 100644 index 0000000..8c36e27 --- /dev/null +++ b/src/routes/v3/tvl.js @@ -0,0 +1,118 @@ +const express = require('express'); +const router = express.Router(); +const { log, postgresClient, getCache, setCache } = require('../../utils'); + +const cacheKey = 'v3-tvl'; + +fetchDataFromPostgres(); +const cacheTime = + ((process.env.CACHE_TIME = + typeof process.env.CACHE_TIME === 'string' + ? parseInt(process.env.CACHE_TIME) + : process.env.CACHE_TIME) - + 30) * + 1000; +setInterval(fetchDataFromPostgres, cacheTime < 30000 ? 30000 : cacheTime); + +/** + * @openapi + * /v3/tvl: + * get: + * tags: + * - v3 + * description: Returns total value locked across all Synthetix V3 deployments. + * responses: + * 200: + * description: Successful response. + * content: + * application/json: + * schema: + * type: object + * properties: + * timestamp: + * type: string + * format: date-time + * example: '2024-05-23T14:00:00.000Z' + * tvl: + * type: number + * example: 123456789.123456789 + * 401: + * description: Unauthorized. + * 403: + * description: You have been banned by WAF. + * 429: + * description: Too many requests, you're being rate-limited. + * 5XX: + * description: Service unavailable. + * default: + * description: Unexpected error. + */ +router.get('/', async (req, res, next) => { + try { + log.debug('Checking cache..'); + const cachedResponse = await getCache(cacheKey); + if (cachedResponse) { + log.debug('Cache found'); + res.json(cachedResponse); + } else { + log.debug('Cache not found, executing..'); + const responseData = await fetchDataFromPostgres(); + res.json(responseData); + } + } catch (error) { + log.error(`[v3Tvl] Error: ${error.message}`); + next(error); + } +}); + +module.exports = router; + +async function fetchDataFromPostgres() { + log.debug('[v3Tvl] Fetching data from postgres..'); + const queryResult = await postgresClient.query( + `SELECT round(sum(collateral_value), 2) as tvl + FROM ( + SELECT 'base' as chain, + collateral_type, + collateral_value, + ts, + ROW_NUMBER() OVER ( + PARTITION BY collateral_type + ORDER BY ts DESC + ) AS rn + FROM prod_base_mainnet.fct_core_apr_base_mainnet + union ALL + SELECT 'ethereum' as chain, + collateral_type, + collateral_value, + ts, + ROW_NUMBER() OVER ( + PARTITION BY collateral_type + ORDER BY ts DESC + ) AS rn + FROM prod_eth_mainnet.fct_core_apr_eth_mainnet + union ALL + SELECT 'arbitrum' as chain, + collateral_type, + collateral_value, + ts, + ROW_NUMBER() OVER ( + PARTITION BY collateral_type + ORDER BY ts DESC + ) AS rn + FROM prod_arbitrum_mainnet.fct_core_apr_arbitrum_mainnet + ) sub + WHERE rn = 1;`, + ); + + const tvl = queryResult.rows[0].tvl; + const tvlUsd = parseFloat(tvl); + + const responseData = { + timestamp: new Date().toISOString(), + tvl: tvlUsd, + }; + log.debug('[v3Tvl] Setting cache..'); + await setCache(cacheKey, responseData, 60); + return responseData; +} diff --git a/src/server.js b/src/server.js index b64f441..c203949 100644 --- a/src/server.js +++ b/src/server.js @@ -132,6 +132,16 @@ redisClient.on('ready', () => { const v3SnaxVotesRouter = require('./routes/v3/snax/votes.js'); app.use('/v3/snax/votes', v3SnaxVotesRouter); + const v3TvlRouter = require('./routes/v3/tvl.js'); + app.use('/v3/tvl', v3TvlRouter); + + const v3TopAsset = require('./routes/v3/top-asset.js'); + app.use('/v3/top-asset', v3TopAsset); + + // stats + const statsPerpsVolumeRouter = require('./routes/stats/perps-volume.js'); + app.use('/stats/perps-volume', statsPerpsVolumeRouter); + log.debug('[Express] Starting server..'); const port = typeof process.env.API_PORT === 'string'