From 07b502d797f3ca2f60b450da8347667ab139e577 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Wed, 25 May 2022 15:49:20 +0300 Subject: [PATCH 01/51] Group wallets by protocol and service --- apps/ui/src/components/MultiWalletModal.scss | 15 ++ apps/ui/src/components/MultiWalletModal.tsx | 203 +++++++++--------- apps/ui/src/components/SingleWalletModal.tsx | 6 +- apps/ui/src/config/ecosystem.ts | 21 ++ apps/ui/src/contexts/evmWallet.tsx | 7 +- apps/ui/src/contexts/solanaWallet.tsx | 11 +- .../ui/src/hooks/crossEcosystem/useWallets.ts | 11 +- apps/ui/src/models/wallets/services.tsx | 19 +- 8 files changed, 175 insertions(+), 118 deletions(-) create mode 100644 apps/ui/src/components/MultiWalletModal.scss diff --git a/apps/ui/src/components/MultiWalletModal.scss b/apps/ui/src/components/MultiWalletModal.scss new file mode 100644 index 000000000..827cbaeb9 --- /dev/null +++ b/apps/ui/src/components/MultiWalletModal.scss @@ -0,0 +1,15 @@ +.walletServiceButton { + white-space: nowrap; + + &__ecosystems { + margin: 10px 0 0 30px; + + & > li { + margin-bottom: 15px; + + img { + margin-right: 15px; + } + } + } +} diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index d24bf317f..58474c262 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -12,28 +12,25 @@ import { import type { ReactElement } from "react"; import { Fragment } from "react"; -import { EcosystemId } from "../config"; -import { useConfig } from "../contexts"; +import type { Ecosystem } from "../config"; +import { Protocol, getEcosystemsForProtocol } from "../config"; import { useWallets } from "../hooks"; -import AVALANCHE_SVG from "../images/ecosystems/avalanche.svg"; -import BSC_SVG from "../images/ecosystems/bsc.svg"; import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; -import POLYGON_SVG from "../images/ecosystems/polygon.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; -import { - AVALANCHE_WALLET_SERVICES, - BSC_WALLET_SERVICES, - ETHEREUM_WALLET_SERVICES, - POLYGON_WALLET_SERVICES, - SOLANA_WALLET_SERVICES, -} from "../models"; +import { WALLET_SERVICES } from "../models"; import type { WalletService } from "../models"; -import { isUserOnMobileDevice, shortenAddress } from "../utils"; +import { + findOrThrow, + groupBy, + isUserOnMobileDevice, + shortenAddress, +} from "../utils"; import { CustomModal } from "./CustomModal"; import { MobileDeviceDisclaimer } from "./MobileDeviceDisclaimer"; import "./ConnectButton.scss"; +import "./MultiWalletModal.scss"; interface WalletServiceButtonProps { readonly service: W; @@ -41,6 +38,7 @@ interface WalletServiceButtonProps { readonly disconnect: () => void; readonly serviceConnected: boolean; readonly address: string | null; + readonly ecosystems: ReadonlyArray; } const WalletServiceButton = ({ @@ -49,12 +47,13 @@ const WalletServiceButton = ({ onClick, serviceConnected, address, + ecosystems, }: WalletServiceButtonProps): ReactElement => { const { info: { icon, name, helpText }, } = service; return ( - + ({ {helpText && <>{helpText}} + {ecosystems.length > 1 && ( +
    + {ecosystems.map((ecosystem) => ( +
  • + + {ecosystem.displayName} +
  • + ))} +
+ )}
); }; -interface EcosystemWalletOptionsListProps< - W extends WalletService = WalletService, -> { - readonly address: string | null; - readonly connected: boolean; +interface ProtocolWalletOptionsListProps { readonly icon: string; - readonly ecosystemName: string; - readonly walletServices: readonly W[]; - readonly ecosystemId: EcosystemId; - readonly createServiceClickHandler: (service: W) => () => void; + readonly protocol: Protocol; } -const EcosystemWalletOptionsList = ({ - address, +const ProtocolWalletOptionsList = ({ icon, - connected, - ecosystemName, - walletServices, - ecosystemId, - createServiceClickHandler, -}: EcosystemWalletOptionsListProps): ReactElement => { - // needed for wallet extraction to work - if (ecosystemId === EcosystemId.Terra) { - throw new Error("Unsupported ecosystem"); + protocol, +}: ProtocolWalletOptionsListProps): ReactElement => { + if (protocol === Protocol.Cosmos) { + throw new Error("Unsupported protocol"); } + const wallets = useWallets(); - const { wallet, service: currentService } = wallets[ecosystemId]; + const ecosystemIds = getEcosystemsForProtocol(protocol); + const protocolWalletServices = ecosystemIds.flatMap( + (ecosystemId) => WALLET_SERVICES[ecosystemId], + ); + const protocolWalletServicesByServiceId = groupBy( + protocolWalletServices, + (protocolwalletService) => protocolwalletService.id, + ); + const protocolWallets = ecosystemIds.map( + (ecosystemId) => wallets[ecosystemId], + ); + + const connectedWallets = protocolWallets.filter((wallet) => wallet.connected); - const disconnect = (): void => { - void wallet?.disconnect(); + const disconnect = (serviceId: string): void => { + protocolWalletServicesByServiceId[serviceId].forEach((walletService) => { + const ecosystemId = walletService.info.ecosystem.id; + const wallet = wallets[ecosystemId]; + + if (wallet.connected && wallet.service?.id === serviceId) { + wallets[ecosystemId].wallet?.disconnect().catch(console.error); + } + }); + }; + + const connect = (serviceId: string) => { + protocolWalletServicesByServiceId[serviceId].forEach((walletService) => { + const ecosystemId = walletService.info.ecosystem.id; + const wallet = wallets[ecosystemId]; + + if (wallet.createServiceClickHandler) { + wallet.createServiceClickHandler(serviceId)(); + } + }); }; return ( @@ -110,25 +137,39 @@ const EcosystemWalletOptionsList = ({

- {ecosystemName} + {protocol}

- {walletServices.map((service) => { - return ( - - - - ); - })} + {Object.entries(protocolWalletServicesByServiceId).map( + ([serviceId, serviceWalletServices]) => { + const service = findOrThrow( + protocolWalletServices, + (walletService) => walletService.id === serviceId, + ); + + const ecosystems = serviceWalletServices.map( + (walletService) => walletService.info.ecosystem, + ); + + const connectedWallet = connectedWallets.find( + (wallet) => wallet.service?.id === serviceId, + ); + + return ( + + disconnect(service.id)} + onClick={() => connect(service.id)} + /> + + ); + }, + )} ); }; @@ -140,15 +181,6 @@ export interface MultiWalletModalProps { export const MultiWalletModal = ({ handleClose, }: MultiWalletModalProps): ReactElement => { - const { solana, ethereum, bsc, avalanche, polygon } = useWallets(); - - const { ecosystems } = useConfig(); - const solanaEcosystem = ecosystems[EcosystemId.Solana]; - const ethereumEcosystem = ecosystems[EcosystemId.Ethereum]; - const bscEcosystem = ecosystems[EcosystemId.Bsc]; - const avalancheEcosystem = ecosystems[EcosystemId.Avalanche]; - const polygonEcosystem = ecosystems[EcosystemId.Polygon]; - return ( @@ -161,50 +193,13 @@ export const MultiWalletModal = ({ {isUserOnMobileDevice() ? : ""} - - - - - diff --git a/apps/ui/src/components/SingleWalletModal.tsx b/apps/ui/src/components/SingleWalletModal.tsx index 01c3bf4b1..22a4ba97b 100644 --- a/apps/ui/src/components/SingleWalletModal.tsx +++ b/apps/ui/src/components/SingleWalletModal.tsx @@ -9,7 +9,7 @@ import { import type { ReactElement } from "react"; import { Fragment } from "react"; -import type { WalletService } from "../models"; +import type { WalletAdapter, WalletService } from "../models"; import { isUserOnMobileDevice } from "../utils"; import { CustomModal } from "./CustomModal"; @@ -22,7 +22,7 @@ export interface SingleWalletModalProps< readonly services: readonly W[]; readonly handleClose: () => void; readonly createServiceClickHandler: ( - service: W, + serviceId: WalletService["id"], callback?: () => any, ) => () => void; } @@ -51,7 +51,7 @@ export const SingleWalletModal = ({ {name} diff --git a/apps/ui/src/config/ecosystem.ts b/apps/ui/src/config/ecosystem.ts index 04781a06b..8b1ef5a88 100644 --- a/apps/ui/src/config/ecosystem.ts +++ b/apps/ui/src/config/ecosystem.ts @@ -77,6 +77,7 @@ export const isEvmEcosystemId = ( export type CosmosEcosystemId = Extract; export interface Ecosystem { + readonly id: EcosystemId; readonly protocol: Protocol; readonly wormholeChainId: WormholeChainId; readonly displayName: string; @@ -86,6 +87,7 @@ export interface Ecosystem { export const ecosystems: ReadonlyRecord = { [EcosystemId.Solana]: { + id: EcosystemId.Solana, protocol: Protocol.Solana, wormholeChainId: WormholeChainId.Solana, displayName: "Solana", @@ -93,6 +95,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "SOL", }, [EcosystemId.Ethereum]: { + id: EcosystemId.Ethereum, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Ethereum, displayName: "Ethereum", @@ -100,6 +103,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "ETH", }, [EcosystemId.Terra]: { + id: EcosystemId.Terra, protocol: Protocol.Cosmos, wormholeChainId: WormholeChainId.Terra, displayName: "Terra", @@ -107,6 +111,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "LUNA", }, [EcosystemId.Bsc]: { + id: EcosystemId.Bsc, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Bsc, displayName: "BNB Chain", @@ -114,6 +119,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "BNB", }, [EcosystemId.Avalanche]: { + id: EcosystemId.Avalanche, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Avalanche, displayName: "Avalanche", @@ -121,6 +127,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "AVAX", }, [EcosystemId.Polygon]: { + id: EcosystemId.Polygon, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Polygon, displayName: "Polygon", @@ -128,6 +135,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "MATIC", }, [EcosystemId.Aurora]: { + id: EcosystemId.Aurora, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Aurora, displayName: "Aurora", @@ -135,6 +143,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "ETH", }, [EcosystemId.Fantom]: { + id: EcosystemId.Fantom, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Fantom, displayName: "Fantom", @@ -142,6 +151,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "FTM", }, [EcosystemId.Karura]: { + id: EcosystemId.Karura, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Karura, displayName: "Karura", @@ -149,6 +159,7 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "KAR", }, [EcosystemId.Acala]: { + id: EcosystemId.Acala, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Acala, displayName: "Acala", @@ -156,3 +167,13 @@ export const ecosystems: ReadonlyRecord = { nativeTokenSymbol: "ACA", }, }; + +export const getEcosystemsForProtocol = ( + protocol: Protocol, +): readonly EcosystemId[] => { + return Object.entries(ecosystems) + .filter(([ecosystemId, ecosystem]) => { + return ecosystem.protocol === protocol; + }) + .map(([ecosystemId]) => ecosystemId as EcosystemId); +}; diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index feae90027..26ec67b8e 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -108,7 +108,9 @@ export interface EvmWalletContextInterface { readonly connected: boolean; readonly select: () => void; readonly service: WalletService | null; - readonly createServiceClickHandler: (service: WalletService) => () => void; + readonly createServiceClickHandler: ( + serviceId: WalletService["id"], + ) => () => void; } const defaultEvmWalletContext: EvmWalletContextInterface = { @@ -249,8 +251,7 @@ export const EvmWalletProvider = ({ const select = useCallback(() => setIsModalVisible(true), []); const closeModal = useCallback(() => setIsModalVisible(false), []); const createServiceClickHandler = - ({ id }: WalletService, callback?: () => any) => - (): void => { + (id: WalletService["id"], callback?: () => any) => (): void => { setServiceId(id); setAutoConnect(true); callback?.(); diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index f2835685a..c82438189 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -15,7 +15,11 @@ import { Protocol } from "../config"; import { selectNotify } from "../core/selectors"; import { useNotification } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; -import type { SolanaWalletAdapter, SolanaWalletService } from "../models"; +import type { + SolanaWalletAdapter, + SolanaWalletService, + WalletService, +} from "../models"; import { SOLANA_WALLET_SERVICES } from "../models"; import { shortenAddress } from "../utils"; @@ -28,7 +32,7 @@ export interface SolanaWalletContextInterface { readonly select: () => void; readonly service: SolanaWalletService | null; readonly createServiceClickHandler: ( - service: SolanaWalletService, + serviceId: WalletService["id"], ) => () => void; } @@ -137,8 +141,7 @@ export const SolanaWalletProvider = ({ const select = useCallback(() => setIsModalVisible(true), []); const closeModal = useCallback(() => setIsModalVisible(false), []); const createServiceClickHandler = - ({ id }: SolanaWalletService, callback?: () => any) => - (): void => { + (id: WalletService["id"], callback?: () => any) => (): void => { setServiceId(id); setAutoConnect(true); callback?.(); diff --git a/apps/ui/src/hooks/crossEcosystem/useWallets.ts b/apps/ui/src/hooks/crossEcosystem/useWallets.ts index 10c372357..41aa4af1c 100644 --- a/apps/ui/src/hooks/crossEcosystem/useWallets.ts +++ b/apps/ui/src/hooks/crossEcosystem/useWallets.ts @@ -13,6 +13,9 @@ export interface Wallets extends ReadonlyRecord { readonly [EcosystemId.Terra]: { readonly address: null; readonly connected: false; + readonly service: null; + readonly wallet: null; + readonly createServiceClickHandler: null; }; readonly [EcosystemId.Bsc]: EvmWalletContextInterface; readonly [EcosystemId.Avalanche]: EvmWalletContextInterface; @@ -26,7 +29,13 @@ export interface Wallets extends ReadonlyRecord { export const useWallets = (): Wallets => ({ [EcosystemId.Solana]: useSolanaWallet(), [EcosystemId.Ethereum]: useEvmWallet(EcosystemId.Ethereum), - [EcosystemId.Terra]: { address: null, connected: false }, + [EcosystemId.Terra]: { + address: null, + connected: false, + service: null, + createServiceClickHandler: null, + wallet: null, + }, [EcosystemId.Bsc]: useEvmWallet(EcosystemId.Bsc), [EcosystemId.Avalanche]: useEvmWallet(EcosystemId.Avalanche), [EcosystemId.Polygon]: useEvmWallet(EcosystemId.Polygon), diff --git a/apps/ui/src/models/wallets/services.tsx b/apps/ui/src/models/wallets/services.tsx index 6e652497f..2f0dfb4a6 100644 --- a/apps/ui/src/models/wallets/services.tsx +++ b/apps/ui/src/models/wallets/services.tsx @@ -20,6 +20,7 @@ export interface WalletServiceInfo { readonly url: string; readonly icon: string; readonly helpText?: ReactElement; + readonly ecosystem: Ecosystem; } export interface WalletService { @@ -41,47 +42,54 @@ const solletInfo: WalletServiceInfo = { name: "Sollet", url: "https://www.sollet.io", icon: `${OYSTER_ASSETS_URL}sollet.svg`, + ecosystem: ecosystems[EcosystemId.Solana], }; const solongInfo: WalletServiceInfo = { name: "Solong", url: "https://solongwallet.com", icon: `${OYSTER_ASSETS_URL}solong.png`, + ecosystem: ecosystems[EcosystemId.Solana], }; const solflareInfo: WalletServiceInfo = { name: "Solflare", url: "https://solflare.com/access-wallet", icon: `${OYSTER_ASSETS_URL}solflare.svg`, + ecosystem: ecosystems[EcosystemId.Solana], }; const mathWalletInfo: WalletServiceInfo = { name: "MathWallet", url: "https://www.mathwallet.org", icon: MATHWALLET_ICON, + ecosystem: ecosystems[EcosystemId.Solana], }; const ledgerInfo: WalletServiceInfo = { name: "Ledger", url: "https://www.ledger.com", icon: LEDGER_ICON, + ecosystem: ecosystems[EcosystemId.Solana], }; const phantomInfo: WalletServiceInfo = { name: "Phantom", url: "https://phantom.app", icon: PHANTOM_ICON, + ecosystem: ecosystems[EcosystemId.Solana], }; -const metaMaskInfo: WalletServiceInfo = { +const metaMaskInfo: Omit = { name: "Metamask", url: "https://metamask.io", icon: METAMASK_ICON, }; const addHelpTextToMetaMaskInfo = ( - info: WalletServiceInfo, + info: Omit, ecosystem: Ecosystem, url: string, ): WalletServiceInfo => { const title = `How to add ${ecosystem.displayName} to Metamask`; return { ...info, + ecosystem, helpText: ( [ [ { id: "metamask", - info: metaMaskInfo, + info: ethereumMetaMaskInfo, adapter: ethereum.MetaMaskAdapter, }, ]; From ac2ff71d2c144e9aa6e0d58c99212b90795d9027 Mon Sep 17 00:00:00 2001 From: nicomiicro <104464506+nicomiicro@users.noreply.github.com> Date: Thu, 26 May 2022 15:25:11 +0300 Subject: [PATCH 02/51] Fix camelCase Co-authored-by: Will <86618292+wormat@users.noreply.github.com> --- apps/ui/src/components/MultiWalletModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 58474c262..c0ad4d12c 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -102,7 +102,7 @@ const ProtocolWalletOptionsList = ({ ); const protocolWalletServicesByServiceId = groupBy( protocolWalletServices, - (protocolwalletService) => protocolwalletService.id, + (protocolWalletService) => protocolWalletService.id, ); const protocolWallets = ecosystemIds.map( (ecosystemId) => wallets[ecosystemId], From 1277d685627fc2065a3e75236e719e8730f0fd15 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 15:30:18 +0300 Subject: [PATCH 03/51] Use void instead of catch --- apps/ui/src/components/MultiWalletModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index c0ad4d12c..5f355a873 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -116,7 +116,7 @@ const ProtocolWalletOptionsList = ({ const wallet = wallets[ecosystemId]; if (wallet.connected && wallet.service?.id === serviceId) { - wallets[ecosystemId].wallet?.disconnect().catch(console.error); + void wallets[ecosystemId].wallet?.disconnect(); } }); }; From a3809ba6553794c110a17e948bcd904681a967d7 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 15:44:08 +0300 Subject: [PATCH 04/51] Prefer null over undefined --- apps/ui/src/components/MultiWalletModal.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 5f355a873..8760862ad 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -152,9 +152,10 @@ const ProtocolWalletOptionsList = ({ (walletService) => walletService.info.ecosystem, ); - const connectedWallet = connectedWallets.find( - (wallet) => wallet.service?.id === serviceId, - ); + const connectedWallet = + connectedWallets.find( + (wallet) => wallet.service?.id === serviceId, + ) ?? null; return ( From e0cb57f70683bbe0d6c65017d6f4df7627ccc079 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 15:47:17 +0300 Subject: [PATCH 05/51] Remove unnecessary Fragment --- apps/ui/src/components/MultiWalletModal.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 8760862ad..8f3a48c6d 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -10,7 +10,6 @@ import { EuiTitle, } from "@elastic/eui"; import type { ReactElement } from "react"; -import { Fragment } from "react"; import type { Ecosystem } from "../config"; import { Protocol, getEcosystemsForProtocol } from "../config"; @@ -158,16 +157,15 @@ const ProtocolWalletOptionsList = ({ ) ?? null; return ( - - disconnect(service.id)} - onClick={() => connect(service.id)} - /> - + disconnect(service.id)} + onClick={() => connect(service.id)} + /> ); }, )} From 9db4eddf3bd4b36ba9d66b52adf571f7d3f9124f Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 15:59:01 +0300 Subject: [PATCH 06/51] Rename addHelpTextToMetaMaskInfo to addMetamaskEcosystemInfo --- apps/ui/src/models/wallets/services.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/models/wallets/services.tsx b/apps/ui/src/models/wallets/services.tsx index 2f0dfb4a6..428e3c87f 100644 --- a/apps/ui/src/models/wallets/services.tsx +++ b/apps/ui/src/models/wallets/services.tsx @@ -81,7 +81,7 @@ const metaMaskInfo: Omit = { icon: METAMASK_ICON, }; -const addHelpTextToMetaMaskInfo = ( +const addMetamaskEcosystemInfo = ( info: Omit, ecosystem: Ecosystem, url: string, @@ -108,37 +108,37 @@ const ethereumMetaMaskInfo: WalletServiceInfo = { ecosystem: ecosystems[EcosystemId.Ethereum], }; -const bscMetaMaskInfo = addHelpTextToMetaMaskInfo( +const bscMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Bsc], "https://academy.binance.com/en/articles/connecting-metamask-to-binance-smart-chain", ); -const avalancheMetaMaskInfo = addHelpTextToMetaMaskInfo( +const avalancheMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Avalanche], "https://support.avax.network/en/articles/4626956-how-do-i-set-up-metamask-on-avalanche", ); -const polygonMetaMaskInfo = addHelpTextToMetaMaskInfo( +const polygonMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Polygon], "https://docs.polygon.technology/docs/develop/metamask/config-polygon-on-metamask/", ); -const auroraMetaMaskInfo = addHelpTextToMetaMaskInfo( +const auroraMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Aurora], "https://doc.aurora.dev/interact/metamask/", ); -const fantomMetaMaskInfo = addHelpTextToMetaMaskInfo( +const fantomMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Fantom], "https://docs.fantom.foundation/tutorials/set-up-metamask", ); -const karuraMetaMaskInfo = addHelpTextToMetaMaskInfo( +const karuraMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Karura], "https://evmdocs.acala.network/tooling/metamask/connect-to-the-network", // TODO: Update link when mainnet is live ); -const acalaMetaMaskInfo = addHelpTextToMetaMaskInfo( +const acalaMetaMaskInfo = addMetamaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Acala], "https://evmdocs.acala.network/tooling/metamask/connect-to-the-network", // TODO: Update link when mainnet is live From fb2635160fdfba54a52d0ff55050e54c2c6ee995 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 16:43:24 +0300 Subject: [PATCH 07/51] Fix wallet icons in MultiConnectButton --- apps/ui/src/components/ConnectButton.scss | 14 ++++++++++++++ apps/ui/src/components/ConnectButton.tsx | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/components/ConnectButton.scss b/apps/ui/src/components/ConnectButton.scss index 3d48cbb91..19dff05ec 100644 --- a/apps/ui/src/components/ConnectButton.scss +++ b/apps/ui/src/components/ConnectButton.scss @@ -21,3 +21,17 @@ border: none; } } + +.multiConnectButton { + img { + margin: 0 3px; + + &:first-of-type { + margin-left: 0; + } + + &:last-of-type { + margin-right: 6px; + } + } +} diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index 16bd12b1d..bb8e383de 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -123,7 +123,11 @@ export const MultiConnectButton = ({ return ( <> - + {label} {isWalletModalOpen && } From ab71b61505ff0b2ba6e16a04ba17bf03090ff63c Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 16:51:18 +0300 Subject: [PATCH 08/51] Remove curried function --- apps/ui/src/components/MultiWalletModal.tsx | 2 +- apps/ui/src/contexts/evmWallet.tsx | 6 ++---- apps/ui/src/contexts/solanaWallet.tsx | 6 ++---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 8f3a48c6d..28bbae9dc 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -126,7 +126,7 @@ const ProtocolWalletOptionsList = ({ const wallet = wallets[ecosystemId]; if (wallet.createServiceClickHandler) { - wallet.createServiceClickHandler(serviceId)(); + wallet.createServiceClickHandler(serviceId); } }); }; diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index a7f166ae0..17748a05f 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -105,9 +105,7 @@ export interface EvmWalletContextInterface { readonly connected: boolean; readonly select: () => void; readonly service: WalletService | null; - readonly createServiceClickHandler: ( - serviceId: WalletService["id"], - ) => () => void; + readonly createServiceClickHandler: (serviceId: WalletService["id"]) => void; } const defaultEvmWalletContext: EvmWalletContextInterface = { @@ -116,7 +114,7 @@ const defaultEvmWalletContext: EvmWalletContextInterface = { connected: false, service: null, select: () => {}, - createServiceClickHandler: () => () => {}, + createServiceClickHandler: () => {}, }; const [ diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index d503c8d3a..ae90b7fb6 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -28,9 +28,7 @@ export interface SolanaWalletContextInterface { readonly connected: boolean; readonly select: () => void; readonly service: SolanaWalletService | null; - readonly createServiceClickHandler: ( - serviceId: WalletService["id"], - ) => () => void; + readonly createServiceClickHandler: (serviceId: WalletService["id"]) => void; } const defaultSolanaWalletContext: SolanaWalletContextInterface = { @@ -39,7 +37,7 @@ const defaultSolanaWalletContext: SolanaWalletContextInterface = { connected: false, select() {}, service: null, - createServiceClickHandler: () => () => {}, + createServiceClickHandler: () => {}, }; const SolanaWalletContext = createContext( From be9e6b399cd33d47da49a2454dcbe6c4d093728e Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 26 May 2022 17:09:32 +0300 Subject: [PATCH 09/51] Fix curried function implementation --- apps/ui/src/components/SingleWalletModal.tsx | 4 ++-- apps/ui/src/contexts/evmWallet.tsx | 14 ++++++++------ apps/ui/src/contexts/solanaWallet.tsx | 14 ++++++++------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/ui/src/components/SingleWalletModal.tsx b/apps/ui/src/components/SingleWalletModal.tsx index 22a4ba97b..899a75aae 100644 --- a/apps/ui/src/components/SingleWalletModal.tsx +++ b/apps/ui/src/components/SingleWalletModal.tsx @@ -24,7 +24,7 @@ export interface SingleWalletModalProps< readonly createServiceClickHandler: ( serviceId: WalletService["id"], callback?: () => any, - ) => () => void; + ) => void; } export const SingleWalletModal = ({ @@ -51,7 +51,7 @@ export const SingleWalletModal = ({ createServiceClickHandler(service.id, handleClose)} > {name} diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index 17748a05f..60a656b0e 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -245,12 +245,14 @@ export const EvmWalletProvider = ({ const select = useCallback(() => setIsModalVisible(true), []); const closeModal = useCallback(() => setIsModalVisible(false), []); - const createServiceClickHandler = - (id: WalletService["id"], callback?: () => any) => (): void => { - setServiceId(id); - setAutoConnect(true); - callback?.(); - }; + const createServiceClickHandler = ( + id: WalletService["id"], + callback?: () => any, + ) => { + setServiceId(id); + setAutoConnect(true); + callback?.(); + }; const EvmWalletContext = ecosystemToContext[ecosystemId]; diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index ae90b7fb6..9282faa3a 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -137,12 +137,14 @@ export const SolanaWalletProvider = ({ const select = useCallback(() => setIsModalVisible(true), []); const closeModal = useCallback(() => setIsModalVisible(false), []); - const createServiceClickHandler = - (id: WalletService["id"], callback?: () => any) => (): void => { - setServiceId(id); - setAutoConnect(true); - callback?.(); - }; + const createServiceClickHandler = ( + id: WalletService["id"], + callback?: () => any, + ) => { + setServiceId(id); + setAutoConnect(true); + callback?.(); + }; return ( Date: Fri, 27 May 2022 13:53:24 +0300 Subject: [PATCH 10/51] Remove chain from EvmWalletAdapter constructor --- apps/ui/src/contexts/evmWallet.tsx | 56 ++------------- apps/ui/src/hooks/evm/useEvmChainId.ts | 12 ++++ .../ui/src/hooks/evm/useRegisterErc20Token.ts | 5 +- .../wallets/adapters/evm/EvmWalletAdapter.ts | 70 ++++++++++--------- .../wallets/adapters/evm/MathWalletAdapter.ts | 9 +-- .../wallets/adapters/evm/MetaMaskAdapter.ts | 4 +- apps/ui/src/models/wallets/services.tsx | 2 +- apps/ui/src/models/wormhole/attestation.ts | 4 +- apps/ui/src/models/wormhole/evm.ts | 4 +- apps/ui/src/pages/TestPage.tsx | 13 ++-- 10 files changed, 72 insertions(+), 107 deletions(-) create mode 100644 apps/ui/src/hooks/evm/useEvmChainId.ts diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index 60a656b0e..eb95b7ba7 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -11,8 +11,8 @@ import * as React from "react"; import { SingleWalletModal } from "../components/SingleWalletModal"; import type { EvmEcosystemId } from "../config"; -import { EcosystemId, Env, EvmChainId } from "../config"; -import { useEnvironment, useNotification } from "../core/store"; +import { EcosystemId } from "../config"; +import { useNotification } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { EvmWalletAdapter, WalletService } from "../models"; import { @@ -28,52 +28,6 @@ import { import type { ReadonlyRecord } from "../utils"; import { shortenAddress } from "../utils"; -const envToEcosystemToChainId: ReadonlyRecord< - Env, - ReadonlyRecord -> = { - [Env.Mainnet]: { - [EcosystemId.Ethereum]: EvmChainId.EthereumMainnet, - [EcosystemId.Bsc]: EvmChainId.BscMainnet, - [EcosystemId.Avalanche]: EvmChainId.AvalancheMainnet, - [EcosystemId.Polygon]: EvmChainId.PolygonMainnet, - [EcosystemId.Aurora]: EvmChainId.AuroraMainnet, - [EcosystemId.Fantom]: EvmChainId.FantomMainnet, - [EcosystemId.Karura]: EvmChainId.KaruraMainnet, - [EcosystemId.Acala]: EvmChainId.AcalaMainnet, - }, - [Env.Devnet]: { - [EcosystemId.Ethereum]: EvmChainId.EthereumGoerli, - [EcosystemId.Bsc]: EvmChainId.BscTestnet, - [EcosystemId.Avalanche]: EvmChainId.AvalancheTestnet, - [EcosystemId.Polygon]: EvmChainId.PolygonTestnet, - [EcosystemId.Aurora]: EvmChainId.AuroraTestnet, - [EcosystemId.Fantom]: EvmChainId.FantomTestnet, - [EcosystemId.Karura]: EvmChainId.KaruraTestnet, - [EcosystemId.Acala]: EvmChainId.AcalaTestnet, - }, - [Env.Localnet]: { - [EcosystemId.Ethereum]: EvmChainId.EthereumLocalnet, - [EcosystemId.Bsc]: EvmChainId.BscLocalnet, - [EcosystemId.Avalanche]: EvmChainId.AvalancheLocalnet, - [EcosystemId.Polygon]: EvmChainId.PolygonLocalnet, - [EcosystemId.Aurora]: EvmChainId.AuroraLocalnet, - [EcosystemId.Fantom]: EvmChainId.FantomLocalnet, - [EcosystemId.Karura]: EvmChainId.KaruraLocalnet, - [EcosystemId.Acala]: EvmChainId.AcalaLocalnet, - }, - [Env.CustomLocalnet]: { - [EcosystemId.Ethereum]: EvmChainId.EthereumLocalnet, - [EcosystemId.Bsc]: EvmChainId.BscLocalnet, - [EcosystemId.Avalanche]: EvmChainId.AvalancheLocalnet, - [EcosystemId.Polygon]: EvmChainId.PolygonLocalnet, - [EcosystemId.Aurora]: EvmChainId.AuroraLocalnet, - [EcosystemId.Fantom]: EvmChainId.FantomLocalnet, - [EcosystemId.Karura]: EvmChainId.KaruraLocalnet, - [EcosystemId.Acala]: EvmChainId.AcalaLocalnet, - }, -}; - const ecosystemToWalletServices: ReadonlyRecord< EvmEcosystemId, readonly WalletService[] @@ -163,7 +117,6 @@ export const EvmWalletProvider = ({ }: EvmWalletProviderProps): ReactElement => { const { notify } = useNotification(); - const { env } = useEnvironment(); const [connected, setConnected] = useState(false); const [autoConnect, setAutoConnect] = useState(false); @@ -171,7 +124,6 @@ export const EvmWalletProvider = ({ ecosystemToLocalStorageKey[ecosystemId], ); - const chainId = envToEcosystemToChainId[env][ecosystemId]; const services = ecosystemToWalletServices[ecosystemId]; const service = useMemo( () => services.find(({ id }) => id === serviceId) ?? null, @@ -182,8 +134,8 @@ export const EvmWalletProvider = ({ return null; } - return new service.adapter(chainId); - }, [chainId, service]); + return new service.adapter(); + }, [service]); const previousWalletRef = useRef(wallet); const address = wallet?.address ?? null; diff --git a/apps/ui/src/hooks/evm/useEvmChainId.ts b/apps/ui/src/hooks/evm/useEvmChainId.ts new file mode 100644 index 000000000..6117cc9b8 --- /dev/null +++ b/apps/ui/src/hooks/evm/useEvmChainId.ts @@ -0,0 +1,12 @@ +import type { EvmChainId, EvmEcosystemId } from "../../config"; +import { Protocol, chains } from "../../config"; +import { useEnvironment } from "../../core/store"; +import { findOrThrow } from "../../utils"; + +export const useEvmChainId = (ecosystemId: EvmEcosystemId): EvmChainId => { + const { env } = useEnvironment(); + return findOrThrow( + chains[env][Protocol.Evm], + (evmSpec) => evmSpec.ecosystem === ecosystemId, + ).chainId; +}; diff --git a/apps/ui/src/hooks/evm/useRegisterErc20Token.ts b/apps/ui/src/hooks/evm/useRegisterErc20Token.ts index 3c95386b0..de019a276 100644 --- a/apps/ui/src/hooks/evm/useRegisterErc20Token.ts +++ b/apps/ui/src/hooks/evm/useRegisterErc20Token.ts @@ -5,6 +5,8 @@ import { ecosystems } from "../../config"; import { useEvmWallet } from "../../contexts"; import { useNotification } from "../../core/store"; +import { useEvmChainId } from "./useEvmChainId"; + interface RegisterErc20TokenResult { readonly showPrompt: (tokenSpec: TokenSpec) => Promise; } @@ -14,6 +16,7 @@ export const useRegisterErc20Token = ( ): RegisterErc20TokenResult => { const { notify } = useNotification(); const { wallet } = useEvmWallet(ecosystemId); + const evmChainId = useEvmChainId(ecosystemId); const showPrompt = async (tokenSpec: TokenSpec): Promise => { if (!wallet) { @@ -26,7 +29,7 @@ export const useRegisterErc20Token = ( } try { - await wallet.registerToken(tokenSpec); + await wallet.registerToken(tokenSpec, ecosystemId, evmChainId); } catch (error) { notify("Error", "Failed to add token", "error"); Sentry.captureException(error); diff --git a/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts b/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts index 42c1bb1b2..644d4a475 100644 --- a/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts +++ b/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts @@ -3,13 +3,8 @@ import type { Signer } from "ethers"; import { ethers } from "ethers"; import EventEmitter from "eventemitter3"; -import type { EvmChainId, EvmEcosystemId, TokenSpec } from "../../../../config"; -import { - Protocol, - allUniqueChains, - ecosystems, - evmChainIdToEcosystem, -} from "../../../../config"; +import type { EcosystemId, EvmChainId, TokenSpec } from "../../../../config"; +import { Protocol, allUniqueChains, ecosystems } from "../../../../config"; import { sleep } from "../../../../utils"; type Web3Provider = ethers.providers.Web3Provider; @@ -23,34 +18,34 @@ const METAMASK_methodNotFound = -32601; // TODO: Make this class wallet-agnostic, currently assumes MetaMask. export interface EvmWalletAdapter extends EventEmitter { - readonly chainId: EvmChainId; readonly signer: Signer | null; readonly address: string | null; readonly connect: () => Promise; readonly disconnect: () => Promise; - readonly switchNetwork: () => Promise; - readonly registerToken: (tokenSpec: TokenSpec) => Promise; + readonly switchNetwork: (chainId: EvmChainId) => Promise; + readonly registerToken: ( + tokenSpec: TokenSpec, + ecosystemId: EcosystemId, + chainId: EvmChainId, + ) => Promise; } export class EvmWeb3WalletAdapter extends EventEmitter implements EvmWalletAdapter { - readonly chainId: EvmChainId; readonly serviceName: string; readonly serviceUrl: string; address: string | null; + connecting: boolean; private readonly getWalletProvider: () => Web3Provider | null; - private connecting: boolean; constructor( - chainId: EvmChainId, serviceName: string, serviceUrl: string, getWalletProvider: () => Web3Provider | null, ) { super(); - this.chainId = chainId; this.serviceName = serviceName; this.serviceUrl = serviceUrl; this.getWalletProvider = getWalletProvider; @@ -62,14 +57,6 @@ export class EvmWeb3WalletAdapter return this.getWalletProvider()?.getSigner() ?? null; } - private get ecosystem(): EvmEcosystemId { - return evmChainIdToEcosystem[this.chainId]; - } - - private get sentryContextKey(): string { - return `${ecosystems[this.ecosystem].displayName} Wallet`; - } - private get walletProvider(): Web3Provider | null { return this.getWalletProvider(); } @@ -96,13 +83,14 @@ export class EvmWeb3WalletAdapter this.address = address; this.emit("connect", this.address); - Sentry.setContext(this.sentryContextKey, { + const sentryContextKey = await this.sentryContextKey(); + Sentry.setContext(sentryContextKey, { walletName: this.serviceName, address: this.address, }); Sentry.addBreadcrumb({ category: "wallet", - message: `Connected to ${this.sentryContextKey} ${this.address}`, + message: `Connected to ${sentryContextKey} ${this.address}`, level: Sentry.Severity.Info, }); } catch (error) { @@ -119,28 +107,29 @@ export class EvmWeb3WalletAdapter this.address = null; this.emit("disconnect"); - Sentry.setContext(this.sentryContextKey, {}); + const sentryContextKey = await this.sentryContextKey(); + Sentry.setContext(sentryContextKey, {}); Sentry.addBreadcrumb({ category: "wallet", - message: `Disconnected from ${this.sentryContextKey}`, + message: `Disconnected from ${sentryContextKey}`, level: Sentry.Severity.Info, }); } } - async switchNetwork(): Promise { + async switchNetwork(chainId: EvmChainId): Promise { if (!this.walletProvider) { throw new Error("No wallet provider"); } try { await this.walletProvider.send("wallet_switchEthereumChain", [ - { chainId: hexValue(this.chainId) }, + { chainId: hexValue(chainId) }, ]); } catch (switchError: any) { if (switchError.code === METAMASK_unrecognizedChainId) { const evmSpec = allUniqueChains[Protocol.Evm].find( - (spec) => spec.chainId === this.chainId, + (spec) => spec.chainId === chainId, ); if (!evmSpec) { throw new Error("No EVM spec found for chain ID"); @@ -148,7 +137,7 @@ export class EvmWeb3WalletAdapter // this also asks to switch to that chain afterwards await this.walletProvider.send("wallet_addEthereumChain", [ { - chainId: hexValue(this.chainId), + chainId: hexValue(chainId), chainName: evmSpec.chainName, nativeCurrency: evmSpec.nativeCurrency, rpcUrls: evmSpec.rpcUrls, @@ -166,17 +155,21 @@ export class EvmWeb3WalletAdapter } } - async registerToken(tokenSpec: TokenSpec): Promise { + async registerToken( + tokenSpec: TokenSpec, + ecosystemId: EcosystemId, + chainId: EvmChainId, + ): Promise { if (!this.walletProvider) { throw new Error("No wallet provider"); } - const details = tokenSpec.detailsByEcosystem.get(this.ecosystem); + const details = tokenSpec.detailsByEcosystem.get(ecosystemId); if (!details) { throw new Error( - `No ${ecosystems[this.ecosystem].displayName} details for token`, + `No ${ecosystems[ecosystemId].displayName} details for token`, ); } - await this.switchNetwork(); + await this.switchNetwork(chainId); await sleep(200); // Sleep briefly, otherwise Metamask ignores the second prompt @@ -194,4 +187,13 @@ export class EvmWeb3WalletAdapter ); return wasAdded; } + + private async sentryContextKey(): Promise { + try { + const network = await this.getWalletProvider()?.getNetwork(); + return `${network?.name || "Unknown Network"} Wallet`; + } catch { + return `Unknown Network Wallet`; + } + } } diff --git a/apps/ui/src/models/wallets/adapters/evm/MathWalletAdapter.ts b/apps/ui/src/models/wallets/adapters/evm/MathWalletAdapter.ts index fd196b420..3d6a24650 100644 --- a/apps/ui/src/models/wallets/adapters/evm/MathWalletAdapter.ts +++ b/apps/ui/src/models/wallets/adapters/evm/MathWalletAdapter.ts @@ -8,12 +8,7 @@ const getMathWalletService = (): ethers.providers.Web3Provider | null => : null; export class MathWalletAdapter extends EvmWeb3WalletAdapter { - constructor(chainId: number) { - super( - chainId, - "MathWallet", - "https://mathwallet.org", - getMathWalletService, - ); + constructor() { + super("MathWallet", "https://mathwallet.org", getMathWalletService); } } diff --git a/apps/ui/src/models/wallets/adapters/evm/MetaMaskAdapter.ts b/apps/ui/src/models/wallets/adapters/evm/MetaMaskAdapter.ts index 3e0ac683a..96f20c925 100644 --- a/apps/ui/src/models/wallets/adapters/evm/MetaMaskAdapter.ts +++ b/apps/ui/src/models/wallets/adapters/evm/MetaMaskAdapter.ts @@ -8,8 +8,8 @@ const getMetaMaskService = (): ethers.providers.Web3Provider | null => : null; export class MetaMaskAdapter extends EvmWeb3WalletAdapter { - constructor(chainId: number) { - super(chainId, "MetaMask", "https://metamask.io", getMetaMaskService); + constructor() { + super("MetaMask", "https://metamask.io", getMetaMaskService); } async connect(): Promise { diff --git a/apps/ui/src/models/wallets/services.tsx b/apps/ui/src/models/wallets/services.tsx index 428e3c87f..d45ce69a9 100644 --- a/apps/ui/src/models/wallets/services.tsx +++ b/apps/ui/src/models/wallets/services.tsx @@ -26,7 +26,7 @@ export interface WalletServiceInfo { export interface WalletService { readonly id: string; readonly info: WalletServiceInfo; - readonly adapter?: new (chainId: number) => T; + readonly adapter?: new () => T; } export interface SolanaWalletService< diff --git a/apps/ui/src/models/wormhole/attestation.ts b/apps/ui/src/models/wormhole/attestation.ts index 355cc32c3..50047cc72 100644 --- a/apps/ui/src/models/wormhole/attestation.ts +++ b/apps/ui/src/models/wormhole/attestation.ts @@ -76,7 +76,7 @@ export const setUpSplTokensOnEvm = async ( throw new Error("No EVM chain wallet signer"); } - await evmWallet.switchNetwork(); + await evmWallet.switchNetwork(evmChain.chainId); const attestations = await Promise.all( mintAddresses.map((mintAddress) => @@ -129,7 +129,7 @@ export const attestErc20Token = async ( throw new Error("No EVM chain wallet signer"); } - await evmWallet.switchNetwork(); + await evmWallet.switchNetwork(evmChain.chainId); const receipt = await attestFromEth( evmChain.wormhole.tokenBridge, diff --git a/apps/ui/src/models/wormhole/evm.ts b/apps/ui/src/models/wormhole/evm.ts index 5450c1b22..b3ecaf1cf 100644 --- a/apps/ui/src/models/wormhole/evm.ts +++ b/apps/ui/src/models/wormhole/evm.ts @@ -68,7 +68,7 @@ export const lockEvmToken = async ({ throw new Error("No EVM signer"); } - await evmWallet.switchNetwork(); + await evmWallet.switchNetwork(evmChain.chainId); const transferAmountAtomicString = amount.toAtomicString(evmChain.ecosystem); const allowance = await getAllowanceEth( @@ -142,7 +142,7 @@ export const unlockEvmToken = async ( throw new Error("Missing EVM signer"); } - await evmWallet.switchNetwork(); + await evmWallet.switchNetwork(evmChain.chainId); const redeemResponse = await redeemOnEth( interactionId, diff --git a/apps/ui/src/pages/TestPage.tsx b/apps/ui/src/pages/TestPage.tsx index 6c572efd2..5a49dbde1 100644 --- a/apps/ui/src/pages/TestPage.tsx +++ b/apps/ui/src/pages/TestPage.tsx @@ -165,10 +165,11 @@ const TestPage = (): ReactElement => { throw new Error(`No ${ecosystem} wallet`); } - await evmWallet.switchNetwork(); - const evmChain = ecosystem === EcosystemId.Ethereum ? ethereumChain : bscChain; + + await evmWallet.switchNetwork(evmChain.chainId); + const splTokenSetupResult = await setUpSplTokensOnEvm( wormholeConfig, solanaChain.wormhole, @@ -280,7 +281,7 @@ const TestPage = (): ReactElement => { const hugeAmount = "1" + "0".repeat(32); - await ethereumWallet.switchNetwork(); + await ethereumWallet.switchNetwork(ethereumChain.chainId); for (const token of tokens) { const ethereumDetails = token.detailsByEcosystem.get( EcosystemId.Ethereum, @@ -297,7 +298,7 @@ const TestPage = (): ReactElement => { console.info(receipt.transactionHash); } - await bscWallet.switchNetwork(); + await bscWallet.switchNetwork(bscChain.chainId); for (const token of tokens) { const bscDetails = token.detailsByEcosystem.get(EcosystemId.Bsc); if (!bscDetails) { @@ -320,7 +321,7 @@ const TestPage = (): ReactElement => { const zero = "0"; - await ethereumWallet.switchNetwork(); + await ethereumWallet.switchNetwork(ethereumChain.chainId); for (const token of tokens) { const ethereumDetails = token.detailsByEcosystem.get( EcosystemId.Ethereum, @@ -337,7 +338,7 @@ const TestPage = (): ReactElement => { console.info(receipt.transactionHash); } - await bscWallet.switchNetwork(); + await bscWallet.switchNetwork(bscChain.chainId); for (const token of tokens) { const bscDetails = token.detailsByEcosystem.get(EcosystemId.Bsc); if (!bscDetails) { From 4b96f6c95d14f940148e374a9e3ff88cb562de1f Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 27 May 2022 23:03:24 +0300 Subject: [PATCH 11/51] Add useWalletService for wallet adapters per service/protocol --- apps/ui/src/components/MultiWalletModal.tsx | 21 +- apps/ui/src/components/SingleWalletModal.tsx | 78 ++++--- apps/ui/src/contexts/evmWallet.tsx | 81 ++------ apps/ui/src/contexts/solanaWallet.tsx | 81 ++------ apps/ui/src/core/selectors/environment.ts | 4 +- apps/ui/src/core/store/index.ts | 1 + apps/ui/src/core/store/useWalletService.ts | 196 ++++++++++++++++++ .../ui/src/hooks/crossEcosystem/useWallets.ts | 4 +- .../wallets/adapters/evm/EvmWalletAdapter.ts | 8 + .../solana/SolanaDefaultWalletAdapter.ts | 21 ++ .../adapters/solana/SolanaWalletAdapter.ts | 14 ++ .../wallets/adapters/solana/ledger/index.ts | 11 + apps/ui/src/models/wallets/services.tsx | 23 +- 13 files changed, 355 insertions(+), 188 deletions(-) create mode 100644 apps/ui/src/core/store/useWalletService.ts create mode 100644 apps/ui/src/models/wallets/adapters/solana/SolanaDefaultWalletAdapter.ts diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 28bbae9dc..65f6021a4 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -13,6 +13,7 @@ import type { ReactElement } from "react"; import type { Ecosystem } from "../config"; import { Protocol, getEcosystemsForProtocol } from "../config"; +import { useWalletService } from "../core/store"; import { useWallets } from "../hooks"; import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; @@ -90,11 +91,8 @@ const ProtocolWalletOptionsList = ({ icon, protocol, }: ProtocolWalletOptionsListProps): ReactElement => { - if (protocol === Protocol.Cosmos) { - throw new Error("Unsupported protocol"); - } - const wallets = useWallets(); + const { connectService, disconnectService } = useWalletService(); const ecosystemIds = getEcosystemsForProtocol(protocol); const protocolWalletServices = ecosystemIds.flatMap( (ecosystemId) => WALLET_SERVICES[ecosystemId], @@ -110,23 +108,18 @@ const ProtocolWalletOptionsList = ({ const connectedWallets = protocolWallets.filter((wallet) => wallet.connected); const disconnect = (serviceId: string): void => { - protocolWalletServicesByServiceId[serviceId].forEach((walletService) => { - const ecosystemId = walletService.info.ecosystem.id; - const wallet = wallets[ecosystemId]; - - if (wallet.connected && wallet.service?.id === serviceId) { - void wallets[ecosystemId].wallet?.disconnect(); - } - }); + void disconnectService(serviceId, protocol); }; const connect = (serviceId: string) => { + void connectService(serviceId, protocol); + protocolWalletServicesByServiceId[serviceId].forEach((walletService) => { const ecosystemId = walletService.info.ecosystem.id; const wallet = wallets[ecosystemId]; - if (wallet.createServiceClickHandler) { - wallet.createServiceClickHandler(serviceId); + if (wallet.setServiceId) { + wallet.setServiceId(serviceId); } }); }; diff --git a/apps/ui/src/components/SingleWalletModal.tsx b/apps/ui/src/components/SingleWalletModal.tsx index 899a75aae..d9601134a 100644 --- a/apps/ui/src/components/SingleWalletModal.tsx +++ b/apps/ui/src/components/SingleWalletModal.tsx @@ -9,6 +9,8 @@ import { import type { ReactElement } from "react"; import { Fragment } from "react"; +import type { Protocol } from "../config"; +import { useWalletService } from "../core/store"; import type { WalletAdapter, WalletService } from "../models"; import { isUserOnMobileDevice } from "../utils"; @@ -19,48 +21,56 @@ export interface SingleWalletModalProps< W extends WalletService = WalletService, > { readonly currentService: string; + readonly protocol: Protocol; readonly services: readonly W[]; readonly handleClose: () => void; - readonly createServiceClickHandler: ( + readonly setServiceId: ( serviceId: WalletService["id"], - callback?: () => any, ) => void; } export const SingleWalletModal = ({ currentService, + protocol, services, handleClose, - createServiceClickHandler, -}: SingleWalletModalProps): ReactElement => ( - - - -

Select Wallet

-
-
+ setServiceId, +}: SingleWalletModalProps): ReactElement => { + const { connectService } = useWalletService(); + return ( + + + +

Select Wallet

+
+
- - {isUserOnMobileDevice() ? : ""} - {services.map((service) => { - const { - id, - info: { icon, name, helpText }, - } = service; - return ( - - createServiceClickHandler(service.id, handleClose)} - > - - {name} - - {helpText && <>{helpText}} - - - ); - })} - -
-); + + {isUserOnMobileDevice() ? : ""} + {services.map((service) => { + const { + id, + info: { icon, name, helpText }, + } = service; + return ( + + { + void connectService(service.id, protocol); + setServiceId(service.id); + handleClose(); + }} + > + + {name} + + {helpText && <>{helpText}} + + + ); + })} + +
+ ); +}; diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index eb95b7ba7..318e12bbd 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -1,18 +1,11 @@ import type { ReactElement, ReactNode } from "react"; -import { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import * as React from "react"; import { SingleWalletModal } from "../components/SingleWalletModal"; import type { EvmEcosystemId } from "../config"; -import { EcosystemId } from "../config"; -import { useNotification } from "../core/store"; +import { EcosystemId, Protocol } from "../config"; +import { useWalletService } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { EvmWalletAdapter, WalletService } from "../models"; import { @@ -26,7 +19,6 @@ import { POLYGON_WALLET_SERVICES, } from "../models"; import type { ReadonlyRecord } from "../utils"; -import { shortenAddress } from "../utils"; const ecosystemToWalletServices: ReadonlyRecord< EvmEcosystemId, @@ -59,7 +51,7 @@ export interface EvmWalletContextInterface { readonly connected: boolean; readonly select: () => void; readonly service: WalletService | null; - readonly createServiceClickHandler: (serviceId: WalletService["id"]) => void; + readonly setServiceId: (serviceId: WalletService["id"]) => void; } const defaultEvmWalletContext: EvmWalletContextInterface = { @@ -68,7 +60,7 @@ const defaultEvmWalletContext: EvmWalletContextInterface = { connected: false, service: null, select: () => {}, - createServiceClickHandler: () => {}, + setServiceId: () => {}, }; const [ @@ -115,11 +107,7 @@ export const EvmWalletProvider = ({ ecosystemId, children, }: EvmWalletProviderProps): ReactElement => { - const { notify } = useNotification(); - const [connected, setConnected] = useState(false); - const [autoConnect, setAutoConnect] = useState(false); - const [serviceId, setServiceId] = useLocalStorageState( ecosystemToLocalStorageKey[ecosystemId], ); @@ -129,82 +117,40 @@ export const EvmWalletProvider = ({ () => services.find(({ id }) => id === serviceId) ?? null, [serviceId, services], ); - const wallet = useMemo(() => { - if (!service?.adapter) { - return null; - } - return new service.adapter(); - }, [service]); - const previousWalletRef = useRef(wallet); + const { evm } = useWalletService(); + + const wallet: EvmWalletAdapter | null = (service && evm[service.id]) || null; const address = wallet?.address ?? null; useEffect(() => { - const previousWallet = previousWalletRef.current; if (wallet) { - if (wallet !== previousWallet) { - previousWallet?.disconnect().catch(console.error); - setConnected(false); - // eslint-disable-next-line functional/immutable-data - previousWalletRef.current = wallet; - } - const handleConnect = (): void => { - if (wallet.address) { - setConnected(true); - notify( - "Wallet update", - `Connected to wallet ${shortenAddress(wallet.address)}`, - "info", - 7000, - ); - } + setConnected(true); }; + const handleDisconnect = (): void => { setConnected(false); - notify("Wallet update", "Disconnected from wallet", "warning"); - }; - const handleError = (title: string, description: string): void => { - notify(title, description, "error"); }; wallet.on("connect", handleConnect); wallet.on("disconnect", handleDisconnect); - wallet.on("error", handleError); return () => { wallet.removeListener("connect", handleConnect); wallet.removeListener("disconnect", handleDisconnect); - wallet.removeListener("error", handleError); }; } return () => { setConnected(false); - // eslint-disable-next-line functional/immutable-data - previousWalletRef.current = wallet; }; - }, [wallet, notify]); - - useEffect(() => { - if (wallet && autoConnect) { - wallet.connect().catch(console.error); - setAutoConnect(false); - } - }, [wallet, autoConnect]); + }, [wallet]); const [isModalVisible, setIsModalVisible] = useState(false); const select = useCallback(() => setIsModalVisible(true), []); const closeModal = useCallback(() => setIsModalVisible(false), []); - const createServiceClickHandler = ( - id: WalletService["id"], - callback?: () => any, - ) => { - setServiceId(id); - setAutoConnect(true); - callback?.(); - }; const EvmWalletContext = ecosystemToContext[ecosystemId]; @@ -216,16 +162,17 @@ export const EvmWalletProvider = ({ connected, select, service, - createServiceClickHandler, + setServiceId, }} > {children} {isModalVisible && ( )} diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index 51d99712f..cc8b49004 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -1,4 +1,3 @@ -import Wallet from "@project-serum/sol-wallet-adapter"; import type { ReactElement, ReactNode } from "react"; import { createContext, @@ -6,15 +5,12 @@ import { useContext, useEffect, useMemo, - useRef, useState, } from "react"; -import shallow from "zustand/shallow.js"; import { SingleWalletModal } from "../components/SingleWalletModal"; import { Protocol } from "../config"; -import { selectConfig } from "../core/selectors"; -import { useEnvironment, useNotification } from "../core/store"; +import { useWalletService } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { SolanaWalletAdapter, @@ -22,7 +18,6 @@ import type { WalletService, } from "../models"; import { SOLANA_WALLET_SERVICES } from "../models"; -import { shortenAddress } from "../utils"; export interface SolanaWalletContextInterface { readonly wallet: SolanaWalletAdapter | null; @@ -30,7 +25,7 @@ export interface SolanaWalletContextInterface { readonly connected: boolean; readonly select: () => void; readonly service: SolanaWalletService | null; - readonly createServiceClickHandler: (serviceId: WalletService["id"]) => void; + readonly setServiceId: (serviceId: WalletService["id"]) => void; } const defaultSolanaWalletContext: SolanaWalletContextInterface = { @@ -39,7 +34,7 @@ const defaultSolanaWalletContext: SolanaWalletContextInterface = { connected: false, select() {}, service: null, - createServiceClickHandler: () => {}, + setServiceId: () => {}, }; const SolanaWalletContext = createContext( @@ -53,12 +48,7 @@ interface SolanaWalletProviderProps { export const SolanaWalletProvider = ({ children, }: SolanaWalletProviderProps): ReactElement => { - const { chains } = useEnvironment(selectConfig, shallow); - const [{ endpoint }] = chains[Protocol.Solana]; - const { notify } = useNotification(); - const [connected, setConnected] = useState(false); - const [autoConnect, setAutoConnect] = useState(false); const [serviceId, setServiceId] = useLocalStorageState( "solanaWalletService", ); @@ -68,83 +58,39 @@ export const SolanaWalletProvider = ({ [serviceId], ); - const wallet = useMemo(() => { - if (!service) { - return null; - } - return service.adapter - ? new service.adapter() - : new Wallet(service.info.url, endpoint); - }, [service, endpoint]); - const previousWalletRef = useRef(wallet); - const address = wallet?.publicKey?.toBase58() ?? null; + const { solana } = useWalletService(); + + const wallet: SolanaWalletAdapter | null = + (service && solana[service.id]) || null; + const address = wallet?.address ?? null; useEffect(() => { - const previousWallet = previousWalletRef.current; if (wallet) { - if (wallet !== previousWallet) { - previousWallet?.disconnect().catch(console.error); - setConnected(false); - // eslint-disable-next-line functional/immutable-data - previousWalletRef.current = wallet; - } - const handleConnect = (): void => { - const { publicKey } = wallet; - if (publicKey) { - setConnected(true); - notify( - "Wallet update", - `Connected to wallet ${shortenAddress(publicKey.toBase58())}`, - "info", - 7000, - ); - } + setConnected(true); }; const handleDisconnect = (): void => { setConnected(false); - notify("Wallet update", "Disconnected from wallet", "warning"); - }; - const handleError = (title: string, description: string): void => { - notify(title, description, "error"); }; + wallet.on("connect", handleConnect); wallet.on("disconnect", handleDisconnect); - wallet.on("error", handleError); return () => { wallet.removeListener("connect", handleConnect); wallet.removeListener("disconnect", handleDisconnect); - wallet.removeListener("error", handleError); }; } return () => { setConnected(false); - // eslint-disable-next-line functional/immutable-data - previousWalletRef.current = wallet; }; - }, [wallet, notify]); - - useEffect(() => { - if (wallet && autoConnect) { - wallet.connect().catch(console.error); - setAutoConnect(false); - } - }, [wallet, autoConnect]); + }, [wallet]); const [isModalVisible, setIsModalVisible] = useState(false); const select = useCallback(() => setIsModalVisible(true), []); const closeModal = useCallback(() => setIsModalVisible(false), []); - const createServiceClickHandler = ( - id: WalletService["id"], - callback?: () => any, - ) => { - setServiceId(id); - setAutoConnect(true); - callback?.(); - }; return ( {children} {isModalVisible && ( )} diff --git a/apps/ui/src/core/selectors/environment.ts b/apps/ui/src/core/selectors/environment.ts index d6391b6c8..9bb216f75 100644 --- a/apps/ui/src/core/selectors/environment.ts +++ b/apps/ui/src/core/selectors/environment.ts @@ -1,6 +1,6 @@ import { DEFAULT_ENV, configs, overrideLocalnetIp } from "../../config"; -import { Env } from "../store"; -import type { EnvironmentState } from "../store"; +import { Env } from "../store/useEnvironment"; +import type { EnvironmentState } from "../store/useEnvironment"; export const selectEnvs = (state: EnvironmentState) => state.customLocalnetIp === null ? [DEFAULT_ENV] : Object.values(Env); diff --git a/apps/ui/src/core/store/index.ts b/apps/ui/src/core/store/index.ts index 6e84407f6..65c712379 100644 --- a/apps/ui/src/core/store/index.ts +++ b/apps/ui/src/core/store/index.ts @@ -1,2 +1,3 @@ export * from "./useNotification"; export * from "./useEnvironment"; +export * from "./useWalletService"; diff --git a/apps/ui/src/core/store/useWalletService.ts b/apps/ui/src/core/store/useWalletService.ts new file mode 100644 index 000000000..929298b41 --- /dev/null +++ b/apps/ui/src/core/store/useWalletService.ts @@ -0,0 +1,196 @@ +/* eslint-disable functional/immutable-data */ +import { produce } from "immer"; +import type { GetState, SetState } from "zustand"; +import create from "zustand"; + +import { Protocol } from "../../config"; +import type { WalletService } from "../../models"; +import { findServiceForProtocol } from "../../models"; +import type { + EvmWalletAdapter, + SolanaWalletAdapter, + WalletAdapter, +} from "../../models/wallets/adapters"; +import { SolanaDefaultWalletAdapter } from "../../models/wallets/adapters/solana/SolanaDefaultWalletAdapter"; +import type { ReadonlyRecord } from "../../utils"; +import { shortenAddress } from "../../utils"; +import { selectConfig } from "../selectors/environment"; + +import { useEnvironment as environmentStore } from "./useEnvironment"; +import { useNotification as notificationStore } from "./useNotification"; + +type WalletAdapterListeners = { + readonly connect: () => void; + readonly disconnect: () => void; + readonly error: (title: string, description: string) => void; +}; + +export interface WalletServiceState { + readonly evm: ReadonlyRecord; + readonly evmListeners: ReadonlyRecord< + string, + WalletAdapterListeners | undefined + >; + readonly solana: ReadonlyRecord; + readonly solanaListeners: ReadonlyRecord< + string, + WalletAdapterListeners | undefined + >; + readonly connectService: ( + serviceId: string, + protocol: Protocol, + ) => Promise; + readonly disconnectService: ( + serviceId: string, + protocol: Protocol, + ) => Promise; +} + +export const useWalletService = create( + (set: SetState, get: GetState) => ({ + evm: {}, + evmListeners: {}, + solana: {}, + solanaListeners: {}, + connectService: async (serviceId: string, protocol: Protocol) => { + const state = get(); + const service = findServiceForProtocol(serviceId, protocol); + + const protocolAdapters = + protocol === Protocol.Evm ? state.evm : state.solana; + const previous = protocolAdapters[serviceId]; + + if (previous) { + if (previous.connected) { + const listeners = + protocol === Protocol.Evm + ? state.evmListeners + : state.solanaListeners; + + // call on connect handler for successful connection toast + listeners[serviceId]?.connect(); + return; + } + + await state.disconnectService(serviceId, protocol); + } + + const adapter = createAdapter(service, protocol); + + const { notify } = notificationStore.getState(); + + const handleConnect = (): void => { + if (adapter.address) { + notify( + "Wallet update", + `Connected to wallet ${shortenAddress(adapter.address)}`, + "info", + 7000, + ); + } + }; + const handleDisconnect = (): void => { + notify("Wallet update", "Disconnected from wallet", "warning"); + }; + const handleError = (title: string, description: string): void => { + notify(title, description, "error"); + }; + + const listeners: WalletAdapterListeners = { + connect: handleConnect, + disconnect: handleDisconnect, + error: handleError, + }; + + Object.entries(listeners).forEach(([eventName, handler]) => { + adapter.on(eventName, handler); + }); + + set( + produce((draft) => { + switch (adapter.protocol) { + case Protocol.Evm: { + draft.evm[serviceId] = adapter; + draft.evmListeners[serviceId] = listeners; + break; + } + case Protocol.Solana: { + draft.solana[serviceId] = adapter; + draft.solanaListeners[serviceId] = listeners; + break; + } + } + }), + ); + + await adapter.connect().catch(console.error); + }, + disconnectService: async (serviceId: string, protocol: Protocol) => { + const state = get(); + const protocolAdapters = + protocol === Protocol.Evm ? state.evm : state.solana; + const adapter = protocolAdapters[serviceId]; + + if (adapter) { + await adapter.disconnect().catch(console.error); + + const protocolListeners = + protocol === Protocol.Evm + ? state.evmListeners + : state.solanaListeners; + + const listeners = protocolListeners[serviceId]; + + if (listeners) { + Object.entries(listeners).forEach(([eventName, handler]) => { + adapter.off(eventName, handler); + }); + } + } + + set( + produce((draft) => { + switch (protocol) { + case Protocol.Evm: { + draft.evm[serviceId] = undefined; + break; + } + case Protocol.Solana: { + draft.solana[serviceId] = undefined; + break; + } + case Protocol.Cosmos: { + throw new Error(`Cosmos disconnect not implemented`); + } + } + }), + ); + }, + }), +); + +const createAdapter = ( + service: WalletService, + protocol: Protocol, +): WalletAdapter => { + switch (protocol) { + case Protocol.Evm: { + if (!service.adapter) + throw new Error(`Adapter is required for protocol ${protocol}`); + return new service.adapter(); + } + case Protocol.Solana: { + if (service.adapter) { + return new service.adapter(); + } else { + const environmentStoreState = environmentStore.getState(); + const { chains } = selectConfig(environmentStoreState); + const [{ endpoint }] = chains[Protocol.Solana]; + return new SolanaDefaultWalletAdapter(service.info.url, endpoint); + } + } + case Protocol.Cosmos: { + throw new Error(`Cosmos adapters not implemented yet`); + } + } +}; diff --git a/apps/ui/src/hooks/crossEcosystem/useWallets.ts b/apps/ui/src/hooks/crossEcosystem/useWallets.ts index 41aa4af1c..177ea5a50 100644 --- a/apps/ui/src/hooks/crossEcosystem/useWallets.ts +++ b/apps/ui/src/hooks/crossEcosystem/useWallets.ts @@ -15,7 +15,7 @@ export interface Wallets extends ReadonlyRecord { readonly connected: false; readonly service: null; readonly wallet: null; - readonly createServiceClickHandler: null; + readonly setServiceId: null; }; readonly [EcosystemId.Bsc]: EvmWalletContextInterface; readonly [EcosystemId.Avalanche]: EvmWalletContextInterface; @@ -33,7 +33,7 @@ export const useWallets = (): Wallets => ({ address: null, connected: false, service: null, - createServiceClickHandler: null, + setServiceId: null, wallet: null, }, [EcosystemId.Bsc]: useEvmWallet(EcosystemId.Bsc), diff --git a/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts b/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts index 644d4a475..d14893fcb 100644 --- a/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts +++ b/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts @@ -20,6 +20,7 @@ const METAMASK_methodNotFound = -32601; export interface EvmWalletAdapter extends EventEmitter { readonly signer: Signer | null; readonly address: string | null; + readonly connected: boolean; readonly connect: () => Promise; readonly disconnect: () => Promise; readonly switchNetwork: (chainId: EvmChainId) => Promise; @@ -28,6 +29,7 @@ export interface EvmWalletAdapter extends EventEmitter { ecosystemId: EcosystemId, chainId: EvmChainId, ) => Promise; + readonly protocol: Protocol.Evm; } export class EvmWeb3WalletAdapter @@ -36,6 +38,7 @@ export class EvmWeb3WalletAdapter { readonly serviceName: string; readonly serviceUrl: string; + readonly protocol: Protocol.Evm; address: string | null; connecting: boolean; private readonly getWalletProvider: () => Web3Provider | null; @@ -51,6 +54,11 @@ export class EvmWeb3WalletAdapter this.getWalletProvider = getWalletProvider; this.address = null; this.connecting = false; + this.protocol = Protocol.Evm; + } + + public get connected(): boolean { + return !!this.address; } public get signer(): Signer | null { diff --git a/apps/ui/src/models/wallets/adapters/solana/SolanaDefaultWalletAdapter.ts b/apps/ui/src/models/wallets/adapters/solana/SolanaDefaultWalletAdapter.ts new file mode 100644 index 000000000..94caaa864 --- /dev/null +++ b/apps/ui/src/models/wallets/adapters/solana/SolanaDefaultWalletAdapter.ts @@ -0,0 +1,21 @@ +import Wallet from "@project-serum/sol-wallet-adapter"; + +import { Protocol } from "../../../../config"; + +import type { SolanaWalletAdapter } from "./SolanaWalletAdapter"; + +export class SolanaDefaultWalletAdapter + extends Wallet + implements SolanaWalletAdapter +{ + readonly protocol: Protocol.Solana; + + constructor(provider: unknown, network: string) { + super(provider, network); + this.protocol = Protocol.Solana; + } + + public get address(): string | null { + return this.publicKey?.toBase58() || null; + } +} diff --git a/apps/ui/src/models/wallets/adapters/solana/SolanaWalletAdapter.ts b/apps/ui/src/models/wallets/adapters/solana/SolanaWalletAdapter.ts index 405f6046f..8ed38c628 100644 --- a/apps/ui/src/models/wallets/adapters/solana/SolanaWalletAdapter.ts +++ b/apps/ui/src/models/wallets/adapters/solana/SolanaWalletAdapter.ts @@ -3,13 +3,17 @@ import type { Transaction } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js"; import EventEmitter from "eventemitter3"; +import { Protocol } from "../../../../config"; import { SolanaWalletError } from "../../../../errors"; export interface SolanaWalletAdapter extends EventEmitter { readonly publicKey: PublicKey | null; + readonly address: string | null; + readonly connected: boolean; readonly signTransaction: (transaction: Transaction) => Promise; readonly connect: () => Promise; readonly disconnect: () => Promise; + readonly protocol: Protocol.Solana; } export class SolanaWeb3WalletAdapter @@ -19,6 +23,7 @@ export class SolanaWeb3WalletAdapter serviceName: string; serviceUrl: string; publicKey: PublicKey | null; + readonly protocol: Protocol.Solana; protected getService: () => any; protected connecting: boolean; @@ -29,10 +34,19 @@ export class SolanaWeb3WalletAdapter this.getService = getService; this.publicKey = null; this.connecting = false; + this.protocol = Protocol.Solana; this.on("connect", this.onPublicKeySet); } + public get address(): string | null { + return this.publicKey?.toBase58() || null; + } + + public get connected(): boolean { + return !!this.address; + } + protected get service(): any { return this.getService(); } diff --git a/apps/ui/src/models/wallets/adapters/solana/ledger/index.ts b/apps/ui/src/models/wallets/adapters/solana/ledger/index.ts index 6ec5dbd68..9569157c6 100644 --- a/apps/ui/src/models/wallets/adapters/solana/ledger/index.ts +++ b/apps/ui/src/models/wallets/adapters/solana/ledger/index.ts @@ -3,6 +3,7 @@ import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; import type { PublicKey, Transaction } from "@solana/web3.js"; import EventEmitter from "eventemitter3"; +import { Protocol } from "../../../../../config"; import type { SolanaWalletAdapter } from "../SolanaWalletAdapter"; import { getPublicKey, signTransaction } from "./core"; @@ -14,18 +15,28 @@ export class LedgerWalletAdapter _connecting: boolean; _publicKey: PublicKey | null; _transport: Transport | null; + readonly protocol: Protocol.Solana; constructor() { super(); this._connecting = false; this._publicKey = null; this._transport = null; + this.protocol = Protocol.Solana; } get publicKey(): PublicKey | null { return this._publicKey; } + public get address(): string | null { + return this._publicKey?.toBase58() || null; + } + + public get connected(): boolean { + return !!this.address; + } + async signTransaction(transaction: Transaction): Promise { if (!this._transport || !this._publicKey) { throw new Error("Not connected to Ledger"); diff --git a/apps/ui/src/models/wallets/services.tsx b/apps/ui/src/models/wallets/services.tsx index d45ce69a9..f0cd3da05 100644 --- a/apps/ui/src/models/wallets/services.tsx +++ b/apps/ui/src/models/wallets/services.tsx @@ -1,12 +1,17 @@ import { EuiButtonIcon } from "@elastic/eui"; import type { ReactElement } from "react"; -import type { Ecosystem } from "../../config"; -import { EcosystemId, ecosystems } from "../../config"; +import type { Ecosystem, Protocol } from "../../config"; +import { + EcosystemId, + ecosystems, + getEcosystemsForProtocol, +} from "../../config"; import LEDGER_ICON from "../../images/wallets/ledger.svg"; import MATHWALLET_ICON from "../../images/wallets/mathwallet.svg"; import METAMASK_ICON from "../../images/wallets/metamask.svg"; import PHANTOM_ICON from "../../images/wallets/phantom.svg"; +import { findOrThrow } from "../../utils"; import type { EvmWalletAdapter, @@ -246,3 +251,17 @@ export const WALLET_SERVICES: Record = { [EcosystemId.Karura]: KARURA_WALLET_SERVICES, [EcosystemId.Acala]: ACALA_WALLET_SERVICES, }; + +export const findServiceForProtocol = ( + serviceId: string, + protocol: Protocol, +): WalletService => { + const ecosystemIds = getEcosystemsForProtocol(protocol); + const protocolWalletServices = ecosystemIds.flatMap( + (ecosystemId) => WALLET_SERVICES[ecosystemId], + ); + return findOrThrow( + protocolWalletServices, + (service) => service.id === serviceId, + ); +}; From aca8a32739edafcc780811b634b304d6a5f4a79c Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 15:17:16 +0300 Subject: [PATCH 12/51] Simplify walletService state and move notifications out of the store --- apps/ui/src/App.tsx | 3 + apps/ui/src/components/ConnectButton.tsx | 7 +- apps/ui/src/components/MultiWalletModal.tsx | 6 +- apps/ui/src/contexts/evmWallet.tsx | 2 +- apps/ui/src/contexts/solanaWallet.tsx | 3 +- apps/ui/src/core/store/useWalletService.ts | 118 +++--------------- apps/ui/src/hooks/wallets/index.ts | 1 + .../ui/src/hooks/wallets/useWalletsMonitor.ts | 61 +++++++++ 8 files changed, 90 insertions(+), 111 deletions(-) create mode 100644 apps/ui/src/hooks/wallets/index.ts create mode 100644 apps/ui/src/hooks/wallets/useWalletsMonitor.ts diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index edd921934..5249545ed 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -8,6 +8,7 @@ import { AppCrashed, NewVersionAlert } from "./components/AppCrashed"; import { Layout } from "./components/Layout"; import Notification from "./components/Notification"; import { AppContext } from "./contexts"; +import { useWalletsMonitor } from "./hooks/wallets"; import CollectiblesPage from "./pages/CollectiblesPage"; import HelpPage from "./pages/HelpPage"; import HomePage from "./pages/HomePage"; @@ -24,6 +25,8 @@ import TestPage from "./pages/TestPage"; import TosPage from "./pages/TosPage"; function App(): ReactElement { + useWalletsMonitor(); + return ( { diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index 02f53795c..c65d55b8c 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -6,7 +6,7 @@ import shallow from "zustand/shallow.js"; import { EcosystemId } from "../config"; import { selectConfig } from "../core/selectors"; -import { useEnvironment } from "../core/store"; +import { useEnvironment, useWalletService } from "../core/store"; import { useWallets } from "../hooks"; import AVALANCHE_SVG from "../images/ecosystems/avalanche.svg"; import BSC_SVG from "../images/ecosystems/bsc.svg"; @@ -29,15 +29,16 @@ export const ConnectButton = ({ ...rest }: ConnectButtonProps): ReactElement => { const { ecosystems } = useEnvironment(selectConfig, shallow); + const { disconnectService } = useWalletService(); if (ecosystemId === EcosystemId.Terra) { throw new Error("Unsupported ecosystem"); } const ecosystem = ecosystems[ecosystemId]; const wallets = useWallets(); - const { connected, select, address, wallet } = wallets[ecosystemId]; + const { connected, select, address } = wallets[ecosystemId]; const disconnect = (): void => { - void wallet?.disconnect(); + void disconnectService(ecosystem.protocol); }; const handleClick = connected ? disconnect : select; diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 65f6021a4..70e847ecd 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -107,8 +107,8 @@ const ProtocolWalletOptionsList = ({ const connectedWallets = protocolWallets.filter((wallet) => wallet.connected); - const disconnect = (serviceId: string): void => { - void disconnectService(serviceId, protocol); + const disconnect = (): void => { + void disconnectService(protocol); }; const connect = (serviceId: string) => { @@ -156,7 +156,7 @@ const ProtocolWalletOptionsList = ({ ecosystems={ecosystems} serviceConnected={!!connectedWallet} address={connectedWallet ? connectedWallet.address : null} - disconnect={() => disconnect(service.id)} + disconnect={() => disconnect()} onClick={() => connect(service.id)} /> ); diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index 318e12bbd..be8dad599 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -120,7 +120,7 @@ export const EvmWalletProvider = ({ const { evm } = useWalletService(); - const wallet: EvmWalletAdapter | null = (service && evm[service.id]) || null; + const wallet: EvmWalletAdapter | null = (service && evm) || null; const address = wallet?.address ?? null; useEffect(() => { diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index cc8b49004..d786fec68 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -60,8 +60,7 @@ export const SolanaWalletProvider = ({ const { solana } = useWalletService(); - const wallet: SolanaWalletAdapter | null = - (service && solana[service.id]) || null; + const wallet: SolanaWalletAdapter | null = (service && solana) || null; const address = wallet?.address ?? null; useEffect(() => { diff --git a/apps/ui/src/core/store/useWalletService.ts b/apps/ui/src/core/store/useWalletService.ts index 929298b41..0d5ac60de 100644 --- a/apps/ui/src/core/store/useWalletService.ts +++ b/apps/ui/src/core/store/useWalletService.ts @@ -12,111 +12,37 @@ import type { WalletAdapter, } from "../../models/wallets/adapters"; import { SolanaDefaultWalletAdapter } from "../../models/wallets/adapters/solana/SolanaDefaultWalletAdapter"; -import type { ReadonlyRecord } from "../../utils"; -import { shortenAddress } from "../../utils"; import { selectConfig } from "../selectors/environment"; import { useEnvironment as environmentStore } from "./useEnvironment"; -import { useNotification as notificationStore } from "./useNotification"; - -type WalletAdapterListeners = { - readonly connect: () => void; - readonly disconnect: () => void; - readonly error: (title: string, description: string) => void; -}; export interface WalletServiceState { - readonly evm: ReadonlyRecord; - readonly evmListeners: ReadonlyRecord< - string, - WalletAdapterListeners | undefined - >; - readonly solana: ReadonlyRecord; - readonly solanaListeners: ReadonlyRecord< - string, - WalletAdapterListeners | undefined - >; + readonly evm: EvmWalletAdapter | null; + readonly solana: SolanaWalletAdapter | null; readonly connectService: ( serviceId: string, protocol: Protocol, ) => Promise; - readonly disconnectService: ( - serviceId: string, - protocol: Protocol, - ) => Promise; + readonly disconnectService: (protocol: Protocol) => Promise; } export const useWalletService = create( (set: SetState, get: GetState) => ({ - evm: {}, - evmListeners: {}, - solana: {}, - solanaListeners: {}, + evm: null, + solana: null, connectService: async (serviceId: string, protocol: Protocol) => { - const state = get(); const service = findServiceForProtocol(serviceId, protocol); - - const protocolAdapters = - protocol === Protocol.Evm ? state.evm : state.solana; - const previous = protocolAdapters[serviceId]; - - if (previous) { - if (previous.connected) { - const listeners = - protocol === Protocol.Evm - ? state.evmListeners - : state.solanaListeners; - - // call on connect handler for successful connection toast - listeners[serviceId]?.connect(); - return; - } - - await state.disconnectService(serviceId, protocol); - } - const adapter = createAdapter(service, protocol); - const { notify } = notificationStore.getState(); - - const handleConnect = (): void => { - if (adapter.address) { - notify( - "Wallet update", - `Connected to wallet ${shortenAddress(adapter.address)}`, - "info", - 7000, - ); - } - }; - const handleDisconnect = (): void => { - notify("Wallet update", "Disconnected from wallet", "warning"); - }; - const handleError = (title: string, description: string): void => { - notify(title, description, "error"); - }; - - const listeners: WalletAdapterListeners = { - connect: handleConnect, - disconnect: handleDisconnect, - error: handleError, - }; - - Object.entries(listeners).forEach(([eventName, handler]) => { - adapter.on(eventName, handler); - }); - set( produce((draft) => { switch (adapter.protocol) { case Protocol.Evm: { - draft.evm[serviceId] = adapter; - draft.evmListeners[serviceId] = listeners; + draft.evm = adapter; break; } case Protocol.Solana: { - draft.solana[serviceId] = adapter; - draft.solanaListeners[serviceId] = listeners; + draft.solana = adapter; break; } } @@ -125,38 +51,26 @@ export const useWalletService = create( await adapter.connect().catch(console.error); }, - disconnectService: async (serviceId: string, protocol: Protocol) => { + disconnectService: async (protocol: Protocol) => { const state = get(); - const protocolAdapters = - protocol === Protocol.Evm ? state.evm : state.solana; - const adapter = protocolAdapters[serviceId]; - - if (adapter) { - await adapter.disconnect().catch(console.error); + const adapter = protocol === Protocol.Evm ? state.evm : state.solana; - const protocolListeners = - protocol === Protocol.Evm - ? state.evmListeners - : state.solanaListeners; + if (!adapter) + throw new Error( + `disconnectService called but no adapter found for protocol ${protocol}`, + ); - const listeners = protocolListeners[serviceId]; - - if (listeners) { - Object.entries(listeners).forEach(([eventName, handler]) => { - adapter.off(eventName, handler); - }); - } - } + await adapter.disconnect().catch(console.error); set( produce((draft) => { switch (protocol) { case Protocol.Evm: { - draft.evm[serviceId] = undefined; + draft.evm = null; break; } case Protocol.Solana: { - draft.solana[serviceId] = undefined; + draft.solana = null; break; } case Protocol.Cosmos: { diff --git a/apps/ui/src/hooks/wallets/index.ts b/apps/ui/src/hooks/wallets/index.ts new file mode 100644 index 000000000..46c2d578c --- /dev/null +++ b/apps/ui/src/hooks/wallets/index.ts @@ -0,0 +1 @@ +export * from "./useWalletsMonitor"; diff --git a/apps/ui/src/hooks/wallets/useWalletsMonitor.ts b/apps/ui/src/hooks/wallets/useWalletsMonitor.ts new file mode 100644 index 000000000..f21fbec61 --- /dev/null +++ b/apps/ui/src/hooks/wallets/useWalletsMonitor.ts @@ -0,0 +1,61 @@ +import { useEffect } from "react"; + +import { useNotification, useWalletService } from "../../core/store"; +import { isNotNull, shortenAddress } from "../../utils"; + +type WalletAdapterListeners = { + readonly connect: () => void; + readonly disconnect: () => void; + readonly error: (title: string, description: string) => void; +}; + +export const useWalletsMonitor = (): void => { + const { notify } = useNotification(); + const { evm, solana } = useWalletService(); + + useEffect(() => { + const walletAdapters = [evm, solana].filter(isNotNull); + + if (!walletAdapters.length) return; + + const listeners: readonly WalletAdapterListeners[] = walletAdapters.map( + (walletAdapter) => { + const handleConnect = (): void => { + if (walletAdapter.address) { + notify( + "Wallet update", + `Connected to wallet ${shortenAddress(walletAdapter.address)}`, + "info", + 7000, + ); + } + }; + const handleDisconnect = (): void => { + notify("Wallet update", "Disconnected from wallet", "warning"); + }; + const handleError = (title: string, description: string): void => { + notify(title, description, "error"); + }; + + walletAdapter.on("connect", handleConnect); + walletAdapter.on("disconnect", handleDisconnect); + walletAdapter.on("error", handleError); + + return { + connect: handleConnect, + disconnect: handleDisconnect, + error: handleError, + }; + }, + ); + + return () => { + walletAdapters.forEach((walletAdapter, index) => { + const { connect, disconnect, error } = listeners[index]; + walletAdapter.off("connect", connect); + walletAdapter.off("disconnect", disconnect); + walletAdapter.off("error", error); + }); + }; + }, [evm, solana, notify]); +}; From 71429bcefdabaf22e600a669fe103a575aea3756 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 15:19:50 +0300 Subject: [PATCH 13/51] Rename useWalletService to useWalletAdapter --- apps/ui/src/components/ConnectButton.tsx | 4 ++-- apps/ui/src/components/MultiWalletModal.tsx | 4 ++-- apps/ui/src/components/SingleWalletModal.tsx | 4 ++-- apps/ui/src/contexts/evmWallet.tsx | 4 ++-- apps/ui/src/contexts/solanaWallet.tsx | 4 ++-- .../store/{useWalletService.ts => useWalletAdapter.ts} | 10 +++++----- apps/ui/src/hooks/wallets/useWalletsMonitor.ts | 4 ++-- 7 files changed, 17 insertions(+), 17 deletions(-) rename apps/ui/src/core/store/{useWalletService.ts => useWalletAdapter.ts} (91%) diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index c65d55b8c..c6000532c 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -6,7 +6,7 @@ import shallow from "zustand/shallow.js"; import { EcosystemId } from "../config"; import { selectConfig } from "../core/selectors"; -import { useEnvironment, useWalletService } from "../core/store"; +import { useEnvironment, useWalletAdapter } from "../core/store"; import { useWallets } from "../hooks"; import AVALANCHE_SVG from "../images/ecosystems/avalanche.svg"; import BSC_SVG from "../images/ecosystems/bsc.svg"; @@ -29,7 +29,7 @@ export const ConnectButton = ({ ...rest }: ConnectButtonProps): ReactElement => { const { ecosystems } = useEnvironment(selectConfig, shallow); - const { disconnectService } = useWalletService(); + const { disconnectService } = useWalletAdapter(); if (ecosystemId === EcosystemId.Terra) { throw new Error("Unsupported ecosystem"); } diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 70e847ecd..b68a2e8c9 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -13,7 +13,7 @@ import type { ReactElement } from "react"; import type { Ecosystem } from "../config"; import { Protocol, getEcosystemsForProtocol } from "../config"; -import { useWalletService } from "../core/store"; +import { useWalletAdapter } from "../core/store"; import { useWallets } from "../hooks"; import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; @@ -92,7 +92,7 @@ const ProtocolWalletOptionsList = ({ protocol, }: ProtocolWalletOptionsListProps): ReactElement => { const wallets = useWallets(); - const { connectService, disconnectService } = useWalletService(); + const { connectService, disconnectService } = useWalletAdapter(); const ecosystemIds = getEcosystemsForProtocol(protocol); const protocolWalletServices = ecosystemIds.flatMap( (ecosystemId) => WALLET_SERVICES[ecosystemId], diff --git a/apps/ui/src/components/SingleWalletModal.tsx b/apps/ui/src/components/SingleWalletModal.tsx index d9601134a..68e52d16e 100644 --- a/apps/ui/src/components/SingleWalletModal.tsx +++ b/apps/ui/src/components/SingleWalletModal.tsx @@ -10,7 +10,7 @@ import type { ReactElement } from "react"; import { Fragment } from "react"; import type { Protocol } from "../config"; -import { useWalletService } from "../core/store"; +import { useWalletAdapter } from "../core/store"; import type { WalletAdapter, WalletService } from "../models"; import { isUserOnMobileDevice } from "../utils"; @@ -36,7 +36,7 @@ export const SingleWalletModal = ({ handleClose, setServiceId, }: SingleWalletModalProps): ReactElement => { - const { connectService } = useWalletService(); + const { connectService } = useWalletAdapter(); return ( diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index be8dad599..7b3f2e8ab 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -5,7 +5,7 @@ import * as React from "react"; import { SingleWalletModal } from "../components/SingleWalletModal"; import type { EvmEcosystemId } from "../config"; import { EcosystemId, Protocol } from "../config"; -import { useWalletService } from "../core/store"; +import { useWalletAdapter } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { EvmWalletAdapter, WalletService } from "../models"; import { @@ -118,7 +118,7 @@ export const EvmWalletProvider = ({ [serviceId, services], ); - const { evm } = useWalletService(); + const { evm } = useWalletAdapter(); const wallet: EvmWalletAdapter | null = (service && evm) || null; const address = wallet?.address ?? null; diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index d786fec68..e90ff2136 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -10,7 +10,7 @@ import { import { SingleWalletModal } from "../components/SingleWalletModal"; import { Protocol } from "../config"; -import { useWalletService } from "../core/store"; +import { useWalletAdapter } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { SolanaWalletAdapter, @@ -58,7 +58,7 @@ export const SolanaWalletProvider = ({ [serviceId], ); - const { solana } = useWalletService(); + const { solana } = useWalletAdapter(); const wallet: SolanaWalletAdapter | null = (service && solana) || null; const address = wallet?.address ?? null; diff --git a/apps/ui/src/core/store/useWalletService.ts b/apps/ui/src/core/store/useWalletAdapter.ts similarity index 91% rename from apps/ui/src/core/store/useWalletService.ts rename to apps/ui/src/core/store/useWalletAdapter.ts index 0d5ac60de..ae22847da 100644 --- a/apps/ui/src/core/store/useWalletService.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -16,7 +16,7 @@ import { selectConfig } from "../selectors/environment"; import { useEnvironment as environmentStore } from "./useEnvironment"; -export interface WalletServiceState { +export interface WalletAdapterState { readonly evm: EvmWalletAdapter | null; readonly solana: SolanaWalletAdapter | null; readonly connectService: ( @@ -26,8 +26,8 @@ export interface WalletServiceState { readonly disconnectService: (protocol: Protocol) => Promise; } -export const useWalletService = create( - (set: SetState, get: GetState) => ({ +export const useWalletAdapter = create( + (set: SetState, get: GetState) => ({ evm: null, solana: null, connectService: async (serviceId: string, protocol: Protocol) => { @@ -35,7 +35,7 @@ export const useWalletService = create( const adapter = createAdapter(service, protocol); set( - produce((draft) => { + produce((draft) => { switch (adapter.protocol) { case Protocol.Evm: { draft.evm = adapter; @@ -63,7 +63,7 @@ export const useWalletService = create( await adapter.disconnect().catch(console.error); set( - produce((draft) => { + produce((draft) => { switch (protocol) { case Protocol.Evm: { draft.evm = null; diff --git a/apps/ui/src/hooks/wallets/useWalletsMonitor.ts b/apps/ui/src/hooks/wallets/useWalletsMonitor.ts index f21fbec61..e3e54f651 100644 --- a/apps/ui/src/hooks/wallets/useWalletsMonitor.ts +++ b/apps/ui/src/hooks/wallets/useWalletsMonitor.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import { useNotification, useWalletService } from "../../core/store"; +import { useNotification, useWalletAdapter } from "../../core/store"; import { isNotNull, shortenAddress } from "../../utils"; type WalletAdapterListeners = { @@ -11,7 +11,7 @@ type WalletAdapterListeners = { export const useWalletsMonitor = (): void => { const { notify } = useNotification(); - const { evm, solana } = useWalletService(); + const { evm, solana } = useWalletAdapter(); useEffect(() => { const walletAdapters = [evm, solana].filter(isNotNull); From a1fa3bf3e5b35df22e6a95ee3a172d8547f3d3cf Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 16:08:16 +0300 Subject: [PATCH 14/51] Add useWalletSevice hook to avoid accessing the environment store directly --- apps/ui/src/components/MultiWalletModal.tsx | 5 +-- apps/ui/src/components/SingleWalletModal.tsx | 4 +- apps/ui/src/core/store/index.ts | 2 +- apps/ui/src/core/store/useWalletAdapter.ts | 42 ++++--------------- apps/ui/src/hooks/index.ts | 1 + apps/ui/src/hooks/wallets/index.ts | 1 + apps/ui/src/hooks/wallets/useWalletService.ts | 38 +++++++++++++++++ .../models/wallets/adapters/solana/index.ts | 2 + apps/ui/src/models/wallets/services.tsx | 34 ++++++++++++++- 9 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 apps/ui/src/hooks/wallets/useWalletService.ts diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index b68a2e8c9..752bdb22c 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -13,8 +13,7 @@ import type { ReactElement } from "react"; import type { Ecosystem } from "../config"; import { Protocol, getEcosystemsForProtocol } from "../config"; -import { useWalletAdapter } from "../core/store"; -import { useWallets } from "../hooks"; +import { useWalletService, useWallets } from "../hooks"; import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; import { WALLET_SERVICES } from "../models"; @@ -92,7 +91,7 @@ const ProtocolWalletOptionsList = ({ protocol, }: ProtocolWalletOptionsListProps): ReactElement => { const wallets = useWallets(); - const { connectService, disconnectService } = useWalletAdapter(); + const { connectService, disconnectService } = useWalletService(); const ecosystemIds = getEcosystemsForProtocol(protocol); const protocolWalletServices = ecosystemIds.flatMap( (ecosystemId) => WALLET_SERVICES[ecosystemId], diff --git a/apps/ui/src/components/SingleWalletModal.tsx b/apps/ui/src/components/SingleWalletModal.tsx index 68e52d16e..5736eb130 100644 --- a/apps/ui/src/components/SingleWalletModal.tsx +++ b/apps/ui/src/components/SingleWalletModal.tsx @@ -10,7 +10,7 @@ import type { ReactElement } from "react"; import { Fragment } from "react"; import type { Protocol } from "../config"; -import { useWalletAdapter } from "../core/store"; +import { useWalletService } from "../hooks/wallets"; import type { WalletAdapter, WalletService } from "../models"; import { isUserOnMobileDevice } from "../utils"; @@ -36,7 +36,7 @@ export const SingleWalletModal = ({ handleClose, setServiceId, }: SingleWalletModalProps): ReactElement => { - const { connectService } = useWalletAdapter(); + const { connectService } = useWalletService(); return ( diff --git a/apps/ui/src/core/store/index.ts b/apps/ui/src/core/store/index.ts index 65c712379..85e80f965 100644 --- a/apps/ui/src/core/store/index.ts +++ b/apps/ui/src/core/store/index.ts @@ -1,3 +1,3 @@ export * from "./useNotification"; export * from "./useEnvironment"; -export * from "./useWalletService"; +export * from "./useWalletAdapter"; diff --git a/apps/ui/src/core/store/useWalletAdapter.ts b/apps/ui/src/core/store/useWalletAdapter.ts index ae22847da..e5b0538e5 100644 --- a/apps/ui/src/core/store/useWalletAdapter.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -4,17 +4,13 @@ import type { GetState, SetState } from "zustand"; import create from "zustand"; import { Protocol } from "../../config"; -import type { WalletService } from "../../models"; -import { findServiceForProtocol } from "../../models"; import type { EvmWalletAdapter, SolanaWalletAdapter, WalletAdapter, } from "../../models/wallets/adapters"; -import { SolanaDefaultWalletAdapter } from "../../models/wallets/adapters/solana/SolanaDefaultWalletAdapter"; -import { selectConfig } from "../selectors/environment"; -import { useEnvironment as environmentStore } from "./useEnvironment"; +type AdapterFactory = (serviceId: string, protocol: Protocol) => WalletAdapter; export interface WalletAdapterState { readonly evm: EvmWalletAdapter | null; @@ -22,6 +18,7 @@ export interface WalletAdapterState { readonly connectService: ( serviceId: string, protocol: Protocol, + createAdapter: AdapterFactory, ) => Promise; readonly disconnectService: (protocol: Protocol) => Promise; } @@ -30,9 +27,12 @@ export const useWalletAdapter = create( (set: SetState, get: GetState) => ({ evm: null, solana: null, - connectService: async (serviceId: string, protocol: Protocol) => { - const service = findServiceForProtocol(serviceId, protocol); - const adapter = createAdapter(service, protocol); + connectService: async ( + serviceId: string, + protocol: Protocol, + createAdapter, + ) => { + const adapter = createAdapter(serviceId, protocol); set( produce((draft) => { @@ -82,29 +82,3 @@ export const useWalletAdapter = create( }, }), ); - -const createAdapter = ( - service: WalletService, - protocol: Protocol, -): WalletAdapter => { - switch (protocol) { - case Protocol.Evm: { - if (!service.adapter) - throw new Error(`Adapter is required for protocol ${protocol}`); - return new service.adapter(); - } - case Protocol.Solana: { - if (service.adapter) { - return new service.adapter(); - } else { - const environmentStoreState = environmentStore.getState(); - const { chains } = selectConfig(environmentStoreState); - const [{ endpoint }] = chains[Protocol.Solana]; - return new SolanaDefaultWalletAdapter(service.info.url, endpoint); - } - } - case Protocol.Cosmos: { - throw new Error(`Cosmos adapters not implemented yet`); - } - } -}; diff --git a/apps/ui/src/hooks/index.ts b/apps/ui/src/hooks/index.ts index 3b4b7be60..45cd724b0 100644 --- a/apps/ui/src/hooks/index.ts +++ b/apps/ui/src/hooks/index.ts @@ -4,4 +4,5 @@ export * from "./evm"; export * from "./solana"; export * from "./swim"; export * from "./utils"; +export * from "./wallets"; export * from "./wormhole"; diff --git a/apps/ui/src/hooks/wallets/index.ts b/apps/ui/src/hooks/wallets/index.ts index 46c2d578c..e1ab029cc 100644 --- a/apps/ui/src/hooks/wallets/index.ts +++ b/apps/ui/src/hooks/wallets/index.ts @@ -1 +1,2 @@ export * from "./useWalletsMonitor"; +export * from "./useWalletService"; diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts new file mode 100644 index 000000000..d75acfe30 --- /dev/null +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -0,0 +1,38 @@ +import { useCallback, useMemo } from "react"; +import shallow from "zustand/shallow.js"; + +import { Protocol } from "../../config"; +import { selectConfig } from "../../core/selectors"; +import type { WalletAdapterState } from "../../core/store"; +import { useEnvironment, useWalletAdapter } from "../../core/store"; +import type { WalletService } from "../../models"; +import { createAdapter } from "../../models/wallets/services"; + +type WalletServiceState = Pick & { + readonly connectService: ( + serviceId: WalletService["id"], + protocol: Protocol, + ) => Promise; +}; + +export const useWalletService = (): WalletServiceState => { + const { connectService, disconnectService } = useWalletAdapter(); + const { chains } = useEnvironment(selectConfig, shallow); + const [{ endpoint }] = chains[Protocol.Solana]; + + const createAdapterMemoized = useCallback( + (serviceId: WalletService["id"], protocol: Protocol) => { + return createAdapter(serviceId, protocol, endpoint); + }, + [endpoint], + ); + + return useMemo( + () => ({ + connectService: (serviceId: WalletService["id"], protocol: Protocol) => + connectService(serviceId, protocol, createAdapterMemoized), + disconnectService, + }), + [connectService, disconnectService, createAdapterMemoized], + ); +}; diff --git a/apps/ui/src/models/wallets/adapters/solana/index.ts b/apps/ui/src/models/wallets/adapters/solana/index.ts index bd0f03a45..02c9beeba 100644 --- a/apps/ui/src/models/wallets/adapters/solana/index.ts +++ b/apps/ui/src/models/wallets/adapters/solana/index.ts @@ -1,5 +1,6 @@ import { MathWalletAdapter } from "./MathWalletAdapter"; import { PhantomAdapter } from "./PhantomAdapter"; +import { SolanaDefaultWalletAdapter } from "./SolanaDefaultWalletAdapter"; import { SolongAdapter } from "./SolongAdapter"; import { LedgerWalletAdapter } from "./ledger"; @@ -10,4 +11,5 @@ export const solanaAdapters = { MathWalletAdapter, PhantomAdapter, SolongAdapter, + SolanaDefaultWalletAdapter, }; diff --git a/apps/ui/src/models/wallets/services.tsx b/apps/ui/src/models/wallets/services.tsx index f0cd3da05..a0bab094e 100644 --- a/apps/ui/src/models/wallets/services.tsx +++ b/apps/ui/src/models/wallets/services.tsx @@ -1,9 +1,10 @@ import { EuiButtonIcon } from "@elastic/eui"; import type { ReactElement } from "react"; -import type { Ecosystem, Protocol } from "../../config"; +import type { Ecosystem } from "../../config"; import { EcosystemId, + Protocol, ecosystems, getEcosystemsForProtocol, } from "../../config"; @@ -252,7 +253,7 @@ export const WALLET_SERVICES: Record = { [EcosystemId.Acala]: ACALA_WALLET_SERVICES, }; -export const findServiceForProtocol = ( +const findServiceForProtocol = ( serviceId: string, protocol: Protocol, ): WalletService => { @@ -265,3 +266,32 @@ export const findServiceForProtocol = ( (service) => service.id === serviceId, ); }; + +export const createAdapter = ( + serviceId: WalletService["id"], + protocol: Protocol, + solanaEndpoint: string, +): WalletAdapter => { + const service = findServiceForProtocol(serviceId, protocol); + + switch (protocol) { + case Protocol.Evm: { + if (!service.adapter) + throw new Error(`Adapter is required for protocol ${protocol}`); + return new service.adapter(); + } + case Protocol.Solana: { + if (service.adapter) { + return new service.adapter(); + } else { + return new adapters.solana.SolanaDefaultWalletAdapter( + service.info.url, + solanaEndpoint, + ); + } + } + case Protocol.Cosmos: { + throw new Error(`Cosmos adapters not implemented yet`); + } + } +}; From 1715741cf11e63dc58753fdb8c299bdc2e440423 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 16:18:34 +0300 Subject: [PATCH 15/51] Disconnect previous adapter if exists --- apps/ui/src/core/store/useWalletAdapter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/ui/src/core/store/useWalletAdapter.ts b/apps/ui/src/core/store/useWalletAdapter.ts index e5b0538e5..005d72d33 100644 --- a/apps/ui/src/core/store/useWalletAdapter.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -32,6 +32,11 @@ export const useWalletAdapter = create( protocol: Protocol, createAdapter, ) => { + const state = get(); + const previous = protocol === Protocol.Evm ? state.evm : state.solana; + + if (previous) await state.disconnectService(protocol); + const adapter = createAdapter(serviceId, protocol); set( From a3641c7583c5c1ce9ba5bd5efbe2f0955c38421c Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 18:54:27 +0300 Subject: [PATCH 16/51] Move useWalletsMonitor to its own component to avoid re-rendering the whole App component --- apps/ui/src/App.tsx | 5 ++--- apps/ui/src/components/WalletsMonitor.tsx | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 apps/ui/src/components/WalletsMonitor.tsx diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 5249545ed..5121db0ac 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -7,8 +7,8 @@ import "./App.scss"; import { AppCrashed, NewVersionAlert } from "./components/AppCrashed"; import { Layout } from "./components/Layout"; import Notification from "./components/Notification"; +import WalletsMonitor from "./components/WalletsMonitor"; import { AppContext } from "./contexts"; -import { useWalletsMonitor } from "./hooks/wallets"; import CollectiblesPage from "./pages/CollectiblesPage"; import HelpPage from "./pages/HelpPage"; import HomePage from "./pages/HomePage"; @@ -25,8 +25,6 @@ import TestPage from "./pages/TestPage"; import TosPage from "./pages/TosPage"; function App(): ReactElement { - useWalletsMonitor(); - return ( { @@ -105,6 +103,7 @@ function App(): ReactElement { + ); } diff --git a/apps/ui/src/components/WalletsMonitor.tsx b/apps/ui/src/components/WalletsMonitor.tsx new file mode 100644 index 000000000..61970d56e --- /dev/null +++ b/apps/ui/src/components/WalletsMonitor.tsx @@ -0,0 +1,8 @@ +import { useWalletsMonitor } from "../hooks/wallets"; + +const WalletsMonitor = (): null => { + useWalletsMonitor(); + return null; +}; + +export default WalletsMonitor; From 817e253f2bf1bf12d31813f52b5971e3154d1cc6 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 19:11:51 +0300 Subject: [PATCH 17/51] Update EVM logo and protocol names --- apps/ui/src/components/MultiWalletModal.tsx | 11 +++----- apps/ui/src/config/ecosystem.ts | 6 ++++ .../src/images/ecosystems/ethereum-color.svg | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 apps/ui/src/images/ecosystems/ethereum-color.svg diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 752bdb22c..6ad80b4d2 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -12,9 +12,9 @@ import { import type { ReactElement } from "react"; import type { Ecosystem } from "../config"; -import { Protocol, getEcosystemsForProtocol } from "../config"; +import { Protocol, protocolNames, getEcosystemsForProtocol } from "../config"; import { useWalletService, useWallets } from "../hooks"; -import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; +import EVM_SVG from "../images/ecosystems/ethereum-color.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; import { WALLET_SERVICES } from "../models"; import type { WalletService } from "../models"; @@ -128,7 +128,7 @@ const ProtocolWalletOptionsList = ({

- {protocol} + {protocolNames[protocol]}

@@ -188,10 +188,7 @@ export const MultiWalletModal = ({ icon={SOLANA_SVG} protocol={Protocol.Solana} /> - +
diff --git a/apps/ui/src/config/ecosystem.ts b/apps/ui/src/config/ecosystem.ts index 8b1ef5a88..3adee794d 100644 --- a/apps/ui/src/config/ecosystem.ts +++ b/apps/ui/src/config/ecosystem.ts @@ -18,6 +18,12 @@ export const enum Protocol { Cosmos = "cosmos", } +export const protocolNames: Record = { + [Protocol.Solana]: "Solana", + [Protocol.Evm]: "Ethereum Virtual Machine", + [Protocol.Cosmos]: "Cosmos", +}; + /** * Maps 1:1 onto @certusone/wormhole-sdk ChainId * For a given Env, this encodes both Protocol and Protocol-specific ChainId diff --git a/apps/ui/src/images/ecosystems/ethereum-color.svg b/apps/ui/src/images/ecosystems/ethereum-color.svg new file mode 100644 index 000000000..83f801d8e --- /dev/null +++ b/apps/ui/src/images/ecosystems/ethereum-color.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 9c5b82ec59e0bfc2cf552399bd58aff15a9b377d Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 19:13:20 +0300 Subject: [PATCH 18/51] Lint order of imports --- apps/ui/src/components/MultiWalletModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 6ad80b4d2..5dc95cbba 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -12,7 +12,7 @@ import { import type { ReactElement } from "react"; import type { Ecosystem } from "../config"; -import { Protocol, protocolNames, getEcosystemsForProtocol } from "../config"; +import { Protocol, getEcosystemsForProtocol, protocolNames } from "../config"; import { useWalletService, useWallets } from "../hooks"; import EVM_SVG from "../images/ecosystems/ethereum-color.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; From 2f9da4c8a6983a3099d62301e889e5ecc031523f Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 19:17:16 +0300 Subject: [PATCH 19/51] Use EVM instead of Ethereum Virtual Machine --- apps/ui/src/config/ecosystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/config/ecosystem.ts b/apps/ui/src/config/ecosystem.ts index 3adee794d..4dd233e82 100644 --- a/apps/ui/src/config/ecosystem.ts +++ b/apps/ui/src/config/ecosystem.ts @@ -20,7 +20,7 @@ export const enum Protocol { export const protocolNames: Record = { [Protocol.Solana]: "Solana", - [Protocol.Evm]: "Ethereum Virtual Machine", + [Protocol.Evm]: "EVM", [Protocol.Cosmos]: "Cosmos", }; From 35b377e255a5afbf12c1a18742e144c4e4c2f1cb Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 19:43:01 +0300 Subject: [PATCH 20/51] Revert imports in selectors --- apps/ui/src/core/selectors/environment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/core/selectors/environment.ts b/apps/ui/src/core/selectors/environment.ts index 9bb216f75..d6391b6c8 100644 --- a/apps/ui/src/core/selectors/environment.ts +++ b/apps/ui/src/core/selectors/environment.ts @@ -1,6 +1,6 @@ import { DEFAULT_ENV, configs, overrideLocalnetIp } from "../../config"; -import { Env } from "../store/useEnvironment"; -import type { EnvironmentState } from "../store/useEnvironment"; +import { Env } from "../store"; +import type { EnvironmentState } from "../store"; export const selectEnvs = (state: EnvironmentState) => state.customLocalnetIp === null ? [DEFAULT_ENV] : Object.values(Env); From 7cfbc8d5caf77715ad2f14dbdc82fe0d5abf3173 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 20:10:02 +0300 Subject: [PATCH 21/51] Display wallets count/icons if any wallet is connected --- apps/ui/src/components/ConnectButton.tsx | 50 +++++++++++++----------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index c6000532c..08a93e0b6 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -8,12 +8,11 @@ import { EcosystemId } from "../config"; import { selectConfig } from "../core/selectors"; import { useEnvironment, useWalletAdapter } from "../core/store"; import { useWallets } from "../hooks"; -import AVALANCHE_SVG from "../images/ecosystems/avalanche.svg"; import BSC_SVG from "../images/ecosystems/bsc.svg"; import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; -import POLYGON_SVG from "../images/ecosystems/polygon.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; -import { shortenAddress } from "../utils"; +import type { WalletService } from "../models"; +import { deduplicate, isNotNull, shortenAddress } from "../utils"; import { MultiWalletModal } from "./MultiWalletModal"; @@ -82,29 +81,36 @@ export const MultiConnectButton = ({ const openModal = (): void => setIsWalletModalOpen(true); const { - solana: { connected: isSolanaConnected }, - ethereum: { connected: isEthereumConnected }, - bsc: { connected: isBscConnected }, - avalanche: { connected: isAvalancheConnected }, - polygon: { connected: isPolygonConnected }, + solana: { connected: isSolanaConnected, service: solanaService }, + ethereum: { connected: isEthereumConnected, service: ethereumService }, + bsc: { connected: isBscConnected, service: bscService }, + avalanche: { connected: isAvalancheConnected, service: avalanceService }, + polygon: { connected: isPolygonConnected, service: polygonService }, } = useWallets(); - const connectedStatuses = [ - isSolanaConnected, - isEthereumConnected, - isBscConnected, - isAvalancheConnected, - isPolygonConnected, - ]; - const nConnected = connectedStatuses.filter(Boolean).length; + const connectedServices = [ + isSolanaConnected ? solanaService : null, + isEthereumConnected ? ethereumService : null, + isBscConnected ? bscService : null, + isAvalancheConnected ? avalanceService : null, + isPolygonConnected ? polygonService : null, + ].filter(isNotNull); + + const uniqueServices = deduplicate( + (walletService) => walletService.id, + connectedServices, + ); + const nConnected = uniqueServices.length; const label = nConnected > 0 ? ( <> - {isSolanaConnected && } - {isEthereumConnected && } - {isBscConnected && } - {isAvalancheConnected && } - {isPolygonConnected && } + {uniqueServices.map((walletService) => ( + + ))}  {nConnected}  connected @@ -113,7 +119,7 @@ export const MultiConnectButton = ({ - {/* TODO: Consider adding these: + {/* TODO: Consider adding these (OR maybe switch to wallet icons here too?): */}   From 783b8f8a83c79845c90e8adc67f75d223569dfef Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 21:06:58 +0300 Subject: [PATCH 22/51] DRY up ecosystems and use filterMap --- apps/ui/src/config/ecosystem.ts | 40 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/ui/src/config/ecosystem.ts b/apps/ui/src/config/ecosystem.ts index 4dd233e82..0c8b06057 100644 --- a/apps/ui/src/config/ecosystem.ts +++ b/apps/ui/src/config/ecosystem.ts @@ -9,6 +9,7 @@ import POLYGON_SVG from "../images/ecosystems/polygon.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; import TERRA_SVG from "../images/ecosystems/terra.svg"; import type { ReadonlyRecord } from "../utils"; +import { filterMap } from "../utils"; import { WormholeChainId } from "./wormhole"; @@ -91,8 +92,8 @@ export interface Ecosystem { readonly nativeTokenSymbol: string; } -export const ecosystems: ReadonlyRecord = { - [EcosystemId.Solana]: { +export const ecosystemList: readonly Ecosystem[] = [ + { id: EcosystemId.Solana, protocol: Protocol.Solana, wormholeChainId: WormholeChainId.Solana, @@ -100,7 +101,7 @@ export const ecosystems: ReadonlyRecord = { logo: SOLANA_SVG, nativeTokenSymbol: "SOL", }, - [EcosystemId.Ethereum]: { + { id: EcosystemId.Ethereum, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Ethereum, @@ -108,7 +109,7 @@ export const ecosystems: ReadonlyRecord = { logo: ETHEREUM_SVG, nativeTokenSymbol: "ETH", }, - [EcosystemId.Terra]: { + { id: EcosystemId.Terra, protocol: Protocol.Cosmos, wormholeChainId: WormholeChainId.Terra, @@ -116,7 +117,7 @@ export const ecosystems: ReadonlyRecord = { logo: TERRA_SVG, nativeTokenSymbol: "LUNA", }, - [EcosystemId.Bsc]: { + { id: EcosystemId.Bsc, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Bsc, @@ -124,7 +125,7 @@ export const ecosystems: ReadonlyRecord = { logo: BSC_SVG, nativeTokenSymbol: "BNB", }, - [EcosystemId.Avalanche]: { + { id: EcosystemId.Avalanche, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Avalanche, @@ -132,7 +133,7 @@ export const ecosystems: ReadonlyRecord = { logo: AVALANCHE_SVG, nativeTokenSymbol: "AVAX", }, - [EcosystemId.Polygon]: { + { id: EcosystemId.Polygon, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Polygon, @@ -140,7 +141,7 @@ export const ecosystems: ReadonlyRecord = { logo: POLYGON_SVG, nativeTokenSymbol: "MATIC", }, - [EcosystemId.Aurora]: { + { id: EcosystemId.Aurora, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Aurora, @@ -148,7 +149,7 @@ export const ecosystems: ReadonlyRecord = { logo: AURORA_SVG, nativeTokenSymbol: "ETH", }, - [EcosystemId.Fantom]: { + { id: EcosystemId.Fantom, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Fantom, @@ -156,7 +157,7 @@ export const ecosystems: ReadonlyRecord = { logo: FANTOM_SVG, nativeTokenSymbol: "FTM", }, - [EcosystemId.Karura]: { + { id: EcosystemId.Karura, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Karura, @@ -164,7 +165,7 @@ export const ecosystems: ReadonlyRecord = { logo: KARURA_SVG, nativeTokenSymbol: "KAR", }, - [EcosystemId.Acala]: { + { id: EcosystemId.Acala, protocol: Protocol.Evm, wormholeChainId: WormholeChainId.Acala, @@ -172,14 +173,19 @@ export const ecosystems: ReadonlyRecord = { logo: ACALA_SVG, nativeTokenSymbol: "ACA", }, -}; +]; + +export const ecosystems: ReadonlyRecord = + Object.fromEntries( + new Map(ecosystemList.map((ecosystem) => [ecosystem.id, ecosystem])), + ) as ReadonlyRecord; export const getEcosystemsForProtocol = ( protocol: Protocol, ): readonly EcosystemId[] => { - return Object.entries(ecosystems) - .filter(([ecosystemId, ecosystem]) => { - return ecosystem.protocol === protocol; - }) - .map(([ecosystemId]) => ecosystemId as EcosystemId); + return filterMap( + (ecosystem: Ecosystem) => ecosystem.protocol === protocol, + (ecosystem) => ecosystem.id, + ecosystemList, + ); }; From 4f0ba6625d9f7511297d0d5b40920288ad47941a Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 30 May 2022 21:46:11 +0300 Subject: [PATCH 23/51] Remove unnecessary Map --- apps/ui/src/config/ecosystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/config/ecosystem.ts b/apps/ui/src/config/ecosystem.ts index 0c8b06057..998becefd 100644 --- a/apps/ui/src/config/ecosystem.ts +++ b/apps/ui/src/config/ecosystem.ts @@ -177,7 +177,7 @@ export const ecosystemList: readonly Ecosystem[] = [ export const ecosystems: ReadonlyRecord = Object.fromEntries( - new Map(ecosystemList.map((ecosystem) => [ecosystem.id, ecosystem])), + ecosystemList.map((ecosystem) => [ecosystem.id, ecosystem]), ) as ReadonlyRecord; export const getEcosystemsForProtocol = ( From e982f53bbebb42668307ee22d9f101bd68959dd8 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 10:19:17 +0300 Subject: [PATCH 24/51] Remove ecosystem icons from connect button --- apps/ui/src/components/ConnectButton.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index 08a93e0b6..df184f47b 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -8,9 +8,6 @@ import { EcosystemId } from "../config"; import { selectConfig } from "../core/selectors"; import { useEnvironment, useWalletAdapter } from "../core/store"; import { useWallets } from "../hooks"; -import BSC_SVG from "../images/ecosystems/bsc.svg"; -import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; -import SOLANA_SVG from "../images/ecosystems/solana.svg"; import type { WalletService } from "../models"; import { deduplicate, isNotNull, shortenAddress } from "../utils"; @@ -116,13 +113,6 @@ export const MultiConnectButton = ({ ) : ( <> - - - - {/* TODO: Consider adding these (OR maybe switch to wallet icons here too?): - - */} -   Connect Connect Wallets From 943300ca13f2d90dc0e2bd3e0ef3bb6b022a30b6 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 10:43:26 +0300 Subject: [PATCH 25/51] Add ecosystem icons to home page --- apps/ui/src/pages/HomePage.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/ui/src/pages/HomePage.tsx b/apps/ui/src/pages/HomePage.tsx index ceb12babc..395a01159 100644 --- a/apps/ui/src/pages/HomePage.tsx +++ b/apps/ui/src/pages/HomePage.tsx @@ -21,6 +21,9 @@ import { InvestorsList } from "../components/InvestorsList"; import { Roadmap } from "../components/Roadmap"; import { useTitle } from "../hooks"; import DIAGRAM from "../images/diagram.svg"; +import BSC_SVG from "../images/ecosystems/bsc.svg"; +import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; +import SOLANA_SVG from "../images/ecosystems/solana.svg"; import DISCORD_SVG from "../images/social/discord.svg"; import TELEGRAM_SVG from "../images/social/telegram.svg"; import TWITTER_SVG from "../images/social/twitter.svg"; @@ -37,6 +40,18 @@ const HomePage = (): ReactElement => { + + + + + + + + + + + +
From 39c258e56e51b61f148a918bbeb5680df9ff288f Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 10:45:00 +0300 Subject: [PATCH 26/51] Rename type in useWalletService --- apps/ui/src/hooks/wallets/useWalletService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index d75acfe30..7fe2487fa 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -8,14 +8,14 @@ import { useEnvironment, useWalletAdapter } from "../../core/store"; import type { WalletService } from "../../models"; import { createAdapter } from "../../models/wallets/services"; -type WalletServiceState = Pick & { +type WalletServiceAPI = Pick & { readonly connectService: ( serviceId: WalletService["id"], protocol: Protocol, ) => Promise; }; -export const useWalletService = (): WalletServiceState => { +export const useWalletService = (): WalletServiceAPI => { const { connectService, disconnectService } = useWalletAdapter(); const { chains } = useEnvironment(selectConfig, shallow); const [{ endpoint }] = chains[Protocol.Solana]; From 7318734de78319222739bee30ac61b7ddf589b05 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 11:51:55 +0300 Subject: [PATCH 27/51] Access useWalletAdapter state via selectors --- apps/ui/src/components/ConnectButton.tsx | 7 +++++-- apps/ui/src/contexts/evmWallet.tsx | 3 ++- apps/ui/src/contexts/solanaWallet.tsx | 3 ++- apps/ui/src/core/selectors/index.ts | 1 + apps/ui/src/core/selectors/walletAdapter.ts | 10 ++++++++++ apps/ui/src/hooks/wallets/useWalletService.ts | 7 +++++-- 6 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 apps/ui/src/core/selectors/walletAdapter.ts diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index df184f47b..f26080bdd 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import shallow from "zustand/shallow.js"; import { EcosystemId } from "../config"; -import { selectConfig } from "../core/selectors"; +import { selectConfig, selectWalletAdapterApi } from "../core/selectors"; import { useEnvironment, useWalletAdapter } from "../core/store"; import { useWallets } from "../hooks"; import type { WalletService } from "../models"; @@ -25,7 +25,10 @@ export const ConnectButton = ({ ...rest }: ConnectButtonProps): ReactElement => { const { ecosystems } = useEnvironment(selectConfig, shallow); - const { disconnectService } = useWalletAdapter(); + const { disconnectService } = useWalletAdapter( + selectWalletAdapterApi, + shallow, + ); if (ecosystemId === EcosystemId.Terra) { throw new Error("Unsupported ecosystem"); } diff --git a/apps/ui/src/contexts/evmWallet.tsx b/apps/ui/src/contexts/evmWallet.tsx index 7b3f2e8ab..983dd9e1d 100644 --- a/apps/ui/src/contexts/evmWallet.tsx +++ b/apps/ui/src/contexts/evmWallet.tsx @@ -5,6 +5,7 @@ import * as React from "react"; import { SingleWalletModal } from "../components/SingleWalletModal"; import type { EvmEcosystemId } from "../config"; import { EcosystemId, Protocol } from "../config"; +import { selectEvmAdapter } from "../core/selectors"; import { useWalletAdapter } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { EvmWalletAdapter, WalletService } from "../models"; @@ -118,7 +119,7 @@ export const EvmWalletProvider = ({ [serviceId, services], ); - const { evm } = useWalletAdapter(); + const evm = useWalletAdapter(selectEvmAdapter); const wallet: EvmWalletAdapter | null = (service && evm) || null; const address = wallet?.address ?? null; diff --git a/apps/ui/src/contexts/solanaWallet.tsx b/apps/ui/src/contexts/solanaWallet.tsx index e90ff2136..dba731864 100644 --- a/apps/ui/src/contexts/solanaWallet.tsx +++ b/apps/ui/src/contexts/solanaWallet.tsx @@ -10,6 +10,7 @@ import { import { SingleWalletModal } from "../components/SingleWalletModal"; import { Protocol } from "../config"; +import { selectSolanaAdapter } from "../core/selectors"; import { useWalletAdapter } from "../core/store"; import { useLocalStorageState } from "../hooks/browser"; import type { @@ -58,7 +59,7 @@ export const SolanaWalletProvider = ({ [serviceId], ); - const { solana } = useWalletAdapter(); + const solana = useWalletAdapter(selectSolanaAdapter); const wallet: SolanaWalletAdapter | null = (service && solana) || null; const address = wallet?.address ?? null; diff --git a/apps/ui/src/core/selectors/index.ts b/apps/ui/src/core/selectors/index.ts index 82f1fccd7..a8fce7d07 100644 --- a/apps/ui/src/core/selectors/index.ts +++ b/apps/ui/src/core/selectors/index.ts @@ -1 +1,2 @@ export * from "./environment"; +export * from "./walletAdapter"; diff --git a/apps/ui/src/core/selectors/walletAdapter.ts b/apps/ui/src/core/selectors/walletAdapter.ts new file mode 100644 index 000000000..eb2c7b69a --- /dev/null +++ b/apps/ui/src/core/selectors/walletAdapter.ts @@ -0,0 +1,10 @@ +import type { WalletAdapterState } from "../store"; + +export const selectWalletAdapterApi = (state: WalletAdapterState) => ({ + connectService: state.connectService, + disconnectService: state.disconnectService, +}); + +export const selectEvmAdapter = (state: WalletAdapterState) => state.evm; + +export const selectSolanaAdapter = (state: WalletAdapterState) => state.solana; diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index 7fe2487fa..5e3d274ee 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react"; import shallow from "zustand/shallow.js"; import { Protocol } from "../../config"; -import { selectConfig } from "../../core/selectors"; +import { selectConfig, selectWalletAdapterApi } from "../../core/selectors"; import type { WalletAdapterState } from "../../core/store"; import { useEnvironment, useWalletAdapter } from "../../core/store"; import type { WalletService } from "../../models"; @@ -16,7 +16,10 @@ type WalletServiceAPI = Pick & { }; export const useWalletService = (): WalletServiceAPI => { - const { connectService, disconnectService } = useWalletAdapter(); + const { connectService, disconnectService } = useWalletAdapter( + selectWalletAdapterApi, + shallow, + ); const { chains } = useEnvironment(selectConfig, shallow); const [{ endpoint }] = chains[Protocol.Solana]; From d0c0e8fb710a6c32772beb3bdd4aa49bc8014296 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 14:35:37 +0300 Subject: [PATCH 28/51] Update ecosystem icons in HomePage --- apps/ui/src/pages/HomePage.tsx | 61 +++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/pages/HomePage.tsx b/apps/ui/src/pages/HomePage.tsx index 395a01159..7e7e7823f 100644 --- a/apps/ui/src/pages/HomePage.tsx +++ b/apps/ui/src/pages/HomePage.tsx @@ -2,11 +2,13 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiHideFor, EuiIcon, EuiImage, EuiPage, EuiPageBody, EuiPanel, + EuiShowFor, EuiSpacer, EuiText, EuiTextColor, @@ -21,8 +23,10 @@ import { InvestorsList } from "../components/InvestorsList"; import { Roadmap } from "../components/Roadmap"; import { useTitle } from "../hooks"; import DIAGRAM from "../images/diagram.svg"; +import AVALANCHE_SVG from "../images/ecosystems/avalanche.svg"; import BSC_SVG from "../images/ecosystems/bsc.svg"; import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; +import POLYGON_SVG from "../images/ecosystems/polygon.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; import DISCORD_SVG from "../images/social/discord.svg"; import TELEGRAM_SVG from "../images/social/telegram.svg"; @@ -40,15 +44,56 @@ const HomePage = (): ReactElement => { - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1bd1c229bba7717615279a36e81caeb55dfbe460 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 14:52:21 +0300 Subject: [PATCH 29/51] Add tests for useWalletAdapter --- .../core/store/tests/useWalletAdapter.test.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 apps/ui/src/core/store/tests/useWalletAdapter.test.ts diff --git a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts new file mode 100644 index 000000000..3e7b08100 --- /dev/null +++ b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts @@ -0,0 +1,124 @@ +import { act, renderHook } from "@testing-library/react-hooks"; + +import { useWalletAdapter } from ".."; +import { Protocol } from "../../../config"; +import { EvmWeb3WalletAdapter } from "../../../models/wallets/adapters/evm"; +import { PhantomAdapter } from "../../../models/wallets/adapters/solana/PhantomAdapter"; +import type { WalletAdapterState } from "../useWalletAdapter"; + +const getProtocolAdapter = (state: WalletAdapterState, protocol: Protocol) => + protocol === Protocol.Evm ? state.evm : state.solana; + +const createWalletAdapter = (protocol: Protocol) => + protocol === Protocol.Evm + ? new EvmWeb3WalletAdapter("serviceName", "serviceUrl", () => null) + : new PhantomAdapter(); + +describe("useWalletAdapter", () => { + it("initially returns empty adapters for EVM and Solana", async () => { + const { result } = renderHook(() => useWalletAdapter()); + expect(result.current.evm).toBeNull(); + expect(result.current.solana).toBeNull(); + }); + + [Protocol.Evm, Protocol.Solana].forEach((protocol) => { + describe(`Protocol ${protocol}`, () => { + it("connects to a service/protocol", async () => { + const { result } = renderHook(() => useWalletAdapter()); + + const service = "metamask"; + const adapter = createWalletAdapter(protocol); + const connectSpy = jest + .spyOn(adapter, "connect") + .mockImplementation(() => Promise.resolve()); + const createAdapter = jest.fn().mockImplementation(() => adapter); + + await act(() => + result.current.connectService(service, protocol, createAdapter), + ); + + expect(getProtocolAdapter(result.current, protocol)).toEqual(adapter); + expect(connectSpy).toHaveBeenCalledTimes(1); + expect(createAdapter).toHaveBeenCalledTimes(1); + expect(createAdapter).toHaveBeenCalledWith(service, protocol); + }); + + it("connects to a service/protocol and disconnects the existing adapter", async () => { + const { result } = renderHook(() => useWalletAdapter()); + + const service = "metamask"; + const adapter = createWalletAdapter(protocol); + jest + .spyOn(adapter, "connect") + .mockImplementation(() => Promise.resolve()); + const disconnectSpy = jest + .spyOn(adapter, "disconnect") + .mockImplementation(() => Promise.resolve()); + const createAdapter = jest.fn().mockImplementation(() => adapter); + + await act(() => + result.current.connectService(service, protocol, createAdapter), + ); + + const secondAdapter = createWalletAdapter(protocol); + jest + .spyOn(adapter, "connect") + .mockImplementation(() => Promise.resolve()); + + const createSecondAdapter = jest + .fn() + .mockImplementation(() => secondAdapter); + + await act(() => + result.current.connectService(service, protocol, createSecondAdapter), + ); + + expect(getProtocolAdapter(result.current, protocol)).toEqual( + secondAdapter, + ); + expect(disconnectSpy).toHaveBeenCalledTimes(1); + }); + + it("disconnects the protocol's adapter", async () => { + const { result } = renderHook(() => useWalletAdapter()); + + const service = "metamask"; + const adapter = createWalletAdapter(protocol); + jest + .spyOn(adapter, "connect") + .mockImplementation(() => Promise.resolve()); + + const disconnectSpy = jest + .spyOn(adapter, "disconnect") + .mockImplementation(() => Promise.resolve()); + const createAdapter = jest.fn().mockImplementation(() => adapter); + + await act(() => + result.current.connectService(service, protocol, createAdapter), + ); + + await act(() => result.current.disconnectService(protocol)); + + expect(getProtocolAdapter(result.current, protocol)).toBeNull(); + expect(disconnectSpy).toHaveBeenCalledTimes(1); + }); + + it("throws if there is no adapter to disconnect", async () => { + const { result } = renderHook(() => useWalletAdapter()); + + await act(async () => { + try { + await result.current.disconnectService(protocol); + } catch (e) { + // eslint-disable-next-line jest/no-conditional-expect + expect(e).toEqual( + Error( + `disconnectService called but no adapter found for protocol ${protocol}`, + ), + ); + } + }); + }); + }); + }); +}); From af0988ab0d1b4ca30d04266f03acb2b82fc24391 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 15:42:28 +0300 Subject: [PATCH 30/51] Add tests for useWalletsMonitor --- .../wallets/tests/useWalletsMonitor.test.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts diff --git a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts new file mode 100644 index 000000000..77552191c --- /dev/null +++ b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts @@ -0,0 +1,98 @@ +import { act, renderHook } from "@testing-library/react-hooks"; + +import { useNotification, useWalletAdapter } from "../../../core/store"; +import { EvmWeb3WalletAdapter } from "../../../models/wallets/adapters/evm"; +import { mockOf } from "../../../testUtils"; +import { useWalletsMonitor } from "../useWalletsMonitor"; + +jest.mock("../../../core/store", () => ({ + useNotification: jest.fn(), + useWalletAdapter: jest.fn(), +})); + +// Make typescript happy with jest +const useNotificationMock = mockOf(useNotification); +const useWalletAdapterMock = mockOf(useWalletAdapter); + +describe("useWalletsMonitor", () => { + it("should notify on every event", () => { + const notify = jest.fn(); + useNotificationMock.mockReturnValue({ notify }); + + const adapter = new EvmWeb3WalletAdapter( + "serviceName", + "serviceUrl", + () => null, + ); + // we use the address in the connect notification + adapter.address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; // eslint-disable-line functional/immutable-data + + useWalletAdapterMock.mockReturnValue({ evm: adapter, solana: null }); + + renderHook(() => useWalletsMonitor()); + + act(() => { + adapter.emit("connect"); + }); + + expect(notify).toBeCalledTimes(1); + expect(notify.mock.calls[0]).toEqual([ + "Wallet update", + "Connected to wallet 0x90F...8c9C1", + "info", + 7000, + ]); + + act(() => { + adapter.emit("disconnect"); + }); + + expect(notify).toBeCalledTimes(2); + expect(notify.mock.calls[1]).toEqual([ + "Wallet update", + "Disconnected from wallet", + "warning", + ]); + + const errorTitle = "error title"; + const errorDescription = "error description"; + + act(() => { + adapter.emit("error", errorTitle, errorDescription); + }); + + expect(notify).toBeCalledTimes(3); + expect(notify.mock.calls[2]).toEqual([ + "error title", + "error description", + "error", + ]); + }); + + it("should not notify when wallet adapter is stale", () => { + const notify = jest.fn(); + useNotificationMock.mockReturnValue({ notify }); + + const adapter = new EvmWeb3WalletAdapter( + "serviceName", + "serviceUrl", + () => null, + ); + // we use the address in the connect notification + adapter.address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; // eslint-disable-line functional/immutable-data + + useWalletAdapterMock.mockReturnValue({ evm: adapter, solana: null }); + + const { rerender } = renderHook(() => useWalletsMonitor()); + + useWalletAdapterMock.mockReturnValue({ evm: null, solana: null }); + + rerender(); + + act(() => { + adapter.emit("disconnect"); + }); + + expect(notify).not.toHaveBeenCalled(); + }); +}); From 601d4d2a4050076da0867fa313e16aa956c1d493 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 16:17:23 +0300 Subject: [PATCH 31/51] Add tests for useWalletService --- .../wallets/tests/useWalletService.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 apps/ui/src/hooks/wallets/tests/useWalletService.test.ts diff --git a/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts new file mode 100644 index 000000000..6833eec52 --- /dev/null +++ b/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts @@ -0,0 +1,84 @@ +import { act, renderHook } from "@testing-library/react-hooks"; + +import { Protocol, configs } from "../../../config"; +import { Env, useEnvironment, useWalletAdapter } from "../../../core/store"; +import { createAdapter } from "../../../models/wallets/services"; +import { mockOf } from "../../../testUtils"; +import { useWalletService } from "../useWalletService"; + +jest.mock("../../../core/store", () => ({ + ...jest.requireActual("../../../core/store"), + useEnvironment: jest.fn(), + useWalletAdapter: jest.fn(), +})); + +jest.mock("../../../models/wallets/services", () => ({ + createAdapter: jest.fn(), +})); + +// Make typescript happy with jest +const useEnvironmentMock = mockOf(useEnvironment); +const useWalletAdapterMock = mockOf(useWalletAdapter); +const createAdapterMock = mockOf(createAdapter); + +describe("useWalletService", () => { + beforeEach(() => { + useEnvironmentMock.mockReturnValue(configs[Env.Localnet]); + }); + + it("should call useWalletAdapter connectService with the correct createAdapter", async () => { + const config = configs[Env.Localnet]; + const [{ endpoint }] = config.chains[Protocol.Solana]; + + const connectServiceMock = jest.fn(); + const disconnectServiceMock = jest.fn(); + useWalletAdapterMock.mockReturnValue({ + connectService: connectServiceMock, + disconnectService: disconnectServiceMock, + }); + const serviceId = "metamask"; + const protocol = Protocol.Evm; + + const { result } = renderHook(() => useWalletService()); + + await act(async () => { + await result.current.connectService(serviceId, protocol); + }); + + expect(connectServiceMock).toBeCalledTimes(1); + expect(connectServiceMock).toBeCalledWith( + serviceId, + protocol, + expect.any(Function), + ); + + const createAdapterFn = connectServiceMock.mock.calls[0][2]; + createAdapterFn("metamask", Protocol.Evm); + + expect(createAdapterMock).toBeCalledTimes(1); + expect(createAdapterMock).toHaveBeenCalledWith( + "metamask", + Protocol.Evm, + endpoint, + ); + }); + + it("should call useWalletAdapter disconnectService", async () => { + const connectServiceMock = jest.fn(); + const disconnectServiceMock = jest.fn(); + useWalletAdapterMock.mockReturnValue({ + connectService: connectServiceMock, + disconnectService: disconnectServiceMock, + }); + const protocol = Protocol.Evm; + + const { result } = renderHook(() => useWalletService()); + + await act(async () => { + await result.current.disconnectService(protocol); + }); + + expect(disconnectServiceMock).toBeCalledTimes(1); + expect(disconnectServiceMock).toBeCalledWith(protocol); + }); +}); From 3c9aada139847eaaba0c75c08aaa917a70df0cdb Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 31 May 2022 16:31:22 +0300 Subject: [PATCH 32/51] Revert unnecessary access modifier change --- apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts b/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts index d14893fcb..718d3650f 100644 --- a/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts +++ b/apps/ui/src/models/wallets/adapters/evm/EvmWalletAdapter.ts @@ -40,8 +40,8 @@ export class EvmWeb3WalletAdapter readonly serviceUrl: string; readonly protocol: Protocol.Evm; address: string | null; - connecting: boolean; private readonly getWalletProvider: () => Web3Provider | null; + private connecting: boolean; constructor( serviceName: string, From 732ee0c0da6e60fcccecdfe0a3c20c9281131a16 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 2 Jun 2022 17:24:16 +0300 Subject: [PATCH 33/51] Capitalize MetaMask --- apps/ui/src/models/wallets/services.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/models/wallets/services.tsx b/apps/ui/src/models/wallets/services.tsx index a0bab094e..be6942e58 100644 --- a/apps/ui/src/models/wallets/services.tsx +++ b/apps/ui/src/models/wallets/services.tsx @@ -87,7 +87,7 @@ const metaMaskInfo: Omit = { icon: METAMASK_ICON, }; -const addMetamaskEcosystemInfo = ( +const addMetaMaskEcosystemInfo = ( info: Omit, ecosystem: Ecosystem, url: string, @@ -114,37 +114,37 @@ const ethereumMetaMaskInfo: WalletServiceInfo = { ecosystem: ecosystems[EcosystemId.Ethereum], }; -const bscMetaMaskInfo = addMetamaskEcosystemInfo( +const bscMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Bsc], "https://academy.binance.com/en/articles/connecting-metamask-to-binance-smart-chain", ); -const avalancheMetaMaskInfo = addMetamaskEcosystemInfo( +const avalancheMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Avalanche], "https://support.avax.network/en/articles/4626956-how-do-i-set-up-metamask-on-avalanche", ); -const polygonMetaMaskInfo = addMetamaskEcosystemInfo( +const polygonMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Polygon], "https://docs.polygon.technology/docs/develop/metamask/config-polygon-on-metamask/", ); -const auroraMetaMaskInfo = addMetamaskEcosystemInfo( +const auroraMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Aurora], "https://doc.aurora.dev/interact/metamask/", ); -const fantomMetaMaskInfo = addMetamaskEcosystemInfo( +const fantomMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Fantom], "https://docs.fantom.foundation/tutorials/set-up-metamask", ); -const karuraMetaMaskInfo = addMetamaskEcosystemInfo( +const karuraMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Karura], "https://evmdocs.acala.network/tooling/metamask/connect-to-the-network", // TODO: Update link when mainnet is live ); -const acalaMetaMaskInfo = addMetamaskEcosystemInfo( +const acalaMetaMaskInfo = addMetaMaskEcosystemInfo( metaMaskInfo, ecosystems[EcosystemId.Acala], "https://evmdocs.acala.network/tooling/metamask/connect-to-the-network", // TODO: Update link when mainnet is live From 5460327714d18f2efe7bc471ff3f3aa73a001c4c Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 2 Jun 2022 18:25:05 +0300 Subject: [PATCH 34/51] Don't throw error if wallet adapter is missing --- .../core/store/tests/useWalletAdapter.test.ts | 17 ----------------- apps/ui/src/core/store/useWalletAdapter.ts | 8 +------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts index 3e7b08100..9f9af746c 100644 --- a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts +++ b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts @@ -102,23 +102,6 @@ describe("useWalletAdapter", () => { expect(getProtocolAdapter(result.current, protocol)).toBeNull(); expect(disconnectSpy).toHaveBeenCalledTimes(1); }); - - it("throws if there is no adapter to disconnect", async () => { - const { result } = renderHook(() => useWalletAdapter()); - - await act(async () => { - try { - await result.current.disconnectService(protocol); - } catch (e) { - // eslint-disable-next-line jest/no-conditional-expect - expect(e).toEqual( - Error( - `disconnectService called but no adapter found for protocol ${protocol}`, - ), - ); - } - }); - }); }); }); }); diff --git a/apps/ui/src/core/store/useWalletAdapter.ts b/apps/ui/src/core/store/useWalletAdapter.ts index 005d72d33..55a4f746c 100644 --- a/apps/ui/src/core/store/useWalletAdapter.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -59,13 +59,7 @@ export const useWalletAdapter = create( disconnectService: async (protocol: Protocol) => { const state = get(); const adapter = protocol === Protocol.Evm ? state.evm : state.solana; - - if (!adapter) - throw new Error( - `disconnectService called but no adapter found for protocol ${protocol}`, - ); - - await adapter.disconnect().catch(console.error); + await adapter?.disconnect().catch(console.error); set( produce((draft) => { From a573c3fa8c956784fff54c12cbaeacf9a2aecb2b Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 2 Jun 2022 18:27:31 +0300 Subject: [PATCH 35/51] Use root models import --- apps/ui/src/core/store/useWalletAdapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/core/store/useWalletAdapter.ts b/apps/ui/src/core/store/useWalletAdapter.ts index 55a4f746c..b3a0cea03 100644 --- a/apps/ui/src/core/store/useWalletAdapter.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -8,7 +8,7 @@ import type { EvmWalletAdapter, SolanaWalletAdapter, WalletAdapter, -} from "../../models/wallets/adapters"; +} from "../../models"; type AdapterFactory = (serviceId: string, protocol: Protocol) => WalletAdapter; From 3ab0e5d4158d10a028b4e9b1bd5b2122bf633ce8 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 2 Jun 2022 19:55:48 +0300 Subject: [PATCH 36/51] Remove unused useMemo --- apps/ui/src/hooks/wallets/useWalletService.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index 5e3d274ee..af083a711 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback } from "react"; import shallow from "zustand/shallow.js"; import { Protocol } from "../../config"; @@ -30,12 +30,9 @@ export const useWalletService = (): WalletServiceAPI => { [endpoint], ); - return useMemo( - () => ({ - connectService: (serviceId: WalletService["id"], protocol: Protocol) => - connectService(serviceId, protocol, createAdapterMemoized), - disconnectService, - }), - [connectService, disconnectService, createAdapterMemoized], - ); + return { + connectService: (serviceId: WalletService["id"], protocol: Protocol) => + connectService(serviceId, protocol, createAdapterMemoized), + disconnectService, + }; }; From 03a7badce8cdb8ef4411351c5465f52e5a40af3d Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 15:53:33 +0300 Subject: [PATCH 37/51] Use root models import --- apps/ui/src/hooks/wallets/useWalletService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index af083a711..937a80b14 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -6,7 +6,7 @@ import { selectConfig, selectWalletAdapterApi } from "../../core/selectors"; import type { WalletAdapterState } from "../../core/store"; import { useEnvironment, useWalletAdapter } from "../../core/store"; import type { WalletService } from "../../models"; -import { createAdapter } from "../../models/wallets/services"; +import { createAdapter } from "../../models"; type WalletServiceAPI = Pick & { readonly connectService: ( From c2fbeb21a8a51f4152d4cd53c2fba12ebef88f43 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 16:17:29 +0300 Subject: [PATCH 38/51] Pass adapter to useWalletAdapter.connectService --- .../core/store/tests/useWalletAdapter.test.ts | 17 ++++------------- apps/ui/src/core/store/useWalletAdapter.ts | 14 +++----------- .../wallets/tests/useWalletService.test.ts | 12 +++--------- apps/ui/src/hooks/wallets/useWalletService.ts | 14 +++++--------- 4 files changed, 15 insertions(+), 42 deletions(-) diff --git a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts index 9f9af746c..15f610c25 100644 --- a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts +++ b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts @@ -31,16 +31,13 @@ describe("useWalletAdapter", () => { const connectSpy = jest .spyOn(adapter, "connect") .mockImplementation(() => Promise.resolve()); - const createAdapter = jest.fn().mockImplementation(() => adapter); await act(() => - result.current.connectService(service, protocol, createAdapter), + result.current.connectService(service, protocol, adapter), ); expect(getProtocolAdapter(result.current, protocol)).toEqual(adapter); expect(connectSpy).toHaveBeenCalledTimes(1); - expect(createAdapter).toHaveBeenCalledTimes(1); - expect(createAdapter).toHaveBeenCalledWith(service, protocol); }); it("connects to a service/protocol and disconnects the existing adapter", async () => { @@ -54,10 +51,9 @@ describe("useWalletAdapter", () => { const disconnectSpy = jest .spyOn(adapter, "disconnect") .mockImplementation(() => Promise.resolve()); - const createAdapter = jest.fn().mockImplementation(() => adapter); await act(() => - result.current.connectService(service, protocol, createAdapter), + result.current.connectService(service, protocol, adapter), ); const secondAdapter = createWalletAdapter(protocol); @@ -65,12 +61,8 @@ describe("useWalletAdapter", () => { .spyOn(adapter, "connect") .mockImplementation(() => Promise.resolve()); - const createSecondAdapter = jest - .fn() - .mockImplementation(() => secondAdapter); - await act(() => - result.current.connectService(service, protocol, createSecondAdapter), + result.current.connectService(service, protocol, secondAdapter), ); expect(getProtocolAdapter(result.current, protocol)).toEqual( @@ -91,10 +83,9 @@ describe("useWalletAdapter", () => { const disconnectSpy = jest .spyOn(adapter, "disconnect") .mockImplementation(() => Promise.resolve()); - const createAdapter = jest.fn().mockImplementation(() => adapter); await act(() => - result.current.connectService(service, protocol, createAdapter), + result.current.connectService(service, protocol, adapter), ); await act(() => result.current.disconnectService(protocol)); diff --git a/apps/ui/src/core/store/useWalletAdapter.ts b/apps/ui/src/core/store/useWalletAdapter.ts index b3a0cea03..0a3e3c3e9 100644 --- a/apps/ui/src/core/store/useWalletAdapter.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -10,15 +10,13 @@ import type { WalletAdapter, } from "../../models"; -type AdapterFactory = (serviceId: string, protocol: Protocol) => WalletAdapter; - export interface WalletAdapterState { readonly evm: EvmWalletAdapter | null; readonly solana: SolanaWalletAdapter | null; readonly connectService: ( serviceId: string, protocol: Protocol, - createAdapter: AdapterFactory, + adapter: WalletAdapter, ) => Promise; readonly disconnectService: (protocol: Protocol) => Promise; } @@ -27,18 +25,12 @@ export const useWalletAdapter = create( (set: SetState, get: GetState) => ({ evm: null, solana: null, - connectService: async ( - serviceId: string, - protocol: Protocol, - createAdapter, - ) => { + connectService: async (serviceId, protocol, adapter) => { const state = get(); const previous = protocol === Protocol.Evm ? state.evm : state.solana; if (previous) await state.disconnectService(protocol); - const adapter = createAdapter(serviceId, protocol); - set( produce((draft) => { switch (adapter.protocol) { @@ -56,7 +48,7 @@ export const useWalletAdapter = create( await adapter.connect().catch(console.error); }, - disconnectService: async (protocol: Protocol) => { + disconnectService: async (protocol) => { const state = get(); const adapter = protocol === Protocol.Evm ? state.evm : state.solana; await adapter?.disconnect().catch(console.error); diff --git a/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts index 6833eec52..fc8815ea0 100644 --- a/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts +++ b/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts @@ -38,6 +38,8 @@ describe("useWalletService", () => { }); const serviceId = "metamask"; const protocol = Protocol.Evm; + const mockAdapter = {}; + createAdapterMock.mockReturnValue(mockAdapter); const { result } = renderHook(() => useWalletService()); @@ -46,15 +48,7 @@ describe("useWalletService", () => { }); expect(connectServiceMock).toBeCalledTimes(1); - expect(connectServiceMock).toBeCalledWith( - serviceId, - protocol, - expect.any(Function), - ); - - const createAdapterFn = connectServiceMock.mock.calls[0][2]; - createAdapterFn("metamask", Protocol.Evm); - + expect(connectServiceMock).toBeCalledWith(serviceId, protocol, mockAdapter); expect(createAdapterMock).toBeCalledTimes(1); expect(createAdapterMock).toHaveBeenCalledWith( "metamask", diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index 937a80b14..7f3ede276 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -1,4 +1,3 @@ -import { useCallback } from "react"; import shallow from "zustand/shallow.js"; import { Protocol } from "../../config"; @@ -23,16 +22,13 @@ export const useWalletService = (): WalletServiceAPI => { const { chains } = useEnvironment(selectConfig, shallow); const [{ endpoint }] = chains[Protocol.Solana]; - const createAdapterMemoized = useCallback( - (serviceId: WalletService["id"], protocol: Protocol) => { - return createAdapter(serviceId, protocol, endpoint); - }, - [endpoint], - ); - return { connectService: (serviceId: WalletService["id"], protocol: Protocol) => - connectService(serviceId, protocol, createAdapterMemoized), + connectService( + serviceId, + protocol, + createAdapter(serviceId, protocol, endpoint), + ), disconnectService, }; }; From 9633352061232ab19605af7b16c56ed14551cc31 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 16:23:01 +0300 Subject: [PATCH 39/51] Fix camel case issue --- apps/ui/src/hooks/wallets/useWalletService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index 7f3ede276..574e30326 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -7,14 +7,14 @@ import { useEnvironment, useWalletAdapter } from "../../core/store"; import type { WalletService } from "../../models"; import { createAdapter } from "../../models"; -type WalletServiceAPI = Pick & { +type WalletServiceApi = Pick & { readonly connectService: ( serviceId: WalletService["id"], protocol: Protocol, ) => Promise; }; -export const useWalletService = (): WalletServiceAPI => { +export const useWalletService = (): WalletServiceApi => { const { connectService, disconnectService } = useWalletAdapter( selectWalletAdapterApi, shallow, From c3cd50f5d9fee19b85080e1b6fdcbce34d86eb0e Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 16:46:24 +0300 Subject: [PATCH 40/51] Disable eslint "functional/immutable-data" in test files --- apps/ui/.eslintrc.json | 5 ++++- apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/ui/.eslintrc.json b/apps/ui/.eslintrc.json index e9521f186..0c4b1d533 100644 --- a/apps/ui/.eslintrc.json +++ b/apps/ui/.eslintrc.json @@ -78,7 +78,10 @@ } }, { - "files": ["src/**/*.stories.@(js|jsx|ts|tsx)"], + "files": [ + "src/**/*.stories.@(js|jsx|ts|tsx)", + "src/**/*.test.@(js|jsx|ts|tsx)" + ], "rules": { "functional/immutable-data": "off" } diff --git a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts index 77552191c..8e114d303 100644 --- a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts +++ b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts @@ -25,7 +25,7 @@ describe("useWalletsMonitor", () => { () => null, ); // we use the address in the connect notification - adapter.address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; // eslint-disable-line functional/immutable-data + adapter.address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; useWalletAdapterMock.mockReturnValue({ evm: adapter, solana: null }); From 93b796c2a2852ee1ecc15afa47e2533cbadfabce Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 16:50:33 +0300 Subject: [PATCH 41/51] Remove unused object modification --- apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts index 8e114d303..e651d4183 100644 --- a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts +++ b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts @@ -78,8 +78,6 @@ describe("useWalletsMonitor", () => { "serviceUrl", () => null, ); - // we use the address in the connect notification - adapter.address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"; // eslint-disable-line functional/immutable-data useWalletAdapterMock.mockReturnValue({ evm: adapter, solana: null }); From c355ccd40b750f7f9093a08694903aaadc217faf Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 16:53:53 +0300 Subject: [PATCH 42/51] Remove unnecessary eslint-disable comment --- apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts b/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts index 06bd48c2c..9040e5cac 100644 --- a/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts +++ b/apps/ui/src/hooks/interaction/useCreateInteractionState.test.ts @@ -21,7 +21,6 @@ import { mockOf, renderHookWithAppContext } from "../../testUtils"; import { useCreateInteractionState } from "./useCreateInteractionState"; -// eslint-disable-next-line functional/immutable-data Object.defineProperty(global.self, "crypto", { value: { getRandomValues: (arr: string | readonly any[]) => From 87f61d59ea4ffe123b989327110e78bf61dddcde Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Fri, 3 Jun 2022 17:00:27 +0300 Subject: [PATCH 43/51] Remove shorten address format from test expectation --- apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts index e651d4183..a9dc5dcbe 100644 --- a/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts +++ b/apps/ui/src/hooks/wallets/tests/useWalletsMonitor.test.ts @@ -38,7 +38,7 @@ describe("useWalletsMonitor", () => { expect(notify).toBeCalledTimes(1); expect(notify.mock.calls[0]).toEqual([ "Wallet update", - "Connected to wallet 0x90F...8c9C1", + expect.stringContaining("Connected to wallet"), "info", 7000, ]); From e1adce9e2a8490a09c962bfd9e7b94662b488425 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 6 Jun 2022 13:06:56 +0300 Subject: [PATCH 44/51] DRY up connect buttons --- apps/ui/src/components/ConnectButton.scss | 24 ---------- apps/ui/src/components/ConnectButton.tsx | 46 ++++++++----------- apps/ui/src/components/MultiWalletModal.scss | 6 +++ apps/ui/src/components/MultiWalletModal.tsx | 18 +++----- .../ui/src/components/PlainConnectButton.scss | 20 ++++++++ apps/ui/src/components/PlainConnectButton.tsx | 44 ++++++++++++++++++ apps/ui/src/pages/TestPage.tsx | 12 ++--- 7 files changed, 100 insertions(+), 70 deletions(-) create mode 100644 apps/ui/src/components/PlainConnectButton.scss create mode 100644 apps/ui/src/components/PlainConnectButton.tsx diff --git a/apps/ui/src/components/ConnectButton.scss b/apps/ui/src/components/ConnectButton.scss index 19dff05ec..3ba8d933e 100644 --- a/apps/ui/src/components/ConnectButton.scss +++ b/apps/ui/src/components/ConnectButton.scss @@ -1,27 +1,3 @@ -@import "../eui"; - -// Used on ConnectButton and on MultiWalletModal -.connect-button { - .exit-icon { - display: none; - margin-left: 5px; - } - - // only used on MultiWalletModal service buttons for visibility - &.connected-service { - border: 1.5px solid $euiColorSuccess; - } - - &.connected:hover { - .exit-icon { - display: inline-block; - } - background-color: $euiColorDanger !important; - color: $euiColorGhost; - border: none; - } -} - .multiConnectButton { img { margin: 0 3px; diff --git a/apps/ui/src/components/ConnectButton.tsx b/apps/ui/src/components/ConnectButton.tsx index f26080bdd..4d417e1d3 100644 --- a/apps/ui/src/components/ConnectButton.tsx +++ b/apps/ui/src/components/ConnectButton.tsx @@ -12,15 +12,19 @@ import type { WalletService } from "../models"; import { deduplicate, isNotNull, shortenAddress } from "../utils"; import { MultiWalletModal } from "./MultiWalletModal"; +import type { PlainConnectButtonProps } from "./PlainConnectButton"; +import { PlainConnectButton } from "./PlainConnectButton"; import "./ConnectButton.scss"; -export interface ConnectButtonProps extends PropsForButton { +export type ConnectButtonProps = Omit< + PlainConnectButtonProps, + "children" | "connected" | "onClick" +> & { readonly ecosystemId: EcosystemId; -} +}; export const ConnectButton = ({ - children, ecosystemId, ...rest }: ConnectButtonProps): ReactElement => { @@ -43,33 +47,23 @@ export const ConnectButton = ({ const handleClick = connected ? disconnect : select; return ( - - - {connected ? ( - children ? ( - children - ) : address ? ( - shortenAddress(address) - ) : ( - "" - ) - ) : ( - <> - Connect - - Connect {ecosystem.displayName} - - - )} - - - + {connected && address ? ( + shortenAddress(address) + ) : ( + <> + Connect + + Connect {ecosystem.displayName} + + + )} + ); }; diff --git a/apps/ui/src/components/MultiWalletModal.scss b/apps/ui/src/components/MultiWalletModal.scss index 827cbaeb9..4bfde171d 100644 --- a/apps/ui/src/components/MultiWalletModal.scss +++ b/apps/ui/src/components/MultiWalletModal.scss @@ -1,6 +1,12 @@ +@import "../eui"; + .walletServiceButton { white-space: nowrap; + .plainConnectButton--connected { + border: 1.5px solid $euiColorSuccess; + } + &__ecosystems { margin: 10px 0 0 30px; diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 5dc95cbba..82786df86 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -27,8 +27,8 @@ import { import { CustomModal } from "./CustomModal"; import { MobileDeviceDisclaimer } from "./MobileDeviceDisclaimer"; +import { PlainConnectButton } from "./PlainConnectButton"; -import "./ConnectButton.scss"; import "./MultiWalletModal.scss"; interface WalletServiceButtonProps { @@ -53,20 +53,16 @@ const WalletServiceButton = ({ } = service; return ( - - - {serviceConnected && address ? shortenAddress(address) : name} - - {helpText && <>{helpText}} - - + {serviceConnected && address ? shortenAddress(address) : name} + {ecosystems.length > 1 && (
    {ecosystems.map((ecosystem) => ( diff --git a/apps/ui/src/components/PlainConnectButton.scss b/apps/ui/src/components/PlainConnectButton.scss new file mode 100644 index 000000000..fb43a0d81 --- /dev/null +++ b/apps/ui/src/components/PlainConnectButton.scss @@ -0,0 +1,20 @@ +@import "../eui"; + +.plainConnectButton { + $self: &; + + &__exitIcon { + display: none; + margin-left: 5px; + } + + &--connected:hover { + background-color: $euiColorDanger !important; + color: $euiColorGhost; + border: none; + + #{ $self }__exitIcon { + display: inline-block; + } + } +} diff --git a/apps/ui/src/components/PlainConnectButton.tsx b/apps/ui/src/components/PlainConnectButton.tsx new file mode 100644 index 000000000..993aff28d --- /dev/null +++ b/apps/ui/src/components/PlainConnectButton.tsx @@ -0,0 +1,44 @@ +import type { + EuiButtonEmpty, + EuiButtonProps, + PropsForButton, +} from "@elastic/eui"; +import { EuiButton, EuiIcon } from "@elastic/eui"; +import type { ReactElement } from "react"; + +import "./PlainConnectButton.scss"; + +export type PlainConnectButtonProps = Pick< + PropsForButton, + "onClick" | "iconType" | "children" | "className" | "fullWidth" | "size" +> & { + readonly color?: "success" | "primary"; + readonly connected: boolean; + readonly helpText?: ReactElement; + readonly ButtonComponent?: typeof EuiButton | typeof EuiButtonEmpty; +}; + +export const PlainConnectButton = ({ + ButtonComponent = EuiButton, + children, + connected, + className, + helpText, + ...rest +}: PlainConnectButtonProps): ReactElement => ( + + {children} + {helpText && <>{helpText}} + + +); diff --git a/apps/ui/src/pages/TestPage.tsx b/apps/ui/src/pages/TestPage.tsx index 5a49dbde1..3bf865581 100644 --- a/apps/ui/src/pages/TestPage.tsx +++ b/apps/ui/src/pages/TestPage.tsx @@ -402,17 +402,11 @@ const TestPage = (): ReactElement => { - - {solanaAddress && shortenAddress(solanaAddress)} - +   - - {ethereumAddress && shortenAddress(ethereumAddress)} - +   - - {bscAddress && shortenAddress(bscAddress)} - +   Notify   From b2b90fdc5c204e6393e96d3cb9ffbb3d5e6f6b74 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 6 Jun 2022 18:47:17 +0300 Subject: [PATCH 45/51] List EVM chains in a popover --- apps/ui/src/components/MultiWalletModal.scss | 15 +- apps/ui/src/components/MultiWalletModal.tsx | 149 +++++++++---------- 2 files changed, 82 insertions(+), 82 deletions(-) diff --git a/apps/ui/src/components/MultiWalletModal.scss b/apps/ui/src/components/MultiWalletModal.scss index 4bfde171d..03651cf95 100644 --- a/apps/ui/src/components/MultiWalletModal.scss +++ b/apps/ui/src/components/MultiWalletModal.scss @@ -1,21 +1,28 @@ @import "../eui"; -.walletServiceButton { - white-space: nowrap; +.protocolWalletOptionsList { + min-width: 180px; + align-items: flex-start; + + .plainConnectButton { + white-space: nowrap; + } .plainConnectButton--connected { border: 1.5px solid $euiColorSuccess; } &__ecosystems { - margin: 10px 0 0 30px; - & > li { margin-bottom: 15px; img { margin-right: 15px; } + + &:last-of-type { + margin-bottom: 0; + } } } } diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 82786df86..62e8ec92a 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -1,18 +1,27 @@ import { EuiButtonEmpty, + EuiButtonIcon, EuiFlexGrid, + EuiFlexGroup, EuiFlexItem, EuiIcon, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, + EuiPopover, EuiSpacer, EuiTitle, } from "@elastic/eui"; import type { ReactElement } from "react"; +import { useState } from "react"; import type { Ecosystem } from "../config"; -import { Protocol, getEcosystemsForProtocol, protocolNames } from "../config"; +import { + Protocol, + ecosystems, + getEcosystemsForProtocol, + protocolNames, +} from "../config"; import { useWalletService, useWallets } from "../hooks"; import EVM_SVG from "../images/ecosystems/ethereum-color.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; @@ -31,52 +40,6 @@ import { PlainConnectButton } from "./PlainConnectButton"; import "./MultiWalletModal.scss"; -interface WalletServiceButtonProps { - readonly service: W; - readonly onClick: () => void; - readonly disconnect: () => void; - readonly serviceConnected: boolean; - readonly address: string | null; - readonly ecosystems: ReadonlyArray; -} - -const WalletServiceButton = ({ - service, - disconnect, - onClick, - serviceConnected, - address, - ecosystems, -}: WalletServiceButtonProps): ReactElement => { - const { - info: { icon, name, helpText }, - } = service; - return ( - - - {serviceConnected && address ? shortenAddress(address) : name} - - {ecosystems.length > 1 && ( -
      - {ecosystems.map((ecosystem) => ( -
    • - - {ecosystem.displayName} -
    • - ))} -
    - )} -
    - ); -}; - interface ProtocolWalletOptionsListProps { readonly icon: string; readonly protocol: Protocol; @@ -86,6 +49,7 @@ const ProtocolWalletOptionsList = ({ icon, protocol, }: ProtocolWalletOptionsListProps): ReactElement => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const wallets = useWallets(); const { connectService, disconnectService } = useWalletService(); const ecosystemIds = getEcosystemsForProtocol(protocol); @@ -119,44 +83,73 @@ const ProtocolWalletOptionsList = ({ }); }; + const handleButtonClick = () => setIsPopoverOpen((prev: boolean) => !prev); + const handlePopoverClose = () => setIsPopoverOpen(false); + + const infoButton = ( + + ); + + const popover = ( + + {ecosystemIds.length > 1 && ( +
      + {ecosystemIds.map((ecosystemId) => ( +
    • + + {ecosystems[ecosystemId].displayName} +
    • + ))} +
    + )} +
    + ); + return ( - +

    {protocolNames[protocol]} + {ecosystemIds.length > 1 ? {popover} : null}

    - {Object.entries(protocolWalletServicesByServiceId).map( - ([serviceId, serviceWalletServices]) => { - const service = findOrThrow( - protocolWalletServices, - (walletService) => walletService.id === serviceId, - ); - - const ecosystems = serviceWalletServices.map( - (walletService) => walletService.info.ecosystem, - ); - - const connectedWallet = - connectedWallets.find( - (wallet) => wallet.service?.id === serviceId, - ) ?? null; - - return ( - disconnect()} - onClick={() => connect(service.id)} - /> - ); - }, - )} + {Object.keys(protocolWalletServicesByServiceId).map((serviceId) => { + const service = findOrThrow( + protocolWalletServices, + (walletService) => walletService.id === serviceId, + ); + + const connectedWallet = + connectedWallets.find((wallet) => wallet.service?.id === serviceId) ?? + null; + + return ( + connect(service.id)} + color={connectedWallet ? "success" : "primary"} + iconType={service.info.icon} + ButtonComponent={EuiButtonEmpty} + connected={!!connectedWallet} + helpText={service.info.helpText} + > + {connectedWallet && connectedWallet.address + ? shortenAddress(connectedWallet.address) + : service.info.name} + + ); + })}
    ); }; @@ -179,7 +172,7 @@ export const MultiWalletModal = ({ {isUserOnMobileDevice() ? : ""} - + Date: Mon, 6 Jun 2022 18:49:32 +0300 Subject: [PATCH 46/51] Remove white EuiPanel from home page chains --- apps/ui/src/pages/HomePage.tsx | 94 +++++++++++++++++----------------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/apps/ui/src/pages/HomePage.tsx b/apps/ui/src/pages/HomePage.tsx index 7e7e7823f..427a38ead 100644 --- a/apps/ui/src/pages/HomePage.tsx +++ b/apps/ui/src/pages/HomePage.tsx @@ -46,54 +46,52 @@ const HomePage = (): ReactElement => { - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 38e7e8fddb381c079420f1a90954b0c40d527820 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 6 Jun 2022 19:04:17 +0300 Subject: [PATCH 47/51] Remove unused imports --- apps/ui/src/components/MultiWalletModal.tsx | 3 --- apps/ui/src/pages/TestPage.tsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index 62e8ec92a..d71c7cbc0 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -2,7 +2,6 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGrid, - EuiFlexGroup, EuiFlexItem, EuiIcon, EuiModalBody, @@ -15,7 +14,6 @@ import { import type { ReactElement } from "react"; import { useState } from "react"; -import type { Ecosystem } from "../config"; import { Protocol, ecosystems, @@ -26,7 +24,6 @@ import { useWalletService, useWallets } from "../hooks"; import EVM_SVG from "../images/ecosystems/ethereum-color.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; import { WALLET_SERVICES } from "../models"; -import type { WalletService } from "../models"; import { findOrThrow, groupBy, diff --git a/apps/ui/src/pages/TestPage.tsx b/apps/ui/src/pages/TestPage.tsx index 3bf865581..84c388034 100644 --- a/apps/ui/src/pages/TestPage.tsx +++ b/apps/ui/src/pages/TestPage.tsx @@ -40,7 +40,7 @@ import { setUpErc20Tokens, setUpSplTokensOnEvm, } from "../models"; -import { shortenAddress, sleep } from "../utils"; +import { sleep } from "../utils"; const SWIM_POOL_FEE_DECIMALS = 6; From 5b60ec8c0d81544f7c7164d19bc67df5f97c3b4e Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Mon, 6 Jun 2022 21:40:35 +0300 Subject: [PATCH 48/51] Remove unused parameter from useWalletAdapter connectService --- .../core/store/tests/useWalletAdapter.test.ts | 19 ++++--------------- apps/ui/src/core/store/useWalletAdapter.ts | 3 +-- .../wallets/tests/useWalletService.test.ts | 2 +- apps/ui/src/hooks/wallets/useWalletService.ts | 6 +----- 4 files changed, 7 insertions(+), 23 deletions(-) diff --git a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts index 15f610c25..f611458bc 100644 --- a/apps/ui/src/core/store/tests/useWalletAdapter.test.ts +++ b/apps/ui/src/core/store/tests/useWalletAdapter.test.ts @@ -26,15 +26,12 @@ describe("useWalletAdapter", () => { it("connects to a service/protocol", async () => { const { result } = renderHook(() => useWalletAdapter()); - const service = "metamask"; const adapter = createWalletAdapter(protocol); const connectSpy = jest .spyOn(adapter, "connect") .mockImplementation(() => Promise.resolve()); - await act(() => - result.current.connectService(service, protocol, adapter), - ); + await act(() => result.current.connectService(protocol, adapter)); expect(getProtocolAdapter(result.current, protocol)).toEqual(adapter); expect(connectSpy).toHaveBeenCalledTimes(1); @@ -43,7 +40,6 @@ describe("useWalletAdapter", () => { it("connects to a service/protocol and disconnects the existing adapter", async () => { const { result } = renderHook(() => useWalletAdapter()); - const service = "metamask"; const adapter = createWalletAdapter(protocol); jest .spyOn(adapter, "connect") @@ -52,18 +48,14 @@ describe("useWalletAdapter", () => { .spyOn(adapter, "disconnect") .mockImplementation(() => Promise.resolve()); - await act(() => - result.current.connectService(service, protocol, adapter), - ); + await act(() => result.current.connectService(protocol, adapter)); const secondAdapter = createWalletAdapter(protocol); jest .spyOn(adapter, "connect") .mockImplementation(() => Promise.resolve()); - await act(() => - result.current.connectService(service, protocol, secondAdapter), - ); + await act(() => result.current.connectService(protocol, secondAdapter)); expect(getProtocolAdapter(result.current, protocol)).toEqual( secondAdapter, @@ -74,7 +66,6 @@ describe("useWalletAdapter", () => { it("disconnects the protocol's adapter", async () => { const { result } = renderHook(() => useWalletAdapter()); - const service = "metamask"; const adapter = createWalletAdapter(protocol); jest .spyOn(adapter, "connect") @@ -84,9 +75,7 @@ describe("useWalletAdapter", () => { .spyOn(adapter, "disconnect") .mockImplementation(() => Promise.resolve()); - await act(() => - result.current.connectService(service, protocol, adapter), - ); + await act(() => result.current.connectService(protocol, adapter)); await act(() => result.current.disconnectService(protocol)); diff --git a/apps/ui/src/core/store/useWalletAdapter.ts b/apps/ui/src/core/store/useWalletAdapter.ts index 0a3e3c3e9..a8ec17b72 100644 --- a/apps/ui/src/core/store/useWalletAdapter.ts +++ b/apps/ui/src/core/store/useWalletAdapter.ts @@ -14,7 +14,6 @@ export interface WalletAdapterState { readonly evm: EvmWalletAdapter | null; readonly solana: SolanaWalletAdapter | null; readonly connectService: ( - serviceId: string, protocol: Protocol, adapter: WalletAdapter, ) => Promise; @@ -25,7 +24,7 @@ export const useWalletAdapter = create( (set: SetState, get: GetState) => ({ evm: null, solana: null, - connectService: async (serviceId, protocol, adapter) => { + connectService: async (protocol, adapter) => { const state = get(); const previous = protocol === Protocol.Evm ? state.evm : state.solana; diff --git a/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts b/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts index fc8815ea0..1005b2a6c 100644 --- a/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts +++ b/apps/ui/src/hooks/wallets/tests/useWalletService.test.ts @@ -48,7 +48,7 @@ describe("useWalletService", () => { }); expect(connectServiceMock).toBeCalledTimes(1); - expect(connectServiceMock).toBeCalledWith(serviceId, protocol, mockAdapter); + expect(connectServiceMock).toBeCalledWith(protocol, mockAdapter); expect(createAdapterMock).toBeCalledTimes(1); expect(createAdapterMock).toHaveBeenCalledWith( "metamask", diff --git a/apps/ui/src/hooks/wallets/useWalletService.ts b/apps/ui/src/hooks/wallets/useWalletService.ts index 574e30326..4f9735663 100644 --- a/apps/ui/src/hooks/wallets/useWalletService.ts +++ b/apps/ui/src/hooks/wallets/useWalletService.ts @@ -24,11 +24,7 @@ export const useWalletService = (): WalletServiceApi => { return { connectService: (serviceId: WalletService["id"], protocol: Protocol) => - connectService( - serviceId, - protocol, - createAdapter(serviceId, protocol, endpoint), - ), + connectService(protocol, createAdapter(serviceId, protocol, endpoint)), disconnectService, }; }; From 8a0d64fd0b515122e5e0186a47d423ed0fbf25db Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 7 Jun 2022 11:22:37 +0300 Subject: [PATCH 49/51] DRY up promoted ecosystems in home page --- apps/ui/src/pages/HomePage.tsx | 75 +++++++++++++--------------------- 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/apps/ui/src/pages/HomePage.tsx b/apps/ui/src/pages/HomePage.tsx index 427a38ead..11ddc5c76 100644 --- a/apps/ui/src/pages/HomePage.tsx +++ b/apps/ui/src/pages/HomePage.tsx @@ -14,20 +14,17 @@ import { EuiTextColor, EuiTitle, } from "@elastic/eui"; -import type { ReactElement } from "react"; +import type { ReactElement, VFC } from "react"; import { useHistory } from "react-router"; import { SwimIconType } from "../components/CustomIconType"; import { GlassPanel } from "../components/GlassPanel"; import { InvestorsList } from "../components/InvestorsList"; import { Roadmap } from "../components/Roadmap"; +import type { Ecosystem } from "../config"; +import { EcosystemId, ecosystems } from "../config"; import { useTitle } from "../hooks"; import DIAGRAM from "../images/diagram.svg"; -import AVALANCHE_SVG from "../images/ecosystems/avalanche.svg"; -import BSC_SVG from "../images/ecosystems/bsc.svg"; -import ETHEREUM_SVG from "../images/ecosystems/ethereum.svg"; -import POLYGON_SVG from "../images/ecosystems/polygon.svg"; -import SOLANA_SVG from "../images/ecosystems/solana.svg"; import DISCORD_SVG from "../images/social/discord.svg"; import TELEGRAM_SVG from "../images/social/telegram.svg"; import TWITTER_SVG from "../images/social/twitter.svg"; @@ -36,6 +33,14 @@ import "./HomePage.scss"; const HomePage = (): ReactElement => { useTitle(""); const history = useHistory(); + const promotedEcosystems = [ + ecosystems[EcosystemId.Solana], + ecosystems[EcosystemId.Ethereum], + ecosystems[EcosystemId.Bsc], + ecosystems[EcosystemId.Avalanche], + ecosystems[EcosystemId.Polygon], + ]; + return ( @@ -51,46 +56,9 @@ const HomePage = (): ReactElement => { wrap={false} responsive={false} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {promotedEcosystems.map((ecosystem) => ( + + ))}
    @@ -611,3 +579,18 @@ const HomePage = (): ReactElement => { }; export default HomePage; + +type PromotedEcosystemProps = { + readonly ecosystem: Ecosystem; +}; + +const PromotedEcosystem: VFC = ({ ecosystem }) => ( + + + + + + + + +); From 18e802423d9780c1bd8677cf7bb9c59a016bcb77 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 7 Jun 2022 11:56:40 +0300 Subject: [PATCH 50/51] Add isEcosystemEnabled helper --- apps/ui/src/config/ecosystem.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/ui/src/config/ecosystem.ts b/apps/ui/src/config/ecosystem.ts index 998becefd..876aa3143 100644 --- a/apps/ui/src/config/ecosystem.ts +++ b/apps/ui/src/config/ecosystem.ts @@ -53,6 +53,27 @@ export const enum EcosystemId { Acala = "acala", } +export const isEcosystemEnabled = (ecosystemId: EcosystemId): boolean => { + switch (ecosystemId) { + case EcosystemId.Solana: + case EcosystemId.Ethereum: + case EcosystemId.Bsc: + case EcosystemId.Avalanche: + case EcosystemId.Polygon: + return true; + case EcosystemId.Aurora: + return !!process.env.REACT_APP_ENABLE_AURORA; + case EcosystemId.Fantom: + return !!process.env.REACT_APP_ENABLE_FANTOM; + case EcosystemId.Karura: + return !!process.env.REACT_APP_ENABLE_KARURA; + case EcosystemId.Acala: + return !!process.env.REACT_APP_ENABLE_ACALA; + default: + return false; + } +}; + export type SolanaEcosystemId = Extract; export type EvmEcosystemId = Extract< From e3d7c31b71e2e917d35446518b0b3fe3ef9a56c2 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 7 Jun 2022 11:57:00 +0300 Subject: [PATCH 51/51] Filter enabled ecosystems in MultiWalletModal --- apps/ui/src/components/MultiWalletModal.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/ui/src/components/MultiWalletModal.tsx b/apps/ui/src/components/MultiWalletModal.tsx index d71c7cbc0..a60638c24 100644 --- a/apps/ui/src/components/MultiWalletModal.tsx +++ b/apps/ui/src/components/MultiWalletModal.tsx @@ -18,6 +18,7 @@ import { Protocol, ecosystems, getEcosystemsForProtocol, + isEcosystemEnabled, protocolNames, } from "../config"; import { useWalletService, useWallets } from "../hooks"; @@ -25,6 +26,7 @@ import EVM_SVG from "../images/ecosystems/ethereum-color.svg"; import SOLANA_SVG from "../images/ecosystems/solana.svg"; import { WALLET_SERVICES } from "../models"; import { + filterMap, findOrThrow, groupBy, isUserOnMobileDevice, @@ -100,12 +102,16 @@ const ProtocolWalletOptionsList = ({ > {ecosystemIds.length > 1 && (
      - {ecosystemIds.map((ecosystemId) => ( -
    • - - {ecosystems[ecosystemId].displayName} -
    • - ))} + {filterMap( + isEcosystemEnabled, + (ecosystemId) => ( +
    • + + {ecosystems[ecosystemId].displayName} +
    • + ), + ecosystemIds, + )}
    )}