Skip to content

Commit 9e43da4

Browse files
[SDK] Support ERC5792 batch transactions for swaps, add slippage option to Bridge API (#8484)
1 parent 22cea5b commit 9e43da4

File tree

6 files changed

+166
-11
lines changed

6 files changed

+166
-11
lines changed

.changeset/thirty-banks-itch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Support erc5792 batch transactions for swaps, add slippage option to Bridge API

packages/thirdweb/src/bridge/Buy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ export async function prepare(
366366
purchaseData,
367367
maxSteps,
368368
paymentLinkId,
369+
slippageToleranceBps,
369370
} = options;
370371

371372
const clientFetch = getClientFetch(client);
@@ -384,6 +385,7 @@ export async function prepare(
384385
purchaseData,
385386
receiver,
386387
sender,
388+
slippageToleranceBps,
387389
}),
388390
headers: {
389391
"Content-Type": "application/json",
@@ -460,6 +462,8 @@ export declare namespace prepare {
460462
purchaseData?: PurchaseData;
461463
/** Maximum number of steps in the route */
462464
maxSteps?: number;
465+
/** The maximum slippage in basis points (bps) allowed for the transaction. */
466+
slippageToleranceBps?: number;
463467
/**
464468
* @hidden
465469
*/

packages/thirdweb/src/bridge/Sell.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export async function prepare(
352352
purchaseData,
353353
maxSteps,
354354
paymentLinkId,
355+
slippageToleranceBps,
355356
} = options;
356357

357358
const clientFetch = getClientFetch(client);
@@ -370,6 +371,7 @@ export async function prepare(
370371
receiver,
371372
sellAmountWei: amount.toString(),
372373
sender,
374+
slippageToleranceBps,
373375
}),
374376
headers: {
375377
"Content-Type": "application/json",
@@ -448,6 +450,8 @@ export declare namespace prepare {
448450
purchaseData?: PurchaseData;
449451
/** Maximum number of steps in the route */
450452
maxSteps?: number;
453+
/** The maximum slippage in basis points (bps) allowed for the transaction. */
454+
slippageToleranceBps?: number;
451455
/**
452456
* @hidden
453457
*/

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Status } from "../../../bridge/types/Status.js";
99
import { getCachedChain } from "../../../chains/utils.js";
1010
import type { ThirdwebClient } from "../../../client/client.js";
1111
import { waitForReceipt } from "../../../transaction/actions/wait-for-tx-receipt.js";
12+
import { waitForCallsReceipt } from "../../../wallets/eip5792/wait-for-calls-receipt.js";
1213
import type { Account, Wallet } from "../../../wallets/interfaces/wallet.js";
1314
import type { WindowAdapter } from "../adapters/WindowAdapter.js";
1415
import type { BridgePrepareResult } from "./useBridgePrepare.js";
@@ -200,6 +201,7 @@ export function useStepExecutor(
200201
data: tx.data,
201202
to: tx.to,
202203
value: tx.value,
204+
extraGas: 50000n, // add gas buffer
203205
});
204206

205207
// Send the transaction
@@ -276,6 +278,7 @@ export function useStepExecutor(
276278
data: tx.data,
277279
to: tx.to,
278280
value: tx.value,
281+
extraGas: 50000n, // add gas buffer
279282
});
280283
return preparedTx;
281284
}),
@@ -326,6 +329,95 @@ export function useStepExecutor(
326329
[poller, preparedQuote?.type],
327330
);
328331

