BlueWallet/screen/send/psbtMultisig.js

437 lines
14 KiB
JavaScript
Raw Normal View History

2025-02-07 13:16:41 -04:00
import React, { useEffect, useState } from 'react';
2024-05-31 13:18:01 -04:00
import { useRoute } from '@react-navigation/native';
2024-05-20 10:54:13 +01:00
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2024-06-12 12:46:44 -04:00
import { Icon } from '@rneui/themed';
2024-05-20 10:54:13 +01:00
import { satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency';
import { BlueCard, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
2023-11-15 04:40:22 -04:00
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
2024-05-20 10:54:13 +01:00
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
2024-05-31 13:18:01 -04:00
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
2020-10-05 22:25:14 +01:00
const PsbtMultisig = () => {
const { wallets } = useStorage();
2024-05-31 13:18:01 -04:00
const { navigate, setParams } = useExtendedNavigation();
2020-10-05 22:25:14 +01:00
const { colors } = useTheme();
const [flatListHeight, setFlatListHeight] = useState(0);
2021-09-09 12:00:11 +01:00
const { walletID, psbtBase64, memo, receivedPSBTBase64, launchedBy } = useRoute().params;
2020-12-01 00:40:54 -05:00
/** @type MultisigHDWallet */
const wallet = wallets.find(w => w.getID() === walletID);
2025-02-07 13:16:41 -04:00
const [psbt, setPsbt] = useState(bitcoin.Psbt.fromBase64(psbtBase64));
const data = new Array(wallet.getM());
2020-10-05 22:25:14 +01:00
const stylesHook = StyleSheet.create({
2025-02-07 13:16:41 -04:00
root: {
backgroundColor: colors.elevated,
},
whitespace: {
color: colors.elevated,
},
textBtc: {
color: colors.buttonAlternativeTextColor,
},
textBtcUnitValue: {
color: colors.buttonAlternativeTextColor,
},
textFiat: {
color: colors.alternativeTextColor,
},
provideSignatureButton: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
provideSignatureButtonText: {
color: colors.buttonTextColor,
},
vaultKeyCircle: {
backgroundColor: colors.buttonDisabledBackgroundColor,
},
2020-10-05 22:25:14 +01:00
vaultKeyText: {
color: colors.alternativeTextColor,
},
2025-02-07 13:16:41 -04:00
feeFiatText: {
color: colors.alternativeTextColor,
},
2020-10-05 22:25:14 +01:00
vaultKeyCircleSuccess: {
backgroundColor: colors.msSuccessBG,
},
2025-02-07 13:16:41 -04:00
vaultKeyTextSigned: {
color: colors.msSuccessBG,
},
2020-10-05 22:25:14 +01:00
});
2020-12-01 00:40:54 -05:00
2025-02-08 18:02:26 -04:00
// if useFilter is true, include only non-owned addresses.
const getDestinationData = (useFilter = true) => {
const addresses = [];
let totalSat = 0;
const targets = [];
for (const output of psbt.txOutputs) {
if (output.address) {
if (useFilter && wallet.weOwnAddress(output.address)) continue;
totalSat += output.value;
addresses.push(output.address);
targets.push({ address: output.address, value: output.value });
}
2025-02-07 13:16:41 -04:00
}
2025-02-08 18:02:26 -04:00
return { addresses, totalSat, targets };
};
const filteredData = getDestinationData(true);
const unfilteredData = getDestinationData(false);
const targets = filteredData.targets;
2025-02-08 18:15:17 -04:00
// totals for display from unfiltered data.
2025-02-08 18:02:26 -04:00
const displayTotalBtc = new BigNumber(unfilteredData.totalSat).dividedBy(100000000).toNumber();
const displayTotalFiat = satoshiToLocalCurrency(unfilteredData.totalSat);
2020-10-05 22:25:14 +01:00
2025-02-07 13:16:41 -04:00
const getFee = () => {
return wallet.calculateFeeFromPsbt(psbt);
};
2020-10-05 22:25:14 +01:00
2025-02-07 13:16:41 -04:00
const _renderItem = el => {
if (el.index >= howManySignaturesWeHave) return _renderItemUnsigned(el);
else return _renderItemSigned(el);
};
const navigateToPSBTMultisigQRCode = () => {
navigate('PsbtMultisigQRCode', { walletID, psbtBase64: psbt.toBase64(), isShowOpenScanner: isConfirmEnabled() });
2025-02-07 13:16:41 -04:00
};
2025-02-07 13:16:41 -04:00
const _renderItemUnsigned = el => {
const renderProvideSignature = el.index === howManySignaturesWeHave;
return (
<View testID="ItemUnsigned">
<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>
2020-10-05 22:25:14 +01:00
</View>
</View>
2020-10-05 22:25:14 +01:00
2025-02-07 13:16:41 -04:00
{renderProvideSignature && (
<View>
<TouchableOpacity
accessibilityRole="button"
testID="ProvideSignature"
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
onPress={navigateToPSBTMultisigQRCode}
>
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
{loc.multisig.provide_signature}
</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
const _renderItemSigned = el => {
return (
2020-11-05 17:17:27 +00:00
<View style={styles.flexDirectionRow} testID="ItemSigned">
2020-10-05 22:25:14 +01: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>
2025-02-07 13:16:41 -04:00
);
};
2020-10-05 22:25:14 +01:00
2025-02-07 13:16:41 -04:00
useEffect(() => {
if (receivedPSBTBase64) {
_combinePSBT();
setParams({ receivedPSBTBase64: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [receivedPSBTBase64]);
2025-02-07 13:16:41 -04:00
const _combinePSBT = () => {
2020-10-05 22:25:14 +01:00
try {
2025-02-07 13:16:41 -04:00
const receivedPSBT = bitcoin.Psbt.fromBase64(receivedPSBTBase64);
const newPsbt = psbt.combine(receivedPSBT);
setPsbt(newPsbt);
2020-10-05 22:25:14 +01:00
} catch (error) {
2025-02-07 13:16:41 -04:00
presentAlert({ message: error });
2020-10-05 22:25:14 +01:00
}
2025-02-07 13:16:41 -04:00
};
2020-10-05 22:25:14 +01:00
2025-02-07 13:16:41 -04:00
const onConfirm = () => {
2020-10-05 22:25:14 +01:00
try {
psbt.finalizeAllInputs();
2020-10-05 22:25:14 +01:00
} catch (_) {} // ignore if it is already finalized
2021-09-09 12:00:11 +01: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;
}
2025-02-07 13:16:41 -04:00
2020-10-05 22:25:14 +01:00
try {
const tx = psbt.extractTransaction().toHex();
const satoshiPerByte = Math.round(getFee() / psbt.extractTransaction().virtualSize());
2020-12-01 00:40:54 -05:00
navigate('Confirm', {
2020-10-05 22:25:14 +01:00
fee: new BigNumber(getFee()).dividedBy(100000000).toNumber(),
memo,
walletID,
2020-10-05 22:25:14 +01:00
tx,
recipients: targets,
satoshiPerByte,
});
} catch (error) {
presentAlert({ message: error });
2020-10-05 22:25:14 +01:00
}
2025-02-07 13:16:41 -04:00
};
const howManySignaturesWeHave = wallet.calculateHowManySignaturesWeHaveFromPsbt(psbt);
const isConfirmEnabled = () => {
return howManySignaturesWeHave >= wallet.getM();
};
2020-10-05 22:25:14 +01:00
2025-02-08 18:02:26 -04:00
const destinationAddress = (useFilter = true) => {
const addrs = useFilter ? filteredData.addresses : unfilteredData.addresses;
const destinationAddressView = [];
2020-11-29 21:09:09 -05:00
const whitespace = '_';
2025-02-08 18:02:26 -04:00
const destinations = Object.entries(addrs.join(', ').split(','));
2025-02-07 13:16:41 -04:00
for (const [index, address] of destinations) {
2020-10-05 22:25:14 +01:00
if (index > 1) {
2025-02-07 13:16:41 -04:00
destinationAddressView.push(
2020-11-29 21:09:09 -05:00
<View style={styles.destinationTextContainer} key={`end-${index}`}>
2020-10-07 18:49:51 -04:00
<Text numberOfLines={0} style={[styles.textDestinationFirstFour, stylesHook.textFiat]}>
2025-02-07 13:16:41 -04:00
and {destinations.length - 2} more...
2020-10-07 18:49:51 -04:00
</Text>
2025-02-07 13:16:41 -04:00
</View>,
2020-10-05 22:25:14 +01:00
);
2025-02-07 13:16:41 -04:00
break;
} else {
const currentAddress = address;
const firstFour = currentAddress.substring(0, 5);
2025-02-08 18:02:26 -04:00
const lastFour = currentAddress.substring(currentAddress.length - 5);
const middle = currentAddress.length > 10 ? currentAddress.slice(5, currentAddress.length - 5) : '';
2025-02-07 13:16:41 -04:00
destinationAddressView.push(
<View style={styles.destinationTextContainer} key={`${currentAddress}-${index}`}>
2025-02-08 18:40:19 -04:00
<Text style={styles.textAlignCenter} selectable>
2025-02-07 13:16:41 -04:00
<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>
</Text>
2025-02-07 13:16:41 -04:00
</View>,
);
}
}
return destinationAddressView;
};
2025-02-07 13:16:41 -04:00
const header = (
<View style={stylesHook.root}>
<View style={styles.containerText}>
2025-02-08 18:02:26 -04:00
<BlueText style={[styles.textBtc, stylesHook.textBtc]}>{displayTotalBtc}</BlueText>
2025-02-07 13:16:41 -04:00
<View style={styles.textBtcUnit}>
<BlueText style={[styles.textBtcUnitValue, stylesHook.textBtcUnitValue]}> {BitcoinUnit.BTC}</BlueText>
</View>
2020-10-05 22:25:14 +01:00
</View>
2025-02-07 13:16:41 -04:00
<View style={styles.containerText}>
2025-02-08 18:02:26 -04:00
<BlueText style={[styles.textFiat, stylesHook.textFiat]}>{displayTotalFiat}</BlueText>
2025-02-07 13:16:41 -04:00
</View>
2025-02-08 18:02:26 -04:00
<View>{destinationAddress(false)}</View>
2025-02-07 13:16:41 -04:00
</View>
2020-10-05 22:25:14 +01:00
);
2025-02-08 18:02:26 -04:00
2025-02-07 13:16:41 -04:00
const footer = null;
2020-10-05 22:25:14 +01:00
2025-02-07 13:16:41 -04:00
const onLayout = event => {
setFlatListHeight(event.nativeEvent.layout.height);
};
2020-10-05 22:25:14 +01:00
return (
<SafeArea style={stylesHook.root}>
2025-02-07 13:16:41 -04:00
<View style={styles.flexColumnSpaceBetween}>
<View style={styles.flexOne}>
<View style={styles.container}>
<View style={styles.mstopcontainer}>
<View style={styles.mscontainer}>
<View style={[styles.msleft, { height: flatListHeight - 260 }]} />
</View>
<View style={styles.msright}>
<BlueCard>
<FlatList
data={data}
renderItem={_renderItem}
keyExtractor={(_item, index) => `${index}`}
ListHeaderComponent={header}
ListFooterComponent={footer}
onLayout={onLayout}
/>
{isConfirmEnabled() && (
<View style={styles.height80}>
<TouchableOpacity
accessibilityRole="button"
testID="ExportSignedPsbt"
style={[styles.provideSignatureButton, stylesHook.provideSignatureButton]}
onPress={navigateToPSBTMultisigQRCode}
>
<Text style={[styles.provideSignatureButtonText, stylesHook.provideSignatureButtonText]}>
{loc.multisig.export_signed_psbt}
</Text>
</TouchableOpacity>
</View>
)}
</BlueCard>
</View>
</View>
</View>
</View>
<View style={styles.feeConfirmContainer}>
<View style={styles.feeContainer}>
<View style={styles.bottomWrapper}>
<View style={styles.bottomFeesWrapper}>
<BlueText style={[styles.feeFiatText, stylesHook.feeFiatText]}>
{loc.formatString(loc.multisig.fee, { number: satoshiToLocalCurrency(getFee()) })} -{' '}
</BlueText>
<BlueText>{loc.formatString(loc.multisig.fee_btc, { number: satoshiToBTC(getFee()) })}</BlueText>
</View>
</View>
2020-10-05 22:25:14 +01:00
</View>
2025-02-07 13:16:41 -04:00
<View style={styles.flexConfirm}>
<Button disabled={!isConfirmEnabled()} title={loc.multisig.confirm} onPress={onConfirm} testID="PsbtMultisigConfirmButton" />
2020-10-05 22:25:14 +01:00
</View>
</View>
</View>
</SafeArea>
2020-10-05 22:25:14 +01:00
);
};
const styles = StyleSheet.create({
2025-02-07 13:16:41 -04:00
mstopcontainer: {
flex: 1,
flexDirection: 'row',
},
mscontainer: {
flex: 10,
},
flexOne: {
flex: 1,
},
msleft: {
width: 1,
borderStyle: 'dashed',
borderWidth: 0.8,
borderColor: '#c4c4c4',
marginLeft: 40,
marginTop: 160,
},
msright: {
flex: 90,
marginLeft: '-11%',
},
container: {
flexDirection: 'column',
flex: 1,
},
containerText: {
flexDirection: 'row',
justifyContent: 'center',
},
destinationTextContainer: {
flexDirection: 'row',
marginBottom: 4,
paddingHorizontal: 60,
fontSize: 14,
justifyContent: 'center',
},
textFiat: {
fontSize: 16,
fontWeight: '500',
marginBottom: 30,
},
textBtc: {
fontWeight: 'bold',
fontSize: 30,
},
textAlignCenter: {
textAlign: 'center',
},
textDestinationFirstFour: {
fontSize: 14,
},
textDestination: {
paddingTop: 10,
paddingBottom: 40,
fontSize: 14,
flexWrap: 'wrap',
},
2020-10-05 22:25:14 +01: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 },
2025-02-07 13:16:41 -04:00
vaultKeyCircle: {
width: 42,
height: 42,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
vaultKeyCircleSuccess: {
width: 42,
height: 42,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
},
2020-10-05 22:25:14 +01:00
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 15:39:23 -04:00
textBtcUnit: { justifyContent: 'flex-end' },
bottomFeesWrapper: { justifyContent: 'center', alignItems: 'center', flexDirection: 'row' },
bottomWrapper: { marginTop: 16 },
2025-02-07 13:16:41 -04:00
height80: {
height: 80,
},
flexColumnSpaceBetween: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
},
flexConfirm: {
paddingHorizontal: 32,
paddingVertical: 16,
},
feeConfirmContainer: {
paddingHorizontal: 32,
paddingVertical: 16,
},
feeContainer: {
marginBottom: 8,
},
2020-10-05 22:25:14 +01:00
});
export default PsbtMultisig;