diff --git a/.changeset/stupid-experts-shake.md b/.changeset/stupid-experts-shake.md new file mode 100644 index 00000000000..857cf902564 --- /dev/null +++ b/.changeset/stupid-experts-shake.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Requery allowances when getting back to quote screen diff --git a/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts b/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts index 21ef74b8223..3c972437eb8 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts @@ -1,9 +1,6 @@ import type { Hash } from "viem"; import { getCachedChain } from "../../chains/utils.js"; import type { ThirdwebClient } from "../../client/client.js"; -import { getContract } from "../../contract/contract.js"; -import { allowance } from "../../extensions/erc20/__generated__/IERC20/read/allowance.js"; -import { approve } from "../../extensions/erc20/write/approve.js"; import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js"; import { getClientFetch } from "../../utils/fetch.js"; import { stringify } from "../../utils/json.js"; @@ -151,7 +148,7 @@ type BuyWithCryptoQuoteRouteResponse = { */ export type BuyWithCryptoQuote = { transactionRequest: PrepareTransactionOptions; - approval?: PrepareTransactionOptions; + approvalData?: QuoteApprovalInfo; swapDetails: { fromAddress: string; @@ -254,28 +251,6 @@ export async function getBuyWithCryptoQuote( // check if the fromAddress already has approval for the given amount const approvalData = data.approval; - let approval = undefined; - if (approvalData) { - const contract = getContract({ - client: params.client, - address: approvalData.tokenAddress, - chain: getCachedChain(approvalData.chainId), - }); - - const approvedAmount = await allowance({ - contract, - spender: approvalData.spenderAddress, - owner: params.fromAddress, - }); - - if (approvedAmount < BigInt(approvalData.amountWei)) { - approval = approve({ - contract, - spender: approvalData.spenderAddress, - amountWei: BigInt(approvalData.amountWei), - }); - } - } const swapRoute: BuyWithCryptoQuote = { transactionRequest: { @@ -287,7 +262,7 @@ export async function getBuyWithCryptoQuote( gas: BigInt(data.transactionRequest.gasLimit), gasPrice: undefined, // ignore gas price returned by the quote, we handle it ourselves }, - approval: approval, + approvalData, swapDetails: { fromAddress: data.fromAddress, toAddress: data.toAddress, diff --git a/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts b/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts index 2f7d2269094..96580e58f00 100644 --- a/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts +++ b/packages/thirdweb/src/pay/buyWithCrypto/getTransfer.ts @@ -1,8 +1,6 @@ import type { Hash } from "viem"; import { getCachedChain } from "../../chains/utils.js"; import type { ThirdwebClient } from "../../client/client.js"; -import { getContract } from "../../contract/contract.js"; -import { approve } from "../../extensions/erc20/write/approve.js"; import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js"; import type { Address } from "../../utils/address.js"; import { getClientFetch } from "../../utils/fetch.js"; @@ -83,7 +81,7 @@ type BuyWithCryptoTransferResponse = { */ export type BuyWithCryptoTransfer = { transactionRequest: PrepareTransactionOptions; - approval?: PrepareTransactionOptions; + approvalData?: QuoteApprovalInfo; fromAddress: string; toAddress: string; paymentToken: QuotePaymentToken; @@ -159,17 +157,7 @@ export async function getBuyWithCryptoTransfer( value: BigInt(data.transactionRequest.value), gas: BigInt(data.transactionRequest.gasLimit), }, - approval: data.approval - ? approve({ - contract: getContract({ - client: params.client, - address: data.approval.tokenAddress, - chain: getCachedChain(data.approval.chainId), - }), - spender: data.approval.spenderAddress as Address, - amountWei: BigInt(data.approval.amountWei), - }) - : undefined, + approvalData: data.approval, fromAddress: data.fromAddress, toAddress: data.toAddress, paymentToken: data.paymentToken, diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx index a762ed2426f..3ac5f3246ef 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx @@ -1,8 +1,11 @@ -import { useQueryClient } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo, useState } from "react"; import type { Chain } from "../../../../../../chains/types.js"; +import { getCachedChain } from "../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js"; +import { getContract } from "../../../../../../contract/contract.js"; +import { allowance } from "../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js"; import type { GetBuyWithCryptoQuoteParams } from "../../../../../../pay/buyWithCrypto/getQuote.js"; import type { BuyWithCryptoStatus } from "../../../../../../pay/buyWithCrypto/getStatus.js"; import type { BuyWithFiatStatus } from "../../../../../../pay/buyWithFiat/getStatus.js"; @@ -322,6 +325,7 @@ function BuyScreenContent(props: BuyScreenContentProps) { }); }} onSuccess={onSwapSuccess} + approvalAmount={screen.approvalAmount} /> ); } @@ -992,6 +996,30 @@ function SwapScreenContent(props: { gcTime: 30 * 1000, }); + const allowanceQuery = useQuery({ + queryKey: [ + "allowance", + payer.account.address, + quoteQuery.data?.approvalData, + ], + queryFn: () => { + if (!quoteQuery.data?.approvalData) { + return null; + } + return allowance({ + contract: getContract({ + client: props.client, + address: quoteQuery.data.swapDetails.fromToken.tokenAddress, + chain: getCachedChain(quoteQuery.data.swapDetails.fromToken.chainId), + }), + spender: quoteQuery.data.approvalData.spenderAddress, + owner: props.payer.account.address, + }); + }, + enabled: !!quoteQuery.data?.approvalData, + refetchOnMount: true, + }); + const sourceTokenAmount = swapRequired ? quoteQuery.data?.swapDetails.fromAmount : tokenAmount; @@ -1002,7 +1030,9 @@ function SwapScreenContent(props: { Number(fromTokenBalanceQuery.data.displayValue) < Number(sourceTokenAmount); const disableContinue = - (swapRequired && !quoteQuery.data) || isNotEnoughBalance; + (swapRequired && !quoteQuery.data) || + isNotEnoughBalance || + allowanceQuery.isLoading; const switchChainRequired = props.payer.wallet.getChain()?.id !== fromChain.id; @@ -1047,6 +1077,7 @@ function SwapScreenContent(props: { setScreen({ id: "swap-flow", quote: quoteQuery.data, + approvalAmount: allowanceQuery.data ?? undefined, }); } @@ -1155,6 +1186,7 @@ function SwapScreenContent(props: { ) : switchChainRequired && !quoteQuery.isLoading && + !allowanceQuery.isLoading && !isNotEnoughBalance && !quoteQuery.error ? ( function getErrorMessage(err: any): ApiError { - if (typeof err.error === "object") { + if (typeof err.error === "object" && err.error.code) { return err.error; } return { diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx index 0f100892c24..200820251b3 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx @@ -1,6 +1,9 @@ import { useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; +import { getCachedChain } from "../../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; +import { getContract } from "../../../../../../../contract/contract.js"; +import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js"; import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js"; import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; import { getPostOnRampQuote } from "../../../../../../../pay/buyWithFiat/getPostOnRampQuote.js"; @@ -43,11 +46,38 @@ export function PostOnRampSwap(props: { refetchOnWindowFocus: false, }); + const allowanceQuery = useQuery({ + queryKey: [ + "allowance", + props.payer.account.address, + postOnRampQuoteQuery.data?.approvalData, + ], + queryFn: () => { + if (!postOnRampQuoteQuery.data?.approvalData) { + return null; + } + return allowance({ + contract: getContract({ + client: props.client, + address: postOnRampQuoteQuery.data.swapDetails.fromToken.tokenAddress, + chain: getCachedChain( + postOnRampQuoteQuery.data.swapDetails.fromToken.chainId, + ), + }), + spender: postOnRampQuoteQuery.data.approvalData.spenderAddress, + owner: props.payer.account.address, + }); + }, + enabled: !!postOnRampQuoteQuery.data?.approvalData, + refetchOnMount: true, + }); + useEffect(() => { if ( postOnRampQuoteQuery.data && !lockedOnRampQuote && - !postOnRampQuoteQuery.isRefetching + !postOnRampQuoteQuery.isRefetching && + !allowanceQuery.isLoading ) { setLockedOnRampQuote(postOnRampQuoteQuery.data); } @@ -55,6 +85,7 @@ export function PostOnRampSwap(props: { postOnRampQuoteQuery.data, lockedOnRampQuote, postOnRampQuoteQuery.isRefetching, + allowanceQuery.isLoading, ]); if (postOnRampQuoteQuery.isError) { @@ -133,6 +164,7 @@ export function PostOnRampSwap(props: { transactionMode={props.transactionMode} isEmbed={props.isEmbed} onSuccess={props.onSuccess} + approvalAmount={allowanceQuery.data ?? undefined} /> ); } diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts index 8ab5ec98a14..0c2373ab5b9 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/main/types.ts @@ -34,6 +34,7 @@ export type SelectedScreen = | { id: "swap-flow"; quote: BuyWithCryptoQuote; + approvalAmount?: bigint; } | { id: "fiat-flow"; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx index f807aacbbde..dd6161528fe 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx @@ -3,6 +3,8 @@ import { useState } from "react"; import { trackPayEvent } from "../../../../../../../analytics/track/pay.js"; import type { Chain } from "../../../../../../../chains/types.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; +import { getContract } from "../../../../../../../contract/contract.js"; +import { approve } from "../../../../../../../extensions/erc20/write/approve.js"; import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js"; import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js"; import { waitForReceipt } from "../../../../../../../transaction/actions/wait-for-tx-receipt.js"; @@ -51,9 +53,13 @@ export function SwapConfirmationScreen(props: { fromTokenSymbol: string; isFiatFlow: boolean; payer: PayerInfo; + preApprovedAmount?: bigint; }) { - const isApprovalRequired = props.quote.approval !== undefined; - const initialStep = isApprovalRequired ? "approval" : "swap"; + const needsApproval = + props.quote.approvalData && + props.preApprovedAmount !== undefined && + props.preApprovedAmount < BigInt(props.quote.approvalData.amountWei); + const initialStep = needsApproval ? "approval" : "swap"; const [step, setStep] = useState<"approval" | "swap">(initialStep); const [status, setStatus] = useState< @@ -136,7 +142,7 @@ export function SwapConfirmationScreen(props: { {/* Show 2 steps - Approve and confirm */} - {isApprovalRequired && ( + {needsApproval && ( <> { - if (step === "approval" && props.quote.approval) { + if (step === "approval" && props.quote.approvalData) { try { setStatus("pending"); @@ -204,13 +210,22 @@ export function SwapConfirmationScreen(props: { dstChainId: props.quote.swapDetails.toToken.chainId, }); + const transaction = approve({ + contract: getContract({ + client: props.client, + address: props.quote.swapDetails.fromToken.tokenAddress, + chain: props.fromChain, + }), + spender: props.quote.approvalData.spenderAddress, + amountWei: BigInt(props.quote.approvalData.amountWei), + }); + const tx = await sendTransaction({ account: props.payer.account, - transaction: props.quote.approval, + transaction, }); await waitForReceipt({ ...tx, maxBlocksWaitTime: 50 }); - // props.onQuoteFinalized(props.quote); trackPayEvent({ event: "swap_approval_success", diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx index 672ccc8d3a5..8124361dfee 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapFlow.tsx @@ -22,6 +22,7 @@ type SwapFlowProps = { transactionMode: boolean; isEmbed: boolean; onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined; + approvalAmount?: bigint; }; export function SwapFlow(props: SwapFlowProps) { @@ -109,6 +110,7 @@ export function SwapFlow(props: SwapFlowProps) { quote={quote} isFiatFlow={props.isFiatFlow} payer={props.payer} + preApprovedAmount={props.approvalAmount} /> ); } diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx index c675df5f686..e2d1efb7c6f 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/TransferConfirmationScreen.tsx @@ -1,14 +1,18 @@ import { CheckCircledIcon } from "@radix-ui/react-icons"; import { useState } from "react"; import type { Chain } from "../../../../../../../chains/types.js"; +import { getCachedChain } from "../../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js"; import { getContract } from "../../../../../../../contract/contract.js"; +import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js"; +import { approve } from "../../../../../../../extensions/erc20/write/approve.js"; import { transfer } from "../../../../../../../extensions/erc20/write/transfer.js"; import { getBuyWithCryptoTransfer } from "../../../../../../../pay/buyWithCrypto/getTransfer.js"; import { sendAndConfirmTransaction } from "../../../../../../../transaction/actions/send-and-confirm-transaction.js"; import { sendTransaction } from "../../../../../../../transaction/actions/send-transaction.js"; import { prepareTransaction } from "../../../../../../../transaction/prepare-transaction.js"; +import type { Address } from "../../../../../../../utils/address.js"; import { toWei } from "../../../../../../../utils/units.js"; import { iconSize } from "../../../../../../core/design-system/index.js"; import type { PayUIOptions } from "../../../../../../core/hooks/connection/ConnectButtonProps.js"; @@ -248,13 +252,46 @@ export function TransferConfirmationScreen( purchaseData: payOptions?.purchaseData, }); - if (transferResponse.approval) { - setStep("approve"); - // approve the transfer - await sendAndConfirmTransaction({ - account: props.payer.account, - transaction: transferResponse.approval, + if (transferResponse.approvalData) { + // check allowance + const prevAllowance = await allowance({ + contract: getContract({ + client: client, + address: transferResponse.approvalData.tokenAddress, + chain: getCachedChain( + transferResponse.approvalData.chainId, + ), + }), + spender: transferResponse.approvalData + .spenderAddress as Address, + owner: payer.account.address, }); + + if ( + prevAllowance < + BigInt(transferResponse.approvalData.amountWei) + ) { + setStep("approve"); + const transaction = approve({ + contract: getContract({ + client: client, + address: transferResponse.approvalData.tokenAddress, + chain: getCachedChain( + transferResponse.approvalData.chainId, + ), + }), + spender: transferResponse.approvalData + .spenderAddress as Address, + amountWei: BigInt( + transferResponse.approvalData.amountWei, + ), + }); + // approve the transfer + await sendAndConfirmTransaction({ + account: props.payer.account, + transaction, + }); + } } setStep("transfer");