2019-07-13 17:20:38 +02:00
/* global alert */
import React , { Component } from 'react' ;
2020-06-09 23:02:25 +02:00
import { ActivityIndicator , View , TextInput , TouchableOpacity , Linking , ScrollView , StyleSheet } from 'react-native' ;
import Clipboard from '@react-native-community/clipboard' ;
2019-08-27 06:05:27 +02:00
import {
BlueSpacing20 ,
BlueReplaceFeeSuggestions ,
BlueButton ,
SafeBlueArea ,
BlueCard ,
BlueText ,
BlueSpacing ,
BlueNavigationStyle ,
2020-04-28 18:27:35 +02:00
BlueBigCheckmark ,
2019-08-27 06:05:27 +02:00
} from '../../BlueComponents' ;
2019-07-13 17:20:38 +02:00
import PropTypes from 'prop-types' ;
import { HDSegwitBech32Transaction , HDSegwitBech32Wallet } from '../../class' ;
2020-04-28 18:27:35 +02:00
import { Text } from 'react-native-elements' ;
2019-07-13 17:20:38 +02:00
import ReactNativeHapticFeedback from 'react-native-haptic-feedback' ;
/** @type {AppStorage} */
2020-06-01 14:54:23 +02:00
const EV = require ( '../../events' ) ;
const BlueElectrum = require ( '../../BlueElectrum' ) ;
const loc = require ( '../../loc' ) ;
2019-07-14 13:40:31 +02:00
/** @type {AppStorage} */
2020-06-01 14:54:23 +02:00
const BlueApp = require ( '../../BlueApp' ) ;
2019-07-13 17:20:38 +02:00
2020-05-24 11:17:26 +02:00
const styles = StyleSheet . create ( {
root : {
flex : 1 ,
paddingTop : 20 ,
} ,
explain : {
flex : 1 ,
paddingBottom : 16 ,
} ,
center : {
alignItems : 'center' ,
flex : 1 ,
} ,
hex : {
color : '#0c2550' ,
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 ,
} ,
} ) ;
2019-07-13 17:20:38 +02:00
export default class CPFP extends Component {
static navigationOptions = ( ) => ( {
... BlueNavigationStyle ( null , false ) ,
title : 'Bump fee (CPFP)' ,
} ) ;
constructor ( props ) {
super ( props ) ;
let txid ;
let wallet ;
2020-05-27 13:12:17 +02:00
if ( props . route . params ) txid = props . route . params . txid ;
if ( props . route . params ) wallet = props . route . params . wallet ;
2019-07-13 17:20:38 +02:00
this . state = {
isLoading : true ,
stage : 1 ,
txid ,
wallet ,
} ;
}
broadcast ( ) {
this . setState ( { isLoading : true } , async ( ) => {
try {
await BlueElectrum . ping ( ) ;
await BlueElectrum . waitTillConnected ( ) ;
2020-06-01 14:54:23 +02:00
const result = await this . state . wallet . broadcastTx ( this . state . txhex ) ;
2019-07-13 17:20:38 +02:00
if ( result ) {
EV ( EV . enum . REMOTE _TRANSACTIONS _COUNT _CHANGED ) ; // someone should fetch txs
2019-07-14 13:40:31 +02:00
this . setState ( { stage : 3 , isLoading : false } ) ;
this . onSuccessBroadcast ( ) ;
2019-07-13 17:20:38 +02:00
} else {
ReactNativeHapticFeedback . trigger ( 'notificationError' , { ignoreAndroidSystemSettings : false } ) ;
this . setState ( { isLoading : false } ) ;
alert ( 'Broadcast failed' ) ;
}
} catch ( error ) {
ReactNativeHapticFeedback . trigger ( 'notificationError' , { ignoreAndroidSystemSettings : false } ) ;
this . setState ( { isLoading : false } ) ;
alert ( error . message ) ;
}
} ) ;
}
2019-07-14 13:40:31 +02:00
onSuccessBroadcast ( ) {
BlueApp . tx _metadata [ this . state . newTxid ] = { memo : 'Child pays for parent (CPFP)' } ;
}
2019-07-13 17:20:38 +02:00
async componentDidMount ( ) {
console . log ( 'transactions/CPFP - componentDidMount' ) ;
this . setState ( {
isLoading : true ,
newFeeRate : '' ,
nonReplaceable : false ,
} ) ;
await this . checkPossibilityOfCPFP ( ) ;
}
async checkPossibilityOfCPFP ( ) {
if ( this . state . wallet . type !== HDSegwitBech32Wallet . type ) {
return this . setState ( { nonReplaceable : true , isLoading : false } ) ;
}
2020-06-01 14:54:23 +02:00
const tx = new HDSegwitBech32Transaction ( null , this . state . txid , this . state . wallet ) ;
2019-07-14 13:40:31 +02:00
if ( ( await tx . isToUsTransaction ( ) ) && ( await tx . getRemoteConfirmationsNum ( ) ) === 0 ) {
2020-06-01 14:54:23 +02:00
const info = await tx . getInfo ( ) ;
2019-07-14 13:40:31 +02:00
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
2019-07-13 17:20:38 +02:00
} 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 {
2020-06-01 14:54:23 +02:00
const { tx : newTx } = await tx . createCPFPbumpFee ( newFeeRate ) ;
2019-07-14 13:40:31 +02:00
this . setState ( { stage : 2 , txhex : newTx . toHex ( ) , newTxid : newTx . getId ( ) } ) ;
2019-07-13 17:20:38 +02:00
this . setState ( { isLoading : false } ) ;
} catch ( _ ) {
this . setState ( { isLoading : false } ) ;
2019-07-14 13:40:31 +02:00
alert ( 'Failed: ' + _ . message ) ;
2019-07-13 17:20:38 +02:00
}
}
}
render ( ) {
if ( this . state . isLoading ) {
return (
2020-05-24 11:17:26 +02:00
< View style = { styles . root } >
2019-07-13 17:20:38 +02:00
< ActivityIndicator / >
< / V i e w >
) ;
}
if ( this . state . stage === 3 ) {
2019-07-14 13:40:31 +02:00
return this . renderStage3 ( ) ;
2019-07-13 17:20:38 +02:00
}
if ( this . state . stage === 2 ) {
2019-07-14 13:40:31 +02:00
return this . renderStage2 ( ) ;
2019-07-13 17:20:38 +02:00
}
if ( this . state . nonReplaceable ) {
return (
2020-05-24 11:17:26 +02:00
< SafeBlueArea style = { styles . root } >
2019-07-13 17:20:38 +02:00
< BlueSpacing20 / >
< BlueSpacing20 / >
< BlueSpacing20 / >
< BlueSpacing20 / >
< BlueSpacing20 / >
< BlueText h4 > This transaction is not bumpable < / B l u e T e x t >
< / S a f e B l u e A r e a >
) ;
}
2020-05-15 12:41:57 +02:00
return (
2020-05-24 11:17:26 +02:00
< SafeBlueArea style = { styles . explain } >
2020-05-15 13:07:08 +02:00
< ScrollView >
{ this . renderStage1 (
'We will create another transaction that spends your unconfirmed transaction. The total fee will be higher than the original transaction fee, so it should be mined faster. This is called CPFP - Child Pays For Parent.' ,
) }
2020-05-15 12:41:57 +02:00
< / S c r o l l V i e w >
< / S a f e B l u e A r e a >
2020-05-15 13:07:08 +02:00
) ;
}
2019-07-14 13:40:31 +02:00
renderStage2 ( ) {
return (
2020-05-24 11:17:26 +02:00
< View style = { styles . root } >
< BlueCard style = { styles . center } >
< BlueText style = { styles . hex } > { loc . send . create . this _is _hex } < / B l u e T e x t >
< TextInput style = { styles . hexInput } height = { 112 } multiline editable value = { this . state . txhex } / >
2019-07-14 13:40:31 +02:00
2020-05-24 11:17:26 +02:00
< TouchableOpacity style = { styles . action } onPress = { ( ) => Clipboard . setString ( this . state . txhex ) } >
< Text style = { styles . actionText } > Copy and broadcast later < / T e x t >
2019-07-14 13:40:31 +02:00
< / T o u c h a b l e O p a c i t y >
2020-05-24 11:17:26 +02:00
< TouchableOpacity style = { styles . action } onPress = { ( ) => Linking . openURL ( 'https://coinb.in/?verify=' + this . state . txhex ) } >
< Text style = { styles . actionText } > Verify on coinb . in < / T e x t >
2019-07-14 13:40:31 +02:00
< / T o u c h a b l e O p a c i t y >
< BlueButton onPress = { ( ) => this . broadcast ( ) } title = { loc . send . confirm . sendNow } / >
< / B l u e C a r d >
< / V i e w >
) ;
}
renderStage3 ( ) {
return (
2020-05-24 11:17:26 +02:00
< SafeBlueArea style = { styles . doneWrap } >
< BlueCard style = { styles . center } >
< View style = { styles . doneCard } / >
2019-07-14 13:40:31 +02:00
< / B l u e C a r d >
2020-05-24 11:17:26 +02:00
< BlueBigCheckmark style = { styles . blueBigCheckmark } / >
2019-07-14 13:40:31 +02:00
< BlueCard >
< BlueButton
onPress = { ( ) => {
this . props . navigation . popToTop ( ) ;
} }
title = { loc . send . success . done }
/ >
< / B l u e C a r d >
< / S a f e B l u e A r e a >
) ;
}
renderStage1 ( text ) {
2019-07-13 17:20:38 +02:00
return (
2020-05-24 11:17:26 +02:00
< SafeBlueArea style = { styles . root } >
2019-07-13 17:20:38 +02:00
< BlueSpacing / >
2020-05-24 11:17:26 +02:00
< BlueCard style = { styles . center } >
2019-07-14 13:40:31 +02:00
< BlueText > { text } < / B l u e T e x t >
2019-07-13 17:20:38 +02:00
< BlueSpacing20 / >
2019-08-27 06:05:27 +02:00
< BlueReplaceFeeSuggestions onFeeSelected = { fee => this . setState ( { newFeeRate : fee } ) } transactionMinimum = { this . state . feeRate } / >
2019-07-13 17:20:38 +02:00
< BlueSpacing / >
2019-08-27 06:05:27 +02:00
< BlueButton disabled = { this . state . newFeeRate <= this . state . feeRate } onPress = { ( ) => this . createTransaction ( ) } title = "Create" / >
2019-07-13 17:20:38 +02:00
< / B l u e C a r d >
< / S a f e B l u e A r e a >
) ;
}
}
CPFP . propTypes = {
navigation : PropTypes . shape ( {
popToTop : PropTypes . func ,
navigate : PropTypes . func ,
2020-05-27 13:12:17 +02:00
} ) ,
route : PropTypes . shape ( {
params : PropTypes . shape ( {
txid : PropTypes . string ,
wallet : PropTypes . object ,
2019-07-13 17:20:38 +02:00
} ) ,
} ) ,
} ;