diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b6a7d89 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16 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/components/expense-list/template.hbs b/app/components/expense-list/template.hbs index c9cf3e6..0f65333 100644 --- a/app/components/expense-list/template.hbs +++ b/app/components/expense-list/template.hbs @@ -5,7 +5,7 @@ {{fmt-date-localized expense.date}}: {{expense.title}} -
+
{{fmt-fiat-currency expense.amount expense.currency}}

diff --git a/app/components/reimbursement-item/template.hbs b/app/components/reimbursement-item/template.hbs index 408860e..2d9f21d 100644 --- a/app/components/reimbursement-item/template.hbs +++ b/app/components/reimbursement-item/template.hbs @@ -1,7 +1,7 @@

  • -

    + class="{{item-status @reimbursement}}"> +

    diff --git a/app/helpers/fmt-amount-sats-title.js b/app/helpers/fmt-amount-sats-title.js new file mode 100644 index 0000000..464496d --- /dev/null +++ b/app/helpers/fmt-amount-sats-title.js @@ -0,0 +1,11 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function fmtAmountSatsTitle(params) { + const amountSats = params[0]; + + if (typeof amountSats === 'number' && amountSats > 0) { + return `${amountSats} sats`; + } else { + return ''; + } +}); 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/app/styles/app.scss b/app/styles/app.scss index 3697c22..9d620fb 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -45,6 +45,10 @@ a { color: $primary-color; } +.cursor-pointer { + cursor: pointer; +} + section { h2 { font-size: 1.5rem; diff --git a/app/utils/btc-conversions.js b/app/utils/btc-conversions.js new file mode 100644 index 0000000..48cc9f9 --- /dev/null +++ b/app/utils/btc-conversions.js @@ -0,0 +1,11 @@ +export function floatToBtc(number) { + return Number(number.toFixed(8)) +} + +export function btcToSats(btc) { + return Math.round(btc * 100_000_000); +} + +export function satsToBtc(sats) { + return Math.round((sats / 100_000_000) * 100_000_000) / 100_000_000; +} diff --git a/app/utils/iso-date-is-today.js b/app/utils/iso-date-is-today.js new file mode 100644 index 0000000..5573644 --- /dev/null +++ b/app/utils/iso-date-is-today.js @@ -0,0 +1,4 @@ +export default function(value) { + const today = new Date().toISOString().split('T')[0]; + return value === today; +} diff --git a/config/environment.js b/config/environment.js index 7d53fde..f67c067 100644 --- a/config/environment.js +++ b/config/environment.js @@ -76,10 +76,10 @@ module.exports = function(environment) { ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github'; ENV.ipfs = { - host: 'localhost', + host: '10.1.1.198', port: '5001', protocol: 'http', - gatewayUrl: 'http://localhost:8080/ipfs' + gatewayUrl: 'http://10.1.1.198:9090/ipfs' }; // Uncomment if you want to use local akkounts diff --git a/package-lock.json b/package-lock.json index 27ced0d..7968d88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@ember/optional-features": "^1.3.0", "@glimmer/component": "^1.0.0", "@glimmer/tracking": "^1.0.0", - "@kredits/contracts": "^7.3.0", + "@kredits/contracts": "^7.5.0", "babel-preset-es2015": "^6.22.0", "broccoli-asset-rev": "^3.0.0", "ember-auto-import": "^1.12.1", @@ -3062,17 +3062,18 @@ } }, "node_modules/@kosmos/schemas": { - "version": "3.1.0", - "dev": true, - "license": "MIT" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@kosmos/schemas/-/schemas-3.2.0.tgz", + "integrity": "sha512-GyWXmWcATcakmEdPOpafdckBgWFqcpSh+tB8nm11VGgtgA94pfoFhKZ3t88WELItKfuP/GFHgMiZmdMB3bSkdQ==", + "dev": true }, "node_modules/@kredits/contracts": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz", - "integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.5.0.tgz", + "integrity": "sha512-4Jw6lXn9DfZiTnMtWHwGLS5zhxEJZ2ANfAgcr7tZxqRl7Y0ijpoWvPCA+LkSGqRgKspyiw9+7vSVTtyvAyQbPQ==", "dev": true, "dependencies": { - "@kosmos/schemas": "^3.1.0", + "@kosmos/schemas": "^3.2.0", "ethers": "^5.4.7", "ipfs-http-client": "^56.0.3", "multihashes": "^4.0.3", @@ -26067,16 +26068,18 @@ } }, "@kosmos/schemas": { - "version": "3.1.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@kosmos/schemas/-/schemas-3.2.0.tgz", + "integrity": "sha512-GyWXmWcATcakmEdPOpafdckBgWFqcpSh+tB8nm11VGgtgA94pfoFhKZ3t88WELItKfuP/GFHgMiZmdMB3bSkdQ==", "dev": true }, "@kredits/contracts": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz", - "integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.5.0.tgz", + "integrity": "sha512-4Jw6lXn9DfZiTnMtWHwGLS5zhxEJZ2ANfAgcr7tZxqRl7Y0ijpoWvPCA+LkSGqRgKspyiw9+7vSVTtyvAyQbPQ==", "dev": true, "requires": { - "@kosmos/schemas": "^3.1.0", + "@kosmos/schemas": "^3.2.0", "ethers": "^5.4.7", "ipfs-http-client": "^56.0.3", "multihashes": "^4.0.3", diff --git a/package.json b/package.json index 3e41954..056aa1a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@ember/optional-features": "^1.3.0", "@glimmer/component": "^1.0.0", "@glimmer/tracking": "^1.0.0", - "@kredits/contracts": "^7.3.0", + "@kredits/contracts": "^7.5.0", "babel-preset-es2015": "^6.22.0", "broccoli-asset-rev": "^3.0.0", "ember-auto-import": "^1.12.1", 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'); }); }); diff --git a/tests/unit/utils/btc-conversions-test.js b/tests/unit/utils/btc-conversions-test.js new file mode 100644 index 0000000..c86673d --- /dev/null +++ b/tests/unit/utils/btc-conversions-test.js @@ -0,0 +1,16 @@ +import { floatToBtc, btcToSats, satsToBtc } from 'kredits-web/utils/btc-conversions'; +import { module, test } from 'qunit'; + +module('Unit | Utility | btc-conversions', function() { + test('floatToBtc', function(assert) { + assert.equal(floatToBtc(0.001429007), 0.00142901); + }); + + test('btcToSats', function(assert) { + assert.equal(btcToSats(0.001429007), 142901); + }); + + test('satsToBtc', function(assert) { + assert.equal(satsToBtc(142901), 0.00142901); + }); +});