Browse Source

Development (#103)

ADD: New send screen
ADD: Support for BIP70 decoding
localNotifications
Igor Korsakov 6 years ago
committed by GitHub
parent
commit
f5dd8252e1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .eslintrc
  2. 23
      BlueComponents.js
  3. 35
      MainBottomTabs.js
  4. 74
      bip70/bip70.js
  5. 4
      class/abstract-hd-wallet.js
  6. 6
      class/hd-segwit-p2sh-wallet.js
  7. 8
      class/legacy-wallet.js
  8. 2
      class/lightning-custodian-wallet.js
  9. 4
      class/segwit-p2sh-wallet.js
  10. 6
      currency.js
  11. 18
      loc/en.js
  12. 11
      loc/es.js
  13. 44
      loc/index.js
  14. 11
      loc/pt_BR.js
  15. 11
      loc/pt_PT.js
  16. 11
      loc/ru.js
  17. 11
      loc/ua.js
  18. 6
      models/bitcoinUnits.js
  19. 30
      models/networkTransactionFees.js
  20. 39
      package-lock.json
  21. 4
      package.json
  22. 4
      screen/selftest.js
  23. 279
      screen/send/create.js
  24. 512
      screen/send/details.js
  25. 2
      screen/send/scanQrAddress.js
  26. 8
      screen/transactions/RBF-create.js
  27. 36
      screen/wallets/add.js
  28. 2
      screen/wallets/list.js
  29. 44
      screen/wallets/transactions.js

6
.eslintrc

@ -13,5 +13,9 @@
trailingComma: 'all'
}
]
}
},
"env":{
"es6": true
},
"globals": { "fetch": false }
}

23
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 {
>
<View
style={{
width: 30,
height: 30,
borderBottomLeftRadius: 15,
minWidth: 30,
minHeight: 30,
backgroundColor: 'transparent',
transform: [{ rotate: '-45deg' }],
alignItems: 'center',
@ -716,7 +715,6 @@ export class BlueSendButtonIcon extends Component {
flexDirection: 'row',
width: 110,
height: 40,
position: 'relative',
backgroundColor: '#ccddf9',
alignItems: 'center',
paddingLeft: 15,
@ -724,10 +722,9 @@ export class BlueSendButtonIcon extends Component {
>
<View
style={{
width: 30,
height: 30,
minWidth: 30,
minHeight: 30,
left: 5,
borderBottomLeftRadius: 15,
backgroundColor: 'transparent',
transform: [{ rotate: '225deg' }],
}}
@ -760,9 +757,8 @@ export class ManageFundsBigButton extends Component {
style={{
flex: 1,
flexDirection: 'row',
width: 160,
height: 40,
position: 'relative',
minWidth: 160,
minHeight: 40,
backgroundColor: '#ccddf9',
alignItems: 'center',
}}
@ -782,7 +778,6 @@ export class ManageFundsBigButton extends Component {
fontSize: (isIpad && 10) || 16,
fontWeight: '500',
backgroundColor: 'transparent',
position: 'relative',
}}
>
{loc.lnd.title}

35
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: {

74
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;
}
}

4
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));

6
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,

8
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());
}

2
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) {

4
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);
}

6
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';
}

18
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',

11
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',

44
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;

11
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: {

11
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: {

11
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: {

11
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: 'Залишок балансу',
},

6
models/bitcoinUnits.js

@ -0,0 +1,6 @@
export const BitcoinUnit = Object.freeze({
BTC: 'BTC',
MBTC: 'mBTC',
BITS: 'bits',
SATOSHIS: 'satoshis',
});

30
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);
}
});
}
}

39
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",

4
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",

