Files
lab5/app/pages/home/TokenCard.tsx
2026-03-25 01:53:02 +01:00

128 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { erc20Abi, formatUnits, isAddress, parseUnits, type Address } from "viem";
import { useReadContract, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import type { SimpleErc20Token } from "~/types/api";
export function TokenCard({ token, address }: { token: SimpleErc20Token, address: Address }) {
const [isTransferSectionOpen, setIsTransferSectionOpen] = useState(false);
const [inputAddress, setInputAddress] = useState("");
const isAddressValid = isAddress(inputAddress);
const [inputAmount, setInputAmount] = useState("");
const isAmountValid = inputAmount && !isNaN(Number(inputAmount)) && Number(inputAmount) > 0;
const { data: balance, error: errorBalance } = useReadContract({
address: token.address,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address]
});
const { data: decimals, error: errorDecimals } = useReadContract({
address: token.address,
abi: erc20Abi,
functionName: 'decimals'
});
const { data: hash, mutate: makeTransfer, isPending: isWaitingForUser, error: userError } = useWriteContract();
const { data: receipt, isPending, isFetching, error: receiptError } = useWaitForTransactionReceipt({
hash
})
const queryClient = useQueryClient();
useEffect(() => {
if (hash && receipt?.status === 'success') {
queryClient.invalidateQueries();
}
}, [receipt, hash, queryClient]);
function onSubmit(e) {
e.preventDefault();
if (!isAddressValid) {
toast.error('Invalid address.');
return;
}
if (!isAmountValid || !decimals) {
toast.error('Invalid amount or decimals not loaded yet.');
return;
}
makeTransfer({
address: token.address,
abi: erc20Abi,
functionName: 'transfer',
args: [inputAddress, parseUnits(inputAmount, decimals)]
})
}
if (errorBalance || errorDecimals) {
return <p>Cannot read balance</p>;
}
return (
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-4">
<h3 className="font-bold text-lg">{token.name}</h3>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 gap-y-5">
<p className="opacity-60">Identifier</p>
<p className="col-span-2">{token.id}</p>
<p className="opacity-60">Symbol</p>
<p className="col-span-2">{token.symbol}</p>
<p className="opacity-60">Contract address</p>
<p className="col-span-2 break-all">{token.address}</p>
<p className="opacity-60">Balance</p>
<p className="col-span-2">
{balance !== undefined && decimals !== undefined ? formatUnits(balance, decimals) : '...'} {token.symbol}
</p>
</div>
{isTransferSectionOpen && (
<div className="flex flex-col mt-10 mb-6 gap-3 rounded-xl p-3 lg:p-5 bg-amber-200/50">
<div className="flex flex-row gap-3 justify-between items-center">
<h4 className="font-semibold text-md w-full">Transfer funds</h4><span
className="p-2 cursor-pointer" onClick={() => setIsTransferSectionOpen(false)}>×</span>
</div>
<form className="flex flex-col gap-3" onSubmit={onSubmit}>
<input
className="border border-gray-500 text-black rounded-md p-2"
placeholder="Enter destination address" type="text" value={inputAddress}
onChange={e => setInputAddress(e.target.value)} />
<input
className="border border-gray-500 rounded-md p-2" placeholder="Enter amount"
type="text" value={inputAmount} onChange={e => setInputAmount(e.target.value)} />
<div className="flex flex-row justify-end">
<button disabled={!isAddressValid || !isAmountValid || isWaitingForUser}
className="bg-indigo-500 py-2 px-4 rounded-lg cursor-pointer text-white disabled:opacity-60">
Execute transfer
</button>
</div>
</form>
{isWaitingForUser && <p>Waiting for user...</p>}
{(userError || receiptError) && <p>Failed to execute transfer.</p>}
{(isPending && isFetching) && <p>Waiting for receipt...</p>}
{receipt && <p>Status: {receipt.status}</p>}
</div>
)}
<div className="flex flex-row items-center justify-end gap-2 mt-6">
<a href={`https://etherscan.code-camp.org/address/${token.address}`} className="bg-emerald-600 py-2 px-4 rounded-lg text-white" target="_blank">View contract</a>
<button
className="bg-blue-600 py-2 px-4 rounded-lg text-white cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setIsTransferSectionOpen(!isTransferSectionOpen)}>Transfer</button>
</div>
</div>
);
}