Merge pull request #226 from 67P/feature/currencies
Use historic BTC rate for expense items; allow BTC as currency
This commit was merged in pull request #226.
This commit is contained in:
@@ -18,7 +18,8 @@ export default class AddExpenseItemComponent extends Component {
|
|||||||
|
|
||||||
currencies = [
|
currencies = [
|
||||||
{ code: 'EUR' },
|
{ code: 'EUR' },
|
||||||
{ code: 'USD' }
|
{ code: 'USD' },
|
||||||
|
{ code: 'BTC' }
|
||||||
];
|
];
|
||||||
|
|
||||||
get isValidAmount () {
|
get isValidAmount () {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { inject as service } from '@ember/service';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { scheduleOnce } from '@ember/runloop';
|
import { scheduleOnce } from '@ember/runloop';
|
||||||
|
import { btcToSats, satsToBtc } from 'kredits-web/utils/btc-conversions';
|
||||||
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
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 readFileContent from 'kredits-web/utils/read-file-content';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
@@ -73,22 +75,6 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
anchor.scrollIntoView();
|
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
|
// TODO use ember-concurrency here
|
||||||
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
||||||
@action
|
@action
|
||||||
@@ -118,16 +104,38 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addExpenseItem (expenseItem) {
|
async addExpenseItem (expense) {
|
||||||
this.expenses.pushObject(expenseItem);
|
let totalBTC = parseFloat(this.total);
|
||||||
this.updateTotalAmountFromFiat();
|
|
||||||
|
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;
|
this.expenseFormVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeExpenseItem (expenseItem) {
|
async removeExpenseItem (expense) {
|
||||||
this.expenses.removeObject(expenseItem);
|
let totalBTC = parseFloat(this.total);
|
||||||
this.updateTotalAmountFromFiat();
|
let amountBTC = satsToBtc(expense.amountSats);
|
||||||
|
totalBTC = totalBTC - amountBTC;
|
||||||
|
this.total = totalBTC.toFixed(8);
|
||||||
|
|
||||||
|
this.expenses.removeObject(expense);
|
||||||
|
|
||||||
if (this.expenses.length === 0) {
|
if (this.expenses.length === 0) {
|
||||||
this.expenseFormVisible = true;
|
this.expenseFormVisible = true;
|
||||||
@@ -143,7 +151,7 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
const contributor = this.contributors.findBy('id', this.contributorId);
|
const contributor = this.contributors.findBy('id', this.contributorId);
|
||||||
|
|
||||||
const attributes = {
|
const attributes = {
|
||||||
amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats
|
amount: btcToSats(this.total),
|
||||||
token: config.tokens['BTC'],
|
token: config.tokens['BTC'],
|
||||||
recipientId: this.contributorId,
|
recipientId: this.contributorId,
|
||||||
title: `Expenses covered by ${contributor.name}`,
|
title: `Expenses covered by ${contributor.name}`,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<span class="date">{{fmt-date-localized expense.date}}:</span>
|
<span class="date">{{fmt-date-localized expense.date}}:</span>
|
||||||
<span class="title">{{expense.title}}</span>
|
<span class="title">{{expense.title}}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="amount">
|
<div class="amount" title="{{fmt-amount-sats-title expense.amountSats}}">
|
||||||
{{fmt-fiat-currency expense.amount expense.currency}}
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
</div>
|
</div>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<li data-reimbursement-id={{@reimbursement.id}}
|
<li data-reimbursement-id={{@reimbursement.id}}
|
||||||
class="{{item-status @reimbursement}}"
|
class="{{item-status @reimbursement}}">
|
||||||
role="button" {{on "click" this.toggleExpenseDetails}}>
|
<p class="meta cursor-pointer" role="button"
|
||||||
<p class="meta">
|
{{on "click" this.toggleExpenseDetails}}>
|
||||||
<span class="recipient">
|
<span class="recipient">
|
||||||
<UserAvatar @contributor={{@reimbursement.contributor}} />
|
<UserAvatar @contributor={{@reimbursement.contributor}} />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -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 '';
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -2,10 +2,14 @@ import { helper } from '@ember/component/helper';
|
|||||||
|
|
||||||
export default helper(function fmtFiatCurrency(params) {
|
export default helper(function fmtFiatCurrency(params) {
|
||||||
const lang = navigator.language || navigator.userLanguage;
|
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, {
|
const formatter = new Intl.NumberFormat(lang, {
|
||||||
style: 'currency',
|
style: 'currency', currency, currencyDisplay: 'code'
|
||||||
currency: params[1] || 'EUR',
|
|
||||||
currencyDisplay: 'code'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return formatter.format(params[0]);
|
return formatter.format(params[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default class CommunityFundsService extends Service {
|
|||||||
|
|
||||||
// Format and round the approximate USD value
|
// Format and round the approximate USD value
|
||||||
const lang = navigator.language || navigator.userLanguage;
|
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);
|
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
||||||
|
|
||||||
this.balances.pushObject({
|
this.balances.pushObject({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import config from 'kredits-web/config/environment';
|
|||||||
|
|
||||||
// Need to go through proxy for CORS headers
|
// Need to go through proxy for CORS headers
|
||||||
const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`;
|
const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`;
|
||||||
|
const kosmosBtcPriceBaseUrl = "https://storage.kosmos.org/kosmos/public/btc-price";
|
||||||
|
|
||||||
async function fetchFromBitstamp(currencyPair) {
|
async function fetchFromBitstamp(currencyPair) {
|
||||||
try {
|
try {
|
||||||
@@ -16,11 +17,12 @@ async function fetchFromBitstamp(currencyPair) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class ExchangeRatesService extends Service {
|
export default class ExchangeRatesService extends Service {
|
||||||
@tracked btceur = 0;
|
@tracked EUR = 0;
|
||||||
@tracked btcusd = 0;
|
@tracked USD = 0;
|
||||||
|
@tracked historic = {};
|
||||||
|
|
||||||
get exchangeRatesLoaded () {
|
get exchangeRatesLoaded () {
|
||||||
return (this.btceur !== 0) && (this.btcusd !== 0);
|
return (this.EUR !== 0) && (this.USD !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchRates (source='bitstamp') {
|
async fetchRates (source='bitstamp') {
|
||||||
@@ -28,8 +30,24 @@ export default class ExchangeRatesService extends Service {
|
|||||||
|
|
||||||
switch(source) {
|
switch(source) {
|
||||||
case 'bitstamp':
|
case 'bitstamp':
|
||||||
this.btceur = await fetchFromBitstamp('btceur');
|
this.EUR = await fetchFromBitstamp('btceur');
|
||||||
this.btcusd = await fetchFromBitstamp('btcusd');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ a {
|
|||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default function(value) {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
return value === today;
|
||||||
|
}
|
||||||
@@ -76,10 +76,10 @@ module.exports = function(environment) {
|
|||||||
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
|
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
|
||||||
|
|
||||||
ENV.ipfs = {
|
ENV.ipfs = {
|
||||||
host: 'localhost',
|
host: '10.1.1.198',
|
||||||
port: '5001',
|
port: '5001',
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
gatewayUrl: 'http://localhost:8080/ipfs'
|
gatewayUrl: 'http://10.1.1.198:9090/ipfs'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Uncomment if you want to use local akkounts
|
// Uncomment if you want to use local akkounts
|
||||||
|
|||||||
Generated
+16
-13
@@ -14,7 +14,7 @@
|
|||||||
"@ember/optional-features": "^1.3.0",
|
"@ember/optional-features": "^1.3.0",
|
||||||
"@glimmer/component": "^1.0.0",
|
"@glimmer/component": "^1.0.0",
|
||||||
"@glimmer/tracking": "^1.0.0",
|
"@glimmer/tracking": "^1.0.0",
|
||||||
"@kredits/contracts": "^7.3.0",
|
"@kredits/contracts": "^7.5.0",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.12.1",
|
"ember-auto-import": "^1.12.1",
|
||||||
@@ -3062,17 +3062,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@kosmos/schemas": {
|
"node_modules/@kosmos/schemas": {
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@kosmos/schemas/-/schemas-3.2.0.tgz",
|
||||||
"license": "MIT"
|
"integrity": "sha512-GyWXmWcATcakmEdPOpafdckBgWFqcpSh+tB8nm11VGgtgA94pfoFhKZ3t88WELItKfuP/GFHgMiZmdMB3bSkdQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@kredits/contracts": {
|
"node_modules/@kredits/contracts": {
|
||||||
"version": "7.3.0",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.5.0.tgz",
|
||||||
"integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==",
|
"integrity": "sha512-4Jw6lXn9DfZiTnMtWHwGLS5zhxEJZ2ANfAgcr7tZxqRl7Y0ijpoWvPCA+LkSGqRgKspyiw9+7vSVTtyvAyQbPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kosmos/schemas": "^3.1.0",
|
"@kosmos/schemas": "^3.2.0",
|
||||||
"ethers": "^5.4.7",
|
"ethers": "^5.4.7",
|
||||||
"ipfs-http-client": "^56.0.3",
|
"ipfs-http-client": "^56.0.3",
|
||||||
"multihashes": "^4.0.3",
|
"multihashes": "^4.0.3",
|
||||||
@@ -26067,16 +26068,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@kosmos/schemas": {
|
"@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
|
"dev": true
|
||||||
},
|
},
|
||||||
"@kredits/contracts": {
|
"@kredits/contracts": {
|
||||||
"version": "7.3.0",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.5.0.tgz",
|
||||||
"integrity": "sha512-7Bo2LFEqGIK39G54AStas1RWouBOp+r7Vcv2vP1uLbY01Usea8SHvd2OjLP6nFgsQa0n/rto9oremHv5IAylrQ==",
|
"integrity": "sha512-4Jw6lXn9DfZiTnMtWHwGLS5zhxEJZ2ANfAgcr7tZxqRl7Y0ijpoWvPCA+LkSGqRgKspyiw9+7vSVTtyvAyQbPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@kosmos/schemas": "^3.1.0",
|
"@kosmos/schemas": "^3.2.0",
|
||||||
"ethers": "^5.4.7",
|
"ethers": "^5.4.7",
|
||||||
"ipfs-http-client": "^56.0.3",
|
"ipfs-http-client": "^56.0.3",
|
||||||
"multihashes": "^4.0.3",
|
"multihashes": "^4.0.3",
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
"@ember/optional-features": "^1.3.0",
|
"@ember/optional-features": "^1.3.0",
|
||||||
"@glimmer/component": "^1.0.0",
|
"@glimmer/component": "^1.0.0",
|
||||||
"@glimmer/tracking": "^1.0.0",
|
"@glimmer/tracking": "^1.0.0",
|
||||||
"@kredits/contracts": "^7.3.0",
|
"@kredits/contracts": "^7.5.0",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.12.1",
|
"ember-auto-import": "^1.12.1",
|
||||||
|
|||||||
@@ -18,5 +18,12 @@ module('Integration | Helper | fmt-fiat-currency', function(hooks) {
|
|||||||
await render(hbs`{{fmt-fiat-currency this.amount 'USD'}}`);
|
await render(hbs`{{fmt-fiat-currency this.amount 'USD'}}`);
|
||||||
assert.ok(this.element.textContent.trim().match(/USD/),
|
assert.ok(this.element.textContent.trim().match(/USD/),
|
||||||
'using defined currency when given');
|
'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');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,12 +39,4 @@ module('Unit | Component | add-reimbursement', function(hooks) {
|
|||||||
assert.equal(component.totalEUR, '71');
|
assert.equal(component.totalEUR, '71');
|
||||||
assert.equal(component.totalUSD, '59');
|
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');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module('Unit | Service | exchange-rates', function(hooks) {
|
|||||||
test('#fetchRates', async function(assert) {
|
test('#fetchRates', async function(assert) {
|
||||||
let service = this.owner.lookup('service:exchange-rates');
|
let service = this.owner.lookup('service:exchange-rates');
|
||||||
await service.fetchRates();
|
await service.fetchRates();
|
||||||
assert.equal(service.btceur, 9167.57, 'fetches BTCEUR from Bitstamp');
|
assert.equal(service.EUR, 9167.57, 'fetches BTCEUR from Bitstamp');
|
||||||
assert.equal(service.btcusd, 10749.70, 'fetches BTCUSD from Bitstamp');
|
assert.equal(service.USD, 10749.70, 'fetches BTCUSD from Bitstamp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user