Skip to content

Commit f77165e

Browse files
[SDK] fix: Skip swap approvals and use local gas prices (#6182)
1 parent dfd64d2 commit f77165e

File tree

8 files changed

+111
-126
lines changed

8 files changed

+111
-126
lines changed

.changeset/cuddly-wombats-boil.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Skip swap approvals if already approved and always calculate gas prices locally

apps/playground-web/src/components/pay/embed.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,29 @@ import { THIRDWEB_CLIENT } from "@/lib/client";
44
import { useTheme } from "next-themes";
55
import { base } from "thirdweb/chains";
66
import { PayEmbed } from "thirdweb/react";
7+
import { StyledConnectButton } from "../styled-connect-button";
78

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

1112
return (
12-
<PayEmbed
13-
client={THIRDWEB_CLIENT}
14-
theme={theme === "light" ? "light" : "dark"}
15-
payOptions={{
16-
mode: "fund_wallet",
17-
metadata: {
18-
name: "Get funds",
19-
},
20-
prefillBuy: {
21-
chain: base,
22-
amount: "0.01",
23-
},
24-
}}
25-
/>
13+
<>
14+
<StyledConnectButton />
15+
<div className="h-10" />
16+
<PayEmbed
17+
client={THIRDWEB_CLIENT}
18+
theme={theme === "light" ? "light" : "dark"}
19+
payOptions={{
20+
mode: "fund_wallet",
21+
metadata: {
22+
name: "Get funds",
23+
},
24+
prefillBuy: {
25+
chain: base,
26+
amount: "0.01",
27+
},
28+
}}
29+
/>
30+
</>
2631
);
2732
}

