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

Allow passing an activeWallet to SwapWidget
7 changes: 5 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
Expand All @@ -18,8 +20,9 @@
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.fixAll.biome": "explicit"
"quickfix.biome": "always",
"source.fixAll.biome": "always",
"source.organizeImports.biome": "always"
},
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/@/constants/thirdweb.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {
THIRDWEB_BRIDGE_URL,
THIRDWEB_BUNDLER_DOMAIN,
THIRDWEB_ENGINE_CLOUD_URL,
THIRDWEB_INAPP_WALLET_DOMAIN,
THIRDWEB_INSIGHT_API_DOMAIN,
THIRDWEB_PAY_DOMAIN,
Expand All @@ -38,6 +39,7 @@ export function getConfiguredThirdwebClient(options: {
rpc: THIRDWEB_RPC_DOMAIN,
social: THIRDWEB_SOCIAL_API_DOMAIN,
storage: THIRDWEB_STORAGE_DOMAIN,
engineCloud: new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname,
});
}

Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/src/@/constants/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ export const THIRDWEB_INSIGHT_API_DOMAIN =

export const THIRDWEB_BRIDGE_URL =
process.env.NEXT_PUBLIC_BRIDGE_URL || "bridge.thirdweb-dev.com";

export const THIRDWEB_ENGINE_CLOUD_URL =
process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "https://engine.thirdweb-dev.com";
Comment on lines +29 to +30
Copy link

Choose a reason for hiding this comment

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

The THIRDWEB_ENGINE_CLOUD_URL constant uses a full URL format with https:// protocol, but this causes a crash when the environment variable is set to just a hostname (which is the pattern used for all other domain constants).

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/@/constants/thirdweb.server.ts b/apps/dashboard/src/@/constants/thirdweb.server.ts
index 3525c4089..9046a21cf 100644
--- a/apps/dashboard/src/@/constants/thirdweb.server.ts
+++ b/apps/dashboard/src/@/constants/thirdweb.server.ts
@@ -39,7 +39,7 @@ export function getConfiguredThirdwebClient(options: {
       rpc: THIRDWEB_RPC_DOMAIN,
       social: THIRDWEB_SOCIAL_API_DOMAIN,
       storage: THIRDWEB_STORAGE_DOMAIN,
-      engineCloud: new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname,
+      engineCloud: THIRDWEB_ENGINE_CLOUD_URL,
     });
   }
 
diff --git a/apps/dashboard/src/@/constants/urls.ts b/apps/dashboard/src/@/constants/urls.ts
index a2856c5f3..7bc691737 100644
--- a/apps/dashboard/src/@/constants/urls.ts
+++ b/apps/dashboard/src/@/constants/urls.ts
@@ -27,4 +27,4 @@ export const THIRDWEB_BRIDGE_URL =
   process.env.NEXT_PUBLIC_BRIDGE_URL || "bridge.thirdweb-dev.com";
 
 export const THIRDWEB_ENGINE_CLOUD_URL =
-  process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "https://engine.thirdweb-dev.com";
+  process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "engine.thirdweb-dev.com";

Analysis

Inconsistent URL format in THIRDWEB_ENGINE_CLOUD_URL causes crash with environment variable override

What fails: THIRDWEB_ENGINE_CLOUD_URL uses a full URL format with protocol ("https://engine.thirdweb-dev.com"), but the code that uses it in thirdweb.server.ts line 42 calls new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname to extract the hostname. This causes a TypeError: Invalid URL when the environment variable NEXT_PUBLIC_ENGINE_CLOUD_URL is set to a hostname-only value (following the pattern of all other domain constants like THIRDWEB_PAY_DOMAIN, THIRDWEB_BUNDLER_DOMAIN, etc.).

How to reproduce:

  1. Set NEXT_PUBLIC_ENGINE_CLOUD_URL=engine.thirdweb-dev.com (hostname only, like other domain env vars)
  2. Run the application in non-production environment
  3. The getConfiguredThirdwebClient() function in thirdweb.server.ts calls setThirdwebDomains() which executes new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname
  4. This crashes with: TypeError: Invalid URL

Result: Application crashes with invalid URL error in non-production environments when the environment variable is set to a hostname-only value.

