make lnc work
This commit is contained in:
parent
5d03e08514
commit
a0c1d4f602
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?.()
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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']
|
||||||
}
|
}
|
||||||
|
|
|
@ -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([
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue