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 ( -
{ - 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 - } - }} - > -

Unlock LNC

- -
- { onClose(); cancelAndReject() }} /> - unlock -
- - ) - }, { 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
+ +
{ + try { + await wallet.validate(values) + wallet.saveConfig(values) + wallet.enable() + toaster.success('saved settings') + router.push('/settings/wallets') + } catch (err) { + console.error(err) + toaster.danger('failed to attach: ' + err.message || err.toString?.()) + } + }} + > + + + { + try { + wallet.clearConfig() + toaster.success('saved settings') + router.push('/settings/wallets') + } catch (err) { + console.error(err) + toaster.danger('failed to detach: ' + err.message || err.toString?.()) + } + }} + /> + +
+ +
+
+ ) +} + +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
-
{ - 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) - } - }} - > - - invoice only rune - - - {'We only accept runes that *only* allow `method=invoice`.\nRun this to generate one:\n\n```lightning-cli createrune restrictions=\'["method=invoice"]\'```'} - - - - } - name='rune' - clear - hint='must be restricted to method=invoice' - placeholder='S34KtUW-6gqS_hD_9cc_PNhfF-NinZyBOCgr1aIrark9NCZtZXRob2Q9aW52b2ljZQ==' - required - /> - cert optional if from CA (e.g. voltage)} - name='cert' - clear - hint='hex or base64 encoded' - placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K' - /> - - { - try { - await removeWallet({ variables: { id: wallet?.id } }) - toaster.success('saved settings') - router.push('/settings/wallets') - } catch (err) { - console.error(err) - } - }} - /> - -
- -
-
- ) -} - -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
-
{ - 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) - } - }} - > - - - { - try { - await removeWallet({ variables: { id: wallet?.id } }) - toaster.success('saved settings') - router.push('/settings/wallets') - } catch (err) { - console.error(err) - } - }} - /> - -
- -
-
- ) -} - -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
- -
{ - 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?.()) - } - }} - > - - - - { - 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?.()) - } - }} - /> - -
- -
-
- ) -} - -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
- -
{ - 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?.()) - } - }} - > - pairing phrase - - - {'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance ```\n\n```$ litcli sessions add --type custom --label --account_id --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.'} - - - - } - name='pairingPhrase' - initialValue={config?.pairingPhrase} - newPass={config?.pairingPhrase === undefined} - readOnly={configured} - required - autoFocus - /> - password optional} - name='password' - initialValue={defaultPassword ? '' : config?.password} - newPass={config?.password === undefined || defaultPassword} - readOnly={configured} - hint='encrypts your pairing phrase when stored locally' - /> - - { - 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?.()) - } - }} - /> - -
- -
-
- ) -} - -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
-
{ - 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) - } - }} - > - - invoice macaroon - - - {'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```'} - - - - } - name='macaroon' - clear - hint='hex or base64 encoded' - placeholder='AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs' - required - /> - cert optional if from CA (e.g. voltage)} - name='cert' - clear - hint='hex or base64 encoded' - placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K' - /> - - { - try { - await removeWallet({ variables: { id: wallet?.id } }) - toaster.success('saved settings') - router.push('/settings/wallets') - } catch (err) { - console.error(err) - } - }} - /> - -
- -
-
- ) -} - -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
- -
{ - 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?.()) - } - }} - > - - - { - 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?.()) - } - }} - /> - -
- -
-
- ) -} - -export function NWCCard () { - const { status } = useNWC() - return ( - - ) -}