Skip to content

Commit

Permalink
frontend: update 'My portfolio' with Lightning account
Browse files Browse the repository at this point in the history
This commit adds functionality to display Lightning account
in 'My portfolio' in cases where it is active but not associated
with a connected or remembered wallet, or when all mainnet
accounts in the connected wallet are disabled.

The total coins table is updated to include the Lightning account,
and the Lightning configuration now also contains the keystore
name.
  • Loading branch information
strmci committed May 8, 2024
1 parent 9571381 commit 3db7734
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 45 deletions.
1 change: 1 addition & 0 deletions backend/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func TestSetLightningConfig(t *testing.T) {
Code: "v0-test-ln-0",
Number: 0,
RootFingerprint: []byte("fingerprint"),
KeyStoreName: "test",
})
require.NoError(t, cfg.SetLightningConfig(lightningCfg))

Expand Down
2 changes: 2 additions & 0 deletions backend/config/lightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type LightningAccountConfig struct {
Mnemonic string `json:"mnemonic"`
// RootFingerprint is fingerprint of the keystore that generated the entropy.
RootFingerprint jsonp.HexBytes `json:"rootFingerprint"`
// KeyStoreName is name of the keystore.
KeyStoreName string `json:"keystoreName"`
// Code is the code of the lightning account.
Code types.Code `json:"code"`
// Number is the lightning account incremental number.
Expand Down
25 changes: 25 additions & 0 deletions backend/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,31 @@ func (handlers *Handlers) getCoinsTotalBalance(_ *http.Request) (interface{}, er
util.FormatBtcAsSat(handlers.backend.Config().AppConfig().Backend.BtcUnit))
}

// Add lightning balance to bitcoin totals.
if handlers.backend.Config().LightningConfig().LightningEnabled() {
lightningBalance, err := handlers.backend.Lightning().Balance()
if err != nil {
return nil, err
}

btcCoin, err := handlers.backend.Coin(coinpkg.CodeBTC)
if err != nil {
return nil, err
}

if _, ok := totalPerCoin[coin.CodeBTC]; !ok {
totalPerCoin[coin.CodeBTC] = lightningBalance.Available().BigInt()
} else {
totalPerCoin[coin.CodeBTC] = new(big.Int).Add(totalPerCoin[coin.CodeBTC], lightningBalance.Available().BigInt())
}
conversionsPerCoin[coin.CodeBTC] = coin.Conversions(
coin.NewAmount(totalPerCoin[coin.CodeBTC]),
btcCoin,
false,
handlers.backend.RatesUpdater(),
util.FormatBtcAsSat(handlers.backend.Config().AppConfig().Backend.BtcUnit))
}

