@lifi/wallet-management
Version:
LI.FI Wallet Management solution.
369 lines (327 loc) • 11.5 kB
text/typescript
import type { Connector as BigmiConnector } from '@bigmi/client'
import { useConnect as useBigmiConnect } from '@bigmi/react'
import { ChainType } from '@lifi/sdk'
import type { Theme } from '@mui/material'
import { useMediaQuery } from '@mui/material'
import { useWallets } from '@mysten/dapp-kit'
import type { WalletWithRequiredFeatures } from '@mysten/wallet-standard'
import { WalletReadyState } from '@solana/wallet-adapter-base'
import type { Wallet } from '@solana/wallet-adapter-react'
import { useWallet } from '@solana/wallet-adapter-react'
import { useEffect, useState } from 'react'
import type { Connector } from 'wagmi'
import { useConnect } from 'wagmi'
import { defaultBaseAccountConfig } from '../config/baseAccount.js'
import { defaultCoinbaseConfig } from '../config/coinbase.js'
import { defaultMetaMaskConfig } from '../config/metaMask.js'
import { defaultWalletConnectConfig } from '../config/walletConnect.js'
import { createBaseAccountConnector } from '../connectors/baseAccount.js'
import { createCoinbaseConnector } from '../connectors/coinbase.js'
import { createMetaMaskConnector } from '../connectors/metaMask.js'
import { createPortoConnector } from '../connectors/porto.js'
import type { CreateConnectorFnExtended } from '../connectors/types.js'
import { createWalletConnectConnector } from '../connectors/walletConnect.js'
import { useWalletManagementConfig } from '../providers/WalletManagementProvider/WalletManagementContext.js'
import type { WalletConnector } from '../types/walletConnector.js'
import { getConnectorIcon } from '../utils/getConnectorIcon.js'
import { getWalletPriority } from '../utils/getWalletPriority.js'
import { isWalletInstalled } from '../utils/isWalletInstalled.js'
type CombinedWalletConnector = {
connector: WalletConnector
chainType: ChainType
}
export type CombinedWallet = {
id: string
name: string
icon?: string
connectors: CombinedWalletConnector[]
}
type CombinedWallets = {
installedWallets: CombinedWallet[]
notDetectedWallets: CombinedWallet[]
}
const normalizeName = (name: string) => name.split(' ')[0].toLowerCase().trim()
const combineWalletLists = (
utxoConnectorList: BigmiConnector[],
evmConnectorList: (CreateConnectorFnExtended | Connector)[],
svmWalletList: Wallet[],
suiWalletList: WalletWithRequiredFeatures[],
walletEcosystemsOrder?: Record<string, ChainType[]>
): CombinedWallet[] => {
const walletMap = new Map<string, CombinedWallet>()
utxoConnectorList.forEach((utxo) => {
const utxoName = utxo.name
const normalizedName = normalizeName(utxoName)
const existing = walletMap.get(normalizedName) || {
id: utxo.id,
name: utxoName,
icon: getConnectorIcon(utxo as BigmiConnector),
connectors: [] as CombinedWalletConnector[],
}
existing.connectors.push({ connector: utxo, chainType: ChainType.UTXO })
walletMap.set(normalizedName, existing)
})
evmConnectorList.forEach((evm) => {
const evmName =
(evm as CreateConnectorFnExtended)?.displayName ||
(evm as Connector)?.name
const normalizedName = normalizeName(evmName)
const existing = walletMap.get(normalizedName) || {
id: evm.id,
name: evmName,
icon: getConnectorIcon(evm as Connector),
connectors: [] as CombinedWalletConnector[],
}
existing.connectors.push({ connector: evm, chainType: ChainType.EVM })
walletMap.set(normalizedName, existing)
})
svmWalletList.forEach((svm) => {
const normalizedName = normalizeName(svm.adapter.name)
const existing = walletMap.get(normalizedName) || {
id: svm.adapter.name,
name: svm.adapter.name,
icon: svm.adapter.icon,
connectors: [] as CombinedWalletConnector[],
}
existing.connectors.push({
connector: svm.adapter,
chainType: ChainType.SVM,
})
walletMap.set(normalizedName, existing)
})
suiWalletList.forEach((sui) => {
const normalizedName = normalizeName(sui.name)
const existing = walletMap.get(normalizedName) || {
id: sui.name,
name: sui.name,
icon: sui.icon,
connectors: [] as CombinedWalletConnector[],
}
existing.connectors.push({ connector: sui, chainType: ChainType.MVM })
walletMap.set(normalizedName, existing)
})
let combinedWallets = Array.from(walletMap.values())
if (walletEcosystemsOrder) {
combinedWallets = combinedWallets.map((wallet) => {
const order = walletEcosystemsOrder[wallet.name]
if (order) {
return {
...wallet,
connectors: wallet.connectors.sort((a, b) =>
walletEcosystemsComparator(a, b, order)
),
}
}
return wallet
})
}
combinedWallets.sort(walletComparator)
return combinedWallets
}
export const useCombinedWallets = () => {
const walletConfig = useWalletManagementConfig()
const { connectors: wagmiConnectors } = useConnect()
const { connectors: bigmiConnectors } = useBigmiConnect()
const { wallets: solanaWallets } = useWallet()
const suiWallets = useWallets()
const [combinedWallets, setCombinedWallets] = useState<CombinedWallets>(
() => {
return {
installedWallets: [],
notDetectedWallets: [],
}
}
)
const isDesktopView = useMediaQuery((theme: Theme) =>
theme.breakpoints.up('sm')
)
useEffect(() => {
;(async () => {
let evmConnectors: (CreateConnectorFnExtended | Connector)[] = Array.from(
wagmiConnectors
// Remove duplicate connectors
).filter(
(connector, index, self) =>
index === self.findIndex((c) => c.id === connector.id)
)
// Check if Safe connector exists and can get a provider
const safeConnector = evmConnectors.find(
(connector) => connector.id === 'safe'
) as Connector | undefined
let shouldFilterOutSafeConnector = false
if (safeConnector) {
try {
const provider = await safeConnector.getProvider()
// If no provider is available, we should filter out the Safe connector
if (!provider) {
shouldFilterOutSafeConnector = true
}
} catch {
// If getting provider fails, filter out the Safe connector
shouldFilterOutSafeConnector = true
}
}
if (shouldFilterOutSafeConnector) {
evmConnectors = evmConnectors.filter(
(connector) => connector.id !== 'safe'
)
}
// Ensure standard connectors are included
if (
!evmConnectors.some((connector) =>
connector.id.toLowerCase().includes('walletconnect')
)
) {
evmConnectors.unshift(
createWalletConnectConnector(
walletConfig?.walletConnect ?? defaultWalletConnectConfig
)
)
}
if (
!evmConnectors.some((connector) =>
connector.id.toLowerCase().includes('coinbase')
)
) {
evmConnectors.unshift(
createCoinbaseConnector(
walletConfig?.coinbase ?? defaultCoinbaseConfig
)
)
}
if (
!evmConnectors.some((connector) =>
connector.id.toLowerCase().includes('metamask')
)
) {
evmConnectors.unshift(
createMetaMaskConnector(
walletConfig?.metaMask ?? defaultMetaMaskConfig
)
)
}
if (
!evmConnectors.some((connector) =>
connector.id.toLowerCase().includes('baseaccount')
)
) {
evmConnectors.unshift(
createBaseAccountConnector(
walletConfig?.baseAccount ?? defaultBaseAccountConfig
)
)
}
if (
!evmConnectors.some((connector) =>
connector.id.toLowerCase().includes('porto')
)
) {
evmConnectors.unshift(createPortoConnector(walletConfig?.porto))
}
const includeEcosystem = (chainType: ChainType) =>
!walletConfig.enabledChainTypes ||
walletConfig.enabledChainTypes.includes(chainType)
const installedUTXOConnectors = includeEcosystem(ChainType.UTXO)
? bigmiConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
return isInstalled
})
: []
const installedEVMConnectors = includeEcosystem(ChainType.EVM)
? evmConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
return isInstalled
})
: []
const installedSVMWallets = includeEcosystem(ChainType.SVM)
? solanaWallets.filter((wallet) => {
const isInstalled =
wallet.adapter.readyState === WalletReadyState.Installed ||
wallet.adapter.readyState === WalletReadyState.Loadable
return isInstalled
})
: []
const installedSuiWallets = includeEcosystem(ChainType.MVM)
? suiWallets
: []
const installedCombinedWallets = combineWalletLists(
installedUTXOConnectors,
installedEVMConnectors,
installedSVMWallets,
installedSuiWallets,
walletConfig.walletEcosystemsOrder
)
const notDetectedUTXOConnectors = bigmiConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
return !isInstalled && isDesktopView
})
const notDetectedEVMConnectors = evmConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
return !isInstalled && isDesktopView
})
const notDetectedSVMWallets = solanaWallets.filter((wallet) => {
const isInstalled =
wallet.adapter.readyState === WalletReadyState.Installed ||
wallet.adapter.readyState === WalletReadyState.Loadable
return !isInstalled && isDesktopView
})
const notDetectedCombinedWallets = combineWalletLists(
notDetectedUTXOConnectors,
notDetectedEVMConnectors,
notDetectedSVMWallets,
[]
)
installedCombinedWallets.sort(walletComparator)
notDetectedCombinedWallets.sort(walletComparator)
setCombinedWallets({
installedWallets: installedCombinedWallets,
notDetectedWallets: notDetectedCombinedWallets,
})
})()
}, [
bigmiConnectors,
isDesktopView,
solanaWallets,
suiWallets,
wagmiConnectors,
walletConfig,
])
return combinedWallets
}
// Ensure the walletComparator function is updated to handle CombinedWallet
const walletComparator = (a: CombinedWallet, b: CombinedWallet) => {
const priorityA = getWalletPriority(a.id)
const priorityB = getWalletPriority(b.id)
if (priorityA !== priorityB) {
return priorityA - priorityB
}
return a.id?.localeCompare(b.id)
}
const walletEcosystemsComparator = (
a: CombinedWalletConnector,
b: CombinedWalletConnector,
order: ChainType[]
) => {
if (!order.length) {
return 0
}
const ecosystemA = a.chainType
const ecosystemB = b.chainType
if (ecosystemA === ecosystemB) {
return 0
}
const indexA = order.indexOf(ecosystemA)
const indexB = order.indexOf(ecosystemB)
// If both are in the order list, sort by their position
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB
}
// If only one is in the order list, prioritize it
if (indexA !== -1) {
return -1
}
if (indexB !== -1) {
return 1
}
return 0
}