make lnc work

This commit is contained in:
keyan 2024-07-18 18:56:49 -05:00
parent 5d03e08514
commit a0c1d4f602
6 changed files with 165 additions and 130 deletions

View File

@ -131,7 +131,7 @@ export const useWalletPayment = () => {
const wallet = useWallet() const wallet = useWallet()
const waitForWalletPayment = useCallback(async ({ id, bolt11 }, waitFor) => { const waitForWalletPayment = useCallback(async ({ id, bolt11 }, waitFor) => {
if (!wallet) { if (!wallet?.enabled) {
throw new NoAttachedWalletError() throw new NoAttachedWalletError()
} }
try { try {

View File

@ -44,7 +44,7 @@ export default function WalletSettings () {
<Form <Form
initial={initial} initial={initial}
schema={schema} schema={schema}
onSubmit={async (values) => { onSubmit={async ({ amount, ...values }) => {
try { try {
const newConfig = !wallet.isConfigured const newConfig = !wallet.isConfigured
@ -99,37 +99,42 @@ export default function WalletSettings () {
) )
} }
function WalletFields ({ wallet: { config, fields } }) { function WalletFields ({ wallet: { config, fields, isConfigured } }) {
return fields.map(({ name, label, type, help, optional, ...props }, i) => { return fields
const rawProps = { .map(({ name, label, type, help, optional, editable, ...props }, i) => {
...props, const rawProps = {
name, ...props,
initialValue: config?.[name], name,
label: ( initialValue: config?.[name],
<div className='d-flex align-items-center'> readOnly: isConfigured && editable === false,
{label} groupClassName: props.hidden ? 'd-none' : undefined,
{/* help can be a string or object to customize the label */} label: label
{help && ( ? (
<Info label={help.label || 'help'}> <div className='d-flex align-items-center'>
<Text>{help.text || help}</Text> {label}
</Info> {/* help can be a string or object to customize the label */}
)} {help && (
{optional && ( <Info label={help.label || 'help'}>
<small className='text-muted ms-2'> <Text>{help.text || help}</Text>
{typeof optional === 'boolean' ? 'optional' : <Text>{optional}</Text>} </Info>
</small> )}
)} {optional && (
</div> <small className='text-muted ms-2'>
), {typeof optional === 'boolean' ? 'optional' : <Text>{optional}</Text>}
required: !optional, </small>
autoFocus: i === 0 )}
} </div>
if (type === 'text') { )
return <ClientInput key={i} {...rawProps} /> : undefined,
} required: !optional,
if (type === 'password') { autoFocus: i === 0
return <PasswordInput key={i} {...rawProps} newPass /> }
} if (type === 'text') {
return null return <ClientInput key={i} {...rawProps} />
}) }
if (type === 'password') {
return <PasswordInput key={i} {...rawProps} newPass />
}
return null
})
} }

View File

@ -73,8 +73,8 @@ export function useWallet (name) {
// validate should log custom INFO and OK message // validate should log custom INFO and OK message
// validate is optional since validation might happen during save on server // validate is optional since validation might happen during save on server
// TODO: add timeout // TODO: add timeout
await wallet.validate?.(newConfig, { me, logger }) const validConfig = await wallet.validate?.(newConfig, { me, logger })
await saveConfig(newConfig) await saveConfig(validConfig ?? newConfig)
logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached') logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached')
} catch (err) { } catch (err) {
const message = err.message || err.toString?.() const message = err.message || err.toString?.()

View File

@ -1,86 +1,64 @@
import CancelButton from '@/components/cancel-button'
import { Form, PasswordInput, SubmitButton } from '@/components/form'
import { InvoiceCanceledError, InvoiceExpiredError } from '@/components/payment' import { InvoiceCanceledError, InvoiceExpiredError } from '@/components/payment'
import { bolt11Tags } from '@/lib/bolt11' import { bolt11Tags } from '@/lib/bolt11'
import LNC from '@lightninglabs/lnc-web'
import { Mutex } from 'async-mutex' import { Mutex } from 'async-mutex'
import { Status } from 'wallets'
export * from 'wallets/lnc' export * from 'wallets/lnc'
const XXX_DEFAULT_PASSWORD = 'password' async function disconnect (lnc, logger) {
if (lnc) {
try {
lnc.disconnect()
logger.info('disconnecting...')
// 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)
}
}
}
export async function validate ({ pairingPhrase, password }, { me, logger }) { export async function validate (credentials, { me, logger }) {
const lnc = await getLNC({ me }) let lnc
try { try {
lnc.credentials.pairingPhrase = pairingPhrase lnc = await getLNC(credentials)
logger.info('connecting ...') logger.info('connecting ...')
await lnc.connect() await lnc.connect()
logger.ok('connected') logger.ok('connected')
logger.info('validating permissions ...') logger.info('validating permissions ...')
await validateNarrowPerms(lnc) await validateNarrowPerms(lnc)
logger.ok('permissions ok') logger.ok('permissions ok')
lnc.credentials.password = password || XXX_DEFAULT_PASSWORD
return lnc.credentials.credentials
} finally { } finally {
lnc.disconnect() await disconnect(lnc, logger)
} }
} }
const mutex = new Mutex() const mutex = new Mutex()
async function unlock ({ password }, { lnc, status, showModal, logger }) { export async function sendPayment (bolt11, credentials, { me, status, logger }) {
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 unlocked')
onClose()
resolve(values.password)
} catch (err) {
logger.error('failed 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>
)
}
)
})
}
// FIXME: pass me, status, showModal in useWallet hook
export async function sendPayment (bolt11, { pairingPhrase, password: configuredPassword }, { me, status, showModal, logger }) {
const hash = bolt11Tags(bolt11).payment_hash const hash = bolt11Tags(bolt11).payment_hash
return await mutex.runExclusive(async () => { return await mutex.runExclusive(async () => {
let lnc let lnc
try { try {
lnc = await getLNC({ me }) lnc = await getLNC(credentials)
// TODO: pass status, showModal to unlock
const password = await unlock({ password: configuredPassword }, { lnc, status, showModal, logger })
// credentials need to be decrypted before connecting after a disconnect
lnc.credentials.password = password || XXX_DEFAULT_PASSWORD
await lnc.connect() await lnc.connect()
const { paymentError, paymentPreimage: preimage } = const { paymentError, paymentPreimage: preimage } =
await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 }) await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
@ -99,36 +77,16 @@ export async function sendPayment (bolt11, { pairingPhrase, password: configured
} }
throw err throw err
} finally { } finally {
try { await disconnect(lnc, logger)
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 }) { async function getLNC (credentials = {}) {
if (window.lnc) return window.lnc const { default: { default: LNC } } = await import('@lightninglabs/lnc-web')
window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined }) return new LNC({
return window.lnc credentialStore: new LncCredentialStore({ ...credentials, serverHost: 'mailbox.terminal.lightning.today:443' })
})
} }
function validateNarrowPerms (lnc) { function validateNarrowPerms (lnc) {
@ -141,3 +99,63 @@ function validateNarrowPerms (lnc) {
// TODO: need to check for more narrow permissions // TODO: need to check for more narrow permissions
// blocked by https://github.com/lightninglabs/lnc-web/issues/112 // blocked by https://github.com/lightninglabs/lnc-web/issues/112
} }
// default credential store can go fuck itself
class LncCredentialStore {
credentials = {
localKey: '',
remoteKey: '',
pairingPhrase: '',
serverHost: ''
}
constructor (credentials = {}) {
this.credentials = { ...this.credentials, ...credentials }
}
get password () {
return ''
}
set password (password) { }
get serverHost () {
return this.credentials.serverHost
}
set serverHost (host) {
this.credentials.serverHost = host
}
get pairingPhrase () {
return this.credentials.pairingPhrase
}
set pairingPhrase (phrase) {
this.credentials.pairingPhrase = phrase
}
get localKey () {
return this.credentials.localKey
}
set localKey (key) {
this.credentials.localKey = key
}
get remoteKey () {
return this.credentials.remoteKey
}
set remoteKey (key) {
this.credentials.remoteKey = key
}
get isPaired () {
return !!this.credentials.remoteKey || !!this.credentials.pairingPhrase
}
clear () {
this.credentials = {}
}
}

View File

@ -12,19 +12,31 @@ export const fields = [
words: bip39Words, words: bip39Words,
min: 2, min: 2,
max: 10 max: 10
} },
editable: false
}, },
{ {
name: 'password', name: 'localKey',
label: 'password', type: 'text',
type: 'password', optional: true,
hint: 'encrypts your pairing phrase when stored locally', hidden: true
optional: true },
{
name: 'remoteKey',
type: 'text',
optional: true,
hidden: true
},
{
name: 'serverHost',
type: 'text',
optional: true,
hidden: true
} }
] ]
export const card = { export const card = {
title: 'LNC', title: 'LNC',
subtitle: 'use Lightning Node Connect for LND payments', subtitle: 'use Lightning Node Connect for LND payments',
badges: ['send only', 'non-custodialish', 'budgetable'] badges: ['send only', 'non-custodial', 'budgetable']
} }

View File

@ -18,7 +18,7 @@ export async function validate ({ nwcUrl }, { logger }) {
logger.ok(`connected to ${relayUrl}`) logger.ok(`connected to ${relayUrl}`)
try { try {
return await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
let found = false let found = false
const sub = relay.subscribe([ const sub = relay.subscribe([
{ {