import React, { useContext, useEffect, useRef, useState } from 'react'; import { View, StyleSheet, Text, Keyboard, TouchableOpacity, SectionList } from 'react-native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { Psbt } from 'bitcoinjs-lib'; import { 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 from '../../components/Button'; import { LightningLdkWallet } from '../../class'; import presentAlert from '../../components/Alert'; import { useTheme } from '../../components/themes'; import StyledButton, { StyledButtonType } from '../../components/StyledButton'; import SafeArea from '../../components/SafeArea'; import confirm from '../../helpers/confirm'; import selectWallet from '../../helpers/select-wallet'; import { TWallet } from '../../class/wallets/types'; 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 = wallets.find(w => w.getID() === walletID) as LightningLdkWallet; const { colors } = 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, }, listHeaderText: { color: colors.foregroundColor, backgroundColor: colors.background, }, listHeaderBack: { backgroundColor: colors.background, }, detailsText: { color: colors.alternativeTextColor, }, 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 inactive = listChannels.filter(channel => !channel.is_usable && channel.is_funding_locked); setInactiveChannels(inactive); } 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, }, }); // 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 => w.chain === Chain.ONCHAIN); const toWallet = 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 presentAlert({ message: '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) { presentAlert({ message: loc._.success }); return refetchData(); } }; const claimBalance = async () => { const wallets2use = wallets.filter(w => w.chain === Chain.ONCHAIN); const selectedWallet = 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) { presentAlert({ message: loc._.success }); 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 && ( <> handleOnConnectPeerTapped(channelData)} text={loc.lnd.reconnect_peer} buttonStyle={StyledButtonType.grey} /> )} closeChannel(channelData)} text={loc.lnd.close_channel} buttonStyle={StyledButtonType.destroy} /> ); }; const renderSectionItem = (item: any) => { switch (item.section.key) { case LdkNodeInfoChannelStatus.ACTIVE: return renderItemChannel({ status: LdkNodeInfoChannelStatus.ACTIVE, channel: item }); case LdkNodeInfoChannelStatus.PENDING: return renderItemChannel({ status: LdkNodeInfoChannelStatus.PENDING, channel: item }); case LdkNodeInfoChannelStatus.INACTIVE: return renderItemChannel({ status: LdkNodeInfoChannelStatus.INACTIVE, channel: item }); default: return null; } }; const renderItemChannel = (channel: any) => { const channelData = channel.channel.item; return ( showModal(channel)}> ); }; const navigateToOpenPrivateChannel = async () => { navigateToOpenChannel({ isPrivateChannel: true }); }; const navigateToOpenChannel = async ({ isPrivateChannel }: { isPrivateChannel: boolean }) => { const availableWallets = [...wallets.filter(item => item.isSegwit() && item.allowSend())]; if (availableWallets.length === 0) { return presentAlert({ message: loc.lnd.refill_create }); } // @ts-ignore: Address types later navigate('LDKOpenChannelRoot', { screen: 'SelectWallet', params: { availableWallets, chainType: Chain.ONCHAIN, onWalletSelect: (selectedWallet: TWallet) => { const selectedWalletID = selectedWallet.getID(); selectedWallet.getAddressAsync().then((address): void => { if (!address) { presentAlert({ message: 'Error: could not get address for channel withdrawal' }); return; } wallet.setRefundAddress(address); }); // @ts-ignore: Address types later navigate('LDKOpenChannelRoot', { screen: 'LDKOpenChannelSetAmount', params: { isPrivateChannel, fundingWalletID: selectedWalletID, ldkWalletID: walletID, }, }); }, }, }); }; const itemSeparatorComponent = () => { return ; }; const renderSectionHeader = (section: any) => { switch (section.section.key) { case LdkNodeInfoChannelStatus.PENDING: return {loc.transactions.pending}; case LdkNodeInfoChannelStatus.ACTIVE: return {loc.lnd.active}; case LdkNodeInfoChannelStatus.INACTIVE: return {loc.lnd.inactive}; default: return null; } }; const sections = () => { const sectionForList = []; if (channels.length > 0) { sectionForList.push({ key: LdkNodeInfoChannelStatus.ACTIVE, data: channels }); } if (inactiveChannels.length > 0) { sectionForList.push({ key: LdkNodeInfoChannelStatus.INACTIVE, data: inactiveChannels }); } if (pendingChannels.length > 0) { sectionForList.push({ key: LdkNodeInfoChannelStatus.PENDING, data: pendingChannels }); } return sectionForList; }; return ( { sectionList.current = ref; }} renderItem={renderSectionItem} keyExtractor={channel => channel.channel_id} initialNumToRender={7} ItemSeparatorComponent={itemSeparatorComponent} renderSectionHeader={section => ( {renderSectionHeader(section)} )} contentContainerStyle={[centerContent ? {} : styles.contentContainerStyle, stylesHook.root]} contentInset={{ top: 0, left: 0, bottom: 8, right: 0 }} centerContent={centerContent} sections={sections()} ListEmptyComponent={isLoading ? : {loc.lnd.no_channels}} /> {renderModal()} {wBalance && wBalance.confirmedBalance ? ( <>