diff --git a/apps/web/src/app/features/account/account-data-access.tsx b/apps/web/src/app/features/account/account-data-access.tsx index fa10dee..db3176e 100644 --- a/apps/web/src/app/features/account/account-data-access.tsx +++ b/apps/web/src/app/features/account/account-data-access.tsx @@ -10,126 +10,159 @@ import { TransactionSignature, VersionedTransaction, } from '@solana/web3.js' -import { useMutation, useQuery } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCluster } from '../cluster/cluster-data-access' +import { NotificationData } from '@mantine/notifications' +import { Anchor } from '@mantine/core' -export function transactionToast(signature: string) { - toastSuccess({ - title: 'Transaction sent', - message: ` -
-
- Transaction ${signature} sent -
- - View on Solana Explorer - -
`, - }) -} - -export function useAccount({ address }: { address: PublicKey }) { - const { cluster } = useCluster() +export function useQueries({ address }: { address: PublicKey }) { const { connection } = useConnection() - const wallet = useWallet() - const getBalance = useQuery({ - queryKey: ['balance', { cluster, address }], - queryFn: () => connection.getBalance(address), - }) - - const getSignatures = useQuery({ - queryKey: ['signatures', { cluster, address }], - queryFn: () => connection.getConfirmedSignaturesForAddress2(address), - }) - - const getTokenAccounts = useQuery({ - queryKey: ['token-accounts', { endpoint: connection.rpcEndpoint, address: address.toString() }], - queryFn: async () => { - const [tokenAccounts, token2022Accounts] = await Promise.all([ - connection.getParsedTokenAccountsByOwner(address, { - programId: TOKEN_PROGRAM_ID, - }), - connection.getParsedTokenAccountsByOwner(address, { - programId: TOKEN_2022_PROGRAM_ID, - }), - ]) - return [...tokenAccounts.value, ...token2022Accounts.value] + return { + getBalance: { + queryKey: ['getBalance', { endpoint: connection?.rpcEndpoint, address }], + queryFn: () => connection.getBalance(address), }, - }) - - const getTokenBalance = useQuery({ - queryKey: ['getTokenAccountBalance', { endpoint: connection.rpcEndpoint, account: address.toString() }], - queryFn: () => connection.getTokenAccountBalance(address), - }) - - const requestAirdrop = useMutation({ - mutationKey: ['airdrop', { cluster, address }], - mutationFn: async (amount: number = 1) => { - const [latestBlockhash, signature] = await Promise.all([ - connection.getLatestBlockhash(), - connection.requestAirdrop(address, amount * LAMPORTS_PER_SOL), - ]) - - await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed') - return signature + getSignatures: { + queryKey: ['getSignatures', { endpoint: connection?.rpcEndpoint, address }], + queryFn: () => connection.getConfirmedSignaturesForAddress2(address), }, - onSuccess: (signature) => { - transactionToast(signature) - return Promise.all([getBalance.refetch(), getSignatures.refetch()]) + getTokenAccounts: { + queryKey: ['getTokenAccounts', { endpoint: connection?.rpcEndpoint, address }], + queryFn: () => getAllTokenAccounts(connection, address), }, - }) - - const transferSol = useMutation({ - mutationKey: ['transfer-sol', { cluster, address }], - mutationFn: async (input: { destination: PublicKey; amount: number }) => { - let signature: TransactionSignature = '' - try { - const { transaction, latestBlockhash } = await createTransaction({ - publicKey: address, - destination: input.destination, - amount: input.amount, - connection, - }) - - // Send transaction and await for signature - signature = await wallet.sendTransaction(transaction, connection) - - // Send transaction and await for signature - await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed') - - console.log(signature) - return signature - } catch (error: unknown) { - console.log('error', `Transaction failed! ${error}`, signature) - - return - } + getTokenBalance: { + queryKey: ['getTokenBalance', { endpoint: connection?.rpcEndpoint, account: address }], + queryFn: () => connection.getTokenAccountBalance(address), + }, + requestAirdrop: { + mutationKey: ['requestAirdrop', { endpoint: connection?.rpcEndpoint, address }], + mutationFn: (amount: string) => requestAndConfirmAirdrop({ address, amount, connection }), + }, + transferSol: { + mutationKey: ['transferSol', { endpoint: connection?.rpcEndpoint, address }], + mutationFn: async ({ amount, destination }: { amount: string; destination: PublicKey }) => { + try { + const { transaction, latestBlockhash } = await createTransaction({ + amount, + connection, + destination, + publicKey: address, + }) + + // Send transaction and await for signature + const signature: TransactionSignature = await wallet.sendTransaction(transaction, connection) + + // Send transaction and await for signature + await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed') + + return signature + } catch (error: unknown) { + console.log('error', `Transaction failed! ${error}`) + return + } + }, }, - onSuccess: (signature) => { - if (signature) { - transactionToast(signature) - } - return Promise.all([getBalance.refetch(), getSignatures.refetch()]) + } +} + +export function useGetBalance({ address }: { address: PublicKey }) { + return useQuery(useQueries({ address }).getBalance) +} +export function useGetSignatures({ address }: { address: PublicKey }) { + return useQuery(useQueries({ address }).getSignatures) +} +export function useGetTokenAccounts({ address }: { address: PublicKey }) { + return useQuery(useQueries({ address }).getTokenAccounts) +} +export function useGetTokenBalance({ address }: { address: PublicKey }) { + return useQuery(useQueries({ address }).getTokenBalance) +} +export function useRequestAirdrop({ address }: { address: PublicKey }) { + const { + requestAirdrop: { mutationKey, mutationFn }, + } = useQueries({ address }) + const onSuccess = useOnTransactionSuccess({ address }) + return useMutation({ + mutationKey, + mutationFn, + onSuccess, + onError: (error: unknown) => { + toastError(`Requesting airdrop failed! ${error}`) }, - onError: (error) => { - toastError(`Transaction failed! ${error}`) + }) +} +export function useTransferSol({ address }: { address: PublicKey }) { + const onSuccess = useOnTransactionSuccess({ address }) + return useMutation({ + ...useQueries({ address }).transferSol, + onSuccess, + onError: (error: unknown) => { + toastError(`Sending transaction failed! ${error}`) }, }) +} - return { - getBalance, - getSignatures, - getTokenAccounts, - getTokenBalance, - requestAirdrop, - transferSol, +async function getAllTokenAccounts(connection: Connection, address: PublicKey) { + const [tokenAccounts, token2022Accounts] = await Promise.all([ + connection.getParsedTokenAccountsByOwner(address, { programId: TOKEN_PROGRAM_ID }), + connection.getParsedTokenAccountsByOwner(address, { programId: TOKEN_2022_PROGRAM_ID }), + ]) + return [...tokenAccounts.value, ...token2022Accounts.value] +} + +async function requestAndConfirmAirdrop({ + address, + amount, + connection, +}: { + connection: Connection + address: PublicKey + amount: string +}) { + const [latestBlockhash, signature] = await Promise.all([ + connection.getLatestBlockhash(), + connection.requestAirdrop(address, parseFloat(amount) * LAMPORTS_PER_SOL), + ]) + + await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed') + return signature +} + +function useOnTransactionSuccess({ address }: { address: PublicKey }) { + const { getExplorerUrl } = useCluster() + const client = useQueryClient() + const { getBalance, getSignatures } = useQueries({ address }) + + return (signature?: TransactionSignature) => { + if (signature) { + uiToastLink({ link: getExplorerUrl(`tx/${signature}`), label: 'View Transaction' }) + } + return Promise.all([ + client.invalidateQueries({ queryKey: getBalance.queryKey }), + client.invalidateQueries({ queryKey: getSignatures.queryKey }), + ]) } } -async function createTransaction({ +export function uiToastLink({ + label, + link, + ...props +}: Omit & { link: string; label: string }) { + return toastSuccess({ + ...props, + message: ( + + {label} + + ), + }) +} + +export async function createTransaction({ publicKey, destination, amount, @@ -137,7 +170,7 @@ async function createTransaction({ }: { publicKey: PublicKey destination: PublicKey - amount: number + amount: string connection: Connection }): Promise<{ transaction: VersionedTransaction @@ -151,7 +184,7 @@ async function createTransaction({ SystemProgram.transfer({ fromPubkey: publicKey, toPubkey: destination, - lamports: amount * LAMPORTS_PER_SOL, + lamports: parseFloat(amount) * LAMPORTS_PER_SOL, }), ] diff --git a/apps/web/src/app/features/account/account-ui.tsx b/apps/web/src/app/features/account/account-ui.tsx index b902cd0..1849e07 100644 --- a/apps/web/src/app/features/account/account-ui.tsx +++ b/apps/web/src/app/features/account/account-ui.tsx @@ -22,7 +22,14 @@ import { useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useCluster } from '../cluster/cluster-data-access' import { ExplorerLink } from '../cluster/cluster-ui' -import { useAccount } from './account-data-access' +import { + useGetBalance, + useGetSignatures, + useGetTokenAccounts, + useGetTokenBalance, + useRequestAirdrop, + useTransferSol, +} from './account-data-access' export function ellipsify(str = '', len = 4) { if (str.length > 30) { @@ -32,7 +39,7 @@ export function ellipsify(str = '', len = 4) { } export function AccountBalance({ address, ...props }: { address: PublicKey } & TitleProps) { - const { getBalance: query } = useAccount({ address }) + const query = useGetBalance({ address }) return ( query.refetch()} {...props}> @@ -49,7 +56,8 @@ export function AccountChecker() { } export function AccountBalanceCheck({ address }: { address: PublicKey }) { const { cluster } = useCluster() - const { getBalance: query, requestAirdrop } = useAccount({ address }) + const query = useGetBalance({ address }) + const requestAirdrop = useRequestAirdrop({ address }) if (query.isLoading) { return null @@ -72,7 +80,7 @@ export function AccountBalanceCheck({ address }: { address: PublicKey }) { variant="light" color="yellow" size="xs" - onClick={() => requestAirdrop.mutateAsync(1).catch((err) => console.log(err))} + onClick={() => requestAirdrop.mutateAsync('1').catch((err) => console.log(err))} > Request Airdrop </Button> @@ -99,7 +107,7 @@ export function AccountButtons({ address }: { address: PublicKey }) { export function AccountTokens({ address }: { address: PublicKey }) { const [showAll, setShowAll] = useState(false) - const { getTokenAccounts: query } = useAccount({ address }) + const query = useGetTokenAccounts({ address }) const client = useQueryClient() const items = useMemo(() => { if (showAll) return query.data @@ -188,18 +196,18 @@ export function AccountTokens({ address }: { address: PublicKey }) { } export function AccountTokenBalance({ address, ...props }: { address: PublicKey } & TextProps) { - const { getTokenBalance } = useAccount({ address }) - return getTokenBalance.isLoading ? ( + const query = useGetTokenBalance({ address }) + return query.isLoading ? ( <Loader /> - ) : getTokenBalance.data ? ( - <Text {...props}>{getTokenBalance.data?.value.uiAmount}</Text> + ) : query.data ? ( + <Text {...props}>{query.data?.value.uiAmount}</Text> ) : ( <div>Error</div> ) } export function AccountTransactions({ address }: { address: PublicKey }) { - const { getSignatures: query } = useAccount({ address }) + const query = useGetSignatures({ address }) const [showAll, setShowAll] = useState(false) const items = useMemo(() => { @@ -294,8 +302,8 @@ function ModalReceive({ address, ...props }: { address: PublicKey }) { function ModalAirdrop({ address, ...props }: ButtonProps & { address: PublicKey }) { const [opened, { close, open }] = useDisclosure(false) - const { requestAirdrop: mutation } = useAccount({ address }) - const [amount, setAmount] = useState(2) + const mutation = useRequestAirdrop({ address }) + const [amount, setAmount] = useState('2') return ( <> @@ -306,9 +314,11 @@ function ModalAirdrop({ address, ...props }: ButtonProps & { address: PublicKey <TextInput disabled={mutation.isPending} type="number" + step="any" + min="0" placeholder="Amount" value={amount} - onChange={(e) => setAmount(Number(e.target.value))} + onChange={(e) => setAmount(e.target.value)} /> <Button disabled={!amount || mutation.isPending} @@ -326,9 +336,9 @@ function ModalAirdrop({ address, ...props }: ButtonProps & { address: PublicKey function ModalSend({ address, ...props }: ButtonProps & { address: PublicKey }) { const [opened, { close, open }] = useDisclosure(false) const wallet = useWallet() - const { transferSol: mutation } = useAccount({ address }) + const mutation = useTransferSol({ address }) const [destination, setDestination] = useState('') - const [amount, setAmount] = useState(1) + const [amount, setAmount] = useState('1') if (!address || !wallet.sendTransaction) { return <div>Wallet not connected</div> @@ -350,9 +360,11 @@ function ModalSend({ address, ...props }: ButtonProps & { address: PublicKey }) <TextInput disabled={mutation.isPending} type="number" + step="any" + min="0" placeholder="Amount" value={amount} - onChange={(e) => setAmount(Number(e.target.value))} + onChange={(e) => setAmount(e.target.value)} /> <Button disabled={!destination || !amount || mutation.isPending}