mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 18:00:17 +01:00
325 lines
12 KiB
TypeScript
325 lines
12 KiB
TypeScript
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
import { View, StyleSheet } from 'react-native';
|
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
|
import { BlueLoading, BlueDismissKeyboardInputAccessory, BlueSpacing20, BlueText } from '../../BlueComponents';
|
|
import navigationStyle from '../../components/navigationStyle';
|
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
|
import BigNumber from 'bignumber.js';
|
|
import AddressInput from '../../components/AddressInput';
|
|
import AmountInput from '../../components/AmountInput';
|
|
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
|
import loc from '../../loc';
|
|
import { AbstractWallet, HDSegwitBech32Wallet, LightningLdkWallet } from '../../class';
|
|
import { ArrowPicker } from '../../components/ArrowPicker';
|
|
import { Psbt } from 'bitcoinjs-lib';
|
|
import Biometric from '../../class/biometrics';
|
|
import alert from '../../components/Alert';
|
|
import { useTheme } from '../../components/themes';
|
|
import Button from '../../components/Button';
|
|
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
|
import SafeArea from '../../components/SafeArea';
|
|
const currency = require('../../blue_modules/currency');
|
|
|
|
type LdkOpenChannelProps = RouteProp<
|
|
{
|
|
params: {
|
|
isPrivateChannel: boolean;
|
|
psbt: Psbt;
|
|
fundingWalletID: string;
|
|
ldkWalletID: string;
|
|
remoteHostWithPubkey: string;
|
|
};
|
|
},
|
|
'params'
|
|
>;
|
|
|
|
const LdkOpenChannel = (props: any) => {
|
|
const { wallets, fetchAndSaveWalletTransactions } = useContext(BlueStorageContext);
|
|
const [isBiometricUseCapableAndEnabled, setIsBiometricUseCapableAndEnabled] = useState(false);
|
|
const { colors }: { colors: any } = useTheme();
|
|
const { navigate, setParams } = useNavigation();
|
|
const {
|
|
fundingWalletID,
|
|
isPrivateChannel,
|
|
ldkWalletID,
|
|
psbt,
|
|
remoteHostWithPubkey = '030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f@52.50.244.44:9735' /* Bitrefill */,
|
|
} = useRoute<LdkOpenChannelProps>().params;
|
|
const fundingWallet: HDSegwitBech32Wallet = wallets.find((w: AbstractWallet) => w.getID() === fundingWalletID);
|
|
const ldkWallet: LightningLdkWallet = wallets.find((w: AbstractWallet) => w.getID() === ldkWalletID);
|
|
const [unit, setUnit] = useState<BitcoinUnit | string>(ldkWallet.getPreferredBalanceUnit());
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const psbtOpenChannelStartedTs = useRef<number>();
|
|
const name = useRoute().name;
|
|
const [fundingAmount, setFundingAmount] = useState<any>({ amount: null, amountSats: null });
|
|
const [verified, setVerified] = useState(false);
|
|
|
|
const stylesHook = StyleSheet.create({
|
|
root: {
|
|
backgroundColor: colors.elevated,
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Handles when user navigates back from transaction creation flow and has PSBT object
|
|
*/
|
|
useEffect(() => {
|
|
if (!psbt) return;
|
|
(async () => {
|
|
if (psbtOpenChannelStartedTs.current ? +new Date() - psbtOpenChannelStartedTs.current >= 5 * 60 * 1000 : false) {
|
|
// its 10 min actually, but lets check 5 min just for any case
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
return alert('Channel opening expired. Please try again');
|
|
}
|
|
|
|
setVerified(true);
|
|
})();
|
|
}, [psbt]);
|
|
|
|
useEffect(() => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
Biometric.isBiometricUseCapableAndEnabled().then(setIsBiometricUseCapableAndEnabled);
|
|
}, []);
|
|
|
|
const finalizeOpenChannel = async () => {
|
|
setIsLoading(true);
|
|
if (isBiometricUseCapableAndEnabled) {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
if (!(await Biometric.unlockWithBiometrics())) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
}
|
|
if (psbtOpenChannelStartedTs.current ? +new Date() - psbtOpenChannelStartedTs.current >= 5 * 60 * 1000 : false) {
|
|
// its 10 min actually, but lets check 5 min just for any case
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
setIsLoading(false);
|
|
return alert('Channel opening expired. Please try again');
|
|
}
|
|
|
|
const tx = psbt.extractTransaction();
|
|
const res = await ldkWallet.fundingStateStepFinalize(tx.toHex()); // comment this out to debug
|
|
// const res = true; // debug
|
|
if (!res) {
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
setIsLoading(false);
|
|
return alert('Something wend wrong during opening channel tx broadcast');
|
|
}
|
|
fetchAndSaveWalletTransactions(ldkWallet.getID());
|
|
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep to make sure network propagates
|
|
fetchAndSaveWalletTransactions(fundingWalletID);
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
|
// @ts-ignore: Address types later
|
|
navigate('Success', { amount: undefined });
|
|
setIsLoading(false);
|
|
};
|
|
|
|
const openChannel = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const amountSatsNumber = new BigNumber(fundingAmount.amountSats).toNumber();
|
|
if (!amountSatsNumber) {
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
return alert('Amount is not valid');
|
|
}
|
|
|
|
const pubkey = remoteHostWithPubkey.split('@')[0];
|
|
const host = remoteHostWithPubkey.split('@')[1];
|
|
if (!pubkey || !host) {
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
return alert('Remote node address is not valid');
|
|
}
|
|
|
|
const fundingAddressTemp = await ldkWallet.openChannel(pubkey, host, fundingAmount.amountSats, isPrivateChannel);
|
|
console.warn('initiated channel opening');
|
|
|
|
if (!fundingAddressTemp) {
|
|
let reason = '';
|
|
const channelsClosed = ldkWallet.getChannelsClosedEvents();
|
|
const event = channelsClosed.pop();
|
|
if (event) {
|
|
reason += event.reason + ' ' + event.text;
|
|
}
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
return alert('Initiating channel open failed: ' + reason);
|
|
}
|
|
|
|
psbtOpenChannelStartedTs.current = +new Date();
|
|
// @ts-ignore: Address types later
|
|
navigate('SendDetailsRoot', {
|
|
screen: 'SendDetails',
|
|
params: {
|
|
memo: 'open channel',
|
|
address: fundingAddressTemp,
|
|
walletID: fundingWalletID,
|
|
amount: fundingAmount.amount,
|
|
amountSats: fundingAmount.amountSats,
|
|
unit,
|
|
noRbf: true,
|
|
launchedBy: name,
|
|
isEditable: false,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
|
alert(error.message);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const onBarScanned = (ret: { data?: any }) => {
|
|
if (!ret.data) ret = { data: ret };
|
|
// @ts-ignore: Address types later
|
|
setParams({ remoteHostWithPubkey: ret.data });
|
|
};
|
|
|
|
const render = () => {
|
|
if (isLoading || !ldkWallet || !fundingWallet) {
|
|
return (
|
|
<View style={[styles.root, styles.justifyContentCenter, stylesHook.root]}>
|
|
<BlueLoading style={{}} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (verified) {
|
|
return (
|
|
<View style={[styles.activeRoot, stylesHook.root]}>
|
|
<BlueText>
|
|
{loc.formatString(loc.lnd.opening_channnel_for_from, {
|
|
forWalletLabel: ldkWallet.getLabel(),
|
|
fromWalletLabel: fundingWallet.getLabel(),
|
|
})}
|
|
</BlueText>
|
|
|
|
<BlueText>{loc.lnd.are_you_sure_open_channel}</BlueText>
|
|
<BlueSpacing20 />
|
|
<View style={styles.horizontalButtons}>
|
|
<Button onPress={finalizeOpenChannel} title={loc._.continue} />
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.activeRoot, stylesHook.root]}>
|
|
<BlueText>
|
|
{loc.formatString(loc.lnd.opening_channnel_for_from, {
|
|
forWalletLabel: ldkWallet.getLabel(),
|
|
fromWalletLabel: fundingWallet.getLabel(),
|
|
})}
|
|
</BlueText>
|
|
<AmountInput
|
|
placeholder={loc.lnd.funding_amount_placeholder}
|
|
isLoading={isLoading}
|
|
amount={fundingAmount.amount}
|
|
onAmountUnitChange={(newUnit: string) => {
|
|
let amountSats = fundingAmount.amountSats;
|
|
switch (newUnit) {
|
|
case BitcoinUnit.SATS:
|
|
amountSats = parseInt(fundingAmount.amount, 10);
|
|
break;
|
|
case BitcoinUnit.BTC:
|
|
amountSats = currency.btcToSatoshi(fundingAmount.amount);
|
|
break;
|
|
case BitcoinUnit.LOCAL_CURRENCY:
|
|
// also accounting for cached fiat->sat conversion to avoid rounding error
|
|
amountSats = currency.btcToSatoshi(currency.fiatToBTC(fundingAmount.amount));
|
|
break;
|
|
}
|
|
setFundingAmount({ amount: fundingAmount.amount, amountSats });
|
|
setUnit(newUnit);
|
|
}}
|
|
onChangeText={(text: string) => {
|
|
let amountSats = fundingAmount.amountSats;
|
|
switch (unit) {
|
|
case BitcoinUnit.BTC:
|
|
amountSats = currency.btcToSatoshi(text);
|
|
break;
|
|
case BitcoinUnit.LOCAL_CURRENCY:
|
|
amountSats = currency.btcToSatoshi(currency.fiatToBTC(text));
|
|
break;
|
|
case BitcoinUnit.SATS:
|
|
amountSats = parseInt(text, 10);
|
|
break;
|
|
}
|
|
setFundingAmount({ amount: text, amountSats });
|
|
}}
|
|
unit={unit}
|
|
inputAccessoryViewID={(BlueDismissKeyboardInputAccessory as any).InputAccessoryViewID}
|
|
/>
|
|
|
|
<AddressInput
|
|
placeholder={loc.lnd.remote_host}
|
|
address={remoteHostWithPubkey}
|
|
isLoading={isLoading}
|
|
inputAccessoryViewID={(BlueDismissKeyboardInputAccessory as any).InputAccessoryViewID}
|
|
onChangeText={text =>
|
|
// @ts-ignore: Address types later
|
|
setParams({ remoteHostWithPubkey: text })
|
|
}
|
|
onBarScanned={onBarScanned}
|
|
launchedBy={name}
|
|
/>
|
|
<BlueDismissKeyboardInputAccessory />
|
|
|
|
<ArrowPicker
|
|
onChange={newKey => {
|
|
const nodes = LightningLdkWallet.getPredefinedNodes();
|
|
if (nodes[newKey])
|
|
// @ts-ignore: Address types later
|
|
setParams({ remoteHostWithPubkey: nodes[newKey] });
|
|
}}
|
|
items={LightningLdkWallet.getPredefinedNodes()}
|
|
isItemUnknown={!Object.values(LightningLdkWallet.getPredefinedNodes()).some(node => node === remoteHostWithPubkey)}
|
|
/>
|
|
<BlueSpacing20 />
|
|
<View style={styles.horizontalButtons}>
|
|
<Button onPress={openChannel} disabled={remoteHostWithPubkey.length === 0} title={loc.lnd.open_channel} />
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return <SafeArea style={[styles.root, stylesHook.root]}>{render()}</SafeArea>;
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
root: {
|
|
flex: 1,
|
|
},
|
|
justifyContentCenter: {
|
|
justifyContent: 'center',
|
|
},
|
|
horizontalButtons: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
},
|
|
activeRoot: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
padding: 16,
|
|
},
|
|
});
|
|
|
|
LdkOpenChannel.navigationOptions = navigationStyle(
|
|
{
|
|
closeButton: true,
|
|
closeButtonFunc: ({ navigation }) => navigation.getParent().pop(),
|
|
},
|
|
(options, { theme, navigation, route }) => {
|
|
return {
|
|
...options,
|
|
headerTitle: loc.lnd.new_channel,
|
|
headerLargeTitle: true,
|
|
statusBarStyle: 'auto',
|
|
};
|
|
},
|
|
);
|
|
|
|
export default LdkOpenChannel;
|