diff --git a/packages/app/components/strategies/constants.tsx b/packages/app/components/strategies/constants.tsx index 94464bd2..c4e14a48 100644 --- a/packages/app/components/strategies/constants.tsx +++ b/packages/app/components/strategies/constants.tsx @@ -1,6 +1,7 @@ import { FREQUENCY_OPTIONS } from "@/models/stack"; import { gnosisTokens, mainnetTokens } from "@/models/token"; import { Strategy } from "@/contexts"; +import { ChainId } from "@stackly/sdk"; type ChainStrategy = { label: string; @@ -19,7 +20,7 @@ export const FREQUENCY_LABEL = { }; export const STRATEGY_CATEGORIES: { [chainId: number]: ChainStrategies } = { - 1: { + [ChainId.ETHEREUM]: { popular: { label: "Popular Strategies", strategies: [ @@ -62,7 +63,7 @@ export const STRATEGY_CATEGORIES: { [chainId: number]: ChainStrategies } = { ], }, }, - 100: { + [ChainId.GNOSIS]: { popular: { label: "Popular Strategies", strategies: [ diff --git a/packages/app/components/token-picker/constants.ts b/packages/app/components/token-picker/constants.ts index 5a3af059..b0b79585 100644 --- a/packages/app/components/token-picker/constants.ts +++ b/packages/app/components/token-picker/constants.ts @@ -3,17 +3,18 @@ import { mainnetTokens, TokenFromTokenlist, } from "@/models/token"; +import { ChainId } from "@stackly/sdk"; export const TOKEN_PICKER_COMMON_TOKENS: { [chainId: number]: TokenFromTokenlist[]; } = { - 1: [ + [ChainId.ETHEREUM]: [ mainnetTokens.USDC, mainnetTokens.WETH, mainnetTokens.WBTC, mainnetTokens.RETH, ], - 100: [ + [ChainId.GNOSIS]: [ gnosisTokens.GNO, gnosisTokens.SWAPR, gnosisTokens.WETH, diff --git a/packages/app/constants/index.ts b/packages/app/constants/index.ts index 00a2e13b..f4820775 100644 --- a/packages/app/constants/index.ts +++ b/packages/app/constants/index.ts @@ -1,10 +1,11 @@ // RPC endpoints -export const RPC_GNOSIS = - process.env.RPC_GNOSIS ?? "https://rpc.gnosis.gateway.fm"; +import { ChainId } from "@stackly/sdk"; -export const RPC_MAINNET = - process.env.RPC_MAINNET ?? "https://eth.meowrpc.com/"; +export const RPC_LIST: { [chainId: number]: string } = { + [ChainId.ETHEREUM]: process.env.RPC_MAINNET ?? "https://eth.meowrpc.com/", + [ChainId.GNOSIS]: process.env.RPC_GNOSIS ?? "https://rpc.gnosis.gateway.fm", +}; // App URLs diff --git a/packages/app/contexts/TokenListContext.tsx b/packages/app/contexts/TokenListContext.tsx index c666cc2b..bcad3461 100644 --- a/packages/app/contexts/TokenListContext.tsx +++ b/packages/app/contexts/TokenListContext.tsx @@ -1,199 +1,206 @@ -"use client"; - -import { - PropsWithChildren, - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import { erc20ABI, useAccount, useNetwork } from "wagmi"; -import { formatUnits } from "viem"; -import { multicall } from "@wagmi/core"; - -import { ChainId } from "@stackly/sdk"; -import defaultGnosisTokenlist from "public/assets/blockchains/gnosis/tokenlist.json"; -import defaultEthereumTokenlist from "public/assets/blockchains/ethereum/tokenlist.json"; -import { TokenFromTokenlist } from "@/models/token/types"; - -export interface TokenWithBalance extends TokenFromTokenlist { - balance?: string; -} - -const DEFAULT_TOKEN_LIST_BY_CHAIN: { - [chainId: number]: TokenFromTokenlist[]; -} = { - 1: defaultEthereumTokenlist, - 100: defaultGnosisTokenlist, -}; - -const TOKEN_LISTS_BY_CHAIN_URL: { [chainId: number]: string[] } = { - 1: [ - "https://tokens.1inch.eth.link/", - "https://files.cow.fi/tokens/CoinGecko.json", - "https://files.cow.fi/tokens/CowSwap.json", - ], - 100: [ - "https://tokens.honeyswap.org/", - "https://files.cow.fi/tokens/CowSwap.json", - ], -}; - -const TokenListContext = createContext<{ - tokenList: TokenFromTokenlist[]; - tokenListWithBalances?: TokenWithBalance[]; - getTokenLogoURL: (tokenAddress: string) => string; - getTokenFromList: (tokenAddress: string) => TokenFromTokenlist | false; -}>({ - tokenList: DEFAULT_TOKEN_LIST_BY_CHAIN[ChainId.GNOSIS], - getTokenLogoURL: (tokenAddress: string) => "#", - getTokenFromList: (tokenAddress: string) => false, -}); - -const mergeTokenlists = ( - tokenList: TokenFromTokenlist[], - newTokenlist: TokenFromTokenlist[] -) => { - const addresses = new Set( - tokenList.map((token) => token.address.toLowerCase()) - ); - const mergedLists = [ - ...tokenList, - ...newTokenlist.filter( - (token) => !addresses.has(token.address.toLowerCase()) - ), - ]; - - return mergedLists; -}; - -export const TokenListProvider = ({ children }: PropsWithChildren) => { - const [tokenList, setTokenList] = useState( - defaultGnosisTokenlist - ); - const [tokenListWithBalances, setTokenListWithBalances] = - useState(); - const { chain } = useNetwork(); - const { address } = useAccount(); - - const chainId = chain?.id ?? ChainId.GNOSIS; - - const defaultTokenList = chain - ? DEFAULT_TOKEN_LIST_BY_CHAIN[chain.id] - : DEFAULT_TOKEN_LIST_BY_CHAIN[ChainId.GNOSIS]; - - const fetchTokenlistURLS = chain - ? TOKEN_LISTS_BY_CHAIN_URL[chain.id] - : TOKEN_LISTS_BY_CHAIN_URL[ChainId.GNOSIS]; - - const setupTokenList = useCallback(async () => { - let mergedTokenlistTokens = defaultTokenList; - - async function getTokenListData(tokenlistURL: string) { - const res = await fetch(tokenlistURL); - if (!res.ok) { - throw new Error("Failed to fetch tokenlist data"); - } - return res.json(); - } - - Promise.allSettled( - fetchTokenlistURLS.map((list) => getTokenListData(list)) - ).then((results) => { - results.forEach((result) => { - if (result.status === "fulfilled") { - mergedTokenlistTokens = mergeTokenlists( - mergedTokenlistTokens, - result.value.tokens - ); - } else { - console.error("Error fetching tokenlist data:", result.reason); - } - }); - - setTokenList( - mergedTokenlistTokens.filter( - (token: TokenFromTokenlist) => token.chainId === chainId - ) - ); - }); - }, [chainId, defaultTokenList, fetchTokenlistURLS]); - - useEffect(() => { - setupTokenList(); - }, [address, chainId, setupTokenList]); - - /** - * Once the tokenList is defined and we have an address - * connected, we batch fetch all the ERC-20 token balances - * for the connected wallet and the supported token list, - * then we desc. sort them - */ - useEffect(() => { - if (address) { - const fetchErc20Balances = async () => { - try { - const batchFetchdata = await multicall({ - contracts: tokenList - .filter((token) => token.chainId === chainId) - .map((token) => ({ - address: token.address as `0x${string}`, - abi: erc20ABI, - functionName: "balanceOf", - args: [address as `0x${string}`], - chainId: token.chainId, - })), - allowFailure: true, - }); - - const listWithBalances = tokenList - .map((token, index) => ({ - ...token, - balance: formatUnits( - batchFetchdata[index]?.result as bigint, - token.decimals - ), - })) - .sort( - (tokenA, tokenB) => - Number(tokenB.balance) - Number(tokenA.balance) - ); - - setTokenListWithBalances(listWithBalances); - } catch (error) { - console.error("Error fetching tokenlist balances:", error); - } - }; - - fetchErc20Balances(); - } else { - setTokenListWithBalances([]); - } - }, [address, chainId, tokenList]); - - const tokenListContext = useMemo( - () => ({ - tokenList, - tokenListWithBalances, - getTokenFromList: (tokenAddress: string) => - tokenList.find( - (token) => token.address.toUpperCase() === tokenAddress?.toUpperCase() - ) ?? false, - getTokenLogoURL: (tokenAddress: string) => - tokenList.find( - (token) => token.address.toUpperCase() === tokenAddress?.toUpperCase() - )?.logoURI ?? "#", - }), - [tokenList, tokenListWithBalances] - ); - - return ( - - {children} - - ); -}; - -export const useTokenListContext = () => useContext(TokenListContext); +"use client"; + +import { + PropsWithChildren, + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useAccount, useNetwork } from "wagmi"; +import { formatUnits, hexToBigInt } from "viem"; + +import { ChainId, MULTICALL_ADDRESS } from "@stackly/sdk"; +import { Erc20Abi, MulticallAbi } from "@stackly/sdk/abis"; +import defaultGnosisTokenlist from "public/assets/blockchains/gnosis/tokenlist.json"; +import defaultEthereumTokenlist from "public/assets/blockchains/ethereum/tokenlist.json"; +import { TokenFromTokenlist } from "@/models/token/types"; +import { ethers } from "ethers"; +import { RPC_LIST } from "@/constants"; + +export interface TokenWithBalance extends TokenFromTokenlist { + balance?: string; +} + +const DEFAULT_TOKEN_LIST_BY_CHAIN: { + [chainId: number]: TokenFromTokenlist[]; +} = { + [ChainId.ETHEREUM]: defaultEthereumTokenlist, + [ChainId.GNOSIS]: defaultGnosisTokenlist, +}; + +const TOKEN_LISTS_BY_CHAIN_URL: { [chainId: number]: string[] } = { + [ChainId.ETHEREUM]: [ + "https://tokens.1inch.eth.link/", + "https://files.cow.fi/tokens/CoinGecko.json", + "https://files.cow.fi/tokens/CowSwap.json", + ], + [ChainId.GNOSIS]: [ + "https://tokens.honeyswap.org/", + "https://files.cow.fi/tokens/CowSwap.json", + ], +}; + +const TokenListContext = createContext<{ + tokenList: TokenFromTokenlist[]; + tokenListWithBalances?: TokenWithBalance[]; + getTokenLogoURL: (tokenAddress: string) => string; + getTokenFromList: (tokenAddress: string) => TokenFromTokenlist | false; +}>({ + tokenList: DEFAULT_TOKEN_LIST_BY_CHAIN[ChainId.GNOSIS], + getTokenLogoURL: (tokenAddress: string) => "#", + getTokenFromList: (tokenAddress: string) => false, +}); + +const mergeTokenlists = ( + tokenList: TokenFromTokenlist[], + newTokenlist: TokenFromTokenlist[] +) => { + const addresses = new Set( + tokenList.map((token) => token.address.toLowerCase()) + ); + const mergedLists = [ + ...tokenList, + ...newTokenlist.filter( + (token) => !addresses.has(token.address.toLowerCase()) + ), + ]; + + return mergedLists; +}; + +export const TokenListProvider = ({ children }: PropsWithChildren) => { + const [tokenList, setTokenList] = useState( + defaultGnosisTokenlist + ); + const [tokenListWithBalances, setTokenListWithBalances] = + useState(); + const { chain } = useNetwork(); + const { address } = useAccount(); + + const chainId = chain?.id ?? ChainId.GNOSIS; + + const defaultTokenList = chain + ? DEFAULT_TOKEN_LIST_BY_CHAIN[chain.id] + : DEFAULT_TOKEN_LIST_BY_CHAIN[ChainId.GNOSIS]; + + const fetchTokenlistURLS = chain + ? TOKEN_LISTS_BY_CHAIN_URL[chain.id] + : TOKEN_LISTS_BY_CHAIN_URL[ChainId.GNOSIS]; + + const callArray = useMemo(() => { + const erc20Interface = new ethers.utils.Interface(Erc20Abi); + + if (address && tokenList) { + return tokenList.map((token) => [ + token.address, + erc20Interface.encodeFunctionData("balanceOf", [address]), + ]); + } + }, [address, tokenList]); + + /** + * Once the tokenList is defined and we have an address + * connected, we read data from the blockchain using + * the Provider through the Multicall SC to batch fetch + * all the ERC-20 token balances for the connected wallet + * and the supported token list, then we desc. sort them + */ + useEffect(() => { + const multicallInterface = new ethers.utils.Interface(MulticallAbi); + const provider = new ethers.providers.JsonRpcProvider(RPC_LIST[chainId]); + + if (address) { + provider + .call({ + to: MULTICALL_ADDRESS, + data: multicallInterface.encodeFunctionData("aggregate", [callArray]), + }) + .then((response: any) => { + const callResult = multicallInterface.decodeFunctionResult( + "aggregate", + response + ); + const balances = callResult.returnData; + + const listWithBalances = tokenList + .map((token, index) => ({ + ...token, + balance: formatUnits( + hexToBigInt(balances[index]), + token.decimals + ), + })) + .sort( + (tokenA, tokenB) => + Number(tokenB.balance) - Number(tokenA.balance) + ); + + setTokenListWithBalances(listWithBalances); + }); + } + }, [address, callArray, chainId, tokenList]); + + const setupTokenList = useCallback(async () => { + let mergedTokenlistTokens = defaultTokenList; + + async function getTokenListData(tokenlistURL: string) { + const res = await fetch(tokenlistURL); + if (!res.ok) { + throw new Error("Failed to fetch tokenlist data"); + } + return res.json(); + } + + Promise.allSettled( + fetchTokenlistURLS.map((list) => getTokenListData(list)) + ).then((results) => { + results.forEach((result) => { + if (result.status === "fulfilled") { + mergedTokenlistTokens = mergeTokenlists( + mergedTokenlistTokens, + result.value.tokens + ); + } else { + console.error("Error fetching tokenlist data:", result.reason); + } + }); + + setTokenList( + mergedTokenlistTokens.filter( + (token: TokenFromTokenlist) => token.chainId === chainId + ) + ); + }); + }, [chainId, defaultTokenList, fetchTokenlistURLS]); + + useEffect(() => { + setupTokenList(); + }, [address, chainId, setupTokenList]); + + const tokenListContext = useMemo( + () => ({ + tokenList, + tokenListWithBalances, + getTokenFromList: (tokenAddress: string) => + tokenList.find( + (token) => token.address.toUpperCase() === tokenAddress?.toUpperCase() + ) ?? false, + getTokenLogoURL: (tokenAddress: string) => + tokenList.find( + (token) => token.address.toUpperCase() === tokenAddress?.toUpperCase() + )?.logoURI ?? "#", + }), + [tokenList, tokenListWithBalances] + ); + + return ( + + {children} + + ); +}; + +export const useTokenListContext = () => useContext(TokenListContext); diff --git a/packages/app/models/cow-order/cow-order.ts b/packages/app/models/cow-order/cow-order.ts index 99e8ef06..37709a26 100644 --- a/packages/app/models/cow-order/cow-order.ts +++ b/packages/app/models/cow-order/cow-order.ts @@ -1,15 +1,15 @@ import { ChainId } from "@stackly/sdk"; export const COW_API_BASE_URL: Readonly> = { - 1: `https://api.cow.fi/mainnet/api/v1`, - 100: `https://api.cow.fi/xdai/api/v1`, + [ChainId.ETHEREUM]: `https://api.cow.fi/mainnet/api/v1`, + [ChainId.GNOSIS]: `https://api.cow.fi/xdai/api/v1`, }; const COW_EXPLORER_BASE_URL = "https://explorer.cow.fi/"; export const COW_API_EXPLORER_URL: Readonly> = { - 1: COW_EXPLORER_BASE_URL + "orders/", - 100: COW_EXPLORER_BASE_URL + "gc/orders/", + [ChainId.ETHEREUM]: COW_EXPLORER_BASE_URL + "orders/", + [ChainId.GNOSIS]: COW_EXPLORER_BASE_URL + "gc/orders/", }; export const cowExplorerUrl = (chainId: ChainId, uid: string) => diff --git a/packages/app/providers/wagmi-config.ts b/packages/app/providers/wagmi-config.ts index a197f99d..fd0e2600 100644 --- a/packages/app/providers/wagmi-config.ts +++ b/packages/app/providers/wagmi-config.ts @@ -4,14 +4,14 @@ import { gnosis, mainnet } from "wagmi/chains"; import { jsonRpcProvider } from "wagmi/providers/jsonRpc"; import { ChainId } from "@stackly/sdk"; import { SafeConnector } from "wagmi/connectors/safe"; -import { RPC_GNOSIS, RPC_MAINNET } from "@/constants"; +import { RPC_LIST } from "@/constants"; const chainJsonRpc: Record = { [ChainId.GNOSIS]: { - http: RPC_GNOSIS, + http: RPC_LIST[ChainId.GNOSIS], }, [ChainId.ETHEREUM]: { - http: RPC_MAINNET, + http: RPC_LIST[ChainId.ETHEREUM], }, }; @@ -21,7 +21,8 @@ const { chains, publicClient, webSocketPublicClient } = configureChains( jsonRpcProvider({ rpc: (chain) => chainJsonRpc[chain.id], }), - ] + ], + { batch: { multicall: true } } ); const defaultConfig = getDefaultConfig({ diff --git a/packages/app/utils/constants.ts b/packages/app/utils/constants.ts index 431ae397..6b8f19dd 100644 --- a/packages/app/utils/constants.ts +++ b/packages/app/utils/constants.ts @@ -3,6 +3,7 @@ import { mainnetTokens, TokenFromTokenlist, } from "@/models/token"; +import { ChainId } from "@stackly/sdk"; interface DefaultTokens { from: TokenFromTokenlist; @@ -10,11 +11,11 @@ interface DefaultTokens { } export const DEFAULT_TOKENS_BY_CHAIN: { [chainId: number]: DefaultTokens } = { - 1: { + [ChainId.ETHEREUM]: { from: mainnetTokens.USDC, to: mainnetTokens.WETH, }, - 100: { + [ChainId.GNOSIS]: { from: gnosisTokens.WXDAI, to: gnosisTokens.WETH, }, diff --git a/packages/app/utils/transaction.ts b/packages/app/utils/transaction.ts index 0a99656b..e9397e4c 100644 --- a/packages/app/utils/transaction.ts +++ b/packages/app/utils/transaction.ts @@ -1,8 +1,8 @@ import { ChainId } from "@stackly/sdk"; const EXPLORER_URL_BY_CHAIN = { - 1: "https://etherscan.io", - 100: "https://gnosisscan.io", + [ChainId.ETHEREUM]: "https://etherscan.io", + [ChainId.GNOSIS]: "https://gnosisscan.io", }; export const getExplorerLink = ( diff --git a/packages/sdk/abis/index.ts b/packages/sdk/abis/index.ts new file mode 100644 index 00000000..6c76cb56 --- /dev/null +++ b/packages/sdk/abis/index.ts @@ -0,0 +1,13 @@ +import DcaOrderAbi from "./DCAOrder.json"; +import Erc20Abi from "./ERC20.json"; +import Erc20Bytes32Abi from "./ERC20Bytes32.json"; +import MulticallAbi from "./Multicall.json"; +import OrderFactoryAbi from "./OrderFactory.json"; + +export { + DcaOrderAbi, + Erc20Abi, + Erc20Bytes32Abi, + MulticallAbi, + OrderFactoryAbi, +}; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 9efbd8a6..2a3af40d 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -8,7 +8,8 @@ ".": { "require": "./dist/index.js", "import": "./dist/index.js" - } + }, + "./abis": "./abis" }, "scripts": { "lint": "npx prettier --check . !./dist !./.turbo && eslint \"./**!(dist|.turbo)/*+(.tsx|.ts)\"",