diff --git a/.github/workflows/audit_and_lint.yml b/.github/workflows/audit_and_lint.yml index f0cd8d7..aeab9bb 100644 --- a/.github/workflows/audit_and_lint.yml +++ b/.github/workflows/audit_and_lint.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Audit dependencies run: audit-ci --critical --report-type full @@ -36,14 +36,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Set pnpm cache directory run: pnpm config set store-dir .pnpm-store continue-on-error: true - name: Setup cache - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # pin@v2 + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # pin@v2 with: path: | .pnpm-store diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index de0275b..d9bee23 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Initialize CodeQL uses: github/codeql-action/init@99c9897648dded3fe63d6f328c46089dd57735ca 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'