diff --git a/components/UI/modalProfilePic.tsx b/components/UI/modalProfilePic.tsx index dc393dae..f82c4322 100644 --- a/components/UI/modalProfilePic.tsx +++ b/components/UI/modalProfilePic.tsx @@ -7,7 +7,7 @@ import ClickableAction from "./iconsComponents/clickableAction"; import theme from "../../styles/theme"; import DoneFilledIcon from "./iconsComponents/icons/doneFilledIcon"; import ArrowLeftIcon from "./iconsComponents/icons/arrowLeftIcon"; -import { useContractWrite } from "@starknet-react/core"; +import { useSendTransaction } from "@starknet-react/core"; import { Call } from "starknet"; import identityChangeCalls from "../../utils/callData/identityChangeCalls"; import { hexToDecimal, toUint256 } from "../../utils/feltService"; @@ -32,7 +32,7 @@ const ModalProfilePic: FunctionComponent = ({ }) => { const [callData, setCallData] = useState([]); const { addTransaction } = useNotificationManager(); - const { writeAsync: execute, data: updateData } = useContractWrite({ + const { sendAsync: execute, data: updateData } = useSendTransaction({ calls: callData, }); diff --git a/components/discount/freeRegisterCheckout.tsx b/components/discount/freeRegisterCheckout.tsx index 353dc34e..42a4842a 100644 --- a/components/discount/freeRegisterCheckout.tsx +++ b/components/discount/freeRegisterCheckout.tsx @@ -24,7 +24,7 @@ import usePaymaster from "@/hooks/paymaster"; type FreeRegisterCheckoutProps = { domain: string; - duration: number; + durationInDays: number; goBack: () => void; couponCode?: boolean; couponHelper?: string; @@ -33,7 +33,7 @@ type FreeRegisterCheckoutProps = { const FreeRegisterCheckout: FunctionComponent = ({ domain, - duration, + durationInDays, goBack, couponCode, couponHelper, @@ -41,6 +41,7 @@ const FreeRegisterCheckout: FunctionComponent = ({ }) => { const [targetAddress, setTargetAddress] = useState(""); const [callData, setCallData] = useState([]); + const [loadingCallData, setLoadingCallData] = useState(true); const [salt, setSalt] = useState(); const encodedDomain = utils .encodeDomain(domain) @@ -67,6 +68,7 @@ const FreeRegisterCheckout: FunctionComponent = ({ loadingDeploymentData, refreshRewards, invalidTx, + txError, loadingTypedData, } = usePaymaster( callData, @@ -76,7 +78,7 @@ const FreeRegisterCheckout: FunctionComponent = ({ ); if (transactionHash) setTransactionHash(transactionHash); }, - !coupon + loadingCallData ); useEffect(() => { @@ -107,7 +109,7 @@ const FreeRegisterCheckout: FunctionComponent = ({ }, [salt]); useEffect(() => { - if (signature[0] === null) return; + if (!signature[0]) return; // Variables const newTokenId: number = Math.floor(Math.random() * 1000000000000); setTokenId(newTokenId); @@ -118,7 +120,8 @@ const FreeRegisterCheckout: FunctionComponent = ({ signature, txMetadataHash ); - return setCallData(freeRegisterCalls); + setCallData(freeRegisterCalls); + setLoadingCallData(false); }, [metadataHash, encodedDomain, signature]); function changeCoupon(value: string): void { @@ -146,17 +149,8 @@ const FreeRegisterCheckout: FunctionComponent = ({ useEffect(() => { if (!coupon) return setLoadingCoupon(false); - const lastSuccessCoupon = localStorage.getItem("lastSuccessCoupon"); - if (coupon === lastSuccessCoupon) { - setCouponError(""); - setLoadingCoupon(false); - const signature = JSON.parse( - localStorage.getItem("couponSignature") as string - ); - setSignature(signature); - return; - } if (!address) return; + setLoadingCallData(true); getFreeDomain(address, `${domain}.stark`, coupon).then((res) => { if (res.error) setCouponError( @@ -166,9 +160,6 @@ const FreeRegisterCheckout: FunctionComponent = ({ const signature = [res.r, res.s]; setSignature(signature); setCouponError(""); - // Write in local storage - localStorage.setItem("lastSuccessCoupon", coupon); - localStorage.setItem("couponSignature", JSON.stringify(signature)); } setLoadingCoupon(false); }); @@ -198,12 +189,18 @@ const FreeRegisterCheckout: FunctionComponent = ({
- + setTermsBox(!termsBox)} /> + {invalidTx && txError?.message ? ( +

{txError.message}

+ ) : null} {address ? (
diff --git a/components/discount/freeRegisterSummary.tsx b/components/discount/freeRegisterSummary.tsx index 0bfb3da3..e39cc50d 100644 --- a/components/discount/freeRegisterSummary.tsx +++ b/components/discount/freeRegisterSummary.tsx @@ -1,22 +1,22 @@ import React, { FunctionComponent } from "react"; import styles from "../../styles/components/registerV3.module.css"; -import { getYearlyPrice } from "@/utils/priceService"; +import { getDisplayablePrice, getYearlyPriceWei } from "@/utils/priceService"; import DoneIcon from "../UI/iconsComponents/icons/doneIcon"; import { useAccount } from "@starknet-react/core"; type FreeRegisterSummaryProps = { - duration: number; + durationInDays: number; domain: string; }; const FreeRegisterSummary: FunctionComponent = ({ domain, - duration, + durationInDays, }) => { const { address } = useAccount(); function getMessage() { - return `${Math.floor(duration / 30)} months of domain registration`; + return `${Math.floor(durationInDays / 30)} months of domain registration`; } return ( @@ -26,7 +26,9 @@ const FreeRegisterSummary: FunctionComponent = ({

{getMessage()}

- {getYearlyPrice(domain)} ETH + + {getDisplayablePrice(getYearlyPriceWei(domain))} ETH +    Free

diff --git a/components/discount/freeRenewalDiscount.tsx b/components/discount/freeRenewalCheckout.tsx similarity index 85% rename from components/discount/freeRenewalDiscount.tsx rename to components/discount/freeRenewalCheckout.tsx index 2bb6b29f..7c50ad38 100644 --- a/components/discount/freeRenewalDiscount.tsx +++ b/components/discount/freeRenewalCheckout.tsx @@ -1,17 +1,14 @@ import React from "react"; import { FunctionComponent, useEffect, useState } from "react"; import Button from "../UI/button"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { formatHexString, isValidEmail, selectedDomainsToArray, selectedDomainsToEncodedArray, } from "../../utils/stringService"; -import { - applyRateToBigInt, - numberToFixedString, -} from "../../utils/feltService"; +import { applyRateToBigInt } from "../../utils/feltService"; import { Call } from "starknet"; import styles from "../../styles/components/registerV2.module.css"; import TextField from "../UI/textField"; @@ -21,7 +18,8 @@ import RegisterSummary from "../domains/registerSummary"; import { computeMetadataHash, generateSalt } from "../../utils/userDataService"; import { areDomainSelected, - getTotalYearlyPrice, + getApprovalAmount, + getDisplayablePrice, } from "../../utils/priceService"; import RenewalDomainsBox from "../domains/renewalDomainsBox"; import registrationCalls from "../../utils/callData/registrationCalls"; @@ -45,8 +43,8 @@ import ConnectButton from "../UI/connectButton"; import { getAutoRenewAllowance, getDomainPrice, - getDomainPriceAltcoin, getTokenQuote, + getTotalYearlyPrice, } from "../../utils/altcoinService"; import { areArraysEqual } from "@/utils/arrayService"; import useNeedSubscription from "@/hooks/useNeedSubscription"; @@ -54,29 +52,21 @@ import useNeedSubscription from "@/hooks/useNeedSubscription"; type FreeRenewalCheckoutProps = { groups: string[]; goBack: () => void; - duration: number; - discountId: string; - customMessage: string; - priceInEth: string; - renewPrice: string; + offer: Discount; }; const FreeRenewalCheckout: FunctionComponent = ({ groups, - priceInEth, - renewPrice, - duration, - discountId, - customMessage, + offer, goBack, }) => { const [email, setEmail] = useState(""); const [emailError, setEmailError] = useState(true); const [isSwissResident, setIsSwissResident] = useState(false); const [salesTaxRate, setSalesTaxRate] = useState(0); - const [salesTaxAmount, setSalesTaxAmount] = useState("0"); + const [salesTaxAmount, setSalesTaxAmount] = useState(BigInt(0)); const [callData, setCallData] = useState([]); - const [price, setPrice] = useState(priceInEth); + // price paid by the user including discount const [quoteData, setQuoteData] = useState(null); // null if in ETH const [displayedCurrencies, setDisplayedCurrencies] = useState< CurrencyType[] @@ -86,11 +76,14 @@ const FreeRenewalCheckout: FunctionComponent = ({ const [salt, setSalt] = useState(); const [metadataHash, setMetadataHash] = useState(); const [needMetadata, setNeedMetadata] = useState(false); - const [potentialPrice, setPotentialPrice] = useState("0"); + const [potentialPrice, setPotentialPrice] = useState(BigInt(0)); + const [customCheckoutMessage, setCustomCheckoutMessage] = + useState(""); + const [selectedDomains, setSelectedDomains] = useState>(); const { address } = useAccount(); - const { writeAsync: execute, data: renewData } = useContractWrite({ + const { sendAsync: execute, data: renewData } = useSendTransaction({ calls: callData, }); const [domainsMinting, setDomainsMinting] = @@ -98,13 +91,9 @@ const FreeRenewalCheckout: FunctionComponent = ({ const { addTransaction } = useNotificationManager(); const router = useRouter(); const [loadingPrice, setLoadingPrice] = useState(false); - const needsAllowances = useNeedsAllowances(address); + const allowanceStatus = useNeedsAllowances(address); const needSubscription = useNeedSubscription(address); - useEffect(() => { - setPotentialPrice(getTotalYearlyPrice(selectedDomains)); - }, [selectedDomains, setPotentialPrice]); - useEffect(() => { if (!renewData?.transaction_hash || !salt || !metadataHash) return; @@ -193,7 +182,7 @@ const FreeRenewalCheckout: FunctionComponent = ({ // Start the refetch scheduling scheduleRefetch(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displayedCurrencies, price]); // We don't add quoteData because it would create an infinite loop + }, [displayedCurrencies]); // We don't add quoteData because it would create an infinite loop // on first load, we generate a salt useEffect(() => { @@ -245,12 +234,12 @@ const FreeRenewalCheckout: FunctionComponent = ({ useEffect(() => { if (isSwissResident) { setSalesTaxRate(swissVatRate); - setSalesTaxAmount(applyRateToBigInt(price, swissVatRate)); + setSalesTaxAmount(applyRateToBigInt(potentialPrice, swissVatRate)); } else { setSalesTaxRate(0); - setSalesTaxAmount(""); + setSalesTaxAmount(BigInt(0)); } - }, [isSwissResident, price]); + }, [isSwissResident, potentialPrice]); // build free renewal call useEffect(() => { @@ -271,14 +260,19 @@ const FreeRenewalCheckout: FunctionComponent = ({ displayedCurrencies.map((currency) => { // Add ERC20 allowance for all currencies if needed - if (needsAllowances[currency]) { - const priceToApprove = - currency === CurrencyType.ETH ? priceInEth : price; + if (allowanceStatus[currency].needsAllowance) { + const amountToApprove = getApprovalAmount( + potentialPrice, + salesTaxAmount, + 1, + allowanceStatus[currency].currentAllowance + ); + calls.unshift( autoRenewalCalls.approve( ERC20Contract[currency], AutoRenewalContracts[currency], - priceToApprove + amountToApprove ) ); } @@ -295,6 +289,7 @@ const FreeRenewalCheckout: FunctionComponent = ({ const domainPrice = getDomainPrice( domain, currency, + 365, quoteData?.quote ); const allowance = getAutoRenewAllowance( @@ -307,7 +302,7 @@ const FreeRenewalCheckout: FunctionComponent = ({ autoRenewalCalls.enableRenewal( AutoRenewalContracts[currency], encodedDomain, - allowance, + allowance, // Warning review: Do we need to multiply by 10 here to have 10 years ? txMetadataHash ) ); @@ -318,34 +313,43 @@ const FreeRenewalCheckout: FunctionComponent = ({ } }, [ selectedDomains, - price, salesTaxAmount, - needsAllowances, + allowanceStatus, metadataHash, salesTaxRate, - duration, - discountId, needMetadata, quoteData, displayedCurrencies, needSubscription, - priceInEth, + potentialPrice, ]); useEffect(() => { - const isCurrencyETH = areArraysEqual(displayedCurrencies, [ - CurrencyType.ETH, + const isCurrencySTRK = areArraysEqual(displayedCurrencies, [ + CurrencyType.STRK, ]); - - if (isCurrencyETH) { - setPrice(priceInEth); - setLoadingPrice(false); - } else if (quoteData) { - const priceInAltcoin = getDomainPriceAltcoin(quoteData.quote, priceInEth); - setPrice(priceInAltcoin); - setLoadingPrice(false); - } - }, [priceInEth, quoteData, displayedCurrencies]); + const currencyDisplayed = isCurrencySTRK + ? CurrencyType.STRK + : CurrencyType.ETH; + const totalYearlyPrice = getTotalYearlyPrice( + selectedDomains, + currencyDisplayed, + quoteData?.quote + ); + const potentialCurrency = quoteData?.quote + ? currencyDisplayed + : CurrencyType.ETH; + + // Setting the potential price in the displayed currency + setPotentialPrice(totalYearlyPrice); + setCustomCheckoutMessage( + `${offer.customMessage} and then ${getDisplayablePrice( + totalYearlyPrice + )} ${potentialCurrency} per year` + ); + + setLoadingPrice(false); + }, [quoteData, displayedCurrencies, selectedDomains, offer.customMessage]); const onCurrencySwitch = (currency: CurrencyType[]) => { setDisplayedCurrencies(currency as CurrencyType[]); @@ -391,23 +395,23 @@ const FreeRenewalCheckout: FunctionComponent = ({
setTermsBox(!termsBox)} termsBox={termsBox} isArOnforced={true} - ethRenewalPrice={renewPrice} />
{address ? ( diff --git a/components/discount/registerDiscount.tsx b/components/discount/registerDiscount.tsx index 2406e90a..2c4fc95c 100644 --- a/components/discount/registerDiscount.tsx +++ b/components/discount/registerDiscount.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FunctionComponent, useEffect, useState } from "react"; import Button from "../UI/button"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { utils } from "starknetid.js"; import { formatHexString, @@ -39,16 +39,16 @@ import { getDomainPriceAltcoin, getTokenQuote, } from "../../utils/altcoinService"; -import { getPriceFromDomain } from "@/utils/priceService"; +import { getDomainPriceWei } from "@/utils/priceService"; import { useRouter } from "next/router"; import { formatDomainData } from "@/utils/cacheDomainData"; type RegisterDiscountProps = { domain: string; - duration: number; + durationInDays: number; discountId: string; customMessage: string; - priceInEth: string; + priceInEth: bigint; mailGroups: string[]; goBack: () => void; sponsor?: string; @@ -56,7 +56,7 @@ type RegisterDiscountProps = { const RegisterDiscount: FunctionComponent = ({ domain, - duration, + durationInDays, discountId, customMessage, priceInEth, @@ -70,9 +70,9 @@ const RegisterDiscount: FunctionComponent = ({ const [emailError, setEmailError] = useState(true); const [isSwissResident, setIsSwissResident] = useState(false); const [salesTaxRate, setSalesTaxRate] = useState(0); - const [salesTaxAmount, setSalesTaxAmount] = useState("0"); + const [salesTaxAmount, setSalesTaxAmount] = useState(BigInt(0)); const [callData, setCallData] = useState([]); - const [price, setPrice] = useState(priceInEth); // set to priceInEth at initialization and updated to altcoin if selected by user + const [price, setPrice] = useState(priceInEth); // set to priceInEth at initialization and updated to altcoin if selected by user const [quoteData, setQuoteData] = useState(null); // null if in ETH const [displayedCurrency, setDisplayedCurrency] = useState( CurrencyType.ETH @@ -86,7 +86,7 @@ const RegisterDiscount: FunctionComponent = ({ const [renewalBox, setRenewalBox] = useState(false); const [metadataHash, setMetadataHash] = useState(); const { account, address } = useAccount(); - const { writeAsync: execute, data: registerData } = useContractWrite({ + const { sendAsync: execute, data: registerData } = useSendTransaction({ calls: callData, }); const hasMainDomain = !useDisplayName(address ?? "", false).startsWith("0x"); @@ -194,7 +194,7 @@ const RegisterDiscount: FunctionComponent = ({ encodedDomain, newTokenId, sponsor, - duration, + durationInDays, txMetadataHash, discountId ) @@ -205,7 +205,7 @@ const RegisterDiscount: FunctionComponent = ({ encodedDomain, newTokenId, sponsor, - duration, + durationInDays, txMetadataHash, ERC20Contract[displayedCurrency], quoteData as QuoteQueryData, @@ -231,16 +231,13 @@ const RegisterDiscount: FunctionComponent = ({ // If the user has toggled autorenewal if (renewalBox) { - const yearlyPriceInEth = getPriceFromDomain(1, domain); + const yearlyPriceInEth = getDomainPriceWei(365, domain); const allowance = getAutoRenewAllowance( displayedCurrency, salesTaxRate, displayedCurrency === CurrencyType.ETH - ? String(yearlyPriceInEth) - : String( - (yearlyPriceInEth * BigInt(quoteData?.quote ?? "0")) / - BigInt(1e18) - ) // Convert the yearly price in eth to the altcoin yearly price + ? yearlyPriceInEth + : (yearlyPriceInEth * BigInt(quoteData?.quote ?? "0")) / BigInt(1e18) ); if (needsAllowance) { @@ -248,7 +245,7 @@ const RegisterDiscount: FunctionComponent = ({ autoRenewalCalls.approve( ERC20Contract[displayedCurrency], AutoRenewalContracts[displayedCurrency], - allowance + allowance * BigInt(10) // Allow 10 years ) ); } @@ -267,7 +264,7 @@ const RegisterDiscount: FunctionComponent = ({ setTokenIdRedirect(String(newTokenId)); setCallData(calls); }, [ - duration, + durationInDays, targetAddress, price, domain, @@ -318,7 +315,7 @@ const RegisterDiscount: FunctionComponent = ({ tokenIdRedirect, formatHexString(address as string), getDomainWithStark(domain), - duration, + durationInDays, Boolean(!hasMainDomain), // isMainDomain undefined // Selected PFPs ); @@ -337,7 +334,7 @@ const RegisterDiscount: FunctionComponent = ({ setSalesTaxAmount(applyRateToBigInt(price, swissVatRate)); } else { setSalesTaxRate(0); - setSalesTaxAmount(""); + setSalesTaxAmount(BigInt(0)); } }, [isSwissResident, price]); @@ -383,9 +380,9 @@ const RegisterDiscount: FunctionComponent = ({
= ({ disabled={ (domainsMinting.get(encodedDomain) as boolean) || !account || - !duration || + !durationInDays || !targetAddress || invalidBalance || !termsBox || diff --git a/components/discount/renewalDiscount.tsx b/components/discount/renewalDiscount.tsx deleted file mode 100644 index 306e6645..00000000 --- a/components/discount/renewalDiscount.tsx +++ /dev/null @@ -1,474 +0,0 @@ -import React from "react"; -import { FunctionComponent, useEffect, useState } from "react"; -import Button from "../UI/button"; -import { useAccount, useContractWrite } from "@starknet-react/core"; -import { - formatHexString, - isValidEmail, - selectedDomainsToArray, - selectedDomainsToEncodedArray, -} from "../../utils/stringService"; -import { - applyRateToBigInt, - hexToDecimal, - numberToFixedString, -} from "../../utils/feltService"; -import { Call } from "starknet"; -import styles from "../../styles/components/registerV2.module.css"; -import TextField from "../UI/textField"; -import SwissForm from "../domains/swissForm"; -import { Divider } from "@mui/material"; -import RegisterSummary from "../domains/registerSummary"; -import { computeMetadataHash, generateSalt } from "../../utils/userDataService"; -import { areDomainSelected } from "../../utils/priceService"; -import RenewalDomainsBox from "../domains/renewalDomainsBox"; -import registrationCalls from "../../utils/callData/registrationCalls"; -import autoRenewalCalls from "../../utils/callData/autoRenewalCalls"; -import BackButton from "../UI/backButton"; -import { useRouter } from "next/router"; -import { useNotificationManager } from "../../hooks/useNotificationManager"; -import { - AutoRenewalContracts, - CurrencyType, - ERC20Contract, - NotificationType, - TransactionType, - swissVatRate, -} from "../../utils/constants"; -import RegisterCheckboxes from "../domains/registerCheckboxes"; -import { utils } from "starknetid.js"; -import RegisterConfirmationModal from "../UI/registerConfirmationModal"; -import useAllowanceCheck from "../../hooks/useAllowanceCheck"; -import ConnectButton from "../UI/connectButton"; -import useBalances from "../../hooks/useBalances"; -import { - getAutoRenewAllowance, - getDomainPrice, - getDomainPriceAltcoin, - getTokenQuote, - smartCurrencyChoosing, -} from "../../utils/altcoinService"; - -type RenewalDiscountProps = { - groups: string[]; - goBack: () => void; - duration: number; - discountId: string; - customMessage: string; - priceInEth: string; - renewPrice: string; - isArOnforced?: boolean; -}; - -const RenewalDiscount: FunctionComponent = ({ - groups, - priceInEth, - renewPrice, - duration, - discountId, - customMessage, - goBack, - isArOnforced = false, -}) => { - const [email, setEmail] = useState(""); - const [emailError, setEmailError] = useState(true); - const [isSwissResident, setIsSwissResident] = useState(false); - const [salesTaxRate, setSalesTaxRate] = useState(0); - const [salesTaxAmount, setSalesTaxAmount] = useState("0"); - const [callData, setCallData] = useState([]); - const [price, setPrice] = useState(priceInEth); // price in displayedCurrency, set to priceInEth on first load as ETH is the default currency - const [quoteData, setQuoteData] = useState(null); // null if in ETH - const [displayedCurrency, setDisplayedCurrency] = useState( - CurrencyType.ETH - ); - const [invalidBalance, setInvalidBalance] = useState(false); - const [isTxModalOpen, setIsTxModalOpen] = useState(false); - const [termsBox, setTermsBox] = useState(true); - const [renewalBox, setRenewalBox] = useState(true); - const [salt, setSalt] = useState(); - const [metadataHash, setMetadataHash] = useState(); - const [needMetadata, setNeedMetadata] = useState(false); - const [selectedDomains, setSelectedDomains] = - useState>(); - const [nonSubscribedDomains, setNonSubscribedDomains] = useState(); - const { address } = useAccount(); - const { writeAsync: execute, data: renewData } = useContractWrite({ - calls: callData, - }); - const [domainsMinting, setDomainsMinting] = - useState>(); - const { addTransaction } = useNotificationManager(); - const router = useRouter(); - const needsAllowance = useAllowanceCheck(displayedCurrency, address); - const tokenBalances = useBalances(address); // fetch the user balances for all whitelisted tokens - const [hasChoseCurrency, setHasChoseCurrency] = useState(false); - const [loadingPrice, setLoadingPrice] = useState(false); - - useEffect(() => { - if (!renewData?.transaction_hash || !salt || !metadataHash) return; - - if (needMetadata) { - // register the metadata to the sales manager db - fetch(`${process.env.NEXT_PUBLIC_SALES_SERVER_LINK}/add_metadata`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - meta_hash: metadataHash, - email: email, - tax_state: isSwissResident ? "switzerland" : "none", - salt: salt, - }), - }) - .then((res) => res.json()) - .catch((err) => console.log("Error on sending metadata:", err)); - } - - // Subscribe to auto renewal mailing list if renewal box is checked - if (renewalBox) { - fetch(`${process.env.NEXT_PUBLIC_SALES_SERVER_LINK}/mail_subscribe`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - tx_hash: formatHexString(renewData.transaction_hash), - groups, - }), - }) - .then((res) => res.json()) - .catch((err) => console.log("Error on registering to email:", err)); - } - - addTransaction({ - timestamp: Date.now(), - subtext: "Domain renewal", - type: NotificationType.TRANSACTION, - data: { - type: TransactionType.RENEW_DOMAIN, - hash: renewData.transaction_hash, - status: "pending", - }, - }); - setIsTxModalOpen(true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [renewData]); // We only need renewData here because we don't want to send the metadata twice (we send it once the tx is sent) - - // refetch new quote if the timestamp from quote is expired - useEffect(() => { - const fetchQuote = () => { - if (displayedCurrency === CurrencyType.ETH) return; - getTokenQuote(ERC20Contract[displayedCurrency]).then((data) => { - setQuoteData(data); - }); - }; - - const scheduleRefetch = () => { - const now = parseInt((new Date().getTime() / 1000).toFixed(0)); - const timeLimit = now - 60; // 60 seconds - // Check if we need to refetch - if (!quoteData || displayedCurrency === CurrencyType.ETH) { - setQuoteData(null); - // we don't need to check for quote until displayedCurrency is updated - return; - } - - if (quoteData.max_quote_validity <= timeLimit) { - fetchQuote(); - } - - // Calculate the time until the next validity check - const timeUntilNextCheck = quoteData.max_quote_validity - timeLimit; - setTimeout(scheduleRefetch, Math.max(15000, timeUntilNextCheck * 100)); - }; - - // Initial fetch - fetchQuote(); - // Start the refetch scheduling - scheduleRefetch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displayedCurrency, price]); // We don't add quoteData because it would create an infinite loop - - // on first load, we generate a salt - useEffect(() => { - setSalt(generateSalt()); - }, [address]); - - useEffect(() => { - if (!address) return; - fetch( - `${process.env.NEXT_PUBLIC_SERVER_LINK}/renewal/get_metahash?addr=${address}` - ) - .then((response) => response.json()) - .then((data) => { - if (data.meta_hash && parseInt(data.meta_hash) !== 0) { - setNeedMetadata(false); - setMetadataHash(data.meta_hash); - data.tax_rate ? setSalesTaxRate(data.tax_rate) : setSalesTaxRate(0); - } else { - setNeedMetadata(true); - } - }) - .catch((err) => { - console.log("Error while fetching metadata:", err); - setNeedMetadata(true); - }); - }, [address]); - - useEffect(() => { - // salt must not be empty to preserve privacy - if (!salt || !needMetadata) return; - - const computeHashes = async () => { - const hash = await computeMetadataHash( - email, - isSwissResident ? "switzerland" : "none", - salt - ); - setMetadataHash(hash); - }; - - computeHashes(); - }, [email, salt, renewalBox, isSwissResident, needMetadata]); - - // we choose the currency based on the user balances - useEffect(() => { - if ( - tokenBalances && - Object.keys(tokenBalances).length > 0 && - !hasChoseCurrency && - priceInEth - ) { - smartCurrencyChoosing(tokenBalances, priceInEth).then((currency) => { - onCurrencySwitch(currency); - setHasChoseCurrency(true); - }); - } - }, [tokenBalances, priceInEth, hasChoseCurrency]); - - // we ensure user has enough balance of the token selected - useEffect(() => { - if (tokenBalances && price && displayedCurrency) { - const tokenBalance = tokenBalances[displayedCurrency]; - if (tokenBalance && BigInt(tokenBalance) >= BigInt(price)) { - setInvalidBalance(false); - } else { - setInvalidBalance(true); - } - } - }, [price, displayedCurrency, tokenBalances]); - - function changeEmail(value: string): void { - setEmail(value); - setEmailError(isValidEmail(value) ? false : true); - } - - useEffect(() => { - if (isSwissResident) { - setSalesTaxRate(swissVatRate); - setSalesTaxAmount(applyRateToBigInt(price, swissVatRate)); - } else { - setSalesTaxRate(0); - setSalesTaxAmount(""); - } - }, [isSwissResident, price]); - - // build free renewal call - useEffect(() => { - if (displayedCurrency !== CurrencyType.ETH && !quoteData) return; - const txMetadataHash = `0x${metadataHash}` as HexString; - if (selectedDomains) { - // Free renewal - const calls = [ - ...registrationCalls.multiCallFreeRenewals( - selectedDomainsToEncodedArray(selectedDomains), - AutoRenewalContracts[displayedCurrency] - ), - ]; - - if (renewalBox) { - if (needsAllowance) { - calls.unshift( - autoRenewalCalls.approve( - ERC20Contract[displayedCurrency], - AutoRenewalContracts[displayedCurrency], - String(Number(price) / duration) - ) - ); - } - - selectedDomainsToArray(selectedDomains).map((domain) => { - if (nonSubscribedDomains?.includes(domain)) { - const encodedDomain = utils - .encodeDomain(domain) - .map((element) => element.toString())[0]; - - const domainPrice = getDomainPrice( - domain, - displayedCurrency, - quoteData?.quote - ); - const allowance = getAutoRenewAllowance( - displayedCurrency, - salesTaxRate, - domainPrice - ); - - calls.unshift( - autoRenewalCalls.enableRenewal( - AutoRenewalContracts[displayedCurrency], - encodedDomain, - allowance, - txMetadataHash - ) - ); - } - }); - } - setCallData(calls); - } - }, [ - selectedDomains, - price, - salesTaxAmount, - needsAllowance, - metadataHash, - salesTaxRate, - duration, - renewalBox, - discountId, - needMetadata, - quoteData, - displayedCurrency, - nonSubscribedDomains, - ]); - - // we get the list of domains that do not have a autorenewal already enabled - useEffect(() => { - if (address) { - fetch( - `${ - process.env.NEXT_PUBLIC_SERVER_LINK - }/renewal/get_non_subscribed_domains?addr=${hexToDecimal(address)}` - ) - .then((response) => response.json()) - .then((data) => { - setNonSubscribedDomains(data); - }); - } - }, [address, setSelectedDomains]); - - useEffect(() => { - if (displayedCurrency === CurrencyType.ETH) { - setPrice(priceInEth); - } else if (quoteData) { - const priceInAltcoin = getDomainPriceAltcoin(quoteData.quote, priceInEth); - setPrice(priceInAltcoin); - setLoadingPrice(false); - } - }, [priceInEth, quoteData, displayedCurrency]); - - const onCurrencySwitch = (type: CurrencyType) => { - if (type !== CurrencyType.ETH) setLoadingPrice(true); - setDisplayedCurrency(type); - }; - - return ( -
-
-
- -
-

Your renewal

-

Renew Your domain(s)

-
-
- {needMetadata && ( - changeEmail(e.target.value)} - color="secondary" - error={emailError} - errorMessage="Please enter a valid email address" - type="email" - /> - )} - {needMetadata && ( - - setIsSwissResident(!isSwissResident) - } - /> - )} - - -
-
-
- - - setTermsBox(!termsBox)} - termsBox={termsBox} - onChangeRenewalBox={() => setRenewalBox(!renewalBox)} - renewalBox={renewalBox} - isArOnforced={isArOnforced} - ethRenewalPrice={renewPrice} - /> - {address ? ( - - ) : ( - - )} -
-
- - router.push("/identities")} - /> -
- ); -}; - -export default RenewalDiscount; diff --git a/components/domains/autorenewal.tsx b/components/domains/autorenewal.tsx index 28014a87..f3b45f96 100644 --- a/components/domains/autorenewal.tsx +++ b/components/domains/autorenewal.tsx @@ -1,7 +1,7 @@ import React from "react"; import { FunctionComponent, useEffect, useState } from "react"; import Button from "../UI/button"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { formatHexString, isValidEmail, @@ -16,7 +16,8 @@ import SwissForm from "./swissForm"; import { computeMetadataHash, generateSalt } from "../../utils/userDataService"; import { areDomainSelected, - getPriceFromDomains, + getApprovalAmount, + getTotalYearlyPrice, } from "../../utils/priceService"; import autoRenewalCalls from "../../utils/callData/autoRenewalCalls"; import BackButton from "../UI/backButton"; @@ -42,9 +43,10 @@ import { } from "../../utils/altcoinService"; import ArCurrencyDropdown from "./arCurrencyDropdown"; import { areArraysEqual } from "@/utils/arrayService"; -import useNeedsAllowances from "@/hooks/useNeedAllowances"; +import useNeedAllowances from "@/hooks/useNeedAllowances"; import useNeedSubscription from "@/hooks/useNeedSubscription"; import AutoRenewalDomainsBox from "./autoRenewalDomainsBox"; +import Notification from "../UI/notification"; type SubscriptionProps = { groups: string[]; @@ -55,10 +57,10 @@ const Subscription: FunctionComponent = ({ groups }) => { const [emailError, setEmailError] = useState(true); const [isSwissResident, setIsSwissResident] = useState(false); const [salesTaxRate, setSalesTaxRate] = useState(0); - const [salesTaxAmount, setSalesTaxAmount] = useState("0"); + const [salesTaxAmount, setSalesTaxAmount] = useState(BigInt(0)); const [callData, setCallData] = useState([]); - const [priceInEth, setPriceInEth] = useState(""); // price in ETH - const [price, setPrice] = useState(""); // price in displayedCurrencies, set to priceInEth on first load as ETH is the default currency + const [priceInEth, setPriceInEth] = useState(BigInt(0)); // price in ETH + const [price, setPrice] = useState(BigInt(0)); // price in displayedCurrencies, set to priceInEth on first load as ETH is the default currency const [quoteData, setQuoteData] = useState(null); // null if in ETH const [displayedCurrencies, setDisplayedCurrencies] = useState< CurrencyType[] @@ -72,17 +74,17 @@ const Subscription: FunctionComponent = ({ groups }) => { const [selectedDomains, setSelectedDomains] = useState>(); const { address } = useAccount(); - const { writeAsync: execute, data: autorenewData } = useContractWrite({ + const { sendAsync: execute, data: autorenewData } = useSendTransaction({ calls: callData, }); const [domainsMinting, setDomainsMinting] = useState>(); const { addTransaction } = useNotificationManager(); const router = useRouter(); - const duration = 1; - const needsAllowance = useNeedsAllowances(address); + const allowanceStatus = useNeedAllowances(address); const { needSubscription, isLoading: needSubscriptionLoading } = useNeedSubscription(address); + const [currencyError, setCurrencyError] = useState(false); useEffect(() => { if (!address) return; @@ -188,7 +190,13 @@ const Subscription: FunctionComponent = ({ groups }) => { if (isCurrencyETH || !contractToQuote) return; getTokenQuote(contractToQuote).then((data) => { - setQuoteData(data); + if (data) { + setQuoteData(data); + setCurrencyError(false); + } else { + setDisplayedCurrencies([CurrencyType.ETH]); + setCurrencyError(true); + } }); }; @@ -221,13 +229,8 @@ const Subscription: FunctionComponent = ({ groups }) => { // if selectedDomains or duration have changed, we update priceInEth useEffect(() => { if (!selectedDomains) return; - setPriceInEth( - getPriceFromDomains( - selectedDomainsToArray(selectedDomains), - duration - ).toString() - ); - }, [selectedDomains, duration]); + setPriceInEth(getTotalYearlyPrice(selectedDomains)); + }, [selectedDomains]); // if priceInEth or quoteData have changed, we update the price in altcoin useEffect(() => { @@ -251,7 +254,7 @@ const Subscription: FunctionComponent = ({ groups }) => { setSalesTaxAmount(applyRateToBigInt(price, swissVatRate)); } else { setSalesTaxRate(0); - setSalesTaxAmount(""); + setSalesTaxAmount(BigInt(0)); } } }, [isSwissResident, price, needMedadata, salesTaxRate]); @@ -267,15 +270,19 @@ const Subscription: FunctionComponent = ({ groups }) => { displayedCurrencies.map((currency) => { // Add ERC20 allowance for all currencies if needed - if (needsAllowance[currency]) { - const priceToApprove = - currency === CurrencyType.ETH ? priceInEth : price; + if (allowanceStatus[currency].needsAllowance) { + const amountToApprove = getApprovalAmount( + price, + salesTaxAmount, + 1, + allowanceStatus[currency].currentAllowance + ); calls.push( autoRenewalCalls.approve( ERC20Contract[currency], AutoRenewalContracts[currency], - priceToApprove + amountToApprove ) ); } @@ -290,6 +297,7 @@ const Subscription: FunctionComponent = ({ groups }) => { const domainPrice = getDomainPrice( domain, currency, + 365, quoteData?.quote ); const allowance = getAutoRenewAllowance( @@ -315,7 +323,7 @@ const Subscription: FunctionComponent = ({ groups }) => { selectedDomains, price, salesTaxAmount, - needsAllowance, + allowanceStatus, metadataHash, salesTaxRate, displayedCurrencies, @@ -423,6 +431,12 @@ const Subscription: FunctionComponent = ({ groups }) => { isTxModalOpen={isTxModalOpen} closeModal={() => window.history.back()} /> + setCurrencyError(false)} + > +

Failed to get token quote. Please use ETH for now.

+
); }; diff --git a/components/domains/externalDomainActions.tsx b/components/domains/externalDomainActions.tsx index a13e2e35..3f78a4b7 100644 --- a/components/domains/externalDomainActions.tsx +++ b/components/domains/externalDomainActions.tsx @@ -1,6 +1,6 @@ import React from "react"; import { FunctionComponent, useEffect, useState } from "react"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import styles from "../../styles/components/identityMenu.module.css"; import { utils } from "starknetid.js"; import ClickableAction from "../UI/iconsComponents/clickableAction"; @@ -42,7 +42,7 @@ const ExternalDomainActions: FunctionComponent = ({ }); //Set as main domain execute - const { writeAsync: set_address_to_domain } = useContractWrite({ + const { sendAsync: set_address_to_domain } = useSendTransaction({ calls: [resolverCalls.setAddresstoDomain(callDataEncodedDomain)], }); diff --git a/components/domains/externalDomainTransferModal.tsx b/components/domains/externalDomainTransferModal.tsx index 0c390228..42a36bcb 100644 --- a/components/domains/externalDomainTransferModal.tsx +++ b/components/domains/externalDomainTransferModal.tsx @@ -5,7 +5,7 @@ import React, { useContext, } from "react"; import { TextField, InputAdornment } from "@mui/material"; -import { useContractWrite } from "@starknet-react/core"; +import { useSendTransaction } from "@starknet-react/core"; import { useRouter } from "next/router"; import { isHexString, minifyAddress } from "../../utils/stringService"; import { utils } from "starknetid.js"; @@ -45,7 +45,7 @@ const ExternalDomainsTransferModal: FunctionComponent< const [isSendingTx, setIsSendingTx] = useState(false); const [callData, setCallData] = useState([]); - const { writeAsync: transfer_name, data: transferData } = useContractWrite({ + const { sendAsync: transfer_name, data: transferData } = useSendTransaction({ calls: callData, }); diff --git a/components/domains/registerCheckboxes.tsx b/components/domains/registerCheckboxes.tsx index ad552b44..1f494586 100644 --- a/components/domains/registerCheckboxes.tsx +++ b/components/domains/registerCheckboxes.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent } from "react"; import { Checkbox } from "@mui/material"; import InputHelper from "../UI/inputHelper"; -import { gweiToEth } from "../../utils/feltService"; +import { weiToEth } from "../../utils/feltService"; import { CurrencyType } from "@/utils/constants"; import TermCheckbox from "./termCheckbox"; @@ -12,13 +12,12 @@ type RegisterCheckboxes = { onChangeRenewalBox?: () => void; variant?: "default" | "white"; isArOnforced?: boolean; - ethRenewalPrice?: string; showMainDomainBox?: boolean; onChangeMainDomainBox?: () => void; mainDomainBox?: boolean; domain?: string; displayedCurrency?: CurrencyType; - maxPriceRange?: string; + maxPriceRange?: bigint; }; const RegisterCheckboxes: FunctionComponent = ({ @@ -38,7 +37,7 @@ const RegisterCheckboxes: FunctionComponent = ({ const getHelperText = (): string => { return `Enabling a subscription permits Starknet ID to renew your domain automatically every year for you! This approval gives us only the possibility to renew your domain once per year ${ maxPriceRange - ? `(maximum ${Number(gweiToEth(maxPriceRange)).toFixed( + ? `(maximum ${weiToEth(maxPriceRange).toFixed( 3 )} ${displayedCurrency}/year)` : "" diff --git a/components/domains/registerSummary.tsx b/components/domains/registerSummary.tsx index 05aff124..6fea5f35 100644 --- a/components/domains/registerSummary.tsx +++ b/components/domains/registerSummary.tsx @@ -5,17 +5,18 @@ import React, { useState, } from "react"; import styles from "../../styles/components/registerV3.module.css"; -import { gweiToEth, numberToFixedString } from "../../utils/feltService"; +import { weiToEth } from "../../utils/feltService"; import { CurrencyType } from "../../utils/constants"; import CurrencyDropdown from "./currencyDropdown"; import { Skeleton } from "@mui/material"; import ArrowRightIcon from "../UI/iconsComponents/icons/arrowRightIcon"; import ArCurrencyDropdown from "./arCurrencyDropdown"; +import { getDisplayablePrice } from "@/utils/priceService"; type RegisterSummaryProps = { - duration: number; - ethRegistrationPrice: string; - registrationPrice: string; // price in displayedCurrency, set to priceInEth on first load as ETH is the default currency + durationInYears: number; + priceInEth: bigint; + price: bigint; renewalBox?: boolean; salesTaxRate: number; isSwissResident?: boolean; @@ -26,15 +27,16 @@ type RegisterSummaryProps = { | ((type: CurrencyType) => void); loadingPrice?: boolean; isUpselled?: boolean; - discountedPrice?: string; // price the user will pay after discount - discountedDuration?: number; // years the user will have the domain for after discount + discountedPrice?: bigint; + discountedPriceInEth?: bigint; areArCurrenciesEnabled?: boolean; + isUsdPriceHidden?: boolean; }; const RegisterSummary: FunctionComponent = ({ - duration, - ethRegistrationPrice, - registrationPrice, + durationInYears, + priceInEth, + price, renewalBox = true, salesTaxRate, isSwissResident, @@ -44,12 +46,14 @@ const RegisterSummary: FunctionComponent = ({ loadingPrice, isUpselled = false, discountedPrice, - discountedDuration, + discountedPriceInEth, areArCurrenciesEnabled = false, + isUsdPriceHidden = false, }) => { - const [ethUsdPrice, setEthUsdPrice] = useState("0"); // price of 1ETH in USD + const [ethUsdPrice, setEthUsdPrice] = useState("0"); // price of 1 ETH in USD const [usdRegistrationPrice, setUsdRegistrationPrice] = useState("0"); - const recurrence = renewalBox && duration === 1 ? "/year" : ""; + const [salesTaxAmountUsd, setSalesTaxAmountUsd] = useState("0"); + const recurrence = renewalBox && durationInYears === 1 ? "/year" : ""; useEffect(() => { fetch( "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" @@ -57,7 +61,7 @@ const RegisterSummary: FunctionComponent = ({ .then((res) => res.json()) .then((data) => { console.log("Coingecko API Data:", data); - setEthUsdPrice(data?.ethereum?.usd.toString()); + setEthUsdPrice(data?.ethereum?.usd); }) .catch((err) => console.log("Coingecko API Error:", err)); }, []); @@ -68,20 +72,33 @@ const RegisterSummary: FunctionComponent = ({ : displayedCurrency; useEffect(() => { - function computeUsdPrice() { - const durationToUse = duration > 1 ? duration : 1; - if (ethUsdPrice && ethRegistrationPrice) { - return ( - Number(ethUsdPrice) * - Number(gweiToEth(ethRegistrationPrice)) * - durationToUse - ).toFixed(2); - } - return "0"; - } + const effectivePrice = + discountedPrice && discountedPrice !== BigInt(0) && discountedPriceInEth + ? discountedPriceInEth + : priceInEth; + + const computeUsdRegistrationPrice = () => { + if (!ethUsdPrice || !priceInEth) return 0; + + return Number(ethUsdPrice) * weiToEth(effectivePrice); + }; + + const computeUsdSalesTaxAmount = () => { + if (!ethUsdPrice || !priceInEth) return 0; - setUsdRegistrationPrice(computeUsdPrice()); - }, [ethRegistrationPrice, ethUsdPrice, duration]); + return salesTaxRate * weiToEth(effectivePrice) * Number(ethUsdPrice); + }; + + setUsdRegistrationPrice(computeUsdRegistrationPrice().toFixed(2)); + setSalesTaxAmountUsd(computeUsdSalesTaxAmount().toFixed(2)); + }, [ + priceInEth, + ethUsdPrice, + durationInYears, + discountedPrice, + discountedPriceInEth, + salesTaxRate, + ]); // Ideally, this should be a separate components function displayPrice(priceToPay: string, salesTaxInfo: string): ReactNode { @@ -117,37 +134,27 @@ const RegisterSummary: FunctionComponent = ({ } function displayTokenPrice(): ReactNode { - const salesTaxAmountUsd = - salesTaxRate * - Number(gweiToEth(ethRegistrationPrice)) * - Number(ethUsdPrice); - const salesTaxInfo = salesTaxAmountUsd - ? ` (+ ${numberToFixedString( - salesTaxAmountUsd - )}$ worth of ${announcedCurrency} for Swiss VAT)` + ? ` (+ ${salesTaxAmountUsd}$ worth of ${announcedCurrency} for Swiss VAT)` : ""; - const registerPrice = Number(gweiToEth(registrationPrice)); - const registerPriceStr = - registerPrice != 0 ? numberToFixedString(registerPrice, 4) : "0"; if (isUpselled && discountedPrice) { return displayDiscountedPrice( - registerPriceStr, - numberToFixedString(Number(gweiToEth(discountedPrice)), 3), + getDisplayablePrice(price), + getDisplayablePrice(discountedPrice), salesTaxInfo ); } - return displayPrice(registerPriceStr, salesTaxInfo); + return displayPrice(getDisplayablePrice(price), salesTaxInfo); } - function getMessage() { - if (!ethRegistrationPrice) return "0"; + function getCheckoutMessage() { if (customMessage) return customMessage; + if (!priceInEth) return "0"; else { - return `${gweiToEth(ethRegistrationPrice)} ETH x ${ - isUpselled ? discountedDuration : duration - } ${isUpselled || duration > 1 ? "years" : "year"}`; + return `${getDisplayablePrice(priceInEth)} ETH x ${durationInYears} ${ + isUpselled || durationInYears > 1 ? "years" : "year" + }`; } } @@ -156,13 +163,15 @@ const RegisterSummary: FunctionComponent = ({

Total due:

-

{getMessage()}

+

{getCheckoutMessage()}

{loadingPrice ? ( ) : ( displayTokenPrice() )} -

β‰ˆ ${usdRegistrationPrice}

+ {isUsdPriceHidden ? null : ( +

β‰ˆ ${usdRegistrationPrice}

+ )}
{areArCurrenciesEnabled ? ( diff --git a/components/domains/registerV3.tsx b/components/domains/registerV3.tsx index 423063d4..7218231f 100644 --- a/components/domains/registerV3.tsx +++ b/components/domains/registerV3.tsx @@ -34,7 +34,7 @@ const RegisterV3: FunctionComponent = ({ updateFormState({ selectedDomains: { [domain]: true }, isUpselled: true, - duration: 1, + durationInYears: 1, tokenId: 0, selectedPfp: undefined, }); diff --git a/components/domains/renewalV2.tsx b/components/domains/renewalV2.tsx index 54107671..2550e06c 100644 --- a/components/domains/renewalV2.tsx +++ b/components/domains/renewalV2.tsx @@ -30,7 +30,7 @@ const RenewalV2: FunctionComponent = ({ groups }) => { // Initialize the upsell state updateFormState({ isUpselled: true, - duration: 1, + durationInYears: 1, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [address]); // Don't call updateFromState on every render diff --git a/components/domains/steps/checkoutCard.tsx b/components/domains/steps/checkoutCard.tsx index 33352f48..43b88a2b 100644 --- a/components/domains/steps/checkoutCard.tsx +++ b/components/domains/steps/checkoutCard.tsx @@ -3,21 +3,17 @@ import React, { useCallback, useContext, useEffect, - useMemo, useState, } from "react"; import styles from "../../../styles/components/registerV3.module.css"; import { FormContext } from "@/context/FormProvider"; import { - AutoRenewalContracts, CurrencyType, - ERC20Contract, FormType, NotificationType, TransactionType, - swissVatRate, } from "@/utils/constants"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import Button from "@/components/UI/button"; import RegisterSummary from "../registerSummary"; import { useNotificationManager } from "@/hooks/useNotificationManager"; @@ -25,37 +21,19 @@ import useAllowanceCheck from "@/hooks/useAllowanceCheck"; import useBalances from "@/hooks/useBalances"; import { Divider } from "@mui/material"; import { Call } from "starknet"; -import { - getAutoRenewAllowance, - getDomainPrice, - getDomainPriceAltcoin, - getPriceForDuration, - getTokenQuote, - smartCurrencyChoosing, -} from "@/utils/altcoinService"; -import { getPriceFromDomains } from "@/utils/priceService"; -import { - applyRateToBigInt, - hexToDecimal, - toUint256, -} from "@/utils/feltService"; import RegisterCheckboxes from "../registerCheckboxes"; -import { - formatHexString, - getDomainWithStark, - selectedDomainsToArray, - selectedDomainsToEncodedArray, -} from "@/utils/stringService"; +import { formatHexString, getDomainWithStark } from "@/utils/stringService"; import UpsellCard from "./upsellCard"; -import registrationCalls from "@/utils/callData/registrationCalls"; -import { utils } from "starknetid.js"; -import autoRenewalCalls from "@/utils/callData/autoRenewalCalls"; -import { useDomainFromAddress } from "@/hooks/naming"; -import identityChangeCalls from "@/utils/callData/identityChangeCalls"; import posthog from "posthog-js"; import { useRouter } from "next/router"; import { formatDomainData } from "@/utils/cacheDomainData"; import ReduceDuration from "./reduceDuration"; +import Notification from "@/components/UI/notification"; +import { useCurrencyManagement } from "@/hooks/checkout/useCurrencyManagement"; +import { usePriceManagement } from "@/hooks/checkout/usePriceManagement"; +import { useCheckoutState } from "@/hooks/checkout/useCheckoutState"; +import { useRegisterTxPrep } from "@/hooks/checkout/useRegisterTxPrep"; +import { useRenewalTxPrep } from "@/hooks/checkout/useRenewalTxPrep"; type CheckoutCardProps = { type: FormType; @@ -71,556 +49,106 @@ const CheckoutCard: FunctionComponent = ({ const router = useRouter(); const { account, address } = useAccount(); const { formState, updateFormState, clearForm } = useContext(FormContext); - const [priceInEth, setPriceInEth] = useState(""); // price in ETH for 1 year - const [price, setPrice] = useState(""); // total price in displayedCurrency, set to priceInEth on first load as ETH is the default currency - const [discountedPrice, setDiscountedPrice] = useState(""); // discounted price in displayedCurrency - const [maxPriceRange, setMaxPriceRange] = useState("0"); // max price range for the displayedCurrency that will be spent on yearly subscription - const [quoteData, setQuoteData] = useState(null); // null if in ETH - const [salesTaxAmount, setSalesTaxAmount] = useState("0"); - const [invalidBalance, setInvalidBalance] = useState(false); - const [renewalBox, setRenewalBox] = useState(true); - const [termsBox, setTermsBox] = useState(false); - const mainDomain = useDomainFromAddress(address ?? ""); - const [mainDomainBox, setMainDomainBox] = useState(true); - const [sponsor, setSponsor] = useState("0"); - const [hasUserSelectedOffer, setHasUserSelectedOffer] = - useState(false); - const [displayedCurrency, setDisplayedCurrency] = useState( - CurrencyType.ETH - ); - const [loadingPrice, setLoadingPrice] = useState(false); - const [reloadingPrice, setReloadingPrice] = useState(false); // used to know if the user changes the currency - const [hasReverseAddressRecord, setHasReverseAddressRecord] = - useState(false); - const [domainsMinting, setDomainsMinting] = - useState>(); const { addTransaction } = useNotificationManager(); - const needsAllowance = useAllowanceCheck(displayedCurrency, address); const tokenBalances = useBalances(address); // fetch the user balances for all whitelisted tokens - const [hasChosenCurrency, setHasChosenCurrency] = useState(false); const [callData, setCallData] = useState([]); - const [tokenIdRedirect, setTokenIdRedirect] = useState("0"); - const { writeAsync: execute, data: checkoutData } = useContractWrite({ + const { sendAsync: execute, data: checkoutData } = useSendTransaction({ calls: callData, }); - const [reducedDuration, setReducedDuration] = useState(0); // reduced duration for the user to buy the domain - - // Renewals - const [nonSubscribedDomains, setNonSubscribedDomains] = useState(); - - const hasMainDomain = useMemo(() => { - if (!mainDomain || !mainDomain.domain) return false; - return mainDomain.domain.endsWith(".stark"); - }, [mainDomain]); - - // refetch new quote if the timestamp from quote is expired - useEffect(() => { - const fetchQuote = () => { - if (displayedCurrency === CurrencyType.ETH) return; - getTokenQuote(ERC20Contract[displayedCurrency]).then((data) => { - setQuoteData(data); - }); - }; - - const scheduleRefetch = () => { - const now = parseInt((new Date().getTime() / 1000).toFixed(0)); - const timeLimit = now - 60; // 60 seconds - // Check if we need to refetch - if (!quoteData || displayedCurrency === CurrencyType.ETH) { - setQuoteData(null); - // we don't need to check for quote until displayedCurrency is updated - return; - } - if (quoteData.max_quote_validity <= timeLimit) { - fetchQuote(); - } - - // Calculate the time until the next validity check - const timeUntilNextCheck = quoteData.max_quote_validity - timeLimit; - setTimeout(scheduleRefetch, Math.max(15000, timeUntilNextCheck * 100)); - }; - - // Initial fetch - fetchQuote(); - // Start the refetch scheduling - scheduleRefetch(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [displayedCurrency, priceInEth]); // We don't add quoteData because it would create an infinite loop - - useEffect(() => { - // get the price for domains for a year - if (!formState.selectedDomains) return; - setPriceInEth( - getPriceFromDomains( - selectedDomainsToArray(formState.selectedDomains), - 1 - ).toString() - ); - }, [formState.selectedDomains, formState.duration]); - - useEffect(() => { - // update discountedPrice based on isUpselled selected or not - if (formState.isUpselled) { - if (displayedCurrency === CurrencyType.ETH) { - setDiscountedPrice( - (BigInt(priceInEth) * BigInt(discount.paidDuration)).toString() - ); - } else if (quoteData) { - const priceInAltcoin = getDomainPriceAltcoin( - quoteData?.quote as string, - (BigInt(priceInEth) * BigInt(discount.paidDuration)).toString() - ); - setDiscountedPrice(priceInAltcoin); - } - } else setDiscountedPrice(""); - }, [ - priceInEth, - formState.isUpselled, + const { + termsBox, + renewalBox, + mainDomainBox, + hasUserSelectedOffer, + hasReverseAddressRecord, + domainsMinting, + reducedDuration, + nonSubscribedDomains, + sponsor, + hasMainDomain, + setHasUserSelectedOffer, + setDomainsMinting, + setReducedDuration, + onChangeTermsBox, + onChangeRenewalBox, + onChangeMainDomainBox, + } = useCheckoutState(type, address); + + const { displayedCurrency, quoteData, - discount, - ]); - - // we choose the currency based on the user balances - useEffect(() => { - if ( - tokenBalances && - Object.keys(tokenBalances).length > 0 && - !hasChosenCurrency && - priceInEth - ) { - const domainPrice = formState.isUpselled - ? (BigInt(priceInEth) * BigInt(discount.paidDuration)).toString() - : (BigInt(priceInEth) * BigInt(formState.duration)).toString(); - smartCurrencyChoosing(tokenBalances, domainPrice).then((currency) => { - onCurrencySwitch(currency); - setHasChosenCurrency(true); - }); - } - }, [ - tokenBalances, - priceInEth, - formState.isUpselled, + currencyError, hasChosenCurrency, - formState.duration, - discount.paidDuration, - ]); - - // we ensure user has enough balance of the token selected - useEffect(() => { - if ( - tokenBalances && - price && - displayedCurrency && - !loadingPrice && - !reloadingPrice - ) { - const tokenBalance = tokenBalances[displayedCurrency]; - if (!tokenBalance) return; - const _price = formState.isUpselled ? discountedPrice : price; - if (tokenBalance && BigInt(tokenBalance) >= BigInt(_price)) - setInvalidBalance(false); - else setInvalidBalance(true); - } - }, [ + onCurrencySwitch, + setHasChosenCurrency, + setCurrencyError, + } = useCurrencyManagement(); + + const { + dailyPriceInEth, + discountedPriceInEth, price, discountedPrice, - formState.isUpselled, + loadingPrice, + setLoadingPrice, + setReloadingPrice, + salesTaxAmount, + maxPriceRange, + invalidBalance, + } = usePriceManagement( + formState, displayedCurrency, + quoteData, + discount, tokenBalances, - loadingPrice, - reloadingPrice, - ]); - - useEffect(() => { - const referralData = localStorage.getItem("referralData"); - if (referralData) { - const data = JSON.parse(referralData); - if (data.sponsor && data?.expiry >= new Date().getTime()) { - setSponsor(data.sponsor); - } else { - setSponsor("0"); - } - } else { - setSponsor("0"); - } - }, [address, formState.selectedDomains]); + hasChosenCurrency, + onCurrencySwitch, + setHasChosenCurrency, + updateFormState + ); + const allowanceStatus = useAllowanceCheck(displayedCurrency, address); - useEffect(() => { - const _price = formState.isUpselled ? discountedPrice : price; - if (!formState.needMetadata && _price) { - setSalesTaxAmount(applyRateToBigInt(_price, formState.salesTaxRate)); - } else { - if (formState.isSwissResident) { - updateFormState({ salesTaxRate: swissVatRate }); - setSalesTaxAmount(applyRateToBigInt(_price, swissVatRate)); - } else { - updateFormState({ salesTaxRate: 0 }); - setSalesTaxAmount(""); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - formState.isSwissResident, + const { + callData: registerCallData, + tokenIdRedirect: registerTokenIdRedirect, + } = useRegisterTxPrep( + formState, + displayedCurrency, + quoteData, price, - formState.isUpselled, discountedPrice, - formState.needMetadata, - formState.salesTaxRate, - ]); // Don't call updateFromState on every render - - // if priceInEth or quoteData have changed, we update the price in altcoin - useEffect(() => { - if (!priceInEth) return; - const _price = getPriceForDuration( - priceInEth, - formState.isUpselled ? discount.duration : formState.duration - ); - if (displayedCurrency === CurrencyType.ETH) { - setPrice(_price); - } else if (quoteData) { - setPrice(getDomainPriceAltcoin(quoteData.quote, _price)); - } - setLoadingPrice(false); - if (reloadingPrice) setReloadingPrice(false); - }, [ - priceInEth, - quoteData, - displayedCurrency, - formState.isUpselled, - formState.duration, - discount.duration, - reloadingPrice, - ]); + salesTaxAmount, + sponsor, + hasMainDomain, + mainDomainBox, + hasReverseAddressRecord, + discount, + renewalBox, + allowanceStatus + ); - useEffect(() => { - if (displayedCurrency !== CurrencyType.ETH && !quoteData) return; - const limitPrice = getAutoRenewAllowance( - displayedCurrency, - formState.salesTaxRate, - getDomainPrice( - Object.keys(formState.selectedDomains)[0], - displayedCurrency, - quoteData?.quote - ) - ); - setMaxPriceRange(limitPrice); - }, [ + const { callData: renewalCallData } = useRenewalTxPrep( + formState, displayedCurrency, - formState.selectedDomains, quoteData, - formState.salesTaxRate, - ]); - - useEffect(() => { - if (!address) return; - fetch(`${process.env.NEXT_PUBLIC_SERVER_LINK}/addr_has_rev?addr=${address}`) - .then((response) => response.json()) - .then((reverseAddressData) => { - setHasReverseAddressRecord(reverseAddressData.has_rev); - }); - }, [address]); - - // we get the list of domains that do not have a autorenewal already enabled - useEffect(() => { - if (type !== FormType.RENEW) return; - if (address) { - fetch( - `${process.env.NEXT_PUBLIC_SERVER_LINK}/renewal/get_non_subscribed_domains?addr=${address}` - ) - .then((response) => response.json()) - .then((data) => { - setNonSubscribedDomains(data); - }); - } - }, [address, formState.selectedDomains, renewalBox, type]); - - // Set Register Multicall - useEffect(() => { - if ( - (displayedCurrency !== CurrencyType.ETH && !quoteData) || - type !== FormType.REGISTER - ) - return; - // Variables - const newTokenId: number = Math.floor(Math.random() * 1000000000000); - const txMetadataHash = `0x${formState.metadataHash}` as HexString; - const encodedDomain = utils - .encodeDomain(Object.keys(formState.selectedDomains)[0]) - .map((element) => element.toString())[0]; - const finalDuration = formState.isUpselled - ? discount.duration - : formState.duration; - const priceToPay = formState.isUpselled ? discountedPrice : price; - - // Common calls - const calls = [ - registrationCalls.approve(priceToPay, ERC20Contract[displayedCurrency]), - ]; - - if (displayedCurrency === CurrencyType.ETH) { - calls.push( - registrationCalls.buy( - encodedDomain, - formState.tokenId === 0 ? newTokenId : formState.tokenId, - sponsor, - finalDuration, - txMetadataHash, - formState.isUpselled ? discount.discountId : "0" - ) - ); - } else { - calls.push( - registrationCalls.altcoinBuy( - encodedDomain, - formState.tokenId === 0 ? newTokenId : formState.tokenId, - sponsor, - finalDuration, - txMetadataHash, - ERC20Contract[displayedCurrency], - quoteData as QuoteQueryData, - formState.isUpselled ? discount.discountId : "0" - ) - ); - } - - // If the user is a Swiss resident, we add the sales tax - if (formState.salesTaxRate) { - calls.unshift( - registrationCalls.vatTransfer( - salesTaxAmount, - ERC20Contract[displayedCurrency] - ) - ); // IMPORTANT: We use unshift to put the call at the beginning of the array - } - - // If the user choose to mint a new identity - let tokenIdToUse = formState.tokenId; - if (formState.tokenId === 0) { - calls.unshift(registrationCalls.mint(newTokenId)); // IMPORTANT: We use unshift to put the call at the beginning of the array - tokenIdToUse = newTokenId; - } - setTokenIdRedirect(tokenIdToUse.toString()); - - // If the user does not have a main domain or has checked the mainDomain box - if (!hasMainDomain || mainDomainBox) { - if (hasReverseAddressRecord) - calls.push(registrationCalls.resetAddrToDomain()); - calls.push(registrationCalls.mainId(tokenIdToUse)); - } - - // If the user has toggled autorenewal - if (renewalBox) { - if (needsAllowance) { - const amountToApprove = String( - Number(price) / (discountedPrice ? 3 : formState.duration) - ); - calls.push( - autoRenewalCalls.approve( - ERC20Contract[displayedCurrency], - AutoRenewalContracts[displayedCurrency], - amountToApprove - ) - ); - } - - const allowance = getAutoRenewAllowance( - displayedCurrency, - formState.salesTaxRate, - getDomainPrice( - Object.keys(formState.selectedDomains)[0], - displayedCurrency, - quoteData?.quote - ) - ); - calls.push( - autoRenewalCalls.enableRenewal( - AutoRenewalContracts[displayedCurrency], - encodedDomain, - allowance, - `0x${formState.metadataHash}` - ) - ); - } - - // if the user has selected a profile picture - if (formState.selectedPfp) { - const nftData = formState.selectedPfp; - const nft_id = toUint256(nftData.token_id); - calls.push( - identityChangeCalls.updateProfilePicture( - hexToDecimal(nftData.contract_address), - nft_id.low, - nft_id.high, - tokenIdToUse.toString() - ) - ); - } - - // Merge and set the call data - setCallData(calls); - }, [ - formState.tokenId, - formState.isUpselled, - formState.duration, price, - formState.selectedDomains, - hasMainDomain, - address, + discountedPrice, + salesTaxAmount, sponsor, - formState.metadataHash, - formState.salesTaxRate, + discount, renewalBox, - mainDomainBox, - salesTaxAmount, - needsAllowance, - quoteData, - displayedCurrency, - formState.selectedPfp, - type, - discount.duration, - discount.discountId, - discountedPrice, - hasReverseAddressRecord, - ]); + allowanceStatus, + nonSubscribedDomains + ); - // Set Renewal Multicall useEffect(() => { - if (type !== FormType.RENEW) return; - if (displayedCurrency !== CurrencyType.ETH && !quoteData) return; - - // Variables - const finalDuration = formState.isUpselled - ? discount.duration - : formState.duration; - const priceToPay = formState.isUpselled ? discountedPrice : price; - const txMetadataHash = `0x${formState.metadataHash}` as HexString; - - // Common calls - const calls = [ - registrationCalls.approve(priceToPay, ERC20Contract[displayedCurrency]), - ]; - - if (displayedCurrency === CurrencyType.ETH) { - calls.push( - ...registrationCalls.multiCallRenewal( - selectedDomainsToEncodedArray(formState.selectedDomains), - finalDuration, - txMetadataHash, - sponsor, - formState.isUpselled ? discount.discountId : "0" - ) - ); - } else { - calls.push( - ...registrationCalls.multiCallRenewalAltcoin( - selectedDomainsToEncodedArray(formState.selectedDomains), - finalDuration, - txMetadataHash, - ERC20Contract[displayedCurrency], - quoteData as QuoteQueryData, - sponsor, - formState.isUpselled ? discount.discountId : "0" - ) - ); - } - - // If the user is a Swiss resident, we add the sales tax - if (formState.salesTaxRate) { - calls.unshift( - registrationCalls.vatTransfer( - salesTaxAmount, - ERC20Contract[displayedCurrency] - ) - ); // IMPORTANT: We use unshift to put the call at the beginning of the array - } - - // If the user has toggled autorenewal - if (renewalBox) { - if (needsAllowance) { - const amountToApprove = String( - Number(price) / (discountedPrice ? 3 : formState.duration) - ); - calls.push( - autoRenewalCalls.approve( - ERC20Contract[displayedCurrency], - AutoRenewalContracts[displayedCurrency], - amountToApprove - ) - ); - } - - selectedDomainsToArray(formState.selectedDomains).map((domain) => { - // we enable renewal only for the domains that are not already subscribed - if (nonSubscribedDomains?.includes(domain)) { - const encodedDomain = utils - .encodeDomain(domain) - .map((element) => element.toString())[0]; - - const domainPrice = getDomainPrice( - domain, - displayedCurrency, - quoteData?.quote - ); - const allowance = getAutoRenewAllowance( - displayedCurrency, - formState.salesTaxRate, - domainPrice - ); - - calls.push( - autoRenewalCalls.enableRenewal( - AutoRenewalContracts[displayedCurrency], - encodedDomain, - allowance, - txMetadataHash - ) - ); - } - }); - } - - // if the user has selected a profile picture - if (formState.selectedPfp) { - const nftData = formState.selectedPfp; - const nft_id = toUint256(nftData.token_id); - calls.push( - identityChangeCalls.updateProfilePicture( - hexToDecimal(nftData.contract_address), - nft_id.low, - nft_id.high, - formState.tokenId.toString() - ) - ); + if (type === FormType.REGISTER) { + setCallData(registerCallData); + } else if (type === FormType.RENEW) { + setCallData(renewalCallData); } - - // Merge and set the call data - setCallData(calls); - }, [ - formState.tokenId, - formState.isUpselled, - formState.duration, - price, - formState.selectedDomains, - hasMainDomain, - address, - sponsor, - formState.metadataHash, - formState.salesTaxRate, - renewalBox, - mainDomainBox, - salesTaxAmount, - needsAllowance, - quoteData, - displayedCurrency, - formState.selectedPfp, - type, - discount.duration, - discount.discountId, - discountedPrice, - nonSubscribedDomains, - ]); + }, [type, registerCallData, renewalCallData]); // on execute transaction, useEffect(() => { @@ -676,10 +204,12 @@ const CheckoutCard: FunctionComponent = ({ // if registration, store domain data in local storage to use it until it's indexed if (type === FormType.REGISTER) { formatDomainData( - tokenIdRedirect, + registerTokenIdRedirect, formatHexString(address as string), getDomainWithStark(Object.keys(formState.selectedDomains)[0]), - formState.isUpselled ? discount.duration : formState.duration, + formState.isUpselled + ? discount.durationInDays + : formState.durationInYears * 365, !hasMainDomain || mainDomainBox ? true : false, formState.selectedPfp ); @@ -690,17 +220,17 @@ const CheckoutCard: FunctionComponent = ({ // Redirect to confirmation page if (type === FormType.REGISTER) - router.push(`/confirmation?tokenId=${tokenIdRedirect}`); + router.push(`/confirmation?tokenId=${registerTokenIdRedirect}`); else router.push(`/confirmation`); // eslint-disable-next-line react-hooks/exhaustive-deps }, [checkoutData]); // We only need registerData here because we don't want to send the metadata twice (we send it once the tx is sent) - const onCurrencySwitch = (type: CurrencyType) => { + const handleCurrencySwitch = (type: CurrencyType) => { setReloadingPrice(true); if (type !== CurrencyType.ETH) setLoadingPrice(true); setReducedDuration(0); - setDisplayedCurrency(type); + onCurrencySwitch(type); setHasUserSelectedOffer(false); }; @@ -711,42 +241,9 @@ const CheckoutCard: FunctionComponent = ({ [updateFormState] ); - useEffect(() => { - const duration = formState.duration; - if (!invalidBalance || !priceInEth) { - if (!reducedDuration) setReducedDuration(0); - return; - } - let found = false; - for (let newDuration = duration - 1; newDuration > 0; newDuration--) { - const newPriceInEth = getPriceForDuration(priceInEth, newDuration); - let newPrice = newPriceInEth; - if (displayedCurrency !== CurrencyType.ETH && quoteData) - newPrice = getDomainPriceAltcoin(quoteData.quote, newPriceInEth); - const balance = tokenBalances[displayedCurrency]; - if (!balance) continue; - if (BigInt(balance) >= BigInt(newPrice)) { - if (reducedDuration !== newDuration) setReducedDuration(newDuration); - found = true; - break; - } - } - if (!found && reducedDuration) setReducedDuration(0); - }, [ - formState.duration, - invalidBalance, - discount.duration, - priceInEth, - formState.isUpselled, - tokenBalances, - displayedCurrency, - quoteData, - reducedDuration, - ]); - return ( <> - {formState.duration === 1 ? ( + {formState.durationInYears === 1 ? ( = ({ ) : null} {reducedDuration > 0 && invalidBalance && - reducedDuration !== formState.duration ? ( + reducedDuration !== formState.durationInYears * 365 ? ( @@ -771,32 +268,35 @@ const CheckoutCard: FunctionComponent = ({
setTermsBox(!termsBox)} + onChangeTermsBox={onChangeTermsBox} termsBox={termsBox} - onChangeRenewalBox={() => setRenewalBox(!renewalBox)} + onChangeRenewalBox={onChangeRenewalBox} renewalBox={renewalBox} - ethRenewalPrice={ - type === FormType.REGISTER ? priceInEth : undefined - } showMainDomainBox={type === FormType.REGISTER && hasMainDomain} mainDomainBox={mainDomainBox} - onChangeMainDomainBox={() => setMainDomainBox(!mainDomainBox)} + onChangeMainDomainBox={onChangeMainDomainBox} domain={getDomainWithStark( Object.keys(formState.selectedDomains)[0] )} @@ -813,8 +313,8 @@ const CheckoutCard: FunctionComponent = ({ disabled={ domainsMinting === formState.selectedDomains || !account || - !formState.duration || - formState.duration < 1 || + !formState.durationInYears || + formState.durationInYears < 1 || invalidBalance || !termsBox } @@ -829,6 +329,12 @@ const CheckoutCard: FunctionComponent = ({
+ setCurrencyError(false)} + > +

Failed to get token quote. Please use ETH for now.

+
); }; diff --git a/components/domains/steps/reduceDuration.tsx b/components/domains/steps/reduceDuration.tsx index e35623cb..61006086 100644 --- a/components/domains/steps/reduceDuration.tsx +++ b/components/domains/steps/reduceDuration.tsx @@ -6,7 +6,7 @@ import { CurrencyType } from "@/utils/constants"; type ReduceDurationProps = { currentDuration: number; newDuration: number; - updateFormState: ({ duration }: { duration: number }) => void; + updateFormState: ({ durationInYears }: { durationInYears: number }) => void; displayCurrency: CurrencyType; }; @@ -17,7 +17,7 @@ const ReduceDuration: FunctionComponent = ({ displayCurrency, }) => { const handleSwitchDuration = () => { - updateFormState({ duration: newDuration }); + updateFormState({ durationInYears: newDuration }); }; return (
diff --git a/components/domains/steps/userInfoForm.tsx b/components/domains/steps/userInfoForm.tsx index 3a3b62a3..a15f2c3b 100644 --- a/components/domains/steps/userInfoForm.tsx +++ b/components/domains/steps/userInfoForm.tsx @@ -63,7 +63,7 @@ const UserInfoForm: FunctionComponent = ({ function changeDuration( type: IncrementType, - value: number = formState.duration + value: number = formState.durationInYears ): void { let newValue = value; @@ -81,7 +81,7 @@ const UserInfoForm: FunctionComponent = ({ if (isNaN(newValue) || newValue > maxYearsToRegister || newValue < 1) return; updateFormState({ - duration: newValue, + durationInYears: newValue, isUpselled: newValue === 1 ? true : false, }); } @@ -108,8 +108,8 @@ const UserInfoForm: FunctionComponent = ({ const isDisabled = (): boolean => { return ( - !formState.duration || - formState.duration < 1 || + !formState.durationInYears || + formState.durationInYears < 1 || (formState.needMetadata && emailError) || (type === FormType.RENEW && !areDomainSelected(formState.selectedDomains)) ); @@ -154,7 +154,7 @@ const UserInfoForm: FunctionComponent = ({ /> ) : null} diff --git a/components/identities/actions/addEvmModal.tsx b/components/identities/actions/addEvmModal.tsx index 4c94c17d..9628f97e 100644 --- a/components/identities/actions/addEvmModal.tsx +++ b/components/identities/actions/addEvmModal.tsx @@ -4,7 +4,7 @@ import { Modal, TextField, } from "@mui/material"; -import { useContractWrite } from "@starknet-react/core"; +import { useSendTransaction } from "@starknet-react/core"; import React, { FunctionComponent, useEffect, useRef, useState } from "react"; import styles from "../../../styles/components/evmModalMessage.module.css"; import Button from "../../UI/button"; @@ -42,7 +42,7 @@ const AddEvmModal: FunctionComponent = ({ const abortControllerRef = useRef(null); const [isSendingTx, setIsSendingTx] = useState(false); - const { writeAsync: set_user_data, data: userData } = useContractWrite({ + const { sendAsync: set_user_data, data: userData } = useSendTransaction({ calls: identity && isValid ? [ diff --git a/components/identities/actions/changeAddressModal.tsx b/components/identities/actions/changeAddressModal.tsx index f510f2e3..c1df5a20 100644 --- a/components/identities/actions/changeAddressModal.tsx +++ b/components/identities/actions/changeAddressModal.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent, useEffect, useState } from "react"; import { TextField } from "@mui/material"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { isHexString, minifyAddress } from "../../../utils/stringService"; import { hexToDecimal } from "../../../utils/feltService"; import { useNotificationManager } from "../../../hooks/useNotificationManager"; @@ -30,8 +30,8 @@ const ChangeAddressModal: FunctionComponent = ({ const [isTxSent, setIsTxSent] = useState(false); const [isSendingTx, setIsSendingTx] = useState(false); - const { writeAsync: set_domain_to_address, data: domainToAddressData } = - useContractWrite({ + const { sendAsync: set_domain_to_address, data: domainToAddressData } = + useSendTransaction({ calls: identity ? identityChangeCalls.setStarknetAddress( identity, diff --git a/components/identities/actions/clickable/clickablePersonhoodIcon.tsx b/components/identities/actions/clickable/clickablePersonhoodIcon.tsx index 5518802a..7d060045 100644 --- a/components/identities/actions/clickable/clickablePersonhoodIcon.tsx +++ b/components/identities/actions/clickable/clickablePersonhoodIcon.tsx @@ -13,8 +13,8 @@ import { StarknetSignature, } from "@anima-protocol/personhood-sdk-react"; import { useAccount } from "@starknet-react/core"; -import { Call, TypedData, constants } from "starknet"; -import { useContractWrite } from "@starknet-react/core"; +import { Call, TypedData } from "starknet"; +import { useSendTransaction } from "@starknet-react/core"; import { hexToDecimal } from "../../../../utils/feltService"; import { bigintToStringHex, @@ -44,10 +44,10 @@ const ClickablePersonhoodIcon: FunctionComponent< const [sessionId, setSessionId] = useState(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [callData, setCallData] = useState(); + const [callData, setCallData] = useState([]); const { addTransaction } = useNotificationManager(); - const { writeAsync: execute, data: verifierData } = useContractWrite({ - calls: [callData as Call], + const { sendAsync: execute, data: verifierData } = useSendTransaction({ + calls: callData, }); const network = process.env.NEXT_PUBLIC_IS_TESTNET === "true" ? "testnet" : "mainnet"; @@ -70,7 +70,7 @@ const ClickablePersonhoodIcon: FunctionComponent< useEffect(() => { const executeVerification = () => { execute().finally(() => { - setCallData(undefined); + setCallData([]); setIsLoading(false); setIsOpen(false); setSessionId(undefined); @@ -124,7 +124,7 @@ const ClickablePersonhoodIcon: FunctionComponent< .then((response) => response.json()) .then((sig) => { const hexSessionId = "0x" + (sessionId as string).replace(/-/g, ""); - setCallData( + setCallData([ identityChangeCalls.writeVerifierData( process.env.NEXT_PUBLIC_VERIFIER_POP_CONTRACT as string, tokenId, @@ -132,8 +132,8 @@ const ClickablePersonhoodIcon: FunctionComponent< "proof_of_personhood", hexToDecimal(hexSessionId), [sig.r, sig.s] - ) - ); + ), + ]); }) .catch((error) => console.log("An error occurred while fetching signture", error) diff --git a/components/identities/actions/identityActions.tsx b/components/identities/actions/identityActions.tsx index 2535cf7c..625f2b8e 100644 --- a/components/identities/actions/identityActions.tsx +++ b/components/identities/actions/identityActions.tsx @@ -1,6 +1,6 @@ import React, { useContext, useMemo } from "react"; import { FunctionComponent, useEffect, useState } from "react"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import ChangeAddressModal from "./changeAddressModal"; import TransferFormModal from "./transferFormModal"; import SubdomainModal from "./subdomainModal"; @@ -66,8 +66,8 @@ const IdentityActions: FunctionComponent = ({ const [disableRenewalCalldata, setDisableRenewalCalldata] = useState( [] ); - const { writeAsync: disableRenewal, data: disableRenewalData } = - useContractWrite({ + const { sendAsync: disableRenewal, data: disableRenewalData } = + useSendTransaction({ calls: disableRenewalCalldata, }); @@ -102,7 +102,7 @@ const IdentityActions: FunctionComponent = ({ callDataEncodedDomain.push(domain.toString(10)); }); - const { writeAsync: setMainId, data: mainDomainData } = useContractWrite({ + const { sendAsync: setMainId, data: mainDomainData } = useSendTransaction({ calls: identity ? identityChangeCalls.setAsMainId( identity, diff --git a/components/identities/actions/subdomainModal.tsx b/components/identities/actions/subdomainModal.tsx index 5b23a7f6..bb26363f 100644 --- a/components/identities/actions/subdomainModal.tsx +++ b/components/identities/actions/subdomainModal.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent, useEffect, useState } from "react"; import { TextField } from "@mui/material"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { useIsValid } from "../../../hooks/naming"; import { numberToString } from "../../../utils/stringService"; import SelectIdentity from "../../domains/selectIdentity"; @@ -32,8 +32,8 @@ const SubdomainModal: FunctionComponent = ({ const [callData, setCallData] = useState([]); const { address } = useAccount(); const { addTransaction } = useNotificationManager(); - const { writeAsync: transfer_domain, data: transferDomainData } = - useContractWrite({ + const { sendAsync: transfer_domain, data: transferDomainData } = + useSendTransaction({ calls: callData, }); const [isTxSent, setIsTxSent] = useState(false); diff --git a/components/identities/actions/transferFormModal.tsx b/components/identities/actions/transferFormModal.tsx index 8c9faa69..65bd927e 100644 --- a/components/identities/actions/transferFormModal.tsx +++ b/components/identities/actions/transferFormModal.tsx @@ -5,7 +5,7 @@ import React, { useContext, } from "react"; import { TextField, InputAdornment } from "@mui/material"; -import { useContractWrite } from "@starknet-react/core"; +import { useSendTransaction } from "@starknet-react/core"; import { useRouter } from "next/router"; import { isHexString, minifyAddress } from "../../../utils/stringService"; import { utils } from "starknetid.js"; @@ -36,8 +36,8 @@ const TransferFormModal: FunctionComponent = ({ const [isTxSent, setIsTxSent] = useState(false); const [isSendingTx, setIsSendingTx] = useState(false); - const { writeAsync: transfer_identity_and_set_domain, data: transferData } = - useContractWrite({ + const { sendAsync: transfer_identity_and_set_domain, data: transferData } = + useSendTransaction({ calls: identity ? identityChangeCalls.transfer(identity, targetAddress) : [], diff --git a/components/solana/changeAddressModal.tsx b/components/solana/changeAddressModal.tsx index 6294dd55..900c9706 100644 --- a/components/solana/changeAddressModal.tsx +++ b/components/solana/changeAddressModal.tsx @@ -1,5 +1,5 @@ import { Modal, TextField } from "@mui/material"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import React, { FunctionComponent, useEffect, useState } from "react"; import { isHexString, minifyAddress } from "../../utils/stringService"; import styles from "../../styles/components/modalMessage.module.css"; @@ -30,7 +30,7 @@ const ChangeAddressModal: FunctionComponent = ({ const { addTransaction } = useNotificationManager(); const [isTxSent, setIsTxSent] = useState(false); const [callData, setCallData] = useState([]); - const { writeAsync: setResolving, data: resolvingData } = useContractWrite({ + const { sendAsync: setResolving, data: resolvingData } = useSendTransaction({ calls: callData, }); diff --git a/components/solana/domainActions.tsx b/components/solana/domainActions.tsx index e7685678..db711fba 100644 --- a/components/solana/domainActions.tsx +++ b/components/solana/domainActions.tsx @@ -12,8 +12,8 @@ import styles from "../../styles/solana.module.css"; import { Abi, Call } from "starknet"; import { useAccount, - useContractRead, - useContractWrite, + useReadContract, + useSendTransaction, } from "@starknet-react/core"; import SolanaCalls from "../../utils/callData/solanaCalls"; import { utils } from "starknetid.js"; @@ -36,16 +36,16 @@ const DomainActions: FunctionComponent = ({ name }) => { const { addTransaction } = useNotificationManager(); const [isTxSent, setIsTxSent] = useState(false); const [mainDomainCalldata, setMainDomainCalldata] = useState([]); - const { writeAsync: executeMainDomain, data: mainDomainData } = - useContractWrite({ + const { sendAsync: executeMainDomain, data: mainDomainData } = + useSendTransaction({ calls: mainDomainCalldata, }); const { contract } = useNamingContract(); const encodedDomain = utils .encodeDomain(`${name}.sol.stark`) .map((x) => x.toString()); - const { data: resolveData } = useContractRead({ - address: contract?.address as string, + const { data: resolveData } = useReadContract({ + address: contract?.address as HexString, abi: contract?.abi as Abi, functionName: "domain_to_address", args: [encodedDomain, []], diff --git a/context/FormProvider.tsx b/context/FormProvider.tsx index eac60f96..4fe11087 100644 --- a/context/FormProvider.tsx +++ b/context/FormProvider.tsx @@ -1,14 +1,14 @@ import { useAccount } from "@starknet-react/core"; -import React, { FunctionComponent, useState } from "react"; +import React, { FunctionComponent, useCallback, useState } from "react"; import { createContext, useMemo } from "react"; import { computeMetadataHash, generateSalt } from "@/utils/userDataService"; import useWhitelistedNFTs from "@/hooks/useWhitelistedNFTs"; -type FormState = { +export type FormState = { email: string; isSwissResident: boolean; tokenId: number; - duration: number; + durationInYears: number; selectedDomains: Record; // metadata salt?: string; @@ -34,7 +34,7 @@ const initialState: FormState = { email: "", isSwissResident: false, tokenId: 0, - duration: 1, + durationInYears: 1, selectedDomains: {}, needMetadata: false, salesTaxRate: 0, @@ -44,7 +44,9 @@ const initialState: FormState = { export const FormContext = createContext({ formState: initialState, + // eslint-disable-next-line @typescript-eslint/no-empty-function clearForm: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function updateFormState: () => {}, }); @@ -55,11 +57,11 @@ export const FormProvider: FunctionComponent = ({ children }) => { address as string ); - const updateFormState = (updates: Partial) => { + const updateFormState = useCallback((updates: Partial) => { setFormState((prevState) => ({ ...prevState, ...updates })); - }; + }, []); - const clearForm = () => { + const clearForm = useCallback(() => { setFormState((prevState) => ({ ...prevState, selectedDomains: {}, @@ -68,12 +70,12 @@ export const FormProvider: FunctionComponent = ({ children }) => { isUpselled: false, selectedPfp: undefined, })); - }; + }, []); // Handle metadataHash and salt useMemo(() => { updateFormState({ salt: generateSalt() }); - }, [formState.selectedDomains]); + }, [updateFormState]); useMemo(() => { if (!address) return; @@ -94,7 +96,7 @@ export const FormProvider: FunctionComponent = ({ children }) => { console.log("Error while fetching metadata:", err); updateFormState({ needMetadata: true }); }); - }, [address]); + }, [address, updateFormState]); useMemo(() => { if (!formState.salt || !formState.needMetadata) return; @@ -112,6 +114,7 @@ export const FormProvider: FunctionComponent = ({ children }) => { formState.email, formState.isSwissResident, formState.needMetadata, + updateFormState, ]); const contextValues = useMemo(() => { diff --git a/hooks/checkout/useCheckoutState.tsx b/hooks/checkout/useCheckoutState.tsx new file mode 100644 index 00000000..8e5ee35a --- /dev/null +++ b/hooks/checkout/useCheckoutState.tsx @@ -0,0 +1,100 @@ +import { useEffect, useMemo, useState } from "react"; +import { useDomainFromAddress } from "../naming"; +import { FormType } from "@/utils/constants"; + +export const useCheckoutState = (type: FormType, address?: string) => { + const [termsBox, setTermsBox] = useState(false); + const [renewalBox, setRenewalBox] = useState(true); + const [mainDomainBox, setMainDomainBox] = useState(true); + const [hasUserSelectedOffer, setHasUserSelectedOffer] = + useState(false); + const [hasReverseAddressRecord, setHasReverseAddressRecord] = + useState(false); + const [domainsMinting, setDomainsMinting] = + useState>(); + const [reducedDuration, setReducedDuration] = useState(0); + const [nonSubscribedDomains, setNonSubscribedDomains] = useState(); + const [sponsor, setSponsor] = useState("0x0"); + const mainDomain = useDomainFromAddress(address ?? ""); + + const hasMainDomain = useMemo(() => { + if (!mainDomain || !mainDomain.domain) return false; + return mainDomain.domain.endsWith(".stark"); + }, [mainDomain]); + + useEffect(() => { + const referralData = localStorage.getItem("referralData"); + if (referralData) { + const data = JSON.parse(referralData); + if (data.sponsor && data?.expiry >= new Date().getTime()) { + setSponsor(data.sponsor); + } else { + setSponsor("0"); + } + } else { + setSponsor("0"); + } + }, []); + + useEffect(() => { + if (!address) return; + fetch(`${process.env.NEXT_PUBLIC_SERVER_LINK}/addr_has_rev?addr=${address}`) + .then((response) => response.json()) + .then((reverseAddressData) => { + setHasReverseAddressRecord(reverseAddressData.has_rev); + }); + }, [address, setHasReverseAddressRecord]); + + // we get the list of domains that do not have a autorenewal already enabled + useEffect(() => { + if (type !== FormType.RENEW) return; + if (address) { + fetch( + `${process.env.NEXT_PUBLIC_SERVER_LINK}/renewal/get_non_subscribed_domains?addr=${address}` + ) + .then((response) => response.json()) + .then((data) => { + setNonSubscribedDomains(data); + }); + } + }, [address, renewalBox, type]); + + // we get the list of domains that do not have a autorenewal already enabled + useEffect(() => { + if (type !== FormType.RENEW) return; + if (address) { + fetch( + `${process.env.NEXT_PUBLIC_SERVER_LINK}/renewal/get_non_subscribed_domains?addr=${address}` + ) + .then((response) => response.json()) + .then((data) => { + setNonSubscribedDomains(data); + }); + } + }, [address, renewalBox, type]); + + const onChangeTermsBox = () => setTermsBox(!termsBox); + const onChangeRenewalBox = () => setRenewalBox(!renewalBox); + const onChangeMainDomainBox = () => setMainDomainBox(!mainDomainBox); + + return { + termsBox, + renewalBox, + mainDomainBox, + hasUserSelectedOffer, + hasReverseAddressRecord, + domainsMinting, + reducedDuration, + nonSubscribedDomains, + sponsor, + hasMainDomain, + setHasUserSelectedOffer, + setHasReverseAddressRecord, + setDomainsMinting, + setReducedDuration, + setNonSubscribedDomains, + onChangeTermsBox, + onChangeRenewalBox, + onChangeMainDomainBox, + }; +}; diff --git a/hooks/checkout/useCurrencyManagement.tsx b/hooks/checkout/useCurrencyManagement.tsx new file mode 100644 index 00000000..db49e639 --- /dev/null +++ b/hooks/checkout/useCurrencyManagement.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect } from "react"; +import { CurrencyType, ERC20Contract } from "@/utils/constants"; +import { getTokenQuote } from "@/utils/altcoinService"; + +export const useCurrencyManagement = () => { + const [displayedCurrency, setDisplayedCurrency] = useState( + CurrencyType.ETH + ); + const [quoteData, setQuoteData] = useState(null); + const [currencyError, setCurrencyError] = useState(false); + const [hasChosenCurrency, setHasChosenCurrency] = useState(false); + + const onCurrencySwitch = (currency: CurrencyType) => { + setDisplayedCurrency(currency); + setHasChosenCurrency(true); + }; + + // we fetch the quote for the currency selected + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + const fetchQuote = async () => { + if (displayedCurrency === CurrencyType.ETH) { + setQuoteData(null); + return; + } + const data = await getTokenQuote(ERC20Contract[displayedCurrency]); + if (data) { + setQuoteData(data); + setCurrencyError(false); + } else { + setDisplayedCurrency(CurrencyType.ETH); + setCurrencyError(true); + } + }; + + const scheduleRefetch = () => { + const now = parseInt((new Date().getTime() / 1000).toFixed(0)); + const timeLimit = now - 60; // 60 seconds + if (!quoteData || displayedCurrency === CurrencyType.ETH) { + setQuoteData(null); + return; + } + + if (quoteData.max_quote_validity <= timeLimit) { + fetchQuote(); + } + + const timeUntilNextCheck = quoteData.max_quote_validity - timeLimit; + timeoutId = setTimeout( + scheduleRefetch, + Math.max(15000, timeUntilNextCheck * 100) + ); + }; + + fetchQuote(); + scheduleRefetch(); + + return () => clearTimeout(timeoutId); + }, [displayedCurrency, quoteData]); + + return { + displayedCurrency, + quoteData, + currencyError, + hasChosenCurrency, + onCurrencySwitch, + setHasChosenCurrency, + setCurrencyError, + }; +}; diff --git a/hooks/checkout/usePriceManagement.tsx b/hooks/checkout/usePriceManagement.tsx new file mode 100644 index 00000000..f5686d8b --- /dev/null +++ b/hooks/checkout/usePriceManagement.tsx @@ -0,0 +1,218 @@ +import { useState, useEffect } from "react"; +import { CurrencyType, swissVatRate } from "@/utils/constants"; +import { getManyDomainsPriceWei } from "@/utils/priceService"; +import { + getAutoRenewAllowance, + getDomainPriceAltcoin, + getYearlyPrice, + smartCurrencyChoosing, +} from "@/utils/altcoinService"; +import { applyRateToBigInt } from "@/utils/feltService"; +import { FormState } from "@/context/FormProvider"; + +export const usePriceManagement = ( + formState: { + selectedDomains: Record; + isUpselled: boolean; + durationInYears: number; + salesTaxRate: number; + isSwissResident: boolean; + needMetadata: boolean; + }, + displayedCurrency: CurrencyType, + quoteData: QuoteQueryData | null, + discount: { paidDurationInDays: number; durationInDays: number }, + tokenBalances: Record, + hasChosenCurrency: boolean, + onCurrencySwitch: (currency: CurrencyType) => void, + setHasChosenCurrency: (value: boolean) => void, + updateFormState: (updates: Partial) => void +) => { + const [dailyPriceInEth, setDailyPriceInEth] = useState(BigInt(0)); + const [discountedPriceInEth, setDiscountedPriceInEth] = useState( + BigInt(0) + ); + const [price, setPrice] = useState(BigInt(0)); + const [discountedPrice, setDiscountedPrice] = useState(BigInt(0)); + const [loadingPrice, setLoadingPrice] = useState(false); + const [reloadingPrice, setReloadingPrice] = useState(false); + const [salesTaxAmount, setSalesTaxAmount] = useState(BigInt(0)); + const [maxPriceRange, setMaxPriceRange] = useState(BigInt(0)); + const [invalidBalance, setInvalidBalance] = useState(false); + + useEffect(() => { + if (!formState.selectedDomains) return; + const _dailyPriceInEth = getManyDomainsPriceWei( + formState.selectedDomains, + 1 + ); + setDailyPriceInEth(_dailyPriceInEth); + setDiscountedPriceInEth( + _dailyPriceInEth * BigInt(discount.paidDurationInDays) + ); + }, [formState.selectedDomains, discount.paidDurationInDays]); + + useEffect(() => { + const discountedPrice = + dailyPriceInEth * BigInt(discount.paidDurationInDays); + if (formState.isUpselled) { + if (displayedCurrency === CurrencyType.ETH) { + setDiscountedPrice(discountedPrice); + } else if (quoteData) { + const priceInAltcoin = getDomainPriceAltcoin( + quoteData.quote, + discountedPrice + ); + setDiscountedPrice(priceInAltcoin); + } + } else { + setDiscountedPrice(BigInt(0)); + } + }, [ + dailyPriceInEth, + formState.isUpselled, + displayedCurrency, + quoteData, + discount, + ]); + + useEffect(() => { + if (!dailyPriceInEth) return; + const _price = getManyDomainsPriceWei( + formState.selectedDomains, + formState.isUpselled + ? discount.durationInDays + : formState.durationInYears * 365 + ); + if (displayedCurrency === CurrencyType.ETH) { + setPrice(_price); + } else if (quoteData) { + setPrice(getDomainPriceAltcoin(quoteData.quote, _price)); + } + setLoadingPrice(false); + if (reloadingPrice) setReloadingPrice(false); + }, [ + dailyPriceInEth, + quoteData, + displayedCurrency, + formState.isUpselled, + formState.durationInYears, + discount.durationInDays, + formState.selectedDomains, + reloadingPrice, + ]); + + useEffect(() => { + const _price = formState.isUpselled ? discountedPrice : price; + if (!formState.needMetadata && _price) { + setSalesTaxAmount(applyRateToBigInt(_price, formState.salesTaxRate)); + } else { + if (formState.isSwissResident) { + updateFormState({ salesTaxRate: swissVatRate }); + setSalesTaxAmount(applyRateToBigInt(_price, swissVatRate)); + } else { + updateFormState({ salesTaxRate: 0 }); + setSalesTaxAmount(BigInt(0)); + } + } + }, [ + formState.isSwissResident, + price, + formState.isUpselled, + discountedPrice, + formState.needMetadata, + formState.salesTaxRate, + updateFormState, + ]); + + useEffect(() => { + if (displayedCurrency !== CurrencyType.ETH && !quoteData) return; + const limitPrice = getAutoRenewAllowance( + displayedCurrency, + formState.salesTaxRate, + getYearlyPrice( + Object.keys(formState.selectedDomains)[0], + displayedCurrency, + quoteData?.quote + ) + ); + setMaxPriceRange(limitPrice); + }, [ + displayedCurrency, + formState.selectedDomains, + quoteData, + formState.salesTaxRate, + ]); + + // we choose the currency based on the user balances + useEffect(() => { + if ( + tokenBalances && + Object.keys(tokenBalances).length > 0 && + !hasChosenCurrency && + dailyPriceInEth + ) { + const domainPrice = formState.isUpselled + ? dailyPriceInEth * BigInt(discount.paidDurationInDays) + : dailyPriceInEth * BigInt(formState.durationInYears * 365); + smartCurrencyChoosing(tokenBalances, domainPrice).then((currency) => { + onCurrencySwitch(currency); + setHasChosenCurrency(true); + }); + } + }, [ + tokenBalances, + dailyPriceInEth, + formState.isUpselled, + hasChosenCurrency, + formState.durationInYears, + discount.paidDurationInDays, + setHasChosenCurrency, + onCurrencySwitch, + ]); + + // we ensure user has enough balance of the token selected + useEffect(() => { + if ( + tokenBalances && + price && + displayedCurrency && + !loadingPrice && + !reloadingPrice + ) { + const tokenBalance = tokenBalances[displayedCurrency]; + if (!tokenBalance) return; + const _price = formState.isUpselled ? discountedPrice : price; + if (tokenBalance && BigInt(tokenBalance) >= BigInt(_price)) + setInvalidBalance(false); + else setInvalidBalance(true); + } + }, [ + price, + discountedPrice, + formState.isUpselled, + displayedCurrency, + tokenBalances, + loadingPrice, + reloadingPrice, + ]); + + // Log salesTaxAmount when it changes + useEffect(() => { + console.log("Sales Tax Amount:", salesTaxAmount); + }, [salesTaxAmount]); + + return { + dailyPriceInEth, + discountedPriceInEth, + price, + discountedPrice, + loadingPrice, + reloadingPrice, + setLoadingPrice, + setReloadingPrice, + salesTaxAmount, + maxPriceRange, + invalidBalance, + }; +}; diff --git a/hooks/checkout/useRegisterTxPrep.tsx b/hooks/checkout/useRegisterTxPrep.tsx new file mode 100644 index 00000000..de3fe770 --- /dev/null +++ b/hooks/checkout/useRegisterTxPrep.tsx @@ -0,0 +1,187 @@ +import { useState, useEffect } from "react"; +import { + AutoRenewalContracts, + CurrencyType, + ERC20Contract, +} from "@/utils/constants"; +import { Call } from "starknet"; +import registrationCalls from "@/utils/callData/registrationCalls"; +import { utils } from "starknetid.js"; +import { FormState } from "@/context/FormProvider"; +import { getAutoRenewAllowance, getYearlyPrice } from "@/utils/altcoinService"; +import { getApprovalAmount } from "@/utils/priceService"; +import autoRenewalCalls from "@/utils/callData/autoRenewalCalls"; +import identityChangeCalls from "@/utils/callData/identityChangeCalls"; +import { hexToDecimal, toUint256 } from "@/utils/feltService"; + +export const useRegisterTxPrep = ( + formState: FormState, + displayedCurrency: CurrencyType, + quoteData: QuoteQueryData | null, + price: bigint, + discountedPrice: bigint, + salesTaxAmount: bigint, + sponsor: string, + hasMainDomain: boolean, + mainDomainBox: boolean, + hasReverseAddressRecord: boolean, + discount: { discountId: string; durationInDays: number }, + renewalBox: boolean, + allowanceStatus: AllowanceStatus +) => { + const [callData, setCallData] = useState([]); + const [tokenIdRedirect, setTokenIdRedirect] = useState("0x0"); + + // Set Register Multicall + useEffect(() => { + if (displayedCurrency !== CurrencyType.ETH && !quoteData) return; + // Variables + const newTokenId: number = Math.floor(Math.random() * 1000000000000); + const txMetadataHash = `0x${formState.metadataHash}` as HexString; + const encodedDomain = utils + .encodeDomain(Object.keys(formState.selectedDomains)[0]) + .map((element) => element.toString())[0]; + const finalDurationInDays = formState.isUpselled + ? discount.durationInDays + : formState.durationInYears * 365; + const priceToPay = formState.isUpselled ? discountedPrice : price; + + // Common calls + const calls = [ + registrationCalls.approve(priceToPay, ERC20Contract[displayedCurrency]), + ]; + + if (displayedCurrency === CurrencyType.ETH) { + calls.push( + registrationCalls.buy( + encodedDomain, + formState.tokenId === 0 ? newTokenId : formState.tokenId, + sponsor, + finalDurationInDays, + txMetadataHash, + formState.isUpselled ? discount.discountId : "0" + ) + ); + } else { + calls.push( + registrationCalls.altcoinBuy( + encodedDomain, + formState.tokenId === 0 ? newTokenId : formState.tokenId, + sponsor, + finalDurationInDays, + txMetadataHash, + ERC20Contract[displayedCurrency], + quoteData as QuoteQueryData, + formState.isUpselled ? discount.discountId : "0" + ) + ); + } + + // If the user is a Swiss resident, we add the sales tax + if (formState.salesTaxRate) { + calls.unshift( + registrationCalls.vatTransfer( + salesTaxAmount, + ERC20Contract[displayedCurrency] + ) + ); // IMPORTANT: We use unshift to put the call at the beginning of the array + } + + // If the user choose to mint a new identity + let tokenIdToUse = formState.tokenId; + if (formState.tokenId === 0) { + calls.unshift(registrationCalls.mint(newTokenId)); // IMPORTANT: We use unshift to put the call at the beginning of the array + tokenIdToUse = newTokenId; + } + setTokenIdRedirect(tokenIdToUse.toString()); + + // If the user does not have a main domain or has checked the mainDomain box + if (!hasMainDomain || mainDomainBox) { + if (hasReverseAddressRecord) + calls.push(registrationCalls.resetAddrToDomain()); + calls.push(registrationCalls.mainId(tokenIdToUse)); + } + + if (renewalBox) { + if (allowanceStatus.needsAllowance) { + const amountToApprove = getApprovalAmount( + price, + salesTaxAmount, + discountedPrice + ? Number( + BigInt(discount.durationInDays) / BigInt(finalDurationInDays) + ) + : formState.durationInYears, + allowanceStatus.currentAllowance + ); + + calls.push( + autoRenewalCalls.approve( + ERC20Contract[displayedCurrency], + AutoRenewalContracts[displayedCurrency], + amountToApprove + ) + ); + } + + const allowance = getAutoRenewAllowance( + displayedCurrency, + formState.salesTaxRate, + getYearlyPrice( + Object.keys(formState.selectedDomains)[0], + displayedCurrency, + quoteData?.quote + ) + ); + calls.push( + autoRenewalCalls.enableRenewal( + AutoRenewalContracts[displayedCurrency], + encodedDomain, + allowance, + `0x${formState.metadataHash}` + ) + ); + } + + // if the user has selected a profile picture + if (formState.selectedPfp) { + const nftData = formState.selectedPfp; + const nft_id = toUint256(nftData.token_id); + calls.push( + identityChangeCalls.updateProfilePicture( + hexToDecimal(nftData.contract_address), + nft_id.low, + nft_id.high, + tokenIdToUse.toString() + ) + ); + } + + // Merge and set the call data + setCallData(calls); + }, [ + formState.tokenId, + formState.isUpselled, + formState.durationInYears, + price, + formState.selectedDomains, + hasMainDomain, + sponsor, + formState.metadataHash, + formState.salesTaxRate, + mainDomainBox, + salesTaxAmount, + quoteData, + displayedCurrency, + formState.selectedPfp, + discount.durationInDays, + discount.discountId, + discountedPrice, + hasReverseAddressRecord, + renewalBox, + allowanceStatus.needsAllowance, + allowanceStatus.currentAllowance, + ]); + + return { callData, tokenIdRedirect }; +}; diff --git a/hooks/checkout/useRenewalTxPrep.tsx b/hooks/checkout/useRenewalTxPrep.tsx new file mode 100644 index 00000000..df02782a --- /dev/null +++ b/hooks/checkout/useRenewalTxPrep.tsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from "react"; +import { + AutoRenewalContracts, + CurrencyType, + ERC20Contract, +} from "@/utils/constants"; +import { Call } from "starknet"; +import registrationCalls from "@/utils/callData/registrationCalls"; +import { utils } from "starknetid.js"; +import { FormState } from "@/context/FormProvider"; +import { getAutoRenewAllowance, getYearlyPrice } from "@/utils/altcoinService"; +import { getApprovalAmount } from "@/utils/priceService"; +import autoRenewalCalls from "@/utils/callData/autoRenewalCalls"; +import identityChangeCalls from "@/utils/callData/identityChangeCalls"; +import { hexToDecimal, toUint256 } from "@/utils/feltService"; +import { + selectedDomainsToArray, + selectedDomainsToEncodedArray, +} from "@/utils/stringService"; + +export const useRenewalTxPrep = ( + formState: FormState, + displayedCurrency: CurrencyType, + quoteData: QuoteQueryData | null, + price: bigint, + discountedPrice: bigint, + salesTaxAmount: bigint, + sponsor: string, + discount: { discountId: string; durationInDays: number }, + renewalBox: boolean, + allowanceStatus: AllowanceStatus, + nonSubscribedDomains: string[] | undefined +) => { + const [callData, setCallData] = useState([]); + + useEffect(() => { + if (displayedCurrency !== CurrencyType.ETH && !quoteData) return; + + // Variables + const finalDurationInDays = formState.isUpselled + ? discount.durationInDays + : formState.durationInYears * 365; + const priceToPay = formState.isUpselled ? discountedPrice : price; + const txMetadataHash = `0x${formState.metadataHash}` as HexString; + + // Common calls + const calls = [ + registrationCalls.approve(priceToPay, ERC20Contract[displayedCurrency]), + ]; + + if (displayedCurrency === CurrencyType.ETH) { + calls.push( + ...registrationCalls.multiCallRenewal( + selectedDomainsToEncodedArray(formState.selectedDomains), + finalDurationInDays, + txMetadataHash, + sponsor, + formState.isUpselled ? discount.discountId : "0" + ) + ); + } else { + calls.push( + ...registrationCalls.multiCallRenewalAltcoin( + selectedDomainsToEncodedArray(formState.selectedDomains), + finalDurationInDays, + txMetadataHash, + ERC20Contract[displayedCurrency], + quoteData as QuoteQueryData, + sponsor, + formState.isUpselled ? discount.discountId : "0" + ) + ); + } + + // If the user is a Swiss resident, we add the sales tax + if (formState.salesTaxRate) { + calls.unshift( + registrationCalls.vatTransfer( + salesTaxAmount, + ERC20Contract[displayedCurrency] + ) + ); // IMPORTANT: We use unshift to put the call at the beginning of the array + } + + // If the user has toggled autorenewal + if (renewalBox) { + if (allowanceStatus.needsAllowance) { + const amountToApprove = getApprovalAmount( + price, + salesTaxAmount, + discountedPrice + ? discount.durationInDays / finalDurationInDays + : formState.durationInYears, + allowanceStatus.currentAllowance + ); + + calls.push( + autoRenewalCalls.approve( + ERC20Contract[displayedCurrency], + AutoRenewalContracts[displayedCurrency], + amountToApprove + ) + ); + } + + selectedDomainsToArray(formState.selectedDomains).map((domain) => { + // we enable renewal only for the domains that are not already subscribed + if (nonSubscribedDomains?.includes(domain)) { + const encodedDomain = utils + .encodeDomain(domain) + .map((element) => element.toString())[0]; + + const domainPrice = getYearlyPrice( + domain, + displayedCurrency, + quoteData?.quote + ); + const allowance = getAutoRenewAllowance( + displayedCurrency, + formState.salesTaxRate, + domainPrice + ); + + calls.push( + autoRenewalCalls.enableRenewal( + AutoRenewalContracts[displayedCurrency], + encodedDomain, + allowance, + txMetadataHash + ) + ); + } + }); + } + + // if the user has selected a profile picture + if (formState.selectedPfp) { + const nftData = formState.selectedPfp; + const nft_id = toUint256(nftData.token_id); + calls.push( + identityChangeCalls.updateProfilePicture( + hexToDecimal(nftData.contract_address), + nft_id.low, + nft_id.high, + formState.tokenId.toString() + ) + ); + } + + // Set the call data + setCallData(calls); + }, [ + formState.tokenId, + formState.isUpselled, + formState.durationInYears, + price, + formState.selectedDomains, + sponsor, + formState.metadataHash, + formState.salesTaxRate, + renewalBox, + salesTaxAmount, + allowanceStatus.needsAllowance, + allowanceStatus.currentAllowance, + quoteData, + displayedCurrency, + formState.selectedPfp, + discount.durationInDays, + discount.discountId, + discountedPrice, + nonSubscribedDomains, + ]); + + return { callData }; +}; diff --git a/hooks/contracts.ts b/hooks/contracts.ts index 43ea5df1..e2f1b320 100644 --- a/hooks/contracts.ts +++ b/hooks/contracts.ts @@ -14,69 +14,69 @@ import { Abi } from "starknet"; export function useStarknetIdContract() { return useContract({ abi: starknet_id_abi as Abi, - address: process.env.NEXT_PUBLIC_IDENTITY_CONTRACT, + address: process.env.NEXT_PUBLIC_IDENTITY_CONTRACT as HexString, }); } export function useNamingContract() { return useContract({ abi: naming_abi as Abi, - address: process.env.NEXT_PUBLIC_NAMING_CONTRACT, + address: process.env.NEXT_PUBLIC_NAMING_CONTRACT as HexString, }); } export function usePricingContract() { return useContract({ abi: pricing_abi as Abi, - address: process.env.NEXT_PUBLIC_PRICING_CONTRACT, + address: process.env.NEXT_PUBLIC_PRICING_CONTRACT as HexString, }); } export function useVerifierIdContract() { return useContract({ abi: verifier_abi as Abi, - address: process.env.NEXT_PUBLIC_VERIFIER_CONTRACT, + address: process.env.NEXT_PUBLIC_VERIFIER_CONTRACT as HexString, }); } export function useEtherContract() { return useContract({ abi: erc20_abi as Abi, - address: process.env.NEXT_PUBLIC_ETHER_CONTRACT, + address: process.env.NEXT_PUBLIC_ETHER_CONTRACT as HexString, }); } export function useBraavosNftContract() { return useContract({ abi: braavosNFT_abi as Abi, - address: process.env.NEXT_PUBLIC_BRAAVOS_SHIELD_CONTRACT, + address: process.env.NEXT_PUBLIC_BRAAVOS_SHIELD_CONTRACT as HexString, }); } export function useRenewalContract() { return useContract({ abi: renewal_abi.abi as Abi, - address: process.env.NEXT_PUBLIC_RENEWAL_CONTRACT, + address: process.env.NEXT_PUBLIC_RENEWAL_CONTRACT as HexString, }); } export function useNftPpVerifierContract() { return useContract({ abi: nft_pp_verifier_abi.abi as Abi, - address: process.env.NEXT_PUBLIC_NFT_PP_VERIFIER, + address: process.env.NEXT_PUBLIC_NFT_PP_VERIFIER as HexString, }); } export function useSolSubdomainContract() { return useContract({ abi: sol_subdomain_abi.abi as Abi, - address: process.env.NEXT_PUBLIC_SOL_SUBDOMAINS, + address: process.env.NEXT_PUBLIC_SOL_SUBDOMAINS as HexString, }); } export function useMulticallContract() { return useContract({ abi: multicall_abi.abi as Abi, - address: process.env.NEXT_PUBLIC_MULTICALL_CONTRACT, + address: process.env.NEXT_PUBLIC_MULTICALL_CONTRACT as HexString, }); } diff --git a/hooks/naming.ts b/hooks/naming.ts index 8609b5f6..8641b165 100644 --- a/hooks/naming.ts +++ b/hooks/naming.ts @@ -1,4 +1,4 @@ -import { useContractRead } from "@starknet-react/core"; +import { useReadContract } from "@starknet-react/core"; import { useNamingContract } from "./contracts"; import { useContext, useEffect, useState } from "react"; import { utils } from "starknetid.js"; @@ -105,8 +105,8 @@ export function useDataFromDomain(domain: string): FullDomainData { ? utils.encodeDomain(domain).map((elem) => elem.toString()) // remove when dapp uses starknet.js v5 : []; - const { data, error } = useContractRead({ - address: contract?.address as string, + const { data, error } = useReadContract({ + address: contract?.address as HexString, abi: contract?.abi as Abi, functionName: "domain_to_data", args: [encoded], diff --git a/hooks/paymaster.tsx b/hooks/paymaster.tsx index be15b6f7..871b1847 100644 --- a/hooks/paymaster.tsx +++ b/hooks/paymaster.tsx @@ -13,7 +13,7 @@ import { useProvider, useAccount, useConnect, - useContractWrite, + useSendTransaction, } from "@starknet-react/core"; import { AccountInterface, @@ -28,6 +28,11 @@ import isStarknetDeployed from "./isDeployed"; import { gaslessOptions } from "@/utils/constants"; import { decimalToHex } from "@/utils/feltService"; +type ErrorMessage = { + message: string; + short: string; +}; + const usePaymaster = ( callData: Call[], then: (transactionHash: string) => void, @@ -44,13 +49,14 @@ const usePaymaster = ( ); const [gasTokenPrice, setGasTokenPrice] = useState(); const [loadingGas, setLoadingGas] = useState(false); - const { writeAsync: execute, data } = useContractWrite({ + const { sendAsync: execute, data } = useSendTransaction({ calls: callData, }); const { connector } = useConnect(); const { isDeployed, deploymentData } = isStarknetDeployed(account?.address); const [deploymentTypedData, setDeploymentTypedData] = useState(); const [invalidTx, setInvalidTx] = useState(false); + const [txError, setTxError] = useState(); const argentWallet = useMemo( () => connector?.id === "argentX" /*|| connector?.id === "argentMobile"*/, @@ -105,6 +111,10 @@ const usePaymaster = ( ) .catch((e) => { console.error(e); + const stringError = e.toString(); + if (stringError.includes("Invalid signature")) + setTxError({ message: "", short: "Invalid signature" }); + else setTxError({ message: stringError, short: "TX error" }); setInvalidTx(true); }); }, @@ -123,6 +133,7 @@ const usePaymaster = ( if (!account || !gasTokenPrice || !gaslessCompatibility || loadingCallData) return; setLoadingGas(true); + setInvalidTx(false); estimateCalls(account, callData).then((fees) => { if (!fees) return; setInvalidTx(false); @@ -147,7 +158,7 @@ const usePaymaster = ( ]); const loadingDeploymentData = - connector?.id !== "argentMobile" && !isDeployed && !deploymentData; + connector?.id === "argentX" && !isDeployed && !deploymentData; useEffect(() => { if ( @@ -258,6 +269,7 @@ const usePaymaster = ( refreshRewards, invalidTx, loadingTypedData, + txError, }; }; diff --git a/hooks/useAllowanceCheck.tsx b/hooks/useAllowanceCheck.tsx index 2daf4464..2bbfdd68 100644 --- a/hooks/useAllowanceCheck.tsx +++ b/hooks/useAllowanceCheck.tsx @@ -1,4 +1,4 @@ -import { useContractRead } from "@starknet-react/core"; +import { useReadContract } from "@starknet-react/core"; import { useEtherContract } from "./contracts"; import { Abi } from "starknet"; import { useEffect, useState } from "react"; @@ -12,11 +12,12 @@ import { export default function useAllowanceCheck( erc20: CurrencyType, address?: string -) { +): AllowanceStatus { const [needsAllowance, setNeedsAllowance] = useState(false); + const [currentAllowance, setCurrentAllowance] = useState(BigInt(0)); const { contract: etherContract } = useEtherContract(); const { data: erc20AllowanceData, error: erc20AllowanceError } = - useContractRead({ + useReadContract({ address: ERC20Contract[erc20], abi: etherContract?.abi as Abi, functionName: "allowance", @@ -25,6 +26,7 @@ export default function useAllowanceCheck( useEffect(() => { const erc20AllowanceRes = erc20AllowanceData as CallResult; + if ( erc20AllowanceError || (erc20AllowanceRes && @@ -32,10 +34,12 @@ export default function useAllowanceCheck( erc20AllowanceRes["remaining"].high !== UINT_128_MAX) ) { setNeedsAllowance(true); + setCurrentAllowance(BigInt(erc20AllowanceRes["remaining"].low)); } else { setNeedsAllowance(false); + setCurrentAllowance(BigInt(0)); } }, [erc20AllowanceData, erc20AllowanceError]); - return needsAllowance; + return { needsAllowance, currentAllowance }; } diff --git a/hooks/useBalances.tsx b/hooks/useBalances.tsx index 7fad5210..66a41a12 100644 --- a/hooks/useBalances.tsx +++ b/hooks/useBalances.tsx @@ -1,4 +1,4 @@ -import { useContractRead } from "@starknet-react/core"; +import { useReadContract } from "@starknet-react/core"; import { useMulticallContract } from "./contracts"; import { Abi, BlockTag, CairoCustomEnum, Call, RawArgs, hash } from "starknet"; import { useEffect, useState } from "react"; @@ -9,8 +9,8 @@ export default function useBalances(address?: string) { const [balances, setBalances] = useState({}); const [callData, setCallData] = useState([]); const { contract: multicallContract } = useMulticallContract(); - const { data: erc20BalanceData, error: erc20BalanceError } = useContractRead({ - address: multicallContract?.address as string, + const { data: erc20BalanceData, error: erc20BalanceError } = useReadContract({ + address: multicallContract?.address as HexString, abi: multicallContract?.abi as Abi, functionName: "aggregate", args: callData, diff --git a/hooks/useHasClaimSolSubdomain.ts b/hooks/useHasClaimSolSubdomain.ts index 0a05298a..a1109247 100644 --- a/hooks/useHasClaimSolSubdomain.ts +++ b/hooks/useHasClaimSolSubdomain.ts @@ -1,4 +1,4 @@ -import { useContractRead } from "@starknet-react/core"; +import { useReadContract } from "@starknet-react/core"; import { useSolSubdomainContract } from "./contracts"; import { Abi } from "starknet"; import { useEffect, useState } from "react"; @@ -11,8 +11,8 @@ export default function useHasClaimSolSubdomain( ) { const [claimedDomains, setClaimedDomains] = useState([]); const { contract } = useSolSubdomainContract(); - const { data: claimedData, error: claimedError } = useContractRead({ - address: contract?.address as string, + const { data: claimedData, error: claimedError } = useReadContract({ + address: contract?.address as HexString, abi: contract?.abi as Abi, functionName: "were_claimed", args: [ diff --git a/hooks/useNeedAllowances.tsx b/hooks/useNeedAllowances.tsx index bbf12f4f..986ecb1f 100644 --- a/hooks/useNeedAllowances.tsx +++ b/hooks/useNeedAllowances.tsx @@ -1,25 +1,31 @@ -import { useContractRead } from "@starknet-react/core"; +import { useReadContract } from "@starknet-react/core"; import { useMulticallContract } from "./contracts"; import { Abi, BlockTag, CairoCustomEnum, Call, RawArgs, hash } from "starknet"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { ERC20Contract, CurrencyType, AutoRenewalContracts, } from "../utils/constants"; -import { fromUint256 } from "../utils/feltService"; +import { isApprovalInfinite } from "@/utils/priceService"; + +const createInitialAllowances = (): TokenNeedsAllowance => + Object.values(CurrencyType).reduce((acc, currency) => { + acc[currency] = { needsAllowance: false, currentAllowance: BigInt(0) }; + return acc; + }, {} as TokenNeedsAllowance); export default function useNeedsAllowances( address?: string ): TokenNeedsAllowance { - const [needsAllowances, setNeedsAllowances] = useState< - Record - >({}); + const initialAllowances = useMemo(() => createInitialAllowances(), []); + const [needsAllowances, setNeedsAllowances] = + useState(initialAllowances); const [callData, setCallData] = useState([]); const { contract: multicallContract } = useMulticallContract(); const { data: erc20AllowanceData, error: erc20AllowanceError } = - useContractRead({ - address: multicallContract?.address as string, + useReadContract({ + address: multicallContract?.address as HexString, abi: multicallContract?.abi as Abi, functionName: "aggregate", args: callData, @@ -66,16 +72,18 @@ export default function useNeedsAllowances( useEffect(() => { if (erc20AllowanceError || !erc20AllowanceData) return; const currencyNames = Object.values(CurrencyType); - const needsAllowancesEntries: Record = {}; - const erc20AllowanceRes = erc20AllowanceData as bigint[][]; + const newNeedsAllowances: TokenNeedsAllowance = {}; + const erc20AllowanceRes = erc20AllowanceData as CallResult[]; currencyNames.forEach((currency, index) => { - const balance = fromUint256( - BigInt(erc20AllowanceRes[index][0]), - BigInt(erc20AllowanceRes[index][1]) - ); - needsAllowancesEntries[currency] = balance === "0"; + newNeedsAllowances[currency] = { + needsAllowance: !isApprovalInfinite(erc20AllowanceRes[index][0]), + currentAllowance: erc20AllowanceRes[index][0], + }; }); - setNeedsAllowances(needsAllowancesEntries); + setNeedsAllowances((prevAllowances) => ({ + ...prevAllowances, + ...newNeedsAllowances, + })); }, [erc20AllowanceData, erc20AllowanceError]); return needsAllowances; diff --git a/hooks/useWhitelistedNFTs.tsx b/hooks/useWhitelistedNFTs.tsx index 1cb87fe0..3d02c206 100644 --- a/hooks/useWhitelistedNFTs.tsx +++ b/hooks/useWhitelistedNFTs.tsx @@ -1,4 +1,4 @@ -import { useContractRead } from "@starknet-react/core"; +import { useReadContract } from "@starknet-react/core"; import { useNftPpVerifierContract } from "./contracts"; import { Abi } from "starknet"; import { useEffect, useState } from "react"; @@ -11,8 +11,8 @@ export default function useWhitelistedNFTs(address: string) { ); const [userNfts, setUserNfts] = useState([]); const { contract } = useNftPpVerifierContract(); - const { data: whitelistData, error: whitelistError } = useContractRead({ - address: contract?.address as string, + const { data: whitelistData, error: whitelistError } = useReadContract({ + address: contract?.address as HexString, abi: contract?.abi as Abi, functionName: "get_whitelisted_contracts", args: [], diff --git a/package-lock.json b/package-lock.json index dd5c0904..5f09f878 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,8 @@ "@solana/wallet-adapter-react-ui": "^0.9.34", "@solana/wallet-adapter-wallets": "^0.19.23", "@solana/web3.js": "^1.87.6", - "@starknet-react/chains": "^0.1.7", - "@starknet-react/core": "^2.8.3", + "@starknet-react/chains": "^3.0.0", + "@starknet-react/core": "^3.0.0", "@vercel/analytics": "^0.1.5", "@walnuthq/sdk": "^1.1.10", "axios": "^1.4.0", @@ -45,7 +45,7 @@ "react-loader-spinner": "5.4.5", "starknet": "6.9.0", "starknetid.js": "^4.0.0", - "starknetkit": "^1.1.9", + "starknetkit": "^2.3.0", "tldts": "^6.1.20", "twitter-api-sdk": "^1.2.1" }, @@ -5563,9 +5563,9 @@ "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==" }, "node_modules/@scure/base": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", - "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.8.tgz", + "integrity": "sha512-6CyAclxj3Nb0XT7GHK6K4zK6k2xJm6E4Ft0Ohjt4WgegiFUHEtFb2CGzmPmGBwoIhrLsqNLYfLr04Y1GePrzZg==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -7418,31 +7418,50 @@ "@stablelib/wipe": "^1.0.1" } }, + "node_modules/@starknet-io/get-starknet": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet/-/get-starknet-4.0.0.tgz", + "integrity": "sha512-SmnRzBewS0BVjtKzViSrWXi+SvOnSrj9hnvlx8B3ZnCq9A2NuX8pNI550lDBLl/ilIr587FH2VNAj6jdgsyhJQ==", + "dependencies": { + "@starknet-io/get-starknet-core": "4.0.0", + "bowser": "^2.11.0" + } + }, + "node_modules/@starknet-io/get-starknet-core": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-core/-/get-starknet-core-4.0.0.tgz", + "integrity": "sha512-M++JTbMxZJ5wCkw1f4vAXCY3BTlRMdxFScqsIgZonLXD3GKHPyM/pFi/JqorPO1o4RKHLnFX6M7r0izZ/NWpvA==", + "dependencies": { + "@module-federation/runtime": "^0.1.2", + "@starknet-io/types-js": "^0.7.7" + } + }, "node_modules/@starknet-io/types-js": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.7.7.tgz", "integrity": "sha512-WLrpK7LIaIb8Ymxu6KF/6JkGW1sso988DweWu7p5QY/3y7waBIiPvzh27D9bX5KIJNRDyOoOVoHVEKYUYWZ/RQ==" }, "node_modules/@starknet-react/chains": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@starknet-react/chains/-/chains-0.1.7.tgz", - "integrity": "sha512-UNh97I1SvuJKaAhKOmpEk8JcWuZWMlPG/ba2HcvFYL9x/47BKndJ+Da9V+iJFtkHUjreVnajT1snsaz1XMG+UQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@starknet-react/chains/-/chains-3.0.0.tgz", + "integrity": "sha512-+XulhJZMSDAEX9JXHI5qJetHlf1Xr0nc7K2+efUMotGxXM31ovKWc/0JFlJh9Jwjgik/UO859Dh4yLkb92PAhw==" }, "node_modules/@starknet-react/core": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/@starknet-react/core/-/core-2.8.3.tgz", - "integrity": "sha512-WfiC5hVqA5FdinM2PPT3qEZGmGdh9ay7iRR8L1Ooc8bYKH6bpaXHLG+n42OHeenOWVXAH8U7ZYmhcF6OFWopCA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@starknet-react/core/-/core-3.0.0.tgz", + "integrity": "sha512-r3oZ5oGC2V3fNoCK1yua6yab4aJpmcuCnxN/RGGdTnJwy5GNO8q819qmsqBB7x61+zMCrwc+QN/FxvKGDbwJjw==", "dependencies": { - "@starknet-react/chains": "^0.1.7", - "@tanstack/react-query": "^5.0.1", + "@starknet-io/types-js": "^0.7.7", + "@starknet-react/chains": "^3.0.0", + "@tanstack/react-query": "^5.25.0", "eventemitter3": "^5.0.1", - "immutable": "^4.3.4", - "zod": "^3.22.2" + "viem": "^2.19.1", + "zod": "^3.22.4" }, "peerDependencies": { - "get-starknet-core": "^3.2.0", + "get-starknet-core": "^4.0.0", "react": "^18.0", - "starknet": "^5.25.0" + "starknet": "^6.11.0" } }, "node_modules/@starknet-react/core/node_modules/eventemitter3": { @@ -7465,27 +7484,27 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.48.0.tgz", - "integrity": "sha512-lZAfPPeVIqXCswE9SSbG33B6/91XOWt/Iq41bFeWb/mnHwQSIfFRbkS4bfs+WhIk9abRArF9Id2fp0Mgo+hq6Q==", + "version": "5.56.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz", + "integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.48.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.48.0.tgz", - "integrity": "sha512-GDExbjYWzvDokyRqMSWXdrPiYpp95Aig0oeMIrxTaruOJJgWiWfUP//OAaowm2RrRkGVsavSZdko/XmIrrV2Nw==", + "version": "5.56.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz", + "integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==", "dependencies": { - "@tanstack/query-core": "5.48.0" + "@tanstack/query-core": "5.56.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^18.0.0" + "react": "^18 || ^19" } }, "node_modules/@toruslabs/base-controllers": { @@ -9131,6 +9150,26 @@ "generate": "dist/generate.js" } }, + "node_modules/abitype": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.5.tgz", + "integrity": "sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -13302,11 +13341,6 @@ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, - "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==" - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -13926,6 +13960,20 @@ "ws": "*" } }, + "node_modules/isows": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.4.tgz", + "integrity": "sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -18870,24 +18918,26 @@ } }, "node_modules/starknetkit": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/starknetkit/-/starknetkit-1.1.9.tgz", - "integrity": "sha512-KarnNS9sJoImTdpTKizyNzDlQSAOutbzuZ6CzHQpJHWzaf8ION9aIf+d87sY7hSlbmD7cqGRUG28Hpke24arCg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/starknetkit/-/starknetkit-2.3.0.tgz", + "integrity": "sha512-xW78arRWHgXvAJuzM1kgiVWeAfG5t9QsxWLgET+pliuqXkBCJTbK/11UdByQ3ga69q+4JWEopDdChBI3YyP4Bw==", "dependencies": { + "@starknet-io/get-starknet": "^4.0.0", + "@starknet-io/get-starknet-core": "^4.0.0", + "@starknet-io/types-js": "^0.7.7", "@trpc/client": "^10.38.1", "@trpc/server": "^10.38.1", - "@walletconnect/sign-client": "^2.10.1", + "@walletconnect/sign-client": "^2.11.0", "bowser": "^2.11.0", "detect-browser": "^5.3.0", "eventemitter3": "^5.0.1", "events": "^3.3.0", - "get-starknet-core": "^3.1.0", "lodash-es": "^4.17.21", "svelte-forms": "^2.3.1", "trpc-browser": "^1.3.2" }, "peerDependencies": { - "starknet": "^6.7.0" + "starknet": "^6.9.0" } }, "node_modules/starknetkit/node_modules/detect-browser": { @@ -20230,6 +20280,64 @@ "node": ">= 0.8" } }, + "node_modules/viem": { + "version": "2.21.6", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.21.6.tgz", + "integrity": "sha512-YX48IVl6nZ4FRsY4ypv2RrxtQVWysIY146/lBW53tma8u32h8EsiA7vecw9ZbrueNUy/asHR4Egu68Z6FOvDzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.4.0", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.4.0", + "abitype": "1.0.5", + "isows": "1.0.4", + "webauthn-p256": "0.0.5", + "ws": "8.17.1" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, + "node_modules/viem/node_modules/@scure/bip39": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.4.0.tgz", + "integrity": "sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==", + "dependencies": { + "@noble/hashes": "~1.5.0", + "@scure/base": "~1.1.8" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/vite": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", @@ -20515,6 +20623,21 @@ "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.0.tgz", "integrity": "sha512-ohj72kbtVWCpKYMxcbJ+xaOBV3En76hW47j52dG+tEGG36LZQgfFw5yHl9xyjmosy3XUMn8d/GBUAy4YPM839w==" }, + "node_modules/webauthn-p256": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/webauthn-p256/-/webauthn-p256-0.0.5.tgz", + "integrity": "sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index a9496006..346a239f 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "@solana/wallet-adapter-react-ui": "^0.9.34", "@solana/wallet-adapter-wallets": "^0.19.23", "@solana/web3.js": "^1.87.6", - "@starknet-react/chains": "^0.1.7", - "@starknet-react/core": "^2.8.3", + "@starknet-react/chains": "^3.0.0", + "@starknet-react/core": "^3.0.0", "@vercel/analytics": "^0.1.5", "@walnuthq/sdk": "^1.1.10", "axios": "^1.4.0", @@ -46,8 +46,8 @@ "react-icons": "^4.4.0", "react-loader-spinner": "5.4.5", "starknet": "6.9.0", - "starknetid.js": "^4.0.0", - "starknetkit": "^1.1.9", + "starknetid.js": "^4.0.1", + "starknetkit": "^2.3.0", "tldts": "^6.1.20", "twitter-api-sdk": "^1.2.1" }, diff --git a/pages/_app.tsx b/pages/_app.tsx index 673fc288..2c545004 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -5,14 +5,17 @@ import Navbar from "../components/UI/navbar"; import Head from "next/head"; import { ThemeProvider } from "@mui/material"; import theme from "../styles/theme"; -import { StarknetConfig, jsonRpcProvider } from "@starknet-react/core"; +import { + Connector, + StarknetConfig, + jsonRpcProvider, +} from "@starknet-react/core"; import { Analytics } from "@vercel/analytics/react"; import { StarknetIdJsProvider } from "../context/StarknetIdJsProvider"; import { PostHogProvider } from "posthog-js/react"; import posthog from "posthog-js"; import AcceptCookies from "../components/legal/acceptCookies"; import { Chain, sepolia, mainnet } from "@starknet-react/chains"; -import { addWalnutLogsToConnectors } from "@walnuthq/sdk"; // Solana import { clusterApiUrl } from "@solana/web3.js"; import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; @@ -93,10 +96,11 @@ function MyApp({ Component, pageProps }: AppProps) { chains={chains} provider={providers} connectors={ - addWalnutLogsToConnectors({ - connectors: getConnectors(), - apiKey: process.env.NEXT_PUBLIC_WALNUT_API_KEY as string, - }) as any + getConnectors() as Connector[] + // addWalnutLogsToConnectors({ + // connectors: getConnectors(), + // apiKey: process.env.NEXT_PUBLIC_WALNUT_API_KEY as string, + // }) as any } autoConnect > diff --git a/pages/argent.tsx b/pages/argent.tsx index f1d62624..d7c2b397 100644 --- a/pages/argent.tsx +++ b/pages/argent.tsx @@ -1,109 +1,15 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import type { NextPage } from "next"; import homeStyles from "../styles/Home.module.css"; import DiscountEndScreen from "../components/discount/discountEndScreen"; -import DiscountOfferScreen from "../components/discount/discountOfferScreen"; - -// Create a new discount in utils to create a new discount campaign -import { argentDiscount } from "../utils/discounts/argent"; -import RegisterDiscount from "@/components/discount/registerDiscount"; -import styles from "../styles/discount.module.css"; -import { useAccount, useConnect } from "@starknet-react/core"; -import ConnectButton from "@/components/UI/connectButton"; -import ArgentIcon from "@/components/UI/iconsComponents/icons/argentIcon"; const Argent: NextPage = () => { - const [searchResult, setSearchResult] = useState(); - const [screen, setScreen] = useState(1); - const [isArgent, setIsArgent] = useState(true); - const { account } = useAccount(); - const connector = useConnect(); - - useEffect(() => { - const currentDate = new Date(); - const timestamp = currentDate.getTime(); - - if (timestamp >= argentDiscount.expiry) { - setScreen(0); - } - }, []); - - useEffect(() => { - if (!connector?.connector || !connector.connector?.id.includes("argent")) { - setIsArgent(false); - return; - } - setIsArgent(true); - }, [connector]); - - function goBack() { - setScreen(screen - 1); - } - - const handleSetScreen = (screen: number) => { - const referralData = { - sponsor: argentDiscount.sponsor, // the sponsor address - expiry: new Date().getTime() + 7 * 24 * 60 * 60 * 1000, // the current date of expiration + 1 week - }; - - localStorage.setItem("referralData", JSON.stringify(referralData)); - setScreen(screen); - }; - return (
- {!isArgent || !account ? ( -
-
- -
-
Connect Argent wallet
-
- To access this discount, you need to connect an Argent wallet. -
- {!account ? ( -
- -
- ) : null} -
- ) : ( - <> - {screen === 0 ? ( - - ) : null} - {screen === 1 ? ( - - ) : null} - {screen === 2 ? ( -
- -
- ) : null} - - )} +
); }; diff --git a/pages/discord.tsx b/pages/discord.tsx index 44a93314..bab1b617 100644 --- a/pages/discord.tsx +++ b/pages/discord.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import styles from "../styles/Home.module.css"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { useEffect } from "react"; import { useRouter } from "next/router"; import ErrorScreen from "../components/UI/screens/errorScreen"; @@ -32,7 +32,7 @@ const Discord: NextPage = () => { // Access localStorage const [tokenId, setTokenId] = useState(""); - const [calls, setCalls] = useState(); + const [calls, setCalls] = useState({} as Call); useEffect(() => { if (!tokenId) { @@ -101,9 +101,9 @@ const Discord: NextPage = () => { //Contract const { data: discordVerificationData, - writeAsync: execute, + sendAsync: execute, error: discordVerificationError, - } = useContractWrite({ calls: [calls as Call] }); + } = useSendTransaction({ calls: [calls as Call] }); function verifyDiscord() { execute(); diff --git a/pages/freerenewal.tsx b/pages/freerenewal.tsx index 10dea8e3..d63c2a84 100644 --- a/pages/freerenewal.tsx +++ b/pages/freerenewal.tsx @@ -5,7 +5,7 @@ import styles from "../styles/search.module.css"; import { freeRenewalDiscount } from "../utils/discounts/freeRenewal"; import DiscountEndScreen from "../components/discount/discountEndScreen"; import FreeRenewalPresentation from "@/components/discount/freeRenewalPresentation"; -import FreeRenewalCheckout from "@/components/discount/freeRenewalDiscount"; +import FreeRenewalCheckout from "@/components/discount/freeRenewalCheckout"; const FreeRenewalPage: NextPage = () => { const [screen, setScreen] = useState(1); @@ -48,12 +48,8 @@ const FreeRenewalPage: NextPage = () => { process.env.NEXT_PUBLIC_MAILING_LIST_GROUP_AUTO_RENEWAL ?? "", freeRenewalDiscount.discountMailGroupId, ]} - duration={freeRenewalDiscount.offer.duration} - discountId={freeRenewalDiscount.offer.discountId} - customMessage={freeRenewalDiscount.offer.customMessage as string} - priceInEth={freeRenewalDiscount.offer.price} + offer={freeRenewalDiscount.offer} goBack={goBack} - renewPrice="0" />
) : null} diff --git a/pages/gift.tsx b/pages/gift.tsx index 44c7d84f..3c8ccad2 100644 --- a/pages/gift.tsx +++ b/pages/gift.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import type { NextPage } from "next"; import homeStyles from "../styles/Home.module.css"; import styles from "../styles/discount.module.css"; @@ -11,14 +11,6 @@ import FreeRegisterCheckout from "@/components/discount/freeRegisterCheckout"; const FreeRegistration: NextPage = () => { const [searchResult, setSearchResult] = useState(); const [screen, setScreen] = useState(1); - - useEffect(() => { - const currentDate = new Date(); - const timestamp = currentDate.getTime(); - - if (timestamp >= freeRegistration.expiry) setScreen(0); - }, []); - const goBack = () => setScreen(screen - 1); return ( @@ -36,14 +28,13 @@ const FreeRegistration: NextPage = () => { image={freeRegistration.offer.image ?? freeRegistration.image} setSearchResult={setSearchResult} setScreen={setScreen} - expiry={freeRegistration.expiry} /> ) : null} {screen === 2 ? (
{ // Access localStorage const [tokenId, setTokenId] = useState(""); - const [calls, setCalls] = useState(); + const [calls, setCalls] = useState({} as Call); useEffect(() => { if (!tokenId) { @@ -93,9 +93,9 @@ const Github: NextPage = () => { //Contract const { data: githubVerificationData, - writeAsync: execute, + sendAsync: execute, error: githubVerificationError, - } = useContractWrite({ calls: [calls as Call] }); + } = useSendTransaction({ calls: [calls as Call] }); function verifyGithub() { execute(); diff --git a/pages/identities.tsx b/pages/identities.tsx index 249feba8..aa6c9564 100644 --- a/pages/identities.tsx +++ b/pages/identities.tsx @@ -1,7 +1,11 @@ import React, { useMemo } from "react"; import type { NextPage } from "next"; import styles from "../styles/Home.module.css"; -import { useAccount, useConnect, useContractWrite } from "@starknet-react/core"; +import { + useAccount, + useConnect, + useSendTransaction, +} from "@starknet-react/core"; import { useEffect, useState } from "react"; import IdentitiesGallery from "../components/identities/identitiesGalleryV1"; import MintIcon from "../components/UI/iconsComponents/icons/mintIcon"; @@ -37,7 +41,7 @@ const Identities: NextPage = () => { }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // We want this to run only once - const { writeAsync: execute, data: mintData } = useContractWrite({ + const { sendAsync: execute, data: mintData } = useSendTransaction({ calls: [callData], }); diff --git a/pages/quantumleap.tsx b/pages/quantumleap.tsx index 10ff1d5b..90094744 100644 --- a/pages/quantumleap.tsx +++ b/pages/quantumleap.tsx @@ -1,106 +1,15 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import type { NextPage } from "next"; import homeStyles from "../styles/Home.module.css"; import DiscountEndScreen from "../components/discount/discountEndScreen"; -import DiscountOfferScreen from "../components/discount/discountOfferScreen"; -import DiscountUpsellScreen from "../components/discount/discountUpsellScreen"; -import styles from "../styles/discount.module.css"; - -// Create a new discount in utils to create a new discount campaign -import { quantumLeapDiscount } from "../utils/discounts/quantumLeap"; -import RegisterDiscount from "@/components/discount/registerDiscount"; const QuantumLeap: NextPage = () => { - const [searchResult, setSearchResult] = useState(); - const [isUpselled, setIsUpselled] = useState(true); - const [screen, setScreen] = useState(1); - - useEffect(() => { - const currentDate = new Date(); - const timestamp = currentDate.getTime(); - - if (timestamp >= quantumLeapDiscount.expiry) { - setScreen(0); - } - }, []); - - function onUpsellChoice(isUpselled: boolean) { - setIsUpselled(isUpselled); - setScreen(3); - } - - function getDuration() { - return isUpselled - ? quantumLeapDiscount.upsell.duration - : quantumLeapDiscount.offer.duration; - } - - function getDiscountId() { - return isUpselled - ? quantumLeapDiscount.upsell.discountId - : quantumLeapDiscount.offer.discountId; - } - - function getCustomMessage() { - return isUpselled - ? quantumLeapDiscount.upsell.customMessage - : quantumLeapDiscount.offer.customMessage; - } - - function getPrice() { - return isUpselled - ? quantumLeapDiscount.upsell.price - : quantumLeapDiscount.offer.price; - } - - function goBack() { - setScreen(screen - 1); - } - return (
- {screen === 0 ? ( - - ) : null} - {screen === 1 ? ( - - ) : null} - {screen === 2 ? ( - - ) : null} - {screen === 3 ? ( -
- -
- ) : null} +
); }; diff --git a/pages/solana.tsx b/pages/solana.tsx index 76f8831f..8c6223e8 100644 --- a/pages/solana.tsx +++ b/pages/solana.tsx @@ -4,7 +4,11 @@ import Image from "next/image"; import AffiliateImage from "../public/visuals/affiliate.webp"; import Button from "../components/UI/button"; import StarknetIcon from "../components/UI/iconsComponents/icons/starknetIcon"; -import { useAccount, useConnect, useContractWrite } from "@starknet-react/core"; +import { + useAccount, + useConnect, + useSendTransaction, +} from "@starknet-react/core"; import ProgressBar from "../components/UI/progressBar"; import { NextPage } from "next"; import { WalletMultiButton } from "@solana/wallet-adapter-react-ui"; @@ -50,7 +54,7 @@ const Solana: NextPage = () => { starknetAddress as string ); const [callData, setCallData] = useState([]); - const { writeAsync: execute, data: registerData } = useContractWrite({ + const { sendAsync: execute, data: registerData } = useSendTransaction({ calls: callData, }); const [open, setOpen] = useState(true); diff --git a/pages/twitter.tsx b/pages/twitter.tsx index 8d26dd74..9813fb61 100644 --- a/pages/twitter.tsx +++ b/pages/twitter.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import homeStyles from "../styles/Home.module.css"; -import { useAccount, useContractWrite } from "@starknet-react/core"; +import { useAccount, useSendTransaction } from "@starknet-react/core"; import { useEffect } from "react"; import { useRouter } from "next/router"; import ErrorScreen from "../components/UI/screens/errorScreen"; @@ -26,7 +26,7 @@ const Twitter: NextPage = () => { // Access localStorage const [tokenId, setTokenId] = useState(""); - const [calls, setCalls] = useState(); + const [calls, setCalls] = useState({} as Call); useEffect(() => { if (!tokenId) { @@ -95,9 +95,9 @@ const Twitter: NextPage = () => { //Contract const { data: twitterVerificationData, - writeAsync: execute, + sendAsync: execute, error: twitterVerificationError, - } = useContractWrite({ calls: [calls as Call] }); + } = useSendTransaction({ calls: [calls as Call] }); function verifyTwitter() { execute(); diff --git a/styles/components/registerV2.module.css b/styles/components/registerV2.module.css index 06059c68..029310a6 100644 --- a/styles/components/registerV2.module.css +++ b/styles/components/registerV2.module.css @@ -180,6 +180,13 @@ opacity: 0.7; } +.errorMessage { + font-size: 12px; + color: rgb(210, 27, 27); + max-width: 100%; + word-break: break-word; +} + @media (max-width: 1084px) { .image { visibility: hidden; diff --git a/styles/components/textField.module.css b/styles/components/textField.module.css index 356a9220..774c3bef 100644 --- a/styles/components/textField.module.css +++ b/styles/components/textField.module.css @@ -10,7 +10,7 @@ } .radioWhite { - background-color: #FFFFFF; + background-color: #ffffff; border-radius: 7.983px; padding: 8px 15px; box-shadow: 0px 2px 30px 0px rgba(0, 0, 0, 0.06); @@ -30,6 +30,8 @@ font-weight: 500; line-height: 20px; /* 142.857% */ align-self: flex-start; + max-width: 100%; + word-break: break-word; } .errorLegend { diff --git a/tests/utils/altcoinService.test.js b/tests/utils/altcoinService.test.js index 0b2b9ac1..0d9fa27a 100644 --- a/tests/utils/altcoinService.test.js +++ b/tests/utils/altcoinService.test.js @@ -22,22 +22,34 @@ const CurrencyType = { describe("getDomainPriceAltcoin function", () => { it("should correctly calculate domain price in altcoin for valid inputs", () => { // priceInEth is in wei (1e18 wei = 1 ETH) and quote is the number of altcoin per ETH - expect(getDomainPriceAltcoin("100", "5000000000000000000")).toBe("500"); // 5 ETH to altcoin at a 1:100 rate - expect(getDomainPriceAltcoin("50", "10000000000000000000")).toBe("500"); // 10 ETH to altcoin at a 1:50 rate - expect(getDomainPriceAltcoin("200", "2500000000000000000")).toBe("500"); // 2.5 ETH to altcoin at a 1:200 rate + expect(getDomainPriceAltcoin("100", BigInt("5000000000000000000"))).toBe( + BigInt("500") + ); // 5 ETH to altcoin at a 1:100 rate + expect(getDomainPriceAltcoin("50", BigInt("10000000000000000000"))).toBe( + BigInt("500") + ); // 10 ETH to altcoin at a 1:50 rate + expect(getDomainPriceAltcoin("200", BigInt("2500000000000000000"))).toBe( + BigInt("500") + ); // 2.5 ETH to altcoin at a 1:200 rate }); it("should handle edge cases gracefully", () => { - expect(getDomainPriceAltcoin("0", "1000000000000000000")).toBe("0"); // Zero rate - expect(getDomainPriceAltcoin("100", "0")).toBe("0"); // Zero priceInEth - expect(getDomainPriceAltcoin("1", "1000000000000000000")).toBe( - "1000000000000000000" + expect(getDomainPriceAltcoin("0", BigInt("1000000000000000000"))).toBe( + BigInt("0") + ); // Zero rate + expect(getDomainPriceAltcoin("100", BigInt(0))).toBe(BigInt("0")); // Zero priceInEth + expect(getDomainPriceAltcoin("1", BigInt("1000000000000000000"))).toBe( + BigInt("1000000000000000000") ); // 1:1 rate }); it("should return a rounded value to nearest altcoin", () => { - expect(getDomainPriceAltcoin("100", "5500000000000000000")).toBe("550"); // 5.5 ETH at a 1:100 rate, rounded - expect(getDomainPriceAltcoin("150", "3333333333333333333")).toBe("500"); // 3.333... ETH at a 1:150 rate, rounded + expect(getDomainPriceAltcoin("100", BigInt("5500000000000000000"))).toBe( + BigInt("550") + ); // 5.5 ETH at a 1:100 rate, rounded + expect(getDomainPriceAltcoin("150", BigInt("3333333333333333333"))).toBe( + BigInt("500") + ); // 3.333... ETH at a 1:150 rate, rounded }); }); @@ -125,8 +137,8 @@ describe("getRenewalPriceETH", () => { describe("getDomainPrice", () => { it("returns price in ETH when currency type is ETH", () => { - const domainPrice = PRICES.FIVE.toString(); - const result = getDomainPrice("example.com", CurrencyType.ETH); + const domainPrice = PRICES.FIVE; + const result = getDomainPrice("example.com", CurrencyType.ETH, 1); expect(result).toBe(domainPrice); }); @@ -134,25 +146,19 @@ describe("getDomainPrice", () => { it("returns price in alternative currency when currency type is not ETH", () => { // price of 1 ETH in STRK const quote = "1644663352891940798464"; - const domainPriceInETH = PRICES.FIVE.toString(); - const domainPriceSTRK = getDomainPriceAltcoin(domainPriceInETH, quote); + const domainPriceInETH = PRICES.FIVE; + const domainPriceSTRK = getDomainPriceAltcoin(quote, domainPriceInETH); - const result = getDomainPrice("example.com", CurrencyType.STRK, quote); + const result = getDomainPrice("example.com", CurrencyType.STRK, 1, quote); expect(result).toBe(domainPriceSTRK); }); - - it("throws an error when quote is missing for non-ETH currency type", () => { - expect(() => { - getDomainPrice("example.com", CurrencyType.STRK); - }).toThrow("[big.js] Invalid number"); - }); }); describe("getAutoRenewAllowance", () => { it("calculates allowance without sales tax when salesTaxRate is 0", () => { const currencyType = CurrencyType.ETH; const salesTaxRate = 0; - const domainPrice = PRICES.FIVE.toString(); + const domainPrice = PRICES.FIVE; const result = getAutoRenewAllowance( currencyType, @@ -166,7 +172,7 @@ describe("getAutoRenewAllowance", () => { it("calculates allowance with sales tax when salesTaxRate is provided", () => { const currencyType = CurrencyType.ETH; const salesTaxRate = 0.1; // 10% - const domainPrice = "1000000000000000000"; // 1 ETH + const domainPrice = BigInt("1000000000000000000"); // 1 ETH const limitPrice = BigInt(domainPrice); const taxAmount = BigInt("100000000000000000"); // 0.1 ETH @@ -178,37 +184,37 @@ describe("getAutoRenewAllowance", () => { domainPrice ); - expect(result).toBe(expectedAllowance.toString()); + expect(result).toBe(expectedAllowance); }); }); describe("getPriceForDuration function", () => { it("should correctly calculate domain price for valid inputs", () => { - expect(getPriceForDuration("1000000000000000000", 5)).toBe( - "5000000000000000000" + expect(getPriceForDuration(BigInt("1000000000000000000"), 5)).toBe( + BigInt("5000000000000000000") ); // 1 ETH for 5 years - expect(getPriceForDuration("500000000000000000", 10)).toBe( - "5000000000000000000" + expect(getPriceForDuration(BigInt("500000000000000000"), 10)).toBe( + BigInt("5000000000000000000") ); // 0.5 ETH for 10 years - expect(getPriceForDuration("2000000000000000000", 2)).toBe( - "4000000000000000000" + expect(getPriceForDuration(BigInt("2000000000000000000"), 2)).toBe( + BigInt("4000000000000000000") ); // 2 ETH for 2 years }); it("should handle edge cases gracefully", () => { - expect(getPriceForDuration("0", 10)).toBe("0"); // Zero priceFor1Y - expect(getPriceForDuration("1000000000000000000", 1)).toBe( - "1000000000000000000" + expect(getPriceForDuration(BigInt(0), 10)).toBe(BigInt(0)); // Zero priceFor1Y + expect(getPriceForDuration(BigInt("1000000000000000000"), 1)).toBe( + BigInt("1000000000000000000") ); // Duration is 1 - expect(getPriceForDuration("0", 0)).toBe("0"); // Zero priceFor1Y and zero duration + expect(getPriceForDuration(BigInt(0), 0)).toBe(BigInt(0)); // Zero priceFor1Y and zero duration }); it("should return a rounded value correctly", () => { - expect(getPriceForDuration("1234567890000000000", 3)).toBe( - "3703703670000000000" + expect(getPriceForDuration(BigInt("1234567890000000000"), 3)).toBe( + BigInt("3703703670000000000") ); - expect(getPriceForDuration("1234567890000000000", 4)).toBe( - "4938271560000000000" + expect(getPriceForDuration(BigInt("1234567890000000000"), 4)).toBe( + BigInt("4938271560000000000") ); }); }); diff --git a/tests/utils/feltService.test.js b/tests/utils/feltService.test.js index 9774e1d6..2576a880 100644 --- a/tests/utils/feltService.test.js +++ b/tests/utils/feltService.test.js @@ -2,7 +2,7 @@ import { hexToDecimal, decimalToHex, stringToHex, - gweiToEth, + weiToEth, applyRateToBigInt, fromUint256, toUint256, @@ -46,11 +46,15 @@ describe("Should test decimalToHex function", () => { }); it("Should convert a number to its hex representation", () => { - expect(decimalToHex(123)).toEqual("0x000000000000000000000000000000000000000000000000000000000000007b"); + expect(decimalToHex(123)).toEqual( + "0x000000000000000000000000000000000000000000000000000000000000007b" + ); }); it("Should convert 0 to 0x0", () => { - expect(decimalToHex(0)).toEqual("0x0000000000000000000000000000000000000000000000000000000000000000"); + expect(decimalToHex(0)).toEqual( + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); }); }); @@ -68,39 +72,39 @@ describe("Should test the stringToHex function", () => { }); }); -describe("Should test gweiToEth function", () => { - it("Should return the right ETH value from a given Gwei value", () => { - expect(gweiToEth("1000000000000000000")).toEqual("1"); - expect(gweiToEth("10000000000000000")).toEqual("0.01"); +describe("Should test weiToEth function", () => { + it("Should return the right ETH value from a given wei value in bigint or string", () => { + expect(weiToEth("1000000000000000000")).toEqual(1); + expect(weiToEth(BigInt("10000000000000000"))).toEqual(0.01); }); it("Should return 0 if the argument is an empty string", () => { - expect(gweiToEth("")).toEqual("0"); + expect(weiToEth("")).toEqual(0); }); }); describe("Should test applyRateToBigInt function", () => { it("Should return the correct value after multiplying by a percentage", () => { - expect(applyRateToBigInt("100000000000000000000", 0.35)).toEqual( - "35000000000000000000" + expect(applyRateToBigInt(BigInt("100000000000000000000"), 0.35)).toEqual( + BigInt("35000000000000000000") ); - expect(applyRateToBigInt("100000000000000000000", 0.75)).toEqual( - "75000000000000000000" + expect(applyRateToBigInt(BigInt("100000000000000000000"), 0.75)).toEqual( + BigInt("75000000000000000000") ); expect(applyRateToBigInt(BigInt("100000000000000000000"), 0.5)).toEqual( - "50000000000000000000" + BigInt("50000000000000000000") ); }); it("Should return 0 if the argument is an empty string or zero", () => { - expect(applyRateToBigInt("", 0.35)).toEqual("0"); - expect(applyRateToBigInt("0", 0.75)).toEqual("0"); - expect(applyRateToBigInt(BigInt(0), 0.5)).toEqual("0"); + expect(applyRateToBigInt(BigInt(0), 0.35)).toEqual(BigInt(0)); + expect(applyRateToBigInt(BigInt(0), 0.75)).toEqual(BigInt(0)); + expect(applyRateToBigInt(BigInt(0), 0.5)).toEqual(BigInt(0)); }); it("Should handle negative percentages", () => { - expect(applyRateToBigInt("100000000000000000000", -0.35)).toEqual( - "-35000000000000000000" + expect(applyRateToBigInt(BigInt("100000000000000000000"), -0.35)).toEqual( + BigInt("-35000000000000000000") ); }); }); @@ -108,10 +112,14 @@ describe("Should test applyRateToBigInt function", () => { describe("fromUint256 function", () => { it("should correctly combine low and high BigInts", () => { expect(fromUint256(BigInt(1), BigInt(0))).toBe("1"); - expect(fromUint256(BigInt(0), BigInt(1))).toBe("340282366920938463463374607431768211456"); // 2^128 - expect(fromUint256(BigInt(1), BigInt(1))).toBe("340282366920938463463374607431768211457"); // 2^128 + 1 + expect(fromUint256(BigInt(0), BigInt(1))).toBe( + "340282366920938463463374607431768211456" + ); // 2^128 + expect(fromUint256(BigInt(1), BigInt(1))).toBe( + "340282366920938463463374607431768211457" + ); // 2^128 + 1 }); - + it("should handle edge cases", () => { expect(fromUint256(BigInt(0), BigInt(0))).toBe("0"); expect( @@ -119,11 +127,13 @@ describe("fromUint256 function", () => { ).toBe("340282366920938463463374607431768211455"); // 2^128 - 1 expect( fromUint256( - BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") ) - ).toBe("115792089237316195423570985008687907853269984665640564039457584007913129639935"); // 2^256 - 1 - }); + ).toBe( + "115792089237316195423570985008687907853269984665640564039457584007913129639935" + ); // 2^256 - 1 + }); }); describe("Should test the toUint256 function", () => { @@ -139,7 +149,8 @@ describe("Should test the toUint256 function", () => { }); it("Should handle extremely large numbers", () => { - const largeInput = "1206167596222043737899107594365023368541035738443865566657697352045290673496"; + const largeInput = + "1206167596222043737899107594365023368541035738443865566657697352045290673496"; const result = toUint256(largeInput); const expectedLow = "113427455640312821154458202477256070488"; @@ -154,4 +165,4 @@ describe("Should test the toUint256 function", () => { expect(() => toUint256(invalidInput)).toThrow(); }); -}) +}); diff --git a/tests/utils/priceService.test.js b/tests/utils/priceService.test.js index c9fa76c6..4730371b 100644 --- a/tests/utils/priceService.test.js +++ b/tests/utils/priceService.test.js @@ -2,74 +2,83 @@ import { totalAlphabet } from "../../utils/constants"; import { PRICES, - getPriceFromDomain, - getPriceFromDomains, + getDomainPriceWei, + getManyDomainsPriceWei, areDomainSelected, getTotalYearlyPrice, - getYearlyPrice, + getYearlyPriceWei, + getApprovalAmount, + getDisplayablePrice, + isApprovalInfinite, } from "../../utils/priceService"; import { generateString } from "../../utils/stringService"; -import { gweiToEth } from "../../utils/feltService"; describe("Should test price service file", () => { - it("Test getPriceFromDomain functions with different domains", () => { + it("Test getDomainPriceWei functions with different domains", () => { let randomString = generateString(0, totalAlphabet); - expect(getPriceFromDomain(1, randomString.concat(".stark"))).toEqual( + expect(getDomainPriceWei(1, randomString.concat(".stark"))).toEqual( PRICES.FIVE ); randomString = generateString(1, totalAlphabet); - expect(getPriceFromDomain(1, randomString.concat(".stark"))).toEqual( + expect(getDomainPriceWei(1, randomString.concat(".stark"))).toEqual( PRICES.ONE ); randomString = generateString(2, totalAlphabet); - expect(getPriceFromDomain(1, randomString.concat(".stark"))).toEqual( + expect(getDomainPriceWei(1, randomString.concat(".stark"))).toEqual( PRICES.TWO ); randomString = generateString(3, totalAlphabet); - expect(getPriceFromDomain(1, randomString)).toEqual(PRICES.THREE); + expect(getDomainPriceWei(1, randomString)).toEqual(PRICES.THREE); randomString = generateString(4, totalAlphabet); - expect(getPriceFromDomain(1, randomString)).toEqual(PRICES.FOUR); + expect(getDomainPriceWei(1, randomString)).toEqual(PRICES.FOUR); randomString = generateString(5, totalAlphabet); - expect(getPriceFromDomain(1, randomString)).toEqual(PRICES.FIVE); + expect(getDomainPriceWei(1, randomString)).toEqual(PRICES.FIVE); randomString = generateString(6, totalAlphabet); - expect(getPriceFromDomain(1, randomString)).toEqual(PRICES.FIVE); + expect(getDomainPriceWei(1, randomString)).toEqual(PRICES.FIVE); }); }); -describe("getPriceFromDomains function", () => { +describe("getManyDomainsPriceWei function", () => { it("should return the total price for given domains and duration", () => { - const domains = ["example.stark", "test.stark"]; + const domains = { + "example.stark": true, + "test.stark": true, + }; const duration = 1; const expectedPrice = - getPriceFromDomain(1, "example.stark") + - getPriceFromDomain(1, "test.stark"); - expect(getPriceFromDomains(domains, duration)).toEqual(expectedPrice); + getDomainPriceWei(1, "example.stark") + + getDomainPriceWei(1, "test.stark"); + expect(getManyDomainsPriceWei(domains, duration)).toEqual(expectedPrice); }); it("should return zero if no domains are provided", () => { - const domains: string[] = []; + const domains = []; const duration = 1; const expectedPrice = BigInt(0); - expect(getPriceFromDomains(domains, duration)).toEqual(expectedPrice); + expect(getManyDomainsPriceWei(domains, duration)).toEqual(expectedPrice); }); it("should return the total price for all provided domains", () => { - const domains = ["example.stark", "qsd.stark", "a"]; + const domains = { + "example.stark": true, + "qsd.stark": true, + "q.stark": true, + }; const duration = 2; const expectedPrice = - getPriceFromDomain(2, "example.stark") + - getPriceFromDomain(2, "qsd") + - getPriceFromDomain(2, "q.stark"); - expect(getPriceFromDomains(domains, duration)).toEqual(expectedPrice); + getDomainPriceWei(2, "example.stark") + + getDomainPriceWei(2, "qsd") + + getDomainPriceWei(2, "q.stark"); + expect(getManyDomainsPriceWei(domains, duration)).toEqual(expectedPrice); }); }); @@ -123,14 +132,7 @@ describe("getTotalYearlyPrice function", () => { "allerpsg.stark": true, }; - const expectedPrice = gweiToEth( - String( - getPriceFromDomains( - ["fricoben.stark", "fricobens.stark", "allerpsg.stark"], - 1 - ) - ) - ); + const expectedPrice = getManyDomainsPriceWei(selectedDomains, 365); expect(getTotalYearlyPrice(selectedDomains)).toEqual(expectedPrice); }); @@ -143,29 +145,147 @@ describe("getTotalYearlyPrice function", () => { "allerpsg.stark": false, }; - expect(getTotalYearlyPrice(selectedDomains)).toEqual("0"); + expect(getTotalYearlyPrice(selectedDomains)).toEqual(BigInt(0)); }); it("should return '0' if selectedDomains is undefined", () => { - expect(getTotalYearlyPrice(undefined)).toEqual("0"); + expect(getTotalYearlyPrice(undefined)).toEqual(BigInt(0)); }); }); -describe("getYearlyPrice function", () => { +describe("getYearlyPriceWei function", () => { it("should return the yearly price in ETH for a given domain", () => { const domain = "example.stark"; - const expectedPrice = gweiToEth(String(getPriceFromDomain(1, domain))); - expect(getYearlyPrice(domain)).toEqual(expectedPrice); + const expectedPrice = getDomainPriceWei(365, domain); + expect(getYearlyPriceWei(domain)).toEqual(expectedPrice); }); it("should return '0' if the domain is an empty string", () => { const domain = ""; - expect(getYearlyPrice(domain)).toEqual("0"); + expect(getYearlyPriceWei(domain)).toEqual(BigInt("0")); }); it("should return the yearly price in ETH for another domain", () => { const domain = "test.stark"; - const expectedPrice = gweiToEth(String(getPriceFromDomain(1, domain))); - expect(getYearlyPrice(domain)).toEqual(expectedPrice); + const expectedPrice = getDomainPriceWei(365, domain); + expect(getYearlyPriceWei(domain)).toEqual(expectedPrice); + }); +}); + +describe("getApprovalAmount function", () => { + it("should calculate the correct approval amount", () => { + const price = BigInt(1000); + const salesTaxAmount = BigInt(100); + const durationInYears = 2; + const currentAllowance = BigInt(500); + + const result = getApprovalAmount( + price, + salesTaxAmount, + durationInYears, + currentAllowance + ); + + const expectedAmount = + ((BigInt(1000) + BigInt(100)) / BigInt(2)) * BigInt(10) + BigInt(500); + expect(result).toEqual(expectedAmount); + }); + + it("should handle zero values correctly", () => { + const price = BigInt(0); + const salesTaxAmount = BigInt(0); + const durationInYears = 1; + const currentAllowance = BigInt(0); + + const result = getApprovalAmount( + price, + salesTaxAmount, + durationInYears, + currentAllowance + ); + + expect(result).toEqual(BigInt(0)); + }); + + it("should calculate correctly for different duration years", () => { + const price = BigInt(1000); + const salesTaxAmount = BigInt(100); + const durationInYears = 5; + const currentAllowance = BigInt(200); + + const result = getApprovalAmount( + price, + salesTaxAmount, + durationInYears, + currentAllowance + ); + + const expectedAmount = + ((BigInt(1000) + BigInt(100)) / BigInt(5)) * BigInt(10) + BigInt(200); + expect(result).toEqual(expectedAmount); + }); +}); + +describe("getDisplayablePrice function", () => { + it("should format price correctly for whole numbers", () => { + const price = BigInt("1000000000000000000"); // 1 ETH in wei + expect(getDisplayablePrice(price)).toBe("1.000"); + }); + + it("should format price correctly for fractional numbers", () => { + const price = BigInt("1234567890000000000"); // 1.23456789 ETH in wei + expect(getDisplayablePrice(price)).toBe("1.235"); + }); + + it("should handle small numbers correctly", () => { + const price = BigInt("1000000000000000"); // 0.001 ETH in wei + expect(getDisplayablePrice(price)).toBe("0.001"); + }); + + it("should handle zero correctly", () => { + const price = BigInt("0"); + expect(getDisplayablePrice(price)).toBe("0.000"); + }); + + it("should handle large numbers correctly", () => { + const price = BigInt("123456789000000000000000"); // 123,456.789 ETH in wei + expect(getDisplayablePrice(price)).toBe("123456.789"); + }); +}); + +describe("isApprovalInfinite function", () => { + it("should return true for values equal to or greater than UINT_128_MAX", () => { + const UINT_128_MAX = (BigInt(1) << BigInt(128)) - BigInt(1); + expect(isApprovalInfinite(UINT_128_MAX)).toBe(true); + expect(isApprovalInfinite(UINT_128_MAX + BigInt(1))).toBe(true); + }); + + it("should return true for UINT_256_MINUS_UINT_128", () => { + const UINT_256_MINUS_UINT_128 = + (BigInt(1) << BigInt(256)) - (BigInt(1) << BigInt(128)); + expect(isApprovalInfinite(UINT_256_MINUS_UINT_128)).toBe(true); + }); + + it("should return true for UINT_256_MINUS_UINT_128", () => { + const UINT_256_MINUS_UINT_128 = + (BigInt(1) << BigInt(256)) - (BigInt(1) << BigInt(128)); + expect(isApprovalInfinite(UINT_256_MINUS_UINT_128)).toBe(true); + }); + + it("should return false for values less than 10K ETH", () => { + const THRESHOLD = BigInt(10000) * BigInt(10 ** 18); + expect(isApprovalInfinite(THRESHOLD - BigInt(1))).toBe(false); + expect(isApprovalInfinite(THRESHOLD + BigInt(1))).toBe(true); + }); + + it("should handle string inputs", () => { + const UINT_128_MAX = ((BigInt(1) << BigInt(128)) - BigInt(1)).toString(); + expect(isApprovalInfinite(UINT_128_MAX)).toBe(true); + expect(isApprovalInfinite("1000000")).toBe(false); + }); + + it("should return false for zero", () => { + expect(isApprovalInfinite(BigInt(0))).toBe(false); + expect(isApprovalInfinite("0")).toBe(false); }); }); diff --git a/tests/utils/subscriptionService.test.js b/tests/utils/subscriptionService.test.js index bc0dae32..5b4b72cf 100644 --- a/tests/utils/subscriptionService.test.js +++ b/tests/utils/subscriptionService.test.js @@ -1,69 +1,8 @@ import { - processSubscriptionData, getNonSubscribedDomains, fullIdsToDomains, + processSubscriptionData, } from "../../utils/subscriptionService"; -import { ERC20Contract } from "../../utils/constants"; - -describe("processSubscriptionData function", () => { - const mockSubscriptionInfos = { - "domain1.stark": { - eth_subscriptions: [ - { - enabled: true, - allowance: "0x0", - renewer_address: "0xabc", - auto_renew_contract: null, - token: ERC20Contract.ETH, - }, - ], - altcoin_subscriptions: [ - { - enabled: true, - allowance: "0x0", - renewer_address: "0xdef", - auto_renew_contract: null, - token: ERC20Contract.STRK, - }, - ], - }, - "domain2.stark": { - eth_subscriptions: [ - { - enabled: true, - allowance: "0x1", - renewer_address: "0xghi", - auto_renew_contract: null, - token: ERC20Contract.ETH, - }, - ], - altcoin_subscriptions: [ - { - enabled: true, - allowance: "0x1", - renewer_address: "0xjkl", - auto_renew_contract: null, - token: ERC20Contract.STRK, - }, - ], - }, - "domain3.stark": { - eth_subscriptions: null, - altcoin_subscriptions: null, - }, - }; - - it("should process subscription data and set the correct needs allowance", () => { - const expectedOutput = { - "domain1.stark": { ETH: true, STRK: true }, - "domain2.stark": { ETH: false, STRK: false }, - "domain3.stark": { ETH: true, STRK: true }, - }; - - const result = processSubscriptionData(mockSubscriptionInfos); - expect(result).toEqual(expectedOutput); - }); -}); describe("getNonSubscribedDomains function", () => { const mockData = { @@ -128,3 +67,43 @@ describe("fullIdsToDomains", () => { expect(result).toEqual([]); }); }); + +describe("processSubscriptionData function", () => { + it("should process subscription data correctly", () => { + const mockData = { + "example.stark": { eth_subscriptions: null }, + "example2.stark": { eth_subscriptions: {} }, + }; + + const expectedOutput = { + "example.stark": { + ETH: { needsAllowance: true, currentAllowance: BigInt(0) }, + STRK: { needsAllowance: true, currentAllowance: BigInt(0) }, + }, + }; + + const result = processSubscriptionData(mockData); + expect(result).toEqual(expectedOutput); + }); + + it("should return an empty object if no domains need subscriptions", () => { + const mockData = { + "example.stark": { eth_subscriptions: {} }, + "example2.stark": { eth_subscriptions: {} }, + }; + + const expectedOutput = {}; + + const result = processSubscriptionData(mockData); + expect(result).toEqual(expectedOutput); + }); + + it("should handle an empty input object", () => { + const mockData = {}; + + const expectedOutput = {}; + + const result = processSubscriptionData(mockData); + expect(result).toEqual(expectedOutput); + }); +}); diff --git a/types/frontTypes.d.ts b/types/frontTypes.d.ts index fe0eb77c..9af5dee2 100644 --- a/types/frontTypes.d.ts +++ b/types/frontTypes.d.ts @@ -51,10 +51,10 @@ type UsState = { }; type Discount = { - duration: number; + durationInDays: number; customMessage?: string; discountId: string; - price: string; + price: bigint; desc: string; title: { desc: string; catch: string; descAfter?: string }; image?: string; @@ -126,7 +126,12 @@ type TokenAllowance = { }; type TokenNeedsAllowance = { - [key in CurrencyType]: boolean; + [key in CurrencyType]: AllowanceStatus; +}; + +type AllowanceStatus = { + needsAllowance: boolean; + currentAllowance: bigint; }; type NeedSubscription = { @@ -141,9 +146,9 @@ type MulticallCallData = { }; type Upsell = { - duration: number; // duration you get - paidDuration: number; // duration you pay for - maxDuration: number; // if user selects a duration higher, upsell won't be applied + durationInDays: number; // duration you get + paidDurationInDays: number; // duration you pay for + maxDurationInDays: number; // if user selects a duration higher, upsell won't be applied discountId: string; imageUrl: string; title: { diff --git a/utils/altcoinService.ts b/utils/altcoinService.ts index 53e9d508..8156e840 100644 --- a/utils/altcoinService.ts +++ b/utils/altcoinService.ts @@ -1,8 +1,8 @@ -import Big from "big.js"; import { CurrenciesRange, CurrencyType, ERC20Contract } from "./constants"; import { applyRateToBigInt, hexToDecimal } from "./feltService"; -import { getPriceFromDomain } from "./priceService"; +import { getDomainPriceWei } from "./priceService"; import { Result } from "starknet"; +import { selectedDomainsToArray } from "./stringService"; export const getTokenQuote = async (tokenAddress: string) => { try { @@ -15,29 +15,36 @@ export const getTokenQuote = async (tokenAddress: string) => { } }; -export const getDomainPriceAltcoin = (quote: string, priceInEth: string) => { +export const getDomainPriceAltcoin = (quote: string, priceInEth: bigint) => { if (quote === "1") return priceInEth; - const priceBigInt = new Big(priceInEth); - const quoteBigInt = new Big(quote); - const scaleFactor = new Big(10 ** 18); + const quoteBigInt = BigInt(quote); + const scaleFactor = BigInt(10 ** 18); + const priceBeforeRounding = (priceInEth * quoteBigInt) / scaleFactor; - const price = priceBigInt.mul(quoteBigInt).div(scaleFactor).toFixed(0); + // Rounding to nearest integer + const remainder = + (priceInEth * quoteBigInt * BigInt(2)) % (scaleFactor * BigInt(2)); + const roundingAdjustment = remainder >= scaleFactor ? BigInt(1) : BigInt(0); + const price = priceBeforeRounding + roundingAdjustment; return price; }; export const getPriceForDuration = ( - priceFor1Y: string, - duration: number -): string => { - if (duration === 1) return priceFor1Y; + priceFor1Y: bigint, + durationInDays: number +): bigint => { + if (durationInDays === 365) return priceFor1Y; - const priceBigInt = new Big(priceFor1Y); - const durationBigInt = new Big(duration); + // Convert the string to BigInt + const priceBigInt = BigInt(priceFor1Y); + const durationBigInt = BigInt(durationInDays); - const price = priceBigInt.mul(durationBigInt).toFixed(0); + // Perform the multiplication + const price = priceBigInt * durationBigInt; + // Convert the result back to string return price; }; @@ -74,7 +81,7 @@ export const getRenewalPriceETH = ( domain: string, duration: number ): string => { - if (priceError || !priceData) return getPriceFromDomain(1, domain).toString(); + if (priceError || !priceData) return getDomainPriceWei(1, domain).toString(); else { const res = priceData as CallResult; // Divide the priceData by the duration to get the renewal price @@ -88,31 +95,69 @@ export const getRenewalPriceETH = ( export const getDomainPrice = ( domain: string, currencyType: CurrencyType, + durationInDays: number, quote?: string -): string => { - if (currencyType === CurrencyType.ETH) { - return getPriceFromDomain(1, domain).toString(); +): bigint => { + if (currencyType === CurrencyType.ETH || !quote) { + // When quote is missing we return ETH price + return getDomainPriceWei(durationInDays, domain); } else { return getDomainPriceAltcoin( quote as string, - getPriceFromDomain(1, domain).toString() + getDomainPriceWei(durationInDays, domain) ); } }; +export function getYearlyPrice( + domain: string, + currencyType: CurrencyType, + quote?: string +): bigint { + const priceInWei = getDomainPrice(domain, currencyType, 365, quote); + + return priceInWei; +} + +export function getManyDomainsPrice( + selectedDomains: Record | undefined, + currencyType: CurrencyType, + durationInDays: number, + quote?: string +): bigint { + if (!selectedDomains) return BigInt(0); + + // Calculate the sum of all prices with getPriceFromDomain + return selectedDomainsToArray(selectedDomains).reduce( + (acc, domain) => + acc + getDomainPrice(domain, currencyType, durationInDays, quote), + BigInt(0) + ); +} + +export function getTotalYearlyPrice( + selectedDomains: Record | undefined, + currencyType: CurrencyType, + quote?: string +): bigint { + if (!selectedDomains) return BigInt(0); + + const price = getManyDomainsPrice(selectedDomains, currencyType, 365, quote); + + return price; +} + // function to compute the limit price for the auto renewal contract // depending on the token selected by the user export const getAutoRenewAllowance = ( currencyType: CurrencyType, salesTaxRate: number, - domainPrice: string -): string => { - const limitPrice = getLimitPriceRange(currencyType, BigInt(domainPrice)); - const allowance: string = salesTaxRate - ? ( - BigInt(limitPrice) + BigInt(applyRateToBigInt(limitPrice, salesTaxRate)) - ).toString() - : limitPrice.toString(); + domainPrice: bigint +): bigint => { + const limitPrice = getLimitPriceRange(currencyType, domainPrice); + const allowance: bigint = salesTaxRate + ? BigInt(limitPrice) + BigInt(applyRateToBigInt(limitPrice, salesTaxRate)) + : limitPrice; return allowance; }; @@ -133,7 +178,7 @@ export async function fetchAvnuQuoteData(): Promise { // Determine the currency type based on token balances and STRK quote export const smartCurrencyChoosing = async ( tokenBalances: TokenBalance, - priceIntEth?: string // price to pay in ETH + priceInEth?: bigint // price to pay in ETH ): Promise => { // Early returns based on token presence if (tokenBalances.ETH && !tokenBalances.STRK) return CurrencyType.ETH; @@ -159,8 +204,8 @@ export const smartCurrencyChoosing = async ( // If domain price is provided, use it to determine currency type // if a user can only pay with one currency then we'll select it - if (priceIntEth) { - const priceBigInt = BigInt(priceIntEth); + if (priceInEth) { + const priceBigInt = priceInEth; const ethBalance = BigInt(tokenBalances.ETH); // if user can only pay in STRK and doesn't have enough ETH to pay for the domain if (strkConvertedBalance > priceBigInt && ethBalance < priceBigInt) { diff --git a/utils/callData/autoRenewalCalls.ts b/utils/callData/autoRenewalCalls.ts index 9ff9695d..4b4a1662 100644 --- a/utils/callData/autoRenewalCalls.ts +++ b/utils/callData/autoRenewalCalls.ts @@ -3,20 +3,19 @@ import { Call } from "starknet"; function approve( erc20Contract: string, renewalContract: string, - erc20Price: string + erc20Price: bigint ): Call { - const amountToApprove = erc20Price.concat("0"); // multiply by 10 to approve 10 years return { contractAddress: erc20Contract, entrypoint: "approve", - calldata: [renewalContract, amountToApprove, "0"], + calldata: [renewalContract, erc20Price.toString(), "0"], }; } function enableRenewal( autoRenewalContract: string, encodedDomain: string, - price: string, + price: bigint, metahash: string ): Call { return { @@ -24,7 +23,7 @@ function enableRenewal( entrypoint: "enable_renewals", calldata: [ encodedDomain.toString(), - price, + price.toString(), 0, // sponsor metahash, ], diff --git a/utils/callData/registrationCalls.ts b/utils/callData/registrationCalls.ts index 6865a609..e73fc3ca 100644 --- a/utils/callData/registrationCalls.ts +++ b/utils/callData/registrationCalls.ts @@ -1,11 +1,15 @@ import { Call } from "starknet"; import { numberToString, numberToStringHex } from "../stringService"; -function approve(price: string, erc20Address: string): Call { +function approve(price: bigint, erc20Address: string): Call { return { contractAddress: erc20Address, entrypoint: "approve", - calldata: [process.env.NEXT_PUBLIC_NAMING_CONTRACT as string, price, 0], + calldata: [ + process.env.NEXT_PUBLIC_NAMING_CONTRACT as string, + price.toString(), + 0, + ], }; } @@ -13,7 +17,7 @@ function buy( encodedDomain: string, tokenId: number, sponsor: string, - durationInYears: number, + durationInDays: number, metadata: HexString, discountId?: string ): Call { @@ -26,7 +30,7 @@ function buy( // domain encodedDomain, // days - numberToString(durationInYears * 365), + numberToString(durationInDays), // resolver 0, // sponsor @@ -43,7 +47,7 @@ function altcoinBuy( encodedDomain: string, tokenId: number, sponsor: string, - durationInYears: number, + durationInDays: number, metadata: HexString, erc20Address: string, quoteData: QuoteQueryData, @@ -58,7 +62,7 @@ function altcoinBuy( // domain encodedDomain, // days - numberToString(durationInYears * 365), + numberToString(durationInDays), // resolver 0, // sponsor @@ -96,17 +100,21 @@ function mainId(tokenId: number): Call { }; } -function vatTransfer(amount: string, erc20_contract: string): Call { +function vatTransfer(amount: bigint, erc20_contract: string): Call { return { contractAddress: erc20_contract, entrypoint: "transfer", - calldata: [process.env.NEXT_PUBLIC_VAT_CONTRACT as string, amount, "0"], + calldata: [ + process.env.NEXT_PUBLIC_VAT_CONTRACT as string, + amount.toString(), + "0", + ], }; } function renew( encodedDomain: string, - durationInYears: number, + durationInDays: number, metadataHash: HexString, sponsor?: string, discountId?: string @@ -116,7 +124,7 @@ function renew( entrypoint: "renew", calldata: [ encodedDomain, - durationInYears * 365, + durationInDays, sponsor ?? 0, discountId ?? 0, metadataHash, @@ -126,7 +134,7 @@ function renew( function altcoinRenew( encodedDomain: string, - durationInYears: number, + durationInDays: number, metadataHash: HexString, erc20Address: string, quoteData: QuoteQueryData, @@ -138,7 +146,7 @@ function altcoinRenew( entrypoint: "altcoin_renew", calldata: [ encodedDomain, - durationInYears * 365, + durationInDays, sponsor ?? 0, discountId ?? 0, metadataHash, diff --git a/utils/connectorWrapper.ts b/utils/connectorWrapper.ts index ac189be4..019ff815 100644 --- a/utils/connectorWrapper.ts +++ b/utils/connectorWrapper.ts @@ -11,17 +11,16 @@ export const getConnectors = () => { new InjectedConnector({ options: { id: "braavos" } }), new InjectedConnector({ options: { id: "okxwallet" } }), new InjectedConnector({ options: { id: "bitkeep" } }), - new ArgentMobileConnector({ - dappName: "Starknet ID", - url: process.env.NEXT_PUBLIC_APP_LINK as string, - chainId: constants.NetworkName.SN_MAIN, - icons: ["https://app.starknet.id/visuals/StarknetIdLogo.svg"], + ArgentMobileConnector.init({ + options: { + dappName: "Starknet ID", + url: process.env.NEXT_PUBLIC_APP_LINK as string, + chainId: constants.NetworkName.SN_MAIN, + icons: ["https://app.starknet.id/visuals/StarknetIdLogo.svg"], + }, }), new WebWalletConnector({ - url: - process.env.NEXT_PUBLIC_IS_TESTNET === "true" - ? "https://web.hydrogen.argent47.net" - : "https://web.argent.xyz/", + url: "https://web.argent.xyz/", }), ]; diff --git a/utils/constants.ts b/utils/constants.ts index 5dd4445d..9c1744fd 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -5,7 +5,7 @@ export const bigAlphabet = "θΏ™ζ₯"; export const totalAlphabet = basicAlphabet + bigAlphabet; export const UINT_128_MAX = (BigInt(1) << BigInt(128)) - BigInt(1); export const MONTH_IN_SECONDS = 30 * 24 * 60 * 60; -export const swissVatRate = 0.077; +export const swissVatRate = 0.081; export const PFP_WL_CONTRACTS_TESTNET = [ "0x041e1382e604688da7f22e7fbb6113ba3649b84a87b58f4dc1cf5bfa96dfc2cf", diff --git a/utils/discounts/argent.ts b/utils/discounts/argent.ts deleted file mode 100644 index 57c14bd0..00000000 --- a/utils/discounts/argent.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const argentDiscount = { - offer: { - duration: 0.246, // 90 days in years - customMessage: "3 months (with Argent discount)", - discountId: "0x617267656e745f6f6e655f646f6c6c6172", - price: "288493150684927", - desc: "Get your domain for the next 3 months at a special discounted price. Don't miss out on this exclusive opportunity only available with your Argent wallet!", - title: { desc: "Mint your domain", catch: "for 1$" }, - image: "/argent/argentdiscount.webp", - }, - name: "Argent 1$ domain", - image: "/argent/argentdiscount.webp", - expiry: 1722470399000, // timestamp in ms - discountMailGroupId: "124587870775149633", - sponsor: "0x64d28d1d1d53a0b5de12e3678699bc9ba32c1cb19ce1c048578581ebb7f8396", -}; diff --git a/utils/discounts/evergreen.ts b/utils/discounts/evergreen.ts index 395080ff..0dd99215 100644 --- a/utils/discounts/evergreen.ts +++ b/utils/discounts/evergreen.ts @@ -1,7 +1,7 @@ export const renewal: Upsell = { - duration: 3, - paidDuration: 2, - maxDuration: 1, + durationInDays: 3 * 365, + paidDurationInDays: 2 * 365, + maxDurationInDays: 1 * 365, discountId: "1", imageUrl: "/register/gift.webp", title: { @@ -12,9 +12,9 @@ export const renewal: Upsell = { }; export const registration: Upsell = { - duration: 3, - paidDuration: 2, - maxDuration: 1, + durationInDays: 3 * 365, + paidDurationInDays: 2 * 365, + maxDurationInDays: 1 * 365, discountId: "1", imageUrl: "/register/gift.webp", title: { diff --git a/utils/discounts/freeRegistration.ts b/utils/discounts/freeRegistration.ts index ce5cbb8b..61a38e2b 100644 --- a/utils/discounts/freeRegistration.ts +++ b/utils/discounts/freeRegistration.ts @@ -13,9 +13,9 @@ export const freeRegistration: FreeRegistration = { expiry: 1726382280 * 1000, // timestamp in ms discountMailGroupId: "X", offer: { - duration: 90, // in days + durationInDays: 90, // in days discountId: "X", - price: "0", + price: BigInt(0), desc: "Unlock your .stark domain for free and secure your Starknet profile!", title: { desc: "Get your", diff --git a/utils/discounts/freeRenewal.ts b/utils/discounts/freeRenewal.ts index 01061b04..058f0f7a 100644 --- a/utils/discounts/freeRenewal.ts +++ b/utils/discounts/freeRenewal.ts @@ -13,10 +13,10 @@ export const freeRenewalDiscount: FreeRenewalDiscount = { expiry: 1816767999000, // timestamp in ms discountMailGroupId: "106085143136961963", offer: { - duration: 90, // in days + durationInDays: 90, // in days customMessage: "3 months free", discountId: "X", // No need - price: "0", + price: BigInt(0), desc: "Get a free 3-months renewal for all your .STARK domains.", title: { desc: "Renew your domain", catch: "for FREE" }, image: "/freeRenewal/freeRenewal.webp", diff --git a/utils/discounts/quantumLeap.ts b/utils/discounts/quantumLeap.ts deleted file mode 100644 index 1615faa7..00000000 --- a/utils/discounts/quantumLeap.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Discount details to fill in case of a duplicate of this page -type QuantumLeapDiscount = { - offer: Discount; - upsell: Discount; - expiry: number; - image: string; - name: string; - discountMailGroupId: string; -}; - -export const quantumLeapDiscount: QuantumLeapDiscount = { - offer: { - duration: 81, // in days - customMessage: "3 months (with Quantum Leap discount)", - discountId: "1", - price: "599178082191783", - desc: "To celebrate the Quantum Leap, get your domain for just $1 and unlock a world of possibilities. Customize your On-chain StarkNet username, elevate your blockchain user experience, and access domain benefits.", - title: { desc: "Get your domain", catch: "for 1$" }, - image: "/quantumleap/quantumLeapAstro.webp", - }, - upsell: { - duration: 1095, // in days - customMessage: "3 years (with Quantum Leap discount)", - discountId: "2", - price: "18899999999999739", - desc: "Don't miss out on this one-time offer special Quantum Leap! Elevate your domain experience with an exclusive opportunity to renew for 3 years, while paying only for 2. Act now before it's gone!", - title: { - desc: "Special Offer:", - catch: "Get 3 years for the price of 2", - }, - image: "/quantumleap/quantumLeapAstro2.webp", - }, - name: "The Quantum Leap", - image: "/quantumleap/quantumLeapAstro.webp", - expiry: 1695308400 * 1000, // timestamp in ms - discountMailGroupId: "98859014745490932", -}; diff --git a/utils/feltService.ts b/utils/feltService.ts index eeea2af5..b538c1cc 100644 --- a/utils/feltService.ts +++ b/utils/feltService.ts @@ -25,36 +25,28 @@ export function hexToDecimal(hex: string | undefined): string { return new BN(hex.slice(2), 16).toString(10); } -export function gweiToEth(gwei: string): string { - if (!gwei || isNaN(Number(gwei))) { - return "0"; +export function weiToEth(wei: string | bigint): number { + const stringed = wei.toString(); + if (!stringed || Number.isNaN(Number(stringed))) { + return 0; } - const gweiBigInt = new Big(gwei); + const weiBig = new Big(stringed); const scaleFactor = new Big(10 ** 18); - const ethBigInt = gweiBigInt.div(scaleFactor).round(5); - - return ethBigInt.toString(); + const ethBig = weiBig.div(scaleFactor); + return Number(ethBig.toString()); } -export function applyRateToBigInt( - bigIntStr: string | bigint, - percentage: number -): string { - // Convert the string to a BigInt - if (typeof bigIntStr === "string") { - bigIntStr = BigInt(bigIntStr); - } - +export function applyRateToBigInt(bigInt: bigint, percentage: number): bigint { // Convert the percentage to an integer by scaling it up by 100 const integerPercentage = BigInt(Math.round(percentage * 100)); // Perform the multiplication - const result = (bigIntStr * integerPercentage) / BigInt(100); + const result = (BigInt(bigInt) * integerPercentage) / BigInt(100); // Convert the result back to a string - return result.toString(); + return result; } // A function that converts a number to a string with max 2 decimals diff --git a/utils/priceService.ts b/utils/priceService.ts index b0169be9..ac3ba199 100644 --- a/utils/priceService.ts +++ b/utils/priceService.ts @@ -1,17 +1,22 @@ -import { gweiToEth } from "./feltService"; -import { getDomainLength } from "./stringService"; +import { weiToEth } from "./feltService"; +import { getDomainLength, selectedDomainsToArray } from "./stringService"; export const PRICES = { - ONE: BigInt("801369863013699") * BigInt(365), - TWO: BigInt("657534246575343") * BigInt(365), - THREE: BigInt("160000000000000") * BigInt(365), - FOUR: BigInt("36986301369863") * BigInt(365), - FIVE: BigInt("24657534246575") * BigInt(365), + ONE: BigInt("801369863013699"), + TWO: BigInt("657534246575343"), + THREE: BigInt("160000000000000"), + FOUR: BigInt("36986301369863"), + FIVE: BigInt("24657534246575"), }; -export function getPriceFromDomain(duration: number, domain: string): bigint { +export function getDomainPriceWei( + durationInDays: number, + domain: string +): bigint { + if (!domain) return BigInt(0); + const domainLength = getDomainLength(domain); - const durationBigInt = BigInt(duration); + const durationBigInt = BigInt(durationInDays); switch (domainLength) { case 0: @@ -29,13 +34,15 @@ export function getPriceFromDomain(duration: number, domain: string): bigint { } } -export function getPriceFromDomains( - domains: string[], - duration: number +export function getManyDomainsPriceWei( + selectedDomains: Record | undefined, + durationInDays: number ): bigint { - // Calculate the sum of all prices with getPriceFromDomain - return domains.reduce( - (acc, domain) => acc + getPriceFromDomain(duration, domain), + if (!selectedDomains) return BigInt(0); + + // Calculate the sum of all prices with getDomainPriceWei + return selectedDomainsToArray(selectedDomains).reduce( + (acc, domain) => acc + getDomainPriceWei(durationInDays, domain), BigInt(0) ); } @@ -48,23 +55,51 @@ export function areDomainSelected( return Object.values(selectedDomains).some((isSelected) => isSelected); } -export function getYearlyPrice(domain: string): string { - if (!domain) return "0"; +export function getYearlyPriceWei(domain: string): bigint { + const priceInWei = getDomainPriceWei(365, domain); - return gweiToEth(String(getPriceFromDomain(1, domain))); + return priceInWei; } export function getTotalYearlyPrice( selectedDomains: Record | undefined -): string { - if (!selectedDomains) return "0"; - - return gweiToEth( - String( - getPriceFromDomains( - Object.keys(selectedDomains).filter((key) => selectedDomains[key]), - 1 - ) - ) +): bigint { + if (!selectedDomains) return BigInt(0); + + const priceInWei = getManyDomainsPriceWei(selectedDomains, 365); + + return priceInWei; +} + +export function getDisplayablePrice(priceInWei: bigint): string { + return weiToEth(priceInWei).toFixed(3).toString(); +} + +export function getApprovalAmount( + price: bigint, + salesTaxAmount: bigint, + durationInYears: number, + currentAllowance: bigint +): bigint { + const TotalPrice = price + salesTaxAmount; + const baseAmount = TotalPrice / BigInt(durationInYears); + const baseApproval = baseAmount * BigInt(10); // 10 years of approval + const amountToApprove = baseApproval + currentAllowance; + + return amountToApprove; +} +export function isApprovalInfinite(approval: bigint | string): boolean { + // Convert approval to a BigInt if it's not already + const approvalBigInt = BigInt(approval); + + // Define the threshold values + const UINT_256_MINUS_UINT_128 = + (BigInt(1) << BigInt(256)) - (BigInt(1) << BigInt(128)); + + // Define a threshold of 10K ETH in wei (10,000 * 10^18) + const THRESHOLD = BigInt(10000) * BigInt(10 ** 18); + + return ( + approvalBigInt >= THRESHOLD || approvalBigInt === UINT_256_MINUS_UINT_128 ); } diff --git a/utils/subscriptionService.ts b/utils/subscriptionService.ts index 70ed4690..f57858da 100644 --- a/utils/subscriptionService.ts +++ b/utils/subscriptionService.ts @@ -1,63 +1,5 @@ -import { CurrencyType, ERC20Contract } from "../utils/constants"; import { isStarkRootDomain } from "./stringService"; -// Processes subscription data to determine if tokens need allowances -export function processSubscriptionData( - data: SubscriptionInfos -): NeedSubscription { - const newNeedSubscription: NeedSubscription = {}; - - // Iterate over each subscription type (e.g., eth_subscriptions, altcoin_subscriptions) - Object.keys(data).forEach((key) => { - const tokenNeedsAllowance: TokenNeedsAllowance = {}; - - // Initialize token needs allowance for each currency type - Object.values(CurrencyType).forEach((currency) => { - tokenNeedsAllowance[currency] = false; - }); - - // Process Ethereum-based subscriptions - if (data[key]?.eth_subscriptions) { - data[key].eth_subscriptions?.forEach((sub) => { - const currency = Object.values(CurrencyType).find( - (currency) => sub.token === ERC20Contract[currency] - ); - // Set allowance requirement to true if allowance is zero - if (currency && BigInt(sub.allowance) === BigInt(0)) { - tokenNeedsAllowance[currency] = true; - } - }); - } else { - // Set allowance requirement to true if eth_subscriptions is null (meaning the domain is not subscribed to Ethereum-based subscriptions) - tokenNeedsAllowance[CurrencyType.ETH] = true; - } - - // Process altcoin-based subscriptions - if (data[key]?.altcoin_subscriptions) { - data[key].altcoin_subscriptions?.forEach((sub) => { - const currency = Object.values(CurrencyType).find( - (currency) => sub.token === ERC20Contract[currency] - ); - // Set allowance requirement to true if allowance is zero - if (currency && BigInt(sub.allowance) === BigInt(0)) { - tokenNeedsAllowance[currency] = true; - } - }); - } else { - // Set allowance to all altcoin currencies to true if altcoin_subscriptions is null (meaning the domain is not subscribed to any altcoin-based subscriptions) - Object.values(CurrencyType).forEach((currency) => { - if (currency !== CurrencyType.ETH) { - tokenNeedsAllowance[currency] = true; - } - }); - } - - // Update the subscription needs for the current key - newNeedSubscription[key] = tokenNeedsAllowance; - }); - return newNeedSubscription; -} - export function getNonSubscribedDomains(data: NeedSubscription): string[] { const result: string[] = []; for (const domain in data) { @@ -73,3 +15,18 @@ export function fullIdsToDomains(fullIds: FullId[]): string[] { .filter((identity: FullId) => isStarkRootDomain(identity.domain)) .map((identity: FullId) => identity.domain); } + +export function processSubscriptionData( + data: SubscriptionInfos +): NeedSubscription { + const processedData: NeedSubscription = {}; + Object.entries(data).forEach(([domain, subscriptions]) => { + if (subscriptions.eth_subscriptions === null) { + processedData[domain] = { + ETH: { needsAllowance: true, currentAllowance: BigInt(0) }, + STRK: { needsAllowance: true, currentAllowance: BigInt(0) }, + }; + } + }); + return processedData; +}