import { Adapter, SendTransactionOptions, WalletError, WalletNotConnectedError, WalletNotReadyError, WalletReadyState, } from '@solana/wallet-adapter-base' import { Connection, PublicKey, Transaction } from '@solana/web3.js' import React, { FC, ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from 'react' import { Wallet, WalletContext } from '@solana/wallet-adapter-react' import { useLocalStorageStringState } from '../../hooks/useLocalStorageState' export interface WalletProviderProps { children: ReactNode wallets: Adapter[] autoConnect?: boolean onError?: (error: WalletError) => void localStorageKey?: string } class WalletNotSelectedError extends WalletError { name = 'WalletNotSelectedError' } const initialState: { wallet: Wallet | null adapter: Adapter | null publicKey: PublicKey | null connected: boolean } = { wallet: null, adapter: null, publicKey: null, connected: false, } /* * This is a refactored version of the Solana Labs wallet. * For Mango's use case we needed to remove the clearing of the wallet after disconnect and errors. * Original: https://github.com/solana-labs/wallet-adapter/blob/master/packages/core/react/src/WalletProvider.tsx */ export const WalletProvider: FC = ({ children, wallets: adapters, autoConnect = false, onError, localStorageKey = 'walletName', }) => { const [name, setName] = useLocalStorageStringState(localStorageKey, null) const [{ wallet, adapter, publicKey, connected }, setState] = useState(initialState) const readyState = adapter?.readyState || WalletReadyState.Unsupported const [connecting, setConnecting] = useState(false) const [disconnecting, setDisconnecting] = useState(false) const isConnecting = useRef(false) const isDisconnecting = useRef(false) const isUnloading = useRef(false) // Wrap adapters to conform to the `Wallet` interface const [wallets, setWallets] = useState(() => adapters.map((adapter) => ({ adapter, readyState: adapter.readyState, })) ) // When the wallets change, start to listen for changes to their `readyState` useEffect(() => { function handleReadyStateChange( this: Adapter, readyState: WalletReadyState ) { setWallets((prevWallets) => { const walletIndex = prevWallets.findIndex( ({ adapter }) => adapter.name === this.name ) if (walletIndex === -1) return prevWallets return [ ...prevWallets.slice(0, walletIndex), { ...prevWallets[walletIndex], readyState }, ...prevWallets.slice(walletIndex + 1), ] }) } for (const adapter of adapters) { adapter.on('readyStateChange', handleReadyStateChange, adapter) } return () => { for (const adapter of adapters) { adapter.off('readyStateChange', handleReadyStateChange, adapter) } } }, [adapters]) // When the selected wallet changes, initialize the state useEffect(() => { const wallet = wallets.find(({ adapter }) => adapter.name === name) if (wallet) { setState({ wallet, adapter: wallet.adapter, connected: wallet.adapter.connected, publicKey: wallet.adapter.publicKey, }) } else { setState(initialState) } }, [name, wallets]) // If autoConnect is enabled, try to connect when the adapter changes and is ready useEffect(() => { if ( isConnecting.current || connecting || connected || !autoConnect || !adapter || !( readyState === WalletReadyState.Installed || readyState === WalletReadyState.Loadable ) ) return ;(async function () { isConnecting.current = true setConnecting(true) try { await adapter.connect() } catch (error: any) { // Clear the selected wallet // Don't throw error, but handleError will still be called } finally { setConnecting(false) isConnecting.current = false } })() }, [isConnecting, connecting, connected, autoConnect, adapter, readyState]) // If the window is closing or reloading, ignore disconnect and error events from the adapter useEffect(() => { function listener() { isUnloading.current = true } window.addEventListener('beforeunload', listener) return () => window.removeEventListener('beforeunload', listener) }, [isUnloading]) // Handle the adapter's connect event const handleConnect = useCallback(() => { if (!adapter) return setState((state) => ({ ...state, connected: adapter.connected, publicKey: adapter.publicKey, })) }, [adapter]) // Handle the adapter's disconnect event const handleDisconnect = useCallback(() => { setState((state) => ({ ...state, connected: false, publicKey: null, })) }, []) // Handle the adapter's error event, and local errors const handleError = useCallback( (error: WalletError) => { // Call onError unless the window is unloading if (!isUnloading.current) (onError || console.error)(error) return error }, [isUnloading, onError] ) // Setup and teardown event listeners when the adapter changes useEffect(() => { if (adapter) { adapter.on('connect', handleConnect) adapter.on('disconnect', handleDisconnect) adapter.on('error', handleError) return () => { adapter.off('connect', handleConnect) adapter.off('disconnect', handleDisconnect) adapter.off('error', handleError) } } }, [adapter, handleConnect, handleDisconnect, handleError]) // When the adapter changes, disconnect the old one useEffect(() => { return () => { adapter?.disconnect() } }, [adapter]) // Connect the adapter to the wallet const connect = useCallback(async () => { if (isConnecting.current || connecting || disconnecting || connected) return if (!adapter) throw handleError(new WalletNotSelectedError()) if ( !( readyState === WalletReadyState.Installed || readyState === WalletReadyState.Loadable ) ) { if (typeof window !== 'undefined') { window.open(adapter.url, '_blank') } throw handleError(new WalletNotReadyError()) } isConnecting.current = true setConnecting(true) await adapter.connect() setConnecting(false) isConnecting.current = false }, [ isConnecting, connecting, disconnecting, connected, adapter, readyState, handleError, ]) // Disconnect the adapter from the wallet const disconnect = useCallback(async () => { if (isDisconnecting.current || disconnecting) return if (!adapter) return isDisconnecting.current = true setDisconnecting(true) try { await adapter.disconnect() } catch (error: any) { setDisconnecting(false) isDisconnecting.current = false throw error } }, [isDisconnecting, disconnecting, adapter]) // Send a transaction using the provided connection const sendTransaction = useCallback( async ( transaction: Transaction, connection: Connection, options?: SendTransactionOptions ) => { if (!adapter) throw handleError(new WalletNotSelectedError()) if (!connected) throw handleError(new WalletNotConnectedError()) return await adapter.sendTransaction(transaction, connection, options) }, [adapter, handleError, connected] ) // Sign a transaction if the wallet supports it const signTransaction = useMemo( () => adapter && 'signTransaction' in adapter ? async (transaction: Transaction): Promise => { if (!connected) throw handleError(new WalletNotConnectedError()) return await adapter.signTransaction(transaction) } : undefined, [adapter, handleError, connected] ) // Sign multiple transactions if the wallet supports it const signAllTransactions = useMemo( () => adapter && 'signAllTransactions' in adapter ? async (transactions: Transaction[]): Promise => { if (!connected) throw handleError(new WalletNotConnectedError()) return await adapter.signAllTransactions(transactions) } : undefined, [adapter, handleError, connected] ) // Sign an arbitrary message if the wallet supports it const signMessage = useMemo( () => adapter && 'signMessage' in adapter ? async (message: Uint8Array): Promise => { if (!connected) throw handleError(new WalletNotConnectedError()) return await adapter.signMessage(message) } : undefined, [adapter, handleError, connected] ) return ( {children} ) }