import React, { createContext, forwardRef, useCallback, useContext, useState, useEffect, } from 'react'; import { Just, Nothing } from 'folktale/maybe'; import { includes, noop } from 'lodash'; import { randomHex } from 'web3-utils'; import { walletFromMnemonic } from 'lib/wallet'; import { DEFAULT_HD_PATH, NONCUSTODIAL_WALLETS, WALLET_TYPES, } from 'lib/constants'; import { getAuthToken } from 'lib/authToken'; import { BRIDGE_ERROR } from 'lib/error'; import { useNetwork } from 'store/network'; import { publicToAddress } from 'lib/utils/address'; const initialContext = { // walletType: Nothing(), setWalletType: noop, // walletHdPath: '', setWalletHdPath: noop, // wallet: Nothing(), setWallet: noop, // urbitWallet: Nothing(), setUrbitWallet: noop, authMnemonic: Nothing(), setAuthMnemonic: noop, // networkSeed: Nothing(), setNetworkSeed: noop, networkRevision: Nothing(), setNetworkRevision: noop, // resetWallet: noop, // authToken: Nothing(), setAuthToken: noop, useLegacyTokenSigning: Nothing(), setUseLegacyTokenSigning: noop, skipLoginSigning: false, setSkipLoginSigning: noop, setFakeToken: noop, } export const WalletContext = createContext(initialContext); const DEFAULT_WALLET_TYPE = WALLET_TYPES.TICKET; // NB (jtobin): // // Note that the 'wallet' prop has type depending on the 'walletType' prop. // These could be bound together in a single structure (so that // 'walletType' acted more explicitly as a data constructor of sorts) but // it doesn't necessarily help us much, since we branch on 'walletType' // before setting 'wallet'. // // Wallets are always wrapped in For most authentication // mechanisms, Maybe wraps a BIP32 HD wallet; for Ethereum private keys, // JSON keystore files, and Metamask authentication, it wraps an // 'EthereumWallet'. function _useWallet(initialWallet = Nothing(), initialMnemonic = Nothing()) { const [walletType, _setWalletType] = useState(() => initialMnemonic.matchWith({ Nothing: () => DEFAULT_WALLET_TYPE, Just: () => WALLET_TYPES.MNEMONIC, }) ); const [walletHdPath, setWalletHdPath] = useState(DEFAULT_HD_PATH); const [wallet, _setWallet] = useState(initialWallet); const [urbitWallet, _setUrbitWallet] = useState(Nothing()); const [authMnemonic, setAuthMnemonic] = useState(initialMnemonic); const [networkSeed, setNetworkSeed] = useState(Nothing()); const [networkRevision, setNetworkRevision] = useState(Nothing()); const [authToken, setAuthToken] = useState(Nothing()); // Allow users to skip signing the auth token on login const [skipLoginSigning, setSkipLoginSigning] = useState(false); const setFakeToken = () => { setAuthToken(Just(randomHex(32))); }; // See: https://github.com/urbit/bridge/issues/549#issuecomment-1048359617 // This is used for legacy compatibility; this flow should eventually be // be removed after ~1 year. const [useLegacyTokenSigning, setUseLegacyTokenSigning] = useState(false); const { web3 } = useNetwork(); useEffect(() => { (async () => { if ( !Just.hasInstance(wallet) || !Just.hasInstance(web3) || NONCUSTODIAL_WALLETS.has(walletType) ) { return; } const _wallet = wallet.value; const _web3 = web3.value; if (skipLoginSigning) { setFakeToken(); return; } const token = await getAuthToken({ wallet: _wallet, walletType, walletHdPath, web3: _web3, useLegacyTokenSigning, }); setAuthToken(Just(token)); })(); }, [ wallet, walletType, walletHdPath, web3, useLegacyTokenSigning, setAuthToken, skipLoginSigning, ]); const setWalletType = useCallback( walletType => { if (!includes(WALLET_TYPES, walletType)) { throw new Error(BRIDGE_ERROR.INVALID_WALLET_TYPE); } _setWalletType(walletType); }, [_setWalletType] ); const setWallet = useCallback( wallet => { // force that public addresses are derived for each wallet // NOTE wallet is Maybe<> and .map is Maybe#map wallet.map(wal => { wal.address = wal.address || publicToAddress(wal.publicKey); return wal; }); _setWallet(wallet); }, [_setWallet] ); const setUrbitWallet = useCallback( urbitWallet => { if (Just.hasInstance(urbitWallet)) { // when an urbit wallet is set, also derive // a normal bip32 wallet using the ownership address const wallet = walletFromMnemonic( urbitWallet.value.ownership.seed, DEFAULT_HD_PATH, urbitWallet.value.meta.passphrase ); setWallet(wallet); } else { setWallet(Nothing()); } _setUrbitWallet(urbitWallet); }, [setWallet] ); const resetWallet = useCallback(() => { _setWalletType(DEFAULT_WALLET_TYPE); setWalletHdPath(DEFAULT_HD_PATH); _setWallet(Nothing()); _setUrbitWallet(Nothing()); setAuthMnemonic(Nothing()); setNetworkSeed(Nothing()); setNetworkRevision(Nothing()); setUseLegacyTokenSigning(false); }, [ _setWalletType, setWalletHdPath, _setWallet, _setUrbitWallet, setAuthMnemonic, setNetworkSeed, setNetworkRevision, ]); return { // walletType, setWalletType, // walletHdPath, setWalletHdPath, // wallet, setWallet, // urbitWallet, setUrbitWallet, authMnemonic, setAuthMnemonic, // networkSeed, setNetworkSeed, networkRevision, setNetworkRevision, // resetWallet, // authToken, setAuthToken, useLegacyTokenSigning, setUseLegacyTokenSigning, skipLoginSigning, setSkipLoginSigning, setFakeToken, }; } export function WalletProvider({ initialWallet, initialMnemonic, children }) { const wallet = _useWallet(initialWallet, initialMnemonic); return ( {children} ); } // Hook version export function useWallet() { return useContext(WalletContext); } // HOC version export const withWallet = Component => forwardRef((props, ref) => ( {wallet => } ));