Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-wombats-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Skip swap approvals if already approved and always calculate gas prices locally
33 changes: 19 additions & 14 deletions apps/playground-web/src/components/pay/embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,29 @@ import { THIRDWEB_CLIENT } from "@/lib/client";
import { useTheme } from "next-themes";
import { base } from "thirdweb/chains";
import { PayEmbed } from "thirdweb/react";
import { StyledConnectButton } from "../styled-connect-button";

export function StyledPayEmbedPreview() {
const { theme } = useTheme();

return (
<PayEmbed
client={THIRDWEB_CLIENT}
theme={theme === "light" ? "light" : "dark"}
payOptions={{
mode: "fund_wallet",
metadata: {
name: "Get funds",
},
prefillBuy: {
chain: base,
amount: "0.01",
},
}}
/>
<>
<StyledConnectButton />
<div className="h-10" />
<PayEmbed
client={THIRDWEB_CLIENT}
theme={theme === "light" ? "light" : "dark"}
payOptions={{
mode: "fund_wallet",
metadata: {
name: "Get funds",
},
prefillBuy: {
chain: base,
amount: "0.01",
},
}}
/>
</>
);
}
40 changes: 28 additions & 12 deletions packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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";
Expand Down Expand Up @@ -251,6 +252,31 @@ export async function getBuyWithCryptoQuote(
const data: BuyWithCryptoQuoteRouteResponse = (await response.json())
.result;

// 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: {
chain: getCachedChain(data.transactionRequest.chainId),
Expand All @@ -259,19 +285,9 @@ export async function getBuyWithCryptoQuote(
to: data.transactionRequest.to,
value: BigInt(data.transactionRequest.value),
gas: BigInt(data.transactionRequest.gasLimit),
gasPrice: BigInt(data.transactionRequest.gasPrice),
gasPrice: undefined, // ignore gas price returned by the quote, we handle it ourselves
},
approval: data.approval
? approve({
contract: getContract({
client: params.client,
address: data.approval.tokenAddress,
chain: getCachedChain(data.approval.chainId),
}),
spender: data.approval?.spenderAddress,
amountWei: BigInt(data.approval.amountWei),
})
: undefined,
approval: approval,
swapDetails: {
fromAddress: data.fromAddress,
toAddress: data.toAddress,
Expand Down
2 changes: 1 addition & 1 deletion packages/thirdweb/src/pay/utils/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ export type PayOnChainTransactionDetails = {
explorerLink?: string;
};

export type FiatProvider = "STRIPE" | "TRANSAK" | "KADO";
export type FiatProvider = "STRIPE" | "TRANSAK" | "KADO" | "COINBASE";
Original file line number Diff line number Diff line change
Expand Up @@ -1006,34 +1006,6 @@ function SwapScreenContent(props: {
const switchChainRequired =
props.payer.wallet.getChain()?.id !== fromChain.id;

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function getErrorMessage(err: any) {
type AmountTooLowError = {
code: "MINIMUM_PURCHASE_AMOUNT";
data: {
minimumAmountUSDCents: number;
requestedAmountUSDCents: number;
minimumAmountWei: string;
minimumAmountEth: string;
};
};

const defaultMessage = "Unable to get price quote";
try {
if (err.error.code === "MINIMUM_PURCHASE_AMOUNT") {
const obj = err.error as AmountTooLowError;
const minAmountToken = obj.data.minimumAmountEth;
return {
minAmount: formatNumber(Number(minAmountToken), 6),
};
}
} catch {}

return {
msg: [defaultMessage],
};
}

const errorMsg =
!quoteQuery.isLoading && quoteQuery.error
? getErrorMessage(quoteQuery.error)
Expand Down Expand Up @@ -1133,9 +1105,10 @@ function SwapScreenContent(props: {
{/* Error message */}
{errorMsg && (
<div>
{errorMsg.minAmount && (
{errorMsg.data?.minimumAmountEth ? (
<Text color="danger" size="sm" center multiline>
Minimum amount is {errorMsg.minAmount}{" "}
Minimum amount is{" "}
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
<TokenSymbol
token={toToken}
chain={toChain}
Expand All @@ -1144,13 +1117,11 @@ function SwapScreenContent(props: {
color="danger"
/>
</Text>
)}

{errorMsg.msg?.map((msg) => (
<Text color="danger" size="sm" center multiline key={msg}>
{msg}
) : (
<Text color="danger" size="sm" center multiline>
{errorMsg.message || defaultMessage}
</Text>
))}
)}
</div>
)}

Expand All @@ -1166,12 +1137,17 @@ function SwapScreenContent(props: {
)}

{/* Button */}
{errorMsg?.minAmount ? (
{errorMsg?.data?.minimumAmountEth ? (
<Button
variant="accent"
fullWidth
onClick={() => {
props.setTokenAmount(String(errorMsg.minAmount));
props.setTokenAmount(
formatNumber(
Number(errorMsg.data?.minimumAmountEth),
6,
).toString(),
);
props.setHasEditedAmount(true);
}}
>
Expand Down Expand Up @@ -1306,34 +1282,6 @@ function FiatScreenContent(props: {
setIsOpen(true);
}

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function getErrorMessage(err: any) {
type AmountTooLowError = {
code: "MINIMUM_PURCHASE_AMOUNT";
data: {
minimumAmountUSDCents: number;
requestedAmountUSDCents: number;
minimumAmountWei: string;
minimumAmountEth: string;
};
};

const defaultMessage = "Unable to get price quote";
try {
if (err.error.code === "MINIMUM_PURCHASE_AMOUNT") {
const obj = err.error as AmountTooLowError;
const minAmountToken = obj.data.minimumAmountEth;
return {
minAmount: formatNumber(Number(minAmountToken), 6),
};
}
} catch {}

return {
msg: [defaultMessage],
};
}

const disableSubmit = !fiatQuoteQuery.data;

const errorMsg =
Expand Down Expand Up @@ -1381,9 +1329,10 @@ function FiatScreenContent(props: {
{/* Error message */}
{errorMsg && (
<div>
{errorMsg.minAmount && (
{errorMsg.data?.minimumAmountEth ? (
<Text color="danger" size="sm" center multiline>
Minimum amount is {errorMsg.minAmount}{" "}
Minimum amount is{" "}
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
<TokenSymbol
token={toToken}
chain={toChain}
Expand All @@ -1392,22 +1341,25 @@ function FiatScreenContent(props: {
color="danger"
/>
</Text>
)}

{errorMsg.msg?.map((msg) => (
<Text color="danger" size="sm" center multiline key={msg}>
{msg}
) : (
<Text color="danger" size="sm" center multiline>
{errorMsg.message || defaultMessage}
</Text>
))}
)}
</div>
)}

{errorMsg?.minAmount ? (
{errorMsg?.data?.minimumAmountEth ? (
<Button
variant="accent"
fullWidth
onClick={() => {
props.setTokenAmount(String(errorMsg.minAmount));
props.setTokenAmount(
formatNumber(
Number(errorMsg.data?.minimumAmountEth),
6,
).toString(),
);
props.setHasEditedAmount(true);
}}
>
Expand Down Expand Up @@ -1526,3 +1478,26 @@ function ChainSelectionScreen(props: {
/>
);
}

type ApiError = {
code: string;
message?: string;
data?: {
minimumAmountUSDCents?: string;
requestedAmountUSDCents?: string;
minimumAmountWei?: string;
minimumAmountEth?: string;
};
};

const defaultMessage = "Unable to get price quote";
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
function getErrorMessage(err: any): ApiError {
if (typeof err.error === "object") {
return err.error;
}
return {
code: "UNABLE_TO_GET_PRICE_QUOTE",
message: defaultMessage,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,9 @@ function OnrampStatusScreenUI(props: {
</>
)}

{!props.isEmbed && (
<Button variant="accent" fullWidth onClick={props.onDone}>
{props.transactionMode ? "Continue Transaction" : "Done"}
</Button>
)}
<Button variant="accent" fullWidth onClick={props.onDone}>
{props.transactionMode ? "Continue Transaction" : "Done"}
</Button>
</>
)}
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,19 +236,7 @@ export function SwapConfirmationScreen(props: {
if (step === "swap") {
setStatus("pending");
try {
let tx = props.quote.transactionRequest;

// Fix for inApp wallet
// Ideally - the pay server sends a non-legacy transaction to avoid this issue
if (
props.payer.wallet.id === "inApp" ||
props.payer.wallet.id === "embedded"
) {
tx = {
...props.quote.transactionRequest,
gasPrice: undefined,
};
}
const tx = props.quote.transactionRequest;

trackPayEvent({
event: "prompt_swap_execution",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,9 @@ export function SwapStatusScreen(props: {
<Spacer y="xl" />
{swapDetails}
<Spacer y="sm" />
{!props.isEmbed && (
<Button variant="accent" fullWidth onClick={props.onDone}>
{props.transactionMode ? "Continue Transaction" : "Done"}
</Button>
)}
<Button variant="accent" fullWidth onClick={props.onDone}>
{props.transactionMode ? "Continue Transaction" : "Done"}
</Button>
</>
)}

Expand Down
Loading