From 50b0f2cd7ad58d874062f384c1e78c07475c4e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 18 Dec 2024 12:30:12 +0400 Subject: [PATCH 01/10] Add BTC conversion utils --- app/utils/btc-conversions.js | 11 +++++++++++ tests/unit/utils/btc-conversions-test.js | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 app/utils/btc-conversions.js create mode 100644 tests/unit/utils/btc-conversions-test.js 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/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); + }); +}); From 89ddde28b8c716cb0f1dd242c9c920ea376bbdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 18 Dec 2024 12:32:52 +0400 Subject: [PATCH 02/10] Add util for checking if ISO date string is today --- app/utils/iso-date-is-today.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 app/utils/iso-date-is-today.js 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; +} From 697ace35b50b25ffe112960a50b02b5325073fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Wed, 18 Dec 2024 12:34:31 +0400 Subject: [PATCH 03/10] Use historic BTC rate for expense items; allow BTC as currency --- app/components/add-expense-item/component.js | 3 +- app/components/add-reimbursement/component.js | 54 +++++++++++-------- app/helpers/fmt-fiat-currency.js | 10 ++-- app/services/community-funds.js | 2 +- app/services/exchange-rates.js | 28 ++++++++-- .../helpers/fmt-fiat-currency-test.js | 7 +++ .../unit/components/add-reimbursement-test.js | 8 --- tests/unit/services/exchange-rates-test.js | 4 +- 8 files changed, 73 insertions(+), 43 deletions(-) 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'); }); }); From 7a61d1c569f350720e124ff071b5974f85e7ffef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Jan 2025 12:31:37 -0500 Subject: [PATCH 04/10] Add nvmrc config --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..b6a7d89 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16 From aad2aaecd12bdd1c73525e34aa38f223cf201f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Jan 2025 15:45:16 -0500 Subject: [PATCH 05/10] Update contracts, schemas --- package-lock.json | 29 ++++++++++++++++------------- package.json | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) 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", From f02e5572ba4c03ae98cce56ae85e90087be3079f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Jan 2025 15:45:36 -0500 Subject: [PATCH 06/10] Show amountSats as title of expense item amounts --- app/components/expense-list/template.hbs | 2 +- app/helpers/fmt-amount-sats-title.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 app/helpers/fmt-amount-sats-title.js 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/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 ''; + } +}); From 731e01f2c5ea9dc02e761d894ccafc5615bcd2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Jan 2025 16:05:04 -0500 Subject: [PATCH 07/10] Only expand item details on header click It's annoying not being able to copy details otherwise --- app/components/reimbursement-item/template.hbs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/components/reimbursement-item/template.hbs b/app/components/reimbursement-item/template.hbs index 408860e..b7498be 100644 --- a/app/components/reimbursement-item/template.hbs +++ b/app/components/reimbursement-item/template.hbs @@ -1,7 +1,6 @@

  • -

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

    From 0dd5f6c5ad8b18e90f2a8ba7cba2342df872822d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Jan 2025 16:07:24 -0500 Subject: [PATCH 08/10] Change cursor to indicate possible click --- app/components/reimbursement-item/template.hbs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/reimbursement-item/template.hbs b/app/components/reimbursement-item/template.hbs index b7498be..fe94749 100644 --- a/app/components/reimbursement-item/template.hbs +++ b/app/components/reimbursement-item/template.hbs @@ -1,6 +1,7 @@

  • -

    +

    From 473522986db24ab4e083af6ff92772ef4ed24a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Mon, 20 Jan 2025 16:19:41 -0500 Subject: [PATCH 09/10] Fix linter error --- app/components/reimbursement-item/template.hbs | 4 ++-- app/styles/app.scss | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/components/reimbursement-item/template.hbs b/app/components/reimbursement-item/template.hbs index fe94749..2d9f21d 100644 --- a/app/components/reimbursement-item/template.hbs +++ b/app/components/reimbursement-item/template.hbs @@ -1,7 +1,7 @@

  • -

    +

    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; From 67538a9437fda91dd8bcb2468bb6db44772d8712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sun, 16 Feb 2025 12:56:55 +0400 Subject: [PATCH 10/10] Use stable/legacy IPFS node in dev Newer kubo/ipfs daemons don't work well with the old HTTP client. --- config/environment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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