[SDK] Allow passing activeWallet to SwapWidget (#8504)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the `SwapWidget` by allowing the passing of an `activeWallet` prop and improving the project wallet functionality in the dashboard. It also includes updates to the VSCode settings and introduces new components for swapping tokens. ### Detailed summary - Added `activeWallet` prop to `SwapWidgetProps`. - Modified `useActiveWalletInfo` to accept an `activeWalletOverride`. - Introduced `SwapProjectWalletModalContent` for token swapping. - Updated `ProjectWalletDetailsSection` to include swap functionality. - Adjusted VSCode settings for formatting preferences. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added in-app token swap functionality for project wallets, allowing users to exchange tokens directly from the wallet interface with credentials management. * SwapWidget now supports wallet pre-selection, enabling faster and more streamlined token swap initiation. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"thirdweb": patch
|
||||
---
|
||||
|
||||
Allow passing an activeWallet to SwapWidget
|
||||
Vendored
+5
-2
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
+223
-1
@@ -3,6 +3,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowLeftRightIcon,
|
||||
EllipsisVerticalIcon,
|
||||
RefreshCcwIcon,
|
||||
@@ -10,17 +11,22 @@ import {
|
||||
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";
|
||||
@@ -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: {
|
||||
@@ -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(() => {
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -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<{
|
||||
|
||||
@@ -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 {
|
||||
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
|
||||
? {
|
||||
activeChain,
|
||||
activeWallet,
|
||||
activeAccount,
|
||||
activeChain: chain,
|
||||
activeWallet: wallet,
|
||||
activeAccount: account,
|
||||
}
|
||||
: undefined;
|
||||
}, [activeAccount, activeWallet, activeChain]);
|
||||
}, [activeAccount, activeWallet, activeChain, activeWalletOverride]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user