4
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) {

279
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 <BlueHeaderDefaultSub leftText={loc.send.create.title} onClose={() => 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 (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 19 }}>
<ScrollView>
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
<BlueText>{loc.send.create.error}</BlueText>
<FormValidationMessage>{this.state.errorMessage}</FormValidationMessage>
<BlueText style={{ color: '#0c2550', fontWeight: '500' }}>{loc.send.create.this_is_hex}</BlueText>
<TextInput
style={{
borderColor: '#ebebeb',
backgroundColor: '#d2f8d6',
borderRadius: 4,
marginTop: 20,
color: '#37c0a1',
fontWeight: '500',
fontSize: 14,
paddingHorizontal: 16,
paddingBottom: 16,
paddingTop: 16,
}}
height={72}
multiline
editable={false}
value={this.state.tx}
/>
<TouchableOpacity style={{ marginVertical: 24 }} onPress={() => Clipboard.setString(this.state.tx)}>
<Text style={{ color: '#0c2550', fontSize: 15, fontWeight: '500', alignSelf: 'center' }}>Copy and broadcast later</Text>
</TouchableOpacity>
{this.state.isLoading ? (
<ActivityIndicator />
) : (
<BlueButton onPress={() => this.broadcast()} title={loc.send.details.send} style={{ maxWidth: 263, paddingHorizontal: 56 }} />
)}
</BlueCard>
<BlueButton onPress={() => this.props.navigation.goBack()} title={loc.send.create.go_back} />
</SafeBlueArea>
);
}
<BlueCard>
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.address}</Text>
if (this.state.isLoading) {
return <BlueLoading />;
}
<Text style={styles.transactionDetailsTitle}>{loc.send.create.amount}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.amount} BTC</Text>
return (
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
<BlueSpacingVariable />
<Text style={styles.transactionDetailsTitle}>{loc.send.create.fee}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.fee} BTC</Text>
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
<BlueText>{loc.send.create.this_is_hex}</BlueText>
<Text style={styles.transactionDetailsTitle}>{loc.send.create.tx_size}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.size} bytes</Text>
<TextInput
style={{
borderColor: '#ebebeb',
borderWidth: 1,
marginTop: 20,
color: '#ebebeb',
}}
maxHeight={70}
multiline
editable={false}
value={this.state.tx}
/>
<Text style={styles.transactionDetailsTitle}>{loc.send.create.satoshi_per_byte}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.satoshiPerByte} Sat/B</Text>
<BlueSpacing20 />
<BlueText style={{ paddingTop: 20 }}>
{loc.send.create.to}: {this.state.address}
</BlueText>
<BlueText>
{loc.send.create.amount}: {this.state.amount} BTC
</BlueText>
<BlueText>
{loc.send.create.fee}: {this.state.fee} BTC
</BlueText>
<BlueText>
{loc.send.create.tx_size}: {this.state.size} Bytes
</BlueText>
<BlueText>
{loc.send.create.satoshi_per_byte}: {this.state.satoshiPerByte} Sat/B
</BlueText>
<BlueText>
{loc.send.create.memo}: {this.state.memo}
</BlueText>
</BlueCard>
<BlueButton
icon={{
name: 'megaphone',
type: 'octicon',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.broadcast()}
title={loc.send.create.broadcast}
/>
<BlueButton
icon={{
name: 'arrow-left',
type: 'octicon',
color: BlueApp.settings.buttonTextColor,
}}
onPress={() => this.props.navigation.goBack()}
title={loc.send.create.go_back}
/>
<FormValidationMessage>{this.state.broadcastErrorMessage}</FormValidationMessage>
<Text style={{ padding: 0, color: '#0f0' }}>{this.state.broadcastSuccessMessage}</Text>
<Text style={styles.transactionDetailsTitle}>{loc.send.create.memo}</Text>
<Text style={styles.transactionDetailsSubtitle}>{this.state.memo}</Text>
</BlueCard>
</ScrollView>
</SafeBlueArea>
);
}
}
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,
}),
}),
}),
}),

