Skip to content

Commit

Permalink
feat: paymaster feature (#841)
Browse files Browse the repository at this point in the history
* feat: paymaster feature

* fixing build

* remove console.log

* forcing qs import

* token menu

* showing usd price

* hiding price

* providing button type

* disabling gasless loading when trad tx selected

* incompatibilty tooltip

* feat: reset gas method if user switch to incompatible account

* cleaning the code

* cleaning the code

* fixed import

* revert package-lock.json

* fix type error

* fix: build

* price debug

* disabling paymaster for non-deployed braavos accounts

* using paymaster rewards

* payment token warning

* fetching gas prices automatically

* cleaning imports

* sponsoring non deployed wallets

* signing & executing wallet deployment sponsoring

* mini updates

* ETH and STRK support for paymaster

* removing gasPrice for non deployed wallets

* fix typed data for non deployed wallets

* providing API key

* showing tx result screen

* removing unused paymaster api key
  • Loading branch information
Marchand-Nicolas authored Jul 10, 2024
1 parent 5cc87db commit bd658e6
Show file tree
Hide file tree
Showing 17 changed files with 12,761 additions and 7,741 deletions.
11 changes: 5 additions & 6 deletions components/UI/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import styles from "../../styles/components/navbar.module.css";
import connectStyles from "../../styles/components/walletConnect.module.css";
import Button from "./button";
import { useConnect, useAccount, useDisconnect } from "@starknet-react/core";
import { sepolia, mainnet } from "@starknet-react/chains";
import ModalMessage from "./modalMessage";
import { useDisplayName } from "../../hooks/displayName.tsx";
import { useMediaQuery } from "@mui/material";
import { CircularProgress } from "@mui/material";
import ModalWallet from "./modalWallet";
import { constants } from "starknet";
import { useTheme } from "@mui/material/styles";
import ProfilFilledIcon from "./iconsComponents/icons/profilFilledIcon";
import DesktopNav from "./desktopNav";
Expand All @@ -32,6 +32,7 @@ import {
import WalletConnect from "./walletConnect";
import ArrowDownIcon from "./iconsComponents/icons/arrowDownIcon";
import errorLottie from "../../public/visuals/errorLottie.json";
import { bigintToStringHex } from "@/utils/stringService";

const Navbar: FunctionComponent = () => {
const theme = useTheme();
Expand All @@ -52,8 +53,8 @@ const Navbar: FunctionComponent = () => {
const { starknetIdNavigator } = useContext(StarknetIdJsContext);
const [showWalletConnectModal, setShowWalletConnectModal] =
useState<boolean>(false);
const [lastConnector, setLastConnector] = useState<Connector | null>(null);

const [lastConnector, setLastConnector] = useState<Connector | null>(null);
// could be replaced by a useProfileData from starknet-react when updated
useEffect(() => {
if (starknetIdNavigator !== null && address !== undefined) {
Expand Down Expand Up @@ -92,10 +93,8 @@ const Navbar: FunctionComponent = () => {
if (!isConnected || !account) return;
account.getChainId().then((chainId) => {
const isWrongNetwork =
(chainId === constants.StarknetChainId.SN_SEPOLIA &&
network === "mainnet") ||
(chainId === constants.StarknetChainId.SN_MAIN &&
network === "testnet");
(chainId === bigintToStringHex(sepolia.id) && network === "mainnet") ||
(chainId === bigintToStringHex(mainnet.id) && network === "testnet");
setIsWrongNetwork(isWrongNetwork);
});
}, [account, network, isConnected]);
Expand Down
76 changes: 58 additions & 18 deletions components/discount/freeRegisterCheckout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from "react";
import type { FunctionComponent } from "react";
import { useEffect, useState } from "react";
import type { Call } from "starknet";
import Button from "../UI/button";
import { useAccount, useContractWrite } from "@starknet-react/core";
import { utils } from "starknetid.js";
import { getDomainWithStark } from "../../utils/stringService";
import { posthog } from "posthog-js";
Expand All @@ -20,6 +18,9 @@ import { getFreeDomain } from "@/utils/campaignService";
import TermCheckbox from "../domains/termCheckbox";
import { useRouter } from "next/router";
import FreeRegisterSummary from "./freeRegisterSummary";
import { useAccount } from "@starknet-react/core";
import { Call } from "starknet";
import usePaymaster from "@/hooks/paymaster";

type FreeRegisterCheckoutProps = {
domain: string;
Expand Down Expand Up @@ -47,19 +48,43 @@ const FreeRegisterCheckout: FunctionComponent<FreeRegisterCheckoutProps> = ({
const [termsBox, setTermsBox] = useState<boolean>(true);
const [metadataHash, setMetadataHash] = useState<string | undefined>();
const { account, address } = useAccount();
const { writeAsync: execute, data: registerData } = useContractWrite({
calls: callData,
});
const [domainsMinting, setDomainsMinting] = useState<Map<string, boolean>>(
new Map()
);
const { addTransaction } = useNotificationManager();
const router = useRouter();
const [tokenId, setTokenId] = useState<number>(0);
const [coupon, setCoupon] = useState<string>("");
const [couponError, setCouponError] = useState<string>("");
const [signature, setSignature] = useState<string[]>(["", ""]);
const [loadingCoupon, setLoadingCoupon] = useState<boolean>(false);
const { addTransaction } = useNotificationManager();
const [transactionHash, setTransactionHash] = useState<string | undefined>();
const {
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);
});

useEffect(() => {
if (!registerData?.transaction_hash) return;
setTransactionHash(registerData.transaction_hash);
}, [registerData]);

// on first load, we generate a salt
useEffect(() => {
Expand Down Expand Up @@ -99,22 +124,22 @@ const FreeRegisterCheckout: FunctionComponent<FreeRegisterCheckoutProps> = ({
}

useEffect(() => {
if (!registerData?.transaction_hash) return;
if (!transactionHash) return;
posthog?.capture("register");
addTransaction({
timestamp: Date.now(),
subtext: "Domain registration",
type: NotificationType.TRANSACTION,
data: {
type: TransactionType.BUY_DOMAIN,
hash: registerData.transaction_hash,
hash: transactionHash,
status: "pending",
},
});

router.push(`/confirmation?tokenId=${tokenId}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerData, tokenId]);
}, [transactionHash, tokenId]);

useEffect(() => {
if (!coupon) {
Expand Down Expand Up @@ -150,13 +175,6 @@ const FreeRegisterCheckout: FunctionComponent<FreeRegisterCheckoutProps> = ({
});
}, [coupon, domain, address]);

const handleRegister = () =>
execute().then(() =>
setDomainsMinting((prev) =>
new Map(prev).set(encodedDomain.toString(), true)
)
);

return (
<div className={styles.container}>
<div className={styles.card}>
Expand All @@ -181,7 +199,21 @@ const FreeRegisterCheckout: FunctionComponent<FreeRegisterCheckoutProps> = ({
</div>
</div>
<div className={styles.summary}>
<FreeRegisterSummary duration={duration} domain={domain} />
<FreeRegisterSummary
duration={duration}
domain={domain}
hasPaymasterRewards={paymasterRewards.length > 0}
gasTokenPrices={gasTokenPrices}
gasTokenPrice={gasTokenPrice}
setGasTokenPrice={setGasTokenPrice}
gasMethod={gasMethod}
setGasMethod={setGasMethod}
paymasterAvailable={
gaslessCompatibility?.isCompatible || sponsoredDeploymentAvailable
}
maxGasTokenAmount={maxGasTokenAmount}
deployed={gaslessCompatibility?.isCompatible}
/>
<Divider className="w-full" />
<TermCheckbox
checked={termsBox}
Expand All @@ -197,13 +229,21 @@ const FreeRegisterCheckout: FunctionComponent<FreeRegisterCheckoutProps> = ({
!targetAddress ||
!termsBox ||
Boolean(couponError) ||
loadingCoupon
loadingCoupon ||
loadingGas ||
loadingDeploymentData
}
>
{!termsBox
? "Please accept terms & policies"
: couponError
? "Enter a valid Coupon"
: loadingGas
? "Loading gas"
: loadingDeploymentData
? paymasterRewards.length > 0
? "Loading deployment data"
: "No Paymaster reward available"
: "Register my domain"}
</Button>
) : (
Expand Down
136 changes: 136 additions & 0 deletions components/discount/freeRegisterSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
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";

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<FreeRegisterSummaryProps> = ({
domain,
duration,
hasPaymasterRewards,
gasTokenPrices,
gasTokenPrice,
setGasTokenPrice,
gasMethod,
setGasMethod,
paymasterAvailable,
maxGasTokenAmount,
deployed,
}) => {
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 (
<div className={styles.pricesSummary}>
<div className={styles.totalDue}>
Expand All @@ -27,6 +56,113 @@ const FreeRegisterSummary: FunctionComponent<FreeRegisterSummaryProps> = ({
<strong>Free</strong>
</p>
</div>
<div className={styles.gasMethods}>
<button
disabled={gasMethod === "traditional"}
onClick={() => setGasMethod("traditional")}
className={
gasMethod === "traditional"
? styles.gasMethodSelected
: styles.gasMethod
}
type="button"
>
Traditional Transaction
</button>
<StyledToolTip
title={`Allows you to pay less gas and choose other currencies to pay fees. ${
paymasterAvailable
? ""
: "Wallet not compatible. Please deploy it or switch to ArgentX in order to use Paymaster."
}`}
>
<button
onClick={() => setGasMethod("paymaster")}
className={
gasMethod === "paymaster"
? styles.gasMethodSelected
: styles.gasMethod
}
type="button"
disabled={!paymasterAvailable}
>
Gasless Transaction
</button>
</StyledToolTip>
</div>
{gasMethod === "paymaster" ? (
hasPaymasterRewards ? (
<div className="flex items-center gap-2">
<DoneIcon width="24" color="green" />
<p className="text-sm">
No gas fees to pay. You have a{" "}
<a
href="https://doc.avnu.fi/starknet-paymaster/introduction"
target="_blank"
rel="noreferrer noopener"
className="underline"
>
Paymaster
</a>{" "}
reward.
</p>
</div>
) : (
<div className="flex flex-col gap-2 w-full">
<p className="text-sm">
No{" "}
<a
href="https://doc.avnu.fi/starknet-paymaster/introduction"
target="_blank"
rel="noreferrer noopener"
className="underline"
>
Paymaster
</a>{" "}
reward. {deployed ? "Please select a gas token." : ""}
</p>
{deployed ? (
<>
<div className={styles.gasMethods}>
{gasTokenPrices?.map((price) => (
<button
disabled={
price.tokenAddress === gasTokenPrice?.tokenAddress
}
onClick={() => setGasTokenPrice(price)}
key={price.tokenAddress}
className={
price.tokenAddress === gasTokenPrice?.tokenAddress
? styles.gasMethodSelected
: styles.gasMethod
}
type="button"
>
{getTokenName(price)}{" "}
</button>
))}
</div>
{gasTokenPrice ? (
<Alert severity="info">
{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.`}
</Alert>
) : null}
</>
) : (
<Alert severity="error">
Your wallet is not deployed. To sponsor its deployment, please
gather rewards.
</Alert>
)}
</div>
)
) : null}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ import { useAccount } from "@starknet-react/core";
import { Call, TypedData, constants } from "starknet";
import { useContractWrite } from "@starknet-react/core";
import { hexToDecimal } from "../../../../utils/feltService";
import { minifyDomain } from "../../../../utils/stringService";
import {
bigintToStringHex,
minifyDomain,
} from "../../../../utils/stringService";
import VerifiedIcon from "../../../UI/iconsComponents/icons/verifiedIcon";
import theme from "../../../../styles/theme";
import { posthog } from "posthog-js";
import ProfilSecurityIcon from "../../../UI/iconsComponents/icons/profilSecurityIcon";
import identityChangeCalls from "../../../../utils/callData/identityChangeCalls";
import { useNotificationManager } from "../../../../hooks/useNotificationManager";
import { NotificationType, TransactionType } from "../../../../utils/constants";
import { mainnet, sepolia } from "@starknet-react/chains";

type ClickablePersonhoodIconProps = {
width: string;
Expand Down Expand Up @@ -221,8 +225,8 @@ const ClickablePersonhoodIcon: FunctionComponent<
walletAddress={address}
starknetChainId={
network === "testnet"
? constants.StarknetChainId.SN_SEPOLIA
: constants.StarknetChainId.SN_MAIN
? bigintToStringHex(sepolia.id)
: bigintToStringHex(mainnet.id)
}
chainType="STARKNET"
/>
Expand Down
Loading

0 comments on commit bd658e6

Please sign in to comment.