diff --git a/components/invoice.js b/components/invoice.js
index c5ca23f2..2ca71b13 100644
--- a/components/invoice.js
+++ b/components/invoice.js
@@ -8,17 +8,13 @@ import Bolt11Info from './bolt11-info'
import { useQuery } from '@apollo/client'
import { INVOICE } from '@/fragments/wallet'
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
-import { WebLnNotEnabledError } from './payment'
+import { NoAttachedWalletError } from './payment'
import ItemJob from './item-job'
import Item from './item'
import { CommentFlat } from './comment'
import classNames from 'classnames'
-export default function Invoice ({
- id, query = INVOICE, modal, onPayment, onCanceled,
- info, successVerb, webLn = true, webLnError,
- poll, waitFor, ...props
-}) {
+export default function Invoice ({ id, query = INVOICE, modal, onPayment, info, successVerb, useWallet = true, walletError, poll, waitFor, ...props }) {
const [expired, setExpired] = useState(false)
const { data, error } = useQuery(query, SSR
? {}
@@ -58,15 +54,15 @@ export default function Invoice ({
if (invoice.cancelled) {
variant = 'failed'
status = 'cancelled'
- webLn = false
+ useWallet = false
} else if (invoice.confirmedAt || (invoice.isHeld && invoice.satsReceived && !expired)) {
variant = 'confirmed'
status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb || 'deposited'}`
- webLn = false
+ useWallet = false
} else if (expired) {
variant = 'failed'
status = 'expired'
- webLn = false
+ useWallet = false
} else if (invoice.expiresAt) {
variant = 'pending'
status = (
@@ -82,13 +78,13 @@ export default function Invoice ({
return (
<>
- {webLnError && !(webLnError instanceof WebLnNotEnabledError) &&
+ {walletError && !(walletError instanceof NoAttachedWalletError) &&
Paying from attached wallet failed:
- {webLnError.message}
+ {walletError.message}
}
diff --git a/components/logger.js b/components/logger.js
index 28092de8..9f2923c1 100644
--- a/components/logger.js
+++ b/components/logger.js
@@ -4,6 +4,8 @@ import fancyNames from '@/lib/fancy-names.json'
import { gql, useMutation, useQuery } from '@apollo/client'
import { WALLET_LOGS } from '@/fragments/wallet'
import { getWalletBy } from '@/lib/constants'
+// TODO: why can't I import this without errors?
+// import { getWalletByName } from './wallet'
const generateFancyName = () => {
// 100 adjectives * 100 nouns * 10000 = 100M possible names
@@ -245,21 +247,23 @@ const WalletLoggerProvider = ({ children }) => {
return () => idb.current?.close()
}, [])
- const appendLog = useCallback((wallet, level, message) => {
- const log = { wallet: wallet.logTag, level, message, ts: +new Date() }
+ const appendLog = useCallback((walletName, level, message) => {
+ const log = { wallet: walletName, level, message, ts: +new Date() }
saveLog(log)
setLogs((prevLogs) => [...prevLogs, log])
}, [saveLog])
- const deleteLogs = useCallback(async (wallet) => {
- if (!wallet || wallet.server) {
+ const deleteLogs = useCallback(async (walletName) => {
+ const wallet = getWalletByName(walletName, me)
+
+ if (!walletName || wallet.server) {
await deleteServerWalletLogs({ variables: { wallet: wallet?.type } })
}
- if (!wallet || !wallet.server) {
+ if (!walletName || !wallet.server) {
const tx = idb.current.transaction(idbStoreName, 'readwrite')
const objectStore = tx.objectStore(idbStoreName)
const idx = objectStore.index('wallet_ts')
- const request = wallet ? idx.openCursor(window.IDBKeyRange.bound([wallet.logTag, -Infinity], [wallet.logTag, Infinity])) : idx.openCursor()
+ const request = walletName ? idx.openCursor(window.IDBKeyRange.bound([walletName, -Infinity], [walletName, Infinity])) : idx.openCursor()
request.onsuccess = function (event) {
const cursor = event.target.result
if (cursor) {
@@ -267,11 +271,11 @@ const WalletLoggerProvider = ({ children }) => {
cursor.continue()
} else {
// finished
- setLogs((logs) => logs.filter(l => wallet ? l.wallet !== wallet.logTag : false))
+ setLogs((logs) => logs.filter(l => walletName ? l.wallet !== walletName : false))
}
}
}
- }, [setLogs])
+ }, [me, setLogs])
return (
@@ -282,29 +286,29 @@ const WalletLoggerProvider = ({ children }) => {
)
}
-export function useWalletLogger (wallet) {
+export function useWalletLogger (walletName) {
const { appendLog, deleteLogs: innerDeleteLogs } = useContext(WalletLoggerContext)
const log = useCallback(level => message => {
// TODO:
// also send this to us if diagnostics was enabled,
// very similar to how the service worker logger works.
- appendLog(wallet, level, message)
- console[level !== 'error' ? 'info' : 'error'](`[${wallet.logTag}]`, message)
- }, [appendLog, wallet])
+ appendLog(walletName, level, message)
+ console[level !== 'error' ? 'info' : 'error'](`[${walletName}]`, message)
+ }, [appendLog, walletName])
const logger = useMemo(() => ({
ok: (...message) => log('ok')(message.join(' ')),
info: (...message) => log('info')(message.join(' ')),
error: (...message) => log('error')(message.join(' '))
- }), [log, wallet])
+ }), [log, walletName])
- const deleteLogs = useCallback((w) => innerDeleteLogs(w || wallet), [innerDeleteLogs, wallet])
+ const deleteLogs = useCallback((w) => innerDeleteLogs(w || walletName), [innerDeleteLogs, walletName])
return { logger, deleteLogs }
}
-export function useWalletLogs (wallet) {
+export function useWalletLogs (walletName) {
const logs = useContext(WalletLogsContext)
- return logs.filter(l => !wallet || l.wallet === wallet.logTag)
+ return logs.filter(l => !walletName || l.wallet === walletName)
}
diff --git a/components/nav/common.js b/components/nav/common.js
index 5116658d..667a34db 100644
--- a/components/nav/common.js
+++ b/components/nav/common.js
@@ -22,8 +22,8 @@ import SearchIcon from '../../svgs/search-line.svg'
import classNames from 'classnames'
import SnIcon from '@/svgs/sn.svg'
import { useHasNewNotes } from '../use-has-new-notes'
-import { useWalletLogger } from '../logger'
-import { useWebLNConfigurator } from '../webln'
+import { useWalletLogger } from '@/components/logger'
+// import { useWallet } from '@/components/wallet'
export function Brand ({ className }) {
return (
@@ -257,7 +257,7 @@ export default function LoginButton ({ className }) {
export function LogoutDropdownItem () {
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
- const webLN = useWebLNConfigurator()
+ // const wallet = useWallet()
const { deleteLogs } = useWalletLogger()
return (
{
return { create, waitUntilPaid: waitController.wait, stopWaiting: waitController.stop, cancel }
}
-export const useWebLnPayment = () => {
+export const useWalletPayment = () => {
const invoice = useInvoice()
- const provider = useWebLN()
+ const wallet = useWallet()
- const waitForWebLnPayment = useCallback(async ({ id, bolt11 }, waitFor) => {
- if (!provider) {
- throw new WebLnNotEnabledError()
+ const waitForWalletPayment = useCallback(async ({ id, bolt11 }, waitFor) => {
+ if (!wallet) {
+ throw new NoAttachedWalletError()
}
try {
return await new Promise((resolve, reject) => {
// can't use await here since we might pay JIT invoices and sendPaymentAsync is not supported yet.
// see https://www.webln.guide/building-lightning-apps/webln-reference/webln.sendpaymentasync
- provider.sendPayment(bolt11)
+ wallet.sendPayment(bolt11)
// JIT invoice payments will never resolve here
// since they only get resolved after settlement which can't happen here
.then(resolve)
@@ -147,21 +147,21 @@ export const useWebLnPayment = () => {
.catch(reject)
})
} catch (err) {
- console.error('WebLN payment failed:', err)
+ console.error('payment failed:', err)
throw err
} finally {
invoice.stopWaiting()
}
- }, [provider, invoice])
+ }, [wallet, invoice])
- return waitForWebLnPayment
+ return waitForWalletPayment
}
export const useQrPayment = () => {
const invoice = useInvoice()
const showModal = useShowModal()
- const waitForQrPayment = useCallback(async (inv, webLnError,
+ const waitForQrPayment = useCallback(async (inv, walletError,
{
keepOpen = true,
cancelOnClose = true,
@@ -185,8 +185,8 @@ export const useQrPayment = () => {
description
status='loading'
successVerb='received'
- webLn={false}
- webLnError={webLnError}
+ useWallet={false}
+ walletError={walletError}
waitFor={waitFor}
onCanceled={inv => { onClose(); reject(new InvoiceCanceledError(inv?.hash, inv?.actionError)) }}
onPayment={() => { paid = true; onClose(); resolve() }}
@@ -203,22 +203,22 @@ export const usePayment = () => {
const me = useMe()
const feeButton = useFeeButton()
const invoice = useInvoice()
- const waitForWebLnPayment = useWebLnPayment()
+ const waitForWalletPayment = useWalletPayment()
const waitForQrPayment = useQrPayment()
const waitForPayment = useCallback(async (invoice) => {
- let webLnError
+ let walletError
try {
- return await waitForWebLnPayment(invoice)
+ return await waitForWalletPayment(invoice)
} catch (err) {
if (err instanceof InvoiceCanceledError || err instanceof InvoiceExpiredError) {
// bail since qr code payment will also fail
throw err
}
- webLnError = err
+ walletError = err
}
- return await waitForQrPayment(invoice, webLnError)
- }, [waitForWebLnPayment, waitForQrPayment])
+ return await waitForQrPayment(invoice, walletError)
+ }, [waitForWalletPayment, waitForQrPayment])
const request = useCallback(async (amount) => {
amount ??= feeButton?.total
diff --git a/components/qr.js b/components/qr.js
index 4b75e46c..bc8cfd5b 100644
--- a/components/qr.js
+++ b/components/qr.js
@@ -2,25 +2,25 @@ import QRCode from 'qrcode.react'
import { CopyInput, InputSkeleton } from './form'
import InvoiceStatus from './invoice-status'
import { useEffect } from 'react'
-import { useWebLN } from './webln'
+import { useWallet } from './wallet'
import Bolt11Info from './bolt11-info'
-export default function Qr ({ asIs, value, webLn, statusVariant, description, status }) {
+export default function Qr ({ asIs, value, useWallet: automated, statusVariant, description, status }) {
const qrValue = asIs ? value : 'lightning:' + value.toUpperCase()
- const provider = useWebLN()
+ const wallet = useWallet()
useEffect(() => {
async function effect () {
- if (webLn && provider) {
+ if (automated && wallet) {
try {
- await provider.sendPayment(value)
+ await wallet.sendPayment(value)
} catch (e) {
console.log(e?.message)
}
}
}
effect()
- }, [provider])
+ }, [wallet])
return (
<>
diff --git a/components/use-local-state.js b/components/use-local-state.js
new file mode 100644
index 00000000..4b620bdc
--- /dev/null
+++ b/components/use-local-state.js
@@ -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]
+}
diff --git a/components/use-paid-mutation.js b/components/use-paid-mutation.js
index 6e55c7e1..cdd5abdd 100644
--- a/components/use-paid-mutation.js
+++ b/components/use-paid-mutation.js
@@ -1,6 +1,6 @@
import { useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
import { useCallback, useState } from 'react'
-import { InvoiceCanceledError, InvoiceExpiredError, useQrPayment, useWebLnPayment } from './payment'
+import { InvoiceCanceledError, InvoiceExpiredError, useQrPayment, useWalletPayment } from './payment'
import { GET_PAID_ACTION } from '@/fragments/paidAction'
/*
@@ -22,27 +22,27 @@ export function usePaidMutation (mutation,
const [getPaidAction] = useLazyQuery(GET_PAID_ACTION, {
fetchPolicy: 'network-only'
})
- const waitForWebLnPayment = useWebLnPayment()
+ const waitForWalletPayment = useWalletPayment()
const waitForQrPayment = useQrPayment()
const client = useApolloClient()
// innerResult is used to store/control the result of the mutation when innerMutate runs
const [innerResult, setInnerResult] = useState(result)
const waitForPayment = useCallback(async (invoice, { persistOnNavigate = false, waitFor }) => {
- let webLnError
+ let walletError
const start = Date.now()
try {
- return await waitForWebLnPayment(invoice, waitFor)
+ return await waitForWalletPayment(invoice, waitFor)
} catch (err) {
if (Date.now() - start > 1000 || err instanceof InvoiceCanceledError || err instanceof InvoiceExpiredError) {
// bail since qr code payment will also fail
// also bail if the payment took more than 1 second
throw err
}
- webLnError = err
+ walletError = err
}
- return await waitForQrPayment(invoice, webLnError, { persistOnNavigate, waitFor })
- }, [waitForWebLnPayment, waitForQrPayment])
+ return await waitForQrPayment(invoice, walletError, { persistOnNavigate, waitFor })
+ }, [waitForWalletPayment, waitForQrPayment])
const innerMutate = useCallback(async ({
onCompleted: innerOnCompleted, ...innerOptions
diff --git a/components/wallet-card.js b/components/wallet-card.js
index 7abcd304..f607e3ca 100644
--- a/components/wallet-card.js
+++ b/components/wallet-card.js
@@ -5,14 +5,13 @@ 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'
+import { useWallet, Status } from './wallet'
-export const isConfigured = status => [Status.Enabled, Status.Locked, Status.Error, true].includes(status)
+export function WalletCard ({ name, title, badges, status }) {
+ const wallet = useWallet(name)
-export function WalletCard ({ title, badges, provider, status }) {
- const configured = isConfigured(status)
let indicator = styles.disabled
- switch (status) {
+ switch (wallet.status) {
case Status.Enabled:
case true:
indicator = styles.success
@@ -42,33 +41,31 @@ export function WalletCard ({ title, badges, provider, status }) {
)}
- {provider &&
-
-
- {configured
- ? <>configure>
- : <>attach>}
-
- }
+
+
+ {wallet.isConfigured
+ ? <>configure>
+ : <>attach>}
+
+
)
}
export function WalletButtonBar ({
- status, disable,
+ wallet, disable,
className, children, onDelete, onCancel, hasCancel = true,
createText = 'attach', deleteText = 'detach', editText = 'save'
}) {
- const configured = isConfigured(status)
return (
- {configured &&
+ {wallet.isConfigured &&
}
{children}
{hasCancel && }
- {configured ? editText : createText}
+ {wallet.isConfigured ? editText : createText}
diff --git a/components/wallet/index.js b/components/wallet/index.js
new file mode 100644
index 00000000..fbddb07f
--- /dev/null
+++ b/components/wallet/index.js
@@ -0,0 +1,82 @@
+import { useCallback } from 'react'
+import { useMe } from '@/components/me'
+import useLocalState from '@/components/use-local-state'
+import { useWalletLogger } from '@/components/logger'
+import { SSR } from '@/lib/constants'
+
+// wallet definitions
+export const WALLET_DEFS = [
+ await import('@/components/wallet/lnbits')
+]
+
+export const Status = {
+ Initialized: 'Initialized',
+ Enabled: 'Enabled',
+ Locked: 'Locked',
+ Error: 'Error'
+}
+
+export function useWallet (name) {
+ const me = useMe()
+ const { logger } = useWalletLogger(name)
+
+ const wallet = getWalletByName(name, me)
+ const storageKey = getStorageKey(wallet?.name, me)
+ const [config, saveConfig, clearConfig] = useLocalState(storageKey)
+
+ const isConfigured = !!config
+
+ const sendPayment = useCallback(async (bolt11) => {
+ return await wallet.sendPayment({ bolt11, config, logger })
+ }, [wallet, config, logger])
+
+ const validate = useCallback(async (values) => {
+ return await wallet.validate({ logger, ...values })
+ }, [logger])
+
+ const enable = useCallback(() => {
+ enableWallet(name, me)
+ }, [name, me])
+
+ return {
+ ...wallet,
+ sendPayment,
+ validate,
+ config,
+ saveConfig,
+ clearConfig,
+ enable,
+ isConfigured,
+ status: config?.enabled ? Status.Enabled : Status.Initialized
+ }
+}
+
+export function getWalletByName (name, me) {
+ return name
+ ? WALLET_DEFS.find(def => def.name === name)
+ : WALLET_DEFS.find(def => {
+ const key = getStorageKey(def.name, me)
+ const config = SSR ? null : JSON.parse(window?.localStorage.getItem(key))
+ return config?.enabled
+ })
+}
+
+function getStorageKey (name, me) {
+ let storageKey = `wallet:${name}`
+ if (me) {
+ storageKey = `${storageKey}:${me.id}`
+ }
+ return storageKey
+}
+
+function enableWallet (name, me) {
+ for (const walletDef of WALLET_DEFS) {
+ const toEnable = walletDef.name === name
+ const key = getStorageKey(name, me)
+ const config = JSON.parse(window.localStorage.getItem(key))
+ if (config.enabled || toEnable) {
+ config.enabled = toEnable
+ window.localStorage.setItem(key, JSON.stringify(config))
+ }
+ }
+}
diff --git a/components/wallet/lnbits.js b/components/wallet/lnbits.js
new file mode 100644
index 00000000..af3aeb6a
--- /dev/null
+++ b/components/wallet/lnbits.js
@@ -0,0 +1,124 @@
+import { bolt11Tags } from '@/lib/bolt11'
+
+export const name = 'lnbits'
+
+export const fields = [
+ {
+ name: 'url',
+ label: 'lnbits url',
+ type: 'text'
+ },
+ {
+ name: 'adminKey',
+ label: 'admin key',
+ type: 'password'
+ }
+]
+
+export const card = {
+ title: 'LNbits',
+ badges: ['send only', 'non-custodialish']
+}
+
+export async function validate ({ logger, ...config }) {
+ return await getInfo({ logger, ...config })
+}
+
+async function getInfo ({ logger, ...config }) {
+ const response = await getWallet(config.url, config.adminKey)
+ return {
+ node: {
+ alias: response.name,
+ pubkey: ''
+ },
+ methods: [
+ 'getInfo',
+ 'getBalance',
+ 'sendPayment'
+ ],
+ version: '1.0',
+ supports: ['lightning']
+ }
+}
+
+export async function sendPayment ({ bolt11, config, logger }) {
+ 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
+}
diff --git a/components/webln/index.js b/components/webln/index.js
deleted file mode 100644
index 4eed911c..00000000
--- a/components/webln/index.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
-import { LNbitsProvider, useLNbits } from './lnbits'
-import { NWCProvider, useNWC } from './nwc'
-import { LNCProvider, useLNC } from './lnc'
-
-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 = {
- Initialized: 'Initialized',
- Enabled: 'Enabled',
- Locked: 'Locked',
- Error: 'Error'
-}
-
-export function migrateLocalStorage (oldStorageKey, newStorageKey) {
- 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 (
-
- {children}
-
- )
-}
-
-export function WebLNProvider ({ children }) {
- return (
-
-
-
-
- {children}
-
-
-
-
- )
-}
-
-export function useWebLN () {
- const { provider } = useContext(WebLNContext)
- return provider
-}
-
-export function useWebLNConfigurator () {
- return useContext(WebLNContext)
-}
diff --git a/components/webln/lnbits.js b/components/webln/lnbits.js
deleted file mode 100644
index 68eddcd9..00000000
--- a/components/webln/lnbits.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
-import { useWalletLogger } from '../logger'
-import { Status, migrateLocalStorage } from '.'
-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
-
-const LNbitsContext = createContext()
-
-const getWallet = async (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
-}
-
-const postPayment = async (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
-}
-
-const getPayment = async (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
-}
-
-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 (
-
- {children}
-
- )
-}
-
-export function useLNbits () {
- return useContext(LNbitsContext)
-}
diff --git a/components/webln/lnc.js b/components/webln/lnc.js
deleted file mode 100644
index 6d21415a..00000000
--- a/components/webln/lnc.js
+++ /dev/null
@@ -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 (
-
- )
- }, { 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 (
-
- {children}
- {modal}
-
- )
-}
-
-export function useLNC () {
- return useContext(LNCContext)
-}
diff --git a/components/webln/nwc.js b/components/webln/nwc.js
deleted file mode 100644
index 212c848a..00000000
--- a/components/webln/nwc.js
+++ /dev/null
@@ -1,287 +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
- try {
- relay = await Relay.connect(relayUrl)
- logger.ok(`connected to ${relayUrl}`)
- } catch (err) {
- const msg = `failed to connect to ${relayUrl}`
- logger.error(msg)
- throw new Error(msg)
- }
-
- try {
- 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))
- }, timeout)
-
- let found = false
- 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))
- }
- }
- })
- })
- } finally {
- relay?.close()?.catch()
- 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
- try {
- relay = await Relay.connect(relayUrl)
- logger.ok(`connected to ${relayUrl}`)
- } catch (err) {
- const msg = `failed to connect to ${relayUrl}`
- logger.error(msg)
- throw new Error(msg)
- }
-
- try {
- 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))
- }, 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]
- }
- 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 {
- relay?.close()?.catch()
- 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 (
-
- {children}
-
- )
-}
-
-export function useNWC () {
- return useContext(NWCContext)
-}
diff --git a/pages/_app.js b/pages/_app.js
index 94d4eb90..82bd105a 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -18,7 +18,6 @@ import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { LoggerProvider } from '@/components/logger'
import { ChainFeeProvider } from '@/components/chain-fee.js'
-import { WebLNProvider } from '@/components/webln'
import dynamic from 'next/dynamic'
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
@@ -109,18 +108,16 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
-
-
-
-
-
-
- {!router?.query?.disablePrompt && }
-
-
-
-
-
+
+
+
+
+
+ {!router?.query?.disablePrompt && }
+
+
+
+
diff --git a/pages/invoices/[id].js b/pages/invoices/[id].js
index 653d3fb6..182fcd02 100644
--- a/pages/invoices/[id].js
+++ b/pages/invoices/[id].js
@@ -12,7 +12,7 @@ export default function FullInvoice () {
return (
-
+
)
}
diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js
new file mode 100644
index 00000000..de6a223d
--- /dev/null
+++ b/pages/settings/wallets/[wallet].js
@@ -0,0 +1,94 @@
+import { getGetServerSideProps } from '@/api/ssrApollo'
+import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
+import { CenterLayout } from '@/components/layout'
+import { WalletButtonBar } from '@/components/wallet-card'
+import { lnbitsSchema } from '@/lib/validate'
+import { WalletSecurityBanner } from '@/components/banners'
+import WalletLogs from '@/components/wallet-logs'
+import { useToast } from '@/components/toast'
+import { useRouter } from 'next/router'
+import { useWallet } from '@/components/wallet'
+
+export const getServerSideProps = getGetServerSideProps({ authRequired: true })
+
+export default function WalletSettings () {
+ const toaster = useToast()
+ const router = useRouter()
+ const { wallet: name } = router.query
+ const wallet = useWallet(name)
+
+ const initial = wallet.fields.reduce((acc, field) => {
+ return {
+ ...acc,
+ [field.name]: wallet.config?.[field.name] || ''
+ }
+ }, {
+ isDefault: wallet.isDefault || false
+ })
+
+ return (
+
+ {wallet.card.title}
+ use {wallet.card.title} for payments
+
+
+
+
+
+
+ )
+}
+
+function WalletFields ({ wallet: { config, fields } }) {
+ return fields.map(({ name, label, type }, i) => {
+ const props = {
+ initialValue: config?.[name],
+ label,
+ name,
+ required: true,
+ autoFocus: i === 0
+ }
+ if (type === 'text') {
+ return
+ }
+ if (type === 'password') {
+ return
+ }
+ return null
+ })
+}
diff --git a/pages/settings/wallets/cln.js b/pages/settings/wallets/cln.js
deleted file mode 100644
index b8456075..00000000
--- a/pages/settings/wallets/cln.js
+++ /dev/null
@@ -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 (
-
- CLN
- autowithdraw to your Core Lightning node via CLNRest
-
-
-
-
-
- )
-}
-
-export function CLNCard ({ wallet }) {
- return (
-
- )
-}
diff --git a/pages/settings/wallets/index.js b/pages/settings/wallets/index.js
index 8adbcec5..32725dfc 100644
--- a/pages/settings/wallets/index.js
+++ b/pages/settings/wallets/index.js
@@ -2,29 +2,13 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
import Layout from '@/components/layout'
import styles from '@/styles/wallet.module.css'
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 { useQuery } from '@apollo/client'
-import PageLoading from '@/components/page-loading'
-import { LNCCard } from './lnc'
+import { WALLETS as WALLETS_QUERY } from '@/fragments/wallet'
import Link from 'next/link'
-import { Wallet as W } from '@/lib/constants'
+import { WALLET_DEFS } from '@/components/wallet'
-export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true })
+export const getServerSideProps = getGetServerSideProps({ query: WALLETS_QUERY, authRequired: true })
export default function Wallet ({ ssrData }) {
- const { data } = useQuery(WALLETS)
-
- if (!data && !ssrData) return
- 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 (
@@ -36,15 +20,9 @@ export default function Wallet ({ ssrData }) {
-
-
-
-
-
-
-
-
-
+ {WALLET_DEFS.map((def, i) =>
+
+ )}
diff --git a/pages/settings/wallets/lightning-address.js b/pages/settings/wallets/lightning-address.js
deleted file mode 100644
index 38713c8a..00000000
--- a/pages/settings/wallets/lightning-address.js
+++ /dev/null
@@ -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 (
-
- lightning address
- autowithdraw to a lightning address
-
-
-
-
-
- )
-}
-
-export function LightningAddressWalletCard ({ wallet }) {
- return (
-
- )
-}
diff --git a/pages/settings/wallets/lnbits.js b/pages/settings/wallets/lnbits.js
deleted file mode 100644
index b1905c0b..00000000
--- a/pages/settings/wallets/lnbits.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { getGetServerSideProps } from '@/api/ssrApollo'
-import { Form, ClientInput, ClientCheckbox, PasswordInput } from '@/components/form'
-import { CenterLayout } from '@/components/layout'
-import { WalletButtonBar, WalletCard, isConfigured } from '@/components/wallet-card'
-import { lnbitsSchema } from '@/lib/validate'
-import { useToast } from '@/components/toast'
-import { useRouter } from 'next/router'
-import { useLNbits } from '@/components/webln/lnbits'
-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 LNbits () {
- const { provider, enabledProviders, setProvider } = useWebLNConfigurator()
- const lnbits = useLNbits()
- const { name, url, adminKey, saveConfig, clearConfig, status } = lnbits
- const isDefault = provider?.name === name
- const configured = isConfigured(status)
- const toaster = useToast()
- const router = useRouter()
-
- return (
-
- LNbits
- use LNbits for payments
-
-
-
-
-
-
- )
-}
-
-export function LNbitsCard () {
- const { status } = useLNbits()
- return (
-
- )
-}
diff --git a/pages/settings/wallets/lnc.js b/pages/settings/wallets/lnc.js
deleted file mode 100644
index 6f97c191..00000000
--- a/pages/settings/wallets/lnc.js
+++ /dev/null
@@ -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 (
-
- Lightning Node Connect for LND
- use Lightning Node Connect for LND payments
-
-
-
-
-
-
- )
-}
-
-export function LNCCard () {
- const { status } = useLNC()
- return (
-
- )
-}
diff --git a/pages/settings/wallets/lnd.js b/pages/settings/wallets/lnd.js
deleted file mode 100644
index 1d5d0858..00000000
--- a/pages/settings/wallets/lnd.js
+++ /dev/null
@@ -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 (
-
- LND
- autowithdraw to your Lightning Labs node
-
-
-
-
-
- )
-}
-
-export function LNDCard ({ wallet }) {
- return (
-
- )
-}
diff --git a/pages/settings/wallets/nwc.js b/pages/settings/wallets/nwc.js
deleted file mode 100644
index 81800832..00000000
--- a/pages/settings/wallets/nwc.js
+++ /dev/null
@@ -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 (
-
- Nostr Wallet Connect
- use Nostr Wallet Connect for payments
-
-
-
-
-
-
- )
-}
-
-export function NWCCard () {
- const { status } = useNWC()
- return (
-
- )
-}