Merge pull request #14 from sansegkh/master

merged from master
This commit is contained in:
San Segkhoonthod 2019-02-10 14:59:41 +07:00 committed by GitHub
commit 2533a848ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 3963 additions and 2719 deletions

View file

@ -67,4 +67,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[version]
^0.85.0
^0.86.0

4
.gitignore vendored
View file

@ -54,3 +54,7 @@ buck-out/
# Bundle artifact
*.jsbundle
#BlueWallet
release-notes.json
release-notes.txt

View file

@ -7,6 +7,7 @@ let EV = require('./events');
let currency = require('./currency');
let loc = require('./loc');
let A = require('./analytics');
let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line
/** @type {AppStorage} */
let BlueApp = new AppStorage();
@ -31,7 +32,7 @@ async function startAndDecrypt(retry) {
let securityAlert = require('./security-alert');
await securityAlert.start();
// now, lets try to fetch balance and txs for first wallet if it is time for it
let hadToRefresh = false;
/* let hadToRefresh = false;
let noErr = true;
try {
let wallets = BlueApp.getWallets();
@ -57,7 +58,7 @@ async function startAndDecrypt(retry) {
if (hadToRefresh && noErr) {
await BlueApp.saveToDisk(); // caching
}
} */
}
if (!success && password) {

View file

@ -4,7 +4,7 @@
import React, { Component } from 'react';
import Ionicons from 'react-native-vector-icons/Ionicons';
import PropTypes from 'prop-types';
import { Icon, Button, FormLabel, FormInput, Text, Header, List, ListItem } from 'react-native-elements';
import { Icon, FormLabel, FormInput, Text, Header, List, ListItem } from 'react-native-elements';
import {
TouchableOpacity,
TouchableWithoutFeedback,
@ -15,10 +15,11 @@ import {
StyleSheet,
Dimensions,
Image,
Keyboard,
SafeAreaView,
InputAccessoryView,
Clipboard,
Platform,
LayoutAnimation,
TextInput,
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
@ -44,14 +45,19 @@ if (aspectRatio > 1.6) {
export class BlueButton extends Component {
render() {
const backgroundColor = this.props.disabled ? '#99a0ab' : '#ccddf9';
let backgroundColor = '#ccddf9';
let fontColor = '#0c2550';
if (this.props.hasOwnProperty('disabled') && this.props.disabled === true) {
backgroundColor = '#eef0f4';
fontColor = '#9aa0aa';
}
return (
<TouchableOpacity
style={{
flex: 1,
borderWidth: 0.7,
borderColor: 'transparent',
backgroundColor: this.props.hasOwnProperty('backgroundColor') ? this.props.backgroundColor : backgroundColor,
backgroundColor: backgroundColor,
minHeight: 45,
height: 45,
maxHeight: 45,
@ -64,7 +70,7 @@ export class BlueButton extends Component {
>
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
{this.props.icon && <Icon name={this.props.icon.name} type={this.props.icon.type} color={this.props.icon.color} />}
{this.props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: '#0c2550' }}>{this.props.title}</Text>}
{this.props.title && <Text style={{ marginHorizontal: 8, fontSize: 16, color: fontColor }}>{this.props.title}</Text>}
</View>
</TouchableOpacity>
);
@ -141,26 +147,18 @@ export class LightningButton extends Component {
export class BlueButtonLink extends Component {
render() {
// eslint-disable-next-line
this.props.buttonStyle = this.props.buttonStyle || {};
return (
<Button
activeOpacity={0.1}
delayPressIn={0}
{...this.props}
<TouchableOpacity
style={{
marginTop: 20,
borderWidth: 0.7,
borderColor: 'transparent',
minHeight: 60,
minWidth: 100,
height: 60,
justifyContent: 'center',
}}
buttonStyle={{
height: 45,
width: width / 1.5,
}}
backgroundColor="transparent"
color="#0c2550"
/>
{...this.props}
>
<Text style={{ color: '#0c2550', textAlign: 'center', fontSize: 16 }}>{this.props.title}</Text>
</TouchableOpacity>
);
}
}
@ -204,36 +202,32 @@ export class BlueCopyTextToClipboard extends Component {
text: '',
};
state = { hasTappedText: false };
constructor() {
super();
if (Platform.OS === 'android') UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
constructor(props) {
super(props);
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}
this.state = { hasTappedText: false, address: props.text };
}
copyToClipboard = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring, () => {
this.setState({ hasTappedText: true }, () => {
Clipboard.setString(this.props.text);
setTimeout(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
this.setState({ hasTappedText: false });
}, 1000);
this.setState({ address: loc.wallets.xpub.copiedToClipboard }, () => {
setTimeout(() => {
this.setState({ hasTappedText: false, address: this.props.text });
}, 1000);
});
});
this.setState({ hasTappedText: true });
};
render() {
return (
<View style={{ justifyContent: 'center', alignItems: 'center', paddingHorizontal: 16 }}>
<TouchableOpacity onPress={this.copyToClipboard} disabled={this.state.hasTappedText}>
<Text style={styleCopyTextToClipboard.address} numberOfLines={0}>
{this.props.text}
</Text>
{this.state.hasTappedText && (
<Text style={styleCopyTextToClipboard.address} numberOfLines={0}>
{loc.wallets.xpub.copiedToClipboard}
</Text>
)}
<Animated.Text style={styleCopyTextToClipboard.address} numberOfLines={0}>
{this.state.address}
</Animated.Text>
</TouchableOpacity>
</View>
);
@ -551,6 +545,27 @@ export class BlueList extends Component {
}
}
export class BlueUseAllFundsButton extends Component {
static InputAccessoryViewID = 'useMaxInputAccessoryViewID';
static propTypes = {
wallet: PropTypes.shape().isRequired,
onUseAllPressed: PropTypes.func.isRequired,
};
render() {
return (
<InputAccessoryView nativeID={BlueUseAllFundsButton.InputAccessoryViewID}>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ color: '#9aa0aa', fontSize: 16, marginHorizontal: 8 }}>
Total: {this.props.wallet.getBalance()} {BitcoinUnit.BTC}
</Text>
<BlueButtonLink title="Use All" onPress={this.props.onUseAllPressed} />
</View>
</InputAccessoryView>
);
}
}
export class BlueLoading extends Component {
render() {
return (
@ -576,7 +591,7 @@ const stylesBlueIcon = StyleSheet.create({
paddingHorizontal: 14,
paddingTop: 8,
},
boxIncomming: {
boxIncoming: {
position: 'relative',
},
ball: {
@ -585,14 +600,14 @@ const stylesBlueIcon = StyleSheet.create({
borderRadius: 15,
backgroundColor: '#ccddf9',
},
ballIncomming: {
ballIncoming: {
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#d2f8d6',
transform: [{ rotate: '-45deg' }],
},
ballIncommingWithoutRotate: {
ballIncomingWithoutRotate: {
width: 30,
height: 30,
borderRadius: 15,
@ -655,12 +670,12 @@ export class BluePlusIcon extends Component {
}
}
export class BlueTransactionIncommingIcon extends Component {
export class BlueTransactionIncomingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballIncoming}>
<Icon {...this.props} name="arrow-down" size={16} type="font-awesome" color="#37c0a1" iconStyle={{ left: 0, top: 8 }} />
</View>
</View>
@ -673,7 +688,7 @@ export class BlueTransactionPendingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ball}>
<Icon
{...this.props}
@ -694,7 +709,7 @@ export class BlueTransactionExpiredIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballOutgoingWithoutRotate}>
<Icon {...this.props} name="hourglass-end" size={16} type="font-awesome" color="#d0021b" iconStyle={{ left: 0, top: 6 }} />
</View>
@ -708,8 +723,8 @@ export class BlueTransactionOnchainIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballIncoming}>
<Icon
{...this.props}
name="link"
@ -729,7 +744,7 @@ export class BlueTransactionOffchainIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballOutgoingWithoutRotate}>
<Icon {...this.props} name="bolt" size={16} type="font-awesome" color="#d0021b" iconStyle={{ left: 0, top: 7 }} />
</View>
@ -743,8 +758,8 @@ export class BlueTransactionOffchainIncomingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.ballIncommingWithoutRotate}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballIncomingWithoutRotate}>
<Icon {...this.props} name="bolt" size={16} type="font-awesome" color="#37c0a1" iconStyle={{ left: 0, top: 7 }} />
</View>
</View>
@ -757,7 +772,7 @@ export class BlueTransactionOutgoingIcon extends Component {
render() {
return (
<View {...this.props}>
<View style={stylesBlueIcon.boxIncomming}>
<View style={stylesBlueIcon.boxIncoming}>
<View style={stylesBlueIcon.ballOutgoing}>
<Icon {...this.props} name="arrow-down" size={16} type="font-awesome" color="#d0021b" iconStyle={{ left: 0, top: 8 }} />
</View>
@ -983,6 +998,386 @@ export class NewWalletPanel extends Component {
}
}
export class BlueTransactionListItem extends Component {
static propTypes = {
item: PropTypes.shape().isRequired,
itemPriceUnit: PropTypes.string,
};
static defaultProps = {
itemPriceUnit: BitcoinUnit.BTC,
};
txMemo = () => {
if (BlueApp.tx_metadata[this.props.item.hash] && BlueApp.tx_metadata[this.props.item.hash]['memo']) {
return BlueApp.tx_metadata[this.props.item.hash]['memo'];
}
return '';
};
rowTitle = () => {
const item = this.props.item;
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else if (invoiceExpiration < now) {
if (item.ispaid) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
}
} else {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
}
};
rowTitleStyle = () => {
const item = this.props.item;
let color = '#37c0a1';
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
color = '#37c0a1';
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = '#37c0a1';
} else {
color = '#FF0000';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueApp.settings.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 16,
color: color,
};
};
avatar = () => {
// is it lightning refill tx?
if (this.props.item.category === 'receive' && this.props.item.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
}
if (this.props.item.type && this.props.item.type === 'bitcoind_tx') {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
if (this.props.item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIcon />
</View>
);
}
if (this.props.item.type === 'user_invoice' || this.props.item.type === 'payment_request') {
if (!this.props.item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = this.props.item.timestamp + this.props.item.expire_time;
if (invoiceExpiration < now) {
return (
<View style={{ width: 25 }}>
<BlueTransactionExpiredIcon />
</View>
);
}
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIncomingIcon />
</View>
);
}
}
if (!this.props.item.confirmations) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else if (this.props.item.value < 0) {
return (
<View style={{ width: 25 }}>
<BlueTransactionOutgoingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionIncomingIcon />
</View>
);
}
};
subtitle = () => {
return (
(this.props.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + this.props.item.confirmations + ' ' : '') +
this.txMemo() +
(this.props.item.memo || '')
);
};
onPress = () => {
if (this.props.item.hash) {
NavigationService.navigate('TransactionDetails', { hash: this.props.item.hash });
} else if (
this.props.item.type === 'user_invoice' ||
this.props.item.type === 'payment_request' ||
this.props.item.type === 'paid_invoice'
) {
const lightningWallet = BlueApp.getWallets().filter(wallet => {
if (typeof wallet === 'object') {
if (wallet.hasOwnProperty('secret')) {
return wallet.getSecret() === this.props.item.fromWallet;
}
}
});
if (lightningWallet.length === 1) {
NavigationService.navigate('LNDViewInvoice', {
invoice: this.props.item,
fromWallet: lightningWallet[0],
isModal: false,
});
}
}
};
render() {
return (
<BlueListItem
avatar={this.avatar()}
title={loc.transactionTimeToReadable(this.props.item.received)}
subtitle={this.subtitle()}
onPress={this.onPress}
badge={{
value: 3,
textStyle: { color: 'orange' },
containerStyle: { marginTop: 0 },
}}
hideChevron
rightTitle={this.rowTitle()}
rightTitleStyle={this.rowTitleStyle()}
/>
);
}
}
export class BlueListTransactionItem extends Component {
static propTypes = {
item: PropTypes.shape().isRequired,
itemPriceUnit: PropTypes.string,
};
static defaultProps = {
itemPriceUnit: BitcoinUnit.BTC,
};
txMemo = () => {
if (BlueApp.tx_metadata[this.props.item.hash] && BlueApp.tx_metadata[this.props.item.hash]['memo']) {
return BlueApp.tx_metadata[this.props.item.hash]['memo'];
}
return '';
};
rowTitle = () => {
const item = this.props.item;
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else if (invoiceExpiration < now) {
if (item.ispaid) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
} else {
return loc.lnd.expired;
}
}
} else {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.props.itemPriceUnit, true).toString();
}
};
rowTitleStyle = () => {
const item = this.props.item;
let color = '#37c0a1';
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
color = '#37c0a1';
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = '#37c0a1';
} else {
color = '#FF0000';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueApp.settings.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 16,
color: color,
};
};
avatar = () => {
// is it lightning refill tx?
if (this.props.item.category === 'receive' && this.props.item.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
}
if (this.props.item.type && this.props.item.type === 'bitcoind_tx') {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
if (this.props.item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIcon />
</View>
);
}
if (this.props.item.type === 'user_invoice' || this.props.item.type === 'payment_request') {
if (!this.props.item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = this.props.item.timestamp + this.props.item.expire_time;
if (invoiceExpiration < now) {
return (
<View style={{ width: 25 }}>
<BlueTransactionExpiredIcon />
</View>
);
}
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIncomingIcon />
</View>
);
}
}
if (!this.props.item.confirmations) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else if (this.props.item.value < 0) {
return (
<View style={{ width: 25 }}>
<BlueTransactionOutgoingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionIncomingIcon />
</View>
);
}
};
subtitle = () => {
return (
(this.props.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + this.props.item.confirmations + ' ' : '') +
this.txMemo() +
(this.props.item.memo || '')
);
};
onPress = () => {
if (this.props.item.hash) {
NavigationService.navigate('TransactionDetails', { hash: this.props.item.hash });
} else if (
this.props.item.type === 'user_invoice' ||
this.props.item.type === 'payment_request' ||
this.props.item.type === 'paid_invoice'
) {
const lightningWallet = BlueApp.getWallets().filter(wallet => {
if (typeof wallet === 'object') {
if (wallet.hasOwnProperty('secret')) {
return wallet.getSecret() === this.props.item.fromWallet;
}
}
});
NavigationService.navigate('LNDViewInvoice', {
invoice: this.props.item,
fromWallet: lightningWallet[0],
isModal: false,
});
}
};
render() {
return (
<BlueListItem
avatar={this.avatar()}
title={loc.transactionTimeToReadable(this.props.item.received)}
subtitle={this.subtitle()}
onPress={this.onPress}
badge={{
value: 3,
textStyle: { color: 'orange' },
containerStyle: { marginTop: 0 },
}}
hideChevron
rightTitle={this.rowTitle()}
rightTitleStyle={this.rowTitleStyle()}
/>
);
}
}
const sliderWidth = width * 1;
const itemWidth = width * 0.82;
const sliderHeight = 190;
@ -999,12 +1394,17 @@ export class WalletsCarousel extends Component {
_renderItem({ item, index }) {
let scaleValue = new Animated.Value(1.0);
let props = { duration: 50 };
if (Platform.OS === 'android') {
props['useNativeDriver'] = true;
}
this.onPressedIn = () => {
Animated.spring(scaleValue, { toValue: 0.9, duration: 100, useNativeDriver: Platform.OS === 'android' }).start();
props.toValue = 0.9;
Animated.spring(scaleValue, props).start();
};
this.onPressedOut = () => {
Animated.spring(scaleValue, { toValue: 1.0, duration: 100, useNativeDriver: Platform.OS === 'android' }).start();
props.toValue = 1.0;
Animated.spring(scaleValue, props).start();
};
if (!item) {
@ -1173,6 +1573,7 @@ export class BlueAddressInput extends Component {
value={this.props.address}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.props.isLoading}
onSubmitEditing={Keyboard.dismiss}
/>
<TouchableOpacity
disabled={this.props.isLoading}
@ -1258,7 +1659,6 @@ export class BlueBitcoinAmount extends Component {
ref={textInput => (this.textInput = textInput)}
editable={!this.props.isLoading && !this.props.disabled}
value={amount}
autoFocus={this.props.pointerEvents !== 'none'}
placeholderTextColor={this.props.disabled ? '#99a0ab' : '#0f5cc0'}
style={{
color: this.props.disabled ? '#99a0ab' : '#0f5cc0',

195
BlueElectrum.js Normal file
View file

@ -0,0 +1,195 @@
import { AsyncStorage } from 'react-native';
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
let reverse = require('buffer-reverse');
const storageKey = 'ELECTRUM_PEERS';
const defaultPeer = { host: 'electrum.coinucopia.io', tcp: 50001 };
let mainClient = false;
let mainConnected = false;
async function connectMain() {
let usingPeer = await getRandomHardcodedPeer();
try {
console.log('begin connection:', JSON.stringify(usingPeer));
mainClient = new ElectrumClient(usingPeer.tcp, usingPeer.host, 'tcp');
await mainClient.connect();
const ver = await mainClient.server_version('2.7.11', '1.2');
console.log('connected to ', ver);
let peers = await mainClient.serverPeers_subscribe();
if (peers && peers.length > 0) {
mainConnected = true;
AsyncStorage.setItem(storageKey, JSON.stringify(peers));
}
} catch (e) {
mainConnected = false;
console.log('bad connection:', JSON.stringify(usingPeer));
}
if (!mainConnected) {
console.log('retry');
setTimeout(connectMain, 5000);
}
}
connectMain();
/**
* Returns random hardcoded electrum server guaranteed to work
* at the time of writing.
*
* @returns {Promise<{tcp, host}|*>}
*/
async function getRandomHardcodedPeer() {
let hardcodedPeers = [
{ host: 'node.ispol.sk', tcp: '50001' },
{ host: 'electrum.vom-stausee.de', tcp: '50001' },
{ host: 'orannis.com', tcp: '50001' },
{ host: '139.162.14.142', tcp: '50001' },
{ host: 'daedalus.bauerj.eu', tcp: '50001' },
{ host: 'electrum.eff.ro', tcp: '50001' },
{ host: 'electrum.anduck.net', tcp: '50001' },
{ host: 'mooo.not.fyi', tcp: '50011' },
{ host: 'electrum.coinucopia.io', tcp: '50001' },
];
return hardcodedPeers[(hardcodedPeers.length * Math.random()) | 0];
}
/**
* Returns random electrum server out of list of servers
* previous electrum server told us. Nearly half of them is
* usually offline.
* Not used for now.
*
* @returns {Promise<{tcp: number, host: string}>}
*/
// eslint-disable-next-line
async function getRandomDynamicPeer() {
try {
let peers = JSON.parse(await AsyncStorage.getItem(storageKey));
peers = peers.sort(() => Math.random() - 0.5); // shuffle
for (let peer of peers) {
let ret = {};
ret.host = peer[1];
for (let item of peer[2]) {
if (item.startsWith('t')) {
ret.tcp = item.replace('t', '');
}
}
if (ret.host && ret.tcp) return ret;
}
return defaultPeer; // failed to find random client, using default
} catch (_) {
return defaultPeer; // smth went wrong, using default
}
}
/**
*
* @param address {String}
* @returns {Promise<Object>}
*/
async function getBalanceByAddress(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
balance.addr = address;
return balance;
}
/**
*
* @param address {String}
* @returns {Promise<Array>}
*/
async function getTransactionsByAddress(address) {
if (!mainClient) throw new Error('Electrum client is not connected');
let script = bitcoin.address.toOutputScript(address);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(reverse(hash));
let history = await mainClient.blockchainScripthash_getHistory(reversedHash.toString('hex'));
return history;
}
/**
*
* @param addresses {Array}
* @returns {Promise<{balance: number, unconfirmed_balance: number}>}
*/
async function multiGetBalanceByAddress(addresses) {
if (!mainClient) throw new Error('Electrum client is not connected');
let balance = 0;
let unconfirmedBalance = 0;
for (let addr of addresses) {
let b = await getBalanceByAddress(addr);
balance += b.confirmed;
unconfirmedBalance += b.unconfirmed_balance;
}
return { balance, unconfirmed_balance: unconfirmedBalance };
}
/**
* Simple waiter till `mainConnected` becomes true (which means
* it Electrum was connected in other function), or timeout 30 sec.
*
*
* @returns {Promise<Promise<*> | Promise<*>>}
*/
async function waitTillConnected() {
let waitTillConnectedInterval = false;
let retriesCounter = 0;
return new Promise(function(resolve, reject) {
waitTillConnectedInterval = setInterval(() => {
if (mainConnected) {
clearInterval(waitTillConnectedInterval);
resolve(true);
}
if (retriesCounter++ >= 30) {
clearInterval(waitTillConnectedInterval);
reject(new Error('Waiting for Electrum connection timeout'));
}
}, 1000);
});
}
module.exports.getBalanceByAddress = getBalanceByAddress;
module.exports.getTransactionsByAddress = getTransactionsByAddress;
module.exports.multiGetBalanceByAddress = multiGetBalanceByAddress;
module.exports.waitTillConnected = waitTillConnected;
module.exports.forceDisconnect = () => {
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
};
/*
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
let script = bitcoin.address.toOutputScript(addr4elect);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
addr4elect = '1BWwXJH3q6PRsizBkSGm2Uw4Sz1urZ5sCj';
script = bitcoin.address.toOutputScript(addr4elect);
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
console.log(addr4elect, ' maps to ', reversedHash.toString('hex'));
console.log(await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex')));
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
// setTimeout(()=>process.exit(), 3000); */

65
Electrum.test.js Normal file
View file

@ -0,0 +1,65 @@
/* global it, describe, jasmine */
global.net = require('net');
let BlueElectrum = require('./BlueElectrum');
let assert = require('assert');
describe('Electrum', () => {
it('ElectrumClient can connect and query', async () => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
const ElectrumClient = require('electrum-client');
let bitcoin = require('bitcoinjs-lib');
// let bitcoin = require('bitcoinjs-lib');
const peer = { host: 'electrum.coinucopia.io', ssl: 50002, tcp: 50001, pruning: null, http: null, https: null };
console.log('begin connection:', JSON.stringify(peer));
let mainClient = new ElectrumClient(peer.tcp, peer.host, 'tcp');
try {
await mainClient.connect();
const ver = await mainClient.server_version('2.7.11', '1.2');
console.log('connected to ', ver);
} catch (e) {
console.log('bad connection:', JSON.stringify(peer));
throw new Error();
}
let addr4elect = 'bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej';
let script = bitcoin.address.toOutputScript(addr4elect);
let hash = bitcoin.crypto.sha256(script);
let reversedHash = Buffer.from(hash.reverse());
let balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
assert.ok(balance.confirmed > 0);
addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
script = bitcoin.address.toOutputScript(addr4elect);
hash = bitcoin.crypto.sha256(script);
reversedHash = Buffer.from(hash.reverse());
balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
assert.ok(balance.confirmed === 51432);
// let peers = await mainClient.serverPeers_subscribe();
// console.log(peers);
mainClient.keepAlive = () => {}; // dirty hack to make it stop reconnecting
mainClient.reconnect = () => {}; // dirty hack to make it stop reconnecting
mainClient.close();
// setTimeout(()=>process.exit(), 3000);
});
it('BlueElectrum works', async function() {
let address = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
await BlueElectrum.waitTillConnected();
let balance = await BlueElectrum.getBalanceByAddress(address);
assert.strictEqual(balance.confirmed, 51432);
assert.strictEqual(balance.unconfirmed, 0);
assert.strictEqual(balance.addr, address);
let txs = await BlueElectrum.getTransactionsByAddress(address);
assert.strictEqual(txs.length, 1);
for (let tx of txs) {
assert.ok(tx.tx_hash);
assert.ok(tx.height);
}
BlueElectrum.forceDisconnect();
});
});

View file

@ -1,8 +1,21 @@
/* global it, jasmine */
/* global it, jasmine, afterAll, beforeAll */
import { SegwitP2SHWallet, SegwitBech32Wallet, HDSegwitP2SHWallet, HDLegacyBreadwalletWallet, HDLegacyP2PKHWallet } from './class';
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
let assert = require('assert');
let bitcoin = require('bitcoinjs-lib');
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
let BlueElectrum = require('./BlueElectrum'); // so it connects ASAP
afterAll(() => {
// after all tests we close socket so the test suite can actually terminate
return BlueElectrum.forceDisconnect();
});
beforeAll(async () => {
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
// while app starts up, but for tests we need to wait for it
await BlueElectrum.waitTillConnected();
});
it('can convert witness to address', () => {
let address = SegwitP2SHWallet.witnessToAddress('035c618df829af694cb99e664ce1b34f80ad2c3b49bcd0d9c0b1836c66b2d25fd8');
@ -47,7 +60,7 @@ it('can create a Segwit HD (BIP49)', async function() {
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress);
});
it('Segwit HD (BIP49) can generate addressess only via ypub', async function() {
it('Segwit HD (BIP49) can generate addressess only via ypub', function() {
let ypub = 'ypub6WhHmKBmHNjcrUVNCa3sXduH9yxutMipDcwiKW31vWjcMbfhQHjXdyx4rqXbEtVgzdbhFJ5mZJWmfWwnP4Vjzx97admTUYKQt6b9D7jjSCp';
let hd = new HDSegwitP2SHWallet();
hd._xpub = ypub;
@ -148,6 +161,30 @@ it('Segwit HD (BIP49) can fetch UTXO', async function() {
);
});
it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', async function() {
if (!process.env.HD_MNEMONIC_BIP49_MANY_TX) {
console.error('process.env.HD_MNEMONIC_BIP49_MANY_TX not set, skipped');
return;
}
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000;
let hd = new HDSegwitP2SHWallet();
hd.setSecret(process.env.HD_MNEMONIC_BIP49_MANY_TX);
assert.ok(hd.validateMnemonic());
let start = +new Date();
await hd.fetchBalance();
let end = +new Date();
const took = (end - start) / 1000;
took > 15 && console.warn('took', took, "sec to fetch huge HD wallet's balance");
assert.strictEqual(hd.getBalance(), 0.00051432);
await hd.fetchUtxo();
assert.ok(hd.utxo.length > 0);
await hd.fetchTransactions();
assert.strictEqual(hd.getTransactions().length, 107);
});
it('can work with malformed mnemonic', () => {
let mnemonic =
'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode';

View file

@ -348,4 +348,35 @@ describe('LightningCustodianWallet', () => {
await l2.fetchBalance();
assert.strictEqual(oldBalance - l2.balance, 3);
});
it('cant pay negative free amount', async () => {
let l1 = new LightningCustodianWallet();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000;
assert.ok(l1.refill_addressess.length === 0);
assert.ok(l1._refresh_token_created_ts === 0);
assert.ok(l1._access_token_created_ts === 0);
l1.balance = 'FAKE';
await l1.createAccount(true);
await l1.authorize();
await l1.fetchBalance();
assert.ok(l1.access_token);
assert.ok(l1.refresh_token);
assert.ok(l1._refresh_token_created_ts > 0);
assert.ok(l1._access_token_created_ts > 0);
assert.ok(l1.balance === 0);
let invoice = await l1.addInvoice(0, 'zero amt inv');
let error = false;
try {
await l1.payInvoice(invoice, -1);
} catch (Err) {
error = true;
}
await l1.fetchBalance();
assert.strictEqual(l1.balance, 0);
assert.ok(error);
});
});

View file

@ -2,6 +2,7 @@ import { createStackNavigator, createAppContainer } from 'react-navigation';
import Settings from './screen/settings/settings';
import About from './screen/settings/about';
import ReleaseNotes from './screen/settings/releasenotes';
import Selftest from './screen/selftest';
import Language from './screen/settings/language';
import Currency from './screen/settings/currency';
@ -86,6 +87,10 @@ const WalletsStackNavigator = createStackNavigator(
screen: About,
path: 'About',
},
ReleaseNotes: {
screen: ReleaseNotes,
path: 'ReleaseNotes',
},
Selftest: {
screen: Selftest,
},
@ -180,6 +185,15 @@ const CreateWalletStackNavigator = createStackNavigator({
},
});
const LightningScanInvoiceStackNavigator = createStackNavigator({
ScanLndInvoice: {
screen: ScanLndInvoice,
},
Success: {
screen: Success,
},
});
const MainBottomTabs = createStackNavigator(
{
Wallets: {
@ -236,7 +250,10 @@ const MainBottomTabs = createStackNavigator(
},
},
ScanLndInvoice: {
screen: ScanLndInvoice,
screen: LightningScanInvoiceStackNavigator,
navigationOptions: {
header: null,
},
},
ScanQrAddress: {
screen: sendScanQrAddress,

View file

@ -64,3 +64,8 @@ MIT
Grab an issue from [the backlog](https://github.com/BlueWallet/BlueWallet/projects/1), try to start or submit a PR, any doubts we will try to guide you.
Join us at our [telegram group](https://t.me/bluewallet) where we hangout :+1:
## Responsible disclosure
Found critical bugs/vulnerabilities? Please email them bluewallet@bluewallet.io
Thanks!

View file

@ -108,7 +108,10 @@
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaPrecompile" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javac" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_aapt_derived_proguard_rules" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/legacy_multidex_main_dex_list" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/linked_res_for_bundle" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint_jar" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifest-checker" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_assets" />

View file

@ -102,10 +102,11 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "3.7.0"
versionName "3.7.2"
ndk {
abiFilters "armeabi-v7a", "x86"
}
multiDexEnabled true
}
splits {
abi {
@ -137,6 +138,7 @@ android {
}
dependencies {
implementation project(':react-native-tcp')
implementation project(':@remobile_react-native-qrcode-local-image')
implementation project(':react-native-image-picker')
implementation project(':react-native-webview')
@ -155,6 +157,7 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation 'com.android.support:multidex:1.0.3'
}
// Run this once to be able to run the application with BUCK

View file

@ -3,6 +3,7 @@ package io.bluewallet.bluewallet;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.peel.react.TcpSocketsModule;
import com.remobile.qrcodeLocalImage.RCTQRCodeLocalImagePackage;
import com.imagepicker.ImagePickerPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
@ -55,6 +56,7 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new TcpSocketsModule(),
new RCTQRCodeLocalImagePackage(),
new ImagePickerPackage(),
new RNCWebViewPackage(),

View file

@ -1,4 +1,6 @@
rootProject.name = 'BlueWallet'
include ':react-native-tcp'
project(':react-native-tcp').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-tcp/android')
include ':@remobile_react-native-qrcode-local-image'
project(':@remobile_react-native-qrcode-local-image').projectDir = new File(rootProject.projectDir, '../node_modules/@remobile/react-native-qrcode-local-image/android')
include ':react-native-image-picker'

View file

@ -4,6 +4,7 @@ import { WatchOnlyWallet } from './watch-only-wallet';
const bip39 = require('bip39');
const BigNumber = require('bignumber.js');
const bitcoin = require('bitcoinjs-lib');
const BlueElectrum = require('../BlueElectrum');
export class AbstractHDWallet extends LegacyWallet {
static type = 'abstract';
@ -346,24 +347,71 @@ export class AbstractHDWallet extends LegacyWallet {
async fetchBalance() {
try {
const api = new Frisbee({ baseURI: 'https://www.blockonomics.co' });
let response = await api.post('/api/balance', { body: JSON.stringify({ addr: this.getXpub() }) });
// doing binary search for last used externa address
if (response && response.body && response.body.response) {
this.balance = 0;
this.unconfirmed_balance = 0;
this.usedAddresses = [];
for (let addr of response.body.response) {
this.balance += addr.confirmed;
this.unconfirmed_balance += addr.unconfirmed;
this.usedAddresses.push(addr.addr);
let that = this;
// refactor me
// eslint-disable-next-line
async function binarySearchIterationForInternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) {
if (depth >= 20) return maxUsedIndex + 1; // fail
let txs = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index));
if (txs.length === 0) {
if (index === 0) return 0;
minUnusedIndex = Math.min(minUnusedIndex, index); // set
index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex);
} else {
maxUsedIndex = Math.max(maxUsedIndex, index); // set
let txs2 = await BlueElectrum.getTransactionsByAddress(that._getInternalAddressByIndex(index + 1));
if (txs2.length === 0) return index + 1; // thats our next free address
index = Math.round((minUnusedIndex - index) / 2 + index);
}
this.balance = new BigNumber(this.balance).dividedBy(100000000).toString() * 1;
this.unconfirmed_balance = new BigNumber(this.unconfirmed_balance).dividedBy(100000000).toString() * 1;
this._lastBalanceFetch = +new Date();
} else {
throw new Error('Could not fetch balance from API: ' + response.err);
return binarySearchIterationForInternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1);
}
this.next_free_change_address_index = await binarySearchIterationForInternalAddress(100);
// refactor me
// eslint-disable-next-line
async function binarySearchIterationForExternalAddress(index, maxUsedIndex = 0, minUnusedIndex = 100500100, depth = 0) {
if (depth >= 20) return maxUsedIndex + 1; // fail
let txs = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index));
if (txs.length === 0) {
if (index === 0) return 0;
minUnusedIndex = Math.min(minUnusedIndex, index); // set
index = Math.floor((index - maxUsedIndex) / 2 + maxUsedIndex);
} else {
maxUsedIndex = Math.max(maxUsedIndex, index); // set
let txs2 = await BlueElectrum.getTransactionsByAddress(that._getExternalAddressByIndex(index + 1));
if (txs2.length === 0) return index + 1; // thats our next free address
index = Math.round((minUnusedIndex - index) / 2 + index);
}
return binarySearchIterationForExternalAddress(index, maxUsedIndex, minUnusedIndex, depth + 1);
}
this.next_free_address_index = await binarySearchIterationForExternalAddress(100);
this.balance = 0;
this.unconfirmed_balance = 0;
this.usedAddresses = [];
// generating all involved addresses:
for (let c = 0; c < this.next_free_address_index; c++) {
this.usedAddresses.push(this._getExternalAddressByIndex(c));
}
for (let c = 0; c < this.next_free_change_address_index; c++) {
this.usedAddresses.push(this._getInternalAddressByIndex(c));
}
// finally fetching balance
let balance = await BlueElectrum.multiGetBalanceByAddress(this.usedAddresses);
this.balance = new BigNumber(balance.balance).dividedBy(100000000).toNumber();
this.unconfirmed_balance = new BigNumber(balance.unconfirmed_balance).dividedBy(100000000).toNumber();
this._lastBalanceFetch = +new Date();
} catch (err) {
console.warn(err);
}

View file

@ -24,13 +24,13 @@ class BlueAppComponent extends React.Component {
this.state = { isMigratingData: true };
}
async setIsMigratingData() {
setIsMigratingData = async () => {
await BlueApp.startAndDecrypt();
this.setState({ isMigratingData: false });
}
};
render() {
return this.state.isMigratingData ? <WalletMigrate onComplete={() => this.setIsMigratingData()} /> : <App />;
return this.state.isMigratingData ? <WalletMigrate onComplete={this.setIsMigratingData} /> : <App />;
}
}

View file

@ -43,6 +43,7 @@
2DCD954D1E0B4F2C00145EB5 /* BlueWalletTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* BlueWalletTests.m */; };
2DF0FFEE2056DD460020B375 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DAD3EA31DF850E9000B6D8A /* libReact.a */; };
2F707BDB2EF14D17AF9A2908 /* libReactNativePermissions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DD63E4B5C8344BB9880C9EC /* libReactNativePermissions.a */; };
34582CAA4AD140F7B80C961A /* libTcpSockets.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DF4E6C040764E4BA1ACC1EB /* libTcpSockets.a */; };
34CC55B441594DBB95AD1B50 /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E8E8CE89B3D142C6A8A56C34 /* Octicons.ttf */; };
3EEBC6F85642487DA7C4EE35 /* AntDesign.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C4496FB303574862B40A878A /* AntDesign.ttf */; };
4D6390DDA5B7485F91A6C750 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2654894D4DE44A4C8F71773D /* CoreData.framework */; };
@ -524,6 +525,13 @@
remoteGlobalIDString = 32D980DD1BE9F11C00FA27E5;
remoteInfo = RCTQRCodeLocalImage;
};
B47720652202510900DD0E81 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 910283A2A9EB4D00902DE78E /* TcpSockets.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = TcpSockets;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
@ -579,8 +587,10 @@
7EA61BC8FF6E4AD2A67F1557 /* RCTQRCodeLocalImage.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTQRCodeLocalImage.xcodeproj; path = "../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage.xcodeproj"; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
8637D4B5E14D443A9031DA95 /* libRNFS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFS.a; sourceTree = "<group>"; };
910283A2A9EB4D00902DE78E /* TcpSockets.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = TcpSockets.xcodeproj; path = "../node_modules/react-native-tcp/ios/TcpSockets.xcodeproj"; sourceTree = "<group>"; };
94565BFC6A0C4235B3EC7B01 /* libRNSVG.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSVG.a; sourceTree = "<group>"; };
95208B2A05884A76B5BB99C0 /* libRCTGoogleAnalyticsBridge.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTGoogleAnalyticsBridge.a; sourceTree = "<group>"; };
9DF4E6C040764E4BA1ACC1EB /* libTcpSockets.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libTcpSockets.a; sourceTree = "<group>"; };
9EA3788F4C6643B7B0182587 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = "<group>"; };
9F1F51A83D044F3BB26A35FC /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = "libRNSVG-tvOS.a"; sourceTree = "<group>"; };
A71D2FDE64CF4F729C7298EA /* RNFS.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNFS.xcodeproj; path = "../node_modules/react-native-fs/RNFS.xcodeproj"; sourceTree = "<group>"; };
@ -661,6 +671,7 @@
02DEC1C9F61B405E8E357B2E /* libRCTWKWebView.a in Frameworks */,
A6E5EEC7A4B54F5A9C9D92FC /* libRNImagePicker.a in Frameworks */,
C1056BF235EE4E23AAF21975 /* libRCTQRCodeLocalImage.a in Frameworks */,
34582CAA4AD140F7B80C961A /* libTcpSockets.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -898,6 +909,7 @@
E7173EC6B95B4981AD3D4C70 /* RCTWKWebView.xcodeproj */,
1A03CFBC35DD4AC28FA4A619 /* RNImagePicker.xcodeproj */,
7EA61BC8FF6E4AD2A67F1557 /* RCTQRCodeLocalImage.xcodeproj */,
910283A2A9EB4D00902DE78E /* TcpSockets.xcodeproj */,
);
name = Libraries;
sourceTree = "<group>";
@ -972,6 +984,7 @@
E7078D2FED444DA4B0BD57F9 /* libRCTWKWebView.a */,
FC98DC24A81A463AB8B2E6B1 /* libRNImagePicker.a */,
B642AFB13483418CAB6FF25E /* libRCTQRCodeLocalImage.a */,
9DF4E6C040764E4BA1ACC1EB /* libTcpSockets.a */,
);
name = "Recovered References";
sourceTree = "<group>";
@ -1119,6 +1132,14 @@
name = Products;
sourceTree = "<group>";
};
B47720622202510900DD0E81 /* Products */ = {
isa = PBXGroup;
children = (
B47720662202510900DD0E81 /* libTcpSockets.a */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1352,6 +1373,10 @@
ProductGroup = B40FE5CC21FAD27D005D5578 /* Products */;
ProjectRef = 9EA3788F4C6643B7B0182587 /* RNVectorIcons.xcodeproj */;
},
{
ProductGroup = B47720622202510900DD0E81 /* Products */;
ProjectRef = 910283A2A9EB4D00902DE78E /* TcpSockets.xcodeproj */;
},
);
projectRoot = "";
targets = (
@ -1791,6 +1816,13 @@
remoteRef = B4327F5021FC1B9300F7ADFA /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
B47720662202510900DD0E81 /* libTcpSockets.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libTcpSockets.a;
remoteRef = B47720652202510900DD0E81 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
@ -1964,6 +1996,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = BlueWalletTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
@ -1992,6 +2025,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@ -2028,6 +2062,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = BlueWalletTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
@ -2056,6 +2091,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@ -2093,6 +2129,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = BlueWallet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
@ -2102,7 +2139,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet;
PRODUCT_NAME = BlueWallet;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
@ -2134,6 +2171,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = BlueWallet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
@ -2143,7 +2181,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet;
PRODUCT_NAME = BlueWallet;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
@ -2182,6 +2220,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = "BlueWallet-tvOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@ -2209,6 +2248,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@ -2254,6 +2294,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = "BlueWallet-tvOS/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@ -2281,6 +2322,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@ -2325,6 +2367,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = "BlueWallet-tvOSTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@ -2352,6 +2395,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",
@ -2396,6 +2440,7 @@
"$(SRCROOT)/../node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView",
"$(SRCROOT)/../node_modules/react-native-image-picker/ios",
"$(SRCROOT)/../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage",
"$(SRCROOT)/../node_modules/react-native-tcp/ios/**",
);
INFOPLIST_FILE = "BlueWallet-tvOSTests/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@ -2423,6 +2468,7 @@
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
OTHER_LDFLAGS = (
"-ObjC",

View file

@ -28,7 +28,7 @@
moduleName:@"BlueWallet"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [UIColor blackColor];
rootView.backgroundColor = [UIColor whiteColor];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.7.0</string>
<string>3.7.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -56,9 +56,11 @@
<key>NSCalendarsUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSCameraUsageDescription</key>
<string>In order to quickly scan the recipient&apos;s address, we need your permission to use the camera to scan their QR Code.</string>
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMicrophoneUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMotionUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSPhotoLibraryAddUsageDescription</key>
@ -67,8 +69,6 @@
<string>In order to import an image for scanning, we need your permission to access your photo library.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>NSMicrophoneUsageDescription</key>
<string>This alert should not show up as we do not require this data</string>
<key>UIAppFonts</key>
<array>
<string>AntDesign.ttf</string>

View file

@ -4,23 +4,23 @@ module.exports = {
enter_password: 'Gib das Passwort ein',
bad_password: 'Fasches Passwort, nächster Versuch',
never: 'nie',
continue: 'Continue',
continue: 'Weiter',
ok: 'OK',
},
wallets: {
select_wallet: 'Wähle Wallet',
select_wallet: 'Wähle eine Wallet',
options: 'Einstellungen',
createBitcoinWallet:
'In order to use a Lightning wallet, a Bitcoin wallet is needed in order to fund it. Please, create or import a Bitcoin wallet.',
'Um eine Lightning wallet zu verwenden, muss erstmal eine Bitcoin Wallet eingerichtet werden. Bitte erstell oder importier eine Bitcoin Wallet.',
list: {
app_name: 'BlueWallet',
title: 'Wallets',
header:
'Eine Wallet (Brieftasche) spiegelt ein Paar von kryptographischen Schlüssel wider. Einen geheimen und eine Adresse als öffentlichen Schlüssel. Letztern kann man zum Erhalt von Bitcoin teilen.',
'Eine Wallet spiegelt ein Paar kryptographische Schlüssel wider. Einen geheimen Schlüseel und eine Adresse als öffentlichen Schlüssel. Den öffentlichen Schlüssel kann man zum Empfang von Bitcoin teilen.',
add: 'Wallet hinzufügen',
create_a_wallet: 'Wallet erstellen',
create_a_wallet1: 'Es ist kostenlos und du kannst',
create_a_wallet2: 'so viele erstellen, wie du möchtest',
create_a_wallet2: 'so viele Wallets erstellen, wie du möchtest',
latest_transaction: 'Lezte Transaktion',
empty_txs1: 'Deine Transaktionen erscheinen hier',
empty_txs2: 'Noch keine Transaktionen',
@ -42,7 +42,7 @@ module.exports = {
or: 'oder',
import_wallet: 'Wallet importieren',
imported: 'Importiert',
coming_soon: 'Folgt bald',
coming_soon: 'Demnächst verfügbar',
lightning: 'Lightning',
bitcoin: 'Bitcoin',
},
@ -77,15 +77,15 @@ module.exports = {
imported: 'Importiert',
error: 'Fehler beim Import. Ist die Eingabe korrekt?',
success: 'Erfolg',
do_import: 'Importiere',
do_import: 'Importieren',
scan_qr: 'oder QR-Code scannen?',
},
scanQrWif: {
go_back: 'Zurück',
cancel: 'Abbrechen',
decoding: 'Decodieren',
decoding: 'Entschlüsseln',
input_password: 'Passwort eingeben',
password_explain: 'Das ist ein BIP38 verschlüsselter geheimer Schlüssel',
password_explain: 'Das ist ein mit BIP38 verschlüsselter geheimer Schlüssel',
bad_password: 'Falsches Passwort',
wallet_already_exists: 'Diese Wallet existiert bereits',
bad_wif: 'Falsches WIF',
@ -116,11 +116,11 @@ module.exports = {
header: 'Senden',
details: {
title: 'Transaktion erstellen',
amount_field_is_not_valid: 'Betrageingabe ist nicht valide',
fee_field_is_not_valid: 'Gebühreingabe ist nicht valide',
address_field_is_not_valid: 'Adresseingabe ist nicht valide',
amount_field_is_not_valid: 'Betrageingabe ist nicht korrekt',
fee_field_is_not_valid: 'Gebühreingabe ist nicht korrekt',
address_field_is_not_valid: 'Adresseingabe ist nicht korrekt',
total_exceeds_balance: 'Der zu sendende Betrag ist größer als der verfügbare Betrag.',
create_tx_error: 'Fehler beim Erstellen der Transaktion. Bitte stelle sicher, dass die Adresse valide ist.',
create_tx_error: 'Fehler beim Erstellen der Transaktion. Bitte stelle sicher, dass die Adresse korrekt ist.',
address: 'Adresse',
amount_placeholder: 'Betrag (in BTC)',
fee_placeholder: 'plus Gebühr (in BTC)',
@ -172,36 +172,36 @@ module.exports = {
},
settings: {
header: 'Einstellungen',
plausible_deniability: 'Glaubhafte Abstreitbarkeit...',
storage_not_encrypted: 'Speicher: nicht verschlüsselt',
storage_encrypted: 'Speicher: verschlüsselt',
plausible_deniability: 'Glaubhafte Täuschung...',
storage_not_encrypted: 'Speicher nicht verschlüsselt',
storage_encrypted: 'Speicher verschlüsselt',
password: 'Passwort',
password_explain: 'Erstelle das Passwort zum Entschlüsseln des Speichers',
retype_password: 'Passwort wiederholen',
passwords_do_not_match: 'Passwörter stimmen nicht überein',
encrypt_storage: 'Speicher verschlüsseln',
lightning_settings: 'Lightning settings',
lightning_settings: 'Lightning Einstellungen',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
'Bitte installier Lndhub, um mit deiner eigenen LND Node zu verbinden' +
' und setz seine URL hier in den Einstellungen. Lass das Feld leer, um Standard- ' +
'LndHub\n (lndhub.io) zu verwenden',
save: 'Speichern',
about: 'Über',
language: 'Sprache',
currency: 'Währung',
},
plausibledeniability: {
title: 'Glaubhafte Abstreitbarkeit',
title: 'Glaubhafte Täuschung',
help:
'Unter bestimmten Umständen könntest du dazu gezwungen werden, ' +
'dein Passwort preiszugeben. Um deine Bitcoins zu sichern, kann ' +
'BlueWallet einen weiteren verschlüsselten Speicher mit einem ' +
'anderen Passwort erstellen. Unter äußeren Druck kannst du das ' +
'anderen Passwort erstellen. Unter Druck kannst du das ' +
'zweite Passwort an Fremde weitergeben. Wenn eingegeben, öffnet ' +
'BlueWallet einen anderen Speicher zur Täuschung. Dies wirkt ' +
'auf Fremde täuschen echt und dein Hauptspeicher bleibt geheim ' +
'auf Fremde täuschend echt und dein Hauptspeicher bleibt geheim ' +
'und sicher.',
help2: 'Der weitere Speicher ist voll funktional und man kann einen Minimalbetrag für die Glaubhaftigkeit hinterlegen.',
help2: 'Der andere Speicher ist voll funktional und man kann einen Minimalbetrag für die Glaubhaftigkeit hinterlegen.',
create_fake_storage: 'Erstelle verschlüsselten Speicher zur Täuschung',
go_back: 'Zurück',
create_password: 'Erstelle ein Passwort',
@ -209,14 +209,15 @@ module.exports = {
password_should_not_match: 'Das Passwort für den täuschenden Speicher darf nicht mit dem deines Hauptspeichers übereinstimmen',
retype_password: 'Passwort wiederholen',
passwords_do_not_match: 'Passwörter stimmen nicht überein. Neuer Versuch',
success: 'Erfolg',
success: 'Erfolg!',
},
lnd: {
title: 'Beträge verwalten',
choose_source_wallet: 'Wähle eine Wallet als Quelle',
refill_lnd_balance: 'Fülle deine Lightning Wallet auf',
refill: 'Auffüllen',
choose_source_wallet: 'Wähle eine Wallet als Zahlungsquelle',
refill_lnd_balance: 'Lade deine Lightning Wallet auf',
refill: 'Aufladen',
withdraw: 'Abheben',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
sameWalletAsInvoiceError:
'Du kannst nicht die Rechnung mit der Wallet begleichen, die du für die Erstellung dieser Rechnung verwendet hast.',
},
};

164
loc/ru.js
View file

@ -1,78 +1,78 @@
module.exports = {
_: {
storage_is_encrypted: 'Ваше хранилище зашифровано. Введите пароль для расшифровки',
enter_password: 'Введите пароль',
bad_password: 'Неверный пароль, попробуйте еще раз',
never: 'никогда',
continue: 'Continue',
storage_is_encrypted: 'Твоё хранилище зашифровано. Введи пароль для расшифровки',
enter_password: 'Введи пароль',
bad_password: 'Неверный пароль, попробуй еще раз',
never: 'Никогда',
continue: 'Продолжить',
ok: 'OK',
},
wallets: {
options: 'options',
select_wallet: 'Select Wallet',
createBitcoinWallet: 'In order to use a Lightning wallet, a Bitcoin wallet is needed to fund it. Would you like to continue anyway?',
options: 'Настройки',
select_wallet: 'Выбрать кошелек',
createBitcoinWallet: 'Чтобы воспользоватья кошельком Lightning, нужно сначала пополнить его с помощью кошелька Bitcoin. Продолжить?',
list: {
app_name: 'BlueWallet',
title: 'кошельки',
header: 'Кошелек это секретный (приватный) ключ, и соответствующий ему адрес на который можно получать биткоины',
title: 'Кошельки',
header: 'Кошелек - это секретный (приватный) ключ и соответствующий ему адрес на который можно получать Bitcoin',
add: 'Добавить Кошелек',
create_a_wallet: 'Создать кошелек',
create_a_wallet1: 'Это бесплатно и вы можете создать',
create_a_wallet1: 'Это бесплатно и ты можешь создать',
create_a_wallet2: 'неограниченное количество',
latest_transaction: 'последняя транзакция',
latest_transaction: 'Последняя транзакция',
empty_txs1: 'Список транзакций пока пуст',
empty_txs2: ' ',
tap_here_to_buy: 'Tap here to buy Bitcoin',
tap_here_to_buy: 'Купить Bitcoin',
},
reorder: {
title: 'Reorder Wallets',
title: 'Отсортировать кошельки',
},
add: {
title: 'добавить кошелек',
title: 'Добавить кошелек',
description:
'Вы можете отсканировать QR код (в формате WIF - Wallet Import Format), или создать новый кошелек. Segwit поддерживается по умолчанию.',
'Ты можешь отсканировать QR код (в формате WIF - Wallet Import Format) или создать новый кошелек. Segwit поддерживается по умолчанию.',
scan: 'Отсканировать',
create: 'Создать',
label_new_segwit: 'Новый SegWit',
label_new_lightning: 'Новый Lightning',
wallet_name: 'имя кошелька',
wallet_type: 'тип кошелька',
or: 'or',
wallet_name: 'Имя кошелька',
wallet_type: 'Тип кошелька',
or: 'или',
import_wallet: 'Импортировать кошелек',
imported: 'Импортирован',
coming_soon: 'Пока недоступно',
imported: 'Кошелек импортирован',
coming_soon: 'Скоро будет',
lightning: 'Lightning',
bitcoin: 'Bitcoin',
},
details: {
title: 'Информация о Кошельке',
title: 'Информация о кошельке',
address: 'Адрес',
type: 'Тип',
label: 'Метка',
delete: 'Delete',
save: 'Save',
are_you_sure: 'Вы уверены?',
delete: 'Удалить',
save: 'Сохранить',
are_you_sure: 'Точно удалить?',
yes_delete: 'Да, удалить',
destination: 'destination',
description: 'description',
destination: 'Назначение',
description: 'Описание',
no_cancel: 'Нет, отмена',
delete_this_wallet: 'Удалить этот кошелек',
export_backup: 'Экспорт / резервная копия',
buy_bitcoin: 'Buy Bitcoin',
show_xpub: 'Show wallet XPUB',
buy_bitcoin: 'Купить Bitcoin',
show_xpub: 'Показать XPUB',
},
export: {
title: 'Экспорт Кошелька',
},
xpub: {
title: 'wallet XPUB',
copiedToClipboard: 'скопировано',
title: 'XPUB кошелька',
copiedToClipboard: 'Скопировано',
},
import: {
title: 'import',
explanation: 'Напишите тут вашу мнемонику, приватный ключ, WIF, что угодно, BlueWallet постарается угадать верный формат',
imported: 'Импортирован',
title: 'Импорт',
explanation: 'Напиши тут свою мнемоническую фразу, приватный ключ, WIF - что угодно! BlueWallet постарается угадать верный формат',
imported: 'Импорт завершен',
error: 'Не удалось импортировать',
success: 'Успех',
do_import: 'Импортировать',
@ -81,8 +81,8 @@ module.exports = {
scanQrWif: {
go_back: 'Назад',
cancel: 'Отмена',
decoding: 'Декодирую',
input_password: 'Введите пароль',
decoding: 'Расшивровываю',
input_password: 'Введи пароль',
password_explain: 'Приватный ключ зашифрован по стандарту BIP38',
bad_password: 'Неверный пароль',
wallet_already_exists: 'Такой кошелек уже существует',
@ -106,40 +106,40 @@ module.exports = {
from: 'От',
to: 'Кому',
copy: 'копировать',
transaction_details: 'Transaction details',
show_in_block_explorer: 'Show in block explorer',
transaction_details: 'Детали транзакции',
show_in_block_explorer: 'Показать транзакцию в блокчейне',
},
},
send: {
header: 'Отправить',
confirm: {
header: 'Confirm',
sendNow: 'Send now',
header: 'Подтвердить',
sendNow: 'Отправить',
},
success: {
done: 'Done',
done: 'Готово',
},
details: {
title: 'Создать Транзакцию',
amount_field_is_not_valid: 'Поле не валидно',
fee_field_is_not_valid: 'Поле `комиссия` не валидно',
address_field_is_not_valid: 'Поле `адрес` не валидно',
amount_field_is_not_valid: 'Введенная сумма неверна',
fee_field_is_not_valid: 'Введенная комиссия неверна',
address_field_is_not_valid: 'Введенный адрес неверный',
receiver_placeholder: 'Адрес получателя',
amount_placeholder: 'сколько отправить (в BTC)',
fee_placeholder: 'плюс комиссия за перевод (в BTC)',
note_placeholder: 'примечание платежа',
create_tx_error: 'There was an error creating the transaction. Please, make sure the address is valid.',
create_tx_error: 'Ошибка при создании транзакции. Пожалуйста, проверь правильность адреса.',
cancel: 'Отмена',
scan: 'Скан QR',
scan: 'Отсканировать QR',
create: 'Создать',
send: 'Send',
address: 'Address',
total_exceeds_balance: 'Total amount exceeds balance.',
send: 'Отправить',
address: 'Адрес',
total_exceeds_balance: 'Общая сумма превышает баланс.',
remaining_balance: 'Остаток баланса',
},
create: {
title: 'Создать Транзакцию',
details: 'Details',
details: 'Детали',
error: 'Ошибка при создании транзакции. Неправильный адрес назначения или недостаточно средств?',
go_back: 'Назад',
this_is_hex: 'Это данные транзакции. Транзакция подписана и готова быть транслирована в сеть. Продолжить?',
@ -150,75 +150,75 @@ module.exports = {
satoshi_per_byte: 'Сатоши на байт',
memo: 'Примечание',
broadcast: 'Отправить',
not_enough_fee: 'Слишком маленькая комиссия. Увеличьте комиссию',
not_enough_fee: 'Слишком маленькая комиссия. Увеличь комиссию',
},
},
buyBitcoin: {
header: 'Buy Bitcoin',
tap_your_address: 'Tap your address to copy it to clipboard:',
copied: 'Copied to Clipboard!',
header: 'Купить Bitcoin',
tap_your_address: 'Нажми на адрес, чтобы скопировать его:',
copied: 'Скопировано',
},
receive: {
header: 'Получить',
details: {
title: 'Покажите этот адрес плательщику',
title: 'Покажи этот адрес плательщику',
share: 'Отправить',
copiedToClipboard: 'скопировано',
label: 'Description',
create: 'Create',
setAmount: 'Receive with amount',
copiedToClipboard: 'Скопировано',
label: 'Описание',
create: 'Создать',
setAmount: 'Получить сумму',
},
},
settings: {
tabBarLabel: 'Настройки',
header: 'Настройки',
plausible_deniability: 'Правдоподобное отрицание...',
plausible_deniability: 'Правдоподобная имитация...',
storage_not_encrypted: 'Хранилище: не зашифровано',
storage_encrypted: 'Хранилище: зашифровано',
password: 'Пароль',
password_explain: 'Придумайте пароль для расшифровки хранилища',
retype_password: 'Наберите пароль повторно',
password_explain: 'Придумай пароль для расшифровки хранилища',
retype_password: 'Набери пароль повторно',
passwords_do_not_match: 'Пароли не совпадают',
encrypt_storage: 'Зашифровать хранилище',
lightning_settings: 'Lightning settings',
lightning_settings: 'Настройки Lightning',
lightning_settings_explain:
'To connect to your own LND node please install LndHub' +
' and put its URL here in settings. Leave blank to use default ' +
'ndHub\n (lndhub.io)',
save: 'save',
'Чтобы подключиться к своему узлу LND, пожалуйста, установи LndHub' +
' и добавь его URL в настройки. Оставь поле пустым, чтобы использоавать стандартный ' +
'LndHub\n (lndhub.io)',
save: 'Сохранить',
about: 'О программе',
language: 'Язык',
currency: 'Валюта',
},
plausibledeniability: {
title: 'Правдоподобное Отрицание',
title: 'Правдоподобная имитация',
help:
'При определенных обстоятельствах вас могут вынудить раскрыть пароль. ' +
'Чтобы сохранить ваши биткоины в безопасности, BlueWallet может создать ' +
'еще одно зашифрованое хранилище, с другим паролем. Под давлением, ' +
'вы можете раскрыть третьим лицам этот пароль. Если ввести этот пароль в ' +
'При определенных обстоятельствах тебя могут вынудить раскрыть пароль. ' +
'Чтобы сохранить свой Bitcoin в безопасности, BlueWallet может создать ' +
'еще одно зашифрованое хранилище, с другим паролем. В случае шантажа ' +
'ты можешь раскрыть третьим лицам этот пароль. Если ввести этот пароль в ' +
"BlueWallet, разблокируется 'фальшивое' хранилище. Это будет выглядеть " +
'правдоподобно для третьих лиц, но при этом сохранит ваше основное хранилище ' +
'с биткоинами в безопасности.',
'с Bitcoin в безопасности.',
help2:
'Новое хранилище будет полностью функциональным и вы даже можете хранить на нем немного биткоинов ' +
'Новое хранилище будет полностью функциональным и ты даже можешь хранить на нем немного Bitcoin, ' +
'чтобы это выглядело более правдоподобно.',
create_fake_storage: 'Создать фальшивое хранилище',
go_back: 'Назад',
create_password: 'Придумайте пароль',
create_password: 'Придумай пароль',
create_password_explanation: 'Пароль для фальшивого хранилища не должен быть таким же как основной пароль',
password_should_not_match: 'Пароль для фальшивого хранилища не должен быть таким же как основной пароль',
retype_password: 'Наберите пароль повторно',
passwords_do_not_match: 'Пароли не совпадают, попробуйте еще раз',
success: 'Операция успешна',
retype_password: 'Набери пароль повторно',
passwords_do_not_match: 'Пароли не совпадают, попробуй еще раз',
success: 'Готово',
},
lnd: {
title: 'мои средства',
choose_source_wallet: 'Выберите с какого кошелька',
title: 'Мои средства',
choose_source_wallet: 'Выбери с какого кошелька',
refill_lnd_balance: 'Пополнить баланс Lightning кошелька',
refill: 'Пополнить',
withdraw: 'Вывести',
expired: 'Expired',
sameWalletAsInvoiceError: 'You can not pay an invoice with the same wallet used to create it.',
expired: 'Истекший',
sameWalletAsInvoiceError: 'Ты не можешь оплатить счет тем же кошельком, который ты использовал для его создания.',
},
};

View file

@ -11,6 +11,8 @@ export const FiatUnit = Object.freeze({
INR: { endPointKey: 'INR', symbol: '₹', locale: 'hi-HN' },
JPY: { endPointKey: 'JPY', symbol: '¥', locale: 'ja-JP' },
MXN: { endPointKey: 'MXN', symbol: '$', locale: 'es-MX' },
MYR: { endPointKey: 'MYR', symbol: 'RM', locale: 'ms-MY' },
NZD: { endPointKey: 'NZD', symbol: '$', locale: 'en-NZ' },
PLN: { endPointKey: 'PLN', symbol: 'zł', locale: 'pl-PL' },
RUB: { endPointKey: 'RUB', symbol: '₽', locale: 'ru-RU' },
SGD: { endPointKey: 'SGD', symbol: 'S$', locale: 'zh-SG' },

View file

@ -1,6 +1,8 @@
import Frisbee from 'frisbee';
export class NetworkTransactionFee {
static StorageKey = 'NetworkTransactionFee';
constructor(fastestFee = 1, halfHourFee = 1, hourFee = 1) {
this.fastestFee = fastestFee;
this.halfHourFee = halfHourFee;

4520
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
{
"name": "BlueWallet",
"version": "3.7.0",
"version": "3.7.2",
"devDependencies": {
"babel-eslint": "^10.0.1",
"babel-jest": "^24.0.0",
"eslint": "^5.12.1",
"eslint": "^5.13.0",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.15.0",
"eslint-plugin-node": "^8.0.1",
@ -19,10 +19,11 @@
"scripts": {
"prepare": "./patches/fix_mangle.sh; git apply patches/minifier.js.patch; git apply patches/minify.js.patch; git apply patches/transaction_builder.js.patch; git apply ./patches/transaction.js.patch",
"clean": "cd android/; ./gradlew clean; cd ..; rm -r -f /tmp/metro-cache/; rm -r -f node_modules/; npm cache clean --force; npm i; npm start -- --reset-cache",
"releasenotes2json": "./release-notes.sh > release-notes.txt; node -e 'console.log(JSON.stringify(require(\"fs\").readFileSync(\"release-notes.txt\", \"utf8\")));' > release-notes.json",
"start": "node node_modules/react-native/local-cli/cli.js start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack",
"postinstall": "./node_modules/.bin/rn-nodeify --install buffer,events,process,stream,util,inherits,fs,path --hack; npm run releasenotes2json",
"test": "npm run unit && npm run jest && npm run lint",
"jest": "node node_modules/jest/bin/jest.js",
"lint": "./node_modules/.bin/eslint *.js screen/**/*.js screen/ class/ models/ loc/ --fix",
@ -45,31 +46,32 @@
"buffer-reverse": "^1.0.1",
"crypto-js": "^3.1.9-1",
"dayjs": "^1.8.0",
"eslint-config-prettier": "^3.6.0",
"electrum-client": "git+https://github.com/Overtorment/node-electrum-client.git",
"eslint-config-prettier": "^4.0.0",
"eslint-config-standard": "^12.0.0",
"eslint-config-standard-react": "^7.0.2",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-standard": "^4.0.0",
"frisbee": "^1.6.4",
"frisbee": "^2.0.5",
"intl": "^1.2.5",
"mocha": "^5.2.0",
"node-libs-react-native": "^1.0.1",
"path-browserify": "0.0.0",
"prettier": "^1.16.1",
"path-browserify": "^1.0.0",
"prettier": "^1.16.3",
"process": "^0.11.10",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-localization": "^1.0.10",
"react-native": "^0.58.1",
"react-native-camera": "^1.9.2",
"react-native-camera": "^1.10.0",
"react-native-custom-qr-codes": "^2.0.0",
"react-native-device-info": "^0.25.1",
"react-native-device-info": "^0.26.1",
"react-native-elements": "^0.19.0",
"react-native-flexi-radio-button": "^0.2.2",
"react-native-fs": "^2.13.3",
"react-native-gesture-handler": "^1.0.15",
"react-native-google-analytics-bridge": "^7.0.0",
"react-native-haptic-feedback": "^1.4.2",
"react-native-haptic-feedback": "^1.5.0",
"react-native-image-picker": "^0.28.0",
"react-native-level-fs": "^3.0.1",
"react-native-linear-gradient": "^2.5.3",
@ -79,19 +81,20 @@
"react-native-qrcode": "^0.2.7",
"react-native-randombytes": "^3.5.2",
"react-native-rate": "^1.1.6",
"react-native-sentry": "^0.40.2",
"react-native-sentry": "^0.41.1",
"react-native-snap-carousel": "^3.7.5",
"react-native-sortable-list": "0.0.22",
"react-native-svg": "^9.0.4",
"react-native-svg": "^9.1.1",
"react-native-tcp": "^3.3.0",
"react-native-vector-icons": "^6.2.0",
"react-native-webview": "^3.2.1",
"react-native-webview": "4.1.0",
"react-native-wkwebview-reborn": "^2.0.0",
"react-navigation": "^3.0.9",
"react-navigation": "^3.1.2",
"react-test-render": "^1.1.1",
"readable-stream": "^1.1.14",
"readable-stream": "^3.1.1",
"request-promise-native": "^1.0.5",
"secure-random": "^1.1.1",
"stream-browserify": "^1.0.0",
"stream-browserify": "^2.0.2",
"util": "^0.11.1",
"wif": "^2.0.1"
},

