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/thirty-banks-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Support erc5792 batch transactions for swaps, add slippage option to Bridge API
4 changes: 4 additions & 0 deletions packages/thirdweb/src/bridge/Buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ export async function prepare(
purchaseData,
maxSteps,
paymentLinkId,
slippageToleranceBps,
} = options;

const clientFetch = getClientFetch(client);
Expand All @@ -384,6 +385,7 @@ export async function prepare(
purchaseData,
receiver,
sender,
slippageToleranceBps,
}),
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -460,6 +462,8 @@ export declare namespace prepare {
purchaseData?: PurchaseData;
/** Maximum number of steps in the route */
maxSteps?: number;
/** The maximum slippage in basis points (bps) allowed for the transaction. */
slippageToleranceBps?: number;
/**
* @hidden
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/thirdweb/src/bridge/Sell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ export async function prepare(
purchaseData,
maxSteps,
paymentLinkId,
slippageToleranceBps,
} = options;

const clientFetch = getClientFetch(client);
Expand All @@ -370,6 +371,7 @@ export async function prepare(
receiver,
sellAmountWei: amount.toString(),
sender,
slippageToleranceBps,
}),
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -448,6 +450,8 @@ export declare namespace prepare {
purchaseData?: PurchaseData;
/** Maximum number of steps in the route */
maxSteps?: number;
/** The maximum slippage in basis points (bps) allowed for the transaction. */
slippageToleranceBps?: number;
/**
* @hidden
*/
Expand Down
137 changes: 130 additions & 7 deletions packages/thirdweb/src/react/core/hooks/useStepExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { getCachedChain } from "../../../chains/utils.js";
import type { ThirdwebClient } from "../../../client/client.js";
import { waitForReceipt } from "../../../transaction/actions/wait-for-tx-receipt.js";
import { waitForCallsReceipt } from "../../../wallets/eip5792/wait-for-calls-receipt.js";
import type { Account, Wallet } from "../../../wallets/interfaces/wallet.js";
import type { WindowAdapter } from "../adapters/WindowAdapter.js";
import type { BridgePrepareResult } from "./useBridgePrepare.js";
Expand Down Expand Up @@ -200,6 +201,7 @@
data: tx.data,
to: tx.to,
value: tx.value,
extraGas: 50000n, // add gas buffer

Check warning on line 204 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L204 was not covered by tests
});

// Send the transaction
Expand Down Expand Up @@ -276,6 +278,7 @@
data: tx.data,
to: tx.to,
value: tx.value,
extraGas: 50000n, // add gas buffer

Check warning on line 281 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L281 was not covered by tests
});
return preparedTx;
}),
Expand Down Expand Up @@ -326,6 +329,95 @@
[poller, preparedQuote?.type],
);

