From 210827f64a9ffcddc330e0f557f0c8037b81f3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Torres?= Date: Fri, 26 Apr 2024 10:47:12 +0100 Subject: [PATCH 1/5] Add USDC balance --- packages/web-app/app/_lib/actions.tsx | 1 - packages/web-app/app/_lib/queries.tsx | 40 +++++++++++++++++++ .../acquire-access-grant-button.tsx | 6 +-- .../components/dialogs/apply-dialog/index.tsx | 2 +- .../dialogs/settings-dialog/balance.tsx | 17 ++++---- .../app/_ui/components/wallet-button.tsx | 23 +++++------ yarn.lock | 5 --- 7 files changed, 63 insertions(+), 31 deletions(-) rename packages/web-app/app/_ui/components/dialogs/{ => apply-dialog}/acquire-access-grant-button.tsx (94%) diff --git a/packages/web-app/app/_lib/actions.tsx b/packages/web-app/app/_lib/actions.tsx index b9b809bb..28f41742 100644 --- a/packages/web-app/app/_lib/actions.tsx +++ b/packages/web-app/app/_lib/actions.tsx @@ -204,7 +204,6 @@ export const useSignDelegatedAccessGrant = ( }; export const useContributeToCtznd = (address: `0x${string}`) => { - const [state, setState] = useState(); const [amount, setAmount] = useState(0); const amountInWei = useMemo(() => parseEther(amount.toString()), [amount]); const { data: merkleProof } = useFetchMerkleProof(); diff --git a/packages/web-app/app/_lib/queries.tsx b/packages/web-app/app/_lib/queries.tsx index 57cc19be..af7e9cda 100644 --- a/packages/web-app/app/_lib/queries.tsx +++ b/packages/web-app/app/_lib/queries.tsx @@ -12,6 +12,9 @@ import { getServerPublicInfo, } from '../_server/info'; import { fetchAndGenerateProof } from '../_server/projects/generate-merkle-root'; +import { useAccount, useBalance } from 'wagmi'; +import { useReadCtzndSalePaymentToken } from '@/wagmi.generated'; +import { formatEther } from 'viem'; export const usePublicInfo = () => { return useQuery({ @@ -211,3 +214,40 @@ export const useFetchMerkleProof = () => { enabled: !!address, }); }; + +export const usePaymentTokenBalance = () => { + const { address } = useAccount(); + const { + data: paymentToken, + isLoading: isLoadingToken, + error: errorToken, + } = useReadCtzndSalePaymentToken(); + + const { + data: balance, + isLoading: isLoadingBalance, + error: errorBalance, + } = useBalance({ + token: paymentToken, + address, + query: { + enabled: !!paymentToken, + }, + }); + + if (!paymentToken) { + return { + data: null, + formattedValue: null, + isLoading: isLoadingToken, + error: errorToken, + }; + } + + return { + data: balance, + formattedValue: balance ? formatEther(balance.value) : null, + isLoading: isLoadingBalance || isLoadingToken, + error: errorToken || errorBalance, + }; +}; diff --git a/packages/web-app/app/_ui/components/dialogs/acquire-access-grant-button.tsx b/packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx similarity index 94% rename from packages/web-app/app/_ui/components/dialogs/acquire-access-grant-button.tsx rename to packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx index 3d6eecbf..d3e2942c 100644 --- a/packages/web-app/app/_ui/components/dialogs/acquire-access-grant-button.tsx +++ b/packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx @@ -4,9 +4,9 @@ import { useSignDelegatedAccessGrant } from '@/app/_lib/actions'; import { useTransaction } from 'wagmi'; import { useKyc } from '@/app/_providers/kyc/context'; import { useEffect } from 'react'; -import { Spinner } from '../svg/spinner'; -import { Check } from '../svg/check'; -import { Error } from '../svg/error'; +import { Spinner } from '../../svg/spinner'; +import { Check } from '../../svg/check'; +import { Error } from '../../svg/error'; import { arbitrum, arbitrumSepolia } from 'viem/chains'; type AcquireAccessGrantButton = { diff --git a/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx index f31b4fce..156622e3 100644 --- a/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx +++ b/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx @@ -3,7 +3,7 @@ import { useProjectPublicInfo, usePublicInfo } from '@/app/_lib/queries'; import { useIdOS } from '@/app/_providers/idos'; import { Dialog } from '@headlessui/react'; -import { AcquireAccessGrantButton } from '../acquire-access-grant-button'; +import { AcquireAccessGrantButton } from './acquire-access-grant-button'; import { Button } from '../..'; import { TProps, useDialog } from '@/app/_providers/dialog/context'; import { useMemo } from 'react'; diff --git a/packages/web-app/app/_ui/components/dialogs/settings-dialog/balance.tsx b/packages/web-app/app/_ui/components/dialogs/settings-dialog/balance.tsx index 7eb59cf6..85bf5f45 100644 --- a/packages/web-app/app/_ui/components/dialogs/settings-dialog/balance.tsx +++ b/packages/web-app/app/_ui/components/dialogs/settings-dialog/balance.tsx @@ -3,23 +3,22 @@ import { useAccount, useBalance } from 'wagmi'; import { Wallet } from '../../svg/wallet'; import { formatEther } from 'viem'; +import { usePaymentTokenBalance } from '@/app/_lib/queries'; export const Balance = () => { - const { address } = useAccount(); - const balance = useBalance({ - address: address, - blockTag: 'latest', - }); + const { data: balance, formattedValue } = usePaymentTokenBalance(); + + if (!balance) + return ( +
+ ); return ( <>

Balance

-

- {balance?.data?.formatted ? formatEther(balance?.data.value) : null}{' '} - {balance.data?.symbol} -

+

{`${formattedValue} ${balance.symbol}`}

); diff --git a/packages/web-app/app/_ui/components/wallet-button.tsx b/packages/web-app/app/_ui/components/wallet-button.tsx index 1902a9a7..4aee34b3 100644 --- a/packages/web-app/app/_ui/components/wallet-button.tsx +++ b/packages/web-app/app/_ui/components/wallet-button.tsx @@ -5,24 +5,25 @@ import { useDialog } from '@/app/_providers/dialog/context'; import { SettingsDialog } from './dialogs'; import { EdgeBorderButton, EdgeButton } from './edge'; import { Avatar } from './avatar'; -import { useAccount } from 'wagmi'; +import { useAccount, useBalance } from 'wagmi'; +import { formatEther } from 'viem'; +import { usePaymentTokenBalance } from '@/app/_lib/queries'; -type TConnectedButtonProps = { - displayBalance: string; -}; - -const ConnectedButton = ({ displayBalance }: TConnectedButtonProps) => { +const ConnectedButton = () => { + const { data: balance, formattedValue } = usePaymentTokenBalance(); const { open } = useDialog(); - // should show USDC balance in future, which can be same hook that displays it - // inside the settings tab + if (!balance) + return ( +
+ ); return ( } onClick={() => open(SettingsDialog.displayName)} > - {displayBalance} + {`${formattedValue} ${balance.symbol}`} ); }; @@ -61,9 +62,7 @@ export function WalletButton() { } if (account.displayBalance) { - return ( - - ); + return ; } return ( diff --git a/yarn.lock b/yarn.lock index c86947cc..60d910e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8764,11 +8764,6 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== -husky@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" - integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== - i18next-browser-languagedetector@^7.1.0: version "7.2.1" resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz#1968196d437b4c8db847410c7c33554f6c448f6f" From 24f6b8eb03999e072e31822fe70679c91a7a1739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Torres?= Date: Fri, 26 Apr 2024 11:58:03 +0100 Subject: [PATCH 2/5] Add live dashboard --- .../web-app/app/_ui/project/token-metrics.tsx | 80 +++++++++++++++---- 1 file changed, 64 insertions(+), 16 deletions(-) diff --git a/packages/web-app/app/_ui/project/token-metrics.tsx b/packages/web-app/app/_ui/project/token-metrics.tsx index 50b58e6f..f938ffe1 100644 --- a/packages/web-app/app/_ui/project/token-metrics.tsx +++ b/packages/web-app/app/_ui/project/token-metrics.tsx @@ -1,5 +1,11 @@ +import { + useReadCtzndSaleTokenToPaymentToken, + useReadCtzndSaleTotalUncappedAllocations, +} from '@/wagmi.generated'; import { usdRange } from '../utils/intl-formaters/usd-range'; import { usdValue } from '../utils/intl-formaters/usd-value'; +import { formatEther, parseEther } from 'viem'; +import clsx from 'clsx'; type TTokenMetricsProps = { minTarget: bigint; @@ -15,19 +21,20 @@ const ProgressBar = ({ value, }: { title: string; - max: bigint; - value: bigint; + max: number; + value: number; }) => { - // REVIEW and optimize this code - const valueInMillions = (BigInt(value) / BigInt(1000000n)).toString(); - const halfInMillions = ( - BigInt(max) / - BigInt(2) / - BigInt(1000000n) - ).toString(); - const maxInMillions = (BigInt(max) / BigInt(1000000n)).toString(); - const division = parseFloat(valueInMillions) / parseFloat(maxInMillions); - const percentage = `${Number(division) * 100}%`; + const valueInMillions = value / 1000_000; + const maxInMillions = max / 1000_000; + const halfInMillions = maxInMillions / 2; + const currentRelativeValue = valueInMillions / maxInMillions; + const percentage = currentRelativeValue * 100; + const displayPercentage = percentage > 0 && percentage < 1 ? 1 : percentage; + const percentageRounded = Math.round(displayPercentage) + '%'; + const displayValue = + valueInMillions > 0 && valueInMillions < 0.1 + ? '<0.1' + : valueInMillions.toFixed(2); return (
@@ -47,11 +54,26 @@ const ProgressBar = ({ aria-valuemax={Number(maxInMillions)} >
+
+ {percentageRounded === '100%' ? null : ( + <> +
+
+ {displayValue}M +
+ + )} +
{halfInMillions}M
@@ -66,6 +88,32 @@ const ProgressBar = ({ ); }; +const ProgressBarWrapper = ({ maxTarget }: { maxTarget: bigint }) => { + const { data: ctzndTokensSold, isLoading: isLoadingSaleTokens } = + useReadCtzndSaleTotalUncappedAllocations(); + const { data: tokensInvested, isLoading: isLoadingPaymentTokens } = + useReadCtzndSaleTokenToPaymentToken({ + args: [ctzndTokensSold || 0n], + }); + const value = tokensInvested ? formatEther(tokensInvested) : '0'; + + if (isLoadingSaleTokens || isLoadingPaymentTokens) + return ( + <> +
+
+ + ); + + return ( + + ); +}; + export const TokenMetrics = ({ minTarget, maxTarget, @@ -85,9 +133,9 @@ export const TokenMetrics = ({

Token Metrics

- {process.env.NEXT_PUBLIC_APPLY_OPEN === 'false' ? ( + {process.env.NEXT_PUBLIC_CONTRIBUTE_OPEN === 'true' ? (
- +
) : null}
From 73c7cdae8946bc44cfefd0bc6ab7f69aefcad560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Torres?= Date: Fri, 26 Apr 2024 18:17:53 +0100 Subject: [PATCH 3/5] Implement contribution flow by steps in modal --- packages/web-app/app/_lib/actions.tsx | 104 ++++----- packages/web-app/app/_lib/hooks.tsx | 55 ++++- packages/web-app/app/_lib/queries.tsx | 20 +- .../web-app/app/_providers/dialog/context.tsx | 2 +- .../web-app/app/_providers/dialog/index.tsx | 7 +- .../web-app/app/_providers/wagmi-config.ts | 1 - .../acquire-access-grant-button.tsx | 6 +- .../components/dialogs/apply-dialog/index.tsx | 4 +- .../dialogs/contribute-dialog/index.tsx | 220 ++++++++++++++++++ .../web-app/app/_ui/my-projects/index.tsx | 6 +- .../app/_ui/my-projects/my-project.tsx | 14 +- .../app/_ui/project/project-contribution.tsx | 67 +++--- 12 files changed, 406 insertions(+), 100 deletions(-) create mode 100644 packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx diff --git a/packages/web-app/app/_lib/actions.tsx b/packages/web-app/app/_lib/actions.tsx index 28f41742..c8a44292 100644 --- a/packages/web-app/app/_lib/actions.tsx +++ b/packages/web-app/app/_lib/actions.tsx @@ -203,76 +203,66 @@ export const useSignDelegatedAccessGrant = ( }; }; -export const useContributeToCtznd = (address: `0x${string}`) => { - const [amount, setAmount] = useState(0); - const amountInWei = useMemo(() => parseEther(amount.toString()), [amount]); - const { data: merkleProof } = useFetchMerkleProof(); - +export const useBuyCtzndTokens = () => { const { - writeContract: writeBuy, + writeContract, data: contributionTxHash, - isPending: isWritePending, - error: buyError, + isPending, + error, } = useWriteCtzndSaleBuy(); + const { data: merkleProof } = useFetchMerkleProof(); + + const buyCtzndTokens = useCallback( + (tokensToBuyInWei: bigint) => { + if (tokensToBuyInWei === undefined || merkleProof === undefined) { + return; + } + + writeContract({ args: [tokensToBuyInWei, merkleProof] }); + }, + [merkleProof, writeContract], + ); + + return { + contributionTxHash, + buyCtzndTokens, + isPending, + error, + }; +}; + +export const useSetPaymentTokenAllowance = () => { + const saleAddress = ctzndSaleAddress[sepolia.id]; + const { data: paymentToken } = useReadCtzndSalePaymentToken(); const { - writeContract: approveContract, + writeContract, data: allowanceTxHash, - error: allowanceError, - isPending: isApprovePending, + error, + isPending, } = useWriteErc20Approve(); - const { data: paymentToken } = useReadCtzndSalePaymentToken(); - const saleAddress = ctzndSaleAddress[sepolia.id]; - const { data: allowance } = useReadErc20Allowance({ - address: paymentToken, - args: [address, saleAddress], - query: { - refetchInterval: 1000, - }, - }); - const { data: tokensToBuy, error: tokenError } = - useReadCtzndSalePaymentTokenToToken({ - args: [amountInWei], - }); - const diffToAllowance = useMemo( - () => (allowance || BigInt(0)) - amountInWei, - [allowance, amountInWei], - ); - - const submit = () => { - if ( - amount <= 0 || - allowance === undefined || - paymentToken === undefined || - tokensToBuy === undefined || - merkleProof === undefined - ) { - return; - } + const setAllowance = useCallback( + (amountInWei: bigint) => { + if ( + paymentToken === undefined || + saleAddress === undefined || + amountInWei <= 0 + ) { + return; + } - if (diffToAllowance < 0) { - return approveContract({ + writeContract({ address: paymentToken, args: [saleAddress, amountInWei], }); - } - - writeBuy({ args: [tokensToBuy, merkleProof] }); - }; + }, + [paymentToken, saleAddress, writeContract], + ); return { - contributionTxHash, + setAllowance, allowanceTxHash, - diffToAllowance, - amount, - amountInWei, - setAmount, - tokensToBuy, - isPending: isApprovePending || isWritePending, - error: buyError || allowanceError || tokenError, - buyError, - allowanceError, - tokenError, - submit, + isPending, + error, }; }; diff --git a/packages/web-app/app/_lib/hooks.tsx b/packages/web-app/app/_lib/hooks.tsx index f9000ad1..36761286 100644 --- a/packages/web-app/app/_lib/hooks.tsx +++ b/packages/web-app/app/_lib/hooks.tsx @@ -1,13 +1,23 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useFetchCredentials, useFetchProjectsSaleDetails, + usePaymentTokenBalance, useProjectPublicInfo, usePublicInfo, } from './queries'; import { useKyc } from '../_providers/kyc/context'; import { compareAddresses, isValidGrant } from './utils'; import { TProjectInfoArgs } from '../_server/info'; +import { + ctzndSaleAddress, + useReadCtzndSalePaymentToken, + useReadCtzndSalePaymentTokenToToken, + useReadErc20Allowance, + useWriteErc20Approve, +} from '@/wagmi.generated'; +import { formatEther, parseEther } from 'viem'; +import { sepolia } from 'viem/chains'; export const useKycCredential = () => { const { @@ -177,3 +187,46 @@ export const useHasProjectGrant = (projectId: string) => { }; }, [hasGrant, isLoading, error, isSuccess]); }; + +export const useCtzndPaymentTokenAllowance = (userAddress: `0x${string}`) => { + const saleAddress = ctzndSaleAddress[sepolia.id]; + const { data: paymentToken } = useReadCtzndSalePaymentToken(); + const { + data: allowance, + isLoading, + error, + } = useReadErc20Allowance({ + address: paymentToken, + args: [userAddress, saleAddress], + query: { + staleTime: 0, + refetchInterval: 5000, + }, + }); + + return { + allowance, + isLoading, + error, + }; +}; + +export const useContributeToCtznd = () => { + const [amount, setAmount] = useState(0); + const amountInWei = useMemo(() => parseEther(amount.toString()), [amount]); + const { formattedValue: maxAmount } = usePaymentTokenBalance(); + const { data: tokensToBuyInWei, error: tokenError } = + useReadCtzndSalePaymentTokenToToken({ + args: [amountInWei], + }); + + return { + amount, + setAmount, + amountInWei, + maxAmount: Number(maxAmount || 0), + tokensToBuyInWei: tokensToBuyInWei || 0n, + tokensToBuy: tokensToBuyInWei ? Number(formatEther(tokensToBuyInWei)) : 0, + error: tokenError, + }; +}; diff --git a/packages/web-app/app/_lib/queries.tsx b/packages/web-app/app/_lib/queries.tsx index af7e9cda..6fd5f6a4 100644 --- a/packages/web-app/app/_lib/queries.tsx +++ b/packages/web-app/app/_lib/queries.tsx @@ -13,8 +13,13 @@ import { } from '../_server/info'; import { fetchAndGenerateProof } from '../_server/projects/generate-merkle-root'; import { useAccount, useBalance } from 'wagmi'; -import { useReadCtzndSalePaymentToken } from '@/wagmi.generated'; +import { + useReadCtzndSalePaymentToken, + useReadCtzndSaleRate, + useReadCtzndSaleUncappedAllocation, +} from '@/wagmi.generated'; import { formatEther } from 'viem'; +import { useProject } from '../_providers/project/context'; export const usePublicInfo = () => { return useQuery({ @@ -251,3 +256,16 @@ export const usePaymentTokenBalance = () => { error: errorToken || errorBalance, }; }; + +export const useUserTotalInvestedUsdcCtznd = (address: `0x${string}`) => { + const { data: tokens } = useReadCtzndSaleUncappedAllocation({ + args: [address], + }); + const { data: rate } = useReadCtzndSaleRate(); + const usdcValue = + tokens && rate + ? parseFloat(formatEther(tokens)) * parseFloat(formatEther(rate)) + : 0; + + return usdcValue; +}; diff --git a/packages/web-app/app/_providers/dialog/context.tsx b/packages/web-app/app/_providers/dialog/context.tsx index 01b563a0..058d3564 100644 --- a/packages/web-app/app/_providers/dialog/context.tsx +++ b/packages/web-app/app/_providers/dialog/context.tsx @@ -2,7 +2,7 @@ import { createContext, useContext } from 'react'; export type TProps = { - [key: string]: string; + [key: string]: string | number | bigint; }; type TDialogContextValue = { diff --git a/packages/web-app/app/_providers/dialog/index.tsx b/packages/web-app/app/_providers/dialog/index.tsx index 8bdb02d4..49a9e411 100644 --- a/packages/web-app/app/_providers/dialog/index.tsx +++ b/packages/web-app/app/_providers/dialog/index.tsx @@ -2,13 +2,18 @@ import { useMemo, useState, PropsWithChildren } from 'react'; import { DialogContext, TProps } from './context'; import { DialogWrapper } from '@/app/_ui/components/dialogs/dialog-wrapper'; import { ApplyDialog, SettingsDialog } from '@/app/_ui/components/dialogs'; +import { ContributeDialog } from '@/app/_ui/components/dialogs/contribute-dialog'; type TDialogComponent = { displayName: string; (props: TProps): JSX.Element; }; -const dialogComponents: TDialogComponent[] = [SettingsDialog, ApplyDialog]; +const dialogComponents: TDialogComponent[] = [ + SettingsDialog, + ApplyDialog, + ContributeDialog, +]; const emptyProps = {}; diff --git a/packages/web-app/app/_providers/wagmi-config.ts b/packages/web-app/app/_providers/wagmi-config.ts index 4afac560..530188bf 100644 --- a/packages/web-app/app/_providers/wagmi-config.ts +++ b/packages/web-app/app/_providers/wagmi-config.ts @@ -18,5 +18,4 @@ export const wagmiConfig = getDefaultConfig({ : []), ], ssr: true, - pollingInterval: 500, }); diff --git a/packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx b/packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx index d3e2942c..d75411ad 100644 --- a/packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx +++ b/packages/web-app/app/_ui/components/dialogs/apply-dialog/acquire-access-grant-button.tsx @@ -20,6 +20,10 @@ const Done = ({ hash }: { hash: `0x${string}` }) => { chainId: process.env.NEXT_PUBLIC_ENABLE_TESTNETS ? arbitrumSepolia.id : arbitrum.id, + query: { + staleTime: 0, + refetchIntervalInBackground: true, + }, }); const { refetchGrants, refetchKyc } = useKyc(); @@ -29,7 +33,7 @@ const Done = ({ hash }: { hash: `0x${string}` }) => { await refetchKyc(); await refetch(); - }, 10000); + }, 1000); return () => clearInterval(interval); }, [refetchGrants, refetch, refetchKyc]); diff --git a/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx index 156622e3..4a503ff3 100644 --- a/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx +++ b/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx @@ -190,7 +190,9 @@ export const ApplyDialog = ({ projectId }: TProps) => { return ; } - return ; + if (typeof projectId === 'string') { + return ; + } }; ApplyDialog.displayName = 'applyDialog'; diff --git a/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx new file mode 100644 index 00000000..dda4a65f --- /dev/null +++ b/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useCtzndPaymentTokenAllowance } from '@/app/_lib/hooks'; +import { Dialog } from '@headlessui/react'; +import { formatEther } from 'viem'; +import { Button } from '../../button'; +import { + useBuyCtzndTokens, + useSetPaymentTokenAllowance, +} from '@/app/_lib/actions'; +import { useCallback, useEffect } from 'react'; +import { Spinner } from '../../svg/spinner'; +import { useTransaction } from 'wagmi'; +import { sepolia } from 'viem/chains'; +import { Check } from '../../svg/check'; +import Link from 'next/link'; +import { Error } from '../../svg/error'; + +const Done = ({ hash }: { hash: `0x${string}` }) => { + const { data } = useTransaction({ + hash, + chainId: sepolia.id, + query: { + staleTime: 0, + refetchInterval: 1000, + refetchIntervalInBackground: true, + }, + }); + + if (data?.blockHash) { + return ( + + View on etherscan + + ); + } + + return ( +
+ + + Validating Transaction + +
+ ); +}; + +type TAllowFundsProps = { + amount: number; + amountInWei: bigint; + allowance: bigint; +}; + +const AllowFunds = ({ amount, amountInWei, allowance }: TAllowFundsProps) => { + const { error, setAllowance, isPending, allowanceTxHash } = + useSetPaymentTokenAllowance(); + + const handleClick = useCallback(() => { + setAllowance(amountInWei); + }, [setAllowance, amountInWei]); + + return ( +
+
+
Current allowed value:
+
+ {allowance ? formatEther(allowance) : 0} USDC +
+
+ {allowanceTxHash ? ( +
+ +
+ ) : ( + + )} +
+ ); +}; + +const Contribute = ({ tokensToBuyInWei }: { tokensToBuyInWei: bigint }) => { + const { contributionTxHash, buyCtzndTokens, error } = useBuyCtzndTokens(); + + useEffect(() => { + if (!tokensToBuyInWei || contributionTxHash) return; + buyCtzndTokens(tokensToBuyInWei); + }, [tokensToBuyInWei, buyCtzndTokens, contributionTxHash]); + + if (contributionTxHash) { + return ( + <> + + Contribution submitted +
+ +
+ + ); + } + + if (error) { + const errorMessage = error as unknown as any; + return ( + <> + +
Failed
+
+ {errorMessage?.shortMessage || "Couldn't submit contribution"} +
+ + ); + } + + return ( + <> + + Proceed in your wallet + + ); +}; + +type TDescriptionProps = { + amount: number; + tokensToBuy: number; +}; + +const Description = ({ amount, tokensToBuy }: TDescriptionProps) => ( +
+
+
My Contribution
+
{amount} USDC
+
+
+
CTND Amount
+
{tokensToBuy} CTND
+
+
+); + +type TContributeDialogProps = { + userAddress: `0x${string}`; + amount: number; + tokensToBuy: number; + amountInWei: bigint; + tokensToBuyInWei: bigint; +}; + +export function ContributeDialog({ + userAddress, + amount, + tokensToBuy, + amountInWei, + tokensToBuyInWei, +}: TContributeDialogProps) { + const { allowance, isLoading, error } = + useCtzndPaymentTokenAllowance(userAddress); + + if (isLoading) return
Loading...
; + if (error || allowance === undefined) return
{error?.message}
; + + const hasEnoughAllowance = allowance && allowance >= amountInWei; + if (hasEnoughAllowance) + return ( + <> + + + +
+ +

+ Keep in mind that if you want to contribute again, you’ll need to + use the same wallet. +

+
+ + ); + + return ( + <> + + Allow USDC funds management + +

+ In order to contribute to this project, you need to allow the app to + withdraw you USDC funds. +

+
+

+ Please make sure you allow at least the amount you wish to contribute ( + {amount} USDC). +

+
+ + +
+ + ); +} + +ContributeDialog.displayName = 'contributeDialog'; diff --git a/packages/web-app/app/_ui/my-projects/index.tsx b/packages/web-app/app/_ui/my-projects/index.tsx index 6fc8a290..ea72e70b 100644 --- a/packages/web-app/app/_ui/my-projects/index.tsx +++ b/packages/web-app/app/_ui/my-projects/index.tsx @@ -9,6 +9,8 @@ import { getRelativePath } from '../utils/getRelativePath'; import { NoProjects } from './no-projects'; import { MyProjectSkeleton } from './my-project'; import { useReadCtzndSaleInvestorCount } from '@/wagmi.generated'; +import { useAccount } from 'wagmi'; +import { useUserTotalInvestedUsdcCtznd } from '@/app/_lib/queries'; const ProjectRow = ({ logo, @@ -16,8 +18,10 @@ const ProjectRow = ({ minTarget, maxTarget, }: TProjectSaleDetails) => { + const { address } = useAccount(); const { data: contributions } = useReadCtzndSaleInvestorCount(); const totalContributions = contributions ? contributions.toString() : 0; + const totalUsdc = useUserTotalInvestedUsdcCtznd(address!); return (
{totalContributions}
{usdRange(minTarget, maxTarget)}
-
0 USDC
+
{totalUsdc} USDC
); diff --git a/packages/web-app/app/_ui/my-projects/my-project.tsx b/packages/web-app/app/_ui/my-projects/my-project.tsx index 38f5bef1..87def489 100644 --- a/packages/web-app/app/_ui/my-projects/my-project.tsx +++ b/packages/web-app/app/_ui/my-projects/my-project.tsx @@ -1,5 +1,8 @@ import { useProject } from '@/app/_providers/project/context'; -import { useFetchProjectsSaleDetails } from '@/app/_lib/queries'; +import { + useFetchProjectsSaleDetails, + useUserTotalInvestedUsdcCtznd, +} from '@/app/_lib/queries'; import { NavLink } from '../components/nav-link'; import { Right } from '../components/svg/right'; import Image from 'next/image'; @@ -58,14 +61,7 @@ const Header = ({ const MyContribution = () => { const { address } = useAccount(); const { projectId } = useProject(); - const { data: tokens } = useReadCtzndSaleUncappedAllocation({ - args: [address!], - }); - const { data: rate } = useReadCtzndSaleRate(); - const usdcValue = - tokens && rate - ? parseFloat(formatEther(tokens)) * parseFloat(formatEther(rate)) - : 0; + const usdcValue = useUserTotalInvestedUsdcCtznd(address!); return (
diff --git a/packages/web-app/app/_ui/project/project-contribution.tsx b/packages/web-app/app/_ui/project/project-contribution.tsx index a162f813..052ad982 100644 --- a/packages/web-app/app/_ui/project/project-contribution.tsx +++ b/packages/web-app/app/_ui/project/project-contribution.tsx @@ -5,14 +5,20 @@ import { Button } from '../components'; import { Spinner } from '../components/svg/spinner'; import { DataFields } from './contribution/DataFields'; -import { useContributeToCtznd } from '@/app/_lib/actions'; import { formatEther } from 'viem'; +import { useDialog } from '@/app/_providers/dialog/context'; +import { ContributeDialog } from '../components/dialogs/contribute-dialog'; +import { useContributeToCtznd } from '@/app/_lib/hooks'; -const getErrorMessage = (amount: number, error: any) => { +const getErrorMessage = (amount: number, maxAmount: number, error: any) => { if (amount < 0) { return 'The amount must be greater than 0'; } + if (amount > maxAmount) { + return 'The amount exceeds the maximum balance'; + } + if (error?.shortMessage) { return error.shortMessage; } @@ -29,18 +35,16 @@ type TProjectContribution = { }; export const ProjectContribution = ({ userAddress }: TProjectContribution) => { + const { open } = useDialog(); const { - diffToAllowance, - contributionTxHash, + maxAmount, amount, + amountInWei, + tokensToBuyInWei, tokensToBuy, - tokenError, + error, setAmount, - buyError, - allowanceError, - isPending, - submit: onSubmit, - } = useContributeToCtznd(userAddress); + } = useContributeToCtznd(); const updateAmount = useCallback( (evt: React.ChangeEvent) => { @@ -50,7 +54,18 @@ export const ProjectContribution = ({ userAddress }: TProjectContribution) => { [setAmount], ); - const errorMessage = getErrorMessage(amount, buyError || allowanceError); + const onClick = useCallback(() => { + if (!amount || !tokensToBuy) return; + return open(ContributeDialog.displayName, { + userAddress, + amount, + amountInWei, + tokensToBuy, + tokensToBuyInWei, + }); + }, [open, amount, tokensToBuy, amountInWei, tokensToBuyInWei, userAddress]); + + const errorMessage = getErrorMessage(amount, maxAmount, error); return (
@@ -67,7 +82,7 @@ export const ProjectContribution = ({ userAddress }: TProjectContribution) => { units="USDC" error={errorMessage} className="col-span-2 md:col-span-1" - onSubmit={onSubmit} + onSubmit={onClick} defaultValue={amount} min={0} /> @@ -77,8 +92,7 @@ export const ProjectContribution = ({ userAddress }: TProjectContribution) => { id="ctnd-amount" units="CTND*" disabled - value={tokensToBuy ? formatEther(tokensToBuy).toString() : 0} - error={tokenError?.shortMessage || tokenError?.message} + value={tokensToBuy} className="col-span-2 md:col-span-1" /> @@ -87,27 +101,28 @@ export const ProjectContribution = ({ userAddress }: TProjectContribution) => { the number of contributors and their desired contribution.

- {contributionTxHash ? ( + {/* {contributionTxHash ? (
Contribution submitted

{contributionTxHash}

- ) : ( - - )} + )} */} + + {/* )} */}
); }; From 366d193cb8d45d4b3e074b40220ee2ab1ff7d449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Torres?= Date: Fri, 26 Apr 2024 18:42:43 +0100 Subject: [PATCH 4/5] Fix buy scenario --- .../dialogs/contribute-dialog/index.tsx | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx index dda4a65f..44385992 100644 --- a/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx +++ b/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx @@ -59,7 +59,7 @@ type TAllowFundsProps = { allowance: bigint; }; -const AllowFunds = ({ amount, amountInWei, allowance }: TAllowFundsProps) => { +const AllowFunds = ({ amountInWei, allowance }: TAllowFundsProps) => { const { error, setAllowance, isPending, allowanceTxHash } = useSetPaymentTokenAllowance(); @@ -67,6 +67,8 @@ const AllowFunds = ({ amount, amountInWei, allowance }: TAllowFundsProps) => { setAllowance(amountInWei); }, [setAllowance, amountInWei]); + console.log(error); + return (
@@ -88,9 +90,19 @@ const AllowFunds = ({ amount, amountInWei, allowance }: TAllowFundsProps) => { ); }; -const Contribute = ({ tokensToBuyInWei }: { tokensToBuyInWei: bigint }) => { - const { contributionTxHash, buyCtzndTokens, error } = useBuyCtzndTokens(); +type TContributeProps = { + tokensToBuyInWei: bigint; + contributionTxHash?: `0x${string}`; + buyCtzndTokens: (tokensToBuyInWei: bigint) => void; + error: any; +}; +const Contribute = ({ + tokensToBuyInWei, + contributionTxHash, + buyCtzndTokens, + error, +}: TContributeProps) => { useEffect(() => { if (!tokensToBuyInWei || contributionTxHash) return; buyCtzndTokens(tokensToBuyInWei); @@ -164,19 +176,29 @@ export function ContributeDialog({ }: TContributeDialogProps) { const { allowance, isLoading, error } = useCtzndPaymentTokenAllowance(userAddress); + const { + contributionTxHash, + buyCtzndTokens, + error: buyError, + } = useBuyCtzndTokens(); - if (isLoading) return
Loading...
; + if (isLoading) return null; if (error || allowance === undefined) return
{error?.message}
; const hasEnoughAllowance = allowance && allowance >= amountInWei; - if (hasEnoughAllowance) + if (hasEnoughAllowance || contributionTxHash) return ( <> - +
From d21babf65b8dac219e3ac7fb213a0a5183d71dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Torres?= Date: Mon, 29 Apr 2024 10:26:54 +0100 Subject: [PATCH 5/5] fix linter --- packages/web-app/app/_providers/dialog/index.tsx | 2 +- .../app/_ui/components/dialogs/apply-dialog/index.tsx | 7 +++---- .../_ui/components/dialogs/contribute-dialog/index.tsx | 9 +++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/web-app/app/_providers/dialog/index.tsx b/packages/web-app/app/_providers/dialog/index.tsx index 49a9e411..e577521d 100644 --- a/packages/web-app/app/_providers/dialog/index.tsx +++ b/packages/web-app/app/_providers/dialog/index.tsx @@ -6,7 +6,7 @@ import { ContributeDialog } from '@/app/_ui/components/dialogs/contribute-dialog type TDialogComponent = { displayName: string; - (props: TProps): JSX.Element; + (props: any): JSX.Element; }; const dialogComponents: TDialogComponent[] = [ diff --git a/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx index 4a503ff3..08f0dcca 100644 --- a/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx +++ b/packages/web-app/app/_ui/components/dialogs/apply-dialog/index.tsx @@ -175,7 +175,8 @@ export const ApplyDialog = ({ projectId }: TProps) => { const { address, hasProfile } = useIdOS(); // shouldn't be possible, just warding typescript - if (!projectId) return

No project selected

; + if (!projectId || typeof projectId !== 'string') + return

No project selected

; // shouldn't be possible, just warding typescript if (!address) @@ -190,9 +191,7 @@ export const ApplyDialog = ({ projectId }: TProps) => { return ; } - if (typeof projectId === 'string') { - return ; - } + return ; }; ApplyDialog.displayName = 'applyDialog'; diff --git a/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx b/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx index 44385992..9d570f85 100644 --- a/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx +++ b/packages/web-app/app/_ui/components/dialogs/contribute-dialog/index.tsx @@ -159,7 +159,7 @@ const Description = ({ amount, tokensToBuy }: TDescriptionProps) => (
); -type TContributeDialogProps = { +export type TContributeDialogProps = { userAddress: `0x${string}`; amount: number; tokensToBuy: number; @@ -182,7 +182,12 @@ export function ContributeDialog({ error: buyError, } = useBuyCtzndTokens(); - if (isLoading) return null; + if (isLoading) + return ( +
+ +
+ ); if (error || allowance === undefined) return
{error?.message}
; const hasEnoughAllowance = allowance && allowance >= amountInWei;