add lnc attached wallet (#1104)
* add litd to docker env * lnc payments * handle locked wallet configuration * create new lnc connection for every action * ensure creds are decrypted before reconnecting * perform permissions check
This commit is contained in:
parent
2340df3d8f
commit
c3d709b025
|
@ -3,7 +3,6 @@ import { GraphQLError } from 'graphql'
|
|||
import crypto from 'crypto'
|
||||
import serialize from './serial'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import lnpr from 'bolt11'
|
||||
import { SELECT } from './item'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
|
||||
|
@ -13,6 +12,7 @@ import { datePivot } from '@/lib/time'
|
|||
import assertGofacYourself from './ofac'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
|
||||
export async function getInvoice (parent, { id }, { me, models, lnd }) {
|
||||
const inv = await models.invoice.findUnique({
|
||||
|
@ -282,17 +282,7 @@ export default {
|
|||
f = { ...f, ...f.other }
|
||||
|
||||
if (f.bolt11) {
|
||||
const inv = lnpr.decode(f.bolt11)
|
||||
if (inv) {
|
||||
const { tags } = inv
|
||||
for (const tag of tags) {
|
||||
if (tag.tagName === 'description') {
|
||||
// prioritize description from bolt11 over description from our DB
|
||||
f.description = tag.data
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
f.description = bolt11Tags(f.bolt11).description
|
||||
}
|
||||
|
||||
switch (f.type) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { decode } from 'bolt11'
|
||||
import AccordianItem from './accordian-item'
|
||||
import { CopyInput } from './form'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
|
||||
export default ({ bolt11, preimage, children }) => {
|
||||
let description, paymentHash
|
||||
if (bolt11) {
|
||||
({ tagsObject: { description, payment_hash: paymentHash } } = decode(bolt11))
|
||||
({ description, payment_hash: paymentHash } = bolt11Tags(bolt11))
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1047,7 +1047,7 @@ function Client (Component) {
|
|||
const [,, helpers] = useField(props)
|
||||
|
||||
useEffect(() => {
|
||||
helpers.setValue(initialValue)
|
||||
initialValue && helpers.setValue(initialValue)
|
||||
}, [initialValue])
|
||||
|
||||
return <Component {...props} />
|
||||
|
|
|
@ -241,7 +241,7 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
|||
const fragment = {
|
||||
id: `Item:${itemId}`,
|
||||
fragment: gql`
|
||||
fragment ItemMeSats on Item {
|
||||
fragment ItemMeSatsInvoice on Item {
|
||||
sats
|
||||
meSats
|
||||
}
|
||||
|
@ -308,7 +308,7 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
|||
|
||||
const INVOICE_CANCELED_ERROR = 'invoice canceled'
|
||||
const waitForPayment = async ({ invoice, showModal, provider, pollInvoice, gqlCacheUpdate, flowId }) => {
|
||||
if (provider.enabled) {
|
||||
if (provider) {
|
||||
try {
|
||||
return await waitForWebLNPayment({ provider, invoice, pollInvoice, gqlCacheUpdate, flowId })
|
||||
} catch (err) {
|
||||
|
|
|
@ -100,7 +100,7 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
|
|||
const fragment = {
|
||||
id: `Item:${itemId}`,
|
||||
fragment: gql`
|
||||
fragment ItemMeSats on Item {
|
||||
fragment ItemMeSatsSubmit on Item {
|
||||
path
|
||||
sats
|
||||
meSats
|
||||
|
@ -271,7 +271,7 @@ export function useZap () {
|
|||
const item = cache.readFragment({
|
||||
id: `Item:${id}`,
|
||||
fragment: gql`
|
||||
fragment ItemMeSats on Item {
|
||||
fragment ItemMeSatsZap on Item {
|
||||
meSats
|
||||
}
|
||||
`
|
||||
|
@ -338,7 +338,7 @@ export function useZap () {
|
|||
const fragment = {
|
||||
id: `Item:${itemId}`,
|
||||
fragment: gql`
|
||||
fragment ItemMeSats on Item {
|
||||
fragment ItemMeSatsUndos on Item {
|
||||
sats
|
||||
meSats
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function Qr ({ asIs, value, webLn, statusVariant, description, st
|
|||
|
||||
useEffect(() => {
|
||||
async function effect () {
|
||||
if (webLn && provider?.enabled) {
|
||||
if (webLn && provider) {
|
||||
try {
|
||||
await provider.sendPayment({ bolt11: value })
|
||||
} catch (e) {
|
||||
|
|
|
@ -5,12 +5,33 @@ import Gear from '@/svgs/settings-5-fill.svg'
|
|||
import Link from 'next/link'
|
||||
import CancelButton from './cancel-button'
|
||||
import { SubmitButton } from './form'
|
||||
import { Status } from './webln'
|
||||
|
||||
export const isConfigured = status => [Status.Enabled, Status.Locked, Status.Error, true].includes(status)
|
||||
|
||||
export function WalletCard ({ title, badges, provider, status }) {
|
||||
const configured = isConfigured(status)
|
||||
let indicator = styles.disabled
|
||||
switch (status) {
|
||||
case Status.Enabled:
|
||||
case true:
|
||||
indicator = styles.success
|
||||
break
|
||||
case Status.Locked:
|
||||
indicator = styles.warning
|
||||
break
|
||||
case Status.Error:
|
||||
indicator = styles.error
|
||||
break
|
||||
case Status.Initialized:
|
||||
case false:
|
||||
indicator = styles.disabled
|
||||
break
|
||||
}
|
||||
|
||||
export function WalletCard ({ title, badges, provider, enabled }) {
|
||||
const isConfigured = enabled === true || enabled === false
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={`${styles.indicator} ${enabled === true ? styles.success : enabled === false ? styles.error : styles.disabled}`} />
|
||||
<div className={`${styles.indicator} ${indicator}`} />
|
||||
<Card.Body>
|
||||
<Card.Title>{title}</Card.Title>
|
||||
<Card.Subtitle className='mt-2'>
|
||||
|
@ -24,7 +45,7 @@ export function WalletCard ({ title, badges, provider, enabled }) {
|
|||
{provider &&
|
||||
<Link href={`/settings/wallets/${provider}`}>
|
||||
<Card.Footer className={styles.attach}>
|
||||
{isConfigured
|
||||
{configured
|
||||
? <>configure<Gear width={14} height={14} /></>
|
||||
: <>attach<Plug width={14} height={14} /></>}
|
||||
</Card.Footer>
|
||||
|
@ -34,19 +55,20 @@ export function WalletCard ({ title, badges, provider, enabled }) {
|
|||
}
|
||||
|
||||
export function WalletButtonBar ({
|
||||
enabled, disable,
|
||||
status, disable,
|
||||
className, children, onDelete, onCancel, hasCancel = true,
|
||||
createText = 'attach', deleteText = 'unattach', editText = 'save'
|
||||
}) {
|
||||
const configured = isConfigured(status)
|
||||
return (
|
||||
<div className={`mt-3 ${className}`}>
|
||||
<div className='d-flex justify-content-between'>
|
||||
{enabled !== undefined &&
|
||||
{configured &&
|
||||
<Button onClick={onDelete} variant='grey-medium'>{deleteText}</Button>}
|
||||
{children}
|
||||
<div className='d-flex align-items-center ms-auto'>
|
||||
{hasCancel && <CancelButton onClick={onCancel} />}
|
||||
<SubmitButton variant='primary' disabled={disable}>{enabled ? editText : createText}</SubmitButton>
|
||||
<SubmitButton variant='primary' disabled={disable}>{configured ? editText : createText}</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,29 +3,39 @@ import { LNbitsProvider, useLNbits } from './lnbits'
|
|||
import { NWCProvider, useNWC } from './nwc'
|
||||
import { useToast, withToastFlow } from '@/components/toast'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { LNCProvider, useLNC } from './lnc'
|
||||
|
||||
const WebLNContext = createContext({})
|
||||
|
||||
const syncProvider = (array, provider) => {
|
||||
const idx = array.findIndex(({ name }) => provider.name === name)
|
||||
const enabled = [Status.Enabled, Status.Locked].includes(provider.status)
|
||||
if (idx === -1) {
|
||||
// add provider to end if enabled
|
||||
return provider.enabled ? [...array, provider] : array
|
||||
return enabled ? [...array, provider] : array
|
||||
}
|
||||
return [
|
||||
...array.slice(0, idx),
|
||||
// remove provider if not enabled
|
||||
...provider.enabled ? [provider] : [],
|
||||
...enabled ? [provider] : [],
|
||||
...array.slice(idx + 1)
|
||||
]
|
||||
}
|
||||
|
||||
const storageKey = 'webln:providers'
|
||||
|
||||
export const Status = {
|
||||
Initialized: 'Initialized',
|
||||
Enabled: 'Enabled',
|
||||
Locked: 'Locked',
|
||||
Error: 'Error'
|
||||
}
|
||||
|
||||
function RawWebLNProvider ({ children }) {
|
||||
const lnbits = useLNbits()
|
||||
const nwc = useNWC()
|
||||
const availableProviders = [lnbits, nwc]
|
||||
const lnc = useLNC()
|
||||
const availableProviders = [lnbits, nwc, lnc]
|
||||
const [enabledProviders, setEnabledProviders] = useState([])
|
||||
|
||||
// restore order on page reload
|
||||
|
@ -42,7 +52,7 @@ function RawWebLNProvider ({ children }) {
|
|||
return null
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
}, [...availableProviders])
|
||||
|
||||
// keep list in sync with underlying providers
|
||||
useEffect(() => {
|
||||
|
@ -53,20 +63,13 @@ function RawWebLNProvider ({ children }) {
|
|||
// This can be the case if we're syncing from a page reload
|
||||
// where the providers are initially not enabled.
|
||||
// If provider is no longer enabled, it is removed from the list.
|
||||
const isInitialized = p => p.initialized
|
||||
const isInitialized = p => [Status.Enabled, Status.Locked, Status.Initialized].includes(p.status)
|
||||
const newProviders = availableProviders.filter(isInitialized).reduce(syncProvider, providers)
|
||||
const newOrder = newProviders.map(({ name }) => name)
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(newOrder))
|
||||
return newProviders
|
||||
})
|
||||
}, [lnbits, nwc])
|
||||
|
||||
// sanity check
|
||||
for (const p of enabledProviders) {
|
||||
if (!p.enabled && p.initialized) {
|
||||
console.warn('Expected provider to be enabled but is not:', p.name)
|
||||
}
|
||||
}
|
||||
}, [...availableProviders])
|
||||
|
||||
// first provider in list is the default provider
|
||||
// TODO: implement fallbacks via provider priority
|
||||
|
@ -87,7 +90,12 @@ function RawWebLNProvider ({ children }) {
|
|||
return {
|
||||
flowId: flowId || hash,
|
||||
type: 'payment',
|
||||
onPending: () => provider.sendPayment(bolt11),
|
||||
onPending: async () => {
|
||||
if (provider.status === Status.Locked) {
|
||||
await provider.unlock()
|
||||
}
|
||||
await provider.sendPayment(bolt11)
|
||||
},
|
||||
// hash and hmac are only passed for JIT invoices
|
||||
onCancel: () => hash && hmac ? cancelInvoice({ variables: { hash, hmac } }) : undefined,
|
||||
timeout: expiresIn
|
||||
|
@ -107,9 +115,8 @@ function RawWebLNProvider ({ children }) {
|
|||
})
|
||||
}, [setEnabledProviders])
|
||||
|
||||
const value = { provider: { ...provider, sendPayment: sendPaymentWithToast }, enabledProviders, setProvider }
|
||||
return (
|
||||
<WebLNContext.Provider value={value}>
|
||||
<WebLNContext.Provider value={{ provider: provider ? { sendPayment: sendPaymentWithToast } : null, enabledProviders, setProvider }}>
|
||||
{children}
|
||||
</WebLNContext.Provider>
|
||||
)
|
||||
|
@ -119,9 +126,11 @@ export function WebLNProvider ({ children }) {
|
|||
return (
|
||||
<LNbitsProvider>
|
||||
<NWCProvider>
|
||||
<RawWebLNProvider>
|
||||
{children}
|
||||
</RawWebLNProvider>
|
||||
<LNCProvider>
|
||||
<RawWebLNProvider>
|
||||
{children}
|
||||
</RawWebLNProvider>
|
||||
</LNCProvider>
|
||||
</NWCProvider>
|
||||
</LNbitsProvider>
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import lnpr from 'bolt11'
|
||||
import { Status } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
|
||||
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
||||
|
||||
|
@ -65,8 +66,7 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
|
|||
export function LNbitsProvider ({ children }) {
|
||||
const [url, setUrl] = useState('')
|
||||
const [adminKey, setAdminKey] = useState('')
|
||||
const [enabled, setEnabled] = useState()
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [status, setStatus] = useState()
|
||||
const logger = useWalletLogger('lnbits')
|
||||
|
||||
const name = 'LNbits'
|
||||
|
@ -90,9 +90,9 @@ export function LNbitsProvider ({ children }) {
|
|||
}, [url, adminKey])
|
||||
|
||||
const sendPayment = useCallback(async (bolt11) => {
|
||||
const inv = lnpr.decode(bolt11)
|
||||
const hash = inv.tagsObject.payment_hash
|
||||
const hash = bolt11Tags(bolt11).payment_hash
|
||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
||||
|
||||
try {
|
||||
const response = await postPayment(url, adminKey, bolt11)
|
||||
const checkResponse = await getPayment(url, adminKey, response.payment_hash)
|
||||
|
@ -110,9 +110,8 @@ export function LNbitsProvider ({ children }) {
|
|||
|
||||
const loadConfig = useCallback(async () => {
|
||||
const configStr = window.localStorage.getItem(storageKey)
|
||||
setStatus(Status.Initialized)
|
||||
if (!configStr) {
|
||||
setEnabled(undefined)
|
||||
setInitialized(true)
|
||||
logger.info('no existing config found')
|
||||
return
|
||||
}
|
||||
|
@ -133,15 +132,12 @@ export function LNbitsProvider ({ children }) {
|
|||
logger.info('trying to fetch wallet')
|
||||
await getWallet(url, adminKey)
|
||||
logger.ok('wallet found')
|
||||
setEnabled(true)
|
||||
setStatus(Status.Enabled)
|
||||
logger.ok('wallet enabled')
|
||||
} catch (err) {
|
||||
logger.error('invalid config:', err)
|
||||
setEnabled(false)
|
||||
logger.info('wallet disabled')
|
||||
throw err
|
||||
} finally {
|
||||
setInitialized(true)
|
||||
}
|
||||
}, [logger])
|
||||
|
||||
|
@ -165,28 +161,28 @@ export function LNbitsProvider ({ children }) {
|
|||
logger.info('trying to fetch wallet')
|
||||
await getWallet(config.url, config.adminKey)
|
||||
logger.ok('wallet found')
|
||||
setStatus(Status.Enabled)
|
||||
logger.ok('wallet enabled')
|
||||
} catch (err) {
|
||||
logger.error('invalid config:', err)
|
||||
setEnabled(false)
|
||||
setStatus(Status.Error)
|
||||
logger.info('wallet disabled')
|
||||
throw err
|
||||
}
|
||||
setEnabled(true)
|
||||
logger.ok('wallet enabled')
|
||||
}, [])
|
||||
|
||||
const clearConfig = useCallback(() => {
|
||||
window.localStorage.removeItem(storageKey)
|
||||
setUrl('')
|
||||
setAdminKey('')
|
||||
setEnabled(undefined)
|
||||
setStatus(undefined)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig().catch(console.error)
|
||||
}, [])
|
||||
|
||||
const value = { name, url, adminKey, initialized, enabled, saveConfig, clearConfig, getInfo, sendPayment }
|
||||
const value = { name, url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment }
|
||||
return (
|
||||
<LNbitsContext.Provider value={value}>
|
||||
{children}
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import LNC from '@lightninglabs/lnc-web'
|
||||
import { Status } 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'
|
||||
|
||||
const LNCContext = createContext()
|
||||
const mutex = new Mutex()
|
||||
|
||||
async function getLNC () {
|
||||
if (window.lnc) return window.lnc
|
||||
window.lnc = new LNC({ })
|
||||
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 name = 'lnc'
|
||||
const logger = useWalletLogger(name)
|
||||
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])
|
||||
|
||||
const sendPayment = useCallback(async (bolt11) => {
|
||||
const hash = bolt11Tags(bolt11).payment_hash
|
||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
||||
|
||||
return await mutex.runExclusive(async () => {
|
||||
try {
|
||||
// credentials need to be decrypted before connecting after a disconnect
|
||||
lnc.credentials.password = config?.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) {
|
||||
logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [logger, lnc, config])
|
||||
|
||||
const saveConfig = useCallback(async config => {
|
||||
setConfig(config)
|
||||
|
||||
console.log(config)
|
||||
try {
|
||||
lnc.credentials.pairingPhrase = config.pairingPhrase
|
||||
lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
|
||||
await lnc.connect()
|
||||
await validateNarrowPerms(lnc)
|
||||
lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
|
||||
setStatus(Status.Enabled)
|
||||
logger.ok('wallet enabled')
|
||||
} catch (err) {
|
||||
setStatus(Status.Error)
|
||||
logger.error('invalid config:', err)
|
||||
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)
|
||||
logger.info('cleared config')
|
||||
}, [logger, lnc])
|
||||
|
||||
const unlock = useCallback(async (connect) => {
|
||||
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()
|
||||
} 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])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const lnc = await getLNC()
|
||||
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) {
|
||||
setStatus(Status.Error)
|
||||
logger.error('wallet could not be loaded', err)
|
||||
}
|
||||
})()
|
||||
}, [setStatus, setConfig, logger])
|
||||
|
||||
return (
|
||||
<LNCContext.Provider value={{ name, status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }}>
|
||||
{children}
|
||||
{modal}
|
||||
</LNCContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useLNC () {
|
||||
return useContext(LNCContext)
|
||||
}
|
|
@ -4,7 +4,8 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
|
|||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
||||
import { parseNwcUrl } from '@/lib/url'
|
||||
import { useWalletLogger } from '../logger'
|
||||
import lnpr from 'bolt11'
|
||||
import { Status } from '.'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
|
||||
const NWCContext = createContext()
|
||||
|
||||
|
@ -13,8 +14,7 @@ export function NWCProvider ({ children }) {
|
|||
const [walletPubkey, setWalletPubkey] = useState()
|
||||
const [relayUrl, setRelayUrl] = useState()
|
||||
const [secret, setSecret] = useState()
|
||||
const [enabled, setEnabled] = useState()
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const [status, setStatus] = useState()
|
||||
const logger = useWalletLogger('nwc')
|
||||
|
||||
const name = 'NWC'
|
||||
|
@ -97,9 +97,8 @@ export function NWCProvider ({ children }) {
|
|||
|
||||
const loadConfig = useCallback(async () => {
|
||||
const configStr = window.localStorage.getItem(storageKey)
|
||||
setStatus(Status.Initialized)
|
||||
if (!configStr) {
|
||||
setEnabled(undefined)
|
||||
setInitialized(true)
|
||||
logger.info('no existing config found')
|
||||
return
|
||||
}
|
||||
|
@ -122,14 +121,11 @@ export function NWCProvider ({ children }) {
|
|||
|
||||
try {
|
||||
await validateParams(params)
|
||||
setEnabled(true)
|
||||
setStatus(Status.Enabled)
|
||||
logger.ok('wallet enabled')
|
||||
} catch (err) {
|
||||
setEnabled(false)
|
||||
logger.info('wallet disabled')
|
||||
throw err
|
||||
} finally {
|
||||
setInitialized(true)
|
||||
}
|
||||
}, [validateParams, logger])
|
||||
|
||||
|
@ -138,7 +134,7 @@ export function NWCProvider ({ children }) {
|
|||
const { nwcUrl } = config
|
||||
setNwcUrl(nwcUrl)
|
||||
if (!nwcUrl) {
|
||||
setEnabled(undefined)
|
||||
setStatus(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -159,10 +155,10 @@ export function NWCProvider ({ children }) {
|
|||
|
||||
try {
|
||||
await validateParams(params)
|
||||
setEnabled(true)
|
||||
setStatus(Status.Enabled)
|
||||
logger.ok('wallet enabled')
|
||||
} catch (err) {
|
||||
setEnabled(false)
|
||||
setStatus(Status.Error)
|
||||
logger.info('wallet disabled')
|
||||
throw err
|
||||
}
|
||||
|
@ -174,12 +170,11 @@ export function NWCProvider ({ children }) {
|
|||
setRelayUrl(undefined)
|
||||
setWalletPubkey(undefined)
|
||||
setSecret(undefined)
|
||||
setEnabled(undefined)
|
||||
setStatus(undefined)
|
||||
}, [])
|
||||
|
||||
const sendPayment = useCallback(async (bolt11) => {
|
||||
const inv = lnpr.decode(bolt11)
|
||||
const hash = inv.tagsObject.payment_hash
|
||||
const hash = bolt11Tags(bolt11).payment_hash
|
||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
||||
|
||||
let relay, sub
|
||||
|
@ -262,7 +257,7 @@ export function NWCProvider ({ children }) {
|
|||
loadConfig().catch(err => logger.error(err.message || err.toString?.()))
|
||||
}, [])
|
||||
|
||||
const value = { name, nwcUrl, relayUrl, walletPubkey, secret, initialized, enabled, saveConfig, clearConfig, getInfo, sendPayment }
|
||||
const value = { name, nwcUrl, relayUrl, walletPubkey, secret, status, saveConfig, clearConfig, getInfo, sendPayment }
|
||||
return (
|
||||
<NWCContext.Provider value={value}>
|
||||
{children}
|
||||
|
|
|
@ -334,6 +334,7 @@ services:
|
|||
- '--tlsextradomain=host.docker.internal'
|
||||
- '--listen=0.0.0.0:9735'
|
||||
- '--rpclisten=0.0.0.0:10009'
|
||||
- '--rpcmiddleware.enable'
|
||||
- '--restlisten=0.0.0.0:8080'
|
||||
- '--bitcoin.active'
|
||||
- '--bitcoin.regtest'
|
||||
|
@ -350,6 +351,7 @@ services:
|
|||
- '--maxpendingchannels=10'
|
||||
expose:
|
||||
- "9735"
|
||||
- "10009"
|
||||
ports:
|
||||
- "${STACKER_LND_REST_PORT}:8080"
|
||||
- "${STACKER_LND_GRPC_PORT}:10009"
|
||||
|
@ -367,6 +369,37 @@ services:
|
|||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||
fi
|
||||
"
|
||||
litd:
|
||||
container_name: litd
|
||||
image: lightninglabs/lightning-terminal:v0.12.4-alpha
|
||||
profiles:
|
||||
- payments
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
<<: *healthcheck
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8443"]
|
||||
depends_on:
|
||||
stacker_lnd:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
volumes:
|
||||
- stacker_lnd:/lnd
|
||||
ports:
|
||||
- "8443:8443"
|
||||
command:
|
||||
- 'litd'
|
||||
- '--httpslisten=0.0.0.0:8444'
|
||||
- '--insecure-httplisten=0.0.0.0:8443'
|
||||
- '--uipassword=password'
|
||||
- '--lnd-mode=remote'
|
||||
- '--network=regtest'
|
||||
- '--remote.lit-debuglevel=debug'
|
||||
- '--remote.lnd.rpcserver=stacker_lnd:10009'
|
||||
- '--remote.lnd.macaroonpath=/lnd/data/chain/bitcoin/regtest/admin.macaroon'
|
||||
- '--remote.lnd.tlscertpath=/lnd/tls.cert'
|
||||
- '--autopilot.disable'
|
||||
- '--pool.auctionserver=test.pool.lightning.finance:12010'
|
||||
- '--loop.server.host=test.swap.lightning.today:11010'
|
||||
stacker_cln:
|
||||
build:
|
||||
context: ./docker/cln
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
import { decode } from 'bolt11'
|
||||
|
||||
export function bolt11Tags (bolt11) {
|
||||
return decode(bolt11).tagsObject
|
||||
}
|
|
@ -13,6 +13,7 @@ import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
|||
import { parseNwcUrl } from './url'
|
||||
import { datePivot } from './time'
|
||||
import { decodeRune } from '@/lib/cln'
|
||||
import bip39Words from './bip39-words'
|
||||
|
||||
const { SUB } = subsFragments
|
||||
const { NAME_QUERY } = usersFragments
|
||||
|
@ -634,6 +635,21 @@ export const nwcSchema = object({
|
|||
})
|
||||
})
|
||||
|
||||
export const lncSchema = 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()
|
||||
})
|
||||
|
||||
export const bioSchema = object({
|
||||
bio: string().required('required').trim()
|
||||
})
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@as-integrations/next": "^2.0.2",
|
||||
"@auth/prisma-adapter": "^1.0.3",
|
||||
"@graphql-tools/schema": "^10.0.0",
|
||||
"@lightninglabs/lnc-web": "^0.3.1-alpha",
|
||||
"@noble/curves": "^1.2.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"@prisma/client": "^5.4.2",
|
||||
|
@ -21,6 +22,7 @@
|
|||
"@yudiel/react-qr-scanner": "^1.1.10",
|
||||
"acorn": "^8.10.0",
|
||||
"ajv": "^8.12.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"async-retry": "^1.3.1",
|
||||
"aws-sdk": "^2.1473.0",
|
||||
"bech32": "^2.0.0",
|
||||
|
@ -3815,6 +3817,20 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lightninglabs/lnc-core": {
|
||||
"version": "0.3.1-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@lightninglabs/lnc-core/-/lnc-core-0.3.1-alpha.tgz",
|
||||
"integrity": "sha512-I/hThdItLWJ6RU8Z27ZIXhpBS2JJuD3+TjtaQXX2CabaUYXlcN4sk+Kx8N/zG/fk8qZvjlRWum4vHu4ZX554Fg=="
|
||||
},
|
||||
"node_modules/@lightninglabs/lnc-web": {
|
||||
"version": "0.3.1-alpha",
|
||||
"resolved": "https://registry.npmjs.org/@lightninglabs/lnc-web/-/lnc-web-0.3.1-alpha.tgz",
|
||||
"integrity": "sha512-yL5SgBkl6kd6ISzJHGlSN7TXbiDoo1pfGvTOIdVWYVyXtEeW8PT+x6YGOmyQXGFT2OOf7fC7PfP9VnskDPuFaA==",
|
||||
"dependencies": {
|
||||
"@lightninglabs/lnc-core": "0.3.1-alpha",
|
||||
"crypto-js": "4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "13.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz",
|
||||
|
@ -5840,6 +5856,14 @@
|
|||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
|
||||
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async-retry": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
||||
|
@ -7437,6 +7461,11 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||
},
|
||||
"node_modules/crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"@as-integrations/next": "^2.0.2",
|
||||
"@auth/prisma-adapter": "^1.0.3",
|
||||
"@graphql-tools/schema": "^10.0.0",
|
||||
"@lightninglabs/lnc-web": "^0.3.1-alpha",
|
||||
"@noble/curves": "^1.2.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"@prisma/client": "^5.4.2",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"@yudiel/react-qr-scanner": "^1.1.10",
|
||||
"acorn": "^8.10.0",
|
||||
"ajv": "^8.12.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"async-retry": "^1.3.1",
|
||||
"aws-sdk": "^2.1473.0",
|
||||
"bech32": "^2.0.0",
|
||||
|
|
|
@ -130,7 +130,7 @@ export function CLNCard ({ wallet }) {
|
|||
title='CLN'
|
||||
badges={['receive only', 'non-custodial']}
|
||||
provider='cln'
|
||||
enabled={wallet !== undefined || undefined}
|
||||
status={wallet !== undefined || undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import { CLNCard } from './cln'
|
|||
import { WALLETS } from '@/fragments/wallet'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import PageLoading from '@/components/page-loading'
|
||||
import { LNCCard } from './lnc'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
|
||||
|
||||
|
@ -27,12 +29,18 @@ export default function Wallet ({ ssrData }) {
|
|||
<div className='py-5 w-100'>
|
||||
<h2 className='mb-2 text-center'>attach wallets</h2>
|
||||
<h6 className='text-muted text-center'>attach wallets to supplement your SN wallet</h6>
|
||||
<div className='text-center'>
|
||||
<Link href='/wallet/logs' className='text-muted fw-bold text-underline'>
|
||||
wallet logs
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.walletGrid}>
|
||||
<LightningAddressWalletCard wallet={lnaddr} />
|
||||
<LNDCard wallet={lnd} />
|
||||
<CLNCard wallet={cln} />
|
||||
<LNbitsCard />
|
||||
<NWCCard />
|
||||
<LNCCard />
|
||||
<WalletCard title='coming soon' badges={['probably']} />
|
||||
<WalletCard title='coming soon' badges={['we hope']} />
|
||||
<WalletCard title='coming soon' badges={['tm']} />
|
||||
|
|
|
@ -99,7 +99,7 @@ export function LightningAddressWalletCard ({ wallet }) {
|
|||
title='lightning address'
|
||||
badges={['receive only', 'non-custodialish']}
|
||||
provider='lightning-address'
|
||||
enabled={wallet !== undefined || undefined}
|
||||
status={wallet !== undefined || undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||
import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
|
||||
import { CenterLayout } from '@/components/layout'
|
||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
|
||||
import { lnbitsSchema } from '@/lib/validate'
|
||||
import { useToast } from '@/components/toast'
|
||||
import { useRouter } from 'next/router'
|
||||
|
@ -15,8 +15,9 @@ export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
|||
export default function LNbits () {
|
||||
const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
|
||||
const lnbits = useLNbits()
|
||||
const { name, url, adminKey, saveConfig, clearConfig, enabled } = lnbits
|
||||
const { name, url, adminKey, saveConfig, clearConfig, status } = lnbits
|
||||
const isDefault = provider?.name === name
|
||||
const configured = isConfigured(status)
|
||||
const toaster = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -59,13 +60,13 @@ export default function LNbits () {
|
|||
required
|
||||
/>
|
||||
<ClientCheckbox
|
||||
disabled={!enabled || isDefault || enabledProviders.length === 1}
|
||||
disabled={!configured || isDefault || enabledProviders.length === 1}
|
||||
initialValue={isDefault}
|
||||
label='default payment method'
|
||||
name='isDefault'
|
||||
/>
|
||||
<WalletButtonBar
|
||||
enabled={enabled} onDelete={async () => {
|
||||
status={status} onDelete={async () => {
|
||||
try {
|
||||
await clearConfig()
|
||||
toaster.success('saved settings')
|
||||
|
@ -85,13 +86,13 @@ export default function LNbits () {
|
|||
}
|
||||
|
||||
export function LNbitsCard () {
|
||||
const { enabled } = useLNbits()
|
||||
const { status } = useLNbits()
|
||||
return (
|
||||
<WalletCard
|
||||
title='LNbits'
|
||||
badges={['send only', 'non-custodialish']}
|
||||
provider='lnbits'
|
||||
enabled={enabled}
|
||||
status={status}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||
import { WalletSecurityBanner } from '@/components/banners'
|
||||
import { ClientCheckbox, Form, PasswordInput } from '@/components/form'
|
||||
import Info from '@/components/info'
|
||||
import { CenterLayout } from '@/components/layout'
|
||||
import Text from '@/components/text'
|
||||
import { useToast } from '@/components/toast'
|
||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
||||
import WalletLogs from '@/components/wallet-logs'
|
||||
import { Status, useWebLNConfigurator } from '@/components/webln'
|
||||
import { XXX_DEFAULT_PASSWORD, useLNC } from '@/components/webln/lnc'
|
||||
import { lncSchema } from '@/lib/validate'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
export default function LNC () {
|
||||
const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
|
||||
const toaster = useToast()
|
||||
const router = useRouter()
|
||||
const lnc = useLNC()
|
||||
const { status, clearConfig, saveConfig, config, name, unlock } = lnc
|
||||
const isDefault = provider?.name === name
|
||||
const unlocking = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!unlocking.current && status === Status.Locked) {
|
||||
unlocking.current = true
|
||||
unlock()
|
||||
}
|
||||
}, [status, unlock])
|
||||
|
||||
const defaultPassword = config?.password === XXX_DEFAULT_PASSWORD
|
||||
|
||||
return (
|
||||
<CenterLayout>
|
||||
<h2>Lightning Node Connect for LND</h2>
|
||||
<h6 className='text-muted text-center pb-3'>use Lightning Node Connect for LND payments</h6>
|
||||
<WalletSecurityBanner />
|
||||
<Form
|
||||
initial={{
|
||||
pairingPhrase: config?.pairingPhrase || '',
|
||||
password: (!config?.password || defaultPassword) ? '' : config.password
|
||||
}}
|
||||
schema={lncSchema}
|
||||
onSubmit={async ({ isDefault, ...values }) => {
|
||||
try {
|
||||
await clearConfig()
|
||||
await saveConfig(values)
|
||||
if (isDefault) setProvider(lnc)
|
||||
toaster.success('saved settings')
|
||||
router.push('/settings/wallets')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger('failed to attach: ' + err.message || err.toString?.())
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PasswordInput
|
||||
label={
|
||||
<div className='d-flex align-items-center'>pairing phrase
|
||||
<Info label='help'>
|
||||
<Text>
|
||||
{'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 --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```'}
|
||||
</Text>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='pairingPhrase'
|
||||
initialValue={config?.pairingPhrase}
|
||||
newPass={config?.pairingPhrase === undefined}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<PasswordInput
|
||||
label={<>password <small className='text-muted ms-2'>optional</small></>}
|
||||
name='password'
|
||||
initialValue={defaultPassword ? '' : config?.password}
|
||||
newPass={config?.password === undefined || defaultPassword}
|
||||
hint='encrypts your pairing phrase when stored locally'
|
||||
/>
|
||||
<ClientCheckbox
|
||||
disabled={status !== Status.Enabled || isDefault || enabledProviders?.length === 1}
|
||||
initialValue={isDefault}
|
||||
label='default payment method'
|
||||
name='isDefault'
|
||||
/>
|
||||
<WalletButtonBar
|
||||
status={status} onDelete={async () => {
|
||||
try {
|
||||
await clearConfig()
|
||||
toaster.success('saved settings')
|
||||
router.push('/settings/wallets')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toaster.danger('failed to unattach: ' + err.message || err.toString?.())
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
<div className='mt-3 w-100'>
|
||||
<WalletLogs wallet='lnc' embedded />
|
||||
</div>
|
||||
</CenterLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export function LNCCard () {
|
||||
const { status } = useLNC()
|
||||
return (
|
||||
<WalletCard
|
||||
title='LNC'
|
||||
badges={['send only', 'non-custodial', 'budgetable']}
|
||||
provider='lnc'
|
||||
status={status}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -106,7 +106,7 @@ export default function LND ({ ssrData }) {
|
|||
/>
|
||||
<AutowithdrawSettings />
|
||||
<WalletButtonBar
|
||||
enabled={!!wallet} onDelete={async () => {
|
||||
status={!!wallet} onDelete={async () => {
|
||||
try {
|
||||
await removeWallet({ variables: { id: wallet?.id } })
|
||||
toaster.success('saved settings')
|
||||
|
@ -130,7 +130,7 @@ export function LNDCard ({ wallet }) {
|
|||
title='LND'
|
||||
badges={['receive only', 'non-custodial']}
|
||||
provider='lnd'
|
||||
enabled={wallet !== undefined || undefined}
|
||||
status={wallet !== undefined || undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||
import { Form, ClientCheckbox, PasswordInput } from '@/components/form'
|
||||
import { CenterLayout } from '@/components/layout'
|
||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
|
||||
import { nwcSchema } from '@/lib/validate'
|
||||
import { useToast } from '@/components/toast'
|
||||
import { useRouter } from 'next/router'
|
||||
|
@ -15,8 +15,9 @@ export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
|||
export default function NWC () {
|
||||
const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
|
||||
const nwc = useNWC()
|
||||
const { name, nwcUrl, saveConfig, clearConfig, enabled } = nwc
|
||||
const { name, nwcUrl, saveConfig, clearConfig, status } = nwc
|
||||
const isDefault = provider?.name === name
|
||||
const configured = isConfigured(status)
|
||||
const toaster = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -52,13 +53,13 @@ export default function NWC () {
|
|||
autoFocus
|
||||
/>
|
||||
<ClientCheckbox
|
||||
disabled={!enabled || isDefault || enabledProviders.length === 1}
|
||||
disabled={!configured || isDefault || enabledProviders.length === 1}
|
||||
initialValue={isDefault}
|
||||
label='default payment method'
|
||||
name='isDefault'
|
||||
/>
|
||||
<WalletButtonBar
|
||||
enabled={enabled} onDelete={async () => {
|
||||
status={status} onDelete={async () => {
|
||||
try {
|
||||
await clearConfig()
|
||||
toaster.success('saved settings')
|
||||
|
@ -78,13 +79,13 @@ export default function NWC () {
|
|||
}
|
||||
|
||||
export function NWCCard () {
|
||||
const { enabled } = useNWC()
|
||||
const { status } = useNWC()
|
||||
return (
|
||||
<WalletCard
|
||||
title='NWC'
|
||||
badges={['send only', 'non-custodialish', 'budgetable']}
|
||||
provider='nwc'
|
||||
enabled={enabled}
|
||||
status={status}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -84,11 +84,6 @@ function WalletHistory () {
|
|||
wallet history
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href='/wallet/logs' className='text-muted fw-bold text-underline'>
|
||||
wallet logs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -55,6 +55,12 @@
|
|||
filter: drop-shadow(0 0 2px #c92020);
|
||||
}
|
||||
|
||||
.indicator.warning {
|
||||
color: var(--bs-orange) !important;
|
||||
background-color: var(--bs-orange) !important;
|
||||
border: 1px solid var(--bs-secondary);
|
||||
}
|
||||
|
||||
.indicator.disabled {
|
||||
color: var(--theme-toolbarHover) !important;
|
||||
background-color: var(--theme-toolbarHover) !important;
|
||||
|
|
Loading…
Reference in New Issue