diff --git a/src/features/deployerWallet/create.ts b/src/features/deployerWallet/create.ts deleted file mode 100644 index d9bfe12..0000000 --- a/src/features/deployerWallet/create.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { utils, Wallet } from 'ethers'; -import { config } from '../../consts/config'; -import { logger } from '../../utils/logger'; -import { setTempDeployerWallet } from './storage'; - -function createTempDeployerWallet(): Wallet { - logger.info('Creating temp deployer wallet'); - const entropy = utils.randomBytes(32); - const mnemonic = utils.entropyToMnemonic(entropy); - logger.info('Temp deployer wallet created'); - return Wallet.fromMnemonic(mnemonic); -} - -export async function createAndStoreTempDeployerWallet(): Promise { - const newWallet = createTempDeployerWallet(); - await setTempDeployerWallet( - newWallet, - config.tempWalletEncryptionKey, - config.tempWalletEncryptionSalt, - ); - return newWallet; -} diff --git a/src/features/deployerWallet/fund.ts b/src/features/deployerWallet/fund.ts new file mode 100644 index 0000000..55837c4 --- /dev/null +++ b/src/features/deployerWallet/fund.ts @@ -0,0 +1,39 @@ +import { MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { Numberish } from '@hyperlane-xyz/utils'; +import { useAccounts, useActiveChains, useTransactionFns } from '@hyperlane-xyz/widgets'; +import { useMutation } from '@tanstack/react-query'; +import { Wallet } from 'ethers'; +import { useMultiProvider } from '../chains/hooks'; + +export function useFundDeployerAccount(deployer: Wallet, chainName: ChainName, amount: Numberish) { + const multiProvider = useMultiProvider(); + const activeAccounts = useAccounts(multiProvider); + const activeChains = useActiveChains(multiProvider); + const transactionFns = useTransactionFns(multiProvider); + + const { isPending, mutateAsync, error } = useMutation({ + mutationKey: ['fundDeployerAccount', deployer.address, chainName, amount], + mutationFn: () => + executeTransfer({ multiProvider, activeAccounts, activeChains, transactionFns }), + }); + + return { + fund: mutateAsync, + isPending, + error, + }; +} + +async function executeTransfer({ + multiProvider, + activeAccounts, + activeChains, + transactionFns, +}: { + multiProvider: MultiProtocolProvider; + activeAccounts: ReturnType; + activeChains: ReturnType; + transactionFns: ReturnType; +}) { + //TODO +} diff --git a/src/features/deployerWallet/manage.ts b/src/features/deployerWallet/manage.ts new file mode 100644 index 0000000..cfa1ad9 --- /dev/null +++ b/src/features/deployerWallet/manage.ts @@ -0,0 +1,35 @@ +import { utils, Wallet } from 'ethers'; +import { config } from '../../consts/config'; +import { logger } from '../../utils/logger'; +import { getTempDeployerWallet, setTempDeployerWallet } from './storage'; + +function createTempDeployerWallet(): Wallet { + logger.info('Creating temp deployer wallet'); + const entropy = utils.randomBytes(32); + const mnemonic = utils.entropyToMnemonic(entropy); + logger.info('Temp deployer wallet created'); + return Wallet.fromMnemonic(mnemonic); +} + +export async function getOrCreateTempDeployerWallet(): Promise { + try { + const existingWallet = await getTempDeployerWallet( + config.tempWalletEncryptionKey, + config.tempWalletEncryptionSalt, + ); + if (existingWallet) { + logger.info('Using existing wallet from storage'); + return existingWallet; + } else { + const newWallet = createTempDeployerWallet(); + await setTempDeployerWallet( + newWallet, + config.tempWalletEncryptionKey, + config.tempWalletEncryptionSalt, + ); + return newWallet; + } + } catch (error) { + throw new Error('Error preparing temp deployer wallet', { cause: error }); + } +} diff --git a/src/features/deployerWallet/storage.ts b/src/features/deployerWallet/storage.ts index 9046bf9..ae802ed 100644 --- a/src/features/deployerWallet/storage.ts +++ b/src/features/deployerWallet/storage.ts @@ -11,10 +11,15 @@ export function hasTempDeployerWallet(): boolean { export async function getTempDeployerWallet( password: string, salt: string, -): Promise { - logger.debug('Reading temp deployer wallet from local storage'); +): Promise { + logger.debug('Checking temp deployer wallet from local storage'); const encryptedMnemonic = localStorage.getItem(STORAGE_PATH); - if (!encryptedMnemonic) return null; + if (!encryptedMnemonic) { + logger.debug('No temp deployer wallet found in local storage'); + return undefined; + } else { + logger.debug('Found temp deployer wallet in local storage'); + } const mnemonic = await decryptString(encryptedMnemonic, password, salt); return Wallet.fromMnemonic(mnemonic); } diff --git a/src/features/deployment/warp/WarpDeploymentDeploy.tsx b/src/features/deployment/warp/WarpDeploymentDeploy.tsx index 11441c3..18c0f70 100644 --- a/src/features/deployment/warp/WarpDeploymentDeploy.tsx +++ b/src/features/deployment/warp/WarpDeploymentDeploy.tsx @@ -1,5 +1,14 @@ -import { ArrowIcon, Button, Modal, useModal, WalletIcon } from '@hyperlane-xyz/widgets'; -import { useState } from 'react'; +import { + ArrowIcon, + Button, + Modal, + SpinnerIcon, + useModal, + WalletIcon, +} from '@hyperlane-xyz/widgets'; +import { useQuery } from '@tanstack/react-query'; +import { Wallet } from 'ethers'; +import { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import { PlanetSpinner } from '../../../components/animation/PlanetSpinner'; import { SolidButton } from '../../../components/buttons/SolidButton'; @@ -12,8 +21,16 @@ import { useCardNav } from '../../../flows/hooks'; import { Color } from '../../../styles/Color'; import { useMultiProvider } from '../../chains/hooks'; import { getChainDisplayName } from '../../chains/utils'; +import { getOrCreateTempDeployerWallet } from '../../deployerWallet/manage'; import { useWarpDeploymentConfig } from '../hooks'; +enum DeployStep { + PrepDeployer, + FundDeployer, + ExecuteDeploy, + AddFunds, +} + export function WarpDeploymentDeploy() { return (
@@ -32,24 +49,86 @@ function MainSection() { // TODO remove? // const { deploymentConfig } = useWarpDeploymentConfig(); // const _chains = deploymentConfig?.chains || []; + const { setPage } = useCardNav(); - const [isDeploying, _setIsDeploying] = useState(false); - const [isOutOfFunds] = useState(true); + const [step, setStep] = useState(DeployStep.PrepDeployer); + const [deployer, setDeployer] = useState(undefined); + + const onDeployerReady = (wallet: Wallet) => { + setDeployer(wallet); + setStep(DeployStep.FundDeployer); + }; + + const onDeployerFunded = () => { + setStep(DeployStep.ExecuteDeploy); + }; + + const onFailure = (error: Error) => { + // TODO carry error over via store state + toast.error(error.message); + setPage(CardPage.WarpFailure); + }; return (
- {isDeploying ? : isOutOfFunds ? : } + {step === DeployStep.PrepDeployer && ( + + )} + {step === DeployStep.FundDeployer && deployer && ( + + )} + {step === DeployStep.ExecuteDeploy && deployer && } + {step === DeployStep.AddFunds && deployer && }
); } -function FundAccounts() { +function PrepDeployerAccounts({ + onSuccess, + onFailure, +}: { + onSuccess: (wallet: Wallet) => void; + onFailure: (error: Error) => void; +}) { + const { error, data } = useQuery({ + queryKey: ['getDeployerWallet'], + queryFn: getOrCreateTempDeployerWallet, + }); + + useEffect(() => { + if (error) return onFailure(error); + if (data) return onSuccess(data); + }, [error, data, onSuccess, onFailure]); + + return ( +
+ +

Preparing deployer accounts

+
+ ); +} + +function FundDeployerAccounts({ + deployer, + onSuccess, + onFailure, +}: { + deployer: Wallet; + onSuccess: () => void; + onFailure: (error: Error) => void; +}) { + const multiProvider = useMultiProvider(); const { deploymentConfig } = useWarpDeploymentConfig(); const chains = deploymentConfig?.chains || []; const numChains = chains.length; const [currentChainIndex, setCurrentChainIndex] = useState(0); const currentChain = chains[currentChainIndex]; + const currentChainDisplay = getChainDisplayName(multiProvider, currentChain, true); const onClickFund = () => { // TODO create a temp deployer account and trigger a @@ -59,22 +138,21 @@ function FundAccounts() { }; return ( -
+
-

- To deploy your route, a temporary account must be funded for each chain. Unused amounts are - refunded. +

+ To deploy, a temporary account must be funded for each chain. Unused amounts are refunded.

{`Fund on ${currentChain} (Chain ${currentChainIndex + 1} / ${numChains})`} + >{`Fund on ${currentChainDisplay} (Chain ${currentChainIndex + 1} / ${numChains})`}
); } -function FundSingleAccount() { +function FundSingleDeployerAccount() { const onClickFund = () => { // TODO transfers funds from wallet to deployer }; @@ -98,14 +176,14 @@ function FundSingleAccount() { function FundIcons({ color }: { color: string }) { return (
- - - + + +
); } -function DeployStatus() { +function ExecuteDeploy() { const multiProvider = useMultiProvider(); const { deploymentConfig } = useWarpDeploymentConfig(); const chains = deploymentConfig?.chains || [];