2024-06-24 10:58:22 +00:00
import bip39Words from '@/lib/bip39-words'
import LNC from '@lightninglabs/lnc-web'
import { Mutex } from 'async-mutex'
import { string , array , object } from 'yup'
import { Form , PasswordInput , SubmitButton } from '@/components/form'
import CancelButton from '@/components/cancel-button'
import { InvoiceCanceledError , InvoiceExpiredError } from '@/components/payment'
import { bolt11Tags } from '@/lib/bolt11'
import { Status } from '@/components/wallet'
export const name = 'lnc'
export const fields = [
{
name : 'pairingPhrase' ,
label : 'pairing phrase' ,
type : 'password' ,
help : 'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance <budget>```\n\n```$ litcli sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.'
} ,
{
name : 'password' ,
label : 'password' ,
type : 'password' ,
hint : 'encrypts your pairing phrase when stored locally' ,
optional : true
}
]
export const card = {
title : 'LNC' ,
subtitle : 'use Lightning Node Connect for LND payments' ,
badges : [ 'send only' , 'non-custodialish' , 'budgetable' ]
}
const XXX _DEFAULT _PASSWORD = 'password'
export async function validate ( { me , logger , pairingPhrase , password } ) {
const lnc = await getLNC ( { me } )
try {
lnc . credentials . pairingPhrase = pairingPhrase
2024-06-24 22:58:01 +00:00
logger . info ( 'connecting ...' )
// FIXME: this fails with this error:
// Cannot assign to read only property 'undefined' of object '#<Window>'
2024-06-24 10:58:22 +00:00
await lnc . connect ( )
2024-06-24 22:58:01 +00:00
logger . ok ( 'connected' )
logger . info ( 'validating permissions ...' )
2024-06-24 10:58:22 +00:00
await validateNarrowPerms ( lnc )
2024-06-24 22:58:01 +00:00
logger . ok ( 'permissions ok' )
2024-06-24 10:58:22 +00:00
lnc . credentials . password = password || XXX _DEFAULT _PASSWORD
2024-06-24 22:58:01 +00:00
logger . info ( 'getting lightning info ...' )
await lnc . lightning . getInfo ( )
logger . ok ( 'info received' )
2024-06-24 10:58:22 +00:00
} finally {
2024-06-24 22:58:01 +00:00
// FIXME: this fails with this error:
// Cannot read properties of undefined (reading 'wasmClientDisconnect')
// uncommented because it shadows the error from lnc.connect()
// lnc.disconnect()
2024-06-24 10:58:22 +00:00
}
}
export const schema = object ( {
pairingPhrase : array ( )
. transform ( function ( value , originalValue ) {
if ( this . isType ( value ) && value !== null ) {
return value
}
return originalValue ? originalValue . split ( /[\s]+/ ) : [ ]
} )
. of ( string ( ) . trim ( ) . oneOf ( bip39Words , ( { value } ) => ` ' ${ value } ' is not a valid pairing phrase word ` ) )
. min ( 2 , 'needs at least two words' )
. max ( 10 , 'max 10 words' )
. required ( 'required' ) ,
password : string ( )
} )
const mutex = new Mutex ( )
2024-06-24 22:58:01 +00:00
async function unlock ( { lnc , password , status , showModal , logger } ) {
2024-06-24 10:58:22 +00:00
if ( status === Status . Enabled ) return 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
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 < / h 4 >
< PasswordInput
label = 'password'
name = 'password'
/ >
< div className = 'mt-5 d-flex justify-content-between' >
< CancelButton onClick = { ( ) => { onClose ( ) ; cancelAndReject ( ) } } / >
< SubmitButton variant = 'primary' > unlock < / S u b m i t B u t t o n >
< / d i v >
< / F o r m >
)
}
)
} )
}
2024-06-24 22:58:01 +00:00
// FIXME: pass me, status, showModal in useWallet hook
export async function sendPayment ( { bolt11 , pairingPhrase , password : configuredPassword , me , status , showModal , logger } ) {
2024-06-24 10:58:22 +00:00
const hash = bolt11Tags ( bolt11 ) . payment _hash
return await mutex . runExclusive ( async ( ) => {
let lnc
try {
2024-06-24 22:58:01 +00:00
lnc = await getLNC ( { me } )
// TODO: pass status, showModal to unlock
const password = await unlock ( { lnc , password : configuredPassword , status , showModal , logger } )
2024-06-24 10:58:22 +00:00
// 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' )
return { preimage }
} catch ( err ) {
const msg = err . message || err . toString ? . ( )
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 )
}
}
} )
}
function getLNC ( { me } ) {
if ( window . lnc ) return window . lnc
window . lnc = new LNC ( { namespace : me ? . id ? ` stacker: ${ me . id } ` : undefined } )
return window . lnc
}
2024-06-24 22:58:01 +00:00
function validateNarrowPerms ( lnc ) {
2024-06-24 10:58:22 +00:00
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
}