diff --git a/components/autowithdraw-shared.js b/components/autowithdraw-shared.js index 7a0f6416..86fa1cee 100644 --- a/components/autowithdraw-shared.js +++ b/components/autowithdraw-shared.js @@ -46,12 +46,14 @@ export function AutowithdrawSettings ({ priority }) { }} hint={isNumber(sendThreshold) ? `will attempt auto-withdraw when your balance exceeds ${sendThreshold * 11} sats` : undefined} append={sats} + required /> %} + required /> diff --git a/components/wallet-logger.js b/components/wallet-logger.js index d1ed6279..55d6ea52 100644 --- a/components/wallet-logger.js +++ b/components/wallet-logger.js @@ -5,7 +5,7 @@ import { Button } from 'react-bootstrap' import { useToast } from './toast' import { useShowModal } from './modal' import { WALLET_LOGS } from '@/fragments/wallet' -import { getWalletByName } from './wallet' +import { getServerWallet } from './wallet' import { gql, useMutation, useQuery } from '@apollo/client' import { useMe } from './me' @@ -128,12 +128,11 @@ export const WalletLoggerProvider = ({ children }) => { .map(({ createdAt, wallet: walletType, ...log }) => { return { ts: +new Date(createdAt), - // TODO: use wallet defs - // wallet: getWalletBy('type', walletType).logTag, + wallet: getServerWallet(walletType).name, ...log } }) - return [...prevLogs, ...logs].sort((a, b) => a.ts - b.ts) + return [...prevLogs, ...logs].sort((a, b) => b.ts - a.ts) }) } }) @@ -148,7 +147,7 @@ export const WalletLoggerProvider = ({ children }) => { onCompleted: (_, { variables: { wallet: walletType } }) => { setLogs((logs) => { // TODO: use wallet defs - return logs.filter(l => walletType ? l.wallet !== getWalletByName('type', walletType) : false) + return logs.filter(l => walletType ? l.wallet !== getServerWallet(walletType).name : false) }) } } @@ -206,10 +205,10 @@ export const WalletLoggerProvider = ({ children }) => { }, [saveLog]) const deleteLogs = useCallback(async (wallet) => { - if (!wallet || wallet.canReceive) { - await deleteServerWalletLogs({ variables: { wallet: wallet?.type } }) + if (!wallet || wallet.server) { + await deleteServerWalletLogs({ variables: { wallet: wallet?.server } }) } - if (!wallet || wallet.canPay) { + if (!wallet || wallet.sendPayment) { const tx = idb.current.transaction(idbStoreName, 'readwrite') const objectStore = tx.objectStore(idbStoreName) const idx = objectStore.index('wallet_ts') @@ -244,6 +243,10 @@ export function useWalletLogger (wallet) { console.error('cannot log: no wallet set') return } + + // don't store logs for receiving wallets on client since logs are stored on server + if (wallet.server) return + // TODO: // also send this to us if diagnostics was enabled, // very similar to how the service worker logger works. diff --git a/components/wallet/index.js b/components/wallet/index.js index 9cd7b432..6d9c1a91 100644 --- a/components/wallet/index.js +++ b/components/wallet/index.js @@ -8,9 +8,13 @@ import { bolt11Tags } from '@/lib/bolt11' import * as lnbits from '@/components/wallet/lnbits' import * as nwc from '@/components/wallet/nwc' import * as lnc from '@/components/wallet/lnc' +import * as lnd from '@/components/wallet/lnd' +import { useApolloClient, useMutation, useQuery } from '@apollo/client' +import { REMOVE_WALLET, WALLET_BY_TYPE } from '@/fragments/wallet' +import { autowithdrawInitial } from '../autowithdraw-shared' // wallet definitions -export const WALLET_DEFS = [lnbits, nwc, lnc] +export const WALLET_DEFS = [lnbits, nwc, lnc, lnd] export const Status = { Initialized: 'Initialized', @@ -22,11 +26,12 @@ export const Status = { export function useWallet (name) { const me = useMe() - const wallet = name ? getWalletByName(name, me) : getEnabledWallet(me) + const wallet = name ? getWalletByName(name) : getEnabledWallet(me) const { logger } = useWalletLogger(wallet) - const storageKey = getStorageKey(wallet?.name, me) - const [config, saveConfig, clearConfig] = useLocalState(storageKey) + const [config, saveConfig, clearConfig] = useConfig(wallet) + + // FIXME: This throws 'TypeError: Cannot read properties of undefined (reading 'length')' when I disable LNbits const sendPayment = useCallback(async (bolt11) => { const hash = bolt11Tags(bolt11).payment_hash logger.info('sending payment:', `payment_hash=${hash}`) @@ -53,9 +58,10 @@ export function useWallet (name) { const save = useCallback(async (config) => { try { // validate should log custom INFO and OK message + // validate is optional since validation might happen during save on server // TODO: add timeout - await wallet.validate({ me, logger, ...config }) - saveConfig(config) + await wallet.validate?.({ me, logger, ...config }) + await saveConfig(config) logger.ok('wallet attached') } catch (err) { const message = err.message || err.toString?.() @@ -85,17 +91,97 @@ export function useWallet (name) { enable, disable, isConfigured: !!config, - status: config?.enabled ? Status.Enabled : Status.Initialized, - canPay: !!wallet?.sendPayment, - canReceive: !!wallet?.createInvoice, + status: config?.enabled || config?.priority ? Status.Enabled : Status.Initialized, logger } } -export function getWalletByName (name, me) { +function useConfig (wallet) { + if (!wallet) return [] + + if (wallet.sendPayment) { + // FIXME: this throws 'Error: Should have a queue' when I enable LNbits + // probably because of conditional hooks? + return useLocalConfig(wallet) + } + + if (wallet.server) { + return useServerConfig(wallet) + } + + // TODO: if wallets can do both return a merged version that knows which field goes where + return [] +} + +function useLocalConfig (wallet) { + const me = useMe() + const storageKey = getStorageKey(wallet?.name, me) + return useLocalState(storageKey) +} + +function useServerConfig (wallet) { + const client = useApolloClient() + const me = useMe() + + const { walletType, mutation } = wallet.server + + const { data } = useQuery(WALLET_BY_TYPE, { variables: { type: walletType } }) + + const [upsertWallet] = useMutation(mutation, { + 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 walletId = data?.walletByType.id + const serverConfig = { id: walletId, priority: data?.walletByType.priority, ...data?.walletByType.wallet } + const autowithdrawSettings = autowithdrawInitial({ me, priority: serverConfig?.priority }) + const config = { ...serverConfig, autowithdrawSettings } + + const saveConfig = useCallback(async ({ + autoWithdrawThreshold, + autoWithdrawMaxFeePercent, + priority, + ...config + }) => { + await upsertWallet({ + variables: { + id: walletId, + ...config, + settings: { + autoWithdrawThreshold: Number(autoWithdrawThreshold), + autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent), + priority: !!priority + } + } + }) + }, [upsertWallet, walletId]) + + const clearConfig = useCallback(async () => { + await removeWallet({ variables: { id: config?.id } }) + }, [removeWallet, config?.id]) + + return [config, saveConfig, clearConfig] +} + +export function getWalletByName (name) { return WALLET_DEFS.find(def => def.name === name) } +export function getServerWallet (type) { + return WALLET_DEFS.find(def => def.server?.walletType === type) +} + export function getEnabledWallet (me) { // TODO: handle multiple enabled wallets return WALLET_DEFS diff --git a/components/wallet/lnd.js b/components/wallet/lnd.js new file mode 100644 index 00000000..358a6d9b --- /dev/null +++ b/components/wallet/lnd.js @@ -0,0 +1,48 @@ +import { UPSERT_WALLET_LND } from '@/fragments/wallet' +import { LNDAutowithdrawSchema } from '@/lib/validate' + +export const name = 'lnd' + +export const fields = [ + { + name: 'socket', + label: 'grpc host and port', + type: 'text', + placeholder: '55.5.555.55:10001', + hint: 'tor or clearnet', + clear: true + }, + { + name: 'macaroon', + label: 'invoice macaroon', + help: { + label: 'privacy tip', + text: 'We accept a prebaked ***invoice.macaroon*** for your convenience. To gain better privacy, generate a new macaroon as follows:\n\n```lncli bakemacaroon invoices:write invoices:read```' + }, + type: 'text', + placeholder: 'AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs', + hint: 'hex or base64 encoded', + clear: true + }, + { + name: 'cert', + label: 'cert', + type: 'text', + placeholder: 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K', + optional: <>optional if from CA (e.g. voltage), + clear: true + } +] + +export const card = { + title: 'LND', + subtitle: 'autowithdraw to your Lightning Labs node', + badges: ['receive only', 'non-custodial'] +} + +export const schema = LNDAutowithdrawSchema + +export const server = { + walletType: 'LND', + mutation: UPSERT_WALLET_LND +} diff --git a/lib/macaroon.js b/lib/macaroon.js index d0137b52..dc65fbd6 100644 --- a/lib/macaroon.js +++ b/lib/macaroon.js @@ -22,7 +22,7 @@ function macaroonOPs (macaroon) { } } } catch (e) { - console.error('macaroonOPs error:', e) + console.error('macaroonOPs error:', e.message) } return [] diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js index 23a4adc7..34bd13e8 100644 --- a/pages/settings/wallets/[wallet].js +++ b/pages/settings/wallets/[wallet].js @@ -9,6 +9,7 @@ import { useRouter } from 'next/router' import { useWallet, Status } from '@/components/wallet' import Info from '@/components/info' import Text from '@/components/text' +import { AutowithdrawSettings } from '@/components/autowithdraw-shared' export const getServerSideProps = getGetServerSideProps({ authRequired: true }) @@ -23,7 +24,7 @@ export default function WalletSettings () { ...acc, [field.name]: wallet.config?.[field.name] || '' } - }, {}) + }, wallet.server ? wallet.config.autowithdrawSettings : {}) return ( @@ -50,12 +51,16 @@ export default function WalletSettings () { }} > - + {wallet.server + ? + : ( + + )} { try { @@ -86,12 +91,17 @@ function WalletFields ({ wallet: { config, fields } }) { label: (
{label} + {/* help can be a string or object to customize the label */} {help && ( - - {help} + + {help.text || help} )} - {optional && optional} + {optional && ( + + {typeof optional === 'boolean' ? 'optional' : optional} + + )}
), required: !optional,