diff --git a/app/components/connection/connectModal.tsx b/app/components/connection/connectModal.tsx index 2e3d019..168c53b 100644 --- a/app/components/connection/connectModal.tsx +++ b/app/components/connection/connectModal.tsx @@ -71,7 +71,7 @@ const ConnectModal: FunctionComponent = ({ onClick={connectEvm} icon={} width={300} - variation="default-overlay" + variation="default" > EVM wallet @@ -79,7 +79,7 @@ const ConnectModal: FunctionComponent = ({ onClick={() => setOpenStarknetModal(true)} icon={} width={300} - variation="default-overlay" + variation="default" > Starknet wallet diff --git a/app/components/iconComponents/closeIcon.tsx b/app/components/iconComponents/closeIcon.tsx new file mode 100644 index 0000000..845c30a --- /dev/null +++ b/app/components/iconComponents/closeIcon.tsx @@ -0,0 +1,21 @@ +import { IconProps } from "@/constants/types"; +import React, { FunctionComponent } from "react"; + +const CloseIcon: FunctionComponent = ({ color, width }) => { + return ( + + + + ); +}; + +export default CloseIcon; diff --git a/app/components/notification.tsx b/app/components/notification.tsx new file mode 100644 index 0000000..cd32ef9 --- /dev/null +++ b/app/components/notification.tsx @@ -0,0 +1,36 @@ +"use client"; + +import React, { FunctionComponent, ReactNode } from "react"; +import { Snackbar } from "@mui/material"; +import styles from "../styles/components/notification.module.css"; +import CloseIcon from "./iconComponents/closeIcon"; + +type NotificationProps = { + children: ReactNode; + onClose?: () => void; + severity?: "error" | "warning" | "info" | "success"; + visible?: boolean; +}; + +const Notification: FunctionComponent = ({ + children, + onClose, + severity = "info", + visible = false, +}) => { + return ( + +
+
{children}
+
+ +
+
+
+ ); +}; + +export default Notification; diff --git a/app/components/recoverTokenModal.tsx b/app/components/recoverTokenModal.tsx new file mode 100644 index 0000000..8c56e3a --- /dev/null +++ b/app/components/recoverTokenModal.tsx @@ -0,0 +1,122 @@ +"use client"; + +import React, { FunctionComponent, useState } from "react"; +import { Modal } from "@mui/material"; +import styles from "../styles/components/welcomeModal.module.css"; +import modalStyles from "../styles/components/modal.module.css"; +import Button from "./button"; +import { NetworkType } from "@/constants/types"; +import WalletIcon from "./iconComponents/walletIcon"; +import { + getArgentIcon, + getArgentWebsite, + getBraavosIcon, + getBraavosWebsite, +} from "@/utils/starknetConnectorsWrapper"; +import { storeEthSig } from "@/services/localStorageService"; +import { useSignMessage } from "wagmi"; + +type RecoverTokenModalProps = { + closeModal: () => void; + open: boolean; + addr?: string; +}; + +const RecoverTokenModal: FunctionComponent = ({ + closeModal, + open, + addr, +}) => { + const [step, setStep] = useState(0); + const { signMessageAsync } = useSignMessage(); + + const generateSignature = async () => { + if (!addr) return; + try { + const signature = await signMessageAsync({ + message: `I press the Ethereum button with my address`, + }); + storeEthSig(signature, addr); + setStep(1); + } catch (error) { + console.error("Error while signing message:", error); + } + }; + const modalDescription = + step === 0 ? ( + <> + You are eligible to play again with a starknet wallet, but first you'll + need to regenerate a signature with your ethereum wallet. + + ) : ( + <> + You can now install one of our partner wallets to get your additional + click! + + ); + + return ( + +
+
+
+
+ No clicks left ! +
+ +
{modalDescription}
+ + {step == 0 ? ( +
+ +
+ ) : ( +
+ + +
+ )} + +
+ Close +
+
+
+
+
+ ); +}; + +export default RecoverTokenModal; diff --git a/app/components/welcomeModal.tsx b/app/components/welcomeModal.tsx index 0d43867..2123410 100644 --- a/app/components/welcomeModal.tsx +++ b/app/components/welcomeModal.tsx @@ -5,7 +5,7 @@ import { Modal } from "@mui/material"; import styles from "../styles/components/welcomeModal.module.css"; import modalStyles from "../styles/components/modal.module.css"; import Button from "./button"; -import { NetworkType, RemainingClicks } from "@/constants/types"; +import { EthToken, NetworkType, RemainingClicks } from "@/constants/types"; import WalletIcon from "./iconComponents/walletIcon"; import { numberToWords } from "@/utils/stringService"; import { getTotalClicks } from "@/utils/dataService"; @@ -18,7 +18,7 @@ type WelcomeModalProps = { addrOrName?: string; openWalletModal?: () => void; hasEthTokens: boolean; - ethTokens?: number; + ethTokens?: EthToken[]; }; const WelcomeModal: FunctionComponent = ({ @@ -31,9 +31,8 @@ const WelcomeModal: FunctionComponent = ({ hasEthTokens, ethTokens, }) => { - const totalClicks = getTotalClicks(remainingClicks) + (ethTokens ?? 0); + const totalClicks = getTotalClicks(remainingClicks, network, ethTokens); const isWhitelisted = remainingClicks.whitelisted; - console.log("totalClicks", totalClicks); const btnIcon = network === NetworkType.starknet ? ( diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..a32dc0d 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/page.tsx b/app/page.tsx index 736ec76..f8c9c6f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -22,11 +22,13 @@ import { getNonBlacklistedDomain, getTotalClicks, isOver5mn, + needToRecoverToken, } from "@/utils/dataService"; import WelcomeModal from "./components/welcomeModal"; import { getOutsideExecution } from "@/services/clickService"; import { getTypedData } from "@/utils/callData/typedData"; import { + altStarknetNewAccount, ethResetButton, resetTimer, starknetDomainResetButton, @@ -38,13 +40,19 @@ import { Signature, TypedData } from "starknet"; import { addEthToken, clearEthTokens, + getEthSig, storeVirtualTxId, } from "@/services/localStorageService"; import canPlayOnStarknet from "@/hooks/canPlayOnStarknet"; import isStarknetDeployed from "@/hooks/isDeployed"; import TryAgainModal from "./components/tryAgainModal"; +import Notification from "./components/notification"; +import { hexToDecimal } from "@/utils/feltService"; +import getPriceValue from "@/hooks/getEthQuote"; +import RecoverTokenModal from "./components/recoverTokenModal"; export default function Home() { + const [isLoaded, setIsLoaded] = useState(false); // Starknet hooks const { disconnectAsync } = useDisconnect(); const { account: starknetAccount } = useAccount(); @@ -62,7 +70,7 @@ export default function Home() { const [openConnectModal, setOpenConnectModal] = useState(false); const [welcomeModal, setWelcomeModal] = useState(false); const [tryAgainModal, setTryAgainModal] = useState(false); - // const [recoverTokenModal, setRecoverTokenModal] = useState(false); // todo + const [recoverTokenModal, setRecoverTokenModal] = useState(false); const [isConnected, setIsConnected] = useState(false); const [network, setNetwork] = useState(); const address = @@ -72,8 +80,10 @@ export default function Home() { const [totalClicks, setTotalClicks] = useState(0); const [totalPlayers, setTotalPlayers] = useState(0); const [trackingList, setTrackingList] = useState([]); + const [showNotif, setShowNotif] = useState(false); - const remainingClicks = getRemainingClicks(network, address); + const priceValue = getPriceValue(); + const { isFirstLoad, remainingClicks } = getRemainingClicks(network, address); const { hasEthTokens, ethTokens } = canPlayOnStarknet(network); const deploymentData = isStarknetDeployed(network, address); @@ -81,7 +91,8 @@ export default function Home() { // console.log("isFinished", isFinished, countdownTimestamp); // console.log("deploymentData", deploymentData); - console.log("remainingClicks", remainingClicks); + // console.log("remainingClicks", remainingClicks); + // console.log("isConnected", isConnected); // console.log("hasEthTokens", hasEthTokens, ethTokens); useEffect(() => { @@ -92,6 +103,20 @@ export default function Home() { } }, [evmConnected]); + useEffect(() => { + if (network && isConnected && !isFirstLoad) { + console.log("Show modal welcome on first load"); + if ( + network === NetworkType.evm && + needToRecoverToken(remainingClicks, ethTokens) + ) { + setRecoverTokenModal(true); + } else { + setWelcomeModal(true); + } + } + }, [isFirstLoad]); + useEffect(() => { const ws = new WebSocket(process.env.NEXT_PUBLIC_WEBSOCKET_URL as string); @@ -105,6 +130,7 @@ export default function Home() { setCurrentWinner(data.last_reset_info[1]); setTotalClicks(parseInt(data.click_counter)); setTotalPlayers(parseInt(data.players)); + if (!isLoaded) setIsLoaded(true); }; ws.onclose = () => { @@ -124,23 +150,35 @@ export default function Home() { if (trackingList && trackingList.length > 0) { trackingList.forEach((txId: string) => { trackId(txId).then((res) => { - console.log("res", res); - // todo: if sent and tx hash > notification with link - // + "Youstill have X chances to press the button" - // res = sent, timestamp_added, tx_hash, winner_addr + const decimalAddr = hexToDecimal( + starknetAccount + ? (starknetAccount?.address as string) + : (evmAddress as string) + ); + if (hexToDecimal(res.winner_addr) !== decimalAddr) { + if (getTotalClicks(remainingClicks, network, ethTokens) > 0) { + // user has some remaining clicks with this wallet + setShowNotif(true); + } else { + // user has no more clicks on this wallet but needs to recover a click token + if (needToRecoverToken(remainingClicks, ethTokens)) + setRecoverTokenModal(true); + // show try again modal + else setTryAgainModal(true); + } + // remove txId from trackingList + setTrackingList((prev) => prev.filter((id) => id !== txId)); + } }); }); - console.log("trackingList", trackingList); } - }, [trackingList]); + }, [trackingList, currentWinner]); const onWalletConnected = (network: NetworkType) => { + console.log("onWalletConnected", network); setNetwork(network); setOpenConnectModal(false); setIsConnected(true); - setTimeout(() => { - setWelcomeModal(true); - }, 500); }; const openWalletModal = () => { @@ -242,6 +280,7 @@ export default function Home() { availableDomain ); storeVirtualTxId(virtualTxId.virtual_tx_id); + setTrackingList([...trackingList, virtualTxId.virtual_tx_id]); } catch (error) { console.error("Error during starknet domain reset:", error); } @@ -266,11 +305,36 @@ export default function Home() { console.log("virtualTxId", virtualTxId); storeVirtualTxId(virtualTxId.virtual_tx_id); clearEthTokens(); + setTrackingList([...trackingList, virtualTxId.virtual_tx_id]); } catch (error) { console.error("Error during eth reset from starknet:", error); } } else { - setTryAgainModal(true); + const ethSig = getEthSig(); + if (ethSig) { + // we have eth signature in local storage, so we can call alt_starknet_new_account + // with eth_addr and signature + try { + const signature = await starknetAccount?.signMessage(typedData); + const virtualTxId = await altStarknetNewAccount( + starknetAccount?.address as string, + signature as Signature, + ethSig.eth_addr, + ethSig.sig, + nonce, + executeBefore, + deploymentData + ); + console.log("virtualTxId", virtualTxId); + storeVirtualTxId(virtualTxId.virtual_tx_id); + clearEthTokens(); + setTrackingList([...trackingList, virtualTxId.virtual_tx_id]); + } catch (error) { + console.error("Error during alt starknet new reset:", error); + } + } else { + setTryAgainModal(true); + } } }; @@ -304,6 +368,7 @@ export default function Home() { token: res.token, eth_addr: res.eth_addr, }); + setTrackingList([...trackingList, res.virtual_tx_id]); } catch (error) { console.error("Error during eth reset:", error); } @@ -312,13 +377,14 @@ export default function Home() { remainingClicks.eligibilityAmt === 0 && !remainingClicks.evmBlacklisted ) { - console.log("he can play on starknet"); + console.log("User can play on starknet"); if (!hasEthTokens) { - console.log("he has no tokens in local storage"); - // setRecoverTokenModal(true); - //todo: create recoverTokenModal + console.log( + "User has no tokens in local storage but his eth addr is not blacklisted" + ); + setRecoverTokenModal(true); } else { - console.log("he has tokens in local storage"); + console.log("User has tokens in local storage"); setTryAgainModal(true); } } else { @@ -351,7 +417,7 @@ export default function Home() { <>
- {!isFinished ? ( + {!isLoaded || !isFinished ? ( <> - + {priceValue ? ( + + ) : null} ) : ( <> @@ -440,7 +508,7 @@ export default function Home() {

- {!isFinished ? ( + {!isLoaded || !isFinished ? ( <> WIN Five{" "} ETH ! @@ -463,10 +531,14 @@ export default function Home() {

@@ -483,7 +555,7 @@ export default function Home() { addrOrName={getConnectionBtnText()} openWalletModal={openWalletModal} hasEthTokens={hasEthTokens} - ethTokens={ethTokens.length} + ethTokens={ethTokens} /> setTryAgainModal(false)} @@ -492,6 +564,18 @@ export default function Home() { hasEthTokens={hasEthTokens} openWalletModal={openWalletModal} /> + setRecoverTokenModal(false)} + open={recoverTokenModal} + addr={evmAddress} + /> + setShowNotif(false)}> + <> + Try again! You still have{" "} + {getTotalClicks(remainingClicks, network, ethTokens)} chance to press + the button. + + ); } diff --git a/app/provider.tsx b/app/provider.tsx index fe4b832..100ec63 100644 --- a/app/provider.tsx +++ b/app/provider.tsx @@ -13,7 +13,6 @@ import { getDefaultConfig, RainbowKitProvider } from "@rainbow-me/rainbowkit"; import { WagmiProvider } from "wagmi"; import { mainnet as EthMainnet, sepolia as EthSepolia } from "wagmi/chains"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; -import { NotificationProvider } from "@/context/NotificationProvider"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function Providers({ children }: any) { @@ -44,9 +43,7 @@ export function Providers({ children }: any) { > - - {children} - + {children} diff --git a/app/styles/components/button.module.css b/app/styles/components/button.module.css index 530b49e..bd38fdf 100644 --- a/app/styles/components/button.module.css +++ b/app/styles/components/button.module.css @@ -19,12 +19,7 @@ color: var(--content); } -.default-overlay { - border-image: url("/visuals/button.svg") 18 fill stretch; - color: var(--content); -} - -.default-overlay:hover { +.default:hover { border-image: url("/visuals/buttonColored.svg") 18 fill stretch; color: var(--content); } diff --git a/app/styles/components/notification.module.css b/app/styles/components/notification.module.css new file mode 100644 index 0000000..e8e4998 --- /dev/null +++ b/app/styles/components/notification.module.css @@ -0,0 +1,23 @@ +.info { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 16px; + background-color: var(--content-dark); + color: var(--content); + + font-size: 15px; + font-weight: 500; + line-height: 20px; + text-align: left; + + padding: 16px 24px; + border-radius: 4px; +} + +@media (max-width: 768px) { + .info { + max-width: 300px; + } +} diff --git a/context/NotificationProvider.tsx b/context/NotificationProvider.tsx deleted file mode 100644 index e756ef4..0000000 --- a/context/NotificationProvider.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { createContext, useState, useContext, ReactNode } from "react"; -import { Snackbar, SnackbarContent, IconButton, styled } from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; -import InfoIcon from "@mui/icons-material/Info"; - -const NotificationContentRoot = styled(SnackbarContent)(() => ({ - backgroundColor: "black", -})); - -const IconButtonRoot = styled(IconButton)(({ theme }) => ({ - padding: theme.spacing(0.5), -})); - -interface NotificationProps { - type: "success" | "error" | "info" | "warning"; - message: string; - open: boolean; - onClose: () => void; - autoHideDuration?: number; -} - -interface NotificationContextProps { - showNotification: (message: string, type: NotificationProps["type"]) => void; - hideNotification: () => void; -} - -const NotificationContext = createContext( - undefined -); - -export const NotificationProvider: React.FC<{ children: ReactNode }> = ({ - children, -}) => { - const [notification, setNotification] = useState({ - type: "info", - message: "", - open: false, - onClose: () => hideNotification(), - autoHideDuration: 6000, - }); - - const showNotification = ( - message: string, - type: NotificationProps["type"] - ) => { - setNotification({ - message, - type, - open: true, - onClose: () => hideNotification(), - autoHideDuration: 6000, - }); - }; - - const hideNotification = () => { - setNotification((prevNotification) => ({ - ...prevNotification, - open: false, - })); - }; - - const renderIcon = () => { - // switch (notification.type) { - // case "success": - // return ; - // case "error": - // return ; - // case "info": - // return ; - // default: - // return ; - // } - }; - - return ( - - {children} - - - {/* {renderIcon()} */} - {notification.message} - - } - action={[ - - - , - ]} - /> - - - ); -}; - -export const useNotification = (): NotificationContextProps => { - const context = useContext(NotificationContext); - if (!context) { - throw new Error( - "useNotification must be used within a NotificationProvider" - ); - } - return context; -}; - -// Sample function for the notification context -// const { showNotification} = useNotification(); - -// const handleClick = () => { -// showNotification('This is a success message!', 'success'); -// console.log('Notification opened') -// }; diff --git a/hooks/getEthQuote.tsx b/hooks/getEthQuote.tsx new file mode 100644 index 0000000..1a874b3 --- /dev/null +++ b/hooks/getEthQuote.tsx @@ -0,0 +1,22 @@ +import { useEffect, useState } from "react"; + +export default function getPriceValue() { + const [price, setPrice] = useState(); + + useEffect(() => { + fetch( + "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" + ) + .then((res) => res.json()) + .then((data) => { + console.log("Coingecko API Data:", data); + const total = parseInt( + (parseFloat(data?.ethereum?.usd) * 5).toFixed(0) + ); + setPrice(`$${total.toLocaleString("en-US")}`); + }) + .catch((err) => console.log("Coingecko API Error:", err)); + }, []); + + return price; +} diff --git a/hooks/getRemainingClicks.tsx b/hooks/getRemainingClicks.tsx index 912ef29..04b4351 100644 --- a/hooks/getRemainingClicks.tsx +++ b/hooks/getRemainingClicks.tsx @@ -14,10 +14,12 @@ export default function getRemainingClicks( const [remainingClicks, setRemainingClicks] = useState({ whitelisted: false, }); + const [isFirstLoad, setIsFirstLoad] = useState(true); useEffect(() => { if (!address || !network) { setRemainingClicks({ whitelisted: false }); + setIsFirstLoad(true); return; } @@ -50,12 +52,14 @@ export default function getRemainingClicks( domainStatus, whitelisted, }); + setIsFirstLoad(false); } catch (error) { console.log("Error while fetching starknet data", error); setRemainingClicks({ eligibilityAmt: 0, whitelisted: false, }); + setIsFirstLoad(false); } }; @@ -67,10 +71,12 @@ export default function getRemainingClicks( whitelisted: eligibilityAmt.whitelisted, evmBlacklisted: eligibilityAmt.blacklisted ?? false, }); + setIsFirstLoad(false); return; } catch (error) { console.log("Error while fetching ethereum eligibility", error); setRemainingClicks({ eligibilityAmt: 0, whitelisted: false }); + setIsFirstLoad(false); return; } }; @@ -91,5 +97,5 @@ export default function getRemainingClicks( }; }, [network, address]); - return remainingClicks; + return { isFirstLoad, remainingClicks }; } diff --git a/package-lock.json b/package-lock.json index 58816ca..890e229 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@starknet-react/chains": "^0.1.7", "@starknet-react/core": "^2.8.2", "@types/ws": "^8.5.10", + "bn.js": "^5.2.1", "ethers": "^6.12.1", "next": "14.2.3", "react": "^18", @@ -26,6 +27,7 @@ "wagmi": "^2.9.7" }, "devDependencies": { + "@types/bn.js": "^5.1.5", "@types/jest": "^29.5.12", "@types/node": "^20", "@types/react": "^18", @@ -3250,6 +3252,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bn.js": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", + "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", diff --git a/package.json b/package.json index 9906bdc..2fcf7f8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@starknet-react/chains": "^0.1.7", "@starknet-react/core": "^2.8.2", "@types/ws": "^8.5.10", + "bn.js": "^5.2.1", "ethers": "^6.12.1", "next": "14.2.3", "react": "^18", @@ -28,6 +29,7 @@ "wagmi": "^2.9.7" }, "devDependencies": { + "@types/bn.js": "^5.1.5", "@types/jest": "^29.5.12", "@types/node": "^20", "@types/react": "^18", diff --git a/public/visuals/socials.png b/public/visuals/socials.png new file mode 100644 index 0000000..d87144a Binary files /dev/null and b/public/visuals/socials.png differ diff --git a/services/apiService.ts b/services/apiService.ts index 264303d..003917c 100644 --- a/services/apiService.ts +++ b/services/apiService.ts @@ -144,6 +144,39 @@ export const starknetResetButtonFromEth = async ( } }; +export const altStarknetNewAccount = async ( + starknet_addr: string, + sig: Signature, + eth_addr: string, + eth_sig: String, + nonce: number, + executeBefore: number, + deploymentData?: GetDeploymentDataResult +) => { + try { + const response = await fetch(`${baseurl}/alt_starknet_new_account`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + eth_addr, + starknet_addr, + eth_sig, + sig: stark.signatureToHexArray(sig), + nonce, + execute_before: executeBefore, + class_hash: deploymentData?.class_hash, + salt: deploymentData?.salt, + deployment_calldata: deploymentData?.calldata, + }), + }); + return await response.json(); + } catch (err) { + console.log("Error while calling starknet_reset_button_from_eth", err); + } +}; + // Ethereum related functions export const getEthEligibility = async (address: string) => { try { diff --git a/services/localStorageService.ts b/services/localStorageService.ts index 4431e2b..3835502 100644 --- a/services/localStorageService.ts +++ b/services/localStorageService.ts @@ -98,3 +98,30 @@ export const clearEthTokens = () => { console.error("Error clearing Eth tokens from local storage:", error); } }; + +export const storeEthSig = (ethAddr: string, sig: string): void => { + if (typeof ethAddr !== "string" || typeof sig !== "string") { + throw new Error("Both ethAddr and sig must be strings"); + } + + const ethSigObject = { eth_addr: ethAddr, sig: sig }; + localStorage.setItem("ethbutton-ethSig", JSON.stringify(ethSigObject)); +}; + +export const getEthSig = (): { eth_addr: string; sig: string } | null => { + const ethSigString = localStorage.getItem("ethbutton-ethSig"); + if (!ethSigString) { + return null; + } + + try { + return JSON.parse(ethSigString); + } catch (error) { + console.error("Error parsing JSON from local storage", error); + return null; + } +}; + +export const clearEthSig = (): void => { + localStorage.removeItem("ethbutton-ethSig"); +}; diff --git a/utils/dataService.ts b/utils/dataService.ts index 8569e10..300458a 100644 --- a/utils/dataService.ts +++ b/utils/dataService.ts @@ -1,7 +1,17 @@ -import { RemainingClicks } from "@/constants/types"; +import { EthToken, NetworkType, RemainingClicks } from "@/constants/types"; -export const getTotalClicks = (remaining: RemainingClicks): number => { - return (remaining?.eligibilityAmt ?? 0) + (remaining?.domainClicks ?? 0); +export const getTotalClicks = ( + remaining: RemainingClicks, + network?: NetworkType, + ethTokens?: EthToken[] +): number => { + const tokensRelatedClicks = + network === NetworkType.evm ? 0 : ethTokens?.length ?? 0; + return ( + (remaining?.eligibilityAmt ?? 0) + + (remaining?.domainClicks ?? 0) + + tokensRelatedClicks + ); }; export const getNonBlacklistedDomain = ( @@ -24,3 +34,16 @@ export function isOver5mn(timestamp: number) { return currentTime - timestamp > fiveMinutesInMilliseconds; } + +export function needToRecoverToken( + remainingClicks: RemainingClicks, + ethTokens: EthToken[] +): boolean { + if ( + remainingClicks.eligibilityAmt === 0 && + !remainingClicks.evmBlacklisted && + (!ethTokens || ethTokens.length === 0) + ) + return true; + else return false; +} diff --git a/utils/feltService.ts b/utils/feltService.ts index 39d885a..fe7da22 100644 --- a/utils/feltService.ts +++ b/utils/feltService.ts @@ -1,3 +1,6 @@ +import BN from "bn.js"; +import { isHexString } from "./stringService"; + export const UINT_128_MAX = (BigInt(1) << BigInt(128)) - BigInt(1); export function toUint256(n: string): { low: string; high: string } { @@ -7,3 +10,12 @@ export function toUint256(n: string): { low: string; high: string } { high: (b >> BigInt(128)).toString(), }; } + +export function hexToDecimal(hex: string | undefined): string { + if (hex === undefined) return ""; + else if (!isHexString(hex)) { + throw new Error("Invalid hex string"); + } + + return new BN(hex.slice(2), 16).toString(10); +} diff --git a/utils/stringService.ts b/utils/stringService.ts index 3111299..f77bf88 100644 --- a/utils/stringService.ts +++ b/utils/stringService.ts @@ -30,3 +30,8 @@ export function numberToWords(num: number): string { ]; return numWords[num] || num.toString(); } + +export function isHexString(str: string): boolean { + if (str === "") return true; + return /^0x[0123456789abcdefABCDEF]+$/.test(str); +}