* Inject wallet logger interface * Include method in NWC logs * Fix wrong page total * Poll for new logs every second * Fix overlapping pagination * Remove unused total * Better logs for incoming payments * Use _setLogs instead of wrapper * Remove inconsistent receive log * Remove console.log from wallet logger on server * Fix missing 'wallet detached' log * Fix confirm_withdrawl code * Remove duplicate autowithdrawal log * Add context to log * Add more context * Better table styling * Move CSS for wallet logs into one file * remove unused logNav class * rename classes * Align key with second column * Fix TypeError if context empty * Check content-type header before calling res.json() * Fix duplicate 'failed to create invoice' * Parse details from LND error * Fix invalid DOM property 'colspan' * P2P zap logs with context * Remove unnecessary withdrawal error log * the code assignment was broken anyway * we already log withdrawal errors using .catch on payViaPaymentRequest * Don't show outgoing fee to receiver to avoid confusion * Fix typo in comment * Log if invoice was canceled by payer * Automatically populate context from bolt11 * Fix missing context * Fix wrap errors not logged * Only log cancel if client canceled * Remove unused imports * Log withdrawal/forward success/error in payment flow * Fix boss not passed to checkInvoice * Fix TypeError * Fix database timeouts caused by logger The logger shares the same connection pool with any currently running transaction. This means that we enter a classic deadlock when we await logger calls: the logger call is waiting for a connection but the currently running transaction is waiting for the logger call to finish before it can release a connection. * Fix cache returning undefined * Fix typo in comment * Add padding-right to key in log context * Always use 'incoming payment failed:' --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
		
			
				
	
	
		
			251 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { useMe } from '@/components/me'
 | |
| import { SET_WALLET_PRIORITY, WALLETS } from '@/fragments/wallet'
 | |
| import { SSR } from '@/lib/constants'
 | |
| import { useApolloClient, useMutation, useQuery } from '@apollo/client'
 | |
| import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
 | |
| import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common'
 | |
| import useVault from '@/components/vault/use-vault'
 | |
| import { useWalletLogger } from '@/components/wallet-logger'
 | |
| import { decode as bolt11Decode } from 'bolt11'
 | |
| import walletDefs from 'wallets/client'
 | |
| import { generateMutation } from './graphql'
 | |
| import { formatSats } from '@/lib/format'
 | |
| 
 | |
| const WalletsContext = createContext({
 | |
|   wallets: []
 | |
| })
 | |
| 
 | |
| function useLocalWallets () {
 | |
|   const { me } = useMe()
 | |
|   const [wallets, setWallets] = useState([])
 | |
| 
 | |
|   const loadWallets = useCallback(() => {
 | |
|     // form wallets from local storage into a list of { config, def }
 | |
|     const wallets = walletDefs.map(w => {
 | |
|       try {
 | |
|         const storageKey = getStorageKey(w.name, me?.id)
 | |
|         const config = window.localStorage.getItem(storageKey)
 | |
|         return { def: w, config: JSON.parse(config) }
 | |
|       } catch (e) {
 | |
|         return null
 | |
|       }
 | |
|     }).filter(Boolean)
 | |
|     setWallets(wallets)
 | |
|   }, [me?.id, setWallets])
 | |
| 
 | |
|   const removeWallets = useCallback(() => {
 | |
|     for (const wallet of wallets) {
 | |
|       const storageKey = getStorageKey(wallet.def.name, me?.id)
 | |
|       window.localStorage.removeItem(storageKey)
 | |
|     }
 | |
|     setWallets([])
 | |
|   }, [wallets, setWallets, me?.id])
 | |
| 
 | |
|   useEffect(() => {
 | |
|     // listen for changes to any wallet config in local storage
 | |
|     // from any window with the same origin
 | |
|     const handleStorage = (event) => {
 | |
|       if (event.key.startsWith(getStorageKey(''))) {
 | |
|         loadWallets()
 | |
|       }
 | |
|     }
 | |
|     window.addEventListener('storage', handleStorage)
 | |
| 
 | |
|     loadWallets()
 | |
|     return () => window.removeEventListener('storage', handleStorage)
 | |
|   }, [loadWallets])
 | |
| 
 | |
|   return { wallets, reloadLocalWallets: loadWallets, removeLocalWallets: removeWallets }
 | |
| }
 | |
| 
 | |
| const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
 | |
| 
 | |
