From 87ad861714d5f56f3988293c26b9febcfdaa81d3 Mon Sep 17 00:00:00 2001 From: Willy Bruns Date: Tue, 27 Sep 2016 20:42:03 -0700 Subject: [PATCH 1/2] Payment History CSV export download has correct filename simpler: export is just a data URL link, filename suggested by `download` attribute depends on https://github.com/brave/ledger-client/pull/17/commits/d50d59e6db865a4d2da1bb4b56fe634f5f645d18 fixes #4332 --- app/ledger.js | 13 ------------- js/about/preferences.js | 7 ++++--- js/lib/ledgerExportUtil.js | 13 +++++++++++++ 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 js/lib/ledgerExportUtil.js diff --git a/app/ledger.js b/app/ledger.js index a80cb5d6ad0..8ac9dfc9cc5 100644 --- a/app/ledger.js +++ b/app/ledger.js @@ -53,7 +53,6 @@ const appStore = require('../js/stores/appStore') const eventStore = require('../js/stores/eventStore') const rulesolver = require('./extensions/brave/content/scripts/pageInformation.js') const ledgerUtil = require('./common/lib/ledgerUtil') -const base64Encode = require('../js/lib/base64').encode // TBD: remove these post beta [MTR] const logPath = 'ledger-log.json' @@ -287,18 +286,6 @@ if (ipc) { if (balanceTimeoutId) clearTimeout(balanceTimeoutId) balanceTimeoutId = setTimeout(getBalance, 5 * msecs.second) }) - - ipc.on(messages.OPEN_LEDGER_TRANSACTION_CSV, (event, viewingIds, csvFilename) => { - if (client) { - let txCsvText = client.getTransactionCSVText(viewingIds) - let txCsvTextDataURI = 'data:text/csv;base64,' + base64Encode(txCsvText) - - const win = electron.BrowserWindow.getFocusedWindow() - if (win && win.webContents) { - win.webContents.downloadURL(txCsvTextDataURI) - } - } - }) } /* diff --git a/js/about/preferences.js b/js/about/preferences.js index ccd3ca32073..8d78cd7bb93 100644 --- a/js/about/preferences.js +++ b/js/about/preferences.js @@ -9,6 +9,7 @@ const Immutable = require('immutable') const SwitchControl = require('../components/switchControl') const ModalOverlay = require('../components/modalOverlay') const cx = require('../lib/classSet.js') +const transactionsToCSVDataURL = require('../lib/ledgerExportUtil.js').transactionsToCSVDataURL const { getZoomValuePercentage } = require('../lib/zoom') const config = require('../constants/config') const appConfig = require('../constants/appConfig') @@ -529,8 +530,8 @@ class PaymentHistoryRow extends ImmutableComponent { return `brave_ledger${this.numericDateStr}.csv` } - onReceiptLinkClick () { - aboutActions.receiptLinkClick(this.viewingId, this.receiptFileName) + get dataURL () { + return transactionsToCSVDataURL(this.transaction.toJS()) } render () { @@ -540,7 +541,7 @@ class PaymentHistoryRow extends ImmutableComponent { return {date} {totalAmountStr} - {this.receiptFileName} + {this.receiptFileName} } } diff --git a/js/lib/ledgerExportUtil.js b/js/lib/ledgerExportUtil.js new file mode 100644 index 00000000000..239bdde7f65 --- /dev/null +++ b/js/lib/ledgerExportUtil.js @@ -0,0 +1,13 @@ +const txUtil = require('ledger-client/util') +const base64Encode = require('./base64').encode + +let transactionsToCSVDataURL = function (transactions) { + let csvText = txUtil.getTransactionCSVText(transactions) + return 'data:text/csv;base64,' + base64Encode(csvText) +} + +module.exports = { + transactionsToCSVRows: txUtil.getTransactionCSVRows, + transactionsToCSVText: txUtil.getTransactionCSVText, + transactionsToCSVDataURL: transactionsToCSVDataURL +} From 4c9a99489c00fbd9ba399bbeae1efa800c5d9b6e Mon Sep 17 00:00:00 2001 From: Willy Bruns Date: Wed, 28 Sep 2016 21:23:25 -0700 Subject: [PATCH 2/2] Payments History export improvements (#4332) - closes #4332 - rows are sorted by Publisher - a TOTAL row is added - moved the ledger transaction utilities into `js/lib/ledgerExportUtil.js` from `ledger-client/util` (see https://github.com/brave/browser-laptop/pull/4346#issuecomment-250319588) - removed some obsolete code (https://github.com/brave/browser-laptop/pull/4346#discussion_r81030186) --- js/about/aboutActions.js | 4 - js/constants/messages.js | 1 - js/lib/ledgerExportUtil.js | 282 ++++++++++++++++++++++++++++++++++++- 3 files changed, 277 insertions(+), 10 deletions(-) diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index 91647ebded9..cde4175a0be 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -176,10 +176,6 @@ const AboutActions = { ipc.send(messages.LEDGER_CREATE_WALLET) }, - receiptLinkClick: function (viewingId, receiptFileName) { - ipc.send(messages.OPEN_LEDGER_TRANSACTION_CSV, viewingId, receiptFileName) - }, - setLedgerEnabled: function (enabled) { ipc.send(messages.LEDGER_ENABLE, enabled) }, diff --git a/js/constants/messages.js b/js/constants/messages.js index 0a5e95692ee..20bc5b1607a 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -152,7 +152,6 @@ const messages = { LEDGER_PUBLISHER: _, LEDGER_UPDATED: _, LEDGER_CREATE_WALLET: _, - OPEN_LEDGER_TRANSACTION_CSV: _, CHECK_BITCOIN_HANDLER: _, ADD_FUNDS_CLOSED: _ } diff --git a/js/lib/ledgerExportUtil.js b/js/lib/ledgerExportUtil.js index 239bdde7f65..af4040b9180 100644 --- a/js/lib/ledgerExportUtil.js +++ b/js/lib/ledgerExportUtil.js @@ -1,13 +1,285 @@ -const txUtil = require('ledger-client/util') const base64Encode = require('./base64').encode +const underscore = require('underscore') +/** + * Generates a contribution breakdown by publisher as a CSV data URL from an array of one or more transactions + * @param {Object[]} transactions - array of transactions + */ let transactionsToCSVDataURL = function (transactions) { - let csvText = txUtil.getTransactionCSVText(transactions) + let csvText = getTransactionCSVText(transactions, null, true) return 'data:text/csv;base64,' + base64Encode(csvText) } +/** + * Filter an array of transactions by an array of viewingIds + * @example + * txUtil.getTransactionsByViewingIds(state.transactions, '0ef3a02d-ffdd-41f1-a074-7a7eb1e8c332') + * // [ { viewingId: '0ef3a02d-ffdd-41f1-a074-7a7eb1e8c332', + * // surveyorId: 'DQfCj8PHdIEJOZp9/L+FZcozgvYoIVSjPSdwqRYQDr0', + * // contribution: { fiat: [Object], rates: [Object], satoshis: 813916, fee: 8858 }, + * // ... + * // }] + * + * @param {Object[]} transactions - array of one or more ledger transactions objects (see `client.state.transactions` entries) + * @param {string[]=} viewingIds - OPTIONAL array of one or more viewingIds to filter transactions (single string viewingId supported too) + * if null or undefined, all transactions are returned + */ +let getTransactionsByViewingIds = function getTransactionsByViewingIds (transactions, viewingIds) { + if (!transactions) { + return [] + } + if (!underscore.isArray(transactions)) { + if (!underscore.isObject(transactions)) { + return [] + } + transactions = [transactions] + } + + if (!viewingIds) { + return transactions + } + + if (viewingIds && typeof (viewingIds) === 'string') { + viewingIds = [viewingIds] + } + if (viewingIds && !viewingIds.length) { + viewingIds = null + } + + if (!viewingIds) { + return [] + } + + transactions = transactions.filter(function (tx) { + return tx && tx.viewingId && (viewingIds.indexOf(tx.viewingId) > -1) + }) + + return transactions +} + +/** + * Gives a contribution summary for an array of one or more transactions + * @example + * txUtil.getTotalContribution(client.state.transactions) + * // { satoshis: 1627832, fiat: { amount: 10, currency: 'USD' }, fee: 19900 } + * + * @param {Object[]} transactions - array of one or more ledger transactions objects (see `client.state.transactions` entries) + * @param {string[]} viewingIds - OPTIONAL array/string containing one or more viewingIds to filter by + * if null or undefined, all transactions are used + */ +let getTotalContribution = function getTotalContribution (transactions, viewingIds) { + var txs = getTransactionsByViewingIds(transactions, viewingIds) + + var totalContribution = { + satoshis: 0, + fiat: { amount: 0, currency: null }, + fee: 0 + } + + for (var i = txs.length - 1; i >= 0; i--) { + var tx = txs[i] || {} + var txContribution = tx.contribution || {} + + totalContribution.satoshis += 0 || txContribution.satoshis + + if (txContribution.fiat) { + if (!totalContribution.fiat.currency && txContribution.fiat.currency) { + totalContribution.fiat.currency = txContribution.fiat.currency + } + + if (totalContribution.fiat.currency === txContribution.fiat.currency) { + totalContribution.fiat.amount += 0 || (txContribution.fiat && txContribution.fiat.amount) + } else { + throw new Error('ledgerUtil.totalContribution cannot handle multiple fiat currencies') + } + } + + totalContribution.fee += 0 || txContribution.fee + } + + return totalContribution +} + +/** + * Gives a summary of votes/contributions by Publisher from an array of one or ore transactions + * @example + * txUtil.getPublisherVoteData(client.state.transactions) + * // { + * // 'chronicle.com': + * // { votes: 2, + * // fraction: 0.04081632653061224, + * // contribution: { satoshis: 33221, fiat: 0.2040816326530612, currency: 'USD' } }, + * // 'waitbutwhy.com': + * // { votes: 3, + * // fraction: 0.061224489795918366, + * // contribution: { satoshis: 49832, fiat: 0.30612244897959184, currency: 'USD' } }, + * // 'archlinux.org': + * // { votes: 1, + * // fraction: 0.02040816326530612, + * // contribution: { satoshis: 16611, fiat: 0.1020408163265306, currency: 'USD' } }, + * // /.../ + * // } + * + * @param {Object[]} transactions - array of transactions + * @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx) + **/ +let getPublisherVoteData = function getPublisherVoteData (transactions, viewingIds) { + transactions = getTransactionsByViewingIds(transactions, viewingIds) + + var publishersWithVotes = {} + var totalVotes = 0 + + for (var i = transactions.length - 1; i >= 0; i--) { + var tx = transactions[i] + var ballots = tx.ballots + + if (!ballots) { + continue + } + + var publishersOnBallot = underscore.keys(ballots) + + for (var j = publishersOnBallot.length - 1; j >= 0; j--) { + let publisher = publishersOnBallot[j] + + let voteDataForPublisher = publishersWithVotes[publisher] || {} + + let voteCount = ballots[publisher] + let publisherVotes = (voteDataForPublisher.votes || 0) + voteCount + totalVotes += voteCount + + voteDataForPublisher.votes = publisherVotes + publishersWithVotes[publisher] = voteDataForPublisher + } + } + + var totalContributionAmountSatoshis = null + var totalContributionAmountFiat = null + var currency = null + + var totalContribution = getTotalContribution(transactions) + + if (totalContribution) { + totalContributionAmountSatoshis = totalContributionAmountSatoshis || totalContribution.satoshis + totalContributionAmountFiat = totalContributionAmountFiat || (totalContribution.fiat && totalContribution.fiat.amount) + currency = currency || (totalContribution.fiat && totalContribution.fiat.currency) + } + + for (let publisher in publishersWithVotes) { + let voteDataForPublisher = publishersWithVotes[publisher] + let fraction = voteDataForPublisher.fraction = voteDataForPublisher.votes / totalVotes + + let contribution = voteDataForPublisher.contribution || {} + if (totalContributionAmountSatoshis) { + contribution.satoshis = Math.round(totalContributionAmountSatoshis * fraction) + } + if (totalContributionAmountFiat) { + contribution.fiat = totalContributionAmountFiat * fraction + } + if (currency) { + contribution.currency = currency + } + + voteDataForPublisher.contribution = contribution + + publishersWithVotes[publisher] = voteDataForPublisher + } + + return publishersWithVotes +} + +/** + * Generates a contribution breakdown by publisher in an array of CSV rows from an array of transactions + * @example + * txUtil.getTransactionCSVRows(client.state.transactions) + * // [ 'Publisher,Votes,Fraction,BTC,USD', + * // 'chronicle.com,2,0.04081632653061224,0.0000033221,0.20 USD', + * // 'waitbutwhy.com,3,0.061224489795918366,0.0000049832,0.31 USD', + * // 'archlinux.org,1,0.02040816326530612,0.0000016611,0.10 USD', + * // /.../ + * // ] + * + * @param {Object[]} transactions - array of transactions + * @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx) + * @param (boolean=) addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false) + **/ +let getTransactionCSVRows = function (transactions, viewingIds, addTotalRow) { + let txContribData = getPublisherVoteData(transactions, viewingIds) + var publishers = (underscore.keys(txContribData) || []) + + // sort publishers alphabetically + // TODO: take locale argument and pass to localeCompare below + publishers = publishers.sort(function (a, b) { + return (a && typeof a === 'string' ? a : '').localeCompare(b && typeof b === 'string' ? b : '') + }) + + var currency = txContribData[publishers[0]].contribution.currency + var headerRow = ['Publisher', 'Votes', 'Fraction', 'BTC', currency].join(',') + + var totalsRow = { + label: 'TOTAL', + votes: 0, + fraction: 0, + btc: 0, + fiat: 0 + } + + var rows = [headerRow] + + rows = rows.concat(publishers.map(function (pub) { + var pubRow = txContribData[pub] + + let rowBTC = pubRow.contribution.satoshis / Math.pow(10, 10) + totalsRow.votes += pubRow.votes + totalsRow.fraction += pubRow.fraction + totalsRow.btc += rowBTC + + if (pubRow.contribution.currency === currency) { + totalsRow.fiat += parseFloat(pubRow.contribution.fiat || '0') + } else { + throw new Error('ledgerExportUtil#getTransactionCSVRows does not support mixed currency data (yet)!') + } + + return [pub, + pubRow.votes, + pubRow.fraction, + rowBTC, + pubRow.contribution.fiat.toFixed(2) + ' ' + pubRow.contribution.currency + ].join(',') + })) + + if (addTotalRow) { + rows.push([ + totalsRow.label, + totalsRow.votes, + totalsRow.fraction, + totalsRow.btc, + totalsRow.fiat.toFixed(2) + ' ' + currency + ].join(',')) + } + + return rows +} + +/** + * Generates a contribution breakdown by publisher in an array of CSV rows from an array of transactions + * @example + * txUtil.getTransactionCSVText(state.transactions) + * // 'Publisher,Votes,Fraction,BTC,USD\nchronicle.com,2,0.04081632653061224,0.0000033221,0.20 USD\nwaitbutwhy.com,3,0.061224489795918366,0.0000049832,0.31 USD\narchlinux.org,1,0.02040816326530612,0.0000016611,0.10 USD /.../' + * + * @param {Object[]} transactions - array of transactions + * @param {string[]=} viewingIds - OPTIONAL array/string with one or more viewingIds to filter transactions by (if empty, uses all tx) + * @param (boolean=) addTotalRow - OPTIONAL boolean indicating whether to add a TOTALS row (defaults false) + **/ +let getTransactionCSVText = function (transactions, viewingIds, addTotalRow) { + return getTransactionCSVRows(transactions, viewingIds, addTotalRow).join('\n') +} + module.exports = { - transactionsToCSVRows: txUtil.getTransactionCSVRows, - transactionsToCSVText: txUtil.getTransactionCSVText, - transactionsToCSVDataURL: transactionsToCSVDataURL + transactionsToCSVDataURL: transactionsToCSVDataURL, + getTransactionCSVText: getTransactionCSVText, + getTransactionCSVRows: getTransactionCSVRows, + getPublisherVoteData: getPublisherVoteData, + getTransactionsByViewingIds: getTransactionsByViewingIds, + getTotalContribution: getTotalContribution }