Skip to content

Commit adeda1e

Browse files
feat: enhance TransactionButton and add buy support check (#5207)
1 parent fb5115a commit adeda1e

File tree

8 files changed

+179
-77
lines changed

8 files changed

+179
-77
lines changed

.changeset/cold-eels-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Handle unsupported Pay chains properly for sending paid transactions

apps/playground-web/src/app/connect/pay/transactions/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ function NoFundsPopup() {
131131
to: account.address,
132132
});
133133
}}
134-
/>
134+
>
135+
Buy VIP Pass
136+
</TransactionButton>
135137
);
136138
};`}
137139
lang="tsx"

apps/playground-web/src/components/pay/transaction-button.tsx

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ const nftContract = getContract({
2121
client: THIRDWEB_CLIENT,
2222
});
2323

24+
const USDC = getDefaultToken(sepolia, "USDC");
25+
2426
const usdcContract = getContract({
2527
// biome-ignore lint/style/noNonNullAssertion: its there
26-
address: getDefaultToken(sepolia, "USDC")!.address,
28+
address: USDC!.address,
2729
chain: sepolia,
2830
client: THIRDWEB_CLIENT,
2931
});
@@ -68,24 +70,34 @@ export function PayTransactionButtonPreview() {
6870
<>
6971
<StyledConnectButton />
7072
{account && (
71-
<TransactionButton
72-
transaction={() => {
73-
if (!account) throw new Error("No active account");
74-
return transfer({
75-
contract: usdcContract,
76-
amount: "50",
77-
to: account?.address || "",
78-
});
79-
}}
80-
onError={(e) => {
81-
console.error(e);
82-
}}
83-
payModal={{
84-
theme: theme === "light" ? "light" : "dark",
85-
}}
86-
>
87-
Transfer funds
88-
</TransactionButton>
73+
<div className="flex flex-col items-center justify-center gap-2">
74+
<div className="flex items-center gap-2">
75+
Price:{" "}
76+
{USDC?.icon && (
77+
// eslint-disable-next-line @next/next/no-img-element
78+
<img src={USDC.icon} width={16} alt={USDC.name} />
79+
)}
80+
50 {USDC?.symbol}
81+
</div>
82+
<TransactionButton
83+
transaction={() => {
84+
if (!account) throw new Error("No active account");
85+
return transfer({
86+
contract: usdcContract,
87+
amount: "50",
88+
to: account?.address || "",
89+
});
90+
}}
91+
onError={(e) => {
92+
console.error(e);
93+
}}
94+
payModal={{
95+
theme: theme === "light" ? "light" : "dark",
96+
}}
97+
>
98+
Buy VIP Pass
99+
</TransactionButton>
100+
</div>
89101
)}
90102
</>
91103
);

packages/thirdweb/src/extensions/unstoppable-domains/read/resolveName.test.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,24 @@ import { resolveName } from "./resolveName.js";
55

66
// Double check: https://unstoppabledomains.com/d/thirdwebsdk.unstoppable
77

8-
describe("Unstoppable Domain: resolve name", () => {
9-
it("should resolve name", async () => {
10-
expect(
11-
await resolveName({
12-
address: "0x12345674b599ce99958242b3D3741e7b01841DF3",
13-
client: TEST_CLIENT,
14-
}),
15-
).toBe("thirdwebsdk.unstoppable");
16-
});
8+
describe.runIf(process.env.TW_SECRET_KEY)(
9+
"Unstoppable Domain: resolve name",
10+
() => {
11+
it("should resolve name", async () => {
12+
expect(
13+
await resolveName({
14+
address: "0x12345674b599ce99958242b3D3741e7b01841DF3",
15+
client: TEST_CLIENT,
16+
}),
17+
).toBe("thirdwebsdk.unstoppable");
18+
});
1719

18-
it("should throw error on addresses that dont own any UD", async () => {
19-
await expect(() =>
20-
resolveName({ client: TEST_CLIENT, address: TEST_ACCOUNT_D.address }),
21-
).rejects.toThrowError(
22-
`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`,
23-
);
24-
});
25-
});
20+
it("should throw error on addresses that dont own any UD", async () => {
21+
await expect(() =>
22+
resolveName({ client: TEST_CLIENT, address: TEST_ACCOUNT_D.address }),
23+
).rejects.toThrowError(
24+
`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`,
25+
);
26+
});
27+
},
28+
);

packages/thirdweb/src/react/core/hooks/transaction/useSendTransaction.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { resolvePromisedValue } from "../../../../utils/promise/resolve-promised
1313
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
1414
import { getTokenBalance } from "../../../../wallets/utils/getTokenBalance.js";
1515
import { getWalletBalance } from "../../../../wallets/utils/getWalletBalance.js";
16+
import { fetchBuySupportedDestinations } from "../../../web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.js";
1617
import type { LocaleId } from "../../../web/ui/types.js";
1718
import type { Theme } from "../../design-system/index.js";
1819
import type { SupportedTokens } from "../../utils/defaultTokens.js";
@@ -164,10 +165,39 @@ export function useSendTransactionCore(args: {
164165

165166
(async () => {
166167
try {
167-
const [_nativeValue, _erc20Value] = await Promise.all([
168-
resolvePromisedValue(tx.value),
169-
resolvePromisedValue(tx.erc20Value),
170-
]);
168+
const [_nativeValue, _erc20Value, supportedDestinations] =
169+
await Promise.all([
170+
resolvePromisedValue(tx.value),
171+
resolvePromisedValue(tx.erc20Value),
172+
fetchBuySupportedDestinations(tx.client).catch(() => null),
173+
]);
174+
175+
if (!supportedDestinations) {
176+
// could not fetch supported destinations, just send the tx
177+
sendTx();
178+
return;
179+
}
180+
181+
if (
182+
!supportedDestinations
183+
.map((x) => x.chain.id)
184+
.includes(tx.chain.id) ||
185+
(_erc20Value &&
186+
!supportedDestinations.some(
187+
(x) =>
188+
x.chain.id === tx.chain.id &&
189+
x.tokens.find(
190+
(t) =>
191+
t.address.toLowerCase() ===
192+
_erc20Value.tokenAddress.toLowerCase(),
193+
),
194+
))
195+
) {
196+
// chain/token not supported, just send the tx
197+
sendTx();
198+
return;
199+
}
200+
171201
const nativeValue = _nativeValue || 0n;
172202
const erc20Value = _erc20Value?.amountWei || 0n;
173203

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/TransactionModeScreen.tsx

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,32 @@ import { formatNumber } from "../../../../../../utils/formatNumber.js";
66
import { toTokens } from "../../../../../../utils/units.js";
77
import type { Account } from "../../../../../../wallets/interfaces/wallet.js";
88
import { useCustomTheme } from "../../../../../core/design-system/CustomThemeProvider.js";
9-
import { iconSize, spacing } from "../../../../../core/design-system/index.js";
9+
import { fontSize, spacing } from "../../../../../core/design-system/index.js";
1010
import type { PayUIOptions } from "../../../../../core/hooks/connection/ConnectButtonProps.js";
1111
import { useChainMetadata } from "../../../../../core/hooks/others/useChainQuery.js";
12+
import { useWalletBalance } from "../../../../../core/hooks/others/useWalletBalance.js";
13+
import { useActiveAccount } from "../../../../../core/hooks/wallets/useActiveAccount.js";
1214
import { useActiveWallet } from "../../../../../core/hooks/wallets/useActiveWallet.js";
1315
import { hasSponsoredTransactionsEnabled } from "../../../../../core/utils/wallet.js";
1416
import { LoadingScreen } from "../../../../wallets/shared/LoadingScreen.js";
1517
import type { PayEmbedConnectOptions } from "../../../PayEmbed.js";
1618
import { ChainIcon } from "../../../components/ChainIcon.js";
1719
import { Img } from "../../../components/Img.js";
20+
import { Skeleton } from "../../../components/Skeleton.js";
1821
import { Spacer } from "../../../components/Spacer.js";
1922
import { TokenIcon } from "../../../components/TokenIcon.js";
20-
import { WalletImage } from "../../../components/WalletImage.js";
2123
import { Container, Line, ModalHeader } from "../../../components/basic.js";
2224
import { Button } from "../../../components/buttons.js";
2325
import { Text } from "../../../components/text.js";
26+
import { TokenSymbol } from "../../../components/token/TokenSymbol.js";
2427
import { ConnectButton } from "../../ConnectButton.js";
25-
import type { ERC20OrNativeToken } from "../nativeToken.js";
28+
import { formatTokenBalance } from "../formatTokenBalance.js";
29+
import {
30+
type ERC20OrNativeToken,
31+
NATIVE_TOKEN,
32+
isNativeToken,
33+
} from "../nativeToken.js";
34+
import { WalletRow } from "./WalletSelectorButton.js";
2635
import { useTransactionCostAndData } from "./main/useBuyTxStates.js";
2736
import type { SupportedChainAndTokens } from "./swap/useSwapSupportedChains.js";
2837

@@ -54,8 +63,22 @@ export function TransactionModeScreen(props: {
5463
});
5564
const theme = useCustomTheme();
5665
const activeWallet = useActiveWallet();
66+
const activeAccount = useActiveAccount();
5767
const sponsoredTransactionsEnabled =
5868
hasSponsoredTransactionsEnabled(activeWallet);
69+
const balanceQuery = useWalletBalance(
70+
{
71+
address: activeAccount?.address,
72+
chain: payUiOptions.transaction.chain,
73+
tokenAddress: isNativeToken(transactionCostAndData?.token || NATIVE_TOKEN)
74+
? undefined
75+
: transactionCostAndData?.token.address,
76+
client: props.client,
77+
},
78+
{
79+
enabled: !!transactionCostAndData,
80+
},
81+
);
5982

6083
if (!transactionCostAndData || !chainData) {
6184
return <LoadingScreen />;
@@ -74,39 +97,47 @@ export function TransactionModeScreen(props: {
7497
style={{
7598
width: "100%",
7699
borderRadius: spacing.md,
100+
border: `1px solid ${theme.colors.borderColor}`,
77101
backgroundColor: theme.colors.tertiaryBg,
78102
}}
79103
/>
80-
) : activeWallet ? (
81-
<Container
82-
flex="row"
83-
center="both"
84-
style={{
85-
padding: spacing.md,
86-
marginBottom: spacing.md,
87-
borderRadius: spacing.md,
88-
backgroundColor: theme.colors.tertiaryBg,
89-
}}
90-
>
91-
<WalletImage
92-
size={iconSize.xl}
93-
id={activeWallet.id}
94-
client={client}
95-
/>
96-
<div
104+
) : activeAccount ? (
105+
<Container flex="column" gap="sm">
106+
<Text size="sm" color="danger" style={{ textAlign: "center" }}>
107+
Insufficient funds
108+
</Text>
109+
<Container
110+
flex="row"
97111
style={{
98-
flexGrow: 1,
99-
borderBottom: "6px dotted",
100-
borderColor: theme.colors.secondaryIconColor,
101-
marginLeft: spacing.md,
102-
marginRight: spacing.md,
112+
justifyContent: "space-between",
113+
padding: spacing.sm,
114+
marginBottom: spacing.sm,
115+
borderRadius: spacing.md,
116+
backgroundColor: theme.colors.tertiaryBg,
117+
border: `1px solid ${theme.colors.borderColor}`,
103118
}}
104-
/>
105-
<ChainIcon
106-
client={client}
107-
size={iconSize.xl}
108-
chainIconUrl={chainData.icon?.url}
109-
/>
119+
>
120+
<WalletRow
121+
address={activeAccount?.address}
122+
iconSize="md"
123+
client={client}
124+
/>
125+
{balanceQuery.data ? (
126+
<Container flex="row" gap="3xs" center="y">
127+
<Text size="xs" color="secondaryText" weight={500}>
128+
{formatTokenBalance(balanceQuery.data, false, 3)}
129+
</Text>
130+
<TokenSymbol
131+
token={transactionCostAndData.token}
132+
chain={payUiOptions.transaction.chain}
133+
size="xs"
134+
color="secondaryText"
135+
/>
136+
</Container>
137+
) : (
138+
<Skeleton width="70px" height={fontSize.xs} />
139+
)}
140+
</Container>
110141
</Container>
111142
) : null}
112143
<Spacer y="md" />

packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/swap/useSwapSupportedChains.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type SupportedChainAndTokens = Array<{
3434
}>;
3535
}>;
3636

37-
async function fetchBuySupportedDestinations(
37+
export async function fetchBuySupportedDestinations(
3838
client: ThirdwebClient,
3939
isTestMode?: boolean,
4040
): Promise<SupportedChainAndTokens> {

packages/thirdweb/src/wallets/smart/smart-wallet-dev.test.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22
import { TEST_CLIENT } from "../../../test/src/test-clients.js";
3-
import { arbitrumSepolia } from "../../chains/chain-definitions/arbitrum-sepolia.js";
3+
import { zkSyncSepolia } from "../../chains/chain-definitions/zksync-sepolia.js";
44
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
55
import { balanceOf } from "../../extensions/erc1155/__generated__/IERC1155/read/balanceOf.js";
66
import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js";
77
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
8+
import { sendTransaction } from "../../transaction/actions/send-transaction.js";
9+
import { prepareTransaction } from "../../transaction/prepare-transaction.js";
810
import type { Address } from "../../utils/address.js";
911
import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js";
1012
import { setThirdwebDomains } from "../../utils/domains.js";
@@ -18,7 +20,7 @@ let smartWalletAddress: Address;
1820
let personalAccount: Account;
1921
let accountContract: ThirdwebContract;
2022

21-
const chain = arbitrumSepolia;
23+
const chain = zkSyncSepolia;
2224
const client = TEST_CLIENT;
2325
const contract = getContract({
2426
client,
@@ -61,13 +63,30 @@ describe.runIf(process.env.TW_SECRET_KEY).skip.sequential(
6163
expect(smartWalletAddress).toHaveLength(42);
6264
});
6365

64-
it("can sign a msg", async () => {
66+
it.skip("can sign a msg", async () => {
6567
await smartAccount.signMessage({ message: "hello world" });
6668
const isDeployed = await isContractDeployed(accountContract);
6769
expect(isDeployed).toEqual(true);
6870
});
6971

70-
it("can execute a tx", async () => {
72+
it("should send a transaction", async () => {
73+
const tx = prepareTransaction({
74+
client,
75+
chain,
76+
to: smartAccount.address,
77+
value: 0n,
78+
});
79+
80+
console.log("Sending transaction...");
81+
const receipt = await sendTransaction({
82+
transaction: tx,
83+
account: smartAccount,
84+
});
85+
console.log("Transaction sent:", receipt.transactionHash);
86+
expect(receipt.transactionHash).toBeDefined();
87+
});
88+
89+
it.skip("can execute a tx", async () => {
7190
const tx = await sendAndConfirmTransaction({
7291
transaction: claimTo({
7392
contract,

0 commit comments

Comments
 (0)