Skip to content

Commit

Permalink
Add USDC balance & dashboard (#225)
Browse files Browse the repository at this point in the history
Why:
- We need to display the token payment balance instead of sepolia ETH
- We want to display live values on dashboard
  • Loading branch information
luistorres authored Apr 29, 2024
1 parent a9d5120 commit 4356d1e
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 47 deletions.
1 change: 0 additions & 1 deletion packages/web-app/app/_lib/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
40 changes: 40 additions & 0 deletions packages/web-app/app/_lib/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="h-6 w-44 animate-pulse rounded-md bg-gradient-to-br from-mono-50 to-mono-200" />
);

return (
<>
<Wallet />
<div className="flex flex-col items-start gap-2">
<h3 className="text-sm text-mono-800">Balance</h3>
<p className="font-medium">
{balance?.data?.formatted ? formatEther(balance?.data.value) : null}{' '}
{balance.data?.symbol}
</p>
<p className="font-medium">{`${formattedValue} ${balance.symbol}`}</p>
</div>
</>
);
Expand Down
23 changes: 11 additions & 12 deletions packages/web-app/app/_ui/components/wallet-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="h-14 w-44 animate-pulse rounded-md bg-gradient-to-br from-mono-800 to-mono-900" />
);

return (
<EdgeBorderButton
avatar={<Avatar />}
onClick={() => open(SettingsDialog.displayName)}
>
{displayBalance}
{`${formattedValue} ${balance.symbol}`}
</EdgeBorderButton>
);
};
Expand Down Expand Up @@ -61,9 +62,7 @@ export function WalletButton() {
}

if (account.displayBalance) {
return (
<ConnectedButton displayBalance={account.displayBalance} />
);
return <ConnectedButton />;
}

return (
Expand Down
80 changes: 64 additions & 16 deletions packages/web-app/app/_ui/project/token-metrics.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<div>
Expand All @@ -47,11 +54,26 @@ const ProgressBar = ({
aria-valuemax={Number(maxInMillions)}
>
<div
className="flex flex-col justify-center overflow-hidden whitespace-nowrap rounded-md bg-blue-500 text-center text-xs text-white transition duration-500 dark:bg-blue-500"
style={{ width: percentage }}
className={clsx(
'flex flex-col justify-center overflow-hidden whitespace-nowrap bg-blue-500 text-center text-xs text-white transition duration-500 dark:bg-blue-500',
percentageRounded === '100%' ? 'rounded-md' : 'rounded-l-md',
)}
style={{ width: percentageRounded }}
/>
</div>
<div className="relative z-0 flex">
<div
className={`absolute left-[${percentageRounded}] top-0 flex flex-col items-start`}
>
{percentageRounded === '100%' ? null : (
<>
<div className="absolute -left-1 -top-8 h-2 w-2 rotate-45 transform bg-blue-500" />
<div className="-translate-x-1/2 text-blue-500">
{displayValue}M
</div>
</>
)}
</div>
<div className="absolute left-1/2 top-0 flex flex-col items-start">
<div className="h-2 w-0.5 border-[1px] border-blue-500" />
<div className="-translate-x-1/2">{halfInMillions}M</div>
Expand All @@ -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 (
<>
<div className="mx-auto mb-4 h-6 w-44 animate-pulse rounded-md bg-gradient-to-br from-mono-50 to-mono-200" />
<div className="h-6 w-full animate-pulse rounded-md bg-gradient-to-br from-mono-50 to-mono-200" />
</>
);

return (
<ProgressBar
title="Raise status"
max={Number(maxTarget)}
value={Number(value)}
/>
);
};

export const TokenMetrics = ({
minTarget,
maxTarget,
Expand All @@ -85,9 +133,9 @@ export const TokenMetrics = ({
<h4 className="border-b border-mono-200 px-8 py-6 font-medium uppercase">
Token Metrics
</h4>
{process.env.NEXT_PUBLIC_APPLY_OPEN === 'false' ? (
{process.env.NEXT_PUBLIC_CONTRIBUTE_OPEN === 'true' ? (
<div className="m-8">
<ProgressBar title="Raise status" max={maxTarget} value={1000000n} />
<ProgressBarWrapper maxTarget={maxTarget} />
</div>
) : null}
<div className="flex flex-col gap-4 p-8">
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 4356d1e

Please sign in to comment.