BlueWallet/screen/send/psbtMultisig.js

407 lines
13 KiB
JavaScript
Raw Normal View History

2024-05-31 19:18:01 +02:00
import React, { useEffect, useState } from 'react';
import { useRoute } from '@react-navigation/native';
2024-05-20 11:54:13 +02:00
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2024-06-12 18:46:44 +02:00
import { Icon } from '@rneui/themed';
2024-05-20 11:54:13 +02:00
import { satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency';
import { BlueCard, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
2023-11-15 09:40:22 +01:00
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
2024-05-20 11:54:13 +02:00
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
2024-05-31 19:18:01 +02:00
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
2020-10-05 23:25:14 +02:00
const shortenAddress = addr => {
return addr.substr(0, Math.floor(addr.length / 2) - 1) + '\n' + addr.substr(Math.floor(addr.length / 2) - 1, addr.length);
};
const PsbtMultisig = () => {
const { wallets } = useStorage();
2024-05-31 19:18:01 +02:00
const { navigate, setParams } = useExtendedNavigation();
2020-10-05 23:25:14 +02:00
const { colors } = useTheme();
const [flatListHeight, setFlatListHeight] = useState(0);
2021-09-09 13:00:11 +02:00
const { walletID, psbtBase64, memo, receivedPSBTBase64, launchedBy } = useRoute().params;
2020-12-01 06:40:54 +01:00
/** @type MultisigHDWallet */
const wallet = wallets.find(w => w.getID() === walletID);
const [psbt, setPsbt] = useState(bitcoin.Psbt.fromBase64(psbtBase64));
2020-12-01 06:40:54 +01:00
const data = new Array(wallet.getM());
2020-10-05 23:25:14 +02:00
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
2020-11-30 03:09:09 +01:00
whitespace: {
color: colors.elevated,
},
2020-10-05 23:25:14 +02:00
textBtc: {
color: colors.buttonAlternativeTextColor,
},
textBtcUnitValue: {
color: colors.buttonAlternativeTextColor,
},
textFiat: {
color: colors.alternativeTextColor,
},
provideSignatureButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
provideSignatureButtonText: {
color: colors.buttonTextColor,
},
vaultKeyCircle: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
vaultKeyText: {
color: colors.alternativeTextColor,
},
feeFiatText: {
color: colors.alternativeTextColor,
},
vaultKeyCircleSuccess: {
backgroundColor: colors.msSuccessBG,
},
vaultKeyTextSigned: {
color: colors.msSuccessBG,
},
});
2020-12-01 06:40:54 +01:00
2020-10-05 23:25:14 +02:00
let destination = [];
let totalSat = 0;
const targets = [];
for (const output of psbt.txOutputs) {
2020-10-05 23:25:14 +02:00
if (output.address && !wallet.weOwnAddress(output.address)) {
totalSat += output.value;
destination.push(output.address);
targets.push({ address: output.address, value: output.value });
}
}
destination = shortenAddress(destination.join(', '));
const totalBtc = new BigNumber(totalSat).dividedBy(100000000).toNumber();
2024-01-28 16:11:08 +01:00
const totalFiat = satoshiToLocalCurrency(totalSat);
2020-10-05 23:25:14 +02:00
const getFee = () => {
return wallet.calculateFeeFromPsbt(psbt);
2020-10-05 23:25:14 +02:00
};
const _renderItem = el => {
2020-12-01 06:40:54 +01:00
if (el.index >= howManySignaturesWeHave) return _renderItemUnsigned(el);
2020-10-05 23:25:14 +02:00
else return _renderItemSigned(el);
};
const navigateToPSBTMultisigQRCode = () => {
navigate('PsbtMultisigQRCode', { walletID, psbtBase64: psbt.toBase64(), isShowOpenScanner: isConfirmEnabled() });
};
2020-10-05 23:25:14 +02:00
const _renderItemUnsigned = el => {
2020-12-01 06:40:54 +01:00
const renderProvideSignature = el.index === howManySignaturesWeHave;
2020-10-05 23:25:14 +02:00
return (
2020-11-05 18:17:27 +01:00
<View testID="ItemUnsigned">
2020-10-05 23:25:14 +02:00
<View style={styles.itemUnsignedWrapper}>
<View style={[styles.vaultKeyCircle, stylesHook.vaultKeyCircle]}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>{el.index + 1}</Text>
</View>
<View style={styles.vaultKeyTextWrapper}>
<Text style={[styles.vaultKeyText, stylesHook.vaultKeyText]}>
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
</Text>
</View>
</View>
{renderProvideSignature && (
<View>
<TouchableOpacity
accessibilityRole="button"
2020-11-05 18:17:27 +01:00
testID="ProvideSignature"
2020-10-05 23:25:14 +02:00
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
onPress={navigateToPSBTMultisigQRCode}
2020-10-05 23:25:14 +02:00
>
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
{loc.multisig.provide_signature}
</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
const _renderItemSigned = el => {
return (
2020-11-05 18:17:27 +01:00
<View style={styles.flexDirectionRow} testID="ItemSigned">
2020-10-05 23:25:14 +02:00
<View style={[styles.vaultKeyCircleSuccess, stylesHook.vaultKeyCircleSuccess]}>
<Icon size={24} name="check" type="ionicons" color={colors.msSuccessCheck} />
</View>
<View style={styles.vaultKeyTextSignedWrapper}>
<Text style={[styles.vaultKeyTextSigned, stylesHook.vaultKeyTextSigned]}>
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
</Text>
</View>
</View>
);
};
2020-12-01 06:40:54 +01:00
useEffect(() => {
if (receivedPSBTBase64) {
_combinePSBT();
setParams({ receivedPSBTBase64: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [receivedPSBTBase64]);
const _combinePSBT = () => {
2020-10-05 23:25:14 +02:00
try {
2021-09-13 19:43:26 +02:00
const receivedPSBT = bitcoin.Psbt.fromBase64(receivedPSBTBase64);
const newPsbt = psbt.combine(receivedPSBT);
setPsbt(newPsbt);
2020-10-05 23:25:14 +02:00
} catch (error) {
presentAlert({ message: error });
2020-10-05 23:25:14 +02:00
}
};
const onConfirm = () => {
try {
psbt.finalizeAllInputs();
2020-10-05 23:25:14 +02:00
} catch (_) {} // ignore if it is already finalized
2021-09-09 13:00:11 +02:00
if (launchedBy) {
// we must navigate back to the screen who requested psbt (instead of broadcasting it ourselves)
// most likely for LN channel opening
navigate(launchedBy, { psbt });
return;
}
2020-10-05 23:25:14 +02:00
try {
const tx = psbt.extractTransaction().toHex();
const satoshiPerByte = Math.round(getFee() / psbt.extractTransaction().virtualSize());
2020-12-01 06:40:54 +01:00
navigate('Confirm', {
2020-10-05 23:25:14 +02:00
fee: new BigNumber(getFee()).dividedBy(100000000).toNumber(),
memo,
walletID,
2020-10-05 23:25:14 +02:00
tx,
recipients: targets,
satoshiPerByte,
});
} catch (error) {
presentAlert({ message: error });
2020-10-05 23:25:14 +02:00
}
};
const howManySignaturesWeHave = wallet.calculateHowManySignaturesWeHaveFromPsbt(psbt);
2020-10-05 23:25:14 +02:00
const isConfirmEnabled = () => {
2020-12-01 06:40:54 +01:00
return howManySignaturesWeHave >= wallet.getM();
2020-10-05 23:25:14 +02:00
};
const destinationAddress = () => {
// eslint-disable-next-line prefer-const
let destinationAddressView = [];
2020-11-30 03:09:09 +01:00
const whitespace = '_';
2020-10-05 23:25:14 +02:00
const destinations = Object.entries(destination.split(','));
for (const [index, address] of destinations) {
if (index > 1) {
destinationAddressView.push(
2020-11-30 03:09:09 +01:00
<View style={styles.destinationTextContainer} key={`end-${index}`}>
2020-10-08 00:49:51 +02:00
<Text numberOfLines={0} style={[styles.textDestinationFirstFour, stylesHook.textFiat]}>
and {destinations.length - 2} more...
</Text>
2020-10-05 23:25:14 +02:00
</View>,
);
break;
} else {
2020-10-08 00:49:51 +02:00
const currentAddress = address;
2020-10-05 23:25:14 +02:00
const firstFour = currentAddress.substring(0, 5);
const lastFour = currentAddress.substring(currentAddress.length - 5, currentAddress.length);
const middle = currentAddress.split(firstFour)[1].split(lastFour)[0];
destinationAddressView.push(
2020-11-30 03:09:09 +01:00
<View style={styles.destinationTextContainer} key={`${currentAddress}-${index}`}>
<Text style={styles.textAlignCenter}>
<Text numberOfLines={2} style={[styles.textDestinationFirstFour, stylesHook.textBtc]}>
{firstFour}
<Text style={stylesHook.whitespace}>{whitespace}</Text>
<Text style={[styles.textDestination, stylesHook.textFiat]}>{middle}</Text>
<Text style={stylesHook.whitespace}>{whitespace}</Text>
<Text style={[styles.textDestinationFirstFour, stylesHook.textBtc]}>{lastFour}</Text>
</Text>
2020-10-08 00:49:51 +02:00
</Text>
2020-10-05 23:25:14 +02:00
</View>,
);
}
}
return destinationAddressView;
};
const header = (
<View style={stylesHook.root}>
<View style={styles.containerText}>
<BlueText style={[styles.textBtc, stylesHook.textBtc]}>{totalBtc}</BlueText>
<View style={styles.textBtcUnit}>
<BlueText style={[styles.textBtcUnitValue, stylesHook.textBtcUnitValue]}> {BitcoinUnit.BTC}</BlueText>
</View>
</View>
<View style={styles.containerText}>
<BlueText style={[styles.textFiat, stylesHook.textFiat]}>{totalFiat}</BlueText>
</View>
<View>{destinationAddress()}</View>
</View>
);
const footer = (
<>
<View style={styles.bottomWrapper}>
<View style={styles.bottomFeesWrapper}>
<BlueText style={[styles.feeFiatText, stylesHook.feeFiatText]}>
2024-01-28 16:11:08 +01:00
{loc.formatString(loc.multisig.fee, { number: satoshiToLocalCurrency(getFee()) })} -{' '}
</BlueText>
2024-01-28 16:11:08 +01:00
<BlueText>{loc.formatString(loc.multisig.fee_btc, { number: satoshiToBTC(getFee()) })}</BlueText>
</View>
2020-10-05 23:25:14 +02:00
</View>
<View style={styles.marginConfirmButton}>
2023-11-15 09:40:22 +01:00
<Button disabled={!isConfirmEnabled()} title={loc.multisig.confirm} onPress={onConfirm} testID="PsbtMultisigConfirmButton" />
</View>
</>
2020-10-05 23:25:14 +02:00
);
const onLayout = e => {
setFlatListHeight(e.nativeEvent.layout.height);
};
return (
<SafeArea style={stylesHook.root}>
2020-10-05 23:25:14 +02:00
<View style={styles.container}>
<View style={styles.mstopcontainer}>
<View style={styles.mscontainer}>
<View style={[styles.msleft, { height: flatListHeight - 260 }]} />
2020-10-05 23:25:14 +02:00
</View>
<View style={styles.msright}>
<BlueCard>
<FlatList
data={data}
onLayout={onLayout}
renderItem={_renderItem}
keyExtractor={(_item, index) => `${index}`}
ListHeaderComponent={header}
ListFooterComponent={footer}
2020-10-05 23:25:14 +02:00
/>
2020-11-05 18:17:27 +01:00
{isConfirmEnabled() && (
<View style={styles.height80}>
<TouchableOpacity
accessibilityRole="button"
2020-11-05 18:17:27 +01:00
testID="ExportSignedPsbt"
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
onPress={navigateToPSBTMultisigQRCode}
2020-11-05 18:17:27 +01:00
>
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
{loc.multisig.export_signed_psbt}
</Text>
</TouchableOpacity>
</View>
)}
2020-10-05 23:25:14 +02:00
</BlueCard>
</View>
</View>
</View>
</SafeArea>
2020-10-05 23:25:14 +02:00
);
};
const styles = StyleSheet.create({
mstopcontainer: {
flex: 1,
flexDirection: 'row',
},
mscontainer: {
flex: 10,
},
msleft: {
width: 1,
borderStyle: 'dashed',
borderWidth: 0.8,
borderColor: '#c4c4c4',
marginLeft: 40,
2021-04-11 23:13:09 +02:00
marginTop: 130,
2020-10-05 23:25:14 +02:00
},
msright: {
flex: 90,
marginLeft: '-11%',
},
container: {
flexDirection: 'column',
paddingTop: 24,
flex: 1,
},
containerText: {
flexDirection: 'row',
justifyContent: 'center',
},
2020-11-30 03:09:09 +01:00
destinationTextContainer: {
2020-10-05 23:25:14 +02:00
flexDirection: 'row',
marginBottom: 4,
2020-10-08 00:49:51 +02:00
paddingHorizontal: 60,
fontSize: 14,
justifyContent: 'center',
2020-10-05 23:25:14 +02:00
},
textFiat: {
fontSize: 16,
fontWeight: '500',
marginBottom: 30,
},
textBtc: {
fontWeight: 'bold',
fontSize: 30,
},
2020-11-30 03:09:09 +01:00
textAlignCenter: {
textAlign: 'center',
},
2020-10-05 23:25:14 +02:00
textDestinationFirstFour: {
2020-10-08 00:49:51 +02:00
fontSize: 14,
2020-10-05 23:25:14 +02:00
},
textDestination: {
paddingTop: 10,
paddingBottom: 40,
2020-10-08 00:49:51 +02:00
fontSize: 14,
flexWrap: 'wrap',
2020-10-05 23:25:14 +02:00
},
provideSignatureButton: {
marginTop: 24,
marginLeft: 40,
height: 48,
borderRadius: 8,
flex: 1,
justifyContent: 'center',
paddingHorizontal: 16,
marginBottom: 8,
},
provideSignatureButtonText: { fontWeight: '600', fontSize: 15 },
vaultKeyText: { fontSize: 18, fontWeight: 'bold' },
vaultKeyTextWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
vaultKeyCircle: {
width: 42,
height: 42,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
vaultKeyCircleSuccess: {
width: 42,
height: 42,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
itemUnsignedWrapper: { flexDirection: 'row', paddingTop: 16 },
vaultKeyTextSigned: { fontSize: 18, fontWeight: 'bold' },
vaultKeyTextSignedWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
flexDirectionRow: { flexDirection: 'row', paddingVertical: 12 },
2021-04-11 21:39:23 +02:00
textBtcUnit: { justifyContent: 'flex-end' },
bottomFeesWrapper: { justifyContent: 'center', alignItems: 'center', flexDirection: 'row' },
bottomWrapper: { marginTop: 16 },
2020-12-04 10:33:25 +01:00
marginConfirmButton: { marginTop: 16, marginHorizontal: 32, marginBottom: 48 },
2020-11-05 18:17:27 +01:00
height80: {
height: 80,
},
2020-10-05 23:25:14 +02:00
});
export default PsbtMultisig;