packages/thirdweb/src/pay/buyWithCrypto/getQuote.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Hash } from "viem";
22
import { getCachedChain } from "../../chains/utils.js";
33
import type { ThirdwebClient } from "../../client/client.js";
44
import { getContract } from "../../contract/contract.js";
5+
import { allowance } from "../../extensions/erc20/__generated__/IERC20/read/allowance.js";
56
import { approve } from "../../extensions/erc20/write/approve.js";
67
import type { PrepareTransactionOptions } from "../../transaction/prepare-transaction.js";
78
import { getClientFetch } from "../../utils/fetch.js";
@@ -251,6 +252,31 @@ export async function getBuyWithCryptoQuote(
251252
const data: BuyWithCryptoQuoteRouteResponse = (await response.json())
252253
.result;
253254

255+
// check if the fromAddress already has approval for the given amount
256+
const approvalData = data.approval;
257+
let approval = undefined;
258+
if (approvalData) {
259+
const contract = getContract({
260+
client: params.client,
261+
address: approvalData.tokenAddress,
262+
chain: getCachedChain(approvalData.chainId),
263+
});
264+
265+
const approvedAmount = await allowance({
266+
contract,
267+
spender: approvalData.spenderAddress,
268+
owner: params.fromAddress,
269+
});
270+
271+
if (approvedAmount < BigInt(approvalData.amountWei)) {
272+
approval = approve({
273+
contract,
274+
spender: approvalData.spenderAddress,
275+
amountWei: BigInt(approvalData.amountWei),
276+
});
277+
}
278+
}
279+
254280
const swapRoute: BuyWithCryptoQuote = {
255281
transactionRequest: {
256282
chain: getCachedChain(data.transactionRequest.chainId),
@@ -259,19 +285,9 @@ export async function getBuyWithCryptoQuote(
259285
to: data.transactionRequest.to,
260286
value: BigInt(data.transactionRequest.value),
261287
gas: BigInt(data.transactionRequest.gasLimit),
262-
gasPrice: BigInt(data.transactionRequest.gasPrice),
288+
gasPrice: undefined, // ignore gas price returned by the quote, we handle it ourselves
263289
},
264-
approval: data.approval
265-
? approve({
266-
contract: getContract({
267-
client: params.client,
268-
address: data.approval.tokenAddress,
269-
chain: getCachedChain(data.approval.chainId),
270-
}),
271-
spender: data.approval?.spenderAddress,
272-
amountWei: BigInt(data.approval.amountWei),
273-
})
274-
: undefined,
290+
approval: approval,
275291
swapDetails: {
276292
fromAddress: data.fromAddress,
277293
toAddress: data.toAddress,

packages/thirdweb/src/pay/utils/commonTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ export type PayOnChainTransactionDetails = {
1717
explorerLink?: string;
1818
};
1919

20-
export type FiatProvider = "STRIPE" | "TRANSAK" | "KADO";
20+
export type FiatProvider = "STRIPE" | "TRANSAK" | "KADO" | "COINBASE";

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx

Lines changed: 51 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,34 +1006,6 @@ function SwapScreenContent(props: {
10061006
const switchChainRequired =
10071007
props.payer.wallet.getChain()?.id !== fromChain.id;
10081008

1009-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
1010-
function getErrorMessage(err: any) {
1011-
type AmountTooLowError = {
1012-
code: "MINIMUM_PURCHASE_AMOUNT";
1013-
data: {
1014-
minimumAmountUSDCents: number;
1015-
requestedAmountUSDCents: number;
1016-
minimumAmountWei: string;
1017-
minimumAmountEth: string;
1018-
};
1019-
};
1020-
1021-
const defaultMessage = "Unable to get price quote";
1022-
try {
1023-
if (err.error.code === "MINIMUM_PURCHASE_AMOUNT") {
1024-
const obj = err.error as AmountTooLowError;
1025-
const minAmountToken = obj.data.minimumAmountEth;
1026-
return {
1027-
minAmount: formatNumber(Number(minAmountToken), 6),
1028-
};
1029-
}
1030-
} catch {}
1031-
1032-
return {
1033-
msg: [defaultMessage],
1034-
};
1035-
}
1036-
10371009
const errorMsg =
10381010
!quoteQuery.isLoading && quoteQuery.error
10391011
? getErrorMessage(quoteQuery.error)
@@ -1133,9 +1105,10 @@ function SwapScreenContent(props: {
11331105
{/* Error message */}
11341106
{errorMsg && (
11351107
<div>
1136-
{errorMsg.minAmount && (
1108+
{errorMsg.data?.minimumAmountEth ? (
11371109
<Text color="danger" size="sm" center multiline>
1138-
Minimum amount is {errorMsg.minAmount}{" "}
1110+
Minimum amount is{" "}
1111+
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
11391112
<TokenSymbol
11401113
token={toToken}
11411114
chain={toChain}
@@ -1144,13 +1117,11 @@ function SwapScreenContent(props: {
11441117
color="danger"
11451118
/>
11461119
</Text>
1147-
)}
1148-
1149-
{errorMsg.msg?.map((msg) => (
1150-
<Text color="danger" size="sm" center multiline key={msg}>
1151-
{msg}
1120+
) : (
1121+
<Text color="danger" size="sm" center multiline>
1122+
{errorMsg.message || defaultMessage}
11521123
</Text>
1153-
))}
1124+
)}
11541125
</div>
11551126
)}
11561127

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

11681139
{/* Button */}
1169-
{errorMsg?.minAmount ? (
1140+
{errorMsg?.data?.minimumAmountEth ? (
11701141
<Button
11711142
variant="accent"
11721143
fullWidth
11731144
onClick={() => {
1174-
props.setTokenAmount(String(errorMsg.minAmount));
1145+
props.setTokenAmount(
1146+
formatNumber(
1147+
Number(errorMsg.data?.minimumAmountEth),
1148+
6,
1149+
).toString(),
1150+
);
11751151
props.setHasEditedAmount(true);
11761152
}}
11771153
>
@@ -1306,34 +1282,6 @@ function FiatScreenContent(props: {
13061282
setIsOpen(true);
13071283
}
13081284

1309-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
1310-
function getErrorMessage(err: any) {
1311-
type AmountTooLowError = {
1312-
code: "MINIMUM_PURCHASE_AMOUNT";
1313-
data: {
1314-
minimumAmountUSDCents: number;
1315-
requestedAmountUSDCents: number;
1316-
minimumAmountWei: string;
1317-
minimumAmountEth: string;
1318-
};
1319-
};
1320-
1321-
const defaultMessage = "Unable to get price quote";
1322-
try {
1323-
if (err.error.code === "MINIMUM_PURCHASE_AMOUNT") {
1324-
const obj = err.error as AmountTooLowError;
1325-
const minAmountToken = obj.data.minimumAmountEth;
1326-
return {
1327-
minAmount: formatNumber(Number(minAmountToken), 6),
1328-
};
1329-
}
1330-
} catch {}
1331-
1332-
return {
1333-
msg: [defaultMessage],
1334-
};
1335-
}
1336-
13371285
const disableSubmit = !fiatQuoteQuery.data;
13381286

13391287
const errorMsg =
@@ -1381,9 +1329,10 @@ function FiatScreenContent(props: {
13811329
{/* Error message */}
13821330
{errorMsg && (
13831331
<div>
1384-
{errorMsg.minAmount && (
1332+
{errorMsg.data?.minimumAmountEth ? (
13851333
<Text color="danger" size="sm" center multiline>
1386-
Minimum amount is {errorMsg.minAmount}{" "}
1334+
Minimum amount is{" "}
1335+
{formatNumber(Number(errorMsg.data.minimumAmountEth), 6)}{" "}
13871336
<TokenSymbol
13881337
token={toToken}
13891338
chain={toChain}
@@ -1392,22 +1341,25 @@ function FiatScreenContent(props: {
13921341
color="danger"
13931342
/>
13941343
</Text>
1395-
)}
1396-
1397-
{errorMsg.msg?.map((msg) => (
1398-
<Text color="danger" size="sm" center multiline key={msg}>
1399-
{msg}
1344+
) : (
1345+
<Text color="danger" size="sm" center multiline>
1346+
{errorMsg.message || defaultMessage}
14001347
</Text>
1401-
))}
1348+
)}
14021349
</div>
14031350
)}
14041351

1405-
{errorMsg?.minAmount ? (
1352+
{errorMsg?.data?.minimumAmountEth ? (
14061353
<Button
14071354
variant="accent"
14081355
fullWidth
14091356
onClick={() => {
1410-
props.setTokenAmount(String(errorMsg.minAmount));
1357+
props.setTokenAmount(
1358+
formatNumber(
1359+
Number(errorMsg.data?.minimumAmountEth),
1360+
6,
1361+
).toString(),
1362+
);
14111363
props.setHasEditedAmount(true);
14121364
}}
14131365
>
@@ -1526,3 +1478,26 @@ function ChainSelectionScreen(props: {
15261478
/>
15271479
);
15281480
}
1481+
1482+
type ApiError = {
1483+
code: string;
1484+
message?: string;
1485+
data?: {
1486+
minimumAmountUSDCents?: string;
1487+
requestedAmountUSDCents?: string;
1488+
minimumAmountWei?: string;
1489+
minimumAmountEth?: string;
1490+
};
1491+
};
1492+
1493+
const defaultMessage = "Unable to get price quote";
1494+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
1495+
function getErrorMessage(err: any): ApiError {
1496+
if (typeof err.error === "object") {
1497+
return err.error;
1498+
}
1499+
return {
1500+
code: "UNABLE_TO_GET_PRICE_QUOTE",
1501+
message: defaultMessage,
1502+
};
1503+
}

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatStatusScreen.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,9 @@ function OnrampStatusScreenUI(props: {
248248
</>
249249
)}
250250

251-
{!props.isEmbed && (
252-
<Button variant="accent" fullWidth onClick={props.onDone}>
253-
{props.transactionMode ? "Continue Transaction" : "Done"}
254-
</Button>
255-
)}
251+
<Button variant="accent" fullWidth onClick={props.onDone}>
252+
{props.transactionMode ? "Continue Transaction" : "Done"}
253+
</Button>
256254
</>
257255
)}
258256
</Container>

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/ConfirmationScreen.tsx

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,19 +236,7 @@ export function SwapConfirmationScreen(props: {
236236
if (step === "swap") {
237237
setStatus("pending");
238238
try {
239-
let tx = props.quote.transactionRequest;
240-
241-
// Fix for inApp wallet
242-
// Ideally - the pay server sends a non-legacy transaction to avoid this issue
243-
if (
244-
props.payer.wallet.id === "inApp" ||
245-
props.payer.wallet.id === "embedded"
246-
) {
247-
tx = {
248-
...props.quote.transactionRequest,
249-
gasPrice: undefined,
250-
};
251-
}
239+
const tx = props.quote.transactionRequest;
252240

253241
trackPayEvent({
254242
event: "prompt_swap_execution",

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/SwapStatusScreen.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,9 @@ export function SwapStatusScreen(props: {
113113
<Spacer y="xl" />
114114
{swapDetails}
115115
<Spacer y="sm" />
116-
{!props.isEmbed && (
117-
<Button variant="accent" fullWidth onClick={props.onDone}>
118-
{props.transactionMode ? "Continue Transaction" : "Done"}
119-
</Button>
120-
)}
116+
<Button variant="accent" fullWidth onClick={props.onDone}>
117+
{props.transactionMode ? "Continue Transaction" : "Done"}
118+
</Button>
121119
</>
122120
)}
123121

0 commit comments

Comments
 (0)