Skip to content

Commit

Permalink
[SDK] fix: Skip swap approvals and use local gas prices (#6182)
Browse files Browse the repository at this point in the history
  • Loading branch information
joaquim-verges authored Feb 6, 2025
1 parent dfd64d2 commit f77165e
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 126 deletions.
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

0 comments on commit f77165e

Please sign in to comment.