diff --git a/src/js/controllers/wallet-details.controller.js b/src/js/controllers/wallet-details.controller.js
index 7a06d4fb6..f3109db8b 100644
--- a/src/js/controllers/wallet-details.controller.js
+++ b/src/js/controllers/wallet-details.controller.js
@@ -1,16 +1,52 @@
'use strict';
-angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, $ionicHistory, profileService, lodash, configService, platformInfo, walletService, txpModalService, externalLinkService, popupService, addressbookService, sendFlowService, storageService, $ionicScrollDelegate, $window, bwcError, gettextCatalog, timeService, feeService, appConfigService, rateService) {
-
- var HISTORY_SHOW_LIMIT = 10;
- var currentTxHistoryPage = 0;
+angular.module('copayApp.controllers').controller('walletDetailsController', function($scope, $rootScope, $interval, $timeout, $filter, $log, $ionicModal, $ionicPopover, $state, $stateParams, $ionicHistory, profileService, lodash, configService, platformInfo, walletService, txpModalService, externalLinkService, popupService, addressbookService, sendFlowService, storageService, $ionicScrollDelegate, $window, bwcError, gettextCatalog, timeService, feeService, appConfigService, rateService, walletHistoryService) {
+ // Desktop can display 13 rows of transactions, bump it up to a nice round 15.
+ var DISPLAY_PAGE_SIZE = 15;
+ var currentTxHistoryDisplayPage = 0;
+ var completeTxHistory = []
var listeners = [];
- $scope.txps = [];
- $scope.completeTxHistory = [];
- $scope.openTxpModal = txpModalService.open;
+
+ // For gradual migration for doing it properly
+ $scope.vm = {
+ allowInfiniteScroll: false,
+ gettingCachedHistory: true,
+ gettingInitialHistory: true,
+ updatingTxHistory: false,
+ fetchedAllTxHistory: false,
+ //updateTxHistoryError: false
+ updateTxHistoryFailed: false
+ };
+
+ // Need flag for when to allow infinite scroll at bottom
+ // - ie not when loading initial data and there is no more cached data
+
+ $scope.amountIsCollapsible = false;
+ $scope.color = '#888888';
+
+ $scope.filteredTxHistory = [];
$scope.isCordova = platformInfo.isCordova;
$scope.isAndroid = platformInfo.isAndroid;
$scope.isIOS = platformInfo.isIOS;
+ $scope.isSearching = false;
+ $scope.openTxpModal = txpModalService.open;
+ $scope.requiresMultipleSignatures = false;
+ $scope.showBalanceButton = false;
+ $scope.status = null;
+ // Displaying 50 transactions when entering the screen takes a while, so only display a subset
+ // of everything we have, not the complete history.
+ $scope.txHistory = []; // This is what is displayed
+ $scope.txHistorySearchResults = [];
+ $scope.txps = [];
+ $scope.updatingStatus = false;
+ $scope.updateStatusError = null;
+ $scope.updatingTxHistoryProgress = 0;
+ $scope.wallet = null;
+ $scope.walletId = '';
+ $scope.walletNotRegistered = false;
+
+
+
var channel = "ga";
if (platformInfo.isCordova) {
@@ -50,6 +86,7 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
$scope.updatingStatus = true;
$scope.updateStatusError = null;
$scope.walletNotRegistered = false;
+ $scope.vm.fetchedAllTxHistory = false;
walletService.getStatus($scope.wallet, {
force: !!force,
@@ -133,68 +170,96 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
if (err) return;
$timeout(function() {
walletService.startScan($scope.wallet, function() {
- $scope.updateAll();
+ $scope.updateAll(true, true);
$scope.$apply();
});
});
});
};
- var updateTxHistory = function(cb) {
- if (!cb) cb = function() {};
- $scope.updateTxHistoryError = false;
- $scope.updatingTxHistoryProgress = 0;
-
- feeService.getFeeLevels($scope.wallet.coin, function(err, levels) {
- walletService.getTxHistory($scope.wallet, {
- feeLevels: levels
- }, function(err, txHistory) {
- $scope.updatingTxHistory = false;
- if (err) {
- $scope.txHistory = null;
- $scope.updateTxHistoryError = true;
- return;
- }
-
- applyCurrencyAliases(txHistory);
-
- var config = configService.getSync();
- var fiatCode = config.wallet.settings.alternativeIsoCode;
- lodash.each(txHistory, function(t) {
- var r = rateService.toFiat(t.amount, fiatCode, $scope.wallet.coin);
- t.alternativeAmountStr = r.toFixed(2) + ' ' + fiatCode;
- });
-
- $scope.completeTxHistory = txHistory;
-
- $scope.showHistory();
- $timeout(function() {
- $scope.$apply();
- });
- return cb();
- });
- });
- };
-
function applyCurrencyAliases(txHistory) {
var defaults = configService.getDefaults();
var configCache = configService.getSync();
- lodash.each(txHistory, function(t) {
- t.amountUnitStr = $scope.wallet.coin == 'btc'
+ lodash.each(txHistory, function onTx(tx) {
+ tx.amountUnitStr = $scope.wallet.coin == 'btc'
? (configCache.bitcoinAlias || defaults.bitcoinAlias)
: (configCache.bitcoinCashAlias || defaults.bitcoinCashAlias);
- t.amountUnitStr = t.amountUnitStr.toUpperCase();
+ tx.amountUnitStr = tx.amountUnitStr.toUpperCase();
});
}
- $scope.showHistory = function() {
- if ($scope.completeTxHistory) {
- $scope.txHistory = $scope.completeTxHistory.slice(0, (currentTxHistoryPage + 1) * HISTORY_SHOW_LIMIT);
- $scope.txHistoryShowMore = $scope.completeTxHistory.length > $scope.txHistory.length;
+ function formatTxHistoryForDisplay(txHistory) {
+ applyCurrencyAliases(txHistory);
+
+ var config = configService.getSync();
+ var fiatCode = config.wallet.settings.alternativeIsoCode;
+ lodash.each(txHistory, function(t) {
+ var r = rateService.toFiat(t.amount, fiatCode, $scope.wallet.coin);
+ t.alternativeAmountStr = r.toFixed(2) + ' ' + fiatCode;
+ });
+ }
+
+
+ function updateTxHistoryFromCachedData() {
+ walletHistoryService.getCachedTxHistory($scope.wallet.id, function onGetCachedTxHistory(err, txHistory){
+ $scope.vm.gettingCachedHistory = false;
+ if (err) {
+ // Don't display an error because we are also requesting the history.
+ $log.error('Error getting cached tx history.', err);
+ return;
+ }
+
+ if (!txHistory) {
+ $log.debug('No cached tx history.');
+ return;
+ }
+
+ formatTxHistoryForDisplay(txHistory);
+
+ completeTxHistory = txHistory;
+ showHistory(false);
+ $scope.$apply();
+ });
+ }
+
+ function fetchAndShowTxHistory(getLatest, flushCacheOnNew) {
+ $scope.vm.updatingTxHistory = true;
+
+ walletHistoryService.updateLocalTxHistoryByPage($scope.wallet, getLatest, flushCacheOnNew, function onUpdateLocalTxHistoryByPage(err, txHistory, fetchedAllTransactions) {
+ $scope.vm.gettingInitialHistory = false;
+ $scope.vm.updatingTxHistory = false;
+ $scope.$broadcast('scroll.infiniteScrollComplete');
+
+ if (err) {
+ console.error('pagination Failed to get history.', err);
+ $scope.vm.updateTxHistoryFailed = true;
+ return;
+ }
+
+ if (fetchedAllTransactions) {
+ $scope.vm.fetchedAllTxHistory = true;
+ }
+
+ formatTxHistoryForDisplay(txHistory);
+
+ completeTxHistory = txHistory;
+ showHistory(true);
+ $scope.$apply();
+ });
+ }
+
+
+ function showHistory(showAll) {
+ if (completeTxHistory) {
+ $scope.txHistory = showAll ? completeTxHistory : completeTxHistory.slice(0, (currentTxHistoryDisplayPage + 1) * DISPLAY_PAGE_SIZE);
+ $scope.vm.allowInfiniteScroll = !$scope.vm.fetchedAllTxHistory && !(completeTxHistory.length === $scope.txHistory.length && $scope.vm.gettingInitialHistory);
+ } else {
+ $scope.vm.allowInfiniteScroll = false;
}
- };
+ }
+
$scope.getDate = function(txCreated) {
var date = new Date(txCreated * 1000);
@@ -233,24 +298,35 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
return !tx.confirmations || tx.confirmations === 0;
};
+ // on-infinite="showMore()"
$scope.showMore = function() {
- $timeout(function() {
- currentTxHistoryPage++;
- $scope.showHistory();
+ // Check if we have more than we are displaying
+ if (completeTxHistory.length > $scope.txHistory.length) {
+ currentTxHistoryDisplayPage++;
+ showHistory(false);
$scope.$broadcast('scroll.infiniteScrollComplete');
- }, 100);
+ return;
+ }
+
+ if ($scope.vm.updatingTxHistory) {
+ return;
+ }
+
+ fetchAndShowTxHistory(false, false);
};
+ // on-refresh="onRefresh()"
$scope.onRefresh = function() {
$timeout(function() {
$scope.$broadcast('scroll.refreshComplete');
}, 300);
- $scope.updateAll(true);
+ $scope.updateAll(true, false);
};
- $scope.updateAll = function(force, cb) {
- updateStatus(force);
- updateTxHistory(cb);
+ $scope.updateAll = function(forceStatusUpdate, flushTxCacheOnNew) {
+ updateStatus(forceStatusUpdate);
+ //updateTxHistory(cb);
+ fetchAndShowTxHistory(true, flushTxCacheOnNew);
};
$scope.hideToggle = function() {
@@ -303,7 +379,7 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
if (!$scope.wallet) return;
$scope.requiresMultipleSignatures = $scope.wallet.credentials.m > 1;
- $scope.updatingTxHistory = true;
+ $scope.vm.gettingInitialHistory = true;
addressbookService.list(function(err, ab) {
if (err) $log.error(err);
@@ -313,11 +389,11 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
listeners = [
$rootScope.$on('bwsEvent', function(e, walletId) {
if (walletId == $scope.wallet.id && e.type != 'NewAddress')
- $scope.updateAll();
+ $scope.updateAll(false, false);
}),
$rootScope.$on('Local/TxAction', function(e, walletId) {
if (walletId == $scope.wallet.id)
- $scope.updateAll();
+ $scope.updateAll(false, false);
}),
];
});
diff --git a/src/js/services/wallet-history.service.js b/src/js/services/wallet-history.service.js
new file mode 100644
index 000000000..e10e763e9
--- /dev/null
+++ b/src/js/services/wallet-history.service.js
@@ -0,0 +1,244 @@
+'use strict';
+
+(function(){
+
+ angular
+ .module('bitcoincom.services')
+ .factory('walletHistoryService', walletHistoryService);
+
+ function walletHistoryService(configService, storageService, lodash, $log, txFormatService) {
+ //var PAGE_SIZE = 50;
+ var PAGE_SIZE = 20; // For dev only
+ // How much to overlap on each end of the page, for mitigating inconsistent sort order.
+ var PAGE_OVERLAP_FRACTION = 0.2;
+ var PAGE_OVERLAP = Math.floor(PAGE_SIZE * PAGE_OVERLAP_FRACTION);
+ // The amount of transactions in the new overlapping resultset that we already know about.
+ // If we know about at least this many, then there are probably no gaps.
+ var MIN_KNOWN_TX_OVERLAP = Math.floor(PAGE_OVERLAP * 0.5);
+
+ var SAFE_CONFIRMATIONS = 6;
+
+ var allTransactionsFetched = false;
+ var service = {
+ getCachedTxHistory: getCachedTxHistory,
+ updateLocalTxHistoryByPage: updateLocalTxHistoryByPage,
+ };
+ return service;
+
+ function addEarlyTransactions(walletId, cachedTxs, newTxs) {
+
+ var cachedTxIds = {};
+ cachedTxs.forEach(function forCachedTx(tx){
+ cachedTxIds[tx.txid] = true;
+ });
+
+ var someTransactionsWereNew = false;
+ var overlappingTxsCount = 0;
+
+ newTxs.forEach(function forNewTx(tx){
+ if (cachedTxIds[tx.txid]) {
+ overlappingTxsCount++;
+ } else {
+ someTransactionsWereNew = true;
+ cachedTxs.push(tx);
+ }
+ });
+
+ if (overlappingTxsCount >= MIN_KNOWN_TX_OVERLAP) { // We are good
+ if (someTransactionsWereNew) {
+ saveTxHistory(walletId, cachedTxs);
+ } else if (overlappingTxsCount === newTxs.length) {
+ allTransactionsFetched = true;
+ }
+ return cachedTxs;
+ } else {
+ // We might be missing some txs.
+ console.error('We might be missing some txs in the history.');
+ // Our history is wrong, so remove it - we could instead, try to fetch data that was not so early.
+ storageService.removeTxHistory(walletId, function onRemoveTxHistory(){});
+ return [];
+ }
+
+ }
+
+ function addLatestTransactions(walletId, cachedTxs, newTxs) {
+ var cachedTxIds = {};
+ cachedTxs.forEach(function forCachedTx(tx){
+ cachedTxIds[tx.txid] = true;
+ });
+
+ var someTransactionsWereNew = false;
+ var overlappingTxsCount = 0;
+ var uniqueNewTxs = [];
+
+ newTxs.forEach(function forNewTx(tx){
+ if (cachedTxIds[tx.txid]) {
+ overlappingTxsCount++;
+ } else {
+ someTransactionsWereNew = true;
+ uniqueNewTxs.push(tx);
+ }
+ });
+
+ if (overlappingTxsCount >= MIN_KNOWN_TX_OVERLAP) { // We are good
+ if (someTransactionsWereNew) {
+ var allTxs = uniqueNewTxs.concat(cachedTxs);
+ saveTxHistory(walletId, allTxs);
+ return allTxs;
+ } else {
+ return cachedTxs;
+ }
+ } else {
+ // We might be missing some txs.
+ // Our history is wrong, so just include the latest ones
+ saveTxHistory(walletId, newTxs);
+ return newTxs;
+ }
+
+ }
+
+ // Only clear the cache once we have received new transactions from the server.
+ /**
+ * @param {function(err, txs)} cb - transactions is always an array, may be empty
+ */
+ function fetchTxHistoryByPage(wallet, start, cb) {
+ var skip = Math.max(0, start - PAGE_OVERLAP);
+ var limit = PAGE_SIZE;
+
+ var opts = {
+ skip: skip,
+ limit: limit
+ };
+ wallet.getTxHistory(opts, function onTxHistory(err, txsFromServer) {
+ if (err) {
+ return cb(err, []);
+ }
+
+ if (txsFromServer.length === 0) {
+ return cb(null, []);
+ }
+
+ var processedTxs = processNewTxs(wallet, txsFromServer);
+
+ return cb(null, processedTxs);
+ });
+ }
+
+ /**
+ * @param {string} walletId
+ * @param {function(error, txs)} cb - txs is always an array, may be empty
+ */
+ function getCachedTxHistory(walletId, cb) {
+ storageService.getTxHistory(walletId, function onGetTxHistory(err, txHistoryString){
+ if (err) {
+ return cb(err, []);
+ }
+
+ if (!txHistoryString) {
+ return cb(null, []);
+ }
+
+ try {
+ var txHistory = JSON.parse(txHistoryString);
+ return cb(null, txHistory);
+ } catch (e) {
+ $log.error('Failed to parse tx history.', e);
+ return cb(e, []);
+ }
+ });
+ }
+
+ function processNewTxs(wallet, txs) {
+ var now = Math.floor(Date.now() / 1000);
+ var txHistoryUnique = {};
+ var processedTxs = [];
+ wallet.hasUnsafeConfirmed = false;
+
+ lodash.each(txs, function(tx) {
+ tx = txFormatService.processTx(wallet.coin, tx);
+
+ // no future transactions...
+ if (tx.time > now)
+ tx.time = now;
+
+ if (tx.confirmations >= SAFE_CONFIRMATIONS) {
+ tx.safeConfirmed = SAFE_CONFIRMATIONS + '+';
+ } else {
+ tx.safeConfirmed = false;
+ wallet.hasUnsafeConfirmed = true;
+ }
+
+ if (tx.note) {
+ delete tx.note.encryptedEditedByName;
+ delete tx.note.encryptedBody;
+ }
+
+ if (!txHistoryUnique[tx.txid]) {
+ processedTxs.push(tx);
+ txHistoryUnique[tx.txid] = true;
+ } else {
+ $log.debug('Ignoring duplicate TX in history: ' + tx.txid)
+ }
+ });
+
+ return processedTxs;
+ };
+
+ function saveTxHistory(walletId, processedTxs) {
+ storageService.setTxHistory(processedTxs, walletId, function onSetTxHistory(error){
+ if (error) {
+ $log.error('pagination Failed to save tx history.', error);
+ }
+ });
+ }
+
+
+ function updateLocalTxHistoryByPage(wallet, getLatest, flushCacheOnNew, cb) {
+
+ if (flushCacheOnNew) {
+ fetchTxHistoryByPage(wallet, 0, function onFetchTxHistory(err, txs){
+ if (err) {
+ return cb(err, txs);
+ }
+ saveTxHistory(wallet.id, txs);
+ return cb(null, txs);
+ });
+ } else {
+ getCachedTxHistory(wallet.id, function onCachedHistory(err, cachedTxs){
+ if (err) {
+ $log.error('Failed to get cached tx history.', err);
+ return cb(err, []);
+ }
+
+ var start = getLatest ? 0 : cachedTxs.length;
+ fetchTxHistoryByPage(wallet, start, function onFetchHistory(err, fetchedTxs){
+ if (err) {
+ return cb(err);
+ }
+
+ if (fetchedTxs.length === 0) {
+ return cb(null, cachedTxs, true /*fetchedAllTransactions*/);
+ }
+
+ var txs = [];
+ if (getLatest) {
+ txs = addLatestTransactions(wallet.id, cachedTxs, fetchedTxs);
+ } else {
+ allTransactionsFetched = false;
+ txs = addEarlyTransactions(wallet.id, cachedTxs, fetchedTxs);
+ return cb(null, txs, allTransactionsFetched);
+ }
+ return cb(null, txs);
+ });
+
+
+ });
+ }
+ }
+
+
+
+ }
+
+
+})();
\ No newline at end of file
diff --git a/src/js/services/walletService.js b/src/js/services/walletService.js
index 647c69734..d29f99272 100644
--- a/src/js/services/walletService.js
+++ b/src/js/services/walletService.js
@@ -396,6 +396,23 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
return ret;
};
+ var skipped = 0;
+
+ function fixTxsUnit(txs) {
+ if (!txs || !txs[0] || !txs[0].amountStr) return;
+
+ var cacheCoin = txs[0].amountStr.split(' ')[1];
+
+ if (cacheCoin == 'bits') {
+
+ $log.debug('Fixing Tx Cache Unit to: ' + wallet.coin)
+ lodash.each(txs, function(tx) {
+ tx.amountStr = txFormatService.formatAmountStr(wallet.coin, tx.amount);
+ tx.feeStr = txFormatService.formatAmountStr(wallet.coin, tx.fees);
+ });
+ }
+ };
+
var updateLocalTxHistory = function(wallet, opts, cb) {
var FIRST_LIMIT = 5;
var LIMIT = 50;
@@ -406,25 +423,10 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
var progressFn = opts.progressFn || function() {};
var foundLimitTx = false;
-
if (opts.feeLevels) {
opts.lowAmount = root.getLowAmount(wallet, opts.feeLevels);
}
- var fixTxsUnit = function(txs) {
- if (!txs || !txs[0] || !txs[0].amountStr) return;
-
- var cacheCoin = txs[0].amountStr.split(' ')[1];
-
- if (cacheCoin == 'bits') {
-
- $log.debug('Fixing Tx Cache Unit to: ' + wallet.coin)
- lodash.each(txs, function(tx) {
- tx.amountStr = txFormatService.formatAmountStr(wallet.coin, tx.amount);
- tx.feeStr = txFormatService.formatAmountStr(wallet.coin, tx.fees);
- });
- }
- };
getSavedTxs(walletId, function(err, txsFromLocal) {
if (err) return cb(err);
@@ -435,13 +437,14 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
var endingTxid = confirmedTxs[0] ? confirmedTxs[0].txid : null;
var endingTs = confirmedTxs[0] ? confirmedTxs[0].time : null;
- $log.debug('Confirmed TXs. Got:' + confirmedTxs.length + '/' + txsFromLocal.length);
+ console.log('pagination Hard confirmed TXs. Got:' + confirmedTxs.length + '/' + txsFromLocal.length);
// First update
progressFn(txsFromLocal, 0);
wallet.completeHistory = txsFromLocal;
function getNewTxs(newTxs, skip, next) {
+ console.log('pagination getNewTxs skip: ' + skip);
getTxsFromServer(wallet, skip, endingTxid, requestLimit, function(err, res) {
if (err) {
$log.warn(bwcError.msg(err, 'Server Error')); //TODO
@@ -454,6 +457,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
return next(err);
}
+ console.log('pagination Result count: ' + res.length);
// Check if new txs are founds, if yes, lets investigate in the 50 next
// To be sure we are not missing txs by sorting (maybe a new tx is after the "endingTxid"
var newDiscoveredTxs = res.filter(function (x) {
@@ -462,7 +466,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
}).length == 0;
});
- $log.debug('Discovering TXs. Got:' + newDiscoveredTxs.length);
+ console.log('pagination Discovering new TXs. Got:' + newDiscoveredTxs.length);
var shouldContinue = newDiscoveredTxs.length > 0;
@@ -475,7 +479,7 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
skip = skip + requestLimit;
- $log.debug('Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue);
+ console.log('pagination Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue);
// TODO Dirty