From 42cd8420e985f4f0d179935e8dcf590d40c0ce67 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 29 May 2026 16:12:40 -0500 Subject: [PATCH 1/3] refactor(wallet): remove dead self-custody branches from deploy-web consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the dual-mode wallet primitives that L-1 intentionally left in place: the `selectedWalletType` atom, the `walletUtils` custodial helpers, the `LocalWallet` union, `isCustodial`/`switchWalletType` on the `useWallet()` contract, and every consumer that still branched on them. Also removes the cosmos-kit reads (`useSelectedChain`, `useManager`, `CURRENT_WALLET_KEY`) and custodial analytics branch inside `WalletProvider` itself, plus `useEnforceSelfCustodyFlag` whose input type died with the atom. Legacy `{networkId}/wallets` storage entries are deliberately left as inert data — a separate project sweeps localStorage. Unblocks L-3 (cosmos-kit removal) and L-4 (self_custody flag removal). Also sets `root: true` in the monorepo's ESLint config so working in git worktrees nested under the repo doesn't trip the "plugin loaded twice" error. Fixes CON-408 --- .eslintrc.js | 1 + .../DeploymentDetailTopBar.spec.tsx | 20 +- .../DeploymentDetailTopBar.tsx | 40 +- .../deployments/DeploymentSubHeader.tsx | 49 +- .../ManifestUpdate/ManifestUpdate.spec.tsx | 4 +- .../ManifestUpdate/ManifestUpdate.tsx | 22 +- .../home/YourAccount/YourAccount.spec.tsx | 9 +- .../components/layout/AccountMenu.spec.tsx | 10 +- .../src/components/layout/AccountMenu.tsx | 6 +- .../src/components/layout/Sidebar.tsx | 8 +- .../components/layout/WalletStatus.spec.tsx | 15 +- .../src/components/layout/WalletStatus.tsx | 18 +- .../CreateLease/CreateLease.spec.tsx | 17 +- .../ManifestEdit/ManifestEdit.spec.tsx | 40 +- .../ManifestEdit/ManifestEdit.tsx | 38 +- .../new-deployment/SdlBuilder.spec.tsx | 3 +- .../components/new-deployment/SdlBuilder.tsx | 16 +- .../OnboardingContainer.tsx | 4 +- .../sdl/SimpleServiceFormControl.tsx | 9 - .../src/components/sdl/TokenFormControl.tsx | 64 - .../components/shared/PrerequisiteList.tsx | 105 -- .../shared/PriceEstimateTooltip.tsx | 8 +- .../context/WalletProvider/WalletProvider.tsx | 146 +-- .../deriveWalletIsLoading.spec.ts | 31 +- .../WalletProvider/deriveWalletIsLoading.ts | 7 +- .../useEnforceSelfCustodyFlag.spec.ts | 79 -- .../useEnforceSelfCustodyFlag.ts | 40 - apps/deploy-web/src/hooks/useManagedWallet.ts | 20 +- .../useProviderJwt/useProviderJwt.spec.tsx | 155 +-- .../hooks/useProviderJwt/useProviderJwt.ts | 72 +- .../useSelectedChain/useSelectedChain.ts | 7 - .../queries/deploymentSettingsQuery.spec.ts | 50 +- .../src/queries/deploymentSettingsQuery.ts | 15 +- .../src/queries/useApiKeysQuery.spec.tsx | 68 +- .../deploy-web/src/queries/useApiKeysQuery.ts | 7 +- apps/deploy-web/src/store/walletStore.ts | 4 - apps/deploy-web/src/utils/walletUtils.spec.ts | 1095 ++--------------- apps/deploy-web/src/utils/walletUtils.ts | 230 +--- apps/deploy-web/tests/seeders/localWallet.ts | 13 +- apps/deploy-web/tests/seeders/wallet.ts | 4 +- 40 files changed, 426 insertions(+), 2123 deletions(-) delete mode 100644 apps/deploy-web/src/components/sdl/TokenFormControl.tsx delete mode 100644 apps/deploy-web/src/components/shared/PrerequisiteList.tsx delete mode 100644 apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.spec.ts delete mode 100644 apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.ts delete mode 100644 apps/deploy-web/src/hooks/useSelectedChain/useSelectedChain.ts diff --git a/.eslintrc.js b/.eslintrc.js index 34dc472f21..95d7bea806 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,7 @@ const nextConfig = require("@akashnetwork/dev-config/.eslintrc.next"); module.exports = { ...baseConfig, + root: true, settings: { next: { rootDir: "apps/*" diff --git a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx index 16a50c237e..0477d9fde1 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx @@ -109,24 +109,14 @@ describe(DeploymentDetailTopBar.name, () => { expect(screen.queryByText("Redeploy")).not.toBeInTheDocument(); }); - it("renders auto top-up section when wallet is managed", () => { + it("renders auto top-up section", () => { setup({ - deployment: createDeployment({ state: "active" }), - wallet: { isManaged: true } + deployment: createDeployment({ state: "active" }) }); expect(screen.getByText("Auto top-up")).toBeInTheDocument(); }); - it("does not render auto top-up section when wallet is not managed", () => { - setup({ - deployment: createDeployment({ state: "active" }), - wallet: { isManaged: false } - }); - - expect(screen.queryByText("Auto top-up")).not.toBeInTheDocument(); - }); - it("renders DeploymentDepositModal after Add funds click", () => { const deps = setup({ deployment: createDeployment({ state: "active" }) @@ -244,18 +234,18 @@ describe(DeploymentDetailTopBar.name, () => { })) as typeof DEPENDENCIES.useLocalNotes, useWallet: vi.fn(() => ({ signAndBroadcastTx: input?.wallet?.signAndBroadcastTx ?? vi.fn(() => Promise.resolve(true)), - isManaged: input?.wallet?.isManaged ?? false, - denom: input?.wallet?.denom ?? "uakt", + isManaged: true, + denom: input?.wallet?.denom ?? "uact", address: "akash1test", walletName: "test", isWalletConnected: true, isWalletLoaded: true, connectManagedWallet: vi.fn(), logout: vi.fn(), - isCustodial: false, isWalletLoading: false, isTrialing: false, isOnboarding: false, + topUpMinAmountUsd: 20, hasManagedWallet: false })) as typeof DEPENDENCIES.useWallet, usePricing: vi.fn(() => ({ diff --git a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.tsx b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.tsx index 827aaec20a..912fdc085e 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.tsx @@ -244,28 +244,26 @@ export const DeploymentDetailTopBar: React.FunctionComponent = ({ Add funds - {wallet.isManaged && ( -
- - Auto top-up - -
-
Estimated amount: ${udenomToUsd(deploymentSetting.data?.estimatedTopUpAmount || 0, wallet.denom)}
-
Check period: {formatDuration(intervalToDuration({ start: 0, end: deploymentSetting.data?.topUpFrequencyMs || 0 }))}
-
-
- Auto top-up will only occur if there are insufficient funds to maintain the deployment until the next scheduled check. -
+
+ + Auto top-up + +
+
Estimated amount: ${udenomToUsd(deploymentSetting.data?.estimatedTopUpAmount || 0, wallet.denom)}
+
Check period: {formatDuration(intervalToDuration({ start: 0, end: deploymentSetting.data?.topUpFrequencyMs || 0 }))}
- } - > - -
- {deploymentSetting.isLoading && } -
- )} +
+ Auto top-up will only occur if there are insufficient funds to maintain the deployment until the next scheduled check. +
+
+ } + > + + + {deploymentSetting.isLoading && } + )} diff --git a/apps/deploy-web/src/components/deployments/DeploymentSubHeader.tsx b/apps/deploy-web/src/components/deployments/DeploymentSubHeader.tsx index 6e59bb8e1c..801aaa873c 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentSubHeader.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentSubHeader.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import { CustomTooltip } from "@akashnetwork/ui/components"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; import isValid from "date-fns/isValid"; -import { InfoCircle, WarningCircle } from "iconoir-react"; +import { WarningCircle } from "iconoir-react"; import { CopyTextToClipboardButton } from "@src/components/shared/CopyTextToClipboardButton"; import { LabelValue } from "@src/components/shared/LabelValue"; @@ -15,10 +15,8 @@ import { useServices } from "@src/context/ServicesProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useDeploymentMetrics } from "@src/hooks/useDeploymentMetrics"; import { useTrialDeploymentTimeRemaining } from "@src/hooks/useTrialDeploymentTimeRemaining"; -import { useDenomData } from "@src/hooks/useWalletBalance"; import type { DeploymentDto, LeaseDto } from "@src/types/deployment"; import { udenomToDenom } from "@src/utils/mathHelpers"; -import { getAvgCostPerMonth } from "@src/utils/priceUtils"; type Props = { deployment: DeploymentDto; @@ -28,13 +26,11 @@ type Props = { export const DeploymentSubHeader: React.FunctionComponent = ({ deployment, leases }) => { const { deploymentCost, realTimeLeft } = useDeploymentMetrics({ deployment, leases }); - const avgCost = udenomToDenom(getAvgCostPerMonth(deploymentCost)); const isActive = deployment.state === "active"; const hasLeases = !!leases && leases.length > 0; const hasActiveLeases = hasLeases && leases.some(l => l.state === "active"); const hasGpu = leases?.some(l => l.state === "active" && l.gpuAmount && l.gpuAmount > 0); - const denomData = useDenomData(deployment.escrowAccount.state.funds[0]?.denom || ""); - const { isCustodial, isTrialing } = useWallet(); + const { isTrialing } = useWallet(); const { publicConfig: appConfig } = useServices(); const trialDuration = appConfig.NEXT_PUBLIC_TRIAL_DEPLOYMENTS_DURATION_HOURS; @@ -56,22 +52,6 @@ export const DeploymentSubHeader: React.FunctionComponent = ({ deployment denom={deployment.escrowAccount.state.funds[0]?.denom || ""} value={udenomToDenom(isActive && hasActiveLeases && realTimeLeft ? realTimeLeft?.escrow : deployment.escrowBalance, 6)} /> - {isCustodial && ( - - - {udenomToDenom(isActive && hasActiveLeases && realTimeLeft ? realTimeLeft?.escrow : deployment.escrowBalance, 6)}  - {denomData?.label} - -
- The escrow account balance will be fully returned to your wallet balance when the deployment is closed.{" "} - - } - > - -
- )} {isActive && hasActiveLeases && !!realTimeLeft && realTimeLeft.escrow <= 0 && ( @@ -92,18 +72,6 @@ export const DeploymentSubHeader: React.FunctionComponent = ({ deployment perBlockValue={udenomToDenom(deploymentCost, 10)} showAsHourly={hasGpu} /> - - {isCustodial && ( - - {avgCost} {denomData?.label} / month - - } - > - - - )} ) } @@ -117,19 +85,6 @@ export const DeploymentSubHeader: React.FunctionComponent = ({ deployment denom={deployment.escrowAccount.state.funds[0]?.denom || ""} value={udenomToDenom(isActive && hasActiveLeases && realTimeLeft ? realTimeLeft?.amountSpent : parseFloat(deployment.transferred.amount), 6)} /> - - {isCustodial && ( - - {udenomToDenom(isActive && hasActiveLeases && realTimeLeft ? realTimeLeft?.amountSpent : parseFloat(deployment.transferred.amount), 6)}{" "} - {denomData?.label} - - } - > - - - )} } /> diff --git a/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.spec.tsx b/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.spec.tsx index b8acd8f49c..f443baa8f3 100644 --- a/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.spec.tsx +++ b/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.spec.tsx @@ -326,16 +326,16 @@ describe(ManifestUpdate.name, () => { ({ address: input?.wallet?.address || "akash1test", signAndBroadcastTx: input?.wallet?.signAndBroadcastTx || vi.fn(), - isManaged: input?.wallet?.isManaged ?? false, + isManaged: true, walletName: "", isWalletConnected: true, isWalletLoaded: true, connectManagedWallet: vi.fn(), logout: vi.fn(), - isCustodial: false, isWalletLoading: false, isTrialing: false, isOnboarding: false, + topUpMinAmountUsd: 20, hasManagedWallet: false, denom: "uact" }) as ReturnType; diff --git a/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.tsx b/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.tsx index 92d648726e..ccf28a3e54 100644 --- a/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.tsx +++ b/apps/deploy-web/src/components/deployments/ManifestUpdate/ManifestUpdate.tsx @@ -70,10 +70,10 @@ export const ManifestUpdate: React.FunctionComponent = ({ const [deploymentVersion, setDeploymentVersion] = useState(null); const [showOutsideDeploymentMessage, setShowOutsideDeploymentMessage] = useState(false); const [isSendingManifest, setIsSendingManifest] = useState(false); - const { address, signAndBroadcastTx, isManaged: isManagedWallet } = d.useWallet(); + const { address, signAndBroadcastTx } = d.useWallet(); const { data: providers } = d.useProviderList(); const providerCredentials = d.useProviderCredentials(); - const { enqueueSnackbar, closeSnackbar } = d.useSnackbar(); + const { enqueueSnackbar } = d.useSnackbar(); const { settings } = d.useSettings(); useEffect(() => { @@ -126,7 +126,7 @@ export const ManifestUpdate: React.FunctionComponent = ({ } async function handleUpdateClick() { - let response, sendManifestKey; + let response; try { const doc = yaml.load(editedManifest); @@ -150,13 +150,6 @@ export const ManifestUpdate: React.FunctionComponent = ({ manifestVersion: dd.hash }); - sendManifestKey = - !isManagedWallet && - enqueueSnackbar(, { - variant: "info", - autoHideDuration: null - }); - const leaseProviders = leases.map(lease => lease.provider).filter((v, i, s) => s.indexOf(v) === i); for (const provider of leaseProviders) { @@ -170,11 +163,6 @@ export const ManifestUpdate: React.FunctionComponent = ({ }); setIsSendingManifest(false); - - if (sendManifestKey) { - closeSnackbar(sendManifestKey); - } - closeManifestEditor(); } } catch (error: any) { @@ -185,10 +173,6 @@ export const ManifestUpdate: React.FunctionComponent = ({ console.error(error); } setIsSendingManifest(false); - - if (sendManifestKey) { - closeSnackbar(sendManifestKey); - } } } diff --git a/apps/deploy-web/src/components/home/YourAccount/YourAccount.spec.tsx b/apps/deploy-web/src/components/home/YourAccount/YourAccount.spec.tsx index b408be05d8..7144bd5953 100644 --- a/apps/deploy-web/src/components/home/YourAccount/YourAccount.spec.tsx +++ b/apps/deploy-web/src/components/home/YourAccount/YourAccount.spec.tsx @@ -34,7 +34,7 @@ describe(YourAccount.name, () => { expect(AccountHeaderMock).toHaveBeenCalledWith( expect.objectContaining({ - isManagedWallet: false, + isManagedWallet: true, isBlockchainDown: false }), expect.anything() @@ -71,7 +71,7 @@ describe(YourAccount.name, () => { expect.objectContaining({ walletBalance, activeDeploymentsCount: 0, - isManagedWallet: false + isManagedWallet: true }), expect.anything() ); @@ -304,11 +304,12 @@ describe(YourAccount.name, () => { connectManagedWallet: vi.fn(), logout: vi.fn(), signAndBroadcastTx: vi.fn(), - isManaged: input.wallet?.isManaged ?? false, - isCustodial: false, + isManaged: true, + denom: "uact", isWalletLoading: false, isTrialing: false, isOnboarding: false, + topUpMinAmountUsd: 20, hasManagedWallet: false }) as ReturnType; diff --git a/apps/deploy-web/src/components/layout/AccountMenu.spec.tsx b/apps/deploy-web/src/components/layout/AccountMenu.spec.tsx index 21ffa3c962..1e4f3ccdb2 100644 --- a/apps/deploy-web/src/components/layout/AccountMenu.spec.tsx +++ b/apps/deploy-web/src/components/layout/AccountMenu.spec.tsx @@ -7,7 +7,6 @@ import { AccountMenu } from "./AccountMenu"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { buildWallet } from "@tests/seeders/wallet"; import { TestContainerProvider } from "@tests/unit/TestContainerProvider"; describe(AccountMenu.name, () => { @@ -57,11 +56,10 @@ describe(AccountMenu.name, () => { expect(authService.logout).toHaveBeenCalledTimes(1); }); - it("shows Billing & Usage when flag, userId, and managed wallet are all present", async () => { + it("shows Billing & Usage when flag and userId are both present", async () => { setup({ username: "carol", userId: "user-1", - isManagedWallet: true, isBillingUsageEnabled: true }); @@ -74,7 +72,6 @@ describe(AccountMenu.name, () => { setup({ username: "dan", userId: "user-1", - isManagedWallet: true, isBillingUsageEnabled: false }); @@ -84,7 +81,7 @@ describe(AccountMenu.name, () => { expect(screen.queryByText("Billing & Usage")).not.toBeInTheDocument(); }); - function setup(input: { isLoading?: boolean; username?: string; userId?: string; isManagedWallet?: boolean; isBillingUsageEnabled?: boolean }) { + function setup(input: { isLoading?: boolean; username?: string; userId?: string; isBillingUsageEnabled?: boolean }) { const push = vi.fn(); const authService = mock(); @@ -95,8 +92,7 @@ describe(AccountMenu.name, () => { isLoading: input.isLoading ?? false }), useRouter: () => mock>({ push }), - useFlag: flagName => (flagName === "billing_usage" && input.isBillingUsageEnabled) ?? false, - useWallet: () => buildWallet({ isManaged: input.isManagedWallet ?? false }) + useFlag: flagName => (flagName === "billing_usage" && input.isBillingUsageEnabled) ?? false }; render( diff --git a/apps/deploy-web/src/components/layout/AccountMenu.tsx b/apps/deploy-web/src/components/layout/AccountMenu.tsx index 1550d11b70..ad65e99d80 100644 --- a/apps/deploy-web/src/components/layout/AccountMenu.tsx +++ b/apps/deploy-web/src/components/layout/AccountMenu.tsx @@ -14,12 +14,11 @@ import { GraphUp, Key, LogOut, MultiplePages, Settings, Star, User } from "icono import { useRouter } from "next/navigation"; import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; import { useCustomUser } from "@src/hooks/useCustomUser"; import { useFlag } from "@src/hooks/useFlag"; import { CustomDropdownLinkItem } from "../shared/CustomDropdownLinkItem"; -export const DEPENDENCIES = { useCustomUser, useRouter, useFlag, useWallet }; +export const DEPENDENCIES = { useCustomUser, useRouter, useFlag }; interface Props { dependencies?: typeof DEPENDENCIES; @@ -30,7 +29,6 @@ export function AccountMenu({ dependencies: d = DEPENDENCIES }: Props = {}) { const username = user?.username; const router = d.useRouter(); const isBillingUsageEnabled = d.useFlag("billing_usage"); - const wallet = d.useWallet(); const { authService, urlService } = useServices(); return ( @@ -81,7 +79,7 @@ export function AccountMenu({ dependencies: d = DEPENDENCIES }: Props = {}) { router.push(urlService.userFavorites())} icon={}> Favorites - {isBillingUsageEnabled && user?.userId && wallet.isManaged && ( + {isBillingUsageEnabled && user?.userId && ( router.push(urlService.billing())} icon={}> Billing & Usage diff --git a/apps/deploy-web/src/components/layout/Sidebar.tsx b/apps/deploy-web/src/components/layout/Sidebar.tsx index 5c4ccf5013..e61ae51439 100644 --- a/apps/deploy-web/src/components/layout/Sidebar.tsx +++ b/apps/deploy-web/src/components/layout/Sidebar.tsx @@ -36,7 +36,6 @@ import Image from "next/image"; import Link from "next/link"; import { useSettings } from "@src/context/SettingsProvider"; -import { useWallet } from "@src/context/WalletProvider"; import { useFlag } from "@src/hooks/useFlag"; import { useUser } from "@src/hooks/useUser"; import sdlStore from "@src/store/sdlStore"; @@ -63,7 +62,6 @@ export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDr const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const muiTheme = useMuiTheme(); const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); - const wallet = useWallet(); const { user } = useUser(); const isAlertsEnabled = useFlag("alerts"); @@ -102,7 +100,7 @@ export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDr } ]; - if (isAlertsEnabled && user?.userId && wallet.isManaged) { + if (isAlertsEnabled && user?.userId) { routes.push({ title: "Alerts", icon: props => , @@ -112,7 +110,7 @@ export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDr } return routes; - }, [isAlertsEnabled, user?.userId, wallet.isManaged]); + }, [isAlertsEnabled, user?.userId]); const routeGroups: ISidebarGroupMenu[] = useMemo( () => [ @@ -270,7 +268,7 @@ export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDr ]; return routes; - }, [wallet]); + }, []); const onToggleMenuClick = () => { onOpenMenuClick(); diff --git a/apps/deploy-web/src/components/layout/WalletStatus.spec.tsx b/apps/deploy-web/src/components/layout/WalletStatus.spec.tsx index ded55128d4..65a825edff 100644 --- a/apps/deploy-web/src/components/layout/WalletStatus.spec.tsx +++ b/apps/deploy-web/src/components/layout/WalletStatus.spec.tsx @@ -25,27 +25,26 @@ describe(WalletStatus.name, () => { expect(screen.queryByLabelText("Connected wallet name and balance")).not.toBeInTheDocument(); }); - it("renders a custodial wallet name and balance", () => { + it("renders the managed deployment-grants balance", () => { setup({ walletName: "alice-wallet", - isManaged: false, - balance: { totalUsd: 12.34, totalDeploymentGrantsUSD: 0 } + isTrialing: true, + balance: { totalUsd: 0, totalDeploymentGrantsUSD: 12.34 } }); const container = screen.getByLabelText("Connected wallet name and balance"); - expect(container).toHaveTextContent("alice-wallet"); expect(container.parentElement).toHaveTextContent("$12.34"); }); - it("renders a Trial label when the wallet is managed and trialing", () => { - setup({ isManaged: true, isTrialing: true }); + it("renders a Trial label when the wallet is trialing", () => { + setup({ isTrialing: true }); expect(screen.getByText("Trial")).toBeInTheDocument(); }); it("opens the dropdown with ManagedWalletPopup on click", async () => { const ManagedWalletPopup = vi.fn(ComponentMock); - setup({ isManaged: true, dependencies: { ManagedWalletPopup } }); + setup({ dependencies: { ManagedWalletPopup } }); await userEvent.click(screen.getByLabelText("Connected wallet name and balance")); @@ -56,7 +55,6 @@ describe(WalletStatus.name, () => { walletName?: string; isWalletLoaded?: boolean; isWalletConnected?: boolean; - isManaged?: boolean; isTrialing?: boolean; isWalletLoading?: boolean; balance?: { totalUsd: number; totalDeploymentGrantsUSD: number }; @@ -67,7 +65,6 @@ describe(WalletStatus.name, () => { walletName: input.walletName ?? "test-wallet", isWalletLoaded: input.isWalletLoaded ?? true, isWalletConnected: input.isWalletConnected ?? true, - isManaged: input.isManaged ?? false, isTrialing: input.isTrialing ?? false, isWalletLoading: input.isWalletLoading ?? false }); diff --git a/apps/deploy-web/src/components/layout/WalletStatus.tsx b/apps/deploy-web/src/components/layout/WalletStatus.tsx index 6a36023cc5..4aa9d82824 100644 --- a/apps/deploy-web/src/components/layout/WalletStatus.tsx +++ b/apps/deploy-web/src/components/layout/WalletStatus.tsx @@ -5,7 +5,6 @@ import { cn } from "@akashnetwork/ui/utils"; import { NavArrowDown, Wallet } from "iconoir-react"; import { useWallet } from "@src/context/WalletProvider"; -import { getSplitText } from "@src/hooks/useShortText"; import { useWalletBalance } from "@src/hooks/useWalletBalance"; import { ManagedWalletPopup } from "../wallet/ManagedWalletPopup/ManagedWalletPopup"; import { WalletConnectionButtons } from "../wallet/WalletConnectionButtons"; @@ -23,7 +22,7 @@ interface Props { } export function WalletStatus({ dependencies: d = DEPENDENCIES }: Props = {}) { - const { walletName, isWalletLoaded, isWalletConnected, isManaged, isWalletLoading, isTrialing } = d.useWallet(); + const { isWalletLoaded, isWalletConnected, isWalletLoading, isTrialing } = d.useWallet(); const { balance: walletBalance, isLoading: isWalletBalanceLoading } = d.useWalletBalance(); const isLoadingBalance = isWalletBalanceLoading && !walletBalance; const isInit = isWalletLoaded && !isWalletLoading && !isLoadingBalance; @@ -43,24 +42,15 @@ export function WalletStatus({ dependencies: d = DEPENDENCIES }: Props = {}) { >
- {isManaged && isTrialing && Trial} - {!isManaged && ( - <> - {walletName?.length > 20 ? ( - {getSplitText(walletName, 4, 4)} - ) : ( - {walletName} - )} - - )} + {isTrialing && Trial}
- {walletBalance && ((isManaged && isTrialing) || !isManaged) &&
|
} + {walletBalance && isTrialing &&
|
}
{walletBalance && ( { await vi.waitFor(() => expect(BidGroup).toHaveBeenCalled()); act(() => { - updateStorageWallets([ - { - address: walletAddress, - isManaged: false, - name: "test", - selected: true - } - ]); + updateStorageManagedWallet({ + address: walletAddress, + userId: "user-123", + creditAmount: 100, + isTrialing: false, + selected: true + }); const bidGroupProps = BidGroup.mock.calls[0][0]; bidGroupProps.handleBidSelected(mapToBidDto(bids[0])); }); diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.spec.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.spec.tsx index 62df549ced..d7b83b7eec 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.spec.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.spec.tsx @@ -108,41 +108,7 @@ describe(ManifestEdit.name, () => { expect(SDLEditor).not.toHaveBeenCalled(); }); - it("shows PrerequisiteList when create deployment is clicked for non-managed wallet", async () => { - const PrerequisiteList = vi.fn(ComponentMock); - const SDLEditor = vi.fn(ComponentMock); - - setup({ - editedManifest: "some-manifest", - isManaged: false, - PrerequisiteList, - SDLEditor, - hasComponents: ["yml-editor"], - selectedSdlEditMode: "yaml" - }); - - act(() => { - triggerSdlValidation(SDLEditor, true); - }); - - await vi.waitFor(() => { - expect(screen.getByRole("button", { name: /Create Deployment/i })).not.toBeDisabled(); - }); - - await userEvent.click(screen.getByRole("button", { name: /Create Deployment/i })); - - await vi.waitFor(() => { - expect(PrerequisiteList).toHaveBeenCalledWith( - expect.objectContaining({ - onClose: expect.any(Function), - onContinue: expect.any(Function) - }), - expect.anything() - ); - }); - }); - - it("shows DeploymentDepositModal when create deployment is clicked for managed wallet", async () => { + it("shows DeploymentDepositModal when create deployment is clicked", async () => { const DeploymentDepositModal = vi.fn(ComponentMock); const SDLEditor = vi.fn(ComponentMock); @@ -264,7 +230,6 @@ describe(ManifestEdit.name, () => { templateId?: string | null; SDLEditor?: Mock; SdlBuilder?: Mock | ForwardRefExoticComponent; - PrerequisiteList?: Mock; DeploymentDepositModal?: Mock; analyticsService?: AppDIContainer["analyticsService"]; selectedSdlEditMode?: "yaml" | "builder"; @@ -292,7 +257,6 @@ describe(ManifestEdit.name, () => { TrialDeploymentBadge: ComponentMock, CustomNextSeo: ComponentMock, LinkTo: ComponentMock, - PrerequisiteList: input?.PrerequisiteList ?? ComponentMock, ViewPanel: ComponentMock, useSettings: () => ({ settings: { @@ -315,7 +279,7 @@ describe(ManifestEdit.name, () => { walletName: "test", isWalletConnected: true, isWalletLoaded: true, - isManaged: input?.isManaged ?? false, + isManaged: true, isTrialing: false, denom: input?.walletDenom, signAndBroadcastTx: vi.fn().mockResolvedValue({}) diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx index a488f66d61..4166a27009 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx @@ -37,7 +37,6 @@ import { SDLEditor } from "../../sdl/SDLEditor/SDLEditor"; import { TrialDeploymentBadge } from "../../shared"; import { CustomNextSeo } from "../../shared/CustomNextSeo"; import { LinkTo } from "../../shared/LinkTo"; -import { PrerequisiteList } from "../../shared/PrerequisiteList"; import { ViewPanel } from "../../shared/ViewPanel"; import type { SdlBuilderRefType } from "../SdlBuilder"; import { SdlBuilder } from "../SdlBuilder"; @@ -68,7 +67,6 @@ export const DEPENDENCIES = { TrialDeploymentBadge, CustomNextSeo, LinkTo, - PrerequisiteList, ViewPanel, useServices, useSettings, @@ -96,7 +94,6 @@ export const ManifestEdit: React.FunctionComponent = ({ const [deploymentName, setDeploymentName] = useState(""); const [isCreatingDeployment, setIsCreatingDeployment] = useState(false); const [isDepositingDeployment, setIsDepositingDeployment] = useState(false); - const [isCheckingPrerequisites, setIsCheckingPrerequisites] = useState(false); const [selectedSdlEditMode, setSelectedSdlEditMode] = useAtom(sdlStore.selectedSdlEditMode); const [isRepoInputValid, setIsRepoInputValid] = useState(false); const sdlDenom = useMemo(() => { @@ -112,7 +109,7 @@ export const ManifestEdit: React.FunctionComponent = ({ const { analyticsService, publicConfig: appConfig, deploymentLocalStorage } = d.useServices(); const { settings } = d.useSettings(); - const { address, signAndBroadcastTx, isManaged, isTrialing } = d.useWallet(); + const { address, signAndBroadcastTx, isTrialing } = d.useWallet(); const router = d.useRouter(); const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const muiTheme = d.useMuiTheme(); @@ -128,13 +125,11 @@ export const ManifestEdit: React.FunctionComponent = ({ const services = d.useImportSimpleSdl(isValidSdl ? editedManifest : null); useWhen( - wallet.isManaged && !!wallet.denom && sdlDenom !== wallet.denom && editedManifest, + !!wallet.denom && sdlDenom !== wallet.denom && editedManifest, () => { - if (wallet.isManaged) { - setEditedManifest(prev => (prev ? replaceSdlDenom(prev, wallet.denom) : prev)); - } + setEditedManifest(prev => (prev ? replaceSdlDenom(prev, wallet.denom) : prev)); }, - [editedManifest, wallet.isManaged, wallet.denom, sdlDenom] + [editedManifest, wallet.denom, sdlDenom] ); useWhen(hasComponent("ssh"), () => { setSelectedSdlEditMode("builder"); @@ -217,21 +212,7 @@ export const ManifestEdit: React.FunctionComponent = ({ return; } - if (isManaged) { - setIsDepositingDeployment(true); - } else { - setIsCheckingPrerequisites(true); - } - }; - - const onPrerequisiteContinue = () => { - setIsCheckingPrerequisites(false); - - if (isManaged) { - handleCreateClick(defaultDeposit); - } else { - setIsDepositingDeployment(true); - } + setIsDepositingDeployment(true); }; const onDeploymentDeposit = async (deposit: number) => { @@ -249,12 +230,10 @@ export const ManifestEdit: React.FunctionComponent = ({ return; } - if (wallet.isManaged) { - sdl = appendAuditorRequirement(sdl); + sdl = appendAuditorRequirement(sdl); - if (wallet.denom !== "uakt") { - sdl = replaceSdlDenom(sdl, wallet.denom); - } + if (wallet.denom !== "uakt") { + sdl = replaceSdlDenom(sdl, wallet.denom); } const dd = await createAndValidateDeploymentData(sdl, null, deposit); @@ -458,7 +437,6 @@ export const ManifestEdit: React.FunctionComponent = ({ services={services} /> )} - {isCheckingPrerequisites && setIsCheckingPrerequisites(false)} onContinue={onPrerequisiteContinue} />} ); }; diff --git a/apps/deploy-web/src/components/new-deployment/SdlBuilder.spec.tsx b/apps/deploy-web/src/components/new-deployment/SdlBuilder.spec.tsx index bb5e498dc7..5712a3505e 100644 --- a/apps/deploy-web/src/components/new-deployment/SdlBuilder.spec.tsx +++ b/apps/deploy-web/src/components/new-deployment/SdlBuilder.spec.tsx @@ -118,7 +118,8 @@ describe("SdlBuilder", () => { }), useWallet: () => mock>({ - isManaged: false + isManaged: true, + denom: "uact" }), useSdlServiceManager: () => mock>({ diff --git a/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx b/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx index 7cf58e2566..a1d59a504a 100644 --- a/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx +++ b/apps/deploy-web/src/components/new-deployment/SdlBuilder.tsx @@ -70,16 +70,14 @@ export const SdlBuilder = React.forwardRef( const wallet = d.useWallet(); useEffect(() => { - if (wallet.isManaged) { - formServices.forEach((service, index) => { - const { denom } = service.placement.pricing; + formServices.forEach((service, index) => { + const { denom } = service.placement.pricing; - if (denom !== wallet.denom) { - setValue(`services.${index}.placement.pricing.denom`, wallet.denom); - } - }); - } - }, [formServices, sdlString, wallet.isManaged, wallet.denom]); + if (denom !== wallet.denom) { + setValue(`services.${index}.placement.pricing.denom`, wallet.denom); + } + }); + }, [formServices, sdlString, wallet.denom]); React.useImperativeHandle(ref, () => ({ getSdl: getSdl, diff --git a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx index eba73dc597..b42477ee8b 100644 --- a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx +++ b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx @@ -270,14 +270,14 @@ export const OnboardingContainer: React.FunctionComponent = ({ const _credentials = _services[serviceIndex]?.credentials; const _isGhcr = _credentials?.host === "ghcr.io"; const { imageList, hasComponent, toggleCmp } = useSdlBuilder(); - const wallet = useWallet(); const isLogCollectorEnabled = useFlag("ui_sdl_log_collector_enabled"); const onExpandClick = () => { setServiceCollapsed(prev => { @@ -424,12 +421,6 @@ export const SimpleServiceFormControl: React.FunctionComponent = ({
)} - {!wallet?.isManaged && ( -
- -
- )} - {isLogCollectorEnabled && (
diff --git a/apps/deploy-web/src/components/sdl/TokenFormControl.tsx b/apps/deploy-web/src/components/sdl/TokenFormControl.tsx deleted file mode 100644 index 7db67aff34..0000000000 --- a/apps/deploy-web/src/components/sdl/TokenFormControl.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; -import type { ReactElement } from "react"; -import type { Control, FieldPathValue, FieldValues, Path } from "react-hook-form"; -import { - FormField, - FormItem, - FormLabel, - FormMessage, - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue -} from "@akashnetwork/ui/components"; - -import { useSupportedDenoms } from "@src/hooks/useDenom"; -import type { ServiceType } from "@src/types"; - -interface ServicesFieldValues extends FieldValues { - services: ServiceType[]; -} - -interface Props = Path> { - name: TName; - defaultValue?: FieldPathValue; - control: Control; -} - -export const TokenFormControl = ({ control, name, defaultValue }: Props): ReactElement> => { - const supportedSdlDenoms = useSupportedDenoms(); - - return ( - { - return ( - - Token - - - - ); - }} - /> - ); -}; diff --git a/apps/deploy-web/src/components/shared/PrerequisiteList.tsx b/apps/deploy-web/src/components/shared/PrerequisiteList.tsx deleted file mode 100644 index ef61565386..0000000000 --- a/apps/deploy-web/src/components/shared/PrerequisiteList.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import React, { useEffect, useState } from "react"; -import { Avatar, AvatarFallback, Card, CardContent, Popup, Spinner } from "@akashnetwork/ui/components"; -import { CheckCircle, WarningCircle } from "iconoir-react"; - -import { useWallet } from "@src/context/WalletProvider"; -import { useChainParam } from "@src/hooks/useChainParam/useChainParam"; -import { useWalletBalance } from "@src/hooks/useWalletBalance"; -import { denomToUdenom } from "@src/utils/mathHelpers"; -import { ConnectWallet } from "./ConnectWallet"; -import { Title } from "./Title"; - -type Props = { - onClose: () => void; - onContinue: () => void; -}; - -export const PrerequisiteList: React.FunctionComponent = ({ onClose, onContinue }) => { - const [isLoadingPrerequisites, setIsLoadingPrerequisites] = useState(false); - const [isBalanceValidated, setIsBalanceValidated] = useState(null); - const { address, isManaged } = useWallet(); - const { balance: walletBalance } = useWalletBalance(); - const { minDeposit } = useChainParam(); - - useEffect(() => { - if (isManaged) { - onContinue(); - } - - if (address && (minDeposit.akt !== undefined || minDeposit.act !== undefined) && !!walletBalance) { - setIsLoadingPrerequisites(true); - - const isBalanceValidated = walletBalance.balanceUAKT >= denomToUdenom(minDeposit.akt) || walletBalance.balanceUACT >= denomToUdenom(minDeposit.act); - - setIsBalanceValidated(isBalanceValidated); - setIsLoadingPrerequisites(false); - - if (isBalanceValidated) { - onContinue(); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [address, walletBalance?.balanceUAKT, walletBalance?.balanceUUSDC, walletBalance?.balanceUACT, minDeposit.akt, minDeposit.act, isManaged]); - - return ( - - {address ? ( - - -
    -
  • - - - {isBalanceValidated === null && } - {isBalanceValidated === true && } - {isBalanceValidated === false && } - - - -
    - - Wallet Balance - -

    - {`The balance of the wallet needs to be of at least ${minDeposit.act} ACT to create a deployment.`} -

    -
    -
  • -
-
-
- ) : ( -
- -
- )} -
- ); -}; diff --git a/apps/deploy-web/src/components/shared/PriceEstimateTooltip.tsx b/apps/deploy-web/src/components/shared/PriceEstimateTooltip.tsx index d0096ecb1a..6d89932129 100644 --- a/apps/deploy-web/src/components/shared/PriceEstimateTooltip.tsx +++ b/apps/deploy-web/src/components/shared/PriceEstimateTooltip.tsx @@ -3,11 +3,9 @@ import type { ReactNode } from "react"; import { CustomTooltip } from "@akashnetwork/ui/components"; import { InfoCircle } from "iconoir-react"; -import { useWallet } from "@src/context/WalletProvider"; -import { useDenomData } from "@src/hooks/useWalletBalance"; import { averageDaysInMonth } from "@src/utils/dateUtils"; import { udenomToDenom } from "@src/utils/mathHelpers"; -import { averageBlockTime, getAvgCostPerMonth } from "@src/utils/priceUtils"; +import { averageBlockTime } from "@src/utils/priceUtils"; import { PriceValue } from "./PriceValue"; type Props = { @@ -22,8 +20,6 @@ export const PriceEstimateTooltip: React.FunctionComponent = ({ value, de const perHourValue = _value * (60 / averageBlockTime) * 60; const perDayValue = _value * (60 / averageBlockTime) * 60 * 24; const perMonthValue = _value * (60 / averageBlockTime) * 60 * 24 * averageDaysInMonth; - const denomData = useDenomData(denom); - const { isCustodial } = useWallet(); return ( = ({ value, de   per month
- - {isCustodial &&
({`~${udenomToDenom(getAvgCostPerMonth(value as number))} ${denomData?.label}/month`})
} } > diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index 2eb23be593..e9c57dae78 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -1,36 +1,22 @@ "use client"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "react"; import type { EncodeObject } from "@cosmjs/proto-signing"; import { useAtom } from "jotai"; import { useRouter } from "next/navigation"; import { TransactionModal } from "@src/components/layout/TransactionModal"; import { useManagedWallet } from "@src/hooks/useManagedWallet"; -import { useSelectedChain } from "@src/hooks/useSelectedChain/useSelectedChain"; import { useUser } from "@src/hooks/useUser"; import { useWhen } from "@src/hooks/useWhen"; -import { CURRENT_WALLET_KEY, useManager } from "@src/lib/cosmos-kit-jotai"; import { useBalances } from "@src/queries/useBalancesQuery"; import networkStore from "@src/store/networkStore"; -import walletStore from "@src/store/walletStore"; import type { AppError } from "@src/types"; -import { getStorageWallets, updateStorageWallets } from "@src/utils/walletUtils"; +import { getStorageManagedWallet, updateStorageManagedWallet } from "@src/utils/walletUtils"; import { useServices } from "../ServicesProvider"; -import { useSettings } from "../SettingsProvider"; import { settingsIdAtom } from "../SettingsProvider/settingsStore"; import { deriveWalletIsLoading } from "./deriveWalletIsLoading"; import { useSignAndBroadcast } from "./useSignAndBroadcast"; -type ManagedWalletMarker = - | { - isManaged: true; - denom: string; - } - | { - isManaged: false; - denom: undefined; - }; - export type ContextType = { address: string; walletName: string; @@ -39,7 +25,8 @@ export type ContextType = { connectManagedWallet: () => void; logout: () => void; signAndBroadcastTx: (msgs: EncodeObject[]) => Promise; - isCustodial: boolean; + isManaged: true; + denom: string; isWalletLoading: boolean; isTrialing: boolean; isOnboarding: boolean; @@ -47,7 +34,7 @@ export type ContextType = { topUpMinAmountUsd: number; hasManagedWallet: boolean; managedWalletError?: AppError; -} & ManagedWalletMarker; +}; /** * @private for testing only @@ -63,84 +50,40 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr const [, setSettingsId] = useAtom(settingsIdAtom); const [isWalletLoaded, setIsWalletLoaded] = useState(true); const router = useRouter(); - const { settings } = useSettings(); const { user } = useUser(); - const userWallet = useSelectedChain(); const { wallet: managedWallet, isLoading: isManagedWalletLoading, create: createManagedWallet, createError: managedWalletError } = useManagedWallet(); - const [selectedWalletType, setSelectedWalletType] = useAtom(walletStore.selectedWalletType); - const { - address: walletAddress, - username, - isWalletConnected - } = useMemo(() => (selectedWalletType === "managed" && managedWallet) || userWallet, [managedWallet, userWallet, selectedWalletType]); + const walletAddress = managedWallet?.address; + const username = managedWallet?.username; + const isWalletConnected = !!managedWallet?.isWalletConnected; const { refetch: refetchBalances } = useBalances(walletAddress); - const custodialWalletManager = useManager(); - const managedMarker = useMemo((): ManagedWalletMarker => { - if (!!managedWallet && managedWallet?.address === walletAddress) { - return { isManaged: true, denom: managedWallet.denom }; - } - - return { isManaged: false, denom: undefined }; - }, [walletAddress, managedWallet]); - const { isManaged } = managedMarker; const [selectedNetworkId, setSelectedNetworkId] = networkStore.useSelectedNetworkIdStore(); const isLoading = deriveWalletIsLoading({ hasAuthenticatedUserId: !!user?.userId, - selectedWalletType, - isManagedWalletLoading, - isCustodialConnecting: userWallet.isWalletConnecting + isManagedWalletLoading }); const { signAndBroadcastTx, loadingState } = useSignAndBroadcast({ refetchBalances }); useWhen(walletAddress, loadWallet); - useWhen(isWalletConnected && selectedWalletType, () => { - if (selectedWalletType === "custodial") { - analyticsService.track( - "connect_wallet", - { - category: "wallet", - label: "Connect wallet" - }, - "GA" - ); - analyticsService.identify({ custodialWallet: true }); - analyticsService.trackSwitch("connect_wallet", "custodial", "Amplitude"); - } else if (selectedWalletType === "managed") { - analyticsService.identify({ managedWallet: true }); - analyticsService.trackSwitch("connect_wallet", "managed", "Amplitude"); - } + useWhen(isWalletConnected, () => { + analyticsService.identify({ managedWallet: true }); + analyticsService.trackSwitch("connect_wallet", "managed", "Amplitude"); }); - useEffect(() => { - if (!settings.apiEndpoint || !settings.rpcEndpoint) return; - - custodialWalletManager?.addEndpoints({ - akash: { rest: [settings.apiEndpoint], rpc: [settings.rpcEndpoint] }, - "akash-sandbox": { rest: [settings.apiEndpoint], rpc: [settings.rpcEndpoint] }, - "akash-testnet": { rest: [settings.apiEndpoint], rpc: [settings.rpcEndpoint] } - }); - }, [custodialWalletManager, settings.apiEndpoint, settings.rpcEndpoint]); - useEffect(() => { setSettingsId(walletAddress || null); - }, [walletAddress]); + }, [walletAddress, setSettingsId]); /** - * Force every visitor onto the managed-wallet network on first load, regardless of `selectedWalletType`. + * Force every visitor onto the managed-wallet network on first load. * - * Why unconditional: in the onboarding redesign, every authenticated user gets a managed trial wallet, - * and the entire console experience targets that network. Previously this effect only fired when - * `selectedWalletType === "managed"`, which meant the switch happened *after* `useManagedWallet` - * auto-flipped the wallet type — i.e. mid-deploy if the trial creation completed during a deploy — - * tearing down in-flight requests. Firing on first load instead means the (one-time) reload happens - * before any user action. + * Why unconditional: every authenticated user gets a managed trial wallet, + * and the entire console experience targets that network. Firing on first + * load means the (one-time) reload happens before any user action. * - * Why `reload()` not `href = home`: a hard nav to `/` was sending the user back to home after a - * successful deploy if the wallet-type flip happened post-success. Reloading in place keeps the URL. - * - * The localStorage-backed atom makes this a single reload per browser — subsequent loads see the - * managed network already selected and skip the effect entirely. + * Why `reload()` not `href = home`: a hard nav to `/` was sending the user + * back to home after a successful deploy if the wallet-type flip happened + * post-success. Reloading in place keeps the URL. */ useEffect(() => { if (selectedNetworkId === appConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID) return; @@ -152,44 +95,39 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr if (!managedWallet) { createManagedWallet(); } - setSelectedWalletType("managed"); } function logout() { - userWallet.disconnect(); - - if (typeof window !== "undefined") { - window.localStorage.removeItem(CURRENT_WALLET_KEY); - } - analyticsService.track("disconnect_wallet", { category: "wallet", label: "Disconnect wallet" }); router.push(urlService.home()); - - if (managedWallet) { - setSelectedWalletType("managed"); - } } async function loadWallet(): Promise { - const networkId = - isManaged && selectedNetworkId !== appConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID ? appConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID : undefined; - let currentWallets = getStorageWallets(networkId); - - if (!currentWallets.some(x => x.address === walletAddress)) { - currentWallets = [...currentWallets, { name: username || "", address: walletAddress as string, selected: true, isManaged: false }]; + if (!managedWallet?.userId || !walletAddress) { + setIsWalletLoaded(true); + return; } - currentWallets = currentWallets.map(x => ({ ...x, selected: x.address === walletAddress })); - - updateStorageWallets(currentWallets, networkId); + const networkId = appConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID; + const stored = getStorageManagedWallet(managedWallet.userId, networkId); + + if (!stored || stored.address !== walletAddress || !stored.selected) { + updateStorageManagedWallet({ + address: walletAddress, + userId: managedWallet.userId, + creditAmount: managedWallet.creditAmount, + isTrialing: managedWallet.isTrialing, + selected: true + }); + } setIsWalletLoaded(true); - if (networkId) { + if (selectedNetworkId !== networkId) { setSelectedNetworkId(networkId); } } @@ -204,15 +142,15 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr connectManagedWallet, logout, signAndBroadcastTx, - isCustodial: !isManaged, + isManaged: true, + denom: managedWallet?.denom ?? "", isWalletLoading: isLoading, - isTrialing: isManaged && !!managedWallet?.isTrialing, - isOnboarding: !!user?.userId && isManaged && !!managedWallet?.isTrialing, - creditAmount: isManaged ? managedWallet?.creditAmount : 0, + isTrialing: !!managedWallet?.isTrialing, + isOnboarding: !!user?.userId && !!managedWallet?.isTrialing, + creditAmount: managedWallet?.creditAmount, topUpMinAmountUsd: managedWallet?.topUpMinAmountUsd ?? 20, hasManagedWallet: !!managedWallet, - managedWalletError, - ...managedMarker + managedWalletError }} > {children} diff --git a/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.spec.ts b/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.spec.ts index 57703c052a..b38cf11c00 100644 --- a/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.spec.ts +++ b/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.spec.ts @@ -5,38 +5,23 @@ import { deriveWalletIsLoading, type DeriveWalletIsLoadingInput } from "./derive describe(deriveWalletIsLoading.name, () => { it.each<{ name: string; input: DeriveWalletIsLoadingInput; expected: boolean }>([ { - name: "authenticated user, custodial selected, managed-wallet query still loading (the race-condition case)", - input: { hasAuthenticatedUserId: true, selectedWalletType: "custodial", isManagedWalletLoading: true, isCustodialConnecting: false }, + name: "authenticated user with managed-wallet query still loading", + input: { hasAuthenticatedUserId: true, isManagedWalletLoading: true }, expected: true }, { - name: "authenticated user, managed selected, managed-wallet query still loading", - input: { hasAuthenticatedUserId: true, selectedWalletType: "managed", isManagedWalletLoading: true, isCustodialConnecting: false }, - expected: true - }, - { - name: "unauthenticated, managed selected, managed-wallet query still loading", - input: { hasAuthenticatedUserId: false, selectedWalletType: "managed", isManagedWalletLoading: true, isCustodialConnecting: false }, - expected: true - }, - { - name: "custodial selected and a connection is in progress", - input: { hasAuthenticatedUserId: false, selectedWalletType: "custodial", isManagedWalletLoading: false, isCustodialConnecting: true }, - expected: true - }, - { - name: "unauthenticated, custodial selected, no activity", - input: { hasAuthenticatedUserId: false, selectedWalletType: "custodial", isManagedWalletLoading: false, isCustodialConnecting: false }, + name: "authenticated user with managed-wallet query settled", + input: { hasAuthenticatedUserId: true, isManagedWalletLoading: false }, expected: false }, { - name: "unauthenticated, custodial selected, managed query loading is irrelevant", - input: { hasAuthenticatedUserId: false, selectedWalletType: "custodial", isManagedWalletLoading: true, isCustodialConnecting: false }, + name: "unauthenticated, managed-wallet query loading is irrelevant", + input: { hasAuthenticatedUserId: false, isManagedWalletLoading: true }, expected: false }, { - name: "authenticated, managed selected, nothing loading", - input: { hasAuthenticatedUserId: true, selectedWalletType: "managed", isManagedWalletLoading: false, isCustodialConnecting: false }, + name: "unauthenticated, nothing loading", + input: { hasAuthenticatedUserId: false, isManagedWalletLoading: false }, expected: false } ])("$name → $expected", ({ input, expected }) => { diff --git a/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.ts b/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.ts index b7ed537f4e..7b6cf200f2 100644 --- a/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.ts +++ b/apps/deploy-web/src/context/WalletProvider/deriveWalletIsLoading.ts @@ -1,13 +1,8 @@ -import type { SelectedWalletType } from "@src/store/walletStore"; - export type DeriveWalletIsLoadingInput = { hasAuthenticatedUserId: boolean; - selectedWalletType: SelectedWalletType; isManagedWalletLoading: boolean; - isCustodialConnecting: boolean; }; export const deriveWalletIsLoading = (input: DeriveWalletIsLoadingInput): boolean => { - const isManagedQueryRelevant = input.hasAuthenticatedUserId || input.selectedWalletType === "managed"; - return (isManagedQueryRelevant && input.isManagedWalletLoading) || (input.selectedWalletType === "custodial" && input.isCustodialConnecting); + return input.hasAuthenticatedUserId && input.isManagedWalletLoading; }; diff --git a/apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.spec.ts b/apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.spec.ts deleted file mode 100644 index 2b763492ad..0000000000 --- a/apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { mock } from "vitest-mock-extended"; - -import { CURRENT_WALLET_KEY } from "@src/lib/cosmos-kit-jotai"; -import type { SelectedWalletType } from "@src/store/walletStore"; -import { type DEPENDENCIES, useEnforceSelfCustodyFlag, type UseEnforceSelfCustodyFlagInput } from "./useEnforceSelfCustodyFlag"; - -import { renderHook } from "@testing-library/react"; - -describe(useEnforceSelfCustodyFlag.name, () => { - it("disconnects, clears CURRENT_WALLET_KEY, and switches to managed when flag is OFF and a custodial wallet is connected", () => { - const { disconnect, setSelectedWalletType, localStorage } = setup({ - isSelfCustodyEnabled: false, - isWalletConnected: true, - selectedWalletType: "custodial" - }); - - expect(disconnect).toHaveBeenCalledOnce(); - expect(localStorage.removeItem).toHaveBeenCalledWith(CURRENT_WALLET_KEY); - expect(setSelectedWalletType).toHaveBeenCalledWith("managed"); - }); - - it("clears stored wallet name and switches type when flag is OFF and selectedWalletType is still custodial without an active connection", () => { - const { disconnect, setSelectedWalletType, localStorage } = setup({ - isSelfCustodyEnabled: false, - isWalletConnected: false, - selectedWalletType: "custodial" - }); - - expect(disconnect).not.toHaveBeenCalled(); - expect(localStorage.removeItem).toHaveBeenCalledWith(CURRENT_WALLET_KEY); - expect(setSelectedWalletType).toHaveBeenCalledWith("managed"); - }); - - it("does nothing when flag is OFF and the user is already on managed without a custodial connection", () => { - const { disconnect, setSelectedWalletType, localStorage } = setup({ - isSelfCustodyEnabled: false, - isWalletConnected: false, - selectedWalletType: "managed" - }); - - expect(disconnect).not.toHaveBeenCalled(); - expect(localStorage.removeItem).not.toHaveBeenCalled(); - expect(setSelectedWalletType).not.toHaveBeenCalled(); - }); - - it("does nothing when flag is ON regardless of wallet state", () => { - const { disconnect, setSelectedWalletType, localStorage } = setup({ - isSelfCustodyEnabled: true, - isWalletConnected: true, - selectedWalletType: "custodial" - }); - - expect(disconnect).not.toHaveBeenCalled(); - expect(localStorage.removeItem).not.toHaveBeenCalled(); - expect(setSelectedWalletType).not.toHaveBeenCalled(); - }); - - function setup(input: { isSelfCustodyEnabled: boolean; isWalletConnected: boolean; selectedWalletType: SelectedWalletType }) { - const disconnect = vi.fn(); - const setSelectedWalletType = vi.fn(); - const localStorage = mock(); - const dependencies: typeof DEPENDENCIES = { - useIsSelfCustodyEnabled: () => input.isSelfCustodyEnabled, - localStorage - }; - - const hookInput: UseEnforceSelfCustodyFlagInput = { - isWalletConnected: input.isWalletConnected, - selectedWalletType: input.selectedWalletType, - setSelectedWalletType, - disconnect - }; - - renderHook(() => useEnforceSelfCustodyFlag(hookInput, dependencies)); - - return { disconnect, setSelectedWalletType, localStorage }; - } -}); diff --git a/apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.ts b/apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.ts deleted file mode 100644 index ff41d125c4..0000000000 --- a/apps/deploy-web/src/context/WalletProvider/useEnforceSelfCustodyFlag.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from "react"; - -import { useIsSelfCustodyEnabled } from "@src/hooks/useIsSelfCustodyEnabled"; -import { CURRENT_WALLET_KEY } from "@src/lib/cosmos-kit-jotai"; -import type { SelectedWalletType } from "@src/store/walletStore"; - -export const DEPENDENCIES = { - useIsSelfCustodyEnabled, - get localStorage(): Storage | null { - return typeof window !== "undefined" ? window.localStorage : null; - } -}; - -export type UseEnforceSelfCustodyFlagInput = { - isWalletConnected: boolean; - selectedWalletType: SelectedWalletType; - setSelectedWalletType: (type: SelectedWalletType) => void; - disconnect: () => void; -}; - -export function useEnforceSelfCustodyFlag(input: UseEnforceSelfCustodyFlagInput, dependencies: typeof DEPENDENCIES = DEPENDENCIES): void { - const isSelfCustodyEnabled = dependencies.useIsSelfCustodyEnabled(); - const { localStorage } = dependencies; - const { isWalletConnected, selectedWalletType, setSelectedWalletType, disconnect } = input; - - useEffect(() => { - if (isSelfCustodyEnabled) return; - if (selectedWalletType !== "custodial" && !isWalletConnected) return; - - if (isWalletConnected) { - disconnect(); - } - - localStorage?.removeItem(CURRENT_WALLET_KEY); - - if (selectedWalletType !== "managed") { - setSelectedWalletType("managed"); - } - }, [isSelfCustodyEnabled, isWalletConnected, selectedWalletType, disconnect, setSelectedWalletType, localStorage]); -} diff --git a/apps/deploy-web/src/hooks/useManagedWallet.ts b/apps/deploy-web/src/hooks/useManagedWallet.ts index af18d761f6..df572ecc4a 100644 --- a/apps/deploy-web/src/hooks/useManagedWallet.ts +++ b/apps/deploy-web/src/hooks/useManagedWallet.ts @@ -1,33 +1,21 @@ -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo } from "react"; import type { ApiManagedWalletOutput } from "@akashnetwork/http-sdk"; import { useAtom } from "jotai"; import { useUser } from "@src/hooks/useUser"; import { useCreateManagedWalletMutation, useManagedWalletQuery } from "@src/queries/useManagedWalletQuery"; import walletStore from "@src/store/walletStore"; -import { ensureUserManagedWalletOwnership, getSelectedStorageWallet, updateStorageManagedWallet } from "@src/utils/walletUtils"; +import { ensureUserManagedWalletOwnership, updateStorageManagedWallet } from "@src/utils/walletUtils"; import { useCustomUser } from "./useCustomUser"; export const useManagedWallet = () => { const { user } = useUser(); const { user: signedInUser } = useCustomUser(); - const [selectedWalletType, setSelectedWalletType] = useAtom(walletStore.selectedWalletType); const { data: queried, isLoading: isInitialLoading, isFetching, refetch } = useManagedWalletQuery(user?.id); const { mutate: create, data: created, isPending: isCreating, isSuccess: isCreated, error: createError } = useCreateManagedWalletMutation(); const wallet = useMemo(() => (queried || created) as ApiManagedWalletOutput, [queried, created]); const isLoading = isInitialLoading || isCreating; const [, setIsSignedInWithTrial] = useAtom(walletStore.isSignedInWithTrial); - const selected = getSelectedStorageWallet(); - const hasAutoSwitched = useRef(false); - - useEffect(() => { - if (!hasAutoSwitched.current && queried) { - hasAutoSwitched.current = true; - if (selectedWalletType === "custodial") { - setSelectedWalletType("managed"); - } - } - }, [queried, selectedWalletType, setSelectedWalletType]); useEffect(() => { if (signedInUser?.id && (!!queried || !!created)) { @@ -65,7 +53,7 @@ export const useManagedWallet = () => { username: wallet.username, isWalletConnected: isConfigured, isWalletLoaded: isConfigured, - selected: selected?.address === wallet.address + selected: true } : undefined, isLoading, @@ -73,5 +61,5 @@ export const useManagedWallet = () => { createError, refetch }; - }, [wallet, selected?.address, isLoading, isFetching, createError, refetch, user?.id, create]); + }, [wallet, isLoading, isFetching, createError, refetch, user?.id, create]); }; diff --git a/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.spec.tsx b/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.spec.tsx index 3dd09bd2cd..026bc5b756 100644 --- a/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.spec.tsx +++ b/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.spec.tsx @@ -1,16 +1,16 @@ import type { JwtTokenPayload } from "@akashnetwork/chain-sdk/web"; import type { HttpClient } from "@akashnetwork/http-sdk"; -import type { NetworkStore } from "@akashnetwork/network-store"; import { describe, expect, it, vi } from "vitest"; import { mock } from "vitest-mock-extended"; import type { ContextType as WalletContext } from "@src/context/WalletProvider"; -import type { ChainContext as CustodialWallet } from "@src/lib/cosmos-kit-jotai"; +import type { useUser } from "@src/hooks/useUser"; +import type { CustomUserProfile } from "@src/types/user"; import type * as storedWalletsService from "@src/utils/walletUtils"; -import type { useSelectedChain } from "../useSelectedChain/useSelectedChain"; import { DEPENDENCIES, REFRESH_SKEW_SECONDS, useProviderJwt } from "./useProviderJwt"; -import { act } from "@testing-library/react"; +import { buildWallet } from "@tests/seeders"; +import { buildManagedLocalWallet } from "@tests/seeders/localWallet"; import type { RenderAppHookOptions } from "@tests/unit/query-client"; import { setupQuery } from "@tests/unit/query-client"; @@ -25,48 +25,38 @@ describe(useProviderJwt.name, () => { expect(typeof result.current.generateToken).toBe("function"); }); - it("retrieves token from storage when wallet address changes", () => { + it("reads token from managed-wallet storage on mount", () => { const token = genFakeToken(); + const userId = "user-1"; + const wallet = buildManagedLocalWallet({ userId, token }); const storedWalletsService = mock({ - getStorageWallets: vi.fn().mockReturnValue([{ address: "akash1234567890", token }]) + getStorageManagedWallet: vi.fn().mockReturnValue(wallet) }); const { result } = setup({ - services: { - storedWalletsService: () => storedWalletsService - }, - wallet: { - address: "akash1234567890" - } + services: { storedWalletsService: () => storedWalletsService }, + user: { id: userId } }); expect(result.current.accessToken).toBe(token); - expect(storedWalletsService.getStorageWallets).toHaveBeenCalledWith("mainnet"); + expect(storedWalletsService.getStorageManagedWallet).toHaveBeenCalledWith(userId); }); - it("generates token for managed wallet via API", async () => { + it("generates a token via the API and persists it", async () => { const token = genFakeToken(); + const userId = "user-1"; const consoleApiHttpClient = mock({ - post: vi.fn().mockResolvedValue({ - data: { data: { token } } - }) + post: vi.fn().mockResolvedValue({ data: { data: { token } } }) } as unknown as HttpClient); - const storedWalletsService = mock({ - updateWallet: vi.fn(), - getStorageWallets: vi.fn().mockReturnValue([{ address: "akash1234567890", token }]) + getStorageManagedWallet: vi.fn().mockReturnValue(undefined), + updateStorageManagedWallet: vi.fn() }); const { result } = setup({ - services: { - consoleApiHttpClient: () => consoleApiHttpClient, - storedWalletsService: () => storedWalletsService - }, - wallet: { - isManaged: true, - isWalletConnected: true, - address: "akash1234567890" - } + services: { consoleApiHttpClient: () => consoleApiHttpClient, storedWalletsService: () => storedWalletsService }, + user: { id: userId }, + wallet: { isWalletConnected: true } }); await result.current.generateToken(); @@ -80,83 +70,45 @@ describe(useProviderJwt.name, () => { } } }); - expect(storedWalletsService.updateWallet).toHaveBeenCalledWith("akash1234567890", expect.any(Function)); + expect(storedWalletsService.updateStorageManagedWallet).toHaveBeenCalledWith({ userId, token }); expect(result.current.accessToken).toBe(token); }); - it("generates token for non-managed wallet via direct signing", async () => { - const address = "akash1".padEnd(6 + 38, "0"); - const custodialWallet = mock({ - address, - signArbitrary: vi.fn().mockResolvedValue({ signature: btoa("signature") }) - }); - - const storedWallets: storedWalletsService.LocalWallet[] = [{ address } as storedWalletsService.LocalWallet]; - const storedWalletsService = mock({ - getStorageWallets: vi.fn(() => storedWallets), - updateWallet: vi.fn((address, fn) => { - const walletIndex = storedWallets.findIndex(w => w.address === address); - if (walletIndex !== -1) { - storedWallets[walletIndex] = fn(storedWallets[walletIndex]); - } - return storedWallets; - }) - }); + it("throws when generating a token while wallet is disconnected", async () => { + const consoleApiHttpClient = mock({ post: vi.fn() } as unknown as HttpClient); const { result } = setup({ - services: { - storedWalletsService: () => storedWalletsService - }, - wallet: { - isManaged: false, - address - }, - custodialWallet + services: { consoleApiHttpClient: () => consoleApiHttpClient }, + wallet: { isWalletConnected: false } }); - await act(() => result.current.generateToken()); - - expect(custodialWallet.signArbitrary).toHaveBeenCalledWith(address, expect.any(String)); - expect(storedWalletsService.updateWallet).toHaveBeenCalledWith(address, expect.any(Function)); - - const [, , signature] = result.current.accessToken?.split(".") ?? []; - expect(atob(signature)).toBe("signature"); + await expect(result.current.generateToken()).rejects.toThrow(/wallet is not connected/i); + expect(consoleApiHttpClient.post).not.toHaveBeenCalled(); }); - it("throws when generating a token while wallet is not connected", async () => { - const consoleApiHttpClient = mock({ - post: vi.fn() - } as unknown as HttpClient); + it("throws when generating a token without an authenticated user", async () => { + const consoleApiHttpClient = mock({ post: vi.fn() } as unknown as HttpClient); const { result } = setup({ - services: { - consoleApiHttpClient: () => consoleApiHttpClient - }, - wallet: { - isWalletConnected: false - } + services: { consoleApiHttpClient: () => consoleApiHttpClient }, + wallet: { isWalletConnected: true }, + user: null }); - await expect(result.current.generateToken()).rejects.toThrow(/wallet is not connected/i); + await expect(result.current.generateToken()).rejects.toThrow(/user is not authenticated/i); expect(consoleApiHttpClient.post).not.toHaveBeenCalled(); }); it("detects expired token correctly", () => { const pastTime = Math.floor(Date.now() / 1000) - 100; - - const { result } = setup({ - initialToken: genFakeToken({ exp: pastTime }) - }); + const { result } = setup({ initialToken: genFakeToken({ exp: pastTime }) }); expect(result.current.isTokenExpired).toBe(true); }); it("detects valid token correctly", () => { const futureTime = Math.floor(Date.now() / 1000) + 3600; - - const { result } = setup({ - initialToken: genFakeToken({ exp: futureTime }) - }); + const { result } = setup({ initialToken: genFakeToken({ exp: futureTime }) }); expect(result.current.isTokenExpired).toBe(false); }); @@ -192,50 +144,29 @@ describe(useProviderJwt.name, () => { function setup(input?: { services?: Partial; wallet?: Partial; - custodialWallet?: ReturnType; + user?: Partial | null; initialToken?: string; }) { + const user = input?.user === null ? undefined : { id: "user-1", ...(input?.user ?? {}) }; + const seededWallet = input?.initialToken ? buildManagedLocalWallet({ userId: user?.id ?? "user-1", token: input.initialToken }) : undefined; + return setupQuery( () => useProviderJwt({ dependencies: { ...DEPENDENCIES, - useWallet: () => - ({ - address: "akash1234567890", - walletName: "test-wallet", - isWalletLoaded: true, - connectManagedWallet: vi.fn(), - logout: vi.fn(), - signAndBroadcastTx: vi.fn(), - isManaged: false, - isWalletConnected: true, - isCustodial: false, - isWalletLoading: false, - isTrialing: false, - isOnboarding: false, - creditAmount: 0, - hasManagedWallet: false, - managedWalletError: undefined, - ...input?.wallet - }) as WalletContext, - useSelectedChain: () => - input?.custodialWallet ?? - mock({ - signArbitrary: vi.fn() + useWallet: () => buildWallet({ isWalletConnected: true, ...input?.wallet }), + useUser: () => + mock>({ + user: user as CustomUserProfile | undefined }) } }), { services: { - networkStore: () => - mock({ - useSelectedNetworkId: () => "mainnet" - }), storedWalletsService: () => mock({ - getStorageWallets: () => - input?.initialToken ? [{ address: "akash1234567890", token: input.initialToken } as storedWalletsService.LocalWallet] : [] + getStorageManagedWallet: vi.fn().mockReturnValue(seededWallet) }), consoleApiHttpClient: () => mock(), ...input?.services diff --git a/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts b/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts index 7bf8805fab..f625931a74 100644 --- a/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts +++ b/apps/deploy-web/src/hooks/useProviderJwt/useProviderJwt.ts @@ -1,45 +1,48 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { JwtTokenManager, type JwtTokenPayload } from "@akashnetwork/chain-sdk/web"; +import { JwtTokenManager } from "@akashnetwork/chain-sdk/web"; import { atom, useAtom } from "jotai"; import { useServices } from "@src/context/ServicesProvider"; import { useWallet } from "@src/context/WalletProvider"; -import { useSelectedChain } from "../useSelectedChain/useSelectedChain"; +import { useUser } from "@src/hooks/useUser"; const JWT_TOKEN_ATOM = atom(null); export const REFRESH_SKEW_SECONDS = 60; export const DEPENDENCIES = { - useSelectedChain, useWallet, + useUser, useServices }; export function useProviderJwt({ dependencies: d = DEPENDENCIES }: { dependencies?: typeof DEPENDENCIES } = {}): UseProviderJwtResult { - const { storedWalletsService, networkStore, consoleApiHttpClient } = d.useServices(); - const { isManaged, address, isWalletConnected } = d.useWallet(); - const selectedChain = d.useSelectedChain(); - const selectedNetworkId = networkStore.useSelectedNetworkId(); + const { storedWalletsService, consoleApiHttpClient } = d.useServices(); + const { isWalletConnected } = d.useWallet(); + const { user } = d.useUser(); + const userId = user?.id; const [accessToken, setAccessToken] = useAtom(JWT_TOKEN_ATOM); const [isHydrated, setIsHydrated] = useState(false); useEffect(() => { - const token = storedWalletsService.getStorageWallets(selectedNetworkId).find(w => w.address === address)?.token; + if (!userId) { + setAccessToken(null); + setIsHydrated(true); + return; + } + const token = storedWalletsService.getStorageManagedWallet(userId)?.token; setAccessToken(token || null); setIsHydrated(true); - }, [storedWalletsService, selectedNetworkId, address]); + }, [storedWalletsService, userId, setAccessToken]); const jwtTokenManager = useMemo( () => new JwtTokenManager({ - signArbitrary: selectedChain - ? selectedChain.signArbitrary - : () => { - throw new Error("Cannot sign jwt token: custodial wallet not found"); - } + signArbitrary: () => { + throw new Error("Cannot sign jwt token: managed wallet uses server-side signing"); + } }), - [selectedChain] + [] ); const parsedToken = useMemo(() => { if (!accessToken) return null; @@ -50,36 +53,25 @@ export function useProviderJwt({ dependencies: d = DEPENDENCIES }: { dependencie if (!isWalletConnected) { throw new Error("Cannot generate JWT: wallet is not connected"); } + if (!userId) { + throw new Error("Cannot generate JWT: user is not authenticated"); + } - const leasesAccess: JwtTokenPayload["leases"] = { - access: "scoped", - scope: ["status", "shell", "events", "logs", "send-manifest", "get-manifest"] - }; - const tokenLifetimeInSeconds = 30 * 60; - let token: string; - if (isManaged) { - const response = await consoleApiHttpClient.post<{ data: { token: string } }>("/v1/create-jwt-token", { - data: { - ttl: tokenLifetimeInSeconds, - leases: leasesAccess + const response = await consoleApiHttpClient.post<{ data: { token: string } }>("/v1/create-jwt-token", { + data: { + ttl: 30 * 60, + leases: { + access: "scoped", + scope: ["status", "shell", "events", "logs", "send-manifest", "get-manifest"] } - }); - token = response.data.data.token; - } else { - const now = Math.floor(Date.now() / 1000); - token = await jwtTokenManager.generateToken({ - version: "v1", - iss: address, - exp: now + tokenLifetimeInSeconds, - iat: now, - leases: leasesAccess - }); - } + } + }); + const token = response.data.data.token; - storedWalletsService.updateWallet(address, w => ({ ...w, token })); + storedWalletsService.updateStorageManagedWallet({ userId, token }); setAccessToken(token); return token; - }, [isWalletConnected, isManaged, selectedChain, jwtTokenManager, address, consoleApiHttpClient]); + }, [isWalletConnected, userId, consoleApiHttpClient, storedWalletsService, setAccessToken]); return useMemo( () => ({ diff --git a/apps/deploy-web/src/hooks/useSelectedChain/useSelectedChain.ts b/apps/deploy-web/src/hooks/useSelectedChain/useSelectedChain.ts deleted file mode 100644 index 2f5563b01c..0000000000 --- a/apps/deploy-web/src/hooks/useSelectedChain/useSelectedChain.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type ChainContext, useChain } from "@src/lib/cosmos-kit-jotai"; -import networkStore from "@src/store/networkStore"; - -export function useSelectedChain(): ChainContext { - const { chainRegistryName } = networkStore.useSelectedNetwork(); - return useChain(chainRegistryName); -} diff --git a/apps/deploy-web/src/queries/deploymentSettingsQuery.spec.ts b/apps/deploy-web/src/queries/deploymentSettingsQuery.spec.ts index 3d12623817..0673dc3f54 100644 --- a/apps/deploy-web/src/queries/deploymentSettingsQuery.spec.ts +++ b/apps/deploy-web/src/queries/deploymentSettingsQuery.spec.ts @@ -4,16 +4,14 @@ import { QueryClient } from "@tanstack/react-query"; import { describe, expect, it, vi } from "vitest"; import { mock } from "vitest-mock-extended"; -import type { ContextType as WalletProviderContextType } from "@src/context/WalletProvider/WalletProvider"; -import { USE_DEPLOYMENT_SETTING_DEPENDENCIES, useDeploymentSettingQuery } from "./deploymentSettingsQuery"; +import { useDeploymentSettingQuery } from "./deploymentSettingsQuery"; import { act } from "@testing-library/react"; -import { buildWallet } from "@tests/seeders"; import { setupQuery } from "@tests/unit/query-client"; describe("useDeploymentSettingQuery", () => { describe("query", () => { - it("fetches deployment setting by dseq when wallet is managed", async () => { + it("fetches deployment setting by dseq", async () => { const dseq = faker.string.numeric(6); const settingData = buildDeploymentSetting({ dseq }); const deploymentSettingService = mock({ @@ -28,20 +26,6 @@ describe("useDeploymentSettingQuery", () => { expect(deploymentSettingService.findByDseq).toHaveBeenCalledWith(dseq); }); - it("does not fetch when wallet is not managed", () => { - const dseq = faker.string.numeric(6); - const deploymentSettingService = mock(); - - const { result } = setup({ - dseq, - wallet: buildWallet({ isManaged: false }), - services: { deploymentSetting: () => deploymentSettingService } - }); - - expect(result.current.data).toBeUndefined(); - expect(deploymentSettingService.findByDseq).not.toHaveBeenCalled(); - }); - it("does not fetch when dseq is empty", () => { const deploymentSettingService = mock(); @@ -86,36 +70,10 @@ describe("useDeploymentSettingQuery", () => { }); expect(deploymentSettingService.updateByDseq).toHaveBeenCalledWith(dseq, { autoTopUpEnabled: true }); }); - - it("throws when wallet is not managed", async () => { - const dseq = faker.string.numeric(6); - const deploymentSettingService = mock(); - - const { result } = setup({ - dseq, - wallet: buildWallet({ isManaged: false }), - services: { deploymentSetting: () => deploymentSettingService } - }); - - act(() => { - result.current.update(true); - }); - - await vi.waitFor(() => { - expect(result.current.isLoading).toBe(false); - }); - expect(deploymentSettingService.updateByDseq).not.toHaveBeenCalled(); - }); }); - function setup(input: { dseq: string; wallet?: WalletProviderContextType; services?: Record unknown> }) { - const wallet = input.wallet ?? buildWallet({ isManaged: true }); - const dependencies: typeof USE_DEPLOYMENT_SETTING_DEPENDENCIES = { - ...USE_DEPLOYMENT_SETTING_DEPENDENCIES, - useWallet: () => wallet - }; - - return setupQuery(() => useDeploymentSettingQuery({ dseq: input.dseq }, dependencies), { + function setup(input: { dseq: string; services?: Record unknown> }) { + return setupQuery(() => useDeploymentSettingQuery({ dseq: input.dseq }), { services: { deploymentSetting: () => mock(), ...input.services diff --git a/apps/deploy-web/src/queries/deploymentSettingsQuery.ts b/apps/deploy-web/src/queries/deploymentSettingsQuery.ts index 7540f8f6ce..1019ad59d3 100644 --- a/apps/deploy-web/src/queries/deploymentSettingsQuery.ts +++ b/apps/deploy-web/src/queries/deploymentSettingsQuery.ts @@ -4,16 +4,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { millisecondsInMinute } from "date-fns/constants"; import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; import { QueryKeys } from "./queryKeys"; -export const USE_DEPLOYMENT_SETTING_DEPENDENCIES = { useWallet }; - -export function useDeploymentSettingQuery( - params: { dseq: string }, - dependencies: typeof USE_DEPLOYMENT_SETTING_DEPENDENCIES = USE_DEPLOYMENT_SETTING_DEPENDENCIES -) { - const wallet = dependencies.useWallet(); +export function useDeploymentSettingQuery(params: { dseq: string }) { const queryKey = useMemo(() => QueryKeys.getDeploymentSettingKey(params.dseq), [params.dseq]); const { deploymentSetting } = useServices(); const queryClient = useQueryClient(); @@ -21,7 +14,7 @@ export function useDeploymentSettingQuery( const query = useQuery({ queryKey, queryFn: () => deploymentSetting.findByDseq(params.dseq), - enabled: !!params.dseq && !!wallet.isManaged, + enabled: !!params.dseq, staleTime: 5 * millisecondsInMinute, retry: (failureCount, error) => { if (isHttpError(error) && error.response?.status === 404) { @@ -33,10 +26,6 @@ export function useDeploymentSettingQuery( const update = useMutation({ mutationFn: (autoTopUpEnabled: boolean) => { - if (!wallet.isManaged) { - throw new Error("Cannot update deployment setting for a custodial wallet"); - } - return deploymentSetting.updateByDseq(params.dseq, { autoTopUpEnabled }); }, onSuccess: data => { diff --git a/apps/deploy-web/src/queries/useApiKeysQuery.spec.tsx b/apps/deploy-web/src/queries/useApiKeysQuery.spec.tsx index 7f740c558b..e29c1873eb 100644 --- a/apps/deploy-web/src/queries/useApiKeysQuery.spec.tsx +++ b/apps/deploy-web/src/queries/useApiKeysQuery.spec.tsx @@ -4,18 +4,16 @@ import { QueryClient } from "@tanstack/react-query"; import { describe, expect, it, vi } from "vitest"; import { mock } from "vitest-mock-extended"; -import type { ContextType as WalletProviderContextType } from "@src/context/WalletProvider/WalletProvider"; import type { CustomUserProfile } from "@src/types/user"; import { USE_API_KEYS_DEPENDENCIES, useCreateApiKey, useDeleteApiKey, useUserApiKeys } from "./useApiKeysQuery"; import { act } from "@testing-library/react"; -import { buildApiKey, buildUser, buildWallet } from "@tests/seeders"; +import { buildApiKey, buildUser } from "@tests/seeders"; import { setupQuery } from "@tests/unit/query-client"; const mockApiKeys = [buildApiKey({ id: "key-1", name: "Test Key 1" }), buildApiKey({ id: "key-2", name: "Test Key 2" })]; const mockUser: CustomUserProfile = buildUser(); -const mockWallet: WalletProviderContextType = buildWallet(); describe("useApiKeysQuery", () => { describe("useUserApiKeys", () => { @@ -26,7 +24,6 @@ describe("useApiKeysQuery", () => { const { result } = setupApiKeysQuery({ user: undefined, - wallet: mockWallet, services: { apiKey: () => apiKeyService } @@ -43,31 +40,13 @@ describe("useApiKeysQuery", () => { expect(result.current.query.isSuccess).toBe(false); }); - it("should return undefined and not fetch when wallet is not managed", async () => { - const apiKeyService = mock({ - getApiKeys: vi.fn() - }); - const { result } = setupApiKeysQuery({ - user: mockUser, - wallet: { ...mockWallet, isManaged: false }, - services: { - apiKey: () => apiKeyService - } - }); - - expect(result.current.query.fetchStatus).toBe("idle"); - expect(apiKeyService.getApiKeys).not.toHaveBeenCalled(); - expect(result.current.query.data).toBeUndefined(); - }); - - it("should fetch API keys when user is valid and wallet is managed", async () => { + it("should fetch API keys when user is valid", async () => { const apiKeyService = mock({ getApiKeys: vi.fn().mockResolvedValue(mockApiKeys) }); const { result } = setupApiKeysQuery({ user: mockUser, - wallet: mockWallet, services: { apiKey: () => apiKeyService } @@ -89,7 +68,6 @@ describe("useApiKeysQuery", () => { const queryClient = new QueryClient(); const { result } = setupApiKeysQuery({ user: mockUser, - wallet: mockWallet, services: { apiKey: () => apiKeyService, queryClient: () => queryClient @@ -124,11 +102,11 @@ describe("useApiKeysQuery", () => { () => { const dependencies: typeof USE_API_KEYS_DEPENDENCIES = { ...USE_API_KEYS_DEPENDENCIES, - useUser: () => ({ - user: mockUser, - isLoading: false - }), - useWallet: () => mockWallet + useUser: () => + mock>({ + user: mockUser, + isLoading: false + }) }; return useCreateApiKey(dependencies); }, @@ -172,11 +150,11 @@ describe("useApiKeysQuery", () => { () => { const dependencies: typeof USE_API_KEYS_DEPENDENCIES = { ...USE_API_KEYS_DEPENDENCIES, - useUser: () => ({ - user: mockUser, - isLoading: false - }), - useWallet: () => mockWallet + useUser: () => + mock>({ + user: mockUser, + isLoading: false + }) }; return useDeleteApiKey("key-1", undefined, dependencies); }, @@ -220,11 +198,11 @@ describe("useApiKeysQuery", () => { () => { const dependencies: typeof USE_API_KEYS_DEPENDENCIES = { ...USE_API_KEYS_DEPENDENCIES, - useUser: () => ({ - user: mockUser, - isLoading: false - }), - useWallet: () => mockWallet + useUser: () => + mock>({ + user: mockUser, + isLoading: false + }) }; return useDeleteApiKey("key-1", onSuccess, dependencies); }, @@ -247,14 +225,14 @@ describe("useApiKeysQuery", () => { }); }); - function setupApiKeysQuery(input?: { user?: CustomUserProfile | undefined; wallet?: WalletProviderContextType; services?: Record unknown> }) { + function setupApiKeysQuery(input?: { user?: CustomUserProfile | undefined; services?: Record unknown> }) { const dependencies: typeof USE_API_KEYS_DEPENDENCIES = { ...USE_API_KEYS_DEPENDENCIES, - useUser: () => ({ - user: input?.user as CustomUserProfile, - isLoading: false - }), - useWallet: () => input?.wallet || mockWallet + useUser: () => + mock>({ + user: input?.user as CustomUserProfile, + isLoading: false + }) }; return setupQuery( diff --git a/apps/deploy-web/src/queries/useApiKeysQuery.ts b/apps/deploy-web/src/queries/useApiKeysQuery.ts index c660649a4f..b69f793ffa 100644 --- a/apps/deploy-web/src/queries/useApiKeysQuery.ts +++ b/apps/deploy-web/src/queries/useApiKeysQuery.ts @@ -3,13 +3,11 @@ import type { QueryKey, UseQueryOptions } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useServices } from "@src/context/ServicesProvider"; -import { useWallet } from "@src/context/WalletProvider"; import { useUser } from "@src/hooks/useUser"; import { QueryKeys } from "./queryKeys"; export const USE_API_KEYS_DEPENDENCIES = { - useUser, - useWallet + useUser }; export function useUserApiKeys( @@ -17,13 +15,12 @@ export function useUserApiKeys( dependencies: typeof USE_API_KEYS_DEPENDENCIES = USE_API_KEYS_DEPENDENCIES ) { const { user } = dependencies.useUser(); - const { isManaged } = dependencies.useWallet(); const { apiKey } = useServices(); return useQuery({ queryKey: QueryKeys.getApiKeysKey(user?.userId ?? ""), queryFn: async () => await apiKey.getApiKeys(), - enabled: !!user?.userId && isManaged, + enabled: !!user?.userId, refetchInterval: 10_000, retry: failureCount => failureCount < 5, retryDelay: 10_000, diff --git a/apps/deploy-web/src/store/walletStore.ts b/apps/deploy-web/src/store/walletStore.ts index cea72068b2..04c0121bde 100644 --- a/apps/deploy-web/src/store/walletStore.ts +++ b/apps/deploy-web/src/store/walletStore.ts @@ -3,16 +3,12 @@ import { atomWithStorage } from "jotai/utils"; import type { WalletBalance } from "@src/hooks/useWalletBalance"; -export type SelectedWalletType = "managed" | "custodial"; - const isSignedInWithTrial = atomWithStorage("isSignedInWithTrial", false); -const selectedWalletType = atomWithStorage("selectedWalletType", "custodial"); const isWalletModalOpen = atom(false); const balance = atom(null); const walletStore = { isSignedInWithTrial, - selectedWalletType, isWalletModalOpen, balance }; diff --git a/apps/deploy-web/src/utils/walletUtils.spec.ts b/apps/deploy-web/src/utils/walletUtils.spec.ts index 80ea7e9eb9..e0161deb6b 100644 --- a/apps/deploy-web/src/utils/walletUtils.spec.ts +++ b/apps/deploy-web/src/utils/walletUtils.spec.ts @@ -2,712 +2,36 @@ import { describe, expect, it, vi } from "vitest"; import { mock } from "vitest-mock-extended"; import { browserEnvConfig } from "@src/config/browser-env.config"; -import networkStore from "@src/store/networkStore"; -import { - deleteManagedWalletFromStorage, - deleteWalletFromStorage, - ensureUserManagedWalletOwnership, - getSelectedStorageWallet, - getStorageManagedWallet, - getStorageWallets, - type LocalWallet, - updateStorageManagedWallet, - updateStorageWallets, - updateWallet -} from "./walletUtils"; - -import { buildCustodialLocalWallet, buildManagedLocalWallet } from "@tests/seeders/localWallet"; +import { deleteManagedWalletFromStorage, ensureUserManagedWalletOwnership, getStorageManagedWallet, updateStorageManagedWallet } from "./walletUtils"; -describe("walletUtils", () => { - const NETWORK_ID = "mainnet"; - const MANAGED_NETWORK_ID = "sandbox"; - const USER_ID_1 = "user-123"; - const USER_ID_2 = "user-456"; - const WALLET_ADDRESS_1 = "akash1abc123"; - const WALLET_ADDRESS_2 = "akash1def456"; - const WALLET_ADDRESS_3 = "akash1ghi789"; - - describe("getStorageWallets", () => { - it("returns empty array when no wallets exist", () => { - const { cleanup } = setup(); - - const wallets = getStorageWallets(); - expect(wallets).toEqual([]); - - cleanup(); - }); - - it("returns custodial wallets from storage", () => { - const { storage, cleanup } = setup(); - - const custodialWallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: false }) - ]; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(custodialWallets)); - - const result = getStorageWallets(); - expect(result).toEqual(custodialWallets); - - cleanup(); - }); - - it("merges custodial and managed wallets", () => { - const { storage, cleanup } = setup(); - - const custodialWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: false }); - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_1, - selected: true - }); - - const managedWalletsMap = { - [USER_ID_1]: managedWallet - }; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([custodialWallet])); - storage.set(`${NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - const result = getStorageWallets(); - - expect(result).toHaveLength(2); - expect(result[0]).toEqual(custodialWallet); - expect(result[1]).toEqual(managedWallet); - - cleanup(); - }); - - it("prioritizes selected managed wallet", () => { - const { storage, cleanup } = setup(); - - const custodialWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_1, - selected: true - }); - - const managedWalletsMap = { - [USER_ID_1]: managedWallet - }; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([custodialWallet])); - storage.set(`${NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - const result = getStorageWallets(); - - expect(result[0].selected).toBe(false); - expect(result[1].selected).toBe(true); - expect(result[1].address).toBe(WALLET_ADDRESS_2); - - cleanup(); - }); - - it("removes duplicate managed wallets from old storage", () => { - const { storage, cleanup } = setup(); - - const custodialWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_1, - selected: false - }); - - const walletsWithOldFormat: LocalWallet[] = [custodialWallet, managedWallet]; - - const managedWalletsMap = { - [USER_ID_1]: managedWallet - }; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(walletsWithOldFormat)); - storage.set(`${NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - const result = getStorageWallets(); - - expect(result).toHaveLength(2); - expect(result.filter((w: LocalWallet) => w.isManaged)).toHaveLength(1); - - cleanup(); - }); - - it("handles corrupt managed-wallets JSON gracefully", () => { - const { storage, cleanup } = setup(); - - const custodialWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([custodialWallet])); - storage.set(`${NETWORK_ID}/managed-wallets`, "invalid-json{"); - - const result = getStorageWallets(); - expect(result).toEqual([custodialWallet]); - - cleanup(); - }); - - it("respects networkId parameter", () => { - const { storage, cleanup } = setup(); - - const mainnetWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - const testnetWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: true }); - - storage.set("mainnet/wallets", JSON.stringify([mainnetWallet])); - storage.set("testnet/wallets", JSON.stringify([testnetWallet])); - - expect(getStorageWallets("mainnet")).toEqual([mainnetWallet]); - expect(getStorageWallets("testnet")).toEqual([testnetWallet]); - - cleanup(); - }); - }); - - describe("getSelectedStorageWallet", () => { - it("returns null when no wallets exist", () => { - const { cleanup } = setup(); - - expect(getSelectedStorageWallet()).toBeNull(); - - cleanup(); - }); - - it("returns first wallet when none is selected", () => { - const { storage, cleanup } = setup(); - - const wallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: false }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: false }) - ]; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(wallets)); - - const result = getSelectedStorageWallet(); - expect(result?.address).toBe(WALLET_ADDRESS_1); - - cleanup(); - }); - - it("returns selected wallet", () => { - const { storage, cleanup } = setup(); - - const wallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: false }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: true }) - ]; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(wallets)); - - const result = getSelectedStorageWallet(); - expect(result?.address).toBe(WALLET_ADDRESS_2); - - cleanup(); - }); - - it("returns selected managed wallet", () => { - const { storage, cleanup } = setup(); - - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_1, - selected: true - }); - - const managedWalletsMap = { - [USER_ID_1]: managedWallet - }; - - storage.set(`${NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - const result = getSelectedStorageWallet(); - expect(result?.address).toBe(WALLET_ADDRESS_2); - expect(result?.isManaged).toBe(true); - - cleanup(); - }); - }); - - describe("getStorageManagedWallet", () => { - it("returns undefined when userId is not provided", () => { - const { cleanup } = setup(); - - expect(getStorageManagedWallet()).toBeUndefined(); - expect(getStorageManagedWallet("")).toBeUndefined(); - - cleanup(); - }); - - it("returns undefined when no managed wallets exist", () => { - const { cleanup } = setup(); - - const result = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(result).toBeUndefined(); - - cleanup(); - }); - - it("returns managed wallet for specific user", () => { - const { storage, cleanup } = setup(); - - const wallet1 = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false, - token: "token-cert1" - }); - - const wallet2 = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_2, - selected: false, - creditAmount: 200, - isTrialing: true - }); - - const managedWalletsMap = { - [USER_ID_1]: wallet1, - [USER_ID_2]: wallet2 - }; - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - const result1 = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(result1?.address).toBe(WALLET_ADDRESS_1); - expect(result1?.userId).toBe(USER_ID_1); - expect(result1?.creditAmount).toBe(100); - - const result2 = getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID); - expect(result2?.address).toBe(WALLET_ADDRESS_2); - expect(result2?.userId).toBe(USER_ID_2); - expect(result2?.creditAmount).toBe(200); - - cleanup(); - }); - - it("returns undefined for non-existent userId", () => { - const { storage, cleanup } = setup(); - - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true - }); - - const managedWalletsMap = { - [USER_ID_1]: managedWallet - }; - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - const result = getStorageManagedWallet("non-existent-user", MANAGED_NETWORK_ID); - expect(result).toBeUndefined(); - - cleanup(); - }); - - it("handles corrupt JSON gracefully", () => { - const { storage, cleanup } = setup(); - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, "invalid-json"); - - const result = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(result).toBeUndefined(); - - cleanup(); - }); - }); - - describe("updateStorageManagedWallet", () => { - it("creates new managed wallet", () => { - const { cleanup } = setup(); - - const wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - selected: true - }); - - const result = updateStorageManagedWallet(wallet); - - expect(result.name).toBe("Managed Wallet"); - expect(result.isManaged).toBe(true); - expect(result.address).toBe(WALLET_ADDRESS_1); - expect(result.selected).toBe(true); - - const stored = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(stored).toEqual(result); - - cleanup(); - }); - - it("updates existing managed wallet", () => { - const { cleanup } = setup(); - - const initial = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - token: "token-cert1" - }); - - updateStorageManagedWallet(initial); - - const updated = { - ...initial, - creditAmount: 200, - token: "token-cert2" - }; - - const result = updateStorageManagedWallet(updated); - - expect(result.creditAmount).toBe(200); - expect(result.token).toBe("token-cert2"); - - cleanup(); - }); - - it("preserves other users' wallets (CRITICAL: multi-user isolation)", () => { - const { cleanup } = setup(); - - const user1Wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - token: "token-cert1" - }); - - const user2Wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_2, - creditAmount: 200, - isTrialing: true, - token: "token-cert2" - }); - - updateStorageManagedWallet(user1Wallet); - updateStorageManagedWallet(user2Wallet); - - const storedUser1 = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - const storedUser2 = getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID); - - expect(storedUser1?.address).toBe(WALLET_ADDRESS_1); - expect(storedUser2?.address).toBe(WALLET_ADDRESS_2); - - updateStorageManagedWallet({ ...user1Wallet, creditAmount: 300 }); - - const updatedUser1 = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - const unchangedUser2 = getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID); - - expect(updatedUser1?.creditAmount).toBe(300); - expect(unchangedUser2?.creditAmount).toBe(200); - - cleanup(); - }); - - it("returns same object if no changes (optimization)", () => { - const { cleanup } = setup(); - - const wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - selected: true - }); - - const result1 = updateStorageManagedWallet(wallet); - const result2 = updateStorageManagedWallet(wallet); - - expect(result1).toEqual(result2); - - cleanup(); - }); - - it("defaults selected to false if not specified", () => { - const { cleanup } = setup(); - - const wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false - }); - delete (wallet as Partial).selected; - - const result = updateStorageManagedWallet(wallet); - expect(result.selected).toBe(false); - - cleanup(); - }); - - it("preserves selected state from previous wallet", () => { - const { cleanup } = setup(); - - const wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - selected: true, - token: "token-cert1" - }); - - updateStorageManagedWallet(wallet); - - const updated = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 200, - isTrialing: false, - token: "token-cert2" - }); - delete (updated as Partial).selected; - - const result = updateStorageManagedWallet(updated); - expect(result.selected).toBe(true); - - cleanup(); - }); - }); - - describe("updateStorageWallets", () => { - it("stores custodial wallets in wallets array", () => { - const { storage, cleanup } = setup(); - - const wallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: false }) - ]; - - updateStorageWallets(wallets); - - const stored = storage.get(`${NETWORK_ID}/wallets`); - expect(JSON.parse(stored!)).toEqual(wallets); - - cleanup(); - }); - - it("separates managed wallets to managed-wallets storage (CRITICAL: auto-migration)", () => { - const { storage, cleanup } = setup(); - - const custodialWallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: false }); - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_1, - selected: true - }); - - const wallets: LocalWallet[] = [custodialWallet, managedWallet]; - - updateStorageWallets(wallets); - - const custodialStored = JSON.parse(storage.get(`${NETWORK_ID}/wallets`)!); - expect(custodialStored).toHaveLength(1); - expect(custodialStored[0].address).toBe(WALLET_ADDRESS_1); - - const managedStored = JSON.parse(storage.get(`${NETWORK_ID}/managed-wallets`)!); - expect(managedStored[USER_ID_1].address).toBe(WALLET_ADDRESS_2); - - cleanup(); - }); - - it("preserves existing managed wallets for other users", () => { - const { storage, cleanup } = setup(); - - const existingUser2Wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_3, - userId: USER_ID_2, - selected: false, - creditAmount: 200, - isTrialing: true - }); - - storage.set(`${NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_2]: existingUser2Wallet })); - - const user1Wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false - }); - - const wallets: LocalWallet[] = [user1Wallet]; - - updateStorageWallets(wallets); - - const managedStored = JSON.parse(storage.get(`${NETWORK_ID}/managed-wallets`)!); - expect(managedStored[USER_ID_1].address).toBe(WALLET_ADDRESS_2); - expect(managedStored[USER_ID_2].address).toBe(WALLET_ADDRESS_3); - - cleanup(); - }); - - it("handles empty wallet array", () => { - const { storage, cleanup } = setup(); - - updateStorageWallets([]); - - const stored = storage.get(`${NETWORK_ID}/wallets`); - expect(JSON.parse(stored!)).toEqual([]); - - cleanup(); - }); - }); - - describe("updateWallet", () => { - it("updates custodial wallet", () => { - const { storage, cleanup } = setup(); - - const wallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: false }) - ]; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(wallets)); - - updateWallet(WALLET_ADDRESS_1, (w: LocalWallet) => ({ ...w, token: "new-token" })); - - const updated = getStorageWallets(); - const wallet1 = updated.find((w: LocalWallet) => w.address === WALLET_ADDRESS_1); +import { buildManagedLocalWallet } from "@tests/seeders/localWallet"; - expect(wallet1?.token).toBe("new-token"); - - cleanup(); - }); - - it("updates managed wallet", () => { - const { storage, cleanup } = setup(); - - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false - }); - - storage.set(`${NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_1]: managedWallet })); - - updateWallet(WALLET_ADDRESS_1, (w: LocalWallet) => ({ ...w, token: "new-token" }), NETWORK_ID); - - const updated = getStorageManagedWallet(USER_ID_1, NETWORK_ID); - expect(updated?.token).toBe("new-token"); - expect(updated?.userId).toBe(USER_ID_1); - - cleanup(); - }); - - it("does nothing if wallet not found", () => { - const { storage, cleanup } = setup(); - - const wallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([wallet])); - - updateWallet("non-existent-address", (w: LocalWallet) => ({ ...w, token: "new-token" })); - - const stored = getStorageWallets(); - expect(stored).toHaveLength(1); - expect(stored[0].token).toBeUndefined(); - - cleanup(); - }); - }); - - describe("deleteWalletFromStorage", () => { - it("removes wallet by address", () => { - const { storage, cleanup } = setup(); - - const wallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: false }) - ]; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(wallets)); - - const result = deleteWalletFromStorage(WALLET_ADDRESS_1, false); - - expect(result).toHaveLength(1); - expect(result[0].address).toBe(WALLET_ADDRESS_2); - - cleanup(); - }); - - it("selects first remaining wallet after deletion", () => { - const { storage, cleanup } = setup(); - - const wallets: LocalWallet[] = [ - buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }), - buildCustodialLocalWallet({ address: WALLET_ADDRESS_2, selected: false }) - ]; - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify(wallets)); - - const result = deleteWalletFromStorage(WALLET_ADDRESS_1, false); - - expect(result[0].selected).toBe(true); - - cleanup(); - }); - - it("removes wallet settings from localStorage", () => { - const { storage, mockLocalStorage, cleanup } = setup(); - - const wallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([wallet])); - storage.set(`${NETWORK_ID}/${WALLET_ADDRESS_1}/settings`, "{}"); - storage.set(`${NETWORK_ID}/${WALLET_ADDRESS_1}/provider.data`, "{}"); - - deleteWalletFromStorage(WALLET_ADDRESS_1, false); - - expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(`${NETWORK_ID}/${WALLET_ADDRESS_1}/settings`); - expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(`${NETWORK_ID}/${WALLET_ADDRESS_1}/provider.data`); - - cleanup(); - }); - - it("removes deployments when deleteDeployments is true", () => { - const { storage, cleanup } = setup(); - - const wallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([wallet])); - storage.set(`${NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`, "{}"); - storage.set(`${NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/456`, "{}"); +describe("walletUtils", () => { + const MANAGED_NETWORK_ID = "sandbox"; + const USER_ID_1 = "user-123"; + const USER_ID_2 = "user-456"; + const WALLET_ADDRESS_1 = "akash1abc123"; + const WALLET_ADDRESS_2 = "akash1def456"; - deleteWalletFromStorage(WALLET_ADDRESS_1, true); + describe("getStorageManagedWallet", () => { + it("returns undefined when userId is not provided", () => { + const { cleanup } = setup(); - expect(storage.get(`${NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`)).toBeUndefined(); - expect(storage.get(`${NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/456`)).toBeUndefined(); + expect(getStorageManagedWallet()).toBeUndefined(); + expect(getStorageManagedWallet("")).toBeUndefined(); cleanup(); }); - it("preserves deployments when deleteDeployments is false", () => { - const { storage, cleanup } = setup(); - - const wallet = buildCustodialLocalWallet({ address: WALLET_ADDRESS_1, selected: true }); - - storage.set(`${NETWORK_ID}/wallets`, JSON.stringify([wallet])); - storage.set(`${NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`, "{}"); - - deleteWalletFromStorage(WALLET_ADDRESS_1, false); + it("returns undefined when no managed wallets exist", () => { + const { cleanup } = setup(); - expect(storage.get(`${NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`)).toBeDefined(); + expect(getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID)).toBeUndefined(); cleanup(); }); - }); - describe("deleteManagedWalletFromStorage", () => { - it("removes managed wallet for specific user", () => { + it("returns managed wallet for the requested userId", () => { const { storage, cleanup } = setup(); const wallet1 = buildManagedLocalWallet({ @@ -715,9 +39,9 @@ describe("walletUtils", () => { userId: USER_ID_1, selected: true, creditAmount: 100, - isTrialing: false + isTrialing: false, + token: "token-cert1" }); - const wallet2 = buildManagedLocalWallet({ address: WALLET_ADDRESS_2, userId: USER_ID_2, @@ -726,356 +50,204 @@ describe("walletUtils", () => { isTrialing: true }); - const managedWalletsMap = { - [USER_ID_1]: wallet1, - [USER_ID_2]: wallet2 - }; - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - storage.set(`${MANAGED_NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`, "{}"); - - deleteManagedWalletFromStorage(USER_ID_1, MANAGED_NETWORK_ID); + storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_1]: wallet1, [USER_ID_2]: wallet2 })); - const remainingMap = JSON.parse(storage.get(`${MANAGED_NETWORK_ID}/managed-wallets`)!); - expect(remainingMap[USER_ID_1]).toBeUndefined(); - expect(remainingMap[USER_ID_2]).toBeDefined(); - expect(storage.get(`${MANAGED_NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`)).toBeUndefined(); + expect(getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID)?.address).toBe(WALLET_ADDRESS_1); + expect(getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID)?.address).toBe(WALLET_ADDRESS_2); cleanup(); }); - it("removes managed-wallets key when last wallet is deleted", () => { + it("returns undefined for non-existent userId", () => { const { storage, cleanup } = setup(); - const wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false - }); - - const managedWalletsMap = { - [USER_ID_1]: wallet - }; - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - deleteManagedWalletFromStorage(USER_ID_1, MANAGED_NETWORK_ID); + const managedWallet = buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, selected: true }); + storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_1]: managedWallet })); - expect(storage.get(`${MANAGED_NETWORK_ID}/managed-wallets`)).toBeUndefined(); + expect(getStorageManagedWallet("non-existent-user", MANAGED_NETWORK_ID)).toBeUndefined(); cleanup(); }); - it("does nothing if userId is empty", () => { + it("handles corrupt JSON gracefully", () => { const { storage, cleanup } = setup(); - const wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false - }); - - const managedWalletsMap = { - [USER_ID_1]: wallet - }; - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify(managedWalletsMap)); - - deleteManagedWalletFromStorage("", MANAGED_NETWORK_ID); - - const stored = storage.get(`${MANAGED_NETWORK_ID}/managed-wallets`); - expect(stored).toBeDefined(); - - cleanup(); - }); - - it("does nothing if wallet doesn't exist", () => { - const { cleanup } = setup(); + storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, "invalid-json"); - deleteManagedWalletFromStorage("non-existent-user", MANAGED_NETWORK_ID); + expect(getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID)).toBeUndefined(); cleanup(); }); }); - describe("ensureUserManagedWalletOwnership", () => { - it("adds managed wallet to shared wallet list if missing", () => { - const { storage, cleanup } = setup(); + describe("updateStorageManagedWallet", () => { + it("creates a new managed wallet entry", () => { + const { cleanup } = setup(); - const managedWallet = buildManagedLocalWallet({ + const wallet = buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, - selected: false, creditAmount: 100, - isTrialing: false + isTrialing: false, + selected: true }); - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_1]: managedWallet })); - - ensureUserManagedWalletOwnership(USER_ID_1); + const result = updateStorageManagedWallet(wallet); - const wallets = getStorageWallets(MANAGED_NETWORK_ID); - const found = wallets.find((w: LocalWallet) => w.isManaged && (w as { userId?: string }).userId === USER_ID_1); + expect(result?.name).toBe("Managed Wallet"); + expect(result?.isManaged).toBe(true); + expect(result?.address).toBe(WALLET_ADDRESS_1); + expect(result?.selected).toBe(true); - expect(found).toBeDefined(); - expect(found?.address).toBe(WALLET_ADDRESS_1); + const stored = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); + expect(stored).toEqual(result); cleanup(); }); - it("sets managed wallet as selected when adding to list", () => { - const { storage, cleanup } = setup(); - - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: false, - creditAmount: 100, - isTrialing: false - }); - - const custodialWallet = buildCustodialLocalWallet({ - address: WALLET_ADDRESS_2, - selected: true - }); - - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_1]: managedWallet })); - storage.set(`${MANAGED_NETWORK_ID}/wallets`, JSON.stringify([custodialWallet])); + it("merges partial updates with the existing entry", () => { + const { cleanup } = setup(); - ensureUserManagedWalletOwnership(USER_ID_1); + updateStorageManagedWallet( + buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, creditAmount: 100, isTrialing: false, token: "token-cert1" }) + ); - const wallets = getStorageWallets(MANAGED_NETWORK_ID); - const managed = wallets.find((w: LocalWallet) => w.isManaged && (w as { userId?: string }).userId === USER_ID_1); + const result = updateStorageManagedWallet({ userId: USER_ID_1, token: "token-cert2" }); - expect(managed?.selected).toBe(true); + expect(result?.token).toBe("token-cert2"); + expect(result?.address).toBe(WALLET_ADDRESS_1); + expect(result?.creditAmount).toBe(100); cleanup(); }); - it("does nothing if wallet doesn't exist", () => { + it("returns undefined when no prior entry and required fields missing", () => { const { cleanup } = setup(); - ensureUserManagedWalletOwnership("non-existent-user"); - - const wallets = getStorageWallets(MANAGED_NETWORK_ID); - expect(wallets).toHaveLength(0); + const result = updateStorageManagedWallet({ userId: USER_ID_1, token: "token-cert2" }); + expect(result).toBeUndefined(); cleanup(); }); - it("updates wallet list if address changed", () => { - const { storage, cleanup } = setup(); - - const oldManagedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false - }); + it("preserves other users' entries (multi-user isolation)", () => { + const { cleanup } = setup(); - const newManagedWallet = buildManagedLocalWallet({ - ...oldManagedWallet, - address: WALLET_ADDRESS_2 - }); + const user1Wallet = buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, creditAmount: 100, isTrialing: false }); + const user2Wallet = buildManagedLocalWallet({ address: WALLET_ADDRESS_2, userId: USER_ID_2, creditAmount: 200, isTrialing: true }); - storage.set(`${MANAGED_NETWORK_ID}/wallets`, JSON.stringify([oldManagedWallet])); - storage.set(`${MANAGED_NETWORK_ID}/managed-wallets`, JSON.stringify({ [USER_ID_1]: newManagedWallet })); + updateStorageManagedWallet(user1Wallet); + updateStorageManagedWallet(user2Wallet); - ensureUserManagedWalletOwnership(USER_ID_1); + updateStorageManagedWallet({ ...user1Wallet, creditAmount: 300 }); - const custodialWallets = JSON.parse(storage.get(`${MANAGED_NETWORK_ID}/wallets`)!); - expect(custodialWallets.some((w: LocalWallet) => w.address === WALLET_ADDRESS_2)).toBe(false); + expect(getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID)?.creditAmount).toBe(300); + expect(getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID)?.creditAmount).toBe(200); cleanup(); }); - }); - describe("CRITICAL BUG REGRESSION TESTS: No automatic deletion on disconnect", () => { - it("managed wallet persists when user becomes undefined (disconnect scenario)", () => { + it("preserves selected flag from previous entry when omitted", () => { const { cleanup } = setup(); - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false, - token: "token-cert1" - }); - - updateStorageManagedWallet(managedWallet); - - const beforeDisconnect = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(beforeDisconnect).toBeDefined(); + updateStorageManagedWallet( + buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, creditAmount: 100, isTrialing: false, selected: true }) + ); - const afterDisconnect = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(afterDisconnect).toEqual(beforeDisconnect); - expect(afterDisconnect?.token).toBe("token-cert1"); + const result = updateStorageManagedWallet({ userId: USER_ID_1, creditAmount: 200 }); + expect(result?.selected).toBe(true); cleanup(); }); + }); - it("managed wallet persists across network disconnections", () => { - const { cleanup } = setup(); + describe("deleteManagedWalletFromStorage", () => { + it("removes managed wallet for the requested user", () => { + const { storage, cleanup } = setup(); - const managedWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - selected: true, - creditAmount: 100, - isTrialing: false, - token: "token-important-cert" - }); + storage.set( + `${MANAGED_NETWORK_ID}/managed-wallets`, + JSON.stringify({ + [USER_ID_1]: buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, selected: true, creditAmount: 100, isTrialing: false }), + [USER_ID_2]: buildManagedLocalWallet({ address: WALLET_ADDRESS_2, userId: USER_ID_2, selected: false, creditAmount: 200, isTrialing: true }) + }) + ); + storage.set(`${MANAGED_NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`, "{}"); - updateStorageManagedWallet(managedWallet); + deleteManagedWalletFromStorage(USER_ID_1, MANAGED_NETWORK_ID); - for (let i = 0; i < 10; i++) { - const retrieved = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(retrieved).toBeDefined(); - expect(retrieved?.token).toBe("token-important-cert"); - } + const remaining = JSON.parse(storage.get(`${MANAGED_NETWORK_ID}/managed-wallets`)!); + expect(remaining[USER_ID_1]).toBeUndefined(); + expect(remaining[USER_ID_2]).toBeDefined(); + expect(storage.get(`${MANAGED_NETWORK_ID}/${WALLET_ADDRESS_1}/deployments/123`)).toBeUndefined(); cleanup(); }); - it("multiple users can disconnect and reconnect without losing data", () => { - const { cleanup } = setup(); - - const user1Wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - token: "token-user1-cert" - }); - - const user2Wallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_2, - creditAmount: 200, - isTrialing: true, - token: "token-user2-cert" - }); + it("removes the managed-wallets key when the last entry is deleted", () => { + const { storage, cleanup } = setup(); - updateStorageManagedWallet(user1Wallet); - updateStorageManagedWallet(user2Wallet); + storage.set( + `${MANAGED_NETWORK_ID}/managed-wallets`, + JSON.stringify({ + [USER_ID_1]: buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, selected: true, creditAmount: 100, isTrialing: false }) + }) + ); - const user1Retrieved = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - const user2Retrieved = getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID); + deleteManagedWalletFromStorage(USER_ID_1, MANAGED_NETWORK_ID); - expect(user1Retrieved?.token).toBe("token-user1-cert"); - expect(user2Retrieved?.token).toBe("token-user2-cert"); + expect(storage.get(`${MANAGED_NETWORK_ID}/managed-wallets`)).toBeUndefined(); cleanup(); }); - }); - - describe("Multi-user scenarios", () => { - it("User A and User B can both have managed wallets on same browser", () => { - const { cleanup } = setup(); - const userAWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - token: "token-userA-cert" - }); - - const userBWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_2, - creditAmount: 200, - isTrialing: true, - token: "token-userB-cert" - }); + it("does nothing when userId is empty", () => { + const { storage, cleanup } = setup(); - updateStorageManagedWallet(userAWallet); - updateStorageManagedWallet(userBWallet); + storage.set( + `${MANAGED_NETWORK_ID}/managed-wallets`, + JSON.stringify({ + [USER_ID_1]: buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, selected: true, creditAmount: 100, isTrialing: false }) + }) + ); - const storedA = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - const storedB = getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID); + deleteManagedWalletFromStorage("", MANAGED_NETWORK_ID); - expect(storedA?.userId).toBe(USER_ID_1); - expect(storedB?.userId).toBe(USER_ID_2); - expect(storedA?.token).toBe("token-userA-cert"); - expect(storedB?.token).toBe("token-userB-cert"); + expect(storage.get(`${MANAGED_NETWORK_ID}/managed-wallets`)).toBeDefined(); cleanup(); }); - it("User B login doesn't erase User A data", () => { + it("does nothing when the wallet doesn't exist", () => { const { cleanup } = setup(); - const userAWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - token: "token-userA-cert" - }); - - updateStorageManagedWallet(userAWallet); - - const userBWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_2, - creditAmount: 200, - isTrialing: true, - token: "token-userB-cert", - selected: true - }); - - updateStorageManagedWallet(userBWallet); - - const userAAfterBLogin = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - expect(userAAfterBLogin).toBeDefined(); - expect(userAAfterBLogin?.token).toBe("token-userA-cert"); - expect(userAAfterBLogin?.userId).toBe(USER_ID_1); + deleteManagedWalletFromStorage("non-existent-user", MANAGED_NETWORK_ID); cleanup(); }); + }); - it("Users can switch between accounts without data loss", () => { + describe("ensureUserManagedWalletOwnership", () => { + it("marks the user's managed wallet as selected when it is not", () => { const { cleanup } = setup(); - const userAWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_1, - userId: USER_ID_1, - creditAmount: 100, - isTrialing: false, - token: "token-userA-cert", - selected: true - }); + updateStorageManagedWallet( + buildManagedLocalWallet({ address: WALLET_ADDRESS_1, userId: USER_ID_1, selected: false, creditAmount: 100, isTrialing: false }) + ); - const userBWallet = buildManagedLocalWallet({ - address: WALLET_ADDRESS_2, - userId: USER_ID_2, - creditAmount: 200, - isTrialing: true, - token: "token-userB-cert", - selected: false - }); + ensureUserManagedWalletOwnership(USER_ID_1); - updateStorageManagedWallet(userAWallet); - updateStorageManagedWallet(userBWallet); + expect(getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID)?.selected).toBe(true); - updateStorageManagedWallet({ ...userBWallet, selected: true }); - updateStorageManagedWallet({ ...userAWallet, selected: false }); + cleanup(); + }); - const allWallets = getStorageWallets(MANAGED_NETWORK_ID); - expect(allWallets).toHaveLength(2); + it("is a no-op when the wallet doesn't exist", () => { + const { cleanup } = setup(); - const userAStored = getStorageManagedWallet(USER_ID_1, MANAGED_NETWORK_ID); - const userBStored = getStorageManagedWallet(USER_ID_2, MANAGED_NETWORK_ID); + ensureUserManagedWalletOwnership("non-existent-user"); - expect(userAStored?.token).toBe("token-userA-cert"); - expect(userBStored?.token).toBe("token-userB-cert"); + expect(getStorageManagedWallet("non-existent-user", MANAGED_NETWORK_ID)).toBeUndefined(); cleanup(); }); @@ -1095,10 +267,7 @@ describe("walletUtils", () => { clear: vi.fn(() => { storage.clear(); }), - key: vi.fn((index: number) => { - const keys = Array.from(storage.keys()); - return keys[index] ?? null; - }), + key: vi.fn((index: number) => Array.from(storage.keys())[index] ?? null), length: 0 }); @@ -1111,16 +280,10 @@ describe("walletUtils", () => { const originalLocalStorage = global.localStorage; const localStorageProxy = new Proxy(mockLocalStorage, { - ownKeys: () => { - return Array.from(storage.keys()); - }, - getOwnPropertyDescriptor: (target, key) => { + ownKeys: () => Array.from(storage.keys()), + getOwnPropertyDescriptor: (_target, key) => { if (storage.has(key as string)) { - return { - enumerable: true, - configurable: true, - value: storage.get(key as string) - }; + return { enumerable: true, configurable: true, value: storage.get(key as string) }; } return undefined; } @@ -1132,11 +295,6 @@ describe("walletUtils", () => { configurable: true }); - Object.defineProperty(networkStore, "selectedNetworkId", { - value: NETWORK_ID, - writable: true, - configurable: true - }); Object.defineProperty(browserEnvConfig, "NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID", { value: MANAGED_NETWORK_ID, writable: true, @@ -1144,7 +302,6 @@ describe("walletUtils", () => { }); return { - mockLocalStorage, storage, cleanup: () => { Object.defineProperty(global, "localStorage", { diff --git a/apps/deploy-web/src/utils/walletUtils.ts b/apps/deploy-web/src/utils/walletUtils.ts index aa50bd0f70..e2ba815435 100644 --- a/apps/deploy-web/src/utils/walletUtils.ts +++ b/apps/deploy-web/src/utils/walletUtils.ts @@ -4,18 +4,14 @@ import { isEqual } from "lodash"; import { browserEnvConfig } from "@src/config/browser-env.config"; import { ErrorHandlerService } from "@src/services/error-handler/error-handler.service"; -import networkStore from "@src/store/networkStore"; const logger = new LoggerService({ name: "walletUtils" }); const errorHandler = new ErrorHandlerService(logger); -interface BaseLocalWallet { +export interface ManagedLocalWallet { address: string; token?: string; selected: boolean; -} - -interface ManagedLocalWallet extends BaseLocalWallet { name: "Managed Wallet"; isManaged: true; userId: string; @@ -23,22 +19,10 @@ interface ManagedLocalWallet extends BaseLocalWallet { isTrialing: boolean; } -interface CustodialLocalWallet extends BaseLocalWallet { - name: string; - isManaged: false; -} -export type LocalWallet = ManagedLocalWallet | CustodialLocalWallet; - function getManagedWalletsStorageKey(networkId: NetworkId): string { return `${networkId}/managed-wallets`; } -export function getSelectedStorageWallet() { - const wallets = getStorageWallets(); - - return wallets.find(w => w.selected) ?? wallets[0] ?? null; -} - export function getStorageManagedWallet(userId?: string, networkId?: NetworkId): ManagedLocalWallet | undefined { if (!userId || typeof window === "undefined") { return undefined; @@ -67,18 +51,31 @@ export function getStorageManagedWallet(userId?: string, networkId?: NetworkId): } } -export function updateStorageManagedWallet( - wallet: Pick & { selected?: boolean } -): ManagedLocalWallet { +type ManagedWalletUpdate = { userId: string } & Partial>; + +export function updateStorageManagedWallet(update: ManagedWalletUpdate): ManagedLocalWallet | undefined { const networkId = browserEnvConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID; - const prev = getStorageManagedWallet(wallet.userId, networkId); + const prev = getStorageManagedWallet(update.userId, networkId); + + if (!prev && (update.address === undefined || update.creditAmount === undefined || update.isTrialing === undefined)) { + errorHandler.reportError({ + error: new Error("Cannot create managed wallet entry without address, creditAmount and isTrialing"), + severity: "warning", + tags: { context: "walletUtils.updateStorageManagedWallet" }, + userId: update.userId + }); + return undefined; + } const next: ManagedLocalWallet = { - ...prev, - ...wallet, + address: update.address ?? prev!.address, + token: update.token ?? prev?.token, + creditAmount: update.creditAmount ?? prev!.creditAmount, + isTrialing: update.isTrialing ?? prev!.isTrialing, + userId: update.userId, name: "Managed Wallet", isManaged: true, - selected: typeof wallet.selected === "boolean" ? wallet.selected : prev?.selected ?? false + selected: typeof update.selected === "boolean" ? update.selected : prev?.selected ?? false }; if (isEqual(prev, next)) { @@ -102,7 +99,7 @@ export function updateStorageManagedWallet( } } - walletsMap[wallet.userId] = next; + walletsMap[update.userId] = next; localStorage.setItem(key, JSON.stringify(walletsMap)); return next; @@ -113,179 +110,54 @@ export function deleteManagedWalletFromStorage(userId: string, networkId?: Netwo return; } - const wallet = getStorageManagedWallet(userId, networkId); - if (wallet) { - const selectedNetworkId: NetworkId = networkId || browserEnvConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID; - const key = getManagedWalletsStorageKey(selectedNetworkId); - const walletsMapStr = localStorage.getItem(key); - - if (walletsMapStr) { - try { - const walletsMap: Record = JSON.parse(walletsMapStr); - delete walletsMap[userId]; - - if (Object.keys(walletsMap).length > 0) { - localStorage.setItem(key, JSON.stringify(walletsMap)); - } else { - localStorage.removeItem(key); - } - } catch (error) { - errorHandler.reportError({ - error, - severity: "warning", - tags: { context: "walletUtils.deleteManagedWalletFromStorage" }, - walletsMapStr, - userId - }); - localStorage.removeItem(key); - } - } - - deleteWalletFromStorage(wallet.address, true, networkId); - } -} - -export function getStorageWallets(networkId?: NetworkId) { - if (typeof window === "undefined") { - return []; - } - - const selectedNetworkId: NetworkId = networkId || networkStore.selectedNetworkId; - let wallets: LocalWallet[] = []; + const selectedNetworkId: NetworkId = networkId || browserEnvConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID; + const key = getManagedWalletsStorageKey(selectedNetworkId); + const walletsMapStr = localStorage.getItem(key); - const custodialWalletsStr = localStorage.getItem(`${selectedNetworkId}/wallets`); - if (custodialWalletsStr) { - try { - wallets = JSON.parse(custodialWalletsStr) as LocalWallet[]; - } catch (error) { - errorHandler.reportError({ - error, - severity: "warning", - tags: { context: "walletUtils.getStorageWallets" }, - custodialWalletsStr - }); - } + if (!walletsMapStr) { + return; } - const managedWalletsKey = getManagedWalletsStorageKey(selectedNetworkId); - const managedWalletsMapStr = localStorage.getItem(managedWalletsKey); - - if (managedWalletsMapStr) { - try { - const managedWalletsMap = JSON.parse(managedWalletsMapStr) as Record; - const managedWallets = Object.values(managedWalletsMap); - - const managedWalletAddresses = new Set(managedWallets.map(w => w.address)); - const custodialWallets = wallets.filter(w => !w.isManaged || !managedWalletAddresses.has(w.address)); - - const selectedManagedWallet = managedWallets.find(w => w.selected); - const mergedWallets = [...custodialWallets, ...managedWallets]; - - if (selectedManagedWallet) { - return mergedWallets.map(w => ({ - ...w, - selected: w.address === selectedManagedWallet.address && w.isManaged - })); - } + try { + const walletsMap: Record = JSON.parse(walletsMapStr); + const wallet = walletsMap[userId]; - return mergedWallets; - } catch (error) { - errorHandler.reportError({ - error, - severity: "warning", - tags: { context: "walletUtils.getStorageWallets.managedWallets" }, - managedWalletsMapStr - }); - return wallets; + if (!wallet) { + return; } - } - return wallets; -} + delete walletsMap[userId]; -export function updateWallet(address: string, func: (w: LocalWallet) => LocalWallet, networkId?: NetworkId) { - const wallets = getStorageWallets(networkId); - let wallet = wallets.find(w => w.address === address); - - if (wallet) { - wallet = func(wallet); - - const newWallets = wallets.map(w => (w.address === address ? (wallet as LocalWallet) : w)); - updateStorageWallets(newWallets, networkId); - } -} - -export function updateStorageWallets(wallets: LocalWallet[], networkId?: NetworkId) { - const selectedNetworkId = networkId || networkStore.selectedNetworkId; - - const managedWallets = wallets.filter(w => w.isManaged) as ManagedLocalWallet[]; - const custodialWallets = wallets.filter(w => !w.isManaged); - - if (managedWallets.length > 0) { - const managedWalletsKey = getManagedWalletsStorageKey(selectedNetworkId); - const existingMapStr = localStorage.getItem(managedWalletsKey); - let existingMap: Record = {}; - - if (existingMapStr) { - try { - existingMap = JSON.parse(existingMapStr); - } catch (error) { - errorHandler.reportError({ - error, - severity: "warning", - tags: { context: "walletUtils.updateStorageWallets" }, - existingMapStr - }); - } + if (Object.keys(walletsMap).length > 0) { + localStorage.setItem(key, JSON.stringify(walletsMap)); + } else { + localStorage.removeItem(key); } - managedWallets.forEach(wallet => { - existingMap[wallet.userId] = wallet; - }); - - localStorage.setItem(managedWalletsKey, JSON.stringify(existingMap)); - } - - localStorage.setItem(`${selectedNetworkId}/wallets`, JSON.stringify(custodialWallets)); -} - -export function deleteWalletFromStorage(address: string, deleteDeployments: boolean, networkId?: NetworkId) { - const selectedNetworkId = networkId || networkStore.selectedNetworkId; - const wallets = getStorageWallets(); - const newWallets = wallets.filter(w => w.address !== address).map((w, i) => ({ ...w, selected: i === 0 })); + localStorage.removeItem(`${selectedNetworkId}/${wallet.address}/settings`); + localStorage.removeItem(`${selectedNetworkId}/${wallet.address}/provider.data`); - updateStorageWallets(newWallets); - - localStorage.removeItem(`${selectedNetworkId}/${address}/settings`); - localStorage.removeItem(`${selectedNetworkId}/${address}/provider.data`); - - if (deleteDeployments) { - const deploymentKeys = Object.keys(localStorage).filter(key => key.startsWith(`${selectedNetworkId}/${address}/deployments/`)); + const deploymentKeys = Object.keys(localStorage).filter(k => k.startsWith(`${selectedNetworkId}/${wallet.address}/deployments/`)); for (const deploymentKey of deploymentKeys) { localStorage.removeItem(deploymentKey); } + } catch (error) { + errorHandler.reportError({ + error, + severity: "warning", + tags: { context: "walletUtils.deleteManagedWalletFromStorage" }, + walletsMapStr, + userId + }); + localStorage.removeItem(key); } - - return newWallets; -} - -export function useSelectedWalletFromStorage() { - return getSelectedStorageWallet(); } export function ensureUserManagedWalletOwnership(userId: string) { const networkId = browserEnvConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID; const wallet = getStorageManagedWallet(userId, networkId); - if (wallet) { - const wallets = getStorageWallets(networkId); - const updatedWallets = wallets.map(w => { - if (w.isManaged) { - return { ...w, selected: w.userId === userId }; - } - return { ...w, selected: false }; - }); - - updateStorageWallets(updatedWallets, networkId); + if (wallet && !wallet.selected) { + updateStorageManagedWallet({ userId, selected: true }); } } diff --git a/apps/deploy-web/tests/seeders/localWallet.ts b/apps/deploy-web/tests/seeders/localWallet.ts index 6baee9ccfc..29e8691020 100644 --- a/apps/deploy-web/tests/seeders/localWallet.ts +++ b/apps/deploy-web/tests/seeders/localWallet.ts @@ -1,11 +1,8 @@ import { faker } from "@faker-js/faker"; -import type { LocalWallet } from "@src/utils/walletUtils"; +import type { ManagedLocalWallet } from "@src/utils/walletUtils"; import { genWalletAddress } from "./wallet"; -type ManagedLocalWallet = Extract; -type CustodialLocalWallet = Extract; - export const buildManagedLocalWallet = (overrides: Partial = {}): ManagedLocalWallet => ({ name: "Managed Wallet", address: genWalletAddress(), @@ -16,11 +13,3 @@ export const buildManagedLocalWallet = (overrides: Partial = isTrialing: faker.datatype.boolean(), ...overrides }); - -export const buildCustodialLocalWallet = (overrides: Partial = {}): CustodialLocalWallet => ({ - name: faker.internet.username(), - address: genWalletAddress(), - selected: faker.datatype.boolean(), - isManaged: false, - ...overrides -}); diff --git a/apps/deploy-web/tests/seeders/wallet.ts b/apps/deploy-web/tests/seeders/wallet.ts index 77d52276aa..4552177cfb 100644 --- a/apps/deploy-web/tests/seeders/wallet.ts +++ b/apps/deploy-web/tests/seeders/wallet.ts @@ -14,12 +14,12 @@ export const buildWallet = (overrides: Partial = {}): logout: vi.fn(), signAndBroadcastTx: vi.fn(), isManaged: true, - isCustodial: false, + denom: "uact", isWalletLoading: false, isTrialing: false, isOnboarding: false, creditAmount: faker.number.float({ min: 0, max: 1000 }), - switchWalletType: vi.fn(), + topUpMinAmountUsd: 20, hasManagedWallet: true, managedWalletError: undefined, ...overrides From 1b88b2f275d9e47f936a11bb8ed89d29334e6cc2 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Fri, 29 May 2026 17:00:26 -0500 Subject: [PATCH 2/3] refactor(wallet): reuse useNotificator and AddFundsLink, drop dead self-custody branches Replaces the duplicated transaction-error snackbar wiring in useSignAndBroadcast with the existing useNotificator helper, and swaps the inline Add Funds for AddFundsLink so the verified-login gate is back in place. Also removes branches that are unreachable now that deploy-web is managed-wallet-only (uakt denom guard, useFlag dependency in ConnectManagedWalletButton) and trims a stale narration comment in WalletProvider. --- .../ManifestEdit/ManifestEdit.tsx | 5 +- .../OnboardingContainer.tsx | 7 +-- .../ConnectManagedWalletButton.spec.tsx | 9 --- .../wallet/ConnectManagedWalletButton.tsx | 2 - .../context/WalletProvider/WalletProvider.tsx | 12 +--- .../WalletProvider/useSignAndBroadcast.tsx | 57 ++++++------------- 6 files changed, 20 insertions(+), 72 deletions(-) diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx index 4166a27009..f3386e1613 100644 --- a/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx +++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit/ManifestEdit.tsx @@ -231,10 +231,7 @@ export const ManifestEdit: React.FunctionComponent = ({ } sdl = appendAuditorRequirement(sdl); - - if (wallet.denom !== "uakt") { - sdl = replaceSdlDenom(sdl, wallet.denom); - } + sdl = replaceSdlDenom(sdl, wallet.denom); const dd = await createAndValidateDeploymentData(sdl, null, deposit); diff --git a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx index b42477ee8b..d552b8b9f5 100644 --- a/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx +++ b/apps/deploy-web/src/components/onboarding/OnboardingContainer/OnboardingContainer.tsx @@ -270,15 +270,12 @@ export const OnboardingContainer: React.FunctionComponent { }); function setup(input?: { isRegistered?: boolean; isBlockchainDown?: boolean }) { - const mockUseFlag = vi.fn((flag: string) => { - if (flag === "notifications_general_alerts_update") { - return true; - } - return false; - }) as unknown as ReturnType; - return render( mockUseFlag, useRouter: () => mock(), useSettings: () => ({ settings: { diff --git a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx index 6349b832e3..1bfa586d0c 100644 --- a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx +++ b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx @@ -8,11 +8,9 @@ import { useRouter } from "next/navigation"; import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; -import { useFlag } from "@src/hooks/useFlag"; import { UrlService } from "@src/utils/urlUtils"; const DEPENDENCIES = { - useFlag, useRouter, useSettings }; diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index e9c57dae78..a6b8077b92 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -74,17 +74,7 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr setSettingsId(walletAddress || null); }, [walletAddress, setSettingsId]); - /** - * Force every visitor onto the managed-wallet network on first load. - * - * Why unconditional: every authenticated user gets a managed trial wallet, - * and the entire console experience targets that network. Firing on first - * load means the (one-time) reload happens before any user action. - * - * Why `reload()` not `href = home`: a hard nav to `/` was sending the user - * back to home after a successful deploy if the wallet-type flip happened - * post-success. Reloading in place keeps the URL. - */ + // Reload in place (not nav home) so a successful deploy doesn't bounce the user back to `/`. useEffect(() => { if (selectedNetworkId === appConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID) return; setSelectedNetworkId(appConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID); diff --git a/apps/deploy-web/src/context/WalletProvider/useSignAndBroadcast.tsx b/apps/deploy-web/src/context/WalletProvider/useSignAndBroadcast.tsx index 8cd9bd55e5..91825ded22 100644 --- a/apps/deploy-web/src/context/WalletProvider/useSignAndBroadcast.tsx +++ b/apps/deploy-web/src/context/WalletProvider/useSignAndBroadcast.tsx @@ -3,10 +3,11 @@ import React, { useState } from "react"; import { buttonVariants, Snackbar } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; import type { EncodeObject } from "@cosmjs/proto-signing"; -import Link from "next/link"; import { useSnackbar } from "notistack"; import type { LoadingState } from "@src/components/layout/TransactionModal"; +import { AddFundsLink } from "@src/components/user/AddFundsLink"; +import { useNotificator } from "@src/hooks/useNotificator"; import { useUser } from "@src/hooks/useUser"; import { UrlService } from "@src/utils/urlUtils"; import { useServices } from "../ServicesProvider"; @@ -21,19 +22,15 @@ export type UseSignAndBroadcastReturn = { loadingState: LoadingState | undefined; }; -const SUPPORT_EMAIL = "support@akash.network"; - export function useSignAndBroadcast({ refetchBalances }: UseSignAndBroadcastInput): UseSignAndBroadcastReturn { const { tx: txHttpService, analyticsService } = useServices(); const { user } = useUser(); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const notificator = useNotificator(); const [loadingState, setLoadingState] = useState(undefined); const showTransactionErrorSnackbar = (snackTitle: string, snackMessage?: string) => { - enqueueSnackbar(} iconVariant="error" />, { - variant: "error", - autoHideDuration: 10000 - }); + notificator.error(snackMessage?.trim() || "An error has occurred", { title: snackTitle }); }; const showAddCreditsSnackbar = (snackTitle: string, snackMessage?: string) => { @@ -61,37 +58,15 @@ export function useSignAndBroadcast({ refetchBalances }: UseSignAndBroadcastInpu return { signAndBroadcastTx, loadingState }; } -const AddCreditsSnackbarContent: React.FC<{ message?: string; onAction?: () => void }> = ({ message, onAction }) => { - const { analyticsService } = useServices(); - return ( - <> - {message &&
{message}
} - { - analyticsService.track("add_funds_btn_clk"); - onAction?.(); - }} - > - Add Funds - - - ); -}; - -const TransactionErrorSnackbarContent: React.FC<{ message?: string }> = ({ message }) => { - const safeMessage = message?.trim() || "An error has occurred"; - return ( - <> - {safeMessage} -
-
- Need help?{" "} - - Contact {SUPPORT_EMAIL} - -
- - ); -}; +const AddCreditsSnackbarContent: React.FC<{ message?: string; onAction?: () => void }> = ({ message, onAction }) => ( + <> + {message &&
{message}
} + + Add Funds + + +); From fd66c0cca410070bb2b64949f1018c265b0c2bc2 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:50:29 -0400 Subject: [PATCH 3/3] test(deployment): assert auto top-up switch renders, not just its label --- .../DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx index 0477d9fde1..cb0c3cd294 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentDetailTopBar/DeploymentDetailTopBar.spec.tsx @@ -110,11 +110,12 @@ describe(DeploymentDetailTopBar.name, () => { }); it("renders auto top-up section", () => { - setup({ + const deps = setup({ deployment: createDeployment({ state: "active" }) }); expect(screen.getByText("Auto top-up")).toBeInTheDocument(); + expect(deps.Switch).toHaveBeenCalledWith(expect.objectContaining({ checked: false, disabled: false, onCheckedChange: expect.any(Function) }), {}); }); it("renders DeploymentDepositModal after Add funds click", () => {