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/cold-eels-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Handle unsupported Pay chains properly for sending paid transactions
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ function NoFundsPopup() {
to: account.address,
});
}}
/>
>
Buy VIP Pass
</TransactionButton>
);
};`}
lang="tsx"
Expand Down
50 changes: 31 additions & 19 deletions apps/playground-web/src/components/pay/transaction-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -68,24 +70,34 @@ export function PayTransactionButtonPreview() {
<>
<StyledConnectButton />
{account && (
<TransactionButton
transaction={() => {
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
</TransactionButton>
<div className="flex flex-col items-center justify-center gap-2">
<div className="flex items-center gap-2">
Price:{" "}
{USDC?.icon && (
// eslint-disable-next-line @next/next/no-img-element
<img src={USDC.icon} width={16} alt={USDC.name} />
)}
50 {USDC?.symbol}
</div>
<TransactionButton
transaction={() => {
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
</TransactionButton>
</div>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 <LoadingScreen />;
Expand All @@ -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 ? (
<Container
flex="row"
center="both"
style={{
padding: spacing.md,
marginBottom: spacing.md,
borderRadius: spacing.md,
backgroundColor: theme.colors.tertiaryBg,
}}
>
<WalletImage
size={iconSize.xl}
id={activeWallet.id}
client={client}
/>
<div
) : activeAccount ? (
<Container flex="column" gap="sm">
<Text size="sm" color="danger" style={{ textAlign: "center" }}>
Insufficient funds
</Text>
<Container
flex="row"
style={{
flexGrow: 1,
borderBottom: "6px dotted",
borderColor: theme.colors.secondaryIconColor,
marginLeft: spacing.md,
marginRight: spacing.md,
justifyContent: "space-between",
padding: spacing.sm,
marginBottom: spacing.sm,
borderRadius: spacing.md,
backgroundColor: theme.colors.tertiaryBg,
border: `1px solid ${theme.colors.borderColor}`,
}}
/>
<ChainIcon
client={client}
size={iconSize.xl}
chainIconUrl={chainData.icon?.url}
/>
>
<WalletRow
address={activeAccount?.address}
iconSize="md"
client={client}
/>
{balanceQuery.data ? (
<Container flex="row" gap="3xs" center="y">
<Text size="xs" color="secondaryText" weight={500}>
{formatTokenBalance(balanceQuery.data, false, 3)}
</Text>
<TokenSymbol
token={transactionCostAndData.token}
chain={payUiOptions.transaction.chain}
size="xs"
color="secondaryText"
/>
</Container>
) : (
<Skeleton width="70px" height={fontSize.xs} />
)}
</Container>
</Container>
) : null}
<Spacer y="md" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type SupportedChainAndTokens = Array<{
}>;
}>;

async function fetchBuySupportedDestinations(
export async function fetchBuySupportedDestinations(
client: ThirdwebClient,
isTestMode?: boolean,
): Promise<SupportedChainAndTokens> {
Expand Down
27 changes: 23 additions & 4 deletions packages/thirdweb/src/wallets/smart/smart-wallet-dev.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down