512
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 <BlueHeaderDefaultSub leftText={loc.send.details.title.toLowerCase()} onClose={() => navigation.goBack(null)} />;
return <BlueHeaderDefaultSub leftText={loc.send.header.toLowerCase()} onClose={() => 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 (
<View style={{ flex: 1, paddingTop: 20 }}>
renderFeeSelectionModal = () => {
return (
<Modal
isVisible={this.state.isFeeSelectionModalVisible}
style={styles.bottomModal}
onBackdropPress={() => this.setState({ isFeeSelectionModalVisible: false })}
>
<KeyboardAvoidingView behavior="position">
<View style={styles.modalContent}>
<TouchableOpacity style={styles.satoshisTextInput} onPress={() => this.textInput.focus()}>
<TextInput
keyboardType="numeric"
ref={ref => {
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 }}
/>
<Text
style={{
fontWeight: '600',
color: '#37c0a1',
paddingRight: 4,
textAlign: 'left',
fontSize: 16,
alignSelf: 'flex-end',
marginBottom: 14,
}}
>
sat/b
</Text>
</TouchableOpacity>
{this.state.networkTransactionFees.fastestFee > 1 && (
<View style={{ flex: 1, marginTop: 32, minWidth: 240, width: 240 }}>
<Slider
onValueChange={value => 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 }}
/>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between', marginTop: 14 }}>
<Text style={{ fontWeight: '500', fontSize: 13, color: '#37c0a1' }}>slow</Text>
<Text style={{ fontWeight: '500', fontSize: 13, color: '#37c0a1' }}>fast</Text>
</View>
</View>
)}
</View>
</KeyboardAvoidingView>
</Modal>
);
};
renderCreateButton = () => {
return (
<View style={{ paddingHorizontal: 56, paddingVertical: 16, alignContent: 'center', backgroundColor: '#FFFFFF' }}>
{this.state.isLoading ? (
<ActivityIndicator />
</View>
);
}
) : (
<BlueButton onPress={() => this.createTransaction()} title={loc.send.details.create} />
)}
</View>
);
};
render() {
if (!this.state.fromWallet.getAddress) {
return (
<View style={{ flex: 1, paddingTop: 20 }}>
@ -183,62 +345,190 @@ export default class SendDetails extends Component {
}
return (
<SafeBlueArea style={{ flex: 1 }}>
<View>
<BlueFormInputAddress
onChangeText={text => this.setState({ address: text })}
placeholder={loc.send.details.receiver_placeholder}
value={this.state.address}
/>
<BlueFormInput
onChangeText={text => this.setState({ amount: text.replace(',', '.') })}
keyboardType={'numeric'}
placeholder={loc.send.details.amount_placeholder}
value={this.state.amount + ''}
/>
<BlueFormInput
onChangeText={text => this.setState({ fee: text.replace(',', '.') })}
keyboardType={'numeric'}
placeholder={loc.send.details.fee_placeholder}
value={this.state.fee + ''}
/>
<BlueFormInput
onChangeText={text => this.setState({ memo: text })}
placeholder={loc.send.details.memo_placeholder}
value={this.state.memo}
/>
<BlueSpacing20 />
<BlueText style={{ paddingLeft: 20 }}>
{loc.send.details.remaining_balance}:{' '}
{this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), this.state.amount, this.state.fee)} BTC
</BlueText>
</View>
<FormValidationMessage>{this.state.errorMessage}</FormValidationMessage>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between' }}>
<BlueButton onPress={() => this.props.navigation.goBack()} title={loc.send.details.cancel} />
<BlueButton
icon={{
name: 'qrcode',
type: 'font-awesome',
color: BlueApp.settings.buttonTextColor,
}}
style={{}}
title={loc.send.details.scan}
onPress={() => this.props.navigation.navigate('ScanQrAddress')}
/>
<BlueButton onPress={() => this.createTransaction()} title={loc.send.details.create} />
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1, backgroundColor: '#FFFFFF' }}>
<KeyboardAvoidingView behavior="position">
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 16 }}>
<TextInput
keyboardType="numeric"
onChangeText={text => 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',
}}
/>
<Text
style={{
color: '#0f5cc0',
fontSize: 16,
marginHorizontal: 4,
paddingBottom: 6,
fontWeight: '600',
alignSelf: 'flex-end',
}}
>
{' ' + BitcoinUnit.BTC}
</Text>
</View>
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
onChangeText={text => {
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}
/>
<TouchableOpacity
disabled={this.state.isLoading}
onPress={() => 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,
}}
>
<Icon name="qrcode" size={22} type="font-awesome" color="#FFFFFF" />
<Text style={{ color: '#FFFFFF' }}>{loc.send.details.scan}</Text>
</TouchableOpacity>
</View>
)}
<View
hide={!this.state.showMemoRow}
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
}}
>
<TextInput
onChangeText={text => 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}
/>
</View>
)}
<TouchableOpacity
onPress={() => this.setState({ isFeeSelectionModalVisible: true })}
disabled={this.state.isLoading}
style={{ flexDirection: 'row', marginHorizontal: 20, justifyContent: 'space-between', alignItems: 'center' }}
>
<Text style={{ color: '#81868e', fontSize: 14 }}>Fee</Text>
<View
style={{
backgroundColor: '#d2f8d6',
minWidth: 40,
height: 25,
borderRadius: 4,
justifyContent: 'space-between',
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
}}
>
<Text style={{ color: '#37c0a1', marginBottom: 0, marginRight: 4, textAlign: 'right' }}>{this.state.fee}</Text>
<Text style={{ color: '#37c0a1', paddingRight: 4, textAlign: 'left' }}>sat/b</Text>
</View>
</TouchableOpacity>
)}
{this.renderCreateButton()}
{this.renderFeeSelectionModal()}
</KeyboardAvoidingView>
</View>
</SafeBlueArea>
</TouchableWithoutFeedback>
);
}
}
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,

