import React, { useContext, useEffect, useState } from 'react'; import { ActivityIndicator, FlatList, TouchableOpacity, StyleSheet, Switch, View } from 'react-native'; import { Text } from 'react-native-elements'; import { PayjoinClient } from 'payjoin-client'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import PropTypes from 'prop-types'; import PayjoinTransaction from '../../class/payjoin-transaction'; import { BlueText, SafeBlueArea, BlueCard } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import { BitcoinUnit } from '../../models/bitcoinUnits'; import Biometric from '../../class/biometrics'; import loc, { formatBalance, formatBalanceWithoutSuffix } from '../../loc'; import Notifications from '../../blue_modules/notifications'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import { Psbt } from 'bitcoinjs-lib'; import { useNavigation, useRoute } from '@react-navigation/native'; import alert from '../../components/Alert'; import { useTheme } from '../../components/themes'; import { isTorCapable } from '../../blue_modules/environment'; import Button from '../../components/Button'; const currency = require('../../blue_modules/currency'); const BlueElectrum = require('../../blue_modules/BlueElectrum'); const Bignumber = require('bignumber.js'); const bitcoin = require('bitcoinjs-lib'); const torrific = isTorCapable ? require('../../blue_modules/torrific') : require('../../scripts/maccatalystpatches/torrific.js'); const Confirm = () => { const { wallets, fetchAndSaveWalletTransactions, isElectrumDisabled, isTorDisabled } = useContext(BlueStorageContext); const [isBiometricUseCapableAndEnabled, setIsBiometricUseCapableAndEnabled] = useState(false); const { params } = useRoute(); const { recipients = [], walletID, fee, memo, tx, satoshiPerByte, psbt } = params; const [isLoading, setIsLoading] = useState(false); const [isPayjoinEnabled, setIsPayjoinEnabled] = useState(false); const wallet = wallets.find(w => w.getID() === walletID); const payjoinUrl = wallet.allowPayJoin() ? params.payjoinUrl : false; const feeSatoshi = new Bignumber(fee).multipliedBy(100000000).toNumber(); const { navigate, setOptions } = useNavigation(); const { colors } = useTheme(); const stylesHook = StyleSheet.create({ transactionDetailsTitle: { color: colors.foregroundColor, }, transactionDetailsSubtitle: { color: colors.feeText, }, transactionAmountFiat: { color: colors.feeText, }, txDetails: { backgroundColor: colors.lightButton, }, valueValue: { color: colors.alternativeTextColor2, }, valueUnit: { color: colors.buttonTextColor, }, root: { backgroundColor: colors.elevated, }, payjoinWrapper: { backgroundColor: colors.buttonDisabledBackgroundColor, }, }); useEffect(() => { console.log('send/confirm - useEffect'); console.log('address = ', recipients); Biometric.isBiometricUseCapableAndEnabled().then(setIsBiometricUseCapableAndEnabled); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { setOptions({ // eslint-disable-next-line react/no-unstable-nested-components headerRight: () => ( { if (isBiometricUseCapableAndEnabled) { if (!(await Biometric.unlockWithBiometrics())) { return; } } navigate('CreateTransaction', { fee, recipients, memo, tx, satoshiPerByte, wallet, feeSatoshi, }); }} > {loc.send.create_details} ), }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [colors, fee, feeSatoshi, isBiometricUseCapableAndEnabled, memo, recipients, satoshiPerByte, tx, wallet]); /** * we need to look into `recipients`, find destination address and return its outputScript * (needed for payjoin) * * @return {string} */ const getPaymentScript = () => { return bitcoin.address.toOutputScript(recipients[0].address); }; const send = async () => { setIsLoading(true); try { const txids2watch = []; if (!isPayjoinEnabled) { await broadcast(tx); } else { const payJoinWallet = new PayjoinTransaction(psbt, txHex => broadcast(txHex), wallet); const paymentScript = getPaymentScript(); let payjoinClient; if (!isTorDisabled && payjoinUrl.includes('.onion')) { console.warn('trying TOR....'); // working through TOR - crafting custom requester that will handle TOR http request const customPayjoinRequester = { requestPayjoin: async function (psbt2) { console.warn('requesting payjoin with psbt:', psbt2.toBase64()); const api = new torrific.Torsbee(); const torResponse = await api.post(payjoinUrl, { headers: { 'Content-Type': 'text/plain', }, body: psbt2.toBase64(), }); console.warn('got torResponse.body'); if (!torResponse.body) throw new Error('TOR failure, got ' + JSON.stringify(torResponse)); return Psbt.fromBase64(torResponse.body); }, }; payjoinClient = new PayjoinClient({ paymentScript, wallet: payJoinWallet, payjoinRequester: customPayjoinRequester, }); } else { payjoinClient = new PayjoinClient({ paymentScript, wallet: payJoinWallet, payjoinUrl, }); } await payjoinClient.run(); const payjoinPsbt = payJoinWallet.getPayjoinPsbt(); if (payjoinPsbt) { const tx2watch = payjoinPsbt.extractTransaction(); txids2watch.push(tx2watch.getId()); } } const txid = bitcoin.Transaction.fromHex(tx).getId(); txids2watch.push(txid); Notifications.majorTomToGroundControl([], [], txids2watch); let amount = 0; for (const recipient of recipients) { amount += recipient.value; } amount = formatBalanceWithoutSuffix(amount, BitcoinUnit.BTC, false); ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false }); navigate('Success', { fee: Number(fee), amount, }); setIsLoading(false); await new Promise(resolve => setTimeout(resolve, 3000)); // sleep to make sure network propagates fetchAndSaveWalletTransactions(walletID); } catch (error) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false, }); setIsLoading(false); alert(error.message); } }; const broadcast = async transaction => { await BlueElectrum.ping(); await BlueElectrum.waitTillConnected(); if (isBiometricUseCapableAndEnabled) { if (!(await Biometric.unlockWithBiometrics())) { return; } } const result = await wallet.broadcastTx(transaction); if (!result) { throw new Error(loc.errors.broadcast); } return result; }; const _renderItem = ({ index, item }) => { return ( <> {currency.satoshiToBTC(item.value)} {' ' + loc.units[BitcoinUnit.BTC]} {currency.satoshiToLocalCurrency(item.value)} {loc.send.create_to} {item.address} {recipients.length > 1 && ( {loc.formatString(loc._.of, { number: index + 1, total: recipients.length })} )} ); }; _renderItem.propTypes = { index: PropTypes.number.isRequired, item: PropTypes.object.isRequired, }; const renderSeparator = () => { return ; }; return ( 1} extraData={recipients} data={recipients} renderItem={_renderItem} keyExtractor={(_item, index) => `${index}`} ItemSeparatorComponent={renderSeparator} /> {!!payjoinUrl && ( Payjoin )} {loc.send.create_fee}: {formatBalance(feeSatoshi, BitcoinUnit.BTC)} ({currency.satoshiToLocalCurrency(feeSatoshi)}) {isLoading ? :