From 5f2ef2fa00322313f6e08c561b03a083210132f2 Mon Sep 17 00:00:00 2001 From: Ansaf Ahmed <40786269+1saf@users.noreply.github.com> Date: Tue, 27 Jul 2021 17:31:31 +1000 Subject: [PATCH] Providers patch (#602) * update: usages of the composables which now rely on a provider * update: move composable content to a top level global provider pattern * fix: linting errors * update: uptick version * update: shuffle providers to its own folder in src * fix: imports * update: provider structure change * update: provider is kill * update: uptick version * fix: type and lint errors * fix: patch overidden registry change * fix: linting errors --- src/Root.ts | 31 +++++ .../SelectTokenModal/SelectTokenModal.vue | 13 +- src/composables/trade/useTokenApproval.ts | 4 +- src/composables/trade/useValidation.ts | 4 +- src/composables/useAccountBalances.ts | 87 ++---------- src/composables/useAllowances.ts | 93 +++---------- src/composables/useTokens.ts | 81 ++--------- src/composables/useTokensStore.ts | 118 +++------------- src/main.ts | 8 +- src/pages/Trade.vue | 4 +- src/providers/allowances.provider.ts | 119 ++++++++++++++++ src/providers/balances.provider.ts | 116 ++++++++++++++++ src/providers/index.ts | 4 + src/providers/tokens.provider.ts | 93 +++++++++++++ src/providers/tokenstore.provider.ts | 129 ++++++++++++++++++ src/store/modules/registry.ts | 22 --- 16 files changed, 570 insertions(+), 356 deletions(-) create mode 100644 src/Root.ts create mode 100644 src/providers/allowances.provider.ts create mode 100644 src/providers/balances.provider.ts create mode 100644 src/providers/index.ts create mode 100644 src/providers/tokens.provider.ts create mode 100644 src/providers/tokenstore.provider.ts diff --git a/src/Root.ts b/src/Root.ts new file mode 100644 index 0000000000..0edfecd7cd --- /dev/null +++ b/src/Root.ts @@ -0,0 +1,31 @@ +import { defineComponent, h } from 'vue'; +import App from './App.vue'; +import * as providerMap from './providers'; + +const providers = Object.values(providerMap); + +export default defineComponent({ + components: { + App, + ...providerMap + }, + + render() { + function renderProviders(providers) { + if (!providers.length) return h(App); + + const [provider, ...remainingProviders] = providers; + return h( + provider, + {}, + { + default() { + return [renderProviders(remainingProviders)]; + } + } + ); + } + + return renderProviders(providers); + } +}); diff --git a/src/components/modals/SelectTokenModal/SelectTokenModal.vue b/src/components/modals/SelectTokenModal/SelectTokenModal.vue index d945250330..7bc0f129bf 100644 --- a/src/components/modals/SelectTokenModal/SelectTokenModal.vue +++ b/src/components/modals/SelectTokenModal/SelectTokenModal.vue @@ -108,7 +108,7 @@ import { defineComponent, reactive, toRefs, computed } from 'vue'; import { useStore } from 'vuex'; import { useI18n } from 'vue-i18n'; import { isAddress, getAddress } from '@ethersproject/address'; -import useTokenLists from '@/composables/useTokensStore'; +import useTokenStore from '@/composables/useTokensStore'; // import useTokenLists2 from '@/composables/useTokenLists2'; import TokenListItem from '@/components/lists/TokenListItem.vue'; import TokenListsListItem from '@/components/lists/TokenListsListItem.vue'; @@ -147,15 +147,18 @@ export default defineComponent({ isActiveList, listMap, activeTokenLists - } = useTokenLists(); - const { tokens: tokenMap } = useTokens(data); - const tokens = computed(() => Object.values(tokenMap.value)); + } = useTokenStore(); + + const { tokens: tokenMap } = useTokens(toRefs(data)); + const tokens = computed(() => { + return Object.values(tokenMap.value); + }); // const { // approvedTokenLists, // toggleList, // toggled: toggledTokenLists, // isToggled: isToggledList - // } = useTokenLists2(); + // } = useTokenStore2(); // COMPOSABLES const store = useStore(); diff --git a/src/composables/trade/useTokenApproval.ts b/src/composables/trade/useTokenApproval.ts index 111a8c4681..154e197176 100644 --- a/src/composables/trade/useTokenApproval.ts +++ b/src/composables/trade/useTokenApproval.ts @@ -1,4 +1,4 @@ -import { computed, ComputedRef, Ref, ref, watch } from 'vue'; +import { computed, Ref, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { parseUnits } from '@ethersproject/units'; import { TransactionResponse } from '@ethersproject/providers'; @@ -18,7 +18,7 @@ import { TokenMap } from '@/types'; export default function useTokenApproval( tokenInAddress: Ref, amount: Ref, - tokens: ComputedRef + tokens: Ref ) { const approving = ref(false); const approved = ref(false); diff --git a/src/composables/trade/useValidation.ts b/src/composables/trade/useValidation.ts index 138d555503..6d8d6cb700 100644 --- a/src/composables/trade/useValidation.ts +++ b/src/composables/trade/useValidation.ts @@ -1,4 +1,4 @@ -import { computed, ComputedRef, Ref } from 'vue'; +import { computed, Ref } from 'vue'; import useWeb3 from '@/services/web3/useWeb3'; import { configService } from '@/services/config/config.service'; @@ -21,7 +21,7 @@ export default function useValidation( tokenInAmount: Ref, tokenOutAddress: Ref, tokenOutAmount: Ref, - tokens: ComputedRef + tokens: Ref ) { const { isWalletReady } = useWeb3(); diff --git a/src/composables/useAccountBalances.ts b/src/composables/useAccountBalances.ts index 25c1cf1a99..bad1242e78 100644 --- a/src/composables/useAccountBalances.ts +++ b/src/composables/useAccountBalances.ts @@ -1,90 +1,21 @@ -import getProvider from '@/lib/utils/provider'; -import { useQuery } from 'vue-query'; -import { computed, reactive } from 'vue'; -import { getBalances } from '@/lib/utils/balancer/tokens'; -import { formatEther, formatUnits } from '@ethersproject/units'; -import { getAddress } from '@ethersproject/address'; -import QUERY_KEYS from '@/constants/queryKeys'; -import { ETHER } from '@/constants/tokenlists'; -import useWeb3 from '@/services/web3/useWeb3'; -import useTokenStore from './useTokensStore'; +import { + BalancesProviderPayload, + BalancesProviderSymbol +} from '@/providers/balances.provider'; +import { inject } from 'vue'; // THE CONTENTS OF THIS WILL BE REPLACED/ALTERED WITH THE REGISTRY REFACTOR export default function useAccountBalances() { - const { account, userNetworkConfig, isWalletReady } = useWeb3(); - const { allTokens: tokens, isLoading: isLoadingTokens } = useTokenStore(); - - const isQueryEnabled = computed( - () => - account.value !== null && - Object.keys(tokens).length !== 0 && - isWalletReady.value && - !isLoadingTokens.value - ); - const { - data, + balances, + hasBalance, error, isLoading, isIdle, isError, isFetching, - refetch: refetchBalances - } = useQuery( - reactive(QUERY_KEYS.Balances.All(account, userNetworkConfig, tokens)), - () => { - return Promise.all([ - getBalances( - String(userNetworkConfig.value?.chainId), - getProvider(userNetworkConfig.value?.key), - account.value, - Object.values(tokens.value) - .map(token => token.address) - .filter(token => token !== ETHER.address) - ), - getProvider(userNetworkConfig.value?.key).getBalance( - account.value.toLowerCase() - ) - ]); - }, - reactive({ - enabled: isQueryEnabled, - keepPreviousData: isWalletReady - }) - ); - - const balances = computed(() => { - if (data.value) { - const balances = {}; - Object.keys(data.value[0]).forEach((tokenAddress: string) => { - const balance = formatUnits( - data.value[0][tokenAddress], - tokens.value[getAddress(tokenAddress)]?.decimals || 18 - ); - // not concerned with tokens which have a 0 balance - if (balance === '0.0') return; - balances[tokenAddress] = { - balance, - symbol: tokens.value[getAddress(tokenAddress)].symbol, - address: getAddress(tokenAddress) - }; - }); - - // separate case for native ether - balances[ETHER.address.toLowerCase()] = { - balance: formatEther(data.value[1] || 0), - symbol: ETHER.symbol, - address: ETHER.address - }; - return balances; - } - return null; - }); - - function hasBalance(address: string): boolean { - return !!(balances.value || {})[address]; - } - + refetchBalances + } = inject(BalancesProviderSymbol) as BalancesProviderPayload; return { balances, hasBalance, diff --git a/src/composables/useAllowances.ts b/src/composables/useAllowances.ts index d9711b0962..d62781bd1e 100644 --- a/src/composables/useAllowances.ts +++ b/src/composables/useAllowances.ts @@ -1,88 +1,29 @@ -import useWeb3 from '@/services/web3/useWeb3'; -import { useQuery } from 'vue-query'; -import { getAllowances } from '@/lib/utils/balancer/tokens'; -import getProvider from '@/lib/utils/provider'; -import { computed, reactive, Ref, ref } from 'vue'; -import { ETHER } from '@/constants/tokenlists'; -import { isAddress } from 'web3-utils'; -import QUERY_KEYS from '@/constants/queryKeys'; -import useTokens from './useTokens'; +import { inject, onBeforeMount, Ref } from 'vue'; +import { + AllowancesProviderSymbol, + AllowancesProviderPayload +} from '@/providers/allowances.provider'; type UseAccountPayload = { tokens?: Ref; dstList?: Ref; }; -const dstAllowanceMap = ref({}); - // THE CONTENTS OF THIS WILL BE REPLACED/ALTERED WITH THE REGISTRY REFACTOR export default function useAllowances(payload?: UseAccountPayload) { - const { userNetworkConfig, account } = useWeb3(); - const { tokens: allTokens } = useTokens(); - const provider = getProvider(String(userNetworkConfig.value?.chainId)); - // filter out ether and any bad addresses - const tokens = computed(() => - (payload?.tokens?.value || Object.keys(allTokens.value)).filter( - t => t !== ETHER.address && isAddress(t) - ) - ); - const dstList = computed(() => [ - ...(payload?.dstList?.value || []), - userNetworkConfig.value?.addresses.vault - ]); - - const isQueryEnabled = computed( - () => account.value != '' && tokens.value.length > 0 - ); const { - data: allowances, - isLoading, - isFetching, - refetch: refetchAllowances - } = useQuery( - QUERY_KEYS.Account.Allowances(userNetworkConfig, account, dstList, tokens), - () => - Promise.all( - dstList.value.map(async dst => - getAllowances( - String(userNetworkConfig.value?.chainId), - provider, - account.value, - dst, - tokens.value - ) - ) - ), - reactive({ - enabled: isQueryEnabled, - onSuccess: allowances => { - allowances.forEach((allowance, i) => { - dstAllowanceMap.value[dstList.value[i]] = allowance; - }); - } - }) - ); - - const isLoadingOrFetching = computed( - () => isLoading.value || isFetching.value - ); - - const getRequiredAllowances = query => { - const tokens = query.tokens; - const amounts = query.amounts; - const dst = query.dst || userNetworkConfig.value?.addresses.vault; - - const requiredAllowances = tokens.filter((token, index) => { - const amount = amounts[index]; - if (parseFloat(amount) == 0) return false; - if (!dstAllowanceMap.value) return false; - if (!dstAllowanceMap.value[dst]) return true; - if (!dstAllowanceMap.value[dst][token.toLowerCase()]) return true; - return dstAllowanceMap.value[dst][token.toLowerCase()].lt(amount); - }); - - return requiredAllowances; - }; + allowances, + getRequiredAllowances, + isLoading: isLoadingOrFetching, + refetchAllowances, + updateAllowanceRequest + } = inject(AllowancesProviderSymbol) as AllowancesProviderPayload; + + onBeforeMount(() => { + if (payload) { + updateAllowanceRequest(payload); + } + }); return { allowances, diff --git a/src/composables/useTokens.ts b/src/composables/useTokens.ts index d8fcf8a073..a3fb414afd 100644 --- a/src/composables/useTokens.ts +++ b/src/composables/useTokens.ts @@ -1,73 +1,22 @@ -import { Token, TokenMap } from '@/types'; -import { getAddress } from '@ethersproject/address'; -import { keyBy, orderBy, uniqBy } from 'lodash'; -import { computed } from 'vue'; -import { useStore } from 'vuex'; -import useAccountBalances from './useAccountBalances'; -import useTokenStore from './useTokensStore'; - -type TokenRequest = { - query?: string; - queryAddress?: string; -}; +import { + TokenRequest, + TokensProviderPayload, + TokensProviderSymbol +} from '@/providers/tokens.provider'; +import { inject, onBeforeMount } from 'vue'; export default function useTokens(request?: TokenRequest) { - const store = useStore(); - const prices = computed(() => store.state.market.prices); - const { allTokens: _allTokens } = useTokenStore(); - const { balances } = useAccountBalances(); - - const tokensList = computed(() => { - const _tokens = uniqBy( - orderBy( - // populate token data into list of tokens - Object.values(_allTokens.value).map(token => { - const balance = - (balances.value || {})[token.address.toLowerCase()]?.balance || '0'; - const price = prices.value[token.address.toLowerCase()]?.price || 0; - const value = balance * price; - const price24HChange = - prices.value[token.address.toLowerCase()]?.price24HChange || 0; - const value24HChange = (value / 100) * price24HChange; - return { - ...token, - address: getAddress(token.address), // Enforce that we use checksummed addresses - value, - price, - price24HChange, - balance, - value24HChange - }; - }), - ['value', 'balance'], - ['desc', 'desc'] - ), - 'address' - ); - - if (request?.queryAddress) { - const queryAddressLC = request?.queryAddress?.toLowerCase(); - - return _tokens.filter( - token => token.address?.toLowerCase() === queryAddressLC - ); - } - - // search functionality, this can be better - if (request?.query) { - const queryLC = request?.query?.toLowerCase(); + const { tokens, updateTokenRequest } = inject( + TokensProviderSymbol + ) as TokensProviderPayload; - return _tokens.filter( - token => - token.name.toLowerCase().includes(queryLC) || - token.symbol.toLowerCase().includes(queryLC) - ); + onBeforeMount(() => { + if (request) { + updateTokenRequest(request); } - - return _tokens; }); - const tokens = computed(() => keyBy(tokensList.value, 'address') as TokenMap); - - return { tokens }; + return { + tokens + }; } diff --git a/src/composables/useTokensStore.ts b/src/composables/useTokensStore.ts index 52a31690cb..c3140b52fa 100644 --- a/src/composables/useTokensStore.ts +++ b/src/composables/useTokensStore.ts @@ -1,15 +1,8 @@ -import { computed, ref } from 'vue'; -import { useStore } from 'vuex'; -import { useQuery } from 'vue-query'; -import { flatten, keyBy } from 'lodash'; - -import QUERY_KEYS from '@/constants/queryKeys'; -import TOKEN_LISTS, { ETHER } from '@/constants/tokenlists'; - -import { getTokensListURL, loadTokenlist } from '@/lib/utils/tokenlists'; -import { lsSet } from '@/lib/utils'; - -import { Token } from '@/types'; +import { inject } from 'vue'; +import { + TokenStoreProviderPayload, + TokenStoreProviderSymbol +} from '@/providers/tokenstore.provider'; export type TokenListItem = { address: string; @@ -35,104 +28,31 @@ type TokenList = { tokenListsURL: string; }; -const loadAllTokenLists = async () => { - // since a request to retrieve the list can fail - // it is best to use allSettled as we still want to - // retrieve what we can - return ( - await Promise.allSettled( - TOKEN_LISTS.Approved.map(async listURI => { - const tokenList = (await loadTokenlist(listURI)) as Omit< - TokenList, - 'tokenListsURL' - >; - - return { - ...tokenList, - tokenListsURL: getTokensListURL(listURI) - }; - }) - ) - ) - .filter(result => result.status === 'fulfilled') - .map(result => (result as PromiseFulfilledResult).value); -}; - // THE CONTENTS OF THIS WILL BE REPLACED/ALTERED WITH THE REGISTRY REFACTOR // This composable retrieves all the tokens from active token lists, // all the injected tokens from the store and the ETHER token // for other composables to build upon export default function useTokenStore() { - const store = useStore(); - const activeTokenLists = ref(['Balancer']); - const queryKey = QUERY_KEYS.TokenLists; - const queryFn = loadAllTokenLists; - - const getEther = () => { - const ether: any = ETHER; - ether.balance = 0; - ether.balanceDenorm = '0'; - ether.price = - store.state.market.prices[ether.address.toLowerCase()]?.price || 0; - ether.price24HChange = - store.state.market.prices[ether.address.toLowerCase()]?.price24HChange || - 0; - ether.chainId = Number(process.env.VUE_APP_NETWORK || 1); - return ether; - }; - - const { data: lists, isLoading, refetch: refreshTokenLists } = useQuery< - TokenList[] - >(queryKey, queryFn, { - refetchOnMount: false, - refetchOnWindowFocus: false - }); - - const listMap = computed(() => keyBy(lists.value, 'name')); - const injectedTokens = computed(() => store.state.registry.injected); - - const allTokens = computed(() => { - // get all the tokens from all the active lists - // get all tokens that are injected - // get the ETHER token ALWAYS - return keyBy( - flatten([ - ...Object.values(injectedTokens.value).map((t: any) => ({ ...t })), - ...activeTokenLists.value.map(name => listMap.value[name]?.tokens), - getEther() - ]) - // invalid network tokens get filtered out - .filter( - token => token?.chainId === Number(process.env.VUE_APP_NETWORK || 1) - ) as Token[], - 'address' - ); - }); - - const toggleActiveTokenList = (name: string) => { - if (activeTokenLists.value.includes(name)) { - activeTokenLists.value = activeTokenLists.value.filter( - listName => listName !== name - ); - } else { - activeTokenLists.value.push(name); - } - lsSet('activeTokenLists', activeTokenLists.value); - }; - - const isActiveList = (name: string) => { - return activeTokenLists.value.includes(name); - }; + const { + isLoading, + lists, + allTokens, + listMap, + activeTokenLists, + refreshTokenLists, + toggleActiveTokenList, + isActiveList + } = inject(TokenStoreProviderSymbol) as TokenStoreProviderPayload; return { isLoading, lists, - toggleActiveTokenList, - isActiveList, - refreshTokenLists, allTokens, listMap, - activeTokenLists + activeTokenLists, + refreshTokenLists, + toggleActiveTokenList, + isActiveList }; } diff --git a/src/main.ts b/src/main.ts index a6cb87c64a..f08580bfd0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,4 @@ import { createApp } from 'vue'; -import App from '@/App.vue'; import store from '@/store'; import router from '@/plugins/router'; import mixins from '@/plugins/mixins'; @@ -28,6 +27,7 @@ import '@/assets/css/tailwind.css'; import '@/assets/css/index.css'; import 'vue3-virtual-scroller/dist/vue3-virtual-scroller.css'; import { Web3Provider } from '@ethersproject/providers'; +import Root from './Root'; use([ TitleComponent, @@ -41,16 +41,16 @@ use([ MarkLineComponent ]); -const app = createApp(App) +const app = createApp(Root) .use(i18n) .use(router) .use(store) .use(blocknative) .use(VueApexCharts) .use(vueQuery) - .use(VueVirtualScroller) .use(Web3Plugin, Web3Provider) - .mixin(mixins); + .mixin(mixins) + .use(VueVirtualScroller); registerDirectives(app); registerGlobalComponents(app); diff --git a/src/pages/Trade.vue b/src/pages/Trade.vue index 22bf3b4929..5f557f8a19 100644 --- a/src/pages/Trade.vue +++ b/src/pages/Trade.vue @@ -16,7 +16,7 @@ import { useStore } from 'vuex'; import TradeCard from '@/components/cards/TradeCard/TradeCard.vue'; import TradeCardGP from '@/components/cards/TradeCardGP/TradeCardGP.vue'; -import useTokenLists from '@/composables/useTokensStore'; +import useTokenStore from '@/composables/useTokensStore'; import { TradeInterface } from '@/store/modules/app'; import usePoolFilters from '@/composables/pools/usePoolFilters'; @@ -29,7 +29,7 @@ export default defineComponent({ setup() { // COMPOSABLES const store = useStore(); - const { isLoading: isLoadingTokens } = useTokenLists(); + const { isLoading: isLoadingTokens } = useTokenStore(); const { setSelectedTokens } = usePoolFilters(); // COMPUTED diff --git a/src/providers/allowances.provider.ts b/src/providers/allowances.provider.ts new file mode 100644 index 0000000000..607b94b019 --- /dev/null +++ b/src/providers/allowances.provider.ts @@ -0,0 +1,119 @@ +import useWeb3 from '@/services/web3/useWeb3'; +import { useQuery } from 'vue-query'; +import { getAllowances } from '@/lib/utils/balancer/tokens'; +import getProvider from '@/lib/utils/provider'; +import { computed, provide, reactive, Ref, ref } from 'vue'; +import { ETHER } from '@/constants/tokenlists'; +import { isAddress } from 'web3-utils'; +import QUERY_KEYS from '@/constants/queryKeys'; +import useTokens from '@/composables/useTokens'; + +type UseAccountPayload = { + tokens?: Ref; + dstList?: Ref; +}; + +export type AllowancesProviderPayload = { + allowances: Ref; + getRequiredAllowances: (query: any) => any; + isLoading: Ref; + refetchAllowances: Ref; + updateAllowanceRequest: (payload: UseAccountPayload) => void; +}; + +export const AllowancesProviderSymbol = Symbol('ALLOWANCES_PROVIDER'); + +const dstAllowanceMap = ref({}); + +// THE CONTENTS OF THIS WILL BE REPLACED/ALTERED WITH THE REGISTRY REFACTOR +export default { + name: 'AllowancesProvider', + setup(props, { slots }) { + const requestPayload = ref(); + const { userNetworkConfig, account } = useWeb3(); + const { tokens: allTokens } = useTokens(); + const provider = getProvider(String(userNetworkConfig.value?.chainId)); + // filter out ether and any bad addresses + const tokens = computed(() => + ( + requestPayload?.value?.tokens?.value || Object.keys(allTokens.value) + ).filter(t => t !== ETHER.address && isAddress(t)) + ); + const dstList = computed(() => [ + ...(requestPayload?.value?.dstList?.value || []), + userNetworkConfig.value?.addresses.vault + ]); + + const isQueryEnabled = computed( + () => account.value != '' && tokens.value.length > 0 + ); + const { + data: allowances, + isLoading, + isFetching, + refetch: refetchAllowances + } = useQuery( + QUERY_KEYS.Account.Allowances( + userNetworkConfig, + account, + dstList, + tokens + ), + () => + Promise.all( + dstList.value.map(async dst => + getAllowances( + String(userNetworkConfig.value?.chainId), + provider, + account.value, + dst, + tokens.value + ) + ) + ), + reactive({ + enabled: isQueryEnabled, + onSuccess: allowances => { + allowances.forEach((allowance, i) => { + dstAllowanceMap.value[dstList.value[i]] = allowance; + }); + } + }) + ); + + const isLoadingOrFetching = computed( + () => isLoading.value || isFetching.value + ); + + const getRequiredAllowances = query => { + const tokens = query.tokens; + const amounts = query.amounts; + const dst = query.dst || userNetworkConfig.value?.addresses.vault; + + const requiredAllowances = tokens.filter((token, index) => { + const amount = amounts[index]; + if (parseFloat(amount) == 0) return false; + if (!dstAllowanceMap.value) return false; + if (!dstAllowanceMap.value[dst]) return true; + if (!dstAllowanceMap.value[dst][token.toLowerCase()]) return true; + return dstAllowanceMap.value[dst][token.toLowerCase()].lt(amount); + }); + + return requiredAllowances; + }; + + const updateAllowanceRequest = (payload: UseAccountPayload) => { + requestPayload.value = payload; + }; + + provide(AllowancesProviderSymbol, { + allowances, + getRequiredAllowances, + isLoading: isLoadingOrFetching, + refetchAllowances, + updateAllowanceRequest + }); + + return () => slots.default(); + } +}; diff --git a/src/providers/balances.provider.ts b/src/providers/balances.provider.ts new file mode 100644 index 0000000000..97bca9458f --- /dev/null +++ b/src/providers/balances.provider.ts @@ -0,0 +1,116 @@ +import getProvider from '@/lib/utils/provider'; +import { useQuery } from 'vue-query'; +import { computed, provide, reactive, Ref } from 'vue'; +import { getBalances } from '@/lib/utils/balancer/tokens'; +import { formatEther, formatUnits } from '@ethersproject/units'; +import { getAddress } from '@ethersproject/address'; +import QUERY_KEYS from '@/constants/queryKeys'; +import { ETHER } from '@/constants/tokenlists'; +import useWeb3 from '@/services/web3/useWeb3'; +import useTokenStore from '@/composables/useTokensStore'; + +export const BalancesProviderSymbol = Symbol('BALANCES_PROVIDER'); + +export type BalancesProviderPayload = { + balances: Ref>; + hasBalance: (address: string) => boolean; + error: Ref | Ref; + isLoading: Ref; + isIdle: Ref; + isError: Ref; + isFetching: Ref; + refetchBalances: Ref; +}; + +// THE CONTENTS OF THIS WILL BE REPLACED/ALTERED WITH THE REGISTRY REFACTOR +export default { + name: 'BalancesProvider', + setup(props, { slots }) { + const { account, userNetworkConfig, isWalletReady } = useWeb3(); + const { allTokens: tokens, isLoading: isLoadingTokens } = useTokenStore(); + + const isQueryEnabled = computed( + () => + account.value !== null && + Object.keys(tokens).length !== 0 && + isWalletReady.value && + !isLoadingTokens.value + ); + + const { + data, + error, + isLoading, + isIdle, + isError, + isFetching, + refetch: refetchBalances + } = useQuery( + reactive(QUERY_KEYS.Balances.All(account, userNetworkConfig, tokens)), + () => { + return Promise.all([ + getBalances( + String(userNetworkConfig.value?.chainId), + getProvider(userNetworkConfig.value?.key), + account.value, + Object.values(tokens.value) + .map(token => token.address) + .filter(token => token !== ETHER.address) + ), + getProvider(userNetworkConfig.value?.key).getBalance( + account.value.toLowerCase() + ) + ]); + }, + reactive({ + enabled: isQueryEnabled, + keepPreviousData: isWalletReady + }) + ); + + const balances = computed(() => { + if (data.value) { + const balances = {}; + Object.keys(data.value[0]).forEach((tokenAddress: string) => { + const balance = formatUnits( + data.value[0][tokenAddress], + tokens.value[getAddress(tokenAddress)]?.decimals || 18 + ); + // not concerned with tokens which have a 0 balance + if (balance === '0.0') return; + balances[tokenAddress] = { + balance, + symbol: tokens.value[getAddress(tokenAddress)].symbol, + address: getAddress(tokenAddress) + }; + }); + + // separate case for native ether + balances[ETHER.address.toLowerCase()] = { + balance: formatEther(data.value[1] || 0), + symbol: ETHER.symbol, + address: ETHER.address + }; + return balances; + } + return null; + }); + + function hasBalance(address: string): boolean { + return (balances.value || {})[address]; + } + + provide(BalancesProviderSymbol, { + balances, + hasBalance, + error, + isLoading, + isIdle, + isError, + isFetching, + refetchBalances + }); + + return () => slots.default(); + } +}; diff --git a/src/providers/index.ts b/src/providers/index.ts new file mode 100644 index 0000000000..e54392fdf5 --- /dev/null +++ b/src/providers/index.ts @@ -0,0 +1,4 @@ +export { default as TokenStoreProvider } from './tokenstore.provider'; +export { default as BalancesProvider } from './balances.provider'; +export { default as TokensProvider } from './tokens.provider'; +export { default as AllowancesProvider } from './allowances.provider'; diff --git a/src/providers/tokens.provider.ts b/src/providers/tokens.provider.ts new file mode 100644 index 0000000000..9420a4edea --- /dev/null +++ b/src/providers/tokens.provider.ts @@ -0,0 +1,93 @@ +import { Token, TokenMap } from '@/types'; +import { getAddress } from '@ethersproject/address'; +import { keyBy, orderBy, uniqBy } from 'lodash'; +import { computed, provide, Ref, ref } from 'vue'; +import { useStore } from 'vuex'; +import useAccountBalances from '@/composables/useAccountBalances'; +import useTokenStore from '@/composables/useTokensStore'; + +export type TokenRequest = { + query?: Ref; + queryAddress?: Ref; +}; + +export const TokensProviderSymbol = Symbol('TOKENS_PROVIDER'); + +export type TokensProviderPayload = { + tokens: Ref; + updateTokenRequest: (request: TokenRequest) => void; +}; + +export default { + name: 'TokenListsProvider', + setup(props, { slots }) { + const request = ref(); + const store = useStore(); + const prices = computed(() => store.state.market.prices); + const { allTokens: _allTokens } = useTokenStore(); + const { balances } = useAccountBalances(); + + const updateTokenRequest = (_request: TokenRequest) => { + request.value = _request; + }; + + const tokensList = computed(() => { + const _tokens = uniqBy( + orderBy( + // populate token data into list of tokens + Object.values(_allTokens.value).map(token => { + const balance = + (balances.value || {})[token.address.toLowerCase()]?.balance || + '0'; + const price = prices.value[token.address.toLowerCase()]?.price || 0; + const value = Number(balance) * price; + const price24HChange = + prices.value[token.address.toLowerCase()]?.price24HChange || 0; + const value24HChange = (value / 100) * price24HChange; + return { + ...token, + address: getAddress(token.address), // Enforce that we use checksummed addresses + value, + price, + price24HChange, + balance, + value24HChange + }; + }), + ['value', 'balance'], + ['desc', 'desc'] + ), + 'address' + ); + + if (request.value?.queryAddress) { + const queryAddressLC = request?.value.queryAddress?.value.toLowerCase(); + + return _tokens.filter( + token => token.address?.toLowerCase() === queryAddressLC + ); + } + + // search functionality, this can be better + if (request?.value?.query) { + const queryLC = request?.value.query?.value.toLowerCase(); + + return _tokens.filter( + token => + token.name.toLowerCase().includes(queryLC) || + token.symbol.toLowerCase().includes(queryLC) + ); + } + + return _tokens; + }); + const tokens = computed( + () => keyBy(tokensList.value, 'address') as TokenMap + ); + + const payload = { tokens, updateTokenRequest }; + provide(TokensProviderSymbol, payload); + + return () => slots.default(); + } +}; diff --git a/src/providers/tokenstore.provider.ts b/src/providers/tokenstore.provider.ts new file mode 100644 index 0000000000..6f96b9d158 --- /dev/null +++ b/src/providers/tokenstore.provider.ts @@ -0,0 +1,129 @@ +import QUERY_KEYS from '@/constants/queryKeys'; +import TOKEN_LISTS, { ETHER } from '@/constants/tokenlists'; +import { lsGet, lsSet } from '@/lib/utils'; +import { getTokensListURL, loadTokenlist } from '@/lib/utils/tokenlists'; +import { Token, TokenMap } from '@/types'; +import { TokenList } from '@/types/TokenList'; +import { flatten, keyBy } from 'lodash'; +import { computed, provide, Ref, ref } from 'vue'; +import { useQuery } from 'vue-query'; +import { useStore } from 'vuex'; + +export const TokenStoreProviderSymbol = Symbol('TOKENSTORE_PROVIDER'); + +export type TokenStoreProviderPayload = { + isLoading: Ref; + lists: Ref | Ref; + listMap: Ref>; + activeTokenLists: Ref; + allTokens: Ref; + toggleActiveTokenList: (name: string) => void; + isActiveList: (name: string) => boolean; + refreshTokenLists: Ref; +}; + +const loadAllTokenLists = async () => { + // since a request to retrieve the list can fail + // it is best to use allSettled as we still want to + // retrieve what we can + return ( + await Promise.allSettled( + TOKEN_LISTS.Approved.map(async listURI => { + const tokenList = (await loadTokenlist(listURI)) as Omit< + TokenList, + 'tokenListsURL' + >; + + return { + ...tokenList, + tokenListsURL: getTokensListURL(listURI) + }; + }) + ) + ) + .filter(result => result.status === 'fulfilled') + .map(result => (result as PromiseFulfilledResult).value); +}; + +export default { + name: 'TokenStoreProvider', + setup(props, { slots }) { + const store = useStore(); + const activeTokenLists = ref( + lsGet('activeTokenLists', ['Balancer']) + ); + const queryKey = QUERY_KEYS.TokenLists; + const queryFn = loadAllTokenLists; + + const { data: lists, isLoading, refetch: refreshTokenLists } = useQuery< + TokenList[] + >(queryKey, queryFn, { + refetchOnMount: false, + refetchOnWindowFocus: false + }); + + const getEther = () => { + const ether: any = ETHER; + ether.balance = 0; + ether.balanceDenorm = '0'; + ether.price = + store.state.market.prices[ether.address.toLowerCase()]?.price || 0; + ether.price24HChange = + store.state.market.prices[ether.address.toLowerCase()] + ?.price24HChange || 0; + ether.chainId = Number(process.env.VUE_APP_NETWORK || 1); + return ether; + }; + + const listMap = computed(() => keyBy(lists.value, 'name')); + const injectedTokens = computed(() => store.state.registry.injected); + + const allTokens = computed(() => { + // get all the tokens from all the active lists + // get all tokens that are injected + // get the ETHER token ALWAYS + // activeTokenLists; + return keyBy( + flatten([ + ...Object.values(injectedTokens.value).map((t: any) => ({ ...t })), + ...activeTokenLists.value.map(name => listMap.value[name]?.tokens), + getEther() + ]) + // invalid network tokens get filtered out + .filter( + token => token?.chainId === Number(process.env.VUE_APP_NETWORK || 1) + ) as Token[], + 'address' + ); + }); + + const toggleActiveTokenList = (name: string) => { + if (activeTokenLists.value.includes(name)) { + activeTokenLists.value = activeTokenLists.value.filter( + listName => listName !== name + ); + } else { + activeTokenLists.value = [...activeTokenLists.value, name]; + } + lsSet('activeTokenLists', activeTokenLists.value); + }; + + const isActiveList = (name: string) => { + return activeTokenLists.value.includes(name); + }; + + const payload: TokenStoreProviderPayload = { + isLoading, + lists, + allTokens, + listMap, + activeTokenLists, + refreshTokenLists, + toggleActiveTokenList, + isActiveList + }; + + provide(TokenStoreProviderSymbol, payload); + return () => slots.default(); + } +}; diff --git a/src/store/modules/registry.ts b/src/store/modules/registry.ts index 7717b2abe5..3aeb938fe9 100644 --- a/src/store/modules/registry.ts +++ b/src/store/modules/registry.ts @@ -1,6 +1,4 @@ -import { formatUnits } from '@ethersproject/units'; import { getAddress, isAddress } from '@ethersproject/address'; -import orderBy from 'lodash/orderBy'; import { loadTokenlist } from '@/lib/utils/tokenlists'; import TOKEN_LISTS, { ETHER } from '@/constants/tokenlists'; import { clone, lsGet, lsSet } from '@/lib/utils'; @@ -39,13 +37,6 @@ const getters = { ether.price24HChange = rootState.market.prices[ether.address.toLowerCase()]?.price24HChange || 0; ether.chainId = Number(process.env.VUE_APP_NETWORK || 1); - if (rootState.web3.account) { - ether.balanceDenorm = rootState.account.balances.ether || '0'; - ether.balance = formatUnits(ether.balanceDenorm, ether.decimals); - ether.value = ether.balance * ether.price; - ether.value24HChange = - (parseFloat(ether.value) / 100) * ether.price24HChange; - } return ether; }, @@ -85,19 +76,6 @@ const getters = { return token; }); - if (rootState?.web3?.account) { - tokens = tokens.map(token => { - const address = token.address.toLowerCase(); - token.balanceDenorm = rootState.account.balances[address] || '0'; - token.balance = formatUnits(token.balanceDenorm, token.decimals); - token.value = token.balance * token.price; - token.value24HChange = - (parseFloat(token.value) / 100) * token.price24HChange; - return token; - }); - tokens = orderBy(tokens, ['value', 'balance'], ['desc', 'desc']); - } - // Query filters if (includeEther) {