diff --git a/.flowconfig b/.flowconfig index 9bded78b..ebf6585f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -67,4 +67,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError [version] -^0.86.0 +^0.97.0 diff --git a/.gitignore b/.gitignore index 04555cfa..4e6fc7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,6 @@ buck-out/ #BlueWallet release-notes.json -release-notes.txt \ No newline at end of file +release-notes.txt + +ios/Pods/ diff --git a/App.js b/App.js index 068f3641..67c4c65d 100644 --- a/App.js +++ b/App.js @@ -1,5 +1,6 @@ import React from 'react'; -import { Linking, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View, AsyncStorage } from 'react-native'; +import { Linking, AppState, Clipboard, StyleSheet, KeyboardAvoidingView, Platform, View } from 'react-native'; +import AsyncStorage from '@react-native-community/async-storage'; import Modal from 'react-native-modal'; import { NavigationActions } from 'react-navigation'; import MainBottomTabs from './MainBottomTabs'; diff --git a/App.test.js b/App.test.js index 971c583f..b150b91e 100644 --- a/App.test.js +++ b/App.test.js @@ -5,13 +5,11 @@ import TestRenderer from 'react-test-renderer'; import Settings from './screen/settings/settings'; import Selftest from './screen/selftest'; import { BlueHeader } from './BlueComponents'; -import MockStorage from './MockStorage'; import { FiatUnit } from './models/fiatUnit'; +import AsyncStorage from '@react-native-community/async-storage'; global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment let assert = require('assert'); jest.mock('react-native-qrcode-svg', () => 'Video'); -const AsyncStorage = new MockStorage(); -jest.setMock('AsyncStorage', AsyncStorage); jest.useFakeTimers(); jest.mock('Picker', () => { // eslint-disable-next-line import/no-unresolved @@ -105,7 +103,6 @@ it('Selftest work', () => { }); it('Appstorage - loadFromDisk works', async () => { - AsyncStorage.storageCache = {}; // cleanup from other tests /** @type {AppStorage} */ let Storage = new AppStorage(); let w = new SegwitP2SHWallet(); @@ -125,16 +122,14 @@ it('Appstorage - loadFromDisk works', async () => { // emulating encrypted storage (and testing flag) - AsyncStorage.storageCache.data = false; - AsyncStorage.storageCache.data_encrypted = '1'; // flag + await AsyncStorage.setItem('data', false); + await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, '1'); let Storage3 = new AppStorage(); isEncrypted = await Storage3.storageIsEncrypted(); assert.ok(isEncrypted); }); it('Appstorage - encryptStorage & load encrypted storage works', async () => { - AsyncStorage.storageCache = {}; // cleanup from other tests - /** @type {AppStorage} */ let Storage = new AppStorage(); let w = new SegwitP2SHWallet(); @@ -236,7 +231,7 @@ it('Wallet can fetch balance', async () => { assert.ok(w.getUnconfirmedBalance() === 0); assert.ok(w._lastBalanceFetch === 0); await w.fetchBalance(); - assert.ok(w.getBalance() === 0.18262); + assert.ok(w.getBalance() === 18262000); assert.ok(w.getUnconfirmedBalance() === 0); assert.ok(w._lastBalanceFetch > 0); }); @@ -302,19 +297,18 @@ it('Wallet can fetch TXs', async () => { describe('currency', () => { it('fetches exchange rate and saves to AsyncStorage', async () => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; - AsyncStorage.storageCache = {}; // cleanup from other tests let currency = require('./currency'); await currency.startUpdater(); - let cur = AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES]; + let cur = await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES); cur = JSON.parse(cur); assert.ok(Number.isInteger(cur[currency.STRUCT.LAST_UPDATED])); assert.ok(cur[currency.STRUCT.LAST_UPDATED] > 0); assert.ok(cur['BTC_USD'] > 0); // now, setting other currency as default - AsyncStorage.storageCache[AppStorage.PREFERRED_CURRENCY] = JSON.stringify(FiatUnit.JPY); + await AsyncStorage.setItem(AppStorage.PREFERRED_CURRENCY, JSON.stringify(FiatUnit.JPY)); await currency.startUpdater(); - cur = JSON.parse(AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES]); + cur = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES)); assert.ok(cur['BTC_JPY'] > 0); // now setting with a proper setter @@ -322,7 +316,7 @@ describe('currency', () => { await currency.startUpdater(); let preferred = await currency.getPreferredCurrency(); assert.strictEqual(preferred.endPointKey, 'EUR'); - cur = JSON.parse(AsyncStorage.storageCache[AppStorage.EXCHANGE_RATES]); + cur = JSON.parse(await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES)); assert.ok(cur['BTC_EUR'] > 0); }); }); diff --git a/BlueApp.js b/BlueApp.js index 99feb095..ae8afb73 100644 --- a/BlueApp.js +++ b/BlueApp.js @@ -10,7 +10,7 @@ let A = require('./analytics'); let BlueElectrum = require('./BlueElectrum'); // eslint-disable-line /** @type {AppStorage} */ -let BlueApp = new AppStorage(); +const BlueApp = new AppStorage(); async function startAndDecrypt(retry) { console.log('startAndDecrypt'); diff --git a/BlueComponents.js b/BlueComponents.js index 9b960438..c7a728dd 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -25,7 +25,6 @@ import { import LinearGradient from 'react-native-linear-gradient'; import { LightningCustodianWallet } from './class'; import Carousel from 'react-native-snap-carousel'; -import DeviceInfo from 'react-native-device-info'; import { BitcoinUnit } from './models/bitcoinUnits'; import NavigationService from './NavigationService'; import ImagePicker from 'react-native-image-picker'; @@ -36,6 +35,7 @@ let loc = require('./loc/'); let BlueApp = require('./BlueApp'); const { height, width } = Dimensions.get('window'); const aspectRatio = height / width; +const BigNumber = require('bignumber.js'); let isIpad; if (aspectRatio > 1.6) { isIpad = false; @@ -241,6 +241,14 @@ export class BlueCopyTextToClipboard extends Component { this.state = { hasTappedText: false, address: props.text }; } + static getDerivedStateFromProps(props, state) { + if (state.hasTappedText) { + return { hasTappedText: state.hasTappedText, address: state.address }; + } else { + return { hasTappedText: state.hasTappedText, address: props.text }; + } + } + copyToClipboard = () => { this.setState({ hasTappedText: true }, () => { Clipboard.setString(this.props.text); @@ -404,29 +412,6 @@ export class BlueFormMultiInput extends Component { } } -export class BlueFormInputAddress extends Component { - render() { - return ( - - ); - } -} - export class BlueHeader extends Component { render() { return ( @@ -560,13 +545,6 @@ export class is { static ipad() { return isIpad; } - - static iphone8() { - if (Platform.OS !== 'ios') { - return false; - } - return DeviceInfo.getDeviceId() === 'iPhone10,4'; - } } export class BlueSpacing20 extends Component { @@ -1733,7 +1711,7 @@ export class BlueAddressInput extends Component { export class BlueBitcoinAmount extends Component { static propTypes = { isLoading: PropTypes.bool, - amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), onChangeText: PropTypes.func, disabled: PropTypes.bool, unit: PropTypes.string, @@ -1744,8 +1722,15 @@ export class BlueBitcoinAmount extends Component { }; render() { - const amount = typeof this.props.amount === 'number' ? this.props.amount.toString() : this.props.amount; - + const amount = this.props.amount || 0; + let localCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false); + if (this.props.unit === BitcoinUnit.BTC) { + let sat = new BigNumber(amount); + sat = sat.multipliedBy(100000000).toString(); + localCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false); + } else { + localCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false); + } return ( this.textInput.focus()}> @@ -1788,13 +1773,7 @@ export class BlueBitcoinAmount extends Component { - - {loc.formatBalance( - this.props.unit === BitcoinUnit.BTC ? amount || 0 : loc.formatBalanceWithoutSuffix(amount || 0, BitcoinUnit.BTC, false), - BitcoinUnit.LOCAL_CURRENCY, - false, - )} - + {localCurrency} diff --git a/BlueElectrum.js b/BlueElectrum.js index 3f0524bd..7c30bea9 100644 --- a/BlueElectrum.js +++ b/BlueElectrum.js @@ -1,10 +1,10 @@ -import { AsyncStorage } from 'react-native'; +import AsyncStorage from '@react-native-community/async-storage'; const ElectrumClient = require('electrum-client'); let bitcoin = require('bitcoinjs-lib'); let reverse = require('buffer-reverse'); const storageKey = 'ELECTRUM_PEERS'; -const defaultPeer = { host: 'electrum1.bluewallet.io', tcp: 50001 }; +const defaultPeer = { host: 'electrum1.bluewallet.io', tcp: '50001' }; const hardcodedPeers = [ // { host: 'noveltybobble.coinjoined.com', tcp: '50001' }, // down // { host: 'electrum.be', tcp: '50001' }, @@ -170,8 +170,8 @@ async function waitTillConnected() { async function estimateFees() { if (!mainClient) throw new Error('Electrum client is not connected'); const fast = await mainClient.blockchainEstimatefee(1); - const medium = await mainClient.blockchainEstimatefee(6); - const slow = await mainClient.blockchainEstimatefee(12); + const medium = await mainClient.blockchainEstimatefee(5); + const slow = await mainClient.blockchainEstimatefee(10); return { fast, medium, slow }; } diff --git a/Electrum.test.js b/Electrum.test.js index f3f53844..09403ba9 100644 --- a/Electrum.test.js +++ b/Electrum.test.js @@ -14,8 +14,8 @@ beforeAll(async () => { // while app starts up, but for tests we need to wait for it try { await BlueElectrum.waitTillConnected(); - } catch (Err) { - console.log('failed to connect to Electrum:', Err); + } catch (err) { + console.log('failed to connect to Electrum:', err); process.exit(1); } }); @@ -52,7 +52,6 @@ describe('Electrum', () => { 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); diff --git a/HDWallet.test.js b/HDWallet.test.js index acb84c0a..df76ae23 100644 --- a/HDWallet.test.js +++ b/HDWallet.test.js @@ -207,7 +207,7 @@ it('Segwit HD (BIP49) can fetch balance with many used addresses in hierarchy', 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); + assert.strictEqual(hd.getBalance(), 51432); await hd.fetchUtxo(); assert.ok(hd.utxo.length > 0); diff --git a/WatchConnectivity.js b/WatchConnectivity.js new file mode 100644 index 00000000..2efe80dd --- /dev/null +++ b/WatchConnectivity.js @@ -0,0 +1,138 @@ +import * as watch from 'react-native-watch-connectivity'; +import { InteractionManager } from 'react-native'; +const loc = require('./loc'); +export default class WatchConnectivity { + isAppInstalled = false; + BlueApp = require('./BlueApp'); + + constructor() { + this.getIsWatchAppInstalled(); + } + + getIsWatchAppInstalled() { + watch.getIsWatchAppInstalled((err, isAppInstalled) => { + if (!err) { + this.isAppInstalled = isAppInstalled; + this.sendWalletsToWatch(); + } + }); + watch.subscribeToMessages(async (err, message, reply) => { + if (!err) { + if (message.request === 'createInvoice') { + const createInvoiceRequest = await this.handleLightningInvoiceCreateRequest( + message.walletIndex, + message.amount, + message.description, + ); + reply({ invoicePaymentRequest: createInvoiceRequest }); + } + } else { + reply(err); + } + }); + } + + async handleLightningInvoiceCreateRequest(walletIndex, amount, description) { + const wallet = this.BlueApp.getWallets()[walletIndex]; + if (wallet.allowReceive() && amount > 0 && description.trim().length > 0) { + try { + const invoiceRequest = await wallet.addInvoice(amount, description); + return invoiceRequest; + } catch (error) { + return error; + } + } + } + + async sendWalletsToWatch() { + InteractionManager.runAfterInteractions(async () => { + if (this.isAppInstalled) { + const allWallets = this.BlueApp.getWallets(); + let wallets = []; + for (const wallet of allWallets) { + let receiveAddress = ''; + if (wallet.allowReceive()) { + if (wallet.getAddressAsync) { + receiveAddress = await wallet.getAddressAsync(); + } else { + receiveAddress = wallet.getAddress(); + } + } + let transactions = wallet.getTransactions(10); + let watchTransactions = []; + for (const transaction of transactions) { + let type = 'pendingConfirmation'; + let memo = ''; + let amount = 0; + + if (transaction.hasOwnProperty('confirmations') && !transaction.confirmations > 0) { + type = 'pendingConfirmation'; + } else if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') { + const currentDate = new Date(); + const now = (currentDate.getTime() / 1000) | 0; + const invoiceExpiration = transaction.timestamp + transaction.expire_time; + + if (invoiceExpiration > now) { + type = 'pendingConfirmation'; + } else if (invoiceExpiration < now) { + if (transaction.ispaid) { + type = 'received'; + } else { + type = 'sent'; + } + } + } else if (transaction.value / 100000000 < 0) { + type = 'sent'; + } else { + type = 'received'; + } + if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') { + if (isNaN(transaction.value)) { + amount = '0'; + } + const currentDate = new Date(); + const now = (currentDate.getTime() / 1000) | 0; + const invoiceExpiration = transaction.timestamp + transaction.expire_time; + + if (invoiceExpiration > now) { + amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); + } else if (invoiceExpiration < now) { + if (transaction.ispaid) { + amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); + } else { + amount = loc.lnd.expired; + } + } else { + amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); + } + } else { + amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); + } + if (this.BlueApp.tx_metadata[transaction.hash] && this.BlueApp.tx_metadata[transaction.hash]['memo']) { + memo = this.BlueApp.tx_metadata[transaction.hash]['memo']; + } else if (transaction.memo) { + memo = transaction.memo; + } + const watchTX = { type, amount, memo, time: loc.transactionTimeToReadable(transaction.received) }; + watchTransactions.push(watchTX); + } + wallets.push({ + label: wallet.getLabel(), + balance: loc.formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true), + type: wallet.type, + preferredBalanceUnit: wallet.getPreferredBalanceUnit(), + receiveAddress: receiveAddress, + transactions: watchTransactions, + }); + } + + watch.updateApplicationContext({ wallets }); + } + }); + } +} + +WatchConnectivity.init = function() { + if (WatchConnectivity.shared) return; + WatchConnectivity.shared = new WatchConnectivity(); +}; diff --git a/__mocks__/@react-native-community/async-storage.js b/__mocks__/@react-native-community/async-storage.js new file mode 100644 index 00000000..272ea598 --- /dev/null +++ b/__mocks__/@react-native-community/async-storage.js @@ -0,0 +1 @@ +export default from '@react-native-community/async-storage/jest/async-storage-mock' diff --git a/android/app/app.iml b/android/app/app.iml index dd3e70e3..2afd4f72 100644 --- a/android/app/app.iml +++ b/android/app/app.iml @@ -17,7 +17,7 @@