webln saves at least *double kazoo*

This commit is contained in:
k00b 2024-10-23 17:17:35 -05:00
parent 48640cbed6
commit 2bdbb433df
9 changed files with 59 additions and 64 deletions

View File

@ -263,7 +263,7 @@ export default function LoginButton () {
function LogoutObstacle ({ onClose }) { function LogoutObstacle ({ onClose }) {
const { registration: swRegistration, togglePushSubscription } = useServiceWorker() const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const wallets = useWallets() const { wallets } = useWallets()
const { multiAuthSignout } = useAccounts() const { multiAuthSignout } = useAccounts()
return ( return (

View File

@ -132,7 +132,7 @@ export function useWalletLogger (wallet, setLogs) {
const deleteLogs = useCallback(async (wallet, options) => { const deleteLogs = useCallback(async (wallet, options) => {
if ((!wallet || wallet.def.walletType) && !options?.clientOnly) { if ((!wallet || wallet.def.walletType) && !options?.clientOnly) {
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } }) await deleteServerWalletLogs({ variables: { wallet: wallet?.def.walletType } })
} }
if (!wallet || wallet.sendPayment) { if (!wallet || wallet.sendPayment) {
try { try {
@ -163,13 +163,13 @@ export function useWalletLogger (wallet, setLogs) {
ok: (...message) => log('ok')(message.join(' ')), ok: (...message) => log('ok')(message.join(' ')),
info: (...message) => log('info')(message.join(' ')), info: (...message) => log('info')(message.join(' ')),
error: (...message) => log('error')(message.join(' ')) error: (...message) => log('error')(message.join(' '))
}), [log, wallet?.name]) }), [log])
return { logger, deleteLogs } return { logger, deleteLogs }
} }
function tag (wallet) { function tag (walletDef) {
return wallet?.shortName || wallet?.name return walletDef.shortName || walletDef.name
} }
export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) { export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
@ -183,24 +183,24 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
const { getPage, error, notSupported } = useWalletLogDB() const { getPage, error, notSupported } = useWalletLogDB()
const [getWalletLogs] = useLazyQuery(WALLET_LOGS, SSR ? {} : { fetchPolicy: 'cache-and-network' }) const [getWalletLogs] = useLazyQuery(WALLET_LOGS, SSR ? {} : { fetchPolicy: 'cache-and-network' })
const loadLogsPage = useCallback(async (page, pageSize, wallet) => { const loadLogsPage = useCallback(async (page, pageSize, walletDef) => {
try { try {
let result = { data: [], hasMore: false } let result = { data: [], hasMore: false }
if (notSupported) { if (notSupported) {
console.log('cannot get client wallet logs: indexeddb not supported') console.log('cannot get client wallet logs: indexeddb not supported')
} else { } else {
const indexName = wallet ? 'wallet_ts' : 'ts' const indexName = walletDef ? 'wallet_ts' : 'ts'
const query = wallet ? window.IDBKeyRange.bound([tag(wallet), -Infinity], [tag(wallet), Infinity]) : null const query = walletDef ? window.IDBKeyRange.bound([tag(walletDef), -Infinity], [tag(walletDef), Infinity]) : null
result = await getPage(page, pageSize, indexName, query, 'prev') result = await getPage(page, pageSize, indexName, query, 'prev')
// no walletType means we're using the local IDB // no walletType means we're using the local IDB
if (wallet && !wallet.def.walletType) { if (!walletDef?.walletType) {
return result return result
} }
} }
const { data } = await getWalletLogs({ const { data } = await getWalletLogs({
variables: { variables: {
type: wallet?.walletType, type: walletDef.walletType,
// if it client logs has more, page based on it's range // if it client logs has more, page based on it's range
from: result?.data[result.data.length - 1]?.ts && result.hasMore ? String(result.data[result.data.length - 1].ts) : null, from: result?.data[result.data.length - 1]?.ts && result.hasMore ? String(result.data[result.data.length - 1].ts) : null,
// if we have a cursor (this isn't the first page), page based on it's range // if we have a cursor (this isn't the first page), page based on it's range
@ -231,28 +231,28 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
if (hasMore) { if (hasMore) {
setLoading(true) setLoading(true)
const result = await loadLogsPage(page + 1, logsPerPage, wallet) const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
setLogs(prevLogs => [...prevLogs, ...result.data]) setLogs(prevLogs => [...prevLogs, ...result.data])
setHasMore(result.hasMore) setHasMore(result.hasMore)
setTotal(result.total) setTotal(result.total)
setPage(prevPage => prevPage + 1) setPage(prevPage => prevPage + 1)
setLoading(false) setLoading(false)
} }
}, [loadLogsPage, page, logsPerPage, wallet, hasMore]) }, [loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
const loadLogs = useCallback(async () => { const loadLogs = useCallback(async () => {
setLoading(true) setLoading(true)
const result = await loadLogsPage(1, logsPerPage, wallet) const result = await loadLogsPage(1, logsPerPage, wallet?.def)
setLogs(result.data) setLogs(result.data)
setHasMore(result.hasMore) setHasMore(result.hasMore)
setTotal(result.total) setTotal(result.total)
setPage(1) setPage(1)
setLoading(false) setLoading(false)
}, [wallet, loadLogsPage]) }, [wallet?.def, loadLogsPage])
useEffect(() => { useEffect(() => {
loadLogs() loadLogs()
}, [wallet]) }, [wallet?.def])
return { logs, hasMore, total, loadMore, loadLogs, setLogs, loading } return { logs, hasMore, total, loadMore, loadLogs, setLogs, loading }
} }

View File

@ -43,10 +43,10 @@ export async function formikValidate (validate, data) {
} }
export async function walletValidate (wallet, data) { export async function walletValidate (wallet, data) {
if (typeof wallet.fieldValidation === 'function') { if (typeof wallet.def.fieldValidation === 'function') {
return await formikValidate(wallet.fieldValidation, data) return await formikValidate(wallet.def.fieldValidation, data)
} else { } else {
return await ssValidate(wallet.fieldValidation, data) return await ssValidate(wallet.def.fieldValidation, data)
} }
} }

View File

@ -9,9 +9,10 @@ import { useWallet } from '@/wallets/index'
import Info from '@/components/info' import Info from '@/components/info'
import Text from '@/components/text' import Text from '@/components/text'
import { AutowithdrawSettings } from '@/components/autowithdraw-shared' import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
import { isConfigured } from '@/wallets/common' import { canSend, isConfigured } from '@/wallets/common'
import { SSR } from '@/lib/constants' import { SSR } from '@/lib/constants'
import WalletButtonBar from '@/components/wallet-buttonbar' import WalletButtonBar from '@/components/wallet-buttonbar'
import { useWalletConfigurator } from '@/wallets/config'
export const getServerSideProps = getGetServerSideProps({ authRequired: true }) export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -20,6 +21,7 @@ export default function WalletSettings () {
const router = useRouter() const router = useRouter()
const { wallet: name } = router.query const { wallet: name } = router.query
const wallet = useWallet(name) const wallet = useWallet(name)
const { save, detach } = useWalletConfigurator(wallet)
const initial = wallet?.def.fields.reduce((acc, field) => { const initial = wallet?.def.fields.reduce((acc, field) => {
// We still need to run over all wallet fields via reduce // We still need to run over all wallet fields via reduce
@ -42,7 +44,7 @@ export default function WalletSettings () {
<CenterLayout> <CenterLayout>
<h2 className='pb-2'>{wallet?.def.card.title}</h2> <h2 className='pb-2'>{wallet?.def.card.title}</h2>
<h6 className='text-muted text-center pb-3'><Text>{wallet?.def.card.subtitle}</Text></h6> <h6 className='text-muted text-center pb-3'><Text>{wallet?.def.card.subtitle}</Text></h6>
{wallet?.canSend && wallet?.hasConfig > 0 && <WalletSecurityBanner />} {canSend(wallet) && <WalletSecurityBanner />}
<Form <Form
initial={initial} initial={initial}
enableReinitialize enableReinitialize
@ -56,7 +58,7 @@ export default function WalletSettings () {
values.enabled = true values.enabled = true
} }
await wallet.save(values) await save(values, true)
toaster.success('saved settings') toaster.success('saved settings')
router.push('/settings/wallets') router.push('/settings/wallets')
@ -82,7 +84,7 @@ export default function WalletSettings () {
<WalletButtonBar <WalletButtonBar
wallet={wallet} onDelete={async () => { wallet={wallet} onDelete={async () => {
try { try {
await wallet?.delete() await detach()
toaster.success('saved settings') toaster.success('saved settings')
router.push('/settings/wallets') router.push('/settings/wallets')
} catch (err) { } catch (err) {
@ -101,8 +103,6 @@ export default function WalletSettings () {
} }
function WalletFields ({ wallet }) { function WalletFields ({ wallet }) {
console.log('wallet', wallet)
return wallet.def.fields return wallet.def.fields
.map(({ name, label = '', type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => { .map(({ name, label = '', type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
const rawProps = { const rawProps = {

View File

@ -26,7 +26,7 @@ async function reorder (wallets, sourceIndex, targetIndex) {
} }
export default function Wallet ({ ssrData }) { export default function Wallet ({ ssrData }) {
const wallets = useWallets() const { wallets } = useWallets()
const isClient = useIsClient() const isClient = useIsClient()
const [sourceIndex, setSourceIndex] = useState(null) const [sourceIndex, setSourceIndex] = useState(null)

View File

@ -13,13 +13,13 @@ export function getWalletByType (type) {
return walletDefs.find(def => def.walletType === type) return walletDefs.find(def => def.walletType === type)
} }
export function getStorageKey (name, me) { export function getStorageKey (name, userId) {
let storageKey = `wallet:${name}` let storageKey = `wallet:${name}`
// WebLN has no credentials we need to scope to users // WebLN has no credentials we need to scope to users
// so we can use the same storage key for all users // so we can use the same storage key for all users
if (me && name !== 'webln') { if (userId && name !== 'webln') {
storageKey = `${storageKey}:${me.id}` storageKey = `${storageKey}:${userId}`
} }
return storageKey return storageKey

View File

@ -1,15 +1,17 @@
import { useMe } from '@/components/me' import { useMe } from '@/components/me'
import useVault from '@/components/vault/use-vault' import useVault from '@/components/vault/use-vault'
import { useCallback } from 'react' import { useCallback } from 'react'
import { getStorageKey, isClientField, isServerField } from './common' import { canReceive, canSend, getStorageKey, isClientField, isServerField } from './common'
import { useMutation } from '@apollo/client' import { useMutation } from '@apollo/client'
import { generateMutation } from './graphql' import { generateMutation } from './graphql'
import { REMOVE_WALLET } from '@/fragments/wallet' import { REMOVE_WALLET } from '@/fragments/wallet'
import { walletValidate } from '@/lib/validate' import { walletValidate } from '@/lib/validate'
import { useWalletLogger } from '@/components/wallet-logger' import { useWalletLogger } from '@/components/wallet-logger'
import { useWallets } from '.'
export function useWalletConfigurator (wallet) { export function useWalletConfigurator (wallet) {
const { me } = useMe() const { me } = useMe()
const { reloadLocalWallets } = useWallets()
const { encrypt, isActive } = useVault() const { encrypt, isActive } = useVault()
const { logger } = useWalletLogger(wallet?.def) const { logger } = useWalletLogger(wallet?.def)
const [upsertWallet] = useMutation(generateMutation(wallet?.def)) const [upsertWallet] = useMutation(generateMutation(wallet?.def))
@ -26,26 +28,29 @@ export function useWalletConfigurator (wallet) {
}, [encrypt, isActive]) }, [encrypt, isActive])
const _saveToLocal = useCallback(async (newConfig) => { const _saveToLocal = useCallback(async (newConfig) => {
window.localStorage.setItem(getStorageKey(wallet.name, me), JSON.stringify(newConfig)) window.localStorage.setItem(getStorageKey(wallet.def.name, me?.id), JSON.stringify(newConfig))
}, [me, wallet.name]) reloadLocalWallets()
}, [me?.id, wallet.def.name, reloadLocalWallets])
const save = useCallback(async (newConfig, validate = true) => { const save = useCallback(async (newConfig, validate = true) => {
let clientConfig = extractClientConfig(wallet.def.fields, newConfig) let clientConfig = extractClientConfig(wallet.def.fields, newConfig)
let serverConfig = extractServerConfig(wallet.def.fields, newConfig) let serverConfig = extractServerConfig(wallet.def.fields, newConfig)
if (validate) { if (validate) {
if (clientConfig) { if (canSend(wallet)) {
let transformedConfig = await walletValidate(wallet, clientConfig) let transformedConfig = await walletValidate(wallet, clientConfig)
if (transformedConfig) { if (transformedConfig) {
clientConfig = Object.assign(clientConfig, transformedConfig) clientConfig = Object.assign(clientConfig, transformedConfig)
} }
transformedConfig = await wallet.def.testSendPayment(clientConfig, { me, logger }) if (wallet.def.testSendPayment) {
if (transformedConfig) { transformedConfig = await wallet.def.testSendPayment(clientConfig, { me, logger })
clientConfig = Object.assign(clientConfig, transformedConfig) if (transformedConfig) {
clientConfig = Object.assign(clientConfig, transformedConfig)
}
} }
} }
if (serverConfig) { if (canReceive(wallet)) {
const transformedConfig = await walletValidate(wallet, serverConfig) const transformedConfig = await walletValidate(wallet, serverConfig)
if (transformedConfig) { if (transformedConfig) {
serverConfig = Object.assign(serverConfig, transformedConfig) serverConfig = Object.assign(serverConfig, transformedConfig)
@ -57,14 +62,14 @@ export function useWalletConfigurator (wallet) {
if (isActive) { if (isActive) {
await _saveToServer(serverConfig, clientConfig) await _saveToServer(serverConfig, clientConfig)
} else { } else {
if (clientConfig) { if (canSend(wallet)) {
await _saveToLocal(clientConfig) await _saveToLocal(clientConfig)
} }
if (serverConfig) { if (canReceive(wallet)) {
await _saveToServer(serverConfig) await _saveToServer(serverConfig)
} }
} }
}, [wallet.def, encrypt, isActive]) }, [wallet, encrypt, isActive])
const _detachFromServer = useCallback(async () => { const _detachFromServer = useCallback(async () => {
await removeWallet({ variables: { id: wallet.config.id } }) await removeWallet({ variables: { id: wallet.config.id } })
@ -72,8 +77,8 @@ export function useWalletConfigurator (wallet) {
const _detachFromLocal = useCallback(async () => { const _detachFromLocal = useCallback(async () => {
// if vault is not active and has a client config, delete from local storage // if vault is not active and has a client config, delete from local storage
window.localStorage.removeItem(getStorageKey(wallet.name, me)) window.localStorage.removeItem(getStorageKey(wallet.def.name, me?.id))
}, [me, wallet.name]) }, [me?.id, wallet.def.name])
const detach = useCallback(async () => { const detach = useCallback(async () => {
if (isActive) { if (isActive) {
@ -87,7 +92,7 @@ export function useWalletConfigurator (wallet) {
} }
}, [isActive, _detachFromServer, _detachFromLocal]) }, [isActive, _detachFromServer, _detachFromLocal])
return [save, detach] return { save, detach }
} }
function extractConfig (fields, config, client, includeMeta = true) { function extractConfig (fields, config, client, includeMeta = true) {
@ -111,7 +116,7 @@ function extractConfig (fields, config, client, includeMeta = true) {
} }
function extractClientConfig (fields, config) { function extractClientConfig (fields, config) {
return extractConfig(fields, config, true, false) return extractConfig(fields, config, true, true)
} }
function extractServerConfig (fields, config) { function extractServerConfig (fields, config) {

View File

@ -1,6 +1,6 @@
import { useMe } from '@/components/me' import { useMe } from '@/components/me'
import { WALLETS } from '@/fragments/wallet' import { WALLETS } from '@/fragments/wallet'
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants' import { LONG_POLL_INTERVAL, SSR } from '@/lib/constants'
import { useQuery } from '@apollo/client' import { useQuery } from '@apollo/client'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend } from './common' import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend } from './common'
@ -21,44 +21,34 @@ function useLocalWallets () {
// form wallets into a list of { config, def } // form wallets into a list of { config, def }
const wallets = walletDefs.map(w => { const wallets = walletDefs.map(w => {
try { try {
const config = window.localStorage.getItem(getStorageKey(w.name, me)) const config = window.localStorage.getItem(getStorageKey(w.name, me?.id))
return { def: w, config: JSON.parse(config) } return { def: w, config: JSON.parse(config) }
} catch (e) { } catch (e) {
return null return null
} }
}).filter(Boolean) }).filter(Boolean)
setWallets(wallets) setWallets(wallets)
}, [me, setWallets]) }, [me?.id, setWallets])
// watch for changes to local storage
useEffect(() => { useEffect(() => {
loadWallets() loadWallets()
// reload wallets if local storage to wallet changes
const handler = (event) => {
if (event.key.startsWith('wallet:')) {
loadWallets()
}
}
window.addEventListener('storage', handler)
return () => window.removeEventListener('storage', handler)
}, [loadWallets]) }, [loadWallets])
return wallets return { wallets, reloadLocalWallets: loadWallets }
} }
const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} })) const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
export function WalletsProvider ({ children }) { export function WalletsProvider ({ children }) {
const { me } = useMe()
const { decrypt } = useVault() const { decrypt } = useVault()
const localWallets = useLocalWallets() const { wallets: localWallets, reloadLocalWallets } = useLocalWallets()
// TODO: instead of polling, this should only be called when the vault key is updated // TODO: instead of polling, this should only be called when the vault key is updated
// or a denormalized field on the user 'vaultUpdatedAt' is changed // or a denormalized field on the user 'vaultUpdatedAt' is changed
const { data } = useQuery(WALLETS, { const { data } = useQuery(WALLETS, {
pollInterval: NORMAL_POLL_INTERVAL, pollInterval: LONG_POLL_INTERVAL,
nextFetchPolicy: 'cache-and-network', nextFetchPolicy: 'cache-and-network',
skip: !me?.id || SSR skip: SSR
}) })
const wallets = useMemo(() => { const wallets = useMemo(() => {
@ -85,7 +75,7 @@ export function WalletsProvider ({ children }) {
// provides priority sorted wallets to children // provides priority sorted wallets to children
return ( return (
<WalletsContext.Provider value={wallets}> <WalletsContext.Provider value={{ wallets, reloadLocalWallets }}>
{children} {children}
</WalletsContext.Provider> </WalletsContext.Provider>
) )
@ -96,7 +86,7 @@ export function useWallets () {
} }
export function useWallet (name) { export function useWallet (name) {
const wallets = useWallets() const { wallets } = useWallets()
const wallet = useMemo(() => { const wallet = useMemo(() => {
if (name) { if (name) {

View File

@ -39,14 +39,14 @@ export async function createInvoice (userId, { msats, description, descriptionHa
msats = toPositiveNumber(msats) msats = toPositiveNumber(msats)
for (const wallet of wallets) { for (const wallet of wallets) {
const w = walletDefs.find(w => w.walletType === wallet.type) const w = walletDefs.find(w => w.walletType === wallet.def.walletType)
try { try {
const { walletType, walletField, createInvoice } = w const { walletType, walletField, createInvoice } = w
const walletFull = await models.wallet.findFirst({ const walletFull = await models.wallet.findFirst({
where: { where: {
userId, userId,
type: walletType type: wallet.def.walletType
}, },
include: { include: {
[walletField]: true [walletField]: true