import React, { useEffect, useState } from 'react';
import { useRoute } from '@react-navigation/native';
import BigNumber from 'bignumber.js';
import * as bitcoin from 'bitcoinjs-lib';
import { FlatList, StyleSheet, Text, TouchableOpacity, View, LayoutAnimation } from 'react-native';
import { Icon } from '@rneui/themed';
import { satoshiToBTC, satoshiToLocalCurrency } from '../../blue_modules/currency';
import { BlueCard, BlueText } from '../../BlueComponents';
import presentAlert from '../../components/Alert';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { useStorage } from '../../hooks/context/useStorage';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { combinePSBTs } from '../../utils/combinePSBTs';
const PsbtMultisig = () => {
const { wallets } = useStorage();
const { navigate, setParams } = useExtendedNavigation();
const { colors } = useTheme();
const [flatListHeight, setFlatListHeight] = useState(0);
const { walletID, psbtBase64, memo, receivedPSBTBase64, launchedBy } = useRoute().params;
/** @type MultisigHDWallet */
const wallet = wallets.find(w => w.getID() === walletID);
const [psbt, setPsbt] = useState(() => {
try {
const initial = bitcoin.Psbt.fromBase64(psbtBase64);
return initial;
} catch (error) {
console.error('Error loading initial PSBT:', error);
presentAlert({ message: loc.send.invalid_psbt });
return null;
}
});
useEffect(() => {
if (receivedPSBTBase64) {
_combinePSBT();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [receivedPSBTBase64]);
const data = new Array(wallet.getM());
const stylesHook = StyleSheet.create({
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,
},
vaultKeyText: {
color: colors.alternativeTextColor,
},
feeFiatText: {
color: colors.alternativeTextColor,
},
vaultKeyCircleSuccess: {
backgroundColor: colors.msSuccessBG,
},
vaultKeyTextSigned: {
color: colors.msSuccessBG,
},
});
const [isFiltered, setIsFiltered] = useState(true);
if (!psbt) return null;
// 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 });
}
}
return { addresses, totalSat, targets };
};
const filteredData = getDestinationData(true);
const unfilteredData = getDestinationData(false);
const targets = filteredData.targets;
const displayData = isFiltered ? filteredData : unfilteredData;
const displayTotalBtc = new BigNumber(displayData.totalSat).dividedBy(100000000).toNumber();
const displayTotalFiat = satoshiToLocalCurrency(displayData.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: psbt.toBase64(), isShowOpenScanner: isConfirmEnabled() });
};
const _renderItemUnsigned = el => {
const renderProvideSignature = el.index === howManySignaturesWeHave;
return (
{el.index + 1}
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
{renderProvideSignature && (
{loc.multisig.provide_signature}
)}
);
};
const _renderItemSigned = el => {
return (
{loc.formatString(loc.multisig.vault_key, { number: el.index + 1 })}
);
};
const _combinePSBT = () => {
if (receivedPSBTBase64 && receivedPSBTBase64 !== psbt.toBase64()) {
try {
const combined = combinePSBTs({ psbtBase64: psbt.toBase64(), newPSBTBase64: receivedPSBTBase64 });
setPsbt(combined);
setParams({ receivedPSBTBase64: undefined });
} catch (error) {
console.error('Error during PSBT combination:', error);
presentAlert({ message: error.message });
}
}
};
const onConfirm = () => {
try {
psbt.finalizeAllInputs();
} catch (err) {
console.warn('Finalize error (ignored if already finalized):', err);
}
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;
}
try {
const tx = psbt.extractTransaction().toHex();
const satoshiPerByte = Math.round(getFee() / psbt.extractTransaction().virtualSize());
navigate('Confirm', {
fee: new BigNumber(getFee()).dividedBy(100000000).toNumber(),
memo,
walletID,
tx,
recipients: targets,
satoshiPerByte,
});
} catch (error) {
presentAlert({ message: error });
}
};
const howManySignaturesWeHave = wallet.calculateHowManySignaturesWeHaveFromPsbt(psbt);
const isConfirmEnabled = () => {
return howManySignaturesWeHave >= wallet.getM();
};
const destinationAddress = (useFilter = true) => {
const addrs = useFilter ? filteredData.addresses : unfilteredData.addresses;
const displayAddrs = useFilter ? addrs : [...new Set(addrs)];
const destinationAddressView = [];
const whitespace = '_';
const destinations = Object.entries(displayAddrs);
for (const [index, address] of destinations) {
if (index > 1) {
destinationAddressView.push(
and {destinations.length - 2} more...
,
);
break;
} else {
const currentAddress = address;
const firstFour = currentAddress.substring(0, 5);
const lastFour = currentAddress.substring(currentAddress.length - 5);
const middle = currentAddress.length > 10 ? currentAddress.slice(5, currentAddress.length - 5) : '';
destinationAddressView.push(
{firstFour}
{whitespace}
{middle}
{whitespace}
{lastFour}
,
);
}
}
return destinationAddressView;
};
const handleToggleFilter = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setIsFiltered(prev => !prev);
};
const header = (
{displayTotalBtc}
{' '}
{BitcoinUnit.BTC}
{displayTotalFiat}
{destinationAddress(isFiltered)}
);
const footer = null;
const onLayout = event => {
const newHeight = event.nativeEvent.layout.height;
setFlatListHeight(newHeight);
};
return (
`${index}`}
extraData={psbt} // Ensure FlatList updates when psbt changes
ListHeaderComponent={header}
ListFooterComponent={footer}
onLayout={onLayout}
/>
{isConfirmEnabled() && (
{
navigateToPSBTMultisigQRCode();
}}
>
{loc.multisig.export_signed_psbt}
)}
{loc.formatString(loc.multisig.fee, { number: satoshiToLocalCurrency(getFee()) })} -{' '}
{loc.formatString(loc.multisig.fee_btc, { number: satoshiToBTC(getFee()) })}
);
};
const styles = StyleSheet.create({
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,
marginVertical: 8,
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 },
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 },
height80: {
height: 80,
},
flexColumnSpaceBetween: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
},
flexConfirm: {
paddingHorizontal: 32,
paddingVertical: 16,
},
feeConfirmContainer: {
paddingHorizontal: 32,
paddingVertical: 16,
},
feeContainer: {
marginBottom: 8,
},
});
export default PsbtMultisig;