diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx
index f2944a8..e4f0107 100644
--- a/app/components/layout/Sidebar.tsx
+++ b/app/components/layout/Sidebar.tsx
@@ -3,7 +3,8 @@ import { Link, useLocation } from "react-router";
const navItems = [
{ route: "/", label: "Explorer", icon: "home" },
{ route: "/status", label: "Status", icon: "notepad" },
- { route: "/transfer", label: "Transfer", icon: "money" }
+ { route: "/transfer", label: "Transfer", icon: "money" },
+ { route: "/auction", label: "Auction", icon: "auction" }
];
export function Sidebar() {
diff --git a/app/config/auction.ts b/app/config/auction.ts
new file mode 100644
index 0000000..23d5d15
--- /dev/null
+++ b/app/config/auction.ts
@@ -0,0 +1,133 @@
+export const auctionContractAddress = '0x9d1430b67a7560c5eb450D543947AdB80C03B55A';
+
+export const auctionABI = [
+ {
+ "inputs": [],
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "amount",
+ "type": "uint256"
+ }
+ ],
+ "name": "AuctionClosed",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "address",
+ "name": "bidder",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "amount",
+ "type": "uint256"
+ }
+ ],
+ "name": "BidIncreased",
+ "type": "event"
+ },
+ {
+ "inputs": [],
+ "name": "bid",
+ "outputs": [],
+ "stateMutability": "payable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "closeAuction",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "closed",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "creator",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "highestAmount",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "highestBidder",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "addr",
+ "type": "address"
+ }
+ ],
+ "name": "pendingReturnAmount",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "withdraw",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+] as const;
diff --git a/app/pages/auction/AuctionPage.tsx b/app/pages/auction/AuctionPage.tsx
new file mode 100644
index 0000000..b5440d7
--- /dev/null
+++ b/app/pages/auction/AuctionPage.tsx
@@ -0,0 +1,300 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router";
+import { toast } from "react-toastify";
+import { formatEther, parseEther, type Address } from "viem";
+import { useBalance, useConnection, useReadContract, useReadContracts, useWaitForTransactionReceipt, useWatchContractEvent, useWriteContract } from "wagmi";
+import { LoadingView } from "~/components/loading/LoadingView";
+import { auctionABI, auctionContractAddress } from "~/config/auction";
+
+
+function WalletBalanceCard({ address }: { address: Address }) {
+ const { data: balance, error } = useBalance({ address });
+
+ if (error) {
+ return
Failed to fetch wallet balance.
;
+ }
+
+ return (
+
+
+
+ Wallet Balance
+
+ Funds
+
+
+ {balance ? Number(formatEther(balance.value)).toLocaleString() : '...'}
+ ETH
+
+
+ )
+}
+
+function BestOfferCard({ address }: { address: Address }) {
+ const { data: bestOffer, error } = useReadContracts({
+ contracts: [
+ {
+ address: auctionContractAddress,
+ abi: auctionABI,
+ functionName: 'highestAmount'
+ },
+ {
+ address: auctionContractAddress,
+ abi: auctionABI,
+ functionName: 'highestBidder'
+ }
+ ]
+ });
+
+ const [amount, bidder] = bestOffer || [];
+
+ if (error || amount?.error || bidder?.error) {
+ return Failed to fetch best bid.
;
+ }
+
+ return (
+
+
+
+ Best Offer
+
+ Top Bid
+
+
+ {amount?.result !== undefined ? Number(formatEther(amount.result)).toLocaleString() : '...'}
+ ETH ({bidder?.result === address ? "by you" : "by someone else"})
+
+
+ )
+}
+
+function LastEventCard({ event }: { event?: string }) {
+ return (
+
+
+
+ Last Event
+
+ Update
+
+
+ {event || 'No events yet...'}
+
+
+ )
+}
+
+function PendingReturnCard({ address }: { address: Address }) {
+ const { data: refundValue, error } = useReadContract({
+ address: auctionContractAddress,
+ abi: auctionABI,
+ functionName: 'pendingReturnAmount',
+ args: [address]
+ });
+
+ if (error) {
+ return Failed to fetch refund amount.
;
+ }
+
+ return (
+
+
+
+ Pending Return
+
+ Your Refund
+
+
+
+ {refundValue !== undefined ? Number(formatEther(refundValue)).toLocaleString() : '...'}
+
+ ETH
+
+
+ )
+}
+
+
+function MakeBidCard() {
+ const [inputValue, setInputValue] = useState("");
+ const isValidInput = inputValue && !isNaN(Number(inputValue)) && Number(inputValue) > 0;
+
+ const { data: hash, mutate: makeBid, isPending: isWaitingForUser, error: userError, reset } = useWriteContract();
+ const { data: receipt, isPending, isFetching, error: receiptError } = useWaitForTransactionReceipt({
+ hash
+ })
+
+ function onSubmit(e) {
+ e.preventDefault();
+
+ if (!isValidInput) {
+ toast.error('Invalid bid amount.');
+ return;
+ }
+
+ makeBid({
+ address: auctionContractAddress,
+ abi: auctionABI,
+ functionName: 'bid',
+ value: parseEther(inputValue)
+ })
+ }
+
+ return (
+
+
+
+ Make Bid
+
+ Bid
+
+
+
+

+
+ Enter your bid amount in ETH
+
+
+
+
+
+ {isWaitingForUser &&
Waiting for user...
}
+ {(userError || receiptError) &&
Failed to make bid.
}
+ {(isPending && isFetching) &&
Waiting for receipt...
}
+ {receipt &&
Status: {receipt.status}
}
+
+
+
+ )
+}
+
+export function WithdrawCard({ address }: { address: Address }) {
+
+ const { data: refundValue, isPending: isRefundValuePending, error: refundValueError } = useReadContract({
+ address: auctionContractAddress,
+ abi: auctionABI,
+ functionName: 'pendingReturnAmount',
+ args: [address]
+ });
+
+ const queryClient = useQueryClient();
+ const { data: hash, mutate: withdraw, isPending: isWaitingForUser, error: userError } = useWriteContract();
+ const { data: receipt, isPending, isFetching, error: receiptError } = useWaitForTransactionReceipt({
+ hash
+ });
+
+ useEffect(() => {
+ if (hash && receipt?.status === 'success') {
+ queryClient.invalidateQueries();
+ }
+ }, [receipt, hash, queryClient]);
+
+
+ function onWithdraw(e) {
+ e.preventDefault();
+
+ withdraw({
+ address: auctionContractAddress,
+ abi: auctionABI,
+ functionName: 'withdraw'
+ })
+ }
+
+ if (refundValueError) {
+ return Failed to fetch refund value.
;
+ }
+
+ if (isRefundValuePending || !refundValue) {
+ // don't render the component if there is nothing to withdraw
+ return null;
+ }
+
+ return (
+
+
+
+ Withdraw
+
+ Refund
+
+
+
+

+
+ Withdraw your funds
+
+
+
+
+ Click the button to withdraw your funds
+
+
+
+ {isWaitingForUser &&
Waiting for user...
}
+ {(userError || receiptError) &&
Failed to withdraw.
}
+ {(isFetching && isPending) &&
Waiting for receipt...
}
+ {receipt &&
Status: {receipt.status}
}
+
+
+
+ )
+}
+
+
+export function AuctionPage() {
+ const { address, isConnected } = useConnection();
+ const [isReady, setIsReady] = useState(false);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!isConnected) {
+ navigate("/status");
+ } else if (address && !isReady) {
+ setIsReady(true);
+ }
+ }, [isConnected, address, isReady, navigate, setIsReady]);
+
+ const [lastEvent, setLastEvent] = useState();
+ const queryClient = useQueryClient();
+ useWatchContractEvent({
+ address: auctionContractAddress,
+ abi: auctionABI,
+ onLogs: (logs) => {
+ if (logs.length > 0) {
+ setLastEvent(logs[logs.length - 1].eventName);
+ queryClient.invalidateQueries();
+ }
+ }
+ })
+
+ if (!isConnected || !address || !isReady) {
+ return ;
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/app/routes.ts b/app/routes.ts
index 1981238..493f44b 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -3,5 +3,6 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("status", "routes/status.tsx"),
- route("transfer", "routes/transfer.tsx")
+ route("transfer", "routes/transfer.tsx"),
+ route("auction", "routes/auction.tsx")
] satisfies RouteConfig;
diff --git a/app/routes/auction.tsx b/app/routes/auction.tsx
new file mode 100644
index 0000000..6eb12f1
--- /dev/null
+++ b/app/routes/auction.tsx
@@ -0,0 +1,7 @@
+import { AuctionPage } from "~/pages/auction/AuctionPage";
+
+export default function Auction() {
+ return (
+
+ )
+}
\ No newline at end of file