From a6b8d0e04abf619b2dcce51ab233c45c4bfbd726 Mon Sep 17 00:00:00 2001 From: Christophe Deveaux Date: Thu, 7 Mar 2024 13:20:25 +0100 Subject: [PATCH] feat: Add safe wallet support (#77) * Bump wagmi version * Add RainbowKit, Merge Providers in one file * Add sepolia for getTargetChainId * Map over l2Networks in recover funds page * Add Sepolia support in RecoverFunds * Fix style * Fix prettier * Fix error logging * Fix recover process * Set popup for retryable * Bump version * Remove uppercase in walletConnect modal * Fix autologin with walletConnect * Add Sepolia to list of L1 network * Add loading in RecoverFunds page * Use network name rather than chainId * Add checkbox * Fix popup * Add comments * Fix alignment * Add check for loading account type * Fix alignment * Add error for balance call * Properly clear localStorage for walletConnect --- .env.sample | 2 + .nvmrc | 2 +- next.config.js | 15 +- package.json | 3 +- src/app/global.css | 12 +- src/app/page.tsx | 6 +- .../recover-funds/[address]/RecoverFunds.tsx | 52 +- .../[address]/RecoverFundsButton.tsx | 60 +- src/app/recover-funds/[address]/page.tsx | 193 +- src/app/recover-funds/layout.tsx | 10 +- src/app/recover-funds/style.css | 6 + src/app/retryables-tracker-all/layout.tsx | 4 +- .../retryables-tracker/[address]/layout.tsx | 4 +- src/app/tx/[tx]/L2ToL1MsgsDisplay.tsx | 1 + src/app/tx/[tx]/Redeem.tsx | 21 +- src/app/tx/[tx]/page.tsx | 28 +- src/components/ConnectButton.tsx | 38 +- src/components/Providers.tsx | 162 ++ src/components/Redeem.tsx | 1 + src/components/WagmiProvider.tsx | 96 - src/utils/getTargetChainId.ts | 2 + src/utils/network.ts | 12 + src/utils/useAccountType.ts | 37 + yarn.lock | 1828 +++++++++++++---- 24 files changed, 1965 insertions(+), 630 deletions(-) create mode 100644 src/components/Providers.tsx delete mode 100644 src/components/WagmiProvider.tsx create mode 100644 src/utils/useAccountType.ts diff --git a/.env.sample b/.env.sample index 57f831a..b4a4d84 100644 --- a/.env.sample +++ b/.env.sample @@ -9,5 +9,7 @@ NEXT_PUBLIC_ARBITRUM_GOERLI_RPC_URL= NEXT_PUBLIC_ARBITRUM_NOVA_RPC_URL= NEXT_PUBLIC_ARBITRUM_SEPOLIA_RPC_URL= +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= + NEXT_PUBLIC_LOCAL_ETHEREUM_RPC_URL=http://localhost:8545 NEXT_PUBLIC_LOCAL_ARBITRUM_RPC_URL=http://localhost:8547 diff --git a/.nvmrc b/.nvmrc index 5397c87..3c03207 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.18.1 +18 diff --git a/next.config.js b/next.config.js index f053ebf..87822ef 100644 --- a/next.config.js +++ b/next.config.js @@ -1 +1,14 @@ -module.exports = {}; +module.exports = { + // pino throw error during yarn build, + // see https://github.com/WalletConnect/walletconnect-monorepo/issues/1908#issuecomment-1487801131 + webpack: (config, context) => { + if (config.plugins) { + config.plugins.push( + new context.webpack.IgnorePlugin({ + resourceRegExp: /^(lokijs|pino-pretty|encoding)$/, + }), + ); + } + return config; + }, +}; diff --git a/package.json b/package.json index f530198..dbf42b0 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@arbitrum/sdk": "^3.1.12", "@ethersproject/bignumber": "^5.1.1", + "@rainbow-me/rainbowkit": "^0.12.18", "@types/node": "^16.7.13", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", @@ -16,7 +17,7 @@ "react-feather": "^2.0.10", "react-tooltip": "^5.5.1", "typescript": "^4.4.2", - "wagmi": "^0.8.10" + "wagmi": "^0.12.19" }, "engines": { "node": ">=16" diff --git a/src/app/global.css b/src/app/global.css index fca2166..2963a62 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -89,6 +89,8 @@ main { max-width: 740px; gap: 10px; margin-bottom: var(--space-xl); + margin-left: auto; + margin-right: auto; } .form-inner { @@ -119,7 +121,7 @@ main { background-color: rgb(255, 255, 255); } -button, +.button, input[type='submit'] { background-color: var(--blue); color: #fff; @@ -135,12 +137,12 @@ input[type='submit'] { cursor: pointer; } -button:hover, +.button:hover, input[type='submit']:hover { background-color: var(--blue-dark); } -button:disabled { +.button:disabled { background-color: var(--blue-lighter); cursor: inherit; } @@ -274,3 +276,7 @@ ul { .redeem-button-container { text-align: center; } + +div[data-rk] { + width: 100%; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 6745bb0..17d48bc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ import { NextPage } from 'next'; -import { WagmiProvider } from '@/components/WagmiProvider'; +import { Providers } from '@/components/Providers'; import { Logo } from '@/components/Logo'; import { Form } from '@/components/Form'; @@ -12,9 +12,9 @@ const PageIndex: NextPage = () => {
- +
- +
); diff --git a/src/app/recover-funds/[address]/RecoverFunds.tsx b/src/app/recover-funds/[address]/RecoverFunds.tsx index ec5cefa..d37c51a 100644 --- a/src/app/recover-funds/[address]/RecoverFunds.tsx +++ b/src/app/recover-funds/[address]/RecoverFunds.tsx @@ -1,13 +1,11 @@ -'use client'; -import { Address } from '@arbitrum/sdk'; -import { BigNumber, utils, constants } from 'ethers'; -import { useNetwork } from 'wagmi'; -import { getProviderFromChainId, getTargetChainId } from '@/utils'; +import { mapChainIdToName } from '@/utils/network'; +import { BigNumber, utils } from 'ethers'; import '../style.css'; export interface OperationInfo { balanceToRecover: BigNumber; aliasedAddress: string; + chainId: string; } export const hasBalanceOverThreshold = (balanceToRecover: BigNumber) => { @@ -16,59 +14,19 @@ export const hasBalanceOverThreshold = (balanceToRecover: BigNumber) => { return balanceToRecover.gte(BigNumber.from(5_000_000_000_000_000)); }; -export async function getData( - chainID: number, - address: string, -): Promise { - // First, obtain the aliased address of the signer - const destinationAddress = new Address(address); - const { value: aliasedAddress } = destinationAddress.applyAlias(); - - // And get its balance to find out the amount we are transferring - try { - const l2Provider = getProviderFromChainId(chainID); - const aliasedSignerBalance = await l2Provider.getBalance(aliasedAddress); - - return { - balanceToRecover: hasBalanceOverThreshold(aliasedSignerBalance) - ? aliasedSignerBalance - : constants.Zero, - aliasedAddress, - }; - } catch (e) { - return { - balanceToRecover: constants.Zero, - aliasedAddress, - }; - } -} - type Props = { operationInfo: OperationInfo; address: string; }; const RecoverFunds = ({ operationInfo, address }: Props) => { - const { chain } = useNetwork(); - const targetChainID = getTargetChainId(chain?.id); - - // No funds to recover - if (!hasBalanceOverThreshold(operationInfo.balanceToRecover)) { - return ( -
- There are no funds stuck on {operationInfo.aliasedAddress} -
- (Alias of {address}) on this network - {targetChainID ? ` (${targetChainID})` : ''}. -
- ); - } + const l2ChainId = operationInfo.chainId; return (
There are {utils.formatEther(operationInfo.balanceToRecover)} ETH on{' '} {operationInfo.aliasedAddress}
- (Alias of {address}). + (Alias of {address}) on this network {mapChainIdToName[l2ChainId] ?? ''}.
); }; diff --git a/src/app/recover-funds/[address]/RecoverFundsButton.tsx b/src/app/recover-funds/[address]/RecoverFundsButton.tsx index bf608d4..95f9049 100644 --- a/src/app/recover-funds/[address]/RecoverFundsButton.tsx +++ b/src/app/recover-funds/[address]/RecoverFundsButton.tsx @@ -8,9 +8,23 @@ import { } from '@arbitrum/sdk'; import { Inbox__factory } from '@arbitrum/sdk/dist/lib/abi/factories/Inbox__factory'; import { getBaseFee } from '@arbitrum/sdk/dist/lib/utils/lib'; -import { useNetwork, useSigner } from 'wagmi'; +import { goerli, mainnet, sepolia, useNetwork, useSigner } from 'wagmi'; import { getProviderFromChainId, getTargetChainId } from '@/utils'; import { BigNumber } from 'ethers'; +import { ChainId } from '@/utils/network'; +import { useAccountType } from '@/utils/useAccountType'; + +function getL1ChainIdFromL2ChainId(l2ChainId: number | undefined) { + if (!l2ChainId) { + return ChainId.Mainnet; + } + + return { + [ChainId.ArbitrumOne]: ChainId.Mainnet, + [ChainId.ArbitrumGoerli]: ChainId.Goerli, + [ChainId.ArbitrumSepolia]: ChainId.Sepolia, + }[l2ChainId]; +} function RecoverFundsButton({ balanceToRecover, @@ -25,8 +39,12 @@ function RecoverFundsButton({ }) { const [message, setMessage] = useState(''); const [loading, setLoading] = useState(false); + const { isSmartContractWallet, isLoading: isLoadingAccountType } = + useAccountType(); const { chain } = useNetwork(); - const { data: signer } = useSigner({ chainId: chain?.id }); + const { data: signer } = useSigner({ + chainId: getL1ChainIdFromL2ChainId(chainID), + }); const handleRecover = useCallback(async () => { if (!signer) { @@ -45,10 +63,7 @@ function RecoverFundsButton({ setLoading(true); setMessage(''); - // We instantiate the Inbox factory object to make use of its methods - const targetChainID = getTargetChainId(chainID); - - const baseL2Provider = getProviderFromChainId(targetChainID); + const baseL2Provider = getProviderFromChainId(chainID); const l2Network = await getL2Network(baseL2Provider); const inbox = Inbox__factory.connect( l2Network.ethBridge.inbox, @@ -143,16 +158,20 @@ function RecoverFundsButton({ if (!signer) return null; - if (chain?.id !== 1 && chain?.id !== 5) { + if ( + chain?.id !== mainnet.id && + chain?.id !== goerli.id && + chain?.id !== sepolia.id + ) { return (
Unknown L1 chain id. This chain is not supported by this tool
); } - if (chain?.id !== chainID) { + if (getTargetChainId(chain?.id) !== chainID) { return (
- To recover funds, connect to chain ${chain?.id} (${chain?.name}) + To recover funds, connect to chain {chain?.id} ({chain?.name})
); } @@ -160,9 +179,26 @@ function RecoverFundsButton({ return ( <>
- +
+ +
+ {loading && isSmartContractWallet && !isLoadingAccountType && ( +
+ + + To continue, please approve tx on your smart contract wallet. + {' '} + If you have k of n signers, then k of n will need to sign. + +
+ )}
{message && ( diff --git a/src/app/recover-funds/[address]/page.tsx b/src/app/recover-funds/[address]/page.tsx index 5ce79bf..79ef0f5 100644 --- a/src/app/recover-funds/[address]/page.tsx +++ b/src/app/recover-funds/[address]/page.tsx @@ -1,53 +1,79 @@ 'use client'; -import { getTargetChainId } from '@/utils'; -import { utils } from 'ethers'; +import { mapChainIdToName, supportedL2Networks } from '@/utils/network'; +import { Address } from '@arbitrum/sdk'; +import { constants, utils } from 'ethers'; import dynamic from 'next/dynamic'; import React, { useEffect, useState } from 'react'; import { useNetwork } from 'wagmi'; -import { - getData, - hasBalanceOverThreshold, - OperationInfo, -} from './RecoverFunds'; +import { hasBalanceOverThreshold, OperationInfo } from './RecoverFunds'; import { RecoverFundsButton } from './RecoverFundsButton'; +import { JsonRpcProvider } from '@ethersproject/providers'; const RecoverFunds = dynamic(() => import('./RecoverFunds'), { ssr: false, }); -const RecoverFundsPage = ({ - params: { address }, -}: { - params: { address: string }; -}) => { - const { chain } = useNetwork(); - const [operationInfo, setOperationInfo] = useState( - null, - ); - const [destinationAddress, setDestinationAddress] = useState( - null, - ); +type OperationInfoByChainId = { + [chainId: string]: OperationInfo & { error?: true }; +}; +async function getOperationInfoByChainId( + address: string, +): Promise { + // First, obtain the aliased address of the signer + const destinationAddress = new Address(address); + const { value: aliasedAddress } = destinationAddress.applyAlias(); - const targetChainID = getTargetChainId(chain?.id); + // And get its balance to find out the amount we are transferring + const operationInfoPromises = Object.entries(supportedL2Networks).map( + async ([chainId, rpcURL]) => { + const l2Provider = new JsonRpcProvider(rpcURL); - useEffect(() => { - if (!targetChainID) { - return; - } + try { + const aliasedSignerBalance = await l2Provider.getBalance( + aliasedAddress, + ); - getData(targetChainID, address).then((data) => { - setOperationInfo(data); - }); - }, [address, targetChainID]); + return { + balanceToRecover: hasBalanceOverThreshold(aliasedSignerBalance) + ? aliasedSignerBalance + : constants.Zero, + aliasedAddress, + chainId, + }; + } catch (e) { + return { + balanceToRecover: constants.Zero, + aliasedAddress, + chainId, + error: true, + }; + } + }, + ); - const handleChange: React.ChangeEventHandler = (e) => { - const value = e.target.value; - setDestinationAddress(value); - }; + const result = Promise.all(operationInfoPromises); + return result.then((operationInfo) => { + return operationInfo.reduce( + (acc, info) => ({ + ...acc, + [info.chainId]: info, + }), + {}, + ); + }); +} - if (!operationInfo) { - return; - } +function RecoverFundsDetail({ + operationInfo, + address, +}: { + operationInfo: OperationInfo; + address: string; +}) { + const { chain } = useNetwork(); + const [destinationAddress, setDestinationAddress] = useState( + null, + ); const hasBalanceToRecover = hasBalanceOverThreshold( operationInfo.balanceToRecover, @@ -58,6 +84,17 @@ const RecoverFundsPage = ({ utils.isAddress(destinationAddress) && operationInfo.aliasedAddress; + const handleChange: React.ChangeEventHandler = (e) => { + const value = e.target.value; + setDestinationAddress(value); + }; + + const [checkboxAccepted, setCheckboxAccepted] = useState(false); + + if (!hasBalanceOverThreshold(operationInfo.balanceToRecover)) { + return null; + } + return ( <> @@ -65,15 +102,32 @@ const RecoverFundsPage = ({
)} - {hasBalanceToRecover && hasDestinationAddress && ( + {hasDestinationAddress && ( + <> + setCheckboxAccepted(e.target.checked)} + /> + + + )} + {checkboxAccepted && hasBalanceToRecover && hasDestinationAddress && ( ); +} + +const RecoverFundsPage = ({ + params: { address }, +}: { + params: { address: string }; +}) => { + const [operationInfos, setOperationInfos] = + useState(null); + + useEffect(() => { + getOperationInfoByChainId(address).then((operationInfoByChainId) => { + setOperationInfos(operationInfoByChainId); + }); + }, [address]); + + if (!operationInfos) { + return
Loading...
; + } + + const operationInfoKeys = Object.keys(operationInfos); + // No balance to recover on any chains + if ( + operationInfoKeys.every( + (chainId) => + !hasBalanceOverThreshold(operationInfos[chainId].balanceToRecover), + ) + ) { + const aliasedAddress = operationInfos[operationInfoKeys[0]].aliasedAddress; + const errors = operationInfoKeys.filter( + (operationInfoKey) => operationInfos[operationInfoKey].error, + ); + + if (errors.length > 0) { + return ( +
+ There was an error checking the balance of {aliasedAddress} +
+ (Alias of {address}) on Arbitrum networks +
+ ); + } + + return ( +
+ There are no funds stuck on {aliasedAddress} +
+ (Alias of {address}) on Arbitrum networks +
+ ); + } + + return ( + <> + {Object.keys(operationInfos).map((chainId) => ( + + ))} + + ); }; export default RecoverFundsPage; diff --git a/src/app/recover-funds/layout.tsx b/src/app/recover-funds/layout.tsx index ab1e2d4..3f3bdef 100644 --- a/src/app/recover-funds/layout.tsx +++ b/src/app/recover-funds/layout.tsx @@ -1,7 +1,7 @@ import dynamic from 'next/dynamic'; import { Suspense } from 'react'; import { Logo } from '@/components/Logo'; -import { WagmiProvider } from '@/components/WagmiProvider'; +import { Providers } from '@/components/Providers'; import { Form } from '@/components/Form'; import './style.css'; @@ -24,19 +24,19 @@ export default function Layout({ children }: LayoutProps) {

Tool to recover funds that are locked in an aliased L2 address.
- Connect to either Ethereum mainnet or Goerli to start the recovery - process. + Connect to either Ethereum mainnet, Goerli or Sepolia to start the + recovery process.

- + {children} - +
); diff --git a/src/app/recover-funds/style.css b/src/app/recover-funds/style.css index 38fa956..df21654 100644 --- a/src/app/recover-funds/style.css +++ b/src/app/recover-funds/style.css @@ -3,7 +3,10 @@ } .recover-funds-form { + max-width: 740px; margin-top: var(--space-m); + margin-left: auto; + margin-right: auto; } .recover-funds-form input { width: 500px; @@ -17,6 +20,9 @@ .redeemtext, .recoverfundstext { width: 100%; + max-width: 740px; + margin-left: auto; + margin-right: auto; margin-top: var(--space-m); border: 1px solid white; border-radius: 5px; diff --git a/src/app/retryables-tracker-all/layout.tsx b/src/app/retryables-tracker-all/layout.tsx index 884c678..dbe8811 100644 --- a/src/app/retryables-tracker-all/layout.tsx +++ b/src/app/retryables-tracker-all/layout.tsx @@ -1,5 +1,5 @@ import { Logo } from '@/components/Logo'; -import { WagmiProvider } from '@/components/WagmiProvider'; +import { Providers } from '@/components/Providers'; import { PropsWithChildren } from 'react'; export default function Layout({ children }: PropsWithChildren) { @@ -12,7 +12,7 @@ export default function Layout({ children }: PropsWithChildren) {
- {children} + {children}
); diff --git a/src/app/retryables-tracker/[address]/layout.tsx b/src/app/retryables-tracker/[address]/layout.tsx index 26aa8f1..efa602f 100644 --- a/src/app/retryables-tracker/[address]/layout.tsx +++ b/src/app/retryables-tracker/[address]/layout.tsx @@ -1,6 +1,6 @@ import { Form } from '@/components/Form'; import { Logo } from '@/components/Logo'; -import { WagmiProvider } from '@/components/WagmiProvider'; +import { Providers } from '@/components/Providers'; import { PropsWithChildren } from 'react'; export default function Layout({ children }: PropsWithChildren) { @@ -14,7 +14,7 @@ export default function Layout({ children }: PropsWithChildren) {
- {children} + {children}
); diff --git a/src/app/tx/[tx]/L2ToL1MsgsDisplay.tsx b/src/app/tx/[tx]/L2ToL1MsgsDisplay.tsx index 2190d68..c697d17 100644 --- a/src/app/tx/[tx]/L2ToL1MsgsDisplay.tsx +++ b/src/app/tx/[tx]/L2ToL1MsgsDisplay.tsx @@ -82,6 +82,7 @@ function L2ToL1MsgsDisplay({ l2ToL1Messages }: Props) { ) : (
); }, [ - chain?.id, signer, + chain?.id, chainID, + isLoading, networkName, + messageData, sender, messageNumber, l1BaseFee, - messageData, ]); return ( <> {redeemButton} + {isLoading && isSmartContractWallet && ( +
+ + To continue, please approve tx on your smart contract wallet.{' '} + If you have k of n signers, then k of n will need to sign. + +
+ )}
{message && (