wip: refactor WebLN providers in frontend
This commit is contained in:
parent
8329da1f56
commit
a8450be083
|
@ -23,7 +23,6 @@ import classNames from 'classnames'
|
||||||
import SnIcon from '@/svgs/sn.svg'
|
import SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWalletLogger } from '../logger'
|
import { useWalletLogger } from '../logger'
|
||||||
import { useWebLNConfigurator } from '../webln'
|
|
||||||
|
|
||||||
export function Brand ({ className }) {
|
export function Brand ({ className }) {
|
||||||
return (
|
return (
|
||||||
|
@ -257,7 +256,7 @@ export default function LoginButton ({ className }) {
|
||||||
|
|
||||||
export function LogoutDropdownItem () {
|
export function LogoutDropdownItem () {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
const webLN = useWebLNConfigurator()
|
// const webLN = useWebLNConfigurator()
|
||||||
const { deleteLogs } = useWalletLogger()
|
const { deleteLogs } = useWalletLogger()
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
|
@ -267,8 +266,8 @@ export function LogoutDropdownItem () {
|
||||||
if (pushSubscription) {
|
if (pushSubscription) {
|
||||||
await togglePushSubscription().catch(console.error)
|
await togglePushSubscription().catch(console.error)
|
||||||
}
|
}
|
||||||
// detach wallets
|
// TODO: detach wallets
|
||||||
await webLN.clearConfig().catch(console.error)
|
// await webLN.clearConfig().catch(console.error)
|
||||||
// delete client wallet logs to prevent leak of private data if a shared device was used
|
// delete client wallet logs to prevent leak of private data if a shared device was used
|
||||||
await deleteLogs(Wallet.NWC).catch(console.error)
|
await deleteLogs(Wallet.NWC).catch(console.error)
|
||||||
await deleteLogs(Wallet.LNbits).catch(console.error)
|
await deleteLogs(Wallet.LNbits).catch(console.error)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export default function useLocalState (storageKey, initialValue = '') {
|
||||||
|
const [value, innerSetValue] = useState(initialValue)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const value = window.localStorage.getItem(storageKey)
|
||||||
|
innerSetValue(JSON.parse(value))
|
||||||
|
}, [storageKey])
|
||||||
|
|
||||||
|
const setValue = useCallback((newValue) => {
|
||||||
|
window.localStorage.setItem(storageKey, JSON.stringify(newValue))
|
||||||
|
innerSetValue(newValue)
|
||||||
|
}, [storageKey])
|
||||||
|
|
||||||
|
const clearValue = useCallback(() => {
|
||||||
|
window.localStorage.removeItem(storageKey)
|
||||||
|
innerSetValue(null)
|
||||||
|
}, [storageKey])
|
||||||
|
|
||||||
|
return [value, setValue, clearValue]
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { Status } from './webln'
|
||||||
|
|
||||||
export const isConfigured = status => [Status.Enabled, Status.Locked, Status.Error, true].includes(status)
|
export const isConfigured = status => [Status.Enabled, Status.Locked, Status.Error, true].includes(status)
|
||||||
|
|
||||||
export function WalletCard ({ title, badges, provider, status }) {
|
export function WalletCard ({ title, badges, provider, status, href }) {
|
||||||
const configured = isConfigured(status)
|
const configured = isConfigured(status)
|
||||||
let indicator = styles.disabled
|
let indicator = styles.disabled
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
@ -42,14 +42,13 @@ export function WalletCard ({ title, badges, provider, status }) {
|
||||||
</Badge>)}
|
</Badge>)}
|
||||||
</Card.Subtitle>
|
</Card.Subtitle>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
{provider &&
|
<Link href={href}>
|
||||||
<Link href={`/settings/wallets/${provider}`}>
|
<Card.Footer className={styles.attach}>
|
||||||
<Card.Footer className={styles.attach}>
|
{configured
|
||||||
{configured
|
? <>configure<Gear width={14} height={14} /></>
|
||||||
? <>configure<Gear width={14} height={14} /></>
|
: <>attach<Plug width={14} height={14} /></>}
|
||||||
: <>attach<Plug width={14} height={14} /></>}
|
</Card.Footer>
|
||||||
</Card.Footer>
|
</Link>
|
||||||
</Link>}
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { WalletSecurityBanner } from './banners'
|
||||||
|
import { Form } from './form'
|
||||||
|
import { CenterLayout } from './layout'
|
||||||
|
|
||||||
|
export default function WalletConfigurator ({ config }) {
|
||||||
|
const initial = config.provider
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CenterLayout>
|
||||||
|
<h2 className='pb-2'>{config.title}</h2>
|
||||||
|
<h6 className='text-muted text-center pb-3'>use {config.title} for payments</h6>
|
||||||
|
<WalletSecurityBanner />
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
url: url || '',
|
||||||
|
adminKey: adminKey || '',
|
||||||
|
isDefault: isDefault || false
|
||||||
|
}}
|
||||||
|
schema={lnbitsSchema}
|
||||||
|
onSubmit={async ({ isDefault, ...values }) => {
|
||||||
|
try {
|
||||||
|
await saveConfig(values)
|
||||||
|
if (isDefault) setProvider(lnbits)
|
||||||
|
toaster.success('saved settings')
|
||||||
|
router.push('/settings/wallets')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toaster.danger('failed to attach: ' + err.message || err.toString?.())
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClientInput
|
||||||
|
initialValue={url}
|
||||||
|
label='lnbits url'
|
||||||
|
name='url'
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
initialValue={adminKey}
|
||||||
|
label='admin key'
|
||||||
|
name='adminKey'
|
||||||
|
newPass
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<ClientCheckbox
|
||||||
|
disabled={!configured || 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 detach: ' + err.message || err.toString?.())
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
<div className='mt-3 w-100'>
|
||||||
|
<WalletLogs wallet={Wallet.LNbits} embedded />
|
||||||
|
</div>
|
||||||
|
</CenterLayout>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,29 +1,7 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
import { createContext, useContext } from 'react'
|
||||||
import { LNbitsProvider, useLNbits } from './lnbits'
|
|
||||||
import { NWCProvider, useNWC } from './nwc'
|
|
||||||
import { LNCProvider, useLNC } from './lnc'
|
|
||||||
|
|
||||||
const WebLNContext = createContext({})
|
const WebLNContext = createContext({})
|
||||||
|
|
||||||
const isEnabled = p => [Status.Enabled, Status.Locked].includes(p?.status)
|
|
||||||
|
|
||||||
const syncProvider = (array, provider) => {
|
|
||||||
const idx = array.findIndex(({ name }) => provider.name === name)
|
|
||||||
const enabled = isEnabled(provider)
|
|
||||||
if (idx === -1) {
|
|
||||||
// add provider to end if enabled
|
|
||||||
return enabled ? [...array, provider] : array
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
...array.slice(0, idx),
|
|
||||||
// remove provider if not enabled
|
|
||||||
...enabled ? [provider] : [],
|
|
||||||
...array.slice(idx + 1)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const storageKey = 'webln:providers'
|
|
||||||
|
|
||||||
export const Status = {
|
export const Status = {
|
||||||
Initialized: 'Initialized',
|
Initialized: 'Initialized',
|
||||||
Enabled: 'Enabled',
|
Enabled: 'Enabled',
|
||||||
|
@ -31,112 +9,14 @@ export const Status = {
|
||||||
Error: 'Error'
|
Error: 'Error'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migrateLocalStorage (oldStorageKey, newStorageKey) {
|
export function WebLNProvider ({ children }) {
|
||||||
const item = window.localStorage.getItem(oldStorageKey)
|
|
||||||
if (item) {
|
|
||||||
window.localStorage.setItem(newStorageKey, item)
|
|
||||||
window.localStorage.removeItem(oldStorageKey)
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
function RawWebLNProvider ({ children }) {
|
|
||||||
const lnbits = useLNbits()
|
|
||||||
const nwc = useNWC()
|
|
||||||
const lnc = useLNC()
|
|
||||||
const availableProviders = [lnbits, nwc, lnc]
|
|
||||||
const [enabledProviders, setEnabledProviders] = useState([])
|
|
||||||
|
|
||||||
// restore order on page reload
|
|
||||||
useEffect(() => {
|
|
||||||
const storedOrder = window.localStorage.getItem(storageKey)
|
|
||||||
if (!storedOrder) return
|
|
||||||
const providerNames = JSON.parse(storedOrder)
|
|
||||||
setEnabledProviders(providers => {
|
|
||||||
return providerNames.map(name => {
|
|
||||||
for (const p of availableProviders) {
|
|
||||||
if (p.name === name) return p
|
|
||||||
}
|
|
||||||
console.warn(`Stored provider with name ${name} not available`)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// keep list in sync with underlying providers
|
|
||||||
useEffect(() => {
|
|
||||||
setEnabledProviders(providers => {
|
|
||||||
// Sync existing provider state with new provider state
|
|
||||||
// in the list while keeping the order they are in.
|
|
||||||
// If provider does not exist but is enabled, it is just added to the end of the list.
|
|
||||||
// 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 => [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
|
|
||||||
})
|
|
||||||
}, [...availableProviders])
|
|
||||||
|
|
||||||
// first provider in list is the default provider
|
|
||||||
// TODO: implement fallbacks via provider priority
|
|
||||||
const provider = enabledProviders[0]
|
|
||||||
|
|
||||||
const setProvider = useCallback((defaultProvider) => {
|
|
||||||
// move provider to the start to set it as default
|
|
||||||
setEnabledProviders(providers => {
|
|
||||||
const idx = providers.findIndex(({ name }) => defaultProvider.name === name)
|
|
||||||
if (idx === -1) {
|
|
||||||
console.warn(`tried to set unenabled provider ${defaultProvider.name} as default`)
|
|
||||||
return providers
|
|
||||||
}
|
|
||||||
return [defaultProvider, ...providers.slice(0, idx), ...providers.slice(idx + 1)]
|
|
||||||
})
|
|
||||||
}, [setEnabledProviders])
|
|
||||||
|
|
||||||
const clearConfig = useCallback(async () => {
|
|
||||||
lnbits.clearConfig()
|
|
||||||
nwc.clearConfig()
|
|
||||||
await lnc.clearConfig()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
|
||||||
provider: isEnabled(provider)
|
|
||||||
? { name: provider.name, sendPayment: provider.sendPayment }
|
|
||||||
: null,
|
|
||||||
enabledProviders,
|
|
||||||
setProvider,
|
|
||||||
clearConfig
|
|
||||||
}), [provider, enabledProviders, setProvider])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WebLNContext.Provider value={value}>
|
<WebLNContext.Provider value={null}>
|
||||||
{children}
|
{children}
|
||||||
</WebLNContext.Provider>
|
</WebLNContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebLNProvider ({ children }) {
|
|
||||||
return (
|
|
||||||
<LNbitsProvider>
|
|
||||||
<NWCProvider>
|
|
||||||
<LNCProvider>
|
|
||||||
<RawWebLNProvider>
|
|
||||||
{children}
|
|
||||||
</RawWebLNProvider>
|
|
||||||
</LNCProvider>
|
|
||||||
</NWCProvider>
|
|
||||||
</LNbitsProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWebLN () {
|
export function useWebLN () {
|
||||||
const { provider } = useContext(WebLNContext)
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWebLNConfigurator () {
|
|
||||||
return useContext(WebLNContext)
|
return useContext(WebLNContext)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,70 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { useWalletLogger } from '../logger'
|
|
||||||
import { Status, migrateLocalStorage } from '.'
|
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
import { useMe } from '../me'
|
|
||||||
|
|
||||||
// Reference: https://github.com/getAlby/bitcoin-connect/blob/v3.2.0-alpha/src/connectors/LnbitsConnector.ts
|
export const name = 'LNbits'
|
||||||
|
|
||||||
const LNbitsContext = createContext()
|
export const config = {
|
||||||
|
provider: {
|
||||||
|
url: {
|
||||||
|
label: 'lnbits url',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
adminKey: {
|
||||||
|
label: 'admin key',
|
||||||
|
type: 'password'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
title: 'LNbits',
|
||||||
|
badges: ['send only', 'non-custodialish'],
|
||||||
|
href: '/settings/wallets/lnbits'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getWallet = async (baseUrl, adminKey) => {
|
export function getInfo ({ config, logger }) {
|
||||||
|
return async function () {
|
||||||
|
const response = await getWallet(config.url, config.adminKey)
|
||||||
|
return {
|
||||||
|
node: {
|
||||||
|
alias: response.name,
|
||||||
|
pubkey: ''
|
||||||
|
},
|
||||||
|
methods: [
|
||||||
|
'getInfo',
|
||||||
|
'getBalance',
|
||||||
|
'sendPayment'
|
||||||
|
],
|
||||||
|
version: '1.0',
|
||||||
|
supports: ['lightning']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendPayment ({ config, logger }) {
|
||||||
|
return async function (bolt11) {
|
||||||
|
const { url, adminKey } = config
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (!checkResponse.preimage) {
|
||||||
|
throw new Error('No preimage')
|
||||||
|
}
|
||||||
|
|
||||||
|
const preimage = checkResponse.preimage
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWallet (baseUrl, adminKey) {
|
||||||
const url = baseUrl.replace(/\/+$/, '')
|
const url = baseUrl.replace(/\/+$/, '')
|
||||||
const path = '/api/v1/wallet'
|
const path = '/api/v1/wallet'
|
||||||
|
|
||||||
|
@ -23,11 +78,12 @@ const getWallet = async (baseUrl, adminKey) => {
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
const wallet = await res.json()
|
const wallet = await res.json()
|
||||||
return wallet
|
return wallet
|
||||||
}
|
}
|
||||||
|
|
||||||
const postPayment = async (baseUrl, adminKey, bolt11) => {
|
async function postPayment (baseUrl, adminKey, bolt11) {
|
||||||
const url = baseUrl.replace(/\/+$/, '')
|
const url = baseUrl.replace(/\/+$/, '')
|
||||||
const path = '/api/v1/payments'
|
const path = '/api/v1/payments'
|
||||||
|
|
||||||
|
@ -43,11 +99,12 @@ const postPayment = async (baseUrl, adminKey, bolt11) => {
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
const payment = await res.json()
|
const payment = await res.json()
|
||||||
return payment
|
return payment
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPayment = async (baseUrl, adminKey, paymentHash) => {
|
async function getPayment (baseUrl, adminKey, paymentHash) {
|
||||||
const url = baseUrl.replace(/\/+$/, '')
|
const url = baseUrl.replace(/\/+$/, '')
|
||||||
const path = `/api/v1/payments/${paymentHash}`
|
const path = `/api/v1/payments/${paymentHash}`
|
||||||
|
|
||||||
|
@ -61,150 +118,7 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
|
||||||
const errBody = await res.json()
|
const errBody = await res.json()
|
||||||
throw new Error(errBody.detail)
|
throw new Error(errBody.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
const payment = await res.json()
|
const payment = await res.json()
|
||||||
return payment
|
return payment
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LNbitsProvider ({ children }) {
|
|
||||||
const me = useMe()
|
|
||||||
const [url, setUrl] = useState('')
|
|
||||||
const [adminKey, setAdminKey] = useState('')
|
|
||||||
const [status, setStatus] = useState()
|
|
||||||
const { logger } = useWalletLogger(Wallet.LNbits)
|
|
||||||
|
|
||||||
let storageKey = 'webln:provider:lnbits'
|
|
||||||
if (me) {
|
|
||||||
storageKey = `${storageKey}:${me.id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInfo = useCallback(async () => {
|
|
||||||
const response = await getWallet(url, adminKey)
|
|
||||||
return {
|
|
||||||
node: {
|
|
||||||
alias: response.name,
|
|
||||||
pubkey: ''
|
|
||||||
},
|
|
||||||
methods: [
|
|
||||||
'getInfo',
|
|
||||||
'getBalance',
|
|
||||||
'sendPayment'
|
|
||||||
],
|
|
||||||
version: '1.0',
|
|
||||||
supports: ['lightning']
|
|
||||||
}
|
|
||||||
}, [url, adminKey])
|
|
||||||
|
|
||||||
const sendPayment = useCallback(async (bolt11) => {
|
|
||||||
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)
|
|
||||||
if (!checkResponse.preimage) {
|
|
||||||
throw new Error('No preimage')
|
|
||||||
}
|
|
||||||
const preimage = checkResponse.preimage
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}, [logger, url, adminKey])
|
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
|
||||||
let configStr = window.localStorage.getItem(storageKey)
|
|
||||||
setStatus(Status.Initialized)
|
|
||||||
if (!configStr) {
|
|
||||||
if (me) {
|
|
||||||
// backwards compatibility: try old storageKey
|
|
||||||
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
|
||||||
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
|
||||||
}
|
|
||||||
if (!configStr) {
|
|
||||||
logger.info('no existing config found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(configStr)
|
|
||||||
|
|
||||||
const { url, adminKey } = config
|
|
||||||
setUrl(url)
|
|
||||||
setAdminKey(adminKey)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'loaded wallet config: ' +
|
|
||||||
'adminKey=****** ' +
|
|
||||||
`url=${url}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// validate config by trying to fetch wallet
|
|
||||||
logger.info('trying to fetch wallet')
|
|
||||||
await getWallet(url, adminKey)
|
|
||||||
logger.ok('wallet found')
|
|
||||||
setStatus(Status.Enabled)
|
|
||||||
logger.ok('wallet enabled')
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('invalid config:', err)
|
|
||||||
setStatus(Status.Error)
|
|
||||||
logger.info('wallet disabled')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}, [me, logger])
|
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
|
||||||
// immediately store config so it's not lost even if config is invalid
|
|
||||||
setUrl(config.url)
|
|
||||||
setAdminKey(config.adminKey)
|
|
||||||
|
|
||||||
// XXX This is insecure, XSS vulns could lead to loss of funds!
|
|
||||||
// -> check how mutiny encrypts their wallet and/or check if we can leverage web workers
|
|
||||||
// https://thenewstack.io/leveraging-web-workers-to-safely-store-access-tokens/
|
|
||||||
window.localStorage.setItem(storageKey, JSON.stringify(config))
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'saved wallet config: ' +
|
|
||||||
'adminKey=****** ' +
|
|
||||||
`url=${config.url}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// validate config by trying to fetch wallet
|
|
||||||
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)
|
|
||||||
setStatus(Status.Error)
|
|
||||||
logger.info('wallet disabled')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const clearConfig = useCallback(() => {
|
|
||||||
window.localStorage.removeItem(storageKey)
|
|
||||||
setUrl('')
|
|
||||||
setAdminKey('')
|
|
||||||
setStatus(undefined)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadConfig().catch(console.error)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({ name: 'LNbits', url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment }),
|
|
||||||
[url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment])
|
|
||||||
return (
|
|
||||||
<LNbitsContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</LNbitsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLNbits () {
|
|
||||||
return useContext(LNbitsContext)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,215 +0,0 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { useWalletLogger } from '../logger'
|
|
||||||
import LNC from '@lightninglabs/lnc-web'
|
|
||||||
import { Status, migrateLocalStorage } 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'
|
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
import { useMe } from '../me'
|
|
||||||
import { InvoiceCanceledError, InvoiceExpiredError } from '@/components/payment'
|
|
||||||
|
|
||||||
const LNCContext = createContext()
|
|
||||||
const mutex = new Mutex()
|
|
||||||
|
|
||||||
async function getLNC ({ me }) {
|
|
||||||
if (window.lnc) return window.lnc
|
|
||||||
// backwards compatibility: migrate to new storage key
|
|
||||||
if (me) migrateLocalStorage('lnc-web:default', `lnc-web:stacker:${me.id}`)
|
|
||||||
window.lnc = new LNC({ namespace: me?.id ? `stacker:${me.id}` : undefined })
|
|
||||||
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 me = useMe()
|
|
||||||
const { logger } = useWalletLogger(Wallet.LNC)
|
|
||||||
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 unlock = useCallback(async (connect) => {
|
|
||||||
if (status === Status.Enabled) return config.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
|
|
||||||
setStatus(Status.Enabled)
|
|
||||||
setConfig({ pairingPhrase: lnc.credentials.pairingPhrase, 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</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, status])
|
|
||||||
|
|
||||||
const sendPayment = useCallback(async (bolt11) => {
|
|
||||||
const hash = bolt11Tags(bolt11).payment_hash
|
|
||||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
|
||||||
|
|
||||||
return await mutex.runExclusive(async () => {
|
|
||||||
try {
|
|
||||||
const password = await unlock()
|
|
||||||
// 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')
|
|
||||||
|
|
||||||
logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
|
|
||||||
return { preimage }
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err.message || err.toString?.()
|
|
||||||
logger.error('payment failed:', `payment_hash=${hash}`, msg)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [logger, lnc, unlock])
|
|
||||||
|
|
||||||
const saveConfig = useCallback(async config => {
|
|
||||||
setConfig(config)
|
|
||||||
|
|
||||||
try {
|
|
||||||
lnc.credentials.pairingPhrase = config.pairingPhrase
|
|
||||||
await lnc.connect()
|
|
||||||
await validateNarrowPerms(lnc)
|
|
||||||
lnc.credentials.password = config?.password || XXX_DEFAULT_PASSWORD
|
|
||||||
setStatus(Status.Enabled)
|
|
||||||
logger.ok('wallet enabled')
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('invalid config:', err)
|
|
||||||
setStatus(Status.Error)
|
|
||||||
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)
|
|
||||||
setConfig({})
|
|
||||||
logger.info('cleared config')
|
|
||||||
}, [logger, lnc])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const lnc = await getLNC({ me })
|
|
||||||
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) {
|
|
||||||
logger.error('wallet could not be loaded:', err)
|
|
||||||
setStatus(Status.Error)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}, [me, setStatus, setConfig, logger])
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({ name: 'lnc', status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig }),
|
|
||||||
[status, unlock, getInfo, sendPayment, config, saveConfig, clearConfig])
|
|
||||||
return (
|
|
||||||
<LNCContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
{modal}
|
|
||||||
</LNCContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLNC () {
|
|
||||||
return useContext(LNCContext)
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
// https://github.com/getAlby/js-sdk/blob/master/src/webln/NostrWeblnProvider.ts
|
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { Relay, finalizeEvent, nip04 } from 'nostr-tools'
|
|
||||||
import { parseNwcUrl } from '@/lib/url'
|
|
||||||
import { useWalletLogger } from '../logger'
|
|
||||||
import { Status, migrateLocalStorage } from '.'
|
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
|
||||||
import { JIT_INVOICE_TIMEOUT_MS, Wallet } from '@/lib/constants'
|
|
||||||
import { useMe } from '../me'
|
|
||||||
import { InvoiceExpiredError } from '../payment'
|
|
||||||
|
|
||||||
const NWCContext = createContext()
|
|
||||||
|
|
||||||
export function NWCProvider ({ children }) {
|
|
||||||
const me = useMe()
|
|
||||||
const [nwcUrl, setNwcUrl] = useState('')
|
|
||||||
const [walletPubkey, setWalletPubkey] = useState()
|
|
||||||
const [relayUrl, setRelayUrl] = useState()
|
|
||||||
const [secret, setSecret] = useState()
|
|
||||||
const [status, setStatus] = useState()
|
|
||||||
const { logger } = useWalletLogger(Wallet.NWC)
|
|
||||||
|
|
||||||
let storageKey = 'webln:provider:nwc'
|
|
||||||
if (me) {
|
|
||||||
storageKey = `${storageKey}:${me.id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInfo = useCallback(async (relayUrl, walletPubkey) => {
|
|
||||||
logger.info(`requesting info event from ${relayUrl}`)
|
|
||||||
|
|
||||||
let relay, sub
|
|
||||||
try {
|
|
||||||
relay = await Relay.connect(relayUrl).catch(() => {
|
|
||||||
// NOTE: passed error is undefined for some reason
|
|
||||||
const msg = `failed to connect to ${relayUrl}`
|
|
||||||
logger.error(msg)
|
|
||||||
throw new Error(msg)
|
|
||||||
})
|
|
||||||
logger.ok(`connected to ${relayUrl}`)
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const timeout = 5000
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
const msg = 'timeout waiting for info event'
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new Error(msg))
|
|
||||||
sub?.close()
|
|
||||||
}, timeout)
|
|
||||||
|
|
||||||
let found = false
|
|
||||||
sub = relay.subscribe([
|
|
||||||
{
|
|
||||||
kinds: [13194],
|
|
||||||
authors: [walletPubkey]
|
|
||||||
}
|
|
||||||
], {
|
|
||||||
onevent (event) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
found = true
|
|
||||||
logger.ok(`received info event from ${relayUrl}`)
|
|
||||||
resolve(event)
|
|
||||||
},
|
|
||||||
onclose (reason) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
if (!['closed by caller', 'relay connection closed by us'].includes(reason)) {
|
|
||||||
// only log if not closed by us (caller)
|
|
||||||
const msg = 'connection closed: ' + (reason || 'unknown reason')
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new Error(msg))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oneose () {
|
|
||||||
clearTimeout(timer)
|
|
||||||
if (!found) {
|
|
||||||
const msg = 'EOSE received without info event'
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new Error(msg))
|
|
||||||
}
|
|
||||||
sub?.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
// For some reason, websocket is already in CLOSING or CLOSED state.
|
|
||||||
// relay?.close()
|
|
||||||
if (relay) logger.info(`closed connection to ${relayUrl}`)
|
|
||||||
}
|
|
||||||
}, [logger])
|
|
||||||
|
|
||||||
const validateParams = useCallback(async ({ relayUrl, walletPubkey }) => {
|
|
||||||
// validate connection by fetching info event
|
|
||||||
// function needs to throw an error for formik validation to fail
|
|
||||||
const event = await getInfo(relayUrl, walletPubkey)
|
|
||||||
const supported = event.content.split(/[\s,]+/) // handle both spaces and commas
|
|
||||||
logger.info('supported methods:', supported)
|
|
||||||
if (!supported.includes('pay_invoice')) {
|
|
||||||
const msg = 'wallet does not support pay_invoice'
|
|
||||||
logger.error(msg)
|
|
||||||
throw new Error(msg)
|
|
||||||
}
|
|
||||||
logger.ok('wallet supports pay_invoice')
|
|
||||||
}, [logger])
|
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
|
||||||
let configStr = window.localStorage.getItem(storageKey)
|
|
||||||
setStatus(Status.Initialized)
|
|
||||||
if (!configStr) {
|
|
||||||
if (me) {
|
|
||||||
// backwards compatibility: try old storageKey
|
|
||||||
const oldStorageKey = storageKey.split(':').slice(0, -1).join(':')
|
|
||||||
configStr = migrateLocalStorage(oldStorageKey, storageKey)
|
|
||||||
}
|
|
||||||
if (!configStr) {
|
|
||||||
logger.info('no existing config found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(configStr)
|
|
||||||
|
|
||||||
const { nwcUrl } = config
|
|
||||||
setNwcUrl(nwcUrl)
|
|
||||||
|
|
||||||
const params = parseNwcUrl(nwcUrl)
|
|
||||||
setRelayUrl(params.relayUrl)
|
|
||||||
setWalletPubkey(params.walletPubkey)
|
|
||||||
setSecret(params.secret)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'loaded wallet config: ' +
|
|
||||||
'secret=****** ' +
|
|
||||||
`pubkey=${params.walletPubkey.slice(0, 6)}..${params.walletPubkey.slice(-6)} ` +
|
|
||||||
`relay=${params.relayUrl}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await validateParams(params)
|
|
||||||
setStatus(Status.Enabled)
|
|
||||||
logger.ok('wallet enabled')
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('invalid config:', err)
|
|
||||||
setStatus(Status.Error)
|
|
||||||
logger.info('wallet disabled')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}, [me, validateParams, logger])
|
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
|
||||||
// immediately store config so it's not lost even if config is invalid
|
|
||||||
const { nwcUrl } = config
|
|
||||||
setNwcUrl(nwcUrl)
|
|
||||||
if (!nwcUrl) {
|
|
||||||
setStatus(undefined)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = parseNwcUrl(nwcUrl)
|
|
||||||
setRelayUrl(params.relayUrl)
|
|
||||||
setWalletPubkey(params.walletPubkey)
|
|
||||||
setSecret(params.secret)
|
|
||||||
|
|
||||||
// XXX Even though NWC allows to configure budget,
|
|
||||||
// this is definitely not ideal from a security perspective.
|
|
||||||
window.localStorage.setItem(storageKey, JSON.stringify(config))
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'saved wallet config: ' +
|
|
||||||
'secret=****** ' +
|
|
||||||
`pubkey=${params.walletPubkey.slice(0, 6)}..${params.walletPubkey.slice(-6)} ` +
|
|
||||||
`relay=${params.relayUrl}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await validateParams(params)
|
|
||||||
setStatus(Status.Enabled)
|
|
||||||
logger.ok('wallet enabled')
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('invalid config:', err)
|
|
||||||
setStatus(Status.Error)
|
|
||||||
logger.info('wallet disabled')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}, [validateParams, logger])
|
|
||||||
|
|
||||||
const clearConfig = useCallback(() => {
|
|
||||||
window.localStorage.removeItem(storageKey)
|
|
||||||
setNwcUrl('')
|
|
||||||
setRelayUrl(undefined)
|
|
||||||
setWalletPubkey(undefined)
|
|
||||||
setSecret(undefined)
|
|
||||||
setStatus(undefined)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const sendPayment = useCallback(async (bolt11) => {
|
|
||||||
const hash = bolt11Tags(bolt11).payment_hash
|
|
||||||
logger.info('sending payment:', `payment_hash=${hash}`)
|
|
||||||
|
|
||||||
let relay, sub
|
|
||||||
try {
|
|
||||||
relay = await Relay.connect(relayUrl).catch(() => {
|
|
||||||
// NOTE: passed error is undefined for some reason
|
|
||||||
const msg = `failed to connect to ${relayUrl}`
|
|
||||||
logger.error(msg)
|
|
||||||
throw new Error(msg)
|
|
||||||
})
|
|
||||||
logger.ok(`connected to ${relayUrl}`)
|
|
||||||
const ret = await new Promise(function (resolve, reject) {
|
|
||||||
(async function () {
|
|
||||||
// timeout since NWC is async (user needs to confirm payment in wallet)
|
|
||||||
// timeout is same as invoice expiry
|
|
||||||
const timeout = JIT_INVOICE_TIMEOUT_MS
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
const msg = 'timeout waiting for payment'
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new InvoiceExpiredError(hash))
|
|
||||||
sub?.close()
|
|
||||||
}, timeout)
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
method: 'pay_invoice',
|
|
||||||
params: { invoice: bolt11 }
|
|
||||||
}
|
|
||||||
const content = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
|
||||||
|
|
||||||
const request = finalizeEvent({
|
|
||||||
kind: 23194,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [['p', walletPubkey]],
|
|
||||||
content
|
|
||||||
}, secret)
|
|
||||||
await relay.publish(request)
|
|
||||||
|
|
||||||
const filter = {
|
|
||||||
kinds: [23195],
|
|
||||||
authors: [walletPubkey],
|
|
||||||
'#e': [request.id]
|
|
||||||
}
|
|
||||||
sub = relay.subscribe([filter], {
|
|
||||||
async onevent (response) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
try {
|
|
||||||
const content = JSON.parse(await nip04.decrypt(secret, walletPubkey, response.content))
|
|
||||||
if (content.error) return reject(new Error(content.error.message))
|
|
||||||
if (content.result) return resolve({ preimage: content.result.preimage })
|
|
||||||
} catch (err) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose (reason) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
if (!['closed by caller', 'relay connection closed by us'].includes(reason)) {
|
|
||||||
// only log if not closed by us (caller)
|
|
||||||
const msg = 'connection closed: ' + (reason || 'unknown reason')
|
|
||||||
logger.error(msg)
|
|
||||||
reject(new Error(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})().catch(reject)
|
|
||||||
})
|
|
||||||
const preimage = ret.preimage
|
|
||||||
logger.ok('payment successful:', `payment_hash=${hash}`, `preimage=${preimage}`)
|
|
||||||
return ret
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
// For some reason, websocket is already in CLOSING or CLOSED state.
|
|
||||||
// relay?.close()
|
|
||||||
if (relay) logger.info(`closed connection to ${relayUrl}`)
|
|
||||||
}
|
|
||||||
}, [walletPubkey, relayUrl, secret, logger])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadConfig().catch(err => logger.error(err.message || err.toString?.()))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const value = useMemo(
|
|
||||||
() => ({ name: 'NWC', nwcUrl, relayUrl, walletPubkey, secret, status, saveConfig, clearConfig, getInfo, sendPayment }),
|
|
||||||
[nwcUrl, relayUrl, walletPubkey, secret, status, saveConfig, clearConfig, getInfo, sendPayment])
|
|
||||||
return (
|
|
||||||
<NWCContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</NWCContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNWC () {
|
|
||||||
return useContext(NWCContext)
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
|
||||||
import { Form, Input } from '@/components/form'
|
|
||||||
import { CenterLayout } from '@/components/layout'
|
|
||||||
import { useMe } from '@/components/me'
|
|
||||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
|
||||||
import { useApolloClient, useMutation } from '@apollo/client'
|
|
||||||
import { useToast } from '@/components/toast'
|
|
||||||
import { CLNAutowithdrawSchema } from '@/lib/validate'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
|
|
||||||
import { REMOVE_WALLET, UPSERT_WALLET_CLN, WALLET_BY_TYPE } from '@/fragments/wallet'
|
|
||||||
import WalletLogs from '@/components/wallet-logs'
|
|
||||||
import Info from '@/components/info'
|
|
||||||
import Text from '@/components/text'
|
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
|
|
||||||
const variables = { type: Wallet.CLN.type }
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
|
||||||
|
|
||||||
export default function CLN ({ ssrData }) {
|
|
||||||
const me = useMe()
|
|
||||||
const toaster = useToast()
|
|
||||||
const router = useRouter()
|
|
||||||
const client = useApolloClient()
|
|
||||||
const [upsertWalletCLN] = useMutation(UPSERT_WALLET_CLN, {
|
|
||||||
refetchQueries: ['WalletLogs'],
|
|
||||||
onError: (err) => {
|
|
||||||
client.refetchQueries({ include: ['WalletLogs'] })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const [removeWallet] = useMutation(REMOVE_WALLET, {
|
|
||||||
refetchQueries: ['WalletLogs'],
|
|
||||||
onError: (err) => {
|
|
||||||
client.refetchQueries({ include: ['WalletLogs'] })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { walletByType: wallet } = ssrData || {}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CenterLayout>
|
|
||||||
<h2 className='pb-2'>CLN</h2>
|
|
||||||
<h6 className='text-muted text-center'>autowithdraw to your Core Lightning node via <a href='https://docs.corelightning.org/docs/rest' target='_blank' noreferrer rel='noreferrer'>CLNRest</a></h6>
|
|
||||||
<Form
|
|
||||||
initial={{
|
|
||||||
socket: wallet?.wallet?.socket || '',
|
|
||||||
rune: wallet?.wallet?.rune || '',
|
|
||||||
cert: wallet?.wallet?.cert || '',
|
|
||||||
...autowithdrawInitial({ me, priority: wallet?.priority })
|
|
||||||
}}
|
|
||||||
schema={CLNAutowithdrawSchema({ me })}
|
|
||||||
onSubmit={async ({ socket, rune, cert, ...settings }) => {
|
|
||||||
try {
|
|
||||||
await upsertWalletCLN({
|
|
||||||
variables: {
|
|
||||||
id: wallet?.id,
|
|
||||||
socket,
|
|
||||||
rune,
|
|
||||||
cert,
|
|
||||||
settings: {
|
|
||||||
...settings,
|
|
||||||
autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
|
|
||||||
autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label='rest host and port'
|
|
||||||
name='socket'
|
|
||||||
hint='tor or clearnet'
|
|
||||||
placeholder='55.5.555.55:3010'
|
|
||||||
clear
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={
|
|
||||||
<div className='d-flex align-items-center'>invoice only rune
|
|
||||||
<Info>
|
|
||||||
<Text>
|
|
||||||
{'We only accept runes that *only* allow `method=invoice`.\nRun this to generate one:\n\n```lightning-cli createrune restrictions=\'["method=invoice"]\'```'}
|
|
||||||
</Text>
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
name='rune'
|
|
||||||
clear
|
|
||||||
hint='must be restricted to method=invoice'
|
|
||||||
placeholder='S34KtUW-6gqS_hD_9cc_PNhfF-NinZyBOCgr1aIrark9NCZtZXRob2Q9aW52b2ljZQ=='
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={<>cert <small className='text-muted ms-2'>optional if from <a href='https://en.wikipedia.org/wiki/Certificate_authority' target='_blank' rel='noreferrer'>CA</a> (e.g. voltage)</small></>}
|
|
||||||
name='cert'
|
|
||||||
clear
|
|
||||||
hint='hex or base64 encoded'
|
|
||||||
placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
|
|
||||||
/>
|
|
||||||
<AutowithdrawSettings />
|
|
||||||
<WalletButtonBar
|
|
||||||
status={!!wallet} onDelete={async () => {
|
|
||||||
try {
|
|
||||||
await removeWallet({ variables: { id: wallet?.id } })
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className='mt-3 w-100'>
|
|
||||||
<WalletLogs wallet={Wallet.CLN} embedded />
|
|
||||||
</div>
|
|
||||||
</CenterLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CLNCard ({ wallet }) {
|
|
||||||
return (
|
|
||||||
<WalletCard
|
|
||||||
title='CLN'
|
|
||||||
badges={['receive only', 'non-custodial']}
|
|
||||||
provider='cln'
|
|
||||||
status={wallet !== undefined || undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -2,28 +2,18 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
import Layout from '@/components/layout'
|
import Layout from '@/components/layout'
|
||||||
import styles from '@/styles/wallet.module.css'
|
import styles from '@/styles/wallet.module.css'
|
||||||
import { WalletCard } from '@/components/wallet-card'
|
import { WalletCard } from '@/components/wallet-card'
|
||||||
import { LightningAddressWalletCard } from './lightning-address'
|
|
||||||
import { LNbitsCard } from './lnbits'
|
|
||||||
import { NWCCard } from './nwc'
|
|
||||||
import { LNDCard } from './lnd'
|
|
||||||
import { CLNCard } from './cln'
|
|
||||||
import { WALLETS } from '@/fragments/wallet'
|
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'
|
import Link from 'next/link'
|
||||||
import { Wallet as W } from '@/lib/constants'
|
|
||||||
|
const wallets = [
|
||||||
|
await import('@/components/webln/lnbits')
|
||||||
|
]
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
|
||||||
|
|
||||||
export default function Wallet ({ ssrData }) {
|
export default function Wallet ({ ssrData }) {
|
||||||
const { data } = useQuery(WALLETS)
|
// TODO: set wallet status
|
||||||
|
// TODO: load server wallets
|
||||||
if (!data && !ssrData) return <PageLoading />
|
|
||||||
const { wallets } = data || ssrData
|
|
||||||
const lnd = wallets.find(w => w.type === W.LND.type)
|
|
||||||
const lnaddr = wallets.find(w => w.type === W.LnAddr.type)
|
|
||||||
const cln = wallets.find(w => w.type === W.CLN.type)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -36,15 +26,9 @@ export default function Wallet ({ ssrData }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.walletGrid}>
|
<div className={styles.walletGrid}>
|
||||||
<LightningAddressWalletCard wallet={lnaddr} />
|
{wallets.map((w, i) => (
|
||||||
<LNDCard wallet={lnd} />
|
<WalletCard key={i} {...w.config.card} />
|
||||||
<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']} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
|
||||||
import { Form, Input } from '@/components/form'
|
|
||||||
import { CenterLayout } from '@/components/layout'
|
|
||||||
import { useMe } from '@/components/me'
|
|
||||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
|
||||||
import { useApolloClient, useMutation } from '@apollo/client'
|
|
||||||
import { useToast } from '@/components/toast'
|
|
||||||
import { lnAddrAutowithdrawSchema } from '@/lib/validate'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
|
|
||||||
import { REMOVE_WALLET, UPSERT_WALLET_LNADDR, WALLET_BY_TYPE } from '@/fragments/wallet'
|
|
||||||
import WalletLogs from '@/components/wallet-logs'
|
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
|
|
||||||
const variables = { type: Wallet.LnAddr.type }
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
|
||||||
|
|
||||||
export default function LightningAddress ({ ssrData }) {
|
|
||||||
const me = useMe()
|
|
||||||
const toaster = useToast()
|
|
||||||
const router = useRouter()
|
|
||||||
const client = useApolloClient()
|
|
||||||
const [upsertWalletLNAddr] = useMutation(UPSERT_WALLET_LNADDR, {
|
|
||||||
refetchQueries: ['WalletLogs'],
|
|
||||||
onError: (err) => {
|
|
||||||
client.refetchQueries({ include: ['WalletLogs'] })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const [removeWallet] = useMutation(REMOVE_WALLET, {
|
|
||||||
refetchQueries: ['WalletLogs'],
|
|
||||||
onError: (err) => {
|
|
||||||
client.refetchQueries({ include: ['WalletLogs'] })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { walletByType: wallet } = ssrData || {}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CenterLayout>
|
|
||||||
<h2 className='pb-2'>lightning address</h2>
|
|
||||||
<h6 className='text-muted text-center pb-3'>autowithdraw to a lightning address</h6>
|
|
||||||
<Form
|
|
||||||
initial={{
|
|
||||||
address: wallet?.wallet?.address || '',
|
|
||||||
...autowithdrawInitial({ me, priority: wallet?.priority })
|
|
||||||
}}
|
|
||||||
schema={lnAddrAutowithdrawSchema({ me })}
|
|
||||||
onSubmit={async ({ address, ...settings }) => {
|
|
||||||
try {
|
|
||||||
await upsertWalletLNAddr({
|
|
||||||
variables: {
|
|
||||||
id: wallet?.id,
|
|
||||||
address,
|
|
||||||
settings: {
|
|
||||||
...settings,
|
|
||||||
autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
|
|
||||||
autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label='lightning address'
|
|
||||||
name='address'
|
|
||||||
autoComplete='off'
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<AutowithdrawSettings />
|
|
||||||
<WalletButtonBar
|
|
||||||
status={!!wallet} onDelete={async () => {
|
|
||||||
try {
|
|
||||||
await removeWallet({ variables: { id: wallet?.id } })
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className='mt-3 w-100'>
|
|
||||||
<WalletLogs wallet={Wallet.LnAddr} embedded />
|
|
||||||
</div>
|
|
||||||
</CenterLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LightningAddressWalletCard ({ wallet }) {
|
|
||||||
return (
|
|
||||||
<WalletCard
|
|
||||||
title='lightning address'
|
|
||||||
badges={['receive only', 'non-custodialish']}
|
|
||||||
provider='lightning-address'
|
|
||||||
status={wallet !== undefined || undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,27 +1,32 @@
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
|
import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
|
||||||
import { CenterLayout } from '@/components/layout'
|
import { CenterLayout } from '@/components/layout'
|
||||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
|
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
||||||
import { lnbitsSchema } from '@/lib/validate'
|
import { lnbitsSchema } from '@/lib/validate'
|
||||||
import { useToast } from '@/components/toast'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useLNbits } from '@/components/webln/lnbits'
|
import { useLNbits } from '@/components/webln/lnbits'
|
||||||
import { WalletSecurityBanner } from '@/components/banners'
|
import { WalletSecurityBanner } from '@/components/banners'
|
||||||
import { useWebLNConfigurator } from '@/components/webln'
|
|
||||||
import WalletLogs from '@/components/wallet-logs'
|
import WalletLogs from '@/components/wallet-logs'
|
||||||
import { Wallet } from '@/lib/constants'
|
import { Wallet } from '@/lib/constants'
|
||||||
|
import useLocalState from '@/components/use-local-state'
|
||||||
|
import { useMe } from '@/components/me'
|
||||||
|
import { useToast } from '@/components/toast'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Status } from '@/components/webln'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
|
||||||
export default function LNbits () {
|
export default function LNbits () {
|
||||||
const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
|
const me = useMe()
|
||||||
const lnbits = useLNbits()
|
|
||||||
const { name, url, adminKey, saveConfig, clearConfig, status } = lnbits
|
|
||||||
const isDefault = provider?.name === name
|
|
||||||
const configured = isConfigured(status)
|
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
let storageKey = 'webln:provider:lnbits'
|
||||||
|
if (me) {
|
||||||
|
storageKey = `${storageKey}:${me.id}`
|
||||||
|
}
|
||||||
|
const [config, setConfig, clearConfig] = useLocalState(storageKey)
|
||||||
|
const status = config ? Status.Initialized : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenterLayout>
|
<CenterLayout>
|
||||||
<h2 className='pb-2'>LNbits</h2>
|
<h2 className='pb-2'>LNbits</h2>
|
||||||
|
@ -29,15 +34,14 @@ export default function LNbits () {
|
||||||
<WalletSecurityBanner />
|
<WalletSecurityBanner />
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
url: url || '',
|
url: config?.url || '',
|
||||||
adminKey: adminKey || '',
|
adminKey: config?.adminKey || '',
|
||||||
isDefault: isDefault || false
|
isDefault: config?.isDefault || false
|
||||||
}}
|
}}
|
||||||
schema={lnbitsSchema}
|
schema={lnbitsSchema}
|
||||||
onSubmit={async ({ isDefault, ...values }) => {
|
onSubmit={async ({ isDefault, ...values }) => {
|
||||||
try {
|
try {
|
||||||
await saveConfig(values)
|
await setConfig(values)
|
||||||
if (isDefault) setProvider(lnbits)
|
|
||||||
toaster.success('saved settings')
|
toaster.success('saved settings')
|
||||||
router.push('/settings/wallets')
|
router.push('/settings/wallets')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -47,29 +51,29 @@ export default function LNbits () {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClientInput
|
<ClientInput
|
||||||
initialValue={url}
|
initialValue={config?.url}
|
||||||
label='lnbits url'
|
label='lnbits url'
|
||||||
name='url'
|
name='url'
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
initialValue={adminKey}
|
initialValue={config?.adminKey}
|
||||||
label='admin key'
|
label='admin key'
|
||||||
name='adminKey'
|
name='adminKey'
|
||||||
newPass
|
newPass
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<ClientCheckbox
|
<ClientCheckbox
|
||||||
disabled={!configured || isDefault || enabledProviders.length === 1}
|
disabled={false}
|
||||||
initialValue={isDefault}
|
initialValue={false}
|
||||||
label='default payment method'
|
label='default payment method'
|
||||||
name='isDefault'
|
name='isDefault'
|
||||||
/>
|
/>
|
||||||
<WalletButtonBar
|
<WalletButtonBar
|
||||||
status={status} onDelete={async () => {
|
status={status} onDelete={async () => {
|
||||||
try {
|
try {
|
||||||
await clearConfig()
|
clearConfig()
|
||||||
toaster.success('saved settings')
|
toaster.success('saved settings')
|
||||||
router.push('/settings/wallets')
|
router.push('/settings/wallets')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
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, isConfigured } 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'
|
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
|
|
||||||
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)
|
|
||||||
const configured = isConfigured(status)
|
|
||||||
|
|
||||||
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 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 --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.'}
|
|
||||||
</Text>
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
name='pairingPhrase'
|
|
||||||
initialValue={config?.pairingPhrase}
|
|
||||||
newPass={config?.pairingPhrase === undefined}
|
|
||||||
readOnly={configured}
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<PasswordInput
|
|
||||||
label={<>password <small className='text-muted ms-2'>optional</small></>}
|
|
||||||
name='password'
|
|
||||||
initialValue={defaultPassword ? '' : config?.password}
|
|
||||||
newPass={config?.password === undefined || defaultPassword}
|
|
||||||
readOnly={configured}
|
|
||||||
hint='encrypts your pairing phrase when stored locally'
|
|
||||||
/>
|
|
||||||
<ClientCheckbox
|
|
||||||
disabled={!configured || 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 detach: ' + err.message || err.toString?.())
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className='mt-3 w-100'>
|
|
||||||
<WalletLogs wallet={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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
|
||||||
import { Form, Input } from '@/components/form'
|
|
||||||
import { CenterLayout } from '@/components/layout'
|
|
||||||
import { useMe } from '@/components/me'
|
|
||||||
import { WalletButtonBar, WalletCard } from '@/components/wallet-card'
|
|
||||||
import { useApolloClient, useMutation } from '@apollo/client'
|
|
||||||
import { useToast } from '@/components/toast'
|
|
||||||
import { LNDAutowithdrawSchema } from '@/lib/validate'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
|
|
||||||
import { REMOVE_WALLET, UPSERT_WALLET_LND, WALLET_BY_TYPE } from '@/fragments/wallet'
|
|
||||||
import Info from '@/components/info'
|
|
||||||
import Text from '@/components/text'
|
|
||||||
import WalletLogs from '@/components/wallet-logs'
|
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
|
|
||||||
const variables = { type: Wallet.LND.type }
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
|
||||||
|
|
||||||
export default function LND ({ ssrData }) {
|
|
||||||
const me = useMe()
|
|
||||||
const toaster = useToast()
|
|
||||||
const router = useRouter()
|
|
||||||
const client = useApolloClient()
|
|
||||||
const [upsertWalletLND] = useMutation(UPSERT_WALLET_LND, {
|
|
||||||
refetchQueries: ['WalletLogs'],
|
|
||||||
onError: (err) => {
|
|
||||||
client.refetchQueries({ include: ['WalletLogs'] })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const [removeWallet] = useMutation(REMOVE_WALLET, {
|
|
||||||
refetchQueries: ['WalletLogs'],
|
|
||||||
onError: (err) => {
|
|
||||||
client.refetchQueries({ include: ['WalletLogs'] })
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { walletByType: wallet } = ssrData || {}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CenterLayout>
|
|
||||||
<h2 className='pb-2'>LND</h2>
|
|
||||||
<h6 className='text-muted text-center pb-3'>autowithdraw to your Lightning Labs node</h6>
|
|
||||||
<Form
|
|
||||||
initial={{
|
|
||||||
socket: wallet?.wallet?.socket || '',
|
|
||||||
macaroon: wallet?.wallet?.macaroon || '',
|
|
||||||
cert: wallet?.wallet?.cert || '',
|
|
||||||
...autowithdrawInitial({ me, priority: wallet?.priority })
|
|
||||||
}}
|
|
||||||
schema={LNDAutowithdrawSchema({ me })}
|
|
||||||
onSubmit={async ({ socket, cert, macaroon, ...settings }) => {
|
|
||||||
try {
|
|
||||||
await upsertWalletLND({
|
|
||||||
variables: {
|
|
||||||
id: wallet?.id,
|
|
||||||
socket,
|
|
||||||
macaroon,
|
|
||||||
cert,
|
|
||||||
settings: {
|
|
||||||
...settings,
|
|
||||||
autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
|
|
||||||
autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
label='grpc host and port'
|
|
||||||
name='socket'
|
|
||||||
hint='tor or clearnet'
|
|
||||||
placeholder='55.5.555.55:10001'
|
|
||||||
clear
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={
|
|
||||||
<div className='d-flex align-items-center'>invoice macaroon
|
|
||||||
<Info label='privacy tip'>
|
|
||||||
<Text>
|
|
||||||
{'We accept a prebaked ***invoice.macaroon*** for your convenience. To gain better privacy, generate a new macaroon as follows:\n\n```lncli bakemacaroon invoices:write invoices:read```'}
|
|
||||||
</Text>
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
name='macaroon'
|
|
||||||
clear
|
|
||||||
hint='hex or base64 encoded'
|
|
||||||
placeholder='AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs'
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={<>cert <small className='text-muted ms-2'>optional if from <a href='https://en.wikipedia.org/wiki/Certificate_authority' target='_blank' rel='noreferrer'>CA</a> (e.g. voltage)</small></>}
|
|
||||||
name='cert'
|
|
||||||
clear
|
|
||||||
hint='hex or base64 encoded'
|
|
||||||
placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
|
|
||||||
/>
|
|
||||||
<AutowithdrawSettings />
|
|
||||||
<WalletButtonBar
|
|
||||||
status={!!wallet} onDelete={async () => {
|
|
||||||
try {
|
|
||||||
await removeWallet({ variables: { id: wallet?.id } })
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className='mt-3 w-100'>
|
|
||||||
<WalletLogs wallet={Wallet.LND} embedded />
|
|
||||||
</div>
|
|
||||||
</CenterLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LNDCard ({ wallet }) {
|
|
||||||
return (
|
|
||||||
<WalletCard
|
|
||||||
title='LND'
|
|
||||||
badges={['receive only', 'non-custodial']}
|
|
||||||
provider='lnd'
|
|
||||||
status={wallet !== undefined || undefined}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
|
||||||
import { Form, ClientCheckbox, PasswordInput } from '@/components/form'
|
|
||||||
import { CenterLayout } from '@/components/layout'
|
|
||||||
import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
|
|
||||||
import { nwcSchema } from '@/lib/validate'
|
|
||||||
import { useToast } from '@/components/toast'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useNWC } from '@/components/webln/nwc'
|
|
||||||
import { WalletSecurityBanner } from '@/components/banners'
|
|
||||||
import { useWebLNConfigurator } from '@/components/webln'
|
|
||||||
import WalletLogs from '@/components/wallet-logs'
|
|
||||||
import { Wallet } from '@/lib/constants'
|
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
|
||||||
|
|
||||||
export default function NWC () {
|
|
||||||
const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
|
|
||||||
const nwc = useNWC()
|
|
||||||
const { name, nwcUrl, saveConfig, clearConfig, status } = nwc
|
|
||||||
const isDefault = provider?.name === name
|
|
||||||
const configured = isConfigured(status)
|
|
||||||
const toaster = useToast()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CenterLayout>
|
|
||||||
<h2 className='pb-2'>Nostr Wallet Connect</h2>
|
|
||||||
<h6 className='text-muted text-center pb-3'>use Nostr Wallet Connect for payments</h6>
|
|
||||||
<WalletSecurityBanner />
|
|
||||||
<Form
|
|
||||||
initial={{
|
|
||||||
nwcUrl: nwcUrl || '',
|
|
||||||
isDefault: isDefault || false
|
|
||||||
}}
|
|
||||||
schema={nwcSchema}
|
|
||||||
onSubmit={async ({ isDefault, ...values }) => {
|
|
||||||
try {
|
|
||||||
await saveConfig(values)
|
|
||||||
if (isDefault) setProvider(nwc)
|
|
||||||
toaster.success('saved settings')
|
|
||||||
router.push('/settings/wallets')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toaster.danger('failed to attach: ' + err.message || err.toString?.())
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PasswordInput
|
|
||||||
initialValue={nwcUrl}
|
|
||||||
label='connection'
|
|
||||||
name='nwcUrl'
|
|
||||||
newPass
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<ClientCheckbox
|
|
||||||
disabled={!configured || 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 detach: ' + err.message || err.toString?.())
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
<div className='mt-3 w-100'>
|
|
||||||
<WalletLogs wallet={Wallet.NWC} embedded />
|
|
||||||
</div>
|
|
||||||
</CenterLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NWCCard () {
|
|
||||||
const { status } = useNWC()
|
|
||||||
return (
|
|
||||||
<WalletCard
|
|
||||||
title='NWC'
|
|
||||||
badges={['send only', 'non-custodialish', 'budgetable']}
|
|
||||||
provider='nwc'
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
Loading…
Reference in New Issue