From b1fc341017ea3489e3dbec2582abaf1dded3d712 Mon Sep 17 00:00:00 2001 From: k00b Date: Mon, 28 Oct 2024 13:41:20 -0500 Subject: [PATCH] sync/desync from localstorage on vault connect/disconnect --- api/resolvers/vault.js | 1 - api/resolvers/wallet.js | 61 +++++++++--------- api/typeDefs/wallet.js | 2 +- components/invoice.js | 2 +- components/use-paid-mutation.js | 3 +- components/vault/use-vault-configurator.js | 21 ++++--- fragments/users.js | 2 +- lib/apollo.js | 7 +++ lib/yup.js | 12 ++-- pages/settings/passphrase/index.js | 6 +- wallets/common.js | 65 +++++++++++++++++++ wallets/config.js | 70 ++++----------------- wallets/graphql.js | 2 +- wallets/index.js | 73 ++++++++++++++++++++-- wallets/validate.js | 7 ++- wallets/webln/client.js | 1 + 16 files changed, 216 insertions(+), 119 deletions(-) diff --git a/api/resolvers/vault.js b/api/resolvers/vault.js index 8ee4237e..cd82d4e1 100644 --- a/api/resolvers/vault.js +++ b/api/resolvers/vault.js @@ -52,7 +52,6 @@ export default { } for (const entry of entries) { - console.log(entry) txs.push(models.vaultEntry.update({ where: { userId_key: { userId: me.id, key: entry.key } }, data: { value: entry.value, iv: entry.iv } diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index ecf5aacf..4b21ac40 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -31,10 +31,11 @@ function injectResolvers (resolvers) { const resolverName = generateResolverName(walletDef.walletField) console.log(resolverName) resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => { + console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data }) const validData = await validateWallet(walletDef, { ...data, ...settings, vaultEntries }, { serverSide: true }) 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] }) + data && Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] }) + settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] }) } return await upsertWallet({ @@ -654,7 +655,7 @@ export const addWalletLog = async ({ wallet, level, message }, { models }) => { } async function upsertWallet ( - { wallet, testCreateInvoice }, { settings, data, vaultEntries = [] }, { me, models }) { + { wallet, testCreateInvoice }, { settings, data, vaultEntries }, { me, models }) { if (!me) { throw new GqlAuthenticationError() } @@ -674,11 +675,6 @@ async function upsertWallet ( } const { id, enabled, priority, ...walletData } = data - const { - autoWithdrawThreshold, - autoWithdrawMaxFeePercent, - autoWithdrawMaxFeeTotal - } = settings const txs = [] @@ -709,18 +705,23 @@ async function upsertWallet ( } } : {}), - vaultEntries: { - deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({ - userId: me.id, key - })), - create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({ - key, iv, value, userId: me.id - })), - update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({ - where: { userId_key: { userId: me.id, key } }, - data: { value, iv } - })) - } + ...(vaultEntries + ? { + vaultEntries: { + deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({ + userId: me.id, key + })), + create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({ + key, iv, value, userId: me.id + })), + update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({ + where: { userId_key: { userId: me.id, key } }, + data: { value, iv } + })) + } + } + : {}) + }, include: { vaultEntries: true @@ -742,7 +743,7 @@ async function upsertWallet ( ...(Object.keys(walletData).length > 0 ? { [wallet.field]: { create: walletData } } : {}), vaultEntries: { createMany: { - data: vaultEntries.map(({ key, value }) => ({ key, value, userId: me.id })) + data: vaultEntries?.map(({ key, iv, value }) => ({ key, iv, value, userId: me.id })) } } } @@ -750,16 +751,14 @@ async function upsertWallet ( ) } - txs.push( - models.user.update({ - where: { id: me.id }, - data: { - autoWithdrawMaxFeePercent, - autoWithdrawThreshold, - autoWithdrawMaxFeeTotal - } - }) - ) + if (settings) { + txs.push( + models.user.update({ + where: { id: me.id }, + data: settings + }) + ) + } txs.push( models.walletLog.createMany({ diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index c26a5b9d..4cd011cf 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 += 'enabled: Boolean, priority: Int, vaultEntries: [VaultEntryInput!], settings: AutowithdrawSettings!, validateLightning: 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) diff --git a/components/invoice.js b/components/invoice.js index 1d8ab28e..3f92c9ba 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -8,7 +8,7 @@ import Bolt11Info from './bolt11-info' import { useQuery } from '@apollo/client' import { INVOICE } from '@/fragments/wallet' import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants' -import { NoAttachedWalletError } from './payment' +import { NoAttachedWalletError } from '@/wallets/errors' import ItemJob from './item-job' import Item from './item' import { CommentFlat } from './comment' diff --git a/components/use-paid-mutation.js b/components/use-paid-mutation.js index 93009609..765508b5 100644 --- a/components/use-paid-mutation.js +++ b/components/use-paid-mutation.js @@ -1,6 +1,7 @@ import { useApolloClient, useLazyQuery, useMutation } from '@apollo/client' import { useCallback, useState } from 'react' -import { InvoiceCanceledError, InvoiceExpiredError, useInvoice, useQrPayment, useWalletPayment } from './payment' +import { useInvoice, useQrPayment, useWalletPayment } from './payment' +import { InvoiceCanceledError, InvoiceExpiredError } from '@/wallets/errors' import { GET_PAID_ACTION } from '@/fragments/paidAction' /* diff --git a/components/vault/use-vault-configurator.js b/components/vault/use-vault-configurator.js index b62b4cee..4ef5068f 100644 --- a/components/vault/use-vault-configurator.js +++ b/components/vault/use-vault-configurator.js @@ -18,7 +18,7 @@ const useImperativeQuery = (query) => { return imperativelyCallQuery } -export function useVaultConfigurator () { +export function useVaultConfigurator ({ onVaultKeySet, beforeDisconnectVault } = {}) { const { me } = useMe() const toaster = useToast() const idbConfig = useMemo(() => ({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault', options: {} }), [me?.id]) @@ -63,6 +63,7 @@ export function useVaultConfigurator () { }) const disconnectVault = useCallback(async () => { + beforeDisconnectVault?.() await remove('key') setKey(null) setKeyHash(null) @@ -75,12 +76,16 @@ export function useVaultConfigurator () { const vaultKey = await deriveKey(me.id, passphrase) const { data } = await getVaultEntries() - // TODO: push any local configurations to the server so long as they don't conflict - // delete any locally stored configurations after vault key is set + const encrypt = async value => { + return await encryptValue(vaultKey.key, value) + } + const entries = [] - for (const { key, iv, value } of data.getVaultEntries) { - const plainValue = await decryptValue(oldKeyValue.key, { iv, value }) - entries.push({ key, ...await encryptValue(vaultKey.key, plainValue) }) + if (oldKeyValue?.key) { + for (const { key, iv, value } of data.getVaultEntries) { + const plainValue = await decryptValue(oldKeyValue.key, { iv, value }) + entries.push({ key, ...await encrypt(plainValue) }) + } } await updateVaultKey({ @@ -93,13 +98,15 @@ export function useVaultConfigurator () { toaster.danger(error.graphQLErrors[0].message) } }) + setKey(vaultKey) setKeyHash(vaultKey.hash) await set('key', vaultKey) + onVaultKeySet?.(encrypt).catch(console.error) } catch (e) { toaster.danger(e.message) } - }, [getVaultEntries, updateVaultKey, set, get, remove]) + }, [getVaultEntries, updateVaultKey, set, get, remove, onVaultKeySet]) return { key, setVaultKey, clearVault, disconnectVault } } diff --git a/fragments/users.js b/fragments/users.js index 6feac837..1eaae258 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -3,7 +3,7 @@ import { COMMENTS, COMMENTS_ITEM_EXT_FIELDS } from './comments' import { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items' import { SUB_FULL_FIELDS } from './subs' -const STREAK_FIELDS = gql` +export const STREAK_FIELDS = gql` fragment StreakFields on User { optional { streak diff --git a/lib/apollo.js b/lib/apollo.js index 2bde508f..d72a035d 100644 --- a/lib/apollo.js +++ b/lib/apollo.js @@ -99,6 +99,13 @@ function getClient (uri) { Fact: { keyFields: ['id', 'type'] }, + Wallet: { + fields: { + vaultEntries: { + replace: true + } + } + }, Query: { fields: { sub: { diff --git a/lib/yup.js b/lib/yup.js index 0571696d..fbbf0b12 100644 --- a/lib/yup.js +++ b/lib/yup.js @@ -140,13 +140,13 @@ addMethod(string, 'hex', function (msg) { addMethod(string, 'nwcUrl', function () { return this.test({ - test: async (nwcUrl, context) => { + test: (nwcUrl, context) => { if (!nwcUrl) return true // run validation in sequence to control order of errors // inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180 try { - await string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validate(nwcUrl) + string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validateSync(nwcUrl) let relayUrl, walletPubkey, secret try { ({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)) @@ -154,9 +154,9 @@ addMethod(string, 'nwcUrl', function () { // invalid URL error. handle as if pubkey validation failed to not confuse user. throw new Error('pubkey must be 64 hex chars') } - await string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validate(walletPubkey) - await string().required('relay url required').trim().wss('relay must use wss://').validate(relayUrl) - await string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validate(secret) + string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validateSync(walletPubkey) + string().required('relay url required').trim().wss('relay must use wss://').validateSync(relayUrl) + string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validateSync(secret) } catch (err) { return context.createError({ message: err.message }) } @@ -172,7 +172,7 @@ addMethod(array, 'equalto', function equals ( return this.test({ name: 'equalto', message: message || `${this.path} has invalid values`, - test: function (items) { + test: function (items = []) { if (items.length < required.length) { return this.createError({ message: `Expected ${this.path} to be at least ${required.length} items, but got ${items.length}` }) } diff --git a/pages/settings/passphrase/index.js b/pages/settings/passphrase/index.js index 39461c25..612d53ca 100644 --- a/pages/settings/passphrase/index.js +++ b/pages/settings/passphrase/index.js @@ -10,12 +10,14 @@ import { deviceSyncSchema } from '@/lib/validate' import RefreshIcon from '@/svgs/refresh-line.svg' import { useCallback, useEffect, useState } from 'react' import { useToast } from '@/components/toast' +import { useWallets } from '@/wallets/index' export const getServerSideProps = getGetServerSideProps({ authRequired: true }) export default function DeviceSync ({ ssrData }) { const { me } = useMe() - const { key, setVaultKey, clearVault, disconnectVault } = useVaultConfigurator() + const { onVaultKeySet, beforeDisconnectVault } = useWallets() + const { key, setVaultKey, clearVault, disconnectVault } = useVaultConfigurator({ onVaultKeySet, beforeDisconnectVault }) const [passphrase, setPassphrase] = useState() const setSeedPassphrase = useCallback(async (passphrase) => { @@ -51,7 +53,7 @@ function Connect ({ passphrase }) { On your other devices, navigate to device sync settings and enter this exact passphrase.

- Once you leave this page, this passphrase cannot be shown again. Connect all the devices you plan to use or write this passphrase down somewhere safe. + Once you leave this page, this passphrase cannot be shown again. Connect all the devices you plan to use or write this passphrase down somewhere safe.

f.serverOnly) && isReceiveConfigured({ def, config }) } + +export function siftConfig (fields, config) { + const sifted = { + clientOnly: {}, + serverOnly: {}, + shared: {}, + serverWithShared: {}, + clientWithShared: {}, + settings: null + } + + for (const [key, value] of Object.entries(config)) { + if (['id'].includes(key)) { + sifted.serverOnly[key] = value + continue + } + + if (['autoWithdrawMaxFeePercent', 'autoWithdrawThreshold', 'autoWithdrawMaxFeeTotal'].includes(key)) { + sifted.serverOnly[key] = value + sifted.settings = { ...sifted.settings, [key]: value } + continue + } + + const field = fields.find(({ name }) => name === key) + + if (field) { + if (field.serverOnly) { + sifted.serverOnly[key] = value + } else if (field.clientOnly) { + sifted.clientOnly[key] = value + } else { + sifted.shared[key] = value + } + } else if (['enabled', 'priority'].includes(key)) { + sifted.shared[key] = value + } + } + + sifted.serverWithShared = { ...sifted.shared, ...sifted.serverOnly } + sifted.clientWithShared = { ...sifted.shared, ...sifted.clientOnly } + + return sifted +} + +export async function upsertWalletVariables ({ def, config }, encrypt, append = {}) { + const { serverWithShared, settings, clientOnly } = siftConfig(def.fields, config) + // if we are disconnected from the vault, we leave vaultEntries undefined so we don't + // delete entries from connected devices + let vaultEntries + if (clientOnly && encrypt) { + vaultEntries = [] + for (const [key, value] of Object.entries(clientOnly)) { + if (value) { + vaultEntries.push({ key, ...await encrypt(value) }) + } + } + } + + return { ...serverWithShared, settings, vaultEntries, ...append } +} + +export async function saveWalletLocally (name, config, userId) { + const storageKey = getStorageKey(name, userId) + window.localStorage.setItem(storageKey, JSON.stringify(config)) +} diff --git a/wallets/config.js b/wallets/config.js index 3a7e4a67..16014040 100644 --- a/wallets/config.js +++ b/wallets/config.js @@ -1,7 +1,7 @@ import { useMe } from '@/components/me' import useVault from '@/components/vault/use-vault' import { useCallback } from 'react' -import { canReceive, canSend, getStorageKey } from './common' +import { canReceive, canSend, getStorageKey, saveWalletLocally, siftConfig, upsertWalletVariables } from './common' import { useMutation } from '@apollo/client' import { generateMutation } from './graphql' import { REMOVE_WALLET } from '@/fragments/wallet' @@ -18,21 +18,15 @@ export function useWalletConfigurator (wallet) { const [removeWallet] = useMutation(REMOVE_WALLET) const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => { - const { serverWithShared, settings, clientOnly } = siftConfig(wallet.def.fields, { ...serverConfig, ...clientConfig }) - const vaultEntries = [] - if (clientOnly && isActive) { - for (const [key, value] of Object.entries(clientOnly)) { - if (value) { - vaultEntries.push({ key, ...await encrypt(value) }) - } - } - } - - await upsertWallet({ variables: { ...serverWithShared, settings, validateLightning, vaultEntries } }) - }, [encrypt, isActive, wallet.def.fields]) + const variables = await upsertWalletVariables( + { def: wallet.def, config: { ...serverConfig, ...clientConfig } }, + isActive && encrypt, + { validateLightning }) + await upsertWallet({ variables }) + }, [encrypt, isActive, wallet.def]) const _saveToLocal = useCallback(async (newConfig) => { - window.localStorage.setItem(getStorageKey(wallet.def.name, me?.id), JSON.stringify(newConfig)) + saveWalletLocally(wallet.def.name, newConfig, me?.id) reloadLocalWallets() }, [me?.id, wallet.def.name, reloadLocalWallets]) @@ -89,12 +83,13 @@ export function useWalletConfigurator (wallet) { } if (canReceive({ def: wallet.def, config: serverConfig })) { await _saveToServer(serverConfig, clientConfig, validateLightning) - } else { + } else if (wallet.config.id) { // if it previously had a server config, remove it await _detachFromServer() } } - }, [isActive, _saveToServer, _saveToLocal, _validate, _detachFromLocal, _detachFromServer]) + }, [isActive, wallet.def, _saveToServer, _saveToLocal, _validate, + _detachFromLocal, _detachFromServer]) const detach = useCallback(async () => { if (isActive) { @@ -112,46 +107,3 @@ export function useWalletConfigurator (wallet) { return { save, detach } } - -function siftConfig (fields, config) { - const sifted = { - clientOnly: {}, - serverOnly: {}, - shared: {}, - serverWithShared: {}, - clientWithShared: {}, - settings: {} - } - - for (const [key, value] of Object.entries(config)) { - if (['id'].includes(key)) { - sifted.serverOnly[key] = value - continue - } - - if (['autoWithdrawMaxFeePercent', 'autoWithdrawThreshold', 'autoWithdrawMaxFeeTotal'].includes(key)) { - sifted.serverOnly[key] = value - sifted.settings[key] = value - continue - } - - const field = fields.find(({ name }) => name === key) - - if (field) { - if (field.serverOnly) { - sifted.serverOnly[key] = value - } else if (field.clientOnly) { - sifted.clientOnly[key] = value - } else { - sifted.shared[key] = value - } - } else { - sifted.shared[key] = value - } - } - - sifted.serverWithShared = { ...sifted.shared, ...sifted.serverOnly } - sifted.clientWithShared = { ...sifted.shared, ...sifted.clientOnly } - - return sifted -} diff --git a/wallets/graphql.js b/wallets/graphql.js index 0fbd055d..b39b6ebd 100644 --- a/wallets/graphql.js +++ b/wallets/graphql.js @@ -33,7 +33,7 @@ export function generateMutation (wallet) { .filter(isServerField) .map(f => `$${f.name}: String`) .join(', ') - headerArgs += ', $enabled: Boolean, $priority: Int, $vaultEntries: [VaultEntryInput!], $settings: AutowithdrawSettings!, $validateLightning: Boolean' + headerArgs += ', $enabled: Boolean, $priority: Int, $vaultEntries: [VaultEntryInput!], $settings: AutowithdrawSettings, $validateLightning: Boolean' let inputArgs = 'id: $id, ' inputArgs += wallet.fields diff --git a/wallets/index.js b/wallets/index.js index ba8502ec..fa7d0c8a 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -1,13 +1,14 @@ import { useMe } from '@/components/me' import { SET_WALLET_PRIORITY, WALLETS } from '@/fragments/wallet' import { SSR } from '@/lib/constants' -import { useMutation, useQuery } from '@apollo/client' +import { useApolloClient, useMutation, useQuery } from '@apollo/client' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured } from './common' +import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common' import useVault from '@/components/vault/use-vault' import { useWalletLogger } from '@/components/wallet-logger' import { bolt11Tags } from '@/lib/bolt11' import walletDefs from 'wallets/client' +import { generateMutation } from './graphql' const WalletsContext = createContext({ wallets: [] @@ -31,11 +32,19 @@ function useLocalWallets () { setWallets(wallets) }, [me?.id, setWallets]) + const removeWallets = useCallback(() => { + for (const wallet of wallets) { + const storageKey = getStorageKey(wallet.def.name, me?.id) + window.localStorage.removeItem(storageKey) + } + setWallets([]) + }, [wallets, setWallets, me?.id]) + useEffect(() => { loadWallets() }, [loadWallets]) - return { wallets, reloadLocalWallets: loadWallets } + return { wallets, reloadLocalWallets: loadWallets, removeLocalWallets: removeWallets } } const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} })) @@ -43,9 +52,10 @@ const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} })) export function WalletsProvider ({ children }) { const { isActive, decrypt } = useVault() const { me } = useMe() - const { wallets: localWallets, reloadLocalWallets } = useLocalWallets() + const { wallets: localWallets, reloadLocalWallets, removeLocalWallets } = useLocalWallets() const [setWalletPriority] = useMutation(SET_WALLET_PRIORITY) const [serverWallets, setServerWallets] = useState([]) + const client = useApolloClient() const { data, refetch } = useQuery(WALLETS, SSR ? {} : { nextFetchPolicy: 'cache-and-network' }) @@ -58,7 +68,7 @@ export function WalletsProvider ({ children }) { }, [me?.privates?.walletsUpdatedAt, me?.privates?.vaultKeyHash, refetch]) useEffect(() => { - async function loadWallets () { + const loadWallets = async () => { if (!data?.wallets) return // form wallets into a list of { config, def } const wallets = [] @@ -79,6 +89,7 @@ export function WalletsProvider ({ children }) { // on the client, it's stored unnested wallets.push({ config: { ...config, ...w.wallet }, def }) } + setServerWallets(wallets) } loadWallets() @@ -108,6 +119,46 @@ export function WalletsProvider ({ children }) { .map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled })) }, [serverWallets, localWallets]) + const settings = useMemo(() => { + return { + autoWithdrawMaxFeePercent: me?.privates?.autoWithdrawMaxFeePercent, + autoWithdrawThreshold: me?.privates?.autoWithdrawThreshold, + autoWithdrawMaxFeeTotal: me?.privates?.autoWithdrawMaxFeeTotal + } + }, [me?.privates?.autoWithdrawMaxFeePercent, me?.privates?.autoWithdrawThreshold, me?.privates?.autoWithdrawMaxFeeTotal]) + + // if the vault key is set, and we have local wallets, + // we'll send any merged local wallets to the server, and delete them from local storage + const syncLocalWallets = useCallback(async encrypt => { + const walletsToSync = wallets.filter(w => + // only sync wallets that have a local config + localWallets.some(localWallet => localWallet.def.name === w.def.name && !!localWallet.config) + ) + if (encrypt && walletsToSync.length > 0) { + for (const wallet of walletsToSync) { + const mutation = generateMutation(wallet.def) + const append = {} + // if the wallet has server-only fields set, add the settings to the mutation variables + if (wallet.def.fields.some(f => f.serverOnly && wallet.config[f.name])) { + append.settings = settings + } + const variables = await upsertWalletVariables(wallet, encrypt, append) + await client.mutate({ mutation, variables }) + } + removeLocalWallets() + } + }, [wallets, localWallets, removeLocalWallets, settings]) + + const unsyncLocalWallets = useCallback(() => { + for (const wallet of wallets) { + const { clientWithShared } = siftConfig(wallet.def.fields, wallet.config) + if (canSend({ def: wallet.def, config: clientWithShared })) { + saveWalletLocally(wallet.def.name, clientWithShared, me?.id) + } + } + reloadLocalWallets() + }, [wallets, me?.id, reloadLocalWallets]) + const setPriorities = useCallback(async (priorities) => { for (const { wallet, priority } of priorities) { if (!isConfigured(wallet)) { @@ -133,7 +184,15 @@ export function WalletsProvider ({ children }) { // provides priority sorted wallets to children, a function to reload local wallets, // and a function to set priorities return ( - + {children} ) @@ -172,5 +231,7 @@ export function useWallet (name) { } }, [wallet, logger]) + if (!wallet) return null + return { ...wallet, sendPayment } } diff --git a/wallets/validate.js b/wallets/validate.js index 0e91a559..8131189c 100644 --- a/wallets/validate.js +++ b/wallets/validate.js @@ -74,8 +74,11 @@ function composeWalletSchema (walletDef, serverSide) { if (!optional) { acc[name] = acc[name].required('Required') } else if (requiredWithout) { - acc[name] = acc[name].when([requiredWithout], ([pairSetting], schema) => { - if (!pairSetting) return schema.required(`required if ${requiredWithout} not set`) + // if we are the server, the pairSetting will be in the vaultEntries array + acc[name] = acc[name].when([serverSide ? 'vaultEntries' : requiredWithout], ([pairSetting], schema) => { + if (!pairSetting || (serverSide && !pairSetting.some(v => v.key === requiredWithout))) { + return schema.required(`required if ${requiredWithout} not set`) + } return Yup.mixed().or([schema.test({ test: value => value !== pairSetting, message: `${name} cannot be the same as ${requiredWithout}` diff --git a/wallets/webln/client.js b/wallets/webln/client.js index f47e494c..fd66dd02 100644 --- a/wallets/webln/client.js +++ b/wallets/webln/client.js @@ -1,6 +1,7 @@ import { useEffect } from 'react' import { SSR } from '@/lib/constants' export * from 'wallets/webln' + export const sendPayment = async (bolt11) => { if (typeof window.webln === 'undefined') { throw new Error('WebLN provider not found')