Fix like/tip modal spendable balance against the live wallet

The Like/Tip modal crashed with "utxos.reduce is not a function" because
MemoLike.getSpendableSats assumed wallet.utxos was an array, while
minimal-slp-wallet exposes it as a UtxoStore object whose spendable BCH
outputs live under utxoStore.bchUtxos. Accept both shapes, and refresh the
wallet UTXOs before the modal's gating balance check.

By specifier.
This commit is contained in:
Chris Troutner
2026-08-26 07:02:53 -07:00
parent 874a10992f
commit e7e758d370
3 changed files with 55 additions and 6 deletions
+25 -5
View File
@@ -40,12 +40,32 @@ function LikeTipModal ({ show, post, wallet, profiles = {}, onHide, onSuccess })
setError('') setError('')
setSubmitting(false) setSubmitting(false)
const memoLike = new MemoLike({ wallet }) let cancelled = false
const page = new LikeTipPage({ memoLike })
const result = page.open(post.txid, post.addr) const checkBalance = async () => {
if (!result.ok) { try {
setError(formatError(result)) // Refresh the wallet's spendable UTXOs so the gating balance check is
// accurate against the live minimal-slp-wallet (which exposes
// wallet.utxos as a UtxoStore object, not an array).
if (typeof wallet.getUtxos === 'function') {
await wallet.getUtxos()
}
if (cancelled) return
const memoLike = new MemoLike({ wallet })
const page = new LikeTipPage({ memoLike })
const result = page.open(post.txid, post.addr)
if (!result.ok) {
setError(formatError(result))
}
} catch (err) {
if (!cancelled) setError(err.message)
}
} }
checkBalance()
return () => { cancelled = true }
}, [show, post, wallet]) }, [show, post, wallet])
async function handleSubmit () { async function handleSubmit () {
+6 -1
View File
@@ -98,7 +98,12 @@ class MemoLike extends MemoAction {
// used by different wallet adapters. // used by different wallet adapters.
getSpendableSats () { getSpendableSats () {
if (!this.wallet) return 0 if (!this.wallet) return 0
const utxos = this.wallet.utxos || [] // minimal-slp-wallet exposes wallet.utxos as a UtxoStore object whose
// spendable BCH outputs live under utxoStore.bchUtxos, while the test and
// acceptance wallets use a plain array. Accept both shapes.
const utxos = Array.isArray(this.wallet.utxos)
? this.wallet.utxos
: (this.wallet.utxos?.utxoStore?.bchUtxos || [])
return utxos.reduce((sum, u) => { return utxos.reduce((sum, u) => {
const value = u.value ?? u.satoshis ?? u.amount ?? 0 const value = u.value ?? u.satoshis ?? u.amount ?? 0
return sum + value return sum + value
+24
View File
@@ -260,3 +260,27 @@ test('getSpendableSats returns 0 without a wallet or spendable values', () => {
assert.equal(new MemoLike({ wallet: { utxos: undefined } }).getSpendableSats(), 0) assert.equal(new MemoLike({ wallet: { utxos: undefined } }).getSpendableSats(), 0)
assert.equal(new MemoLike({ wallet: { utxos: [{ txid: 'u1' }] } }).getSpendableSats(), 0) assert.equal(new MemoLike({ wallet: { utxos: [{ txid: 'u1' }] } }).getSpendableSats(), 0)
}) })
test('getSpendableSats reads spendable BCH outputs from the wallet UtxoStore object', () => {
// minimal-slp-wallet exposes wallet.utxos as a UtxoStore object whose
// spendable BCH outputs live under utxoStore.bchUtxos.
const utxoStore = {
utxoStore: {
bchUtxos: [
{ txid: 'u1', satoshis: 1500 },
{ txid: 'u2', value: 2000 },
{ txid: 'u3', satoshis: 2500 }
],
slpUtxos: { type1: { tokens: [] }, nft: [] }
}
}
const wallet = fakeWallet({ utxos: utxoStore })
assert.equal(new MemoLike({ wallet }).getSpendableSats(), 6000)
})
test('getSpendableSats returns 0 for an empty UtxoStore object', () => {
const wallet = fakeWallet({ utxos: { utxoStore: { bchUtxos: [] } } })
assert.equal(new MemoLike({ wallet }).getSpendableSats(), 0)
})