mango-v4-ui/components/wallet/WalletProvider.tsx

331 lines
9.1 KiB
TypeScript
Raw Normal View History

2022-07-26 18:30:51 -07:00
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'
2022-07-26 18:47:11 -07:00
import { useLocalStorageStringState } from '../../hooks/useLocalStorageState'
2022-07-26 18:30:51 -07:00
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<WalletProviderProps> = ({
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<Transaction> => {
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<Transaction[]> => {
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<Uint8Array> => {
if (!connected) throw handleError(new WalletNotConnectedError())
return await adapter.signMessage(message)
}
: undefined,
[adapter, handleError, connected]
)
return (
<WalletContext.Provider
value={{
autoConnect,
wallets,
wallet,
publicKey,
connected,
connecting,
disconnecting,
select: setName,
connect,
disconnect,
sendTransaction,
signTransaction,
signAllTransactions,
signMessage,
}}
>
{children}
</WalletContext.Provider>
)
}