2
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' });
}

8
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,

36
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 {
<SafeBlueArea forceInset={{ horizontal: 'always' }} style={{ flex: 1, paddingTop: 40 }}>
<BlueCard>
<BlueFormLabel>{loc.wallets.add.wallet_name}</BlueFormLabel>
<BlueFormInput
value={this.state.label}
placeholder={loc.wallets.add.label_new_segwit}
onChangeText={text => {
this.setLabel(text);
<View
style={{
flexDirection: 'row',
borderColor: '#d2d2d2',
borderBottomColor: '#d2d2d2',
borderWidth: 1.0,
borderBottomWidth: 0.5,
backgroundColor: '#f5f5f5',
minHeight: 44,
height: 44,
marginHorizontal: 20,
alignItems: 'center',
marginVertical: 16,
borderRadius: 4,
}}
/>
>
<TextInput
value={this.state.label}
placeholderTextColor="#81868e"
placeholder={loc.wallets.add.label_new_segwit}
onChangeText={text => {
this.setLabel(text);
}}
style={{ flex: 1, marginHorizontal: 8, color: '#81868e' }}
editable={!this.state.isLoading}
/>
</View>
<BlueFormLabel>{loc.wallets.add.wallet_type}</BlueFormLabel>
<View style={{ flexDirection: 'row', paddingTop: 10, paddingLeft: 20, width: width - 80, borderColor: 'red', borderWidth: 0 }}>

2
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,

44
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 (
<LinearGradient colors={[this.state.gradientColors[0], this.state.gradientColors[1]]} style={{ padding: 15, height: 164 }}>
@ -225,18 +239,20 @@ export default class WalletTransactions extends Component {
>
{this.state.wallet.getLabel()}
</Text>
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={{
backgroundColor: 'transparent',
fontWeight: 'bold',
fontSize: 36,
color: '#fff',
}}
>
{loc.formatBalance(this.state.wallet.getBalance())}
</Text>
<TouchableOpacity onPress={() => this.changeWalletBalanceUnit()}>
<Text
numberOfLines={1}
adjustsFontSizeToFit
style={{
backgroundColor: 'transparent',
fontWeight: 'bold',
fontSize: 36,
color: '#fff',
}}
>
{loc.formatBalance(this.state.wallet.getBalance(), this.state.walletBalanceUnit)}
</Text>
</TouchableOpacity>
<Text style={{ backgroundColor: 'transparent' }} />
<Text
numberOfLines={1}
@ -420,7 +436,7 @@ export default class WalletTransactions 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,

Loading…
Cancel
Save