View file

@ -495,7 +495,7 @@ export default class Browser extends Component {
Browser.propTypes = {
navigation: PropTypes.shape({
getParam: PropTypes.function,
getParam: PropTypes.func,
navigate: PropTypes.func,
}),
};

View file

@ -103,6 +103,7 @@ export default class LNDCreateInvoice extends Component {
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
onSubmitEditing={Keyboard.dismiss}
/>
</View>
{this.renderCreateButton()}
@ -116,7 +117,7 @@ export default class LNDCreateInvoice extends Component {
LNDCreateInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
}),

View file

@ -82,8 +82,8 @@ export default class LNDViewAdditionalInvoiceInformation extends Component {
LNDViewAdditionalInvoiceInformation.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
dismiss: PropTypes.func,
}),
};

View file

@ -236,9 +236,9 @@ export default class LNDViewInvoice extends Component {
LNDViewInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
getParam: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
dismiss: PropTypes.func,
}),
};

View file

@ -83,10 +83,10 @@ export default class ManageFunds extends Component {
ManageFunds.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
dismiss: PropTypes.function,
navigate: PropTypes.function,
getParam: PropTypes.function,
goBack: PropTypes.func,
dismiss: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
fromSecret: PropTypes.string,

View file

@ -72,12 +72,6 @@ export default class ScanLndInvoice extends React.Component {
processInvoice = data => {
this.setState({ isLoading: true }, async () => {
if (this.ignoreRead) return;
this.ignoreRead = true;
setTimeout(() => {
this.ignoreRead = false;
}, 6000);
if (!this.state.fromWallet) {
alert('Before paying a Lightning invoice, you must first add a Lightning wallet.');
return this.props.navigation.goBack();
@ -149,23 +143,20 @@ export default class ScanLndInvoice extends React.Component {
return alert(loc.lnd.sameWalletAsInvoiceError);
}
let start = +new Date();
let end;
try {
await fromWallet.payInvoice(this.state.invoice, this.state.decoded.num_satoshis);
end = +new Date();
} catch (Err) {
console.log(Err.message);
this.setState({ isLoading: false });
this.props.navigation.goBack();
return alert('Error');
return alert(Err.message);
}
console.log('payInvoice took', (end - start) / 1000, 'sec');
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
alert('Success');
this.props.navigation.goBack();
this.props.navigation.navigate('Success', {
amount: this.state.decoded.num_satoshis,
amountUnit: BitcoinUnit.SATS,
invoiceDescription: this.state.decoded.description,
});
},
);
}
@ -237,24 +228,26 @@ export default class ScanLndInvoice extends React.Component {
)}
<BlueSpacing20 />
<BlueSpacing20 />
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<BlueButton
icon={{
name: 'bolt',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
title={'Pay'}
onPress={() => {
this.pay();
}}
disabled={this.shouldDisablePayButton()}
/>
)}
<BlueCard>
{this.state.isLoading ? (
<View>
<ActivityIndicator />
</View>
) : (
<BlueButton
icon={{
name: 'bolt',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
title={'Pay'}
onPress={() => {
this.pay();
}}
disabled={this.shouldDisablePayButton()}
/>
)}
</BlueCard>
</BlueCard>
</SafeBlueArea>
</TouchableWithoutFeedback>
@ -264,10 +257,10 @@ export default class ScanLndInvoice extends React.Component {
ScanLndInvoice.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
getParam: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
uri: PropTypes.string,

View file

@ -79,7 +79,6 @@ export default class ReceiveDetails extends Component {
}
render() {
console.log('render() receive/details, address,secret=', this.state.address, ',', this.state.secret);
if (this.state.isLoading) {
return <BlueLoading />;
}
@ -128,8 +127,8 @@ export default class ReceiveDetails extends Component {
ReceiveDetails.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
navigate: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
address: PropTypes.string,

View file

@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
import { SegwitP2SHWallet, LegacyWallet, HDSegwitP2SHWallet } from '../class';
let BigNumber = require('bignumber.js');
let encryption = require('../encryption');
let bitcoin = require('bitcoinjs-lib');
let BlueElectrum = require('../BlueElectrum');
export default class Selftest extends Component {
static navigationOptions = () => ({
@ -42,6 +44,20 @@ export default class Selftest extends Component {
//
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
let addr4elect = '3GCvDBAktgQQtsbN6x5DYiQCMmgZ9Yk8BK';
let electrumBalance = await BlueElectrum.getBalanceByAddress(addr4elect);
if (electrumBalance.confirmed !== 51432)
throw new Error('BlueElectrum getBalanceByAddress failure, got ' + JSON.stringify(electrumBalance));
let electrumTxs = await BlueElectrum.getTransactionsByAddress(addr4elect);
if (electrumTxs.length !== 1) throw new Error('BlueElectrum getTransactionsByAddress failure, got ' + JSON.stringify(electrumTxs));
} else {
console.warn('skipping RN-specific test');
}
//
let l = new LegacyWallet();
l.setSecret('Kxr9tQED9H44gCmp6HAdmemAzU3n84H3dGkuWTKvE23JgHMW8gct');
if (l.getAddress() !== '19AAjaTUbRjQCMuVczepkoPswiZRhjtg31') {
@ -158,7 +174,6 @@ export default class Selftest extends Component {
];
let tx = l.createTx(utxo, 0.001, 0.0001, '1QHf8Gp3wfmFiSdEX4FtrssCGR68diN1cj');
let bitcoin = require('bitcoinjs-lib');
let txDecoded = bitcoin.Transaction.fromHex(tx);
let txid = txDecoded.getId();

View file

@ -105,15 +105,7 @@ export default class Confirm extends Component {
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.address}</Text>
<BlueSpacing40 />
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton
onPress={() => this.broadcast()}
title={loc.send.confirm.sendNow}
style={{ maxWidth: 263, paddingHorizontal: 56 }}
/>
)}
{this.state.isLoading ? <ActivityIndicator /> : <BlueButton onPress={() => this.broadcast()} title={loc.send.confirm.sendNow} />}
<TouchableOpacity
style={{ marginVertical: 24 }}
onPress={() =>
@ -154,10 +146,10 @@ const styles = StyleSheet.create({
Confirm.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
navigate: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.string,

View file

@ -108,10 +108,10 @@ const styles = StyleSheet.create({
SendCreate.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
navigate: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.string,

View file

@ -11,6 +11,7 @@ import {
StyleSheet,
Platform,
Slider,
AsyncStorage,
Text,
} from 'react-native';
import { Icon } from 'react-native-elements';
@ -71,13 +72,14 @@ export default class SendDetails extends Component {
fromAddress,
fromWallet,
fromSecret,
isLoading: true,
isLoading: false,
address,
memo,
fee: 1,
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
feeSliderValue: 1,
bip70TransactionExpiration: null,
renderWalletSelectionButtonHidden: false,
};
}
@ -91,7 +93,7 @@ export default class SendDetails extends Component {
const dataWithoutSchema = data.replace('bitcoin:', '');
if (btcAddressRx.test(dataWithoutSchema) || dataWithoutSchema.indexOf('bc1') === 0) {
this.setState({
address: data,
address: dataWithoutSchema,
bip70TransactionExpiration: null,
isLoading: false,
});
@ -126,20 +128,27 @@ export default class SendDetails extends Component {
};
async componentDidMount() {
let recommendedFees = await NetworkTransactionFees.recommendedFees().catch(response => {
this.setState({
fee: response.halfHourFee,
networkTransactionFees: response,
feeSliderValue: response.halfHourFee,
isLoading: false,
});
});
if (recommendedFees) {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
try {
const cachedNetworkTransactionFees = JSON.parse(await AsyncStorage.getItem(NetworkTransactionFee.StorageKey));
if (cachedNetworkTransactionFees && cachedNetworkTransactionFees.hasOwnProperty('halfHourFee')) {
this.setState({
fee: cachedNetworkTransactionFees.halfHourFee,
networkTransactionFees: cachedNetworkTransactionFees,
feeSliderValue: cachedNetworkTransactionFees.halfHourFee,
});
}
} catch (_) {}
let recommendedFees = await NetworkTransactionFees.recommendedFees();
if (recommendedFees && recommendedFees.hasOwnProperty('halfHourFee')) {
await AsyncStorage.setItem(NetworkTransactionFee.StorageKey, JSON.stringify(recommendedFees));
this.setState({
fee: recommendedFees.halfHourFee,
networkTransactionFees: recommendedFees,
feeSliderValue: recommendedFees.halfHourFee,
isLoading: false,
});
if (this.props.navigation.state.params.uri) {
@ -148,16 +157,34 @@ export default class SendDetails extends Component {
} else {
try {
const { address, amount, memo } = this.decodeBitcoinUri(this.props.navigation.getParam('uri'));
this.setState({ address, amount, memo });
this.setState({ address, amount, memo, isLoading: false });
} catch (error) {
console.log(error);
this.setState({ isLoading: false });
alert('Error: Unable to decode Bitcoin address');
}
}
} else {
this.setState({ isLoading: false });
}
} else {
this.setState({ isLoading: false });
}
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow = () => {
this.setState({ renderWalletSelectionButtonHidden: true });
};
_keyboardDidHide = () => {
this.setState({ renderWalletSelectionButtonHidden: false });
};
decodeBitcoinUri(uri) {
try {
let amount = '';
@ -382,9 +409,9 @@ export default class SendDetails extends Component {
}
onWalletSelect = wallet => {
this.setState({ fromAddress: wallet.getAddress(), fromSecret: wallet.getSecret(), fromWallet: wallet }, () =>
this.props.navigation.goBack(null),
);
this.setState({ fromAddress: wallet.getAddress(), fromSecret: wallet.getSecret(), fromWallet: wallet }, () => {
this.props.navigation.pop();
});
};
renderFeeSelectionModal = () => {
@ -392,7 +419,13 @@ export default class SendDetails extends Component {
<Modal
isVisible={this.state.isFeeSelectionModalVisible}
style={styles.bottomModal}
onBackdropPress={() => this.setState({ isFeeSelectionModalVisible: false })}
onBackdropPress={() => {
if (this.state.fee < 1 || this.state.feeSliderValue < 1) {
this.setState({ fee: Number(1), feeSliderValue: Number(1) });
}
Keyboard.dismiss();
this.setState({ isFeeSelectionModalVisible: false });
}}
>
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
<View style={styles.modalContent}>
@ -403,12 +436,14 @@ export default class SendDetails extends Component {
this.textInput = ref;
}}
value={this.state.fee.toString()}
onEndEditing={() => {
if (this.state.fee < 1 || this.state.feeSliderValue < 1) {
this.setState({ fee: Number(1), feeSliderValue: Number(1) });
}
}}
onChangeText={value => {
let newValue = value.replace(/\D/g, '');
if (newValue.length === 0) {
newValue = 1;
}
this.setState({ fee: newValue, feeSliderValue: newValue });
this.setState({ fee: Number(newValue), feeSliderValue: Number(newValue) });
}}
maxLength={9}
editable={!this.state.isLoading}
@ -466,6 +501,7 @@ export default class SendDetails extends Component {
};
renderWalletSelectionButton = () => {
if (this.state.renderWalletSelectionButtonHidden) return;
return (
<View style={{ marginBottom: 24, alignItems: 'center' }}>
{!this.state.isLoading && (
@ -500,7 +536,6 @@ export default class SendDetails extends Component {
</View>
);
}
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1, justifyContent: 'space-between' }}>
@ -556,6 +591,7 @@ export default class SendDetails extends Component {
numberOfLines={1}
style={{ flex: 1, marginHorizontal: 8, minHeight: 33 }}
editable={!this.state.isLoading}
onSubmitEditing={Keyboard.dismiss}
/>
</View>
<TouchableOpacity
@ -620,7 +656,7 @@ const styles = StyleSheet.create({
SendDetails.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
pop: PropTypes.func,
navigate: PropTypes.func,
getParam: PropTypes.func,
state: PropTypes.shape({

View file

@ -16,10 +16,12 @@ export default class CameraExample extends React.Component {
};
onBarCodeScanned(ret) {
console.warn(ret);
const onBarScanned = this.props.navigation.getParam('onBarScanned');
onBarScanned(ret.data);
this.props.navigation.goBack(null);
if (this.state.isLoading) return;
this.setState({ isLoading: true }, () => {
const onBarScanned = this.props.navigation.getParam('onBarScanned');
this.props.navigation.goBack();
onBarScanned(ret.data);
});
} // end
componentDidMount() {

View file

@ -18,7 +18,9 @@ export default class Success extends Component {
this.state = {
amount: props.navigation.getParam('amount'),
fee: props.navigation.getParam('fee'),
fee: props.navigation.getParam('fee') || 0,
amountUnit: props.navigation.getParam('amountUnit') || BitcoinUnit.BTC,
invoiceDescription: props.navigation.getParam('invoiceDescription') || '',
};
}
@ -51,21 +53,38 @@ export default class Success extends Component {
alignSelf: 'flex-end',
}}
>
{' ' + BitcoinUnit.BTC}
{' ' + this.state.amountUnit}
</Text>
</View>
<Text
style={{
color: '#37c0a1',
fontSize: 14,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '500',
alignSelf: 'center',
}}
>
{loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)}
</Text>
{this.state.fee > 0 && (
<Text
style={{
color: '#37c0a1',
fontSize: 14,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '500',
alignSelf: 'center',
}}
>
{loc.send.create.fee}: {loc.formatBalance(this.state.fee, BitcoinUnit.SATS)}
</Text>
)}
{this.state.fee <= 0 && (
<Text
numberOfLines={0}
style={{
color: '#37c0a1',
fontSize: 14,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '500',
alignSelf: 'center',
}}
>
{this.state.invoiceDescription}
</Text>
)}
</BlueCard>
<View
style={{
@ -87,7 +106,6 @@ export default class Success extends Component {
this.props.navigation.dismiss();
}}
title={loc.send.success.done}
style={{ maxWidth: 263, paddingHorizontal: 56 }}
/>
</BlueCard>
</SafeBlueArea>
@ -97,10 +115,10 @@ export default class Success extends Component {
Success.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
getParam: PropTypes.function,
navigate: PropTypes.function,
dismiss: PropTypes.function,
goBack: PropTypes.func,
getParam: PropTypes.func,
navigate: PropTypes.func,
dismiss: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
amount: PropTypes.string,

View file

@ -127,6 +127,14 @@ export default class About extends Component {
<BlueText h4>* bignumber.js</BlueText>
<BlueSpacing20 />
<BlueButton
onPress={() => {
this.props.navigation.navigate('ReleaseNotes');
}}
title="Release notes"
/>
<BlueSpacing20 />
<BlueButton
onPress={() => {
this.props.navigation.navigate('Selftest');
@ -135,8 +143,9 @@ export default class About extends Component {
/>
<BlueTextCentered />
<BlueTextCentered>
{DeviceInfo.getApplicationName()} ver {DeviceInfo.getVersion()} (build number {DeviceInfo.getBuildNumber()})
{DeviceInfo.getApplicationName()} ver {DeviceInfo.getVersion()} (build {DeviceInfo.getBuildNumber()})
</BlueTextCentered>
<BlueTextCentered>{new Date(DeviceInfo.getBuildNumber() * 1000).toGMTString()}</BlueTextCentered>
<BlueTextCentered>{DeviceInfo.getBundleId()}</BlueTextCentered>
<BlueTextCentered>
w, h = {width}, {height}

View file

@ -0,0 +1,51 @@
import React, { Component } from 'react';
import { ScrollView } from 'react-native';
import { BlueLoading, SafeBlueArea, BlueCard, BlueText, BlueNavigationStyle } from '../../BlueComponents';
import PropTypes from 'prop-types';
/** @type {AppStorage} */
const notes = require('../../release-notes');
export default class ReleaseNotes extends Component {
static navigationOptions = () => ({
...BlueNavigationStyle(),
title: 'Release notes',
});
constructor(props) {
super(props);
this.state = {
isLoading: true,
};
}
async componentDidMount() {
console.log(notes);
this.setState({
isLoading: false,
notes: notes,
});
}
render() {
if (this.state.isLoading) {
return <BlueLoading />;
}
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<ScrollView>
<BlueCard>
<BlueText>{this.state.notes}</BlueText>
</BlueCard>
</ScrollView>
</SafeBlueArea>
);
}
}
ReleaseNotes.propTypes = {
navigation: PropTypes.shape({
navigate: PropTypes.func,
goBack: PropTypes.func,
}),
};

View file

@ -234,7 +234,7 @@ export default class SendCreate extends Component {
SendCreate.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({

View file

@ -191,7 +191,7 @@ export default class RBF extends Component {
RBF.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({

View file

@ -89,22 +89,25 @@ export default class TransactionsDetails extends Component {
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1 }}>
<BlueHeaderDefaultSub leftText={loc.transactions.details.title} rightComponent={null} />
<ScrollView style={{ flex: 1 }}>
{(() => {
if (this.state.tx.confirmations === 0 && this.state.wallet && this.state.wallet.allowRBF()) {
return (
<BlueButton
onPress={() =>
this.props.navigation.navigate('RBF', {
txid: this.state.tx.hash,
})
}
title="Replace-By-Fee (RBF)"
/>
);
}
})()}
<BlueCard>
{(() => {
if (this.state.tx.confirmations === 0 && this.state.wallet && this.state.wallet.allowRBF()) {
return (
<React.Fragment>
<BlueButton
onPress={() =>
this.props.navigation.navigate('RBF', {
txid: this.state.tx.hash,
})
}
title="Replace-By-Fee (RBF)"
/>
<BlueSpacing20 />
</React.Fragment>
);
}
})()}
{(() => {
if (BlueApp.tx_metadata[this.state.tx.hash]) {
if (BlueApp.tx_metadata[this.state.tx.hash]['memo']) {
@ -176,14 +179,14 @@ export default class TransactionsDetails extends Component {
</React.Fragment>
)}
{this.state.tx.hasOwnProperty('block_height') && this.state.block_height > 0 && (
{this.state.tx.hasOwnProperty('block_height') && this.state.tx.block_height > 0 && (
<React.Fragment>
<BlueText style={{ fontSize: 16, fontWeight: '500', marginBottom: 4 }}>Block Height</BlueText>
<BlueText style={{ marginBottom: 26, color: 'grey' }}>{this.state.tx.block_height}</BlueText>
</React.Fragment>
)}
{this.state.tx.hasOwnProperty('confirmations') && (
{this.state.tx.hasOwnProperty('confirmations') && this.state.tx.confirmations > 0 && (
<React.Fragment>
<BlueText style={{ fontSize: 16, fontWeight: '500', marginBottom: 4 }}>Confirmations</BlueText>
<BlueText style={{ marginBottom: 26, color: 'grey' }}>{this.state.tx.confirmations}</BlueText>
@ -197,7 +200,7 @@ export default class TransactionsDetails extends Component {
</React.Fragment>
)}
{this.state.tx.hasOwnProperty('outputs') && (
{this.state.tx.hasOwnProperty('outputs') && this.state.tx.outputs.length > 0 && (
<React.Fragment>
<BlueText style={{ fontSize: 16, fontWeight: '500', marginBottom: 4 }}>Outputs</BlueText>
<BlueText style={{ marginBottom: 26, color: 'grey' }}>{this.state.tx.outputs.length}</BlueText>
@ -212,7 +215,7 @@ export default class TransactionsDetails extends Component {
TransactionsDetails.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
goBack: PropTypes.func,
navigate: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({

View file

@ -72,7 +72,7 @@ export default class WalletsAdd extends Component {
return (
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false} style={{ flex: 1 }}>
<BlueCard>
<BlueFormLabel>{loc.wallets.add.wallet_name}</BlueFormLabel>
<View

View file

@ -67,7 +67,6 @@ export default class BuyBitcoin extends Component {
}
render() {
console.log('render() receive/details, address,secret=', this.state.address, ',', this.state.secret);
if (this.state.isLoading) {
return <BlueLoading />;
}
@ -107,7 +106,7 @@ export default class BuyBitcoin extends Component {
BuyBitcoin.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.function,
goBack: PropTypes.func,
state: PropTypes.shape({
params: PropTypes.shape({
address: PropTypes.string,

View file

@ -215,38 +215,39 @@ export default class WalletsImport extends Component {
this.setLabel(text);
}}
/>
<BlueSpacing20 />
<View
style={{
alignItems: 'center',
}}
>
<BlueButton
disabled={!this.state.label}
title={loc.wallets.import.do_import}
buttonStyle={{
width: width / 1.5,
}}
onPress={async () => {
if (!this.state.label) {
return;
}
this.setState({ isLoading: true });
setTimeout(async () => {
await this.importMnemonic(this.state.label.trim());
this.setState({ isLoading: false });
}, 1);
}}
/>
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
this.props.navigation.navigate('ScanQrWif');
}}
/>
</View>
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
<BlueSpacing20 />
<View
style={{
alignItems: 'center',
}}
>
<BlueButton
disabled={!this.state.label}
title={loc.wallets.import.do_import}
buttonStyle={{
width: width / 1.5,
}}
onPress={async () => {
if (!this.state.label) {
return;
}
this.setState({ isLoading: true });
setTimeout(async () => {
await this.importMnemonic(this.state.label.trim());
this.setState({ isLoading: false });
}, 1);
}}
/>
<BlueButtonLink
title={loc.wallets.import.scan_qr}
onPress={() => {
this.props.navigation.navigate('ScanQrWif');
}}
/>
</View>
</SafeBlueArea>
);
}

View file

@ -1,20 +1,6 @@
import React, { Component } from 'react';
import { View, TouchableOpacity, Text, FlatList, RefreshControl, ScrollView } from 'react-native';
import {
BlueTransactionOnchainIcon,
BlueLoading,
SafeBlueArea,
WalletsCarousel,
BlueTransactionIncommingIcon,
BlueTransactionOutgoingIcon,
BlueTransactionPendingIcon,
BlueTransactionOffchainIcon,
BlueTransactionExpiredIcon,
BlueList,
BlueListItem,
BlueHeaderDefaultMain,
BlueTransactionOffchainIncomingIcon,
} from '../../BlueComponents';
import { BlueLoading, SafeBlueArea, WalletsCarousel, BlueList, BlueHeaderDefaultMain, BlueListTransactionItem } from '../../BlueComponents';
import { Icon } from 'react-native-elements';
import { NavigationEvents } from 'react-navigation';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
@ -82,7 +68,10 @@ export default class WalletsList extends Component {
// more responsive
let noErr = true;
try {
let balanceStart = +new Date();
await BlueApp.fetchWalletBalances(that.lastSnappedTo || 0);
let balanceEnd = +new Date();
console.log('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
let start = +new Date();
await BlueApp.fetchWalletTransactions(that.lastSnappedTo || 0);
let end = +new Date();
@ -229,60 +218,9 @@ export default class WalletsList extends Component {
}
};
rowTitle = item => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, item.walletPreferredBalanceUnit, true).toString();
} else if (invoiceExpiration < now) {
if (item.ispaid) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, item.walletPreferredBalanceUnit, true).toString();
} else {
return loc.lnd.expired;
}
}
} else {
return loc.formatBalanceWithoutSuffix(item.value && item.value, item.walletPreferredBalanceUnit, true).toString();
}
};
rowTitleStyle = item => {
let color = '#37c0a1';
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
color = '#37c0a1';
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = '#37c0a1';
} else {
color = '#FF0000';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueApp.settings.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 16,
color: color,
};
};
_renderItem = data => <BlueListTransactionItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} />;
render() {
const { navigate } = this.props.navigation;
if (this.state.isLoading) {
return <BlueLoading />;
}
@ -335,117 +273,7 @@ export default class WalletsList extends Component {
data={this.state.dataSource}
extraData={this.state.dataSource}
keyExtractor={this._keyExtractor}
renderItem={rowData => {
return (
<BlueListItem
avatar={(() => {
// is it lightning refill tx?
if (rowData.item.category === 'receive' && rowData.item.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
}
if (rowData.item.type && rowData.item.type === 'bitcoind_tx') {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
if (rowData.item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIcon />
</View>
);
}
if (rowData.item.type === 'user_invoice' || rowData.item.type === 'payment_request') {
if (!rowData.item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = rowData.item.timestamp + rowData.item.expire_time;
if (invoiceExpiration < now) {
return (
<View style={{ width: 25 }}>
<BlueTransactionExpiredIcon />
</View>
);
}
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIncomingIcon />
</View>
);
}
}
if (!rowData.item.confirmations) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else if (rowData.item.value < 0) {
return (
<View style={{ width: 25 }}>
<BlueTransactionOutgoingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionIncommingIcon />
</View>
);
}
})()}
title={loc.transactionTimeToReadable(rowData.item.received)}
subtitle={
(rowData.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + rowData.item.confirmations + ' ' : '') +
this.txMemo(rowData.item.hash) +
(rowData.item.memo || '')
}
onPress={() => {
if (rowData.item.hash) {
navigate('TransactionDetails', {
hash: rowData.item.hash,
});
} else if (
rowData.item.type === 'user_invoice' ||
rowData.item.type === 'payment_request' ||
rowData.item.type === 'paid_invoice'
) {
const lightningWallet = this.state.wallets.filter(wallet => {
if (typeof wallet === 'object') {
if (wallet.hasOwnProperty('secret')) {
return wallet.getSecret() === rowData.item.fromWallet;
}
}
});
this.props.navigation.navigate('LNDViewInvoice', {
invoice: rowData.item,
fromWallet: lightningWallet[0],
isModal: false,
});
}
}}
badge={{
value: 3,
textStyle: { color: 'orange' },
containerStyle: { marginTop: 0 },
}}
hideChevron
rightTitle={this.rowTitle(rowData.item)}
rightTitleStyle={this.rowTitleStyle(rowData.item)}
/>
);
}}
renderItem={this._renderItem}
/>
</BlueList>
</ScrollView>

View file

@ -28,6 +28,7 @@ export default class ScanQrWif extends React.Component {
};
async onBarCodeScanned(ret) {
this.setState({ isLoading: true });
if (+new Date() - this.lastTimeIveBeenHere < 6000) {
this.lastTimeIveBeenHere = +new Date();
return;
@ -57,16 +58,17 @@ export default class ScanQrWif extends React.Component {
ret.data = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed);
} catch (e) {
console.log(e.message);
this.setState({ message: false });
this.setState({ message: false, isLoading: false });
return alert(loc.wallets.scanQrWif.bad_password);
}
this.setState({ message: false });
this.setState({ message: false, isLoading: false });
}
for (let w of BlueApp.wallets) {
if (w.getSecret() === ret.data) {
// lookig for duplicates
this.setState({ isLoading: false });
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
@ -78,10 +80,10 @@ export default class ScanQrWif extends React.Component {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
this.setState({ isLoading: true });
await hd.fetchTransactions();
if (hd.getTransactions().length !== 0) {
await hd.fetchBalance();
@ -91,6 +93,7 @@ export default class ScanQrWif extends React.Component {
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
}
@ -103,6 +106,7 @@ export default class ScanQrWif extends React.Component {
for (let w of BlueApp.wallets) {
if (w.getSecret() === hd.getSecret()) {
// lookig for duplicates
this.setState({ isLoading: false });
return alert(loc.wallets.scanQrWif.wallet_already_exists); // duplicate, not adding
}
}
@ -115,6 +119,7 @@ export default class ScanQrWif extends React.Component {
alert(loc.wallets.import.success);
this.props.navigation.popToTop();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
@ -130,6 +135,7 @@ export default class ScanQrWif extends React.Component {
await lnd.fetchBalance();
} catch (Err) {
console.log(Err);
this.setState({ isLoading: false });
return;
}
@ -138,6 +144,7 @@ export default class ScanQrWif extends React.Component {
this.props.navigation.popToTop();
alert(loc.wallets.import.success);
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
@ -165,6 +172,7 @@ export default class ScanQrWif extends React.Component {
await watchOnly.fetchTransactions();
await BlueApp.saveToDisk();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
this.setState({ isLoading: false });
return;
}
// nope
@ -176,6 +184,7 @@ export default class ScanQrWif extends React.Component {
if (newWallet.getAddress() === false || newLegacyWallet.getAddress() === false) {
alert(loc.wallets.scanQrWif.bad_wif);
this.setState({ isLoading: false });
return;
}
@ -196,7 +205,7 @@ export default class ScanQrWif extends React.Component {
alert(loc.wallets.scanQrWif.imported_wif + ret.data + loc.wallets.scanQrWif.with_address + newWallet.getAddress());
}
await BlueApp.saveToDisk();
this.props.navigation.popToTop();
this.props.navigation.dismiss();
setTimeout(() => EV(EV.enum.WALLETS_COUNT_CHANGED), 500);
} // end
@ -210,7 +219,7 @@ export default class ScanQrWif extends React.Component {
render() {
if (this.state.isLoading) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
<View style={{ flex: 1, paddingTop: 20, justifyContent: 'center', alignContent: 'center' }}>
<ActivityIndicator />
</View>
);
@ -275,6 +284,7 @@ ScanQrWif.propTypes = {
navigation: PropTypes.shape({
goBack: PropTypes.func,
popToTop: PropTypes.func,
dismiss: PropTypes.func,
navigate: PropTypes.func,
}),
};

View file

@ -3,20 +3,7 @@ import { Text, View, Image, FlatList, RefreshControl, TouchableOpacity, StatusBa
import LinearGradient from 'react-native-linear-gradient';
import PropTypes from 'prop-types';
import { NavigationEvents } from 'react-navigation';
import {
BlueText,
BlueTransactionOnchainIcon,
ManageFundsBigButton,
BlueTransactionExpiredIcon,
BlueTransactionIncommingIcon,
BlueTransactionOutgoingIcon,
BlueTransactionPendingIcon,
BlueTransactionOffchainIcon,
BlueSendButtonIcon,
BlueReceiveButtonIcon,
BlueListItem,
BlueTransactionOffchainIncomingIcon,
} from '../../BlueComponents';
import { BlueText, ManageFundsBigButton, BlueSendButtonIcon, BlueReceiveButtonIcon, BlueTransactionListItem } from '../../BlueComponents';
import { Icon } from 'react-native-elements';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { LightningCustodianWallet } from '../../class';
@ -150,9 +137,12 @@ export default class WalletTransactions extends Component {
try {
/** @type {LegacyWallet} */
let wallet = that.state.wallet;
let balanceStart = +new Date();
const oldBalance = wallet.getBalance();
await wallet.fetchBalance();
if (oldBalance !== wallet.getBalance()) smthChanged = true;
let balanceEnd = +new Date();
console.log(wallet.getLabel(), 'fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
let start = +new Date();
const oldTxLen = wallet.getTransactions().length;
await wallet.fetchTransactions();
@ -269,13 +259,6 @@ export default class WalletTransactions extends Component {
);
};
txMemo(hash) {
if (BlueApp.tx_metadata[hash] && BlueApp.tx_metadata[hash]['memo']) {
return BlueApp.tx_metadata[hash]['memo'];
}
return '';
}
_keyExtractor = (_item, index) => index.toString();
renderListHeaderComponent = () => {
@ -305,55 +288,8 @@ export default class WalletTransactions extends Component {
this.onWillBlur();
}
rowTitle = item => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = 0;
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.state.wallet.getPreferredBalanceUnit(), true).toString();
} else if (invoiceExpiration < now) {
if (item.ispaid) {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.state.wallet.getPreferredBalanceUnit(), true).toString();
} else {
return loc.lnd.expired;
}
}
} else {
return loc.formatBalanceWithoutSuffix(item.value && item.value, this.state.wallet.getPreferredBalanceUnit(), true).toString();
}
};
rowTitleStyle = item => {
let color = '#37c0a1';
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = item.timestamp + item.expire_time;
if (invoiceExpiration > now) {
color = '#37c0a1';
} else if (invoiceExpiration < now) {
if (item.ispaid) {
color = '#37c0a1';
} else {
color = '#FF0000';
}
}
} else if (item.value / 100000000 < 0) {
color = BlueApp.settings.foregroundColor;
}
return {
fontWeight: '600',
fontSize: 16,
color: color,
};
renderItem = item => {
return <BlueTransactionListItem item={item.item} itemPriceUnit={this.state.wallet.getPreferredBalanceUnit()} />;
};
render() {
@ -413,8 +349,9 @@ export default class WalletTransactions extends Component {
<FlatList
ListHeaderComponent={this.renderListHeaderComponent}
ListEmptyComponent={
<View style={{ top: 50, minHeight: 200 }}>
<View style={{ top: 50, minHeight: 200, paddingHorizontal: 16 }}>
<Text
numberOfLines={0}
style={{
fontSize: 18,
color: '#9aa0aa',
@ -422,7 +359,7 @@ export default class WalletTransactions extends Component {
}}
>
{(this.isLightning() &&
'Lightning wallet should be used for your daily\ntransactions. Fees are unfairly cheap and\nspeed is blazing fast.') ||
'Lightning wallet should be used for your daily transactions. Fees are unfairly cheap and speed is blazing fast.') ||
loc.wallets.list.empty_txs1}
</Text>
<Text
@ -432,7 +369,7 @@ export default class WalletTransactions extends Component {
textAlign: 'center',
}}
>
{(this.isLightning() && '\nTo start using it tap on "manage funds"\nand topup your balance') ||
{(this.isLightning() && '\nTo start using it tap on "manage funds" and topup your balance') ||
loc.wallets.list.empty_txs2}
</Text>
@ -462,110 +399,8 @@ export default class WalletTransactions extends Component {
refreshControl={<RefreshControl onRefresh={() => this.refreshTransactions()} refreshing={this.state.isTransactionsLoading} />}
data={this.state.dataSource}
keyExtractor={this._keyExtractor}
renderItem={rowData => {
return (
<BlueListItem
avatar={(() => {
// is it lightning refill tx?
if (rowData.item.category === 'receive' && rowData.item.confirmations < 3) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
}
if (rowData.item.type && rowData.item.type === 'bitcoind_tx') {
return (
<View style={{ width: 25 }}>
<BlueTransactionOnchainIcon />
</View>
);
}
if (rowData.item.type === 'paid_invoice') {
// is it lightning offchain payment?
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIcon />
</View>
);
}
if (rowData.item.type === 'user_invoice' || rowData.item.type === 'payment_request') {
if (!rowData.item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0;
const invoiceExpiration = rowData.item.timestamp + rowData.item.expire_time;
if (invoiceExpiration < now) {
return (
<View style={{ width: 25 }}>
<BlueTransactionExpiredIcon />
</View>
);
}
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionOffchainIncomingIcon />
</View>
);
}
}
if (!rowData.item.confirmations) {
return (
<View style={{ width: 25 }}>
<BlueTransactionPendingIcon />
</View>
);
} else if (rowData.item.value < 0) {
return (
<View style={{ width: 25 }}>
<BlueTransactionOutgoingIcon />
</View>
);
} else {
return (
<View style={{ width: 25 }}>
<BlueTransactionIncommingIcon />
</View>
);
}
})()}
title={loc.transactionTimeToReadable(rowData.item.received)}
subtitle={
(rowData.item.confirmations < 7 ? loc.transactions.list.conf + ': ' + rowData.item.confirmations + ' ' : '') +
this.txMemo(rowData.item.hash) +
(rowData.item.memo || '')
}
onPress={() => {
if (rowData.item.hash) {
navigate('TransactionDetails', {
hash: rowData.item.hash,
});
} else if (
rowData.item.type === 'user_invoice' ||
rowData.item.type === 'payment_request' ||
rowData.item.type === 'paid_invoice'
) {
this.props.navigation.navigate('LNDViewInvoice', {
invoice: rowData.item,
fromWallet: this.state.wallet,
isModal: false,
});
}
}}
badge={{
value: 3,
textStyle: { color: 'orange' },
containerStyle: { marginTop: 0 },
}}
hideChevron
rightTitle={this.rowTitle(rowData.item)}
rightTitleStyle={this.rowTitleStyle(rowData.item)}
/>
);
}}
initialNumToRender={10}
renderItem={this.renderItem}
/>
</View>
<View

View file

@ -1,4 +1,5 @@
/* global __DEV__, localStorage */
global.net = require('react-native-tcp');
if (typeof __dirname === 'undefined') global.__dirname = '/';
if (typeof __filename === 'undefined') global.__filename = '';
if (typeof process === 'undefined') {