import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useWalletLogger } from '../logger' import { Status, migrateLocalStorage } from '.' import { bolt11Tags } from '@/lib/bolt11' import useModal from '../modal' import { Form, PasswordInput, SubmitButton } from '../form' import CancelButton from '../cancel-button' import { Mutex } from 'async-mutex' import { Wallet } from '@/lib/constants' import { useMe } from '../me' import { InvoiceCanceledError, InvoiceExpiredError } from '@/components/payment' const LNCContext = createContext() const mutex = new Mutex() async function getLNC ({ me }) { if (window.lnc) return window.lnc const { default: LNC } = await import('@lightninglabs/lnc-web') // backwards compatibility: migrate to new storage key if (me) migrateLocalStorage('lnc-web:default', `lnc-web:stacker:${me.id}`) window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined }) return window.lnc } // default password if the user hasn't set one export const XXX_DEFAULT_PASSWORD = 'password' function validateNarrowPerms (lnc) { if (!lnc.hasPerms('lnrpc.Lightning.SendPaymentSync')) { throw new Error('missing permission: lnrpc.Lightning.SendPaymentSync') } if (lnc.hasPerms('lnrpc.Lightning.SendCoins')) { throw new Error('too broad permission: lnrpc.Wallet.SendCoins') } // TODO: need to check for more narrow permissions // blocked by https://github.com/lightninglabs/lnc-web/issues/112 } export function LNCProvider ({ children }) { const me = useMe() const { logger } = useWalletLogger(Wallet.LNC) const [config, setConfig] = useState({}) const [lnc, setLNC] = useState() const [status, setStatus] = useState() const [modal, showModal] = useModal() const getInfo = useCallback(async () => { logger.info('getInfo called') return await lnc.lightning.getInfo() }, [logger, lnc]) const unlock = useCallback(async (connect) => { if (status === Status.Enabled) return config.password return await new Promise((resolve, reject) => { const cancelAndReject = async () => { reject(new Error('password canceled')) } showModal(onClose => { return (
{ try { lnc.credentials.password = values?.password setStatus(Status.Enabled) setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, password: values.password }) logger.ok('wallet enabled') onClose() resolve(values.password) } catch (err) { logger.error('failed attempt to unlock wallet', err) throw err } }} >

Unlock LNC

{ onClose(); cancelAndReject() }} /> unlock
) }, { onClose: cancelAndReject }) }) }, [logger, showModal, setConfig, lnc, status]) const sendPayment = useCallback(async (bolt11) => { const hash = bolt11Tags(bolt11).payment_hash logger.info('sending payment:', `payment_hash=${hash}`) return await mutex.runExclusive(async () => { try { const password = await unlock() // credentials need to be decrypted before connecting after a disconnect lnc.credentials.password = password || XXX_DEFAULT_PASSWORD await lnc.connect() const { paymentError, paymentPreimage: preimage } = await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 }) if (paymentError) throw new Error(paymentError) if (!preimage) throw new Error('No preimage in response') logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`) return { preimage } } catch (err) { const msg = err.message || err.toString?.() logger.error('payment failed:', `payment_hash=${hash}`, msg) if (msg.includes('invoice expired')) { throw new InvoiceExpiredError(hash) } if (msg.includes('canceled')) { throw new InvoiceCanceledError(hash) } throw err } finally { try { lnc.disconnect() logger.info('disconnecting after:', `payment_hash=${hash}`) // wait for lnc to disconnect before releasing the mutex await new Promise((resolve, reject) => { let counter = 0 const interval = setInterval(() => { if (lnc.isConnected) { if (counter++ > 100) { logger.error('failed to disconnect from lnc') clearInterval(interval) reject(new Error('failed to disconnect from lnc')) } return } clearInterval(interval) resolve() }) }, 50) } catch (err) { logger.error('failed to disconnect from lnc', err) } } }) }, [logger, lnc, unlock]) const saveConfig = useCallback(async config => { setConfig(config) try { lnc.credentials.pairingPhrase = config.pairingPhrase await lnc.connect() await validateNarrowPerms(lnc) lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD setStatus(Status.Enabled) logger.ok('wallet enabled') } catch (err) { logger.error('invalid config:', err) setStatus(Status.Error) logger.info('wallet disabled') throw err } finally { lnc.disconnect() } }, [logger, lnc]) const clearConfig = useCallback(async () => { await lnc.credentials.clear(false) if (lnc.isConnected) lnc.disconnect() setStatus(undefined) setConfig({}) logger.info('cleared config') }, [logger, lnc]) useEffect(() => { (async () => { try { const lnc = await getLNC({ me }) setLNC(lnc) setStatus(Status.Initialized) if (lnc.credentials.isPaired) { try { // try the default password lnc.credentials.password = XXX_DEFAULT_PASSWORD } catch (err) { setStatus(Status.Locked) logger.info('wallet needs password before enabling') return } setStatus(Status.Enabled) setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, password: lnc.credentials.password }) } } catch (err) { logger.error('wallet could not be loaded:', err) setStatus(Status.Error) } })() }, [me, setStatus, setConfig, logger]) const value = useMemo( () => ({ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }), [status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig]) return ( {children} {modal} ) } export function useLNC () { return useContext(LNCContext) }