mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 18:00:17 +01:00
413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
/* global alert */
|
|
import React, { useContext, useEffect, useState } from 'react';
|
|
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
import { Icon } from 'react-native-elements';
|
|
import { useNavigation, useRoute, useTheme } from '@react-navigation/native';
|
|
|
|
import { BlueButton, BlueCard, BlueText, SafeBlueArea } from '../../BlueComponents';
|
|
import navigationStyle from '../../components/navigationStyle';
|
|
import loc from '../../loc';
|
|
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
|
import { BlueStorageContext } from '../../blue_modules/storage-context';
|
|
const bitcoin = require('bitcoinjs-lib');
|
|
const BigNumber = require('bignumber.js');
|
|
const currency = require('../../blue_modules/currency');
|
|
|
|
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 } = useContext(BlueStorageContext);
|
|
const { navigate, setParams } = useNavigation();
|
|
const { colors } = useTheme();
|
|
const [flatListHeight, setFlatListHeight] = useState(0);
|
|
const { walletID, psbtBase64, memo, receivedPSBTBase64 } = useRoute().params;
|
|
/** @type MultisigHDWallet */
|
|
const wallet = wallets.find(w => w.getID() === walletID);
|
|
const [psbt, setPsbt] = useState(bitcoin.Psbt.fromBase64(psbtBase64));
|
|
const data = new Array(wallet.getM());
|
|
const stylesHook = StyleSheet.create({
|
|
root: {
|
|
backgroundColor: colors.elevated,
|
|
},
|
|
whitespace: {
|
|
color: colors.elevated,
|
|
},
|
|
textBtc: {
|
|
color: colors.buttonAlternativeTextColor,
|
|
},
|
|
textDestinationFirstFour: {
|
|
color: colors.buttonAlternativeTextColor,
|
|
},
|
|
textBtcUnitValue: {
|
|
color: colors.buttonAlternativeTextColor,
|
|
},
|
|
textDestination: {
|
|
color: colors.foregroundColor,
|
|
},
|
|
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,
|
|
},
|
|
});
|
|
|
|
let destination = [];
|
|
let totalSat = 0;
|
|
const targets = [];
|
|
for (const output of psbt.txOutputs) {
|
|
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();
|
|
const totalFiat = currency.satoshiToLocalCurrency(totalSat);
|
|
|
|
const getFee = () => {
|
|
return wallet.calculateFeeFromPsbt(psbt);
|
|
};
|
|
|
|
const _renderItem = el => {
|
|
if (el.index >= howManySignaturesWeHave) return _renderItemUnsigned(el);
|
|
else return _renderItemSigned(el);
|
|
};
|
|
|
|
const navigateToPSBTMultisigQRCode = () => {
|
|
navigate('PsbtMultisigQRCode', { walletID, psbtBase64, isShowOpenScanner: isConfirmEnabled() });
|
|
};
|
|
|
|
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>
|
|
</View>
|
|
</View>
|
|
|
|
{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 (
|
|
<View style={styles.flexDirectionRow} testID="ItemSigned">
|
|
<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>
|
|
);
|
|
};
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
useEffect(() => {
|
|
if (receivedPSBTBase64) {
|
|
_combinePSBT();
|
|
setParams({ receivedPSBTBase64: undefined });
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [receivedPSBTBase64]);
|
|
|
|
const _combinePSBT = () => {
|
|
const receivedPSBT = bitcoin.Psbt.fromBase64(receivedPSBTBase64);
|
|
try {
|
|
const newPsbt = psbt.combine(receivedPSBT);
|
|
setPsbt(newPsbt);
|
|
} catch (error) {
|
|
alert(error);
|
|
}
|
|
};
|
|
|
|
const onConfirm = () => {
|
|
try {
|
|
psbt.finalizeAllInputs();
|
|
} catch (_) {} // ignore if it is already finalized
|
|
|
|
try {
|
|
const tx = psbt.extractTransaction().toHex();
|
|
const satoshiPerByte = Math.round(getFee() / (tx.length / 2));
|
|
navigate('Confirm', {
|
|
fee: new BigNumber(getFee()).dividedBy(100000000).toNumber(),
|
|
memo: memo,
|
|
fromWallet: wallet,
|
|
tx,
|
|
recipients: targets,
|
|
satoshiPerByte,
|
|
});
|
|
} catch (error) {
|
|
alert(error);
|
|
}
|
|
};
|
|
|
|
const howManySignaturesWeHave = wallet.calculateHowManySignaturesWeHaveFromPsbt(psbt);
|
|
const isConfirmEnabled = () => {
|
|
return howManySignaturesWeHave >= wallet.getM();
|
|
};
|
|
|
|
const destinationAddress = () => {
|
|
// eslint-disable-next-line prefer-const
|
|
let destinationAddressView = [];
|
|
const whitespace = '_';
|
|
const destinations = Object.entries(destination.split(','));
|
|
for (const [index, address] of destinations) {
|
|
if (index > 1) {
|
|
destinationAddressView.push(
|
|
<View style={styles.destinationTextContainer} key={`end-${index}`}>
|
|
<Text numberOfLines={0} style={[styles.textDestinationFirstFour, stylesHook.textFiat]}>
|
|
and {destinations.length - 2} more...
|
|
</Text>
|
|
</View>,
|
|
);
|
|
break;
|
|
} else {
|
|
const currentAddress = address;
|
|
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(
|
|
<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>
|
|
</Text>
|
|
</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]}>
|
|
{loc.formatString(loc.multisig.fee, { number: currency.satoshiToLocalCurrency(getFee()) })} -{' '}
|
|
</BlueText>
|
|
<BlueText>{loc.formatString(loc.multisig.fee_btc, { number: currency.satoshiToBTC(getFee()) })}</BlueText>
|
|
</View>
|
|
</View>
|
|
<View style={styles.marginConfirmButton}>
|
|
<BlueButton disabled={!isConfirmEnabled()} title={loc.multisig.confirm} onPress={onConfirm} testID="PsbtMultisigConfirmButton" />
|
|
</View>
|
|
</>
|
|
);
|
|
|
|
const onLayout = e => {
|
|
setFlatListHeight(e.nativeEvent.layout.height);
|
|
};
|
|
|
|
return (
|
|
<SafeBlueArea style={stylesHook.root}>
|
|
<View style={styles.container}>
|
|
<View style={styles.mstopcontainer}>
|
|
<View style={styles.mscontainer}>
|
|
<View style={[styles.msleft, { height: flatListHeight - 130 }]} />
|
|
</View>
|
|
<View style={styles.msright}>
|
|
<BlueCard>
|
|
<FlatList
|
|
data={data}
|
|
onLayout={onLayout}
|
|
renderItem={_renderItem}
|
|
keyExtractor={(_item, index) => `${index}`}
|
|
ListHeaderComponent={header}
|
|
scrollEnabled={false}
|
|
/>
|
|
{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>
|
|
{footer}
|
|
</View>
|
|
</SafeBlueArea>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
mstopcontainer: {
|
|
flex: 1,
|
|
flexDirection: 'row',
|
|
},
|
|
mscontainer: {
|
|
flex: 10,
|
|
},
|
|
msleft: {
|
|
width: 1,
|
|
borderStyle: 'dashed',
|
|
borderWidth: 0.8,
|
|
borderColor: '#c4c4c4',
|
|
marginLeft: 40,
|
|
marginTop: 130,
|
|
},
|
|
msright: {
|
|
flex: 90,
|
|
marginLeft: '-11%',
|
|
},
|
|
scrollViewContent: {
|
|
flexGrow: 1,
|
|
justifyContent: 'space-between',
|
|
},
|
|
container: {
|
|
flexDirection: 'column',
|
|
paddingTop: 24,
|
|
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',
|
|
},
|
|
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 },
|
|
textDestinationSpacingRight: { marginRight: 4 },
|
|
textDestinationSpacingLeft: { marginLeft: 4 },
|
|
vaultKeyTextSigned: { fontSize: 18, fontWeight: 'bold' },
|
|
vaultKeyTextSignedWrapper: { justifyContent: 'center', alignItems: 'center', paddingLeft: 16 },
|
|
flexDirectionRow: { flexDirection: 'row', paddingVertical: 12 },
|
|
textBtcUnit: { justifyContent: 'flex-end' },
|
|
bottomFeesWrapper: { justifyContent: 'center', alignItems: 'center', flexDirection: 'row' },
|
|
bottomWrapper: { marginTop: 16 },
|
|
marginConfirmButton: { marginTop: 16, marginHorizontal: 32, marginBottom: 48 },
|
|
height80: {
|
|
height: 80,
|
|
},
|
|
});
|
|
|
|
PsbtMultisig.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.multisig.header }));
|
|
|
|
export default PsbtMultisig;
|