mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-25 16:04:17 +01:00
ADD: fiat input (#1190)
This commit is contained in:
parent
b159674411
commit
2243a19a2f
20 changed files with 683 additions and 210 deletions
|
@ -5,7 +5,7 @@ import { AppStorage } from './class';
|
|||
import DeviceQuickActions from './class/quick-actions';
|
||||
const prompt = require('./prompt');
|
||||
const EV = require('./events');
|
||||
const currency = require('./currency');
|
||||
const currency = require('./blue_modules/currency');
|
||||
const loc = require('./loc');
|
||||
const BlueElectrum = require('./BlueElectrum'); // eslint-disable-line no-unused-vars
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ const BlueApp = require('./BlueApp');
|
|||
const { height, width } = Dimensions.get('window');
|
||||
const aspectRatio = height / width;
|
||||
const BigNumber = require('bignumber.js');
|
||||
const currency = require('./blue_modules/currency');
|
||||
let isIpad;
|
||||
if (aspectRatio > 1.6) {
|
||||
isIpad = false;
|
||||
|
@ -579,7 +580,6 @@ export class BlueText extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BlueTextCentered extends Component {
|
||||
render() {
|
||||
return <Text {...this.props} style={{ color: BlueApp.settings.foregroundColor, textAlign: 'center' }} />;
|
||||
|
@ -828,6 +828,10 @@ export class BlueUseAllFundsButton extends Component {
|
|||
onUseAllPressed: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
unit: BitcoinUnit.BTC,
|
||||
};
|
||||
|
||||
render() {
|
||||
const inputView = (
|
||||
<View
|
||||
|
@ -1809,7 +1813,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
|
|||
return (
|
||||
<NewWalletPanel
|
||||
onPress={() => {
|
||||
onPressedOut();
|
||||
onPress(index);
|
||||
onPressedOut();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -1828,7 +1834,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
|
|||
onPressOut={item.getIsFailure() ? onPressedOut : null}
|
||||
onPress={() => {
|
||||
if (item.getIsFailure()) {
|
||||
onPressedOut();
|
||||
onPress(index);
|
||||
onPressedOut();
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -1896,7 +1904,9 @@ const WalletCarouselItem = ({ item, index, onPress, handleLongPress }) => {
|
|||
onPressOut={onPressedOut}
|
||||
onLongPress={handleLongPress}
|
||||
onPress={() => {
|
||||
onPressedOut();
|
||||
onPress(index);
|
||||
onPressedOut();
|
||||
}}
|
||||
>
|
||||
<LinearGradient
|
||||
|
@ -2016,7 +2026,7 @@ export class WalletsCarousel extends Component {
|
|||
itemWidth={itemWidth}
|
||||
inactiveSlideScale={1}
|
||||
inactiveSlideOpacity={0.7}
|
||||
initialNumToRender={4}
|
||||
initialNumToRender={20}
|
||||
onLayout={this.onLayout}
|
||||
contentContainerCustomStyle={{ left: -20 }}
|
||||
/>
|
||||
|
@ -2240,14 +2250,104 @@ export class BlueReplaceFeeSuggestions extends Component {
|
|||
export class BlueBitcoinAmount extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
/**
|
||||
* amount is a sting thats always in current unit denomination, e.g. '0.001' or '9.43' or '10000'
|
||||
*/
|
||||
amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
/**
|
||||
* callback that returns currently typed amount, in current denomination, e.g. 0.001 or 10000 or $9.34
|
||||
* (btc, sat, fiat)
|
||||
*/
|
||||
onChangeText: PropTypes.func,
|
||||
/**
|
||||
* callback thats fired to notify of currently selected denomination, returns <BitcoinUnit.*>
|
||||
*/
|
||||
onAmountUnitChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
unit: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
unit: BitcoinUnit.BTC,
|
||||
/**
|
||||
* cache of conversions fiat amount => satoshi
|
||||
* @type {{}}
|
||||
*/
|
||||
static conversionCache = {};
|
||||
|
||||
static getCachedSatoshis(amount) {
|
||||
return BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY] || false;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { unit: props.unit || BitcoinUnit.BTC, previousUnit: BitcoinUnit.SATS };
|
||||
}
|
||||
|
||||
/**
|
||||
* here we must recalculate old amont value (which was denominated in `previousUnit`) to new denomination `newUnit`
|
||||
* and fill this value in input box, so user can switch between, for example, 0.001 BTC <=> 100000 sats
|
||||
*
|
||||
* @param previousUnit {string} one of {BitcoinUnit.*}
|
||||
* @param newUnit {string} one of {BitcoinUnit.*}
|
||||
*/
|
||||
onAmountUnitChange(previousUnit, newUnit) {
|
||||
const amount = this.props.amount || 0;
|
||||
console.log('was:', amount, previousUnit, '; converting to', newUnit);
|
||||
let sats = 0;
|
||||
switch (previousUnit) {
|
||||
case BitcoinUnit.BTC:
|
||||
sats = new BigNumber(amount).multipliedBy(100000000).toString();
|
||||
break;
|
||||
case BitcoinUnit.SATS:
|
||||
sats = amount;
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
sats = new BigNumber(currency.fiatToBTC(amount)).multipliedBy(100000000).toString();
|
||||
break;
|
||||
}
|
||||
if (previousUnit === BitcoinUnit.LOCAL_CURRENCY && BlueBitcoinAmount.conversionCache[amount + previousUnit]) {
|
||||
// cache hit! we reuse old value that supposedly doesnt have rounding errors
|
||||
sats = BlueBitcoinAmount.conversionCache[amount + previousUnit];
|
||||
}
|
||||
console.log('so, in sats its', sats);
|
||||
|
||||
const newInputValue = loc.formatBalancePlain(sats, newUnit, false);
|
||||
console.log('and in', newUnit, 'its', newInputValue);
|
||||
|
||||
if (newUnit === BitcoinUnit.LOCAL_CURRENCY && previousUnit === BitcoinUnit.SATS) {
|
||||
// we cache conversion, so when we will need reverse conversion there wont be a rounding error
|
||||
BlueBitcoinAmount.conversionCache[newInputValue + newUnit] = amount;
|
||||
}
|
||||
this.props.onChangeText(newInputValue);
|
||||
if (this.props.onAmountUnitChange) this.props.onAmountUnitChange(newUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* responsible for cycling currently selected denomination, BTC->SAT->LOCAL_CURRENCY->BTC
|
||||
*/
|
||||
changeAmountUnit = () => {
|
||||
let previousUnit = this.state.unit;
|
||||
let newUnit;
|
||||
if (previousUnit === BitcoinUnit.BTC) {
|
||||
newUnit = BitcoinUnit.SATS;
|
||||
} else if (previousUnit === BitcoinUnit.SATS) {
|
||||
newUnit = BitcoinUnit.LOCAL_CURRENCY;
|
||||
} else if (previousUnit === BitcoinUnit.LOCAL_CURRENCY) {
|
||||
newUnit = BitcoinUnit.BTC;
|
||||
} else {
|
||||
newUnit = BitcoinUnit.BTC;
|
||||
previousUnit = BitcoinUnit.SATS;
|
||||
}
|
||||
this.setState({ unit: newUnit, previousUnit }, () => this.onAmountUnitChange(previousUnit, newUnit));
|
||||
};
|
||||
|
||||
maxLength = () => {
|
||||
switch (this.state.unit) {
|
||||
case BitcoinUnit.BTC:
|
||||
return 10;
|
||||
case BitcoinUnit.SATS:
|
||||
return 15;
|
||||
default:
|
||||
return 15;
|
||||
}
|
||||
};
|
||||
|
||||
textInput = React.createRef();
|
||||
|
@ -2257,85 +2357,155 @@ export class BlueBitcoinAmount extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const amount = this.props.amount || '0';
|
||||
let localCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
if (this.props.unit === BitcoinUnit.BTC) {
|
||||
let sat = new BigNumber(amount);
|
||||
sat = sat.multipliedBy(100000000).toString();
|
||||
localCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
} else {
|
||||
localCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
}
|
||||
if (amount === BitcoinUnit.MAX) localCurrency = ''; // we dont want to display NaN
|
||||
return (
|
||||
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={this.handleTextInputOnPress}>
|
||||
<View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'center', paddingTop: 16, paddingBottom: 2 }}>
|
||||
<TextInput
|
||||
{...this.props}
|
||||
testID="BitcoinAmountInput"
|
||||
keyboardType="numeric"
|
||||
onChangeText={text => {
|
||||
text = text.trim();
|
||||
text = text.replace(',', '.');
|
||||
const split = text.split('.');
|
||||
if (split.length >= 2) {
|
||||
text = `${parseInt(split[0], 10)}.${split[1]}`;
|
||||
} else {
|
||||
text = `${parseInt(split[0], 10)}`;
|
||||
}
|
||||
text = this.props.unit === BitcoinUnit.BTC ? text.replace(/[^0-9.]/g, '') : text.replace(/[^0-9]/g, '');
|
||||
text = text.replace(/(\..*)\./g, '$1');
|
||||
const amount = this.props.amount || 0;
|
||||
let secondaryDisplayCurrency = loc.formatBalanceWithoutSuffix(amount, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
|
||||
if (text.startsWith('.')) {
|
||||
text = '0.';
|
||||
}
|
||||
text = text.replace(/(0{1,}.)\./g, '$1');
|
||||
if (this.props.unit !== BitcoinUnit.BTC) {
|
||||
text = text.replace(/[^0-9.]/g, '');
|
||||
}
|
||||
this.props.onChangeText(text);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (this.props.onBlur) this.props.onBlur();
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (this.props.onFocus) this.props.onFocus();
|
||||
}}
|
||||
placeholder="0"
|
||||
maxLength={10}
|
||||
ref={this.textInput}
|
||||
editable={!this.props.isLoading && !this.props.disabled}
|
||||
value={amount}
|
||||
placeholderTextColor={this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2}
|
||||
style={{
|
||||
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
|
||||
fontSize: 36,
|
||||
fontWeight: '600',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
|
||||
fontSize: 16,
|
||||
marginHorizontal: 4,
|
||||
paddingBottom: 6,
|
||||
fontWeight: '600',
|
||||
alignSelf: 'flex-end',
|
||||
}}
|
||||
// if main display is sat or btc - secondary display is fiat
|
||||
// if main display is fiat - secondary dislay is btc
|
||||
let sat;
|
||||
switch (this.state.unit) {
|
||||
case BitcoinUnit.BTC:
|
||||
sat = new BigNumber(amount).multipliedBy(100000000).toString();
|
||||
secondaryDisplayCurrency = loc.formatBalanceWithoutSuffix(sat, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
break;
|
||||
case BitcoinUnit.SATS:
|
||||
secondaryDisplayCurrency = loc.formatBalanceWithoutSuffix(amount.toString(), BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
secondaryDisplayCurrency = currency.fiatToBTC(parseFloat(amount));
|
||||
if (BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]) {
|
||||
// cache hit! we reuse old value that supposedly doesnt have rounding errors
|
||||
const sats = BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY];
|
||||
secondaryDisplayCurrency = currency.satoshiToBTC(sats);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (amount === BitcoinUnit.MAX) secondaryDisplayCurrency = ''; // we dont want to display NaN
|
||||
return (
|
||||
<TouchableWithoutFeedback disabled={this.props.pointerEvents === 'none'} onPress={() => this.textInput.focus()}>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
{!this.props.disabled && <View style={{ alignSelf: 'center', marginLeft: 16, padding: 15 }} />}
|
||||
<View style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{ flexDirection: 'row', alignContent: 'space-between', justifyContent: 'center', paddingTop: 16, paddingBottom: 2 }}
|
||||
>
|
||||
{' ' + this.props.unit}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ alignItems: 'center', marginBottom: 22, marginTop: 4 }}>
|
||||
<Text style={{ fontSize: 18, color: '#d4d4d4', fontWeight: '600' }}>{localCurrency}</Text>
|
||||
{this.state.unit === BitcoinUnit.LOCAL_CURRENCY && (
|
||||
<Text
|
||||
style={{
|
||||
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
|
||||
fontSize: 18,
|
||||
marginHorizontal: 4,
|
||||
fontWeight: 'bold',
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{currency.getCurrencySymbol() + ' '}
|
||||
</Text>
|
||||
)}
|
||||
<TextInput
|
||||
{...this.props}
|
||||
testID="BitcoinAmountInput"
|
||||
keyboardType="numeric"
|
||||
adjustsFontSizeToFit
|
||||
onChangeText={text => {
|
||||
text = text.trim();
|
||||
if (this.state.unit !== BitcoinUnit.LOCAL_CURRENCY) {
|
||||
text = text.replace(',', '.');
|
||||
const split = text.split('.');
|
||||
if (split.length >= 2) {
|
||||
text = `${parseInt(split[0], 10)}.${split[1]}`;
|
||||
} else {
|
||||
text = `${parseInt(split[0], 10)}`;
|
||||
}
|
||||
text = this.state.unit === BitcoinUnit.BTC ? text.replace(/[^0-9.]/g, '') : text.replace(/[^0-9]/g, '');
|
||||
text = text.replace(/(\..*)\./g, '$1');
|
||||
|
||||
if (text.startsWith('.')) {
|
||||
text = '0.';
|
||||
}
|
||||
text = text.replace(/(0{1,}.)\./g, '$1');
|
||||
if (this.state.unit !== BitcoinUnit.BTC) {
|
||||
text = text.replace(/[^0-9.]/g, '');
|
||||
}
|
||||
} else if (this.state.unit === BitcoinUnit.LOCAL_CURRENCY) {
|
||||
text = text.replace(/,/gi, '');
|
||||
if (text.split('.').length > 2) {
|
||||
// too many dots. stupid code to remove all but first dot:
|
||||
let rez = '';
|
||||
let first = true;
|
||||
for (const part of text.split('.')) {
|
||||
rez += part;
|
||||
if (first) {
|
||||
rez += '.';
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
text = rez;
|
||||
}
|
||||
text = text.replace(/[^\d.,-]/g, ''); // remove all but numberd, dots & commas
|
||||
}
|
||||
|
||||
this.props.onChangeText(text);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (this.props.onBlur) this.props.onBlur();
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (this.props.onFocus) this.props.onFocus();
|
||||
}}
|
||||
placeholder="0"
|
||||
maxLength={this.maxLength()}
|
||||
ref={textInput => (this.textInput = textInput)}
|
||||
editable={!this.props.isLoading && !this.props.disabled}
|
||||
value={parseFloat(amount) > 0 || amount === BitcoinUnit.MAX ? amount : undefined}
|
||||
placeholderTextColor={
|
||||
this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2
|
||||
}
|
||||
style={{
|
||||
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
|
||||
fontWeight: 'bold',
|
||||
fontSize: amount.length > 10 ? 20 : 36,
|
||||
}}
|
||||
/>
|
||||
{this.state.unit !== BitcoinUnit.LOCAL_CURRENCY && (
|
||||
<Text
|
||||
style={{
|
||||
color: this.props.disabled ? BlueApp.settings.buttonDisabledTextColor : BlueApp.settings.alternativeTextColor2,
|
||||
fontSize: 15,
|
||||
marginHorizontal: 4,
|
||||
fontWeight: '600',
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{' ' + this.state.unit}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={{ alignItems: 'center', marginBottom: 22 }}>
|
||||
<Text style={{ fontSize: 16, color: '#9BA0A9', fontWeight: '600' }}>
|
||||
{this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX
|
||||
? loc.removeTrailingZeros(secondaryDisplayCurrency)
|
||||
: secondaryDisplayCurrency}
|
||||
{this.state.unit === BitcoinUnit.LOCAL_CURRENCY && amount !== BitcoinUnit.MAX ? ` ${BitcoinUnit.BTC}` : null}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{!this.props.disabled && (
|
||||
<TouchableOpacity
|
||||
style={{ alignSelf: 'center', marginRight: 16, paddingLeft: 16, paddingVertical: 16 }}
|
||||
onPress={this.changeAmountUnit}
|
||||
>
|
||||
<Image source={require('./img/round-compare-arrows-24-px.png')} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
balanceBlur: {
|
||||
height: 30,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Frisbee from 'frisbee';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import { AppStorage } from './class';
|
||||
import { FiatUnit } from './models/fiatUnit';
|
||||
import { AppStorage } from '../class';
|
||||
import { FiatUnit } from '../models/fiatUnit';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
import DeviceQuickActions from './class/quick-actions';
|
||||
import DeviceQuickActions from '../class/quick-actions';
|
||||
const BigNumber = require('bignumber.js');
|
||||
let preferredFiatCurrency = FiatUnit.USD;
|
||||
const exchangeRates = {};
|
||||
|
@ -129,13 +129,49 @@ function satoshiToBTC(satoshi) {
|
|||
return b.toString(10);
|
||||
}
|
||||
|
||||
function btcToSatoshi(btc) {
|
||||
return new BigNumber(btc).multipliedBy(100000000).toNumber();
|
||||
}
|
||||
|
||||
function fiatToBTC(fiatFloat) {
|
||||
let b = new BigNumber(fiatFloat);
|
||||
b = b.dividedBy(exchangeRates['BTC_' + preferredFiatCurrency.endPointKey]).toFixed(8);
|
||||
return b;
|
||||
}
|
||||
|
||||
function getCurrencySymbol() {
|
||||
return preferredFiatCurrency.symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to mock data in tests
|
||||
*
|
||||
* @param {object} currency, one of FiatUnit.*
|
||||
*/
|
||||
function _setPreferredFiatCurrency(currency) {
|
||||
preferredFiatCurrency = currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to mock data in tests
|
||||
*
|
||||
* @param {string} pair as expected by rest of this module, e.g 'BTC_JPY' or 'BTC_USD'
|
||||
* @param {number} rate exchange rate
|
||||
*/
|
||||
function _setExchangeRate(pair, rate) {
|
||||
exchangeRates[pair] = rate;
|
||||
}
|
||||
|
||||
module.exports.updateExchangeRate = updateExchangeRate;
|
||||
module.exports.startUpdater = startUpdater;
|
||||
module.exports.STRUCT = STRUCT;
|
||||
module.exports.satoshiToLocalCurrency = satoshiToLocalCurrency;
|
||||
module.exports.fiatToBTC = fiatToBTC;
|
||||
module.exports.satoshiToBTC = satoshiToBTC;
|
||||
module.exports.BTCToLocalCurrency = BTCToLocalCurrency;
|
||||
module.exports.setPrefferedCurrency = setPrefferedCurrency;
|
||||
module.exports.getPreferredCurrency = getPreferredCurrency;
|
||||
module.exports.exchangeRates = exchangeRates; // export it to mock data in tests
|
||||
module.exports.preferredFiatCurrency = preferredFiatCurrency; // export it to mock data in tests
|
||||
module.exports.btcToSatoshi = btcToSatoshi;
|
||||
module.exports.getCurrencySymbol = getCurrencySymbol;
|
||||
module.exports._setPreferredFiatCurrency = _setPreferredFiatCurrency; // export it to mock data in tests
|
||||
module.exports._setExchangeRate = _setExchangeRate; // export it to mock data in tests
|
BIN
img/round-compare-arrows-24-px.png
Executable file
BIN
img/round-compare-arrows-24-px.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 380 B |
BIN
img/round-compare-arrows-24-px@2x.png
Executable file
BIN
img/round-compare-arrows-24-px@2x.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 636 B |
BIN
img/round-compare-arrows-24-px@3x.png
Executable file
BIN
img/round-compare-arrows-24-px@3x.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 942 B |
54
loc/index.js
54
loc/index.js
|
@ -4,7 +4,7 @@ import { AppStorage } from '../class';
|
|||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
const dayjs = require('dayjs');
|
||||
const currency = require('../currency');
|
||||
const currency = require('../blue_modules/currency');
|
||||
const BigNumber = require('bignumber.js');
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
@ -149,7 +149,7 @@ strings.transactionTimeToReadable = time => {
|
|||
return ret;
|
||||
};
|
||||
|
||||
function removeTrailingZeros(value) {
|
||||
strings.removeTrailingZeros = value => {
|
||||
value = value.toString();
|
||||
|
||||
if (value.indexOf('.') === -1) {
|
||||
|
@ -159,21 +159,22 @@ function removeTrailingZeros(value) {
|
|||
value = value.substr(0, value.length - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param balance {Number} Float amount of bitcoins
|
||||
* @param balance {number} Satoshis
|
||||
* @param toUnit {String} Value from models/bitcoinUnits.js
|
||||
* @param withFormatting {boolean} Works only with `BitcoinUnit.SATS`, makes spaces wetween groups of 000
|
||||
* @returns {string}
|
||||
*/
|
||||
strings.formatBalance = (balance, toUnit, withFormatting = false) => {
|
||||
function formatBalance(balance, toUnit, withFormatting = false) {
|
||||
if (toUnit === undefined) {
|
||||
return balance + ' ' + BitcoinUnit.BTC;
|
||||
}
|
||||
if (toUnit === BitcoinUnit.BTC) {
|
||||
const value = new BigNumber(balance).dividedBy(100000000).toFixed(8);
|
||||
return removeTrailingZeros(value) + ' ' + BitcoinUnit.BTC;
|
||||
return strings.removeTrailingZeros(value) + ' ' + BitcoinUnit.BTC;
|
||||
} else if (toUnit === BitcoinUnit.SATS) {
|
||||
return (
|
||||
(balance < 0 ? '-' : '') +
|
||||
|
@ -184,22 +185,23 @@ strings.formatBalance = (balance, toUnit, withFormatting = false) => {
|
|||
} else if (toUnit === BitcoinUnit.LOCAL_CURRENCY) {
|
||||
return currency.satoshiToLocalCurrency(balance);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param balance {Integer} Satoshis
|
||||
* @param toUnit {String} Value from models/bitcoinUnits.js
|
||||
* @param toUnit {String} Value from models/bitcoinUnits.js, for example `BitcoinUnit.SATS`
|
||||
* @param withFormatting {boolean} Works only with `BitcoinUnit.SATS`, makes spaces wetween groups of 000
|
||||
* @returns {string}
|
||||
*/
|
||||
strings.formatBalanceWithoutSuffix = (balance = 0, toUnit, withFormatting = false) => {
|
||||
function formatBalanceWithoutSuffix(balance = 0, toUnit, withFormatting = false) {
|
||||
if (toUnit === undefined) {
|
||||
return balance;
|
||||
}
|
||||
if (balance !== 0) {
|
||||
if (toUnit === BitcoinUnit.BTC) {
|
||||
const value = new BigNumber(balance).dividedBy(100000000).toFixed(8);
|
||||
return removeTrailingZeros(value);
|
||||
return strings.removeTrailingZeros(value);
|
||||
} else if (toUnit === BitcoinUnit.SATS) {
|
||||
return (balance < 0 ? '-' : '') + (withFormatting ? new Intl.NumberFormat().format(balance).replace(/[^0-9]/g, ' ') : balance);
|
||||
} else if (toUnit === BitcoinUnit.LOCAL_CURRENCY) {
|
||||
|
@ -207,6 +209,36 @@ strings.formatBalanceWithoutSuffix = (balance = 0, toUnit, withFormatting = fals
|
|||
}
|
||||
}
|
||||
return balance.toString();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be used when we need a simple string to be put in text input, for example
|
||||
*
|
||||
* @param balance {integer} Satoshis
|
||||
* @param toUnit {String} Value from models/bitcoinUnits.js, for example `BitcoinUnit.SATS`
|
||||
* @param withFormatting {boolean} Works only with `BitcoinUnit.SATS`, makes spaces wetween groups of 000
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatBalancePlain(balance = 0, toUnit, withFormatting = false) {
|
||||
const newInputValue = formatBalanceWithoutSuffix(balance, toUnit, withFormatting);
|
||||
return _leaveNumbersAndDots(newInputValue);
|
||||
}
|
||||
|
||||
function _leaveNumbersAndDots(newInputValue) {
|
||||
newInputValue = newInputValue.replace(/[^\d.,-]/g, ''); // filtering, leaving only numbers, dots & commas
|
||||
if (newInputValue.endsWith('.00') || newInputValue.endsWith(',00')) newInputValue = newInputValue.substring(0, newInputValue.length - 3);
|
||||
|
||||
if (newInputValue[newInputValue.length - 3] === ',') {
|
||||
// this is a fractional value, lets replace comma to dot so it represents actual fractional value for normal people
|
||||
newInputValue = newInputValue.substring(0, newInputValue.length - 3) + '.' + newInputValue.substring(newInputValue.length - 2);
|
||||
}
|
||||
newInputValue = newInputValue.replace(/,/gi, '');
|
||||
|
||||
return newInputValue;
|
||||
}
|
||||
|
||||
module.exports = strings;
|
||||
module.exports.formatBalanceWithoutSuffix = formatBalanceWithoutSuffix;
|
||||
module.exports.formatBalance = formatBalance;
|
||||
module.exports.formatBalancePlain = formatBalancePlain;
|
||||
module.exports._leaveNumbersAndDots = _leaveNumbersAndDots;
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
export class BitcoinTransaction {
|
||||
constructor(address = '', amount) {
|
||||
/**
|
||||
*
|
||||
* @param address
|
||||
* @param amount {number}
|
||||
* @param amountSats {integer} satoshi
|
||||
*/
|
||||
constructor(address = '', amount, amountSats) {
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
this.amountSats = amountSats;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
|||
import NavigationService from '../../NavigationService';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import { Icon } from 'react-native-elements';
|
||||
const currency = require('../../blue_modules/currency');
|
||||
const BlueApp = require('../../BlueApp');
|
||||
const EV = require('../../events');
|
||||
const loc = require('../../loc');
|
||||
|
@ -137,6 +138,7 @@ export default class LNDCreateInvoice extends Component {
|
|||
super(props);
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
|
||||
/** @type LightningCustodianWallet */
|
||||
let fromWallet;
|
||||
if (props.route.params.fromWallet) fromWallet = props.route.params.fromWallet;
|
||||
|
||||
|
@ -152,6 +154,7 @@ export default class LNDCreateInvoice extends Component {
|
|||
this.state = {
|
||||
fromWallet,
|
||||
amount: '',
|
||||
unit: fromWallet.preferredBalanceUnit,
|
||||
description: '',
|
||||
lnurl: '',
|
||||
lnurlParams: null,
|
||||
|
@ -170,6 +173,7 @@ export default class LNDCreateInvoice extends Component {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
console.log('lnd/lndCreateInvoice mounted');
|
||||
if (this.state.fromWallet.getUserHasSavedExport()) {
|
||||
this.renderReceiveDetails();
|
||||
} else {
|
||||
|
@ -201,7 +205,22 @@ export default class LNDCreateInvoice extends Component {
|
|||
async createInvoice() {
|
||||
this.setState({ isLoading: true }, async () => {
|
||||
try {
|
||||
const invoiceRequest = await this.state.fromWallet.addInvoice(this.state.amount, this.state.description);
|
||||
this.setState({ isLoading: false });
|
||||
let amount = this.state.amount;
|
||||
switch (this.state.unit) {
|
||||
case BitcoinUnit.SATS:
|
||||
amount = parseInt(amount); // basically nop
|
||||
break;
|
||||
case BitcoinUnit.BTC:
|
||||
amount = currency.btcToSatoshi(amount);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
// trying to fetch cached sat equivalent for this fiat amount
|
||||
amount = BlueBitcoinAmount.getCachedSatoshis(amount) || currency.btcToSatoshi(currency.fiatToBTC(amount));
|
||||
break;
|
||||
}
|
||||
|
||||
const invoiceRequest = await this.state.fromWallet.addInvoice(amount, this.state.description);
|
||||
EV(EV.enum.TRANSACTIONS_COUNT_CHANGED);
|
||||
ReactNativeHapticFeedback.trigger('notificationSuccess', { ignoreAndroidSystemSettings: false });
|
||||
|
||||
|
@ -379,6 +398,9 @@ export default class LNDCreateInvoice extends Component {
|
|||
<BlueBitcoinAmount
|
||||
isLoading={this.state.isLoading}
|
||||
amount={this.state.amount}
|
||||
onAmountUnitChange={unit => {
|
||||
this.setState({ unit });
|
||||
}}
|
||||
onChangeText={text => {
|
||||
if (this.state.lnurlParams) {
|
||||
// in this case we prevent the user from changing the amount to < min or > max
|
||||
|
@ -394,7 +416,7 @@ export default class LNDCreateInvoice extends Component {
|
|||
this.setState({ amount: text });
|
||||
}}
|
||||
disabled={this.state.isLoading || (this.state.lnurlParams && this.state.lnurlParams.fixed)}
|
||||
unit={BitcoinUnit.SATS}
|
||||
unit={this.state.unit}
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
<View style={styles.fiat}>
|
||||
|
|
|
@ -21,6 +21,7 @@ import Biometric from '../../class/biometrics';
|
|||
const BlueApp = require('../../BlueApp');
|
||||
const EV = require('../../events');
|
||||
const loc = require('../../loc');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
walletSelectRoot: {
|
||||
|
@ -138,6 +139,7 @@ export default class ScanLndInvoice extends React.Component {
|
|||
this.state = {
|
||||
fromWallet,
|
||||
fromSecret,
|
||||
unit: BitcoinUnit.SATS,
|
||||
destination: '',
|
||||
};
|
||||
}
|
||||
|
@ -174,6 +176,8 @@ export default class ScanLndInvoice extends React.Component {
|
|||
return {
|
||||
invoice: data,
|
||||
decoded,
|
||||
unit: state.unit,
|
||||
amount: decoded.num_satoshis,
|
||||
expiresIn,
|
||||
destination: data,
|
||||
isAmountInitiallyEmpty: decoded.num_satoshis === '0',
|
||||
|
@ -220,6 +224,19 @@ export default class ScanLndInvoice extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
let amountSats = this.state.amount;
|
||||
switch (this.state.unit) {
|
||||
case BitcoinUnit.SATS:
|
||||
amountSats = parseInt(amountSats); // nop
|
||||
break;
|
||||
case BitcoinUnit.BTC:
|
||||
amountSats = currency.btcToSatoshi(amountSats);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
amountSats = currency.btcToSatoshi(currency.fiatToBTC(amountSats));
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
isLoading: true,
|
||||
|
@ -245,7 +262,7 @@ export default class ScanLndInvoice extends React.Component {
|
|||
}
|
||||
|
||||
try {
|
||||
await fromWallet.payInvoice(this.state.invoice, this.state.decoded.num_satoshis);
|
||||
await fromWallet.payInvoice(this.state.invoice, amountSats);
|
||||
} catch (Err) {
|
||||
console.log(Err.message);
|
||||
this.setState({ isLoading: false });
|
||||
|
@ -255,7 +272,7 @@ export default class ScanLndInvoice extends React.Component {
|
|||
|
||||
EV(EV.enum.REMOTE_TRANSACTIONS_COUNT_CHANGED); // someone should fetch txs
|
||||
this.props.navigation.navigate('Success', {
|
||||
amount: this.state.decoded.num_satoshis,
|
||||
amount: amountSats,
|
||||
amountUnit: BitcoinUnit.SATS,
|
||||
invoiceDescription: this.state.decoded.description,
|
||||
});
|
||||
|
@ -275,11 +292,12 @@ export default class ScanLndInvoice extends React.Component {
|
|||
if (typeof this.state.decoded !== 'object') {
|
||||
return true;
|
||||
} else {
|
||||
if (!this.state.decoded.num_satoshis) {
|
||||
if (!this.state.amount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.state.decoded.num_satoshis <= 0 || this.state.isLoading || isNaN(this.state.decoded.num_satoshis);
|
||||
return !(this.state.amount > 0);
|
||||
// return this.state.decoded.num_satoshis <= 0 || this.state.isLoading || isNaN(this.state.decoded.num_satoshis);
|
||||
};
|
||||
|
||||
renderWalletSelectionButton = () => {
|
||||
|
@ -327,6 +345,10 @@ export default class ScanLndInvoice extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
console.log('scanLndInvoice did mount');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.fromWallet) {
|
||||
return <BlueLoading />;
|
||||
|
@ -340,16 +362,25 @@ export default class ScanLndInvoice extends React.Component {
|
|||
<BlueBitcoinAmount
|
||||
pointerEvents={this.state.isAmountInitiallyEmpty ? 'auto' : 'none'}
|
||||
isLoading={this.state.isLoading}
|
||||
amount={typeof this.state.decoded === 'object' ? this.state.decoded.num_satoshis : 0}
|
||||
amount={this.state.amount}
|
||||
onAmountUnitChange={unit => {
|
||||
this.setState({ unit });
|
||||
}}
|
||||
onChangeText={text => {
|
||||
if (typeof this.state.decoded === 'object') {
|
||||
this.setState({ amount: text });
|
||||
|
||||
/* if (typeof this.state.decoded === 'object') {
|
||||
text = parseInt(text || 0);
|
||||
const decoded = this.state.decoded;
|
||||
decoded.num_satoshis = text;
|
||||
this.setState({ decoded: decoded });
|
||||
}
|
||||
} */
|
||||
}}
|
||||
disabled={typeof this.state.decoded !== 'object' || this.state.isLoading}
|
||||
disabled={
|
||||
typeof this.state.decoded !== 'object' ||
|
||||
this.state.isLoading ||
|
||||
(this.state.decoded && this.state.decoded.num_satoshis > 0)
|
||||
}
|
||||
unit={BitcoinUnit.SATS}
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { View, InteractionManager, Platform, TextInput, KeyboardAvoidingView, Keyboard, StyleSheet, ScrollView } from 'react-native';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { useNavigation, useRoute, useIsFocused } from '@react-navigation/native';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import {
|
||||
BlueLoading,
|
||||
SafeBlueArea,
|
||||
|
@ -25,20 +25,21 @@ import Handoff from 'react-native-handoff';
|
|||
/** @type {AppStorage} */
|
||||
const BlueApp = require('../../BlueApp');
|
||||
const loc = require('../../loc');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
|
||||
const ReceiveDetails = () => {
|
||||
const { secret } = useRoute().params;
|
||||
const [wallet, setWallet] = useState();
|
||||
const wallet = BlueApp.getWallets().find(w => w.getSecret() === secret);
|
||||
const [isHandOffUseEnabled, setIsHandOffUseEnabled] = useState(false);
|
||||
const [address, setAddress] = useState('');
|
||||
const [customLabel, setCustomLabel] = useState();
|
||||
const [customAmount, setCustomAmount] = useState(0);
|
||||
const [customUnit, setCustomUnit] = useState(BitcoinUnit.BTC);
|
||||
const [bip21encoded, setBip21encoded] = useState();
|
||||
const [qrCodeSVG, setQrCodeSVG] = useState();
|
||||
const [isCustom, setIsCustom] = useState(false);
|
||||
const [isCustomModalVisible, setIsCustomModalVisible] = useState(false);
|
||||
const { navigate, goBack } = useNavigation();
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const renderReceiveDetails = useCallback(async () => {
|
||||
console.log('receive/details - componentDidMount');
|
||||
|
@ -82,10 +83,6 @@ const ReceiveDetails = () => {
|
|||
}, [wallet]);
|
||||
|
||||
useEffect(() => {
|
||||
Privacy.enableBlur();
|
||||
|
||||
setWallet(BlueApp.getWallets().find(w => w.getSecret() === secret));
|
||||
|
||||
if (wallet) {
|
||||
if (!wallet.getUserHasSavedExport()) {
|
||||
BlueAlertWalletExportReminder({
|
||||
|
@ -102,6 +99,7 @@ const ReceiveDetails = () => {
|
|||
}
|
||||
}
|
||||
HandoffSettings.isHandoffUseEnabled().then(setIsHandOffUseEnabled);
|
||||
Privacy.enableBlur();
|
||||
return () => Privacy.disableBlur();
|
||||
}, [goBack, navigate, renderReceiveDetails, secret, wallet]);
|
||||
|
||||
|
@ -117,7 +115,24 @@ const ReceiveDetails = () => {
|
|||
const createCustomAmountAddress = () => {
|
||||
setIsCustom(true);
|
||||
setIsCustomModalVisible(false);
|
||||
setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount: customAmount, label: customLabel }));
|
||||
let amount = customAmount;
|
||||
switch (customUnit) {
|
||||
case BitcoinUnit.BTC:
|
||||
// nop
|
||||
break;
|
||||
case BitcoinUnit.SATS:
|
||||
amount = currency.satoshiToBTC(customAmount);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
if (BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]) {
|
||||
// cache hit! we reuse old value that supposedly doesnt have rounding errors
|
||||
amount = currency.satoshiToBTC(BlueBitcoinAmount.conversionCache[amount + BitcoinUnit.LOCAL_CURRENCY]);
|
||||
} else {
|
||||
amount = currency.fiatToBTC(customAmount);
|
||||
}
|
||||
break;
|
||||
}
|
||||
setBip21encoded(DeeplinkSchemaMatch.bip21encode(address, { amount, label: customLabel }));
|
||||
};
|
||||
|
||||
const clearCustomAmount = () => {
|
||||
|
@ -133,7 +148,12 @@ const ReceiveDetails = () => {
|
|||
<Modal isVisible={isCustomModalVisible} style={styles.bottomModal} onBackdropPress={dismissCustomAmountModal}>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'position' : null}>
|
||||
<View style={styles.modalContent}>
|
||||
<BlueBitcoinAmount amount={customAmount || ''} onChangeText={setCustomAmount} />
|
||||
<BlueBitcoinAmount
|
||||
unit={customUnit}
|
||||
amount={customAmount || ''}
|
||||
onChangeText={setCustomAmount}
|
||||
onAmountUnitChange={setCustomUnit}
|
||||
/>
|
||||
<View style={styles.customAmount}>
|
||||
<TextInput
|
||||
onChangeText={setCustomLabel}
|
||||
|
@ -172,6 +192,21 @@ const ReceiveDetails = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {string} BTC amount, accounting for current `customUnit` and `customUnit`
|
||||
*/
|
||||
const getDisplayAmount = () => {
|
||||
switch (customUnit) {
|
||||
case BitcoinUnit.BTC:
|
||||
return customAmount + ' BTC';
|
||||
case BitcoinUnit.SATS:
|
||||
return currency.satoshiToBTC(customAmount) + ' BTC';
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
return currency.fiatToBTC(customAmount) + ' BTC';
|
||||
}
|
||||
return customAmount + ' ' + customUnit;
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeBlueArea style={styles.root}>
|
||||
{isHandOffUseEnabled && address !== undefined && (
|
||||
|
@ -181,19 +216,19 @@ const ReceiveDetails = () => {
|
|||
url={`https://blockstream.info/address/${address}`}
|
||||
/>
|
||||
)}
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="always">
|
||||
<View style={styles.scrollBody}>
|
||||
{isCustom && (
|
||||
<>
|
||||
<BlueText style={styles.amount} numberOfLines={1}>
|
||||
{customAmount} {BitcoinUnit.BTC}
|
||||
{getDisplayAmount()}
|
||||
</BlueText>
|
||||
<BlueText style={styles.label} numberOfLines={1}>
|
||||
{customLabel}
|
||||
</BlueText>
|
||||
</>
|
||||
)}
|
||||
{bip21encoded === undefined && isFocused ? (
|
||||
{bip21encoded === undefined ? (
|
||||
<View style={styles.loading}>
|
||||
<BlueLoading />
|
||||
</View>
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '../../class';
|
||||
const loc = require('../../loc');
|
||||
const EV = require('../../events');
|
||||
const currency = require('../../currency');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
const BlueElectrum = require('../../BlueElectrum');
|
||||
const Bignumber = require('bignumber.js');
|
||||
/** @type {AppStorage} */
|
||||
|
@ -125,6 +125,11 @@ export default class Confirm extends Component {
|
|||
</Text>
|
||||
<Text style={styles.valueUnit}>{' ' + BitcoinUnit.BTC}</Text>
|
||||
</View>
|
||||
<Text style={styles.transactionAmountFiat}>
|
||||
{item.value !== BitcoinUnit.MAX && item.value
|
||||
? currency.satoshiToLocalCurrency(item.value)
|
||||
: currency.satoshiToLocalCurrency(this.state.fromWallet.getBalance() - this.state.feeSatoshi)}
|
||||
</Text>
|
||||
<BlueCard>
|
||||
<Text style={styles.transactionDetailsTitle}>{loc.send.create.to}</Text>
|
||||
<Text style={styles.transactionDetailsSubtitle}>{item.address}</Text>
|
||||
|
@ -212,6 +217,13 @@ const styles = StyleSheet.create({
|
|||
fontSize: 15,
|
||||
marginBottom: 20,
|
||||
},
|
||||
transactionAmountFiat: {
|
||||
color: '#9aa0aa',
|
||||
fontWeight: '500',
|
||||
fontSize: 15,
|
||||
marginVertical: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
valueWrap: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
|
@ -247,6 +259,7 @@ const styles = StyleSheet.create({
|
|||
marginTop: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
flat: {
|
||||
maxHeight: '55%',
|
||||
|
@ -256,6 +269,7 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
cardText: {
|
||||
color: '#37c0a1',
|
||||
|
|
|
@ -25,7 +25,7 @@ import RNFS from 'react-native-fs';
|
|||
/** @type {AppStorage} */
|
||||
const BlueApp = require('../../BlueApp');
|
||||
const loc = require('../../loc');
|
||||
const currency = require('../../currency');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
|
||||
export default class SendCreate extends Component {
|
||||
static navigationOptions = ({ navigation, route }) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global alert */
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
View,
|
||||
TextInput,
|
||||
Alert,
|
||||
|
@ -13,7 +14,6 @@ import {
|
|||
Dimensions,
|
||||
Platform,
|
||||
ScrollView,
|
||||
ActivityIndicator,
|
||||
Text,
|
||||
} from 'react-native';
|
||||
import { Icon } from 'react-native-elements';
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
BlueBitcoinAmount,
|
||||
BlueAddressInput,
|
||||
BlueDismissKeyboardInputAccessory,
|
||||
BlueLoading,
|
||||
BlueUseAllFundsButton,
|
||||
BlueListItem,
|
||||
BlueText,
|
||||
|
@ -41,6 +42,7 @@ import DocumentPicker from 'react-native-document-picker';
|
|||
import RNFS from 'react-native-fs';
|
||||
import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
|
||||
const bitcoin = require('bitcoinjs-lib');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
const BigNumber = require('bignumber.js');
|
||||
const { width } = Dimensions.get('window');
|
||||
const BlueApp: AppStorage = require('../../BlueApp');
|
||||
|
@ -52,6 +54,7 @@ const styles = StyleSheet.create({
|
|||
loading: {
|
||||
flex: 1,
|
||||
paddingTop: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
root: {
|
||||
flex: 1,
|
||||
|
@ -254,10 +257,12 @@ export default class SendDetails extends Component {
|
|||
recipientsScrollIndex: 0,
|
||||
fromWallet,
|
||||
addresses: [],
|
||||
units: [],
|
||||
memo: '',
|
||||
networkTransactionFees: new NetworkTransactionFee(1, 1, 1),
|
||||
fee: 1,
|
||||
feeSliderValue: 1,
|
||||
amountUnit: fromWallet.preferredBalanceUnit, // default for whole screen
|
||||
bip70TransactionExpiration: null,
|
||||
renderWalletSelectionButtonHidden: false,
|
||||
};
|
||||
|
@ -289,18 +294,22 @@ export default class SendDetails extends Component {
|
|||
feeSliderValue: bip70.feeSliderValue,
|
||||
fee: bip70.fee,
|
||||
isLoading: false,
|
||||
amountUnit: BitcoinUnit.BTC,
|
||||
bip70TransactionExpiration: bip70.bip70TransactionExpiration,
|
||||
});
|
||||
} else {
|
||||
console.warn('2');
|
||||
const recipients = this.state.addresses;
|
||||
const dataWithoutSchema = data.replace('bitcoin:', '').replace('BITCOIN:', '');
|
||||
if (this.state.fromWallet.isAddressValid(dataWithoutSchema)) {
|
||||
recipients[[this.state.recipientsScrollIndex]].address = dataWithoutSchema;
|
||||
const units = this.state.units;
|
||||
units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
|
||||
this.setState({
|
||||
address: recipients,
|
||||
bip70TransactionExpiration: null,
|
||||
isLoading: false,
|
||||
amountUnit: BitcoinUnit.BTC,
|
||||
units,
|
||||
});
|
||||
} else {
|
||||
let address = '';
|
||||
|
@ -322,6 +331,8 @@ export default class SendDetails extends Component {
|
|||
}
|
||||
console.log(options);
|
||||
if (btcAddressRx.test(address) || address.indexOf('bc1') === 0 || address.indexOf('BC1') === 0) {
|
||||
const units = this.state.units;
|
||||
units[this.state.recipientsScrollIndex] = BitcoinUnit.BTC; // also resetting current unit to BTC
|
||||
recipients[[this.state.recipientsScrollIndex]].address = address;
|
||||
recipients[[this.state.recipientsScrollIndex]].amount = options.amount;
|
||||
this.setState({
|
||||
|
@ -329,6 +340,8 @@ export default class SendDetails extends Component {
|
|||
memo: options.label || options.message,
|
||||
bip70TransactionExpiration: null,
|
||||
isLoading: false,
|
||||
amountUnit: BitcoinUnit.BTC,
|
||||
units,
|
||||
});
|
||||
} else {
|
||||
this.setState({ isLoading: false });
|
||||
|
@ -342,6 +355,7 @@ export default class SendDetails extends Component {
|
|||
this.renderNavigationHeader();
|
||||
console.log('send/details - componentDidMount');
|
||||
StatusBar.setBarStyle('dark-content');
|
||||
/** @type {BitcoinTransaction[]} */
|
||||
const addresses = [];
|
||||
let initialMemo = '';
|
||||
if (this.props.route.params.uri) {
|
||||
|
@ -350,13 +364,13 @@ export default class SendDetails extends Component {
|
|||
const { recipient, memo, fee, feeSliderValue } = await this.processBIP70Invoice(uri);
|
||||
addresses.push(recipient);
|
||||
initialMemo = memo;
|
||||
this.setState({ addresses, memo: initialMemo, fee, feeSliderValue, isLoading: false });
|
||||
this.setState({ addresses, memo: initialMemo, fee, feeSliderValue, isLoading: false, amountUnit: BitcoinUnit.BTC });
|
||||
} else {
|
||||
try {
|
||||
const { address, amount, memo } = this.decodeBitcoinUri(uri);
|
||||
addresses.push(new BitcoinTransaction(address, amount));
|
||||
addresses.push(new BitcoinTransaction(address, amount, currency.btcToSatoshi(amount)));
|
||||
initialMemo = memo;
|
||||
this.setState({ addresses, memo: initialMemo, isLoading: false });
|
||||
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert('Error: Unable to decode Bitcoin address');
|
||||
|
@ -365,7 +379,7 @@ export default class SendDetails extends Component {
|
|||
} else if (this.props.route.params.address) {
|
||||
addresses.push(new BitcoinTransaction(this.props.route.params.address));
|
||||
if (this.props.route.params.memo) initialMemo = this.props.route.params.memo;
|
||||
this.setState({ addresses, memo: initialMemo, isLoading: false });
|
||||
this.setState({ addresses, memo: initialMemo, isLoading: false, amountUnit: BitcoinUnit.BTC });
|
||||
} else {
|
||||
this.setState({ addresses: [new BitcoinTransaction()], isLoading: false });
|
||||
}
|
||||
|
@ -446,33 +460,13 @@ export default class SendDetails extends Component {
|
|||
return { address, amount, memo };
|
||||
}
|
||||
|
||||
recalculateAvailableBalance(balance, amount, fee) {
|
||||
if (!amount) amount = 0;
|
||||
if (!fee) fee = 0;
|
||||
let availableBalance;
|
||||
try {
|
||||
availableBalance = new BigNumber(balance);
|
||||
availableBalance = availableBalance.div(100000000); // sat2btc
|
||||
availableBalance = availableBalance.minus(amount);
|
||||
availableBalance = availableBalance.minus(fee);
|
||||
availableBalance = availableBalance.toString(10);
|
||||
} catch (err) {
|
||||
return balance;
|
||||
}
|
||||
|
||||
return (availableBalance === 'NaN' && balance) || availableBalance;
|
||||
}
|
||||
|
||||
async processBIP70Invoice(text) {
|
||||
try {
|
||||
if (BitcoinBIP70TransactionDecode.matchesPaymentURL(text)) {
|
||||
Keyboard.dismiss();
|
||||
return BitcoinBIP70TransactionDecode.decode(text)
|
||||
.then(response => {
|
||||
const recipient = new BitcoinTransaction(
|
||||
response.address,
|
||||
loc.formatBalanceWithoutSuffix(response.amount, BitcoinUnit.BTC, false),
|
||||
);
|
||||
const recipient = new BitcoinTransaction(response.address, currency.satoshiToBTC(response.amount), response.amount);
|
||||
return {
|
||||
recipient,
|
||||
memo: response.memo,
|
||||
|
@ -507,7 +501,7 @@ export default class SendDetails extends Component {
|
|||
} else if (!transaction.address) {
|
||||
error = loc.send.details.address_field_is_not_valid;
|
||||
console.log('validation error');
|
||||
} else if (this.recalculateAvailableBalance(this.state.fromWallet.getBalance(), transaction.amount, 0) < 0) {
|
||||
} else if (this.state.fromWallet.getBalance() - transaction.amountSats < 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');
|
||||
|
@ -577,7 +571,7 @@ export default class SendDetails extends Component {
|
|||
targets = [{ address: transaction.address }];
|
||||
break;
|
||||
}
|
||||
const value = new BigNumber(transaction.amount).multipliedBy(100000000).toNumber();
|
||||
const value = parseInt(transaction.amountSats);
|
||||
if (value > 0) {
|
||||
targets.push({ address: transaction.address, value });
|
||||
}
|
||||
|
@ -787,7 +781,7 @@ export default class SendDetails extends Component {
|
|||
const isSendMaxUsed = this.state.addresses.some(element => element.amount === BitcoinUnit.MAX);
|
||||
return (
|
||||
<Modal
|
||||
isVisible={this.state.isAdvancedTransactionOptionsVisible && !this.state.isLoading}
|
||||
isVisible={this.state.isAdvancedTransactionOptionsVisible}
|
||||
style={styles.bottomModal}
|
||||
onBackdropPress={() => {
|
||||
Keyboard.dismiss();
|
||||
|
@ -835,6 +829,8 @@ export default class SendDetails extends Component {
|
|||
() => {
|
||||
this.scrollView.scrollToEnd();
|
||||
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
|
||||
// after adding recipient it automatically scrolls to the last one
|
||||
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
|
||||
},
|
||||
);
|
||||
}}
|
||||
|
@ -854,7 +850,8 @@ export default class SendDetails extends Component {
|
|||
},
|
||||
() => {
|
||||
if (this.state.addresses.length > 1) this.scrollView.flashScrollIndicators();
|
||||
this.setState({ recipientsScrollIndex: this.scrollViewCurrentIndex });
|
||||
// after deletion it automatically scrolls to the last one
|
||||
this.setState({ recipientsScrollIndex: this.state.addresses.length - 1 });
|
||||
},
|
||||
);
|
||||
}}
|
||||
|
@ -941,12 +938,49 @@ export default class SendDetails extends Component {
|
|||
<BlueBitcoinAmount
|
||||
isLoading={this.state.isLoading}
|
||||
amount={item.amount ? item.amount.toString() : null}
|
||||
onAmountUnitChange={unit => {
|
||||
const units = this.state.units;
|
||||
units[index] = unit;
|
||||
|
||||
const addresses = this.state.addresses;
|
||||
const item = addresses[index];
|
||||
|
||||
switch (unit) {
|
||||
case BitcoinUnit.SATS:
|
||||
item.amountSats = parseInt(item.amount);
|
||||
break;
|
||||
case BitcoinUnit.BTC:
|
||||
item.amountSats = currency.btcToSatoshi(item.amount);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
// also accounting for cached fiat->sat conversion to avoid rounding error
|
||||
item.amountSats =
|
||||
BlueBitcoinAmount.getCachedSatoshis(item.amount) || currency.btcToSatoshi(currency.fiatToBTC(item.amount));
|
||||
break;
|
||||
}
|
||||
|
||||
addresses[index] = item;
|
||||
this.setState({ units, addresses });
|
||||
}}
|
||||
onChangeText={text => {
|
||||
item.amount = text;
|
||||
const transactions = this.state.addresses;
|
||||
transactions[index] = item;
|
||||
this.setState({ addresses: transactions });
|
||||
switch (this.state.units[index] || this.state.amountUnit) {
|
||||
case BitcoinUnit.BTC:
|
||||
item.amountSats = currency.btcToSatoshi(item.amount);
|
||||
break;
|
||||
case BitcoinUnit.LOCAL_CURRENCY:
|
||||
item.amountSats = currency.btcToSatoshi(currency.fiatToBTC(item.amount));
|
||||
break;
|
||||
default:
|
||||
case BitcoinUnit.SATS:
|
||||
item.amountSats = parseInt(text);
|
||||
break;
|
||||
}
|
||||
const addresses = this.state.addresses;
|
||||
addresses[index] = item;
|
||||
this.setState({ addresses });
|
||||
}}
|
||||
unit={this.state.units[index] || this.state.amountUnit}
|
||||
inputAccessoryViewID={this.state.fromWallet.allowSendMax() ? BlueUseAllFundsButton.InputAccessoryViewID : null}
|
||||
/>
|
||||
<BlueAddressInput
|
||||
|
@ -1002,7 +1036,12 @@ export default class SendDetails extends Component {
|
|||
Keyboard.dismiss();
|
||||
const recipient = this.state.addresses[this.state.recipientsScrollIndex];
|
||||
recipient.amount = BitcoinUnit.MAX;
|
||||
this.setState({ addresses: [recipient], recipientsScrollIndex: 0, isAdvancedTransactionOptionsVisible: false });
|
||||
this.setState({
|
||||
addresses: [recipient],
|
||||
units: [BitcoinUnit.BTC],
|
||||
recipientsScrollIndex: 0,
|
||||
isAdvancedTransactionOptionsVisible: false,
|
||||
});
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
|
@ -1013,61 +1052,68 @@ export default class SendDetails extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const opacity = this.state.isLoading ? 0.5 : 1.0;
|
||||
if (this.state.isLoading || typeof this.state.fromWallet === 'undefined') {
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<BlueLoading />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
|
||||
<View style={styles.root} pointerEvents={this.state.isLoading ? 'none' : 'auto'}>
|
||||
<View style={styles.root}>
|
||||
<View>
|
||||
<View style={{ opacity: opacity }}>
|
||||
<KeyboardAvoidingView behavior="position">
|
||||
<ScrollView
|
||||
pagingEnabled
|
||||
horizontal
|
||||
contentContainerStyle={styles.scrollViewContent}
|
||||
ref={ref => (this.scrollView = ref)}
|
||||
onContentSizeChange={() => this.scrollView.scrollToEnd()}
|
||||
onLayout={() => this.scrollView.scrollToEnd()}
|
||||
onMomentumScrollEnd={this.handlePageChange}
|
||||
scrollEnabled={this.state.addresses.length > 1}
|
||||
scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
|
||||
>
|
||||
{this.renderBitcoinTransactionInfoFields()}
|
||||
</ScrollView>
|
||||
<View hide={!this.state.showMemoRow} style={styles.memo}>
|
||||
<TextInput
|
||||
onChangeText={text => this.setState({ memo: text })}
|
||||
placeholder={loc.send.details.note_placeholder}
|
||||
placeholderTextColor="#81868e"
|
||||
value={this.state.memo}
|
||||
numberOfLines={1}
|
||||
style={styles.memoText}
|
||||
editable={!this.state.isLoading}
|
||||
onSubmitEditing={Keyboard.dismiss}
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
<KeyboardAvoidingView behavior="position">
|
||||
<ScrollView
|
||||
pagingEnabled
|
||||
horizontal
|
||||
contentContainerStyle={styles.scrollViewContent}
|
||||
ref={ref => (this.scrollView = ref)}
|
||||
keyboardShouldPersistTaps="always"
|
||||
onContentSizeChange={() => this.scrollView.scrollToEnd()}
|
||||
onLayout={() => this.scrollView.scrollToEnd()}
|
||||
onMomentumScrollEnd={this.handlePageChange}
|
||||
scrollEnabled={this.state.addresses.length > 1}
|
||||
scrollIndicatorInsets={{ top: 0, left: 8, bottom: 0, right: 8 }}
|
||||
>
|
||||
{this.renderBitcoinTransactionInfoFields()}
|
||||
</ScrollView>
|
||||
<View hide={!this.state.showMemoRow} style={styles.memo}>
|
||||
<TextInput
|
||||
onChangeText={text => this.setState({ memo: text })}
|
||||
placeholder={loc.send.details.note_placeholder}
|
||||
placeholderTextColor="#81868e"
|
||||
value={this.state.memo}
|
||||
numberOfLines={1}
|
||||
style={styles.memoText}
|
||||
editable={!this.state.isLoading}
|
||||
onSubmitEditing={Keyboard.dismiss}
|
||||
inputAccessoryViewID={BlueDismissKeyboardInputAccessory.InputAccessoryViewID}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => this.setState({ isFeeSelectionModalVisible: true })}
|
||||
disabled={this.state.isLoading}
|
||||
style={styles.fee}
|
||||
>
|
||||
<Text style={styles.feeLabel}>Fee</Text>
|
||||
<View style={styles.feeRow}>
|
||||
<Text style={styles.feeValue}>{this.state.fee}</Text>
|
||||
<Text style={styles.feeUnit}>sat/b</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => this.setState({ isFeeSelectionModalVisible: true })}
|
||||
disabled={this.state.isLoading}
|
||||
style={styles.fee}
|
||||
>
|
||||
<Text style={styles.feeLabel}>Fee</Text>
|
||||
<View style={styles.feeRow}>
|
||||
<Text style={styles.feeValue}>{this.state.fee}</Text>
|
||||
<Text style={styles.feeUnit}>sat/b</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{this.renderFeeSelectionModal()}
|
||||
{this.renderAdvancedTransactionOptionsModal()}
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
{this.renderCreateButton()}
|
||||
</TouchableOpacity>
|
||||
{this.renderCreateButton()}
|
||||
{this.renderFeeSelectionModal()}
|
||||
{this.renderAdvancedTransactionOptionsModal()}
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
<BlueDismissKeyboardInputAccessory />
|
||||
{Platform.select({
|
||||
ios: <BlueUseAllFundsButton onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />,
|
||||
ios: (
|
||||
<BlueUseAllFundsButton unit={this.state.amountUnit} onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
|
||||
),
|
||||
android: this.state.isAmountToolbarVisibleForAndroid && (
|
||||
<BlueUseAllFundsButton onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
|
||||
<BlueUseAllFundsButton unit={this.state.amountUnit} onUseAllPressed={this.onUseAllPressed} wallet={this.state.fromWallet} />
|
||||
),
|
||||
})}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
|||
import { Icon } from 'react-native-elements';
|
||||
import { FiatUnit } from '../../models/fiatUnit';
|
||||
const loc = require('../../loc');
|
||||
const currency = require('../../currency');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
|
||||
const data = Object.values(FiatUnit);
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { BlueNavigationStyle, BlueLoading, SafeBlueArea } from '../../BlueCompon
|
|||
import PropTypes from 'prop-types';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { AppStorage, LightningCustodianWallet, WatchOnlyWallet } from '../../class';
|
||||
const currency = require('../../currency');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
const BlueApp: AppStorage = require('../../BlueApp');
|
||||
const loc = require('../../loc');
|
||||
|
||||
|
|
|
@ -86,6 +86,8 @@ export default class ReorderWallets extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
sortableList = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
this.props.navigation.setParams({
|
||||
customCloseButtonFunction: async () => {
|
||||
|
@ -160,7 +162,7 @@ export default class ReorderWallets extends Component {
|
|||
return (
|
||||
<SafeBlueArea>
|
||||
<SortableList
|
||||
ref={ref => (this.sortableList = ref)}
|
||||
ref={this.sortableList}
|
||||
style={styles.root}
|
||||
data={this.state.data}
|
||||
renderRow={this._renderItem}
|
||||
|
|
|
@ -8,7 +8,7 @@ jest.useFakeTimers();
|
|||
describe('currency', () => {
|
||||
it('fetches exchange rate and saves to AsyncStorage', async () => {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000;
|
||||
const currency = require('../../currency');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
await currency.startUpdater();
|
||||
let cur = await AsyncStorage.getItem(AppStorage.EXCHANGE_RATES);
|
||||
cur = JSON.parse(cur);
|
||||
|
|
|
@ -4,8 +4,8 @@ const assert = require('assert');
|
|||
|
||||
describe('currency', () => {
|
||||
it('formats everything correctly', async () => {
|
||||
const currency = require('../../currency');
|
||||
currency.exchangeRates.BTC_USD = 10000;
|
||||
const currency = require('../../blue_modules/currency');
|
||||
currency._setExchangeRate('BTC_USD', 10000);
|
||||
|
||||
assert.strictEqual(currency.satoshiToLocalCurrency(1), '$0.0001');
|
||||
assert.strictEqual(currency.satoshiToLocalCurrency(-1), '-$0.0001');
|
||||
|
@ -26,10 +26,8 @@ describe('currency', () => {
|
|||
assert.strictEqual(currency.satoshiToBTC(100000000), '1');
|
||||
assert.strictEqual(currency.satoshiToBTC(123456789123456789), '1234567891.2345678');
|
||||
|
||||
currency.preferredFiatCurrency.endPointKey = FiatUnit.JPY.endPointKey;
|
||||
currency.preferredFiatCurrency.symbol = FiatUnit.JPY.symbol;
|
||||
currency.preferredFiatCurrency.locale = FiatUnit.JPY.locale;
|
||||
currency.exchangeRates.BTC_JPY = 1043740.8614;
|
||||
currency._setPreferredFiatCurrency(FiatUnit.JPY);
|
||||
currency._setExchangeRate('BTC_JPY', 1043740.8614);
|
||||
|
||||
assert.strictEqual(currency.satoshiToLocalCurrency(1), '¥0.01');
|
||||
});
|
||||
|
|
|
@ -1,8 +1,88 @@
|
|||
/* global it, describe */
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
import { FiatUnit } from '../../models/fiatUnit';
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const loc = require('../../loc/');
|
||||
const currency = require('../../blue_modules/currency');
|
||||
|
||||
describe('Localization', () => {
|
||||
it('internal formatter', () => {
|
||||
assert.strictEqual(loc._leaveNumbersAndDots('1,00 ₽'), '1');
|
||||
assert.strictEqual(loc._leaveNumbersAndDots('0,50 ₽"'), '0.50');
|
||||
assert.strictEqual(loc._leaveNumbersAndDots('RUB 1,00'), '1');
|
||||
});
|
||||
|
||||
it('formatBalancePlain() && formatBalancePlain()', () => {
|
||||
currency._setExchangeRate('BTC_RUB', 660180.143);
|
||||
currency._setPreferredFiatCurrency(FiatUnit.RUB);
|
||||
let newInputValue = loc.formatBalanceWithoutSuffix(152, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, 'RUB 1.00');
|
||||
newInputValue = loc.formatBalancePlain(152, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '1');
|
||||
|
||||
newInputValue = loc.formatBalanceWithoutSuffix(1515, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, 'RUB 10.00');
|
||||
newInputValue = loc.formatBalancePlain(1515, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '10');
|
||||
|
||||
newInputValue = loc.formatBalanceWithoutSuffix(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, 'RUB 110,869.52');
|
||||
newInputValue = loc.formatBalancePlain(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '110869.52');
|
||||
|
||||
newInputValue = loc.formatBalancePlain(76, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '0.50');
|
||||
|
||||
currency._setExchangeRate('BTC_USD', 10000);
|
||||
currency._setPreferredFiatCurrency(FiatUnit.USD);
|
||||
newInputValue = loc.formatBalanceWithoutSuffix(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '$1,679.38');
|
||||
newInputValue = loc.formatBalancePlain(16793829, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '1679.38');
|
||||
|
||||
newInputValue = loc.formatBalancePlain(16000000, BitcoinUnit.LOCAL_CURRENCY, false);
|
||||
assert.strictEqual(newInputValue, '1600');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[123000000, BitcoinUnit.SATS, false, '123000000', false],
|
||||
[123000000, BitcoinUnit.SATS, true, '123 000 000', false],
|
||||
[123456000, BitcoinUnit.BTC, true, '1.23456', false],
|
||||
['123456000', BitcoinUnit.BTC, true, '1.23456', false], // can handle strings
|
||||
[100000000, BitcoinUnit.BTC, true, '1', false],
|
||||
[10000000, BitcoinUnit.BTC, true, '0.1', false],
|
||||
[1, BitcoinUnit.BTC, true, '0.00000001', false],
|
||||
[10000000, BitcoinUnit.LOCAL_CURRENCY, true, '...', true], // means unknown since we did not receive exchange rate
|
||||
])(
|
||||
'can formatBalanceWithoutSuffix',
|
||||
async (balance, toUnit, withFormatting, expectedResult, shouldResetRate) => {
|
||||
currency._setExchangeRate('BTC_USD', 1);
|
||||
currency._setPreferredFiatCurrency(FiatUnit.USD);
|
||||
if (shouldResetRate) {
|
||||
currency._setExchangeRate('BTC_USD', false);
|
||||
}
|
||||
const actualResult = loc.formatBalanceWithoutSuffix(balance, toUnit, withFormatting);
|
||||
assert.strictEqual(actualResult, expectedResult);
|
||||
},
|
||||
240000,
|
||||
);
|
||||
|
||||
it.each([
|
||||
[123000000, BitcoinUnit.SATS, false, '123000000 sats'],
|
||||
[123000000, BitcoinUnit.BTC, false, '1.23 BTC'],
|
||||
[123000000, BitcoinUnit.LOCAL_CURRENCY, false, '$1.23'],
|
||||
])(
|
||||
'can formatBalance',
|
||||
async (balance, toUnit, withFormatting, expectedResult) => {
|
||||
currency._setExchangeRate('BTC_USD', 1);
|
||||
currency._setPreferredFiatCurrency(FiatUnit.USD);
|
||||
const actualResult = loc.formatBalance(balance, toUnit, withFormatting);
|
||||
assert.strictEqual(actualResult, expectedResult);
|
||||
},
|
||||
240000,
|
||||
);
|
||||
|
||||
it('has all keys in all locales', async () => {
|
||||
const en = require('../../loc/en');
|
||||
let issues = 0;
|
||||
|
|
Loading…
Add table
Reference in a new issue