Skip to content

Commit

Permalink
Add Netlify function tokenPrices, route request to CoinGecko, cache t…
Browse files Browse the repository at this point in the history
…oken prices through Netlify blob
  • Loading branch information
mudrila committed Mar 4, 2024
1 parent 324edbe commit e1f77e1
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ NEXT_PUBLIC_SITE_URL="https://app.dev.fractalframework.xyz/"
# WalletConnect Cloud Project ID
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=""
# CoinGecko API key
NEXT_PUBLIC_COINGECKO_API_KEY=""
COINGECKO_API_KEY=""
# Shutter Public Key
NEXT_PUBLIC_SHUTTER_EON_PUBKEY=0x0e6493bbb4ee8b19aa9b70367685049ff01dc9382c46aed83f8bc07d2a5ba3e6030bd83b942c1fd3dff5b79bef3b40bf6b666e51e7f0be14ed62daaffad47435265f5c9403b1a801921981f7d8659a9bd91fe92fb1cf9afdb16178a532adfaf51a237103874bb03afafe9cab2118dae1be5f08a0a28bf488c1581e9db4bc23ca
84 changes: 84 additions & 0 deletions netlify/functions/tokensPrices.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { getStore } from '@netlify/blobs';

type TokenPriceMetadata = {
expiration: number;
};

export default async function getTokenprices(request: Request) {
const store = getStore('fractal-token-prices-store');
const tokensString = new URL(request.url).searchParams.get('tokens');

if (!tokensString) {
Response.json({ error: 'Tokens to request were not provided' });
}
const tokens = tokensString!.split(',');
try {
const now = new Date().getTime();
const cachedPrices = await Promise.all(
tokens.map(tokenAddress => store.getWithMetadata(tokenAddress, { type: 'json' }))
);
const expiredPrices: string[] = [];
const cachedUnexpiredPrices = cachedPrices
.filter(tokenPrice => {
if (tokenPrice && (tokenPrice?.metadata as any as TokenPriceMetadata).expiration <= now) {
expiredPrices.push(tokenPrice.data.tokenAddress);
return false;
}
return tokenPrice;
})
.map(tokenPrice => ({
tokenAddress: tokenPrice?.data.tokenAddress,
price: tokenPrice?.data.price,
}));
const nonCachedTokensAddresses = tokens.filter(
address => !cachedUnexpiredPrices.find(tokenPrice => tokenPrice.tokenAddress === address)
);
const responseBody: Record<string, number> = {};
cachedUnexpiredPrices.forEach(tokenPrice => {
responseBody[tokenPrice.tokenAddress] = tokenPrice.price;
});
if (nonCachedTokensAddresses.length === 0) {
return Response.json({ data: responseBody });
}
if (!process.env.COINGECKO_API_KEY) {
console.error('CoinGecko API key is missing');
return { error: 'Unknown error while fetching prices' };
}
const PUBLIC_DEMO_API_BASE_URL = 'https://api.coingecko.com/api/v3/';
const AUTH_QUERY_PARAM = `?x_cg_demo_api_key=${process.env.COINGECKO_API_KEY}`;
const tokenPricesUrl = `${PUBLIC_DEMO_API_BASE_URL}simple/token_price/ethereum/${AUTH_QUERY_PARAM}&vs_currencies=usd&contract_addresses=${nonCachedTokensAddresses.join(
','
)}`;

const tokenPricesResponse = await fetch(tokenPricesUrl);
const tokenPricesResponseJson = await tokenPricesResponse.json();
const tokenPriceMetadata = { metadata: { expiration: now + 1000 * 60 * 30 } };
Object.keys(tokenPricesResponseJson).forEach(tokenAddress => {
const price = tokenPricesResponseJson[tokenAddress];
responseBody[tokenAddress] = price;
store.setJSON(tokenAddress, { tokenAddress, price }, tokenPriceMetadata);
});

const ethAsset = nonCachedTokensAddresses.find(token => token === 'ethereum');
if (ethAsset) {
// Unfortunately, there's no way avoiding 2 requests. We either need to fetch asset IDs from CoinGecko for given token contract addresses
// And then use this endpoint to get all the prices. But that brings us way more bandwidth
// Or, we are doing this "hardcoded" call for ETH price. But our request for token prices simpler.
const ethPriceUrl = `${PUBLIC_DEMO_API_BASE_URL}simple/price${AUTH_QUERY_PARAM}&ids=ethereum&vs_currencies=usd`;
const ethPriceResponse = await fetch(ethPriceUrl);
const ethPriceResponseJson = await ethPriceResponse.json();
store.setJSON(
'ethereum',
{ tokenAddress: 'ethereum', price: ethPriceResponseJson.ethereum },
tokenPriceMetadata
);
responseBody.ethereum = ethPriceResponseJson.ethereum;
}
return Response.json({ data: responseBody });
} catch (e) {
console.error('Error while fetching prices', e);
return Response.json({
error: 'Unknown error while fetching prices',
});
}
}
80 changes: 80 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"@fractal-framework/fractal-contracts": "^0.4.0",
"@graphprotocol/client-apollo": "^1.0.16",
"@lido-sdk/contracts": "^3.0.2",
"@netlify/blobs": "^6.5.0",
"@netlify/functions": "^2.6.0",
"@rainbow-me/rainbowkit": "^1.3.3",
"@safe-global/safe-deployments": "^1.23.0",
"@safe-global/safe-ethers-lib": "^1.9.2",
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/DAOTreasury/hooks/useFormatCoins.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SafeBalanceUsdResponse } from '@safe-global/safe-service-client';
import { BigNumber, ethers } from 'ethers';
import { useEffect, useState } from 'react';
import useCoinGeckoAPI from '../../../../providers/App/hooks/useCoinGeckoAPI';
import useCoinGeckoAPI from '../../../../providers/App/hooks/usePriceAPI';
import { useNetworkConfig } from '../../../../providers/NetworkConfig/NetworkConfigProvider';
import { formatCoin, formatUSD } from '../../../../utils/numberFormats';

Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/en/treasury.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
"stake": "Stake",
"unstake": "Unstake",
"claimUnstakedETH": "Claim ETH",
"nonClaimableYet": "Your ETH cannot yet be claimed from Lido"
"nonClaimableYet": "Your ETH cannot yet be claimed from Lido",
"tokenPriceFetchingError": "There was an error obtaining token prices. Please, try visiting Treasury page later."
}
41 changes: 0 additions & 41 deletions src/providers/App/hooks/useCoinGeckoAPI.ts

