Skip to content

Commit a704c3f

Browse files
[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 -->
1 parent b0eccf3 commit a704c3f

File tree

7 files changed

+256
-10
lines changed

7 files changed

+256
-10
lines changed

.changeset/shaky-hoops-battle.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+
Allow passing an activeWallet to SwapWidget

.vscode/settings.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
2+
"editor.insertSpaces": true,
3+
"editor.tabSize": 2,
24
"[css]": {
35
"editor.defaultFormatter": "biomejs.biome"
46
},
@@ -18,8 +20,9 @@
1820
"editor.defaultFormatter": "biomejs.biome"
1921
},
2022
"editor.codeActionsOnSave": {
21-
"quickfix.biome": "explicit",
22-
"source.fixAll.biome": "explicit"
23+
"quickfix.biome": "always",
24+
"source.fixAll.biome": "always",
25+
"source.organizeImports.biome": "always"
2326
},
2427
"editor.defaultFormatter": "biomejs.biome",
2528
"editor.formatOnSave": true,

apps/dashboard/src/@/constants/thirdweb.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {
1414
THIRDWEB_BRIDGE_URL,
1515
THIRDWEB_BUNDLER_DOMAIN,
16+
THIRDWEB_ENGINE_CLOUD_URL,
1617
THIRDWEB_INAPP_WALLET_DOMAIN,
1718
THIRDWEB_INSIGHT_API_DOMAIN,
1819
THIRDWEB_PAY_DOMAIN,
@@ -38,6 +39,7 @@ export function getConfiguredThirdwebClient(options: {
3839
rpc: THIRDWEB_RPC_DOMAIN,
3940
social: THIRDWEB_SOCIAL_API_DOMAIN,
4041
storage: THIRDWEB_STORAGE_DOMAIN,
42+
engineCloud: new URL(THIRDWEB_ENGINE_CLOUD_URL).hostname,
4143
});
4244
}
4345

apps/dashboard/src/@/constants/urls.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ export const THIRDWEB_INSIGHT_API_DOMAIN =
2525

2626
export const THIRDWEB_BRIDGE_URL =
2727
process.env.NEXT_PUBLIC_BRIDGE_URL || "bridge.thirdweb-dev.com";
28+
29+
export const THIRDWEB_ENGINE_CLOUD_URL =
30+
process.env.NEXT_PUBLIC_ENGINE_CLOUD_URL || "https://engine.thirdweb-dev.com";

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet-details.tsx

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@
33
import { zodResolver } from "@hookform/resolvers/zod";
44
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
55
import {
6+
ArrowLeftIcon,
67
ArrowLeftRightIcon,
78
EllipsisVerticalIcon,
89
RefreshCcwIcon,
910
SendIcon,
1011
ShuffleIcon,
1112
WalletIcon,
1213
} from "lucide-react";
14+
import { useTheme } from "next-themes";
1315
import { useCallback, useMemo, useState } from "react";
1416
import { useForm } from "react-hook-form";
1517
import { toast } from "sonner";
1618
import {
19+
createThirdwebClient,
20+
Engine,
1721
getContract,
1822
readContract,
1923
type ThirdwebClient,
2024
toUnits,
2125
} from "thirdweb";
22-
import { useWalletBalance } from "thirdweb/react";
26+
import type { Chain } from "thirdweb/chains";
27+
import { SwapWidget, useWalletBalance } from "thirdweb/react";
2328
import { isAddress, shortenAddress } from "thirdweb/utils";
29+
import { createWalletAdapter } from "thirdweb/wallets";
2430
import { z } from "zod";
2531
import { sendProjectWalletTokens } from "@/actions/project-wallet/send-tokens";
2632
import type { Project } from "@/api/project/projects";
@@ -69,6 +75,7 @@ import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
6975
import { useDashboardRouter } from "@/lib/DashboardRouter";
7076
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
7177
import { cn } from "@/lib/utils";
78+
import { getSDKTheme } from "@/utils/sdk-component-theme";
7279
import { updateDefaultProjectWallet } from "../../transactions/lib/vault.client";
7380

