From f5dd8252e110a8cd6bb5b94cec3e6e66741fabdb Mon Sep 17 00:00:00 2001 From: Igor Korsakov Date: Sat, 20 Oct 2018 22:10:21 +0100 Subject: [PATCH] Development (#103) ADD: New send screen ADD: Support for BIP70 decoding --- .eslintrc | 6 +- BlueComponents.js | 23 +- MainBottomTabs.js | 35 +- bip70/bip70.js | 74 ++++ class/abstract-hd-wallet.js | 4 +- class/hd-segwit-p2sh-wallet.js | 6 +- class/legacy-wallet.js | 8 +- class/lightning-custodian-wallet.js | 2 +- class/segwit-p2sh-wallet.js | 4 +- currency.js | 6 +- loc/en.js | 18 +- loc/es.js | 11 +- loc/index.js | 44 ++- loc/pt_BR.js | 11 +- loc/pt_PT.js | 11 +- loc/ru.js | 11 +- loc/ua.js | 11 +- models/bitcoinUnits.js | 6 + models/networkTransactionFees.js | 30 ++ package-lock.json | 39 ++- package.json | 4 +- screen/selftest.js | 4 +- screen/send/create.js | 279 +++++---------- screen/send/details.js | 512 ++++++++++++++++++++++------ screen/send/scanQrAddress.js | 2 +- screen/transactions/RBF-create.js | 8 +- screen/wallets/add.js | 36 +- screen/wallets/list.js | 2 +- screen/wallets/transactions.js | 44 ++- 29 files changed, 847 insertions(+), 404 deletions(-) create mode 100644 bip70/bip70.js create mode 100644 models/bitcoinUnits.js create mode 100644 models/networkTransactionFees.js diff --git a/.eslintrc b/.eslintrc index 980ecf46..4bf98c89 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,5 +13,9 @@ trailingComma: 'all' } ] - } + }, + "env":{ + "es6": true + }, + "globals": { "fetch": false } } diff --git a/BlueComponents.js b/BlueComponents.js index 46537fba..bae66251 100644 --- a/BlueComponents.js +++ b/BlueComponents.js @@ -33,13 +33,13 @@ export class BlueButton extends Component { delayPressIn={0} {...this.props} style={{ - marginTop: 20, borderWidth: 0.7, borderColor: 'transparent', }} buttonStyle={Object.assign( { backgroundColor: '#ccddf9', + minHeight: 45, height: 45, borderWidth: 0, borderRadius: 25, @@ -668,8 +668,8 @@ export class BlueReceiveButtonIcon extends Component { style={{ flex: 1, flexDirection: 'row', - width: 110, - height: 40, + minWidth: 110, + minHeight: 40, position: 'relative', backgroundColor: '#ccddf9', alignItems: 'center', @@ -677,9 +677,8 @@ export class BlueReceiveButtonIcon extends Component { > {loc.lnd.title} diff --git a/MainBottomTabs.js b/MainBottomTabs.js index e02b4781..1a754672 100644 --- a/MainBottomTabs.js +++ b/MainBottomTabs.js @@ -55,6 +55,25 @@ const WalletsStackNavigator = createStackNavigator({ }, }); +const CreateTransactionStackNavigator = createStackNavigator({ + SendDetails: { + screen: sendDetails, + }, + ScanQrAddress: { + screen: sendScanQrAddress, + }, + CreateTransaction: { + screen: sendCreate, + navigationOptions: { + headerStyle: { + backgroundColor: '#FFFFFF', + borderBottomWidth: 0, + }, + headerTintColor: '#0c2550', + }, + }, +}); + const Tabs = createStackNavigator( { Wallets: { @@ -83,6 +102,12 @@ const Tabs = createStackNavigator( screen: WalletExport, }, // + SendDetails: { + screen: CreateTransactionStackNavigator, + navigationOptions: { + header: null, + }, + }, TransactionDetails: { screen: details, @@ -102,16 +127,6 @@ const Tabs = createStackNavigator( // - SendDetails: { - screen: sendDetails, - }, - ScanQrAddress: { - screen: sendScanQrAddress, - }, - CreateTransaction: { - screen: sendCreate, - }, - // LND: ManageFunds: { diff --git a/bip70/bip70.js b/bip70/bip70.js new file mode 100644 index 00000000..b39834a4 --- /dev/null +++ b/bip70/bip70.js @@ -0,0 +1,74 @@ +import Frisbee from 'frisbee'; + +export class BitcoinBIP70Transaction { + constructor(amount, address, memo, fee, expires) { + this.amount = amount; + this.address = address; + this.memo = memo; + this.fee = fee; + this.expires = expires; + } +} + +export class BitcoinBIP70TransactionError { + constructor(errorMessage) { + this.errorMessage = errorMessage; + } +} + +export default class BitcoinBIP70TransactionDecode { + static decode(data) { + return new Promise(async (resolve, reject) => { + try { + const url = data.match(/bitcoin:\?r=https?:\/\/\S+/gi); + const api = new Frisbee({ + baseURI: url.toString().split('bitcoin:?r=')[1], + headers: { + Accept: 'application/payment-request', + }, + }); + let response = await api.get(); + if (response && response.body) { + const parsedJSON = JSON.parse(response.body); + + // Check that the invoice has not expired + const expires = new Date(parsedJSON.expires).getTime(); + const now = new Date().getTime(); + if (now > expires) { + throw new BitcoinBIP70TransactionError('This invoice has expired.'); + } + // + + const decodedTransaction = new BitcoinBIP70Transaction( + parsedJSON.outputs[0].amount, + parsedJSON.outputs[0].address, + parsedJSON.memo, + parsedJSON.requiredFeeRate.toFixed(0), + parsedJSON.expires, + ); + console.log(decodedTransaction); + resolve(decodedTransaction); + } else { + console.log('Could not fetch transaction details: ' + response.err); + throw new BitcoinBIP70TransactionError('Unable to fetch transaction details. Please, make sure the provided link is valid.'); + } + } catch (err) { + console.warn(err); + reject(err); + } + }); + } + + static isExpired(transactionExpires) { + if (transactionExpires === null) { + return false; + } + const expires = new Date(transactionExpires).getTime(); + const now = new Date().getTime(); + return now > expires; + } + + static matchesPaymentURL(data) { + return data !== null && data.match(/bitcoin:\?r=https?:\/\/\S+/gi) !== null; + } +} diff --git a/class/abstract-hd-wallet.js b/class/abstract-hd-wallet.js index 4a9bc25e..f3cc683f 100644 --- a/class/abstract-hd-wallet.js +++ b/class/abstract-hd-wallet.js @@ -345,14 +345,14 @@ export class AbstractHDWallet extends LegacyWallet { } // no luck - lets iterate over all addressess we have up to first unused address index - for (let c = 0; c <= this.next_free_change_address_index; c++) { + for (let c = 0; c <= this.next_free_change_address_index + 3; c++) { let possibleAddress = this._getInternalAddressByIndex(c); if (possibleAddress === address) { return (this._address_to_wif_cache[address] = this._getInternalWIFByIndex(c)); } } - for (let c = 0; c <= this.next_free_address_index; c++) { + for (let c = 0; c <= this.next_free_address_index + 3; c++) { let possibleAddress = this._getExternalAddressByIndex(c); if (possibleAddress === address) { return (this._address_to_wif_cache[address] = this._getExternalWIFByIndex(c)); diff --git a/class/hd-segwit-p2sh-wallet.js b/class/hd-segwit-p2sh-wallet.js index 2e3b6b5a..4bd83b8b 100644 --- a/class/hd-segwit-p2sh-wallet.js +++ b/class/hd-segwit-p2sh-wallet.js @@ -59,8 +59,8 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { this.unconfirmed_balance += addr.unconfirmed; this.usedAddresses.push(addr.addr); } - this.balance = new BigNumber(this.balance).div(100000000).toString() * 1; - this.unconfirmed_balance = new BigNumber(this.unconfirmed_balance).div(100000000).toString() * 1; + 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); @@ -344,7 +344,7 @@ export class HDSegwitP2SHWallet extends AbstractHDWallet { utxo.wif = this._getWifForAddress(utxo.address); } - let amountPlusFee = parseFloat(new BigNumber(amount).add(fee).toString(10)); + let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); return signer.createHDSegwitTransaction( utxos, address, diff --git a/class/legacy-wallet.js b/class/legacy-wallet.js index 30e7f5bb..29cdc6bd 100644 --- a/class/legacy-wallet.js +++ b/class/legacy-wallet.js @@ -110,9 +110,9 @@ export class LegacyWallet extends AbstractWallet { } this.balance = new BigNumber(json.final_balance); - this.balance = this.balance.div(100000000).toString() * 1; + this.balance = this.balance.dividedBy(100000000).toString() * 1; this.unconfirmed_balance = new BigNumber(json.unconfirmed_balance); - this.unconfirmed_balance = this.unconfirmed_balance.div(100000000).toString() * 1; + this.unconfirmed_balance = this.unconfirmed_balance.dividedBy(100000000).toString() * 1; this._lastBalanceFetch = +new Date(); } catch (err) { console.warn(err); @@ -457,11 +457,11 @@ export class LegacyWallet extends AbstractWallet { u.txid = u.tx_hash; u.vout = u.tx_output_n; u.amount = new BigNumber(u.value); - u.amount = u.amount.div(100000000); + u.amount = u.amount.dividedBy(100000000); u.amount = u.amount.toString(10); } // console.log('creating legacy tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress()); - let amountPlusFee = parseFloat(new BigNumber(amount).add(fee).toString(10)); + let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); return signer.createTransaction(utxos, toAddress, amountPlusFee, fee, this.getSecret(), this.getAddress()); } diff --git a/class/lightning-custodian-wallet.js b/class/lightning-custodian-wallet.js index f97e4c53..2cbe6758 100644 --- a/class/lightning-custodian-wallet.js +++ b/class/lightning-custodian-wallet.js @@ -368,7 +368,7 @@ export class LightningCustodianWallet extends LegacyWallet { } getBalance() { - return new BigNumber(this.balance).div(100000000).toString(10); + return new BigNumber(this.balance).dividedBy(100000000).toString(10); } async fetchBalance(noRetry) { diff --git a/class/segwit-p2sh-wallet.js b/class/segwit-p2sh-wallet.js index 5c039db8..172e0c20 100644 --- a/class/segwit-p2sh-wallet.js +++ b/class/segwit-p2sh-wallet.js @@ -62,11 +62,11 @@ export class SegwitP2SHWallet extends LegacyWallet { u.txid = u.tx_hash; u.vout = u.tx_output_n; u.amount = new BigNumber(u.value); - u.amount = u.amount.div(100000000); + u.amount = u.amount.dividedBy(100000000); u.amount = u.amount.toString(10); } // console.log('creating tx ', amount, ' with fee ', fee, 'secret=', this.getSecret(), 'from address', this.getAddress()); - let amountPlusFee = parseFloat(new BigNumber(amount).add(fee).toString(10)); + let amountPlusFee = parseFloat(new BigNumber(amount).plus(fee).toString(10)); // to compensate that module substracts fee from amount return signer.createSegwitTransaction(utxos, address, amountPlusFee, fee, this.getSecret(), this.getAddress(), sequence); } diff --git a/currency.js b/currency.js index dd5244e1..bdbea1f9 100644 --- a/currency.js +++ b/currency.js @@ -59,8 +59,8 @@ function satoshiToLocalCurrency(satoshi) { let b = new BigNumber(satoshi); b = b - .div(100000000) - .mul(lang[STRUCT.BTC_USD]) + .dividedBy(100000000) + .multipliedBy(lang[STRUCT.BTC_USD]) .toString(10); b = parseFloat(b).toFixed(2); @@ -69,7 +69,7 @@ function satoshiToLocalCurrency(satoshi) { function satoshiToBTC(satoshi) { let b = new BigNumber(satoshi); - b = b.div(100000000); + b = b.dividedBy(100000000); return b.toString(10) + ' BTC'; } diff --git a/loc/en.js b/loc/en.js index 242b472b..8f1d01ab 100644 --- a/loc/en.js +++ b/loc/en.js @@ -31,7 +31,7 @@ module.exports = { create: 'Create', label_new_segwit: 'New SegWit', wallet_name: 'wallet name', - wallet_type: 'wallet type', + wallet_type: 'type', or: 'or', import_wallet: 'Import wallet', imported: 'Imported', @@ -97,15 +97,17 @@ module.exports = { header: 'Send', details: { title: 'create transaction', - amount_fiels_is_not_valid: 'Amount field is not valid', - fee_fiels_is_not_valid: 'Fee field is not valid', - address_fiels_is_not_valid: 'Address field is not valid', - receiver_placeholder: 'receiver address here', + amount_field_is_not_valid: 'Amount field is not valid', + fee_field_is_not_valid: 'Fee field is not valid', + address_field_is_not_valid: 'Address field is not valid', + total_exceeds_balance: 'The sending amount exceeds the available balance.', + address: 'address', amount_placeholder: 'amount to send (in BTC)', fee_placeholder: 'plus transaction fee (in BTC)', - memo_placeholder: 'memo to self', + note_placeholder: 'note to self', cancel: 'Cancel', scan: 'Scan', + send: 'Send', create: 'Create', remaining_balance: 'Remaining balance', }, @@ -113,12 +115,12 @@ module.exports = { title: 'create transaction', error: 'Error creating transaction. Invalid address or send amount?', go_back: 'Go Back', - this_is_hex: 'This is transaction hex, signed and ready to be broadcast to the network. Continue?', + this_is_hex: 'This is transaction hex, signed and ready to be broadcast to the network.', to: 'To', amount: 'Amount', fee: 'Fee', tx_size: 'TX size', - satoshi_per_byte: 'satoshiPerByte', + satoshi_per_byte: 'Satoshi per byte', memo: 'Memo', broadcast: 'Broadcast', not_enough_fee: 'Not enough fee. Increase the fee', diff --git a/loc/es.js b/loc/es.js index c159b628..6dc2b58a 100644 --- a/loc/es.js +++ b/loc/es.js @@ -97,17 +97,20 @@ module.exports = { header: 'enviar', details: { title: 'Crear Transaccion', - amount_fiels_is_not_valid: 'La cantidad no es válida', - fee_fiels_is_not_valid: 'La tasa no es válida', - address_fiels_is_not_valid: 'La dirección no es válida', + amount_field_is_not_valid: 'La cantidad no es válida', + fee_field_is_not_valid: 'La tasa no es válida', + address_field_is_not_valid: 'La dirección no es válida', receiver_placeholder: 'La dirección de recipiente', amount_placeholder: 'cantidad para enviar (in BTC)', fee_placeholder: 'más tasa de transaccion (in BTC)', - memo_placeholder: 'comentario (para ti mismo)', + note_placeholder: 'comentario (para ti mismo)', cancel: 'Cancelar', scan: 'Escaniar', + address: 'Direccion', create: 'Crear', + send: 'Envíar', remaining_balance: 'Balance disponible', + total_exceeds_balance: 'El monto excede el balance disponible.', }, create: { title: 'Crear una Transaccion', diff --git a/loc/index.js b/loc/index.js index cab2b492..60e041e3 100644 --- a/loc/index.js +++ b/loc/index.js @@ -1,6 +1,7 @@ import Localization from 'react-localization'; import { AsyncStorage } from 'react-native'; import { AppStorage } from '../class'; +import { BitcoinUnit } from '../models/bitcoinUnits'; let BigNumber = require('bignumber.js'); let strings; @@ -61,12 +62,47 @@ strings.transactionTimeToReadable = function(time) { } }; -strings.formatBalance = function(balance) { - if (balance < 0.1 && balance !== 0) { +strings.formatBalance = (balance, unit) => { + const conversion = 100000000; + if (unit === undefined) { + if (balance < 0.1 && balance !== 0) { + let b = new BigNumber(balance); + return b.multipliedBy(1000).toString() + ' ' + BitcoinUnit.MBTC; + } + return balance + ' ' + BitcoinUnit.BTC; + } else { + if (balance !== 0) { + let b = new BigNumber(balance); + if (unit === BitcoinUnit.MBTC) { + return b.multipliedBy(1000).toString() + ' ' + BitcoinUnit.MBTC; + } else if (unit === BitcoinUnit.BITS) { + return b.multipliedBy(1000000).toString() + ' ' + BitcoinUnit.BITS; + } else if (unit === BitcoinUnit.SATOSHIS) { + return (b.times(conversion).toString() + ' ' + BitcoinUnit.SATOSHIS).replace(/\./g, ''); + } + } + return balance + ' ' + BitcoinUnit.BTC; + } +}; + +strings.formatBalanceWithoutSuffix = (balance, unit) => { + const conversion = 100000000; + if (balance !== 0) { let b = new BigNumber(balance); - return b.mul(1000).toString() + ' mBTC'; + if (unit === BitcoinUnit.BTC) { + return Number(b.div(conversion)); + } else if (unit === BitcoinUnit.MBTC) { + return b.multipliedBy(1000).toString(); + } else if (unit === BitcoinUnit.BITS) { + return b.multipliedBy(1000000).toString(); + } else if (unit === BitcoinUnit.SATOSHIS) { + return b + .times(conversion) + .toString() + .replace(/\./g, ''); + } } - return balance + ' BTC'; + return balance; }; module.exports = strings; diff --git a/loc/pt_BR.js b/loc/pt_BR.js index 3340db7a..ad8c673d 100644 --- a/loc/pt_BR.js +++ b/loc/pt_BR.js @@ -98,16 +98,19 @@ module.exports = { header: 'Enviar', details: { title: 'Criar Transacção', - amount_fiels_is_not_valid: 'Campo de quantia não é válido', - fee_fiels_is_not_valid: 'Campo de taxa não é válido', - address_fiels_is_not_valid: 'Campo de endereço não é válido', + amount_field_is_not_valid: 'Campo de quantia não é válido', + fee_field_is_not_valid: 'Campo de taxa não é válido', + address_field_is_not_valid: 'Campo de endereço não é válido', receiver_placeholder: 'endereço de envio aqui', amount_placeholder: 'quantia a enviar (em BTC)', fee_placeholder: 'mais a taxa de transacção (em BTC)', - memo_placeholder: 'Nota pessoal', + note_placeholder: 'Nota pessoal', cancel: 'Cancelar', scan: 'Scanear', create: 'Criar', + address: 'Address', + total_exceeds_balance: 'The total amount exceeds balance', + send: 'Send', remaining_balance: 'Saldo restante', }, create: { diff --git a/loc/pt_PT.js b/loc/pt_PT.js index 84bb2c6a..e5fec822 100644 --- a/loc/pt_PT.js +++ b/loc/pt_PT.js @@ -97,16 +97,19 @@ module.exports = { header: 'Enviar', details: { title: 'Criar Transacção', - amount_fiels_is_not_valid: 'Campo de quantia não é válido', - fee_fiels_is_not_valid: 'Campo de taxa não é válido', - address_fiels_is_not_valid: 'Campo de endereço não é válido', + amount_field_is_not_valid: 'Campo de quantia não é válido', + fee_field_is_not_valid: 'Campo de taxa não é válido', + address_field_is_not_valid: 'Campo de endereço não é válido', receiver_placeholder: 'endereço de envio aqui', amount_placeholder: 'quantia a enviar (em BTC)', fee_placeholder: 'mais a taxa de transacção (em BTC)', - memo_placeholder: 'Nota pessoal', + note_placeholder: 'Nota pessoal', + total_exceeds_balance: 'Total amount exceeds available balance.', cancel: 'Cancelar', scan: 'Scan', create: 'Criar', + address: 'Address', + send: 'Send', remaining_balance: 'Saldo restante', }, create: { diff --git a/loc/ru.js b/loc/ru.js index 17cface3..7bc6515e 100644 --- a/loc/ru.js +++ b/loc/ru.js @@ -96,16 +96,19 @@ module.exports = { header: 'Отправить', details: { title: 'Создать Транзакцию', - amount_fiels_is_not_valid: 'Поле не валидно', - fee_fiels_is_not_valid: 'Поле `комиссия` не валидно', - address_fiels_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)', - memo_placeholder: 'примечание платежа', + note_placeholder: 'примечание платежа', cancel: 'Отмена', scan: 'Скан QR', create: 'Создать', + send: 'Send', + address: 'Address', + total_exceeds_balance: 'Total amount exceeds balance.', remaining_balance: 'Остаток баланса', }, create: { diff --git a/loc/ua.js b/loc/ua.js index 12fed94c..c77fb16f 100644 --- a/loc/ua.js +++ b/loc/ua.js @@ -96,15 +96,18 @@ module.exports = { header: 'Переказ', details: { title: 'Створити Транзакцію', - amount_fiels_is_not_valid: 'Поле не валідно', - fee_fiels_is_not_valid: 'Поле `комісія` не валідно', - address_fiels_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)', - memo_placeholder: 'примітка платежу', + note_placeholder: 'примітка платежу', cancel: 'Відміна', scan: 'Скан QR', + send: 'Send', + total_exceeds_balance: 'total_exceeds_balance', + address: 'Address', create: 'Створити', remaining_balance: 'Залишок балансу', }, diff --git a/models/bitcoinUnits.js b/models/bitcoinUnits.js new file mode 100644 index 00000000..982792a6 --- /dev/null +++ b/models/bitcoinUnits.js @@ -0,0 +1,6 @@ +export const BitcoinUnit = Object.freeze({ + BTC: 'BTC', + MBTC: 'mBTC', + BITS: 'bits', + SATOSHIS: 'satoshis', +}); diff --git a/models/networkTransactionFees.js b/models/networkTransactionFees.js new file mode 100644 index 00000000..4cc10b9b --- /dev/null +++ b/models/networkTransactionFees.js @@ -0,0 +1,30 @@ +import Frisbee from 'frisbee'; + +export class NetworkTransactionFee { + constructor(fastestFee, halfHourFee, hourFee) { + this.fastestFee = fastestFee; + this.halfHourFee = halfHourFee; + this.hourFee = hourFee; + } +} + +export default class NetworkTransactionFees { + static recommendedFees() { + return new Promise(async (resolve, reject) => { + try { + const api = new Frisbee({ baseURI: 'https://bitcoinfees.earn.com' }); + let response = await api.get('/api/v1/fees/recommended'); + if (response && response.body) { + const networkFee = new NetworkTransactionFee(response.body.fastestFee, response.body.halfHourFee, response.body.hourFee); + resolve(networkFee); + } else { + throw new Error('Could not fetch recommended network fees: ' + response.err); + } + } catch (err) { + console.warn(err); + const networkFee = new NetworkTransactionFee(1, 1, 1); + reject(networkFee); + } + }); + } +} diff --git a/package-lock.json b/package-lock.json index 59672173..34918dfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "BlueWallet", - "version": "2.6.1", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2952,9 +2952,9 @@ "integrity": "sha1-nGZalfiLiwj8Bc/XMfVhhZ1yWCU=" }, "bignumber.js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-5.0.0.tgz", - "integrity": "sha512-KWTu6ZMVk9sxlDJQh2YH1UOnfDP8O8TpxUxgQG/vKASoSnEjK9aVuOueFaPcQEYQ5fyNXNTOYwYw3099RYebWg==" + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" }, "bip21": { "version": "2.0.2", @@ -11754,6 +11754,14 @@ } } }, + "react-native-animatable": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.0.tgz", + "integrity": "sha512-GGYEYvderfzPZcPnw7xov4nlRmi9d6oqcIzx0fGkUUsMshOQEtq5IEzFp3np0uTB9n8/gZIZcdbUPggVlVydMg==", + "requires": { + "prop-types": "^15.5.10" + } + }, "react-native-branch": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/react-native-branch/-/react-native-branch-2.2.5.tgz", @@ -11840,6 +11848,20 @@ "prop-types": "^15.5.10" } }, + "react-native-iphone-x-helper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.2.0.tgz", + "integrity": "sha512-xIeTo4s77wwKgBZLVRIZC9tM9/PkXS46Ul76NXmvmixEb3ZwqGdQesR3zRiLMOoIdfOURB6N9bba9po7+x9Bag==" + }, + "react-native-keyboard-aware-scroll-view": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.7.4.tgz", + "integrity": "sha512-5AJ11sGcleiX43IJ32T0kPtIxcd1JWw0R+YPwkgyZWro7uuGBlq09CFW5jS9JTNoGq7crFzFfTcoTIY6B41rNQ==", + "requires": { + "prop-types": "^15.6.2", + "react-native-iphone-x-helper": "^1.0.3" + } + }, "react-native-level-fs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/react-native-level-fs/-/react-native-level-fs-3.0.1.tgz", @@ -11950,6 +11972,15 @@ "prop-types": "^15.5.9" } }, + "react-native-modal": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-6.5.0.tgz", + "integrity": "sha512-ewchdETAGd32xLGLK93NETEGkRcePtN7ZwjmLSQnNW1Zd0SRUYE8NqftjamPyfKvK0i2DZjX4YAghGZTqaRUbA==", + "requires": { + "prop-types": "^15.6.1", + "react-native-animatable": "^1.2.4" + } + }, "react-native-qrcode": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/react-native-qrcode/-/react-native-qrcode-0.2.7.tgz", diff --git a/package.json b/package.json index fcca9881..1a0307d1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "asyncstorage-down": "^3.1.1", - "bignumber.js": "^5.0.0", + "bignumber.js": "^7.0.0", "bip21": "^2.0.2", "bip39": "^2.5.0", "bitcoinjs-lib": "^3.3.2", @@ -63,8 +63,10 @@ "react-native-camera": "^0.12.0", "react-native-elements": "^0.19.0", "react-native-flexi-radio-button": "^0.2.2", + "react-native-keyboard-aware-scroll-view": "^0.7.4", "react-native-level-fs": "^3.0.0", "react-native-material-dropdown": "^0.11.1", + "react-native-modal": "^6.5.0", "react-native-qrcode": "^0.2.7", "react-native-snap-carousel": "^3.7.4", "react-navigation": "^2.17.0", diff --git a/screen/selftest.js b/screen/selftest.js index bc4f261d..c916a4b8 100644 --- a/screen/selftest.js +++ b/screen/selftest.js @@ -175,8 +175,8 @@ export default class Selftest extends Component { } let feeSatoshi = new BigNumber(0.0001); - feeSatoshi = feeSatoshi.mul(100000000); - let satoshiPerByte = feeSatoshi.div(Math.round(tx.length / 2)); + feeSatoshi = feeSatoshi.multipliedBy(100000000); + let satoshiPerByte = feeSatoshi.dividedBy(Math.round(tx.length / 2)); satoshiPerByte = Math.round(satoshiPerByte.toString(10)); if (satoshiPerByte !== 46) { diff --git a/screen/send/create.js b/screen/send/create.js index 5f69c2c4..b3793cf2 100644 --- a/screen/send/create.js +++ b/screen/send/create.js @@ -1,236 +1,145 @@ +/* global alert */ import React, { Component } from 'react'; -import { TextInput } from 'react-native'; -import { Text, FormValidationMessage } from 'react-native-elements'; -import { - BlueSpacingVariable, - BlueHeaderDefaultSub, - BlueLoading, - BlueSpacing20, - BlueButton, - SafeBlueArea, - BlueCard, - BlueText, -} from '../../BlueComponents'; +import { TextInput, ActivityIndicator, TouchableOpacity, Clipboard, StyleSheet, ScrollView } from 'react-native'; +import { Text } from 'react-native-elements'; +import { BlueButton, SafeBlueArea, BlueCard, BlueText } from '../../BlueComponents'; import PropTypes from 'prop-types'; -let BigNumber = require('bignumber.js'); /** @type {AppStorage} */ -let BlueApp = require('../../BlueApp'); +// let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); let EV = require('../../events'); export default class SendCreate extends Component { - static navigationOptions = { - header: ({ navigation }) => { - return navigation.goBack(null)} />; - }, - }; - constructor(props) { super(props); console.log('send/create constructor'); + this.state = { - isLoading: true, + isLoading: false, amount: props.navigation.state.params.amount, fee: props.navigation.state.params.fee, address: props.navigation.state.params.address, memo: props.navigation.state.params.memo, - fromAddress: props.navigation.state.params.fromAddress, - fromSecret: props.navigation.state.params.fromSecret, - broadcastErrorMessage: '', + size: Math.round(props.navigation.getParam('tx').length / 2), + tx: props.navigation.getParam('tx'), + satoshiPerByte: props.navigation.getParam('satoshiPerByte'), + fromWallet: props.navigation.getParam('fromWallet'), }; - - let fromWallet = false; - for (let w of BlueApp.getWallets()) { - if (w.getSecret() === this.state.fromSecret) { - fromWallet = w; - break; - } - - if (w.getAddress() && w.getAddress() === this.state.fromAddress) { - fromWallet = w; - break; - } - } - this.state['fromWallet'] = fromWallet; } async componentDidMount() { console.log('send/create - componentDidMount'); console.log('address = ', this.state.address); + } - let utxo; - let satoshiPerByte; - let tx; - - try { - await this.state.fromWallet.fetchUtxo(); - if (this.state.fromWallet.getChangeAddressAsync) { - await this.state.fromWallet.getChangeAddressAsync(); // to refresh internal pointer to next free address + broadcast() { + this.setState({ isLoading: true }, async () => { + let result = await this.state.fromWallet.broadcastTx(this.state.tx); + console.log('broadcast result = ', result); + if (typeof result === 'string') { + result = JSON.parse(result); } - if (this.state.fromWallet.getAddressAsync) { - await this.state.fromWallet.getAddressAsync(); // to refresh internal pointer to next free address + this.setState({ isLoading: false }); + if (result && result.error) { + alert(JSON.stringify(result.error)); + } else { + EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs + alert('Transaction has been successfully broadcasted. Your transaction ID is: ' + JSON.stringify(result.result)); + this.props.navigation.navigate('Wallets'); } - - utxo = this.state.fromWallet.utxo; - let startTime = Date.now(); - - tx = this.state.fromWallet.createTx(utxo, this.state.amount, this.state.fee, this.state.address, this.state.memo); - let endTime = Date.now(); - console.log('create tx ', (endTime - startTime) / 1000, 'sec'); - - let bitcoin = require('bitcoinjs-lib'); - let txDecoded = bitcoin.Transaction.fromHex(tx); - let txid = txDecoded.getId(); - console.log('txid', txid); - console.log('txhex', tx); - - BlueApp.tx_metadata = BlueApp.tx_metadata || {}; - BlueApp.tx_metadata[txid] = { - txhex: tx, - memo: this.state.memo, - }; - BlueApp.saveToDisk(); - - let feeSatoshi = new BigNumber(this.state.fee); - feeSatoshi = feeSatoshi.mul(100000000); - satoshiPerByte = feeSatoshi.div(Math.round(tx.length / 2)); - satoshiPerByte = Math.floor(satoshiPerByte.toString(10)); - if (satoshiPerByte < 1) { - throw new Error(loc.send.create.not_enough_fee); - } - } catch (err) { - console.log(err); - return this.setState({ - isError: true, - errorMessage: JSON.stringify(err.message), - }); - } - - this.setState({ - isLoading: false, - size: Math.round(tx.length / 2), - tx, - satoshiPerByte, }); } - async broadcast() { - let result = await this.state.fromWallet.broadcastTx(this.state.tx); - console.log('broadcast result = ', result); - if (typeof result === 'string') { - result = JSON.parse(result); - } - if (result && result.error) { - this.setState({ - broadcastErrorMessage: JSON.stringify(result.error), - broadcastSuccessMessage: '', - }); - } else { - EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs - this.setState({ broadcastErrorMessage: '' }); - this.setState({ - broadcastSuccessMessage: 'Success! TXID: ' + JSON.stringify(result.result), - }); - } - } - render() { - if (this.state.isError) { - return ( - + return ( + + - {loc.send.create.error} - {this.state.errorMessage} + {loc.send.create.this_is_hex} + + + + Clipboard.setString(this.state.tx)}> + Copy and broadcast later + + {this.state.isLoading ? ( + + ) : ( + this.broadcast()} title={loc.send.details.send} style={{ maxWidth: 263, paddingHorizontal: 56 }} /> + )} - this.props.navigation.goBack()} title={loc.send.create.go_back} /> - - ); - } + + {loc.send.create.to} + {this.state.address} - if (this.state.isLoading) { - return ; - } + {loc.send.create.amount} + {this.state.amount} BTC - return ( - - + {loc.send.create.fee} + {this.state.fee} BTC - - {loc.send.create.this_is_hex} + {loc.send.create.tx_size} + {this.state.size} bytes - + {loc.send.create.satoshi_per_byte} + {this.state.satoshiPerByte} Sat/B - - - - {loc.send.create.to}: {this.state.address} - - - {loc.send.create.amount}: {this.state.amount} BTC - - - {loc.send.create.fee}: {this.state.fee} BTC - - - {loc.send.create.tx_size}: {this.state.size} Bytes - - - {loc.send.create.satoshi_per_byte}: {this.state.satoshiPerByte} Sat/B - - - {loc.send.create.memo}: {this.state.memo} - - - - this.broadcast()} - title={loc.send.create.broadcast} - /> - - this.props.navigation.goBack()} - title={loc.send.create.go_back} - /> - - {this.state.broadcastErrorMessage} - {this.state.broadcastSuccessMessage} + {loc.send.create.memo} + {this.state.memo} + + ); } } +const styles = StyleSheet.create({ + transactionDetailsTitle: { + color: '#0c2550', + fontWeight: '500', + fontSize: 17, + marginBottom: 2, + }, + transactionDetailsSubtitle: { + color: '#9aa0aa', + fontWeight: '500', + fontSize: 15, + marginBottom: 20, + }, +}); + SendCreate.propTypes = { navigation: PropTypes.shape({ goBack: PropTypes.function, + getParam: PropTypes.function, + navigate: PropTypes.function, state: PropTypes.shape({ params: PropTypes.shape({ amount: PropTypes.string, - fee: PropTypes.string, + fee: PropTypes.number, address: PropTypes.string, memo: PropTypes.string, - fromAddress: PropTypes.string, - fromSecret: PropTypes.string, + fromWallet: PropTypes.shape({ + fromAddress: PropTypes.string, + fromSecret: PropTypes.string, + }), }), }), }), diff --git a/screen/send/details.js b/screen/send/details.js index 117fb8cd..c940eb76 100644 --- a/screen/send/details.js +++ b/screen/send/details.js @@ -1,29 +1,37 @@ +/* global alert */ import React, { Component } from 'react'; -import { ActivityIndicator, View } from 'react-native'; -import { Text, FormValidationMessage } from 'react-native-elements'; import { - BlueSpacing20, - BlueHeaderDefaultSub, - BlueButton, - SafeBlueArea, - BlueText, - BlueFormInput, - BlueFormInputAddress, -} from '../../BlueComponents'; + ActivityIndicator, + View, + TextInput, + TouchableOpacity, + KeyboardAvoidingView, + Keyboard, + TouchableWithoutFeedback, + StyleSheet, + Slider, +} from 'react-native'; +import { Text, Icon } from 'react-native-elements'; +import { BlueHeaderDefaultSub, BlueButton } from '../../BlueComponents'; import PropTypes from 'prop-types'; +import Modal from 'react-native-modal'; +import NetworkTransactionFees, { NetworkTransactionFee } from '../../models/networkTransactionFees'; +import BitcoinBIP70TransactionDecode from '../../bip70/bip70'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; const bip21 = require('bip21'); let EV = require('../../events'); let BigNumber = require('bignumber.js'); /** @type {AppStorage} */ let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); +let bitcoin = require('bitcoinjs-lib'); const btcAddressRx = /^[a-zA-Z0-9]{26,35}$/; export default class SendDetails extends Component { static navigationOptions = { header: ({ navigation }) => { - return navigation.goBack(null)} />; + return navigation.goBack(null)} />; }, }; @@ -58,7 +66,7 @@ export default class SendDetails extends Component { console.log({ memo }); this.state = { - errorMessage: false, + isFeeSelectionModalVisible: false, fromAddress: fromAddress, fromWallet: fromWallet, fromSecret: fromSecret, @@ -66,25 +74,40 @@ export default class SendDetails extends Component { address: address, amount: '', memo, - fee: '', + fee: 1, + networkTransactionFees: new NetworkTransactionFee(1, 1, 1), + feeSliderValue: 1, + bip70TransactionExpiration: null, }; EV(EV.enum.CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS, data => { - console.log('received event with ', data); - if (btcAddressRx.test(data)) { this.setState({ address: data, + bip70TransactionExpiration: null, }); } else { const { address, options } = bip21.decode(data); - + console.warn(data); if (btcAddressRx.test(address)) { this.setState({ address, amount: options.amount, memo: options.label, + bip70TransactionExpiration: null, }); + } else if (BitcoinBIP70TransactionDecode.matchesPaymentURL(data)) { + BitcoinBIP70TransactionDecode.decode(data) + .then(response => { + this.setState({ + address: response.address, + amount: loc.formatBalanceWithoutSuffix(response.amount, BitcoinUnit.BTC), + memo: response.memo, + fee: response.fee, + bip70TransactionExpiration: response.expires, + }); + }) + .catch(error => alert(error.errorMessage)); } } }); @@ -93,6 +116,14 @@ 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 }); + }); + this.setState({ + fee: recommendedFees.halfHourFee, + networkTransactionFees: recommendedFees, + feeSliderValue: recommendedFees.halfHourFee, + }); let startTime = Date.now(); console.log('send/details - componentDidMount'); this.setState({ @@ -108,8 +139,8 @@ export default class SendDetails extends Component { let availableBalance; try { availableBalance = new BigNumber(balance); - availableBalance = availableBalance.sub(amount); - availableBalance = availableBalance.sub(fee); + availableBalance = availableBalance.minus(amount); + availableBalance = availableBalance.minus(fee); availableBalance = availableBalance.toString(10); } catch (err) { return balance; @@ -118,62 +149,193 @@ export default class SendDetails extends Component { return (availableBalance === 'NaN' && balance) || availableBalance; } - createTransaction() { - if (!this.state.amount || this.state.amount === '0') { - this.setState({ - errorMessage: loc.send.details.amount_fiels_is_not_valid, - }); - console.log('validation error'); - return; - } + async createTransaction() { + let error = false; + let requestedSatPerByte = this.state.fee.toString().replace(/\D/g, ''); - if (!this.state.fee) { - this.setState({ - errorMessage: loc.send.details.fee_fiels_is_not_valid, - }); - console.log('validation error'); - return; - } + console.log({ requestedSatPerByte }); - if (!this.state.address) { - this.setState({ - errorMessage: loc.send.details.address_fiels_is_not_valid, - }); + if (!this.state.amount || this.state.amount === '0' || parseFloat(this.state.amount) === 0) { + error = loc.send.details.amount_field_is_not_valid; + console.log('validation error'); + } else if (!this.state.fee || !requestedSatPerByte || parseFloat(requestedSatPerByte) < 1) { + error = loc.send.details.fee_field_is_not_valid; + console.log('validation error'); + } else if (!this.state.address) { + error = loc.send.details.address_field_is_not_valid; + console.log('validation error'); + } else if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, 0) < 0) { + // first sanity check is that sending amount is not bigger than available balance + error = loc.send.details.total_exceeds_balance; + console.log('validation error'); + } else if (BitcoinBIP70TransactionDecode.isExpired(this.state.bip70TransactionExpiration)) { + error = 'Transaction has expired.'; console.log('validation error'); - return; } - if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, this.state.fee) < 0) { - this.setState({ - errorMessage: loc.send.details.amount_fiels_is_not_valid, - }); - console.log('validation error'); + if (error) { + alert(error); return; } - this.setState({ - errorMessage: '', - }); + this.setState({ isLoading: true }, async () => { + let utxo; + let actualSatoshiPerByte; + let tx, txid; + let tries = 1; + let fee = 0.000001; // initial fee guess + + try { + await this.state.fromWallet.fetchUtxo(); + if (this.state.fromWallet.getChangeAddressAsync) { + await this.state.fromWallet.getChangeAddressAsync(); // to refresh internal pointer to next free address + } + if (this.state.fromWallet.getAddressAsync) { + await this.state.fromWallet.getAddressAsync(); // to refresh internal pointer to next free address + } + + utxo = this.state.fromWallet.utxo; + + do { + console.log('try #', tries, 'fee=', fee); + if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, fee) < 0) { + // we could not add any fee. user is trying to send all he's got. that wont work + throw new Error(loc.send.details.total_exceeds_balance); + } + + let startTime = Date.now(); + tx = this.state.fromWallet.createTx(utxo, this.state.amount, fee, this.state.address, this.state.memo); + let endTime = Date.now(); + console.log('create tx ', (endTime - startTime) / 1000, 'sec'); - this.props.navigation.navigate('CreateTransaction', { - amount: this.state.amount, - fee: this.state.fee, - address: this.state.address, - memo: this.state.memo, - fromAddress: this.state.fromAddress, - fromSecret: this.state.fromSecret, + let txDecoded = bitcoin.Transaction.fromHex(tx); + txid = txDecoded.getId(); + console.log('txid', txid); + console.log('txhex', tx); + + let feeSatoshi = new BigNumber(fee).multipliedBy(100000000); + actualSatoshiPerByte = feeSatoshi.dividedBy(Math.round(tx.length / 2)); + actualSatoshiPerByte = actualSatoshiPerByte.toNumber(); + console.log({ satoshiPerByte: actualSatoshiPerByte }); + + if (Math.round(actualSatoshiPerByte) !== requestedSatPerByte * 1 || Math.floor(actualSatoshiPerByte) < 1) { + console.log('fee is not correct, retrying'); + fee = feeSatoshi + .multipliedBy(requestedSatPerByte / actualSatoshiPerByte) + .plus(10) + .dividedBy(100000000) + .toNumber(); + } else { + break; + } + } while (tries++ < 5); + + BlueApp.tx_metadata = BlueApp.tx_metadata || {}; + BlueApp.tx_metadata[txid] = { + txhex: tx, + memo: this.state.memo, + }; + await BlueApp.saveToDisk(); + } catch (err) { + console.log(err); + alert(err); + this.setState({ isLoading: false }); + return; + } + + this.setState({ isLoading: false }, () => + this.props.navigation.navigate('CreateTransaction', { + amount: this.state.amount, + fee: fee.toFixed(8), + address: this.state.address, + memo: this.state.memo, + fromWallet: this.state.fromWallet, + tx: tx, + satoshiPerByte: actualSatoshiPerByte.toFixed(2), + }), + ); }); } - render() { - if (this.state.isLoading) { - return ( - + renderFeeSelectionModal = () => { + return ( + this.setState({ isFeeSelectionModalVisible: false })} + > + + + this.textInput.focus()}> + { + this.textInput = ref; + }} + value={this.state.fee.toString()} + onChangeText={value => { + let newValue = value.replace(/\D/g, ''); + if (newValue.length === 0) { + newValue = 1; + } + this.setState({ fee: newValue, feeSliderValue: newValue }); + }} + maxLength={9} + editable={!this.state.isLoading} + placeholderTextColor="#37c0a1" + placeholder={this.state.networkTransactionFees.halfHourFee.toString()} + style={{ fontWeight: '600', color: '#37c0a1', marginBottom: 0, marginRight: 4, textAlign: 'right', fontSize: 36 }} + /> + + sat/b + + + {this.state.networkTransactionFees.fastestFee > 1 && ( + + this.setState({ feeSliderValue: this.state.feeSliderValue, fee: value.toFixed(0) })} + minimumValue={1} + maximumValue={this.state.networkTransactionFees.fastestFee} + value={Number(this.state.feeSliderValue)} + maximumTrackTintColor="#d8d8d8" + minimumTrackTintColor="#37c0a1" + style={{ flex: 1 }} + /> + + slow + fast + + + )} + + + + ); + }; + + renderCreateButton = () => { + return ( + + {this.state.isLoading ? ( - - ); - } + ) : ( + this.createTransaction()} title={loc.send.details.create} /> + )} + + ); + }; + render() { if (!this.state.fromWallet.getAddress) { return ( @@ -183,62 +345,190 @@ export default class SendDetails extends Component { } return ( - - - this.setState({ address: text })} - placeholder={loc.send.details.receiver_placeholder} - value={this.state.address} - /> - - this.setState({ amount: text.replace(',', '.') })} - keyboardType={'numeric'} - placeholder={loc.send.details.amount_placeholder} - value={this.state.amount + ''} - /> - - this.setState({ fee: text.replace(',', '.') })} - keyboardType={'numeric'} - placeholder={loc.send.details.fee_placeholder} - value={this.state.fee + ''} - /> - - this.setState({ memo: text })} - placeholder={loc.send.details.memo_placeholder} - value={this.state.memo} - /> - - - - {loc.send.details.remaining_balance}:{' '} - {this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, this.state.fee)} BTC - - - - {this.state.errorMessage} - - - this.props.navigation.goBack()} title={loc.send.details.cancel} /> - this.props.navigation.navigate('ScanQrAddress')} - /> - this.createTransaction()} title={loc.send.details.create} /> + + + + + this.setState({ amount: text.replace(',', '.') })} + placeholder="0" + maxLength={10} + editable={!this.state.isLoading} + value={this.state.amount + ''} + placeholderTextColor="#0f5cc0" + style={{ + color: '#0f5cc0', + fontSize: 36, + fontWeight: '600', + }} + /> + + {' ' + BitcoinUnit.BTC} + + + + { + if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) { + this.setState( + { + isLoading: true, + }, + () => { + BitcoinBIP70TransactionDecode.decode(text).then(response => { + this.setState({ + address: response.address, + amount: loc.formatBalanceWithoutSuffix(response.amount, BitcoinUnit.BTC), + memo: response.memo, + fee: response.fee, + bip70TransactionExpiration: response.expires, + isLoading: false, + }); + }); + }, + ); + } else { + this.setState({ address: text.replace(' ', ''), isLoading: false, bip70TransactionExpiration: null }); + } + }} + placeholder={loc.send.details.address} + numberOfLines={1} + value={this.state.address} + style={{ flex: 1, marginHorizontal: 8, minHeight: 33, height: 33 }} + editable={!this.state.isLoading} + /> + this.props.navigation.navigate('ScanQrAddress')} + style={{ + width: 75, + height: 36, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: '#bebebe', + borderRadius: 4, + paddingVertical: 4, + paddingHorizontal: 8, + marginHorizontal: 4, + }} + > + + {loc.send.details.scan} + + + )} + + this.setState({ memo: text })} + placeholder={loc.send.details.note_placeholder} + value={this.state.memo} + numberOfLines={1} + style={{ flex: 1, marginHorizontal: 8, minHeight: 33, height: 33 }} + editable={!this.state.isLoading} + /> + + )} + this.setState({ isFeeSelectionModalVisible: true })} + disabled={this.state.isLoading} + style={{ flexDirection: 'row', marginHorizontal: 20, justifyContent: 'space-between', alignItems: 'center' }} + > + Fee + + {this.state.fee} + sat/b + + + )} + {this.renderCreateButton()} + {this.renderFeeSelectionModal()} + - + ); } } +const styles = StyleSheet.create({ + modalContent: { + backgroundColor: '#FFFFFF', + padding: 22, + justifyContent: 'center', + alignItems: 'center', + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + borderColor: 'rgba(0, 0, 0, 0.1)', + minHeight: 200, + height: 200, + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0, + }, + satoshisTextInput: { + backgroundColor: '#d2f8d6', + minWidth: 127, + height: 60, + borderRadius: 8, + flexDirection: 'row', + justifyContent: 'center', + paddingHorizontal: 8, + }, +}); + SendDetails.propTypes = { navigation: PropTypes.shape({ goBack: PropTypes.function, diff --git a/screen/send/scanQrAddress.js b/screen/send/scanQrAddress.js index 2bdc1607..8def8f6a 100644 --- a/screen/send/scanQrAddress.js +++ b/screen/send/scanQrAddress.js @@ -25,7 +25,7 @@ export default class CameraExample extends React.Component { EV(EV.enum.CREATE_TRANSACTION_NEW_DESTINATION_ADDRESS, ret.data); } // end - async componentWillMount() { + async componentDidMount() { const { status } = await Permissions.askAsync(Permissions.CAMERA); this.setState({ hasCameraPermission: status === 'granted' }); } diff --git a/screen/transactions/RBF-create.js b/screen/transactions/RBF-create.js index 3fa9ac38..e0ee18a8 100644 --- a/screen/transactions/RBF-create.js +++ b/screen/transactions/RBF-create.js @@ -78,11 +78,11 @@ export default class SendCreate extends Component { changeAddress = o.addresses[0]; } else { transferAmount = new BigNumber(o.value); - transferAmount = transferAmount.div(100000000).toString(10); + transferAmount = transferAmount.dividedBy(100000000).toString(10); } } let oldFee = new BigNumber(totalInputAmountSatoshi - totalOutputAmountSatoshi); - oldFee = parseFloat(oldFee.div(100000000).toString(10)); + oldFee = parseFloat(oldFee.dividedBy(100000000).toString(10)); console.log('changeAddress = ', changeAddress); console.log('utxo', utxo); @@ -93,7 +93,7 @@ export default class SendCreate extends Component { console.log('oldFee', oldFee); let newFee = new BigNumber(oldFee); - newFee = newFee.add(this.state.feeDelta).toString(10); + newFee = newFee.plus(this.state.feeDelta).toString(10); console.log('new Fee', newFee); // creating TX @@ -124,7 +124,7 @@ export default class SendCreate extends Component { } let newFeeSatoshi = new BigNumber(newFee); - newFeeSatoshi = parseInt(newFeeSatoshi.mul(100000000)); + newFeeSatoshi = parseInt(newFeeSatoshi.multipliedBy(100000000)); let satoshiPerByte = Math.round(newFeeSatoshi / (tx.length / 2)); this.setState({ isLoading: false, diff --git a/screen/wallets/add.js b/screen/wallets/add.js index 5fbcbdd2..d0b85572 100644 --- a/screen/wallets/add.js +++ b/screen/wallets/add.js @@ -1,7 +1,7 @@ /* global alert */ import { SegwitP2SHWallet } from '../../class'; import React, { Component } from 'react'; -import { ActivityIndicator, Dimensions, View } from 'react-native'; +import { ActivityIndicator, Dimensions, View, TextInput } from 'react-native'; import { BlueTextCentered, BlueText, @@ -9,7 +9,6 @@ import { BitcoinButton, BlueButtonLink, BlueFormLabel, - BlueFormInput, BlueButton, SafeBlueArea, BlueCard, @@ -75,14 +74,33 @@ export default class WalletsAdd extends Component { {loc.wallets.add.wallet_name} - { - this.setLabel(text); + - + > + { + this.setLabel(text); + }} + style={{ flex: 1, marginHorizontal: 8, color: '#81868e' }} + editable={!this.state.isLoading} + /> + {loc.wallets.add.wallet_type} diff --git a/screen/wallets/list.js b/screen/wallets/list.js index 6c31af5b..9ba349fd 100644 --- a/screen/wallets/list.js +++ b/screen/wallets/list.js @@ -346,7 +346,7 @@ export default class WalletsList extends Component { containerStyle: { marginTop: 0 }, }} hideChevron - rightTitle={new BigNumber((rowData.item.value && rowData.item.value) || 0).div(100000000).toString()} + rightTitle={new BigNumber((rowData.item.value && rowData.item.value) || 0).dividedBy(100000000).toString()} rightTitleStyle={{ fontWeight: '600', fontSize: 16, diff --git a/screen/wallets/transactions.js b/screen/wallets/transactions.js index fb406cc3..47963e51 100644 --- a/screen/wallets/transactions.js +++ b/screen/wallets/transactions.js @@ -24,7 +24,9 @@ import { BlueListItem, } from '../../BlueComponents'; import { Icon } from 'react-native-elements'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; /** @type {AppStorage} */ + let BlueApp = require('../../BlueApp'); let loc = require('../../loc'); const BigNumber = require('bignumber.js'); @@ -61,8 +63,8 @@ export default class WalletTransactions extends Component { wallet: props.navigation.getParam('wallet'), gradientColors: ['#FFFFFF', '#FFFFFF'], dataSource: props.navigation.getParam('wallet').getTransactions(), + walletBalanceUnit: BitcoinUnit.MBTC, }; - // here, when we receive REMOTE_TRANSACTIONS_COUNT_CHANGED we fetch TXs and balance for current wallet EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED, this.refreshTransactionsFunction.bind(this)); } @@ -197,6 +199,18 @@ export default class WalletTransactions extends Component { ); } + changeWalletBalanceUnit() { + if (this.state.walletBalanceUnit === undefined || this.state.walletBalanceUnit === BitcoinUnit.BTC) { + this.setState({ walletBalanceUnit: BitcoinUnit.MBTC }); + } else if (this.state.walletBalanceUnit === BitcoinUnit.MBTC) { + this.setState({ walletBalanceUnit: BitcoinUnit.BITS }); + } else if (this.state.walletBalanceUnit === BitcoinUnit.BITS) { + this.setState({ walletBalanceUnit: BitcoinUnit.SATOSHIS }); + } else if (this.state.walletBalanceUnit === BitcoinUnit.SATOSHIS) { + this.setState({ walletBalanceUnit: BitcoinUnit.BTC }); + } + } + renderWalletHeader = () => { return ( @@ -225,18 +239,20 @@ export default class WalletTransactions extends Component { > {this.state.wallet.getLabel()} - - {loc.formatBalance(this.state.wallet.getBalance())} - + this.changeWalletBalanceUnit()}> + + {loc.formatBalance(this.state.wallet.getBalance(), this.state.walletBalanceUnit)} + +