From 9ede708883ba6233aaa90f132544221a26a58430 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 20 Nov 2024 15:07:00 +0100 Subject: [PATCH] show attached wallets balance --- components/nav/common.js | 56 +++++++++++++++++++++++++++-- pages/settings/wallets/[wallet].js | 13 +++++++ wallets/blink/client.js | 8 +++++ wallets/blink/common.js | 1 + wallets/common.js | 5 +++ wallets/index.js | 58 +++++++++++++++++++++++++++++- wallets/lnbits/client.js | 6 ++++ wallets/nwc/client.js | 8 +++++ wallets/validate.js | 11 ++++++ wallets/webln/client.js | 19 ++++++++++ 10 files changed, 181 insertions(+), 4 deletions(-) diff --git a/components/nav/common.js b/components/nav/common.js index 47057b7e0..b446f5f65 100644 --- a/components/nav/common.js +++ b/components/nav/common.js @@ -1,12 +1,14 @@ import Link from 'next/link' import { Button, Dropdown, Nav, Navbar } from 'react-bootstrap' +import OverlayTrigger from 'react-bootstrap/OverlayTrigger' +import Tooltip from 'react-bootstrap/Tooltip' import styles from '../header.module.css' import { useRouter } from 'next/router' import BackArrow from '../../svgs/arrow-left-line.svg' import { useCallback, useEffect, useState } from 'react' import Price from '../price' import SubSelect from '../sub-select' -import { USER_ID, BALANCE_LIMIT_MSATS } from '../../lib/constants' +import { USER_ID, BALANCE_LIMIT_MSATS } from '@/lib/constants' import Head from 'next/head' import NoteIcon from '../../svgs/notification-4-fill.svg' import { useMe } from '../me' @@ -25,7 +27,7 @@ import { useHasNewNotes } from '../use-has-new-notes' import { useWallets } from '@/wallets/index' import SwitchAccountList, { useAccounts } from '@/components/account' import { useShowModal } from '@/components/modal' - +import { toPositiveNumber } from '@/lib/validate' export function Brand ({ className }) { return ( @@ -144,7 +146,55 @@ export function WalletSummary () { if (me.privates?.hideWalletBalance) { return } - return `${abbrNum(me.privates?.sats)}` + + const { displayBalances } = useWallets() + + const custodialSats = toPositiveNumber(me.privates?.sats) + + let numNonZeroBalances = custodialSats > 0 ? 1 : 0 + let highestBalance = custodialSats + const walletSummary = [`custodial: ${abbrNum(custodialSats)}`] + for (const [walletName, { msats, error }] of Object.entries(displayBalances).sort((a, b) => Number(b[1].msats - a[1].msats))) { + if (error) { + // if there is an error, we don't know how much is in the wallet, so we assume it has a non-zero balance + numNonZeroBalances++ + walletSummary.push(`${walletName}: ?`) + } else { + if (msats > 0)numNonZeroBalances++ + const balance = msatsToSats(msats) + walletSummary.push(`${walletName}: ${abbrNum(balance)}`) + if (balance > highestBalance) highestBalance = balance + } + } + + return ( + + { + walletSummary.map((w, i) => { + return ( +
{w}
+ ) + }) + } + + } + trigger={['hover', 'focus']} + popperConfig={{ + modifiers: { + preventOverflow: { + enabled: false + } + } + }} + > + + {abbrNum(highestBalance)}{numNonZeroBalances > 1 && '+'} + +
+ ) } export function NavWalletSummary ({ className }) { diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js index 891307ce2..c79c0026a 100644 --- a/pages/settings/wallets/[wallet].js +++ b/pages/settings/wallets/[wallet].js @@ -104,6 +104,7 @@ export default function WalletSettings () { groupClassName='mb-0' /> + { @@ -138,6 +139,18 @@ function ReceiveSettings ({ walletDef }) { return canReceive({ def: walletDef, config: values }) && } +function SendSettings ({ walletDef }) { + const { values } = useFormikContext() + return canSend({ def: walletDef, config: values }) && + + + +} + function WalletFields ({ wallet }) { return wallet.def.fields .map(({ diff --git a/wallets/blink/client.js b/wallets/blink/client.js index 8779d64ae..644ddf503 100644 --- a/wallets/blink/client.js +++ b/wallets/blink/client.js @@ -22,6 +22,14 @@ export async function sendPayment (bolt11, { apiKey, currency }) { return await payInvoice(apiKey, wallet, bolt11) } +export async function getBalance ({ apiKey, currency }) { + if (currency !== 'BTC') { + throw new Error('unsupported currency') + } + const wallet = await getWallet(apiKey, currency) + return BigInt(wallet.balance * 1000) +} + async function payInvoice (authToken, wallet, invoice) { const walletId = wallet.id const out = await request(authToken, ` diff --git a/wallets/blink/common.js b/wallets/blink/common.js index acb11aec7..f67573911 100644 --- a/wallets/blink/common.js +++ b/wallets/blink/common.js @@ -13,6 +13,7 @@ export async function getWallet (authToken, currency) { wallets { id walletCurrency + balance } } } diff --git a/wallets/common.js b/wallets/common.js index c88b46556..be88f5dc2 100644 --- a/wallets/common.js +++ b/wallets/common.js @@ -113,6 +113,11 @@ export function siftConfig (fields, config) { continue } + if (['showBalance'].includes(key)) { + sifted.clientOnly[key] = Boolean(value) + continue + } + if (['autoWithdrawMaxFeePercent', 'autoWithdrawThreshold', 'autoWithdrawMaxFeeTotal'].includes(key)) { sifted.serverOnly[key] = Number(value) sifted.settings = { ...sifted.settings, [key]: Number(value) } diff --git a/wallets/index.js b/wallets/index.js index 719e230f6..eba3b96d2 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -1,6 +1,6 @@ import { useMe } from '@/components/me' import { SET_WALLET_PRIORITY, WALLETS } from '@/fragments/wallet' -import { SSR } from '@/lib/constants' +import { SSR, LONG_POLL_INTERVAL as WALLET_REFRESH_TIME } from '@/lib/constants' import { useApolloClient, useMutation, useQuery } from '@apollo/client' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, isConfigured, upsertWalletVariables, siftConfig, saveWalletLocally } from './common' @@ -193,12 +193,62 @@ export function WalletsProvider ({ children }) { } }, [setWalletPriority, me?.id, reloadLocalWallets]) + const [displayBalances, setDisplayBalances] = useState({}) + + const refreshBalance = useCallback(() => { + const finalStates = {} + Promise.allSettled(wallets.filter(isEnabledSendingWallet).map(async wallet => { + if (!wallet.config?.showBalance) return + const newState = finalStates[wallet.def.name] = { msats: 0, error: null } + try { + if (!wallet.def.getBalance) throw new Error(`${wallet.def.name} does not support getBalance`) + const balance = await wallet.def.getBalance(wallet.config) + newState.msats = balance + } catch (error) { + newState.error = error + newState.msats = 0n + } + + // early state merge + setDisplayBalances((old) => { + const oldState = old[wallet.def.name] + if (!oldState || oldState.msats !== newState.msats || (!!oldState.error) !== (!!newState.error)) { + // ensure the state updata happens only if something changed + return { + ...old, + [wallet.def.name]: newState + } + } + return old + }) + })).then(() => { + // finalize the state update after all promises have settled + setDisplayBalances(finalStates) + }) + }, [wallets]) + + useEffect(() => { + let timeoutId = null + let stop = false + const refreshPeriodically = async () => { + refreshBalance() + if (stop) return + timeoutId = setTimeout(refreshPeriodically, WALLET_REFRESH_TIME) + } + refreshPeriodically() + return () => { + stop = true + if (timeoutId) clearTimeout(timeoutId) + } + }, [wallets]) + // provides priority sorted wallets to children, a function to reload local wallets, // and a function to set priorities return ( { const { name, validate, optional, generated, clientOnly, requiredWithout } = field @@ -101,6 +106,10 @@ function composeWalletSchema (walletDef, serverSide, skipGenerated) { return acc }, {}) + if (!serverSide) { + schemaShape.showBalance = Yup.boolean() + } + // Finalize the vaultEntries schema if it exists if (vaultEntrySchemas.required.length > 0 || vaultEntrySchemas.optional.length > 0) { schemaShape.vaultEntries = Yup.array().equalto(vaultEntrySchemas) @@ -113,5 +122,7 @@ function composeWalletSchema (walletDef, serverSide, skipGenerated) { priority: Yup.number().min(0, 'must be at least 0').max(100, 'must be at most 100') })) + console.log(JSON.stringify(schemaShape.vaultEntries)) + return composedSchema } diff --git a/wallets/webln/client.js b/wallets/webln/client.js index 630b2bf5a..e682e3772 100644 --- a/wallets/webln/client.js +++ b/wallets/webln/client.js @@ -22,6 +22,25 @@ export const sendPayment = async (bolt11) => { return response.preimage } +export async function getBalance () { + if (typeof window.webln === 'undefined') { + throw new Error('WebLN provider not found') + } + + // this will prompt the user to unlock the wallet if it's locked + await window.webln.enable() + + if (typeof window.webln.getBalance === 'undefined') { + throw new Error('getBalance not supported') + } + + const balance = await window.webln.getBalance() + if (balance.currency !== 'sats') { + throw new Error('getBalance returned unsupported currency') + } + return BigInt(balance.balance * 1000) +} + export function isAvailable () { return !SSR && window?.weblnEnabled }