diff --git a/fe/app/_components/burn-dialog.tsx b/fe/app/_components/burn-dialog.tsx new file mode 100644 index 0000000..670eab3 --- /dev/null +++ b/fe/app/_components/burn-dialog.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { LoaderIcon } from "lucide-react"; +import Image from "next/image"; +import type { ComponentProps, FC, PropsWithChildren } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { useBurn } from "../_hooks/use-burn"; +import { BurnForm } from "./burn-form."; + +type Props = ComponentProps & { + tokenId: number; + tokenBalance: number | undefined; +}; + +export const BurnDialog: FC> = (props) => { + const { tokenId: tokenIdToBurn, tokenBalance, ...rest } = props; + + const { burnCall, isPending, isConfirming } = useBurn({ + tokenIdToBurn, + }); + + return ( + + + + + + + Burn +
+
+
+ Token Image +
+
+ + +
+
+
+
+ ); +}; diff --git a/fe/app/_components/burn-form..tsx b/fe/app/_components/burn-form..tsx new file mode 100644 index 0000000..af6bd4d --- /dev/null +++ b/fe/app/_components/burn-form..tsx @@ -0,0 +1,104 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import type { FC } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { DialogClose, DialogFooter } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const FormSchema = z.object({ + amount: z + .string() + .regex(/^\d*\.?\d+$/, "Must be a valid number") + .refine((val) => Number(val) < 7, "Must be a number between 0..6"), +}); + +type FormSchemaT = z.infer; + +type Props = { + onBurn: (amount: string) => void; + tokenId: number; + tokenBalance: number | undefined; +}; + +export const BurnForm: FC = (props) => { + const { onBurn, tokenId, tokenBalance } = props; + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + amount: "1", + }, + }); + + function onSubmit(values: FormSchemaT) { + const { amount } = values; + + onBurn?.(amount); + } + + return ( +
+ + ( + + Amount + + + + )} + /> + + + + + + + + ); +}; diff --git a/fe/app/_components/count-down.tsx b/fe/app/_components/count-down.tsx new file mode 100644 index 0000000..6b02223 --- /dev/null +++ b/fe/app/_components/count-down.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { animate, motion, useMotionValue, useTransform } from "motion/react"; +import { type FC, useEffect } from "react"; + +type Props = { + coolDownDelay: number | null; +}; +export const CountDown: FC = (props) => { + const { coolDownDelay } = props; + + const count = useMotionValue(coolDownDelay ?? 60); + const rounded = useTransform(count, Math.round); + + useEffect(() => { + const animation = animate(count, 0, { + duration: (coolDownDelay ?? 60) + 4, + }); + + return () => animation.cancel(); + }, [count, coolDownDelay]); + + return ( + + {rounded} + + ); +}; diff --git a/fe/app/_components/events-watcher.tsx b/fe/app/_components/events-watcher.tsx new file mode 100644 index 0000000..787fee6 --- /dev/null +++ b/fe/app/_components/events-watcher.tsx @@ -0,0 +1,19 @@ +"use client"; + +import type { FC, PropsWithChildren } from "react"; +import { useBurnEvents } from "../_hooks/_events/use-burn-events"; +import { useForgeEvents } from "../_hooks/_events/use-forge-events"; +import { useMintEvents } from "../_hooks/_events/use-mint-events"; +import { useTradeEvents } from "../_hooks/_events/use-trade-events"; + +// watch for events using alchemy websocket +export const EventsWatcher: FC = (props) => { + const { children } = props; + + useMintEvents(); + useForgeEvents(); + useBurnEvents(); + useTradeEvents(); + + return <>{children}; +}; diff --git a/fe/app/_components/footer.tsx b/fe/app/_components/footer.tsx new file mode 100644 index 0000000..3a9b598 --- /dev/null +++ b/fe/app/_components/footer.tsx @@ -0,0 +1,5 @@ +import type { FC } from "react"; + +export const Footer: FC = () => { + return
Footer
; +}; diff --git a/fe/app/_components/header.tsx b/fe/app/_components/header.tsx index 99631b6..5a5aaf1 100644 --- a/fe/app/_components/header.tsx +++ b/fe/app/_components/header.tsx @@ -8,10 +8,11 @@ import { TypographyH2 } from "./typography/h2"; export const Header: FC = () => { return (
diff --git a/fe/app/_components/hero.tsx b/fe/app/_components/hero.tsx new file mode 100644 index 0000000..00ce12c --- /dev/null +++ b/fe/app/_components/hero.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { motion } from "motion/react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { TypographyH1 } from "./typography/h1"; + +export const Hero = () => { + return ( +
+ + {"The Art of Forging Begins Here.".split(" ").map((word, index) => ( + + key={index} + initial={{ opacity: 0, filter: "blur(4px)", y: 10 }} + animate={{ opacity: 1, filter: "blur(0px)", y: 0 }} + transition={{ + duration: 0.3, + delay: index * 0.1, + ease: "easeInOut", + }} + className="mx-2 inline-block" + > + {word} + + ))} + + + + Mint free ERC1155 tokens, forge new ones, and trade your way to a + complete collection. + + + + +
+ ); +}; diff --git a/fe/app/_components/mint-button.tsx b/fe/app/_components/mint-button.tsx new file mode 100644 index 0000000..181017e --- /dev/null +++ b/fe/app/_components/mint-button.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { LoaderIcon } from "lucide-react"; +import { type FC, useCallback } from "react"; +import { useCoolDown } from "@/app/_hooks/use-cool-down"; +import { useTokens } from "@/app/_hooks/use-tokens"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useMint } from "../_hooks/use-mint"; +import { CountDown } from "./count-down"; + +type Props = React.ComponentProps<"button"> & { + tokenId: number; + isBaseToken: boolean; +}; + +export const MintButton: FC = (props) => { + const { tokenId, isBaseToken, ...rest } = props; + + const { mintCall, error, isPending, isConfirming, isConfirmed } = useMint({ + tokenId, + }); + + const { isCoolDown, setIsCoolDown, forgeabilityByTokenId } = useTokens(); + + const coolDownDelay = useCoolDown({ + isBaseToken, + isMintError: error, + isMintConfirmed: isConfirmed, + }); + + const isForgeable = !!forgeabilityByTokenId[tokenId]; + + const onMint = useCallback(() => { + if (isBaseToken) { + setIsCoolDown(true); + } + mintCall(); + }, [isBaseToken, setIsCoolDown, mintCall]); + + const isDisabled = + isPending || + isConfirming || + (isBaseToken && isCoolDown) || + (!isBaseToken && !isForgeable); + + return ( + + ); +}; diff --git a/fe/app/_components/token-card.tsx b/fe/app/_components/token-card.tsx new file mode 100644 index 0000000..7fa8785 --- /dev/null +++ b/fe/app/_components/token-card.tsx @@ -0,0 +1,151 @@ +"use client"; + +import Image from "next/image"; +import { type FC, useMemo } from "react"; +import { + Card, + CardContent, + CardDescription, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import type { Token } from "../_data/tokens"; +import { useBalanceOf } from "../_hooks/use-balanceOf"; +import { useTokens } from "../_hooks/use-tokens"; +import { BurnDialog } from "./burn-dialog"; +import { MintButton } from "./mint-button"; +import { TradeDialog } from "./trade-dialog"; + +type Props = Token; + +export const TokenCard: FC = (props) => { + const { + id, + name, + description, + isBaseToken = false, + requiredToForgeTokenIds = [], + } = props; + + const { isCoolDown, hoveredForgeTokenId, setHoveredForgeTokenId } = + useTokens(); + + const { tokenBalance, isPending } = useBalanceOf({ + tokenId: id, + }); + + const isWarmLighted = useMemo( + () => + isBaseToken && + hoveredForgeTokenId && + requiredToForgeTokenIds?.includes(hoveredForgeTokenId), + [isBaseToken, hoveredForgeTokenId, requiredToForgeTokenIds], + ); + + return ( + + +
+
+ Token Image +
+
+ {name} + + + + + + {description} + + +
{description}
+
+
+
+
+ +

+ Balance: {" "} + +

+ +
+ + + {!isBaseToken && ( + + )} + + {/** biome-ignore lint/a11y/noStaticElementInteractions: */} + { + return !isBaseToken && setHoveredForgeTokenId(id); + }} + onMouseLeave={() => { + return !isBaseToken && setHoveredForgeTokenId(null); + }} + > + + +
+
+
+
+
+ ); +}; + +type TokenBalanceProps = { + isPending: boolean; + tokenBalance: bigint | undefined; +}; + +const TokenBalance = (props: TokenBalanceProps) => { + const { isPending, tokenBalance } = props; + + return isPending ? ( + + ) : ( + {tokenBalance?.toString()} + ); +}; diff --git a/fe/app/_components/trade-dialog.tsx b/fe/app/_components/trade-dialog.tsx new file mode 100644 index 0000000..d34754a --- /dev/null +++ b/fe/app/_components/trade-dialog.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { LoaderIcon } from "lucide-react"; +import Image from "next/image"; +import type { FC } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { useTrade } from "../_hooks/use-trade"; +import { TradeForm } from "./trade-form"; + +type Props = { + tokenId: number; + tokenBalance: number | undefined; +}; + +export const TradeDialog: FC = (props) => { + const { tokenId: tokenIdToBurn, tokenBalance } = props; + + const { tradeCall, isPending, isConfirming } = useTrade({ + tokenIdToBurn, + }); + + return ( + + + + + + + Burn +
+
+
+ Token Image +
+
+ +
+
+
+
+ ); +}; diff --git a/fe/app/_components/trade-form.tsx b/fe/app/_components/trade-form.tsx new file mode 100644 index 0000000..496ae85 --- /dev/null +++ b/fe/app/_components/trade-form.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { DialogClose } from "@radix-ui/react-dialog"; +import Image from "next/image"; +import { type FC, useCallback, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { DialogFooter } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { BASE_TOKENS_IDS } from "../constant"; + +const FormSchema = z.object({ + tokenIdToMint: z + .string() + .regex(/^\d*\.?\d+$/, "Must be a valid number") + .refine((val) => Number(val) < 3, "Must be a number between 0..2"), +}); + +type FormSchemaT = z.infer; + +type Props = { + onTrade: (tokenIdToMint: number) => void; + tokenIdToBurn: number; + tokenBalance: number | undefined; +}; + +export const TradeForm: FC = (props) => { + const { onTrade, tokenIdToBurn } = props; + + const tokensIdsToMint = useMemo( + () => BASE_TOKENS_IDS.filter((id) => id !== tokenIdToBurn), + [tokenIdToBurn], + ); + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + tokenIdToMint: "0", + }, + }); + + const onSubmit = useCallback( + (values: FormSchemaT) => { + const { tokenIdToMint } = values; + + onTrade?.(Number(tokenIdToMint)); + }, + [onTrade], + ); + + return ( +
+ + ( + + Token To Mint + + + + )} + /> + + + + + + + + ); +}; diff --git a/fe/app/_context/root-providers.tsx b/fe/app/_context/root-providers.tsx index e24dbe0..86d1928 100644 --- a/fe/app/_context/root-providers.tsx +++ b/fe/app/_context/root-providers.tsx @@ -1,6 +1,7 @@ "use client"; import type { FC, PropsWithChildren } from "react"; +import { Toaster } from "sonner"; import type { State as WagmiState } from "wagmi"; import { CustomWagmiProvider } from "./custom-wagmi-provider"; import { ThemeProvider } from "./theme-provider"; @@ -22,6 +23,7 @@ export const RootProviders: FC> = (props) => { {children} + ); }; diff --git a/fe/app/_context/tokens-provider.tsx b/fe/app/_context/tokens-provider.tsx new file mode 100644 index 0000000..73b64f3 --- /dev/null +++ b/fe/app/_context/tokens-provider.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { type FC, type PropsWithChildren, useMemo, useState } from "react"; +import { useForgeability } from "../_hooks/use-forgeability"; +import { TokensContext } from "../_hooks/use-tokens"; + +export const TokensProvider: FC = (props) => { + const { children } = props; + + // state for tracking base-tokens cooldown + const [isCoolDown, setIsCoolDown] = useState(false); + + // state for tracking which forged-token card is hovered + const [hoveredForgeTokenId, setHoveredForgeTokenId] = useState( + null, + ); + + // hook to fetch/compute which forged-token is actually forgeable + const { forgeabilityByTokenId, reCheckForgeability } = useForgeability(); + + const value = useMemo( + () => ({ + // base tokens + isCoolDown, + setIsCoolDown, + + // forged tokens + forgeabilityByTokenId, + reCheckForgeability, + hoveredForgeTokenId, + setHoveredForgeTokenId, + }), + [ + isCoolDown, + forgeabilityByTokenId, + reCheckForgeability, + hoveredForgeTokenId, + ], + ); + + return ( + {children} + ); +}; diff --git a/fe/app/_contracts/forge-contract-config.ts b/fe/app/_contracts/forge-contract-config.ts new file mode 100644 index 0000000..3f3bc0c --- /dev/null +++ b/fe/app/_contracts/forge-contract-config.ts @@ -0,0 +1,206 @@ +export const forgeContractConfig = { + address: "0xD4922B783f762FEB81cEb08D6F1f4c45A8cAa148", + abi: [ + { + inputs: [ + { internalType: "string", name: "_tokenUri", type: "string" }, + { internalType: "uint256", name: "_maxTokenId", type: "uint256" }, + { internalType: "uint256", name: "_coolDownDelay", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "Forge__CanNotTradeForSameToken", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "Forge__CanOnlyBurnForgedToken", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "Forge__CanOnlyTradeForBasicToken", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "Forge__InvalidToken", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "Forge__UserInCoolDown", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Forge__BurnToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Forge__ForgeToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Forge__MintToken", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "burnedTokenId", + type: "uint256", + }, + { + indexed: true, + internalType: "uint256", + name: "mintedTokenId", + type: "uint256", + }, + ], + name: "Forge__Trade", + type: "event", + }, + { + inputs: [], + name: "I_COOL_DOWN_DELAY", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "I_MAX_TOKEN_ID", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "I_TOKEN", + outputs: [{ internalType: "contract FToken", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_tokenId", type: "uint256" }, + { internalType: "uint256", name: "_value", type: "uint256" }, + ], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "_tokenId", type: "uint256" }], + name: "getForgeData", + outputs: [ + { internalType: "uint256[]", name: "", type: "uint256[]" }, + { internalType: "uint256[]", name: "", type: "uint256[]" }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "_tokenId", type: "uint256" }], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_tokenIdToBurn", type: "uint256" }, + { internalType: "uint256", name: "_tokenIdToMint", type: "uint256" }, + ], + name: "trade", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "userCoolDownTimer", + outputs: [ + { internalType: "uint256", name: "coolDownTimer", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + ], +} as const; diff --git a/fe/app/_contracts/ftoken-contract-config.ts b/fe/app/_contracts/ftoken-contract-config.ts new file mode 100644 index 0000000..86f10b1 --- /dev/null +++ b/fe/app/_contracts/ftoken-contract-config.ts @@ -0,0 +1,280 @@ +export const fTokenContractConfig = { + address: "0x8281b01D35A70BDc17D85c6df3d45B67745a5F9f", + abi: [ + { + inputs: [{ internalType: "string", name: "uri_", type: "string" }], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "balance", type: "uint256" }, + { internalType: "uint256", name: "needed", type: "uint256" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "ERC1155InsufficientBalance", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "approver", type: "address" }], + name: "ERC1155InvalidApprover", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "idsLength", type: "uint256" }, + { internalType: "uint256", name: "valuesLength", type: "uint256" }, + ], + name: "ERC1155InvalidArrayLength", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "operator", type: "address" }], + name: "ERC1155InvalidOperator", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "receiver", type: "address" }], + name: "ERC1155InvalidReceiver", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "sender", type: "address" }], + name: "ERC1155InvalidSender", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "operator", type: "address" }, + { internalType: "address", name: "owner", type: "address" }, + ], + name: "ERC1155MissingApprovalForAll", + type: "error", + }, + { inputs: [], name: "FToken__OnlyOwnerAllowed", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { + indexed: false, + internalType: "uint256[]", + name: "ids", + type: "uint256[]", + }, + { + indexed: false, + internalType: "uint256[]", + name: "values", + type: "uint256[]", + }, + ], + name: "TransferBatch", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { + indexed: false, + internalType: "uint256", + name: "id", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "TransferSingle", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "value", + type: "string", + }, + { indexed: true, internalType: "uint256", name: "id", type: "uint256" }, + ], + name: "URI", + type: "event", + }, + { + inputs: [], + name: "I_OWNER", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + ], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address[]", name: "accounts", type: "address[]" }, + { internalType: "uint256[]", name: "ids", type: "uint256[]" }, + ], + name: "balanceOfBatch", + outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_from", type: "address" }, + { internalType: "uint256", name: "_id", type: "uint256" }, + { internalType: "uint256", name: "_value", type: "uint256" }, + ], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_from", type: "address" }, + { internalType: "uint256[]", name: "_ids", type: "uint256[]" }, + { internalType: "uint256[]", name: "_values", type: "uint256[]" }, + ], + name: "burnBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "address", name: "operator", type: "address" }, + ], + name: "isApprovedForAll", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_to", type: "address" }, + { internalType: "uint256", name: "_id", type: "uint256" }, + { internalType: "uint256", name: "_value", type: "uint256" }, + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256[]", name: "ids", type: "uint256[]" }, + { internalType: "uint256[]", name: "values", type: "uint256[]" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "safeBatchTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bool", name: "approved", type: "bool" }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "id", type: "uint256" }], + name: "uri", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + ], +} as const; diff --git a/fe/app/_data/tokens.ts b/fe/app/_data/tokens.ts new file mode 100644 index 0000000..afbf2ee --- /dev/null +++ b/fe/app/_data/tokens.ts @@ -0,0 +1,60 @@ +export type Token = { + id: number; + name: string; + description: string; + isBaseToken?: boolean; + requiredToForgeTokenIds?: number[]; +}; + +export const tokensData: Token[] = [ + // ---- BASE TOKENS ---- + { + id: 0, + description: + "A raw, sturdy material found deep within mines, essential for crafting durable tools and weapons.", + name: "Iron Ore", + isBaseToken: true, + requiredToForgeTokenIds: [3, 5, 6], + }, + { + id: 1, + description: + "A magical energy harvested from powerful sources or rare creatures, used in spellcasting and enchanted items.", + name: "Elemental Essence", + isBaseToken: true, + requiredToForgeTokenIds: [3, 4, 6], + }, + { + id: 2, + description: + "A refined mineral known for its sharp edges and conductive properties, often used in magical crafting4", + name: "Crystal Shards", + isBaseToken: true, + requiredToForgeTokenIds: [4, 5, 6], + }, + // ---- FORGED TOKENS ---- + { + id: 3, + description: + "A tough alloy forged by combining iron ore and elemental essence, creating a stronger, magically enhanced metal.", + name: "Steel Ingot (Iron Ore + Elemental Essence)", + }, + { + id: 4, + description: + "A glowing crystal forged by combining essence and shards, storing magical energy for spells or weapons.", + name: "Enchanted Crystal (Elemental Essence + Crystal Shards)", + }, + { + id: 5, + description: + "A strong, metallic-infused crystal forged by combining iron ore and crystal shards, used for crafting armor.", + name: "Reinforced Crystal (Iron Ore + Crystal Shards)", + }, + { + id: 6, + description: + "A rare, powerful artifact crafted by combining all three base materials, used in legendary weapons and technology.", + name: "Legendary Core (Iron Ore + Elemental Essence + Crystal Shards)", + }, +]; diff --git a/fe/app/_hooks/_events/use-burn-events.ts b/fe/app/_hooks/_events/use-burn-events.ts new file mode 100644 index 0000000..b6f4c07 --- /dev/null +++ b/fe/app/_hooks/_events/use-burn-events.ts @@ -0,0 +1,57 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { watchContractEvent } from "@wagmi/core"; +import { useEffect } from "react"; +import { useAccount } from "wagmi"; +import { wagmiWsConfig } from "@/app/_config/wagmi"; +import { forgeContractConfig } from "@/app/_contracts/forge-contract-config"; + +/** + * useBurnEvents + * + * on Forge burn-event (Forge__BurnToken) + * - Refetch burnt token balance + */ +export const useBurnEvents = () => { + const queryClient = useQueryClient(); + + const { address } = useAccount(); + + useEffect(() => { + if ( + !address || + !forgeContractConfig || + !forgeContractConfig.address || + !forgeContractConfig.abi + ) + return; + + const unwatch = watchContractEvent(wagmiWsConfig, { + address: forgeContractConfig.address, + abi: forgeContractConfig.abi, + // event Forge__BurnToken(address indexed user, uint256 indexed tokenId, uint256 indexed amount); + eventName: "Forge__BurnToken", + onLogs(logs) { + const log = logs[0] as unknown as { + args: { + user: string; + tokenId: bigint; + amount: bigint; + }; + }; + const { tokenId: burntTokenId } = log.args; + + queryClient.invalidateQueries({ + queryKey: ["balance", `${burntTokenId}-${address}`], + }); + }, + }); + + return () => { + unwatch(); + }; + }, [address, queryClient]); + + return null; +}; diff --git a/fe/app/_hooks/_events/use-forge-events.ts b/fe/app/_hooks/_events/use-forge-events.ts new file mode 100644 index 0000000..af36614 --- /dev/null +++ b/fe/app/_hooks/_events/use-forge-events.ts @@ -0,0 +1,77 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { watchContractEvent } from "@wagmi/core"; +import { useEffect } from "react"; +import { useAccount } from "wagmi"; +import { wagmiWsConfig } from "@/app/_config/wagmi"; +import { forgeContractConfig } from "@/app/_contracts/forge-contract-config"; +import { tokensData } from "@/app/_data/tokens"; +import { useTokens } from "../use-tokens"; + +/** + * useForgeEvents + * + * on Forge forge-event (Forge__ForgeToken): + * - Refetch forged-token balance + * - Refetch base-tokens balances (burnt tokens used to forge) + * - Recomputes forgeability of forged-tokens + */ +export const useForgeEvents = () => { + const queryClient = useQueryClient(); + + const { address } = useAccount(); + const { reCheckForgeability } = useTokens(); + + useEffect(() => { + if ( + !address || + !forgeContractConfig || + !forgeContractConfig.address || + !forgeContractConfig.abi + ) + return; + + const unwatch = watchContractEvent(wagmiWsConfig, { + address: forgeContractConfig.address, + abi: forgeContractConfig.abi, + // event Forge__ForgeToken(address indexed user, uint256 indexed tokenId, uint256 indexed amount); + eventName: "Forge__ForgeToken", + onLogs(logs) { + const log = logs[0] as unknown as { + args: { + user: string; + tokenId: bigint; + amount: bigint; + }; + }; + const { tokenId: forgedTokenId } = log.args; + + // compute burnt tokens used to forge forged-token with id === forgedTokenId + const baseTokenIds = tokensData + .filter((token) => { + return token.requiredToForgeTokenIds?.includes( + Number(forgedTokenId), + ); + }) + .map((token) => token.id); + + // react-query invalidates balanceOf forgedTokenId && baseTokenIds + [forgedTokenId, ...baseTokenIds].forEach((tokenId) => { + queryClient.invalidateQueries({ + queryKey: ["balance", `${tokenId}-${address}`], + }); + }); + + // Recomputes forgeability of forged-tokens + reCheckForgeability(); + }, + }); + + return () => { + unwatch(); + }; + }, [address, queryClient, reCheckForgeability]); + + return null; +}; diff --git a/fe/app/_hooks/_events/use-mint-events.ts b/fe/app/_hooks/_events/use-mint-events.ts new file mode 100644 index 0000000..3747e57 --- /dev/null +++ b/fe/app/_hooks/_events/use-mint-events.ts @@ -0,0 +1,63 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { watchContractEvent } from "@wagmi/core"; +import { useEffect } from "react"; +import { useAccount } from "wagmi"; +import { wagmiWsConfig } from "@/app/_config/wagmi"; +import { forgeContractConfig } from "@/app/_contracts/forge-contract-config"; +import { useTokens } from "../use-tokens"; + +/** + * useMintEvents + * + * on Forge mint-event (Forge__MintToken) + * - Refetch minted token balance + * - Recomputes forgeability of forged-tokens + */ +export const useMintEvents = () => { + const queryClient = useQueryClient(); + + const { address } = useAccount(); + const { reCheckForgeability } = useTokens(); + + useEffect(() => { + if ( + !address || + !forgeContractConfig || + !forgeContractConfig.address || + !forgeContractConfig.abi + ) + return; + + const unwatch = watchContractEvent(wagmiWsConfig, { + address: forgeContractConfig.address, + abi: forgeContractConfig.abi, + // event Forge__MintToken(address indexed user, uint256 indexed tokenId, uint256 indexed amount); + eventName: "Forge__MintToken", + onLogs(logs) { + const log = logs[0] as unknown as { + args: { + user: string; + tokenId: bigint; + amount: bigint; + }; + }; + const { tokenId: mintedTokenId } = log.args; + + queryClient.invalidateQueries({ + queryKey: ["balance", `${mintedTokenId}-${address}`], + }); + + // Recomputes forgeability of forged-tokens + reCheckForgeability(); + }, + }); + + return () => { + unwatch(); + }; + }, [address, queryClient, reCheckForgeability]); + + return null; +}; diff --git a/fe/app/_hooks/_events/use-trade-events.ts b/fe/app/_hooks/_events/use-trade-events.ts new file mode 100644 index 0000000..3eba4bb --- /dev/null +++ b/fe/app/_hooks/_events/use-trade-events.ts @@ -0,0 +1,63 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { watchContractEvent } from "@wagmi/core"; +import { useEffect } from "react"; +import { useAccount } from "wagmi"; +import { wagmiWsConfig } from "@/app/_config/wagmi"; +import { forgeContractConfig } from "@/app/_contracts/forge-contract-config"; +import { useTokens } from "../use-tokens"; + +/** + * useTradeEvents + * + * on Forge trade-event (Forge__Trade) + * - Refetch burnt token balance + * - Refetch minted token balance + * - Recomputes forgeability of forged-tokens + */ +export const useTradeEvents = () => { + const queryClient = useQueryClient(); + + const { address } = useAccount(); + const { reCheckForgeability } = useTokens(); + + useEffect(() => { + if ( + !address || + !forgeContractConfig || + !forgeContractConfig.address || + !forgeContractConfig.abi + ) + return; + + const unwatch = watchContractEvent(wagmiWsConfig, { + address: forgeContractConfig.address, + abi: forgeContractConfig.abi, + // event Forge__Trade(address indexed user, uint256 indexed burnedTokenId, uint256 indexed mintedTokenId); + eventName: "Forge__Trade", + onLogs(logs) { + const log = logs[0] as unknown as { + args: { user: string; burnedTokenId: bigint; mintedTokenId: bigint }; + }; + const { burnedTokenId, mintedTokenId } = log.args; + + queryClient.invalidateQueries({ + queryKey: ["balance", `${burnedTokenId}-${address}`], + }); + queryClient.invalidateQueries({ + queryKey: ["balance", `${mintedTokenId}-${address}`], + }); + + // Recomputes forgeability of forged-tokens + reCheckForgeability(); + }, + }); + + return () => { + unwatch(); + }; + }, [address, queryClient, reCheckForgeability]); + + return null; +}; diff --git a/fe/app/_hooks/use-balanceOf.tsx b/fe/app/_hooks/use-balanceOf.tsx new file mode 100644 index 0000000..b705a61 --- /dev/null +++ b/fe/app/_hooks/use-balanceOf.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { + type QueryObserverResult, + type RefetchOptions, + useQuery, +} from "@tanstack/react-query"; +import { readContract } from "@wagmi/core"; +import { toast } from "sonner"; +import type { ReadContractErrorType } from "viem"; +import { useAccount } from "wagmi"; +import { wagmiHttpConfig } from "../_config/wagmi"; +import { fTokenContractConfig } from "../_contracts/ftoken-contract-config"; + +type ParamsT = { + tokenId: number; +}; + +export const useBalanceOf = (params: ParamsT) => { + const { tokenId } = params; + const { address } = useAccount(); + + const { + data: tokenBalance, + error, + isPending, + refetch, + } = useQuery({ + queryKey: ["balance", `${tokenId}-${address}`], // Explicit queryKey + queryFn: async () => { + if (!address) return 0n; + const balance = await readContract(wagmiHttpConfig, { + ...fTokenContractConfig, + functionName: "balanceOf", + args: [address as `0x${string}`, BigInt(tokenId)], + }); + return balance as bigint; + }, + enabled: !!address, + }); + + if (error) { + toast.error((error as ReadContractErrorType).shortMessage || error.message); + } + + return { tokenBalance, refetch, error, isPending }; +}; + +export type RefetchBalanceOf = ( + options?: RefetchOptions | undefined, +) => Promise>; diff --git a/fe/app/_hooks/use-burn.tsx b/fe/app/_hooks/use-burn.tsx new file mode 100644 index 0000000..b4da206 --- /dev/null +++ b/fe/app/_hooks/use-burn.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useCallback } from "react"; +import { toast } from "sonner"; +import type { BaseError } from "wagmi"; +import { forgeContractConfig } from "@/app/_contracts/forge-contract-config"; +import { useWriteAndWait } from "./use-write-and-wait"; + +type ParamsT = { + tokenIdToBurn: number; +}; + +export const useBurn = (params: ParamsT) => { + const { tokenIdToBurn } = params; + + const { writeContract, hash, error, isPending, isConfirming, isConfirmed } = + useWriteAndWait(); + + const burnCall = useCallback( + (amount: string) => { + writeContract({ + ...forgeContractConfig, + functionName: "burn", + args: [BigInt(tokenIdToBurn), BigInt(amount)], + }); + }, + [writeContract, tokenIdToBurn], + ); + + if (error) { + console.log((error as BaseError).shortMessage || error.message); + toast.error(`Burning Token #${tokenIdToBurn} failed.`); + } + + if (hash) { + console.log(`Burn Token #${tokenIdToBurn} Transaction Hash: ${hash}`); + } + + if (isConfirming) { + toast.info(`Waiting for Burn Token #${tokenIdToBurn} confirmation...`); + } + + if (isConfirmed) { + toast.success(`Burn Token #${tokenIdToBurn} confirmed.`); + } + + return { burnCall, hash, error, isPending, isConfirming, isConfirmed }; +}; diff --git a/fe/app/_hooks/use-cool-down.ts b/fe/app/_hooks/use-cool-down.ts new file mode 100644 index 0000000..1ea1c56 --- /dev/null +++ b/fe/app/_hooks/use-cool-down.ts @@ -0,0 +1,59 @@ +"use client"; +import { useEffect, useMemo } from "react"; +import { useReadContract } from "wagmi"; +import { forgeContractConfig } from "../_contracts/forge-contract-config"; +import { useTokens } from "./use-tokens"; + +type ParamsT = { + isBaseToken: boolean; + isMintError: Error | null; + isMintConfirmed: boolean; +}; + +export const useCoolDown = (params: ParamsT) => { + const { isBaseToken, isMintError, isMintConfirmed } = params; + + const { setIsCoolDown } = useTokens(); + + // Fetch cooldown delay from contract + const { data: coolDownDelay } = useReadContract({ + ...forgeContractConfig, + functionName: "I_COOL_DOWN_DELAY", + }); + + // Convert cooldown delay to milliseconds + const coolDownDelayInMs: number | null = useMemo(() => { + return coolDownDelay ? Number(coolDownDelay) * 1000 : null; + }, [coolDownDelay]); + + // Handle cooldown logic + useEffect(() => { + if (!isBaseToken || !coolDownDelayInMs) return; + + // Reset cooldown on error + if (isMintError) { + setIsCoolDown(false); + return; + } + + // Start cooldown timer only after transaction is confirmed + if (isMintConfirmed) { + setIsCoolDown(true); + const timer = setTimeout(() => { + setIsCoolDown(false); + }, coolDownDelayInMs); + + return () => { + clearTimeout(timer); + }; + } + }, [ + isBaseToken, + isMintError, + isMintConfirmed, + coolDownDelayInMs, + setIsCoolDown, + ]); + + return coolDownDelayInMs ? coolDownDelayInMs / 1000 : null; +}; diff --git a/fe/app/_hooks/use-forgeability.ts b/fe/app/_hooks/use-forgeability.ts new file mode 100644 index 0000000..d83b296 --- /dev/null +++ b/fe/app/_hooks/use-forgeability.ts @@ -0,0 +1,100 @@ +"use client"; + +import type { + QueryObserverResult, + RefetchOptions, +} from "@tanstack/react-query"; +import type { ReadContractsErrorType } from "@wagmi/core"; +import { useMemo } from "react"; +import { useAccount, useReadContracts } from "wagmi"; +import { fTokenContractConfig } from "../_contracts/ftoken-contract-config"; +import { tokensData } from "../_data/tokens"; +import { BASE_TOKENS_IDS, FORGED_TOKENS_IDS } from "../constant"; + +export const useForgeability = () => { + const { address } = useAccount(); + + // Prepare contract calls for all base tokens + const calls = useMemo(() => { + if (!address) return []; + + return BASE_TOKENS_IDS.map((id) => ({ + ...fTokenContractConfig, + functionName: "balanceOf", + args: [address as `0x${string}`, BigInt(id)], + query: { + queryKey: ["balance", `${id}-${address}`], // unique per token / user + }, + })); + }, [address]); + + // Fetch balances for all base tokens + const { + data: tokenBalancesData, + error, + isLoading, + refetch, + } = useReadContracts({ + contracts: calls, + query: { enabled: calls.length > 0 && !!address }, + }); + + // Transform results into bigint[] + const tokenBalances = useMemo(() => { + if (!tokenBalancesData) return undefined; + return tokenBalancesData.map((result) => + result.status === "success" ? BigInt(result.result) : BigInt(0), + ); + }, [tokenBalancesData]); + + // Check forgeability for each forgeable token + const forgeabilityByTokenId = useMemo(() => { + if (!tokenBalances) return {}; + + const result: Record = {}; + FORGED_TOKENS_IDS.forEach((tokenId) => { + // Find which base tokens are required for this forgeable token + const requiredBaseTokenIds = BASE_TOKENS_IDS.filter((baseTokenId) => { + const baseToken = tokensData.find((t) => t.id === baseTokenId); + return baseToken?.requiredToForgeTokenIds?.includes(tokenId); + }); + + // Check if the user has all required base tokens + const hasAllBalances = requiredBaseTokenIds.every((baseTokenId) => { + const index = BASE_TOKENS_IDS.indexOf(baseTokenId); + return tokenBalances[index] > BigInt(0); + }); + + result[tokenId] = hasAllBalances; + }); + + return result; + }, [tokenBalances]); + + return { + forgeabilityByTokenId, + isLoading, + error, + reCheckForgeability: refetch, + }; +}; + +export type reCheckForgeability = ( + options?: RefetchOptions | undefined, +) => Promise< + QueryObserverResult< + ( + | { + error?: undefined; + result: `0x${string}`; + status: "success"; + } + | { + error: Error; + result?: undefined; + status: "failure"; + } + )[], + ReadContractsErrorType + > +>; diff --git a/fe/app/_hooks/use-mint.tsx b/fe/app/_hooks/use-mint.tsx new file mode 100644 index 0000000..f6cf66d --- /dev/null +++ b/fe/app/_hooks/use-mint.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useCallback } from "react"; +import { toast } from "sonner"; +import type { BaseError } from "wagmi"; +import { forgeContractConfig } from "../_contracts/forge-contract-config"; +import { useWriteAndWait } from "./use-write-and-wait"; + +type ParamsT = { + tokenId: number; +}; + +export const useMint = (params: ParamsT) => { + const { tokenId } = params; + + const { writeContract, hash, error, isPending, isConfirming, isConfirmed } = + useWriteAndWait(); + + const mintCall = useCallback( + () => + writeContract({ + ...forgeContractConfig, + functionName: "mint", + args: [BigInt(tokenId)], + }), + [writeContract, tokenId], + ); + + if (error) { + console.log((error as BaseError).shortMessage || error.message); + toast.error(`Minting Token #${tokenId} failed.`); + } + + if (hash) { + console.log(`Mint Token #${tokenId} Transaction Hash: ${hash}`); + } + + if (isConfirming) { + toast.info(`Waiting for Mint Token #${tokenId} confirmation...`); + } + + if (isConfirmed) { + toast.success(`Mint Token #${tokenId} confirmed.`); + } + + return { mintCall, hash, error, isPending, isConfirming, isConfirmed }; +}; diff --git a/fe/app/_hooks/use-tokens.ts b/fe/app/_hooks/use-tokens.ts new file mode 100644 index 0000000..5a9ab7c --- /dev/null +++ b/fe/app/_hooks/use-tokens.ts @@ -0,0 +1,26 @@ +"use client"; + +import { createContext, useContext } from "react"; +import type { reCheckForgeability } from "./use-forgeability"; + +export type TokensContextT = { + // base tokens + isCoolDown: boolean; + setIsCoolDown: React.Dispatch>; + + // forge tokens + forgeabilityByTokenId: Record; + reCheckForgeability: reCheckForgeability; + hoveredForgeTokenId: number | null; + setHoveredForgeTokenId: React.Dispatch>; +}; + +export const TokensContext = createContext(null); + +export const useTokens = (): TokensContextT => { + const ctx = useContext(TokensContext); + if (!ctx) { + throw new Error("useTokens must be used with its TokensProvider"); + } + return ctx; +}; diff --git a/fe/app/_hooks/use-trade.tsx b/fe/app/_hooks/use-trade.tsx new file mode 100644 index 0000000..99ea337 --- /dev/null +++ b/fe/app/_hooks/use-trade.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useCallback } from "react"; +import { toast } from "sonner"; +import type { BaseError } from "wagmi"; +import { forgeContractConfig } from "../_contracts/forge-contract-config"; +import { useWriteAndWait } from "./use-write-and-wait"; + +type ParamsT = { + tokenIdToBurn: number; +}; + +export const useTrade = (params: ParamsT) => { + const { tokenIdToBurn } = params; + + const { writeContract, hash, error, isPending, isConfirming, isConfirmed } = + useWriteAndWait(); + + const tradeCall = useCallback( + (tokenIdToMint: number) => { + writeContract({ + ...forgeContractConfig, + functionName: "trade", + args: [BigInt(tokenIdToBurn), BigInt(tokenIdToMint)], + }); + }, + [writeContract, tokenIdToBurn], + ); + + if (error) { + console.log((error as BaseError).shortMessage || error.message); + toast.error(`Trading Token #${tokenIdToBurn} failed.`); + } + + if (hash) { + console.log(`Trading Token #${tokenIdToBurn} Transaction Hash: ${hash}`); + } + + if (isConfirming) { + toast.info(`Waiting for Trading Token #${tokenIdToBurn} confirmation...`); + } + + if (isConfirmed) { + toast.success(`Trading Token #${tokenIdToBurn} confirmed.`); + } + + return { tradeCall, hash, error, isPending, isConfirming, isConfirmed }; +}; diff --git a/fe/app/_hooks/use-write-and-wait.ts b/fe/app/_hooks/use-write-and-wait.ts new file mode 100644 index 0000000..dfde21f --- /dev/null +++ b/fe/app/_hooks/use-write-and-wait.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; + +export const useWriteAndWait = () => { + const { data: hash, error, isPending, writeContract } = useWriteContract(); + + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ + hash, + }); + + return { writeContract, hash, error, isPending, isConfirming, isConfirmed }; +}; diff --git a/fe/app/android-chrome-192x192.png b/fe/app/android-chrome-192x192.png new file mode 100644 index 0000000..c0b2fa2 Binary files /dev/null and b/fe/app/android-chrome-192x192.png differ diff --git a/fe/app/android-chrome-512x512.png b/fe/app/android-chrome-512x512.png new file mode 100644 index 0000000..04d71fb Binary files /dev/null and b/fe/app/android-chrome-512x512.png differ diff --git a/fe/app/apple-touch-icon.png b/fe/app/apple-touch-icon.png new file mode 100644 index 0000000..987d173 Binary files /dev/null and b/fe/app/apple-touch-icon.png differ diff --git a/fe/app/constant.ts b/fe/app/constant.ts new file mode 100644 index 0000000..47cb56d --- /dev/null +++ b/fe/app/constant.ts @@ -0,0 +1,2 @@ +export const BASE_TOKENS_IDS = [0, 1, 2]; +export const FORGED_TOKENS_IDS = [3, 4, 5, 6]; diff --git a/fe/app/favicon-16x16.png b/fe/app/favicon-16x16.png new file mode 100644 index 0000000..fc22f37 Binary files /dev/null and b/fe/app/favicon-16x16.png differ diff --git a/fe/app/favicon-32x32.png b/fe/app/favicon-32x32.png new file mode 100644 index 0000000..d96875e Binary files /dev/null and b/fe/app/favicon-32x32.png differ diff --git a/fe/app/favicon.ico b/fe/app/favicon.ico index 718d6fe..0143c4a 100644 Binary files a/fe/app/favicon.ico and b/fe/app/favicon.ico differ diff --git a/fe/app/layout.tsx b/fe/app/layout.tsx index e4e9252..68da8fa 100644 --- a/fe/app/layout.tsx +++ b/fe/app/layout.tsx @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { cookieToInitialState } from "@wagmi/core"; import { headers } from "next/headers"; +import { Footer } from "./_components/footer"; import { Header } from "./_components/header"; import { wagmiHttpConfig } from "./_config/wagmi"; import { RootProviders } from "./_context/root-providers"; @@ -18,8 +19,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Forge", + description: "The Art of Forging ERC1155 Begins Here.", }; export default async function RootLayout({ @@ -33,13 +34,14 @@ export default async function RootLayout({ ); return ( - +
- {children} +
{children}
+