Expected: The constant should follow the same pattern as all other domain constants in the thirdweb SDK - storing just the hostname without protocol. The setThirdwebDomains() function expects domain names without protocols and automatically adds the correct protocol (https:// for normal domains, http:// for localhost).

Changes made:

  1. Changed THIRDWEB_ENGINE_CLOUD_URL constant from "https://engine.thirdweb-dev.com" to "engine.thirdweb-dev.com" in apps/dashboard/src/@/constants/urls.ts
  2. Updated the usage in apps/dashboard/src/@/constants/thirdweb.server.ts line 42 from new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname to THIRDWEB_ENGINE_CLOUD_URL to pass the hostname directly, consistent with all other domain parameters

Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,30 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
ArrowLeftIcon,
ArrowLeftRightIcon,
EllipsisVerticalIcon,
RefreshCcwIcon,
SendIcon,
ShuffleIcon,
WalletIcon,
} from "lucide-react";
import { useTheme } from "next-themes";
import { useCallback, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import {
createThirdwebClient,
Engine,
getContract,
readContract,
type ThirdwebClient,
toUnits,
} from "thirdweb";
import { useWalletBalance } from "thirdweb/react";
import type { Chain } from "thirdweb/chains";
import { SwapWidget, useWalletBalance } from "thirdweb/react";
import { isAddress, shortenAddress } from "thirdweb/utils";
import { createWalletAdapter } from "thirdweb/wallets";
import { z } from "zod";
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
import type { Project } from "@/api/project/projects";
Expand Down Expand Up @@ -69,6 +75,7 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
import { cn } from "@/lib/utils";
import { getSDKTheme } from "@/utils/sdk-component-theme";
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";

type GetProjectServerWallets = (params: {
Expand Down Expand Up @@ -127,6 +134,11 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
const [isSendOpen, setIsSendOpen] = useState(false);
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
const [isSwapOpen, setIsSwapOpen] = useState(false);

// Persist swap credentials in memory so users don't have to re-enter them
const [swapSecretKey, setSwapSecretKey] = useState("");
const [swapVaultAccessToken, setSwapVaultAccessToken] = useState("");

// Initialize chain and token from localStorage or defaults
const [selectedChainId, setSelectedChainId] = useState(() => {
Expand Down Expand Up @@ -310,6 +322,15 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
<SendIcon className="size-4" />
Withdraw
</Button>
<Button
variant="outline"
size="sm"
className="gap-2 bg-background hover:bg-accent/50"
onClick={() => setIsSwapOpen(true)}
>
<ArrowLeftRightIcon className="size-4" />
Swap
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
Expand Down Expand Up @@ -394,6 +415,24 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
/>
</DialogContent>
</Dialog>

<Dialog onOpenChange={setIsSwapOpen} open={isSwapOpen}>
<DialogContent className="gap-0 p-0 overflow-hidden max-w-md">
<SwapProjectWalletModalContent
chainId={selectedChainId}
tokenAddress={selectedTokenAddress}
walletAddress={projectWallet.address}
chain={chain}
isManagedVault={isManagedVault}
publishableKey={project.publishableKey}
secretKey={swapSecretKey}
setSecretKey={setSwapSecretKey}
vaultAccessToken={swapVaultAccessToken}
setVaultAccessToken={setSwapVaultAccessToken}
onClose={() => setIsSwapOpen(false)}
/>
</DialogContent>
</Dialog>
</div>
);
}
Expand Down Expand Up @@ -573,6 +612,189 @@ function ChangeProjectWalletDialogContent(props: {
);
}

type SwapProjectWalletModalContentProps = {
chainId: number;
tokenAddress: string | undefined;
walletAddress: string;
chain: Chain;
isManagedVault: boolean;
publishableKey: string;
secretKey: string;
setSecretKey: (value: string) => void;
vaultAccessToken: string;
setVaultAccessToken: (value: string) => void;
onClose: () => void;
};

function SwapProjectWalletModalContent(
props: SwapProjectWalletModalContentProps,
) {
const {
chainId,
tokenAddress,
walletAddress,
chain,
isManagedVault,
publishableKey,
secretKey,
setSecretKey,
vaultAccessToken,
setVaultAccessToken,
onClose,
} = props;

const [screen, setScreen] = useState<"credentials" | "swap">("credentials");
const { theme } = useTheme();
const t = theme === "light" ? "light" : "dark";

const hasRequiredCredentials = isManagedVault
? secretKey.trim().length > 0
: secretKey.trim().length > 0 && vaultAccessToken.trim().length > 0;

const swapClient = useMemo(() => {
if (!secretKey.trim()) {
return null;
}
return createThirdwebClient({
clientId: publishableKey,
secretKey: secretKey.trim(),
});
}, [secretKey, publishableKey]);

const activeWallet = useMemo(() => {
if (!swapClient) {
return undefined;
}
const vaultAccessTokenValue = vaultAccessToken.trim();
return createWalletAdapter({
adaptedAccount: Engine.serverWallet({
client: swapClient,
address: walletAddress,
...(vaultAccessTokenValue
? { vaultAccessToken: vaultAccessTokenValue }
: {}),
}),
chain: chain,
client: swapClient,
onDisconnect: () => {},
switchChain: () => {},
});
}, [swapClient, walletAddress, chain, vaultAccessToken]);

// Screen 1: Credentials
if (screen === "credentials") {
return (
<div>
<DialogHeader className="p-4 lg:p-6">
<DialogTitle>Swap Tokens</DialogTitle>
<DialogDescription>
Enter your credentials to swap tokens from your project wallet
</DialogDescription>
</DialogHeader>

<div className="px-4 pb-4 lg:px-6 lg:pb-6 space-y-4">
<div className="space-y-2">
<label
htmlFor="swap-secret-key"
className="text-sm font-medium leading-none"
>
Project secret key
</label>
<Input
id="swap-secret-key"
type="password"
placeholder="Enter your project secret key"
value={secretKey}
onChange={(e) => setSecretKey(e.target.value)}
autoComplete="off"
autoCorrect="off"
spellCheck={false}
/>
<p className="text-xs text-muted-foreground">{secretKeyHelper}</p>
</div>

{!isManagedVault && (
<div className="space-y-2">
<label
htmlFor="swap-vault-access-token"
className="text-sm font-medium leading-none"
>
Vault access token
</label>
<Input
id="swap-vault-access-token"
type="password"
placeholder="Enter a vault access token"
value={vaultAccessToken}
onChange={(e) => setVaultAccessToken(e.target.value)}
autoComplete="off"
autoCorrect="off"
spellCheck={false}
/>
<p className="text-xs text-muted-foreground">
{vaultAccessTokenHelper}
</p>
</div>
)}
</div>

<div className="flex justify-end gap-3 border-t bg-card p-4">
<Button onClick={onClose} type="button" variant="outline">
Cancel
</Button>
<Button
onClick={() => setScreen("swap")}
type="button"
disabled={!hasRequiredCredentials}
>
Continue
</Button>
</div>
</div>
);
}

// Screen 2: Swap Widget
return (
<div className="w-full">
<DialogHeader className="p-4 lg:p-6">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="p-1 h-auto w-auto"
onClick={() => setScreen("credentials")}
>
<ArrowLeftIcon className="size-4" />
</Button>
<div>
<DialogTitle>Swap Tokens</DialogTitle>
<DialogDescription>
Swap tokens from your project wallet
</DialogDescription>
</div>
</div>
</DialogHeader>

<div className="px-4 pb-4 lg:px-6 lg:pb-6 flex justify-center">
{swapClient && activeWallet && (
<SwapWidget
client={swapClient}
prefill={{
sellToken: {
chainId: chainId,
tokenAddress: tokenAddress,
},
}}
activeWallet={activeWallet}
theme={getSDKTheme(t)}
/>
)}
</div>
</div>
);
}

const createSendFormSchema = (secretKeyLabel: string) =>
z.object({
chainId: z.number({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ThirdwebClient } from "../../../../../client/client.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
import { getAddress } from "../../../../../utils/address.js";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import { CustomThemeProvider } from "../../../../core/design-system/CustomThemeProvider.js";
import type { Theme } from "../../../../core/design-system/index.js";
import type { BridgePrepareRequest } from "../../../../core/hooks/useBridgePrepare.js";
Expand Down Expand Up @@ -169,6 +170,10 @@ export type SwapWidgetProps = {
* Called when the user disconnects the active wallet
*/
onDisconnect?: () => void;
/**
* The wallet that should be pre-selected in the SwapWidget UI.
*/
activeWallet?: Wallet;
};

/**
Expand Down Expand Up @@ -320,7 +325,7 @@ function SwapWidgetContent(
},
) {
const [screen, setScreen] = useState<SwapWidgetScreen>({ id: "1:swap-ui" });
const activeWalletInfo = useActiveWalletInfo();
const activeWalletInfo = useActiveWalletInfo(props.activeWallet);
const isPersistEnabled = props.persistTokenSelections !== false;

const [amountSelection, setAmountSelection] = useState<{
Expand Down
18 changes: 12 additions & 6 deletions packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { useMemo } from "react";
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccount.js";
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js";
import type { ActiveWalletInfo } from "./types.js";

export function useActiveWalletInfo(): ActiveWalletInfo | undefined {
export function useActiveWalletInfo(
activeWalletOverride?: Wallet,
): ActiveWalletInfo | undefined {

Check warning on line 10 in packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts#L9-L10

Added lines #L9 - L10 were not covered by tests
const activeAccount = useActiveAccount();
const activeWallet = useActiveWallet();
const activeChain = useActiveWalletChain();

return useMemo(() => {
return activeAccount && activeWallet && activeChain
const wallet = activeWalletOverride || activeWallet;
const chain = activeWalletOverride?.getChain() || activeChain;
const account = activeWalletOverride?.getAccount() || activeAccount;
return wallet && chain && account

Check warning on line 19 in packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts#L16-L19

Added lines #L16 - L19 were not covered by tests
? {
activeChain,
activeWallet,
activeAccount,
activeChain: chain,
activeWallet: wallet,
activeAccount: account,

Check warning on line 23 in packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts#L21-L23

Added lines #L21 - L23 were not covered by tests
}
: undefined;
}, [activeAccount, activeWallet, activeChain]);
}, [activeAccount, activeWallet, activeChain, activeWalletOverride]);

Check warning on line 26 in packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/hooks.ts#L26

Added line #L26 was not covered by tests
}
Loading