safe-global / safe-client-gateway

Serves as a bridge for the Safe{Wallet} clients (Android, iOS, Web)
https://docs.safe.global
MIT License
24 stars 59 forks source link

Implement endpoint to return stablecoin details #1749

Closed iamacook closed 1 month ago

iamacook commented 1 month ago

Description

Clients require a list of stablecoins for swaps. This is currently hardcoded but was scraped from CoinGecko.

Whilst this could be included in the Config Service, we might be able to return it directly from CoinGecko as they are integrated in the project.

Note: if the CoinGecko API does not expose a dedicated stablecoin list, it would make sense to move these to the Config Service. Whether it be to the Chain model or not needs to be decided as we shoul avoid bloating it.

Requirements

Integrate new endpoint to return stablecoins for a given chain, either from CoinGecko or the Config Service.

Additional information

No response

iamacook commented 1 month ago

@compojoom shared the following script he used to scrape the stablecoins from CoinGecko:

const axios = require('axios');
const fs = require('fs');

const API_URL = 'https://api.coingecko.com/api/v3';
const API_KEY = '';  // Replace with your actual API key
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function getStablecoins() {
    try {
        const url = `${API_URL}/coins/markets`;
        const params = {
            vs_currency: 'usd',
            category: 'stablecoins',
            order: 'market_cap_desc',
            per_page: 250, // Get the first 10 stablecoins
            page: 1,
            sparkline: false
        };
        const headers = {
            'accept': 'application/json',
            'x-cg-demo-api-key': API_KEY
        };
        const { data } = await axios.get(url, { params, headers });

        return data.map(coin => ({
            id: coin.id,
            name: coin.name,
            symbol: coin.symbol
        }));
    } catch (error) {
        // console.log(error)
        // if (error.response && error.response.status === 429) {
        //     console.log('Rate limited. Waiting for 10 seconds...');
        //     await delay(10000);
        //     return getCoinDetails(coinId);
        // }
        handleError(error, 'Error fetching stablecoins');
    }
}

const MAX_RETRIES = 3
async function getCoinDetails(coinId, retries = 1) {
    try {
        await delay(3000);
        const url = `${API_URL}/coins/${coinId}?tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false&localization=false`;
        console.log('url for coin details:', url)
        const headers = {
            'accept': 'application/json',
            'x-cg-demo-api-key': API_KEY,
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Accept-Language': 'en-US,en;q=0.9',
        };
        const { data } = await axios.get(url, { headers });
        return data;
    } catch (error) {
        // console.log(error)
        // if error 429, try again after 10 seconds
        // if (error.response && error.response.status === 429) {
        if (retries < MAX_RETRIES) {
            console.log('Rate limited. Waiting for 10 seconds...');
            await delay(10000*retries);
            return getCoinDetails(coinId, retries + 1);
        }
        handleError(error, `Error fetching details for coin ID: ${coinId}`);
    }
}

async function getContractAddresses(coinDetails) {
    const contracts = {};
    const networks = ['ethereum', 'gnosis', 'arbitrum-one'];

    networks.forEach(network => {
        if (coinDetails.platforms[network]) {
            contracts[network] = coinDetails.platforms[network];
        }
    });

    return contracts;
}

async function formatData(stablecoins) {
    const formattedData = {};

    for (const stablecoin of stablecoins) {

        try {
            const coinDetails = await getCoinDetails(stablecoin.id);
            const contracts = await getContractAddresses(coinDetails);

            for (const [network, address] of Object.entries(contracts)) {
                if (!formattedData[address]) {
                    formattedData[address] = {
                        name: stablecoin.name,
                        symbol: stablecoin.symbol,
                        chains: [network]
                    };
                } else {
                    formattedData[address].chains.push(network);
                }
            }
        } catch (error) {
            console.error(`Error processing stablecoin ${stablecoin.name} (${stablecoin.symbol}):`, error.message);
        }

    }

    return formattedData;
}

function handleError(error, message) {
    if (error.response) {
        console.error(`${message}: ${error.response.status} - ${error.response.statusText}`);
    } else if (error.request) {
        console.error(`${message}: No response received from the server`);
    } else {
        console.error(`${message}: ${error.message}`);
    }
    process.exit(1); // Exit the process with an error code
}

(async () => {
    try {
        const stablecoins = await getStablecoins();
        if (!stablecoins) return;

        const formattedData = await formatData(stablecoins);

        // Save data to a JSON file
        fs.writeFileSync('stablecoins.json', JSON.stringify(formattedData, null, 2));
        console.log('Data saved to stablecoins.json');
    } catch (error) {
        handleError(error, 'Error during processing');
    }
})();
iamacook commented 1 month ago

After discussion within the team (and as stablecoins don't frequently change), we will continue using a hardcoded list. Should this chage, we can consider revisiting this. cc @compojoom