From acf15445af8ac81606f3d17b04a5704886634e4e Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 29 Oct 2024 09:48:55 +1300 Subject: [PATCH] feat: enhance TransactionButton and add buy support check --- .changeset/cold-eels-crash.md | 5 + .../src/app/connect/pay/transactions/page.tsx | 4 +- .../src/components/pay/transaction-button.tsx | 50 ++++++---- .../read/resolveName.test.ts | 37 ++++---- .../hooks/transaction/useSendTransaction.ts | 38 +++++++- .../screens/Buy/TransactionModeScreen.tsx | 93 ++++++++++++------- .../Buy/swap/useSwapSupportedChains.ts | 2 +- .../wallets/smart/smart-wallet-dev.test.ts | 27 +++++- 8 files changed, 179 insertions(+), 77 deletions(-) create mode 100644 .changeset/cold-eels-crash.md 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 + {USDC.name} + )} + 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,