import { Result } from "@ethersproject/abi"; import axios from "axios"; import BigNumber from "bignumber.js"; import invariant from "tiny-invariant"; import Web3 from "web3"; import { Log } from "web3-core"; import { Address, API_BASE, ChainId, CHAINS_TO_RPC, formatHeader, } from "../constants"; import { ERC20_INTERFACE, getErc20 } from "../constants/contracts"; import { multicallMultipleContractSingleData, waitForMinedTransaction, } from "../utils"; import { Wallet } from "../wallet"; /** * * Fetches current token prices for a chain. Prices can be up to 5 minutes stale currently, * and will be denominated in the supplied base currency. * * @param apiKey API Key used for authentication * @param chainId ChainId to get prices from. If no ID is provided, will provide prices for all chains * @param currency Base currency. If none is provided, price quotes will be in USD * @returns A mapping from token address to current price */ export const fetchPrices = async ( apiKey: string, chainId?: ChainId, currency?: string ) => { const headers = formatHeader(apiKey); const data = await axios.get<{ data: { address: string; priceusd: number; price?: number }[]; }>( `${API_BASE}/api/Prices?currency=${currency}${ chainId ? `&chainId=${chainId}` : "" }`, { headers, } ); return data.data.data.reduce((accum, cur) => { return { ...accum, [cur.address.toLowerCase()]: cur.priceusd, }; }, {}); }; /** * * Uses multicall to fetch token balances for a given wallet and list of token addresses. * * @param chainId ChainId to fetch balances from * @param wallet Address of wallet to find balances for * @param tokens Array of token addresses to fetch balances for * @returns A map of token address to balance, in TokenAmount. If a wallet has 0 balance for a token, the entry will be undefined. */ export async function getBalances( chainId: ChainId, wallet: string, tokens: Address[] ) { const web3 = new Web3(CHAINS_TO_RPC[chainId].http); const tokenAddresses = tokens; if (!wallet) { return {}; } const balances = await multicallMultipleContractSingleData( web3, ERC20_INTERFACE, tokenAddresses, "balanceOf", [wallet] ); return balances.reduce( (accum: { [address: string]: BigNumber }, cur: Result, i: number) => ({ ...accum, [tokenAddresses[i].toLowerCase()]: new BigNumber(cur[0].toString()), }), {} ); } const onTransfer = async ( callback: (token: Address, newBalance: BigNumber, e?: Log) => void, web3: Web3, wallet: Address, event: Log, error: any ) => { if (error) { return; } const success = await waitForMinedTransaction( event.transactionHash as string, web3 ); if (!success) return; // txn reverted const { address } = event; const token = getErc20(web3, address); const newBalance = await token?.methods.balanceOf(wallet).call(); callback(address, new BigNumber(newBalance?.toString() ?? "0"), event); }; /** * * Listen for an react to any transfer involving a given wallet. * When no longer interested in listending, call the returned callback to terminate the listeners. * * @param wallet Wallet to watch transfers for * @param chain Chain to watch transfers on * @param tokens Tokens to explicitly watch for transfers * @param callback Callback to execute when a transfer is detected * @returns A method to unsubcribe from all event listeners requried to watch transfers * * @exmample * ```ts * const wallet = getMyWallet() * const tokens = getMyTokensToWatchOver * * const onTransfer = (token: Address, _, pureEvent) => console.log(`Transfer detected for token ${token} in transaction ${pureEvent.transactionHash}`) * * const terminate = subscribeToTokenTransfers(wallet, wallet.homeChain, tokens, onTransfer) * * \* // Do something * * terminate() \* // Close subscriptions * ``` * * @exmample * ```ts * const MyComponent() { * const wallet = useWallet; * const tokens = useTokens(); * * const onTransfer = useCallback((token) => console.log(token)) * * useEffect(() => { * const terminate = subscribeToTokenTransfers(wallet, wallet.homeChain, tokens, onTransfer) * * \* // Close event listeners on lifecycle end * return terminate * }, []) * } * ``` */ export function subscribeToTokenTransfers( wallet: string | Wallet, chain: ChainId, tokens: Address[], callback: (token: Address, newBalance: BigNumber, e?: Log) => void ) { invariant( !!CHAINS_TO_RPC[chain].wss, "wss required to subscribe to event data" ); const walletAddress = (typeof wallet === "string" ? wallet : wallet.address) ?? ""; const web3 = new Web3( new Web3.providers.WebsocketProvider( CHAINS_TO_RPC[chain].wss ?? CHAINS_TO_RPC[chain].http ) ); const paddedAddress = web3.utils.padLeft(walletAddress ?? "", 64); const handleTransfer = (err: Error, e: Log) => { if (err) { return; } onTransfer(callback, web3, walletAddress, e, err); }; const sub1 = web3.eth.subscribe( "logs", { address: tokens, topics: [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", null, paddedAddress, ], }, handleTransfer ); const sub2 = web3.eth.subscribe( "logs", { address: tokens, topics: [ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", paddedAddress, null, ], }, handleTransfer ); return () => { sub1.unsubscribe(); sub2.unsubscribe(); }; }