Compare commits

..

2 Commits

Author SHA1 Message Date
ekzyis
34bfa89e74 wip 2024-06-17 02:59:51 -05:00
ekzyis
bb8c1ccffc refactor webln 2024-06-03 17:41:15 -05:00
7 changed files with 261 additions and 51 deletions

View File

@ -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>
) )
} }

View File

@ -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>
)
}

View File

@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState }
import { LNbitsProvider, useLNbits } from './lnbits' import { LNbitsProvider, useLNbits } from './lnbits'
import { NWCProvider, useNWC } from './nwc' import { NWCProvider, useNWC } from './nwc'
import { LNCProvider, useLNC } from './lnc' import { LNCProvider, useLNC } from './lnc'
import lnbits from './lnbits2'
const WebLNContext = createContext({}) const WebLNContext = createContext({})

View File

@ -0,0 +1,22 @@
import { createContext, useContext, useMemo, useState } from 'react'
import lnbits from './lnbits2'
const storageKey = 'webln:providers'
const WebLNContext = createContext({})
export function useWebLN () {
const { provider } = useContext(WebLNContext)
return provider
}
export function RawWebLNProvider ({ children }) {
const [provider, setProvider] = useState()
const value = useMemo(() => ({ provider, setProvider }), [])
return (
<WebLNContext.Provider value={value}>
{children}
</WebLNContext.Provider>
)
}

View File

