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) {
|
for (const entry of entries) {
|
||||||
console.log(entry)
|
|
||||||
txs.push(models.vaultEntry.update({
|
txs.push(models.vaultEntry.update({
|
||||||
where: { userId_key: { userId: me.id, key: entry.key } },
|
where: { userId_key: { userId: me.id, key: entry.key } },
|
||||||
data: { value: entry.value, iv: entry.iv }
|
data: { value: entry.value, iv: entry.iv }
|
||||||
|
|
|
@ -31,10 +31,11 @@ function injectResolvers (resolvers) {
|
||||||
const resolverName = generateResolverName(walletDef.walletField)
|
const resolverName = generateResolverName(walletDef.walletField)
|
||||||
console.log(resolverName)
|
console.log(resolverName)
|
||||||
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => {
|
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => {
|
||||||
|
console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data })
|
||||||
const validData = await validateWallet(walletDef, { ...data, ...settings, vaultEntries }, { serverSide: true })
|
const validData = await validateWallet(walletDef, { ...data, ...settings, vaultEntries }, { serverSide: true })
|
||||||
if (validData) {
|
if (validData) {
|
||||||
Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] })
|
data && 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] })
|
settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] })
|
||||||
}
|
}
|
||||||
|
|
||||||
return await upsertWallet({
|
return await upsertWallet({
|
||||||
|
@ -654,7 +655,7 @@ export const addWalletLog = async ({ wallet, level, message }, { models }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertWallet (
|
async function upsertWallet (
|
||||||
{ wallet, testCreateInvoice }, { settings, data, vaultEntries = [] }, { me, models }) {
|
{ wallet, testCreateInvoice }, { settings, data, vaultEntries }, { me, models }) {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GqlAuthenticationError()
|
throw new GqlAuthenticationError()
|
||||||
}
|
}
|
||||||
|
@ -674,11 +675,6 @@ async function upsertWallet (
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, enabled, priority, ...walletData } = data
|
const { id, enabled, priority, ...walletData } = data
|
||||||
const {
|
|
||||||
autoWithdrawThreshold,
|
|
||||||
autoWithdrawMaxFeePercent,
|
|
||||||
autoWithdrawMaxFeeTotal
|
|
||||||
} = settings
|
|
||||||
|
|
||||||
const txs = []
|
const txs = []
|
||||||
|
|
||||||
|
@ -709,18 +705,23 @@ async function upsertWallet (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
vaultEntries: {
|
...(vaultEntries
|
||||||
deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({
|
? {
|
||||||
userId: me.id, key
|
vaultEntries: {
|
||||||
})),
|
deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({
|
||||||
create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({
|
userId: me.id, key
|
||||||
key, iv, value, userId: me.id
|
})),
|
||||||
})),
|
create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({
|
||||||
update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({
|
key, iv, value, userId: me.id
|
||||||
where: { userId_key: { userId: me.id, key } },
|
})),
|
||||||
data: { value, iv }
|
update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({
|
||||||
}))
|
where: { userId_key: { userId: me.id, key } },
|
||||||
}
|
data: { value, iv }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
vaultEntries: true
|
vaultEntries: true
|
||||||
|
@ -742,7 +743,7 @@ async function upsertWallet (
|
||||||
...(Object.keys(walletData).length > 0 ? { [wallet.field]: { create: walletData } } : {}),
|
...(Object.keys(walletData).length > 0 ? { [wallet.field]: { create: walletData } } : {}),
|
||||||
vaultEntries: {
|
vaultEntries: {
|
||||||
createMany: {
|
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(
|
if (settings) {
|
||||||
models.user.update({
|
txs.push(
|
||||||
where: { id: me.id },
|
models.user.update({
|
||||||
data: {
|
where: { id: me.id },
|
||||||
autoWithdrawMaxFeePercent,
|
data: settings
|
||||||
autoWithdrawThreshold,
|
})
|
||||||
autoWithdrawMaxFeeTotal
|
)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
txs.push(
|
txs.push(
|
||||||
models.walletLog.createMany({
|
models.walletLog.createMany({
|
||||||
|
|
|
@ -17,7 +17,7 @@ function mutationTypeDefs () {
|
||||||
.filter(isServerField)
|
.filter(isServerField)
|
||||||
.map(fieldToGqlArgOptional)
|
.map(fieldToGqlArgOptional)
|
||||||
if (serverFields.length > 0) args += serverFields.join(', ') + ','
|
if (serverFields.length > 0) args += serverFields.join(', ') + ','
|
||||||
args += '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 resolverName = generateResolverName(w.walletField)
|
||||||
const typeDef = `${resolverName}(${args}): Wallet`
|
const typeDef = `${resolverName}(${args}): Wallet`
|
||||||
console.log(typeDef)
|
console.log(typeDef)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Bolt11Info from './bolt11-info'
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import { INVOICE } from '@/fragments/wallet'
|
import { INVOICE } from '@/fragments/wallet'
|
||||||
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
|
||||||
import { NoAttachedWalletError } from './payment'
|
import { NoAttachedWalletError } from '@/wallets/errors'
|
||||||
import ItemJob from './item-job'
|
import ItemJob from './item-job'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
import { CommentFlat } from './comment'
|
import { CommentFlat } from './comment'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
import { useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||||
import { useCallback, useState } from 'react'
|
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'
|
import { GET_PAID_ACTION } from '@/fragments/paidAction'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -18,7 +18,7 @@ const useImperativeQuery = (query) => {
|
||||||
return imperativelyCallQuery
|
return imperativelyCallQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVaultConfigurator () {
|
export function useVaultConfigurator ({ onVaultKeySet, beforeDisconnectVault } = {}) {
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const idbConfig = useMemo(() => ({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault', options: {} }), [me?.id])
|
const idbConfig = useMemo(() => ({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault', options: {} }), [me?.id])
|
||||||
|
@ -63,6 +63,7 @@ export function useVaultConfigurator () {
|
||||||
})
|
})
|
||||||
|
|
||||||
const disconnectVault = useCallback(async () => {
|
const disconnectVault = useCallback(async () => {
|
||||||
|
beforeDisconnectVault?.()
|
||||||
await remove('key')
|
await remove('key')
|
||||||
setKey(null)
|
setKey(null)
|
||||||
setKeyHash(null)
|
setKeyHash(null)
|
||||||
|
@ -75,12 +76,16 @@ export function useVaultConfigurator () {
|
||||||
const vaultKey = await deriveKey(me.id, passphrase)
|
const vaultKey = await deriveKey(me.id, passphrase)
|
||||||
const { data } = await getVaultEntries()
|
const { data } = await getVaultEntries()
|
||||||
|
|
||||||
// TODO: push any local configurations to the server so long as they don't conflict
|
const encrypt = async value => {
|
||||||
// delete any locally stored configurations after vault key is set
|
return await encryptValue(vaultKey.key, value)
|
||||||
|
}
|
||||||
|
|
||||||
const entries = []
|
const entries = []
|
||||||
for (const { key, iv, value } of data.getVaultEntries) {
|
if (oldKeyValue?.key) {
|
||||||
const plainValue = await decryptValue(oldKeyValue.key, { iv, value })
|
for (const { key, iv, value } of data.getVaultEntries) {
|
||||||
entries.push({ key, ...await encryptValue(vaultKey.key, plainValue) })
|
const plainValue = await decryptValue(oldKeyValue.key, { iv, value })
|
||||||
|
entries.push({ key, ...await encrypt(plainValue) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateVaultKey({
|
await updateVaultKey({
|
||||||
|
@ -93,13 +98,15 @@ export function useVaultConfigurator () {
|
||||||
toaster.danger(error.graphQLErrors[0].message)
|
toaster.danger(error.graphQLErrors[0].message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setKey(vaultKey)
|
setKey(vaultKey)
|
||||||
setKeyHash(vaultKey.hash)
|
setKeyHash(vaultKey.hash)
|
||||||
await set('key', vaultKey)
|
await set('key', vaultKey)
|
||||||
|
onVaultKeySet?.(encrypt).catch(console.error)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toaster.danger(e.message)
|
toaster.danger(e.message)
|
||||||
}
|
}
|
||||||
}, [getVaultEntries, updateVaultKey, set, get, remove])
|
}, [getVaultEntries, updateVaultKey, set, get, remove, onVaultKeySet])
|
||||||
|
|
||||||
return { key, setVaultKey, clearVault, disconnectVault }
|
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 { ITEM_FIELDS, ITEM_FULL_FIELDS } from './items'
|
||||||
import { SUB_FULL_FIELDS } from './subs'
|
import { SUB_FULL_FIELDS } from './subs'
|
||||||
|
|
||||||
const STREAK_FIELDS = gql`
|
export const STREAK_FIELDS = gql`
|
||||||
fragment StreakFields on User {
|
fragment StreakFields on User {
|
||||||
optional {
|
optional {
|
||||||
streak
|
streak
|
||||||
|
|
|
@ -99,6 +99,13 @@ function getClient (uri) {
|
||||||
Fact: {
|
Fact: {
|
||||||
keyFields: ['id', 'type']
|
keyFields: ['id', 'type']
|
||||||
},
|
},
|
||||||
|
Wallet: {
|
||||||
|
fields: {
|
||||||
|
vaultEntries: {
|
||||||
|
replace: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Query: {
|
Query: {
|
||||||
fields: {
|
fields: {
|
||||||
sub: {
|
sub: {
|
||||||
|
|
12
lib/yup.js
12
lib/yup.js
|
@ -140,13 +140,13 @@ addMethod(string, 'hex', function (msg) {
|
||||||
|
|
||||||
addMethod(string, 'nwcUrl', function () {
|
addMethod(string, 'nwcUrl', function () {
|
||||||
return this.test({
|
return this.test({
|
||||||
test: async (nwcUrl, context) => {
|
test: (nwcUrl, context) => {
|
||||||
if (!nwcUrl) return true
|
if (!nwcUrl) return true
|
||||||
|
|
||||||
// run validation in sequence to control order of errors
|
// run validation in sequence to control order of errors
|
||||||
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
|
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
|
||||||
try {
|
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
|
let relayUrl, walletPubkey, secret
|
||||||
try {
|
try {
|
||||||
({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl))
|
({ 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.
|
// invalid URL error. handle as if pubkey validation failed to not confuse user.
|
||||||
throw new Error('pubkey must be 64 hex chars')
|
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)
|
string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validateSync(walletPubkey)
|
||||||
await string().required('relay url required').trim().wss('relay must use wss://').validate(relayUrl)
|
string().required('relay url required').trim().wss('relay must use wss://').validateSync(relayUrl)
|
||||||
await string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validate(secret)
|
string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validateSync(secret)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return context.createError({ message: err.message })
|
return context.createError({ message: err.message })
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@ addMethod(array, 'equalto', function equals (
|
||||||
return this.test({
|
return this.test({
|
||||||
name: 'equalto',
|
name: 'equalto',
|
||||||
message: message || `${this.path} has invalid values`,
|
message: message || `${this.path} has invalid values`,
|
||||||
test: function (items) {
|
test: function (items = []) {
|
||||||
if (items.length < required.length) {
|
if (items.length < required.length) {
|
||||||
return this.createError({ message: `Expected ${this.path} to be at least ${required.length} items, but got ${items.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 RefreshIcon from '@/svgs/refresh-line.svg'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useToast } from '@/components/toast'
|
import { useToast } from '@/components/toast'
|
||||||
|
import { useWallets } from '@/wallets/index'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
|
||||||
export default function DeviceSync ({ ssrData }) {
|
export default function DeviceSync ({ ssrData }) {
|
||||||
const { me } = useMe()
|
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 [passphrase, setPassphrase] = useState()
|
||||||
|
|
||||||
const setSeedPassphrase = useCallback(async (passphrase) => {
|
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.
|
On your other devices, navigate to device sync settings and enter this exact passphrase.
|
||||||
</p>
|
</p>
|
||||||
<p className='line-height-md'>
|
<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>
|
</p>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label='passphrase'
|
label='passphrase'
|
||||||
|
|
|
@ -96,3 +96,68 @@ export function canSend ({ def, config }) {
|
||||||
export function canReceive ({ def, config }) {
|
export function canReceive ({ def, config }) {
|
||||||
return def.fields.some(f => f.serverOnly) && isReceiveConfigured({ 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 { useMe } from '@/components/me'
|
||||||
import useVault from '@/components/vault/use-vault'
|
import useVault from '@/components/vault/use-vault'
|
||||||
import { useCallback } from 'react'
|
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 { useMutation } from '@apollo/client'
|
||||||
import { generateMutation } from './graphql'
|
import { generateMutation } from './graphql'
|
||||||
import { REMOVE_WALLET } from '@/fragments/wallet'
|
import { REMOVE_WALLET } from '@/fragments/wallet'
|
||||||
|
@ -18,21 +18,15 @@ export function useWalletConfigurator (wallet) {
|
||||||
const [removeWallet] = useMutation(REMOVE_WALLET)
|
const [removeWallet] = useMutation(REMOVE_WALLET)
|
||||||
|
|
||||||
const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => {
|
const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => {
|
||||||
const { serverWithShared, settings, clientOnly } = siftConfig(wallet.def.fields, { ...serverConfig, ...clientConfig })
|
const variables = await upsertWalletVariables(
|
||||||
const vaultEntries = []
|
{ def: wallet.def, config: { ...serverConfig, ...clientConfig } },
|
||||||
if (clientOnly && isActive) {
|
isActive && encrypt,
|
||||||
for (const [key, value] of Object.entries(clientOnly)) {
|
{ validateLightning })
|
||||||
if (value) {
|
await upsertWallet({ variables })
|
||||||
vaultEntries.push({ key, ...await encrypt(value) })
|
}, [encrypt, isActive, wallet.def])
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await upsertWallet({ variables: { ...serverWithShared, settings, validateLightning, vaultEntries } })
|
|
||||||
}, [encrypt, isActive, wallet.def.fields])
|
|
||||||
|
|
||||||
const _saveToLocal = useCallback(async (newConfig) => {
|
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()
|
reloadLocalWallets()
|
||||||
}, [me?.id, wallet.def.name, reloadLocalWallets])
|
}, [me?.id, wallet.def.name, reloadLocalWallets])
|
||||||
|
|
||||||
|
@ -89,12 +83,13 @@ export function useWalletConfigurator (wallet) {
|
||||||
}
|
}
|
||||||
if (canReceive({ def: wallet.def, config: serverConfig })) {
|
if (canReceive({ def: wallet.def, config: serverConfig })) {
|
||||||
await _saveToServer(serverConfig, clientConfig, validateLightning)
|
await _saveToServer(serverConfig, clientConfig, validateLightning)
|
||||||
} else {
|
} else if (wallet.config.id) {
|
||||||
// if it previously had a server config, remove it
|
// if it previously had a server config, remove it
|
||||||
await _detachFromServer()
|
await _detachFromServer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isActive, _saveToServer, _saveToLocal, _validate, _detachFromLocal, _detachFromServer])
|
}, [isActive, wallet.def, _saveToServer, _saveToLocal, _validate,
|
||||||
|
_detachFromLocal, _detachFromServer])
|
||||||
|
|
||||||
const detach = useCallback(async () => {
|
const detach = useCallback(async () => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
@ -112,46 +107,3 @@ export function useWalletConfigurator (wallet) {
|
||||||
|
|
||||||
return { save, detach }
|
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)
|
.filter(isServerField)
|
||||||
.map(f => `$${f.name}: String`)
|
.map(f => `$${f.name}: String`)
|
||||||
.join(', ')
|
.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, '
|
let inputArgs = 'id: $id, '
|
||||||
inputArgs += wallet.fields
|
inputArgs += wallet.fields
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
import { SET_WALLET_PRIORITY, WALLETS } from '@/fragments/wallet'
|
import { SET_WALLET_PRIORITY, WALLETS } from '@/fragments/wallet'
|
||||||
import { SSR } from '@/lib/constants'
|
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 { 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 useVault from '@/components/vault/use-vault'
|
||||||
import { useWalletLogger } from '@/components/wallet-logger'
|
import { useWalletLogger } from '@/components/wallet-logger'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import walletDefs from 'wallets/client'
|
import walletDefs from 'wallets/client'
|
||||||
|
import { generateMutation } from './graphql'
|
||||||
|
|
||||||
const WalletsContext = createContext({
|
const WalletsContext = createContext({
|
||||||
wallets: []
|
wallets: []
|
||||||
|
@ -31,11 +32,19 @@ function useLocalWallets () {
|
||||||
setWallets(wallets)
|
setWallets(wallets)
|
||||||
}, [me?.id, setWallets])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
loadWallets()
|
loadWallets()
|
||||||
}, [loadWallets])
|
}, [loadWallets])
|
||||||
|
|
||||||
return { wallets, reloadLocalWallets: loadWallets }
|
return { wallets, reloadLocalWallets: loadWallets, removeLocalWallets: removeWallets }
|
||||||
}
|
}
|
||||||
|
|
||||||
const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
|
const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
|
||||||
|
@ -43,9 +52,10 @@ const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
|
||||||
export function WalletsProvider ({ children }) {
|
export function WalletsProvider ({ children }) {
|
||||||
const { isActive, decrypt } = useVault()
|
const { isActive, decrypt } = useVault()
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
const { wallets: localWallets, reloadLocalWallets } = useLocalWallets()
|
const { wallets: localWallets, reloadLocalWallets, removeLocalWallets } = useLocalWallets()
|
||||||
const [setWalletPriority] = useMutation(SET_WALLET_PRIORITY)
|
const [setWalletPriority] = useMutation(SET_WALLET_PRIORITY)
|
||||||
const [serverWallets, setServerWallets] = useState([])
|
const [serverWallets, setServerWallets] = useState([])
|
||||||
|
const client = useApolloClient()
|
||||||
|
|
||||||
const { data, refetch } = useQuery(WALLETS,
|
const { data, refetch } = useQuery(WALLETS,
|
||||||
SSR ? {} : { nextFetchPolicy: 'cache-and-network' })
|
SSR ? {} : { nextFetchPolicy: 'cache-and-network' })
|
||||||
|
@ -58,7 +68,7 @@ export function WalletsProvider ({ children }) {
|
||||||
}, [me?.privates?.walletsUpdatedAt, me?.privates?.vaultKeyHash, refetch])
|
}, [me?.privates?.walletsUpdatedAt, me?.privates?.vaultKeyHash, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadWallets () {
|
const loadWallets = async () => {
|
||||||
if (!data?.wallets) return
|
if (!data?.wallets) return
|
||||||
// form wallets into a list of { config, def }
|
// form wallets into a list of { config, def }
|
||||||
const wallets = []
|
const wallets = []
|
||||||
|
@ -79,6 +89,7 @@ export function WalletsProvider ({ children }) {
|
||||||
// on the client, it's stored unnested
|
// on the client, it's stored unnested
|
||||||
wallets.push({ config: { ...config, ...w.wallet }, def })
|
wallets.push({ config: { ...config, ...w.wallet }, def })
|
||||||
}
|
}
|
||||||
|
|
||||||
setServerWallets(wallets)
|
setServerWallets(wallets)
|
||||||
}
|
}
|
||||||
loadWallets()
|
loadWallets()
|
||||||
|
@ -108,6 +119,46 @@ export function WalletsProvider ({ children }) {
|
||||||
.map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled }))
|
.map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled }))
|
||||||
}, [serverWallets, localWallets])
|
}, [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) => {
|
const setPriorities = useCallback(async (priorities) => {
|
||||||
for (const { wallet, priority } of priorities) {
|
for (const { wallet, priority } of priorities) {
|
||||||
if (!isConfigured(wallet)) {
|
if (!isConfigured(wallet)) {
|
||||||
|
@ -133,7 +184,15 @@ export function WalletsProvider ({ children }) {
|
||||||
// provides priority sorted wallets to children, a function to reload local wallets,
|
// provides priority sorted wallets to children, a function to reload local wallets,
|
||||||
// and a function to set priorities
|
// and a function to set priorities
|
||||||
return (
|
return (
|
||||||
<WalletsContext.Provider value={{ wallets, reloadLocalWallets, setPriorities }}>
|
<WalletsContext.Provider
|
||||||
|
value={{
|
||||||
|
wallets,
|
||||||
|
reloadLocalWallets,
|
||||||
|
setPriorities,
|
||||||
|
onVaultKeySet: syncLocalWallets,
|
||||||
|
beforeDisconnectVault: unsyncLocalWallets
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</WalletsContext.Provider>
|
</WalletsContext.Provider>
|
||||||
)
|
)
|
||||||
|
@ -172,5 +231,7 @@ export function useWallet (name) {
|
||||||
}
|
}
|
||||||
}, [wallet, logger])
|
}, [wallet, logger])
|
||||||
|
|
||||||
|
if (!wallet) return null
|
||||||
|
|
||||||
return { ...wallet, sendPayment }
|
return { ...wallet, sendPayment }
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,8 +74,11 @@ function composeWalletSchema (walletDef, serverSide) {
|
||||||
if (!optional) {
|
if (!optional) {
|
||||||
acc[name] = acc[name].required('Required')
|
acc[name] = acc[name].required('Required')
|
||||||
} else if (requiredWithout) {
|
} else if (requiredWithout) {
|
||||||
acc[name] = acc[name].when([requiredWithout], ([pairSetting], schema) => {
|
// if we are the server, the pairSetting will be in the vaultEntries array
|
||||||
if (!pairSetting) return schema.required(`required if ${requiredWithout} not set`)
|
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({
|
return Yup.mixed().or([schema.test({
|
||||||
test: value => value !== pairSetting,
|
test: value => value !== pairSetting,
|
||||||
message: `${name} cannot be the same as ${requiredWithout}`
|
message: `${name} cannot be the same as ${requiredWithout}`
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { SSR } from '@/lib/constants'
|
import { SSR } from '@/lib/constants'
|
||||||
export * from 'wallets/webln'
|
export * from 'wallets/webln'
|
||||||
|
|
||||||
export const sendPayment = async (bolt11) => {
|
export const sendPayment = async (bolt11) => {
|
||||||
if (typeof window.webln === 'undefined') {
|
if (typeof window.webln === 'undefined') {
|
||||||
throw new Error('WebLN provider not found')
|
throw new Error('WebLN provider not found')
|
||||||
|
|
Loading…
Reference in New Issue