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')