pages load *kazoo*

This commit is contained in:
k00b 2024-10-23 12:42:34 -05:00
parent da020cf899
commit 48640cbed6
23 changed files with 177 additions and 224 deletions

View File

@ -54,10 +54,9 @@ export default {
}
for (const entry of entries) {
txs.push(models.vaultEntry.upsert({
where: { userId: me.id, key: entry.key },
update: { key: entry.key, value: entry.value },
create: { key: entry.key, value: entry.value, userId: me.id, walletId: entry.walletId }
txs.push(models.vaultEntry.update({
where: { id: entry.id },
data: { key: entry.key, value: entry.value }
}))
}
await models.prisma.$transaction(txs)

View File

@ -19,7 +19,8 @@ import assertApiKeyNotPermitted from './apiKey'
import { bolt11Tags } from '@/lib/bolt11'
import { finalizeHodlInvoice } from 'worker/wallet'
import walletDefs from 'wallets/server'
import { generateResolverName, generateTypeDefName, isConfigured } from '@/lib/wallet'
import { generateResolverName, generateTypeDefName } from '@/wallets/graphql'
import { isConfigured } from '@/wallets/common'
import { lnAddrOptions } from '@/lib/lnurl'
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
import { getNodeSockets, getOurPubkey } from '../lnd'

View File

@ -1,6 +1,6 @@
import { gql } from 'graphql-tag'
import { fieldToGqlArg, fieldToGqlArgOptional, generateResolverName, generateTypeDefName, isServerField } from '@/lib/wallet'
import { fieldToGqlArg, fieldToGqlArgOptional, generateResolverName, generateTypeDefName } from '@/wallets/graphql'
import { isServerField } from '@/wallets/common'
import walletDefs from 'wallets/server'
function injectTypeDefs (typeDefs) {

View File

@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'
import { isNumber } from '@/lib/validate'
import { useIsClient } from './use-client'
import Link from 'next/link'
import { isConfigured } from '@/wallets/common'
function autoWithdrawThreshold ({ me }) {
return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
@ -33,7 +34,7 @@ export function AutowithdrawSettings ({ wallet }) {
return (
<>
<Checkbox
disabled={isClient && !wallet.isConfigured}
disabled={isClient && !isConfigured(wallet)}
label='enabled'
id='enabled'
name='enabled'

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { useMe } from './me'
import { useShowModal } from './modal'
import useVault, { useVaultConfigurator, useVaultMigration } from './vault/use-vault'
import { useVaultConfigurator } from './vault/use-vault-configurator'
import { Button, InputGroup } from 'react-bootstrap'
import { Form, Input, PasswordInput, SubmitButton } from './form'
import bip39Words from '@/lib/bip39-words'
@ -21,9 +21,6 @@ export default function DeviceSync () {
const enabled = !!me?.privates?.vaultKeyHash
const connected = !!value?.key
const migrate = useVaultMigration()
const [debugValue, setDebugValue, clearValue] = useVault(me, 'debug')
const manage = useCallback(async () => {
if (enabled && connected) {
showModal((onClose) => (
@ -55,7 +52,7 @@ export default function DeviceSync () {
<ConnectForm onClose={onClose} onConnect={onConnect} enabled={enabled} />
))
}
}, [migrate, enabled, connected, value])
}, [enabled, connected, value])
const reset = useCallback(async () => {
const schema = yup.object().shape({
@ -106,7 +103,6 @@ export default function DeviceSync () {
if (values.passphrase) {
try {
await setVaultKey(values.passphrase)
await migrate()
apollo.cache.evict({ fieldName: 'BestWallets' })
apollo.cache.gc()
await apollo.refetchQueries({ include: ['BestWallets'] })
@ -115,7 +111,7 @@ export default function DeviceSync () {
throw e
}
}
}, [setVaultKey, migrate])
}, [setVaultKey])
return (
<>
@ -139,33 +135,6 @@ export default function DeviceSync () {
</p>
</Info>
</div>
<Button
onClick={async () => {
try {
const v = window.prompt('Enter debug value', debugValue)
await setDebugValue(v)
} catch (e) {
console.error(e)
}
}}
>
Set value
</Button>
<Button
onClick={() => {
clearValue()
}}
>
Clear value
</Button>
<Button
onClick={() => {
window.alert(debugValue)
}}
>
Show value
</Button>
{enabled && !connected && (
<div className='mt-2 d-flex align-items-center'>
<div>

View File

@ -13,7 +13,7 @@ import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction'
import { meAnonSats } from '@/lib/apollo'
import { BoostItemInput } from './adv-post-form'
import { useWallet } from '../wallets/common'
import { useWallet } from '@/wallets/index'
const defaultTips = [100, 1000, 10_000, 100_000]

View File

@ -22,10 +22,9 @@ import SearchIcon from '../../svgs/search-line.svg'
import classNames from 'classnames'
import SnIcon from '@/svgs/sn.svg'
import { useHasNewNotes } from '../use-has-new-notes'
import { useWallets } from '@/wallets/common'
import { useWallets } from '@/wallets/index'
import SwitchAccountList, { useAccounts } from '@/components/account'
import { useShowModal } from '@/components/modal'
import { unsetLocalKey as resetVaultKey } from '@/components/use-vault'
export function Brand ({ className }) {
return (
@ -266,7 +265,6 @@ function LogoutObstacle ({ onClose }) {
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const wallets = useWallets()
const { multiAuthSignout } = useAccounts()
const { me } = useMe()
return (
<div className='d-flex m-auto flex-column w-fit-content'>
@ -295,7 +293,6 @@ function LogoutObstacle ({ onClose }) {
}
await wallets.resetClient().catch(console.error)
await resetVaultKey(me?.id)
await signOut({ callbackUrl: '/' })
}}

View File

@ -1,35 +1,13 @@
import { useCallback, useMemo } from 'react'
import { useMe } from './me'
import { gql, useApolloClient, useMutation } from '@apollo/client'
import { useWallet } from '@/wallets/common'
import { useWallet } from '@/wallets/index'
import { FAST_POLL_INTERVAL, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants'
import { INVOICE } from '@/fragments/wallet'
import Invoice from '@/components/invoice'
import { useFeeButton } from './fee-button'
import { useShowModal } from './modal'
export class InvoiceCanceledError extends Error {
constructor (hash, actionError) {
super(actionError ?? `invoice canceled: ${hash}`)
this.name = 'InvoiceCanceledError'
this.hash = hash
this.actionError = actionError
}
}
export class NoAttachedWalletError extends Error {
constructor () {
super('no attached wallet found')
this.name = 'NoAttachedWalletError'
}
}
export class InvoiceExpiredError extends Error {
constructor (hash) {
super(`invoice expired: ${hash}`)
this.name = 'InvoiceExpiredError'
}
}
import { InvoiceCanceledError, NoAttachedWalletError, InvoiceExpiredError } from '@/wallets/errors'
export const useInvoice = () => {
const client = useApolloClient()

View File

@ -2,7 +2,7 @@ import { QRCodeSVG } from 'qrcode.react'
import { CopyInput, InputSkeleton } from './form'
import InvoiceStatus from './invoice-status'
import { useEffect } from 'react'
import { useWallet } from '@/wallets/common'
import { useWallet } from '@/wallets/index'
import Bolt11Info from './bolt11-info'
export default function Qr ({ asIs, value, useWallet: automated, statusVariant, description, status }) {

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
export function getDbName (userId) {
return `app:storage${userId ? `:${userId}` : ''}`
export function getDbName (userId, name) {
return `app:storage:${userId ?? ''}${name ? `:${name}` : ''}`
}
function useIndexedDB ({ dbName, storeName, options = { keyPath: 'id', autoIncrement: true }, indices = [], version = 1 }) {

View File

@ -1,11 +1,10 @@
import { UPDATE_VAULT_KEY } from '@/fragments/users'
import { useMutation, useQuery } from '@apollo/client'
import { useMe } from '../me'
import { useToast } from '../toast'
import useIndexedDB, { getDbName } from '../use-indexeddb'
import { useCallback, useEffect, useState } from 'react'
import { E_VAULT_KEY_EXISTS } from '@/lib/error'
import { CLEAR_VAULT, GET_VAULT_ENTRIES } from '@/fragments/vault'
import { CLEAR_VAULT, GET_VAULT_ENTRIES, UPDATE_VAULT_KEY } from '@/fragments/vault'
import { toHex } from '@/lib/hex'
import { decryptData, encryptData } from './use-vault'
@ -22,7 +21,7 @@ const useImperativeQuery = (query) => {
export function useVaultConfigurator () {
const { me } = useMe()
const toaster = useToast()
const { set, get, remove } = useIndexedDB({ dbName: getDbName(me?.id), storeName: 'vault' })
const { set, get, remove } = useIndexedDB({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault' })
const [updateVaultKey] = useMutation(UPDATE_VAULT_KEY)
const getVaultEntries = useImperativeQuery(GET_VAULT_ENTRIES)
const [key, setKey] = useState(null)
@ -44,10 +43,10 @@ export function useVaultConfigurator () {
}
setKey(localVaultKey)
} catch (e) {
toaster.danger('error loading vault configuration ' + e.message)
// toaster?.danger('error loading vault configuration ' + e.message)
}
})()
}, [me?.privates?.vaultKeyHash, keyHash, get, remove])
}, [me?.privates?.vaultKeyHash, keyHash, get, remove, toaster])
// clear vault: remove everything and reset the key
const [clearVault] = useMutation(CLEAR_VAULT, {

View File

@ -1,6 +1,7 @@
import { Button } from 'react-bootstrap'
import CancelButton from './cancel-button'
import { SubmitButton } from './form'
import { isConfigured } from '@/wallets/common'
export default function WalletButtonBar ({
wallet, disable,
@ -10,12 +11,12 @@ export default function WalletButtonBar ({
return (
<div className={`mt-3 ${className}`}>
<div className='d-flex justify-content-between'>
{wallet.hasConfig && wallet.isConfigured &&
{isConfigured(wallet) &&
<Button onClick={onDelete} variant='grey-medium'>{deleteText}</Button>}
{children}
<div className='d-flex align-items-center ms-auto'>
{hasCancel && <CancelButton onClick={onCancel} />}
<SubmitButton variant='primary' disabled={disable}>{wallet.isConfigured ? editText : createText}</SubmitButton>
<SubmitButton variant='primary' disabled={disable}>{isConfigured(wallet) ? editText : createText}</SubmitButton>
</div>
</div>
</div>

View File

@ -3,26 +3,18 @@ import styles from '@/styles/wallet.module.css'
import Plug from '@/svgs/plug.svg'
import Gear from '@/svgs/settings-5-fill.svg'
import Link from 'next/link'
import { Status } from '@/wallets/common'
import { Status, isConfigured } from '@/wallets/common'
import DraggableIcon from '@/svgs/draggable.svg'
export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnter, onDragEnd, onTouchStart, sourceIndex, targetIndex, index }) {
const { card: { title, badges } } = wallet
const { card: { title, badges } } = wallet.def
let indicator = styles.disabled
switch (wallet.status) {
case Status.Enabled:
case true:
indicator = styles.success
break
case Status.Locked:
indicator = styles.warning
break
case Status.Error:
indicator = styles.error
break
case Status.Initialized:
case false:
default:
indicator = styles.disabled
break
}
@ -57,9 +49,9 @@ export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnte
</Badge>)}
</Card.Subtitle>
</Card.Body>
<Link href={`/settings/wallets/${wallet.name}`}>
<Link href={`/settings/wallets/${wallet.def.name}`}>
<Card.Footer className={styles.attach}>
{wallet.isConfigured
{isConfigured(wallet)
? <>configure<Gear width={14} height={14} /></>
: <>attach<Plug width={14} height={14} /></>}
</Card.Footer>

View File

@ -86,10 +86,14 @@ const INDICES = [
{ name: 'wallet_ts', keyPath: ['wallet', 'ts'] }
]
function getWalletLogDbName (userId) {
return getDbName(userId)
}
function useWalletLogDB () {
const { me } = useMe()
const { add, getPage, clear, error, notSupported } = useIndexedDB({
dbName: getDbName(me?.id),
dbName: getWalletLogDbName(me?.id),
storeName: 'wallet_logs',
indices: INDICES
})
@ -127,7 +131,7 @@ export function useWalletLogger (wallet, setLogs) {
)
const deleteLogs = useCallback(async (wallet, options) => {
if ((!wallet || wallet.walletType) && !options?.clientOnly) {
if ((!wallet || wallet.def.walletType) && !options?.clientOnly) {
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
}
if (!wallet || wallet.sendPayment) {
@ -190,7 +194,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
result = await getPage(page, pageSize, indexName, query, 'prev')
// no walletType means we're using the local IDB
if (wallet && !wallet.walletType) {
if (wallet && !wallet.def.walletType) {
return result
}
}

View File

@ -22,7 +22,7 @@ import dynamic from 'next/dynamic'
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
import { WebLnProvider } from '@/wallets/webln/client'
import { AccountProvider } from '@/components/account'
import { WalletProvider } from '@/wallets/common'
import { WalletsProvider } from '@/wallets/index'
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
@ -105,7 +105,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
<PlausibleProvider domain='stacker.news' trackOutboundLinks>
<ApolloProvider client={client}>
<MeProvider me={me}>
<WalletProvider>
<WalletsProvider>
<HasNewNotesProvider>
<LoggerProvider>
<WebLnProvider>
@ -132,7 +132,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
</WebLnProvider>
</LoggerProvider>
</HasNewNotesProvider>
</WalletProvider>
</WalletsProvider>
</MeProvider>
</ApolloProvider>
</PlausibleProvider>

View File

@ -5,14 +5,13 @@ import { WalletSecurityBanner } from '@/components/banners'
import { WalletLogs } from '@/components/wallet-logger'
import { useToast } from '@/components/toast'
import { useRouter } from 'next/router'
import { useWallet } from '@/wallets/common'
import { useWallet } from '@/wallets/index'
import Info from '@/components/info'
import Text from '@/components/text'
import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
import dynamic from 'next/dynamic'
import { useIsClient } from '@/components/use-client'
const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false })
import { isConfigured } from '@/wallets/common'
import { SSR } from '@/lib/constants'
import WalletButtonBar from '@/components/wallet-buttonbar'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -22,7 +21,7 @@ export default function WalletSettings () {
const { wallet: name } = router.query
const wallet = useWallet(name)
const initial = wallet?.fields.reduce((acc, field) => {
const initial = wallet?.def.fields.reduce((acc, field) => {
// We still need to run over all wallet fields via reduce
// even though we use wallet.config as the initial value
// since wallet.config is empty when wallet is not configured.
@ -41,8 +40,8 @@ export default function WalletSettings () {
return (
<CenterLayout>
<h2 className='pb-2'>{wallet?.card?.title}</h2>
<h6 className='text-muted text-center pb-3'><Text>{wallet?.card?.subtitle}</Text></h6>
<h2 className='pb-2'>{wallet?.def.card.title}</h2>
<h6 className='text-muted text-center pb-3'><Text>{wallet?.def.card.subtitle}</Text></h6>
{wallet?.canSend && wallet?.hasConfig > 0 && <WalletSecurityBanner />}
<Form
initial={initial}
@ -50,7 +49,7 @@ export default function WalletSettings () {
{...validateProps}
onSubmit={async ({ amount, ...values }) => {
try {
const newConfig = !wallet?.isConfigured
const newConfig = !isConfigured(wallet)
// enable wallet if wallet was just configured
if (newConfig) {
@ -68,11 +67,11 @@ export default function WalletSettings () {
}}
>
{wallet && <WalletFields wallet={wallet} />}
{wallet?.clientOnly
{wallet?.def.clientOnly
? (
<CheckboxGroup name='enabled'>
<Checkbox
disabled={!wallet?.isConfigured}
disabled={!isConfigured(wallet)}
label='enabled'
name='enabled'
groupClassName='mb-0'
@ -101,16 +100,16 @@ export default function WalletSettings () {
)
}
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
const isClient = useIsClient()
function WalletFields ({ wallet }) {
console.log('wallet', wallet)
return fields
return wallet.def.fields
.map(({ name, label = '', type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
const rawProps = {
...props,
name,
initialValue: config?.[name],
readOnly: isClient && isConfigured && editable === false && !!config?.[name],
initialValue: wallet.config?.[name],
readOnly: !SSR && isConfigured(wallet) && editable === false && !!wallet.config?.[name],
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (

View File

@ -2,12 +2,10 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
import Layout from '@/components/layout'
import styles from '@/styles/wallet.module.css'
import Link from 'next/link'
import { useWallets, walletPrioritySort } from '@/wallets/common'
import { useWallets } from '@/wallets/index'
import { useState } from 'react'
import dynamic from 'next/dynamic'
import { useIsClient } from '@/components/use-client'
const WalletCard = dynamic(() => import('@/components/wallet-card'), { ssr: false })
import WalletCard from '@/components/wallet-card'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -28,7 +26,7 @@ async function reorder (wallets, sourceIndex, targetIndex) {
}
export default function Wallet ({ ssrData }) {
const { wallets } = useWallets()
const wallets = useWallets()
const isClient = useIsClient()
const [sourceIndex, setSourceIndex] = useState(null)
@ -76,49 +74,33 @@ export default function Wallet ({ ssrData }) {
</Link>
</div>
<div className={styles.walletGrid} onDragEnd={onDragEnd}>
{wallets
.sort((w1, w2) => {
// enabled/configured wallets always come before disabled/unconfigured wallets
if ((w1.enabled && !w2.enabled) || (w1.isConfigured && !w2.isConfigured)) {
return -1
} else if ((w2.enabled && !w1.enabled) || (w2.isConfigured && !w1.isConfigured)) {
return 1
}
{wallets.map((w, i) => {
const draggable = isClient && w.config?.enabled
return walletPrioritySort(w1, w2)
})
.map((w, i) => {
const draggable = isClient && w.enabled
return (
<div
key={w?.name}
draggable={draggable}
style={{ cursor: draggable ? 'move' : 'default' }}
onDragStart={draggable ? onDragStart(i) : undefined}
onTouchStart={draggable ? onTouchStart(i) : undefined}
onDragEnter={draggable ? onDragEnter(i) : undefined}
className={
return (
<div
key={w.def.name}
className={
!draggable
? ''
: (`${sourceIndex === i ? styles.drag : ''} ${draggable && targetIndex === i ? styles.drop : ''}`)
}
suppressHydrationWarning
>
<WalletCard
wallet={w}
draggable={draggable}
onDragStart={draggable ? onDragStart(i) : undefined}
onTouchStart={draggable ? onTouchStart(i) : undefined}
onDragEnter={draggable ? onDragEnter(i) : undefined}
sourceIndex={sourceIndex}
targetIndex={targetIndex}
index={i}
/>
</div>
)
}
)}
suppressHydrationWarning
>
<WalletCard
wallet={w}
draggable={draggable}
onDragStart={draggable ? onDragStart(i) : undefined}
onTouchStart={draggable ? onTouchStart(i) : undefined}
onDragEnter={draggable ? onDragEnter(i) : undefined}
sourceIndex={sourceIndex}
targetIndex={targetIndex}
index={i}
/>
</div>
)
}
)}
</div>
</div>

View File

@ -1,10 +1,8 @@
import walletDefs from 'wallets/client'
export const Status = {
Initialized: 'Initialized',
Enabled: 'Enabled',
Locked: 'Locked',
Error: 'Error'
Disabled: 'Disabled'
}
export function getWalletByName (name) {
@ -28,13 +26,20 @@ export function getStorageKey (name, me) {
}
export function walletPrioritySort (w1, w2) {
const delta = w1.priority - w2.priority
// enabled/configured wallets always come before disabled/unconfigured wallets
if ((w1.config?.enabled && !w2.config?.enabled) || (isConfigured(w1) && !isConfigured(w2))) {
return -1
} else if ((w2.config?.enabled && !w1.config?.enabled) || (isConfigured(w2) && !isConfigured(w1))) {
return 1
}
const delta = w1.config?.priority - w2.config?.priority
// delta is NaN if either priority is undefined
if (!Number.isNaN(delta) && delta !== 0) return delta
// if one wallet has a priority but the other one doesn't, the one with the priority comes first
if (w1.priority !== undefined && w2.priority === undefined) return -1
if (w1.priority === undefined && w2.priority !== undefined) return 1
if (w1.config?.priority !== undefined && w2.config?.priority === undefined) return -1
if (w1.config?.priority === undefined && w2.config?.priority !== undefined) return 1
// both wallets have no priority set, falling back to other methods
@ -43,5 +48,50 @@ export function walletPrioritySort (w1, w2) {
if (w1.config?.id && w2.config?.id) return Number(w1.config.id) - Number(w2.config.id)
// else we will use the card title as tie breaker
return w1.card.title < w2.card.title ? -1 : 1
return w1.def.card.title < w2.def.card.title ? -1 : 1
}
export function isServerField (f) {
return f.serverOnly || !f.clientOnly
}
export function isClientField (f) {
return f.clientOnly || !f.serverOnly
}
function checkFields ({ fields, config }) {
// a wallet is configured if all of its required fields are set
let val = fields.every(f => {
return f.optional ? true : !!config?.[f.name]
})
// however, a wallet is not configured if all fields are optional and none are set
// since that usually means that one of them is required
if (val && fields.length > 0) {
val = !(fields.every(f => f.optional) && fields.every(f => !config?.[f.name]))
}
return val
}
export function isConfigured (wallet) {
return isSendConfigured(wallet) || isReceiveConfigured(wallet)
}
function isSendConfigured (wallet) {
const fields = wallet.def.fields.filter(isClientField)
return checkFields({ fields, config: wallet.config })
}
function isReceiveConfigured (wallet) {
const fields = wallet.def.fields.filter(isServerField)
return checkFields({ fields, config: wallet.config })
}
export function canSend (wallet) {
return !!wallet.def.sendPayment && isSendConfigured(wallet)
}
export function canReceive (wallet) {
return !wallet.def.clientOnly && isReceiveConfigured(wallet)
}

View File

@ -1,7 +1,7 @@
import { useMe } from '@/components/me'
import useVault from '@/components/use-vault'
import useVault from '@/components/vault/use-vault'
import { useCallback } from 'react'
import { getStorageKey } from './common'
import { getStorageKey, isClientField, isServerField } from './common'
import { useMutation } from '@apollo/client'
import { generateMutation } from './graphql'
import { REMOVE_WALLET } from '@/fragments/wallet'
@ -11,8 +11,8 @@ import { useWalletLogger } from '@/components/wallet-logger'
export function useWalletConfigurator (wallet) {
const { me } = useMe()
const { encrypt, isActive } = useVault()
const { logger } = useWalletLogger(wallet.def)
const [upsertWallet] = useMutation(generateMutation(wallet.def))
const { logger } = useWalletLogger(wallet?.def)
const [upsertWallet] = useMutation(generateMutation(wallet?.def))
const [removeWallet] = useMutation(REMOVE_WALLET)
const _saveToServer = useCallback(async (serverConfig, clientConfig) => {
@ -117,48 +117,3 @@ function extractClientConfig (fields, config) {
function extractServerConfig (fields, config) {
return extractConfig(fields, config, false, true)
}
export function isServerField (f) {
return f.serverOnly || !f.clientOnly
}
export function isClientField (f) {
return f.clientOnly || !f.serverOnly
}
function checkFields ({ fields, config }) {
// a wallet is configured if all of its required fields are set
let val = fields.every(f => {
return f.optional ? true : !!config?.[f.name]
})
// however, a wallet is not configured if all fields are optional and none are set
// since that usually means that one of them is required
if (val && fields.length > 0) {
val = !(fields.every(f => f.optional) && fields.every(f => !config?.[f.name]))
}
return val
}
export function isConfigured (wallet) {
return isSendConfigured(wallet) || isReceiveConfigured(wallet)
}
function isSendConfigured (wallet) {
const fields = wallet.def.fields.filter(isClientField)
return checkFields({ fields, config: wallet.config })
}
function isReceiveConfigured (wallet) {
const fields = wallet.def.fields.filter(isServerField)
return checkFields({ fields, config: wallet.config })
}
export function canSend (wallet) {
return !!wallet.def.sendPayment && isSendConfigured(wallet)
}
export function canReceive (wallet) {
return !wallet.def.clientOnly && isReceiveConfigured(wallet)
}

22
wallets/errors.js Normal file
View File

@ -0,0 +1,22 @@
export class InvoiceCanceledError extends Error {
constructor (hash, actionError) {
super(actionError ?? `invoice canceled: ${hash}`)
this.name = 'InvoiceCanceledError'
this.hash = hash
this.actionError = actionError
}
}
export class NoAttachedWalletError extends Error {
constructor () {
super('no attached wallet found')
this.name = 'NoAttachedWalletError'
}
}
export class InvoiceExpiredError extends Error {
constructor (hash) {
super(`invoice expired: ${hash}`)
this.name = 'InvoiceExpiredError'
}
}

View File

@ -1,5 +1,5 @@
import gql from 'graphql-tag'
import { isServerField } from './config'
import { isServerField } from './common'
export function fieldToGqlArg (field) {
let arg = `${field.name}: String`

View File

@ -3,12 +3,11 @@ import { WALLETS } from '@/fragments/wallet'
import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
import { useQuery } from '@apollo/client'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { getStorageKey, getWalletByType } from './common'
import useVault from '@/components/use-vault'
import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend } 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 { canSend } from './config'
const WalletsContext = createContext({
wallets: []
@ -47,6 +46,8 @@ function useLocalWallets () {
return wallets
}
const walletDefsOnly = walletDefs.map(w => ({ def: w, config: {} }))
export function WalletsProvider ({ children }) {
const { me } = useMe()
const { decrypt } = useVault()
@ -70,16 +71,19 @@ export function WalletsProvider ({ children }) {
}
return { config, def }
})
}) ?? []
// merge wallets on name
const merged = {}
for (const wallet of [...localWallets, ...wallets]) {
for (const wallet of [...walletDefsOnly, ...localWallets, ...wallets]) {
merged[wallet.def.name] = { ...merged[wallet.def.name], ...wallet }
}
return Object.values(merged)
.sort(walletPrioritySort)
.map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled }))
}, [data?.wallets, localWallets])
// provides priority sorted wallets to children
return (
<WalletsContext.Provider value={wallets}>
{children}
@ -101,10 +105,10 @@ export function useWallet (name) {
return wallets
.filter(w => !w.def.isAvailable || w.def.isAvailable())
.filter(w => w.config.enabled && canSend(w))[0]
.filter(w => w.config?.enabled && canSend(w))[0]
}, [wallets, name])
const { logger } = useWalletLogger(wallet.def)
const { logger } = useWalletLogger(wallet?.def)
const sendPayment = useCallback(async (bolt11) => {
const hash = bolt11Tags(bolt11).payment_hash

View File

@ -1,4 +1,4 @@
import { InvoiceCanceledError, InvoiceExpiredError } from '@/components/payment'
import { InvoiceCanceledError, InvoiceExpiredError } from '@/wallets/errors'
import { bolt11Tags } from '@/lib/bolt11'
import { Mutex } from 'async-mutex'
export * from 'wallets/lnc'