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 waitForWalletPayment = useCallback(async ({ id, bolt11 }, waitFor) => {
if (!wallet) {
if (!wallet?.enabled) {
throw new NoAttachedWalletError()
}
try {

View File

@ -44,7 +44,7 @@ export default function WalletSettings () {
<Form
initial={initial}
schema={schema}
onSubmit={async (values) => {
onSubmit={async ({ amount, ...values }) => {
try {
const newConfig = !wallet.isConfigured
@ -99,37 +99,42 @@ export default function WalletSettings () {
)
}
function WalletFields ({ wallet: { config, fields } }) {
return fields.map(({ name, label, type, help, optional, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
label: (
<div className='d-flex align-items-center'>
{label}
{/* help can be a string or object to customize the label */}
{help && (
<Info label={help.label || 'help'}>
<Text>{help.text || help}</Text>
</Info>
)}
{optional && (
<small className='text-muted ms-2'>
{typeof optional === 'boolean' ? 'optional' : <Text>{optional}</Text>}
</small>
)}
</div>
),
required: !optional,
autoFocus: i === 0
}
if (type === 'text') {
return <ClientInput key={i} {...rawProps} />
}
if (type === 'password') {
return <PasswordInput key={i} {...rawProps} newPass />
}
return null
})
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
return fields
.map(({ name, label, type, help, optional, editable, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
readOnly: isConfigured && editable === false,
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (
<div className='d-flex align-items-center'>
{label}
{/* help can be a string or object to customize the label */}
{help && (
<Info label={help.label || 'help'}>
<Text>{help.text || help}</Text>
</Info>
)}
{optional && (
<small className='text-muted ms-2'>
{typeof optional === 'boolean' ? 'optional' : <Text>{optional}</Text>}
</small>
)}
</div>
)
: undefined,
required: !optional,
autoFocus: i === 0
}
if (type === 'text') {
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 is optional since validation might happen during save on server
// TODO: add timeout
await wallet.validate?.(newConfig, { me, logger })
await saveConfig(newConfig)
const validConfig = await wallet.validate?.(newConfig, { me, logger })
await saveConfig(validConfig ?? newConfig)
logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached')
} catch (err) {
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 { bolt11Tags } from '@/lib/bolt11'
import LNC from '@lightninglabs/lnc-web'
import { Mutex } from 'async-mutex'
import { Status } from 'wallets'
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 }) {
const lnc = await getLNC({ me })
export async function validate (credentials, { me, logger }) {
let lnc
try {
lnc.credentials.pairingPhrase = pairingPhrase
lnc = await getLNC(credentials)
logger.info('connecting ...')
await lnc.connect()
logger.ok('connected')
logger.info('validating permissions ...')
await validateNarrowPerms(lnc)
logger.ok('permissions ok')
lnc.credentials.password = password || XXX_DEFAULT_PASSWORD
return lnc.credentials.credentials
} finally {
lnc.disconnect()
await disconnect(lnc, logger)
}
}
const mutex = new Mutex()
async function unlock ({ password }, { lnc, status, showModal, 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 }) {
export async function sendPayment (bolt11, credentials, { me, status, logger }) {
const hash = bolt11Tags(bolt11).payment_hash
return await mutex.runExclusive(async () => {
let lnc
try {
lnc = await getLNC({ me })
// 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
lnc = await getLNC(credentials)
await lnc.connect()
const { paymentError, paymentPreimage: preimage } =
await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
@ -99,36 +77,16 @@ export async function sendPayment (bolt11, { pairingPhrase, password: configured
}
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)
}
await disconnect(lnc, logger)
}
})
}
function getLNC ({ me }) {
if (window.lnc) return window.lnc
window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined })
return window.lnc
async function getLNC (credentials = {}) {
const { default: { default: LNC } } = await import('@lightninglabs/lnc-web')
return new LNC({
credentialStore: new LncCredentialStore({ ...credentials, serverHost: 'mailbox.terminal.lightning.today:443' })
})
}
function validateNarrowPerms (lnc) {
@ -141,3 +99,63 @@ function validateNarrowPerms (lnc) {
// TODO: need to check for more narrow permissions
// 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,
min: 2,
max: 10
}
},
editable: false
},
{
name: 'password',
label: 'password',
type: 'password',
hint: 'encrypts your pairing phrase when stored locally',
optional: true
name: 'localKey',
type: 'text',
optional: true,
hidden: true
},
{
name: 'remoteKey',
type: 'text',
optional: true,
hidden: true
},
{
name: 'serverHost',
type: 'text',
optional: true,
hidden: true
}
]
export const card = {
title: 'LNC',
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}`)
try {
return await new Promise((resolve, reject) => {
await new Promise((resolve, reject) => {
let found = false
const sub = relay.subscribe([
{