7481
type GetProjectServerWallets = (params: {
@@ -127,6 +134,11 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
127134
const [isSendOpen, setIsSendOpen] = useState(false);
128135
const [isReceiveOpen, setIsReceiveOpen] = useState(false);
129136
const [isChangeWalletOpen, setIsChangeWalletOpen] = useState(false);
137+
const [isSwapOpen, setIsSwapOpen] = useState(false);
138+
139+
// Persist swap credentials in memory so users don't have to re-enter them
140+
const [swapSecretKey, setSwapSecretKey] = useState("");
141+
const [swapVaultAccessToken, setSwapVaultAccessToken] = useState("");
130142

131143
// Initialize chain and token from localStorage or defaults
132144
const [selectedChainId, setSelectedChainId] = useState(() => {
@@ -310,6 +322,15 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
310322
<SendIcon className="size-4" />
311323
Withdraw
312324
</Button>
325+
<Button
326+
variant="outline"
327+
size="sm"
328+
className="gap-2 bg-background hover:bg-accent/50"
329+
onClick={() => setIsSwapOpen(true)}
330+
>
331+
<ArrowLeftRightIcon className="size-4" />
332+
Swap
333+
</Button>
313334
<DropdownMenu>
314335
<DropdownMenuTrigger asChild>
315336
<Button
@@ -394,6 +415,24 @@ export function ProjectWalletDetailsSection(props: ProjectWalletControlsProps) {
394415
/>
395416
</DialogContent>
396417
</Dialog>
418+
419+
<Dialog onOpenChange={setIsSwapOpen} open={isSwapOpen}>
420+
<DialogContent className="gap-0 p-0 overflow-hidden max-w-md">
421+
<SwapProjectWalletModalContent
422+
chainId={selectedChainId}
423+
tokenAddress={selectedTokenAddress}
424+
walletAddress={projectWallet.address}
425+
chain={chain}
426+
isManagedVault={isManagedVault}
427+
publishableKey={project.publishableKey}
428+
secretKey={swapSecretKey}
429+
setSecretKey={setSwapSecretKey}
430+
vaultAccessToken={swapVaultAccessToken}
431+
setVaultAccessToken={setSwapVaultAccessToken}
432+
onClose={() => setIsSwapOpen(false)}
433+
/>
434+
</DialogContent>
435+
</Dialog>
397436
</div>
398437
);
399438
}
@@ -573,6 +612,189 @@ function ChangeProjectWalletDialogContent(props: {
573612
);
574613
}
575614

615+
type SwapProjectWalletModalContentProps = {
616+
chainId: number;
617+
tokenAddress: string | undefined;
618+
walletAddress: string;
619+
chain: Chain;
620+
isManagedVault: boolean;
621+
publishableKey: string;
622+
secretKey: string;
623+
setSecretKey: (value: string) => void;
624+
vaultAccessToken: string;
625+
setVaultAccessToken: (value: string) => void;
626+
onClose: () => void;
627+
};
628+
629+
function SwapProjectWalletModalContent(
630+
props: SwapProjectWalletModalContentProps,
631+
) {
632+
const {
633+
chainId,
634+
tokenAddress,
635+
walletAddress,
636+
chain,
637+
isManagedVault,
638+
publishableKey,
639+
secretKey,
640+
setSecretKey,
641+
vaultAccessToken,
642+
setVaultAccessToken,
643+
onClose,
644+
} = props;
645+
646+
const [screen, setScreen] = useState<"credentials" | "swap">("credentials");
647+
const { theme } = useTheme();
648+
const t = theme === "light" ? "light" : "dark";
649+
650+
const hasRequiredCredentials = isManagedVault
651+
? secretKey.trim().length > 0
652+
: secretKey.trim().length > 0 && vaultAccessToken.trim().length > 0;
653+
654+
const swapClient = useMemo(() => {
655+
if (!secretKey.trim()) {
656+
return null;
657+
}
658+
return createThirdwebClient({
659+
clientId: publishableKey,
660+
secretKey: secretKey.trim(),
661+
});
662+
}, [secretKey, publishableKey]);
663+
664+
const activeWallet = useMemo(() => {
665+
if (!swapClient) {
666+
return undefined;
667+
}
668+
const vaultAccessTokenValue = vaultAccessToken.trim();
669+
return createWalletAdapter({
670+
adaptedAccount: Engine.serverWallet({
671+
client: swapClient,
672+
address: walletAddress,
673+
...(vaultAccessTokenValue
674+
? { vaultAccessToken: vaultAccessTokenValue }
675+
: {}),
676+
}),
677+
chain: chain,
678+
client: swapClient,
679+
onDisconnect: () => {},
680+
switchChain: () => {},
681+
});
682+
}, [swapClient, walletAddress, chain, vaultAccessToken]);
683+
684+
// Screen 1: Credentials
685+
if (screen === "credentials") {
686+
return (
687+
<div>
688+
<DialogHeader className="p-4 lg:p-6">
689+
<DialogTitle>Swap Tokens</DialogTitle>
690+
<DialogDescription>
691+
Enter your credentials to swap tokens from your project wallet
692+
</DialogDescription>
693+
</DialogHeader>
694+
695+
<div className="px-4 pb-4 lg:px-6 lg:pb-6 space-y-4">
696+
<div className="space-y-2">
697+
<label
698+
htmlFor="swap-secret-key"
699+
className="text-sm font-medium leading-none"
700+
>
701+
Project secret key
702+
</label>
703+
<Input
704+
id="swap-secret-key"
705+
type="password"
706+
placeholder="Enter your project secret key"
707+
value={secretKey}
708+
onChange={(e) => setSecretKey(e.target.value)}
709+
autoComplete="off"
710+
autoCorrect="off"
711+
spellCheck={false}
712+
/>
713+
<p className="text-xs text-muted-foreground">{secretKeyHelper}</p>
714+
</div>
715+
716+
{!isManagedVault && (
717+
<div className="space-y-2">
718+
<label
719+
htmlFor="swap-vault-access-token"
720+
className="text-sm font-medium leading-none"
721+
>
722+
Vault access token
723+
</label>
724+
<Input
725+
id="swap-vault-access-token"
726+
type="password"
727+
placeholder="Enter a vault access token"
728+
value={vaultAccessToken}
729+
onChange={(e) => setVaultAccessToken(e.target.value)}
730+
autoComplete="off"
731+
autoCorrect="off"
732+
spellCheck={false}
733+
/>
734+
<p className="text-xs text-muted-foreground">
735+
{vaultAccessTokenHelper}
736+
</p>
737+
</div>
738+
)}
739+
</div>
740+
741+
<div className="flex justify-end gap-3 border-t bg-card p-4">
742+
<Button onClick={onClose} type="button" variant="outline">
743+
Cancel
744+
</Button>
745+
<Button
746+
onClick={() => setScreen("swap")}
747+
type="button"
748+
disabled={!hasRequiredCredentials}
749+
>
750+
Continue
751+
</Button>
752+
</div>
753+
</div>
754+
);
755+
}
756+
757+
// Screen 2: Swap Widget
758+
return (
759+
<div className="w-full">
760+
<DialogHeader className="p-4 lg:p-6">
761+
<div className="flex items-center gap-2">
762+
<Button
763+
variant="ghost"
764+
size="sm"
765+
className="p-1 h-auto w-auto"
766+
onClick={() => setScreen("credentials")}
767+
>
768+
<ArrowLeftIcon className="size-4" />
769+
</Button>
770+
<div>
771+
<DialogTitle>Swap Tokens</DialogTitle>
772+
<DialogDescription>
773+
Swap tokens from your project wallet
774+
</DialogDescription>
775+
</div>
776+
</div>
777+
</DialogHeader>
778+
779+
<div className="px-4 pb-4 lg:px-6 lg:pb-6 flex justify-center">
780+
{swapClient && activeWallet && (
781+
<SwapWidget
782+
client={swapClient}
783+
prefill={{
784+
sellToken: {
785+
chainId: chainId,
786+
tokenAddress: tokenAddress,
787+
},
788+
}}
789+
activeWallet={activeWallet}
790+
theme={getSDKTheme(t)}
791+
/>
792+
)}
793+
</div>
794+
</div>
795+
);
796+
}
797+
576798
const createSendFormSchema = (secretKeyLabel: string) =>
577799
z.object({
578800
chainId: z.number({

packages/thirdweb/src/react/web/ui/Bridge/swap-widget/SwapWidget.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { ThirdwebClient } from "../../../../../client/client.js";
88
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
99
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
1010
import { getAddress } from "../../../../../utils/address.js";
11+
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
1112
import { CustomThemeProvider } from "../../../../core/design-system/CustomThemeProvider.js";
1213
import type { Theme } from "../../../../core/design-system/index.js";
1314
import type { BridgePrepareRequest } from "../../../../core/hooks/useBridgePrepare.js";
@@ -169,6 +170,10 @@ export type SwapWidgetProps = {
169170
* Called when the user disconnects the active wallet
170171
*/
171172
onDisconnect?: () => void;
173+
/**
174+
* The wallet that should be pre-selected in the SwapWidget UI.
175+
*/
176+
activeWallet?: Wallet;
172177
};
173178

174179
/**
@@ -320,7 +325,7 @@ function SwapWidgetContent(
320325
},
321326
) {
322327
const [screen, setScreen] = useState<SwapWidgetScreen>({ id: "1:swap-ui" });
323-
const activeWalletInfo = useActiveWalletInfo();
328+
const activeWalletInfo = useActiveWalletInfo(props.activeWallet);
324329
const isPersistEnabled = props.persistTokenSelections !== false;
325330

326331
const [amountSelection, setAmountSelection] = useState<{
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { useMemo } from "react";
2+
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
23
import { useActiveAccount } from "../../../../core/hooks/wallets/useActiveAccount.js";
34
import { useActiveWallet } from "../../../../core/hooks/wallets/useActiveWallet.js";
45
import { useActiveWalletChain } from "../../../../core/hooks/wallets/useActiveWalletChain.js";
56
import type { ActiveWalletInfo } from "./types.js";
67

7-
export function useActiveWalletInfo(): ActiveWalletInfo | undefined {
8+
export function useActiveWalletInfo(
9+
activeWalletOverride?: Wallet,
10+
): ActiveWalletInfo | undefined {
811
const activeAccount = useActiveAccount();
912
const activeWallet = useActiveWallet();
1013
const activeChain = useActiveWalletChain();
1114

1215
return useMemo(() => {
13-
return activeAccount && activeWallet && activeChain
16+
const wallet = activeWalletOverride || activeWallet;
17+
const chain = activeWalletOverride?.getChain() || activeChain;
18+
const account = activeWalletOverride?.getAccount() || activeAccount;
19+
return wallet && chain && account
1420
? {
15-
activeChain,
16-
activeWallet,
17-
activeAccount,
21+
activeChain: chain,
22+
activeWallet: wallet,
23+
activeAccount: account,
1824
}
1925
: undefined;
20-
}, [activeAccount, activeWallet, activeChain]);
26+
}, [activeAccount, activeWallet, activeChain, activeWalletOverride]);
2127
}

0 commit comments

Comments
 (0)