From 918355b069a564480002d29165bffeeadbb631ca Mon Sep 17 00:00:00 2001 From: Davide Silva Date: Tue, 23 Apr 2024 23:19:38 +0100 Subject: [PATCH] Read project data from contracts (#219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: * We had hardcoded values that needed to be replaced by the real values Also: - Adds functional allowance and contribution --------- Co-authored-by: Luís Torres <26875009+luistorres@users.noreply.github.com> Co-authored-by: Luís Torres --- packages/web-app/app/_lib/actions.tsx | 87 ++++++++++- .../web-app/app/_ui/my-projects/index.tsx | 6 +- .../app/_ui/my-projects/my-project.tsx | 21 ++- .../_ui/project/contribution/DataFields.tsx | 96 ++++++++++++ .../app/_ui/project/project-content.tsx | 20 ++- .../app/_ui/project/project-contribution.tsx | 142 ++++++------------ 6 files changed, 268 insertions(+), 104 deletions(-) create mode 100644 packages/web-app/app/_ui/project/contribution/DataFields.tsx diff --git a/packages/web-app/app/_lib/actions.tsx b/packages/web-app/app/_lib/actions.tsx index 1fac77ed..84368579 100644 --- a/packages/web-app/app/_lib/actions.tsx +++ b/packages/web-app/app/_lib/actions.tsx @@ -2,11 +2,21 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useIdOS } from '../_providers/idos'; import { insertGrantBySignature } from '../_server/idos/grants'; import { useFetchGrantMessage } from './contract-queries'; -import { useSignMessage } from 'wagmi'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useAccount, useSignMessage } from 'wagmi'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFetchNewDataId } from './queries'; import { getServerPublicInfo } from '../_server/info'; import { subscribeToNewsletter } from '../_server/active-campaign'; +import { + ctzndSaleAddress, + useReadCtzndSalePaymentToken, + useReadCtzndSalePaymentTokenToToken, + useReadErc20Allowance, + useWriteCtzndSaleBuy, + useWriteErc20Approve, +} from '@/wagmi.generated'; +import { sepolia } from 'viem/chains'; +import { formatEther, parseEther } from 'viem'; export const useAcquireAccessGrantMutation = () => { const { sdk } = useIdOS(); @@ -192,3 +202,76 @@ export const useSignDelegatedAccessGrant = ( insertError, }; }; + +export const useContributeToCtznd = (address: `0x${string}`) => { + const [state, setState] = useState(); + const [amount, setAmount] = useState(0); + const amountInWei = useMemo(() => parseEther(amount.toString()), [amount]); + + const { + writeContract: writeBuy, + data: contributionTxHash, + isPending: isWritePending, + error: buyError, + } = useWriteCtzndSaleBuy(); + const { + writeContract: approveContract, + data: allowanceTxHash, + error: allowanceError, + isPending: isApprovePending, + } = 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 + ) { + return; + } + + if (diffToAllowance < 0) { + return approveContract({ + address: paymentToken, + args: [saleAddress, amountInWei], + }); + } + + writeBuy({ args: [tokensToBuy] }); + }; + + return { + contributionTxHash, + allowanceTxHash, + diffToAllowance, + amount, + amountInWei, + setAmount, + tokensToBuy, + isPending: isApprovePending || isWritePending, + error: buyError || allowanceError || tokenError, + buyError, + allowanceError, + tokenError, + submit, + }; +}; diff --git a/packages/web-app/app/_ui/my-projects/index.tsx b/packages/web-app/app/_ui/my-projects/index.tsx index 8daeb2a4..6fc8a290 100644 --- a/packages/web-app/app/_ui/my-projects/index.tsx +++ b/packages/web-app/app/_ui/my-projects/index.tsx @@ -8,6 +8,7 @@ import Image from 'next/image'; import { getRelativePath } from '../utils/getRelativePath'; import { NoProjects } from './no-projects'; import { MyProjectSkeleton } from './my-project'; +import { useReadCtzndSaleInvestorCount } from '@/wagmi.generated'; const ProjectRow = ({ logo, @@ -15,6 +16,9 @@ const ProjectRow = ({ minTarget, maxTarget, }: TProjectSaleDetails) => { + const { data: contributions } = useReadCtzndSaleInvestorCount(); + const totalContributions = contributions ? contributions.toString() : 0; + return ( {project} -
0
+
{totalContributions}
{usdRange(minTarget, maxTarget)}
0 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 370747d5..edc382d1 100644 --- a/packages/web-app/app/_ui/my-projects/my-project.tsx +++ b/packages/web-app/app/_ui/my-projects/my-project.tsx @@ -9,6 +9,12 @@ import { usdRange } from '../utils/intl-formaters/usd-range'; import { formatDate } from '../utils/intl-formaters/date'; import { EdgeLink } from '../components/edge'; import { CardSkeleton } from '../components/skeletons/card-skeleton'; +import { + useReadCtzndSaleInvestorCount, + useReadCtzndSaleUncappedAllocation, +} from '@/wagmi.generated'; +import { useAccount } from 'wagmi'; +import { formatEther } from 'viem'; const Header = ({ project, @@ -16,6 +22,9 @@ const Header = ({ minTarget, maxTarget, }: TProjectSaleDetails) => { + const { data: contributions } = useReadCtzndSaleInvestorCount(); + const totalContributions = contributions ? contributions.toString() : 0; + return (

@@ -30,7 +39,7 @@ const Header = ({

Contributions

-
0
+
{totalContributions}

Target Raise

@@ -47,10 +56,18 @@ const Header = ({ const MyContribution = () => { const { projectId } = useProject(); + + const { address } = useAccount(); + const { data: contributions } = useReadCtzndSaleUncappedAllocation({ + args: [address!], + }); + + const myContributions = contributions ? formatEther(contributions!) : 0; + return (

Contributions

-
0 USDC
+
{`${myContributions} USDC`}
New Contribution
diff --git a/packages/web-app/app/_ui/project/contribution/DataFields.tsx b/packages/web-app/app/_ui/project/contribution/DataFields.tsx new file mode 100644 index 00000000..43763833 --- /dev/null +++ b/packages/web-app/app/_ui/project/contribution/DataFields.tsx @@ -0,0 +1,96 @@ +import { + useReadCtzndSaleMaxContribution, + useReadCtzndSaleMinContribution, + useReadCtzndSaleInvestorCount, + useReadCtzndSaleMaxTarget, + useReadCtzndSaleMinTarget, +} from '@/wagmi.generated'; +import { formatEther } from 'viem'; +import { usdValue } from '../../utils/intl-formaters/usd-value'; +import { number } from '../../utils/intl-formaters/number'; + +const useMaxParticipants = () => { + const { + data: maxTarget, + isLoading: maxLoading, + error: maxError, + } = useReadCtzndSaleMaxTarget(); + const { + data: minTarget, + isLoading: minLoading, + error: minError, + } = useReadCtzndSaleMinTarget(); + + return { + data: + maxTarget === undefined || minTarget === undefined + ? undefined + : BigInt(maxTarget) - BigInt(minTarget), + isLoading: maxLoading || minLoading, + error: maxError || minError, + }; +}; + +const LoadingField = () => ( +
+); + +export const DataFields = () => { + const { data: maxContribution } = useReadCtzndSaleMaxContribution(); + const { data: minContribution } = useReadCtzndSaleMinContribution(); + const { data: investorCount } = useReadCtzndSaleInvestorCount({ + query: { + refetchInterval: 1000 * 60, // every minute + }, + }); + const { data: maxParticipants } = useMaxParticipants(); + + return ( + <> +
+
Current price
+
0.1 USDC*
+
+
+
Min. contribution
+
+ {minContribution !== undefined ? ( + usdValue(formatEther(minContribution)) + ) : ( + + )} +
+
+
+
Max. contribution
+
+ {maxContribution !== undefined ? ( + usdValue(formatEther(maxContribution)) + ) : ( + + )} +
+
+
+
Current contributors
+
+ {investorCount !== undefined ? ( + number(investorCount) + ) : ( + + )} +
+
+
+
Max. participants
+
+ {maxParticipants !== undefined ? ( + number(maxParticipants) + ) : ( + + )} +
+
+ + ); +}; diff --git a/packages/web-app/app/_ui/project/project-content.tsx b/packages/web-app/app/_ui/project/project-content.tsx index 968790b5..6a84d6c5 100644 --- a/packages/web-app/app/_ui/project/project-content.tsx +++ b/packages/web-app/app/_ui/project/project-content.tsx @@ -12,6 +12,7 @@ import { ProjectContribution } from './project-contribution'; import { useHasCitizendGrant, useHasProjectGrant } from '@/app/_lib/hooks'; import { AppliedSuccess } from './applied-success'; import { CardSkeleton } from '../components/skeletons/card-skeleton'; +import { useAccount } from 'wagmi'; const generateTabClassName = ({ selected }: { selected: boolean }) => clsx( @@ -20,6 +21,7 @@ const generateTabClassName = ({ selected }: { selected: boolean }) => ); export const ProjectContent = () => { + const { address } = useAccount(); const { projectId } = useProject(); const [selectedIndex, setSelectedIndex] = useState(0); const { data, isLoading, isError, error } = useFetchProjectsSaleDetails(); @@ -34,7 +36,7 @@ export const ProjectContent = () => { ); const hasGrant = hasProjectGrant && hasCitizendGrant; - if (isLoading || (!data && !isError)) { + if (isLoading || isLoadingGrant || (!data && !isError)) { return (
@@ -80,8 +82,10 @@ export const ProjectContent = () => { - {process.env.NEXT_PUBLIC_CONTRIBUTE_OPEN === 'true' ? ( - + {process.env.NEXT_PUBLIC_CONTRIBUTE_OPEN === 'true' && + hasGrant && + address ? ( + ) : null} {process.env.NEXT_PUBLIC_APPLY_OPEN === 'true' ? ( { maxContribution={maxContribution} totalTokensForSale={totalTokensForSale} /> - {hasGrant ? : null} + {hasGrant && process.env.NEXT_PUBLIC_APPLY_OPEN === 'true' ? ( + + ) : null} {process.env.NEXT_PUBLIC_APPLY_OPEN === 'true' && !hasGrant ? ( {
- {process.env.NEXT_PUBLIC_CONTRIBUTE_OPEN === 'true' ? ( - + {process.env.NEXT_PUBLIC_CONTRIBUTE_OPEN === 'true' && + hasGrant && + address ? ( + ) : null}
diff --git a/packages/web-app/app/_ui/project/project-contribution.tsx b/packages/web-app/app/_ui/project/project-contribution.tsx index ee863d2c..a162f813 100644 --- a/packages/web-app/app/_ui/project/project-contribution.tsx +++ b/packages/web-app/app/_ui/project/project-contribution.tsx @@ -1,32 +1,12 @@ 'use client'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback } from 'react'; import { Input } from '../components/input'; import { Button } from '../components'; -import { - useReadCtzndSaleMaxContribution, - useReadCtzndSaleMinContribution, - useReadCtzndSaleInvestorCount, - useReadCtzndSaleMaxTarget, - useReadCtzndSaleMinTarget, - useWriteCtzndSaleBuy, - useReadCtzndSaleRate, - useWriteErc20Approve, - ctzndSaleAddress, -} from '@/wagmi.generated'; -import { formatEther, parseEther } from 'viem'; -import { usdValue } from '../utils/intl-formaters/usd-value'; -import { Spinner } from '../components/svg/spinner'; -import { number } from '../utils/intl-formaters/number'; -import { sepolia } from 'viem/chains'; - -const useMaxParticipants = () => { - const { data: maxTarget } = useReadCtzndSaleMaxTarget(); - const { data: minTarget } = useReadCtzndSaleMinTarget(); - - if (!maxTarget || !minTarget) return 0; - return BigInt(maxTarget) - BigInt(minTarget); -}; +import { Spinner } from '../components/svg/spinner'; +import { DataFields } from './contribution/DataFields'; +import { useContributeToCtznd } from '@/app/_lib/actions'; +import { formatEther } from 'viem'; const getErrorMessage = (amount: number, error: any) => { if (amount < 0) { @@ -44,44 +24,33 @@ const getErrorMessage = (amount: number, error: any) => { return null; }; -export const ProjectContribution = () => { - const [amount, setAmount] = useState(0); +type TProjectContribution = { + userAddress: `0x${string}`; +}; + +export const ProjectContribution = ({ userAddress }: TProjectContribution) => { + const { + diffToAllowance, + contributionTxHash, + amount, + tokensToBuy, + tokenError, + setAmount, + buyError, + allowanceError, + isPending, + submit: onSubmit, + } = useContributeToCtznd(userAddress); + const updateAmount = useCallback( (evt: React.ChangeEvent) => { const { value } = evt.target; setAmount(Number(value)); }, - [], - ); - const { data: maxContribution } = useReadCtzndSaleMaxContribution(); - const { data: minContribution } = useReadCtzndSaleMinContribution(); - const { data: investorCount } = useReadCtzndSaleInvestorCount(); - const { data: saleRate } = useReadCtzndSaleRate(); - const ctndTokens = useMemo( - () => (saleRate && amount ? BigInt(amount) * BigInt(saleRate) : 0), - [saleRate, amount], + [setAmount], ); - const { writeContract, data, isError, isPending, isPaused, error } = - useWriteCtzndSaleBuy(); - const { writeContract: approveContract, isError: isApproveError } = - useWriteErc20Approve(); - - const maxParticipants = useMaxParticipants(); - - const onSubmit = () => { - if (amount <= 0) return; - const value = parseEther(amount.toString()); - const address = ctzndSaleAddress[sepolia.id] as `0x${string}`; - approveContract({ - address, - args: [address, value], - }); - writeContract({ args: [value] }); - }; - - const errorMessage = getErrorMessage(amount, error); - console.log(error); + const errorMessage = getErrorMessage(amount, buyError || allowanceError); return (
@@ -108,50 +77,37 @@ export const ProjectContribution = () => { id="ctnd-amount" units="CTND*" disabled - value={ctndTokens.toString()} + value={tokensToBuy ? formatEther(tokensToBuy).toString() : 0} + error={tokenError?.shortMessage || tokenError?.message} className="col-span-2 md:col-span-1" /> -
-

Current price

-

0.1 USDC*

-
-
-

Min. contribution

-

- {minContribution !== undefined - ? usdValue(formatEther(minContribution)) - : ''} -

-
-
-

Max. contribution

-

- {maxContribution !== undefined - ? usdValue(formatEther(maxContribution)) - : ''} -

-
-
-

Current contributors

-

{number(investorCount || 0)}

-
-
-

Max. participants

-

{number(maxParticipants)}

-
+

*Contribution amount/token allocation amount fluctuates depending on the number of contributors and their desired contribution.

- + {contributionTxHash ? ( +
+ Contribution submitted +

{contributionTxHash}

+
+ ) : ( + + )}
); };