diff --git a/src/action/channel.js b/src/action/channel.js index a236f5b6e..5beaf3b1c 100644 --- a/src/action/channel.js +++ b/src/action/channel.js @@ -133,7 +133,7 @@ class ChannelAction { async connectAndOpen() { try { const { channel, settings } = this._store; - const amount = toSatoshis(channel.amount, settings.unit); + const amount = toSatoshis(channel.amount, settings); if (!channel.pubkeyAtHost.includes('@')) { return this._notification.display({ msg: 'Please enter pubkey@host' }); } diff --git a/src/action/invoice.js b/src/action/invoice.js index 5568d59f1..a63934e64 100644 --- a/src/action/invoice.js +++ b/src/action/invoice.js @@ -31,7 +31,7 @@ class InvoiceAction { try { const { invoice, settings } = this._store; const response = await this._grpc.sendCommand('addInvoice', { - value: toSatoshis(invoice.amount, settings.unit), + value: toSatoshis(invoice.amount, settings), memo: invoice.note, }); invoice.encoded = response.payment_request; diff --git a/src/action/payment.js b/src/action/payment.js index fd97ee8b2..60dbbfcdf 100644 --- a/src/action/payment.js +++ b/src/action/payment.js @@ -68,7 +68,7 @@ class PaymentAction { const request = await this._grpc.sendCommand('decodePayReq', { pay_req: invoice.replace(PREFIX_URI, ''), }); - payment.amount = toAmount(parseSat(request.num_satoshis), settings.unit); + payment.amount = toAmount(parseSat(request.num_satoshis), settings); payment.note = request.description; await this.estimateLightningFee({ destination: request.destination, @@ -89,7 +89,7 @@ class PaymentAction { amt: satAmt, num_routes: 1, }); - payment.fee = toAmount(parseSat(routes[0].total_fees), settings.unit); + payment.fee = toAmount(parseSat(routes[0].total_fees), settings); } catch (err) { log.error(`Estimating lightning fee failed!`, err); } @@ -100,7 +100,7 @@ class PaymentAction { const { payment, settings } = this._store; await this._grpc.sendCommand('sendCoins', { addr: payment.address, - amount: toSatoshis(payment.amount, settings.unit), + amount: toSatoshis(payment.amount, settings), }); this._nav.goPayBitcoinDone(); } catch (err) { diff --git a/src/computed/payment.js b/src/computed/payment.js index d58c3bdce..a78248ab8 100644 --- a/src/computed/payment.js +++ b/src/computed/payment.js @@ -9,8 +9,8 @@ const ComputedPayment = store => { paymentFeeLabel: computed(() => toLabel(store.payment.fee, store.settings)), paymentTotalLabel: computed(() => { const { payment, settings } = store; - const satAmount = toSatoshis(payment.amount, settings.unit); - const satFee = toSatoshis(payment.fee, settings.unit); + const satAmount = toSatoshis(payment.amount, settings); + const satFee = toSatoshis(payment.fee, settings); return toAmountLabel(satAmount + satFee, settings); }), }); diff --git a/src/computed/setting.js b/src/computed/setting.js index 3f2d12ff0..e814523b6 100644 --- a/src/computed/setting.js +++ b/src/computed/setting.js @@ -5,13 +5,13 @@ import { UNITS, FIATS } from '../config'; const ComputedSetting = store => { extendObservable(store, { selectedUnitLabel: computed(() => getUnitLabel(store.settings.unit)), - selectedFiatLabel: computed(() => FIATS[store.settings.fiat].display), + selectedFiatLabel: computed(() => FIATS[store.settings.fiat].displayLong), satUnitLabel: computed(() => getUnitLabel('sat')), bitUnitLabel: computed(() => getUnitLabel('bit')), btcUnitLabel: computed(() => getUnitLabel('btc')), - usdFiatLabel: computed(() => FIATS['usd'].display), - eurFiatLabel: computed(() => FIATS['eur'].display), - gbpFiatLabel: computed(() => FIATS['gbp'].display), + usdFiatLabel: computed(() => FIATS['usd'].displayLong), + eurFiatLabel: computed(() => FIATS['eur'].displayLong), + gbpFiatLabel: computed(() => FIATS['gbp'].displayLong), }); }; diff --git a/src/computed/wallet.js b/src/computed/wallet.js index e23d4edb0..6303f6d4d 100644 --- a/src/computed/wallet.js +++ b/src/computed/wallet.js @@ -1,6 +1,6 @@ import { computed, extendObservable } from 'mobx'; import { toAmountLabel } from '../helper'; -import { UNITS } from '../config'; +import { UNITS, FIATS } from '../config'; const ComputedWallet = store => { extendObservable(store, { @@ -13,6 +13,10 @@ const ComputedWallet = store => { channelBalanceLabel: computed(() => toAmountLabel(store.channelBalanceSatoshis, store.settings) ), + unitFiatLabel: computed(() => { + const { displayFiat, unit, fiat } = store.settings; + return displayFiat ? FIATS[fiat].display : UNITS[unit].display; + }), unitLabel: computed(() => { const { settings } = store; return !settings.displayFiat ? UNITS[settings.unit].display : null; diff --git a/src/config.js b/src/config.js index 41419ee31..0ffeb3cde 100644 --- a/src/config.js +++ b/src/config.js @@ -21,9 +21,9 @@ module.exports.UNITS = { btc: { display: 'BTC', displayLong: 'Bitcoin', denominator: 100000000 }, }; module.exports.FIATS = { - usd: { display: 'US Dollar' }, - eur: { display: 'Euro' }, - gbp: { display: 'British Pound' }, + usd: { display: '$', displayLong: 'US Dollar' }, + eur: { display: '€', displayLong: 'Euro' }, + gbp: { display: '£', displayLong: 'British Pound' }, }; module.exports.DEFAULT_UNIT = 'btc'; module.exports.DEFAULT_FIAT = 'usd'; diff --git a/src/helper.js b/src/helper.js index 0125ad2ca..d27c374b7 100644 --- a/src/helper.js +++ b/src/helper.js @@ -56,33 +56,45 @@ export const parseSat = satoshis => { }; /** - * Convert a string formatted BTC amount to satoshis - * @param {string} amount The amount e.g. '0.0001' - * @param {string} unit The BTC unit e.g. 'btc' or 'bit' - * @return {number} The satoshis as an integer + * Convert a string formatted btc/fiat amount to satoshis + * @param {string} amount The amount e.g. '0.0001' + * @param {Object} settings Contains the current exchange rate + * @return {number} The satoshis as an integer */ -export const toSatoshis = (amount, unit) => { +export const toSatoshis = (amount, settings) => { if ( typeof amount !== 'string' || !/^[0-9]*[.]?[0-9]*$/.test(amount) || - !UNITS[unit] + !settings || + typeof settings.displayFiat !== 'boolean' ) { - throw new Error('Missing args!'); + throw new Error('Invalid input!'); + } + if (settings.displayFiat) { + const rate = settings.exchangeRate[settings.fiat] || 0; + return Math.round(Number(amount) * rate * UNITS.btc.denominator); + } else { + return Math.round(Number(amount) * UNITS[settings.unit].denominator); } - return Math.round(Number(amount) * UNITS[unit].denominator); }; /** * Convert satoshis to a BTC values than can set as a text input value * @param {number} satoshis The value as a string or number - * @param {string} unit The BTC unit e.g. 'btc' or 'bit' + * @param {Object} settings Contains the current exchange rate * @return {string} The amount formatted as '0.0001' */ -export const toAmount = (satoshis, unit) => { - if (!Number.isInteger(satoshis) || !UNITS[unit]) { +export const toAmount = (satoshis, settings) => { + if ( + !Number.isInteger(satoshis) || + !settings || + typeof settings.displayFiat !== 'boolean' + ) { throw new Error('Invalid input!'); } - const num = satoshis / UNITS[unit].denominator; + const num = settings.displayFiat + ? calculateExchangeRate(satoshis, settings) + : satoshis / UNITS[settings.unit].denominator; return num.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 8, @@ -103,8 +115,7 @@ export const calculateExchangeRate = (satoshis, settings) => { throw new Error('Invalid input!'); } const rate = settings.exchangeRate[settings.fiat] || 0; - const balance = satoshis / rate / UNITS.btc.denominator; - return formatFiat(balance, settings.fiat); + return satoshis / rate / UNITS.btc.denominator; }; /** @@ -122,22 +133,19 @@ export const toAmountLabel = (satoshis, settings) => { throw new Error('Invalid input!'); } return settings.displayFiat - ? calculateExchangeRate(satoshis, settings) - : formatNumber(toAmount(satoshis, settings.unit)); + ? formatFiat(calculateExchangeRate(satoshis, settings), settings.fiat) + : formatNumber(toAmount(satoshis, settings)); }; /** - * Convert a string formatted BTC amount either to fiat or the selected BTC unit. + * Convert a string formatted btc/fiat amount either to fiat or the selected BTC unit. * The output should be used throughout the UI for value labels. - * @param {string} amount The amount e.g. '0.0001' + * @param {string} amount The amount e.g. '0.0001' * @param {Object} settings Contains the current exchange rate * @return {string} The corresponding value label */ export const toLabel = (amount, settings) => { - if (!settings) { - throw new Error('Missing args!'); - } - const satoshis = toSatoshis(amount, settings.unit); + const satoshis = toSatoshis(amount, settings); return toAmountLabel(satoshis, settings); }; diff --git a/src/view/channel-create.js b/src/view/channel-create.js index 48ca0371a..3303495a2 100644 --- a/src/view/channel-create.js +++ b/src/view/channel-create.js @@ -44,7 +44,9 @@ const ChannelCreateView = ({ store, nav, channel }) => ( onChangeText={amount => channel.setAmount({ amount })} onSubmitEditing={() => channel.connectAndOpen()} /> - {store.unit} + + {store.unitFiatLabel} + ( onChangeText={amount => invoice.setAmount({ amount })} onSubmitEditing={() => invoice.generateUri()} /> - {store.unit} + + {store.unitFiatLabel} + ( onSubmitEditing={() => nav.goPayBitcoinConfirm()} /> - {store.unit} + {store.unitFiatLabel} { it('should format fiat amount', () => { store.settings.displayFiat = true; store.settings.exchangeRate.usd = 0.00014503; - store.invoice.amount = '0.1001'; + store.invoice.amount = '1.10'; ComputedInvoice(store); - expect(store.invoiceAmountLabel, 'to match', /690[,.]20/); + expect(store.invoiceAmountLabel, 'to match', /1[,.]10/); }); }); }); diff --git a/test/unit/computed/payment.spec.js b/test/unit/computed/payment.spec.js index 83ad8d6f1..cc4ae1470 100644 --- a/test/unit/computed/payment.spec.js +++ b/test/unit/computed/payment.spec.js @@ -28,12 +28,12 @@ describe('Computed Payment Unit Tests', () => { it('should calculate fiat total', () => { store.settings.displayFiat = true; store.settings.exchangeRate.usd = 0.00014503; - store.payment.fee = '0.0001'; - store.payment.amount = '0.1'; + store.payment.fee = '0.10'; + store.payment.amount = '1.00'; ComputedPayment(store); - expect(store.paymentAmountLabel, 'to match', /689[,.]51/); - expect(store.paymentFeeLabel, 'to match', /0[,.]69/); - expect(store.paymentTotalLabel, 'to match', /690[,.]20/); + expect(store.paymentAmountLabel, 'to match', /1[,.]00/); + expect(store.paymentFeeLabel, 'to match', /0[,.]10/); + expect(store.paymentTotalLabel, 'to match', /1[,.]10/); }); it('should ignore fee if blank', () => { diff --git a/test/unit/computed/wallet.spec.js b/test/unit/computed/wallet.spec.js index 8ab8960c3..24ada1f79 100644 --- a/test/unit/computed/wallet.spec.js +++ b/test/unit/computed/wallet.spec.js @@ -14,7 +14,9 @@ describe('Computed Wallet Unit Tests', () => { expect(store.walletAddressUri, 'to equal', ''); expect(store.balanceLabel, 'to equal', '0'); expect(store.channelBalanceLabel, 'to equal', '0'); + expect(store.unitFiatLabel, 'to equal', 'BTC'); expect(store.unitLabel, 'to equal', 'BTC'); + expect(store.unit, 'to equal', 'BTC'); }); it('should generate valid wallet address uri', () => { @@ -35,6 +37,7 @@ describe('Computed Wallet Unit Tests', () => { ComputedWallet(store); expect(store.balanceLabel, 'to match', /6[,.]895[,.]13/); expect(store.channelBalanceLabel, 'to match', /0[,.]69/); + expect(store.unitFiatLabel, 'to equal', '$'); expect(store.unitLabel, 'to equal', null); }); @@ -50,6 +53,7 @@ describe('Computed Wallet Unit Tests', () => { /^1{1}[,.]0{3}[,.]0{3}[,.]0{1}1{1}$/ ); expect(store.channelBalanceLabel, 'to equal', '100'); + expect(store.unitFiatLabel, 'to equal', 'bits'); expect(store.unitLabel, 'to equal', 'bits'); }); }); diff --git a/test/unit/helper.spec.js b/test/unit/helper.spec.js index b513789d0..ae2c7f972 100644 --- a/test/unit/helper.spec.js +++ b/test/unit/helper.spec.js @@ -154,82 +154,143 @@ describe('Helpers Unit Tests', () => { }); describe('toSatoshis()', () => { + let settings; + + beforeEach(() => { + settings = { + displayFiat: false, + unit: 'btc', + fiat: 'usd', + exchangeRate: { usd: 0.00014503 }, + }; + }); + it('should throw error if amount is undefined', () => { expect( - helpers.toSatoshis.bind(null, undefined, 'btc'), + helpers.toSatoshis.bind(null, undefined, settings), 'to throw', - /Missing/ + /Invalid/ ); }); it('should throw error if amount is null', () => { - expect(helpers.toSatoshis.bind(null, null, 'btc'), 'to throw', /Missing/); + expect( + helpers.toSatoshis.bind(null, null, settings), + 'to throw', + /Invalid/ + ); }); it('should throw error if amount is number', () => { - expect(helpers.toSatoshis.bind(null, 0.1, 'btc'), 'to throw', /Missing/); + expect( + helpers.toSatoshis.bind(null, 0.1, settings), + 'to throw', + /Invalid/ + ); }); it('should throw error if amount is separated with a comma', () => { expect( - helpers.toSatoshis.bind(null, '0,1', 'btc'), + helpers.toSatoshis.bind(null, '0,1', settings), 'to throw', - /Missing/ + /Invalid/ ); }); - it('should throw error if unit is undefined', () => { + it('should throw error if settings is undefined', () => { expect( helpers.toSatoshis.bind(null, '100', undefined), 'to throw', - /Missing/ + /Invalid/ ); }); + it('should throw error for wrong settings', () => { + expect(helpers.toSatoshis.bind(null, '100', {}), 'to throw', /Invalid/); + }); + it('should be 0 for empty amount', () => { - const num = helpers.toSatoshis('', 'btc'); + const num = helpers.toSatoshis('', settings); expect(num, 'to equal', 0); }); - it('should work for string input', () => { - const num = helpers.toSatoshis('0.10', 'btc'); + it('should work for string amount', () => { + const num = helpers.toSatoshis('0.10', settings); expect(num, 'to equal', 10000000); }); it('should have use ony 8 decimal values', () => { - const num = helpers.toSatoshis('0.000000014', 'btc'); + const num = helpers.toSatoshis('0.000000014', settings); expect(num, 'to equal', 1); }); it('should round up to two satoshis', () => { - const num = helpers.toSatoshis('0.000000019', 'btc'); + const num = helpers.toSatoshis('0.000000019', settings); expect(num, 'to equal', 2); }); + + it('should be 0 is exchange rate is not set (fiat)', () => { + settings.displayFiat = true; + settings.fiat = 'invalid'; + const num = helpers.toSatoshis('100', settings); + expect(num, 'to equal', 0); + }); + + it('should be 0 for empty amount (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toSatoshis('', settings); + expect(num, 'to equal', 0); + }); + + it('should work for string amount (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toSatoshis('10.00', settings); + expect(num, 'to equal', 145030); + }); }); describe('toAmount()', () => { + let settings; + + beforeEach(() => { + settings = { + displayFiat: false, + unit: 'btc', + fiat: 'usd', + exchangeRate: { usd: 0.00014503 }, + }; + }); + it('should throw error if satoshis is undefined', () => { expect( - helpers.toAmount.bind(null, undefined, 'btc'), + helpers.toAmount.bind(null, undefined, settings), 'to throw', /Invalid/ ); }); it('should throw error if satoshis is null', () => { - expect(helpers.toAmount.bind(null, null, 'btc'), 'to throw', /Invalid/); + expect( + helpers.toAmount.bind(null, null, settings), + 'to throw', + /Invalid/ + ); }); it('should throw error if satoshis is not a number', () => { expect( - helpers.toAmount.bind(null, 'not-a-number', 'btc'), + helpers.toAmount.bind(null, 'not-a-number', settings), 'to throw', /Invalid/ ); }); it('should throw error for string number', () => { - expect(helpers.toAmount.bind(null, '100', 'btc'), 'to throw', /Invalid/); + expect( + helpers.toAmount.bind(null, '100', settings), + 'to throw', + /Invalid/ + ); }); it('should throw error if unit is invalid', () => { @@ -240,7 +301,7 @@ describe('Helpers Unit Tests', () => { ); }); - it('should throw error if unit is undefined', () => { + it('should throw error if settings is undefined', () => { expect( helpers.toAmount.bind(null, 100, undefined), 'to throw', @@ -248,53 +309,94 @@ describe('Helpers Unit Tests', () => { ); }); + it('should throw error if settings is invalid', () => { + expect(helpers.toAmount.bind(null, 100, {}), 'to throw', /Invalid/); + }); + it('should throw error for non-integer numbers', () => { expect( - helpers.toAmount.bind(null, 100000000.9, 'btc'), + helpers.toAmount.bind(null, 100000000.9, settings), 'to throw', /Invalid/ ); }); it('should work for number input', () => { - const num = helpers.toAmount(100000000, 'btc'); + const num = helpers.toAmount(100000000, settings); expect(num, 'to equal', '1'); }); it('should not format number input', () => { - const num = helpers.toAmount(100000000000, 'btc'); + const num = helpers.toAmount(100000000000, settings); expect(num, 'to equal', '1000'); }); it('should use period for decimals values', () => { - const num = helpers.toAmount(10000000, 'btc'); + const num = helpers.toAmount(10000000, settings); expect(num, 'to equal', '0.1'); }); it('should work for 0', () => { - const num = helpers.toAmount(0, 'btc'); + const num = helpers.toAmount(0, settings); expect(num, 'to equal', '0'); }); it('should work for 1', () => { - const num = helpers.toAmount(1, 'btc'); + const num = helpers.toAmount(1, settings); expect(num, 'to equal', '0.00000001'); }); it('should work for 10', () => { - const num = helpers.toAmount(10, 'btc'); + const num = helpers.toAmount(10, settings); expect(num, 'to equal', '0.0000001'); }); it('should work for 100', () => { - const num = helpers.toAmount(100, 'btc'); + const num = helpers.toAmount(100, settings); expect(num, 'to equal', '0.000001'); }); it('should work for 1000', () => { - const num = helpers.toAmount(1000, 'btc'); + const num = helpers.toAmount(1000, settings); expect(num, 'to equal', '0.00001'); }); + + it('should be infinity if exchange rate is not set (fiat)', () => { + settings.displayFiat = true; + settings.fiat = 'invalid-fiat'; + const rate = helpers.toAmount(100, settings); + expect(rate, 'to match', /∞/); + }); + + it('should work for number input (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toAmount(100000, settings); + expect(num, 'to equal', '6.89512515'); + }); + + it('should use period for decimals values (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toAmount(100000000, settings); + expect(num, 'to equal', '6895.12514652'); + }); + + it('should work for 0 (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toAmount(0, settings); + expect(num, 'to equal', '0'); + }); + + it('should work for 1 (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toAmount(1, settings); + expect(num, 'to equal', '0.00006895'); + }); + + it('should work for 1000 (fiat)', () => { + settings.displayFiat = true; + const num = helpers.toAmount(1000, settings); + expect(num, 'to equal', '0.06895125'); + }); }); describe('calculateExchangeRate()', () => { @@ -358,13 +460,13 @@ describe('Helpers Unit Tests', () => { it('should work for a number value', () => { const rate = helpers.calculateExchangeRate(100000, settings); - expect(rate, 'to match', /6{1}[,.]9{1}0{1}/); + expect(rate, 'to equal', 6.8951251465214085); }); it('should be infinite for unknown rate', () => { settings.fiat = 'eur'; const rate = helpers.calculateExchangeRate(100000, settings); - expect(rate, 'to match', /∞/); + expect(rate, 'to equal', Infinity); }); }); @@ -466,23 +568,23 @@ describe('Helpers Unit Tests', () => { expect( helpers.toLabel.bind(null, undefined, settings), 'to throw', - /Missing/ + /Invalid/ ); }); it('should throw error if amount is null', () => { - expect(helpers.toLabel.bind(null, null, settings), 'to throw', /Missing/); + expect(helpers.toLabel.bind(null, null, settings), 'to throw', /Invalid/); }); it('should throw error if amount is number', () => { - expect(helpers.toLabel.bind(null, 0.1, settings), 'to throw', /Missing/); + expect(helpers.toLabel.bind(null, 0.1, settings), 'to throw', /Invalid/); }); it('should throw error if amount is separated with a comma', () => { expect( helpers.toLabel.bind(null, '0,1', settings), 'to throw', - /Missing/ + /Invalid/ ); }); @@ -490,7 +592,7 @@ describe('Helpers Unit Tests', () => { expect( helpers.toLabel.bind(null, '100', undefined), 'to throw', - /Missing/ + /Invalid/ ); }); @@ -499,14 +601,14 @@ describe('Helpers Unit Tests', () => { expect(num, 'to match', /0{1}[,.]0{2}/); }); - it('should work for string input', () => { + it('should work for fiat amount', () => { const num = helpers.toLabel('0.10', settings); - expect(num, 'to match', /689[,.]51/); + expect(num, 'to match', /0[,.]10/); }); - it('should format a number value', () => { + it('should work for btc amount', () => { settings.displayFiat = false; const lbl = helpers.toLabel('0.10', settings); - expect(lbl, 'to match', /0[,.]1/); + expect(lbl, 'to match', /^0[,.]1$/); }); });