BlueWallet/screen/send/psbtWithHardwareWallet.js

362 lines
12 KiB
JavaScript
Raw Normal View History

2019-09-27 16:49:56 +02:00
/* global alert */
import React, { Component } from 'react';
2020-01-01 04:31:04 +01:00
import {
ActivityIndicator,
TouchableOpacity,
ScrollView,
View,
Dimensions,
Image,
TextInput,
Clipboard,
Linking,
Platform,
2020-01-15 05:49:57 +01:00
PermissionsAndroid,
2020-01-01 04:31:04 +01:00
} from 'react-native';
2019-09-27 16:49:56 +02:00
import QRCode from 'react-native-qrcode-svg';
2020-04-28 18:27:35 +02:00
import { Text } from 'react-native-elements';
2019-09-27 16:49:56 +02:00
import {
BlueButton,
BlueText,
SafeBlueArea,
BlueCard,
BlueNavigationStyle,
BlueSpacing20,
BlueCopyToClipboardButton,
2020-04-28 18:27:35 +02:00
BlueBigCheckmark,
2019-09-27 16:49:56 +02:00
} from '../../BlueComponents';
import PropTypes from 'prop-types';
2020-01-01 04:31:04 +01:00
import Share from 'react-native-share';
2019-09-27 16:49:56 +02:00
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import { RNCamera } from 'react-native-camera';
2020-01-01 04:31:04 +01:00
import RNFS from 'react-native-fs';
import DocumentPicker from 'react-native-document-picker';
2019-09-27 16:49:56 +02:00
let loc = require('../../loc');
let EV = require('../../events');
let BlueElectrum = require('../../BlueElectrum');
/** @type {AppStorage} */
const BlueApp = require('../../BlueApp');
const bitcoin = require('bitcoinjs-lib');
const { height, width } = Dimensions.get('window');
export default class PsbtWithHardwareWallet extends Component {
static navigationOptions = () => ({
...BlueNavigationStyle(null, false),
title: loc.send.header,
});
cameraRef = null;
onBarCodeRead = ret => {
2020-02-26 15:39:19 +01:00
if (RNCamera.Constants.CameraStatus === RNCamera.Constants.CameraStatus.READY) this.cameraRef.pausePreview();
2020-02-24 22:45:14 +01:00
if (ret.data.indexOf('+') === -1 && ret.data.indexOf('=') === -1 && ret.data.indexOf('=') === -1) {
// this looks like NOT base64, so maybe its transaction's hex
2020-02-26 15:39:19 +01:00
this.setState({ renderScanner: false, txhex: ret.data });
2020-02-24 22:45:14 +01:00
return;
}
2019-09-27 16:49:56 +02:00
this.setState({ renderScanner: false }, () => {
try {
2020-02-24 22:45:14 +01:00
let Tx = this.state.fromWallet.combinePsbt(
this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64(),
ret.data,
);
2019-09-27 16:49:56 +02:00
this.setState({ txhex: Tx.toHex() });
} catch (Err) {
alert(Err);
}
});
};
constructor(props) {
super(props);
this.state = {
isLoading: false,
renderScanner: false,
2020-01-01 04:31:04 +01:00
qrCodeHeight: height > width ? width - 40 : width / 3,
2020-05-27 13:12:17 +02:00
memo: props.route.params.memo,
psbt: props.route.params.psbt,
fromWallet: props.route.params.fromWallet,
isFirstPSBTAlreadyBase64: props.route.params.isFirstPSBTAlreadyBase64,
2020-01-01 04:31:04 +01:00
isSecondPSBTAlreadyBase64: false,
2020-01-03 05:02:41 +01:00
deepLinkPSBT: undefined,
2020-05-27 13:12:17 +02:00
txhex: props.route.params.txhex || undefined,
2019-09-27 16:49:56 +02:00
};
2020-01-15 05:49:57 +01:00
this.fileName = `${Date.now()}.psbt`;
2019-09-27 16:49:56 +02:00
}
2020-01-03 05:02:41 +01:00
static getDerivedStateFromProps(nextProps, prevState) {
2020-05-27 13:12:17 +02:00
const deepLinkPSBT = nextProps.props.route.params.deepLinkPSBT;
const txhex = nextProps.props.route.params.txhex;
2020-01-03 05:02:41 +01:00
if (deepLinkPSBT) {
try {
let Tx = prevState.fromWallet.combinePsbt(
prevState.isFirstPSBTAlreadyBase64 ? prevState.psbt : prevState.psbt.toBase64(),
deepLinkPSBT,
);
return {
...prevState,
txhex: Tx.toHex(),
};
} catch (Err) {
alert(Err);
}
2020-01-04 04:12:29 +01:00
} else if (txhex) {
return {
...prevState,
txhex: txhex,
};
2020-01-03 05:02:41 +01:00
}
return prevState;
}
2020-01-01 04:31:04 +01:00
componentDidMount() {
2019-09-27 16:49:56 +02:00
console.log('send/psbtWithHardwareWallet - componentDidMount');
}
broadcast = () => {
2019-09-27 16:49:56 +02:00
this.setState({ isLoading: true }, async () => {
try {
await BlueElectrum.ping();
await BlueElectrum.waitTillConnected();
let result = await this.state.fromWallet.broadcastTx(this.state.txhex);
if (result) {
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
this.setState({ success: true, isLoading: false });
if (this.state.memo) {
let txDecoded = bitcoin.Transaction.fromHex(this.state.txhex);
const txid = txDecoded.getId();
BlueApp.tx_metadata[txid] = { memo: this.state.memo };
}
} 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-09-27 16:49:56 +02:00
_renderScanner() {
return (
<SafeBlueArea style={{ flex: 1 }}>
<RNCamera
captureAudio={false}
androidCameraPermissionOptions={{
title: 'Permission to use camera',
message: 'We need your permission to use your camera',
buttonPositive: 'OK',
buttonNegative: 'Cancel',
}}
ref={ref => (this.cameraRef = ref)}
style={{ flex: 1, justifyContent: 'space-between' }}
onBarCodeRead={this.onBarCodeRead}
barCodeTypes={[RNCamera.Constants.BarCodeType.qr]}
/>
<TouchableOpacity
style={{
width: 40,
height: 40,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
borderRadius: 20,
position: 'absolute',
right: 16,
top: 64,
}}
onPress={() => this.setState({ renderScanner: false })}
>
<Image style={{ alignSelf: 'center' }} source={require('../../img/close-white.png')} />
</TouchableOpacity>
</SafeBlueArea>
);
}
_renderSuccess() {
return (
<SafeBlueArea style={{ flex: 1 }}>
2020-04-28 18:27:35 +02:00
<BlueBigCheckmark style={{ marginTop: 143, marginBottom: 53 }} />
2019-09-27 16:49:56 +02:00
<BlueCard>
2020-05-27 13:12:17 +02:00
<BlueButton onPress={this.props.navigation.dangerouslyGetParent().pop} title={loc.send.success.done} />
2019-09-27 16:49:56 +02:00
</BlueCard>
</SafeBlueArea>
);
}
_renderBroadcastHex() {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
<BlueText style={{ color: '#0c2550', fontWeight: '500' }}>{loc.send.create.this_is_hex}</BlueText>
<TextInput
style={{
borderColor: '#ebebeb',
backgroundColor: '#d2f8d6',
borderRadius: 4,
marginTop: 20,
color: '#37c0a1',
fontWeight: '500',
fontSize: 14,
paddingHorizontal: 16,
paddingBottom: 16,
paddingTop: 16,
}}
height={112}
multiline
editable
value={this.state.txhex}
/>
<TouchableOpacity style={{ marginVertical: 24 }} onPress={() => Clipboard.setString(this.state.txhex)}>
<Text style={{ color: '#9aa0aa', fontSize: 15, fontWeight: '500', alignSelf: 'center' }}>Copy and broadcast later</Text>
</TouchableOpacity>
<TouchableOpacity style={{ marginVertical: 24 }} onPress={() => Linking.openURL('https://coinb.in/?verify=' + this.state.txhex)}>
<Text style={{ color: '#9aa0aa', fontSize: 15, fontWeight: '500', alignSelf: 'center' }}>Verify on coinb.in</Text>
</TouchableOpacity>
<BlueSpacing20 />
<BlueButton onPress={this.broadcast} title={loc.send.confirm.sendNow} />
</BlueCard>
</View>
);
}
2020-01-01 04:31:04 +01:00
exportPSBT = async () => {
if (Platform.OS === 'ios') {
2020-01-15 05:49:57 +01:00
const filePath = RNFS.TemporaryDirectoryPath + `/${this.fileName}`;
2020-01-03 05:02:41 +01:00
await RNFS.writeFile(filePath, this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64());
2020-01-01 04:31:04 +01:00
Share.open({
url: 'file://' + filePath,
})
.catch(error => console.log(error))
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
2020-01-15 05:49:57 +01:00
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
title: 'BlueWallet Storage Access Permission',
message: 'BlueWallet needs your permission to access your storage to save this transaction.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
});
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
console.log('Storage Permission: Granted');
const filePath = RNFS.ExternalCachesDirectoryPath + `/${this.fileName}`;
await RNFS.writeFile(filePath, this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64());
alert(`This transaction has been saved in ${filePath}`);
} else {
console.log('Storage Permission: Denied');
}
2020-01-01 04:31:04 +01:00
}
};
openSignedTransaction = async () => {
try {
2020-02-24 22:45:14 +01:00
const res = await DocumentPicker.pick({
type: Platform.OS === 'ios' ? ['io.bluewallet.psbt', 'io.bluewallt.psbt.txn'] : [DocumentPicker.types.allFiles],
});
2020-01-03 05:02:41 +01:00
const file = await RNFS.readFile(res.uri);
if (file) {
this.setState({ isSecondPSBTAlreadyBase64: true }, () => this.onBarCodeRead({ data: file }));
2020-01-01 04:31:04 +01:00
} else {
this.setState({ isSecondPSBTAlreadyBase64: false });
throw new Error();
}
} catch (err) {
if (!DocumentPicker.isCancel(err)) {
alert('The selected file does not contain a signed transaction that can be imported.');
}
}
};
2019-09-27 16:49:56 +02:00
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<ActivityIndicator />
</View>
);
}
if (this.state.success) return this._renderSuccess();
if (this.state.renderScanner) return this._renderScanner();
if (this.state.txhex) return this._renderBroadcastHex();
return (
<SafeBlueArea style={{ flex: 1 }}>
2020-01-01 04:31:04 +01:00
<ScrollView centerContent contentContainerStyle={{ flexGrow: 1, justifyContent: 'space-between' }}>
2019-09-27 16:49:56 +02:00
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
<BlueCard>
<BlueText testID={'TextHelperForPSBT'}>
This is partially signed bitcoin transaction (PSBT). Please finish signing it with your hardware wallet.
</BlueText>
2019-09-27 16:49:56 +02:00
<BlueSpacing20 />
<QRCode
2020-01-01 04:31:04 +01:00
value={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
2019-09-27 16:49:56 +02:00
size={this.state.qrCodeHeight}
color={BlueApp.settings.foregroundColor}
logoBackgroundColor={BlueApp.settings.brandingColor}
ecl={'L'}
/>
<BlueSpacing20 />
2020-01-01 04:31:04 +01:00
<BlueButton
icon={{
name: 'qrcode',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.setState({ renderScanner: true })}
title={'Scan Signed Transaction'}
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'file-import',
type: 'material-community',
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.openSignedTransaction}
title={'Open Signed Transaction'}
/>
<BlueSpacing20 />
<BlueButton
icon={{
name: 'share-alternative',
type: 'entypo',
color: BlueApp.settings.buttonTextColor,
}}
onPress={this.exportPSBT}
2020-02-24 22:45:14 +01:00
title={'Export to file'}
2020-01-01 04:31:04 +01:00
/>
2019-09-27 16:49:56 +02:00
<BlueSpacing20 />
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
2020-01-01 04:31:04 +01:00
<BlueCopyToClipboardButton
stringToCopy={this.state.isFirstPSBTAlreadyBase64 ? this.state.psbt : this.state.psbt.toBase64()}
displayText={'Copy to Clipboard'}
/>
2019-09-27 16:49:56 +02:00
</View>
</BlueCard>
</View>
2020-01-01 04:31:04 +01:00
</ScrollView>
2019-09-27 16:49:56 +02:00
</SafeBlueArea>
);
}
}
PsbtWithHardwareWallet.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
navigate: PropTypes.func,
2020-05-27 13:12:17 +02:00
dangerouslyGetParent: PropTypes.func,
}),
route: PropTypes.shape({
params: PropTypes.object,
2019-09-27 16:49:56 +02:00
}),
};