wip: Add LND autowithdrawals
* receiving wallets need to export 'server' object field * don't print macaroon error stack * fix missing wallet logs order update * mark autowithdrawl settings as required * fix server wallet logs deletion * remove canPay and canReceive since it was confusing where it is available TODO * also use numeric priority for sending wallets to be consistent with how status for receiving wallets is determined * define createInvoice function in wallet definition * consistent wallet logs: sending wallets use 'wallet attached'+'wallet enabled/disabled' whereas receiving wallets use 'wallet created/updated' * see FIXMEs
This commit is contained in:
parent
ae0335d537
commit
4082a45618
|
@ -46,12 +46,14 @@ export function AutowithdrawSettings ({ priority }) {
|
|||
}}
|
||||
hint={isNumber(sendThreshold) ? `will attempt auto-withdraw when your balance exceeds ${sendThreshold * 11} sats` : undefined}
|
||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label='max fee'
|
||||
name='autoWithdrawMaxFeePercent'
|
||||
hint='max fee as percent of withdrawal amount'
|
||||
append={<InputGroup.Text>%</InputGroup.Text>}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <a href='https://en.wikipedia.org/wiki/Certificate_authority' target='_blank' rel='noreferrer'>CA</a> (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
|
||||
}
|
|
@ -22,7 +22,7 @@ function macaroonOPs (macaroon) {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('macaroonOPs error:', e)
|
||||
console.error('macaroonOPs error:', e.message)
|
||||
}
|
||||
|
||||
return []
|
||||
|
|
|
@ -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 (
|
||||
<CenterLayout>
|
||||
|
@ -50,12 +51,16 @@ export default function WalletSettings () {
|
|||
}}
|
||||
>
|
||||
<WalletFields wallet={wallet} />
|
||||
<ClientCheckbox
|
||||
disabled={false}
|
||||
initialValue={wallet.status === Status.Enabled}
|
||||
label='enabled'
|
||||
name='enabled'
|
||||
/>
|
||||
{wallet.server
|
||||
? <AutowithdrawSettings />
|
||||
: (
|
||||
<ClientCheckbox
|
||||
disabled={false}
|
||||
initialValue={wallet.status === Status.Enabled}
|
||||
label='enabled'
|
||||
name='enabled'
|
||||
/>
|
||||
)}
|
||||
<WalletButtonBar
|
||||
wallet={wallet} onDelete={async () => {
|
||||
try {
|
||||
|
@ -86,12 +91,17 @@ function WalletFields ({ wallet: { config, fields } }) {
|
|||
label: (
|
||||
<div className='d-flex align-items-center'>
|
||||
{label}
|
||||
{/* help can be a string or object to customize the label */}
|
||||
{help && (
|
||||
<Info label='help'>
|
||||
<Text>{help}</Text>
|
||||
<Info label={help.label || 'help'}>
|
||||
<Text>{help.text || help}</Text>
|
||||
</Info>
|
||||
)}
|
||||
{optional && <small className='text-muted ms-2'>optional</small>}
|
||||
{optional && (
|
||||
<small className='text-muted ms-2'>
|
||||
{typeof optional === 'boolean' ? 'optional' : optional}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
required: !optional,
|
||||
|
|
Loading…
Reference in New Issue