332+
// Execute batch transactions
333+
const executeSendCalls = useCallback(
334+
async (
335+
txs: FlattenedTx[],
336+
wallet: Wallet,
337+
account: Account,
338+
completedStatusResults: CompletedStatusResult[],
339+
abortSignal: AbortSignal,
340+
) => {
341+
if (typeof preparedQuote?.type === "undefined") {
342+
throw new Error("No quote generated. This is unexpected.");
343+
}
344+
if (!account.sendCalls) {
345+
throw new Error("Account does not support eip5792 send calls");
346+
}
347+
348+
const { prepareTransaction } = await import(
349+
"../../../transaction/prepare-transaction.js"
350+
);
351+
const { sendCalls } = await import(
352+
"../../../wallets/eip5792/send-calls.js"
353+
);
354+
355+
if (txs.length === 0) {
356+
throw new Error("No transactions to batch");
357+
}
358+
const firstTx = txs[0];
359+
if (!firstTx) {
360+
throw new Error("Invalid batch transaction");
361+
}
362+
363+
// Prepare and convert all transactions
364+
const serializableTxs = await Promise.all(
365+
txs.map(async (tx) => {
366+
const preparedTx = prepareTransaction({
367+
chain: tx.chain,
368+
client: tx.client,
369+
data: tx.data,
370+
to: tx.to,
371+
value: tx.value,
372+
extraGas: 50000n, // add gas buffer
373+
});
374+
return preparedTx;
375+
}),
376+
);
377+
378+
// Send batch
379+
const result = await sendCalls({
380+
wallet,
381+
calls: serializableTxs,
382+
});
383+
384+
// get tx hash
385+
const callsStatus = await waitForCallsReceipt(result);
386+
const lastReceipt =
387+
callsStatus.receipts?.[callsStatus.receipts.length - 1];
388+
389+
if (!lastReceipt) {
390+
throw new Error("No receipts found");
391+
}
392+
393+
const { status } = await import("../../../bridge/Status.js");
394+
await poller(async () => {
395+
const statusResult = await status({
396+
chainId: firstTx.chainId,
397+
client: firstTx.client,
398+
transactionHash: lastReceipt.transactionHash,
399+
});
400+
401+
if (statusResult.status === "COMPLETED") {
402+
// Add type field from preparedQuote for discriminated union
403+
const typedStatusResult = {
404+
type: preparedQuote.type,
405+
...statusResult,
406+
};
407+
completedStatusResults.push(typedStatusResult);
408+
return { completed: true };
409+
}
410+
411+
if (statusResult.status === "FAILED") {
412+
throw new Error("Payment failed");
413+
}
414+
415+
return { completed: false };
416+
}, abortSignal);
417+
},
418+
[poller, preparedQuote?.type],
419+
);
420+
329421
// Execute onramp step
330422
const executeOnramp = useCallback(
331423
async (
@@ -448,11 +540,15 @@ export function useStepExecutor(
448540
}
449541

450542
// Check if we can batch transactions
543+
const canSendCalls =
544+
(await supportsAtomic(account, currentTx.chainId)) &&
545+
i < flatTxs.length - 1; // Not the last transaction;
546+
451547
const canBatch =
452548
account.sendBatchTransaction !== undefined &&
453549
i < flatTxs.length - 1; // Not the last transaction
454550

455-
if (canBatch) {
551+
if (canBatch || canSendCalls) {
456552
// Find consecutive transactions on the same chain
457553
const batchTxs: FlattenedTx[] = [currentTx];
458554
let j = i + 1;
@@ -467,12 +563,26 @@ export function useStepExecutor(
467563

468564
// Execute batch if we have multiple transactions
469565
if (batchTxs.length > 1) {
470-
await executeBatch(
471-
batchTxs,
472-
account,
473-
completedStatusResults,
474-
abortController.signal,
475-
);
566+
// prefer batching if supported
567+
if (canBatch) {
568+
await executeBatch(
569+
batchTxs,
570+
account,
571+
completedStatusResults,
572+
abortController.signal,
573+
);
574+
} else if (canSendCalls) {
575+
await executeSendCalls(
576+
batchTxs,
577+
wallet,
578+
account,
579+
completedStatusResults,
580+
abortController.signal,
581+
);
582+
} else {
583+
// should never happen
584+
throw new Error("No supported execution mode found");
585+
}
476586

477587
// Mark all batched transactions as completed
478588
for (const tx of batchTxs) {
@@ -530,6 +640,7 @@ export function useStepExecutor(
530640
flatTxs,
531641
executeSingleTx,
532642
executeBatch,
643+
executeSendCalls,
533644
onrampStatus,
534645
executeOnramp,
535646
onComplete,
@@ -602,3 +713,15 @@ export function useStepExecutor(
602713
steps: preparedQuote?.steps,
603714
};
604715
}
716+
717+
async function supportsAtomic(account: Account, chainId: number) {
718+
const capabilitiesFn = account.getCapabilities;
719+
if (!capabilitiesFn) {
720+
return false;
721+
}
722+
const capabilities = await capabilitiesFn({ chainId });
723+
const atomic = capabilities[chainId]?.atomic as
724+
| { status: "supported" | "ready" | "unsupported" }
725+
| undefined;
726+
return atomic?.status === "supported" || atomic?.status === "ready";
727+
}

packages/thirdweb/src/react/web/ui/Bridge/payment-success/SuccessScreen.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
import { CheckIcon } from "@radix-ui/react-icons";
3-
import { useQuery } from "@tanstack/react-query";
3+
import { useQuery, useQueryClient } from "@tanstack/react-query";
44
import { useState } from "react";
55
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
66
import type { ThirdwebClient } from "../../../../../client/client.js";
@@ -61,6 +61,7 @@ export function SuccessScreen({
6161
}: SuccessScreenProps) {
6262
const theme = useCustomTheme();
6363
const [viewState, setViewState] = useState<ViewState>("success");
64+
const queryClient = useQueryClient();
6465

6566
useQuery({
6667
queryFn: () => {
@@ -74,6 +75,26 @@ export function SuccessScreen({
7475
toToken: preparedQuote.intent.destinationTokenAddress,
7576
});
7677
}
78+
if (preparedQuote.type === "transfer") {
79+
trackPayEvent({
80+
chainId: preparedQuote.intent.chainId,
81+
client: client,
82+
event: "ub:ui:success_screen",
83+
fromToken: preparedQuote.intent.tokenAddress,
84+
toChainId: preparedQuote.intent.chainId,
85+
toToken: preparedQuote.intent.tokenAddress,
86+
});
87+
}
88+
queryClient.invalidateQueries({
89+
queryKey: ["bridge/v1/wallets"],
90+
});
91+
queryClient.invalidateQueries({
92+
queryKey: ["walletBalance"],
93+
});
94+
queryClient.invalidateQueries({
95+
queryKey: ["payment-methods"],
96+
});
97+
return true;
7798
},
7899
queryKey: ["success_screen", preparedQuote.type],
79100
});

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,6 @@ export function useTokenBalances(options: {
116116
const json = (await response.json()) as TokenBalancesResponse;
117117
return json.result;
118118
},
119-
refetchOnMount: false,
120-
retry: false,
121-
refetchOnWindowFocus: false,
119+
refetchOnMount: "always",
122120
});
123121
}

0 commit comments

Comments
 (0)