feat: BalancePane: surface pending close balance alongside pending open

This commit is contained in:
Evan Kaloudis
2026-07-12 14:46:09 -04:00
parent 6547addcd6
commit aaebdb6de3
5 changed files with 97 additions and 36 deletions
+26 -6
View File
@@ -347,9 +347,6 @@ export default class LdkNode {
pendingCloseBalance = pendingCloseBalance.plus(sb.amountSatoshis);
}
const totalPendingBalance =
pendingOpenBalance.plus(pendingCloseBalance);
return {
balance: localBalance.toString(),
local_balance: {
@@ -360,10 +357,10 @@ export default class LdkNode {
sat: remoteBalance.toString(),
msat: remoteBalance.times(1000).toString()
},
pending_open_balance: totalPendingBalance.toString(),
pending_open_balance: pendingOpenBalance.toString(),
pending_open_local_balance: {
sat: totalPendingBalance.toString(),
msat: totalPendingBalance.times(1000).toString()
sat: pendingOpenBalance.toString(),
msat: pendingOpenBalance.times(1000).toString()
}
};
};
@@ -686,7 +683,30 @@ export default class LdkNode {
}
}
// total_limbo_balance: sats in close-side limbo (cooperative closes
// awaiting confirmation + force-close timelocks + waiting closes).
// Matches lnrpc.PendingChannelsResponse.total_limbo_balance semantics
// so ChannelsStore can read it uniformly across backends.
let totalLimboBalance = new BigNumber(0);
for (const lb of balances.lightningBalances) {
if (lb.type === 'claimableOnChannelClose') continue;
if (activeChannelIds.has(lb.channelId)) continue;
totalLimboBalance = totalLimboBalance.plus(lb.amountSatoshis);
}
for (const sb of balances.pendingBalancesFromChannelClosures) {
if (sb.channelId && activeChannelIds.has(sb.channelId)) continue;
if (
sb.type === 'awaitingThresholdConfirmations' &&
sb.confirmationHeight != null &&
sb.confirmationHeight <= currentBlockHeight
) {
continue;
}
totalLimboBalance = totalLimboBalance.plus(sb.amountSatoshis);
}
return {
total_limbo_balance: totalLimboBalance.toString(),
pending_open_channels: pendingOpen.map((channel) => ({
channel: this.formatChannel(channel)
})),
+11
View File
@@ -14,6 +14,11 @@ export default class BalanceStore {
@observable public loadingLightningBalance = false;
@observable public error = false;
@observable public pendingOpenBalance: number | string | any;
// Sats locked up in pending close, force close, and waiting close
// channels — i.e. funds in close-side limbo. Mirrors pendingOpenBalance
// semantics but for the closing side. Populated by ChannelsStore from
// PendingChannelsResponse.total_limbo_balance (or the LDK-node analogue).
@observable public pendingCloseBalance: number | string | any;
@observable public lightningBalance: number | string;
@observable public otherAccounts: any = {};
settingsStore: SettingsStore;
@@ -50,10 +55,16 @@ export default class BalanceStore {
private resetLightningBalance = () => {
this.pendingOpenBalance = 0;
this.pendingCloseBalance = 0;
this.lightningBalance = 0;
this.loadingLightningBalance = false;
};
@action
public setPendingCloseBalance = (value: number | string) => {
this.pendingCloseBalance = Number(value || 0);
};
@action
private balanceError = () => {
this.error = true;
+10 -1
View File
@@ -13,6 +13,7 @@ import Peer from '../models/Peer';
import OpenChannelRequest from '../models/OpenChannelRequest';
import CloseChannelRequest from '../models/CloseChannelRequest';
import BalanceStore from './BalanceStore';
import SettingsStore from './SettingsStore';
import BackendUtils from '../utils/BackendUtils';
@@ -115,9 +116,11 @@ export default class ChannelsStore {
@observable public haveAnnouncedChannels = false;
settingsStore: SettingsStore;
balanceStore: BalanceStore;
constructor(settingsStore: SettingsStore) {
constructor(settingsStore: SettingsStore, balanceStore: BalanceStore) {
this.settingsStore = settingsStore;
this.balanceStore = balanceStore;
reaction(
() => this.channelRequest,
@@ -598,6 +601,12 @@ export default class ChannelsStore {
.concat(pendingCloseChannels)
.concat(forceCloseChannels)
.concat(waitCloseChannels);
// Push close-side limbo (cooperative close + force-close
// timelocks + waiting-close confirmations) to BalanceStore
// so BalancePane can render it next to pendingOpenBalance.
this.balanceStore.setPendingCloseBalance(
data.total_limbo_balance || 0
);
})
);
}
+2 -2
View File
@@ -34,7 +34,8 @@ export const connectivityStore = new ConnectivityStore(settingsStore);
export const modalStore = new ModalStore();
export const offersStore = new OffersStore();
export const fiatStore = new FiatStore(settingsStore);
export const channelsStore = new ChannelsStore(settingsStore);
export const balanceStore = new BalanceStore(settingsStore);
export const channelsStore = new ChannelsStore(settingsStore, balanceStore);
export const nodeInfoStore = new NodeInfoStore(channelsStore, settingsStore);
export const alertStore = new AlertStore(settingsStore, nodeInfoStore);
export const lspStore = new LSPStore(
@@ -52,7 +53,6 @@ export const invoicesStore = new InvoicesStore(
channelsStore,
nodeInfoStore
);
export const balanceStore = new BalanceStore(settingsStore);
export const transactionsStore = new TransactionsStore(
settingsStore,
nodeInfoStore,
+48 -27
View File
@@ -182,13 +182,19 @@ export default class BalancePane extends React.PureComponent<
totalBlockchainBalance,
unconfirmedBlockchainBalance,
lightningBalance,
pendingOpenBalance
pendingOpenBalance,
pendingCloseBalance
} = BalanceStore;
const cashuBalance = CashuStore.totalBalanceSats;
const cashuOfflinePendingBalance = CashuStore.offlinePendingBalance;
const { implementation, settings, lndFolderMissing } = SettingsStore;
const pendingUnconfirmedBalance = new BigNumber(pendingOpenBalance)
const lightningPendingTotal = new BigNumber(pendingOpenBalance || 0)
.plus(pendingCloseBalance || 0)
.toNumber();
const hasLightningPending = lightningPendingTotal > 0;
const pendingUnconfirmedBalance = new BigNumber(lightningPendingTotal)
.plus(unconfirmedBlockchainBalance)
.toNumber()
.toFixed(3);
@@ -206,40 +212,55 @@ export default class BalancePane extends React.PureComponent<
jumboText
toggleable
/>
{!(pendingOpenBalance > 0) && (
{!hasLightningPending && (
<View style={styles.conversion}>
<Conversion sats={lightningBalance} sensitive />
</View>
)}
{pendingOpenBalance > 0 ? (
<>
<View style={styles.pendingAmountSpacing}>
<Amount
sats={pendingOpenBalance}
sensitive
jumboText
toggleable
pending
onPendingPress={() =>
this.handlePendingPress('lightning')
}
/>
</View>
<View style={styles.conversion}>
<Conversion
sats={lightningBalance}
satsPending={pendingOpenBalance}
sensitive
/>
</View>
</>
) : null}
{pendingOpenBalance > 0 && (
<View style={styles.pendingAmountSpacing}>
<Amount
sats={pendingOpenBalance}
sensitive
jumboText
toggleable
pending
onPendingPress={() =>
this.handlePendingPress('lightning')
}
/>
</View>
)}
{pendingCloseBalance > 0 && (
<View style={styles.pendingAmountSpacing}>
<Amount
sats={pendingCloseBalance}
sensitive
jumboText
toggleable
pending
onPendingPress={() =>
this.handlePendingPress('lightning')
}
/>
</View>
)}
{hasLightningPending && (
<View style={styles.conversion}>
<Conversion
sats={lightningBalance}
satsPending={lightningPendingTotal}
sensitive
/>
</View>
)}
</View>
);
const BalanceViewCombined = () => {
const hasOnchainPending = Boolean(
Number(unconfirmedBlockchainBalance ?? 0) ||
Number(pendingOpenBalance ?? 0)
Number(pendingOpenBalance ?? 0) ||
Number(pendingCloseBalance ?? 0)
);
const hasAnyPending = hasOnchainPending;
const renderPendingBalance = (sats: string | number) => (