diff --git a/.changeset/cold-eels-crash.md b/.changeset/cold-eels-crash.md
new file mode 100644
index 00000000000..0258328006c
--- /dev/null
+++ b/.changeset/cold-eels-crash.md
@@ -0,0 +1,5 @@
+---
+"thirdweb": patch
+---
+
+Handle unsupported Pay chains properly for sending paid transactions
diff --git a/apps/playground-web/src/app/connect/pay/transactions/page.tsx b/apps/playground-web/src/app/connect/pay/transactions/page.tsx
index 09e618e7b20..8aeb7f7ca8b 100644
--- a/apps/playground-web/src/app/connect/pay/transactions/page.tsx
+++ b/apps/playground-web/src/app/connect/pay/transactions/page.tsx
@@ -131,7 +131,9 @@ function NoFundsPopup() {
to: account.address,
});
}}
- />
+ >
+ Buy VIP Pass
+
);
};`}
lang="tsx"
diff --git a/apps/playground-web/src/components/pay/transaction-button.tsx b/apps/playground-web/src/components/pay/transaction-button.tsx
index abe5aedf5dc..e9ee1775445 100644
--- a/apps/playground-web/src/components/pay/transaction-button.tsx
+++ b/apps/playground-web/src/components/pay/transaction-button.tsx
@@ -21,9 +21,11 @@ const nftContract = getContract({
client: THIRDWEB_CLIENT,
});
+const USDC = getDefaultToken(sepolia, "USDC");
+
const usdcContract = getContract({
// biome-ignore lint/style/noNonNullAssertion: its there
- address: getDefaultToken(sepolia, "USDC")!.address,
+ address: USDC!.address,
chain: sepolia,
client: THIRDWEB_CLIENT,
});
@@ -68,24 +70,34 @@ export function PayTransactionButtonPreview() {
<>
{account && (
- {
- if (!account) throw new Error("No active account");
- return transfer({
- contract: usdcContract,
- amount: "50",
- to: account?.address || "",
- });
- }}
- onError={(e) => {
- console.error(e);
- }}
- payModal={{
- theme: theme === "light" ? "light" : "dark",
- }}
- >
- Transfer funds
-
+
+
+ Price:{" "}
+ {USDC?.icon && (
+ // eslint-disable-next-line @next/next/no-img-element
+

+ )}
+ 50 {USDC?.symbol}
+
+
{
+ if (!account) throw new Error("No active account");
+ return transfer({
+ contract: usdcContract,
+ amount: "50",
+ to: account?.address || "",
+ });
+ }}
+ onError={(e) => {
+ console.error(e);
+ }}
+ payModal={{
+ theme: theme === "light" ? "light" : "dark",
+ }}
+ >
+ Buy VIP Pass
+
+
)}
>
);
diff --git a/packages/thirdweb/src/extensions/unstoppable-domains/read/resolveName.test.ts b/packages/thirdweb/src/extensions/unstoppable-domains/read/resolveName.test.ts
index bfdd26a90d2..6626af18133 100644
--- a/packages/thirdweb/src/extensions/unstoppable-domains/read/resolveName.test.ts
+++ b/packages/thirdweb/src/extensions/unstoppable-domains/read/resolveName.test.ts
@@ -5,21 +5,24 @@ import { resolveName } from "./resolveName.js";
// Double check: https://unstoppabledomains.com/d/thirdwebsdk.unstoppable
-describe("Unstoppable Domain: resolve name", () => {
- it("should resolve name", async () => {
- expect(
- await resolveName({
- address: "0x12345674b599ce99958242b3D3741e7b01841DF3",
- client: TEST_CLIENT,
- }),
- ).toBe("thirdwebsdk.unstoppable");
- });
+describe.runIf(process.env.TW_SECRET_KEY)(
+ "Unstoppable Domain: resolve name",
+ () => {
+ it("should resolve name", async () => {
+ expect(
+ await resolveName({
+ address: "0x12345674b599ce99958242b3D3741e7b01841DF3",
+ client: TEST_CLIENT,
+ }),
+ ).toBe("thirdwebsdk.unstoppable");
+ });
- it("should throw error on addresses that dont own any UD", async () => {
- await expect(() =>
- resolveName({ client: TEST_CLIENT, address: TEST_ACCOUNT_D.address }),
- ).rejects.toThrowError(
- `Failed to retrieve domain for address: ${TEST_ACCOUNT_D.address}. Make sure you have set the Reverse Resolution address for your domain at https://unstoppabledomains.com/manage?page=reverseResolution&domain=your-domain`,
- );
- });
-});
+ it("should throw error on addresses that dont own any UD", async () => {
+ await expect(() =>
+ resolveName({ client: TEST_CLIENT, address: TEST_ACCOUNT_D.address }),
+ ).rejects.toThrowError(
+ `Failed to retrieve domain for address: ${TEST_ACCOUNT_D.address}. Make sure you have set the Reverse Resolution address for your domain at https://unstoppabledomains.com/manage?page=reverseResolution&domain=your-domain`,
+ );
+ });
+ },
+);
diff --git a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
index d1d1603af05..b8b281938cd 100644
--- a/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
+++ b/packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts
@@ -13,6 +13,7 @@ import { resolvePromisedValue } from "../../../../utils/promise/resolve-promised
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
import { getTokenBalance } from "../../../../wallets/utils/getTokenBalance.js";
import { getWalletBalance } from "../../../../wallets/utils/getWalletBalance.js";
+import { fetchBuySupportedDestinations } from "../../../web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.js";
import type { LocaleId } from "../../../web/ui/types.js";
import type { Theme } from "../../design-system/index.js";
import type { SupportedTokens } from "../../utils/defaultTokens.js";
@@ -164,10 +165,39 @@ export function useSendTransactionCore(args: {
(async () => {
try {
- const [_nativeValue, _erc20Value] = await Promise.all([
- resolvePromisedValue(tx.value),
- resolvePromisedValue(tx.erc20Value),
- ]);
+ const [_nativeValue, _erc20Value, supportedDestinations] =
+ await Promise.all([
+ resolvePromisedValue(tx.value),
+ resolvePromisedValue(tx.erc20Value),
+ fetchBuySupportedDestinations(tx.client).catch(() => null),
+ ]);
+
+ if (!supportedDestinations) {
+ // could not fetch supported destinations, just send the tx
+ sendTx();
+ return;
+ }
+
+ if (
+ !supportedDestinations
+ .map((x) => x.chain.id)
+ .includes(tx.chain.id) ||
+ (_erc20Value &&
+ !supportedDestinations.some(
+ (x) =>
+ x.chain.id === tx.chain.id &&
+ x.tokens.find(
+ (t) =>
+ t.address.toLowerCase() ===
+ _erc20Value.tokenAddress.toLowerCase(),
+ ),
+ ))
+ ) {
+ // chain/token not supported, just send the tx
+ sendTx();
+ return;
+ }
+
const nativeValue = _nativeValue || 0n;
const erc20Value = _erc20Value?.amountWei || 0n;
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx
index df47e7d79d9..3d2aca0654e 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx
@@ -6,23 +6,32 @@ import { formatNumber } from "../../../../../../utils/formatNumber.js";
import { toTokens } from "../../../../../../utils/units.js";
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
import { useCustomTheme } from "../../../../../core/design-system/CustomThemeProvider.js";
-import { iconSize, spacing } from "../../../../../core/design-system/index.js";
+import { fontSize, spacing } from "../../../../../core/design-system/index.js";
import type { PayUIOptions } from "../../../../../core/hooks/connection/ConnectButtonProps.js";
import { useChainMetadata } from "../../../../../core/hooks/others/useChainQuery.js";
+import { useWalletBalance } from "../../../../../core/hooks/others/useWalletBalance.js";
+import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js";
import { useActiveWallet } from "../../../../../core/hooks/wallets/useActiveWallet.js";
import { hasSponsoredTransactionsEnabled } from "../../../../../core/utils/wallet.js";
import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js";
import type { PayEmbedConnectOptions } from "../../../PayEmbed.js";
import { ChainIcon } from "../../../components/ChainIcon.js";
import { Img } from "../../../components/Img.js";
+import { Skeleton } from "../../../components/Skeleton.js";
import { Spacer } from "../../../components/Spacer.js";
import { TokenIcon } from "../../../components/TokenIcon.js";
-import { WalletImage } from "../../../components/WalletImage.js";
import { Container, Line, ModalHeader } from "../../../components/basic.js";
import { Button } from "../../../components/buttons.js";
import { Text } from "../../../components/text.js";
+import { TokenSymbol } from "../../../components/token/TokenSymbol.js";
import { ConnectButton } from "../../ConnectButton.js";
-import type { ERC20OrNativeToken } from "../nativeToken.js";
+import { formatTokenBalance } from "../formatTokenBalance.js";
+import {
+ type ERC20OrNativeToken,
+ NATIVE_TOKEN,
+ isNativeToken,
+} from "../nativeToken.js";
+import { WalletRow } from "./WalletSelectorButton.js";
import { useTransactionCostAndData } from "./main/useBuyTxStates.js";
import type { SupportedChainAndTokens } from "./swap/useSwapSupportedChains.js";
@@ -54,8 +63,22 @@ export function TransactionModeScreen(props: {
});
const theme = useCustomTheme();
const activeWallet = useActiveWallet();
+ const activeAccount = useActiveAccount();
const sponsoredTransactionsEnabled =
hasSponsoredTransactionsEnabled(activeWallet);
+ const balanceQuery = useWalletBalance(
+ {
+ address: activeAccount?.address,
+ chain: payUiOptions.transaction.chain,
+ tokenAddress: isNativeToken(transactionCostAndData?.token || NATIVE_TOKEN)
+ ? undefined
+ : transactionCostAndData?.token.address,
+ client: props.client,
+ },
+ {
+ enabled: !!transactionCostAndData,
+ },
+ );
if (!transactionCostAndData || !chainData) {
return ;
@@ -74,39 +97,47 @@ export function TransactionModeScreen(props: {
style={{
width: "100%",
borderRadius: spacing.md,
+ border: `1px solid ${theme.colors.borderColor}`,
backgroundColor: theme.colors.tertiaryBg,
}}
/>
- ) : activeWallet ? (
-
-
-
+
+ Insufficient funds
+
+
-
+ >
+
+ {balanceQuery.data ? (
+
+
+ {formatTokenBalance(balanceQuery.data, false, 3)}
+
+
+
+ ) : (
+
+ )}
+
) : null}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.ts
index 5ddd3f045e6..2987222738d 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.ts
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.ts
@@ -34,7 +34,7 @@ export type SupportedChainAndTokens = Array<{
}>;
}>;
-async function fetchBuySupportedDestinations(
+export async function fetchBuySupportedDestinations(
client: ThirdwebClient,
isTestMode?: boolean,
): Promise {
diff --git a/packages/thirdweb/src/wallets/smart/smart-wallet-dev.test.ts b/packages/thirdweb/src/wallets/smart/smart-wallet-dev.test.ts
index b6e03672e2b..cd84d29ea2c 100644
--- a/packages/thirdweb/src/wallets/smart/smart-wallet-dev.test.ts
+++ b/packages/thirdweb/src/wallets/smart/smart-wallet-dev.test.ts
@@ -1,10 +1,12 @@
import { beforeAll, describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
-import { arbitrumSepolia } from "../../chains/chain-definitions/arbitrum-sepolia.js";
+import { zkSyncSepolia } from "../../chains/chain-definitions/zksync-sepolia.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { balanceOf } from "../../extensions/erc1155/__generated__/IERC1155/read/balanceOf.js";
import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js";
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
+import { sendTransaction } from "../../transaction/actions/send-transaction.js";
+import { prepareTransaction } from "../../transaction/prepare-transaction.js";
import type { Address } from "../../utils/address.js";
import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js";
import { setThirdwebDomains } from "../../utils/domains.js";
@@ -18,7 +20,7 @@ let smartWalletAddress: Address;
let personalAccount: Account;
let accountContract: ThirdwebContract;
-const chain = arbitrumSepolia;
+const chain = zkSyncSepolia;
const client = TEST_CLIENT;
const contract = getContract({
client,
@@ -61,13 +63,30 @@ describe.runIf(process.env.TW_SECRET_KEY).skip.sequential(
expect(smartWalletAddress).toHaveLength(42);
});
- it("can sign a msg", async () => {
+ it.skip("can sign a msg", async () => {
await smartAccount.signMessage({ message: "hello world" });
const isDeployed = await isContractDeployed(accountContract);
expect(isDeployed).toEqual(true);
});
- it("can execute a tx", async () => {
+ it("should send a transaction", async () => {
+ const tx = prepareTransaction({
+ client,
+ chain,
+ to: smartAccount.address,
+ value: 0n,
+ });
+
+ console.log("Sending transaction...");
+ const receipt = await sendTransaction({
+ transaction: tx,
+ account: smartAccount,
+ });
+ console.log("Transaction sent:", receipt.transactionHash);
+ expect(receipt.transactionHash).toBeDefined();
+ });
+
+ it.skip("can execute a tx", async () => {
const tx = await sendAndConfirmTransaction({
transaction: claimTo({
contract,