| export function WalletsProvider ({ children }) {
 | |
|   const { isActive, decrypt } = useVault()
 | |
|   const { me } = useMe()
 | |
|   const { wallets: localWallets, reloadLocalWallets, removeLocalWallets } = useLocalWallets()
 | |
|   const [setWalletPriority] = useMutation(SET_WALLET_PRIORITY)
 | |
|   const [serverWallets, setServerWallets] = useState([])
 | |
|   const client = useApolloClient()
 | |
| 
 | |
|   const { data, refetch } = useQuery(WALLETS,
 | |
|     SSR ? {} : { nextFetchPolicy: 'cache-and-network' })
 | |
| 
 | |
|   // refetch wallets when the vault key hash changes or wallets are updated
 | |
|   useEffect(() => {
 | |
|     if (me?.privates?.walletsUpdatedAt) {
 | |
|       refetch()
 | |
|     }
 | |
|   }, [me?.privates?.walletsUpdatedAt, me?.privates?.vaultKeyHash, refetch])
 | |
| 
 | |
|   useEffect(() => {
 | |
|     const loadWallets = async () => {
 | |
|       if (!data?.wallets) return
 | |
|       // form wallets into a list of { config, def }
 | |
|       const wallets = []
 | |
|       for (const w of data.wallets) {
 | |
|         const def = getWalletByType(w.type)
 | |
|         const { vaultEntries, ...config } = w
 | |
|         if (isActive) {
 | |
|           for (const { key, iv, value } of vaultEntries) {
 | |
|             try {
 | |
|               config[key] = await decrypt({ iv, value })
 | |
|             } catch (e) {
 | |
|               console.error('error decrypting vault entry', e)
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // the specific wallet config on the server is stored in wallet.wallet
 | |
|         // on the client, it's stored unnested
 | |
|         wallets.push({ config: { ...config, ...w.wallet }, def, vaultEntries })
 | |
|       }
 | |
| 
 | |
|       setServerWallets(wallets)
 | |
|     }
 | |
|     loadWallets()
 | |
|   }, [data?.wallets, decrypt, isActive])
 | |
| 
 | |
|   // merge wallets on name like: { ...unconfigured, ...localConfig, ...serverConfig }
 | |
|   const wallets = useMemo(() => {
 | |
|     const merged = {}
 | |
|     for (const wallet of [...walletDefsOnly, ...localWallets, ...serverWallets]) {
 | |
|       merged[wallet.def.name] = {
 | |
|         def: wallet.def,
 | |
|         config: {
 | |
|           ...merged[wallet.def.name]?.config,
 | |
|           ...Object.fromEntries(
 | |
|             Object.entries(wallet.config ?? {}).map(([key, value]) => [
 | |
|               key,
 | |
|               value ?? merged[wallet.def.name]?.config?.[key]
 | |
|             ])
 | |
|           )
 | |
|         },
 | |
|         vaultEntries: wallet.vaultEntries
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // sort by priority, then add status field
 | |
|     return Object.values(merged)
 | |
|       .sort(walletPrioritySort)
 | |
|       .map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled }))
 | |
|   }, [serverWallets, localWallets])
 | |
| 
 | |
|   const settings = useMemo(() => {
 | |
|     return {
 | |
|       autoWithdrawMaxFeePercent: me?.privates?.autoWithdrawMaxFeePercent,
 | |
|       autoWithdrawThreshold: me?.privates?.autoWithdrawThreshold,
 | |
|       autoWithdrawMaxFeeTotal: me?.privates?.autoWithdrawMaxFeeTotal
 | |
|     }
 | |
|   }, [me?.privates?.autoWithdrawMaxFeePercent, me?.privates?.autoWithdrawThreshold, me?.privates?.autoWithdrawMaxFeeTotal])
 | |
| 
 | |
|   // whenever the vault key is set, and we have local wallets,
 | |
|   // we'll send any merged local wallets to the server, and delete them from local storage
 | |
|   const syncLocalWallets = useCallback(async encrypt => {
 | |
|     const walletsToSync = wallets.filter(w =>
 | |
|       // only sync wallets that have a local config
 | |
|       localWallets.some(localWallet => localWallet.def.name === w.def.name && !!localWallet.config)
 | |
|     )
 | |
|     if (encrypt && walletsToSync.length > 0) {
 | |
|       for (const wallet of walletsToSync) {
 | |
|         const mutation = generateMutation(wallet.def)
 | |
|         const append = {}
 | |
|         // if the wallet has server-only fields set, add the settings to the mutation variables
 | |
|         if (wallet.def.fields.some(f => f.serverOnly && wallet.config[f.name])) {
 | |
|           append.settings = settings
 | |
|         }
 | |
|         const variables = await upsertWalletVariables(wallet, encrypt, append)
 | |
|         await client.mutate({ mutation, variables })
 | |
|       }
 | |
|       removeLocalWallets()
 | |
|     }
 | |
|   }, [wallets, localWallets, removeLocalWallets, settings])
 | |
| 
 | |
|   const unsyncLocalWallets = useCallback(() => {
 | |
|     for (const wallet of wallets) {
 | |
|       const { clientWithShared } = siftConfig(wallet.def.fields, wallet.config)
 | |
|       if (canSend({ def: wallet.def, config: clientWithShared })) {
 | |
|         saveWalletLocally(wallet.def.name, clientWithShared, me?.id)
 | |
|       }
 | |
|     }
 | |
|     reloadLocalWallets()
 | |
|   }, [wallets, me?.id, reloadLocalWallets])
 | |
| 
 | |
|   const setPriorities = useCallback(async (priorities) => {
 | |
|     for (const { wallet, priority } of priorities) {
 | |
|       if (!isConfigured(wallet)) {
 | |
|         throw new Error(`cannot set priority for unconfigured wallet: ${wallet.def.name}`)
 | |
|       }
 | |
| 
 | |
|       if (wallet.config?.id) {
 | |
|         // set priority on server if it has an id
 | |
|         await setWalletPriority({ variables: { id: wallet.config.id, priority } })
 | |
|       } else {
 | |
|         const storageKey = getStorageKey(wallet.def.name, me?.id)
 | |
|         const config = window.localStorage.getItem(storageKey)
 | |
|         const newConfig = { ...JSON.parse(config), priority }
 | |
|         window.localStorage.setItem(storageKey, JSON.stringify(newConfig))
 | |
|       }
 | |
|     }
 | |
|     // reload local wallets if any priorities were set
 | |
|     if (priorities.length > 0) {
 | |
|       reloadLocalWallets()
 | |
|     }
 | |
|   }, [setWalletPriority, me?.id, reloadLocalWallets])
 | |
| 
 | |
|   // provides priority sorted wallets to children, a function to reload local wallets,
 | |
|   // and a function to set priorities
 | |
|   return (
 | |
|     <WalletsContext.Provider
 | |
|       value={{
 | |
|         wallets,
 | |
|         reloadLocalWallets,
 | |
|         setPriorities,
 | |
|         onVaultKeySet: syncLocalWallets,
 | |
|         beforeDisconnectVault: unsyncLocalWallets,
 | |
|         removeLocalWallets
 | |
|       }}
 | |
|     >
 | |
|       {children}
 | |
|     </WalletsContext.Provider>
 | |
|   )
 | |
| }
 | |
| 
 | |
| export function useWallets () {
 | |
|   return useContext(WalletsContext)
 | |
| }
 | |
| 
 | |
| export function useWallet (name) {
 | |
|   const { wallets } = useWallets()
 | |
| 
 | |
|   const wallet = useMemo(() => {
 | |
|     if (name) {
 | |
|       return wallets.find(w => w.def.name === name)
 | |
|     }
 | |
| 
 | |
|     // return the first enabled wallet that is available and can send
 | |
|     return wallets
 | |
|       .filter(w => !w.def.isAvailable || w.def.isAvailable())
 | |
|       .filter(w => w.config?.enabled && canSend(w))[0]
 | |
|   }, [wallets, name])
 | |
| 
 | |
|   const { logger } = useWalletLogger(wallet?.def)
 | |
| 
 | |
|   const sendPayment = useCallback(async (bolt11) => {
 | |
|     const decoded = bolt11Decode(bolt11)
 | |
|     logger.info(`↗ sending payment: ${formatSats(decoded.satoshis)}`, { bolt11 })
 | |
|     try {
 | |
|       const preimage = await wallet.def.sendPayment(bolt11, wallet.config, { logger })
 | |
|       logger.ok(`↗ payment sent: ${formatSats(decoded.satoshis)}`, { bolt11, preimage })
 | |
|     } catch (err) {
 | |
|       const message = err.message || err.toString?.()
 | |
|       logger.error(`payment failed: ${message}`, { bolt11 })
 | |
|       throw err
 | |
|     }
 | |
|   }, [wallet, logger])
 | |
| 
 | |
|   if (!wallet) return null
 | |
| 
 | |
|   return { ...wallet, sendPayment }
 | |
| }
 |