mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-03 12:06:21 +01:00
Development (#103)
ADD: New send screen ADD: Support for BIP70 decoding
This commit is contained in:
parent
4000519dfc
commit
f5dd8252e1
29 changed files with 849 additions and 406 deletions
|
@ -13,5 +13,9 @@
|
|||
trailingComma: 'all'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"env":{
|
||||
"es6": true
|
||||
},
|
||||
"globals": { "fetch": false }
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
Normal file
74
bip70/bip70.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
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
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',
|
||||
|
|
42
loc/index.js
42
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) {
|
||||
strings.formatBalance = (balance, unit) => {
|
||||
const conversion = 100000000;
|
||||
if (unit === undefined) {
|
||||
if (balance < 0.1 && balance !== 0) {
|
||||
let b = new BigNumber(balance);
|
||||
return b.mul(1000).toString() + ' mBTC';
|
||||
return b.multipliedBy(1000).toString() + ' ' + BitcoinUnit.MBTC;
|
||||
}
|
||||
return balance + ' BTC';
|
||||
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);
|
||||
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;
|
||||
};
|
||||
|
||||
module.exports = strings;
|
||||
|
|
11
loc/pt_BR.js
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
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
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
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
Normal file
6
models/bitcoinUnits.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const BitcoinUnit = Object.freeze({
|
||||
BTC: 'BTC',
|
||||
MBTC: 'mBTC',
|
||||
BITS: 'bits',
|
||||
SATOSHIS: 'satoshis',
|
||||
});
|
30
models/networkTransactionFees.js
Normal file
30
models/networkTransactionFees.js
Normal file
|
@ -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
generated
39
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,237 +1,146 @@
|
|||
/* 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
|
||||
}
|
||||
if (this.state.fromWallet.getAddressAsync) {
|
||||
await this.state.fromWallet.getAddressAsync(); // to refresh internal pointer to next free address
|
||||
}
|
||||
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
if (result && result.error) {
|
||||
this.setState({
|
||||
broadcastErrorMessage: JSON.stringify(result.error),
|
||||
broadcastSuccessMessage: '',
|
||||
});
|
||||
alert(JSON.stringify(result.error));
|
||||
} else {
|
||||
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
|
||||
this.setState({ broadcastErrorMessage: '' });
|
||||
this.setState({
|
||||
broadcastSuccessMessage: 'Success! TXID: ' + JSON.stringify(result.result),
|
||||
});
|
||||
alert('Transaction has been successfully broadcasted. Your transaction ID is: ' + JSON.stringify(result.result));
|
||||
this.props.navigation.navigate('Wallets');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.isError) {
|
||||
return (
|
||||
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
|
||||
<SafeBlueArea style={{ flex: 1, paddingTop: 19 }}>
|
||||
<ScrollView>
|
||||
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
|
||||
<BlueText>{loc.send.create.error}</BlueText>
|
||||
<FormValidationMessage>{this.state.errorMessage}</FormValidationMessage>
|
||||
</BlueCard>
|
||||
<BlueButton onPress={() => this.props.navigation.goBack()} title={loc.send.create.go_back} />
|
||||
</SafeBlueArea>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.isLoading) {
|
||||
return <BlueLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeBlueArea style={{ flex: 1, paddingTop: 20 }}>
|
||||
<BlueSpacingVariable />
|
||||
|
||||
<BlueCard style={{ alignItems: 'center', flex: 1 }}>
|
||||
<BlueText>{loc.send.create.this_is_hex}</BlueText>
|
||||
<BlueText style={{ color: '#0c2550', fontWeight: '500' }}>{loc.send.create.this_is_hex}</BlueText>
|
||||
|
||||
<TextInput
|
||||
style={{
|
||||
borderColor: '#ebebeb',
|
||||
borderWidth: 1,
|
||||
backgroundColor: '#d2f8d6',
|
||||
borderRadius: 4,
|
||||
marginTop: 20,
|
||||
color: '#ebebeb',
|
||||
color: '#37c0a1',
|
||||
fontWeight: '500',
|
||||
fontSize: 14,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
paddingTop: 16,
|
||||
}}
|
||||
maxHeight={70}
|
||||
height={72}
|
||||
multiline
|
||||
editable={false}
|
||||
value={this.state.tx}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<BlueCard>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.address}</Text>
|
||||
|
||||
<BlueButton
|
||||
icon={{
|
||||
name: 'megaphone',
|
||||
type: 'octicon',
|
||||
color: BlueApp.settings.buttonTextColor,
|
||||
}}
|
||||
onPress={() => this.broadcast()}
|
||||
title={loc.send.create.broadcast}
|
||||
/>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.amount}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.amount} BTC</Text>
|
||||
|
||||
<BlueButton
|
||||
icon={{
|
||||
name: 'arrow-left',
|
||||
type: 'octicon',
|
||||
color: BlueApp.settings.buttonTextColor,
|
||||
}}
|
||||
onPress={() => this.props.navigation.goBack()}
|
||||
title={loc.send.create.go_back}
|
||||
/>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.fee}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.fee} BTC</Text>
|
||||
|
||||
<FormValidationMessage>{this.state.broadcastErrorMessage}</FormValidationMessage>
|
||||
<Text style={{ padding: 0, color: '#0f0' }}>{this.state.broadcastSuccessMessage}</Text>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.tx_size}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.size} bytes</Text>
|
||||
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.satoshi_per_byte}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>{this.state.satoshiPerByte} Sat/B</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,
|
||||
fromWallet: PropTypes.shape({
|
||||
fromAddress: PropTypes.string,
|
||||
fromSecret: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
async createTransaction() {
|
||||
let error = false;
|
||||
let requestedSatPerByte = this.state.fee.toString().replace(/\D/g, '');
|
||||
|
||||
console.log({ requestedSatPerByte });
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
alert(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.fee) {
|
||||
this.setState({
|
||||
errorMessage: loc.send.details.fee_fiels_is_not_valid,
|
||||
});
|
||||
console.log('validation error');
|
||||
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');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!this.state.address) {
|
||||
this.setState({
|
||||
errorMessage: loc.send.details.address_fiels_is_not_valid,
|
||||
});
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
this.setState({ isLoading: false }, () =>
|
||||
this.props.navigation.navigate('CreateTransaction', {
|
||||
amount: this.state.amount,
|
||||
fee: this.state.fee,
|
||||
fee: fee.toFixed(8),
|
||||
address: this.state.address,
|
||||
memo: this.state.memo,
|
||||
fromAddress: this.state.fromAddress,
|
||||
fromSecret: this.state.fromSecret,
|
||||
fromWallet: this.state.fromWallet,
|
||||
tx: tx,
|
||||
satoshiPerByte: actualSatoshiPerByte.toFixed(2),
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
renderFeeSelectionModal = () => {
|
||||
return (
|
||||
<View style={{ flex: 1, paddingTop: 20 }}>
|
||||
<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 />
|
||||
) : (
|
||||
<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
|
||||
<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(',', '.') })}
|
||||
keyboardType={'numeric'}
|
||||
placeholder={loc.send.details.amount_placeholder}
|
||||
placeholder="0"
|
||||
maxLength={10}
|
||||
editable={!this.state.isLoading}
|
||||
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,
|
||||
placeholderTextColor="#0f5cc0"
|
||||
style={{
|
||||
color: '#0f5cc0',
|
||||
fontSize: 36,
|
||||
fontWeight: '600',
|
||||
}}
|
||||
style={{}}
|
||||
title={loc.send.details.scan}
|
||||
onPress={() => this.props.navigation.navigate('ScanQrAddress')}
|
||||
/>
|
||||
<BlueButton onPress={() => this.createTransaction()} title={loc.send.details.create} />
|
||||
<Text
|
||||
style={{
|
||||
color: '#0f5cc0',
|
||||
fontSize: 16,
|
||||
marginHorizontal: 4,
|
||||
paddingBottom: 6,
|
||||
fontWeight: '600',
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
>
|
||||
{' ' + BitcoinUnit.BTC}
|
||||
</Text>
|
||||
</View>
|
||||
</SafeBlueArea>
|
||||
<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>
|
||||
</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,
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
<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 }}>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,6 +239,7 @@ export default class WalletTransactions extends Component {
|
|||
>
|
||||
{this.state.wallet.getLabel()}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => this.changeWalletBalanceUnit()}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
|
@ -235,8 +250,9 @@ export default class WalletTransactions extends Component {
|
|||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{loc.formatBalance(this.state.wallet.getBalance())}
|
||||
{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…
Add table
Reference in a new issue