for k, v := range totalPerCoin {
currentCoin, err := handlers.backend.Coin(k)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions backend/lightning/lightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ func (lightning *Lightning) Activate() error {
return err
}

keystoreName, err := keystore.Name()
if err != nil {
return err
}

entropyMnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
lightning.log.WithError(err).Warn("Error generating mnemonic")
Expand All @@ -111,6 +116,7 @@ func (lightning *Lightning) Activate() error {
lightningAccount := config.LightningAccountConfig{
Mnemonic: entropyMnemonic,
RootFingerprint: fingerprint,
KeyStoreName: keystoreName,
Code: types.Code(strings.Join([]string{"v0-", hex.EncodeToString(fingerprint), "-ln-0"}, "")),
Number: 0,
}
Expand Down
1 change: 1 addition & 0 deletions frontends/web/src/api/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ILightningResponse<T> {
export type TLightningAccountConfig = {
mnemonic: string;
rootFingerprint: string;
keystoreName: string;
code: AccountCode;
num: number;
};
Expand Down
27 changes: 25 additions & 2 deletions frontends/web/src/routes/account/summary/accountssummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { Status } from '../../../components/status/status';
import { GuideWrapper, GuidedContent, Header, Main } from '../../../components/layout';
import { View } from '../../../components/view/view';
import { Chart } from './chart';
import { SummaryBalance } from './summarybalance';
import { SummaryBalance, LightningBalance } from './summarybalance';
import { CoinBalance } from './coinbalance';
import { AddBuyReceiveOnEmptyBalances } from '../info/buyReceiveCTA';
import { Entry } from '../../../components/guide/entry';
Expand All @@ -36,6 +36,7 @@ import { HideAmountsButton } from '../../../components/hideamountsbutton/hideamo
import { AppContext } from '../../../contexts/AppContext';
import { getAccountsByKeystore, isAmbiguiousName } from '../utils';
import { RatesContext } from '../../../contexts/RatesContext';
import { useLightning } from '../../../hooks/lightning';

type TProps = {
accounts: accountApi.IAccount[];
Expand All @@ -60,9 +61,28 @@ export function AccountsSummary({ accounts, devices }: TProps) {
const [accountsTotalBalance, setAccountsTotalBalance] = useState<accountApi.TAccountsTotalBalance>();
const [coinsTotalBalance, setCoinsTotalBalance] = useState<accountApi.TCoinsTotalBalance>();
const [balances, setBalances] = useState<Balances>();
const { lightningConfig } = useLightning();

const hasCard = useSDCard(devices);

// this function returns true when the lightning account exists but outside of connected or remembered accounts
const isUnlinkedLightningAccount = () => {
if (lightningConfig.accounts.length !== 0) {
if (accountsByKeystore.length === 0) {
return true;
} else {
const foundLightning = accountsByKeystore.some((accountsKeystore) => {
return accountsKeystore.keystore.rootFingerprint === lightningConfig.accounts[0].rootFingerprint;
});
return !foundLightning;
}
} else {
return false;
}
};

const showTotalCoins = accountsByKeystore.length > 1 || (isUnlinkedLightningAccount() && accountsByKeystore.length === 1);

const getAccountSummary = useCallback(async () => {
// replace previous timer if present
if (summaryReqTimerID.current) {
Expand Down Expand Up @@ -213,13 +233,16 @@ export function AccountsSummary({ accounts, devices }: TProps) {
<AddBuyReceiveOnEmptyBalances accounts={accounts} balances={balances} />
) : undefined
} />
{accountsByKeystore.length > 1 && (
{showTotalCoins && (
<CoinBalance
accounts={accounts}
summaryData={summaryData}
coinsBalances={coinsTotalBalance}
/>
)}
{isUnlinkedLightningAccount() && (
<LightningBalance/>
)}
{accountsByKeystore &&
(accountsByKeystore.map(({ keystore, accounts }) =>
<SummaryBalance
Expand Down
58 changes: 30 additions & 28 deletions frontends/web/src/routes/account/summary/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,35 +540,37 @@ class Chart extends Component<Props, State> {
</div>
{!isMobile && <Filters {...chartFiltersProps} />}
</header>
<div className={styles.chartCanvas} style={{ minHeight: chartHeight }}>
{chartDataMissing ? (
<div className={styles.chartUpdatingMessage} style={{ height: chartHeight }}>
{t('chart.dataMissing')}
</div>
) : hasData ? !chartIsUpToDate && (
<div className={styles.chartUpdatingMessage}>
{t('chart.dataOldTimestamp', { time: new Date(lastTimestamp).toLocaleString(this.props.i18n.language), })}
</div>
) : noDataPlaceholder}
<div ref={this.ref} className={styles.invisible}></div>
<span
ref={this.refToolTip}
className={styles.tooltip}
style={{ left: toolTipLeft, top: toolTipTop }}
hidden={!toolTipVisible || isMobile}>
{toolTipValue !== undefined ? (
<span>
<h2 className={styles.toolTipValue}>
<Amount amount={toolTipValue} unit={chartFiat}/>
<span className={styles.toolTipUnit}>{chartFiat}</span>
</h2>
<span className={styles.toolTipTime}>
{this.renderDate(toolTipTime * 1000)}
{hasData ? (
<div className={styles.chartCanvas} style={{ minHeight: chartHeight }}>
{chartDataMissing ? (
<div className={styles.chartUpdatingMessage} style={{ height: chartHeight }}>
{t('chart.dataMissing')}
</div>
) : hasData ? !chartIsUpToDate && (
<div className={styles.chartUpdatingMessage}>
{t('chart.dataOldTimestamp', { time: new Date(lastTimestamp).toLocaleString(this.props.i18n.language), })}
</div>
) : noDataPlaceholder}
<div ref={this.ref} className={styles.invisible}></div>
<span
ref={this.refToolTip}
className={styles.tooltip}
style={{ left: toolTipLeft, top: toolTipTop }}
hidden={!toolTipVisible || isMobile}>
{toolTipValue !== undefined ? (
<span>
<h2 className={styles.toolTipValue}>
<Amount amount={toolTipValue} unit={chartFiat}/>
<span className={styles.toolTipUnit}>{chartFiat}</span>
</h2>
<span className={styles.toolTipTime}>
{this.renderDate(toolTipTime * 1000)}
</span>
</span>
</span>
) : null}
</span>
</div>
) : null}
</span>
</div>
) : null}
{isMobile && <Filters {...chartFiltersProps} />}
</section>
);
Expand Down
49 changes: 34 additions & 15 deletions frontends/web/src/routes/account/summary/coinbalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { SubTotalCoinRow } from './subtotalrow';
import { Amount } from '../../../components/amount/amount';
import { Skeleton } from '../../../components/skeleton/skeleton';
import style from './accountssummary.module.css';
import { useLightning } from '../../../hooks/lightning';
import { CoinCode } from '../../../api/account';

type TProps = {
accounts: accountApi.IAccount[],
Expand All @@ -31,6 +33,10 @@ type TAccountCoinMap = {
[code in accountApi.CoinCode]: accountApi.IAccount[];
};

type TCoinMap = {
[code in accountApi.CoinCode]: string;
};

export function CoinBalance ({
accounts,
summaryData,
Expand All @@ -50,6 +56,22 @@ export function CoinBalance ({
const accountsPerCoin = getAccountsPerCoin();
const coins = Object.keys(accountsPerCoin) as accountApi.CoinCode[];

let coinsWithLightning: TCoinMap = {} as TCoinMap;
coins.forEach(coinCode => {
if (accountsPerCoin[coinCode]?.length >= 1) {
const account = accountsPerCoin[coinCode][0];
coinsWithLightning[account.coinCode] = account.coinName;
}
});

const { lightningConfig } = useLightning();
if (lightningConfig.accounts.length > 0) {
const coinCodeToAdd = 'btc';
const coinNameToAdd = 'Bitcoin';
if (!coinsWithLightning[coinCodeToAdd]) {
coinsWithLightning[coinCodeToAdd] = coinNameToAdd;
}
}
return (
<div>
<div className={style.accountName}>
Expand All @@ -70,21 +92,18 @@ export function CoinBalance ({
</tr>
</thead>
<tbody>
{ accounts.length > 0 ? (
coins.map(coinCode => {
if (accountsPerCoin[coinCode]?.length >= 1) {
const account = accountsPerCoin[coinCode][0];
return (
<SubTotalCoinRow
key={account.coinCode}
coinCode={account.coinCode}
coinName={account.coinName}
balance={coinsBalances && coinsBalances[coinCode]}
/>
);
}
return null;
})) : null}
{ (Object.keys(coinsWithLightning).length > 0) ? (
Object.entries(coinsWithLightning).map(([coinCode, coinName]) => {
return (
<SubTotalCoinRow
key={coinCode}
coinCode={coinCode as CoinCode}
coinName={coinName}
balance={coinsBalances && coinsBalances[coinCode]}
/>
);
}
)) : null}
</tbody>
<tfoot>
<tr>
Expand Down
48 changes: 48 additions & 0 deletions frontends/web/src/routes/account/summary/summarybalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,51 @@ export function SummaryBalance({
</div>
);
}


export function LightningBalance() {
const { t } = useTranslation();
const { lightningConfig } = useLightning();
const [lightningBalance, setLightningBalance] = useState<accountApi.IBalance>();

const fetchLightningBalance = useCallback(async () => {
setLightningBalance(await getLightningBalance());
}, []);

useEffect(() => {
fetchLightningBalance();
const subscriptions = [subscribeNodeState(fetchLightningBalance)];
return () => unsubscribe(subscriptions);
}, [fetchLightningBalance, lightningConfig]);

return (
<div>
<div className={style.accountName}>
<p>
{lightningConfig.accounts[0].keystoreName} ({lightningConfig.accounts[0].rootFingerprint})
</p>
</div>
<div className={style.balanceTable}>
<table className={style.table}>
<colgroup>
<col width="33%" />
<col width="33%" />
<col width="*" />
</colgroup>
<thead>
<tr>
<th>{t('accountSummary.name')}</th>
<th>{t('accountSummary.balance')}</th>
<th>{t('accountSummary.fiatBalance')}</th>
</tr>
</thead>
<tbody>
{lightningBalance && (
<BalanceRow key="lightning" code="lightning" name="Lightning" coinCode="lightning" balance={lightningBalance} />
)}
</tbody>
</table>
</div>
</div>
);
}

0 comments on commit 3db7734

Please sign in to comment.