/* global alert */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ActivityIndicator, Platform, View, TextInput, TouchableOpacity, Linking, ScrollView, StyleSheet, KeyboardAvoidingView, } from 'react-native'; import Clipboard from '@react-native-clipboard/clipboard'; import { Text } from 'react-native-elements'; import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import { BlueButton, BlueCard, BlueReplaceFeeSuggestions, BlueSpacing, BlueSpacing20, BlueText, SafeBlueArea } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import { BlueCurrentTheme } from '../../components/themes'; import { HDSegwitBech32Transaction, HDSegwitBech32Wallet } from '../../class'; import loc from '../../loc'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import Notifications from '../../blue_modules/notifications'; const BlueElectrum = require('../../blue_modules/BlueElectrum'); const styles = StyleSheet.create({ root: { flex: 1, paddingTop: 20, }, explain: { paddingBottom: 16, }, center: { alignItems: 'center', flex: 1, }, hex: { color: BlueCurrentTheme.colors.buttonAlternativeTextColor, fontWeight: '500', }, hexInput: { borderColor: '#ebebeb', backgroundColor: '#d2f8d6', borderRadius: 4, marginTop: 20, color: '#37c0a1', fontWeight: '500', fontSize: 14, paddingHorizontal: 16, paddingBottom: 16, paddingTop: 16, }, action: { marginVertical: 24, }, actionText: { color: '#9aa0aa', fontSize: 15, fontWeight: '500', alignSelf: 'center', }, doneWrap: { flex: 1, paddingTop: 19, }, doneCard: { flexDirection: 'row', justifyContent: 'center', paddingTop: 76, paddingBottom: 16, }, blueBigCheckmark: { marginTop: 43, marginBottom: 53, }, }); export default class CPFP extends Component { static contextType = BlueStorageContext; constructor(props) { super(props); let txid; let wallet; if (props.route.params) txid = props.route.params.txid; if (props.route.params) wallet = props.route.params.wallet; this.state = { isLoading: true, stage: 1, txid, wallet, }; } broadcast() { this.setState({ isLoading: true }, async () => { try { await BlueElectrum.ping(); await BlueElectrum.waitTillConnected(); const result = await this.state.wallet.broadcastTx(this.state.txhex); if (result) { this.onSuccessBroadcast(); } else { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); this.setState({ isLoading: false }); alert(loc.errors.broadcast); } } catch (error) { ReactNativeHapticFeedback.trigger('notificationError', { ignoreAndroidSystemSettings: false }); this.setState({ isLoading: false }); alert(error.message); } }); } onSuccessBroadcast() { this.context.txMetadata[this.state.newTxid] = { memo: 'Child pays for parent (CPFP)' }; Notifications.majorTomToGroundControl([], [], [this.state.newTxid]); this.context.sleep(4000).then(() => this.context.fetchAndSaveWalletTransactions(this.state.wallet.getID())); this.props.navigation.navigate('Success', { onDonePressed: () => this.props.navigation.popToTop(), amount: undefined }); } async componentDidMount() { console.log('transactions/CPFP - componentDidMount'); this.setState({ isLoading: true, newFeeRate: '', nonReplaceable: false, }); try { await this.checkPossibilityOfCPFP(); } catch (_) { // if anything goes wrong we just show "this is not bumpable" message this.setState({ nonReplaceable: true, isLoading: false }); } } async checkPossibilityOfCPFP() { if (this.state.wallet.type !== HDSegwitBech32Wallet.type) { return this.setState({ nonReplaceable: true, isLoading: false }); } const tx = new HDSegwitBech32Transaction(null, this.state.txid, this.state.wallet); if ((await tx.isToUsTransaction()) && (await tx.getRemoteConfirmationsNum()) === 0) { const info = await tx.getInfo(); return this.setState({ nonReplaceable: false, feeRate: info.feeRate + 1, isLoading: false, tx }); // 1 sat makes a lot of difference, since sometimes because of rounding created tx's fee might be insufficient } else { return this.setState({ nonReplaceable: true, isLoading: false }); } } async createTransaction() { const newFeeRate = parseInt(this.state.newFeeRate); if (newFeeRate > this.state.feeRate) { /** @type {HDSegwitBech32Transaction} */ const tx = this.state.tx; this.setState({ isLoading: true }); try { const { tx: newTx } = await tx.createCPFPbumpFee(newFeeRate); this.setState({ stage: 2, txhex: newTx.toHex(), newTxid: newTx.getId() }); this.setState({ isLoading: false }); } catch (_) { this.setState({ isLoading: false }); alert(loc.errors.error + ': ' + _.message); } } } renderStage1(text) { return ( {text} this.setState({ newFeeRate: fee })} transactionMinimum={this.state.feeRate} /> this.createTransaction()} title={loc.transactions.cpfp_create} /> ); } renderStage2() { return ( {loc.send.create_this_is_hex} Clipboard.setString(this.state.txhex)}> {loc.send.create_copy} Linking.openURL('https://coinb.in/?verify=' + this.state.txhex)}> {loc.send.create_verify} this.broadcast()} title={loc.send.confirm_sendNow} /> ); } render() { if (this.state.isLoading) { return ( ); } if (this.state.stage === 2) { return this.renderStage2(); } if (this.state.nonReplaceable) { return ( {loc.transactions.cpfp_no_bump} ); } return ( {this.renderStage1(loc.transactions.cpfp_exp)} ); } } CPFP.propTypes = { navigation: PropTypes.shape({ popToTop: PropTypes.func, navigate: PropTypes.func, }), route: PropTypes.shape({ params: PropTypes.shape({ txid: PropTypes.string, wallet: PropTypes.object, }), }), }; CPFP.navigationOptions = navigationStyle({}, opts => ({ ...opts, title: loc.transactions.cpfp_title }));