server side config saves

This commit is contained in:
k00b 2024-10-24 15:30:56 -05:00
parent 4826ae5a7b
commit ccdf346954
24 changed files with 325 additions and 371 deletions

View File

@ -55,8 +55,8 @@ export default {
for (const entry of entries) { for (const entry of entries) {
txs.push(models.vaultEntry.update({ txs.push(models.vaultEntry.update({
where: { id: entry.id }, where: { userId_key: { userId: me.id, key: entry.key } },
data: { key: entry.key, value: entry.value } data: { value: entry.value }
})) }))
} }
await models.prisma.$transaction(txs) await models.prisma.$transaction(txs)

View File

@ -26,12 +26,13 @@ import { getNodeSockets, getOurPubkey } from '../lnd'
function injectResolvers (resolvers) { function injectResolvers (resolvers) {
console.group('injected GraphQL resolvers:') console.group('injected GraphQL resolvers:')
for (const w of walletDefs) { for (const walletDef of walletDefs) {
const resolverName = generateResolverName(w.walletField) const resolverName = generateResolverName(walletDef.walletField)
console.log(resolverName) console.log(resolverName)
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => { resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => {
// allow transformation of the data on validation (this is optional ... won't do anything if not implemented) // allow transformation of the data on validation (this is optional ... won't do anything if not implemented)
const validData = await walletValidate(w, { ...data, ...settings, vaultEntries }) // TODO: our validation should be improved
const validData = await walletValidate(walletDef, { ...data, ...settings, vaultEntries })
if (validData) { if (validData) {
Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] }) Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] })
Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] }) Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] })
@ -39,10 +40,12 @@ function injectResolvers (resolvers) {
return await upsertWallet({ return await upsertWallet({
wallet: { wallet: {
field: w.walletField, field: walletDef.walletField,
type: w.walletType type: walletDef.walletType
}, },
testCreateInvoice: w.testCreateInvoice && validateLightning ? (data) => w.testCreateInvoice(data, { me, models }) : null testCreateInvoice: walletDef.testCreateInvoice && validateLightning
? (data) => walletDef.testCreateInvoice(data, { me, models })
: null
}, { }, {
settings, settings,
data, data,
@ -643,17 +646,13 @@ export const addWalletLog = async ({ wallet, level, message }, { models }) => {
} }
async function upsertWallet ( async function upsertWallet (
{ wallet, testCreateInvoice }, { wallet, testCreateInvoice }, { settings, data, vaultEntries = [] }, { me, models }) {
{ settings, data, priorityOnly, canSend, canReceive }, if (!me) {
{ me, models } throw new GqlAuthenticationError()
) { }
if (!me) throw new GqlAuthenticationError()
assertApiKeyNotPermitted({ me }) assertApiKeyNotPermitted({ me })
const { id, ...walletData } = data if (testCreateInvoice) {
const { autoWithdrawThreshold, autoWithdrawMaxFeePercent, enabled, priority } = settings
if (testCreateInvoice && !priorityOnly && canReceive && enabled) {
try { try {
await testCreateInvoice(data) await testCreateInvoice(data)
} catch (err) { } catch (err) {
@ -666,103 +665,111 @@ async function upsertWallet (
} }
} }
return await models.$transaction(async (tx) => { const { id, enabled, priority, ...walletData } = data
if (canReceive) { const {
await tx.user.update({ autoWithdrawThreshold,
where: { id: me.id }, autoWithdrawMaxFeePercent,
data: { autoWithdrawMaxFeeTotal
autoWithdrawMaxFeePercent, } = settings
autoWithdrawThreshold
}
})
}
let updatedWallet const txs = []
if (id) {
const existingWalletTypeRecord = canReceive
? await tx[wallet.field].findUnique({
where: { walletId: Number(id) }
})
: undefined
updatedWallet = await tx.wallet.update({ if (id) {
const oldVaultEntries = await models.vaultEntry.findMany({ where: { userId: me.id, walletId: Number(id) } })
// createMany is the set difference of the new - old
// deleteMany is the set difference of the old - new
// updateMany is the intersection of the old and new
const difference = (a = [], b = [], key = 'key') => a.filter(x => !b.find(y => y[key] === x[key]))
const intersectionMerge = (a = [], b = [], key = 'key') => a.filter(x => b.find(y => y[key] === x[key]))
.map(x => ({ [key]: x[key], ...b.find(y => y[key] === x[key]) }))
txs.push(
models.wallet.update({
where: { id: Number(id), userId: me.id }, where: { id: Number(id), userId: me.id },
data: { data: {
enabled, enabled,
priority, priority,
canSend, [wallet.field]: {
canReceive, update: {
// if send-only config or priority only, don't update the wallet type record where: { walletId: Number(id) },
...(canReceive && !priorityOnly data: walletData
? { }
[wallet.field]: existingWalletTypeRecord },
? { update: walletData } vaultEntries: {
: { create: walletData } deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({
} userId: me.id, key
: {}) })),
create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, value }) => ({
key, value, userId: me.id
})),
update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, value }) => ({
where: { userId_key: { userId: me.id, key } },
data: { value }
}))
}
}, },
include: { include: {
...(canReceive && !priorityOnly ? { [wallet.field]: true } : {}) vaultEntries: true
} }
}) })
} else { )
updatedWallet = await tx.wallet.create({ } else {
txs.push(
models.wallet.create({
include: {
vaultEntries: true
},
data: { data: {
enabled, enabled,
priority, priority,
canSend,
canReceive,
userId: me.id, userId: me.id,
type: wallet.type, type: wallet.type,
// if send-only config or priority only, don't update the wallet type record [wallet.field]: {
...(canReceive && !priorityOnly create: walletData
? { },
[wallet.field]: { vaultEntries: {
create: walletData createMany: {
} data: vaultEntries.map(({ key, value }) => ({ key, value, userId: me.id }))
} }
: {}) }
} }
}) })
} )
}
const logs = [] txs.push(
if (canReceive) { models.user.update({
logs.push({ where: { id: me.id },
userId: me.id, data: {
wallet: wallet.type, autoWithdrawMaxFeePercent,
level: enabled ? 'SUCCESS' : 'INFO', autoWithdrawThreshold,
message: id ? 'receive details updated' : 'wallet attached for receives' autoWithdrawMaxFeeTotal
}) }
logs.push({
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: enabled ? 'receives enabled' : 'receives disabled'
})
}
if (canSend) {
logs.push({
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: id ? 'send details updated' : 'wallet attached for sends'
})
logs.push({
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: enabled ? 'sends enabled' : 'sends disabled'
})
}
await tx.walletLog.createMany({
data: logs
}) })
)
return updatedWallet txs.push(
}) models.walletLog.createMany({
data: {
userId: me.id,
wallet: wallet.type,
level: 'SUCCESS',
message: id ? 'wallet details updated' : 'wallet attached'
}
}),
models.walletLog.create({
data: {
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: enabled ? 'wallet enabled' : 'wallet disabled'
}
})
)
const [upsertedWallet] = await models.$transaction(txs)
return upsertedWallet
} }
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, walletId = null }) { export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, walletId = null }) {

View File

@ -182,11 +182,9 @@ export default gql`
withdrawMaxFeeDefault: Int! withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int autoWithdrawThreshold: Int
autoWithdrawMaxFeePercent: Float autoWithdrawMaxFeePercent: Float
<<<<<<< HEAD
autoWithdrawMaxFeeTotal: Int autoWithdrawMaxFeeTotal: Int
=======
vaultKeyHash: String vaultKeyHash: String
>>>>>>> 002b1d19 (user vault and server side client wallets) walletsUpdatedAt: Date
} }
type UserOptional { type UserOptional {

View File

@ -17,7 +17,7 @@ function mutationTypeDefs () {
.filter(isServerField) .filter(isServerField)
.map(fieldToGqlArgOptional) .map(fieldToGqlArgOptional)
if (serverFields.length > 0) args += serverFields.join(', ') + ',' if (serverFields.length > 0) args += serverFields.join(', ') + ','
args += 'settings: AutowithdrawSettings!, priorityOnly: Boolean, canSend: Boolean!, canReceive: Boolean!' args += 'enabled: Boolean, priority: Int, vaultEntries: [VaultEntryInput!], settings: AutowithdrawSettings!, validateLightning: Boolean'
const resolverName = generateResolverName(w.walletField) const resolverName = generateResolverName(w.walletField)
const typeDef = `${resolverName}(${args}): Wallet` const typeDef = `${resolverName}(${args}): Wallet`
console.log(typeDef) console.log(typeDef)
@ -91,8 +91,6 @@ const typeDefs = `
enabled: Boolean! enabled: Boolean!
priority: Int! priority: Int!
wallet: WalletDetails! wallet: WalletDetails!
canReceive: Boolean!
canSend: Boolean!
vaultEntries: [VaultEntry!]! vaultEntries: [VaultEntry!]!
} }
@ -100,8 +98,6 @@ const typeDefs = `
autoWithdrawThreshold: Int! autoWithdrawThreshold: Int!
autoWithdrawMaxFeePercent: Float! autoWithdrawMaxFeePercent: Float!
autoWithdrawMaxFeeTotal: Int! autoWithdrawMaxFeeTotal: Int!
priority: Int
enabled: Boolean
} }
type Invoice { type Invoice {

View File

@ -93,7 +93,10 @@ function sortHelper (a, b) {
} }
} }
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) { const DEFAULT_BASE_LINE_ITEMS = {}
const DEFAULT_USE_REMOTE_LINE_ITEMS = () => null
export function FeeButtonProvider ({ baseLineItems = DEFAULT_BASE_LINE_ITEMS, useRemoteLineItems = DEFAULT_USE_REMOTE_LINE_ITEMS, children }) {
const [lineItems, setLineItems] = useState({}) const [lineItems, setLineItems] = useState({})
const [disabled, setDisabled] = useState(false) const [disabled, setDisabled] = useState(false)
const { me } = useMe() const { me } = useMe()

View File

@ -10,7 +10,10 @@ import { LIMIT } from '@/lib/cursor'
import ItemFull from './item-full' import ItemFull from './item-full'
import { useData } from './use-data' import { useData } from './use-data'
export default function Items ({ ssrData, variables = {}, query, destructureData, rank, noMoreText, Footer, filter = () => true }) { const DEFAULT_FILTER = () => true
const DEFAULT_VARIABLES = {}
export default function Items ({ ssrData, variables = DEFAULT_VARIABLES, query, destructureData, rank, noMoreText, Footer, filter = DEFAULT_FILTER }) {
const { data, fetchMore } = useQuery(query || SUB_ITEMS, { variables }) const { data, fetchMore } = useQuery(query || SUB_ITEMS, { variables })
const Foooter = Footer || MoreFooter const Foooter = Footer || MoreFooter
const dat = useData(data, ssrData) const dat = useData(data, ssrData)

View File

@ -15,7 +15,11 @@ export function SubSelectInitial ({ sub }) {
} }
} }
export function useSubs ({ prependSubs = [], sub, filterSubs = () => true, appendSubs = [] }) { const DEFAULT_PREPEND_SUBS = []
const DEFAULT_APPEND_SUBS = []
const DEFAULT_FILTER_SUBS = () => true
export function useSubs ({ prependSubs = DEFAULT_PREPEND_SUBS, sub, filterSubs = DEFAULT_FILTER_SUBS, appendSubs = DEFAULT_APPEND_SUBS }) {
const { data } = useQuery(SUBS, SSR const { data } = useQuery(SUBS, SSR
? {} ? {}
: { : {

View File

@ -17,7 +17,9 @@ export function debounce (fn, time) {
} }
} }
export default function useDebounceCallback (fn, time, deps = []) { const DEFAULT_DEPS = []
export default function useDebounceCallback (fn, time, deps = DEFAULT_DEPS) {
const [args, setArgs] = useState([]) const [args, setArgs] = useState([])
const memoFn = useCallback(fn, deps) const memoFn = useCallback(fn, deps)
useNoInitialEffect(debounce(() => memoFn(...args), time), [memoFn, time, args]) useNoInitialEffect(debounce(() => memoFn(...args), time), [memoFn, time, args])

View File

@ -4,7 +4,11 @@ export function getDbName (userId, name) {
return `app:storage:${userId ?? ''}${name ? `:${name}` : ''}` return `app:storage:${userId ?? ''}${name ? `:${name}` : ''}`
} }
function useIndexedDB ({ dbName, storeName, options = { keyPath: 'id', autoIncrement: true }, indices = [], version = 1 }) { const DEFAULT_OPTIONS = { keyPath: 'id', autoIncrement: true }
const DEFAULT_INDICES = []
const DEFAULT_VERSION = 1
function useIndexedDB ({ dbName, storeName, options = DEFAULT_OPTIONS, indices = DEFAULT_INDICES, version = DEFAULT_VERSION }) {
const [db, setDb] = useState(null) const [db, setDb] = useState(null)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [notSupported, setNotSupported] = useState(false) const [notSupported, setNotSupported] = useState(false)
@ -28,7 +32,7 @@ function useIndexedDB ({ dbName, storeName, options = { keyPath: 'id', autoIncre
} catch (error) { } catch (error) {
handleError(error) handleError(error)
} }
}, [storeName, handleError]) }, [storeName, handleError, operationQueue])
useEffect(() => { useEffect(() => {
let isMounted = true let isMounted = true
@ -81,7 +85,7 @@ function useIndexedDB ({ dbName, storeName, options = { keyPath: 'id', autoIncre
db.close() db.close()
} }
} }
}, [dbName, storeName, version, indices, handleError, processQueue]) }, [dbName, storeName, version, indices, options, handleError, processQueue])
const queueOperation = useCallback((operation) => { const queueOperation = useCallback((operation) => {
if (notSupported) { if (notSupported) {

View File

@ -140,7 +140,9 @@ function UserHidden ({ rank, Embellish }) {
) )
} }
export function ListUsers ({ users, rank, statComps = seperate(STAT_COMPONENTS, Seperator), Embellish, nymActionDropdown }) { const DEFAULT_STAT_COMPONENTS = seperate(STAT_COMPONENTS, Seperator)
export function ListUsers ({ users, rank, statComps = DEFAULT_STAT_COMPONENTS, Embellish, nymActionDropdown }) {
return ( return (
<div className={styles.grid}> <div className={styles.grid}>
{users.map((user, i) => ( {users.map((user, i) => (
@ -155,7 +157,7 @@ export function ListUsers ({ users, rank, statComps = seperate(STAT_COMPONENTS,
export default function UserList ({ ssrData, query, variables, destructureData, rank, footer = true, nymActionDropdown, statCompsProp }) { export default function UserList ({ ssrData, query, variables, destructureData, rank, footer = true, nymActionDropdown, statCompsProp }) {
const { data, fetchMore } = useQuery(query, { variables }) const { data, fetchMore } = useQuery(query, { variables })
const dat = useData(data, ssrData) const dat = useData(data, ssrData)
const [statComps, setStatComps] = useState(seperate(STAT_COMPONENTS, Seperator)) const [statComps, setStatComps] = useState(DEFAULT_STAT_COMPONENTS)
useEffect(() => { useEffect(() => {
// shift the stat we are sorting by to the front // shift the stat we are sorting by to the front

View File

@ -2,7 +2,7 @@ import { useMutation, useQuery } from '@apollo/client'
import { useMe } from '../me' import { useMe } from '../me'
import { useToast } from '../toast' import { useToast } from '../toast'
import useIndexedDB, { getDbName } from '../use-indexeddb' import useIndexedDB, { getDbName } from '../use-indexeddb'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { E_VAULT_KEY_EXISTS } from '@/lib/error' import { E_VAULT_KEY_EXISTS } from '@/lib/error'
import { CLEAR_VAULT, GET_VAULT_ENTRIES, UPDATE_VAULT_KEY } from '@/fragments/vault' import { CLEAR_VAULT, GET_VAULT_ENTRIES, UPDATE_VAULT_KEY } from '@/fragments/vault'
import { toHex } from '@/lib/hex' import { toHex } from '@/lib/hex'
@ -21,7 +21,8 @@ const useImperativeQuery = (query) => {
export function useVaultConfigurator () { export function useVaultConfigurator () {
const { me } = useMe() const { me } = useMe()
const toaster = useToast() const toaster = useToast()
const { set, get, remove } = useIndexedDB({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault' }) const idbConfig = useMemo(() => ({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault' }), [me?.id])
const { set, get, remove } = useIndexedDB(idbConfig)
const [updateVaultKey] = useMutation(UPDATE_VAULT_KEY) const [updateVaultKey] = useMutation(UPDATE_VAULT_KEY)
const getVaultEntries = useImperativeQuery(GET_VAULT_ENTRIES) const getVaultEntries = useImperativeQuery(GET_VAULT_ENTRIES)
const [key, setKey] = useState(null) const [key, setKey] = useState(null)
@ -46,7 +47,7 @@ export function useVaultConfigurator () {
// toaster?.danger('error loading vault configuration ' + e.message) // toaster?.danger('error loading vault configuration ' + e.message)
} }
})() })()
}, [me?.privates?.vaultKeyHash, keyHash, get, remove, toaster]) }, [me?.privates?.vaultKeyHash, keyHash, get, remove])
// clear vault: remove everything and reset the key // clear vault: remove everything and reset the key
const [clearVault] = useMutation(CLEAR_VAULT, { const [clearVault] = useMutation(CLEAR_VAULT, {

View File

@ -92,11 +92,11 @@ function getWalletLogDbName (userId) {
function useWalletLogDB () { function useWalletLogDB () {
const { me } = useMe() const { me } = useMe()
const { add, getPage, clear, error, notSupported } = useIndexedDB({ // memoize the idb config to avoid re-creating it on every render
dbName: getWalletLogDbName(me?.id), const idbConfig = useMemo(() =>
storeName: 'wallet_logs', ({ dbName: getWalletLogDbName(me?.id), storeName: 'wallet_logs', indices: INDICES }), [me?.id])
indices: INDICES const { add, getPage, clear, error, notSupported } = useIndexedDB(idbConfig)
})
return { add, getPage, clear, error, notSupported } return { add, getPage, clear, error, notSupported }
} }

View File

@ -3,68 +3,12 @@ import { COMMENTS, COMMENTS_ITEM_EXT_FIELDS } from './comments'
import { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items' import { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items'
import { SUB_FULL_FIELDS } from './subs' import { SUB_FULL_FIELDS } from './subs'
export const ME = gql` const STREAK_FIELDS = gql`
{ fragment StreakFields on User {
me { optional {
id streak
name gunStreak
bioId horseStreak
photoId
privates {
autoDropBolt11s
diagnostics
noReferralLinks
fiatCurrency
satsFilter
hideCowboyHat
hideFromTopUsers
hideGithub
hideNostr
hideTwitter
hideInvoiceDesc
hideIsContributor
hideWalletBalance
hideWelcomeBanner
imgproxyOnly
showImagesAndVideos
lastCheckedJobs
nostrCrossposting
noteAllDescendants
noteCowboyHat
noteDeposits
noteWithdrawals
noteEarning
noteForwardedSats
noteInvites
noteItemSats
noteJobIndicator
noteMentions
noteItemMentions
sats
tipDefault
tipRandom
tipRandomMin
tipRandomMax
tipPopover
turboTipping
zapUndos
upvotePopover
wildWestMode
withdrawMaxFeeDefault
lnAddr
autoWithdrawMaxFeePercent
autoWithdrawThreshold
disableFreebies
vaultKeyHash
}
optional {
isContributor
stacked
streak
githubId
nostrAuthPubkey
twitterId
}
} }
} }
` `
@ -104,6 +48,8 @@ ${STREAK_FIELDS}
upvotePopover upvotePopover
wildWestMode wildWestMode
disableFreebies disableFreebies
vaultKeyHash
walletsUpdatedAt
} }
optional { optional {
isContributor isContributor

View File

@ -106,86 +106,7 @@ mutation removeWallet($id: ID!) {
removeWallet(id: $id) removeWallet(id: $id)
} }
` `
// XXX [WALLET] this needs to be updated if another server wallet is added // XXX [WALLET] this needs to be updated if another server wallet is added
export const WALLET = gql`
query Wallet($id: ID!) {
wallet(id: $id) {
id
createdAt
priority
type
wallet {
__typename
... on WalletLightningAddress {
address
}
... on WalletLnd {
socket
macaroon
cert
}
... on WalletCln {
socket
rune
cert
}
... on WalletLnbits {
url
invoiceKey
}
... on WalletNwc {
nwcUrlRecv
}
... on WalletPhoenixd {
url
secondaryPassword
}
}
}
}
`
// XXX [WALLET] this needs to be updated if another server wallet is added
export const WALLET_BY_TYPE = gql`
query WalletByType($type: String!) {
walletByType(type: $type) {
id
createdAt
enabled
priority
type
wallet {
__typename
... on WalletLightningAddress {
address
}
... on WalletLnd {
socket
macaroon
cert
}
... on WalletCln {
socket
rune
cert
}
... on WalletLnbits {
url
invoiceKey
}
... on WalletNwc {
nwcUrlRecv
}
... on WalletPhoenixd {
url
secondaryPassword
}
}
}
}
`
export const WALLET_FIELDS = gql` export const WALLET_FIELDS = gql`
fragment WalletFields on Wallet { fragment WalletFields on Wallet {
id id
@ -197,12 +118,57 @@ export const WALLET_FIELDS = gql`
key key
value value
} }
wallet {
__typename
... on WalletLightningAddress {
address
}
... on WalletLnd {
socket
macaroon
cert
}
... on WalletCln {
socket
rune
cert
}
... on WalletLnbits {
url
invoiceKey
}
... on WalletNwc {
nwcUrlRecv
}
... on WalletPhoenixd {
url
secondaryPassword
}
}
}
`
export const WALLET = gql`
${WALLET_FIELDS}
query Wallet($id: ID!) {
wallet(id: $id) {
...WalletFields
}
}
`
// XXX [WALLET] this needs to be updated if another server wallet is added
export const WALLET_BY_TYPE = gql`
${WALLET_FIELDS}
query WalletByType($type: String!) {
walletByType(type: $type) {
...WalletFields
}
} }
` `
export const WALLETS = gql` export const WALLETS = gql`
${WALLET_FIELDS} ${WALLET_FIELDS}
query Wallets { query Wallets {
wallets { wallets {
...WalletFields ...WalletFields

View File

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

88
package-lock.json generated
View File

@ -51,7 +51,7 @@
"mdast-util-gfm": "^3.0.0", "mdast-util-gfm": "^3.0.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0",
"next": "^14.2.15", "next": "^14.2.16",
"next-auth": "^4.24.8", "next-auth": "^4.24.8",
"next-plausible": "^3.12.2", "next-plausible": "^3.12.2",
"next-seo": "^6.6.0", "next-seo": "^6.6.0",
@ -4124,9 +4124,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.16.tgz",
"integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==" "integrity": "sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag=="
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
"version": "14.2.15", "version": "14.2.15",
@ -4184,9 +4184,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.16.tgz",
"integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", "integrity": "sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4199,9 +4199,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.16.tgz",
"integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", "integrity": "sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4214,9 +4214,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.16.tgz",
"integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", "integrity": "sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4229,9 +4229,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.16.tgz",
"integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", "integrity": "sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4244,9 +4244,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.16.tgz",
"integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", "integrity": "sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4259,9 +4259,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.16.tgz",
"integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", "integrity": "sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4274,9 +4274,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.16.tgz",
"integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", "integrity": "sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4289,9 +4289,9 @@
} }
}, },
"node_modules/@next/swc-win32-ia32-msvc": { "node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz",
"integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", "integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -4304,9 +4304,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.16.tgz",
"integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", "integrity": "sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -15494,11 +15494,11 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
}, },
"node_modules/next": { "node_modules/next": {
"version": "14.2.15", "version": "14.2.16",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.2.16.tgz",
"integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", "integrity": "sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==",
"dependencies": { "dependencies": {
"@next/env": "14.2.15", "@next/env": "14.2.16",
"@swc/helpers": "0.5.5", "@swc/helpers": "0.5.5",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
@ -15513,15 +15513,15 @@
"node": ">=18.17.0" "node": ">=18.17.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.15", "@next/swc-darwin-arm64": "14.2.16",
"@next/swc-darwin-x64": "14.2.15", "@next/swc-darwin-x64": "14.2.16",
"@next/swc-linux-arm64-gnu": "14.2.15", "@next/swc-linux-arm64-gnu": "14.2.16",
"@next/swc-linux-arm64-musl": "14.2.15", "@next/swc-linux-arm64-musl": "14.2.16",
"@next/swc-linux-x64-gnu": "14.2.15", "@next/swc-linux-x64-gnu": "14.2.16",
"@next/swc-linux-x64-musl": "14.2.15", "@next/swc-linux-x64-musl": "14.2.16",
"@next/swc-win32-arm64-msvc": "14.2.15", "@next/swc-win32-arm64-msvc": "14.2.16",
"@next/swc-win32-ia32-msvc": "14.2.15", "@next/swc-win32-ia32-msvc": "14.2.16",
"@next/swc-win32-x64-msvc": "14.2.15" "@next/swc-win32-x64-msvc": "14.2.16"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",

View File

@ -56,7 +56,7 @@
"mdast-util-gfm": "^3.0.0", "mdast-util-gfm": "^3.0.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0",
"next": "^14.2.15", "next": "^14.2.16",
"next-auth": "^4.24.8", "next-auth": "^4.24.8",
"next-plausible": "^3.12.2", "next-plausible": "^3.12.2",
"next-seo": "^6.6.0", "next-seo": "^6.6.0",

View File

@ -11,16 +11,14 @@ ALTER TYPE "WalletType" ADD VALUE 'LNC';
ALTER TYPE "WalletType" ADD VALUE 'WEBLN'; ALTER TYPE "WalletType" ADD VALUE 'WEBLN';
-- AlterTable -- AlterTable
ALTER TABLE "Wallet" ADD COLUMN "canReceive" BOOLEAN NOT NULL DEFAULT true, ALTER TABLE "users" ADD COLUMN "vaultKeyHash" TEXT NOT NULL DEFAULT '',
ADD COLUMN "canSend" BOOLEAN NOT NULL DEFAULT false; ADD COLUMN "walletsUpdatedAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "users" ADD COLUMN "vaultKeyHash" TEXT NOT NULL DEFAULT '';
-- CreateTable -- CreateTable
CREATE TABLE "VaultEntry" ( CREATE TABLE "VaultEntry" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"key" VARCHAR(64) NOT NULL, "key" TEXT NOT NULL,
"iv" TEXT NOT NULL,
"value" TEXT NOT NULL, "value" TEXT NOT NULL,
"userId" INTEGER NOT NULL, "userId" INTEGER NOT NULL,
"walletId" INTEGER, "walletId" INTEGER,
@ -30,17 +28,32 @@ CREATE TABLE "VaultEntry" (
CONSTRAINT "VaultEntry_pkey" PRIMARY KEY ("id") CONSTRAINT "VaultEntry_pkey" PRIMARY KEY ("id")
); );
-- CreateIndex
CREATE INDEX "VaultEntry_userId_idx" ON "VaultEntry"("userId");
-- CreateIndex -- CreateIndex
CREATE INDEX "VaultEntry_walletId_idx" ON "VaultEntry"("walletId"); CREATE INDEX "VaultEntry_walletId_idx" ON "VaultEntry"("walletId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "VaultEntry_userId_key_walletId_key" ON "VaultEntry"("userId", "key", "walletId"); CREATE UNIQUE INDEX "VaultEntry_userId_key_key" ON "VaultEntry"("userId", "key");
-- CreateIndex
CREATE INDEX "Wallet_priority_idx" ON "Wallet"("priority");
-- AddForeignKey -- AddForeignKey
ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE FUNCTION wallet_updated_at_trigger() RETURNS TRIGGER AS $$
BEGIN
UPDATE "users" SET "walletsUpdatedAt" = NOW() WHERE "id" = NEW."userId";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER wallet_updated_at_trigger
AFTER INSERT OR UPDATE ON "Wallet"
FOR EACH ROW EXECUTE PROCEDURE wallet_updated_at_trigger();
CREATE TRIGGER vault_entry_updated_at_trigger
AFTER INSERT OR UPDATE ON "VaultEntry"
FOR EACH ROW EXECUTE PROCEDURE wallet_updated_at_trigger();

View File

@ -138,6 +138,7 @@ model User {
oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer") oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer")
oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees") oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees")
vaultKeyHash String @default("") vaultKeyHash String @default("")
walletsUpdatedAt DateTime?
vaultEntries VaultEntry[] @relation("VaultEntries") vaultEntries VaultEntry[] @relation("VaultEntries")
@@index([photoId]) @@index([photoId])
@ -187,16 +188,14 @@ enum WalletType {
} }
model Wallet { model Wallet {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int userId Int
label String? label String?
enabled Boolean @default(true) enabled Boolean @default(true)
priority Int @default(0) priority Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
canReceive Boolean @default(true)
canSend Boolean @default(false)
// NOTE: this denormalized json field exists to make polymorphic joins efficient // NOTE: this denormalized json field exists to make polymorphic joins efficient
// when reading wallets ... it is populated by a trigger when wallet descendants update // when reading wallets ... it is populated by a trigger when wallet descendants update
@ -218,11 +217,13 @@ model Wallet {
InvoiceForward InvoiceForward[] InvoiceForward InvoiceForward[]
@@index([userId]) @@index([userId])
@@index([priority])
} }
model VaultEntry { model VaultEntry {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
key String @db.VarChar(64) key String @db.Text
iv String @db.Text
value String @db.Text value String @db.Text
userId Int userId Int
walletId Int? walletId Int?
@ -231,8 +232,7 @@ model VaultEntry {
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@unique([userId, key, walletId]) @@unique([userId, key])
@@index([userId])
@@index([walletId]) @@index([walletId])
} }

View File

@ -74,24 +74,24 @@ function checkFields ({ fields, config }) {
return val return val
} }
export function isConfigured (wallet) { export function isConfigured ({ def, config }) {
return isSendConfigured(wallet) || isReceiveConfigured(wallet) return isSendConfigured({ def, config }) || isReceiveConfigured({ def, config })
} }
function isSendConfigured (wallet) { function isSendConfigured ({ def, config }) {
const fields = wallet.def.fields.filter(isClientField) const fields = def.fields.filter(isClientField)
return checkFields({ fields, config: wallet.config }) return checkFields({ fields, config })
} }
function isReceiveConfigured (wallet) { function isReceiveConfigured ({ def, config }) {
const fields = wallet.def.fields.filter(isServerField) const fields = def.fields.filter(isServerField)
return checkFields({ fields, config: wallet.config }) return checkFields({ fields, config })
} }
export function canSend (wallet) { export function canSend ({ def, config }) {
return !!wallet.def.sendPayment && isSendConfigured(wallet) return !!def.sendPayment && isSendConfigured({ def, config })
} }
export function canReceive (wallet) { export function canReceive ({ def, config }) {
return !wallet.def.clientOnly && isReceiveConfigured(wallet) return !def.clientOnly && isReceiveConfigured({ def, config })
} }

View File

@ -35,13 +35,12 @@ export function useWalletConfigurator (wallet) {
const _validate = useCallback(async (config, validateLightning = true) => { const _validate = useCallback(async (config, validateLightning = true) => {
const { serverWithShared, clientWithShared } = siftConfig(wallet.def.fields, config) const { serverWithShared, clientWithShared } = siftConfig(wallet.def.fields, config)
console.log('sifted', siftConfig(wallet.def.fields, config))
let clientConfig = clientWithShared let clientConfig = clientWithShared
let serverConfig = serverWithShared let serverConfig = serverWithShared
if (canSend(wallet)) { if (canSend({ def: wallet.def, config: clientConfig })) {
let transformedConfig = await walletValidate(wallet, clientWithShared) let transformedConfig = await walletValidate(wallet.def, clientWithShared)
if (transformedConfig) { if (transformedConfig) {
clientConfig = Object.assign(clientConfig, transformedConfig) clientConfig = Object.assign(clientConfig, transformedConfig)
} }
@ -51,29 +50,29 @@ export function useWalletConfigurator (wallet) {
clientConfig = Object.assign(clientConfig, transformedConfig) clientConfig = Object.assign(clientConfig, transformedConfig)
} }
} }
} } else if (canReceive({ def: wallet.def, config: serverConfig })) {
const transformedConfig = await walletValidate(wallet.def, serverConfig)
if (canReceive(wallet)) {
const transformedConfig = await walletValidate(wallet, serverConfig)
if (transformedConfig) { if (transformedConfig) {
serverConfig = Object.assign(serverConfig, transformedConfig) serverConfig = Object.assign(serverConfig, transformedConfig)
} }
} else {
throw new Error('configuration must be able to send or receive')
} }
return { clientConfig, serverConfig } return { clientConfig, serverConfig }
}, [wallet]) }, [wallet])
const save = useCallback(async (newConfig, validateLightning = true) => { const save = useCallback(async (newConfig, validateLightning = true) => {
const { clientConfig, serverConfig } = _validate(newConfig, validateLightning) const { clientConfig, serverConfig } = await _validate(newConfig, validateLightning)
// if vault is active, encrypt and send to server regardless of wallet type // if vault is active, encrypt and send to server regardless of wallet type
if (isActive) { if (isActive) {
await _saveToServer(serverConfig, clientConfig, validateLightning) await _saveToServer(serverConfig, clientConfig, validateLightning)
} else { } else {
if (canSend(wallet)) { if (canSend({ def: wallet.def, config: clientConfig })) {
await _saveToLocal(clientConfig) await _saveToLocal(clientConfig)
} }
if (canReceive(wallet)) { if (canReceive({ def: wallet.def, config: serverConfig })) {
await _saveToServer(serverConfig, clientConfig, validateLightning) await _saveToServer(serverConfig, clientConfig, validateLightning)
} }
} }
@ -84,18 +83,19 @@ export function useWalletConfigurator (wallet) {
}, [wallet.config?.id]) }, [wallet.config?.id])
const _detachFromLocal = useCallback(async () => { const _detachFromLocal = useCallback(async () => {
// if vault is not active and has a client config, delete from local storage
window.localStorage.removeItem(getStorageKey(wallet.def.name, me?.id)) window.localStorage.removeItem(getStorageKey(wallet.def.name, me?.id))
}, [me?.id, wallet.def.name]) }, [me?.id, wallet.def.name])
const detach = useCallback(async () => { const detach = useCallback(async () => {
if (isActive) { if (isActive) {
// if vault is active, detach all wallets from server
await _detachFromServer() await _detachFromServer()
} else { } else {
if (wallet.config.id) { if (wallet.config.id) {
await _detachFromServer() await _detachFromServer()
} }
// if vault is not active and has a client config, delete from local storage
await _detachFromLocal() await _detachFromLocal()
} }
}, [isActive, _detachFromServer, _detachFromLocal]) }, [isActive, _detachFromServer, _detachFromLocal])

View File

@ -33,17 +33,18 @@ export function generateMutation (wallet) {
.filter(isServerField) .filter(isServerField)
.map(f => `$${f.name}: String`) .map(f => `$${f.name}: String`)
.join(', ') .join(', ')
headerArgs += ', $settings: AutowithdrawSettings!, $validateLightning: Boolean' headerArgs += ', $enabled: Boolean, $priority: Int, $vaultEntries: [VaultEntryInput!], $settings: AutowithdrawSettings!, $validateLightning: Boolean'
let inputArgs = 'id: $id, ' let inputArgs = 'id: $id, '
inputArgs += wallet.fields inputArgs += wallet.fields
.filter(isServerField) .filter(isServerField)
.map(f => `${f.name}: $${f.name}`).join(', ') .map(f => `${f.name}: $${f.name}`).join(', ')
inputArgs += ', settings: $settings, validateLightning: $validateLightning,' inputArgs += ', enabled: $enabled, priority: $priority, vaultEntries: $vaultEntries, settings: $settings, validateLightning: $validateLightning'
return gql`mutation ${resolverName}(${headerArgs}) { return gql`
${WALLET_FIELDS} ${WALLET_FIELDS}
${resolverName}(${inputArgs}) { mutation ${resolverName}(${headerArgs}) {
${resolverName}(${inputArgs}) {
...WalletFields ...WalletFields
} }
}` }`

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 { LONG_POLL_INTERVAL, SSR } from '@/lib/constants' import { 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'
@ -41,15 +41,17 @@ const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
export function WalletsProvider ({ children }) { export function WalletsProvider ({ children }) {
const { decrypt } = useVault() const { decrypt } = useVault()
const { me } = useMe()
const { wallets: localWallets, reloadLocalWallets } = useLocalWallets() const { wallets: localWallets, reloadLocalWallets } = useLocalWallets()
// TODO: instead of polling, this should only be called when the vault key is updated const { data, refetch } = useQuery(WALLETS,
// or a denormalized field on the user 'vaultUpdatedAt' is changed SSR ? {} : { nextFetchPolicy: 'cache-and-network' })
const { data } = useQuery(WALLETS, {
pollInterval: LONG_POLL_INTERVAL, useEffect(() => {
nextFetchPolicy: 'cache-and-network', if (me?.privates?.walletsUpdatedAt) {
skip: SSR refetch()
}) }
}, [me?.privates?.walletsUpdatedAt, me?.privates?.vaultKeyHash, refetch])
const wallets = useMemo(() => { const wallets = useMemo(() => {
// form wallets into a list of { config, def } // form wallets into a list of { config, def }
@ -60,7 +62,9 @@ export function WalletsProvider ({ children }) {
config[key] = decrypt(value) config[key] = decrypt(value)
} }
return { config, def } // the specific wallet config on the server is stored in wallet.wallet
// on the client, it's stored in unnested
return { config: { ...config, ...w.wallet }, def }
}) ?? [] }) ?? []
// merge wallets on name // merge wallets on name

View File

@ -17,6 +17,7 @@ import { parsePaymentRequest } from 'ln-service'
import { toPositiveNumber } from '@/lib/validate' import { toPositiveNumber } from '@/lib/validate'
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
import { withTimeout } from '@/lib/time' import { withTimeout } from '@/lib/time'
import { canReceive } from './common'
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln] export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
@ -25,7 +26,7 @@ const MAX_PENDING_INVOICES_PER_WALLET = 25
export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) { export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) {
// get the wallets in order of priority // get the wallets in order of priority
const wallets = await models.wallet.findMany({ const wallets = await models.wallet.findMany({
where: { userId, enabled: true, canReceive: true }, where: { userId, enabled: true },
include: { include: {
user: true user: true
}, },
@ -42,11 +43,14 @@ export async function createInvoice (userId, { msats, description, descriptionHa
const w = walletDefs.find(w => w.walletType === wallet.def.walletType) const w = walletDefs.find(w => w.walletType === wallet.def.walletType)
try { try {
const { walletType, walletField, createInvoice } = w const { walletType, walletField, createInvoice } = w
if (!canReceive({ def: w, config: wallet })) {
continue
}
const walletFull = await models.wallet.findFirst({ const walletFull = await models.wallet.findFirst({
where: { where: {
userId, userId,
type: wallet.def.walletType type: walletType
}, },
include: { include: {
[walletField]: true [walletField]: true