From e51c76d985e06769fb9aa5a3f541c19198871ede Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 29 Dec 2024 14:28:42 -0500 Subject: [PATCH] Create DeployerRecoveryModal Rename TempDeployer* to Deployer* --- .env.example | 4 +- src/components/nav/FloatingButtonRow.tsx | 56 +++++++ src/consts/config.ts | 13 +- src/consts/consts.ts | 1 + .../deployerWallet/DeployerRecoveryModal.tsx | 117 ++++++++++++++ src/features/deployerWallet/balances.ts | 73 +++++++++ src/features/deployerWallet/refund.ts | 61 ++------ src/features/deployerWallet/types.ts | 6 +- src/features/deployerWallet/wallets.ts | 144 ++++++++++-------- .../deployment/warp/WarpDeploymentDeploy.tsx | 22 +-- src/features/store.ts | 18 +-- src/features/wallet/WalletFloatingButtons.tsx | 37 ----- src/pages/index.tsx | 4 +- src/utils/storage.ts | 17 +++ 14 files changed, 391 insertions(+), 182 deletions(-) create mode 100644 src/components/nav/FloatingButtonRow.tsx create mode 100644 src/features/deployerWallet/DeployerRecoveryModal.tsx create mode 100644 src/features/deployerWallet/balances.ts delete mode 100644 src/features/wallet/WalletFloatingButtons.tsx create mode 100644 src/utils/storage.ts diff --git a/.env.example b/.env.example index 8018be9..d33cf3d 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ NEXT_PUBLIC_WALLET_CONNECT_ID=12345678901234567890123456789012 NEXT_PUBLIC_CHAIN_WALLET_WHITELISTS='{"eclipsemainnet":["Salmon", "Backpack", "Connect by Drift"]}' -NEXT_PUBLIC_TEMP_WALLET_ENCRYPTION_KEY=todoReplaceMeWithAnyStrongPassword! # Any strong password will work -NEXT_PUBLIC_TEMP_WALLET_ENCRYPTION_SALT=3424521f5cc25e6d05bd062eedc3c33d9201118dbb4baa3f7656c8dd34a95e28 # E.g. Sha256 of 'hyperlane-deploy-app' \ No newline at end of file +NEXT_PUBLIC_DEPLOYER_WALLET_ENCRYPTION_KEY=todoReplaceMeWithAnyStrongPassword! # Any strong password will work +NEXT_PUBLIC_DEPLOYER_WALLET_ENCRYPTION_SALT=3424521f5cc25e6d05bd062eedc3c33d9201118dbb4baa3f7656c8dd34a95e28 # E.g. Sha256 of 'hyperlane-deploy-app' \ No newline at end of file diff --git a/src/components/nav/FloatingButtonRow.tsx b/src/components/nav/FloatingButtonRow.tsx new file mode 100644 index 0000000..476c013 --- /dev/null +++ b/src/components/nav/FloatingButtonRow.tsx @@ -0,0 +1,56 @@ +import Link from 'next/link'; + +import { objLength } from '@hyperlane-xyz/utils'; +import { DocsIcon, HistoryIcon, IconButton, useModal } from '@hyperlane-xyz/widgets'; +import { links } from '../../consts/links'; +import { DeployerRecoveryModal } from '../../features/deployerWallet/DeployerRecoveryModal'; +import { useStore } from '../../features/store'; +import { Color } from '../../styles/Color'; +import { GasIcon } from '../icons/GasIcon'; + +export function FloatingButtonRow() { + const { setIsSideBarOpen, isSideBarOpen, deployerKeys } = useStore((s) => ({ + setIsSideBarOpen: s.setIsSideBarOpen, + isSideBarOpen: s.isSideBarOpen, + deployerKeys: s.deployerKeys, + })); + + const { isOpen, open, close } = useModal(); + + const hasTempKeys = objLength(deployerKeys) > 0; + + return ( +
+ {hasTempKeys && ( + + + + )} + setIsSideBarOpen(!isSideBarOpen)} + > + + + + + + +
+ ); +} + +const styles = { + link: 'hover:opacity-70 active:opacity-60', + roundedCircle: 'rounded-full bg-white', +}; diff --git a/src/consts/config.ts b/src/consts/config.ts index d445153..d78855e 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -7,8 +7,9 @@ const isDevMode = process?.env?.NODE_ENV === 'development'; const registryUrl = process?.env?.NEXT_PUBLIC_REGISTRY_URL || undefined; const registryBranch = process?.env?.NEXT_PUBLIC_REGISTRY_BRANCH || undefined; const registryProxyUrl = process?.env?.NEXT_PUBLIC_GITHUB_PROXY || 'https://proxy.hyperlane.xyz'; -const tempWalletEncryptionKey = process?.env?.NEXT_PUBLIC_TEMP_WALLET_ENCRYPTION_KEY || ''; -const tempWalletEncryptionSalt = process?.env?.NEXT_PUBLIC_TEMP_WALLET_ENCRYPTION_SALT || ''; +const deployerWalletEncryptionKey = process?.env?.NEXT_PUBLIC_DEPLOYER_WALLET_ENCRYPTION_KEY || ''; +const deployerWalletEncryptionSalt = + process?.env?.NEXT_PUBLIC_DEPLOYER_WALLET_ENCRYPTION_SALT || ''; const version = process?.env?.NEXT_PUBLIC_VERSION || '0.0.0'; const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || ''; @@ -20,8 +21,8 @@ interface Config { registryUrl: string | undefined; // Optional URL to use a custom registry instead of the published canonical version registryBranch?: string | undefined; // Optional customization of the registry branch instead of main registryProxyUrl?: string; // Optional URL to use a custom proxy for the GithubRegistry - tempWalletEncryptionKey: string; // Encryption key for temporary deployer wallets - tempWalletEncryptionSalt: string; // Encryption salt for temporary deployer wallets + deployerWalletEncryptionKey: string; // Encryption key for temporary deployer wallets + deployerWalletEncryptionSalt: string; // Encryption salt for temporary deployer wallets version: string; // Matches version number in package.json walletConnectProjectId: string; // Project ID provided by walletconnect } @@ -34,8 +35,8 @@ export const config: Config = Object.freeze({ registryUrl, registryBranch, registryProxyUrl, - tempWalletEncryptionKey, - tempWalletEncryptionSalt, + deployerWalletEncryptionKey, + deployerWalletEncryptionSalt, version, walletConnectProjectId, }); diff --git a/src/consts/consts.ts b/src/consts/consts.ts index 6fc9bdb..007d5a8 100644 --- a/src/consts/consts.ts +++ b/src/consts/consts.ts @@ -1,3 +1,4 @@ export const MIN_CHAIN_BALANCE = 1; // 1 Wei export const WARP_DEPLOY_GAS_UNITS = BigInt(1e7); export const REFUND_FEE_PADDING_FACTOR = 1.1; +export const MIN_DEPLOYER_BALANCE_TO_SHOW = BigInt(1e15); // 0.001 ETH diff --git a/src/features/deployerWallet/DeployerRecoveryModal.tsx b/src/features/deployerWallet/DeployerRecoveryModal.tsx new file mode 100644 index 0000000..1c22098 --- /dev/null +++ b/src/features/deployerWallet/DeployerRecoveryModal.tsx @@ -0,0 +1,117 @@ +import { Button, CopyButton, Modal, SpinnerIcon, tryClipboardSet } from '@hyperlane-xyz/widgets'; +import { PropsWithChildren, useEffect, useMemo } from 'react'; +import { toast } from 'react-toastify'; +import { H2 } from '../../components/text/Headers'; +import { MIN_DEPLOYER_BALANCE_TO_SHOW } from '../../consts/consts'; +import { Color } from '../../styles/Color'; +import { useMultiProvider } from '../chains/hooks'; +import { getChainDisplayName } from '../chains/utils'; +import { useDeployerBalances } from './balances'; +import { useRefundDeployerAccounts } from './refund'; +import { DeployerWallets, TypedWallet } from './types'; +import { getDeployerWalletKey, useDeployerWallets, useRemoveDeployerWallet } from './wallets'; + +export function DeployerRecoveryModal({ isOpen, close }: { isOpen: boolean; close: () => void }) { + const { wallets } = useDeployerWallets(); + + // Close modal when no wallets are found + useEffect(() => { + if (isOpen && !Object.values(wallets).length) close(); + }, [isOpen, wallets, close]); + + return ( + +

Temporary Deployer Accounts

+

+ Once the balances are successfully refunded, these temporary accounts can be safely deleted. +

+ +
+ +
+
+ ); +} + +function AccountList({ wallets }: { wallets: DeployerWallets }) { + const removeDeployerKey = useRemoveDeployerWallet(); + + const walletList = useMemo(() => Object.values(wallets), [wallets]); + + const onClickCopyPrivateKey = (wallet: TypedWallet) => { + try { + const pk = getDeployerWalletKey(wallet); + tryClipboardSet(pk); + toast.success('Private key copied to clipboard'); + } catch { + toast.error('Unable to retrieve private key'); + } + }; + + const onClickDeleteAccount = (wallet: TypedWallet) => { + removeDeployerKey(wallet.protocol); + }; + + return ( + <> + {walletList.map((w) => ( +
+
+ {w.address} + +
+
+ + +
+
+ ))} + + ); +} + +function Balances({ isOpen, wallets }: { isOpen: boolean; wallets: DeployerWallets }) { + const multiProvider = useMultiProvider(); + const { balances, isFetching, refetch } = useDeployerBalances(wallets); + const { refund, isPending } = useRefundDeployerAccounts(refetch); + + // Refetch balances when modal is opened + useEffect(() => { + if (isOpen) refetch(); + }, [isOpen, refetch]); + + if (isFetching || !balances) { + return Searching for balances...; + } + + if (isPending) { + return Refunding balances...; + } + + const nonTrivialBalances = balances.filter((b) => b.amount >= MIN_DEPLOYER_BALANCE_TO_SHOW); + const balanceChains = nonTrivialBalances + .map((b) => getChainDisplayName(multiProvider, b.chainName)) + .join(', '); + + if (!nonTrivialBalances.length) { + return
No balances found on deployment chains
; + } + + return ( +
+ {`Balances on found on chains: ${balanceChains}`} + +
+ ); +} + +function BalanceSpinner({ children }: PropsWithChildren) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/features/deployerWallet/balances.ts b/src/features/deployerWallet/balances.ts new file mode 100644 index 0000000..4dfcb26 --- /dev/null +++ b/src/features/deployerWallet/balances.ts @@ -0,0 +1,73 @@ +import { MultiProtocolProvider, Token } from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; +import { useQuery } from '@tanstack/react-query'; +import { logger } from '../../utils/logger'; +import { useMultiProvider } from '../chains/hooks'; +import { useDeploymentChains } from '../deployment/hooks'; +import { DeployerWallets } from './types'; + +export interface Balance { + chainName: ChainName; + protocol: ProtocolType; + address: Address; + amount: bigint; +} + +export function useDeployerBalances(wallets: DeployerWallets) { + const multiProvider = useMultiProvider(); + const { chains } = useDeploymentChains(); + + const { data, isFetching, refetch } = useQuery({ + // MultiProvider cannot be used here because it's not serializable + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: ['getDeployerBalances', chains, wallets], + queryFn: () => getDeployerBalances(chains, wallets, multiProvider), + retry: 3, + staleTime: 10_000, + }); + + return { + isFetching, + balances: data, + refetch, + }; +} + +export async function getDeployerBalances( + chains: ChainName[], + wallets: DeployerWallets, + multiProvider: MultiProtocolProvider, +) { + const balances: Array> = await Promise.allSettled( + chains.map(async (chainName) => { + try { + const chainMetadata = multiProvider.tryGetChainMetadata(chainName); + if (!chainMetadata) return undefined; + const address = wallets[chainMetadata.protocol]?.address; + if (!address) return undefined; + const token = Token.FromChainMetadataNativeToken(chainMetadata); + logger.debug('Checking balance', chainName, address); + const balance = await token.getBalance(multiProvider, address); + logger.debug('Balance retrieved', chainName, address, balance.amount); + return { chainName, protocol: chainMetadata.protocol, address, amount: balance.amount }; + } catch (error: unknown) { + const msg = `Error getting balance for chain ${chainName}`; + logger.error(msg, error); + throw new Error(msg, { cause: error }); + } + }), + ); + + const nonZeroBalances = balances + .filter((b) => b.status === 'fulfilled') + .map((b) => b.value) + .filter((b): b is Balance => !!b && b.amount > 0n); + if (nonZeroBalances.length) { + logger.debug( + 'Non-zero balances found for chains:', + nonZeroBalances.map((b) => b.chainName).join(', '), + ); + } + + return nonZeroBalances; +} diff --git a/src/features/deployerWallet/refund.ts b/src/features/deployerWallet/refund.ts index 77da611..55e36bc 100644 --- a/src/features/deployerWallet/refund.ts +++ b/src/features/deployerWallet/refund.ts @@ -14,17 +14,18 @@ import { logger } from '../../utils/logger'; import { useMultiProvider } from '../chains/hooks'; import { getChainDisplayName } from '../chains/utils'; import { useDeploymentChains } from '../deployment/hooks'; +import { Balance, getDeployerBalances } from './balances'; import { getTransferTx, sendTxFromWallet } from './transactions'; -import { TempDeployerWallets } from './types'; -import { getDeployerAddressForProtocol, useTempDeployerWallets } from './wallets'; +import { DeployerWallets } from './types'; +import { useDeployerWallets } from './wallets'; export function useRefundDeployerAccounts(onSettled?: () => void) { const multiProvider = useMultiProvider(); - const { chains, protocols } = useDeploymentChains(); - const { wallets } = useTempDeployerWallets(protocols); + const { chains } = useDeploymentChains(); + const { wallets } = useDeployerWallets(); const { accounts } = useAccounts(multiProvider); - const { error, mutate, mutateAsync, submittedAt, isIdle } = useMutation({ + const { error, mutate, mutateAsync, submittedAt, isIdle, isPending } = useMutation({ mutationKey: ['refundDeployerAccounts', chains, wallets, accounts], mutationFn: () => refundDeployerAccounts(chains, wallets, multiProvider, accounts), retry: false, @@ -41,12 +42,13 @@ export function useRefundDeployerAccounts(onSettled?: () => void) { refundAsync: mutateAsync, isIdle, hasRun: !!submittedAt, + isPending, }; } async function refundDeployerAccounts( chains: ChainName[], - wallets: TempDeployerWallets, + wallets: DeployerWallets, multiProvider: MultiProtocolProvider, accounts: Record, ) { @@ -57,54 +59,9 @@ async function refundDeployerAccounts( return true; } -interface Balance { - chainName: ChainName; - protocol: ProtocolType; - address: Address; - amount: bigint; -} - -async function getDeployerBalances( - chains: ChainName[], - wallets: TempDeployerWallets, - multiProvider: MultiProtocolProvider, -) { - const balances: Array> = await Promise.allSettled( - chains.map(async (chainName) => { - try { - const chainMetadata = multiProvider.tryGetChainMetadata(chainName); - const address = getDeployerAddressForProtocol(wallets, chainMetadata?.protocol); - if (!chainMetadata || !address) return undefined; - const token = Token.FromChainMetadataNativeToken(chainMetadata); - logger.debug('Checking balance', chainName, address); - const balance = await token.getBalance(multiProvider, address); - logger.debug('Balance retrieved', chainName, address, balance.amount); - return { chainName, protocol: chainMetadata.protocol, address, amount: balance.amount }; - } catch (error: unknown) { - const msg = `Error getting balance for chain ${chainName}`; - logger.error(msg, error); - throw new Error(msg, { cause: error }); - } - }), - ); - - const nonZeroBalances = balances - .filter((b) => b.status === 'fulfilled') - .map((b) => b.value) - .filter((b): b is Balance => !!b && b.amount > 0n); - if (nonZeroBalances.length) { - logger.debug( - 'Non-zero balances found for chains:', - nonZeroBalances.map((b) => b.chainName).join(', '), - ); - } - - return nonZeroBalances; -} - async function transferBalances( balances: Balance[], - wallets: TempDeployerWallets, + wallets: DeployerWallets, multiProvider: MultiProtocolProvider, accounts: Record, ) { diff --git a/src/features/deployerWallet/types.ts b/src/features/deployerWallet/types.ts index 184adbe..94be9ad 100644 --- a/src/features/deployerWallet/types.ts +++ b/src/features/deployerWallet/types.ts @@ -7,7 +7,7 @@ import type { Keypair as SolWeb3Wallet } from '@solana/web3.js'; import { DirectSecp256k1HdWallet as CosmosWallet } from '@cosmjs/proto-signing'; // A map of protocol to encrypted wallet key -export type TempDeployerKeys = Partial>; +export type DeployerKeys = Partial>; /** * Wallets with discriminated union of provider type @@ -16,6 +16,8 @@ export type TempDeployerKeys = Partial>; interface TypedWalletBase { type: ProviderType; wallet: T; + address: Address; + protocol: ProtocolType; } export interface EthersV5Wallet extends TypedWalletBase { @@ -35,4 +37,4 @@ export interface CosmJsWallet extends TypedWalletBase { export type TypedWallet = EthersV5Wallet | SolanaWeb3Wallet | CosmJsWallet; -export type TempDeployerWallets = Partial>; +export type DeployerWallets = Partial>; diff --git a/src/features/deployerWallet/wallets.ts b/src/features/deployerWallet/wallets.ts index 707a574..8b90541 100644 --- a/src/features/deployerWallet/wallets.ts +++ b/src/features/deployerWallet/wallets.ts @@ -1,30 +1,52 @@ import { ProviderType } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { useQuery } from '@tanstack/react-query'; -import { utils, Wallet } from 'ethers'; +import { Wallet } from 'ethers'; import { useEffect } from 'react'; import { toast } from 'react-toastify'; import { config } from '../../consts/config'; import { logger } from '../../utils/logger'; +import { tryPersistBrowserStorage } from '../../utils/storage'; import { useStore } from '../store'; import { decryptString, encryptString } from './encryption'; -import { TempDeployerKeys, TempDeployerWallets, TypedWallet } from './types'; +import { DeployerKeys, DeployerWallets, TypedWallet } from './types'; -export function useTempDeployerWallets( +/** + * Reads and decrypts any existing deployer keys + */ +export function useDeployerWallets() { + const { deployerKeys } = useStore((s) => ({ + deployerKeys: s.deployerKeys, + })); + const { error, isLoading, data } = useQuery({ + queryKey: ['getDeployerWallets', deployerKeys], + queryFn: () => getDeployerWallets(deployerKeys), + retry: false, + }); + + return { + isLoading, + error, + wallets: data || {}, + }; +} + +/** + * Reads and decrypts any existing deployer keys + * or creates new ones if they don't exist + */ +export function useOrCreateDeployerWallets( protocols: ProtocolType[], onFailure?: (error: Error) => void, ) { - // const tempDeployerKeys = useStore((s) => s.tempDeployerKeys); - const { tempDeployerKeys, setDeployerKey } = useStore((s) => ({ - tempDeployerKeys: s.tempDeployerKeys, + const { deployerKeys, setDeployerKey } = useStore((s) => ({ + deployerKeys: s.deployerKeys, setDeployerKey: s.setDeployerKey, })); const { error, isLoading, data } = useQuery({ - queryKey: ['getDeployerWallet', protocols, tempDeployerKeys, setDeployerKey], - queryFn: () => getOrCreateTempDeployerWallets(protocols, tempDeployerKeys, setDeployerKey), + queryKey: ['getOrCreateDeployerWallets', protocols, deployerKeys, setDeployerKey], + queryFn: () => getOrCreateDeployerWallets(protocols, deployerKeys, setDeployerKey), retry: false, - staleTime: Infinity, - gcTime: Infinity, }); useEffect(() => { if (error) { @@ -40,108 +62,104 @@ export function useTempDeployerWallets( }; } -export function useRemoveTempDeployerWallet(protocols: ProtocolType[]) { - const removeDeployerKey = useStore((s) => s.removeDeployerKey); - return () => protocols.map((p) => removeDeployerKey(p)); +export function useRemoveDeployerWallet() { + return useStore((s) => s.removeDeployerKey); +} + +async function getDeployerWallets(encryptedKeys: DeployerKeys) { + const wallets: DeployerWallets = {}; + for (const protocol of Object.values(ProtocolType)) { + try { + const encryptedKey = encryptedKeys[protocol]; + if (!encryptedKey) continue; + logger.debug('Found deployer key in store for:', protocol); + const wallet = await decryptDeployerWallet(protocol, encryptedKey); + wallets[protocol] = wallet; + } catch (error) { + throw new Error(`Error reading deployer wallet for ${protocol}`, { cause: error }); + } + } + return wallets; } -async function getOrCreateTempDeployerWallets( +async function getOrCreateDeployerWallets( protocols: ProtocolType[], - encryptedKeys: TempDeployerKeys, + encryptedKeys: DeployerKeys, storeKey: (protocol: ProtocolType, key: string) => void, -): Promise { - const wallets: TempDeployerWallets = {}; - +): Promise { + const wallets: DeployerWallets = {}; for (const protocol of protocols) { try { const encryptedKey = encryptedKeys[protocol]; if (encryptedKey) { logger.debug('Found deployer key in store for:', protocol); - const wallet = await getTempDeployerWallet(protocol, encryptedKey); + const wallet = await decryptDeployerWallet(protocol, encryptedKey); wallets[protocol] = wallet; } else { logger.debug('No deployer key found in store for:', protocol); - const [wallet, encryptedKey] = await createTempDeployerWallet(protocol); + const [wallet, encryptedKey] = await createDeployerWallet(protocol); storeKey(protocol, encryptedKey); tryPersistBrowserStorage(); wallets[protocol] = wallet; } } catch (error) { - throw new Error(`Error preparing temp deployer wallet for ${protocol}`, { cause: error }); + throw new Error(`Error preparing deployer wallet for ${protocol}`, { cause: error }); } } - return wallets; } // TODO multi-protocol support -async function createTempDeployerWallet(protocol: ProtocolType): Promise<[TypedWallet, string]> { - logger.info('Creating temp deployer wallet for:', protocol); +async function createDeployerWallet(protocol: ProtocolType): Promise<[TypedWallet, string]> { + logger.info('Creating deployer wallet for:', protocol); let wallet: TypedWallet; let key: string; if (protocol === ProtocolType.Ethereum) { - const entropy = utils.randomBytes(32); - key = utils.entropyToMnemonic(entropy); - wallet = { type: ProviderType.EthersV5, wallet: Wallet.fromMnemonic(key) }; + const ethersWallet = Wallet.createRandom(); + wallet = { + type: ProviderType.EthersV5, + wallet: ethersWallet, + address: ethersWallet.address, + protocol, + }; + key = ethersWallet.privateKey; } else { - throw new Error(`Unsupported protocol for temp deployer wallet: ${protocol}`); + throw new Error(`Unsupported protocol for deployer wallet: ${protocol}`); } const encryptedKey = await encryptString( key, - config.tempWalletEncryptionKey, - config.tempWalletEncryptionSalt, + config.deployerWalletEncryptionKey, + config.deployerWalletEncryptionSalt, ); logger.info('Temp deployer wallet created for:', protocol); return [wallet, encryptedKey]; } // TODO multi-protocol support -async function getTempDeployerWallet( +async function decryptDeployerWallet( protocol: ProtocolType, encryptedKey: string, ): Promise { - logger.debug('Instantiating temp deployer wallet from key for:', protocol); + logger.debug('Instantiating deployer wallet from key for:', protocol); const key = await decryptString( encryptedKey, - config.tempWalletEncryptionKey, - config.tempWalletEncryptionSalt, + config.deployerWalletEncryptionKey, + config.deployerWalletEncryptionSalt, ); if (protocol === ProtocolType.Ethereum) { - const wallet = Wallet.fromMnemonic(key); - return { type: ProviderType.EthersV5, wallet }; + const wallet = new Wallet(key); + return { type: ProviderType.EthersV5, wallet, address: wallet.address, protocol }; } else { - throw new Error(`Unsupported protocol for temp deployer wallet: ${protocol}`); + throw new Error(`Unsupported protocol for deployer wallet: ${protocol}`); } } // TODO multi-protocol support -export function getDeployerAddressForProtocol( - wallets: TempDeployerWallets, - protocol?: ProtocolType, -) { - if (!protocol) return undefined; - const typedWallet = wallets[protocol]; - if (!typedWallet) return undefined; - if (typedWallet.type === ProviderType.EthersV5) { - return typedWallet.wallet.address; +export function getDeployerWalletKey(wallet: TypedWallet) { + if (wallet.type === ProviderType.EthersV5) { + return wallet.wallet.privateKey; } else { - throw new Error(`Unsupported wallet type for address: ${typedWallet.type}`); - } -} - -function tryPersistBrowserStorage() { - // Request persistent storage for site - // This prevents browser from clearing local storage when space runs low. Rare but possible. - // Not a critical perm (and not supported in safari) so not blocking on this - if (navigator?.storage?.persist) { - navigator.storage - .persist() - .then((isPersisted) => { - logger.debug(`Is persisted storage granted: ${isPersisted}`); - }) - .catch((reason) => { - logger.error('Error enabling storage persist setting', reason); - }); + throw new Error(`Unsupported wallet type for address: ${wallet.type}`); } } diff --git a/src/features/deployment/warp/WarpDeploymentDeploy.tsx b/src/features/deployment/warp/WarpDeploymentDeploy.tsx index 47b57e9..56af757 100644 --- a/src/features/deployment/warp/WarpDeploymentDeploy.tsx +++ b/src/features/deployment/warp/WarpDeploymentDeploy.tsx @@ -1,5 +1,5 @@ import { Button, Modal, SpinnerIcon, useModal } from '@hyperlane-xyz/widgets'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; import { PlanetSpinner } from '../../../components/animation/PlanetSpinner'; import { SlideIn } from '../../../components/animation/SlideIn'; @@ -17,10 +17,7 @@ import { useMultiProvider } from '../../chains/hooks'; import { getChainDisplayName } from '../../chains/utils'; import { useFundDeployerAccount } from '../../deployerWallet/fund'; import { useRefundDeployerAccounts } from '../../deployerWallet/refund'; -import { - getDeployerAddressForProtocol, - useTempDeployerWallets, -} from '../../deployerWallet/wallets'; +import { useOrCreateDeployerWallets } from '../../deployerWallet/wallets'; import { useDeploymentHistory, useWarpDeploymentConfig } from '../hooks'; import { DeploymentStatus } from '../types'; @@ -84,16 +81,23 @@ function FundDeployerAccounts({ const multiProvider = useMultiProvider(); const { deploymentConfig } = useWarpDeploymentConfig(); - const chains = deploymentConfig?.chains || []; - const protocols = Array.from(new Set(chains.map((c) => multiProvider.getProtocol(c)))); + const { chains, protocols } = useMemo(() => { + const chains = deploymentConfig?.chains || []; + const protocols = Array.from(new Set(chains.map((c) => multiProvider.getProtocol(c)))); + return { chains, protocols }; + }, [deploymentConfig, multiProvider]); + const numChains = chains.length; const [currentChainIndex, setCurrentChainIndex] = useState(0); const currentChain = chains[currentChainIndex]; const currentChainProtocol = multiProvider.getProtocol(currentChain); const currentChainDisplay = getChainDisplayName(multiProvider, currentChain, true); - const { wallets, isLoading: isDeployerLoading } = useTempDeployerWallets(protocols, onFailure); - const deployerAddress = getDeployerAddressForProtocol(wallets, currentChainProtocol); + const { wallets, isLoading: isDeployerLoading } = useOrCreateDeployerWallets( + protocols, + onFailure, + ); + const deployerAddress = wallets[currentChainProtocol]?.address; const { isPending: isTxPending, triggerTransaction } = useFundDeployerAccount( currentChain, diff --git a/src/features/store.ts b/src/features/store.ts index a2ab388..8bd3102 100644 --- a/src/features/store.ts +++ b/src/features/store.ts @@ -8,7 +8,7 @@ import { config } from '../consts/config'; import { CardPage } from '../flows/CardPage'; import { logger } from '../utils/logger'; import { assembleChainMetadata } from './chains/metadata'; -import type { TempDeployerKeys } from './deployerWallet/types'; +import type { DeployerKeys } from './deployerWallet/types'; import { DeploymentConfig, DeploymentContext, @@ -36,7 +36,7 @@ export interface AppState { }) => void; // Encrypted temp deployer keys - tempDeployerKeys: TempDeployerKeys; + deployerKeys: DeployerKeys; setDeployerKey: (protocol: ProtocolType, key: string) => void; removeDeployerKey: (protocol: ProtocolType) => void; @@ -88,17 +88,17 @@ export const useStore = create()( }, // Encrypted deployer keys - tempDeployerKeys: {}, + deployerKeys: {}, setDeployerKey: (protocol: ProtocolType, key: string) => { logger.debug('Setting deployer key in store for:', protocol); - const tempDeployerKeys = { ...get().tempDeployerKeys, [protocol]: key }; - set({ tempDeployerKeys }); + const deployerKeys = { ...get().deployerKeys, [protocol]: key }; + set({ deployerKeys }); }, removeDeployerKey: (protocol: ProtocolType) => { logger.debug('Removing deployer key in store for:', protocol); - const tempDeployerKeys = { ...get().tempDeployerKeys }; - delete tempDeployerKeys[protocol]; - set({ tempDeployerKeys }); + const deployerKeys = { ...get().deployerKeys }; + delete deployerKeys[protocol]; + set({ deployerKeys }); }, // User history @@ -157,7 +157,7 @@ export const useStore = create()( partialize: (state) => ({ // fields to persist chainMetadataOverrides: state.chainMetadataOverrides, - tempDeployerKeys: state.tempDeployerKeys, + deployerKeys: state.deployerKeys, deployments: state.deployments, }), version: PERSIST_STATE_VERSION, diff --git a/src/features/wallet/WalletFloatingButtons.tsx b/src/features/wallet/WalletFloatingButtons.tsx deleted file mode 100644 index a1fc0ed..0000000 --- a/src/features/wallet/WalletFloatingButtons.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from 'next/link'; - -import { DocsIcon, HistoryIcon, IconButton } from '@hyperlane-xyz/widgets'; -import { links } from '../../consts/links'; -import { Color } from '../../styles/Color'; -import { useStore } from '../store'; - -export function WalletFloatingButtons() { - const { setIsSideBarOpen, isSideBarOpen } = useStore((s) => ({ - setIsSideBarOpen: s.setIsSideBarOpen, - isSideBarOpen: s.isSideBarOpen, - })); - - return ( -
- setIsSideBarOpen(!isSideBarOpen)} - > - - - - - -
- ); -} - -const styles = { - link: 'hover:opacity-70 active:opacity-60', - roundedCircle: 'rounded-full bg-white', -}; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 1705e8d..3274956 100755 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,6 @@ import type { NextPage } from 'next'; import { Card } from '../components/layout/Card'; -import { WalletFloatingButtons } from '../features/wallet/WalletFloatingButtons'; +import { FloatingButtonRow } from '../components/nav/FloatingButtonRow'; import { CardFlow } from '../flows/CardFlow'; const Home: NextPage = () => { @@ -9,7 +9,7 @@ const Home: NextPage = () => { - + ); }; diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..8218e72 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,17 @@ +import { logger } from './logger'; + +export function tryPersistBrowserStorage() { + // Request persistent storage for site + // This prevents browser from clearing local storage when space runs low. Rare but possible. + // Not a critical perm (and not supported in safari) so not blocking on this + if (navigator?.storage?.persist) { + navigator.storage + .persist() + .then((isPersisted) => { + logger.debug(`Is persisted storage granted: ${isPersisted}`); + }) + .catch((reason) => { + logger.error('Error enabling storage persist setting', reason); + }); + } +}