sync/desync from localstorage on vault connect/disconnect
This commit is contained in:
parent
0c8180d89c
commit
b1fc341017
|
@ -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 }
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
/*
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -99,6 +99,13 @@ function getClient (uri) {
|
|||
Fact: {
|
||||
keyFields: ['id', 'type']
|
||||
},
|
||||
Wallet: {
|
||||
fields: {
|
||||
vaultEntries: {
|
||||
replace: true
|
||||
}
|
||||
}
|
||||
},
|
||||
Query: {
|
||||
fields: {
|
||||
sub: {
|
||||
|
|
12
lib/yup.js
12
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}` })
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
</p>
|
||||
<p className='line-height-md'>
|
||||
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.
|
||||
<strong>Once you leave this page, this passphrase cannot be shown again.</strong> Connect all the devices you plan to use or write this passphrase down somewhere safe.
|
||||
</p>
|
||||
<PasswordInput
|
||||
label='passphrase'
|
||||
|
|
|
@ -96,3 +96,68 @@ export function canSend ({ def, config }) {
|
|||
export function canReceive ({ def, config }) {
|
||||
return def.fields.some(f => 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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<WalletsContext.Provider value={{ wallets, reloadLocalWallets, setPriorities }}>
|
||||
<WalletsContext.Provider
|
||||
value={{
|
||||
wallets,
|
||||
reloadLocalWallets,
|
||||
setPriorities,
|
||||
onVaultKeySet: syncLocalWallets,
|
||||
beforeDisconnectVault: unsyncLocalWallets
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WalletsContext.Provider>
|
||||
)
|
||||
|
@ -172,5 +231,7 @@ export function useWallet (name) {
|
|||
}
|
||||
}, [wallet, logger])
|
||||
|
||||
if (!wallet) return null
|
||||
|
||||
return { ...wallet, sendPayment }
|
||||
}
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue