Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sender fallbacks #1642

Merged
merged 36 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
35159bf
Remove unused useWallet from QR code component
ekzyis Nov 27, 2024
2b47bf5
Fix comment
ekzyis Nov 24, 2024
5218a03
sender fallbacks
ekzyis Nov 25, 2024
bc0c6d1
Fix SenderError name
ekzyis Nov 26, 2024
7742257
Fix TypeError
ekzyis Nov 26, 2024
7036804
Fix old invoice passed to QR code
ekzyis Nov 26, 2024
d99caa4
Remove unnecessary sort
ekzyis Nov 26, 2024
ed82d9c
Fix payments if recv-only wallet enabled
ekzyis Nov 26, 2024
413f76c
Refactor wallet error handling with inheritance
ekzyis Nov 26, 2024
517d9a9
Abort payment on unexpected errors
ekzyis Nov 26, 2024
974e897
Add comment when err.newInvoice is not set
ekzyis Nov 26, 2024
00f9e05
Ignore wallet configuration errors in QR code
ekzyis Nov 26, 2024
1f2b717
Fix last wallet not returning new invoice
ekzyis Nov 26, 2024
be4ce5d
Allow retries of pessimistic actions
ekzyis Nov 26, 2024
7f5bb33
Fix payment method returned by retries
ekzyis Nov 26, 2024
7e25e29
Show aggregated wallet errors in QR code
ekzyis Nov 26, 2024
b1cdd95
Return last attempted invoice in canceled state
ekzyis Nov 27, 2024
14a92ee
Fix filter for wallets that can send
ekzyis Nov 27, 2024
0051c82
Fix invoice retry even if no payment was attempted
ekzyis Nov 27, 2024
a4144d4
Fix missing item invoice update for optimistic actions
ekzyis Nov 27, 2024
6851355
Remove unnecessary error handling in LNC
ekzyis Nov 27, 2024
9cfc18d
Return latest state of paid or failed invoice
ekzyis Nov 27, 2024
b301b31
Create wrapped invoices on p2p zap retries
ekzyis Nov 27, 2024
7a8db53
Only retry same receiver if forward did not fail
ekzyis Nov 27, 2024
61395a3
Merge branch 'master' into sender-fallbacks
huumn Nov 27, 2024
b608fb6
refactor out array of hooks
huumn Nov 27, 2024
a0d33a2
Fix wallet save
ekzyis Nov 28, 2024
f9169c6
Fix [undefined] in logs
ekzyis Nov 28, 2024
67f6c17
readability improvements
huumn Nov 28, 2024
6c3301a
Fix missing item invoice update on failure
ekzyis Nov 28, 2024
f89286f
make logger use full wallet
huumn Nov 28, 2024
6b59e1f
usesendwallets
huumn Nov 28, 2024
cb028d2
fix zap fallback retries in notifications
huumn Nov 28, 2024
105f7b0
make fallback retry cache updates a special case
huumn Nov 28, 2024
404cf18
function for merging data after retry
huumn Nov 28, 2024
9caeca0
fix cache update option name on qr
huumn Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions components/item-act.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/index'
import { useWalletsWithPayments } from '@/wallets/index'

const defaultTips = [100, 1000, 10_000, 100_000]

Expand Down Expand Up @@ -89,7 +89,7 @@ function BoostForm ({ step, onSubmit, children, item, oValue, inputRef, act = 'B
export default function ItemAct ({ onClose, item, act = 'TIP', step, children, abortSignal }) {
const inputRef = useRef(null)
const { me } = useMe()
const wallet = useWallet()
const wallets = useWalletsWithPayments()
const [oValue, setOValue] = useState()

useEffect(() => {
Expand Down Expand Up @@ -117,7 +117,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
if (!me) setItemMeAnonSats({ id: item.id, amount })
}

const closeImmediately = !!wallet || me?.privates?.sats > Number(amount)
const closeImmediately = wallets.length > 0 || me?.privates?.sats > Number(amount)
if (closeImmediately) {
onPaid()
}
Expand All @@ -143,7 +143,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
})
if (error) throw error
addCustomTip(Number(amount))
}, [me, actor, !!wallet, act, item.id, onClose, abortSignal, strike])
}, [me, actor, wallets.length, act, item.id, onClose, abortSignal, strike])

return act === 'BOOST'
? <BoostForm step={step} onSubmit={onSubmit} item={item} inputRef={inputRef} act={act}>{children}</BoostForm>
Expand Down Expand Up @@ -260,7 +260,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
}

export function useZap () {
const wallet = useWallet()
const wallets = useWalletsWithPayments()
const act = useAct()
const strike = useLightning()
const toaster = useToast()
Expand All @@ -278,7 +278,7 @@ export function useZap () {
await abortSignal.pause({ me, amount: sats })
strike()
// batch zaps if wallet is enabled or using fee credits so they can be executed serially in a single request
const { error } = await act({ variables, optimisticResponse, context: { batch: !!wallet || me?.privates?.sats > sats } })
const { error } = await act({ variables, optimisticResponse, context: { batch: wallets.length > 0 || me?.privates?.sats > sats } })
if (error) throw error
} catch (error) {
if (error instanceof ActCanceledError) {
Expand All @@ -288,7 +288,7 @@ export function useZap () {
const reason = error?.message || error?.toString?.()
toaster.danger(reason)
}
}, [act, toaster, strike, !!wallet])
}, [act, toaster, strike, wallets.length])
}

export class ActCanceledError extends Error {
Expand Down
58 changes: 10 additions & 48 deletions components/wallet-logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import { Button } from 'react-bootstrap'
import { useToast } from './toast'
import { useShowModal } from './modal'
import { WALLET_LOGS } from '@/fragments/wallet'
import { getWalletByType } from '@/wallets/common'
import { getWalletByType, walletTag } from '@/wallets/common'
import { gql, useLazyQuery, useMutation } from '@apollo/client'
import { useMe } from './me'
import useIndexedDB, { getDbName } from './use-indexeddb'
import { SSR } from '@/lib/constants'
import { decode as bolt11Decode } from 'bolt11'
import { formatMsats } from '@/lib/format'
import { useRouter } from 'next/router'

export function WalletLogs ({ wallet, embedded }) {
Expand Down Expand Up @@ -61,7 +59,7 @@ export function WalletLogs ({ wallet, embedded }) {
}

function DeleteWalletLogsObstacle ({ wallet, setLogs, onClose }) {
const { deleteLogs } = useWalletLogger(wallet, setLogs)
const { deleteLogs } = useWalletLogManager(setLogs)
const toaster = useToast()

const prompt = `Do you really want to delete all ${wallet ? '' : 'wallet'} logs ${wallet ? 'of this wallet' : ''}?`
Expand Down Expand Up @@ -110,11 +108,11 @@ function useWalletLogDB () {
return { add, getPage, clear, error, notSupported }
}

export function useWalletLogger (wallet, setLogs) {
export function useWalletLogManager (setLogs) {
const { add, clear, notSupported } = useWalletLogDB()

const appendLog = useCallback(async (wallet, level, message, context) => {
const log = { wallet: tag(wallet), level, message, ts: +new Date(), context }
const log = { wallet: walletTag(wallet), level, message, ts: +new Date(), context }
try {
if (notSupported) {
console.log('cannot persist wallet log: indexeddb not supported')
Expand Down Expand Up @@ -146,56 +144,20 @@ export function useWalletLogger (wallet, setLogs) {
}
if (!wallet || wallet.sendPayment) {
try {
const walletTag = wallet ? tag(wallet) : null
const tag = wallet ? walletTag(wallet) : null
if (notSupported) {
console.log('cannot clear wallet logs: indexeddb not supported')
} else {
await clear('wallet_ts', walletTag ? window.IDBKeyRange.bound([walletTag, 0], [walletTag, Infinity]) : null)
await clear('wallet_ts', tag ? window.IDBKeyRange.bound([tag, 0], [tag, Infinity]) : null)
}
setLogs?.(logs => logs.filter(l => wallet ? l.wallet !== tag(wallet) : false))
setLogs?.(logs => logs.filter(l => wallet ? l.wallet !== tag : false))
} catch (e) {
console.error('failed to delete logs', e)
}
}
}, [clear, deleteServerWalletLogs, setLogs, notSupported])

const log = useCallback(level => (message, context = {}) => {
if (!wallet) {
// console.error('cannot log: no wallet set')
return
}

if (context?.bolt11) {
// automatically populate context from bolt11 to avoid duplicating this code
const decoded = bolt11Decode(context.bolt11)
context = {
...context,
amount: formatMsats(decoded.millisatoshis),
payment_hash: decoded.tagsObject.payment_hash,
description: decoded.tagsObject.description,
created_at: new Date(decoded.timestamp * 1000).toISOString(),
expires_at: new Date(decoded.timeExpireDate * 1000).toISOString(),
// payments should affect wallet status
status: true
}
}
context.send = true

appendLog(wallet, level, message, context)
console[level !== 'error' ? 'info' : 'error'](`[${tag(wallet)}]`, message)
}, [appendLog, wallet])

const logger = useMemo(() => ({
ok: (message, context) => log('ok')(message, context),
info: (message, context) => log('info')(message, context),
error: (message, context) => log('error')(message, context)
}), [log])

return { logger, deleteLogs }
}

function tag (walletDef) {
return walletDef.shortName || walletDef.name
return { appendLog, deleteLogs }
}

export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
Expand Down Expand Up @@ -227,7 +189,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
console.log('cannot get client wallet logs: indexeddb not supported')
} else {
const indexName = walletDef ? 'wallet_ts' : 'ts'
const query = walletDef ? window.IDBKeyRange.bound([tag(walletDef), -Infinity], [tag(walletDef), Infinity]) : null
const query = walletDef ? window.IDBKeyRange.bound([walletTag(walletDef), -Infinity], [walletTag(walletDef), Infinity]) : null

result = await getPage(page, pageSize, indexName, query, 'prev')
// if given wallet has no walletType it means logs are only stored in local IDB
Expand Down Expand Up @@ -272,7 +234,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {

const newLogs = data.walletLogs.entries.map(({ createdAt, wallet: walletType, ...log }) => ({
ts: +new Date(createdAt),
wallet: tag(getWalletByType(walletType)),
wallet: walletTag(getWalletByType(walletType)),
...log
}))
const combinedLogs = uniqueSort([...result.data, ...newLogs])
Expand Down
4 changes: 4 additions & 0 deletions wallets/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export function getStorageKey (name, userId) {
return storageKey
}

export function walletTag (walletDef) {
return walletDef.shortName || walletDef.name
}

export function walletPrioritySort (w1, w2) {
// enabled/configured wallets always come before disabled/unconfigured wallets
if ((w1.config?.enabled && !w2.config?.enabled) || (isConfigured(w1) && !isConfigured(w2))) {
Expand Down
2 changes: 1 addition & 1 deletion wallets/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { canReceive, canSend, getStorageKey, saveWalletLocally, siftConfig, upse
import { useMutation } from '@apollo/client'
import { generateMutation } from './graphql'
import { REMOVE_WALLET } from '@/fragments/wallet'
import { useWalletLogger } from '@/components/wallet-logger'
import { useWalletLogger } from '@/wallets/logger'
import { useWallets } from '.'
import validateWallet from './validate'

Expand Down
40 changes: 8 additions & 32 deletions wallets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ import { useApolloClient, useMutation, useQuery } from '@apollo/client'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { getStorageKey, getWalletByType, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common'
import useVault from '@/components/vault/use-vault'
import { useWalletLogger } from '@/components/wallet-logger'
import { decode as bolt11Decode } from 'bolt11'
import walletDefs from '@/wallets/client'
import { generateMutation } from './graphql'
import { formatSats } from '@/lib/format'

const WalletsContext = createContext({
wallets: []
Expand Down Expand Up @@ -218,34 +215,13 @@ export function useWallets () {

export function useWallet (name) {
const { wallets } = useWallets()
return wallets.find(w => w.def.name === name)
}

const wallet = useMemo(() => {
if (name) {
return wallets.find(w => w.def.name === name)
}

// return the first enabled wallet that is available and can send
return wallets
.filter(w => !w.def.isAvailable || w.def.isAvailable())
.filter(w => w.config?.enabled && canSend(w))[0]
}, [wallets, name])

const { logger } = useWalletLogger(wallet?.def)

const sendPayment = useCallback(async (bolt11) => {
const decoded = bolt11Decode(bolt11)
logger.info(`↗ sending payment: ${formatSats(decoded.satoshis)}`, { bolt11 })
try {
const preimage = await wallet.def.sendPayment(bolt11, wallet.config, { logger })
logger.ok(`↗ payment sent: ${formatSats(decoded.satoshis)}`, { bolt11, preimage })
} catch (err) {
const message = err.message || err.toString?.()
logger.error(`payment failed: ${message}`, { bolt11 })
throw err
}
}, [wallet, logger])

if (!wallet) return null

return { ...wallet, sendPayment }
export function useWalletsWithPayments () {
huumn marked this conversation as resolved.
Show resolved Hide resolved
const { wallets } = useWallets()
// return the first enabled wallet that is available and can send
return wallets
.filter(w => !w.def.isAvailable || w.def.isAvailable())
.filter(w => w.config?.enabled && canSend(w))
}
45 changes: 45 additions & 0 deletions wallets/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useCallback } from 'react'
import { decode as bolt11Decode } from 'bolt11'
import { formatMsats } from '@/lib/format'
import { walletTag } from '@/wallets/common'
import { useWalletLogManager } from '@/components/wallet-logger'

export function useWalletLoggerFactory () {
const { appendLog } = useWalletLogManager()

const log = useCallback((wallet, level) => (message, context = {}) => {
if (!wallet) {
return
}

if (context?.bolt11) {
// automatically populate context from bolt11 to avoid duplicating this code
const decoded = bolt11Decode(context.bolt11)
context = {
...context,
amount: formatMsats(decoded.millisatoshis),
payment_hash: decoded.tagsObject.payment_hash,
description: decoded.tagsObject.description,
created_at: new Date(decoded.timestamp * 1000).toISOString(),
expires_at: new Date(decoded.timeExpireDate * 1000).toISOString(),
// payments should affect wallet status
status: true
}
}
context.send = true

appendLog(wallet, level, message, context)
console[level !== 'error' ? 'info' : 'error'](`[${walletTag(wallet)}]`, message)
}, [appendLog])

return useCallback(wallet => ({
ok: (message, context) => log(wallet, 'ok')(message, context),
info: (message, context) => log(wallet, 'info')(message, context),
error: (message, context) => log(wallet, 'error')(message, context)
}), [log])
}

export function useWalletLogger (wallet) {
const factory = useWalletLoggerFactory()
return factory(wallet)
}
Loading