From 08d3d501f906ca9a3e02b893618d83e76fab088a Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 29 Dec 2023 16:03:50 +0500 Subject: [PATCH 01/18] updated useContractCalls and much of DonateComponent --- packages/app/package.json | 2 +- .../app/src/components/DonateComponent.tsx | 172 +++++++++---- .../components/DonorsList/DonorsListItem.tsx | 3 +- packages/app/src/components/ErrorModal.tsx | 9 +- .../Header/ConnectedAccountDisplay.tsx | 10 +- packages/app/src/components/RoundedButton.tsx | 18 +- .../StewardsList/StewardsListItem.tsx | 3 +- .../app/src/components/ViewCollective.tsx | 14 +- .../WalletCards/StewardCollectiveCard.tsx | 4 +- .../WalletDetails/BothWalletDetails.tsx | 9 +- .../WalletDetails/DonorWalletDetails.tsx | 4 +- .../WalletDetails/StewardWalletDetails.tsx | 4 +- .../src/contexts/WalletConnectionContext.tsx | 6 +- packages/app/src/hooks/useContractCalls.tsx | 238 +++++++++++++----- packages/app/src/hooks/useFlowingBalance.tsx | 4 +- ...eGetBalance.ts => useGetDecimalBalance.ts} | 8 +- packages/app/src/hooks/useGetTokenDecimals.ts | 21 ++ packages/app/src/hooks/useGetTokenPrice.tsx | 16 +- packages/app/src/lib/calculateFlowRate.ts | 17 ++ ...ounts.ts => calculateGoodDollarAmounts.ts} | 3 +- .../app/src/lib/calculateRawTotalDonation.ts | 9 + packages/app/src/lib/displayAddress.ts | 6 - packages/app/src/lib/formatAddress.ts | 4 + .../app/src/lib/totalDurationInSeconds.ts | 43 ++++ packages/app/src/models/constants.ts | 55 ++-- packages/app/src/pages/DonatePage.tsx | 6 +- packages/app/src/pages/ModalTestPage.tsx | 3 +- packages/app/src/utils/index.tsx | 61 +++-- yarn.lock | 4 +- 29 files changed, 538 insertions(+), 218 deletions(-) rename packages/app/src/hooks/{useGetBalance.ts => useGetDecimalBalance.ts} (71%) create mode 100644 packages/app/src/hooks/useGetTokenDecimals.ts create mode 100644 packages/app/src/lib/calculateFlowRate.ts rename packages/app/src/lib/{calculateAmounts.ts => calculateGoodDollarAmounts.ts} (82%) create mode 100644 packages/app/src/lib/calculateRawTotalDonation.ts delete mode 100644 packages/app/src/lib/displayAddress.ts create mode 100644 packages/app/src/lib/formatAddress.ts create mode 100644 packages/app/src/lib/totalDurationInSeconds.ts diff --git a/packages/app/package.json b/packages/app/package.json index bf217613..654b5952 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,7 +20,7 @@ "@celo-tools/celo-ethers-wrapper": "^0.4.0", "@ethersproject/shims": "^5.7.0", "@gooddollar/good-design": "^0.1.31", - "@gooddollar/goodcollective-sdk": "^1.*", + "@gooddollar/goodcollective-sdk": "^1.0.6", "@gooddollar/web3sdk-v2": "^0.2.2", "@react-native-aria/interactions": "0.2.3", "@react-native-async-storage/async-storage": "^1.18.2", diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 8ea65ff9..970b0d38 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -1,49 +1,83 @@ import { useState } from 'react'; -import { Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { Image, StyleSheet, Text, TextInput, View } from 'react-native'; import { InterRegular, InterSemiBold, InterSmall } from '../utils/webFonts'; import RoundedButton from './RoundedButton'; import CompleteDonationModal from './CompleteDonationModal'; import { Colors } from '../utils/colors'; import { Link, useMediaQuery } from 'native-base'; import Dropdown from './Dropdown'; -import { getButtonBGC, getButtonText, getButtonTextColor, getFrequencyTime, getTotalAmount } from '../utils'; -import { useGetTokenPrice, useContractCalls } from '../hooks'; +import { + getDonateButtonBackgroundColor, + getDonateButtonText, + getDonateButtonTextColor, + getFrequencyPlural, +} from '../utils'; +import { useContractCalls, useGetTokenPrice } from '../hooks'; import { useAccount, useNetwork } from 'wagmi'; import { IpfsCollective } from '../models/models'; -import { useGetBalance } from '../hooks/useGetBalance'; -import { currencyOptions, frequencyOptions, SupportedTokens } from '../models/constants'; +import { useGetDecimalBalance } from '../hooks/useGetDecimalBalance'; +import { + currencyOptions, + Frequency, + frequencyOptions, + SupportedNetwork, + SupportedTokenSymbol, +} from '../models/constants'; import { InfoIconOrange } from '../assets'; import { useLocation } from 'react-router-native'; +import { useGetTokenDecimals } from '../hooks/useGetTokenDecimals'; +import Decimal from 'decimal.js'; +import { formatFiatCurrency } from '../lib/formatFiatCurrency'; +import ErrorModal from './ErrorModal'; interface DonateComponentProps { - insufficientLiquidity: boolean; - priceImpact: boolean; collective: IpfsCollective; } -function DonateComponent({ insufficientLiquidity, priceImpact, collective }: DonateComponentProps) { - const [modalVisible, setModalVisible] = useState(false); - const [currency, setCurrency] = useState('G$'); - const [frequency, setFrequency] = useState('One-Time'); - const [duration, setDuration] = useState(1); - const [donationAmount, setDonationAmount] = useState(0); +function DonateComponent({ collective }: DonateComponentProps) { + const [isDesktopResolution] = useMediaQuery({ + minWidth: 612, + }); + const location = useLocation(); + const collectiveId = location.pathname.slice('/donate/'.length); - const { supportFlowWithSwap, supportFlow } = useContractCalls(); const { address, isConnected } = useAccount(); const { chain } = useNetwork(); - const balance = useGetBalance(currency as keyof SupportedTokens, address, chain?.id); - const isInsufficientBalance = balance ? donationAmount > balance : true; + const [completeDonationModalVisible, setCompleteDonationModalVisible] = useState(false); + const [errorMessage, setErrorMessage] = useState(undefined); - const { price } = useGetTokenPrice(currency); - const usdValue = price ? donationAmount * price : undefined; + const [currency, setCurrency] = useState('G$'); + const [frequency, setFrequency] = useState(Frequency.OneTime); + const [duration, setDuration] = useState(1); + const [decimalDonationAmount, setDecimalDonationAmount] = useState(0); - const [isDesktopResolution] = useMediaQuery({ - minWidth: 612, - }); + const currencyDecimals = useGetTokenDecimals(currency, chain?.id); + const donorCurrencyBalance = useGetDecimalBalance(currency as SupportedTokenSymbol, address, chain?.id); - const location = useLocation(); - const collectiveId = location.pathname.slice('/donate/'.length); + const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall } = useContractCalls( + collectiveId, + currency, + decimalDonationAmount, + duration, + frequency, + (error) => setErrorMessage(error) + ); + + const totalDecimalDonation = duration * decimalDonationAmount; + const totalDonationFormatted = new Decimal(totalDecimalDonation) + .toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN) + .toString(); + + const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; + + // TODO: determine if there is sufficient liquidity for swap + const isInsufficientLiquidity = false; + // TODO: determine price impact for swap + const isUnacceptablePriceImpact = false; + + const { price } = useGetTokenPrice(currency as SupportedTokenSymbol); + const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; return ( @@ -79,7 +113,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don placeholder={'0.00'} style={styles.subHeading} maxLength={7} - onChangeText={(value: string) => setDonationAmount(parseFloat(value))} + onChangeText={(value: string) => setDecimalDonationAmount(parseFloat(value))} /> @@ -109,7 +143,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don placeholder={'0.00'} style={styles.subHeading} maxLength={7} - onChangeText={(value: string) => setDonationAmount(parseFloat(value))} + onChangeText={(value: string) => setDecimalDonationAmount(parseFloat(value))} /> @@ -126,7 +160,11 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don How often do you want to donate this {!isDesktopResolution &&
} amount?
- setFrequency(value)} options={frequencyOptions} /> + setFrequency(value as Frequency)} + options={frequencyOptions} + /> {frequency !== 'One-Time' && ( @@ -151,7 +189,9 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don onChangeText={(value: string) => setDuration(Number(value))} /> - {getFrequencyTime(frequency)} + + {getFrequencyPlural(frequency as Frequency)} + )} @@ -165,7 +205,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don <> setFrequency(value)} + onSelect={(value: string) => setFrequency(value as Frequency)} options={frequencyOptions} /> {frequency !== 'One-Time' && ( @@ -181,7 +221,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don onChangeText={(value: any) => setDuration(value)} /> - {getFrequencyTime(frequency)} + {getFrequencyPlural(frequency)} )} @@ -211,7 +251,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don Donation Amount: - {currency} {donationAmount} + {currency} {decimalDonationAmount} {usdValue} USD @@ -222,17 +262,17 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don Donation Duration: - {duration} {getFrequencyTime(frequency)} + {duration} {getFrequencyPlural(frequency)} )} - {frequency !== 'One-Time' && ( + {frequency !== Frequency.OneTime && ( Total Amount: - {currency} {getTotalAmount(duration, donationAmount)} + {currency} {totalDonationFormatted} {usdValue} USD @@ -241,7 +281,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don - {insufficientLiquidity && ( + {isInsufficientLiquidity && ( @@ -270,7 +310,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don )} - {insufficientLiquidity && ( + {isInsufficientBalance && ( @@ -297,7 +337,7 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don )} - {priceImpact && ( + {isUnacceptablePriceImpact && ( @@ -336,25 +376,49 @@ function DonateComponent({ insufficientLiquidity, priceImpact, collective }: Don )} - - { - if (currency === 'G$') { - supportFlow(collectiveId, donationAmount, address); - } else { - supportFlowWithSwap(); - } - }} - /> - + { + if (frequency === Frequency.OneTime) { + supportSingleTransferAndCall(); + } else if (currency === 'G$') { + supportFlow(); + } else { + supportFlowWithSwap(); + } + }} + disabled={address === undefined || chain?.id === undefined || !(chain.id in SupportedNetwork)} + /> - + setErrorMessage(undefined)} + message={errorMessage ?? ''} + /> + ); } diff --git a/packages/app/src/components/DonorsList/DonorsListItem.tsx b/packages/app/src/components/DonorsList/DonorsListItem.tsx index 9df598ad..38c776c7 100644 --- a/packages/app/src/components/DonorsList/DonorsListItem.tsx +++ b/packages/app/src/components/DonorsList/DonorsListItem.tsx @@ -4,6 +4,7 @@ import { InterRegular, InterSemiBold } from '../../utils/webFonts'; import { DonorCollective } from '../../models/models'; import useCrossNavigate from '../../routes/useCrossNavigate'; import Decimal from 'decimal.js'; +import { formatAddress } from '../../lib/formatAddress'; interface DonorsListItemProps { donor: DonorCollective; @@ -15,7 +16,7 @@ export const DonorsListItem = (props: DonorsListItemProps) => { const { navigate } = useCrossNavigate(); const formattedDonations: string = new Decimal(donor.contribution ?? 0).toFixed(3); - const formattedAddress = donor.donor.slice(0, 6) + '...' + donor.donor.slice(-4); + const formattedAddress = formatAddress(donor.donor, 5); if (rank === 1) { return ( diff --git a/packages/app/src/components/ErrorModal.tsx b/packages/app/src/components/ErrorModal.tsx index b8207d1a..f9d44ff0 100644 --- a/packages/app/src/components/ErrorModal.tsx +++ b/packages/app/src/components/ErrorModal.tsx @@ -1,16 +1,15 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; import { InterRegular, InterSemiBold } from '../utils/webFonts'; -// import useCrossNavigate from '../routes/useCrossNavigate'; import { Colors } from '../utils/colors'; import { CloseIcon, ThankYouImg } from '../assets'; interface ErrorModalProps { openModal: boolean; setOpenModal: any; + message: string; } -const ErrorModal = ({ openModal, setOpenModal }: ErrorModalProps) => { - // const { navigate } = useCrossNavigate(); +const ErrorModal = ({ openModal, setOpenModal, message }: ErrorModalProps) => { return ( @@ -23,8 +22,8 @@ const ErrorModal = ({ openModal, setOpenModal }: ErrorModalProps) => { SOMETHING WENT WRONG - Please try againd later. - Reason: {'Error Code'} + Please try again later. + Reason: {message} woman setOpenModal(false)}> OK diff --git a/packages/app/src/components/Header/ConnectedAccountDisplay.tsx b/packages/app/src/components/Header/ConnectedAccountDisplay.tsx index 7b66c911..1b7e77c7 100644 --- a/packages/app/src/components/Header/ConnectedAccountDisplay.tsx +++ b/packages/app/src/components/Header/ConnectedAccountDisplay.tsx @@ -1,11 +1,11 @@ import { Image, StyleSheet, Text, View } from 'react-native'; import { InterRegular } from '../../utils/webFonts'; import { formatAmount } from '../../lib/formatAmount'; -import displayAddress from '../../lib/displayAddress'; +import { formatAddress } from '../../lib/formatAddress'; import { useEnsName, useNetwork } from 'wagmi'; import { Colors } from '../../utils/colors'; import { PlaceholderAvatar } from '../../assets'; -import { useGetBalance } from '../../hooks/useGetBalance'; +import { useGetDecimalBalance } from '../../hooks/useGetDecimalBalance'; interface ConnectedAccountDisplayProps { isDesktopResolution: boolean; @@ -18,7 +18,7 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => const { chain } = useNetwork(); const chainName = chain?.name.replace(/\d+|\s/g, ''); - const tokenBalance = useGetBalance('G$', address, chain?.id); + const tokenBalance = useGetDecimalBalance('G$', address, chain?.id); const { data: ensName } = useEnsName({ address }); return ( @@ -46,7 +46,7 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => {ensName ? ( {ensName} ) : ( - {displayAddress(address)} + {formatAddress(address)} )} @@ -75,7 +75,7 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => {ensName ? ( {ensName} ) : ( - {displayAddress(address)} + {formatAddress(address)} )} diff --git a/packages/app/src/components/RoundedButton.tsx b/packages/app/src/components/RoundedButton.tsx index ef680654..c74b9e1f 100644 --- a/packages/app/src/components/RoundedButton.tsx +++ b/packages/app/src/components/RoundedButton.tsx @@ -11,12 +11,23 @@ interface RoundedButtonProps { seeType: boolean; onPress?: () => void; maxWidth?: number | string; + disabled?: boolean; } -function RoundedButton({ title, backgroundColor, color, fontSize, seeType, onPress, maxWidth }: RoundedButtonProps) { +function RoundedButton({ + title, + backgroundColor, + color, + fontSize, + seeType, + onPress, + maxWidth, + disabled, +}: RoundedButtonProps) { if (!seeType) { return ( + { const { showActions, steward, profileImage, isVerified } = props; - const formattedAddress = steward.steward.slice(0, 6) + '...' + steward.steward.slice(-4); + const formattedAddress = formatAddress(steward.steward, 5); return ( diff --git a/packages/app/src/components/ViewCollective.tsx b/packages/app/src/components/ViewCollective.tsx index 8e9b2ef0..e9c0495e 100644 --- a/packages/app/src/components/ViewCollective.tsx +++ b/packages/app/src/components/ViewCollective.tsx @@ -31,7 +31,7 @@ import { TwitterIcon, WebIcon, } from '../assets/'; -import { calculateAmounts } from '../lib/calculateAmounts'; +import { calculateGoodDollarAmounts } from '../lib/calculateGoodDollarAmounts'; interface ViewCollectiveProps { collective: Collective; @@ -73,12 +73,18 @@ function ViewCollective({ collective }: ViewCollectiveProps) { const { price: tokenPrice } = useGetTokenPrice('G$'); - const { formatted: formattedDonations, usdValue: donationsUsdValue } = calculateAmounts(totalDonations, tokenPrice); - const { formatted: formattedTotalRewards, usdValue: totalRewardsUsdValue } = calculateAmounts( + const { formatted: formattedDonations, usdValue: donationsUsdValue } = calculateGoodDollarAmounts( + totalDonations, + tokenPrice + ); + const { formatted: formattedTotalRewards, usdValue: totalRewardsUsdValue } = calculateGoodDollarAmounts( totalRewards, tokenPrice ); - const { formatted: formattedCurrentPool, usdValue: currentPoolUsdValue } = calculateAmounts(currentPool, tokenPrice); + const { formatted: formattedCurrentPool, usdValue: currentPoolUsdValue } = calculateGoodDollarAmounts( + currentPool, + tokenPrice + ); const renderDonorsButton = () => ( diff --git a/packages/app/src/contexts/WalletConnectionContext.tsx b/packages/app/src/contexts/WalletConnectionContext.tsx index a58dd28e..0017369a 100644 --- a/packages/app/src/contexts/WalletConnectionContext.tsx +++ b/packages/app/src/contexts/WalletConnectionContext.tsx @@ -1,8 +1,8 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { useEthers } from '@usedapp/core'; import { useConnectWallet } from '@web3-onboard/react'; -import { shortenAddress } from '../utils'; import { useSwitchNetwork } from '@gooddollar/web3sdk-v2'; +import { formatAddress } from '../lib/formatAddress'; interface IWalletConnectionContext { disconnectWallet: () => Promise; connectWallet: () => Promise; @@ -42,12 +42,12 @@ const WalletConnectionProvider: React.FC<{ setWalletName('Luis.celo'); return; } - setWalletName(shortenAddress(_account)); + setWalletName(formatAddress(_account, 3)); }; useEffect(() => { setWalletAddress(account || ''); - if (account) getWalletName(shortenAddress(account)); + if (account) getWalletName(formatAddress(account, 3)); }, [account]); return ( diff --git a/packages/app/src/hooks/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls.tsx index e3c69fbe..037772dc 100644 --- a/packages/app/src/hooks/useContractCalls.tsx +++ b/packages/app/src/hooks/useContractCalls.tsx @@ -1,87 +1,213 @@ import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; -import { ethers } from 'ethers'; -import { useAccount, useWalletClient } from 'wagmi'; +import { useAccount, useNetwork } from 'wagmi'; import { useEthersSigner } from './wagmiF'; -import { useLocation } from 'react-router-native'; import useCrossNavigate from '../routes/useCrossNavigate'; +import { + Frequency, + SupportedNetwork, + SupportedNetworkNames, + SupportedTokenSymbol, + tokenMapping, +} from '../models/constants'; +import { useGetTokenDecimals } from './useGetTokenDecimals'; +import { useCallback } from 'react'; +import { calculateFlowRate } from '../lib/calculateFlowRate'; +import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; +import Decimal from 'decimal.js'; -// import { erc20ABI } from 'wagmi'; -// import { getContract } from 'wagmi/actions'; +interface ContractCalls { + supportFlowWithSwap: () => Promise; + supportFlow: () => Promise; + supportSingleTransferAndCall: () => Promise; + supportSingleBatch: () => Promise; +} -export const useContractCalls = () => { - const { data: walletClient } = useWalletClient(); +export const useContractCalls = ( + collective: string, + currency: SupportedTokenSymbol, + decimalAmountIn: number, + duration: number, + frequency: Frequency, + onError: (error: string) => void +): ContractCalls => { const { address } = useAccount(); - const provider = new ethers.providers.JsonRpcProvider( - 'https://celo-mainnet.infura.io/v3/655061f57d2c42d6a6d98259bf196567' - ); - const signer = useEthersSigner(); + const { chain } = useNetwork(); + const signer = useEthersSigner({ chainId: chain?.id }); + const currencyDecimals = useGetTokenDecimals(currency, chain?.id); const { navigate } = useCrossNavigate(); - const calculateFlow = (amount: any): number | null => { - const numAmount = Number(amount); - console.log(numAmount); - if (isNaN(numAmount)) { - alert('You can only calculate a flowRate based on a number'); - return null; + const supportFlow = useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; } - const monthlyAmount = ethers.utils.parseEther(amount.toString()) as any as number; - return Math.floor(monthlyAmount / 3600 / 24 / (365 / 12)) as number; - }; + const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); + if (!flowRate) { + onError('Failed to calculate flow rate.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + try { + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportFlow(signer, collective, flowRate); + await tx.wait(); + navigate(`/profile/${address}`); + return; + } catch (error) { + onError(`An unexpected error occurred: ${error}`); + } + }, [ + address, + chain?.id, + collective, + currencyDecimals, + decimalAmountIn, + duration, + frequency, + navigate, + onError, + signer, + ]); - const supportFlow = async (poolAddress: string, amountIn: any, user: any) => { - const flowRate = calculateFlow(amountIn); + const supportFlowWithSwap = useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); if (!flowRate) { - console.error('Failed to calculate flow rate.'); + onError('Failed to calculate flow rate.'); return; } + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + // swap values + const amountIn = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( + 0, + Decimal.ROUND_DOWN + ); + try { - const sdk = new GoodCollectiveSDK('42220', provider, { - network: 'celo', - nftStorageKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportFlowWithSwap(signer, collective, flowRate, { + amount: amountIn, + minReturn: Number.MAX_SAFE_INTEGER, // TODO: need to get min return using uniswap sdk + path: '0x', // TODO: need to get path using uniswap sdk + swapFrom: tokenMapping[currency], + deadline: (Date.now() + 18000).toString(), }); + await tx.wait(); + navigate(`/profile/${address}`); + return; + } catch (error) { + onError(`An unexpected error occurred: ${error}`); + } + }, [ + address, + chain?.id, + collective, + currency, + currencyDecimals, + decimalAmountIn, + duration, + frequency, + navigate, + onError, + signer, + ]); - if (!address) return; - console.log(flowRate); - const tx = await sdk.deleteFlow(signer as any, poolAddress, flowRate as any); + const supportSingleTransferAndCall = useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + const donationAmount = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( + 0, + Decimal.ROUND_DOWN + ); + + try { + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); await tx.wait(); - navigate('/profile/' + user); + navigate(`/profile/${address}`); return; } catch (error) { - alert('TX Failed'); - console.error('An error occurred:', error); + onError(`An unexpected error occurred: ${error}`); } - }; - const supportFlowWithSwap = async () => { + }, [address, chain?.id, collective, currencyDecimals, decimalAmountIn, duration, navigate, onError, signer]); + + const supportSingleBatch = useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + const donationAmount = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( + 0, + Decimal.ROUND_DOWN + ); + try { - const sdk = new GoodCollectiveSDK('42220', provider, { - network: 'celo', - nftStorageKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', - }); - if (!address) return; - await sdk.supportFlowWithSwap( - walletClient?.sendTransaction as any, - '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', - '100000000000000', - { - amount: 100, - minReturn: 100000000000000, - path: '0x', - swapFrom: '0xCAa7349CEA390F89641fe306D93591f87595dc1F', - deadline: (Date.now() + 1000000 / 1000).toFixed(0), - } - ); + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); + await tx.wait(); + navigate(`/profile/${address}`); + return; } catch (error) { - console.error('An error occurred:', error); + onError(`An unexpected error occurred: ${error}`); } - }; - const supportSingleTransferAndCall = async () => {}; - const supportSingleBatch = async () => {}; + }, [address, chain?.id, collective, currencyDecimals, decimalAmountIn, duration, navigate, onError, signer]); + return { - supportFlowWithSwap, supportFlow, + supportFlowWithSwap, supportSingleTransferAndCall, supportSingleBatch, }; diff --git a/packages/app/src/hooks/useFlowingBalance.tsx b/packages/app/src/hooks/useFlowingBalance.tsx index 3c21dc18..9bec2a8e 100644 --- a/packages/app/src/hooks/useFlowingBalance.tsx +++ b/packages/app/src/hooks/useFlowingBalance.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { BigNumberish, ethers } from 'ethers'; -import { calculateAmounts, CalculatedAmounts } from '../lib/calculateAmounts'; +import { calculateGoodDollarAmounts, CalculatedAmounts } from '../lib/calculateGoodDollarAmounts'; // based on https://github.com/superfluid-finance/superfluid-console/blob/master/src/components/FlowingBalance.tsx @@ -53,5 +53,5 @@ export function useFlowingBalance( // eslint-disable-next-line react-hooks/exhaustive-deps }, [balance, balanceTimestamp, flowRate]); - return calculateAmounts(weiValue.toString(), tokenPrice); + return calculateGoodDollarAmounts(weiValue.toString(), tokenPrice); } diff --git a/packages/app/src/hooks/useGetBalance.ts b/packages/app/src/hooks/useGetDecimalBalance.ts similarity index 71% rename from packages/app/src/hooks/useGetBalance.ts rename to packages/app/src/hooks/useGetDecimalBalance.ts index 96faaacc..849d2a5e 100644 --- a/packages/app/src/hooks/useGetBalance.ts +++ b/packages/app/src/hooks/useGetDecimalBalance.ts @@ -1,11 +1,11 @@ -import { tokenMapping } from '../models/constants'; +import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; import { useEffect, useState } from 'react'; import { fetchBalance } from 'wagmi/actions'; -export const useGetBalance = ( - currencySymbol: keyof typeof tokenMapping, +export const useGetDecimalBalance = ( + currencySymbol: SupportedTokenSymbol, accountAddress: `0x${string}` | undefined, - chainId: number | undefined + chainId: number = SupportedNetwork.celo ): number => { const [tokenBalance, setTokenBalance] = useState('0'); diff --git a/packages/app/src/hooks/useGetTokenDecimals.ts b/packages/app/src/hooks/useGetTokenDecimals.ts new file mode 100644 index 00000000..8afe811d --- /dev/null +++ b/packages/app/src/hooks/useGetTokenDecimals.ts @@ -0,0 +1,21 @@ +import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; +import { useEffect, useState } from 'react'; +import { fetchToken } from 'wagmi/actions'; + +export const useGetTokenDecimals = ( + currencySymbol: SupportedTokenSymbol, + chainId: number = SupportedNetwork.celo +): number => { + const [tokenDecimals, setTokenDecimals] = useState(18); + + useEffect(() => { + fetchToken({ + address: tokenMapping[currencySymbol], + chainId: chainId, + }).then((res) => { + setTokenDecimals(res.decimals); + }); + }, [currencySymbol, chainId]); + + return tokenDecimals; +}; diff --git a/packages/app/src/hooks/useGetTokenPrice.tsx b/packages/app/src/hooks/useGetTokenPrice.tsx index 0f0182ce..5577282a 100644 --- a/packages/app/src/hooks/useGetTokenPrice.tsx +++ b/packages/app/src/hooks/useGetTokenPrice.tsx @@ -1,21 +1,17 @@ import axios from 'axios'; import { useEffect, useState } from 'react'; -import { tokenMapping } from '../models/constants'; +import { SupportedTokenSymbol, tokenMapping } from '../models/constants'; -export const useGetTokenPrice = (currency: string): { price?: number; isLoading: boolean } => { +export const useGetTokenPrice = (currency: SupportedTokenSymbol): { price?: number; isLoading: boolean } => { const [price, setPrice] = useState(undefined); const [isLoading, setIsLoading] = useState(true); useEffect(() => { setIsLoading(true); - for (const [token, tokenAddress] of Object.entries(tokenMapping)) { - if (currency === token) { - getTokenPrice(tokenAddress).then((res: number | undefined) => { - setPrice(res); - }); - break; - } - } + const tokenAddress = tokenMapping[currency]; + getTokenPrice(tokenAddress).then((res: number | undefined) => { + setPrice(res); + }); setIsLoading(false); }, [currency]); diff --git a/packages/app/src/lib/calculateFlowRate.ts b/packages/app/src/lib/calculateFlowRate.ts new file mode 100644 index 00000000..b2b17627 --- /dev/null +++ b/packages/app/src/lib/calculateFlowRate.ts @@ -0,0 +1,17 @@ +import { Frequency } from '../models/constants'; +import Decimal from 'decimal.js'; +import { totalDurationInSeconds } from './totalDurationInSeconds'; + +export const calculateFlowRate = ( + decimalAmount: number, + duration: number, + frequency: Frequency, + currencyDecimals: number +): string | undefined => { + if (frequency === Frequency.OneTime) { + return undefined; + } + const rawAmount = new Decimal(decimalAmount * duration).times(10 ** currencyDecimals); + const totalMilliseconds = totalDurationInSeconds(duration, frequency); + return rawAmount.div(totalMilliseconds).toFixed(0, Decimal.ROUND_DOWN); +}; diff --git a/packages/app/src/lib/calculateAmounts.ts b/packages/app/src/lib/calculateGoodDollarAmounts.ts similarity index 82% rename from packages/app/src/lib/calculateAmounts.ts rename to packages/app/src/lib/calculateGoodDollarAmounts.ts index 6196038e..f1814df6 100644 --- a/packages/app/src/lib/calculateAmounts.ts +++ b/packages/app/src/lib/calculateGoodDollarAmounts.ts @@ -3,7 +3,8 @@ import { ethers } from 'ethers'; export type CalculatedAmounts = { decimal?: Decimal; formatted?: string; usdValue?: number }; -export function calculateAmounts(onChainAmount?: string, tokenPrice?: number): CalculatedAmounts { +// assumes 18 decimals +export function calculateGoodDollarAmounts(onChainAmount?: string, tokenPrice?: number): CalculatedAmounts { if (onChainAmount === undefined) { return { decimal: undefined, diff --git a/packages/app/src/lib/calculateRawTotalDonation.ts b/packages/app/src/lib/calculateRawTotalDonation.ts new file mode 100644 index 00000000..ac1747d9 --- /dev/null +++ b/packages/app/src/lib/calculateRawTotalDonation.ts @@ -0,0 +1,9 @@ +import Decimal from 'decimal.js'; + +export const calculateRawTotalDonation = ( + decimalAmount: number, + duration: number, + currencyDecimals: number +): Decimal => { + return new Decimal(decimalAmount * duration).times(10 ** currencyDecimals); +}; diff --git a/packages/app/src/lib/displayAddress.ts b/packages/app/src/lib/displayAddress.ts deleted file mode 100644 index 6c8eb622..00000000 --- a/packages/app/src/lib/displayAddress.ts +++ /dev/null @@ -1,6 +0,0 @@ -const displayAddress = (address: string, length = 4) => { - if (!address) return ''; - return `${address.substring(0, length)}•••${address.substring(address.length - length, address.length)}`; -}; - -export default displayAddress; diff --git a/packages/app/src/lib/formatAddress.ts b/packages/app/src/lib/formatAddress.ts new file mode 100644 index 00000000..d4f206a8 --- /dev/null +++ b/packages/app/src/lib/formatAddress.ts @@ -0,0 +1,4 @@ +export const formatAddress = (address: string, length = 5) => { + if (!address) return ''; + return address.slice(0, length) + '...' + address.slice(-(length - 1)); +}; diff --git a/packages/app/src/lib/totalDurationInSeconds.ts b/packages/app/src/lib/totalDurationInSeconds.ts new file mode 100644 index 00000000..24d535fe --- /dev/null +++ b/packages/app/src/lib/totalDurationInSeconds.ts @@ -0,0 +1,43 @@ +import { Frequency } from '../models/constants'; + +export const totalDurationInSeconds = (duration: number, frequency: Frequency): number => { + if (frequency === Frequency.OneTime) { + return 0; + } else if (frequency === Frequency.Daily) { + return duration * 24 * 60 * 60; + } else if (frequency === Frequency.Weekly) { + return duration * 7 * 24 * 60 * 60; + } + + const now = new Date(); + let nextDate: Date; + + if (frequency === Frequency.Monthly) { + const naiveMonth = now.getMonth() + duration; + const month = naiveMonth % 12; + const year = now.getFullYear() + Math.floor(naiveMonth / 12); + nextDate = new Date( + year, + month, + now.getDate(), + now.getHours(), + now.getMinutes(), + now.getSeconds(), + now.getMilliseconds() + ); + } else { + // frequency === Frequency.Yearly + nextDate = new Date( + now.getFullYear() + duration, + now.getMonth(), + now.getDate(), + now.getHours(), + now.getMinutes(), + now.getSeconds(), + now.getMilliseconds() + ); + } + + const timeInMilliseconds = nextDate.getTime() - now.getTime(); + return Math.floor(timeInMilliseconds / 1000); +}; diff --git a/packages/app/src/models/constants.ts b/packages/app/src/models/constants.ts index d0e35e79..d025cd36 100644 --- a/packages/app/src/models/constants.ts +++ b/packages/app/src/models/constants.ts @@ -1,34 +1,41 @@ -export type SupportedTokens = { - CELO: `0x${string}`; - cUSD: `0x${string}`; - WBTC: `0x${string}`; - G$: `0x${string}`; - // RECY: `0x${string}`; - WETH: `0x${string}`; +export enum SupportedNetwork { + celo = 42220, + alfajores = 44787, +} + +export const SupportedNetworkNames: Record = { + [SupportedNetwork.celo]: 'celo', + [SupportedNetwork.alfajores]: 'alfajores', }; -export const tokenMapping: SupportedTokens = { +export const tokenMapping: Record = { CELO: '0x471EcE3750Da237f93B8E339c536989b8978a438', cUSD: '0x765de816845861e75a25fca122bb6898b8b1282a', WBTC: '0xD629eb00dEced2a080B7EC630eF6aC117e614f1b', G$: '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', - // RECY: '0x', WETH: '0x66803FB87aBd4aaC3cbB3fAd7C3aa01f6F3FB207', }; -export const currencyOptions: { value: keyof SupportedTokens; label: keyof SupportedTokens }[] = [ - { value: 'G$', label: 'G$' }, - { value: 'CELO', label: 'CELO' }, - { value: 'cUSD', label: 'cUSD' }, - // { value: 'RECY', label: 'RECY' }, - { value: 'WBTC', label: 'WBTC' }, - { value: 'WETH', label: 'WETH' }, -]; +export type SupportedTokenSymbol = keyof typeof tokenMapping; + +// constructed from tokenMapping +export const currencyOptions: { value: SupportedTokenSymbol; label: SupportedTokenSymbol }[] = Object.keys( + tokenMapping +).map((key) => ({ + value: key as SupportedTokenSymbol, + label: key as SupportedTokenSymbol, +})); + +export enum Frequency { + OneTime = 'One-Time', + Daily = 'Daily', + Weekly = 'Weekly', + Monthly = 'Monthly', + Yearly = 'Yearly', +} -export const frequencyOptions = [ - { value: 'One-Time', label: 'One-Time' }, - { value: 'Daily', label: 'Daily' }, - { value: 'Weekly', label: 'Weekly' }, - { value: 'Monthly', label: 'Monthly' }, - { value: 'Yearly', label: 'Yearly' }, -]; +// constructed from Frequency +export const frequencyOptions: { value: Frequency; label: Frequency }[] = Object.values(Frequency).map((value) => ({ + value, + label: value, +})); diff --git a/packages/app/src/pages/DonatePage.tsx b/packages/app/src/pages/DonatePage.tsx index 68e8d66f..4d03fc72 100644 --- a/packages/app/src/pages/DonatePage.tsx +++ b/packages/app/src/pages/DonatePage.tsx @@ -19,11 +19,7 @@ function DonatePage() { return ( {isDesktopResolution && } - {!ipfsCollective ? ( -

Loading...

- ) : ( - - )} + {!ipfsCollective ?

Loading...

: }
); } diff --git a/packages/app/src/pages/ModalTestPage.tsx b/packages/app/src/pages/ModalTestPage.tsx index 670461c4..9605e591 100644 --- a/packages/app/src/pages/ModalTestPage.tsx +++ b/packages/app/src/pages/ModalTestPage.tsx @@ -1,5 +1,4 @@ import Layout from '../components/Layout'; -import DonateComponent from '../components/DonateComponent'; import SwitchModal from '../components/SwitchModal'; import CompleteDonationModal from '../components/CompleteDonationModal'; import ThankYouModal from '../components/ThankYouModal'; @@ -15,7 +14,7 @@ function ModalTestPage() { - + diff --git a/packages/app/src/utils/index.tsx b/packages/app/src/utils/index.tsx index bdea5140..1824d811 100644 --- a/packages/app/src/utils/index.tsx +++ b/packages/app/src/utils/index.tsx @@ -1,26 +1,36 @@ import { Colors } from './colors'; +import { Frequency } from '../models/constants'; -export function shortenAddress(address: string, length = 3) { - if (!address) return ''; - const start = address.substring(0, length); - const end = address.substring(address.length - length); - return `${start}...${end}`; -} - -export function getButtonBGC(insufficientLiquidity: boolean, priceImpace: boolean, insufficientBalance: boolean) { - if (insufficientLiquidity || insufficientBalance) { +export function getDonateButtonBackgroundColor( + hasAddress: boolean, + isValidChainId: boolean, + insufficientLiquidity: boolean, + priceImpact: boolean, + insufficientBalance: boolean +) { + if (!hasAddress || !isValidChainId || insufficientLiquidity || insufficientBalance) { return Colors.gray[1000]; - } else if (priceImpace) { + } else if (priceImpact) { return Colors.orange[100]; } else { return Colors.green[100]; } } -export function getButtonText(insufficientLiquidity: boolean, priceImpace: boolean, insufficientBalance: boolean) { - if (insufficientLiquidity) { +export function getDonateButtonText( + hasAddress: boolean, + isValidChainId: boolean, + insufficientLiquidity: boolean, + priceImpact: boolean, + insufficientBalance: boolean +) { + if (!hasAddress) { + return 'Please connect wallet'; + } else if (!isValidChainId) { + return 'Unsupported network'; + } else if (insufficientLiquidity) { return 'Insufficient liquidity for this trade'; - } else if (priceImpace) { + } else if (priceImpact) { return 'Confirm & Swap Anyway'; } else if (insufficientBalance) { return 'Confirm & Swap Anyway'; @@ -28,17 +38,30 @@ export function getButtonText(insufficientLiquidity: boolean, priceImpace: boole return 'Confirm'; } } -export function getButtonTextColor(insufficientLiquidity: boolean, priceImpace: boolean, insufficientBalance: boolean) { - if (insufficientLiquidity || insufficientBalance) { +export function getDonateButtonTextColor( + hasAddress: boolean, + isValidChainId: boolean, + insufficientLiquidity: boolean, + priceImpact: boolean, + insufficientBalance: boolean +) { + if ( + !hasAddress || + !isValidChainId || + insufficientLiquidity || + insufficientBalance || + insufficientLiquidity || + insufficientBalance + ) { return Colors.gray[300]; - } else if (priceImpace) { + } else if (priceImpact) { return Colors.black; } else { return Colors.green[200]; } } -export function getFrequencyTime(frequency: string) { +export function getFrequencyPlural(frequency: Frequency) { switch (frequency) { case 'Daily': return 'Days'; @@ -50,7 +73,3 @@ export function getFrequencyTime(frequency: string) { return 'Years'; } } - -export function getTotalAmount(duration: number, amount: number) { - return duration * amount; -} diff --git a/yarn.lock b/yarn.lock index ac6f1435..63452531 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4530,7 +4530,7 @@ __metadata: "@celo-tools/celo-ethers-wrapper": ^0.4.0 "@ethersproject/shims": ^5.7.0 "@gooddollar/good-design": ^0.1.31 - "@gooddollar/goodcollective-sdk": ^1.* + "@gooddollar/goodcollective-sdk": ^1.0.6 "@gooddollar/web3sdk-v2": ^0.2.2 "@react-native-aria/interactions": 0.2.3 "@react-native-async-storage/async-storage": ^1.18.2 @@ -4649,7 +4649,7 @@ __metadata: languageName: unknown linkType: soft -"@gooddollar/goodcollective-sdk@^1.*, @gooddollar/goodcollective-sdk@workspace:packages/sdk-js": +"@gooddollar/goodcollective-sdk@^1.0.6, @gooddollar/goodcollective-sdk@workspace:packages/sdk-js": version: 0.0.0-use.local resolution: "@gooddollar/goodcollective-sdk@workspace:packages/sdk-js" dependencies: From 9abd3c478589db335f17c01a707ad5e35d6f1ed8 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 29 Dec 2023 21:27:17 +0500 Subject: [PATCH 02/18] added useApproveSwapTokenCallback and useSwapRoute --- .../app/src/components/DonateComponent.tsx | 12 ++-- .../src/hooks/useApproveSwapTokenCallback.ts | 54 +++++++++++++++ packages/app/src/hooks/useContractCalls.tsx | 2 +- packages/app/src/hooks/useSwap.tsx | 38 ----------- packages/app/src/hooks/useSwapRoute.tsx | 67 +++++++++++++++++++ 5 files changed, 128 insertions(+), 45 deletions(-) create mode 100644 packages/app/src/hooks/useApproveSwapTokenCallback.ts delete mode 100644 packages/app/src/hooks/useSwap.tsx create mode 100644 packages/app/src/hooks/useSwapRoute.tsx diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 970b0d38..2c061f9c 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -55,6 +55,12 @@ function DonateComponent({ collective }: DonateComponentProps) { const currencyDecimals = useGetTokenDecimals(currency, chain?.id); const donorCurrencyBalance = useGetDecimalBalance(currency as SupportedTokenSymbol, address, chain?.id); + const totalDecimalDonation = duration * decimalDonationAmount; + const totalDonationFormatted = new Decimal(totalDecimalDonation) + .toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN) + .toString(); + + // TODO: need to check approval status and implement token approval flow const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall } = useContractCalls( collectiveId, currency, @@ -64,13 +70,7 @@ function DonateComponent({ collective }: DonateComponentProps) { (error) => setErrorMessage(error) ); - const totalDecimalDonation = duration * decimalDonationAmount; - const totalDonationFormatted = new Decimal(totalDecimalDonation) - .toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN) - .toString(); - const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; - // TODO: determine if there is sufficient liquidity for swap const isInsufficientLiquidity = false; // TODO: determine price impact for swap diff --git a/packages/app/src/hooks/useApproveSwapTokenCallback.ts b/packages/app/src/hooks/useApproveSwapTokenCallback.ts new file mode 100644 index 00000000..2e71b2ab --- /dev/null +++ b/packages/app/src/hooks/useApproveSwapTokenCallback.ts @@ -0,0 +1,54 @@ +import { useAccount, useContractWrite, useNetwork, usePrepareContractWrite } from 'wagmi'; +import { useCallback, useMemo } from 'react'; +import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; +import { SupportedTokenSymbol, tokenMapping } from '../models/constants'; +import { useGetTokenDecimals } from './useGetTokenDecimals'; +import Decimal from 'decimal.js'; +import ERC20 from '../abi/ERC20.json'; + +const V3_ROUTER_ADDRESS = '0x5615CDAb10dc425a742d643d949a7F474C01abc4'; + +export function useApproveSwapTokenCallback( + currencyIn: SupportedTokenSymbol, + decimalAmountIn: number, + duration: number +): { + isLoading: boolean; + isSuccess: boolean; + isError: boolean; + data?: { hash: `0x${string}` }; + handleApproveToken?: () => Promise; +} { + const { address } = useAccount(); + const { chain } = useNetwork(); + const currencyInDecimals = useGetTokenDecimals(currencyIn, chain?.id); + + const rawAmountIn = useMemo( + () => calculateRawTotalDonation(decimalAmountIn, duration, currencyInDecimals).toFixed(0, Decimal.ROUND_DOWN), + [decimalAmountIn, duration, currencyInDecimals] + ); + + const { config } = usePrepareContractWrite({ + chainId: chain?.id, + address: tokenMapping[currencyIn], + abi: ERC20, + account: address, + functionName: 'approve', + args: [V3_ROUTER_ADDRESS, rawAmountIn], + }); + + const { data, isLoading, isSuccess, isError, writeAsync } = useContractWrite(config); + + const handleApproveToken = useCallback(async () => { + const testing = await writeAsync?.(); + return testing?.hash; + }, [writeAsync]); + + return { + isLoading, + isSuccess, + isError, + data, + handleApproveToken, + }; +} diff --git a/packages/app/src/hooks/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls.tsx index 037772dc..f75751e3 100644 --- a/packages/app/src/hooks/useContractCalls.tsx +++ b/packages/app/src/hooks/useContractCalls.tsx @@ -117,7 +117,7 @@ export const useContractCalls = ( minReturn: Number.MAX_SAFE_INTEGER, // TODO: need to get min return using uniswap sdk path: '0x', // TODO: need to get path using uniswap sdk swapFrom: tokenMapping[currency], - deadline: (Date.now() + 18000).toString(), + deadline: Math.floor(Date.now() / 1000 + 1800).toString(), }); await tx.wait(); navigate(`/profile/${address}`); diff --git a/packages/app/src/hooks/useSwap.tsx b/packages/app/src/hooks/useSwap.tsx deleted file mode 100644 index 37e8896f..00000000 --- a/packages/app/src/hooks/useSwap.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// import { AlphaRouter } from '@uniswap/smart-order-router'; -// import { Token, CurrencyAmount, TradeType, Percent, Currency } from '@uniswap/sdk-core'; -// import { useAccount, usePublicClient } from 'wagmi'; -// import * as hre from 'ethers'; - -// // eslint-disable-next-line react-hooks/rules-of-hooks -// const { chain } = useNetwork(); -// // eslint-disable-next-line react-hooks/rules-of-hooks -// const { connector }: any = useConnect(); - -// const provider_ = connector.getProvider(); - -// const router = new AlphaRouter({ chainId: 42220, provider: provider_ }); - -// const tokenIn = new Token(); - -// export default async (userAddress: string, token: Currency) => { -// const V3_ROUTER_ADDRESS = '0x5615CDAb10dc425a742d643d949a7F474C01abc4'; -// const publicClient = usePublicClient(); -// const { address, isConnected } = useAccount(); -// const router = new AlphaRouter({ chainId: 42220, provider: publicClient as any }); -// const inputAmount = CurrencyAmount.fromRawAmount(WETH, JSBI.BigInt(wei)); -// const route = await router.route(inputAmount, token, TradeType.EXACT_INPUT, { -// recipient: userAddress, -// slippageTolerance: new Percent(25, 100), -// deadline: Math.floor(Date.now() / 1000 + 1800), -// }); -// console.log(`Quote Exact In: ${route.quote.toFixed(10)}`); - -// const transaction = { -// data: route.methodParameters.calldata, -// to: V3_ROUTER_ADDRESS, -// value: hre.BigNumber.from(route.methodParameters.value), -// from: address, -// gasPrice: hre.BigNumber.from(route.gasPriceWei), -// gasLimit: hre.utils.hexlify(1000000), -// }; -// }; diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx new file mode 100644 index 00000000..7a05ad44 --- /dev/null +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -0,0 +1,67 @@ +import { AlphaRouter, SwapRoute, SwapType } from '@uniswap/smart-order-router'; +import { Token, CurrencyAmount, TradeType, Percent } from '@uniswap/sdk-core'; +import { useAccount, useNetwork } from 'wagmi'; +import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; +import { useEthersSigner } from './wagmiF'; +import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; +import { useGetTokenDecimals } from './useGetTokenDecimals'; +import Decimal from 'decimal.js'; +import { useEffect, useState } from 'react'; + +export enum SwapRouteState { + LOADING, + READY, + NO_ROUTE, +} + +const GDToken = new Token(SupportedNetwork.celo, tokenMapping.G$, 18); + +export function useSwapRoute( + currencyIn: SupportedTokenSymbol, + decimalAmountIn: number, + duration: number +): { + route?: SwapRoute; + approveTokenCallback?: () => Promise; + status: SwapRouteState; +} { + const { address } = useAccount(); + const { chain } = useNetwork(); + const signer = useEthersSigner({ chainId: chain?.id }); + const currencyInDecimals = useGetTokenDecimals(currencyIn, chain?.id); + + const [route, setRoute] = useState(undefined); + + useEffect(() => { + if (!address || !chain?.id || !signer?.provider) { + setRoute(undefined); + return; + } + const router = new AlphaRouter({ + chainId: chain.id, + provider: signer.provider, + }); + + const inputToken = new Token(chain.id, tokenMapping[currencyIn], currencyInDecimals); + const rawAmountIn = calculateRawTotalDonation(decimalAmountIn, duration, currencyInDecimals); + const inputAmount = CurrencyAmount.fromRawAmount(inputToken, rawAmountIn.toFixed(0, Decimal.ROUND_DOWN)); + router + .route(inputAmount, GDToken, TradeType.EXACT_INPUT, { + type: SwapType.SWAP_ROUTER_02, + recipient: address, + slippageTolerance: new Percent(1, 100), + deadline: Math.floor(Date.now() / 1000 + 1800), + }) + .then((swapRoute) => { + setRoute(swapRoute ?? undefined); + }); + }, [address, chain?.id, signer?.provider, currencyIn, currencyInDecimals, decimalAmountIn, duration]); + + if (!route) { + return { status: SwapRouteState.LOADING }; + } else if (!route.route) { + return { status: SwapRouteState.NO_ROUTE }; + } else { + return { route, status: SwapRouteState.READY }; + } +} From 4e02064e5eedf70ae62a41229a35b6fe0aefbb44 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 30 Dec 2023 12:10:48 +0500 Subject: [PATCH 03/18] draft swap integration --- ...roveSwapModal.tsx => ApproveSwapModal.tsx} | 12 +- .../src/components/CompleteDonationModal.tsx | 1 - .../app/src/components/DonateComponent.tsx | 93 ++++++-- packages/app/src/hooks/index.ts | 2 +- .../src/hooks/useApproveSwapTokenCallback.ts | 20 +- packages/app/src/hooks/useContractCalls.tsx | 214 ------------------ .../useContractCalls/useContractCalls.tsx | 72 ++++++ .../hooks/useContractCalls/useSupportFlow.ts | 69 ++++++ .../useSupportFlowWithSwap.ts | 100 ++++++++ .../useContractCalls/useSupportSingleBatch.ts | 65 ++++++ .../useSupportSingleTransferAndCall.ts | 65 ++++++ packages/app/src/hooks/useSwapRoute.tsx | 23 +- packages/app/src/pages/ModalTestPage.tsx | 4 +- 13 files changed, 481 insertions(+), 259 deletions(-) rename packages/app/src/components/{AproveSwapModal.tsx => ApproveSwapModal.tsx} (87%) delete mode 100644 packages/app/src/hooks/useContractCalls.tsx create mode 100644 packages/app/src/hooks/useContractCalls/useContractCalls.tsx create mode 100644 packages/app/src/hooks/useContractCalls/useSupportFlow.ts create mode 100644 packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts create mode 100644 packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts create mode 100644 packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts diff --git a/packages/app/src/components/AproveSwapModal.tsx b/packages/app/src/components/ApproveSwapModal.tsx similarity index 87% rename from packages/app/src/components/AproveSwapModal.tsx rename to packages/app/src/components/ApproveSwapModal.tsx index 26f295af..f1eb9402 100644 --- a/packages/app/src/components/AproveSwapModal.tsx +++ b/packages/app/src/components/ApproveSwapModal.tsx @@ -9,18 +9,22 @@ interface AproveSwapModalProps { setOpenModal: any; } -const AproveSwapModal = ({ openModal, setOpenModal }: AproveSwapModalProps) => { +const ApproveSwapModal = ({ openModal, setOpenModal }: AproveSwapModalProps) => { return ( - + { + setOpenModal(false); + }}> - APROVE TOKEN SWAP + APPROVE TOKEN SWAP To approve the exchange from your donation currency to this GoodCollective's currency, sign with your wallet. @@ -112,4 +116,4 @@ const styles = StyleSheet.create({ }, }); -export default AproveSwapModal; +export default ApproveSwapModal; diff --git a/packages/app/src/components/CompleteDonationModal.tsx b/packages/app/src/components/CompleteDonationModal.tsx index f19799c4..fb7dfd8b 100644 --- a/packages/app/src/components/CompleteDonationModal.tsx +++ b/packages/app/src/components/CompleteDonationModal.tsx @@ -22,7 +22,6 @@ const CompleteDonationModal = ({ openModal, setOpenModal }: CompleteDonationModa style={modalStyles.modalCloseIcon} onPress={() => { setOpenModal(false); - console.log(openModal); }}>
diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 2c061f9c..cfe66e6d 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { Image, StyleSheet, Text, TextInput, View } from 'react-native'; import { InterRegular, InterSemiBold, InterSmall } from '../utils/webFonts'; import RoundedButton from './RoundedButton'; @@ -29,6 +29,10 @@ import { useGetTokenDecimals } from '../hooks/useGetTokenDecimals'; import Decimal from 'decimal.js'; import { formatFiatCurrency } from '../lib/formatFiatCurrency'; import ErrorModal from './ErrorModal'; +import { SwapRouteState, useSwapRoute } from '../hooks/useSwapRoute'; +import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallback'; +import ApproveSwapModal from './ApproveSwapModal'; +import { waitForTransaction } from '@wagmi/core'; interface DonateComponentProps { collective: IpfsCollective; @@ -46,35 +50,87 @@ function DonateComponent({ collective }: DonateComponentProps) { const [completeDonationModalVisible, setCompleteDonationModalVisible] = useState(false); const [errorMessage, setErrorMessage] = useState(undefined); + const [approveSwapModalVisible, setApproveSwapModalVisible] = useState(false); const [currency, setCurrency] = useState('G$'); const [frequency, setFrequency] = useState(Frequency.OneTime); const [duration, setDuration] = useState(1); const [decimalDonationAmount, setDecimalDonationAmount] = useState(0); - const currencyDecimals = useGetTokenDecimals(currency, chain?.id); - const donorCurrencyBalance = useGetDecimalBalance(currency as SupportedTokenSymbol, address, chain?.id); + const { + path: swapPath, + rawMinimumAmountOut, + priceImpact, + status: swapRouteStatus, + } = useSwapRoute(currency, decimalDonationAmount, duration); - const totalDecimalDonation = duration * decimalDonationAmount; - const totalDonationFormatted = new Decimal(totalDecimalDonation) - .toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN) - .toString(); + const { + handleApproveToken, + isLoading: handleApproveTokenIsLoading, + isError: handleApproveTokenIsError, + } = useApproveSwapTokenCallback(currency, decimalDonationAmount, duration); - // TODO: need to check approval status and implement token approval flow const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall } = useContractCalls( collectiveId, currency, decimalDonationAmount, duration, frequency, - (error) => setErrorMessage(error) + (error) => setErrorMessage(error), + () => setCompleteDonationModalVisible(!completeDonationModalVisible), + rawMinimumAmountOut, + swapPath ); + const handleDonate = useCallback(async () => { + if (frequency === Frequency.OneTime) { + await supportSingleTransferAndCall(); + } else if (currency === 'G$') { + await supportFlow(); + } else { + while (handleApproveTokenIsLoading) { + await new Promise((r) => setTimeout(r, 50)); + } + if (handleApproveTokenIsError || handleApproveToken === undefined) { + setErrorMessage('An error occurred while generating a transaction to approve the token.'); + return; + } + const txHash = await handleApproveToken(); + setApproveSwapModalVisible(!approveSwapModalVisible); + const txReceipt = await waitForTransaction({ + chainId: chain?.id, + confirmations: 1, + hash: txHash, + timeout: 1000 * 60 * 5, + }); + if (txReceipt.status === 'success') { + await supportFlowWithSwap(); + } + } + }, [ + approveSwapModalVisible, + chain?.id, + currency, + frequency, + handleApproveToken, + handleApproveTokenIsError, + handleApproveTokenIsLoading, + supportFlow, + supportFlowWithSwap, + supportSingleTransferAndCall, + ]); + + const currencyDecimals = useGetTokenDecimals(currency, chain?.id); + const donorCurrencyBalance = useGetDecimalBalance(currency as SupportedTokenSymbol, address, chain?.id); + + const totalDecimalDonation = duration * decimalDonationAmount; + const totalDonationFormatted = new Decimal(totalDecimalDonation) + .toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN) + .toString(); + const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; - // TODO: determine if there is sufficient liquidity for swap - const isInsufficientLiquidity = false; - // TODO: determine price impact for swap - const isUnacceptablePriceImpact = false; + const isInsufficientLiquidity = swapRouteStatus === SwapRouteState.NO_ROUTE; + const isUnacceptablePriceImpact = priceImpact ? priceImpact.gte(0.1) : false; const { price } = useGetTokenPrice(currency as SupportedTokenSymbol); const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; @@ -401,15 +457,7 @@ function DonateComponent({ collective }: DonateComponentProps) { )} fontSize={18} seeType={false} - onPress={() => { - if (frequency === Frequency.OneTime) { - supportSingleTransferAndCall(); - } else if (currency === 'G$') { - supportFlow(); - } else { - supportFlowWithSwap(); - } - }} + onPress={handleDonate} disabled={address === undefined || chain?.id === undefined || !(chain.id in SupportedNetwork)} /> @@ -418,6 +466,7 @@ function DonateComponent({ collective }: DonateComponentProps) { setOpenModal={() => setErrorMessage(undefined)} message={errorMessage ?? ''} /> + ); diff --git a/packages/app/src/hooks/index.ts b/packages/app/src/hooks/index.ts index e74029e9..dc9ebc40 100644 --- a/packages/app/src/hooks/index.ts +++ b/packages/app/src/hooks/index.ts @@ -2,6 +2,6 @@ export * from './useCollectiveById'; export * from './useDonorById'; export * from './useIsDonorOfCollective'; export * from './useStewardById'; -export * from './useContractCalls'; +export * from './useContractCalls/useContractCalls'; export * from './useGetTokenPrice'; export * from './wagmiF'; diff --git a/packages/app/src/hooks/useApproveSwapTokenCallback.ts b/packages/app/src/hooks/useApproveSwapTokenCallback.ts index 2e71b2ab..320aa6d9 100644 --- a/packages/app/src/hooks/useApproveSwapTokenCallback.ts +++ b/packages/app/src/hooks/useApproveSwapTokenCallback.ts @@ -1,11 +1,12 @@ import { useAccount, useContractWrite, useNetwork, usePrepareContractWrite } from 'wagmi'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; import { SupportedTokenSymbol, tokenMapping } from '../models/constants'; import { useGetTokenDecimals } from './useGetTokenDecimals'; import Decimal from 'decimal.js'; import ERC20 from '../abi/ERC20.json'; +// Uniswap V3 Router on Celo const V3_ROUTER_ADDRESS = '0x5615CDAb10dc425a742d643d949a7F474C01abc4'; export function useApproveSwapTokenCallback( @@ -16,8 +17,7 @@ export function useApproveSwapTokenCallback( isLoading: boolean; isSuccess: boolean; isError: boolean; - data?: { hash: `0x${string}` }; - handleApproveToken?: () => Promise; + handleApproveToken?: () => Promise<`0x${string}`>; } { const { address } = useAccount(); const { chain } = useNetwork(); @@ -37,18 +37,22 @@ export function useApproveSwapTokenCallback( args: [V3_ROUTER_ADDRESS, rawAmountIn], }); - const { data, isLoading, isSuccess, isError, writeAsync } = useContractWrite(config); + const { isLoading, isSuccess, isError, writeAsync } = useContractWrite(config); - const handleApproveToken = useCallback(async () => { - const testing = await writeAsync?.(); - return testing?.hash; + const handleApproveToken = useMemo(() => { + if (!writeAsync) { + return undefined; + } + return async () => { + const result = await writeAsync(); + return result.hash; + }; }, [writeAsync]); return { isLoading, isSuccess, isError, - data, handleApproveToken, }; } diff --git a/packages/app/src/hooks/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls.tsx deleted file mode 100644 index f75751e3..00000000 --- a/packages/app/src/hooks/useContractCalls.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; -import { useAccount, useNetwork } from 'wagmi'; -import { useEthersSigner } from './wagmiF'; -import useCrossNavigate from '../routes/useCrossNavigate'; -import { - Frequency, - SupportedNetwork, - SupportedNetworkNames, - SupportedTokenSymbol, - tokenMapping, -} from '../models/constants'; -import { useGetTokenDecimals } from './useGetTokenDecimals'; -import { useCallback } from 'react'; -import { calculateFlowRate } from '../lib/calculateFlowRate'; -import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; -import Decimal from 'decimal.js'; - -interface ContractCalls { - supportFlowWithSwap: () => Promise; - supportFlow: () => Promise; - supportSingleTransferAndCall: () => Promise; - supportSingleBatch: () => Promise; -} - -export const useContractCalls = ( - collective: string, - currency: SupportedTokenSymbol, - decimalAmountIn: number, - duration: number, - frequency: Frequency, - onError: (error: string) => void -): ContractCalls => { - const { address } = useAccount(); - const { chain } = useNetwork(); - const signer = useEthersSigner({ chainId: chain?.id }); - const currencyDecimals = useGetTokenDecimals(currency, chain?.id); - const { navigate } = useCrossNavigate(); - - const supportFlow = useCallback(async () => { - if (!address) { - onError('No address found. Please connect your wallet.'); - return; - } - if (!chain?.id || !(chain?.id in SupportedNetwork)) { - onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); - return; - } - if (!signer) { - onError('Failed to get signer.'); - return; - } - - const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); - if (!flowRate) { - onError('Failed to calculate flow rate.'); - return; - } - - const chainIdString = chain.id.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chain.id as SupportedNetwork]; - - try { - const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportFlow(signer, collective, flowRate); - await tx.wait(); - navigate(`/profile/${address}`); - return; - } catch (error) { - onError(`An unexpected error occurred: ${error}`); - } - }, [ - address, - chain?.id, - collective, - currencyDecimals, - decimalAmountIn, - duration, - frequency, - navigate, - onError, - signer, - ]); - - const supportFlowWithSwap = useCallback(async () => { - if (!address) { - onError('No address found. Please connect your wallet.'); - return; - } - if (!chain?.id || !(chain?.id in SupportedNetwork)) { - onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); - return; - } - if (!signer) { - onError('Failed to get signer.'); - return; - } - - const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); - if (!flowRate) { - onError('Failed to calculate flow rate.'); - return; - } - - const chainIdString = chain.id.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chain.id as SupportedNetwork]; - - // swap values - const amountIn = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( - 0, - Decimal.ROUND_DOWN - ); - - try { - const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportFlowWithSwap(signer, collective, flowRate, { - amount: amountIn, - minReturn: Number.MAX_SAFE_INTEGER, // TODO: need to get min return using uniswap sdk - path: '0x', // TODO: need to get path using uniswap sdk - swapFrom: tokenMapping[currency], - deadline: Math.floor(Date.now() / 1000 + 1800).toString(), - }); - await tx.wait(); - navigate(`/profile/${address}`); - return; - } catch (error) { - onError(`An unexpected error occurred: ${error}`); - } - }, [ - address, - chain?.id, - collective, - currency, - currencyDecimals, - decimalAmountIn, - duration, - frequency, - navigate, - onError, - signer, - ]); - - const supportSingleTransferAndCall = useCallback(async () => { - if (!address) { - onError('No address found. Please connect your wallet.'); - return; - } - if (!chain?.id || !(chain?.id in SupportedNetwork)) { - onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); - return; - } - if (!signer) { - onError('Failed to get signer.'); - return; - } - - const chainIdString = chain.id.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chain.id as SupportedNetwork]; - - const donationAmount = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( - 0, - Decimal.ROUND_DOWN - ); - - try { - const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); - await tx.wait(); - navigate(`/profile/${address}`); - return; - } catch (error) { - onError(`An unexpected error occurred: ${error}`); - } - }, [address, chain?.id, collective, currencyDecimals, decimalAmountIn, duration, navigate, onError, signer]); - - const supportSingleBatch = useCallback(async () => { - if (!address) { - onError('No address found. Please connect your wallet.'); - return; - } - if (!chain?.id || !(chain?.id in SupportedNetwork)) { - onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); - return; - } - if (!signer) { - onError('Failed to get signer.'); - return; - } - - const chainIdString = chain.id.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chain.id as SupportedNetwork]; - - const donationAmount = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( - 0, - Decimal.ROUND_DOWN - ); - - try { - const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); - await tx.wait(); - navigate(`/profile/${address}`); - return; - } catch (error) { - onError(`An unexpected error occurred: ${error}`); - } - }, [address, chain?.id, collective, currencyDecimals, decimalAmountIn, duration, navigate, onError, signer]); - - return { - supportFlow, - supportFlowWithSwap, - supportSingleTransferAndCall, - supportSingleBatch, - }; -}; diff --git a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx new file mode 100644 index 00000000..c13def57 --- /dev/null +++ b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx @@ -0,0 +1,72 @@ +import { useNetwork } from 'wagmi'; +import { Frequency, SupportedTokenSymbol } from '../../models/constants'; +import { useGetTokenDecimals } from '../useGetTokenDecimals'; +import { useSupportFlow } from './useSupportFlow'; +import { useSupportFlowWithSwap } from './useSupportFlowWithSwap'; +import { useSupportSingleTransferAndCall } from './useSupportSingleTransferAndCall'; +import { useSupportSingleBatch } from './useSupportSingleBatch'; + +interface ContractCalls { + supportFlowWithSwap: () => Promise; + supportFlow: () => Promise; + supportSingleTransferAndCall: () => Promise; + supportSingleBatch: () => Promise; +} + +export const useContractCalls = ( + collective: string, + currency: SupportedTokenSymbol, + decimalAmountIn: number, + duration: number, + frequency: Frequency, + onError: (error: string) => void, + toggleCompleteDonationModal: () => void, + minReturnFromSwap?: string, + swapPath?: string +): ContractCalls => { + const { chain } = useNetwork(); + const currencyDecimals = useGetTokenDecimals(currency, chain?.id); + + const supportFlow = useSupportFlow( + collective, + currencyDecimals, + decimalAmountIn, + duration, + frequency, + onError, + toggleCompleteDonationModal + ); + const supportFlowWithSwap = useSupportFlowWithSwap( + collective, + currency, + currencyDecimals, + decimalAmountIn, + duration, + frequency, + onError, + toggleCompleteDonationModal, + minReturnFromSwap, + swapPath + ); + const supportSingleTransferAndCall = useSupportSingleTransferAndCall( + collective, + currencyDecimals, + decimalAmountIn, + onError, + toggleCompleteDonationModal + ); + const supportSingleBatch = useSupportSingleBatch( + collective, + currencyDecimals, + decimalAmountIn, + onError, + toggleCompleteDonationModal + ); + + return { + supportFlow, + supportFlowWithSwap, + supportSingleTransferAndCall, + supportSingleBatch, + }; +}; diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts new file mode 100644 index 00000000..93c78f88 --- /dev/null +++ b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts @@ -0,0 +1,69 @@ +import { useCallback } from 'react'; +import { Frequency, SupportedNetwork, SupportedNetworkNames } from '../../models/constants'; +import { calculateFlowRate } from '../../lib/calculateFlowRate'; +import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; +import { useAccount, useNetwork } from 'wagmi'; +import { useEthersSigner } from '../wagmiF'; +import useCrossNavigate from '../../routes/useCrossNavigate'; + +export function useSupportFlow( + collective: string, + currencyDecimals: number, + decimalAmountIn: number, + duration: number, + frequency: Frequency, + onError: (error: string) => void, + toggleCompleteDonationModal: () => void +) { + const { address } = useAccount(); + const { chain } = useNetwork(); + const signer = useEthersSigner({ chainId: chain?.id }); + const { navigate } = useCrossNavigate(); + + return useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + + const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); + if (!flowRate) { + onError('Failed to calculate flow rate.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + try { + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportFlow(signer, collective, flowRate); + toggleCompleteDonationModal(); + await tx.wait(); + navigate(`/profile/${address}`); + return; + } catch (error) { + onError(`An unexpected error occurred: ${error}`); + } + }, [ + address, + chain?.id, + collective, + currencyDecimals, + decimalAmountIn, + duration, + frequency, + navigate, + onError, + signer, + toggleCompleteDonationModal, + ]); +} diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts new file mode 100644 index 00000000..649e2d14 --- /dev/null +++ b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts @@ -0,0 +1,100 @@ +import { useCallback } from 'react'; +import { + Frequency, + SupportedNetwork, + SupportedNetworkNames, + SupportedTokenSymbol, + tokenMapping, +} from '../../models/constants'; +import { calculateFlowRate } from '../../lib/calculateFlowRate'; +import { calculateRawTotalDonation } from '../../lib/calculateRawTotalDonation'; +import Decimal from 'decimal.js'; +import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; +import { useAccount, useNetwork } from 'wagmi'; +import { useEthersSigner } from '../wagmiF'; +import useCrossNavigate from '../../routes/useCrossNavigate'; + +export function useSupportFlowWithSwap( + collective: string, + currency: SupportedTokenSymbol, + currencyDecimals: number, + decimalAmountIn: number, + duration: number, + frequency: Frequency, + onError: (error: string) => void, + toggleCompleteDonationModal: () => void, + minReturnFromSwap?: string, + swapPath?: string +) { + const { address } = useAccount(); + const { chain } = useNetwork(); + const signer = useEthersSigner({ chainId: chain?.id }); + const { navigate } = useCrossNavigate(); + + return useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + + if (!minReturnFromSwap || !swapPath) { + onError('Swap route not ready.'); + return; + } + + const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); + if (!flowRate) { + onError('Failed to calculate flow rate.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + // swap values + const amountIn = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( + 0, + Decimal.ROUND_DOWN + ); + + try { + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportFlowWithSwap(signer, collective, flowRate, { + amount: amountIn, + minReturn: minReturnFromSwap, + path: swapPath, + swapFrom: tokenMapping[currency], + deadline: Math.floor(Date.now() / 1000 + 1800).toString(), + }); + toggleCompleteDonationModal(); + await tx.wait(); + navigate(`/profile/${address}`); + return; + } catch (error) { + onError(`An unexpected error occurred: ${error}`); + } + }, [ + address, + chain?.id, + collective, + currency, + currencyDecimals, + decimalAmountIn, + duration, + frequency, + navigate, + onError, + signer, + minReturnFromSwap, + swapPath, + toggleCompleteDonationModal, + ]); +} diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts new file mode 100644 index 00000000..759d572b --- /dev/null +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts @@ -0,0 +1,65 @@ +import { useCallback } from 'react'; +import { SupportedNetwork, SupportedNetworkNames } from '../../models/constants'; +import { calculateRawTotalDonation } from '../../lib/calculateRawTotalDonation'; +import Decimal from 'decimal.js'; +import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; +import { useAccount, useNetwork } from 'wagmi'; +import { useEthersSigner } from '../wagmiF'; +import useCrossNavigate from '../../routes/useCrossNavigate'; + +export function useSupportSingleBatch( + collective: string, + currencyDecimals: number, + decimalAmountIn: number, + onError: (error: string) => void, + toggleCompleteDonationModal: () => void +) { + const { address } = useAccount(); + const { chain } = useNetwork(); + const signer = useEthersSigner({ chainId: chain?.id }); + const { navigate } = useCrossNavigate(); + + return useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + const donationAmount = calculateRawTotalDonation(decimalAmountIn, 1, currencyDecimals).toFixed( + 0, + Decimal.ROUND_DOWN + ); + + try { + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); + toggleCompleteDonationModal(); + await tx.wait(); + navigate(`/profile/${address}`); + return; + } catch (error) { + onError(`An unexpected error occurred: ${error}`); + } + }, [ + address, + chain?.id, + collective, + currencyDecimals, + decimalAmountIn, + navigate, + onError, + signer, + toggleCompleteDonationModal, + ]); +} diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts new file mode 100644 index 00000000..17501b6c --- /dev/null +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts @@ -0,0 +1,65 @@ +import { useCallback } from 'react'; +import { SupportedNetwork, SupportedNetworkNames } from '../../models/constants'; +import { calculateRawTotalDonation } from '../../lib/calculateRawTotalDonation'; +import Decimal from 'decimal.js'; +import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; +import { useAccount, useNetwork } from 'wagmi'; +import { useEthersSigner } from '../wagmiF'; +import useCrossNavigate from '../../routes/useCrossNavigate'; + +export function useSupportSingleTransferAndCall( + collective: string, + currencyDecimals: number, + decimalAmountIn: number, + onError: (error: string) => void, + toggleCompleteDonationModal: () => void +) { + const { address } = useAccount(); + const { chain } = useNetwork(); + const signer = useEthersSigner({ chainId: chain?.id }); + const { navigate } = useCrossNavigate(); + + return useCallback(async () => { + if (!address) { + onError('No address found. Please connect your wallet.'); + return; + } + if (!chain?.id || !(chain?.id in SupportedNetwork)) { + onError('Unsupported network. Please connect to Celo Mainnet or Celo Alfajores.'); + return; + } + if (!signer) { + onError('Failed to get signer.'); + return; + } + + const chainIdString = chain.id.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chain.id as SupportedNetwork]; + + const donationAmount = calculateRawTotalDonation(decimalAmountIn, 1, currencyDecimals).toFixed( + 0, + Decimal.ROUND_DOWN + ); + + try { + const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); + toggleCompleteDonationModal(); + await tx.wait(); + navigate(`/profile/${address}`); + return; + } catch (error) { + onError(`An unexpected error occurred: ${error}`); + } + }, [ + address, + chain?.id, + collective, + currencyDecimals, + decimalAmountIn, + navigate, + onError, + signer, + toggleCompleteDonationModal, + ]); +} diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 7a05ad44..df71983c 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,5 +1,5 @@ -import { AlphaRouter, SwapRoute, SwapType } from '@uniswap/smart-order-router'; -import { Token, CurrencyAmount, TradeType, Percent } from '@uniswap/sdk-core'; +import { AlphaRouter, SwapRoute, SwapType, V3Route } from '@uniswap/smart-order-router'; +import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'; import { useAccount, useNetwork } from 'wagmi'; import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; import { useEthersSigner } from './wagmiF'; @@ -7,6 +7,7 @@ import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; import { useGetTokenDecimals } from './useGetTokenDecimals'; import Decimal from 'decimal.js'; import { useEffect, useState } from 'react'; +import { encodeRouteToPath } from '@uniswap/v3-sdk'; export enum SwapRouteState { LOADING, @@ -19,10 +20,13 @@ const GDToken = new Token(SupportedNetwork.celo, tokenMapping.G$, 18); export function useSwapRoute( currencyIn: SupportedTokenSymbol, decimalAmountIn: number, - duration: number + duration: number, + slippageTolerance: Percent = new Percent(1, 100) ): { - route?: SwapRoute; - approveTokenCallback?: () => Promise; + path?: string; + quote?: Decimal; + rawMinimumAmountOut?: string; + priceImpact?: Decimal; status: SwapRouteState; } { const { address } = useAccount(); @@ -59,9 +63,14 @@ export function useSwapRoute( if (!route) { return { status: SwapRouteState.LOADING }; - } else if (!route.route) { + } else if (!route.methodParameters) { return { status: SwapRouteState.NO_ROUTE }; } else { - return { route, status: SwapRouteState.READY }; + // This typecast is safe because Uniswap v2 is not deployed on Celo + const path = encodeRouteToPath(route.route[0].route as V3Route, false); + const quote = new Decimal(route.quote.toFixed(18)); + const rawMinimumAmountOut = route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); + const priceImpact = new Decimal(route.trade.priceImpact.toFixed(4)); + return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY }; } } diff --git a/packages/app/src/pages/ModalTestPage.tsx b/packages/app/src/pages/ModalTestPage.tsx index 9605e591..074b6659 100644 --- a/packages/app/src/pages/ModalTestPage.tsx +++ b/packages/app/src/pages/ModalTestPage.tsx @@ -3,7 +3,7 @@ import SwitchModal from '../components/SwitchModal'; import CompleteDonationModal from '../components/CompleteDonationModal'; import ThankYouModal from '../components/ThankYouModal'; import ErrorModal from '../components/ErrorModal'; -import AproveSwapModal from '../components/AproveSwapModal'; +import ApproveSwapModal from '../components/ApproveSwapModal'; import StopDonationModal from '../components/StopDonationModal'; import { useState } from 'react'; @@ -15,7 +15,7 @@ function ModalTestPage() { - + ); From 31ff9aebe44e17528c7be190396d27d44a46c5d6 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 30 Dec 2023 12:12:44 +0500 Subject: [PATCH 04/18] added index to hooks/useContractCalls --- packages/app/src/hooks/index.ts | 2 +- packages/app/src/hooks/useContractCalls/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 packages/app/src/hooks/useContractCalls/index.ts diff --git a/packages/app/src/hooks/index.ts b/packages/app/src/hooks/index.ts index dc9ebc40..e74029e9 100644 --- a/packages/app/src/hooks/index.ts +++ b/packages/app/src/hooks/index.ts @@ -2,6 +2,6 @@ export * from './useCollectiveById'; export * from './useDonorById'; export * from './useIsDonorOfCollective'; export * from './useStewardById'; -export * from './useContractCalls/useContractCalls'; +export * from './useContractCalls'; export * from './useGetTokenPrice'; export * from './wagmiF'; diff --git a/packages/app/src/hooks/useContractCalls/index.ts b/packages/app/src/hooks/useContractCalls/index.ts new file mode 100644 index 00000000..e9703121 --- /dev/null +++ b/packages/app/src/hooks/useContractCalls/index.ts @@ -0,0 +1 @@ +export * from './useContractCalls'; From 5d33d5904967675bad946c79bbaa923b1e78eb05 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 30 Dec 2023 13:25:05 +0500 Subject: [PATCH 05/18] prevented swap from G$ to G$ --- packages/app/src/components/ConnectWallet.tsx | 8 ---- .../app/src/components/DonateComponent.tsx | 46 +++++++++---------- packages/app/src/components/Dropdown.tsx | 1 + .../components/Header/ConnectWalletMenu.tsx | 6 +-- packages/app/src/hooks/useGetTokenPrice.tsx | 13 +++--- packages/app/src/hooks/useSwapRoute.tsx | 15 ++++-- packages/app/src/models/constants.ts | 11 ++++- 7 files changed, 55 insertions(+), 45 deletions(-) delete mode 100644 packages/app/src/components/ConnectWallet.tsx diff --git a/packages/app/src/components/ConnectWallet.tsx b/packages/app/src/components/ConnectWallet.tsx deleted file mode 100644 index 4e862a1b..00000000 --- a/packages/app/src/components/ConnectWallet.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Button } from 'native-base'; -import { useConnectWallet } from '@web3-onboard/react'; - -export const ConnectWallet = () => { - const [, connect] = useConnectWallet(); - const walletConnect = () => connect(); - return ; -}; diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index cfe66e6d..03b9e27d 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -83,27 +83,27 @@ function DonateComponent({ collective }: DonateComponentProps) { ); const handleDonate = useCallback(async () => { - if (frequency === Frequency.OneTime) { - await supportSingleTransferAndCall(); - } else if (currency === 'G$') { - await supportFlow(); - } else { - while (handleApproveTokenIsLoading) { - await new Promise((r) => setTimeout(r, 50)); - } - if (handleApproveTokenIsError || handleApproveToken === undefined) { - setErrorMessage('An error occurred while generating a transaction to approve the token.'); - return; - } - const txHash = await handleApproveToken(); - setApproveSwapModalVisible(!approveSwapModalVisible); - const txReceipt = await waitForTransaction({ - chainId: chain?.id, - confirmations: 1, - hash: txHash, - timeout: 1000 * 60 * 5, - }); - if (txReceipt.status === 'success') { + while (handleApproveTokenIsLoading) { + await new Promise((r) => setTimeout(r, 50)); + } + if (handleApproveTokenIsError || handleApproveToken === undefined) { + setErrorMessage('An error occurred while generating a transaction to approve the token.'); + return; + } + const txHash = await handleApproveToken(); + setApproveSwapModalVisible(!approveSwapModalVisible); + const txReceipt = await waitForTransaction({ + chainId: chain?.id, + confirmations: 1, + hash: txHash, + timeout: 1000 * 60 * 5, + }); + if (txReceipt.status === 'success') { + if (frequency === Frequency.OneTime) { + await supportSingleTransferAndCall(); + } else if (currency === 'G$') { + await supportFlow(); + } else { await supportFlowWithSwap(); } } @@ -129,8 +129,8 @@ function DonateComponent({ collective }: DonateComponentProps) { .toString(); const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; - const isInsufficientLiquidity = swapRouteStatus === SwapRouteState.NO_ROUTE; - const isUnacceptablePriceImpact = priceImpact ? priceImpact.gte(0.1) : false; + const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus === SwapRouteState.NO_ROUTE; + const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact.gte(0.1) : false; const { price } = useGetTokenPrice(currency as SupportedTokenSymbol); const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; diff --git a/packages/app/src/components/Dropdown.tsx b/packages/app/src/components/Dropdown.tsx index 1681e1c6..0847880b 100644 --- a/packages/app/src/components/Dropdown.tsx +++ b/packages/app/src/components/Dropdown.tsx @@ -51,6 +51,7 @@ function Dropdown({ onSelect, value, options }: DropdownProps) { {options.map((option) => ( { onSelect(option.value); diff --git a/packages/app/src/components/Header/ConnectWalletMenu.tsx b/packages/app/src/components/Header/ConnectWalletMenu.tsx index c2772a26..4104dc58 100644 --- a/packages/app/src/components/Header/ConnectWalletMenu.tsx +++ b/packages/app/src/components/Header/ConnectWalletMenu.tsx @@ -37,7 +37,7 @@ export const ConnectWalletMenu = (props: ConnectWalletMenuProps) => { {connectors.map( (connector, i) => (connector.ready || isLoading) && ( - <> + { resizeMode="contain" style={[styles.walletConnectorLogo]} /> - + {connector.name} {isLoading && connector.id === pendingConnector?.id && ' (connecting)'} {i < connectors.length - 1 && } - + ) )} diff --git a/packages/app/src/hooks/useGetTokenPrice.tsx b/packages/app/src/hooks/useGetTokenPrice.tsx index 5577282a..422cb8e1 100644 --- a/packages/app/src/hooks/useGetTokenPrice.tsx +++ b/packages/app/src/hooks/useGetTokenPrice.tsx @@ -1,6 +1,6 @@ import axios from 'axios'; import { useEffect, useState } from 'react'; -import { SupportedTokenSymbol, tokenMapping } from '../models/constants'; +import { coingeckoTokenMapping, SupportedTokenSymbol } from '../models/constants'; export const useGetTokenPrice = (currency: SupportedTokenSymbol): { price?: number; isLoading: boolean } => { const [price, setPrice] = useState(undefined); @@ -8,8 +8,7 @@ export const useGetTokenPrice = (currency: SupportedTokenSymbol): { price?: numb useEffect(() => { setIsLoading(true); - const tokenAddress = tokenMapping[currency]; - getTokenPrice(tokenAddress).then((res: number | undefined) => { + getTokenPrice(currency).then((res: number | undefined) => { setPrice(res); }); setIsLoading(false); @@ -18,13 +17,15 @@ export const useGetTokenPrice = (currency: SupportedTokenSymbol): { price?: numb return { price, isLoading }; }; -const getTokenPrice = (contractAddress: string): Promise => { +const getTokenPrice = (currency: string): Promise => { + let tokenAddress = coingeckoTokenMapping[currency]; + const url = `https://api.coingecko.com/api/v3/simple/token_price/celo?contract_addresses=${coingeckoTokenMapping[currency]}&vs_currencies=usd`; return axios - .get(`https://api.coingecko.com/api/v3/coins/celo/contract/${contractAddress}`, { + .get(url, { withCredentials: false, }) .then((res) => { - return res.data.market_data.current_price.usd; + return res.data[tokenAddress.toLowerCase()].usd; }) .catch((err) => { console.error(err); diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index df71983c..e867a671 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -37,7 +37,7 @@ export function useSwapRoute( const [route, setRoute] = useState(undefined); useEffect(() => { - if (!address || !chain?.id || !signer?.provider) { + if (!address || !chain?.id || !signer?.provider || currencyIn === 'G$') { setRoute(undefined); return; } @@ -53,13 +53,22 @@ export function useSwapRoute( .route(inputAmount, GDToken, TradeType.EXACT_INPUT, { type: SwapType.SWAP_ROUTER_02, recipient: address, - slippageTolerance: new Percent(1, 100), + slippageTolerance: slippageTolerance, deadline: Math.floor(Date.now() / 1000 + 1800), }) .then((swapRoute) => { setRoute(swapRoute ?? undefined); }); - }, [address, chain?.id, signer?.provider, currencyIn, currencyInDecimals, decimalAmountIn, duration]); + }, [ + address, + chain?.id, + signer?.provider, + currencyIn, + currencyInDecimals, + decimalAmountIn, + duration, + slippageTolerance, + ]); if (!route) { return { status: SwapRouteState.LOADING }; diff --git a/packages/app/src/models/constants.ts b/packages/app/src/models/constants.ts index d025cd36..550e5983 100644 --- a/packages/app/src/models/constants.ts +++ b/packages/app/src/models/constants.ts @@ -10,14 +10,21 @@ export const SupportedNetworkNames: Record = { CELO: '0x471EcE3750Da237f93B8E339c536989b8978a438', - cUSD: '0x765de816845861e75a25fca122bb6898b8b1282a', - WBTC: '0xD629eb00dEced2a080B7EC630eF6aC117e614f1b', + cUSD: '0x765DE816845861e75A25fCA122bb6898B8B1282a', + USDC: '0x37f750B7cC259A2f741AF45294f6a16572CF5cAd', + WBTC: '0xd71Ffd0940c920786eC4DbB5A12306669b5b81EF', G$: '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', WETH: '0x66803FB87aBd4aaC3cbB3fAd7C3aa01f6F3FB207', }; export type SupportedTokenSymbol = keyof typeof tokenMapping; +export const coingeckoTokenMapping: Record = { + ...tokenMapping, + WBTC: '0xD629eb00dEced2a080B7EC630eF6aC117e614f1b', + WETH: '0x2def4285787d58a2f811af24755a8150622f4361', +}; + // constructed from tokenMapping export const currencyOptions: { value: SupportedTokenSymbol; label: SupportedTokenSymbol }[] = Object.keys( tokenMapping From 56cd466020b3ace936f7c98cea9ed5ce4a5c3587 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 30 Dec 2023 17:05:54 +0500 Subject: [PATCH 06/18] fixed formatting and some swap bugs --- packages/app/package.json | 1 + .../app/src/components/DonateComponent.tsx | 85 +++++++++++-------- .../src/hooks/useApproveSwapTokenCallback.ts | 16 ++-- .../useContractCalls/useContractCalls.tsx | 2 +- .../hooks/useContractCalls/useSupportFlow.ts | 5 +- .../useSupportFlowWithSwap.ts | 5 +- .../useContractCalls/useSupportSingleBatch.ts | 5 +- .../useSupportSingleTransferAndCall.ts | 5 +- packages/app/src/hooks/useCreateStream.tsx | 52 ------------ packages/app/src/hooks/useGetTokenPrice.tsx | 4 +- packages/app/src/hooks/useIPFS.tsx | 68 --------------- packages/app/src/hooks/useSwapRoute.tsx | 2 +- yarn.lock | 8 ++ 13 files changed, 84 insertions(+), 174 deletions(-) delete mode 100644 packages/app/src/hooks/useCreateStream.tsx delete mode 100644 packages/app/src/hooks/useIPFS.tsx diff --git a/packages/app/package.json b/packages/app/package.json index 654b5952..377de5eb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -44,6 +44,7 @@ "ethers": "^5.6.2", "fast-text-encoding": "^1.0.6", "graphql": "^16.6.0", + "jsbi": "3.1.4", "lodash": "^4.17.21", "mixpanel-react-native": "^2.3.1", "mobile-device-detect": "^0.4.3", diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 03b9e27d..e0432e9f 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -33,6 +33,7 @@ import { SwapRouteState, useSwapRoute } from '../hooks/useSwapRoute'; import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallback'; import ApproveSwapModal from './ApproveSwapModal'; import { waitForTransaction } from '@wagmi/core'; +import { TransactionReceipt } from 'viem'; interface DonateComponentProps { collective: IpfsCollective; @@ -68,7 +69,9 @@ function DonateComponent({ collective }: DonateComponentProps) { handleApproveToken, isLoading: handleApproveTokenIsLoading, isError: handleApproveTokenIsError, - } = useApproveSwapTokenCallback(currency, decimalDonationAmount, duration); + } = useApproveSwapTokenCallback(currency, decimalDonationAmount, duration, (value: boolean) => + setApproveSwapModalVisible(value) + ); const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall } = useContractCalls( collectiveId, @@ -77,7 +80,7 @@ function DonateComponent({ collective }: DonateComponentProps) { duration, frequency, (error) => setErrorMessage(error), - () => setCompleteDonationModalVisible(!completeDonationModalVisible), + (value: boolean) => setCompleteDonationModalVisible(value), rawMinimumAmountOut, swapPath ); @@ -90,15 +93,25 @@ function DonateComponent({ collective }: DonateComponentProps) { setErrorMessage('An error occurred while generating a transaction to approve the token.'); return; } + let txReceipt: TransactionReceipt | undefined; const txHash = await handleApproveToken(); - setApproveSwapModalVisible(!approveSwapModalVisible); - const txReceipt = await waitForTransaction({ - chainId: chain?.id, - confirmations: 1, - hash: txHash, - timeout: 1000 * 60 * 5, - }); - if (txReceipt.status === 'success') { + if (txHash === undefined) { + return; + } + try { + txReceipt = await waitForTransaction({ + chainId: chain?.id, + confirmations: 1, + hash: txHash, + timeout: 1000 * 60 * 5, + }); + } catch (error) { + setApproveSwapModalVisible(false); + setErrorMessage( + 'Something went wrong: Your token approval transaction was not confirmed within the timeout period.' + ); + } + if (txReceipt?.status === 'success') { if (frequency === Frequency.OneTime) { await supportSingleTransferAndCall(); } else if (currency === 'G$') { @@ -108,7 +121,6 @@ function DonateComponent({ collective }: DonateComponentProps) { } } }, [ - approveSwapModalVisible, chain?.id, currency, frequency, @@ -224,30 +236,20 @@ function DonateComponent({ collective }: DonateComponentProps) { {frequency !== 'One-Time' && ( - + For How Long: - + setDuration(Number(value))} /> - - {getFrequencyPlural(frequency as Frequency)} - + {getFrequencyPlural(frequency as Frequency)} )} @@ -255,7 +257,7 @@ function DonateComponent({ collective }: DonateComponentProps) { )} - + <> {!isDesktopResolution && ( <> @@ -265,19 +267,20 @@ function DonateComponent({ collective }: DonateComponentProps) { options={frequencyOptions} /> {frequency !== 'One-Time' && ( - - For How Long: + + For How Long: setDuration(value)} /> - {getFrequencyPlural(frequency)} + {getFrequencyPlural(frequency)} )} @@ -528,6 +531,11 @@ const styles = StyleSheet.create({ flexDirection: 'row', gap: 8, }, + desktopActionBox: { + flex: 1, + flexDirection: 'column', + justifyContent: 'space-between', + }, headerLabel: { fontSize: 18, lineHeight: 27, @@ -551,28 +559,33 @@ const styles = StyleSheet.create({ ...InterRegular, }, frequencyDetails: { + height: 32, gap: 8, backgroundColor: Colors.purple[100], - paddingTop: 2, justifyContent: 'center', + alignItems: 'center', borderColor: Colors.gray[600], borderBottomWidth: 1, flex: 1, flexDirection: 'row', }, + desktopFrequencyDetails: { + maxHeight: 59, + }, durationInput: { fontSize: 18, lineHeight: 27, ...InterSemiBold, - // textAlign: 'right', width: '20%', color: Colors.purple[400], textAlign: 'center', }, durationLabel: { ...InterSmall, - textAlign: 'left', - width: 'auto', + fontSize: 18, + lineHeight: 27, + color: Colors.purple[400], + textAlignVertical: 'bottom', }, downIcon: { width: 24, @@ -648,7 +661,7 @@ const styles = StyleSheet.create({ ...InterSmall, }, italic: { fontStyle: 'italic' }, - frecuencyWrapper: { gap: 17, zIndex: -1 }, + frequencyWrapper: { gap: 17, zIndex: -1 }, donationAction: { width: 'auto', flexGrow: 1 }, donationCurrencyHeader: { flexDirection: 'row', width: 'auto', gap: 20 }, }); diff --git a/packages/app/src/hooks/useApproveSwapTokenCallback.ts b/packages/app/src/hooks/useApproveSwapTokenCallback.ts index 320aa6d9..d04e0706 100644 --- a/packages/app/src/hooks/useApproveSwapTokenCallback.ts +++ b/packages/app/src/hooks/useApproveSwapTokenCallback.ts @@ -12,12 +12,13 @@ const V3_ROUTER_ADDRESS = '0x5615CDAb10dc425a742d643d949a7F474C01abc4'; export function useApproveSwapTokenCallback( currencyIn: SupportedTokenSymbol, decimalAmountIn: number, - duration: number + duration: number, + toggleApproveSwapModalVisible: (value: boolean) => void ): { isLoading: boolean; isSuccess: boolean; isError: boolean; - handleApproveToken?: () => Promise<`0x${string}`>; + handleApproveToken?: () => Promise<`0x${string}` | undefined>; } { const { address } = useAccount(); const { chain } = useNetwork(); @@ -44,10 +45,15 @@ export function useApproveSwapTokenCallback( return undefined; } return async () => { - const result = await writeAsync(); - return result.hash; + toggleApproveSwapModalVisible(true); + const result = await writeAsync().catch((_) => { + // user rejected the transaction + toggleApproveSwapModalVisible(false); + return undefined; + }); + return result?.hash; }; - }, [writeAsync]); + }, [writeAsync, toggleApproveSwapModalVisible]); return { isLoading, diff --git a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx index c13def57..e8880b23 100644 --- a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx +++ b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx @@ -20,7 +20,7 @@ export const useContractCalls = ( duration: number, frequency: Frequency, onError: (error: string) => void, - toggleCompleteDonationModal: () => void, + toggleCompleteDonationModal: (value: boolean) => void, minReturnFromSwap?: string, swapPath?: string ): ContractCalls => { diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts index 93c78f88..bb31532f 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts @@ -13,7 +13,7 @@ export function useSupportFlow( duration: number, frequency: Frequency, onError: (error: string) => void, - toggleCompleteDonationModal: () => void + toggleCompleteDonationModal: (value: boolean) => void ) { const { address } = useAccount(); const { chain } = useNetwork(); @@ -46,11 +46,12 @@ export function useSupportFlow( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); const tx = await sdk.supportFlow(signer, collective, flowRate); - toggleCompleteDonationModal(); + toggleCompleteDonationModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { + toggleCompleteDonationModal(false); onError(`An unexpected error occurred: ${error}`); } }, [ diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts index 649e2d14..b82f9925 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts @@ -22,7 +22,7 @@ export function useSupportFlowWithSwap( duration: number, frequency: Frequency, onError: (error: string) => void, - toggleCompleteDonationModal: () => void, + toggleCompleteDonationModal: (value: boolean) => void, minReturnFromSwap?: string, swapPath?: string ) { @@ -74,11 +74,12 @@ export function useSupportFlowWithSwap( swapFrom: tokenMapping[currency], deadline: Math.floor(Date.now() / 1000 + 1800).toString(), }); - toggleCompleteDonationModal(); + toggleCompleteDonationModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { + toggleCompleteDonationModal(false); onError(`An unexpected error occurred: ${error}`); } }, [ diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts index 759d572b..1de7ee8b 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts @@ -12,7 +12,7 @@ export function useSupportSingleBatch( currencyDecimals: number, decimalAmountIn: number, onError: (error: string) => void, - toggleCompleteDonationModal: () => void + toggleCompleteDonationModal: (value: boolean) => void ) { const { address } = useAccount(); const { chain } = useNetwork(); @@ -44,11 +44,12 @@ export function useSupportSingleBatch( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); - toggleCompleteDonationModal(); + toggleCompleteDonationModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { + toggleCompleteDonationModal(false); onError(`An unexpected error occurred: ${error}`); } }, [ diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts index 17501b6c..7591a31b 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts @@ -12,7 +12,7 @@ export function useSupportSingleTransferAndCall( currencyDecimals: number, decimalAmountIn: number, onError: (error: string) => void, - toggleCompleteDonationModal: () => void + toggleCompleteDonationModal: (value: boolean) => void ) { const { address } = useAccount(); const { chain } = useNetwork(); @@ -44,11 +44,12 @@ export function useSupportSingleTransferAndCall( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); - toggleCompleteDonationModal(); + toggleCompleteDonationModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { + toggleCompleteDonationModal(false); onError(`An unexpected error occurred: ${error}`); } }, [ diff --git a/packages/app/src/hooks/useCreateStream.tsx b/packages/app/src/hooks/useCreateStream.tsx deleted file mode 100644 index a91b691f..00000000 --- a/packages/app/src/hooks/useCreateStream.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// import { Framework } from '@superfluid-finance/sdk-core'; -// import { useConnect, useNetwork, useWalletClient } from 'wagmi'; - -// export function useCreateNewFlow() { -// const SF_RESOLVERS: { [key: string]: string } = { -// 44787: '0x6e9CaBE4172344Db81a1E1D735a6AD763700064A', -// 31337: '0x41549B6C39A529EA574f35b745b00f716869D2a0', -// }; -// const createNewFlow = async (flowRate: string, token: string, pool: string) => { -// // eslint-disable-next-line react-hooks/rules-of-hooks -// const { connector }: any = useConnect(); - -// const provider = await connector.getProvider(); -// // eslint-disable-next-line react-hooks/rules-of-hooks -// const { chain }: any = useNetwork(); -// // eslint-disable-next-line react-hooks/rules-of-hooks -// const { data }: any = useWalletClient(); - -// const opts = { -// chainId: Number(chain), -// provider: provider, -// resolverAddress: SF_RESOLVERS[chain], -// protocolReleaseVersion: chain === '31337' ? 'test' : undefined, -// }; - -// const sf = await Framework.create(opts); -// const st = sf?.loadSuperToken(token); -// try { -// if (data.signer) { -// console.log('flowrate', flowRate); - -// const createFlowOperation = sf.cfaV1.createFlow({ -// flowRate: flowRate, -// receiver: pool, -// superToken: st, -// }); - -// console.log('Creating your stream...'); - -// const result = await createFlowOperation.exec(data.signer); -// console.log(result); -// } -// } catch (error) { -// console.log( -// "Hmmm, your transaction threw an error. Make sure that this stream does not already exist, and that you've entered a valid Ethereum address!" -// ); -// console.error(error); -// } -// }; - -// return { createNewFlow }; -// } diff --git a/packages/app/src/hooks/useGetTokenPrice.tsx b/packages/app/src/hooks/useGetTokenPrice.tsx index 422cb8e1..9c17f318 100644 --- a/packages/app/src/hooks/useGetTokenPrice.tsx +++ b/packages/app/src/hooks/useGetTokenPrice.tsx @@ -21,9 +21,7 @@ const getTokenPrice = (currency: string): Promise => { let tokenAddress = coingeckoTokenMapping[currency]; const url = `https://api.coingecko.com/api/v3/simple/token_price/celo?contract_addresses=${coingeckoTokenMapping[currency]}&vs_currencies=usd`; return axios - .get(url, { - withCredentials: false, - }) + .get(url) .then((res) => { return res.data[tokenAddress.toLowerCase()].usd; }) diff --git a/packages/app/src/hooks/useIPFS.tsx b/packages/app/src/hooks/useIPFS.tsx deleted file mode 100644 index 378b96a2..00000000 --- a/packages/app/src/hooks/useIPFS.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// export default function useIPFS() { -// const FormData = require('form-data'); -// const axios = require('axios'); -// // const pinata = pinataSDK(process.env.NEXT_PUBLIC_PINATA_API_KEY, process.env.NEXT_PUBLIC_PINATA_SECRET_KEY) -// const { image, name, description, setIpfsCid } = useMetadataContext(); -// const { mint } = useFayreContracts(); - -// const uploadMetadata = async (mintObj: any) => { -// const data = new FormData(); -// let imageCID: string; -// console.log('hits'); - -// try { -// const url = `https://api.pinata.cloud/pinning/pinFileToIPFS`; -// console.log(image, 'line 24 ipfs'); -// data.append('file', image); -// const uplaodImage = await axios.post(url, data, { -// maxBodyLength: 'Infinity', -// headers: { -// pinata_api_key: '470f87aa78c6c163afb8', -// pinata_secret_api_key: 'a3d7d463e4ba53ceebea81aec98a02e259882cd79bf14c4f18cdc98e20a2ea44', -// }, -// }); -// return ( -// console.log('fires'), -// console.log(uplaodImage.data.IpfsHash as string), -// (imageCID = uplaodImage.data.IpfsHash as string), -// uploadData(mintObj, imageCID) -// ); -// } catch (err: any) { -// console.error(err, 'error uploading image'); -// } -// }; - -// const uploadData = async (mintObj: any, imageCID: string) => { -// const metadataObj = { -// description: description, -// image: imageCID, -// name: name, -// }; - -// try { -// const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`; - -// const uploadData = await axios.post(url, metadataObj, { -// maxBodyLength: 'Infinity', -// headers: { -// pinata_api_key: process.env.NEXT_PUBLIC_PINATA_PUBLIC_KEY, -// pinata_secret_api_key: process.env.NEXT_PUBLIC_PINATA_SECRET_KEY, -// }, -// }); -// return ( -// console.log(uploadData.data.IpfsHash as string), -// await mint( -// mintObj.tokenStandard, -// uploadData.data.IpfsHash as string, -// mintObj.amount, -// mintObj.royalties, -// mintObj.collectionName -// ) -// ); -// } catch (err) { -// console.error(err, 'error uploading metadata'); -// } -// }; - -// return { uploadMetadata }; -// } diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index e867a671..70301598 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,5 +1,5 @@ import { AlphaRouter, SwapRoute, SwapType, V3Route } from '@uniswap/smart-order-router'; -import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'; +import { CurrencyAmount, Fraction, Percent, Token, TradeType } from '@uniswap/sdk-core'; import { useAccount, useNetwork } from 'wagmi'; import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; import { useEthersSigner } from './wagmiF'; diff --git a/yarn.lock b/yarn.lock index 63452531..fbfd9f66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4571,6 +4571,7 @@ __metadata: fast-text-encoding: ^1.0.6 graphql: ^16.6.0 jest: ^29.2.1 + jsbi: 3.1.4 lodash: ^4.17.21 metro-react-native-babel-preset: 0.73.9 mixpanel-react-native: ^2.3.1 @@ -27234,6 +27235,13 @@ __metadata: languageName: node linkType: hard +"jsbi@npm:3.1.4": + version: 3.1.4 + resolution: "jsbi@npm:3.1.4" + checksum: 8dad8122b5060642d5763405f3c210c385747d5b5e95639e8bddbb2bb5d06fc55a25c6d7d7dec7cbc071d4db58a9525578d29a58d1357976c03d75b3712f83a4 + languageName: node + linkType: hard + "jsbi@npm:^3.1.1, jsbi@npm:^3.1.4": version: 3.2.5 resolution: "jsbi@npm:3.2.5" From 5404b9747cb5f4d48f93e4ac099726d6e3347d2d Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 30 Dec 2023 17:07:17 +0500 Subject: [PATCH 07/18] removed unused function from lib --- packages/app/src/lib/formatGdollar.ts | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 packages/app/src/lib/formatGdollar.ts diff --git a/packages/app/src/lib/formatGdollar.ts b/packages/app/src/lib/formatGdollar.ts deleted file mode 100644 index e437e42c..00000000 --- a/packages/app/src/lib/formatGdollar.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const formatGdollar = (gDollar: any): any => { - if (!gDollar) return '0'; - const parsed = (gDollar / 10 ** 18) as any; - const parsed1 = parsed?.split('.') as any; - const beforeDecimal = parsed1[0]; - let formatted; - const afterDecimal = parsed1[1]; - - if (beforeDecimal === '0' && afterDecimal !== '0') { - return (formatted = `0.${afterDecimal?.slice(0, 4) || 0}`); - } else if (beforeDecimal && afterDecimal > 0) { - return (formatted = `${beforeDecimal}.${afterDecimal?.slice(0, 4) || 0}`); - } - return formatted; -}; From b9bbeb5156a3ce85e4c65a988023de371bf2d8f8 Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 4 Jan 2024 17:18:04 +0500 Subject: [PATCH 08/18] now using celo token list --- .../app/src/components/DonateComponent.tsx | 29 +- .../app/src/components/ViewCollective.tsx | 1 - .../src/hooks/useApproveSwapTokenCallback.ts | 19 +- .../useContractCalls/useContractCalls.tsx | 18 +- .../useSupportFlowWithSwap.ts | 21 +- .../app/src/hooks/useGetDecimalBalance.ts | 11 +- packages/app/src/hooks/useGetTokenDecimals.ts | 21 - packages/app/src/hooks/useGetTokenPrice.tsx | 18 +- packages/app/src/hooks/useSwapRoute.tsx | 31 +- packages/app/src/hooks/useTokenList.ts | 35 +- packages/app/src/models/CeloTokenList.json | 645 ++++++++++++++++++ packages/app/src/models/constants.ts | 28 +- 12 files changed, 730 insertions(+), 147 deletions(-) delete mode 100644 packages/app/src/hooks/useGetTokenDecimals.ts create mode 100644 packages/app/src/models/CeloTokenList.json diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index e0432e9f..24e4327d 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { Image, StyleSheet, Text, TextInput, View } from 'react-native'; import { InterRegular, InterSemiBold, InterSmall } from '../utils/webFonts'; import RoundedButton from './RoundedButton'; @@ -16,16 +16,9 @@ import { useContractCalls, useGetTokenPrice } from '../hooks'; import { useAccount, useNetwork } from 'wagmi'; import { IpfsCollective } from '../models/models'; import { useGetDecimalBalance } from '../hooks/useGetDecimalBalance'; -import { - currencyOptions, - Frequency, - frequencyOptions, - SupportedNetwork, - SupportedTokenSymbol, -} from '../models/constants'; +import { Frequency, frequencyOptions, SupportedNetwork } from '../models/constants'; import { InfoIconOrange } from '../assets'; import { useLocation } from 'react-router-native'; -import { useGetTokenDecimals } from '../hooks/useGetTokenDecimals'; import Decimal from 'decimal.js'; import { formatFiatCurrency } from '../lib/formatFiatCurrency'; import ErrorModal from './ErrorModal'; @@ -34,6 +27,7 @@ import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallbac import ApproveSwapModal from './ApproveSwapModal'; import { waitForTransaction } from '@wagmi/core'; import { TransactionReceipt } from 'viem'; +import { useToken, useTokenList } from '../hooks/useTokenList'; interface DonateComponentProps { collective: IpfsCollective; @@ -49,11 +43,20 @@ function DonateComponent({ collective }: DonateComponentProps) { const { address, isConnected } = useAccount(); const { chain } = useNetwork(); + const tokenList = useTokenList(); + + const currencyOptions: { value: string; label: string }[] = useMemo(() => { + return Object.keys(tokenList).map((key) => ({ + value: key, + label: key, + })); + }, [tokenList]); + const [completeDonationModalVisible, setCompleteDonationModalVisible] = useState(false); const [errorMessage, setErrorMessage] = useState(undefined); const [approveSwapModalVisible, setApproveSwapModalVisible] = useState(false); - const [currency, setCurrency] = useState('G$'); + const [currency, setCurrency] = useState('G$'); const [frequency, setFrequency] = useState(Frequency.OneTime); const [duration, setDuration] = useState(1); const [decimalDonationAmount, setDecimalDonationAmount] = useState(0); @@ -132,8 +135,8 @@ function DonateComponent({ collective }: DonateComponentProps) { supportSingleTransferAndCall, ]); - const currencyDecimals = useGetTokenDecimals(currency, chain?.id); - const donorCurrencyBalance = useGetDecimalBalance(currency as SupportedTokenSymbol, address, chain?.id); + const currencyDecimals = useToken(currency).decimals; + const donorCurrencyBalance = useGetDecimalBalance(currency, address, chain?.id); const totalDecimalDonation = duration * decimalDonationAmount; const totalDonationFormatted = new Decimal(totalDecimalDonation) @@ -144,7 +147,7 @@ function DonateComponent({ collective }: DonateComponentProps) { const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus === SwapRouteState.NO_ROUTE; const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact.gte(0.1) : false; - const { price } = useGetTokenPrice(currency as SupportedTokenSymbol); + const { price } = useGetTokenPrice(currency); const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; return ( diff --git a/packages/app/src/components/ViewCollective.tsx b/packages/app/src/components/ViewCollective.tsx index 1c7ecd7e..ab230ca9 100644 --- a/packages/app/src/components/ViewCollective.tsx +++ b/packages/app/src/components/ViewCollective.tsx @@ -30,7 +30,6 @@ import { TwitterIcon, WebIcon, } from '../assets/'; -import { calculateAmounts } from '../lib/calculateAmounts'; import { useDonorCollectivesFlowingBalances } from '../hooks/useFlowingBalance'; import { calculateGoodDollarAmounts } from '../lib/calculateGoodDollarAmounts'; diff --git a/packages/app/src/hooks/useApproveSwapTokenCallback.ts b/packages/app/src/hooks/useApproveSwapTokenCallback.ts index d04e0706..2ebc8b29 100644 --- a/packages/app/src/hooks/useApproveSwapTokenCallback.ts +++ b/packages/app/src/hooks/useApproveSwapTokenCallback.ts @@ -1,16 +1,13 @@ import { useAccount, useContractWrite, useNetwork, usePrepareContractWrite } from 'wagmi'; import { useMemo } from 'react'; import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; -import { SupportedTokenSymbol, tokenMapping } from '../models/constants'; -import { useGetTokenDecimals } from './useGetTokenDecimals'; import Decimal from 'decimal.js'; import ERC20 from '../abi/ERC20.json'; - -// Uniswap V3 Router on Celo -const V3_ROUTER_ADDRESS = '0x5615CDAb10dc425a742d643d949a7F474C01abc4'; +import { useToken } from './useTokenList'; +import { UNISWAP_V3_ROUTER_ADDRESS } from '../models/constants'; export function useApproveSwapTokenCallback( - currencyIn: SupportedTokenSymbol, + currencyIn: string, decimalAmountIn: number, duration: number, toggleApproveSwapModalVisible: (value: boolean) => void @@ -22,20 +19,20 @@ export function useApproveSwapTokenCallback( } { const { address } = useAccount(); const { chain } = useNetwork(); - const currencyInDecimals = useGetTokenDecimals(currencyIn, chain?.id); + const tokenIn = useToken(currencyIn); const rawAmountIn = useMemo( - () => calculateRawTotalDonation(decimalAmountIn, duration, currencyInDecimals).toFixed(0, Decimal.ROUND_DOWN), - [decimalAmountIn, duration, currencyInDecimals] + () => calculateRawTotalDonation(decimalAmountIn, duration, tokenIn.decimals).toFixed(0, Decimal.ROUND_DOWN), + [decimalAmountIn, duration, tokenIn.decimals] ); const { config } = usePrepareContractWrite({ chainId: chain?.id, - address: tokenMapping[currencyIn], + address: tokenIn.address as `0x${string}`, abi: ERC20, account: address, functionName: 'approve', - args: [V3_ROUTER_ADDRESS, rawAmountIn], + args: [UNISWAP_V3_ROUTER_ADDRESS, rawAmountIn], }); const { isLoading, isSuccess, isError, writeAsync } = useContractWrite(config); diff --git a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx index e8880b23..6ad6ebbb 100644 --- a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx +++ b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx @@ -1,10 +1,10 @@ import { useNetwork } from 'wagmi'; -import { Frequency, SupportedTokenSymbol } from '../../models/constants'; -import { useGetTokenDecimals } from '../useGetTokenDecimals'; +import { Frequency } from '../../models/constants'; import { useSupportFlow } from './useSupportFlow'; import { useSupportFlowWithSwap } from './useSupportFlowWithSwap'; import { useSupportSingleTransferAndCall } from './useSupportSingleTransferAndCall'; import { useSupportSingleBatch } from './useSupportSingleBatch'; +import { useToken } from '../useTokenList'; interface ContractCalls { supportFlowWithSwap: () => Promise; @@ -15,7 +15,7 @@ interface ContractCalls { export const useContractCalls = ( collective: string, - currency: SupportedTokenSymbol, + currency: string, decimalAmountIn: number, duration: number, frequency: Frequency, @@ -24,12 +24,11 @@ export const useContractCalls = ( minReturnFromSwap?: string, swapPath?: string ): ContractCalls => { - const { chain } = useNetwork(); - const currencyDecimals = useGetTokenDecimals(currency, chain?.id); + const tokenIn = useToken(currency); const supportFlow = useSupportFlow( collective, - currencyDecimals, + tokenIn.decimals, decimalAmountIn, duration, frequency, @@ -38,8 +37,7 @@ export const useContractCalls = ( ); const supportFlowWithSwap = useSupportFlowWithSwap( collective, - currency, - currencyDecimals, + tokenIn, decimalAmountIn, duration, frequency, @@ -50,14 +48,14 @@ export const useContractCalls = ( ); const supportSingleTransferAndCall = useSupportSingleTransferAndCall( collective, - currencyDecimals, + tokenIn.decimals, decimalAmountIn, onError, toggleCompleteDonationModal ); const supportSingleBatch = useSupportSingleBatch( collective, - currencyDecimals, + tokenIn.decimals, decimalAmountIn, onError, toggleCompleteDonationModal diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts index b82f9925..11ac800f 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts @@ -1,11 +1,5 @@ import { useCallback } from 'react'; -import { - Frequency, - SupportedNetwork, - SupportedNetworkNames, - SupportedTokenSymbol, - tokenMapping, -} from '../../models/constants'; +import { Frequency, SupportedNetwork, SupportedNetworkNames } from '../../models/constants'; import { calculateFlowRate } from '../../lib/calculateFlowRate'; import { calculateRawTotalDonation } from '../../lib/calculateRawTotalDonation'; import Decimal from 'decimal.js'; @@ -13,11 +7,11 @@ import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk'; import { useAccount, useNetwork } from 'wagmi'; import { useEthersSigner } from '../wagmiF'; import useCrossNavigate from '../../routes/useCrossNavigate'; +import { Token } from '@uniswap/sdk-core'; export function useSupportFlowWithSwap( collective: string, - currency: SupportedTokenSymbol, - currencyDecimals: number, + tokenIn: Token, decimalAmountIn: number, duration: number, frequency: Frequency, @@ -50,7 +44,7 @@ export function useSupportFlowWithSwap( return; } - const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, currencyDecimals); + const flowRate = calculateFlowRate(decimalAmountIn, duration, frequency, tokenIn.decimals); if (!flowRate) { onError('Failed to calculate flow rate.'); return; @@ -60,7 +54,7 @@ export function useSupportFlowWithSwap( const network = SupportedNetworkNames[chain.id as SupportedNetwork]; // swap values - const amountIn = calculateRawTotalDonation(decimalAmountIn, duration, currencyDecimals).toFixed( + const amountIn = calculateRawTotalDonation(decimalAmountIn, duration, tokenIn.decimals).toFixed( 0, Decimal.ROUND_DOWN ); @@ -71,7 +65,7 @@ export function useSupportFlowWithSwap( amount: amountIn, minReturn: minReturnFromSwap, path: swapPath, - swapFrom: tokenMapping[currency], + swapFrom: tokenIn.address, deadline: Math.floor(Date.now() / 1000 + 1800).toString(), }); toggleCompleteDonationModal(true); @@ -86,8 +80,7 @@ export function useSupportFlowWithSwap( address, chain?.id, collective, - currency, - currencyDecimals, + tokenIn, decimalAmountIn, duration, frequency, diff --git a/packages/app/src/hooks/useGetDecimalBalance.ts b/packages/app/src/hooks/useGetDecimalBalance.ts index 849d2a5e..018d455f 100644 --- a/packages/app/src/hooks/useGetDecimalBalance.ts +++ b/packages/app/src/hooks/useGetDecimalBalance.ts @@ -1,24 +1,27 @@ -import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; +import { SupportedNetwork } from '../models/constants'; import { useEffect, useState } from 'react'; import { fetchBalance } from 'wagmi/actions'; +import { useToken } from './useTokenList'; export const useGetDecimalBalance = ( - currencySymbol: SupportedTokenSymbol, + currencySymbol: string, accountAddress: `0x${string}` | undefined, chainId: number = SupportedNetwork.celo ): number => { const [tokenBalance, setTokenBalance] = useState('0'); + const token = useToken(currencySymbol); + useEffect(() => { if (!accountAddress) return; fetchBalance({ address: accountAddress, chainId: chainId, - token: tokenMapping[currencySymbol], + token: token.address as `0x${string}`, }).then((res) => { setTokenBalance(res.formatted); }); - }, [currencySymbol, accountAddress, chainId]); + }, [currencySymbol, token.address, accountAddress, chainId]); return parseFloat(tokenBalance); }; diff --git a/packages/app/src/hooks/useGetTokenDecimals.ts b/packages/app/src/hooks/useGetTokenDecimals.ts deleted file mode 100644 index 8afe811d..00000000 --- a/packages/app/src/hooks/useGetTokenDecimals.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; -import { useEffect, useState } from 'react'; -import { fetchToken } from 'wagmi/actions'; - -export const useGetTokenDecimals = ( - currencySymbol: SupportedTokenSymbol, - chainId: number = SupportedNetwork.celo -): number => { - const [tokenDecimals, setTokenDecimals] = useState(18); - - useEffect(() => { - fetchToken({ - address: tokenMapping[currencySymbol], - chainId: chainId, - }).then((res) => { - setTokenDecimals(res.decimals); - }); - }, [currencySymbol, chainId]); - - return tokenDecimals; -}; diff --git a/packages/app/src/hooks/useGetTokenPrice.tsx b/packages/app/src/hooks/useGetTokenPrice.tsx index 9c17f318..c71cf6ed 100644 --- a/packages/app/src/hooks/useGetTokenPrice.tsx +++ b/packages/app/src/hooks/useGetTokenPrice.tsx @@ -1,25 +1,29 @@ import axios from 'axios'; import { useEffect, useState } from 'react'; -import { coingeckoTokenMapping, SupportedTokenSymbol } from '../models/constants'; +import { coingeckoTokenMapping } from '../models/constants'; +import { useToken } from './useTokenList'; +import { Token } from '@uniswap/sdk-core'; -export const useGetTokenPrice = (currency: SupportedTokenSymbol): { price?: number; isLoading: boolean } => { +export const useGetTokenPrice = (currency: string): { price?: number; isLoading: boolean } => { const [price, setPrice] = useState(undefined); const [isLoading, setIsLoading] = useState(true); + const token = useToken(currency); + useEffect(() => { setIsLoading(true); - getTokenPrice(currency).then((res: number | undefined) => { + getTokenPrice(currency, token).then((res: number | undefined) => { setPrice(res); }); setIsLoading(false); - }, [currency]); + }, [currency, token]); return { price, isLoading }; }; -const getTokenPrice = (currency: string): Promise => { - let tokenAddress = coingeckoTokenMapping[currency]; - const url = `https://api.coingecko.com/api/v3/simple/token_price/celo?contract_addresses=${coingeckoTokenMapping[currency]}&vs_currencies=usd`; +const getTokenPrice = (currency: string, token: Token): Promise => { + let tokenAddress = coingeckoTokenMapping[currency] ?? token.address; + const url = `https://api.coingecko.com/api/v3/simple/token_price/celo?contract_addresses=${tokenAddress}&vs_currencies=usd`; return axios .get(url) .then((res) => { diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 70301598..35f50b4f 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,13 +1,13 @@ import { AlphaRouter, SwapRoute, SwapType, V3Route } from '@uniswap/smart-order-router'; -import { CurrencyAmount, Fraction, Percent, Token, TradeType } from '@uniswap/sdk-core'; +import { CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'; import { useAccount, useNetwork } from 'wagmi'; -import { SupportedNetwork, SupportedTokenSymbol, tokenMapping } from '../models/constants'; +import { GDToken } from '../models/constants'; import { useEthersSigner } from './wagmiF'; import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; -import { useGetTokenDecimals } from './useGetTokenDecimals'; import Decimal from 'decimal.js'; import { useEffect, useState } from 'react'; import { encodeRouteToPath } from '@uniswap/v3-sdk'; +import { useToken } from './useTokenList'; export enum SwapRouteState { LOADING, @@ -15,10 +15,8 @@ export enum SwapRouteState { NO_ROUTE, } -const GDToken = new Token(SupportedNetwork.celo, tokenMapping.G$, 18); - export function useSwapRoute( - currencyIn: SupportedTokenSymbol, + currencyIn: string, decimalAmountIn: number, duration: number, slippageTolerance: Percent = new Percent(1, 100) @@ -32,12 +30,13 @@ export function useSwapRoute( const { address } = useAccount(); const { chain } = useNetwork(); const signer = useEthersSigner({ chainId: chain?.id }); - const currencyInDecimals = useGetTokenDecimals(currencyIn, chain?.id); + + const tokenIn = useToken(currencyIn); const [route, setRoute] = useState(undefined); useEffect(() => { - if (!address || !chain?.id || !signer?.provider || currencyIn === 'G$') { + if (!address || !chain?.id || !signer?.provider || tokenIn.symbol === 'G$') { setRoute(undefined); return; } @@ -46,9 +45,8 @@ export function useSwapRoute( provider: signer.provider, }); - const inputToken = new Token(chain.id, tokenMapping[currencyIn], currencyInDecimals); - const rawAmountIn = calculateRawTotalDonation(decimalAmountIn, duration, currencyInDecimals); - const inputAmount = CurrencyAmount.fromRawAmount(inputToken, rawAmountIn.toFixed(0, Decimal.ROUND_DOWN)); + const rawAmountIn = calculateRawTotalDonation(decimalAmountIn, duration, tokenIn.decimals); + const inputAmount = CurrencyAmount.fromRawAmount(tokenIn, rawAmountIn.toFixed(0, Decimal.ROUND_DOWN)); router .route(inputAmount, GDToken, TradeType.EXACT_INPUT, { type: SwapType.SWAP_ROUTER_02, @@ -59,16 +57,7 @@ export function useSwapRoute( .then((swapRoute) => { setRoute(swapRoute ?? undefined); }); - }, [ - address, - chain?.id, - signer?.provider, - currencyIn, - currencyInDecimals, - decimalAmountIn, - duration, - slippageTolerance, - ]); + }, [address, chain?.id, signer?.provider, tokenIn, decimalAmountIn, duration, slippageTolerance]); if (!route) { return { status: SwapRouteState.LOADING }; diff --git a/packages/app/src/hooks/useTokenList.ts b/packages/app/src/hooks/useTokenList.ts index 5f8b9606..8b3b54d1 100644 --- a/packages/app/src/hooks/useTokenList.ts +++ b/packages/app/src/hooks/useTokenList.ts @@ -1,30 +1,17 @@ +import { useMemo } from 'react'; import { Token } from '@uniswap/sdk-core'; -import { useEffect, useState } from 'react'; -import axios from 'axios'; +import CeloTokenList from '../models/CeloTokenList.json'; -interface TokenList { - tokens: { - name: string; - address: string; - symbol: string; - decimals: number; - chainId: number; - }[]; +export function useToken(symbol: string): Token { + return useTokenList()[symbol]; } -export function useTokenList(): Token[] { - const [tokens, setTokens] = useState([]); - - useEffect(() => { - axios - .get('https://raw.githubusercontent.com/celo-org/celo-token-list/main/celo.tokenlist.json') - .then((response) => { - const tokenData = response.data.tokens.map( - (token) => new Token(token.chainId, token.address, token.decimals, token.symbol, token.name) - ); - setTokens(tokenData); - }); +export function useTokenList(): Record { + return useMemo(() => { + const tokenList: Record = {}; + CeloTokenList.tokens.forEach((token) => { + tokenList[token.symbol] = new Token(token.chainId, token.address, token.decimals, token.symbol); + }); + return tokenList; }, []); - - return tokens; } diff --git a/packages/app/src/models/CeloTokenList.json b/packages/app/src/models/CeloTokenList.json new file mode 100644 index 00000000..8fcf9db3 --- /dev/null +++ b/packages/app/src/models/CeloTokenList.json @@ -0,0 +1,645 @@ +{ + "name": "Celo Token List", + "version": { + "major": 2, + "minor": 3, + "patch": 0 + }, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/celo_logo.svg", + "keywords": ["celo", "tokens", "refi"], + "timestamp": "2022-05-25T20:37:00.000+00:00", + "tokens": [ + { + "name": "Green CELO", + "address": "0x8a1639098644a229d08f441ea45a63ae050ee018", + "symbol": "gCELO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/spiralsprotocol/spirals-brand/main/gCELO.svg" + }, + { + "name": "Green cUSD", + "address": "0xFB42E2e90fc79CfA6A6B4EBa4877d5Faf4e29287", + "symbol": "gcUSD", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/spiralsprotocol/spirals-brand/main/gcUSD.svg" + }, + { + "name": "cRecy", + "address": "0x34C11A932853Ae24E845Ad4B633E3cEf91afE583", + "symbol": "cRecy", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://user-images.githubusercontent.com/101748448/187026740-27f51d9d-e60d-48e9-b378-416c1eda0cb1.svg" + }, + { + "name": "Staked Celo", + "address": "0xC668583dcbDc9ae6FA3CE46462758188adfdfC24", + "symbol": "stCelo", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/token-stcelo.svg" + }, + { + "name": "Nature Carbon Tonne", + "address": "0x02de4766c272abc10bc88c220d214a26960a7e92", + "symbol": "NCT", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://toucan.earth/img/icons/nct.svg" + }, + { + "name": "USDC (Portal from Ethereum)", + "address": "0x37f750B7cC259A2f741AF45294f6a16572CF5cAd", + "symbol": "USDCet", + "decimals": 6, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/certusone/wormhole-token-list/main/assets/USDCet_wh_small.png" + }, + { + "name": "DAI Stablecoin (Portal)", + "address": "0x97926a82930bb7B33178E3c2f4ED1BFDc91A9FBF", + "symbol": "DAI", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/certusone/wormhole-token-list/main/assets/DAI_wh_small.png" + }, + { + "name": "Portal WETH", + "address": "0x66803FB87aBd4aaC3cbB3fAd7C3aa01f6F3FB207", + "symbol": "WETH", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_ETH.png" + }, + { + "name": "wrapped.com ETH", + "address": "0x2DEf4285787d58a2f811AF24755A8150622f4361", + "symbol": "cETH", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cETH.svg" + }, + { + "name": "Ubeswap", + "address": "0x00Be915B9dCf56a3CBE739D9B9c202ca692409EC", + "symbol": "UBE", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_UBE.png" + }, + { + "name": "Celo Moss Carbon Credit", + "address": "0x32A9FE697a32135BFd313a6Ac28792DaE4D9979d", + "symbol": "cMCO2", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cMCO2.png" + }, + { + "name": "Celo", + "address": "0x471EcE3750Da237f93B8E339c536989b8978a438", + "symbol": "CELO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/celo_logo.svg" + }, + { + "name": "Celo Dollar", + "address": "0x765DE816845861e75A25fCA122bb6898B8B1282a", + "symbol": "cUSD", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cUSD.png" + }, + { + "name": "Duniapay West African CFA franc", + "address": "0x832F03bCeE999a577cb592948983E35C048B5Aa4", + "symbol": "cXOF", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cXOF.png" + }, + { + "name": "wrapped.com Bitcoin", + "address": "0xD629eb00dEced2a080B7EC630eF6aC117e614f1b", + "symbol": "cBTC", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cBTC.png" + }, + { + "name": "Celo Euro", + "address": "0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73", + "symbol": "cEUR", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cEUR.png" + }, + { + "name": "Beefy Finance", + "address": "0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C", + "decimals": 18, + "symbol": "BIFI", + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/sushiswap/assets/master/blockchains/celo/assets/0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C/logo.png" + }, + { + "name": "Optics v2 WMATIC via Polygon", + "address": "0x2E3487F967DF2Ebc2f236E16f8fCAeac7091324D", + "symbol": "WMATIC", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_WMATIC.png" + }, + { + "name": "Optics v2 SUSHI", + "address": "0x29dFce9c22003A4999930382Fd00f9Fd6133Acd1", + "symbol": "SUSHI", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_SUSHI.png" + }, + { + "name": "Optics v2 WETH", + "address": "0x122013fd7dF1C6F636a5bb8f03108E876548b455", + "symbol": "WETH", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_WETH.png" + }, + { + "name": "Optics v2 WBTC", + "address": "0xBAAB46E28388d2779e6E31Fd00cF0e5Ad95E327B", + "decimals": 8, + "symbol": "WBTC", + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_WBTC.png" + }, + { + "name": "Optics v2 USDC", + "address": "0xef4229c8c3250C675F21BCefa42f58EfbfF6002a", + "decimals": 6, + "symbol": "USDC", + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_USDC.png" + }, + { + "name": "Optics v2 USDT", + "address": "0x88eeC49252c8cbc039DCdB394c0c2BA2f1637EA0", + "decimals": 6, + "symbol": "USDT", + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_USDT.png" + }, + { + "name": "Optics v2 DAI", + "address": "0x90Ca507a5D4458a4C6C6249d186b6dCb02a5BCCd", + "symbol": "DAI", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_DAI.png" + }, + { + "name": "Mobius DAO Token", + "address": "0x73a210637f6F6B7005512677Ba6B3C96bb4AA44B", + "symbol": "MOBI", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_MOBI.png" + }, + { + "name": "impactMarket", + "address": "0x46c9757C5497c5B1f2eb73aE79b6B67D119B0B58", + "symbol": "PACT", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_PACT.png" + }, + { + "name": "Source", + "address": "0x74c0C58B99b68cF16A717279AC2d056A34ba2bFe", + "symbol": "SOURCE", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_SOURCE.png" + }, + { + "name": "Poof", + "address": "0x00400FcbF0816bebB94654259de7273f4A05c762", + "symbol": "POOF", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_POOF.png" + }, + { + "name": "Stabilite USD", + "address": "0x0a60c25Ef6021fC3B479914E6bcA7C03c18A97f1", + "symbol": "stabilUSD", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_stabilUSD.png" + }, + { + "name": "Allbridge SOL", + "address": "0x173234922eB27d5138c5e481be9dF5261fAeD450", + "symbol": "SOL", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_SOL.png" + }, + { + "name": "Moola", + "address": "0x17700282592D6917F6A73D0bF8AcCf4D578c131e", + "symbol": "MOO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_MOO.png" + }, + { + "name": "Ariswap", + "address": "0x20677d4f3d0F08e735aB512393524A3CfCEb250C", + "symbol": "ARI", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_ARI.png" + }, + { + "name": "Anyswap FTM", + "address": "0x218c3c3D49d0E7B37aff0D8bB079de36Ae61A4c0", + "symbol": "FTM", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_FTM.png" + }, + { + "name": "Poof CELO", + "address": "0x301a61D01A63c8D670c2B8a43f37d12eF181F997", + "symbol": "pCELO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_pCELO.png" + }, + { + "name": "CeloStarter", + "address": "0x452EF5a4bD00796e62E5e5758548e0dA6e8CCDF3", + "symbol": "cStar", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cStar.png" + }, + { + "name": "Allbridge SBR", + "address": "0x47264aE1Fc0c8e6418ebe78630718E11a07346A8", + "symbol": "SBR", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_SBR.png" + }, + { + "name": "Allbridge", + "address": "0x6e512BFC33be36F2666754E996ff103AD1680Cc9", + "symbol": "ABR", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_ABR.png" + }, + { + "name": "Staked Allbridge", + "address": "0x788BA01f8E2b87c08B142DB46F82094e0bdCad4F", + "symbol": "xABR", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_xABR.png" + }, + { + "name": "Moola CELO", + "address": "0x7D00cd74FF385c955EA3d79e47BF06bD7386387D", + "symbol": "mCELO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mCELO.png" + }, + { + "name": "Symmetric", + "address": "0x8427bD503dd3169cCC9aFF7326c15258Bc305478", + "symbol": "SYMM", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_SYMM.png" + }, + { + "name": "Allbridge AVAX", + "address": "0x8E3670FD7B0935d3FE832711deBFE13BB689b690", + "symbol": "AVAX", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_AVAX.png" + }, + { + "name": "Moola cUSD", + "address": "0x918146359264C492BD6934071c6Bd31C854EDBc3", + "symbol": "mcUSD", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mcUSD.png" + }, + { + "name": "Premio", + "address": "0x94140c2eA9D208D8476cA4E3045254169791C59e", + "symbol": "PREMIO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_PREMIO.png" + }, + { + "name": "Moola cREAL", + "address": "0x9802d866fdE4563d088a6619F7CeF82C0B991A55", + "symbol": "mcREAL", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mcREAL.png" + }, + { + "name": "Anyswap BNB", + "address": "0xA649325Aa7C5093d12D6F98EB4378deAe68CE23F", + "symbol": "BNB", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_BNB.png" + }, + { + "name": "KnoxEdge", + "address": "0xa81D9a2d29373777E4082d588958678a6Df5645c", + "symbol": "KNX", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_KNX.png" + }, + { + "name": "TrueFeedBack New", + "address": "0xbDd31EFfb9E9f7509fEaAc5B4091b31645A47e4b", + "symbol": "TFBX", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_TFBX.png" + }, + { + "name": "Moola cEUR", + "address": "0xE273Ad7ee11dCfAA87383aD5977EE1504aC07568", + "symbol": "mcEUR", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mcEUR.png" + }, + { + "name": "Immortal", + "address": "0xE685d21b7B0FC7A248a6A8E03b8Db22d013Aa2eE", + "decimals": 9, + "symbol": "IMMO", + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_IMMO.png" + }, + { + "name": "Celo Real", + "address": "0xe8537a3d056DA446677B9E9d6c5dB704EaAb4787", + "symbol": "cREAL", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cREAL.png" + }, + { + "name": "Poof USD", + "address": "0xEadf4A7168A82D30Ba0619e64d5BCf5B30B45226", + "symbol": "pUSD", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_pUSD.png" + }, + { + "name": "Poof v1 EUR", + "address": "0x56072D4832642dB29225dA12d6Fd1290E4744682", + "symbol": "pEURxV1", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_pEUR.png" + }, + { + "name": "Marzipan Finance", + "address": "0x9Ee153D4Fdf0E3222eFD092c442ebB21DFd346AC", + "symbol": "MZPN", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_MZPN.png" + }, + { + "name": "Poof v1 USD", + "address": "0xB4aa2986622249B1F45eb93F28Cfca2b2606d809", + "symbol": "pUSDxV1", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_pUSD.png" + }, + { + "name": "Duino-Coin on Celo", + "address": "0xDB452CC669D3Ae454226AbF232Fe211bAfF2a1F9", + "symbol": "celoDUCO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_celoDUCO.png" + }, + { + "name": "Poof v1 CELO", + "address": "0xE74AbF23E1Fdf7ACbec2F3a30a772eF77f1601E1", + "symbol": "pCELOxV1", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_pCELO.png" + }, + { + "name": "Poof Governance Token", + "address": "0x00400FcbF0816bebB94654259de7273f4A05c762", + "symbol": "POOF", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_POOF.png" + }, + { + "name": "Moola cEUR", + "address": "0x0D9B4311657003251d1eFa085e74f761185F271c", + "symbol": "mcEUR", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mcEUR.png" + }, + { + "name": "Celo Euro", + "address": "0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F", + "symbol": "cEUR", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cEUR.png" + }, + { + "name": "NetM Token", + "address": "0x123ED050805E0998EBEf43671327139224218e50", + "symbol": "NTMX", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_NTMX.png" + }, + { + "name": "Moola", + "address": "0x17700282592D6917F6A73D0bF8AcCf4D578c131e", + "symbol": "MOO", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_MOO.png" + }, + { + "name": "Moola cUSD", + "address": "0x3a0EA4e0806805527C750AB9b34382642448468D", + "symbol": "mcUSD", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mcUSD.png" + }, + { + "name": "Moola cREAL", + "address": "0x3D0EDA535ca4b15c739D46761d24E42e37664Ad7", + "symbol": "mcREAL", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mcREAL.png" + }, + { + "name": "Marzipan Finance", + "address": "0x4d8BF8347600f5207bfdad57363fBa802C9C2031", + "symbol": "MZPN", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_MZPN.png" + }, + { + "name": "Moola CELO", + "address": "0x653cC2Cc0Be398614BAd5d5328336dc79281e246", + "symbol": "mCELO", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_mCELO.png" + }, + { + "name": "Celo Dollar", + "address": "0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1", + "symbol": "cUSD", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cUSD.png" + }, + { + "name": "Celo", + "address": "0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9", + "symbol": "CELO", + "decimals": 18, + "chainId": 44787, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_CELO.png" + }, + { + "name": "AtlasX Carbon Credits", + "address": "0xc3377Ea71F1dc8e55Ba360724eff2d7aD62a8670", + "symbol": "ATLASX", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://myterrablobs.blob.core.windows.net/public/token-icon.png" + }, + { + "name": "PLASTIK Token", + "address": "0x27cd006548dF7C8c8e9fdc4A67fa05C2E3CA5CF9", + "symbol": "PLASTIK", + "decimals": 9, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_PLASTIK.png" + }, + { + "name": "Curve DAO Token", + "address": "0x173fd7434B8B50dF08e3298f173487ebDB35FD14", + "symbol": "CRV", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/curvefi/curve-assets/main/branding/logo.svg" + }, + { + "name": "Axelar Wrapped Bitcoin", + "address": "0x1a35EE4640b0A3B87705B0A4B45D227Ba60Ca2ad", + "symbol": "axlWBTC", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/axelar_wbtc.svg" + }, + { + "name": "Wormhole Wrapped Bitcoin", + "address": "0xd71Ffd0940c920786eC4DbB5A12306669b5b81EF", + "symbol": "WBTC", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/wormhole_wbtc.png" + }, + { + "name": "Good Dollar", + "address": "0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A", + "symbol": "G$", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/GoodDollar/GoodDAPP/master/src/assets/Splash/logo.svg" + }, + { + "name": "Axelar WETH", + "address": "0xb829b68f57cc546da7e5806a929e53be32a4625d", + "symbol": "axlEth", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/axelar_eth.png" + }, + { + "name": "JumpToken", + "address": "0x1d18d0386f51ab03e7e84e71bda1681eba865f1f", + "symbol": "JMPT", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://celo-org.github.io/celo-token-list/assets/jumpToken.png" + }, + { + "name": "Glo Dollar", + "address": "0x4f604735c1cf31399c6e711d5962b2b3e0225ad3", + "symbol": "USDGLO", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://app.glodollar.org/glo-logo.svg" + }, + { + "name": "Curve DAO Token", + "address": "0x173fd7434B8B50dF08e3298f173487ebDB35FD14", + "symbol": "CRV", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/curvefi/curve-assets/main/branding/logo.svg" + }, + { + "name": "AtlasX Carbon Credits", + "address": "0xc3377Ea71F1dc8e55Ba360724eff2d7aD62a8670", + "symbol": "ATLASX", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://myterrablobs.blob.core.windows.net/public/token-icon.png" + }, + { + "name": "agEUR", + "address": "0xC16B81Af351BA9e64C1a069E3Ab18c244A1E3049", + "symbol": "agEUR", + "decimals": 18, + "chainId": 42220, + "logoURI": "https://raw.githubusercontent.com/AngleProtocol/angle-assets/main/0_tokens/agEUR/cross-chain/agEUR-celo.svg" + } + ] +} \ No newline at end of file diff --git a/packages/app/src/models/constants.ts b/packages/app/src/models/constants.ts index 550e5983..8f56b12b 100644 --- a/packages/app/src/models/constants.ts +++ b/packages/app/src/models/constants.ts @@ -1,38 +1,24 @@ +import { Token } from '@uniswap/sdk-core'; + export enum SupportedNetwork { celo = 42220, - alfajores = 44787, } export const SupportedNetworkNames: Record = { [SupportedNetwork.celo]: 'celo', - [SupportedNetwork.alfajores]: 'alfajores', }; -export const tokenMapping: Record = { - CELO: '0x471EcE3750Da237f93B8E339c536989b8978a438', - cUSD: '0x765DE816845861e75A25fCA122bb6898B8B1282a', - USDC: '0x37f750B7cC259A2f741AF45294f6a16572CF5cAd', - WBTC: '0xd71Ffd0940c920786eC4DbB5A12306669b5b81EF', - G$: '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', - WETH: '0x66803FB87aBd4aaC3cbB3fAd7C3aa01f6F3FB207', -}; +// Uniswap V3 Router on Celo +export const UNISWAP_V3_ROUTER_ADDRESS = '0x5615CDAb10dc425a742d643d949a7F474C01abc4'; -export type SupportedTokenSymbol = keyof typeof tokenMapping; +export const GDToken: Token = new Token(SupportedNetwork.celo, '0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A', 18, 'G$'); -export const coingeckoTokenMapping: Record = { - ...tokenMapping, +// if a token is not in this list, the address from the Celo Token List is used +export const coingeckoTokenMapping: Record = { WBTC: '0xD629eb00dEced2a080B7EC630eF6aC117e614f1b', WETH: '0x2def4285787d58a2f811af24755a8150622f4361', }; -// constructed from tokenMapping -export const currencyOptions: { value: SupportedTokenSymbol; label: SupportedTokenSymbol }[] = Object.keys( - tokenMapping -).map((key) => ({ - value: key as SupportedTokenSymbol, - label: key as SupportedTokenSymbol, -})); - export enum Frequency { OneTime = 'One-Time', Daily = 'Daily', From cb1e9aaf3646d95669ced4d8973a7dd76ef0a1b4 Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 4 Jan 2024 17:18:30 +0500 Subject: [PATCH 09/18] removed unused import --- packages/app/src/hooks/useContractCalls/useContractCalls.tsx | 1 - packages/app/src/models/CeloTokenList.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx index 6ad6ebbb..4b9a6146 100644 --- a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx +++ b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx @@ -1,4 +1,3 @@ -import { useNetwork } from 'wagmi'; import { Frequency } from '../../models/constants'; import { useSupportFlow } from './useSupportFlow'; import { useSupportFlowWithSwap } from './useSupportFlowWithSwap'; diff --git a/packages/app/src/models/CeloTokenList.json b/packages/app/src/models/CeloTokenList.json index 8fcf9db3..a1a9c8e3 100644 --- a/packages/app/src/models/CeloTokenList.json +++ b/packages/app/src/models/CeloTokenList.json @@ -642,4 +642,4 @@ "logoURI": "https://raw.githubusercontent.com/AngleProtocol/angle-assets/main/0_tokens/agEUR/cross-chain/agEUR-celo.svg" } ] -} \ No newline at end of file +} From c4b8201364d5d77bdf4a380dd23238bf4eee992e Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 4 Jan 2024 18:50:52 +0500 Subject: [PATCH 10/18] made dropdown menu scrollable --- packages/app/src/components/Dropdown.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/Dropdown.tsx b/packages/app/src/components/Dropdown.tsx index 0847880b..f278927a 100644 --- a/packages/app/src/components/Dropdown.tsx +++ b/packages/app/src/components/Dropdown.tsx @@ -109,8 +109,10 @@ const styles = StyleSheet.create({ height: 24, }, dropdownContainer: { - height: 'auto', width: 'auto', + height: 'auto', + maxHeight: 400, + overflowY: 'scroll', backgroundColor: Colors.white, paddingTop: 10, paddingBottom: 10, @@ -133,6 +135,7 @@ const styles = StyleSheet.create({ paddingVertical: 15, minWidth: 105, width: '100%', + minHeight: 60, alignItems: 'center', }, dropdownSeparator: { From e24a8901769efd9b3f04263c0271d53617d09cd1 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 5 Jan 2024 17:59:02 +0500 Subject: [PATCH 11/18] swap works! --- packages/app/package.json | 4 +- .../app/src/components/DonateComponent.tsx | 10 ++--- .../Header/ConnectedAccountDisplay.tsx | 5 +-- .../components/WalletCards/WalletCards.tsx | 2 + .../src/hooks/useApproveSwapTokenCallback.ts | 2 +- .../hooks/useContractCalls/useSupportFlow.ts | 2 +- .../useSupportFlowWithSwap.ts | 2 +- .../useContractCalls/useSupportSingleBatch.ts | 2 +- .../useSupportSingleTransferAndCall.ts | 2 +- packages/app/src/hooks/useGetTokenPrice.tsx | 2 +- packages/app/src/hooks/useSwapRoute.tsx | 40 +++++++++++++------ packages/app/src/hooks/useTokenList.ts | 7 +++- packages/app/src/lib/formatAmount.ts | 15 ------- .../app/src/lib/formatDecimalStringInput.ts | 7 ++++ yarn.lock | 21 ++++------ 15 files changed, 65 insertions(+), 58 deletions(-) delete mode 100644 packages/app/src/lib/formatAmount.ts create mode 100644 packages/app/src/lib/formatDecimalStringInput.ts diff --git a/packages/app/package.json b/packages/app/package.json index 377de5eb..d5839bd7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -27,8 +27,9 @@ "@react-native-firebase/analytics": "16.7.0", "@react-native-firebase/app": "16.7.0", "@superfluid-finance/sdk-core": "^0.3.2", + "@uniswap/router-sdk": "^1.7.1", "@uniswap/sdk-core": "^4.0.7", - "@uniswap/smart-order-router": "^3.16.23", + "@uniswap/smart-order-router": "^3.20.0", "@uniswap/v3-sdk": "^3.10.0", "@usedapp/core": "^1.2.10", "@wagmi/core": "^1.4.5", @@ -44,7 +45,6 @@ "ethers": "^5.6.2", "fast-text-encoding": "^1.0.6", "graphql": "^16.6.0", - "jsbi": "3.1.4", "lodash": "^4.17.21", "mixpanel-react-native": "^2.3.1", "mobile-device-detect": "^0.4.3", diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 24e4327d..7eb5389d 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -28,6 +28,7 @@ import ApproveSwapModal from './ApproveSwapModal'; import { waitForTransaction } from '@wagmi/core'; import { TransactionReceipt } from 'viem'; import { useToken, useTokenList } from '../hooks/useTokenList'; +import { formatDecimalStringInput } from '../lib/formatDecimalStringInput'; interface DonateComponentProps { collective: IpfsCollective; @@ -96,11 +97,11 @@ function DonateComponent({ collective }: DonateComponentProps) { setErrorMessage('An error occurred while generating a transaction to approve the token.'); return; } - let txReceipt: TransactionReceipt | undefined; const txHash = await handleApproveToken(); if (txHash === undefined) { return; } + let txReceipt: TransactionReceipt | undefined; try { txReceipt = await waitForTransaction({ chainId: chain?.id, @@ -109,7 +110,6 @@ function DonateComponent({ collective }: DonateComponentProps) { timeout: 1000 * 60 * 5, }); } catch (error) { - setApproveSwapModalVisible(false); setErrorMessage( 'Something went wrong: Your token approval transaction was not confirmed within the timeout period.' ); @@ -144,7 +144,7 @@ function DonateComponent({ collective }: DonateComponentProps) { .toString(); const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; - const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus === SwapRouteState.NO_ROUTE; + const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus !== SwapRouteState.READY; const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact.gte(0.1) : false; const { price } = useGetTokenPrice(currency); @@ -184,7 +184,7 @@ function DonateComponent({ collective }: DonateComponentProps) { placeholder={'0.00'} style={styles.subHeading} maxLength={7} - onChangeText={(value: string) => setDecimalDonationAmount(parseFloat(value))} + onChangeText={(value: string) => setDecimalDonationAmount(formatDecimalStringInput(value))} /> @@ -214,7 +214,7 @@ function DonateComponent({ collective }: DonateComponentProps) { placeholder={'0.00'} style={styles.subHeading} maxLength={7} - onChangeText={(value: string) => setDecimalDonationAmount(parseFloat(value))} + onChangeText={(value: string) => setDecimalDonationAmount(formatDecimalStringInput(value))} /> diff --git a/packages/app/src/components/Header/ConnectedAccountDisplay.tsx b/packages/app/src/components/Header/ConnectedAccountDisplay.tsx index 1b7e77c7..c2889cb3 100644 --- a/packages/app/src/components/Header/ConnectedAccountDisplay.tsx +++ b/packages/app/src/components/Header/ConnectedAccountDisplay.tsx @@ -1,6 +1,5 @@ import { Image, StyleSheet, Text, View } from 'react-native'; import { InterRegular } from '../../utils/webFonts'; -import { formatAmount } from '../../lib/formatAmount'; import { formatAddress } from '../../lib/formatAddress'; import { useEnsName, useNetwork } from 'wagmi'; import { Colors } from '../../utils/colors'; @@ -40,7 +39,7 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => width: 240, justifyContent: 'space-between', }}> - {formatAmount(tokenBalance as any)} + {tokenBalance} {ensName ? ( @@ -69,7 +68,7 @@ export const ConnectedAccountDisplay = (props: ConnectedAccountDisplayProps) => flex: 1, justifyContent: 'space-between', }}> - {formatAmount(tokenBalance as any)} + {tokenBalance} {ensName ? ( diff --git a/packages/app/src/components/WalletCards/WalletCards.tsx b/packages/app/src/components/WalletCards/WalletCards.tsx index 220bfc55..f6cb6bd8 100644 --- a/packages/app/src/components/WalletCards/WalletCards.tsx +++ b/packages/app/src/components/WalletCards/WalletCards.tsx @@ -45,6 +45,7 @@ function WalletCards({ stewardIpfsCollectives.length > 0 && steward.collectives?.map((collective, i) => ( 0 && donor.collectives?.map((collective, i) => ( { // user rejected the transaction - toggleApproveSwapModalVisible(false); return undefined; }); + toggleApproveSwapModalVisible(false); return result?.hash; }; }, [writeAsync, toggleApproveSwapModalVisible]); diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts index bb31532f..53addefc 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts @@ -45,8 +45,8 @@ export function useSupportFlow( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportFlow(signer, collective, flowRate); toggleCompleteDonationModal(true); + const tx = await sdk.supportFlow(signer, collective, flowRate); await tx.wait(); navigate(`/profile/${address}`); return; diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts index 11ac800f..ae16d31a 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts @@ -61,6 +61,7 @@ export function useSupportFlowWithSwap( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); + toggleCompleteDonationModal(true); const tx = await sdk.supportFlowWithSwap(signer, collective, flowRate, { amount: amountIn, minReturn: minReturnFromSwap, @@ -68,7 +69,6 @@ export function useSupportFlowWithSwap( swapFrom: tokenIn.address, deadline: Math.floor(Date.now() / 1000 + 1800).toString(), }); - toggleCompleteDonationModal(true); await tx.wait(); navigate(`/profile/${address}`); return; diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts index 1de7ee8b..527b001c 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts @@ -43,8 +43,8 @@ export function useSupportSingleBatch( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); toggleCompleteDonationModal(true); + const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); await tx.wait(); navigate(`/profile/${address}`); return; diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts index 7591a31b..65d9ae6e 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts @@ -43,8 +43,8 @@ export function useSupportSingleTransferAndCall( try { const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); - const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); toggleCompleteDonationModal(true); + const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); await tx.wait(); navigate(`/profile/${address}`); return; diff --git a/packages/app/src/hooks/useGetTokenPrice.tsx b/packages/app/src/hooks/useGetTokenPrice.tsx index c71cf6ed..82191225 100644 --- a/packages/app/src/hooks/useGetTokenPrice.tsx +++ b/packages/app/src/hooks/useGetTokenPrice.tsx @@ -27,7 +27,7 @@ const getTokenPrice = (currency: string, token: Token): Promise { - return res.data[tokenAddress.toLowerCase()].usd; + return res.data[tokenAddress.toLowerCase()]?.usd; }) .catch((err) => { console.error(err); diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 35f50b4f..51ad673d 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,4 +1,4 @@ -import { AlphaRouter, SwapRoute, SwapType, V3Route } from '@uniswap/smart-order-router'; +import { AlphaRouter, SwapRoute, V3Route } from '@uniswap/smart-order-router'; import { CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'; import { useAccount, useNetwork } from 'wagmi'; import { GDToken } from '../models/constants'; @@ -8,6 +8,7 @@ import Decimal from 'decimal.js'; import { useEffect, useState } from 'react'; import { encodeRouteToPath } from '@uniswap/v3-sdk'; import { useToken } from './useTokenList'; +import { Protocol } from '@uniswap/router-sdk'; export enum SwapRouteState { LOADING, @@ -19,7 +20,7 @@ export function useSwapRoute( currencyIn: string, decimalAmountIn: number, duration: number, - slippageTolerance: Percent = new Percent(1, 100) + slippageTolerance: Percent = new Percent(3, 100) ): { path?: string; quote?: Decimal; @@ -40,6 +41,7 @@ export function useSwapRoute( setRoute(undefined); return; } + const router = new AlphaRouter({ chainId: chain.id, provider: signer.provider, @@ -47,28 +49,42 @@ export function useSwapRoute( const rawAmountIn = calculateRawTotalDonation(decimalAmountIn, duration, tokenIn.decimals); const inputAmount = CurrencyAmount.fromRawAmount(tokenIn, rawAmountIn.toFixed(0, Decimal.ROUND_DOWN)); + router - .route(inputAmount, GDToken, TradeType.EXACT_INPUT, { - type: SwapType.SWAP_ROUTER_02, - recipient: address, - slippageTolerance: slippageTolerance, - deadline: Math.floor(Date.now() / 1000 + 1800), - }) + .route( + inputAmount, + GDToken, + TradeType.EXACT_INPUT, + // TODO: use SwapConfig when https://github.com/Uniswap/sdk-core/issues/20 is resolved by https://github.com/Uniswap/sdk-core/pull/69 + // { + // type: SwapType.SWAP_ROUTER_02, + // recipient: address, + // slippageTolerance: slippageTolerance, + // deadline: Math.floor(Date.now() / 1000 + 1800), + // }, + undefined, + { + protocols: [Protocol.V3], + } + ) .then((swapRoute) => { setRoute(swapRoute ?? undefined); + }) + .catch((e) => { + console.error(e); + setRoute(undefined); }); }, [address, chain?.id, signer?.provider, tokenIn, decimalAmountIn, duration, slippageTolerance]); if (!route) { - return { status: SwapRouteState.LOADING }; - } else if (!route.methodParameters) { return { status: SwapRouteState.NO_ROUTE }; } else { // This typecast is safe because Uniswap v2 is not deployed on Celo const path = encodeRouteToPath(route.route[0].route as V3Route, false); const quote = new Decimal(route.quote.toFixed(18)); - const rawMinimumAmountOut = route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); - const priceImpact = new Decimal(route.trade.priceImpact.toFixed(4)); + // TODO: use commented out values when https://github.com/Uniswap/sdk-core/issues/20 is resolved by https://github.com/Uniswap/sdk-core/pull/69 + const rawMinimumAmountOut = route.quoteGasAdjusted?.numerator.toString(); // route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); + const priceImpact = new Decimal(0.05); // new Decimal(route.trade.priceImpact.toFixed(4)); return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY }; } } diff --git a/packages/app/src/hooks/useTokenList.ts b/packages/app/src/hooks/useTokenList.ts index 8b3b54d1..e018a93d 100644 --- a/packages/app/src/hooks/useTokenList.ts +++ b/packages/app/src/hooks/useTokenList.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { Token } from '@uniswap/sdk-core'; import CeloTokenList from '../models/CeloTokenList.json'; +import { SupportedNetwork } from '../models/constants'; export function useToken(symbol: string): Token { return useTokenList()[symbol]; @@ -9,7 +10,11 @@ export function useToken(symbol: string): Token { export function useTokenList(): Record { return useMemo(() => { const tokenList: Record = {}; - CeloTokenList.tokens.forEach((token) => { + const sortedList = CeloTokenList.tokens.sort((a, b) => a.symbol.localeCompare(b.symbol)); + sortedList.forEach((token) => { + if (token.chainId !== SupportedNetwork.celo) { + return; + } tokenList[token.symbol] = new Token(token.chainId, token.address, token.decimals, token.symbol); }); return tokenList; diff --git a/packages/app/src/lib/formatAmount.ts b/packages/app/src/lib/formatAmount.ts deleted file mode 100644 index 28e23507..00000000 --- a/packages/app/src/lib/formatAmount.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const formatAmount = (eth: any): any => { - if (!eth) return '0'; - - const parsed = eth?.split('.'); - const beforeDecimal = parsed[0]; - let formatted; - const afterDecimal = parsed[1]; - - if (beforeDecimal === '0' && afterDecimal !== '0') { - return (formatted = `0.${afterDecimal?.slice(0, 4) || 0}`); - } else if (beforeDecimal && afterDecimal > 0) { - return (formatted = `${beforeDecimal}.${afterDecimal?.slice(0, 4) || 0}`); - } - return formatted; -}; diff --git a/packages/app/src/lib/formatDecimalStringInput.ts b/packages/app/src/lib/formatDecimalStringInput.ts new file mode 100644 index 00000000..4aad5eeb --- /dev/null +++ b/packages/app/src/lib/formatDecimalStringInput.ts @@ -0,0 +1,7 @@ +export function formatDecimalStringInput(value: string) { + const float = parseFloat(value); + if (isNaN(float)) { + return 0; + } + return float; +} diff --git a/yarn.lock b/yarn.lock index fbfd9f66..cb076d55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4549,8 +4549,9 @@ __metadata: "@types/react-dom": ^18.2.5 "@types/react-native": ^0.72.2 "@types/react-test-renderer": ^18.0.0 + "@uniswap/router-sdk": ^1.7.1 "@uniswap/sdk-core": ^4.0.7 - "@uniswap/smart-order-router": ^3.16.23 + "@uniswap/smart-order-router": ^3.20.0 "@uniswap/v3-sdk": ^3.10.0 "@usedapp/core": ^1.2.10 "@vitejs/plugin-react": ^4.1.1 @@ -4571,7 +4572,6 @@ __metadata: fast-text-encoding: ^1.0.6 graphql: ^16.6.0 jest: ^29.2.1 - jsbi: 3.1.4 lodash: ^4.17.21 metro-react-native-babel-preset: 0.73.9 mixpanel-react-native: ^2.3.1 @@ -11602,7 +11602,7 @@ __metadata: languageName: node linkType: hard -"@uniswap/router-sdk@npm:^1.6.0": +"@uniswap/router-sdk@npm:^1.6.0, @uniswap/router-sdk@npm:^1.7.1": version: 1.7.1 resolution: "@uniswap/router-sdk@npm:1.7.1" dependencies: @@ -11664,9 +11664,9 @@ __metadata: languageName: node linkType: hard -"@uniswap/smart-order-router@npm:^3.16.23": - version: 3.17.2 - resolution: "@uniswap/smart-order-router@npm:3.17.2" +"@uniswap/smart-order-router@npm:^3.20.0": + version: 3.20.0 + resolution: "@uniswap/smart-order-router@npm:3.20.0" dependencies: "@uniswap/default-token-list": ^11.2.0 "@uniswap/permit2-sdk": ^1.2.0 @@ -11692,7 +11692,7 @@ __metadata: stats-lite: ^2.2.0 peerDependencies: jsbi: ^3.2.0 - checksum: 892fe402328337a5857caae15a1d6a3b63527424b00efe836bfada417cf7ea078c87dd1194d0f336f99fa6fcff643d4f089b13c6bfb642a502aa00afe6e29cfa + checksum: 714082c5ac4f6ffe17e7a6255cf89692e229d875e3db91ad461e3f6d137200710afc27e464ff7f7e98ff398f79ba05a08f6b165340489f12460f043a06bccd98 languageName: node linkType: hard @@ -27235,13 +27235,6 @@ __metadata: languageName: node linkType: hard -"jsbi@npm:3.1.4": - version: 3.1.4 - resolution: "jsbi@npm:3.1.4" - checksum: 8dad8122b5060642d5763405f3c210c385747d5b5e95639e8bddbb2bb5d06fc55a25c6d7d7dec7cbc071d4db58a9525578d29a58d1357976c03d75b3712f83a4 - languageName: node - linkType: hard - "jsbi@npm:^3.1.1, jsbi@npm:^3.1.4": version: 3.2.5 resolution: "jsbi@npm:3.2.5" From 2fb34b35dbaa1d7b9b70b964dec680e5502d4af3 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 5 Jan 2024 18:18:23 +0500 Subject: [PATCH 12/18] adjustments to reflect uniswap default slippage tolerance --- packages/app/src/hooks/useSwapRoute.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 51ad673d..70626752 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -20,7 +20,7 @@ export function useSwapRoute( currencyIn: string, decimalAmountIn: number, duration: number, - slippageTolerance: Percent = new Percent(3, 100) + slippageTolerance: Percent = new Percent(50, 10_000) ): { path?: string; quote?: Decimal; @@ -83,8 +83,9 @@ export function useSwapRoute( const path = encodeRouteToPath(route.route[0].route as V3Route, false); const quote = new Decimal(route.quote.toFixed(18)); // TODO: use commented out values when https://github.com/Uniswap/sdk-core/issues/20 is resolved by https://github.com/Uniswap/sdk-core/pull/69 - const rawMinimumAmountOut = route.quoteGasAdjusted?.numerator.toString(); // route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); - const priceImpact = new Decimal(0.05); // new Decimal(route.trade.priceImpact.toFixed(4)); + const rawMinimumAmountOut = + route.quoteGasAndPortionAdjusted?.numerator.toString() ?? route.quoteGasAdjusted.numerator.toString(); // route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); + const priceImpact = new Decimal(0.005); // new Decimal(route.trade.priceImpact.toFixed(4)); return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY }; } } From b3ad4e14d3e9783a06792e6077590577bef0be72 Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 6 Jan 2024 10:48:32 +0500 Subject: [PATCH 13/18] fixed jsbi via vite configuration --- .../app/src/components/DonateComponent.tsx | 3 ++- packages/app/src/hooks/useGetTokenPrice.tsx | 24 +++++++++++++++---- packages/app/src/hooks/useSwapRoute.tsx | 24 ++++++++----------- packages/app/web/vite.config.ts | 2 ++ 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 7eb5389d..e093e8d0 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -145,7 +145,8 @@ function DonateComponent({ collective }: DonateComponentProps) { const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus !== SwapRouteState.READY; - const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact.gte(0.1) : false; + // TODO: what is an acceptable price impact? + const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact.gte(10) : false; const { price } = useGetTokenPrice(currency); const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; diff --git a/packages/app/src/hooks/useGetTokenPrice.tsx b/packages/app/src/hooks/useGetTokenPrice.tsx index 82191225..f57e1d68 100644 --- a/packages/app/src/hooks/useGetTokenPrice.tsx +++ b/packages/app/src/hooks/useGetTokenPrice.tsx @@ -21,11 +21,11 @@ export const useGetTokenPrice = (currency: string): { price?: number; isLoading: return { price, isLoading }; }; -const getTokenPrice = (currency: string, token: Token): Promise => { +const getTokenPrice = async (currency: string, token: Token): Promise => { let tokenAddress = coingeckoTokenMapping[currency] ?? token.address; - const url = `https://api.coingecko.com/api/v3/simple/token_price/celo?contract_addresses=${tokenAddress}&vs_currencies=usd`; - return axios - .get(url) + const priceByContractUrl = `https://api.coingecko.com/api/v3/simple/token_price/celo?contract_addresses=${tokenAddress}&vs_currencies=usd`; + const priceByContract: number | undefined = await axios + .get(priceByContractUrl) .then((res) => { return res.data[tokenAddress.toLowerCase()]?.usd; }) @@ -33,4 +33,20 @@ const getTokenPrice = (currency: string, token: Token): Promise { + return res.data[currency.toLowerCase()]?.usd; + }) + .catch((err) => { + console.error(err); + return undefined; + }); }; diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 70626752..a822b080 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,4 +1,4 @@ -import { AlphaRouter, SwapRoute, V3Route } from '@uniswap/smart-order-router'; +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'; @@ -55,14 +55,12 @@ export function useSwapRoute( inputAmount, GDToken, TradeType.EXACT_INPUT, - // TODO: use SwapConfig when https://github.com/Uniswap/sdk-core/issues/20 is resolved by https://github.com/Uniswap/sdk-core/pull/69 - // { - // type: SwapType.SWAP_ROUTER_02, - // recipient: address, - // slippageTolerance: slippageTolerance, - // deadline: Math.floor(Date.now() / 1000 + 1800), - // }, - undefined, + { + type: SwapType.SWAP_ROUTER_02, + recipient: address, + slippageTolerance: slippageTolerance, + deadline: Math.floor(Date.now() / 1000 + 1800), + }, { protocols: [Protocol.V3], } @@ -76,16 +74,14 @@ export function useSwapRoute( }); }, [address, chain?.id, signer?.provider, tokenIn, decimalAmountIn, duration, slippageTolerance]); - if (!route) { + if (!route || !route.methodParameters) { return { status: SwapRouteState.NO_ROUTE }; } else { // This typecast is safe because Uniswap v2 is not deployed on Celo const path = encodeRouteToPath(route.route[0].route as V3Route, false); const quote = new Decimal(route.quote.toFixed(18)); - // TODO: use commented out values when https://github.com/Uniswap/sdk-core/issues/20 is resolved by https://github.com/Uniswap/sdk-core/pull/69 - const rawMinimumAmountOut = - route.quoteGasAndPortionAdjusted?.numerator.toString() ?? route.quoteGasAdjusted.numerator.toString(); // route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); - const priceImpact = new Decimal(0.005); // new Decimal(route.trade.priceImpact.toFixed(4)); + const rawMinimumAmountOut = route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); + const priceImpact = new Decimal(route.trade.priceImpact.toFixed(4)); return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY }; } } diff --git a/packages/app/web/vite.config.ts b/packages/app/web/vite.config.ts index dcd24ece..94c7d59e 100644 --- a/packages/app/web/vite.config.ts +++ b/packages/app/web/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; import dynamicImports from 'vite-plugin-dynamic-import'; import viteTsconfigPaths from 'vite-tsconfig-paths'; +import * as path from 'path'; // https://vitejs.dev/config/ export default defineConfig({ @@ -12,6 +13,7 @@ export default defineConfig({ 'react-native': 'react-native-web', 'react-native-svg': 'react-native-svg-web', 'react-native-webview': 'react-native-web-webview', + jsbi: path.resolve(__dirname, '..', 'node_modules', 'jsbi', 'dist', 'jsbi-cjs.js'), }, dedupe: ['react', 'ethers', 'react-dom', 'native-base'], }, From 72e8905f8e44efe54042dbbfbaa6fbd24ec495dc Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 7 Jan 2024 20:38:45 +0500 Subject: [PATCH 14/18] changed priceImpact to number --- packages/app/src/components/DonateComponent.tsx | 2 +- packages/app/src/hooks/useSwapRoute.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index e093e8d0..d2fd10c6 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -146,7 +146,7 @@ function DonateComponent({ collective }: DonateComponentProps) { const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus !== SwapRouteState.READY; // TODO: what is an acceptable price impact? - const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact.gte(10) : false; + const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact > 10 : false; const { price } = useGetTokenPrice(currency); const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index a822b080..673bf222 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -25,7 +25,7 @@ export function useSwapRoute( path?: string; quote?: Decimal; rawMinimumAmountOut?: string; - priceImpact?: Decimal; + priceImpact?: number; status: SwapRouteState; } { const { address } = useAccount(); @@ -81,7 +81,7 @@ export function useSwapRoute( const path = encodeRouteToPath(route.route[0].route as V3Route, false); const quote = new Decimal(route.quote.toFixed(18)); const rawMinimumAmountOut = route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); - const priceImpact = new Decimal(route.trade.priceImpact.toFixed(4)); + const priceImpact = parseFloat(route.trade.priceImpact.toFixed(4)); return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY }; } } From cf67b5d257c40a5196636788d48621de1b614314 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 8 Jan 2024 18:08:48 +0500 Subject: [PATCH 15/18] removed unused `WalletConnectionProvider` context --- .../src/contexts/WalletConnectionContext.tsx | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 packages/app/src/contexts/WalletConnectionContext.tsx diff --git a/packages/app/src/contexts/WalletConnectionContext.tsx b/packages/app/src/contexts/WalletConnectionContext.tsx deleted file mode 100644 index 0017369a..00000000 --- a/packages/app/src/contexts/WalletConnectionContext.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { createContext, useContext, useEffect, useState } from 'react'; -import { useEthers } from '@usedapp/core'; -import { useConnectWallet } from '@web3-onboard/react'; -import { useSwitchNetwork } from '@gooddollar/web3sdk-v2'; -import { formatAddress } from '../lib/formatAddress'; -interface IWalletConnectionContext { - disconnectWallet: () => Promise; - connectWallet: () => Promise; - walletName: string; - walletAddress: string; -} - -const WalletConnectionContext = createContext({} as IWalletConnectionContext); - -const WalletConnectionProvider: React.FC<{ - children: React.ReactNode; -}> = ({ children }) => { - const { account } = useEthers(); - const { switchNetwork } = useSwitchNetwork(); - const [{ wallet }, connect, disconnect] = useConnectWallet(); - // const [, setChain] = useSetChain(); - - const [walletName, setWalletName] = useState(''); - const [walletAddress, setWalletAddress] = useState(''); - - const connectWallet = async () => { - try { - await connect(); - await switchNetwork(42220); - } catch (error) { - console.log('connectWallet Error - ', error); - } - }; - - const disconnectWallet = async () => { - if (!wallet) return; - await disconnect(wallet); - }; - - const getWalletName = (_account: string) => { - if (!_account) { - setWalletName('Luis.celo'); - return; - } - setWalletName(formatAddress(_account, 3)); - }; - - useEffect(() => { - setWalletAddress(account || ''); - if (account) getWalletName(formatAddress(account, 3)); - }, [account]); - - return ( - - {children} - - ); -}; - -// Custom hook to consume the WalletConnectionContext -export const useWalletConnection = () => useContext(WalletConnectionContext); - -export { WalletConnectionProvider }; From f7cfdcf9791321a11ceebbe7d039ab92aa415bbd Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 8 Jan 2024 18:15:14 +0500 Subject: [PATCH 16/18] by request, handleApproveToken is no longer memoized --- .../src/hooks/useApproveSwapTokenCallback.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/app/src/hooks/useApproveSwapTokenCallback.ts b/packages/app/src/hooks/useApproveSwapTokenCallback.ts index 08d0a480..2ee2d0f3 100644 --- a/packages/app/src/hooks/useApproveSwapTokenCallback.ts +++ b/packages/app/src/hooks/useApproveSwapTokenCallback.ts @@ -37,20 +37,18 @@ export function useApproveSwapTokenCallback( const { isLoading, isSuccess, isError, writeAsync } = useContractWrite(config); - const handleApproveToken = useMemo(() => { - if (!writeAsync) { - return undefined; - } - return async () => { - toggleApproveSwapModalVisible(true); - const result = await writeAsync().catch((_) => { - // user rejected the transaction - return undefined; - }); - toggleApproveSwapModalVisible(false); - return result?.hash; - }; - }, [writeAsync, toggleApproveSwapModalVisible]); + const handleApproveToken = + writeAsync === undefined + ? undefined + : async () => { + toggleApproveSwapModalVisible(true); + const result = await writeAsync().catch((_) => { + // user rejected the transaction + return undefined; + }); + toggleApproveSwapModalVisible(false); + return result?.hash; + }; return { isLoading, From 09dbb709b9c6c91bbb18a48f5a17724b776aa82f Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 9 Jan 2024 08:54:00 +0500 Subject: [PATCH 17/18] fixed GD formatting in Donor List --- packages/app/src/components/DonorsList/DonorsList.tsx | 8 +++++--- packages/app/src/components/DonorsList/DonorsListItem.tsx | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/DonorsList/DonorsList.tsx b/packages/app/src/components/DonorsList/DonorsList.tsx index 4ab4910a..893e1df6 100644 --- a/packages/app/src/components/DonorsList/DonorsList.tsx +++ b/packages/app/src/components/DonorsList/DonorsList.tsx @@ -2,7 +2,7 @@ import { StyleSheet, View } from 'react-native'; import { DonorCollective } from '../../models/models'; import { DonorsListItem } from './DonorsListItem'; import { useMemo } from 'react'; -import { ethers } from 'ethers'; +import Decimal from 'decimal.js'; interface DonorsListProps { donors: DonorCollective[]; @@ -13,8 +13,10 @@ interface DonorsListProps { function DonorsList({ donors, listStyle }: DonorsListProps) { const sortedDonors: DonorCollective[] = useMemo(() => { - return donors.sort((donor) => { - return parseFloat(ethers.utils.formatEther(donor.contribution ?? 0)); + return donors.sort((a, b) => { + const aDecimal = new Decimal(a.contribution ?? 0); + const bDecimal = new Decimal(b.contribution ?? 0); + return bDecimal.cmp(aDecimal); }); }, [donors]); diff --git a/packages/app/src/components/DonorsList/DonorsListItem.tsx b/packages/app/src/components/DonorsList/DonorsListItem.tsx index 872b2280..d6720f42 100644 --- a/packages/app/src/components/DonorsList/DonorsListItem.tsx +++ b/packages/app/src/components/DonorsList/DonorsListItem.tsx @@ -5,6 +5,7 @@ import { DonorCollective } from '../../models/models'; import useCrossNavigate from '../../routes/useCrossNavigate'; import Decimal from 'decimal.js'; import { formatAddress } from '../../lib/formatAddress'; +import { ethers } from 'ethers'; interface DonorsListItemProps { donor: DonorCollective; @@ -15,7 +16,7 @@ export const DonorsListItem = (props: DonorsListItemProps) => { const { donor, rank } = props; const { navigate } = useCrossNavigate(); - const formattedDonations: string = new Decimal(donor.contribution ?? 0).toFixed(3); + const formattedDonations: string = new Decimal(ethers.utils.formatEther(donor.contribution) ?? 0).toFixed(3); const formattedAddress = formatAddress(donor.donor, 5); const circleBackgroundColor = rank === 1 ? Colors.yellow[100] : rank === 2 ? Colors.gray[700] : Colors.orange[400]; From 4ee4e3250d545d183b4950794e062f1eacaf68dd Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 9 Jan 2024 10:34:50 +0500 Subject: [PATCH 18/18] changed acceptable price impact percent to 5% and moved it to constants.ts --- packages/app/src/components/DonateComponent.tsx | 5 ++--- packages/app/src/models/constants.ts | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index d2fd10c6..72dfe4a1 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -16,7 +16,7 @@ import { useContractCalls, useGetTokenPrice } from '../hooks'; import { useAccount, useNetwork } from 'wagmi'; import { IpfsCollective } from '../models/models'; import { useGetDecimalBalance } from '../hooks/useGetDecimalBalance'; -import { Frequency, frequencyOptions, SupportedNetwork } from '../models/constants'; +import { acceptablePriceImpact, Frequency, frequencyOptions, SupportedNetwork } from '../models/constants'; import { InfoIconOrange } from '../assets'; import { useLocation } from 'react-router-native'; import Decimal from 'decimal.js'; @@ -145,8 +145,7 @@ function DonateComponent({ collective }: DonateComponentProps) { const isInsufficientBalance = donorCurrencyBalance ? totalDecimalDonation > donorCurrencyBalance : true; const isInsufficientLiquidity = currency !== 'G$' && swapRouteStatus !== SwapRouteState.READY; - // TODO: what is an acceptable price impact? - const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact > 10 : false; + const isUnacceptablePriceImpact = currency !== 'G$' && priceImpact ? priceImpact > acceptablePriceImpact : false; const { price } = useGetTokenPrice(currency); const usdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; diff --git a/packages/app/src/models/constants.ts b/packages/app/src/models/constants.ts index 8f56b12b..5a3b002d 100644 --- a/packages/app/src/models/constants.ts +++ b/packages/app/src/models/constants.ts @@ -1,5 +1,8 @@ import { Token } from '@uniswap/sdk-core'; +// 5% +export const acceptablePriceImpact = 5; + export enum SupportedNetwork { celo = 42220, }