From 8f3e3d203a0dff1d1eff258d991273ea173ffce3 Mon Sep 17 00:00:00 2001 From: Nico Date: Mon, 22 Jul 2024 10:35:29 +0200 Subject: [PATCH] feat: free registration starknetCC problems (#854) * feat: free registration starknetCC problems * ready for whitelist * improving connect menu * moving to navbar * working for braavos + fix non explicit "loading gas" message * showing errors in console * disable button if no coupon * more detailed error message * refreshing when switching account * improving tx validity detection * making code more resilient * Update hooks/isDeployed.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * improving deployment data gathering * improving support for argent mobile * improving support for argent mobile * disabling paymaster for argent mobile --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- components/UI/connectButton.tsx | 2 +- components/UI/navbar.tsx | 8 + components/discount/freeRegisterCheckout.tsx | 69 ++++----- components/discount/freeRegisterSummary.tsx | 153 +++---------------- hooks/isDeployed.tsx | 23 ++- hooks/paymaster.tsx | 119 +++++++-------- 6 files changed, 131 insertions(+), 243 deletions(-) diff --git a/components/UI/connectButton.tsx b/components/UI/connectButton.tsx index a9c0ff63..ed7ef2fb 100644 --- a/components/UI/connectButton.tsx +++ b/components/UI/connectButton.tsx @@ -40,7 +40,7 @@ const ConnectButton: FunctionComponent = () => { className={styles.btnIcon} /> ) : null} -

Connect wallet

+

Connect wallet

{lastConnector ? (
{ const theme = useTheme(); @@ -53,6 +54,13 @@ const Navbar: FunctionComponent = () => { const { starknetIdNavigator } = useContext(StarknetIdJsContext); const [showWalletConnectModal, setShowWalletConnectModal] = useState(false); + const router = useRouter(); + + useEffect(() => { + const pageName = router.pathname.split("/")[1]; + if (pageName !== "gift" && pageName !== "register") return; + if (isMobile) setShowWalletConnectModal(true); + }, [isMobile, router.pathname]); const [lastConnector, setLastConnector] = useState(null); // could be replaced by a useProfileData from starknet-react when updated diff --git a/components/discount/freeRegisterCheckout.tsx b/components/discount/freeRegisterCheckout.tsx index 6b89dfd4..353dc34e 100644 --- a/components/discount/freeRegisterCheckout.tsx +++ b/components/discount/freeRegisterCheckout.tsx @@ -63,23 +63,21 @@ const FreeRegisterCheckout: FunctionComponent = ({ handleRegister, data: registerData, paymasterRewards, - gasTokenPrices, - gasTokenPrice, loadingGas, - gasMethod, - setGasMethod, - gaslessCompatibility, - setGasTokenPrice, - sponsoredDeploymentAvailable, - maxGasTokenAmount, loadingDeploymentData, - } = usePaymaster(callData, async (transactionHash) => { - setDomainsMinting((prev) => - new Map(prev).set(encodedDomain.toString(), true) - ); - console.log(transactionHash); - if (transactionHash) setTransactionHash(transactionHash); - }); + refreshRewards, + invalidTx, + loadingTypedData, + } = usePaymaster( + callData, + async (transactionHash) => { + setDomainsMinting((prev) => + new Map(prev).set(encodedDomain.toString(), true) + ); + if (transactionHash) setTransactionHash(transactionHash); + }, + !coupon + ); useEffect(() => { if (!registerData?.transaction_hash) return; @@ -95,6 +93,11 @@ const FreeRegisterCheckout: FunctionComponent = ({ if (address) setTargetAddress(address); }, [address]); + useEffect(() => { + if (loadingCoupon || !coupon) return; + refreshRewards(); + }, [loadingCoupon, coupon, refreshRewards, address]); + useEffect(() => { // salt must not be empty to preserve privacy if (!salt) return; @@ -104,11 +107,11 @@ const FreeRegisterCheckout: FunctionComponent = ({ }, [salt]); useEffect(() => { + if (signature[0] === null) return; // Variables const newTokenId: number = Math.floor(Math.random() * 1000000000000); setTokenId(newTokenId); const txMetadataHash = `0x${metadataHash}` as HexString; - const freeRegisterCalls = registrationCalls.getFreeRegistrationCalls( newTokenId, encodedDomain, @@ -142,11 +145,7 @@ const FreeRegisterCheckout: FunctionComponent = ({ }, [transactionHash, tokenId]); useEffect(() => { - if (!coupon) { - setCouponError("Please enter a coupon code"); - setLoadingCoupon(false); - return; - } + if (!coupon) return setLoadingCoupon(false); const lastSuccessCoupon = localStorage.getItem("lastSuccessCoupon"); if (coupon === lastSuccessCoupon) { setCouponError(""); @@ -199,21 +198,7 @@ const FreeRegisterCheckout: FunctionComponent = ({
- 0} - gasTokenPrices={gasTokenPrices} - gasTokenPrice={gasTokenPrice} - setGasTokenPrice={setGasTokenPrice} - gasMethod={gasMethod} - setGasMethod={setGasMethod} - paymasterAvailable={ - gaslessCompatibility?.isCompatible || sponsoredDeploymentAvailable - } - maxGasTokenAmount={maxGasTokenAmount} - deployed={gaslessCompatibility?.isCompatible} - /> + = ({ disabled={ (domainsMinting.get(encodedDomain) as boolean) || !account || + !coupon || !duration || !targetAddress || !termsBox || Boolean(couponError) || loadingCoupon || loadingGas || - loadingDeploymentData + loadingDeploymentData || + loadingTypedData } > {!termsBox ? "Please accept terms & policies" - : couponError + : couponError || !coupon ? "Enter a valid Coupon" : loadingGas - ? "Loading gas" + ? invalidTx + ? "Invalid signature" + : "Loading gas" + : loadingTypedData + ? "Building typed data" : loadingDeploymentData ? paymasterRewards.length > 0 ? "Loading deployment data" diff --git a/components/discount/freeRegisterSummary.tsx b/components/discount/freeRegisterSummary.tsx index cf9bbe8e..0bfb3da3 100644 --- a/components/discount/freeRegisterSummary.tsx +++ b/components/discount/freeRegisterSummary.tsx @@ -2,48 +2,23 @@ import React, { FunctionComponent } from "react"; import styles from "../../styles/components/registerV3.module.css"; import { getYearlyPrice } from "@/utils/priceService"; import DoneIcon from "../UI/iconsComponents/icons/doneIcon"; -import { GasTokenPrice } from "@avnu/gasless-sdk"; -import { tokenNames } from "@/utils/altcoinService"; -import { shortenDomain } from "@/utils/stringService"; -import StyledToolTip from "../UI/styledTooltip"; -import { GasMethod } from "@/hooks/paymaster"; -import { Alert } from "@mui/material"; +import { useAccount } from "@starknet-react/core"; type FreeRegisterSummaryProps = { duration: number; domain: string; - hasPaymasterRewards?: boolean; - gasTokenPrices?: GasTokenPrice[]; - gasTokenPrice?: GasTokenPrice; - setGasTokenPrice: (price: GasTokenPrice) => void; - gasMethod: GasMethod; - setGasMethod: (method: GasMethod) => void; - paymasterAvailable: boolean; - maxGasTokenAmount?: bigint; - deployed?: boolean; }; const FreeRegisterSummary: FunctionComponent = ({ domain, duration, - hasPaymasterRewards, - gasTokenPrices, - gasTokenPrice, - setGasTokenPrice, - gasMethod, - setGasMethod, - paymasterAvailable, - maxGasTokenAmount, - deployed, }) => { + const { address } = useAccount(); + function getMessage() { return `${Math.floor(duration / 30)} months of domain registration`; } - const getTokenName = (price: GasTokenPrice) => - tokenNames[price.tokenAddress as keyof typeof tokenNames] || - shortenDomain(price.tokenAddress); - return (
@@ -56,112 +31,22 @@ const FreeRegisterSummary: FunctionComponent = ({ Free

-
- - - - -
- {gasMethod === "paymaster" ? ( - hasPaymasterRewards ? ( -
- -

- No gas fees to pay. You have a{" "} - - Paymaster - {" "} - reward. -

-
- ) : ( -
-

- No{" "} - - Paymaster - {" "} - reward. {deployed ? "Please select a gas token." : ""} -

- {deployed ? ( - <> -
- {gasTokenPrices?.map((price) => ( - - ))} -
- {gasTokenPrice ? ( - - {maxGasTokenAmount - ? `Please make sure to have at least ${maxGasTokenAmount.toString()} ${getTokenName( - gasTokenPrice - )} to prevent transaction failure.` - : `Please make sure to have enough ${getTokenName( - gasTokenPrice - )} to prevent transaction failure.`} - - ) : null} - - ) : ( - - Your wallet is not deployed. To sponsor its deployment, please - gather rewards. - - )} -
- ) + {address ? ( +
+ +

+ No gas fees to pay. You have a{" "} + + Paymaster + {" "} + reward. +

+
) : null}
diff --git a/hooks/isDeployed.tsx b/hooks/isDeployed.tsx index 037e90a0..25c57a5d 100644 --- a/hooks/isDeployed.tsx +++ b/hooks/isDeployed.tsx @@ -10,9 +10,26 @@ export default function isStarknetDeployed(address?: string) { const [isDeployed, setIsDeployed] = useState(false); const [deploymentData, setDeploymentData] = useState(); + const [nonDeployedAddress, setNonDeployedAddress] = useState(); + const [reload, setReload] = useState(false); useEffect(() => { - if (!address || !provider || !connector?.id) return; + if (isDeployed || deploymentData) return; + const interval = setInterval(() => { + setReload(true); + }, 3000); + return () => clearInterval(interval); + }, [isDeployed, deploymentData]); + + useEffect(() => { + if (reload) return setReload(false); + if ( + !address || + !provider || + !connector?.id || + address === nonDeployedAddress + ) + return; const checkIsDeployed = async () => { try { provider @@ -33,7 +50,6 @@ export default function isStarknetDeployed(address?: string) { setDeploymentData(undefined); return; } - availableWallets.forEach(async (connectedWallet) => { if ( connectedWallet.id === connector?.id && @@ -47,6 +63,7 @@ export default function isStarknetDeployed(address?: string) { ); if (isGetDeploymentDataResult(data)) { setDeploymentData(data); + setNonDeployedAddress(address); } else { console.error( "Received data is not in the expected format:", @@ -63,7 +80,7 @@ export default function isStarknetDeployed(address?: string) { }; checkIsDeployed(); - }, [address, provider, connector?.id]); + }, [address, provider, connector?.id, nonDeployedAddress, reload]); return { isDeployed, deploymentData }; } diff --git a/hooks/paymaster.tsx b/hooks/paymaster.tsx index 8204a829..be15b6f7 100644 --- a/hooks/paymaster.tsx +++ b/hooks/paymaster.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { fetchAccountCompatibility, fetchAccountsRewards, @@ -7,14 +7,13 @@ import { getGasFeesInGasToken, GasTokenPrice, fetchGasTokenPrices, - fetchGaslessStatus, executeCalls, } from "@avnu/gasless-sdk"; import { - useContractWrite, useProvider, useAccount, useConnect, + useContractWrite, } from "@starknet-react/core"; import { AccountInterface, @@ -29,83 +28,63 @@ import isStarknetDeployed from "./isDeployed"; import { gaslessOptions } from "@/utils/constants"; import { decimalToHex } from "@/utils/feltService"; -export type GasMethod = "traditional" | "paymaster"; - const usePaymaster = ( callData: Call[], - then: (transactionHash: string) => void + then: (transactionHash: string) => void, + loadingCallData: boolean ) => { const { account } = useAccount(); - const [gaslessAPIAvailable, setGaslessAPIAvailable] = useState(true); const [gaslessCompatibility, setGaslessCompatibility] = useState(); const [gasTokenPrices, setGasTokenPrices] = useState([]); const [maxGasTokenAmount, setMaxGasTokenAmount] = useState(); - const [gasMethod, setGasMethod] = useState("traditional"); const { provider } = useProvider(); const [paymasterRewards, setPaymasterRewards] = useState( [] ); const [gasTokenPrice, setGasTokenPrice] = useState(); const [loadingGas, setLoadingGas] = useState(false); - const [sponsoredDeploymentAvailable, setSponsoredDeploymentAvailable] = - useState(false); const { writeAsync: execute, data } = useContractWrite({ calls: callData, }); const { connector } = useConnect(); const { isDeployed, deploymentData } = isStarknetDeployed(account?.address); const [deploymentTypedData, setDeploymentTypedData] = useState(); + const [invalidTx, setInvalidTx] = useState(false); - useEffect(() => { - if (!account || !connector) return; - setSponsoredDeploymentAvailable( - connector.id === "argentX" || connector.id === "argentMobile" - ); - }, [account, connector]); + const argentWallet = useMemo( + () => connector?.id === "argentX" /*|| connector?.id === "argentMobile"*/, + [connector] + ); useEffect(() => { if (!gasTokenPrice) setGasTokenPrice(gasTokenPrices[0]); }, [gasTokenPrice, gasTokenPrices]); - useEffect(() => { - if (gaslessCompatibility?.isCompatible && paymasterRewards.length > 0) - setGasMethod("paymaster"); - }, [gaslessCompatibility, paymasterRewards]); - - useEffect(() => { - if (!gaslessCompatibility?.isCompatible) setGasMethod("traditional"); - }, [gaslessCompatibility]); - - useEffect(() => { - fetchGaslessStatus(gaslessOptions).then((res) => { - setGaslessAPIAvailable(res.status); - }); - }, []); - - useEffect(() => { - if (gasMethod === "traditional" && loadingGas) setLoadingGas(false); - }, [gasMethod, loadingGas]); + const refreshRewards = useCallback(() => { + if (!account) return; + fetchAccountsRewards(account.address, { + ...gaslessOptions, + protocol: "STARKNETID", + }).then(setPaymasterRewards); + }, [account]); useEffect(() => { - if (!account || !gaslessAPIAvailable) return; + if (!account) return; fetchAccountCompatibility(account.address, gaslessOptions) .then(setGaslessCompatibility) .catch((e) => { setGaslessCompatibility(undefined); console.error(e); }); - fetchAccountsRewards(account.address, { - ...gaslessOptions, - protocol: "STARKNETID", - }).then(setPaymasterRewards); - }, [account, gaslessAPIAvailable]); + refreshRewards(); + }, [account, refreshRewards]); const estimateCalls = useCallback( async ( account: AccountInterface, calls: Call[] - ): Promise => { + ): Promise => { const contractVersion = await provider.getContractVersion( account.address ); @@ -117,12 +96,17 @@ const usePaymaster = ( calldata: transaction.getExecuteCalldata(calls, contractVersion.cairo), signature: [], }; - return provider.getInvokeEstimateFee( - { ...invocation }, - { ...details, nonce }, - "pending", - true - ); + return provider + .getInvokeEstimateFee( + { ...invocation }, + { ...details, nonce }, + "pending", + true + ) + .catch((e) => { + console.error(e); + setInvalidTx(true); + }); }, [provider] ); @@ -136,16 +120,12 @@ const usePaymaster = ( }, []); useEffect(() => { - if ( - !account || - !gasTokenPrice || - !gaslessCompatibility || - !gaslessAPIAvailable || - gasMethod === "traditional" - ) + if (!account || !gasTokenPrice || !gaslessCompatibility || loadingCallData) return; setLoadingGas(true); estimateCalls(account, callData).then((fees) => { + if (!fees) return; + setInvalidTx(false); const estimatedGasFeesInGasToken = getGasFeesInGasToken( BigInt(fees.overall_fee), gasTokenPrice, @@ -158,21 +138,24 @@ const usePaymaster = ( setLoadingGas(false); }); }, [ - gasMethod, callData, account, gasTokenPrice, gaslessCompatibility, estimateCalls, - gaslessAPIAvailable, + loadingCallData, ]); + const loadingDeploymentData = + connector?.id !== "argentMobile" && !isDeployed && !deploymentData; + useEffect(() => { if ( !account || isDeployed || !deploymentData || - !sponsoredDeploymentAvailable + !argentWallet || + loadingDeploymentData ) return; fetch(`${gaslessOptions.baseUrl}/gasless/v1/build-typed-data`, { @@ -188,7 +171,7 @@ const usePaymaster = ( }) .then((res) => res.json()) .then((data) => { - if (data.messages) return; + if (data.messages || data.error) return; setDeploymentTypedData(data); }) .catch((error) => { @@ -198,14 +181,15 @@ const usePaymaster = ( account, isDeployed, deploymentData, - sponsoredDeploymentAvailable, + argentWallet, callData, maxGasTokenAmount, + loadingDeploymentData, ]); const handleRegister = () => { if (!account) return; - if (gasMethod === "paymaster") { + if (argentWallet && (connector?.id !== "argentMobile" || isDeployed)) { if (deploymentData && deploymentTypedData) { account .signMessage( @@ -232,7 +216,10 @@ const usePaymaster = ( return then(data.transactionHash); }) .catch((error) => { - console.error("Error when executing with Paymaster:", error); + console.error( + "Error when executing (including deployment) with Paymaster:", + error + ); }); }); } else @@ -254,8 +241,8 @@ const usePaymaster = ( } else execute().then((res) => then(res.transaction_hash)); }; - const loadingDeploymentData = - !isDeployed && !deploymentTypedData && gasMethod === "paymaster"; + const loadingTypedData = + connector?.id !== "argentMobile" && deploymentData && !deploymentTypedData; return { handleRegister, @@ -265,12 +252,12 @@ const usePaymaster = ( gasTokenPrice, loadingGas, setGasTokenPrice, - gasMethod, - setGasMethod, gaslessCompatibility, - sponsoredDeploymentAvailable, maxGasTokenAmount, loadingDeploymentData, + refreshRewards, + invalidTx, + loadingTypedData, }; };