This file was deleted.

46 changes: 46 additions & 0 deletions src/providers/App/hooks/usePriceAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { SafeBalanceUsdResponse } from '@safe-global/safe-service-client';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { logError } from '../../../helpers/errorLogging';
import { useNetworkConfig } from '../../NetworkConfig/NetworkConfigProvider';

export default function usePriceAPI() {
const { chainId } = useNetworkConfig();
const { t } = useTranslation('treasury');

const getTokenPrices = useCallback(
async (tokens: SafeBalanceUsdResponse[]) => {
if (chainId !== 1) {
// Support only mainnet for now. CoinGecko does not support Sepolia (obviously, I guess :D) and we don't want to burn API credits to "simulate" prices display
return;
}

try {
const tokensAddresses = tokens
.filter(token => token.balance !== '0' && !!token.tokenAddress)
.map(token => token.tokenAddress);
const ethAsset = tokens.find(token => !token.tokenAddress);
if (ethAsset) {
tokensAddresses.push('ethereum');
}
const pricesResponse = await fetch(
`/.netlify/functions/tokensPrices?tokens=${tokensAddresses.join(',')}`
);

const pricesResponseBody = await pricesResponse.json();
if (pricesResponseBody.error) {
// We don't need to log error here as it is supposed to be logged through Netlify function anyway
toast.warning(t('tokenPriceFetchingError'));
} else {
return pricesResponseBody.data;
}
} catch (e) {
logError('Error while getting tokens prices', e);
return;
}
},
[chainId, t]
);
return { getTokenPrices };
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"tests",
"test",
"app",
"netlify/functions",
".next/types/**/*.ts",
"next-env.d.ts",
"next.config.js",
Expand Down

0 comments on commit e1f77e1

Please sign in to comment.