From ccdf346954baa1b46220e7e4267d01874633fae6 Mon Sep 17 00:00:00 2001 From: k00b Date: Thu, 24 Oct 2024 15:30:56 -0500 Subject: [PATCH] server side config saves --- api/resolvers/vault.js | 4 +- api/resolvers/wallet.js | 191 +++++++++--------- api/typeDefs/user.js | 4 +- api/typeDefs/wallet.js | 6 +- components/fee-button.js | 5 +- components/items.js | 5 +- components/sub-select.js | 6 +- components/use-debounce-callback.js | 4 +- components/use-indexeddb.js | 10 +- components/user-list.js | 6 +- components/vault/use-vault-configurator.js | 7 +- components/wallet-logger.js | 10 +- fragments/users.js | 70 +------ fragments/wallet.js | 126 +++++------- lib/validate.js | 8 +- package-lock.json | 88 ++++---- package.json | 2 +- .../migration.sql | 33 ++- prisma/schema.prisma | 26 +-- wallets/common.js | 24 +-- wallets/config.js | 22 +- wallets/graphql.js | 9 +- wallets/index.js | 22 +- wallets/server.js | 8 +- 24 files changed, 325 insertions(+), 371 deletions(-) rename prisma/migrations/{20241021224248_vault => 20241024175439_vault}/migration.sql (63%) diff --git a/api/resolvers/vault.js b/api/resolvers/vault.js index 99c0486e..5915899f 100644 --- a/api/resolvers/vault.js +++ b/api/resolvers/vault.js @@ -55,8 +55,8 @@ export default { for (const entry of entries) { txs.push(models.vaultEntry.update({ - where: { id: entry.id }, - data: { key: entry.key, value: entry.value } + where: { userId_key: { userId: me.id, key: entry.key } }, + data: { value: entry.value } })) } await models.prisma.$transaction(txs) diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 146c81fe..c6273300 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -26,12 +26,13 @@ import { getNodeSockets, getOurPubkey } from '../lnd' function injectResolvers (resolvers) { console.group('injected GraphQL resolvers:') - for (const w of walletDefs) { - const resolverName = generateResolverName(w.walletField) + for (const walletDef of walletDefs) { + const resolverName = generateResolverName(walletDef.walletField) console.log(resolverName) 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) - const validData = await walletValidate(w, { ...data, ...settings, vaultEntries }) + // TODO: our validation should be improved + const validData = await walletValidate(walletDef, { ...data, ...settings, vaultEntries }) if (validData) { 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] }) @@ -39,10 +40,12 @@ function injectResolvers (resolvers) { return await upsertWallet({ wallet: { - field: w.walletField, - type: w.walletType + field: walletDef.walletField, + 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, data, @@ -643,17 +646,13 @@ export const addWalletLog = async ({ wallet, level, message }, { models }) => { } async function upsertWallet ( - { wallet, testCreateInvoice }, - { settings, data, priorityOnly, canSend, canReceive }, - { me, models } -) { - if (!me) throw new GqlAuthenticationError() + { wallet, testCreateInvoice }, { settings, data, vaultEntries = [] }, { me, models }) { + if (!me) { + throw new GqlAuthenticationError() + } assertApiKeyNotPermitted({ me }) - const { id, ...walletData } = data - const { autoWithdrawThreshold, autoWithdrawMaxFeePercent, enabled, priority } = settings - - if (testCreateInvoice && !priorityOnly && canReceive && enabled) { + if (testCreateInvoice) { try { await testCreateInvoice(data) } catch (err) { @@ -666,103 +665,111 @@ async function upsertWallet ( } } - return await models.$transaction(async (tx) => { - if (canReceive) { - await tx.user.update({ - where: { id: me.id }, - data: { - autoWithdrawMaxFeePercent, - autoWithdrawThreshold - } - }) - } + const { id, enabled, priority, ...walletData } = data + const { + autoWithdrawThreshold, + autoWithdrawMaxFeePercent, + autoWithdrawMaxFeeTotal + } = settings - let updatedWallet - if (id) { - const existingWalletTypeRecord = canReceive - ? await tx[wallet.field].findUnique({ - where: { walletId: Number(id) } - }) - : undefined + const txs = [] - 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 }, data: { enabled, priority, - canSend, - canReceive, - // if send-only config or priority only, don't update the wallet type record - ...(canReceive && !priorityOnly - ? { - [wallet.field]: existingWalletTypeRecord - ? { update: walletData } - : { create: walletData } - } - : {}) + [wallet.field]: { + update: { + where: { walletId: Number(id) }, + data: walletData + } + }, + vaultEntries: { + 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: { - ...(canReceive && !priorityOnly ? { [wallet.field]: true } : {}) + vaultEntries: true } }) - } else { - updatedWallet = await tx.wallet.create({ + ) + } else { + txs.push( + models.wallet.create({ + include: { + vaultEntries: true + }, data: { enabled, priority, - canSend, - canReceive, userId: me.id, type: wallet.type, - // if send-only config or priority only, don't update the wallet type record - ...(canReceive && !priorityOnly - ? { - [wallet.field]: { - create: walletData - } - } - : {}) + [wallet.field]: { + create: walletData + }, + vaultEntries: { + createMany: { + data: vaultEntries.map(({ key, value }) => ({ key, value, userId: me.id })) + } + } } }) - } + ) + } - const logs = [] - if (canReceive) { - logs.push({ - userId: me.id, - wallet: wallet.type, - level: enabled ? 'SUCCESS' : 'INFO', - message: id ? 'receive details updated' : 'wallet attached for receives' - }) - 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 + txs.push( + models.user.update({ + where: { id: me.id }, + data: { + autoWithdrawMaxFeePercent, + autoWithdrawThreshold, + autoWithdrawMaxFeeTotal + } }) + ) - 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 }) { diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index cfbfd98e..daeadf5c 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -182,11 +182,9 @@ export default gql` withdrawMaxFeeDefault: Int! autoWithdrawThreshold: Int autoWithdrawMaxFeePercent: Float -<<<<<<< HEAD autoWithdrawMaxFeeTotal: Int -======= vaultKeyHash: String ->>>>>>> 002b1d19 (user vault and server side client wallets) + walletsUpdatedAt: Date } type UserOptional { diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index ad697217..c0a88ee4 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -17,7 +17,7 @@ function mutationTypeDefs () { .filter(isServerField) .map(fieldToGqlArgOptional) 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 typeDef = `${resolverName}(${args}): Wallet` console.log(typeDef) @@ -91,8 +91,6 @@ const typeDefs = ` enabled: Boolean! priority: Int! wallet: WalletDetails! - canReceive: Boolean! - canSend: Boolean! vaultEntries: [VaultEntry!]! } @@ -100,8 +98,6 @@ const typeDefs = ` autoWithdrawThreshold: Int! autoWithdrawMaxFeePercent: Float! autoWithdrawMaxFeeTotal: Int! - priority: Int - enabled: Boolean } type Invoice { diff --git a/components/fee-button.js b/components/fee-button.js index 7926dc94..194dd123 100644 --- a/components/fee-button.js +++ b/components/fee-button.js @@ -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 [disabled, setDisabled] = useState(false) const { me } = useMe() diff --git a/components/items.js b/components/items.js index e09c2b06..738ee764 100644 --- a/components/items.js +++ b/components/items.js @@ -10,7 +10,10 @@ import { LIMIT } from '@/lib/cursor' import ItemFull from './item-full' 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 Foooter = Footer || MoreFooter const dat = useData(data, ssrData) diff --git a/components/sub-select.js b/components/sub-select.js index c4096222..7a682632 100644 --- a/components/sub-select.js +++ b/components/sub-select.js @@ -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 ? {} : { diff --git a/components/use-debounce-callback.js b/components/use-debounce-callback.js index 3bd4ffc0..7c7f5b24 100644 --- a/components/use-debounce-callback.js +++ b/components/use-debounce-callback.js @@ -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 memoFn = useCallback(fn, deps) useNoInitialEffect(debounce(() => memoFn(...args), time), [memoFn, time, args]) diff --git a/components/use-indexeddb.js b/components/use-indexeddb.js index 5086ff69..eac82872 100644 --- a/components/use-indexeddb.js +++ b/components/use-indexeddb.js @@ -4,7 +4,11 @@ export function getDbName (userId, 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 [error, setError] = useState(null) const [notSupported, setNotSupported] = useState(false) @@ -28,7 +32,7 @@ function useIndexedDB ({ dbName, storeName, options = { keyPath: 'id', autoIncre } catch (error) { handleError(error) } - }, [storeName, handleError]) + }, [storeName, handleError, operationQueue]) useEffect(() => { let isMounted = true @@ -81,7 +85,7 @@ function useIndexedDB ({ dbName, storeName, options = { keyPath: 'id', autoIncre db.close() } } - }, [dbName, storeName, version, indices, handleError, processQueue]) + }, [dbName, storeName, version, indices, options, handleError, processQueue]) const queueOperation = useCallback((operation) => { if (notSupported) { diff --git a/components/user-list.js b/components/user-list.js index c2c2f953..40f77286 100644 --- a/components/user-list.js +++ b/components/user-list.js @@ -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 (
{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 }) { const { data, fetchMore } = useQuery(query, { variables }) const dat = useData(data, ssrData) - const [statComps, setStatComps] = useState(seperate(STAT_COMPONENTS, Seperator)) + const [statComps, setStatComps] = useState(DEFAULT_STAT_COMPONENTS) useEffect(() => { // shift the stat we are sorting by to the front diff --git a/components/vault/use-vault-configurator.js b/components/vault/use-vault-configurator.js index 5056208d..f55e7cf1 100644 --- a/components/vault/use-vault-configurator.js +++ b/components/vault/use-vault-configurator.js @@ -2,7 +2,7 @@ import { useMutation, useQuery } from '@apollo/client' import { useMe } from '../me' import { useToast } from '../toast' 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 { CLEAR_VAULT, GET_VAULT_ENTRIES, UPDATE_VAULT_KEY } from '@/fragments/vault' import { toHex } from '@/lib/hex' @@ -21,7 +21,8 @@ const useImperativeQuery = (query) => { export function useVaultConfigurator () { const { me } = useMe() 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 getVaultEntries = useImperativeQuery(GET_VAULT_ENTRIES) const [key, setKey] = useState(null) @@ -46,7 +47,7 @@ export function useVaultConfigurator () { // 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 const [clearVault] = useMutation(CLEAR_VAULT, { diff --git a/components/wallet-logger.js b/components/wallet-logger.js index ca291c97..11daaade 100644 --- a/components/wallet-logger.js +++ b/components/wallet-logger.js @@ -92,11 +92,11 @@ function getWalletLogDbName (userId) { function useWalletLogDB () { const { me } = useMe() - const { add, getPage, clear, error, notSupported } = useIndexedDB({ - dbName: getWalletLogDbName(me?.id), - storeName: 'wallet_logs', - indices: INDICES - }) + // memoize the idb config to avoid re-creating it on every render + const idbConfig = useMemo(() => + ({ dbName: getWalletLogDbName(me?.id), storeName: 'wallet_logs', indices: INDICES }), [me?.id]) + const { add, getPage, clear, error, notSupported } = useIndexedDB(idbConfig) + return { add, getPage, clear, error, notSupported } } diff --git a/fragments/users.js b/fragments/users.js index 768be056..6feac837 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -3,68 +3,12 @@ import { COMMENTS, COMMENTS_ITEM_EXT_FIELDS } from './comments' import { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items' import { SUB_FULL_FIELDS } from './subs' -export const ME = gql` - { - me { - id - name - bioId - 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 - } +const STREAK_FIELDS = gql` + fragment StreakFields on User { + optional { + streak + gunStreak + horseStreak } } ` @@ -104,6 +48,8 @@ ${STREAK_FIELDS} upvotePopover wildWestMode disableFreebies + vaultKeyHash + walletsUpdatedAt } optional { isContributor diff --git a/fragments/wallet.js b/fragments/wallet.js index 0c880c5f..3e1a445c 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -106,86 +106,7 @@ mutation removeWallet($id: ID!) { removeWallet(id: $id) } ` - // 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` fragment WalletFields on Wallet { id @@ -197,12 +118,57 @@ export const WALLET_FIELDS = gql` key 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` ${WALLET_FIELDS} - query Wallets { wallets { ...WalletFields diff --git a/lib/validate.js b/lib/validate.js index 3ca29757..96753c94 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -42,11 +42,11 @@ export async function formikValidate (validate, data) { return result } -export async function walletValidate (wallet, data) { - if (typeof wallet.def.fieldValidation === 'function') { - return await formikValidate(wallet.def.fieldValidation, data) +export async function walletValidate (walletDef, data) { + if (typeof walletDef.fieldValidation === 'function') { + return await formikValidate(walletDef.fieldValidation, data) } else { - return await ssValidate(wallet.def.fieldValidation, data) + return await ssValidate(walletDef.fieldValidation, data) } } diff --git a/package-lock.json b/package-lock.json index 9d391222..d4dd4c9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "mdast-util-gfm": "^3.0.0", "mdast-util-to-string": "^4.0.0", "micromark-extension-gfm": "^3.0.0", - "next": "^14.2.15", + "next": "^14.2.16", "next-auth": "^4.24.8", "next-plausible": "^3.12.2", "next-seo": "^6.6.0", @@ -4124,9 +4124,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz", - "integrity": "sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ==" + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.16.tgz", + "integrity": "sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.15", @@ -4184,9 +4184,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz", - "integrity": "sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.16.tgz", + "integrity": "sha512-uFT34QojYkf0+nn6MEZ4gIWQ5aqGF11uIZ1HSxG+cSbj+Mg3+tYm8qXYd3dKN5jqKUm5rBVvf1PBRO/MeQ6rxw==", "cpu": [ "arm64" ], @@ -4199,9 +4199,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz", - "integrity": "sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.16.tgz", + "integrity": "sha512-mCecsFkYezem0QiZlg2bau3Xul77VxUD38b/auAjohMA22G9KTJneUYMv78vWoCCFkleFAhY1NIvbyjj1ncG9g==", "cpu": [ "x64" ], @@ -4214,9 +4214,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz", - "integrity": "sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.16.tgz", + "integrity": "sha512-yhkNA36+ECTC91KSyZcgWgKrYIyDnXZj8PqtJ+c2pMvj45xf7y/HrgI17hLdrcYamLfVt7pBaJUMxADtPaczHA==", "cpu": [ "arm64" ], @@ -4229,9 +4229,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz", - "integrity": "sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.16.tgz", + "integrity": "sha512-X2YSyu5RMys8R2lA0yLMCOCtqFOoLxrq2YbazFvcPOE4i/isubYjkh+JCpRmqYfEuCVltvlo+oGfj/b5T2pKUA==", "cpu": [ "arm64" ], @@ -4244,9 +4244,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz", - "integrity": "sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.16.tgz", + "integrity": "sha512-9AGcX7VAkGbc5zTSa+bjQ757tkjr6C/pKS7OK8cX7QEiK6MHIIezBLcQ7gQqbDW2k5yaqba2aDtaBeyyZh1i6Q==", "cpu": [ "x64" ], @@ -4259,9 +4259,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz", - "integrity": "sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.16.tgz", + "integrity": "sha512-Klgeagrdun4WWDaOizdbtIIm8khUDQJ/5cRzdpXHfkbY91LxBXeejL4kbZBrpR/nmgRrQvmz4l3OtttNVkz2Sg==", "cpu": [ "x64" ], @@ -4274,9 +4274,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz", - "integrity": "sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.16.tgz", + "integrity": "sha512-PwW8A1UC1Y0xIm83G3yFGPiOBftJK4zukTmk7DI1CebyMOoaVpd8aSy7K6GhobzhkjYvqS/QmzcfsWG2Dwizdg==", "cpu": [ "arm64" ], @@ -4289,9 +4289,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz", + "integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==", "cpu": [ "ia32" ], @@ -4304,9 +4304,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz", - "integrity": "sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.16.tgz", + "integrity": "sha512-OA7NtfxgirCjfqt+02BqxC3MIgM/JaGjw9tOe4fyZgPsqfseNiMPnCRP44Pfs+Gpo9zPN+SXaFsgP6vk8d571A==", "cpu": [ "x64" ], @@ -15494,11 +15494,11 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.15.tgz", - "integrity": "sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==", + "version": "14.2.16", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.16.tgz", + "integrity": "sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==", "dependencies": { - "@next/env": "14.2.15", + "@next/env": "14.2.16", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -15513,15 +15513,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.15", - "@next/swc-darwin-x64": "14.2.15", - "@next/swc-linux-arm64-gnu": "14.2.15", - "@next/swc-linux-arm64-musl": "14.2.15", - "@next/swc-linux-x64-gnu": "14.2.15", - "@next/swc-linux-x64-musl": "14.2.15", - "@next/swc-win32-arm64-msvc": "14.2.15", - "@next/swc-win32-ia32-msvc": "14.2.15", - "@next/swc-win32-x64-msvc": "14.2.15" + "@next/swc-darwin-arm64": "14.2.16", + "@next/swc-darwin-x64": "14.2.16", + "@next/swc-linux-arm64-gnu": "14.2.16", + "@next/swc-linux-arm64-musl": "14.2.16", + "@next/swc-linux-x64-gnu": "14.2.16", + "@next/swc-linux-x64-musl": "14.2.16", + "@next/swc-win32-arm64-msvc": "14.2.16", + "@next/swc-win32-ia32-msvc": "14.2.16", + "@next/swc-win32-x64-msvc": "14.2.16" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/package.json b/package.json index 6fb481e5..9ea6c48f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "mdast-util-gfm": "^3.0.0", "mdast-util-to-string": "^4.0.0", "micromark-extension-gfm": "^3.0.0", - "next": "^14.2.15", + "next": "^14.2.16", "next-auth": "^4.24.8", "next-plausible": "^3.12.2", "next-seo": "^6.6.0", diff --git a/prisma/migrations/20241021224248_vault/migration.sql b/prisma/migrations/20241024175439_vault/migration.sql similarity index 63% rename from prisma/migrations/20241021224248_vault/migration.sql rename to prisma/migrations/20241024175439_vault/migration.sql index dd318e1f..ca5c3ea4 100644 --- a/prisma/migrations/20241021224248_vault/migration.sql +++ b/prisma/migrations/20241024175439_vault/migration.sql @@ -11,16 +11,14 @@ ALTER TYPE "WalletType" ADD VALUE 'LNC'; ALTER TYPE "WalletType" ADD VALUE 'WEBLN'; -- AlterTable -ALTER TABLE "Wallet" ADD COLUMN "canReceive" BOOLEAN NOT NULL DEFAULT true, -ADD COLUMN "canSend" BOOLEAN NOT NULL DEFAULT false; - --- AlterTable -ALTER TABLE "users" ADD COLUMN "vaultKeyHash" TEXT NOT NULL DEFAULT ''; +ALTER TABLE "users" ADD COLUMN "vaultKeyHash" TEXT NOT NULL DEFAULT '', +ADD COLUMN "walletsUpdatedAt" TIMESTAMP(3); -- CreateTable CREATE TABLE "VaultEntry" ( "id" SERIAL NOT NULL, - "key" VARCHAR(64) NOT NULL, + "key" TEXT NOT NULL, + "iv" TEXT NOT NULL, "value" TEXT NOT NULL, "userId" INTEGER NOT NULL, "walletId" INTEGER, @@ -30,17 +28,32 @@ CREATE TABLE "VaultEntry" ( CONSTRAINT "VaultEntry_pkey" PRIMARY KEY ("id") ); --- CreateIndex -CREATE INDEX "VaultEntry_userId_idx" ON "VaultEntry"("userId"); - -- CreateIndex CREATE INDEX "VaultEntry_walletId_idx" ON "VaultEntry"("walletId"); -- 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 ALTER TABLE "VaultEntry" ADD CONSTRAINT "VaultEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey 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(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e9496a97..cd643e67 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -138,6 +138,7 @@ model User { oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer") oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees") vaultKeyHash String @default("") + walletsUpdatedAt DateTime? vaultEntries VaultEntry[] @relation("VaultEntries") @@index([photoId]) @@ -187,16 +188,14 @@ enum WalletType { } model Wallet { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @default(now()) @updatedAt @map("updated_at") - userId Int - label String? - enabled Boolean @default(true) - priority Int @default(0) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - canReceive Boolean @default(true) - canSend Boolean @default(false) + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + userId Int + label String? + enabled Boolean @default(true) + priority Int @default(0) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) // NOTE: this denormalized json field exists to make polymorphic joins efficient // when reading wallets ... it is populated by a trigger when wallet descendants update @@ -218,11 +217,13 @@ model Wallet { InvoiceForward InvoiceForward[] @@index([userId]) + @@index([priority]) } model VaultEntry { id Int @id @default(autoincrement()) - key String @db.VarChar(64) + key String @db.Text + iv String @db.Text value String @db.Text userId Int walletId Int? @@ -231,8 +232,7 @@ model VaultEntry { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") - @@unique([userId, key, walletId]) - @@index([userId]) + @@unique([userId, key]) @@index([walletId]) } diff --git a/wallets/common.js b/wallets/common.js index 877f49ed..76d33aa9 100644 --- a/wallets/common.js +++ b/wallets/common.js @@ -74,24 +74,24 @@ function checkFields ({ fields, config }) { return val } -export function isConfigured (wallet) { - return isSendConfigured(wallet) || isReceiveConfigured(wallet) +export function isConfigured ({ def, config }) { + return isSendConfigured({ def, config }) || isReceiveConfigured({ def, config }) } -function isSendConfigured (wallet) { - const fields = wallet.def.fields.filter(isClientField) - return checkFields({ fields, config: wallet.config }) +function isSendConfigured ({ def, config }) { + const fields = def.fields.filter(isClientField) + return checkFields({ fields, config }) } -function isReceiveConfigured (wallet) { - const fields = wallet.def.fields.filter(isServerField) - return checkFields({ fields, config: wallet.config }) +function isReceiveConfigured ({ def, config }) { + const fields = def.fields.filter(isServerField) + return checkFields({ fields, config }) } -export function canSend (wallet) { - return !!wallet.def.sendPayment && isSendConfigured(wallet) +export function canSend ({ def, config }) { + return !!def.sendPayment && isSendConfigured({ def, config }) } -export function canReceive (wallet) { - return !wallet.def.clientOnly && isReceiveConfigured(wallet) +export function canReceive ({ def, config }) { + return !def.clientOnly && isReceiveConfigured({ def, config }) } diff --git a/wallets/config.js b/wallets/config.js index c7d6a5b9..d4312ed9 100644 --- a/wallets/config.js +++ b/wallets/config.js @@ -35,13 +35,12 @@ export function useWalletConfigurator (wallet) { const _validate = useCallback(async (config, validateLightning = true) => { const { serverWithShared, clientWithShared } = siftConfig(wallet.def.fields, config) - console.log('sifted', siftConfig(wallet.def.fields, config)) let clientConfig = clientWithShared let serverConfig = serverWithShared - if (canSend(wallet)) { - let transformedConfig = await walletValidate(wallet, clientWithShared) + if (canSend({ def: wallet.def, config: clientConfig })) { + let transformedConfig = await walletValidate(wallet.def, clientWithShared) if (transformedConfig) { clientConfig = Object.assign(clientConfig, transformedConfig) } @@ -51,29 +50,29 @@ export function useWalletConfigurator (wallet) { clientConfig = Object.assign(clientConfig, transformedConfig) } } - } - - if (canReceive(wallet)) { - const transformedConfig = await walletValidate(wallet, serverConfig) + } else if (canReceive({ def: wallet.def, config: serverConfig })) { + const transformedConfig = await walletValidate(wallet.def, serverConfig) if (transformedConfig) { serverConfig = Object.assign(serverConfig, transformedConfig) } + } else { + throw new Error('configuration must be able to send or receive') } return { clientConfig, serverConfig } }, [wallet]) 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 (isActive) { await _saveToServer(serverConfig, clientConfig, validateLightning) } else { - if (canSend(wallet)) { + if (canSend({ def: wallet.def, config: clientConfig })) { await _saveToLocal(clientConfig) } - if (canReceive(wallet)) { + if (canReceive({ def: wallet.def, config: serverConfig })) { await _saveToServer(serverConfig, clientConfig, validateLightning) } } @@ -84,18 +83,19 @@ export function useWalletConfigurator (wallet) { }, [wallet.config?.id]) 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)) }, [me?.id, wallet.def.name]) const detach = useCallback(async () => { if (isActive) { + // if vault is active, detach all wallets from server await _detachFromServer() } else { if (wallet.config.id) { await _detachFromServer() } + // if vault is not active and has a client config, delete from local storage await _detachFromLocal() } }, [isActive, _detachFromServer, _detachFromLocal]) diff --git a/wallets/graphql.js b/wallets/graphql.js index cc399c32..0fbd055d 100644 --- a/wallets/graphql.js +++ b/wallets/graphql.js @@ -33,17 +33,18 @@ export function generateMutation (wallet) { .filter(isServerField) .map(f => `$${f.name}: String`) .join(', ') - headerArgs += ', $settings: AutowithdrawSettings!, $validateLightning: Boolean' + headerArgs += ', $enabled: Boolean, $priority: Int, $vaultEntries: [VaultEntryInput!], $settings: AutowithdrawSettings!, $validateLightning: Boolean' let inputArgs = 'id: $id, ' inputArgs += wallet.fields .filter(isServerField) .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} - ${resolverName}(${inputArgs}) { + mutation ${resolverName}(${headerArgs}) { + ${resolverName}(${inputArgs}) { ...WalletFields } }` diff --git a/wallets/index.js b/wallets/index.js index 8b1ff6b2..d5b65434 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -1,6 +1,6 @@ import { useMe } from '@/components/me' import { WALLETS } from '@/fragments/wallet' -import { LONG_POLL_INTERVAL, SSR } from '@/lib/constants' +import { SSR } from '@/lib/constants' import { useQuery } from '@apollo/client' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend } from './common' @@ -41,15 +41,17 @@ const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} })) export function WalletsProvider ({ children }) { const { decrypt } = useVault() + const { me } = useMe() const { wallets: localWallets, reloadLocalWallets } = useLocalWallets() - // 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 - const { data } = useQuery(WALLETS, { - pollInterval: LONG_POLL_INTERVAL, - nextFetchPolicy: 'cache-and-network', - skip: SSR - }) + const { data, refetch } = useQuery(WALLETS, + SSR ? {} : { nextFetchPolicy: 'cache-and-network' }) + + useEffect(() => { + if (me?.privates?.walletsUpdatedAt) { + refetch() + } + }, [me?.privates?.walletsUpdatedAt, me?.privates?.vaultKeyHash, refetch]) const wallets = useMemo(() => { // form wallets into a list of { config, def } @@ -60,7 +62,9 @@ export function WalletsProvider ({ children }) { 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 diff --git a/wallets/server.js b/wallets/server.js index 09624ecb..7e004aa6 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -17,6 +17,7 @@ import { parsePaymentRequest } from 'ln-service' import { toPositiveNumber } from '@/lib/validate' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' import { withTimeout } from '@/lib/time' +import { canReceive } from './common' 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 }) { // get the wallets in order of priority const wallets = await models.wallet.findMany({ - where: { userId, enabled: true, canReceive: true }, + where: { userId, enabled: true }, include: { user: true }, @@ -42,11 +43,14 @@ export async function createInvoice (userId, { msats, description, descriptionHa const w = walletDefs.find(w => w.walletType === wallet.def.walletType) try { const { walletType, walletField, createInvoice } = w + if (!canReceive({ def: w, config: wallet })) { + continue + } const walletFull = await models.wallet.findFirst({ where: { userId, - type: wallet.def.walletType + type: walletType }, include: { [walletField]: true