From 92067e1ba224b8e75c393e06a08987c64c3e96b0 Mon Sep 17 00:00:00 2001 From: Lewis B Date: Tue, 6 Feb 2024 00:46:48 +0800 Subject: [PATCH 1/5] 126 apollo client retry (#139) * fix: apollo client infinite retry connection * chore: View cannot be child of Text * change log to error, add errorLink to subgraph apollo client * add retry link * chore: small cleanup --- packages/app/src/components/RowItem.tsx | 8 +-- .../apollo/useCreateMongoDbApolloClient.ts | 60 +++++++++++-------- .../apollo/useCreateSubgraphApolloClient.ts | 10 +++- packages/app/src/utils/apolloLinkUtils.ts | 27 +++++++++ 4 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 packages/app/src/utils/apolloLinkUtils.ts diff --git a/packages/app/src/components/RowItem.tsx b/packages/app/src/components/RowItem.tsx index 91eb8758..b8516981 100644 --- a/packages/app/src/components/RowItem.tsx +++ b/packages/app/src/components/RowItem.tsx @@ -25,15 +25,15 @@ function RowItem({ rowInfo, rowData, balance, currency, imageUrl }: RowItemProps {rowInfo} - - + + {currency} {rowData} {isDesktopResolution && currency && = {usdBalance} USD} {!isDesktopResolution && currency && = {usdBalance} USD} - - + + ); } diff --git a/packages/app/src/hooks/apollo/useCreateMongoDbApolloClient.ts b/packages/app/src/hooks/apollo/useCreateMongoDbApolloClient.ts index 2eca336f..58c4730a 100644 --- a/packages/app/src/hooks/apollo/useCreateMongoDbApolloClient.ts +++ b/packages/app/src/hooks/apollo/useCreateMongoDbApolloClient.ts @@ -1,10 +1,12 @@ -import { ApolloClient, HttpLink, NormalizedCacheObject } from '@apollo/client'; import { useEffect, useState } from 'react'; import * as Realm from 'realm-web'; import { InvalidationPolicyCache, RenewalPolicy } from '@nerdwallet/apollo-cache-policies'; +import { ApolloClient, from, HttpLink, NormalizedCacheObject } from '@apollo/client'; import { AsyncStorageWrapper, persistCache } from 'apollo3-cache-persist'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { errorLink, retryLink } from '../../utils/apolloLinkUtils'; + const APP_ID = 'wallet_prod-obclo'; const mongoDbUri = `https://realm.mongodb.com/api/client/v2.0/app/${APP_ID}/graphql`; @@ -42,32 +44,40 @@ export const useCreateMongoDbApolloClient = (): ApolloClient | undefined => storage: new AsyncStorageWrapper(AsyncStorage), }); - const client = new ApolloClient({ - cache, - link: new HttpLink({ - uri: mongoDbUri, - fetch: async (uri, options) => { - const accessToken = await getValidAccessToken(); - if (!options) { - options = {}; - } - if (!options.headers) { - options.headers = {}; - } - (options.headers as Record).Authorization = `Bearer ${accessToken}`; - return fetch(uri, options); - }, - }), - defaultOptions: { - watchQuery: { - fetchPolicy: 'cache-and-network', - }, - query: { - fetchPolicy: 'cache-first', - }, + const httpLink = new HttpLink({ + uri: mongoDbUri, + fetch: async (uri, options) => { + const accessToken = await getValidAccessToken(); + if (!options) { + options = {}; + } + if (!options.headers) { + options.headers = {}; + } + (options.headers as Record).Authorization = `Bearer ${accessToken}`; + return fetch(uri, options); }, }); - setApolloClient(client); + + try { + const client = new ApolloClient({ + cache, + link: from([errorLink, retryLink, httpLink]), + defaultOptions: { + watchQuery: { + fetchPolicy: 'cache-and-network', + }, + query: { + fetchPolicy: 'cache-first', + }, + }, + }); + setApolloClient(client); + } catch (error) { + console.error(error); + } finally { + return; + } } initApollo().catch(console.error); diff --git a/packages/app/src/hooks/apollo/useCreateSubgraphApolloClient.ts b/packages/app/src/hooks/apollo/useCreateSubgraphApolloClient.ts index 30392758..a19904ec 100644 --- a/packages/app/src/hooks/apollo/useCreateSubgraphApolloClient.ts +++ b/packages/app/src/hooks/apollo/useCreateSubgraphApolloClient.ts @@ -1,22 +1,28 @@ import { useEffect, useState } from 'react'; -import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; +import { ApolloClient, from, HttpLink, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { AsyncStorageWrapper, persistCache } from 'apollo3-cache-persist'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { errorLink, retryLink } from '../../utils/apolloLinkUtils'; + const subgraphUri = 'https://api.thegraph.com/subgraphs/name/gooddollar/goodcollective'; export const useCreateSubgraphApolloClient = (): ApolloClient | undefined => { const [apolloClient, setApolloClient] = useState | undefined>(); useEffect(() => { + const httpLink = new HttpLink({ + uri: subgraphUri, + }); async function initApollo() { const cache = new InMemoryCache(); await persistCache({ cache, storage: new AsyncStorageWrapper(AsyncStorage), }); + const client = new ApolloClient({ - uri: subgraphUri, + link: from([errorLink, retryLink, httpLink]), cache, defaultOptions: { watchQuery: { diff --git a/packages/app/src/utils/apolloLinkUtils.ts b/packages/app/src/utils/apolloLinkUtils.ts new file mode 100644 index 00000000..d64d8d90 --- /dev/null +++ b/packages/app/src/utils/apolloLinkUtils.ts @@ -0,0 +1,27 @@ +import { onError } from '@apollo/client/link/error'; +import { RetryLink } from '@apollo/client/link/retry'; + +// ref:https://www.apollographql.com/docs/react/data/error-handling/#advanced-error-handling-with-apollo-link +export const errorLink = onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) + graphQLErrors.forEach(({ message, locations, path }) => + console.error(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) + ); + if (networkError) { + console.error(`[Network error]: ${networkError}`); + throw networkError; + } +}); + +// ref: https://www.apollographql.com/docs/react/api/link/apollo-link-retry/ +export const retryLink = new RetryLink({ + delay: { + initial: 500, + max: Infinity, + jitter: true, + }, + attempts: { + max: 3, + retryIf: (error, _operation) => !!error, + }, +}); From 7c55dafd68ae2318d95cf4e968b373e3a27d1bf0 Mon Sep 17 00:00:00 2001 From: Kris Bitney Date: Mon, 5 Feb 2024 21:55:11 +0500 Subject: [PATCH 2/5] donor list items are now flows (#154) Co-authored-by: LewisB --- .../app/src/components/DonorsList/DonorsListItem.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/DonorsList/DonorsListItem.tsx b/packages/app/src/components/DonorsList/DonorsListItem.tsx index 752c8c53..0e6a3458 100644 --- a/packages/app/src/components/DonorsList/DonorsListItem.tsx +++ b/packages/app/src/components/DonorsList/DonorsListItem.tsx @@ -7,6 +7,7 @@ import Decimal from 'decimal.js'; import { formatAddress } from '../../lib/formatAddress'; import { ethers } from 'ethers'; import { useEnsName } from 'wagmi'; +import { useFlowingBalance } from '../../hooks/useFlowingBalance'; interface DonorsListItemProps { donor: DonorCollective; @@ -18,9 +19,11 @@ export const DonorsListItem = (props: DonorsListItemProps) => { const { donor, rank, userFullName } = props; const { navigate } = useCrossNavigate(); - const formattedDonations: string = new Decimal(ethers.utils.formatEther(donor.contribution) ?? 0).toFixed( - 2, - Decimal.ROUND_DOWN + const { formatted: formattedDonations } = useFlowingBalance( + donor.contribution, + donor.timestamp, + donor.flowRate, + undefined ); const { data: ensName } = useEnsName({ address: donor.donor as `0x${string}`, chainId: 1 }); From 536033fa209e2e5a3a768f489b4a3998f0f7e006 Mon Sep 17 00:00:00 2001 From: Lewis B Date: Tue, 6 Feb 2024 00:59:58 +0800 Subject: [PATCH 3/5] fix: map fullnames to addresses correctly (#157) * fix: map fullnames to addresses correctly * fix: nested view in text * remove unneccesary field * remove comment --- packages/app/package.json | 2 +- .../src/components/DonorsList/DonorsList.tsx | 3 +- .../components/DonorsList/DonorsListItem.tsx | 3 +- .../components/StewardsList/StewardsList.tsx | 2 +- packages/app/src/hooks/useFetchFullName.ts | 45 ++++++++++++++----- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 997d0421..af2f70d6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -10,7 +10,7 @@ "format:check": "prettier -c ./src", "start": "react-native start", "test": "jest", - "web": "vite -c web/vite.config.ts", + "web": "vite --port 3000 -c web/vite.config.ts", "build": "yarn build:web", "build:web": "tsc && vite build -c web/vite.config.ts", "preview:web": "vite preview -c web/vite.config.ts" diff --git a/packages/app/src/components/DonorsList/DonorsList.tsx b/packages/app/src/components/DonorsList/DonorsList.tsx index 3ae7e416..8af41762 100644 --- a/packages/app/src/components/DonorsList/DonorsList.tsx +++ b/packages/app/src/components/DonorsList/DonorsList.tsx @@ -24,12 +24,13 @@ function DonorsList({ donors, listStyle }: DonorsListProps) { const userAddresses = useMemo(() => { return sortedDonors.map((donor) => donor.donor as `0x${string}`); }, [sortedDonors]); + const userFullNames = useFetchFullNames(userAddresses); return ( {sortedDonors.map((donor, index) => ( - + ))} ); diff --git a/packages/app/src/components/DonorsList/DonorsListItem.tsx b/packages/app/src/components/DonorsList/DonorsListItem.tsx index 0e6a3458..d7eda602 100644 --- a/packages/app/src/components/DonorsList/DonorsListItem.tsx +++ b/packages/app/src/components/DonorsList/DonorsListItem.tsx @@ -15,8 +15,7 @@ interface DonorsListItemProps { userFullName?: string; } -export const DonorsListItem = (props: DonorsListItemProps) => { - const { donor, rank, userFullName } = props; +export const DonorsListItem = ({ donor, rank, userFullName }: DonorsListItemProps) => { const { navigate } = useCrossNavigate(); const { formatted: formattedDonations } = useFlowingBalance( diff --git a/packages/app/src/components/StewardsList/StewardsList.tsx b/packages/app/src/components/StewardsList/StewardsList.tsx index 6f0cc632..a8f5aaca 100644 --- a/packages/app/src/components/StewardsList/StewardsList.tsx +++ b/packages/app/src/components/StewardsList/StewardsList.tsx @@ -41,7 +41,7 @@ function StewardList({ listType, stewards, titleStyle, listStyle }: StewardListP showActions={listType === 'viewStewards'} key={steward.steward} profileImage={profileImages[index % profileImages.length]} - userFullName={userFullNames[index]} + userFullName={userFullNames[steward.steward]} /> ))} diff --git a/packages/app/src/hooks/useFetchFullName.ts b/packages/app/src/hooks/useFetchFullName.ts index 424ee79c..78df913c 100644 --- a/packages/app/src/hooks/useFetchFullName.ts +++ b/packages/app/src/hooks/useFetchFullName.ts @@ -5,6 +5,12 @@ import { useMongoDbQuery } from './apollo/useMongoDbQuery'; interface UserProfile { fullName?: { display?: string }; + index: { + walletAddress: { + hash: string; + display?: string; + }; + }; } interface UserProfilesResponse { @@ -17,6 +23,11 @@ const findProfiles = gql` fullName { display } + index { + walletAddress { + hash + } + } } } `; @@ -27,22 +38,34 @@ export function useFetchFullName(address?: string): string | undefined { return names[0]; } -export function useFetchFullNames(addresses: string[]): (string | undefined)[] { - const hashedAddresses = useMemo(() => { - return addresses.map((address: string) => ethers.utils.keccak256(address)); - }, [addresses]); +export function useFetchFullNames(addresses: string[]): any { + const addressToHashMapping = addresses.reduce((acc: any, address) => { + const hash = ethers.utils.keccak256(address); + acc[hash] = address; + return acc; + }, {}); + + const hashedAddresses = Object.keys(addressToHashMapping); const { data, error } = useMongoDbQuery(findProfiles, { - variables: { query: { index: { walletAddress: { hash_in: hashedAddresses } } } }, + variables: { + query: { + index: { walletAddress: { hash_in: hashedAddresses } }, + }, + }, }); return useMemo(() => { - if (error) { - console.error(error); - } if (!data || data.user_profiles.length === 0) { - return []; + return {}; } - return data.user_profiles.map((profile) => profile?.fullName?.display); - }, [data, error]); + return data.user_profiles.reduce((acc: Record, profile) => { + if (!profile) return {}; + const { hash } = profile.index.walletAddress; + const { display } = profile.fullName ?? {}; + const address = addressToHashMapping[hash]; + acc[address] = display ?? ''; + return acc; + }, {}); + }, [data, addressToHashMapping]); } From 8550f04923c6efa62f320c81837d4d4ad70ab2dc Mon Sep 17 00:00:00 2001 From: Kris Bitney Date: Mon, 5 Feb 2024 22:20:02 +0500 Subject: [PATCH 4/5] ViewCollective -> Total Donations Received is now a sum of Current Pool and Total Paid Out (#160) Co-authored-by: LewisB --- .../app/src/components/FlowingCurrentPoolRowItem.tsx | 8 +++++++- packages/app/src/components/ViewCollective.tsx | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/FlowingCurrentPoolRowItem.tsx b/packages/app/src/components/FlowingCurrentPoolRowItem.tsx index 61266dbf..4468acb5 100644 --- a/packages/app/src/components/FlowingCurrentPoolRowItem.tsx +++ b/packages/app/src/components/FlowingCurrentPoolRowItem.tsx @@ -6,6 +6,7 @@ import { useDonorCollectivesFlowingBalancesWithAltStaticBalance } from '../hooks import { DonorCollective } from '../models/models'; import { useGetTokenBalance } from '../hooks/useGetTokenBalance'; import { SupportedNetwork } from '../models/constants'; +import Decimal from 'decimal.js'; interface FlowingDonationsRowItemProps { rowInfo: string; @@ -14,6 +15,7 @@ interface FlowingDonationsRowItemProps { tokenPrice: number | undefined; currency?: string; imageUrl: string; + additionalBalance?: string; } function FlowingDonationsRowItem({ @@ -23,14 +25,18 @@ function FlowingDonationsRowItem({ tokenPrice, currency, imageUrl, + additionalBalance, }: FlowingDonationsRowItemProps) { const [isDesktopResolution] = useMediaQuery({ minWidth: 920, }); const currentBalance = useGetTokenBalance('G$', collective, SupportedNetwork.CELO); + const balanceUsed = additionalBalance + ? new Decimal(currentBalance).add(additionalBalance).toFixed(0, Decimal.ROUND_DOWN) + : currentBalance; const { formatted: formattedCurrentPool, usdValue: usdValueCurrentPool } = - useDonorCollectivesFlowingBalancesWithAltStaticBalance(currentBalance, donorCollectives, tokenPrice); + useDonorCollectivesFlowingBalancesWithAltStaticBalance(balanceUsed, donorCollectives, tokenPrice); return ( diff --git a/packages/app/src/components/ViewCollective.tsx b/packages/app/src/components/ViewCollective.tsx index 7052813c..51fa598b 100644 --- a/packages/app/src/components/ViewCollective.tsx +++ b/packages/app/src/components/ViewCollective.tsx @@ -30,7 +30,6 @@ import { WebIcon, } from '../assets/'; import { calculateGoodDollarAmounts } from '../lib/calculateGoodDollarAmounts'; -import FlowingDonationsRowItem from './FlowingDonationsRowItem'; import { useDeleteFlow } from '../hooks/useContractCalls/useDeleteFlow'; import ErrorModal from './modals/ErrorModal'; import FlowingCurrentPoolRowItem from './FlowingCurrentPoolRowItem'; @@ -189,12 +188,14 @@ function ViewCollective({ collective }: ViewCollectiveProps) { - - Date: Mon, 5 Feb 2024 22:33:46 +0500 Subject: [PATCH 5/5] Fix: The app no longer crashes when connected to networks that are not supported by uniswap and selecting non-G$ token on donate screen (#164) * app no longer crashes on non-celo network change at donate screen * fix: show unsupported network instead of None --------- Co-authored-by: LewisB --- .../src/components/Header/ConnectedAccountDisplay.tsx | 11 +++++++---- packages/app/src/hooks/useSwapRoute.tsx | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/Header/ConnectedAccountDisplay.tsx b/packages/app/src/components/Header/ConnectedAccountDisplay.tsx index 9584294b..9f65613d 100644 --- a/packages/app/src/components/Header/ConnectedAccountDisplay.tsx +++ b/packages/app/src/components/Header/ConnectedAccountDisplay.tsx @@ -1,11 +1,13 @@ import { Image, StyleSheet, Text, View } from 'react-native'; +import { useEnsName, useNetwork } from 'wagmi'; + import { InterRegular } from '../../utils/webFonts'; import { formatAddress } from '../../lib/formatAddress'; -import { useEnsName, useNetwork } from 'wagmi'; import { Colors } from '../../utils/colors'; import { PlaceholderAvatar } from '../../assets'; import { useGetTokenBalance } from '../../hooks/useGetTokenBalance'; import { formatNumberWithCommas } from '../../lib/formatFiatCurrency'; +import { SupportedNetwork } from '../../models/constants'; interface ConnectedAccountDisplayProps { isDesktopResolution: boolean; @@ -17,8 +19,9 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => const { chain } = useNetwork(); let chainName = chain?.name.replace(/\d+|\s/g, ''); - if (chainName !== 'Celo') { - chainName = 'None'; + console.log('chainName', { chainName, chain: chain }); + if (!(chainName && chainName.toUpperCase() in SupportedNetwork)) { + chainName = 'Unsupported Network'; } const tokenBalance = useGetTokenBalance('G$', address, chain?.id, true); @@ -33,7 +36,7 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => {chainName} diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 8da3c913..f4697dc7 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,7 +1,7 @@ import { AlphaRouter, SwapRoute, SwapType, V3Route } from '@uniswap/smart-order-router'; import { CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'; import { useAccount, useNetwork } from 'wagmi'; -import { GDToken } from '../models/constants'; +import { GDToken, SupportedNetwork } from '../models/constants'; import { useEthersSigner } from './useEthersSigner'; import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; import Decimal from 'decimal.js'; @@ -37,13 +37,13 @@ export function useSwapRoute( const [route, setRoute] = useState(undefined); useEffect(() => { - if (!address || !chain?.id || !signer?.provider || tokenIn.symbol === 'G$') { + if (!address || !chain?.id || chain.id !== SupportedNetwork.CELO || !signer?.provider || tokenIn.symbol === 'G$') { setRoute(undefined); return; } const router = new AlphaRouter({ - chainId: chain.id, + chainId: chain.id as number, provider: signer.provider, });