();
diff --git a/src/features/common/utils/formatNumber.ts b/src/features/common/utils/formatNumber.ts
index 71475721..2f681713 100644
--- a/src/features/common/utils/formatNumber.ts
+++ b/src/features/common/utils/formatNumber.ts
@@ -3,6 +3,7 @@ import BigNumber from "bignumber.js";
export const formatNumber = (
number?: number | bigint,
decimals: number | null = null,
+ options?: (Intl.NumberFormatOptions & BigIntToLocaleStringOptions),
) => {
if (number == null) return number;
@@ -21,5 +22,6 @@ export const formatNumber = (
minimumSignificantDigits: 3,
maximumSignificantDigits: 3,
notation: "compact",
+ ...options,
});
};
diff --git a/src/features/rewards/ClaimNftButton.tsx b/src/features/rewards/ClaimNftButton.tsx
new file mode 100644
index 00000000..f3c3ef4d
--- /dev/null
+++ b/src/features/rewards/ClaimNftButton.tsx
@@ -0,0 +1,67 @@
+import { twJoin } from "tailwind-merge";
+import { useAccount } from "wagmi";
+import { useTokenBalances } from "../../hooks/useTokenBalances";
+import { Button } from "../common/Button";
+import { ClaimNftRewardForm } from "./ClaimNftRewardForm";
+import { useState } from "react";
+import { Modal } from "../common/Modal";
+import { requiredBalanceForNftClaim } from "./config";
+import { formatNumber } from "../common/utils/formatNumber";
+import { CheckMark } from "../common/icons/CheckMark";
+import { MdClose } from "react-icons/md";
+
+export const ClaimNftButton = () => {
+ const { isConnected } = useAccount();
+ const [showClaimNftModal, setShowClaimNftModal] = useState(false);
+ const { sAstBalanceRaw, sAstBalanceV4_DeprecatedRaw } = useTokenBalances();
+
+ const totalSastBalance = sAstBalanceRaw + sAstBalanceV4_DeprecatedRaw;
+ const formattedRequiredBalance = formatNumber(requiredBalanceForNftClaim, 4, { minimumSignificantDigits: 4, maximumSignificantDigits: 4 });
+ // const isBalanceEnough = !!totalSastBalance && totalSastBalance >= requiredBalanceForNftClaim;
+ const isBalanceEnough = true;
+ const isEligible = isConnected && isBalanceEnough;
+
+ return (
+ <>
+ {isEligible && (
+
+
+ Free AirSwap NFT
+
+
+
+
+ )}
+
+ {showClaimNftModal && (
+ setShowClaimNftModal(false)}
+ heading={isEligible ? "Eligible for free mint" : "Not eligible for free mint"}
+ subHeading={
+
+ {`Required: ${formattedRequiredBalance} sAST`}
+
+ {isEligible ? : }
+
+
+ }
+ >
+
+
+ )}
+ >
+ );
+};
diff --git a/src/features/rewards/ClaimNftRewardForm.tsx b/src/features/rewards/ClaimNftRewardForm.tsx
new file mode 100644
index 00000000..92e885f5
--- /dev/null
+++ b/src/features/rewards/ClaimNftRewardForm.tsx
@@ -0,0 +1,125 @@
+import { useEffect } from "react";
+import {
+ useAccount,
+ useChainId,
+ useContractWrite,
+ usePrepareContractWrite,
+ usePublicClient,
+ useWaitForTransaction,
+} from "wagmi";
+import { ContractTypes } from "../../config/ContractAddresses";
+import { useContractAddresses } from "../../config/hooks/useContractAddress";
+import { minterAbi } from "../../contracts/minterAbi";
+import { useTokenBalances } from "../../hooks/useTokenBalances";
+import { Button } from "../common/Button";
+import { TransactionTracker } from "../common/TransactionTracker";
+import { requiredBalanceForNftClaim } from "./config";
+import { useNftInfo } from "./hooks/useNftInfo";
+import { InfiniteInitiate } from "./InfiniteInitiate";
+import { useClaimNftStore } from "./store/useClaimNftStore";
+
+const nftAddress = "0xf80cd411d49804d4a80bfe3b26dad4679b7918d7";
+
+export const ClaimNftRewardForm = () => {
+ const [pool] = useContractAddresses([ContractTypes.AirSwapPool], {});
+ const { address: connectedAccount } = useAccount();
+
+ const chainId = useChainId();
+ const publicClient = usePublicClient({ chainId: chainId });
+
+ const { sAstBalanceRaw, sAstBalanceV4_DeprecatedRaw } = useTokenBalances();
+
+ const totalSastBalance = sAstBalanceRaw + sAstBalanceV4_DeprecatedRaw;
+ const isBalanceEnough =
+ !!totalSastBalance && totalSastBalance >= requiredBalanceForNftClaim;
+
+ const [showClaimNftModal, setShowClaimNftModal, setIsClaimLoading] =
+ useClaimNftStore((state) => [
+ state.showClaimNftModal,
+ state.setShowClaimNftModal,
+ state.setIsClaimLoading,
+ ]);
+
+ const { data: nftInfo } = useNftInfo({ address: nftAddress, id: 0n });
+
+ const { config: mintTxConfig } = usePrepareContractWrite({
+ ...pool,
+ abi: minterAbi,
+ functionName: "mintNFT",
+ enabled: isBalanceEnough,
+ });
+
+ const {
+ data: writeResult,
+ write,
+ reset: resetContractWrite,
+ isLoading: waitingForSignature,
+ } = useContractWrite({
+ ...mintTxConfig,
+ onSuccess: async (result) => {
+ const receipt = await publicClient.waitForTransactionReceipt({
+ hash: result.hash,
+ });
+
+ // Show claim success
+ },
+ onError: (e: any) => {
+ if (e?.cause?.code === 4001) {
+ // Do nothing here, the user rejected the tx
+ } else {
+ // Show claim failed
+ }
+ },
+ });
+
+ const { status: txStatus } = useWaitForTransaction({
+ hash: writeResult?.hash,
+ });
+
+ const actionButtons = {
+ afterFailure: {
+ label: "Try again",
+ callback: () => {
+ resetContractWrite();
+ },
+ },
+ afterSuccess: {
+ label: "Close",
+ callback: () => {
+ setShowClaimNftModal(false);
+ },
+ },
+ };
+
+ useEffect(() => {
+ if (txStatus === "loading" || waitingForSignature) {
+ setIsClaimLoading(true);
+ } else {
+ setIsClaimLoading(false);
+ }
+ }, [txStatus, setIsClaimLoading, waitingForSignature]);
+
+ return writeResult?.hash || waitingForSignature ? (
+
+ ) : (
+
+
+
+
+
+ );
+};
diff --git a/src/features/rewards/InfiniteInitiate.tsx b/src/features/rewards/InfiniteInitiate.tsx
new file mode 100644
index 00000000..d0c7c944
--- /dev/null
+++ b/src/features/rewards/InfiniteInitiate.tsx
@@ -0,0 +1,22 @@
+import { GoLinkExternal } from "react-icons/go";
+import infiniteInitiate from "../../assets/video/infinite-initiate.mp4"
+import ethereumLogo from "../../assets/ethereum.svg"
+import { mainnet } from "wagmi";
+
+const url = "https://sepolia.etherscan.io/token/0xf80cd411d49804d4a80bfe3b26dad4679b7918d7";
+
+export const InfiniteInitiate = ({className}: {className?: string}) => {
+ return (
+
+
Infinite Initiate
+
+

+ ERC-1150
+
+
+
+
+
+
+ )
+};
\ No newline at end of file
diff --git a/src/features/rewards/config.ts b/src/features/rewards/config.ts
new file mode 100644
index 00000000..c9c75502
--- /dev/null
+++ b/src/features/rewards/config.ts
@@ -0,0 +1 @@
+export const requiredBalanceForNftClaim = 10010000n;
diff --git a/src/features/rewards/hooks/useNftInfo.ts b/src/features/rewards/hooks/useNftInfo.ts
new file mode 100644
index 00000000..bff03bd8
--- /dev/null
+++ b/src/features/rewards/hooks/useNftInfo.ts
@@ -0,0 +1,48 @@
+import { useQuery } from "@tanstack/react-query";
+import { Address, useChainId, useContractRead } from "wagmi";
+import { erc1155Abi } from "../../../contracts/erc1155Abi";
+
+type AirSwapNftInfo = {
+ created_by: string;
+ description: string;
+ image: string;
+ name: string;
+};
+
+export const useNftInfo = ({
+ address: nftAddress,
+ id,
+}: {
+ address: Address;
+ id: bigint;
+}) => {
+ const chainId = useChainId();
+
+ const { data: uri } = useContractRead({
+ address: nftAddress,
+ abi: erc1155Abi,
+ chainId,
+ functionName: "uri",
+ args: [id],
+ });
+
+ return useQuery({
+ queryKey: ["nftInfo", nftAddress, id],
+ queryFn: async () => {
+ // Handle IPFS URLs (convert ipfs:// to https://ipfs.io/ipfs/)
+ let url = uri as string;
+ if (url.startsWith("ipfs://")) {
+ url = url.replace("ipfs://", "https://ipfs.io/ipfs/");
+ }
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch metadata: ${response.status}`);
+ }
+
+ const jsonData = await response.json();
+ return jsonData;
+ },
+ enabled: !!uri,
+ });
+};
diff --git a/src/features/rewards/store/useClaimNftStore.ts b/src/features/rewards/store/useClaimNftStore.ts
new file mode 100644
index 00000000..07294a26
--- /dev/null
+++ b/src/features/rewards/store/useClaimNftStore.ts
@@ -0,0 +1,32 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+
+export type ClaimNftState = {
+ /** Whether or not we are showing the claim modal */
+ showClaimNftModal: boolean;
+ setShowClaimNftModal: (show: boolean) => void;
+ isClaimLoading: boolean;
+ setIsClaimLoading: (isClaimLoading: boolean) => void;
+}
+
+const defaultState = {
+ showClaimNftModal: false,
+ isClaimLoading: false,
+}
+
+export const useClaimNftStore = create()(
+ persist(
+ (set) => ({
+ ...defaultState,
+ setShowClaimNftModal(show: boolean) {
+ set({ showClaimNftModal: show });
+ },
+ setIsClaimLoading(isClaimLoading: boolean) {
+ set({ isClaimLoading });
+ },
+ }),
+ {
+ name: "claimNftStore",
+ },
+ ),
+);
\ No newline at end of file
diff --git a/src/features/structure/Header.tsx b/src/features/structure/Header.tsx
index 293b13b1..9baf2aef 100644
--- a/src/features/structure/Header.tsx
+++ b/src/features/structure/Header.tsx
@@ -3,6 +3,7 @@ import AirSwapLogoWithText from "../../assets/airswap-logo-with-text.svg";
import AirSwapLogo from "../../assets/airswap-logo.svg";
import WalletConnection from "../chain-connection/WalletConnection";
import { StakingButton } from "../staking/StakingButton";
+import { ClaimNftButton } from "../rewards/ClaimNftButton";
export const Header = ({}: {}) => {
const { isConnected } = useAccount();
@@ -24,8 +25,9 @@ export const Header = ({}: {}) => {
+ {isConnected && }
- {isConnected ? : null}
+ {isConnected && }
);
diff --git a/src/features/votes/VoteList.tsx b/src/features/votes/VoteList.tsx
index 650fa364..95f0705e 100644
--- a/src/features/votes/VoteList.tsx
+++ b/src/features/votes/VoteList.tsx
@@ -2,7 +2,7 @@ import { Fragment } from "react";
import { useAccount, useChainId } from "wagmi";
import { ActivatePointsCard } from "../activate-migration/ActivatePointsCard";
import { NON_MAINNET_START_TIMESTAMP } from "../activate-migration/constants";
-import { ClaimForm } from "../claims/ClaimForm";
+import { ClaimStakingRewardForm } from "../claims/ClaimSelectionForm";
import { ClaimModalSubheading } from "../claims/ClaimModalSubheading";
import { CustomTokensForm } from "../claims/CustomTokensForm";
import { Modal } from "../common/Modal";
@@ -134,9 +134,10 @@ export const VoteList = ({}: {}) => {
subHeading={}
className={`${showCustomTokensModal ? "hidden" : ""}`}
>
-
+
)}
+
{showCustomTokensModal && (
setShowCustomTokensModal(false)}