diff --git a/app/components/add-expense-item/component.js b/app/components/add-expense-item/component.js index a2022ba..07a5aba 100644 --- a/app/components/add-expense-item/component.js +++ b/app/components/add-expense-item/component.js @@ -18,7 +18,8 @@ export default class AddExpenseItemComponent extends Component { currencies = [ { code: 'EUR' }, - { code: 'USD' } + { code: 'USD' }, + { code: 'BTC' } ]; get isValidAmount () { diff --git a/app/components/add-reimbursement/component.js b/app/components/add-reimbursement/component.js index 58bcda1..726b5a8 100644 --- a/app/components/add-reimbursement/component.js +++ b/app/components/add-reimbursement/component.js @@ -5,7 +5,9 @@ import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { A } from '@ember/array'; import { scheduleOnce } from '@ember/runloop'; +import { btcToSats, satsToBtc } from 'kredits-web/utils/btc-conversions'; import isValidAmount from 'kredits-web/utils/is-valid-amount'; +import isoDateIsToday from 'kredits-web/utils/iso-date-is-today'; import readFileContent from 'kredits-web/utils/read-file-content'; import config from 'kredits-web/config/environment'; @@ -73,22 +75,6 @@ export default class AddReimbursementComponent extends Component { anchor.scrollIntoView(); } - updateTotalAmountFromFiat() { - let btcAmount = 0; - - if (this.exchangeRates.btceur > 0 && this.totalEUR > 0) { - btcAmount += (this.totalEUR / this.exchangeRates.btceur); - } - if (this.exchangeRates.btcusd > 0 && this.totalUSD > 0) { - btcAmount += (this.totalUSD / this.exchangeRates.btcusd); - } - if (this.totalUSD === 0 && this.totalEUR === 0) { - btcAmount = 0; - } - - this.total = btcAmount.toFixed(8); - } - // TODO use ember-concurrency here // https://github.com/67P/kredits-web/pull/209#discussion_r1064234421 @action @@ -118,16 +104,38 @@ export default class AddReimbursementComponent extends Component { } @action - addExpenseItem (expenseItem) { - this.expenses.pushObject(expenseItem); - this.updateTotalAmountFromFiat(); + async addExpenseItem (expense) { + let totalBTC = parseFloat(this.total); + + if (expense.currency === "BTC") { + expense.amountSats = btcToSats(expense.amount); + totalBTC += expense.amount; + } else { + let amountSats; + if (isoDateIsToday(expense.date)) { + amountSats = btcToSats(expense.amount / this.exchangeRates[expense.currency]); + } else { + const rates = await this.exchangeRates.fetchHistoricRates(expense.date); + amountSats = btcToSats(expense.amount / rates[expense.currency]); + } + expense.amountSats = amountSats; + totalBTC += satsToBtc(amountSats); + } + console.debug("Adding expense:", expense); + + this.total = totalBTC.toFixed(8); + this.expenses.pushObject(expense); this.expenseFormVisible = false; } @action - removeExpenseItem (expenseItem) { - this.expenses.removeObject(expenseItem); - this.updateTotalAmountFromFiat(); + async removeExpenseItem (expense) { + let totalBTC = parseFloat(this.total); + let amountBTC = satsToBtc(expense.amountSats); + totalBTC = totalBTC - amountBTC; + this.total = totalBTC.toFixed(8); + + this.expenses.removeObject(expense); if (this.expenses.length === 0) { this.expenseFormVisible = true; @@ -143,7 +151,7 @@ export default class AddReimbursementComponent extends Component { const contributor = this.contributors.findBy('id', this.contributorId); const attributes = { - amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats + amount: btcToSats(this.total), token: config.tokens['BTC'], recipientId: this.contributorId, title: `Expenses covered by ${contributor.name}`, diff --git a/app/helpers/fmt-fiat-currency.js b/app/helpers/fmt-fiat-currency.js index 61f75dd..471219c 100644 --- a/app/helpers/fmt-fiat-currency.js +++ b/app/helpers/fmt-fiat-currency.js @@ -2,10 +2,14 @@ import { helper } from '@ember/component/helper'; export default helper(function fmtFiatCurrency(params) { const lang = navigator.language || navigator.userLanguage; + const value = params[0]; + const currency = params[1] || 'EUR'; + + if (currency === 'BTC') return `BTC ${value}`; + const formatter = new Intl.NumberFormat(lang, { - style: 'currency', - currency: params[1] || 'EUR', - currencyDisplay: 'code' + style: 'currency', currency, currencyDisplay: 'code' }) + return formatter.format(params[0]); }); diff --git a/app/services/community-funds.js b/app/services/community-funds.js index ff49fc3..db5d423 100644 --- a/app/services/community-funds.js +++ b/app/services/community-funds.js @@ -42,7 +42,7 @@ export default class CommunityFundsService extends Service { // Format and round the approximate USD value const lang = navigator.language || navigator.userLanguage; - const balanceUSD = (res.confirmed_balance / 100000000) * this.exchangeRates.btcusd; + const balanceUSD = (res.confirmed_balance / 100000000) * this.exchangeRates.USD; res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang); this.balances.pushObject({ diff --git a/app/services/exchange-rates.js b/app/services/exchange-rates.js index 03d6b7a..c48b86b 100644 --- a/app/services/exchange-rates.js +++ b/app/services/exchange-rates.js @@ -4,6 +4,7 @@ import config from 'kredits-web/config/environment'; // Need to go through proxy for CORS headers const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`; +const kosmosBtcPriceBaseUrl = "https://storage.kosmos.org/kosmos/public/btc-price"; async function fetchFromBitstamp(currencyPair) { try { @@ -16,11 +17,12 @@ async function fetchFromBitstamp(currencyPair) { } export default class ExchangeRatesService extends Service { - @tracked btceur = 0; - @tracked btcusd = 0; + @tracked EUR = 0; + @tracked USD = 0; + @tracked historic = {}; get exchangeRatesLoaded () { - return (this.btceur !== 0) && (this.btcusd !== 0); + return (this.EUR !== 0) && (this.USD !== 0); } async fetchRates (source='bitstamp') { @@ -28,8 +30,24 @@ export default class ExchangeRatesService extends Service { switch(source) { case 'bitstamp': - this.btceur = await fetchFromBitstamp('btceur'); - this.btcusd = await fetchFromBitstamp('btcusd'); + this.EUR = await fetchFromBitstamp('btceur'); + this.USD = await fetchFromBitstamp('btcusd'); + } + } + + async fetchHistoricRates (isoDate) { + if (typeof this.historic[isoDate] === "object") { + return this.historic[isoDate]; + } else { + const url = `${kosmosBtcPriceBaseUrl}/${isoDate}`; + try { + const rates = await fetch(url).then(res => res.json()); + this.historic[isoDate] = rates; + return rates; + } catch(e) { + console.error(e); + Promise.reject(e); + } } } } diff --git a/tests/integration/helpers/fmt-fiat-currency-test.js b/tests/integration/helpers/fmt-fiat-currency-test.js index 122760a..8466490 100644 --- a/tests/integration/helpers/fmt-fiat-currency-test.js +++ b/tests/integration/helpers/fmt-fiat-currency-test.js @@ -18,5 +18,12 @@ module('Integration | Helper | fmt-fiat-currency', function(hooks) { await render(hbs`{{fmt-fiat-currency this.amount 'USD'}}`); assert.ok(this.element.textContent.trim().match(/USD/), 'using defined currency when given'); + + await render(hbs`{{fmt-fiat-currency 0.00123 'BTC'}}`); + assert.ok(this.element.textContent.trim().match(/0.00123/), + 'allows more decimals'); + assert.ok(this.element.textContent.trim().match(/BTC/), + 'using defined currency when given'); }); + }); diff --git a/tests/unit/components/add-reimbursement-test.js b/tests/unit/components/add-reimbursement-test.js index acb4da8..dad0289 100644 --- a/tests/unit/components/add-reimbursement-test.js +++ b/tests/unit/components/add-reimbursement-test.js @@ -39,12 +39,4 @@ module('Unit | Component | add-reimbursement', function(hooks) { assert.equal(component.totalEUR, '71'); assert.equal(component.totalUSD, '59'); }); - - test('#updateTotalAmountFromFiat', async function(assert) { - let component = createComponent('component:add-reimbursement'); - component.expenses = expenses; - await component.exchangeRates.fetchRates(); - component.updateTotalAmountFromFiat(); - assert.equal(component.total, '0.01323322', 'converts EUR and USD totals to BTC and rounds to 8 decimals'); - }); }); diff --git a/tests/unit/services/exchange-rates-test.js b/tests/unit/services/exchange-rates-test.js index 9c52d8f..f73781a 100644 --- a/tests/unit/services/exchange-rates-test.js +++ b/tests/unit/services/exchange-rates-test.js @@ -7,7 +7,7 @@ module('Unit | Service | exchange-rates', function(hooks) { test('#fetchRates', async function(assert) { let service = this.owner.lookup('service:exchange-rates'); await service.fetchRates(); - assert.equal(service.btceur, 9167.57, 'fetches BTCEUR from Bitstamp'); - assert.equal(service.btcusd, 10749.70, 'fetches BTCUSD from Bitstamp'); + assert.equal(service.EUR, 9167.57, 'fetches BTCEUR from Bitstamp'); + assert.equal(service.USD, 10749.70, 'fetches BTCUSD from Bitstamp'); }); });