@ -67,8 +67,7 @@ const getPayment = async (baseUrl, adminKey, paymentHash) => {
export function LNbitsProvider ({ children }) { export function LNbitsProvider ({ children }) {
const me = useMe() const me = useMe()
const [url, setUrl] = useState('') const [config, setConfig] = useState({})
const [adminKey, setAdminKey] = useState('')
const [status, setStatus] = useState() const [status, setStatus] = useState()
const { logger } = useWalletLogger(Wallet.LNbits) const { logger } = useWalletLogger(Wallet.LNbits)
@ -78,7 +77,7 @@ export function LNbitsProvider ({ children }) {
} }
const getInfo = useCallback(async () => { const getInfo = useCallback(async () => {
const response = await getWallet(url, adminKey) const response = await getWallet(config.url, config.adminKey)
return { return {
node: { node: {
alias: response.name, alias: response.name,
@ -92,9 +91,11 @@ export function LNbitsProvider ({ children }) {
version: '1.0', version: '1.0',
supports: ['lightning'] supports: ['lightning']
} }
}, [url, adminKey]) }, [config])
const sendPayment = useCallback(async (bolt11) => { const sendPayment = useCallback(async (bolt11) => {
const { url, adminKey } = config
const hash = bolt11Tags(bolt11).payment_hash const hash = bolt11Tags(bolt11).payment_hash
logger.info('sending payment:', `payment_hash=${hash}`) logger.info('sending payment:', `payment_hash=${hash}`)
@ -111,7 +112,7 @@ export function LNbitsProvider ({ children }) {
logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.()) logger.error('payment failed:', `payment_hash=${hash}`, err.message || err.toString?.())
throw err throw err
} }
}, [logger, url, adminKey]) }, [logger, config])
const loadConfig = useCallback(async () => { const loadConfig = useCallback(async () => {
let configStr = window.localStorage.getItem(storageKey) let configStr = window.localStorage.getItem(storageKey)
@ -129,20 +130,17 @@ export function LNbitsProvider ({ children }) {
} }
const config = JSON.parse(configStr) const config = JSON.parse(configStr)
setConfig(config)
const { url, adminKey } = config
setUrl(url)
setAdminKey(adminKey)
logger.info( logger.info(
'loaded wallet config: ' + 'loaded wallet config: ' +
'adminKey=****** ' + 'adminKey=****** ' +
`url=${url}`) `url=${config.url}`)
try { try {
// validate config by trying to fetch wallet // validate config by trying to fetch wallet
logger.info('trying to fetch wallet') logger.info('trying to fetch wallet')
await getWallet(url, adminKey) await getWallet(config.url, config.adminKey)
logger.ok('wallet found') logger.ok('wallet found')
setStatus(Status.Enabled) setStatus(Status.Enabled)
logger.ok('wallet enabled') logger.ok('wallet enabled')
@ -156,8 +154,7 @@ export function LNbitsProvider ({ children }) {
const saveConfig = useCallback(async (config) => { const saveConfig = useCallback(async (config) => {
// immediately store config so it's not lost even if config is invalid // immediately store config so it's not lost even if config is invalid
setUrl(config.url) setConfig(config)
setAdminKey(config.adminKey)
// XXX This is insecure, XSS vulns could lead to loss of funds! // 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 // -> check how mutiny encrypts their wallet and/or check if we can leverage web workers
@ -186,8 +183,7 @@ export function LNbitsProvider ({ children }) {
const clearConfig = useCallback(() => { const clearConfig = useCallback(() => {
window.localStorage.removeItem(storageKey) window.localStorage.removeItem(storageKey)
setUrl('') setConfig({})
setAdminKey('')
setStatus(undefined) setStatus(undefined)
}, []) }, [])
@ -196,8 +192,8 @@ export function LNbitsProvider ({ children }) {
}, []) }, [])
const value = useMemo( const value = useMemo(
() => ({ name: 'LNbits', url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment }), () => ({ name: 'LNbits', ...config, status, saveConfig, clearConfig, getInfo, sendPayment }),
[url, adminKey, status, saveConfig, clearConfig, getInfo, sendPayment]) [config, status, saveConfig, clearConfig, getInfo, sendPayment])
return ( return (
<LNbitsContext.Provider value={value}> <LNbitsContext.Provider value={value}>
{children} {children}

124
components/webln/lnbits2.js Normal file
View File

@ -0,0 +1,124 @@
import { bolt11Tags } from '@/lib/bolt11'
export const name = 'LNbits'
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'
}
}
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 path = '/api/v1/wallet'
const headers = new Headers()
headers.append('Accept', 'application/json')
headers.append('Content-Type', 'application/json')
headers.append('X-Api-Key', adminKey)
const res = await fetch(url + path, { method: 'GET', headers })
if (!res.ok) {
const errBody = await res.json()
throw new Error(errBody.detail)
}
const wallet = await res.json()
return wallet
}
async function postPayment (baseUrl, adminKey, bolt11) {
const url = baseUrl.replace(/\/+$/, '')
const path = '/api/v1/payments'
const headers = new Headers()
headers.append('Accept', 'application/json')
headers.append('Content-Type', 'application/json')
headers.append('X-Api-Key', adminKey)
const body = JSON.stringify({ bolt11, out: true })
const res = await fetch(url + path, { method: 'POST', headers, body })
if (!res.ok) {
const errBody = await res.json()
throw new Error(errBody.detail)
}
const payment = await res.json()
return payment
}
async function getPayment (baseUrl, adminKey, paymentHash) {
const url = baseUrl.replace(/\/+$/, '')
const path = `/api/v1/payments/${paymentHash}`
const headers = new Headers()
headers.append('Accept', 'application/json')
headers.append('Content-Type', 'application/json')
headers.append('X-Api-Key', adminKey)
const res = await fetch(url + path, { method: 'GET', headers })
if (!res.ok) {
const errBody = await res.json()
throw new Error(errBody.detail)
}
const payment = await res.json()
return payment
}

View File

@ -2,28 +2,32 @@ 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 { LightningAddressWalletCard } from './lightning-address'
import { LNbitsCard } from './lnbits' // import { LNbitsCard } from './lnbits'
import { NWCCard } from './nwc' // import { NWCCard } from './nwc'
import { LNDCard } from './lnd' // import { LNDCard } from './lnd'
import { CLNCard } from './cln' // import { CLNCard } from './cln'
import { WALLETS } from '@/fragments/wallet' import { WALLETS } from '@/fragments/wallet'
import { useQuery } from '@apollo/client' // import { useQuery } from '@apollo/client'
import PageLoading from '@/components/page-loading' // import PageLoading from '@/components/page-loading'
import { LNCCard } from './lnc' // import { LNCCard } from './lnc'
import Link from 'next/link' import Link from 'next/link'
import { Wallet as W } from '@/lib/constants' // import { Wallet as W } from '@/lib/constants'
import { config as lnbitsConfig } from '@/components/webln/lnbits2'
// TODO: load configs without individual imports?
const walletConfigs = [lnbitsConfig]
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) // const { data } = useQuery(WALLETS)
//
if (!data && !ssrData) return <PageLoading /> // if (!data && !ssrData) return <PageLoading />
const { wallets } = data || ssrData // const { wallets } = data || ssrData
const lnd = wallets.find(w => w.type === W.LND.type) // const lnd = wallets.find(w => w.type === W.LND.type)
const lnaddr = wallets.find(w => w.type === W.LnAddr.type) // const lnaddr = wallets.find(w => w.type === W.LnAddr.type)
const cln = wallets.find(w => w.type === W.CLN.type) // const cln = wallets.find(w => w.type === W.CLN.type)
return ( return (
<Layout> <Layout>
@ -36,15 +40,9 @@ export default function Wallet ({ ssrData }) {
</Link> </Link>
</div> </div>
<div className={styles.walletGrid}> <div className={styles.walletGrid}>
<LightningAddressWalletCard wallet={lnaddr} /> {walletConfigs.map((config, i) => (
<LNDCard wallet={lnd} /> <WalletCard key={i} {...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>