// Execute batch transactions
const executeSendCalls = useCallback(
async (
txs: FlattenedTx[],
wallet: Wallet,
account: Account,
completedStatusResults: CompletedStatusResult[],
abortSignal: AbortSignal,
) => {
if (typeof preparedQuote?.type === "undefined") {
throw new Error("No quote generated. This is unexpected.");
}
if (!account.sendCalls) {
throw new Error("Account does not support eip5792 send calls");
}

Check warning on line 346 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L333-L346

Added lines #L333 - L346 were not covered by tests

const { prepareTransaction } = await import(
"../../../transaction/prepare-transaction.js"

Check warning on line 349 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L348-L349

Added lines #L348 - L349 were not covered by tests
);
const { sendCalls } = await import(
"../../../wallets/eip5792/send-calls.js"

Check warning on line 352 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L351-L352

Added lines #L351 - L352 were not covered by tests
);

if (txs.length === 0) {
throw new Error("No transactions to batch");
}
const firstTx = txs[0];
if (!firstTx) {
throw new Error("Invalid batch transaction");
}

Check warning on line 361 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L355-L361

Added lines #L355 - L361 were not covered by tests

// Prepare and convert all transactions
const serializableTxs = await Promise.all(
txs.map(async (tx) => {
const preparedTx = prepareTransaction({
chain: tx.chain,
client: tx.client,
data: tx.data,
to: tx.to,
value: tx.value,
extraGas: 50000n, // add gas buffer
});
return preparedTx;
}),
);

Check warning on line 376 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L364-L376

Added lines #L364 - L376 were not covered by tests

// Send batch
const result = await sendCalls({
wallet,
calls: serializableTxs,
});

Check warning on line 382 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L379-L382

Added lines #L379 - L382 were not covered by tests

// get tx hash
const callsStatus = await waitForCallsReceipt(result);
const lastReceipt =
callsStatus.receipts?.[callsStatus.receipts.length - 1];

Check warning on line 387 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L385-L387

Added lines #L385 - L387 were not covered by tests

if (!lastReceipt) {
throw new Error("No receipts found");
}

Check warning on line 391 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L389-L391

Added lines #L389 - L391 were not covered by tests
Comment on lines +384 to +391
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find waitForCallsReceipt implementation
fd -e ts -e tsx | xargs grep -l "waitForCallsReceipt" | head -20

Repository: thirdweb-dev/js

Length of output: 498


🏁 Script executed:

#!/bin/bash
# Search for waitForCallsReceipt definition
rg -n "export.*waitForCallsReceipt|function waitForCallsReceipt" --type=ts

Repository: thirdweb-dev/js

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Look at useStepExecutor.ts to see context around lines 384-391
cat -n packages/thirdweb/src/react/core/hooks/useStepExecutor.ts | sed -n '370,410p'

Repository: thirdweb-dev/js

Length of output: 1514


🏁 Script executed:

#!/bin/bash
# Search for other receipt handling patterns in the codebase
rg -n "receipts\[" --type=ts -C3 | head -50

Repository: thirdweb-dev/js

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check if there's a receipt status field used anywhere
rg -n "receipt.*status|status.*receipt" --type=ts -C2 | head -40

Repository: thirdweb-dev/js

Length of output: 4254


🏁 Script executed:

#!/bin/bash
# Read the waitForCallsReceipt implementation
cat -n packages/thirdweb/src/wallets/eip5792/wait-for-calls-receipt.ts | head -100

Repository: thirdweb-dev/js

Length of output: 3892


🏁 Script executed:

#!/bin/bash
# Look at executeBatch function in useStepExecutor.ts for comparison
cat -n packages/thirdweb/src/react/core/hooks/useStepExecutor.ts | sed -n '250,330p'

Repository: thirdweb-dev/js

Length of output: 3209


🏁 Script executed:

#!/bin/bash
# Check if there's validation inside waitForCallsReceipt
rg -n "status.*reverted|reverted.*status" packages/thirdweb/src/wallets/eip5792/wait-for-calls-receipt.ts -C5

Repository: thirdweb-dev/js

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check GetCallsStatusResponse type definition
rg -n "type GetCallsStatusResponse|interface GetCallsStatusResponse" packages/thirdweb/src/wallets/eip5792/ -A 20

Repository: thirdweb-dev/js

Length of output: 1633


🏁 Script executed:

#!/bin/bash
# Check what status values receipts can have
rg -n "receiptStatuses|receipt.*status" packages/thirdweb/src/wallets/eip5792/get-calls-status.ts -B3 -A3

Repository: thirdweb-dev/js

Length of output: 430


🏁 Script executed:

#!/bin/bash
# Check if individual receipt status is ever validated anywhere
rg -n "receipt\.status.*==|receipt\.status.*===|reverted" packages/thirdweb/src/wallets/eip5792/ --type=ts

Repository: thirdweb-dev/js

Length of output: 266


🏁 Script executed:

#!/bin/bash
# Check if bundle.status === "success" means all receipts are success
cat -n packages/thirdweb/src/wallets/eip5792/get-calls-status.ts | sed -n '1,110p'

Repository: thirdweb-dev/js

Length of output: 4365


🏁 Script executed:

#!/bin/bash
# Check EIP-5792 spec or comments about bundle vs receipt status
rg -n "atomic|bundle|receipt.*reverted" packages/thirdweb/src/wallets/eip5792/ -B2 -A2 | head -60

Repository: thirdweb-dev/js

Length of output: 145


Check individual receipt statuses for non-atomic bundles.

The code only uses lastReceipt.transactionHash for polling without verifying receipt success. ERC-5792 supports both atomic and non-atomic call bundles. For atomic bundles, waitForCallsReceipt returning bundle.status === "success" guarantees all receipts succeeded. However, for non-atomic bundles, individual calls can fail independently—their receipt statuses should be validated.

Consider adding validation for non-atomic cases:

 // get tx hash
 const callsStatus = await waitForCallsReceipt(result);
 const lastReceipt =
   callsStatus.receipts?.[callsStatus.receipts.length - 1];

 if (!lastReceipt) {
   throw new Error("No receipts found");
 }

+// For non-atomic bundles, verify individual receipt success
+if (!callsStatus.atomic) {
+  for (const receipt of callsStatus.receipts) {
+    if (receipt.status === "reverted") {
+      throw new Error("Transaction in batch failed");
+    }
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// get tx hash
const callsStatus = await waitForCallsReceipt(result);
const lastReceipt =
callsStatus.receipts?.[callsStatus.receipts.length - 1];
if (!lastReceipt) {
throw new Error("No receipts found");
}
// get tx hash
const callsStatus = await waitForCallsReceipt(result);
const lastReceipt =
callsStatus.receipts?.[callsStatus.receipts.length - 1];
if (!lastReceipt) {
throw new Error("No receipts found");
}
// For non-atomic bundles, verify individual receipt success
if (!callsStatus.atomic) {
for (const receipt of callsStatus.receipts) {
if (receipt.status === "reverted") {
throw new Error("Transaction in batch failed");
}
}
}
🤖 Prompt for AI Agents
In packages/thirdweb/src/react/core/hooks/useStepExecutor.ts around lines 384 to
391, the code only grabs lastReceipt.transactionHash without validating
individual receipt success for non-atomic bundles; update the logic to detect
non-atomic bundles (e.g., bundle.status !== "success" or appropriate flag from
waitForCallsReceipt result) and iterate over callsStatus.receipts to check each
receipt's status (or reverted/failed fields), and if any receipt indicates
failure, throw or return a descriptive error containing the failing call index
and its transactionHash/revert reason; otherwise continue using the appropriate
successful receipt.transactionHash for polling.


const { status } = await import("../../../bridge/Status.js");
await poller(async () => {
const statusResult = await status({
chainId: firstTx.chainId,
client: firstTx.client,
transactionHash: lastReceipt.transactionHash,
});

Check warning on line 399 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L393-L399

Added lines #L393 - L399 were not covered by tests

if (statusResult.status === "COMPLETED") {

Check warning on line 401 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L401 was not covered by tests
// Add type field from preparedQuote for discriminated union
const typedStatusResult = {
type: preparedQuote.type,
...statusResult,
};
completedStatusResults.push(typedStatusResult);
return { completed: true };
}

Check warning on line 409 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L403-L409

Added lines #L403 - L409 were not covered by tests

if (statusResult.status === "FAILED") {
throw new Error("Payment failed");
}

Check warning on line 413 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L411-L413

Added lines #L411 - L413 were not covered by tests

return { completed: false };
}, abortSignal);
},
[poller, preparedQuote?.type],
);

Check warning on line 419 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L415-L419

Added lines #L415 - L419 were not covered by tests

// Execute onramp step
const executeOnramp = useCallback(
async (
Expand Down Expand Up @@ -448,11 +540,15 @@
}

// Check if we can batch transactions
const canSendCalls =
(await supportsAtomic(account, currentTx.chainId)) &&
i < flatTxs.length - 1; // Not the last transaction;

Check warning on line 545 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L543-L545

Added lines #L543 - L545 were not covered by tests

const canBatch =
account.sendBatchTransaction !== undefined &&
i < flatTxs.length - 1; // Not the last transaction

if (canBatch) {
if (canBatch || canSendCalls) {

Check warning on line 551 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L551 was not covered by tests
// Find consecutive transactions on the same chain
const batchTxs: FlattenedTx[] = [currentTx];
let j = i + 1;
Expand All @@ -467,12 +563,26 @@

// Execute batch if we have multiple transactions
if (batchTxs.length > 1) {
await executeBatch(
batchTxs,
account,
completedStatusResults,
abortController.signal,
);
// prefer batching if supported
if (canBatch) {
await executeBatch(
batchTxs,
account,
completedStatusResults,
abortController.signal,
);
} else if (canSendCalls) {
await executeSendCalls(
batchTxs,
wallet,
account,
completedStatusResults,
abortController.signal,
);
} else {

Check warning on line 582 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L567-L582

Added lines #L567 - L582 were not covered by tests
// should never happen
throw new Error("No supported execution mode found");
}

Check warning on line 585 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L584-L585

Added lines #L584 - L585 were not covered by tests

// Mark all batched transactions as completed
for (const tx of batchTxs) {
Expand Down Expand Up @@ -530,6 +640,7 @@
flatTxs,
executeSingleTx,
executeBatch,
executeSendCalls,

Check warning on line 643 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L643 was not covered by tests
onrampStatus,
executeOnramp,
onComplete,
Expand Down Expand Up @@ -602,3 +713,15 @@
steps: preparedQuote?.steps,
};
}

async function supportsAtomic(account: Account, chainId: number) {
const capabilitiesFn = account.getCapabilities;
if (!capabilitiesFn) {
return false;
}
const capabilities = await capabilitiesFn({ chainId });
const atomic = capabilities[chainId]?.atomic as

Check warning on line 723 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L717-L723

Added lines #L717 - L723 were not covered by tests
| { status: "supported" | "ready" | "unsupported" }
| undefined;
return atomic?.status === "supported" || atomic?.status === "ready";
}

Check warning on line 727 in packages/thirdweb/src/react/core/hooks/useStepExecutor.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/useStepExecutor.ts#L726-L727

Added lines #L726 - L727 were not covered by tests
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { CheckIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { trackPayEvent } from "../../../../../analytics/track/pay.js";
import type { ThirdwebClient } from "../../../../../client/client.js";
Expand Down Expand Up @@ -61,6 +61,7 @@
}: SuccessScreenProps) {
const theme = useCustomTheme();
const [viewState, setViewState] = useState<ViewState>("success");
const queryClient = useQueryClient();

Check warning on line 64 in packages/thirdweb/src/react/web/ui/Bridge/payment-success/SuccessScreen.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L64 was not covered by tests

useQuery({
queryFn: () => {
Expand All @@ -74,6 +75,26 @@
toToken: preparedQuote.intent.destinationTokenAddress,
});
}
if (preparedQuote.type === "transfer") {
trackPayEvent({
chainId: preparedQuote.intent.chainId,
client: client,
event: "ub:ui:success_screen",
fromToken: preparedQuote.intent.tokenAddress,
toChainId: preparedQuote.intent.chainId,
toToken: preparedQuote.intent.tokenAddress,
});
}
queryClient.invalidateQueries({
queryKey: ["bridge/v1/wallets"],
});
queryClient.invalidateQueries({
queryKey: ["walletBalance"],
});
queryClient.invalidateQueries({
queryKey: ["payment-methods"],
});
return true;

Check warning on line 97 in packages/thirdweb/src/react/web/ui/Bridge/payment-success/SuccessScreen.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L78 - L97 were not covered by tests
},
queryKey: ["success_screen", preparedQuote.type],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@
const json = (await response.json()) as TokenBalancesResponse;
return json.result;
},
refetchOnMount: false,
retry: false,
refetchOnWindowFocus: false,
refetchOnMount: "always",

Check warning on line 119 in packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L119 was not covered by tests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refetch behavior change may have unintended side effects. By removing the retry: false and refetchOnWindowFocus: false options, token balance queries will now retry on failure and refetch on window focus, which could increase API calls beyond what was intended.

View Details
📝 Patch Details
diff --git a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts
index 19521d200..de9c92b31 100644
--- a/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts
+++ b/packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts
@@ -117,5 +117,7 @@ export function useTokenBalances(options: {
       return json.result;
     },
     refetchOnMount: "always",
+    retry: false,
+    refetchOnWindowFocus: false,
   });
 }

Analysis

Unintended query behavior change in useTokenBalances() after refactor

What fails: useTokenBalances() in packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts now retries failed requests 3 times and refetches on window focus, contradicting the explicit defensive configuration set in previous commits.

How to reproduce:

  1. Examine git history: git log --oneline packages/thirdweb/src/react/web/ui/Bridge/swap-widget/use-tokens.ts
  2. Compare commit b8afa98 (which explicitly added retry: false) vs commit 3d2c961 (which removed it)
  3. Verify React Query defaults using TanStack Query documentation:
    • retry defaults to 3 (not false)
    • refetchOnWindowFocus defaults to true (not false)

Result: Removed three explicit query options without updating the feature:

  • Commit b8afa98 (Sep 29) explicitly set retry: false to "reduce loading times and improve UI responsiveness" by disabling retries on failed token balance queries
  • Commit 3d2c961 (Dec 3) removed retry: false, refetchOnWindowFocus: false, and changed refetchOnMount: false to refetchOnMount: "always"
  • Now useTokenBalances() will automatically:
    • Retry 3 times on API failure (vs previous: never retry)
    • Refetch when window regains focus (vs previous: never refetch on focus)

Expected: Query options should maintain the explicit defensive configuration (retry: false, refetchOnWindowFocus: false) while still supporting the new refetchOnMount: "always" behavior. This preserves the intentional choice to minimize unnecessary API calls for token balance queries while ensuring fresh data on component mount.

});
}
Loading