import React, { useContext, useEffect, useRef, useState } from 'react'; import { View, StatusBar, StyleSheet, Text, Keyboard, TouchableOpacity, SectionList } from 'react-native'; import { RouteProp, useNavigation, useRoute, useTheme } from '@react-navigation/native'; import { SafeBlueArea, BlueButton, BlueSpacing20, BlueSpacing10, BlueLoading, BlueTextCentered } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import { Chain } from '../../models/bitcoinUnits'; import loc, { formatBalance } from '../../loc'; import LNNodeBar from '../../components/LNNodeBar'; import BottomModal from '../../components/BottomModal'; import Button, { ButtonStyle } from '../../components/Button'; import { Psbt } from 'bitcoinjs-lib'; import { AbstractWallet, LightningLdkWallet } from '../../class'; import alert from '../../components/Alert'; const selectWallet = require('../../helpers/select-wallet'); const confirm = require('../../helpers/confirm'); const LdkNodeInfoChannelStatus = { ACTIVE: 'Active', INACTIVE: 'Inactive', PENDING: 'PENDING', STATUS: 'status' }; type LdkInfoRouteProps = RouteProp< { params: { walletID: string; psbt: Psbt; }; }, 'params' >; const LdkInfo = () => { const { walletID } = useRoute().params; const { wallets } = useContext(BlueStorageContext); const refreshDataInterval = useRef(); const sectionList = useRef(); const wallet: LightningLdkWallet = wallets.find((w: AbstractWallet) => w.getID() === walletID); const { colors }: { colors: any } = useTheme(); const { setOptions, navigate } = useNavigation(); const name = useRoute().name; const [isLoading, setIsLoading] = useState(true); const [channels, setChannels] = useState([]); const [inactiveChannels, setInactiveChannels] = useState([]); const [pendingChannels, setPendingChannels] = useState([]); const [wBalance, setWalletBalance] = useState<{ confirmedBalance?: number }>({}); const [maturingBalance, setMaturingBalance] = useState(0); const [maturingEta, setMaturingEta] = useState(''); const centerContent = channels.length === 0 && pendingChannels.length === 0 && inactiveChannels.length === 0; const allChannelsAmount = useRef(0); // Modals const [selectedChannelIndex, setSelectedChannelIndex] = useState(); // const stylesHook = StyleSheet.create({ root: { backgroundColor: colors.background, }, valueText: { color: colors.alternativeTextColor2, }, listHeaderText: { color: colors.foregroundColor, backgroundColor: colors.background, }, listHeaderBack: { backgroundColor: colors.background, }, valueRoot: { backgroundColor: colors.background, }, textHeader: { color: colors.outputValue, }, valueSats: { color: colors.alternativeTextColor2, }, paidMark: { backgroundColor: colors.success, }, detailsText: { color: colors.alternativeTextColor, }, expired: { backgroundColor: colors.success, }, additionalInfo: { backgroundColor: colors.brandingColor, }, modalContent: { backgroundColor: colors.elevated, }, separator: { backgroundColor: colors.inputBorderColor, }, }); const refetchData = async (withLoadingIndicator = true) => { setIsLoading(withLoadingIndicator); try { const listChannels = await wallet.listChannels(); if (listChannels && Array.isArray(listChannels)) { const activeChannels = listChannels.filter(channel => channel.is_usable === true); setChannels(activeChannels); } else { setChannels([]); } if (listChannels && Array.isArray(listChannels)) { const inactiveChannels = listChannels.filter(channel => !channel.is_usable && channel.is_funding_locked); setInactiveChannels(inactiveChannels); } else { setInactiveChannels([]); } if (listChannels && Array.isArray(listChannels)) { const listPendingChannels = listChannels.filter(channel => !channel.is_funding_locked); setPendingChannels(listPendingChannels); } else { setPendingChannels([]); } const walletBalance: { confirmedBalance?: number } = await wallet.walletBalance(); setWalletBalance(walletBalance); setMaturingBalance(await wallet.getMaturingBalance()); const maturingHeight = await wallet.getMaturingHeight(); if (maturingHeight > 0) { const result = await fetch('https://blockstream.info/api/blocks/tip/height'); const tip = await result.text(); const hrs = Math.ceil((maturingHeight - +tip) / 6); // convert blocks to hours setMaturingEta(`${hrs} hours`); } else { setMaturingEta(''); } } catch (e) { console.log(e); } finally { setIsLoading(false); } }; useEffect(() => { const channelsAvailable = channels.length + pendingChannels.length + inactiveChannels.length; if (allChannelsAmount.current === 0 && channelsAvailable >= 1) { sectionList?.current?.scrollToLocation({ animated: false, sectionIndex: 0, itemIndex: 0 }); } allChannelsAmount.current = channelsAvailable; }, [channels, pendingChannels, inactiveChannels]); // do we even need periodic sync when user stares at this screen..? useEffect(() => { refetchData().then(() => { refreshDataInterval.current = setInterval(() => { refetchData(false); if (wallet.timeToCheckBlockchain()) { wallet.checkBlockchain(); wallet.reconnectPeersWithPendingChannels(); } }, 2000); }); return () => { clearInterval(refreshDataInterval?.current as NodeJS.Timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { setOptions({ headerStyle: { backgroundColor: colors.customHeader, borderBottomWidth: 0, elevation: 0, shadowOpacity: 0, shadowOffset: { height: 0, width: 0 }, }, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [colors]); const showModal = (index: any) => { setSelectedChannelIndex(index); }; const closeChannel = async (channel: any) => { if (!(await confirm())) return; setSelectedChannelIndex(undefined); const wallets2use = wallets.filter((w: AbstractWallet) => w.chain === Chain.ONCHAIN); const toWallet: AbstractWallet = await selectWallet( navigate, name, null, wallets2use, 'Onchain wallet is required to withdraw funds to', ); // using wallets2use instead of simple Chain.ONCHAIN argument because by default this argument only selects wallets // that can send, which is not possible if user wants to withdraw to watch-only wallet if (!toWallet) return; console.warn('want to close to wallet ', toWallet.getLabel()); const address = await toWallet.getAddressAsync(); if (!address) return alert('Error: could not get address for channel withdrawal'); await wallet.setRefundAddress(address); let forceClose = false; if (!channel.is_usable) { if (!(await confirm(loc.lnd.force_close_channel))) return; forceClose = true; } const rez = await wallet.closeChannel(channel.channel_id, forceClose); if (rez) { alert(loc._.success); return refetchData(); } }; const claimBalance = async () => { const wallets2use = wallets.filter((w: AbstractWallet) => w.chain === Chain.ONCHAIN); const selectedWallet: AbstractWallet = await selectWallet( navigate, name, null, wallets2use, 'Onchain wallet is required to withdraw funds to', ); // using wallets2use instead of simple Chain.ONCHAIN argument because by default this argument only selects wallets // that can send, which is not possible if user wants to withdraw to watch-only wallet if (!selectedWallet) return; const address = await selectedWallet.getAddressAsync(); if (address && (await confirm())) { console.warn('selected ', selectedWallet.getLabel(), address); setIsLoading(true); try { const rez = await wallet.claimCoins(address); if (rez) { alert(loc._.sucess); await refetchData(); } } finally { setIsLoading(false); } } }; const closeModal = () => { Keyboard.dismiss(); setSelectedChannelIndex(undefined); }; const handleOnConnectPeerTapped = async (channelData: any) => { closeModal(); const { pubkey, host, port } = await wallet.lookupNodeConnectionDetailsByPubkey(channelData.remote_node_id); return wallet.connectPeer(pubkey, host, port); }; const renderModal = () => { const status = selectedChannelIndex?.status; const channelData = selectedChannelIndex?.channel.item; return ( {loc.lnd.node_alias} {channelData && ( {LightningLdkWallet.pubkeyToAlias(channelData.remote_node_id) + ' (' + channelData.remote_node_id.substr(0, 10) + '...' + channelData.remote_node_id.substr(-6) + ')'} )} {status === LdkNodeInfoChannelStatus.PENDING ? loc.transactions.pending : channelData?.is_usable ? loc.lnd.active : loc.lnd.inactive} {status === LdkNodeInfoChannelStatus.INACTIVE && ( <>