stacker.news/components/webln/lnc.js

216 lines
7.2 KiB
JavaScript
Raw Normal View History

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'
Frontend payment UX cleanup (#1194) * Replace useInvoiceable with usePayment hook * Show WebLnError in QR code fallback * Fix missing removal of old zap undo code * Fix payment timeout message * Fix unused arg in super() * Also bail if invoice expired * Fix revert on reply error * Use JIT_INVOICE_TIMEOUT_MS constant * Remove unnecessary PaymentContext * Fix me as a dependency in FeeButtonContext * Fix anon sats added before act success * Optimistic updates for zaps * Fix modal not closed after custom zap * Optimistic update for custom zaps * Optimistic update for bounty payments * Consistent error handling for zaps and bounty payments * Optimistic update for poll votes * Use var balance in payment.request * Rename invoiceable to prepaid * Log cancelled invoices * Client notifications We now show notifications that are stored on the client to inform the user about following errors in the prepaid payment flow: - if a payment fails - if an invoice expires before it is paid - if a payment was interrupted (for example via page refresh) - if the action fails after payment * Remove unnecessary passing of act * Use AbortController for zap undos * Fix anon zap update not updating bolt color * Fix zap counted towards anon sats even if logged in * Fix duplicate onComplete call * Fix downzap type error * Fix "missing field 'path' while writing result" error * Pass full item in downzap props The previous commit fixed cache updates for downzaps but then the cache update for custom zaps failed because 'path' wasn't included in the server response. This commit is the proper fix. * Parse lnc rpc error messages * Add hash to InvoiceExpiredError
2024-05-28 17:18:54 +00:00
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()
2024-05-03 19:49:31 +00:00
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])
2024-04-28 01:00:54 +00:00
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 (
<Form
initial={{
password: ''
}}
onSubmit={async (values) => {
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
}
}}
>
<h4 className='text-center mb-3'>Unlock LNC</h4>
<PasswordInput
label='password'
name='password'
/>
<div className='mt-5 d-flex justify-content-between'>
<CancelButton onClick={() => { onClose(); cancelAndReject() }} />
<SubmitButton variant='primary'>unlock</SubmitButton>
</div>
</Form>
)
}, { 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 {
2024-04-28 01:00:54 +00:00
const password = await unlock()
// credentials need to be decrypted before connecting after a disconnect
2024-04-28 01:00:54 +00:00
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) {
Frontend payment UX cleanup (#1194) * Replace useInvoiceable with usePayment hook * Show WebLnError in QR code fallback * Fix missing removal of old zap undo code * Fix payment timeout message * Fix unused arg in super() * Also bail if invoice expired * Fix revert on reply error * Use JIT_INVOICE_TIMEOUT_MS constant * Remove unnecessary PaymentContext * Fix me as a dependency in FeeButtonContext * Fix anon sats added before act success * Optimistic updates for zaps * Fix modal not closed after custom zap * Optimistic update for custom zaps * Optimistic update for bounty payments * Consistent error handling for zaps and bounty payments * Optimistic update for poll votes * Use var balance in payment.request * Rename invoiceable to prepaid * Log cancelled invoices * Client notifications We now show notifications that are stored on the client to inform the user about following errors in the prepaid payment flow: - if a payment fails - if an invoice expires before it is paid - if a payment was interrupted (for example via page refresh) - if the action fails after payment * Remove unnecessary passing of act * Use AbortController for zap undos * Fix anon zap update not updating bolt color * Fix zap counted towards anon sats even if logged in * Fix duplicate onComplete call * Fix downzap type error * Fix "missing field 'path' while writing result" error * Pass full item in downzap props The previous commit fixed cache updates for downzaps but then the cache update for custom zaps failed because 'path' wasn't included in the server response. This commit is the proper fix. * Parse lnc rpc error messages * Add hash to InvoiceExpiredError
2024-05-28 17:18:54 +00:00
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)
}
}
})
2024-04-28 01:00:54 +00:00
}, [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)
2024-04-27 23:30:03 +00:00
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 (
<LNCContext.Provider value={value}>
{children}
{modal}
</LNCContext.Provider>
)
}
export function useLNC () {
return useContext(LNCContext)
}