Skip to content

Commit

Permalink
show attached wallets balance
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Nov 20, 2024
1 parent cce7195 commit 9ede708
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 4 deletions.
56 changes: 53 additions & 3 deletions components/nav/common.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 (
<Link href='/' passHref legacyBehavior>
Expand Down Expand Up @@ -144,7 +146,55 @@ export function WalletSummary () {
if (me.privates?.hideWalletBalance) {
return <HiddenWalletSummary abbreviate fixedWidth />
}
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 (
<OverlayTrigger
placement='bottom'
overlay={
<Tooltip>
{
walletSummary.map((w, i) => {
return (
<div key={i}>{w}</div>
)
})
}
</Tooltip>
}
trigger={['hover', 'focus']}
popperConfig={{
modifiers: {
preventOverflow: {
enabled: false
}
}
}}
>
<span>
{abbrNum(highestBalance)}{numNonZeroBalances > 1 && '+'}
</span>
</OverlayTrigger>
)
}

export function NavWalletSummary ({ className }) {
Expand Down
13 changes: 13 additions & 0 deletions pages/settings/wallets/[wallet].js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default function WalletSettings () {
groupClassName='mb-0'
/>
</CheckboxGroup>
<SendSettings walletDef={wallet.def} />
<ReceiveSettings walletDef={wallet.def} />
<WalletButtonBar
wallet={wallet} onDelete={async () => {
Expand Down Expand Up @@ -138,6 +139,18 @@ function ReceiveSettings ({ walletDef }) {
return canReceive({ def: walletDef, config: values }) && <AutowithdrawSettings />
}

function SendSettings ({ walletDef }) {
const { values } = useFormikContext()
return canSend({ def: walletDef, config: values }) &&
<CheckboxGroup name='showBalance'>
<Checkbox
label='show balance'
name='showBalance'
groupClassName='mb-0'
/>
</CheckboxGroup>
}

function WalletFields ({ wallet }) {
return wallet.def.fields
.map(({
Expand Down
8 changes: 8 additions & 0 deletions wallets/blink/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, `
Expand Down
1 change: 1 addition & 0 deletions wallets/blink/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export async function getWallet (authToken, currency) {
wallets {
id
walletCurrency
balance
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions wallets/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
58 changes: 57 additions & 1 deletion wallets/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 (
<WalletsContext.Provider
value={{
wallets,
displayBalances,
reloadLocalWallets,
setPriorities,
onVaultKeySet: syncLocalWallets,
Expand Down Expand Up @@ -248,3 +298,9 @@ export function useWallet (name) {

return { ...wallet, sendPayment }
}

export function isEnabledSendingWallet (w) {
return (!w.def.isAvailable || w.def.isAvailable()) &&
w.config?.enabled &&
canSend(w)
}
6 changes: 6 additions & 0 deletions wallets/lnbits/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export async function sendPayment (bolt11, { url, adminKey }) {
return checkResponse.preimage
}

export async function getBalance ({ url, adminKey }) {
url = url.replace(/\/+$/, '')
const wallet = await getWallet({ url, adminKey })
return BigInt(wallet.balance)
}

async function getWallet ({ url, adminKey, invoiceKey }) {
const path = '/api/v1/wallet'

Expand Down
8 changes: 8 additions & 0 deletions wallets/nwc/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ export async function sendPayment (bolt11, { nwcUrl }, { logger }) {
{ logger })
return result.preimage
}

export async function getBalance ({ nwcUrl }) {
const result = await nwcCall({
nwcUrl,
method: 'get_balance'
}, {})
return BigInt(result.balance)
}
11 changes: 11 additions & 0 deletions wallets/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ function composeWalletSchema (walletDef, serverSide, skipGenerated) {

const vaultEntrySchemas = { required: [], optional: [] }
const cycleBreaker = []

if (serverSide) {
vaultEntrySchemas.optional.push(vaultEntrySchema('showBalance'))
}

const schemaShape = fields.reduce((acc, field) => {
const { name, validate, optional, generated, clientOnly, requiredWithout } = field

Expand Down Expand Up @@ -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)
Expand All @@ -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
}
19 changes: 19 additions & 0 deletions wallets/webln/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 9ede708

Please sign in to comment.