From 155113c0aee2913d2cf4da839126a4a10768de05 Mon Sep 17 00:00:00 2001 From: Yaroslav Grishajev Date: Wed, 28 Aug 2024 18:25:34 +0200 Subject: [PATCH] feat(billing): implement wallet type switch refs #247 --- apps/api/deploy.yml | 82 ++++++++++++++++ apps/api/package.json | 6 +- .../src/components/layout/WalletStatus.tsx | 96 +++++++++++-------- .../src/components/user/LoginRequiredLink.tsx | 48 +--------- .../wallet/ConnectManagedWalletButton.tsx | 10 +- .../CertificateProviderContext.tsx | 25 +++-- .../context/WalletProvider/WalletProvider.tsx | 74 ++++++++++---- .../hooks/useLoginRequiredEventHandler.tsx | 43 +++++++++ apps/deploy-web/src/hooks/useManagedWallet.ts | 19 +++- apps/deploy-web/src/utils/walletUtils.ts | 95 +++++++++++++----- 10 files changed, 344 insertions(+), 154 deletions(-) create mode 100644 apps/api/deploy.yml create mode 100644 apps/deploy-web/src/hooks/useLoginRequiredEventHandler.tsx diff --git a/apps/api/deploy.yml b/apps/api/deploy.yml new file mode 100644 index 000000000..e5bb66502 --- /dev/null +++ b/apps/api/deploy.yml @@ -0,0 +1,82 @@ +--- +version: "2.0" + +services: + api: + image: : + depends-on: cloud-sql-proxy + env: + - AkashlyticsGithubPAT= + - AkashSandboxDatabaseCS= + - UserDatabaseCS= + - SecretToken= + - NETWORK= + - MASTER_WALLET_MNEMONIC= + - POSTGRES_DB_URI= + - ANONYMOUS_USER_TOKEN_SECRET= + - SENTRY_DSN= + - SENTRY_SERVER_NAME= + - DEPLOYMENT_ENV= + - STRIPE_SECRET_KEY= + - STRIPE_WEBHOOK_SECRET= + expose: + - port: 3080 + as: 80 + accept: + - api-sandbox-staging.cloudmos.io + to: + - global: true + cloud-sql-proxy: + image: redm4x/cloud-sql-proxy:1.31.2 + env: + - KeyPath=/custom/proxy_key.json + - ConnectionName=cloudmos-explorer:us-central1:cloudmos-postgresql + - Token= + command: + - "sh" + - "-c" + args: + - 'echo "${Token}" | base64 --decode > $KeyPath && /cloud_sql_proxy -instances=$ConnectionName=tcp:0.0.0.0:5432 -credential_file=$KeyPath' + expose: + - port: 5432 + as: 5432 + to: + - service: api + +profiles: + compute: + api: + resources: + cpu: + units: 1 + memory: + size: 2Gi + storage: + size: 2Gi + cloud-sql-proxy: + resources: + cpu: + units: 0.5 + memory: + size: 512mi + storage: + size: 2Gi + placement: + dcloud: + pricing: + api: + denom: uakt + amount: 1000 + cloud-sql-proxy: + denom: uakt + amount: 1000 + +deployment: + api: + dcloud: + profile: api + count: 1 + cloud-sql-proxy: + dcloud: + profile: cloud-sql-proxy + count: 1 diff --git a/apps/api/package.json b/apps/api/package.json index dadf44fd0..589c4c0f5 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,8 +10,10 @@ "author": "Akash Network", "main": "server.js", "scripts": { - "build": "webpack --config webpack.prod.js", - "console": "webpack --config webpack.prod.js --entry ./src/console.ts --output-filename console.js && node dist/console.js", + "build": "npm run build:app && npm run build:console", + "build:app": "webpack --config webpack.prod.js", + "build:console": "webpack --config webpack.prod.js --entry ./src/console.ts --output-filename console.js", + "console": "npm run build:console && node dist/console.js", "dev": "npm run start", "format": "prettier --write ./*.{js,json} **/*.{ts,js,json}", "lint": "eslint .", diff --git a/apps/deploy-web/src/components/layout/WalletStatus.tsx b/apps/deploy-web/src/components/layout/WalletStatus.tsx index 1803b7442..1b46c868f 100644 --- a/apps/deploy-web/src/components/layout/WalletStatus.tsx +++ b/apps/deploy-web/src/components/layout/WalletStatus.tsx @@ -14,27 +14,32 @@ import { TooltipContent, TooltipTrigger } from "@akashnetwork/ui/components"; -import { Bank, HandCard, LogOut, MoreHoriz, Wallet } from "iconoir-react"; +import { Bank, CoinsSwap, HandCard, LogOut, MoreHoriz, Wallet } from "iconoir-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { LoginRequiredLink } from "@src/components/user/LoginRequiredLink"; import { ConnectManagedWalletButton } from "@src/components/wallet/ConnectManagedWalletButton"; import { envConfig } from "@src/config/env.config"; import { useWallet } from "@src/context/WalletProvider"; +import { useLoginRequiredEventHandler } from "@src/hooks/useLoginRequiredEventHandler"; import { useTotalWalletBalance } from "@src/hooks/useWalletBalance"; import { udenomToDenom } from "@src/utils/mathHelpers"; import { UrlService } from "@src/utils/urlUtils"; import { FormattedDecimal } from "../shared/FormattedDecimal"; import { ConnectWalletButton } from "../wallet/ConnectWalletButton"; +const goToCheckout = () => { + window.location.href = "/api/proxy/v1/checkout"; +}; + +const withBilling = envConfig.NEXT_PUBLIC_BILLING_ENABLED; + export function WalletStatus() { - const { walletName, address, walletBalances, logout, isWalletLoaded, isWalletConnected, isManaged, isWalletLoading, isTrialing } = useWallet(); + const { walletName, address, walletBalances, logout, isWalletLoaded, isWalletConnected, isManaged, isWalletLoading, isTrialing, switchWalletType } = + useWallet(); const walletBalance = useTotalWalletBalance(); const router = useRouter(); - function onDisconnectClick() { - logout(); - } + const whenLoggedIn = useLoginRequiredEventHandler(); const onAuthorizeSpendingClick = () => { router.push(UrlService.settingsAuthorizations()); @@ -46,29 +51,48 @@ export function WalletStatus() { isWalletConnected ? ( <>
- {!isManaged && ( -
- - - - - - onAuthorizeSpendingClick()}> - -  Authorize Spending - - onDisconnectClick()}> - -  Disconnect Wallet - - - -
- )} - +
+ + + + + + {!isManaged && ( + <> + onAuthorizeSpendingClick()}> + +  Authorize Spending + + + +  Disconnect Wallet + + {withBilling && ( + + +  Switch to USD billing + + )} + + )} + {withBilling && isManaged && ( + <> + + +  Top up balance + + + +  Switch to wallet billing + + + )} + + +
@@ -114,18 +138,6 @@ export function WalletStatus() {
)} - {isManaged && ( - - - - Top up balance - - - )}
)} @@ -134,7 +146,7 @@ export function WalletStatus() { ) : (
- {envConfig.NEXT_PUBLIC_BILLING_ENABLED && } + {withBilling && }
) diff --git a/apps/deploy-web/src/components/user/LoginRequiredLink.tsx b/apps/deploy-web/src/components/user/LoginRequiredLink.tsx index 7fa3da6c6..9cb4f1b07 100644 --- a/apps/deploy-web/src/components/user/LoginRequiredLink.tsx +++ b/apps/deploy-web/src/components/user/LoginRequiredLink.tsx @@ -1,10 +1,8 @@ -import React, { useCallback } from "react"; -import { usePopup } from "@akashnetwork/ui/context"; +import React from "react"; import Link, { LinkProps } from "next/link"; -import { useUser } from "@src/hooks/useUser"; +import { useLoginRequiredEventHandler } from "@src/hooks/useLoginRequiredEventHandler"; import { FCWithChildren } from "@src/types/component"; -import { UrlService } from "@src/utils/urlUtils"; export const LoginRequiredLink: FCWithChildren< Omit, keyof LinkProps> & @@ -13,44 +11,6 @@ export const LoginRequiredLink: FCWithChildren< message: string; } & React.RefAttributes > = ({ message, ...props }) => { - const { requireAction } = usePopup(); - const user = useUser(); - const showLoginPrompt = useCallback( - () => - requireAction({ - message, - actions: [ - { - label: "Sign in", - side: "left", - size: "lg", - variant: "secondary", - onClick: () => { - window.location.href = UrlService.login(); - } - }, - { - label: "Sign up", - side: "right", - size: "lg", - onClick: () => { - window.location.href = UrlService.signup(); - } - } - ] - }), - [message, requireAction] - ); - - return ( - { - if (!user.userId) { - event.preventDefault(); - showLoginPrompt(); - } - }} - /> - ); + const whenLoggedIn = useLoginRequiredEventHandler(); + return {}), message)} />; }; diff --git a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx index a9663e430..ab775f2b3 100644 --- a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx +++ b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx @@ -1,6 +1,6 @@ "use client"; import React, { ReactNode } from "react"; -import { Button, ButtonProps } from "@akashnetwork/ui/components"; +import { Button, ButtonProps, Spinner } from "@akashnetwork/ui/components"; import { Rocket } from "iconoir-react"; import { useWallet } from "@src/context/WalletProvider"; @@ -12,12 +12,12 @@ interface Props extends ButtonProps { } export const ConnectManagedWalletButton: React.FunctionComponent = ({ className = "", ...rest }) => { - const { connectManagedWallet } = useWallet(); + const { connectManagedWallet, hasManagedWallet, isWalletLoading } = useWallet(); return ( - ); }; diff --git a/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx b/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx index dccd2a167..b103b4e2e 100644 --- a/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx +++ b/apps/deploy-web/src/context/CertificateProvider/CertificateProviderContext.tsx @@ -10,7 +10,7 @@ import { RestApiCertificatesResponseType } from "@src/types/certificate"; import { AnalyticsEvents } from "@src/utils/analytics"; import { networkVersion } from "@src/utils/constants"; import { TransactionMessageData } from "@src/utils/TransactionMessageData"; -import { getSelectedStorageWallet, getStorageWallets, updateWallet } from "@src/utils/walletUtils"; +import { getStorageWallets, updateWallet } from "@src/utils/walletUtils"; import { useSettings } from "../SettingsProvider"; import { useWallet } from "../WalletProvider"; @@ -122,7 +122,6 @@ export const CertificateProvider = ({ children }) => { useEffect(() => { if (!isSettingsInit) return; - // Clear certs when no selected wallet setValidCertificates([]); setSelectedCertificate(null); setLocalCert(null); @@ -152,22 +151,20 @@ export const CertificateProvider = ({ children }) => { }, [selectedCertificate, localCert, validCertificates]); const loadLocalCert = async () => { - // open certs for all the wallets const wallets = getStorageWallets(); - const currentWallet = getSelectedStorageWallet(); - const certs: LocalCert[] = []; + const certs = wallets.reduce((acc, wallet) => { + const cert: LocalCert | null = wallet.cert && wallet.certKey ? { certPem: wallet.cert, keyPem: wallet.certKey, address: wallet.address } : null; - for (let i = 0; i < wallets.length; i++) { - const _wallet = wallets[i]; - - const _cert = { certPem: _wallet.cert, keyPem: _wallet.certKey, address: _wallet.address }; - - certs.push(_cert as LocalCert); + if (cert) { + acc.push(cert); + } - if (_wallet.address === currentWallet?.address) { - setLocalCert(_cert as LocalCert); + if (wallet.address === address) { + setLocalCert(cert); } - } + + return acc; + }, [] as LocalCert[]); setLocalCerts(certs); }; diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index 433f7e582..0b75a486d 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -13,6 +13,7 @@ import { event } from "nextjs-google-analytics"; import { SnackbarKey, useSnackbar } from "notistack"; import { LoadingState, TransactionModal } from "@src/components/layout/TransactionModal"; +import { useAllowance } from "@src/hooks/useAllowance"; import { useUsdcDenom } from "@src/hooks/useDenom"; import { useManagedWallet } from "@src/hooks/useManagedWallet"; import { useUser } from "@src/hooks/useUser"; @@ -23,10 +24,9 @@ import { AnalyticsEvents } from "@src/utils/analytics"; import { STATS_APP_URL, uAktDenom } from "@src/utils/constants"; import { customRegistry } from "@src/utils/customRegistry"; import { UrlService } from "@src/utils/urlUtils"; -import { LocalWalletDataType } from "@src/utils/walletUtils"; +import { getSelectedStorageWallet, getStorageWallets, updateStorageManagedWallet, updateStorageWallets } from "@src/utils/walletUtils"; import { useSelectedChain } from "../CustomChainProvider"; import { useSettings } from "../SettingsProvider"; -import { useAllowance } from "@src/hooks/useAllowance"; const ERROR_MESSAGES = { 5: "Insufficient funds", @@ -58,6 +58,8 @@ type ContextType = { isWalletLoading: boolean; isTrialing: boolean; creditAmount?: number; + switchWalletType: () => void; + hasManagedWallet: boolean; }; const WalletProviderContext = React.createContext({} as ContextType); @@ -69,6 +71,8 @@ const MESSAGE_STATES: Record = { "/akash.deployment.v1beta3.MsgUpdateDeployment": "updatingDeployment" }; +const initialWallet = getSelectedStorageWallet(); + export const WalletProvider = ({ children }) => { const [walletBalances, setWalletBalances] = useState(null); const [isWalletLoaded, setIsWalletLoaded] = useState(true); @@ -80,15 +84,23 @@ export const WalletProvider = ({ children }) => { const usdcIbcDenom = useUsdcDenom(); const user = useUser(); const userWallet = useSelectedChain(); - const { wallet: managedWallet, isLoading, create, refetch } = useManagedWallet(); - const { address: walletAddress, username, isWalletConnected } = useMemo(() => managedWallet || userWallet, [managedWallet, userWallet]); + const { wallet: managedWallet, isLoading, create: createManagedWallet, refetch } = useManagedWallet(); + const [selectedWalletType, selectWalletType] = useState<"managed" | "custodial">( + initialWallet?.selected && initialWallet?.isManaged ? "managed" : "custodial" + ); + const { + address: walletAddress, + username, + isWalletConnected + } = useMemo(() => (selectedWalletType === "managed" && managedWallet) || userWallet, [managedWallet, userWallet, selectedWalletType]); const { addEndpoints } = useManager(); - const isManaged = !!managedWallet; + const isManaged = useMemo(() => !!managedWallet && managedWallet?.address === walletAddress, [walletAddress, managedWallet]); + const { fee: { default: feeGranter } } = useAllowance(walletAddress as string, isManaged); - useWhen(managedWallet, refreshBalances); + useWhen(isManaged, refreshBalances); useEffect(() => { if (!settings.apiEndpoint || !settings.rpcEndpoint) return; @@ -108,6 +120,33 @@ export const WalletProvider = ({ children }) => { })(); }, [settings?.rpcEndpoint, userWallet.isWalletConnected]); + function switchWalletType() { + if (selectedWalletType === "custodial" && !managedWallet) { + userWallet.disconnect(); + } + + if (selectedWalletType === "managed" && !userWallet.isWalletConnected) { + userWallet.connect(); + } + + if (selectedWalletType === "managed" && managedWallet) { + updateStorageManagedWallet({ + ...managedWallet, + selected: false + }); + } + + selectWalletType(prev => (prev === "custodial" ? "managed" : "custodial")); + } + + function connectManagedWallet() { + if (managedWallet) { + selectWalletType("managed"); + } else { + createManagedWallet(); + } + } + async function createStargateClient() { const selectedNetwork = networkStore.getSelectedNetwork(); @@ -164,18 +203,15 @@ export const WalletProvider = ({ children }) => { useWhen(walletAddress, loadWallet); async function loadWallet(): Promise { - const selectedNetwork = networkStore.getSelectedNetwork(); - const storageWallets = JSON.parse(localStorage.getItem(`${selectedNetwork.id}/wallets`) || "[]") as LocalWalletDataType[]; - - let currentWallets = storageWallets ?? []; + let currentWallets = getStorageWallets(); if (!currentWallets.some(x => x.address === walletAddress)) { - currentWallets.push({ name: username || "", address: walletAddress as string, selected: true }); + currentWallets.push({ name: username || "", address: walletAddress as string, selected: true, isManaged: false }); } currentWallets = currentWallets.map(x => ({ ...x, selected: x.address === walletAddress })); - localStorage.setItem(`${selectedNetwork.id}/wallets`, JSON.stringify(currentWallets)); + updateStorageWallets(currentWallets); await refreshBalances(); setIsWalletLoaded(true); @@ -186,7 +222,7 @@ export const WalletProvider = ({ children }) => { let txResult: TxOutput; try { - if (!!user?.id && managedWallet) { + if (!!user?.id && isManaged) { const mainMessage = msgs.find(msg => msg.typeUrl in MESSAGE_STATES); if (mainMessage) { @@ -300,7 +336,7 @@ export const WalletProvider = ({ children }) => { }; async function refreshBalances(address?: string): Promise<{ uakt: number; usdc: number }> { - if (managedWallet) { + if (isManaged && managedWallet) { const wallet = await refetch(); const walletBalances = { uakt: 0, @@ -345,14 +381,16 @@ export const WalletProvider = ({ children }) => { isWalletConnected: isWalletConnected, isWalletLoaded: isWalletLoaded, connectWallet, - connectManagedWallet: create, + connectManagedWallet, logout, signAndBroadcastTx, refreshBalances, - isManaged: isManaged, + isManaged, isWalletLoading: isLoading, - isTrialing: !!managedWallet?.isTrialing, - creditAmount: managedWallet?.creditAmount + isTrialing: isManaged && !!managedWallet?.isTrialing, + creditAmount: isManaged ? managedWallet?.creditAmount : 0, + hasManagedWallet: !!managedWallet, + switchWalletType }} > {children} diff --git a/apps/deploy-web/src/hooks/useLoginRequiredEventHandler.tsx b/apps/deploy-web/src/hooks/useLoginRequiredEventHandler.tsx new file mode 100644 index 000000000..c3f13b0d7 --- /dev/null +++ b/apps/deploy-web/src/hooks/useLoginRequiredEventHandler.tsx @@ -0,0 +1,43 @@ +import { MouseEventHandler, useCallback } from "react"; +import { usePopup } from "@akashnetwork/ui/context"; + +import { useUser } from "@src/hooks/useUser"; +import { UrlService } from "@src/utils/urlUtils"; + +export const useLoginRequiredEventHandler = (): ((callback: MouseEventHandler, messageOtherwise: string) => MouseEventHandler) => { + const { requireAction } = usePopup(); + const user = useUser(); + + return useCallback( + (handler: MouseEventHandler, messageOtherwise: string) => { + const preventer: MouseEventHandler = event => { + event.preventDefault(); + requireAction({ + message: messageOtherwise, + actions: [ + { + label: "Sign in", + side: "left", + size: "lg", + variant: "secondary", + onClick: () => { + window.location.href = UrlService.login(); + } + }, + { + label: "Sign up", + side: "right", + size: "lg", + onClick: () => { + window.location.href = UrlService.signup(); + } + } + ] + }); + }; + + return user?.userId ? handler : preventer; + }, + [user?.userId, requireAction] + ); +}; diff --git a/apps/deploy-web/src/hooks/useManagedWallet.ts b/apps/deploy-web/src/hooks/useManagedWallet.ts index 5a2c5f4bc..cbee4f997 100644 --- a/apps/deploy-web/src/hooks/useManagedWallet.ts +++ b/apps/deploy-web/src/hooks/useManagedWallet.ts @@ -6,16 +6,24 @@ import { useUser } from "@src/hooks/useUser"; import { useWhen } from "@src/hooks/useWhen"; import { useCreateManagedWalletMutation, useManagedWalletQuery } from "@src/queries/useManagedWalletQuery"; import networkStore from "@src/store/networkStore"; -import { deleteManagedWalletFromStorage, ensureUserManagedWalletOwnership, updateStorageManagedWallet } from "@src/utils/walletUtils"; +import { + deleteManagedWalletFromStorage, + ensureUserManagedWalletOwnership, + getSelectedStorageWallet, + getStorageManagedWallet, + updateStorageManagedWallet +} from "@src/utils/walletUtils"; const isBillingEnabled = envConfig.NEXT_PUBLIC_BILLING_ENABLED; const { NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID } = envConfig; +const storedManagedWallet = getStorageManagedWallet(); + export const useManagedWallet = () => { const user = useUser(); const { data: queried, isFetched, isLoading: isFetching, refetch } = useManagedWalletQuery(isBillingEnabled && user?.id); const { mutate: create, data: created, isLoading: isCreating, isSuccess: isCreated } = useCreateManagedWalletMutation(); - const wallet = useMemo(() => queried || created, [queried, created]); + const wallet = useMemo(() => queried || storedManagedWallet || created, [queried, created]); const isLoading = isFetching || isCreating; const [selectedNetworkId, setSelectedNetworkId] = useAtom(networkStore.selectedNetworkId); @@ -26,6 +34,8 @@ export const useManagedWallet = () => { if (isFetched && isCreated && !wallet) { deleteManagedWalletFromStorage(); + } else if (wallet && isCreated) { + updateStorageManagedWallet({ ...wallet, selected: true }); } else if (wallet) { updateStorageManagedWallet(wallet); } @@ -43,6 +53,7 @@ export const useManagedWallet = () => { return useMemo(() => { const isConfigured = !!wallet; + const selected = getSelectedStorageWallet(); return { create: () => { if (!isBillingEnabled) { @@ -58,8 +69,10 @@ export const useManagedWallet = () => { wallet: wallet ? { ...wallet, + username: "username" in wallet ? wallet.username : wallet.name, isWalletConnected: isConfigured, - isWalletLoaded: isConfigured + isWalletLoaded: isConfigured, + selected: selected?.address === wallet.address } : undefined, isLoading, diff --git a/apps/deploy-web/src/utils/walletUtils.ts b/apps/deploy-web/src/utils/walletUtils.ts index 396afebdf..9e64cd3b7 100644 --- a/apps/deploy-web/src/utils/walletUtils.ts +++ b/apps/deploy-web/src/utils/walletUtils.ts @@ -1,14 +1,28 @@ +import { isEqual } from "lodash"; + +import { envConfig } from "@src/config/env.config"; import { mainnetId } from "./constants"; -export type LocalWalletDataType = { +interface BaseLocalWallet { address: string; cert?: string; certKey?: string; - name: string; selected: boolean; - isManaged?: boolean; - userId?: string; -}; +} + +interface ManagedLocalWallet extends BaseLocalWallet { + name: "Managed Wallet"; + isManaged: true; + userId: string; + creditAmount: number; + isTrialing: boolean; +} + +interface CustodialLocalWallet extends BaseLocalWallet { + name: string; + isManaged: false; +} +export type LocalWallet = ManagedLocalWallet | CustodialLocalWallet; export function getSelectedStorageWallet() { const wallets = getStorageWallets(); @@ -16,58 +30,87 @@ export function getSelectedStorageWallet() { return wallets.find(w => w.selected) ?? wallets[0] ?? null; } -export function getStorageManagedWallet() { - return getStorageWallets().find(wallet => wallet.isManaged); +export function getStorageManagedWallet(networkId?: string): ManagedLocalWallet | undefined { + return getStorageWallets(networkId).find(wallet => wallet.isManaged) as ManagedLocalWallet | undefined; } -export function updateStorageManagedWallet(wallet: Pick) { - const prev = getStorageManagedWallet(); - const next = { +export function updateStorageManagedWallet( + wallet: Pick & { selected?: boolean } +): ManagedLocalWallet { + const networkId = envConfig.NEXT_PUBLIC_MANAGED_WALLET_NETWORK_ID; + const wallets = getStorageWallets(networkId); + const prevIndex = wallets.findIndex(({ isManaged }) => isManaged); + const prev = wallets[prevIndex]; + + const next: ManagedLocalWallet = { ...prev, ...wallet, name: "Managed Wallet", isManaged: true, - selected: true + selected: typeof wallet.selected === "boolean" ? wallet.selected : typeof prev?.selected === "boolean" ? prev.selected : false }; - updateStorageWallets([next]); + if (isEqual(prev, next)) { + return next; + } + + if (prev && prev?.address !== next.address) { + deleteManagedWalletFromStorage(); + } + + if (next.selected && !prev?.selected) { + wallets.forEach(item => { + item.selected = false; + }); + } + + if (prevIndex !== -1) { + wallets.splice(prevIndex, 1, next); + } else { + wallets.push(next); + } + updateStorageWallets(wallets, networkId); return next; } -export function deleteManagedWalletFromStorage() { - const wallet = getStorageManagedWallet(); +export function deleteManagedWalletFromStorage(networkId?: string) { + const wallet = getStorageManagedWallet(networkId); if (wallet) { - deleteWalletFromStorage(wallet.address, true); + deleteWalletFromStorage(wallet.address, true, networkId); } } -export function getStorageWallets() { - const selectedNetworkId = localStorage.getItem("selectedNetworkId") || mainnetId; - const wallets = JSON.parse(localStorage.getItem(`${selectedNetworkId}/wallets`) || "[]") as LocalWalletDataType[]; +export function getStorageWallets(networkId?: string) { + if (typeof window === "undefined") { + return []; + } + + const selectedNetworkId = networkId || localStorage.getItem("selectedNetworkId") || mainnetId; + const wallets = JSON.parse(localStorage.getItem(`${selectedNetworkId}/wallets`) || "[]") as LocalWallet[]; return wallets || []; } -export function updateWallet(address: string, func: (w: LocalWalletDataType) => LocalWalletDataType) { - const wallets = getStorageWallets(); +export function updateWallet(address: string, func: (w: LocalWallet) => LocalWallet, networkId?: string) { + 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 LocalWalletDataType) : w)); - updateStorageWallets(newWallets); + const newWallets = wallets.map(w => (w.address === address ? (wallet as LocalWallet) : w)); + updateStorageWallets(newWallets, networkId); } } -export function updateStorageWallets(wallets: LocalWalletDataType[]) { - const selectedNetworkId = localStorage.getItem("selectedNetworkId") || mainnetId; +export function updateStorageWallets(wallets: LocalWallet[], networkId?: string) { + const selectedNetworkId = networkId || localStorage.getItem("selectedNetworkId") || mainnetId; localStorage.setItem(`${selectedNetworkId}/wallets`, JSON.stringify(wallets)); } -export function deleteWalletFromStorage(address: string, deleteDeployments: boolean) { - const selectedNetworkId = localStorage.getItem("selectedNetworkId") || mainnetId; +export function deleteWalletFromStorage(address: string, deleteDeployments: boolean, networkId?: string) { + const selectedNetworkId = networkId || localStorage.getItem("selectedNetworkId") || mainnetId; const wallets = getStorageWallets(); const newWallets = wallets.filter(w => w.address !== address).map((w, i) => ({ ...w, selected: i === 0 }));