diff --git a/Gruntfile.js b/Gruntfile.js index 7968f2510..4ac23e47a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -71,7 +71,7 @@ module.exports = function(grunt) { sign_android: { // When the build log outputs "Built the following apk(s):", it seems to need the filename to start with "android-release". // It looks like it simply lists all apk files starting with "android-release" - command: 'rm -f platforms/android/build/outputs/apk/android-release-signed-*.apk; jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ../bitcoin-com-release-key.jks -signedjar platforms/android/build/outputs/apk/android-release-signed.apk platforms/android/build/outputs/apk/android-release-unsigned.apk bitcoin-com && zipalign -v 4 platforms/android/build/outputs/apk/android-release-signed.apk platforms/android/build/outputs/apk/bitcoin-com-wallet-<%= pkg.fullVersion %>-android-signed-aligned.apk', + command: 'rm -f platforms/android/build/outputs/apk/release/*-android-signed-aligned.apk; jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ../bitcoin-com-release-key.jks -signedjar platforms/android/build/outputs/apk/release/android-release-signed.apk platforms/android/build/outputs/apk/release/android-release-unsigned.apk bitcoin-com && zipalign -v 4 platforms/android/build/outputs/apk/release/android-release-signed.apk platforms/android/build/outputs/apk/release/bitcoin-com-wallet-<%= pkg.fullVersion %>-android-signed-aligned.apk', stdin: true, }, sign_desktop_dist: { diff --git a/app-template/config-template.xml b/app-template/config-template.xml index 8686b7b36..b24b86fae 100644 --- a/app-template/config-template.xml +++ b/app-template/config-template.xml @@ -77,7 +77,7 @@ - + diff --git a/src/android/build-extras.gradle b/src/android/build-extras.gradle index e7dd50572..c90145418 100644 --- a/src/android/build-extras.gradle +++ b/src/android/build-extras.gradle @@ -1,5 +1,6 @@ ext { ANDROID_SUPPORT_V4_VERSION = '26.1.0' + ANDROID_SUPPORT_ANNOTATIONS_VERSION = '26.1.0' } configurations.all { diff --git a/src/js/services/send-flow.service.js b/src/js/services/send-flow.service.js index e8be2e487..babf5096a 100644 --- a/src/js/services/send-flow.service.js +++ b/src/js/services/send-flow.service.js @@ -52,7 +52,7 @@ angular // Detect some merchant that we know if (payProData.memo.indexOf('eGifter') > -1) { name = 'eGifter' - } else if (paymentUrl.indexOf('https://bitpay.com') > -1) { + } else if (payProData.url.indexOf('https://bitpay.com') > -1) { name = 'BitPay'; } diff --git a/src/js/services/shapeShiftApiService.js b/src/js/services/shapeShiftApiService.js index cc5fb0792..a90e587d4 100644 --- a/src/js/services/shapeShiftApiService.js +++ b/src/js/services/shapeShiftApiService.js @@ -24,7 +24,7 @@ var ShapeShift = (function() { var parsedResponse = JP(xmlhttp.responseText); cb.apply(null, [parsedResponse]); } else { - cb.apply(null, [new Error('Request Failed')]) + cb.apply(null, [new Error('Request Failed')]); } } }; diff --git a/src/js/services/wallet-history.service.js b/src/js/services/wallet-history.service.js index 512a2d8b1..f114a396d 100644 --- a/src/js/services/wallet-history.service.js +++ b/src/js/services/wallet-history.service.js @@ -6,9 +6,9 @@ .module('bitcoincom.services') .factory('walletHistoryService', walletHistoryService); - function walletHistoryService(configService, storageService, lodash, $log, txFormatService) { - //var PAGE_SIZE = 50; - var PAGE_SIZE = 20; // For dev only + function walletHistoryService(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); @@ -28,8 +28,8 @@ function addEarlyTransactions(walletId, cachedTxs, newTxs) { var cachedTxIndexFromId = {}; - cachedTxs.forEach(function forCachedTx(tx){ - cachedTxIndexFromId[tx.txid] = true; + cachedTxs.forEach(function forCachedTx(tx, txIndex){ + cachedTxIndexFromId[tx.txid] = txIndex; }); var confirmationsUpdated = false; @@ -47,10 +47,17 @@ } }); - var overlappingTxFraction = overlappingTxsCount / Math.min(cachedTxs.length, PAGE_OVERLAP); - console.log('overlappingTxFraction:', overlappingTxFraction); - - if (overlappingTxFraction >= MIN_KNOWN_TX_OVERLAP_FRACTION) { // We are good + var txsAreContinuous = false; + if (cachedTxs.length > 0) { + var overlappingTxFraction = overlappingTxsCount / Math.min(cachedTxs.length, PAGE_OVERLAP); + console.log('overlappingTxsCount:', overlappingTxsCount); + console.log('overlappingTxFraction:', overlappingTxFraction); + txsAreContinuous = overlappingTxFraction >= MIN_KNOWN_TX_OVERLAP_FRACTION; + } else { + txsAreContinuous = true; + } + + if (txsAreContinuous) { if (someTransactionsWereNew) { saveTxHistory(walletId, cachedTxs); } else if (confirmationsUpdated) { @@ -61,7 +68,7 @@ return cachedTxs; } else { // We might be missing some txs. - console.error('We might be missing some txs in the history.'); + $log.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 []; @@ -91,9 +98,17 @@ } }); - var overlappingTxFraction = overlappingTxsCount / Math.min(cachedTxs.length, PAGE_OVERLAP); + var txsAreContinuous = false; + if (cachedTxs.length > 0) { + var overlappingTxFraction = overlappingTxsCount / Math.min(cachedTxs.length, PAGE_OVERLAP); + console.log('overlappingTxsCount:', overlappingTxsCount); + console.log('overlappingTxFraction:', overlappingTxFraction); + txsAreContinuous = overlappingTxFraction >= MIN_KNOWN_TX_OVERLAP_FRACTION; + } else { + txsAreContinuous = true; + } - if (overlappingTxFraction >= MIN_KNOWN_TX_OVERLAP_FRACTION) { // We are good + if (txsAreContinuous) { if (someTransactionsWereNew) { var allTxs = uniqueNewTxs.concat(cachedTxs); saveTxHistory(walletId, allTxs); @@ -106,6 +121,7 @@ } } else { // We might be missing some txs. + $log.error('We might be missing some txs in the history.'); // Our history is wrong, so just include the latest ones saveTxHistory(walletId, newTxs); return newTxs; @@ -147,7 +163,6 @@ * @param {function(error, txs)} cb - txs is always an array, may be empty */ function getCachedTxHistory(walletId, cb) { - console.log('txhistory updateLocalTxHistoryByPage()'); storageService.getTxHistory(walletId, function onGetTxHistory(err, txHistoryString){ if (err) { return cb(err, []); @@ -230,7 +245,6 @@ } function updateLocalTxHistoryByPage(wallet, getLatest, flushCacheOnNew, cb) { - console.log('txhistory updaetLocalTxHistoryByPage()'); if (flushCacheOnNew) { fetchTxHistoryByPage(wallet, 0, function onFetchTxHistory(err, txs){ if (err) { diff --git a/src/js/services/wallet-history.service.spec.js b/src/js/services/wallet-history.service.spec.js new file mode 100644 index 000000000..a6d9e12a7 --- /dev/null +++ b/src/js/services/wallet-history.service.spec.js @@ -0,0 +1,471 @@ +fdescribe('walletHistoryService', function(){ + var history = []; + var historyStringFull; + var storageServiceMock; + var txFormatServiceMock; + var walletMock; + var walletHistoryService; + + + beforeEach(function(){ + module('ngLodash'); + module('bitcoincom.services'); + + storageServiceMock = jasmine.createSpyObj(['getTxHistory', 'removeTxHistory', 'setTxHistory']); + txFormatServiceMock = jasmine.createSpyObj(['processTx']); + txFormatServiceMock.processTx.and.callFake(function(coin, tx){ + return tx; + }); + walletMock = jasmine.createSpyObj(['getTxHistory']); + + module(function($provide) { + $provide.value('storageService', storageServiceMock); + $provide.value('txFormatService', txFormatServiceMock); + }); + + inject(function($injector){ + walletHistoryService = $injector.get('walletHistoryService'); + }); + + for(var i = 0; i < 100; i++) { + history.push({ + confirmations: i, + time: (Date.now() / 1000) - i, + txid: 'id' + i.toString() + }); + } + historyStringFull = JSON.stringify(history); + }); + + it('getCachedHistory empty', function() { + var returnedErr; + var returnedHistory; + var walletIdForStorageGet = ''; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + + cb(null, "[]"); + }); + + walletHistoryService.getCachedTxHistory('wallet1234', function(err, txHistory){ + returnedErr = err; + returnedHistory = txHistory; + }); + + expect(walletIdForStorageGet).toBe('wallet1234'); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(0); + }); + + it('getCachedHistory error from storage', function() { + var returnedErr; + var returnedHistory; + var walletIdForStorageGet = ''; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + + cb(new Error('Something went wrong.'), null); + }); + + walletHistoryService.getCachedTxHistory('wallet12345', function(err, txHistory){ + returnedErr = err; + returnedHistory = txHistory; + }); + + expect(walletIdForStorageGet).toBe('wallet12345'); + expect(returnedErr.message).toBe('Something went wrong.'); + expect(returnedHistory.length).toBe(0); + }); + + it('getCachedHistory page full', function() { + var returnedErr; + var returnedHistory; + var walletIdForStorageGet = ''; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + + cb(null, JSON.stringify(history.slice(0, 50))); + }); + + walletHistoryService.getCachedTxHistory('wallet1234', function(err, txHistory){ + returnedErr = err; + returnedHistory = txHistory; + }); + + expect(walletIdForStorageGet).toBe('wallet1234'); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(50); + }); + + it('updateLocalTxHistoryByPage, 2 in cache, getEarlier, keep cache, same 2 returned, so all transactions received.', function(){ + var fetchLimit; + var fetchSkip; + var returnedAllFetched; + var returnedErr; + var returnedHistory; + var walletIdForStorageGet; + walletMock.id = 'wallet456'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + + cb(null, JSON.stringify(history.slice(0, 2))); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(0, 2)); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, false, false, function(err, txs, allFetched){ + returnedErr = err; + returnedHistory = txs; + returnedAllFetched = allFetched; + }); + + expect(walletIdForStorageGet).toBe('wallet456'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(2); + expect(returnedAllFetched).toBe(true); + expect(storageServiceMock.setTxHistory.calls.any()).toBe(false); + }); + + it('updateLocalTxHistoryByPage, getEarlier, keep cache, sufficient overlap so saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedErr; + var returnedHistory; + var returnedAllFetched; + var savedTxs; + var walletIdForStorageGet; + var walletIdForStorageSet; + walletMock.id = 'wallet67890'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + + cb(null, JSON.stringify(history.slice(0, 40))); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(fetchSkip, fetchSkip + fetchLimit)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, false, false, function(err, txs, allFetched){ + returnedErr = err; + returnedHistory = txs; + returnedAllFetched = allFetched; + }); + + expect(walletIdForStorageGet).toBe('wallet67890'); + expect(walletIdForStorageSet).toBe('wallet67890'); + expect(fetchSkip).toBe(30); + expect(fetchLimit).toBe(50); + expect(savedTxs.length).toBe(80); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(80); + expect(returnedAllFetched).toBe(false); + }); + + it('updateLocalTxHistoryByPage, cache empty, getLatest, do not flush cache, one new so saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var savedTxs; + var walletIdForStorageGet; + var walletIdForStorageSet; + walletMock.id = 'wallet789'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + cb(null, "[]"); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(0, 1)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, false, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageGet).toBe('wallet789'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(savedTxs.length).toBe(1); + expect(walletIdForStorageSet).toBe('wallet789'); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(1); + }); + + it('updateLocalTxHistoryByPage, cache empty, getLatest, do not flush cache, some new so saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var savedTxs; + var walletIdForStorageGet; + var walletIdForStorageSet; + walletMock.id = 'wallet789'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + cb(null, "[]"); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(0, 10)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, false, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageGet).toBe('wallet789'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(savedTxs.length).toBe(10); + expect(walletIdForStorageSet).toBe('wallet789'); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(10); + }); + + it('updateLocalTxHistoryByPage, some cachedTx, getLatest, do not flush cache, nothing new so nothing added.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var walletIdForStorageGet; + walletMock.id = 'wallet789'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + cb(null, JSON.stringify(history.slice(0, 40))); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, []); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, false, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageGet).toBe('wallet789'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(40); + expect(storageServiceMock.setTxHistory.calls.any()).toBe(false); + }); + + it('updateLocalTxHistoryByPage, some cachedTx, getLatest, do not flush cache, confirmations increased, saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var savedTxs; + var walletIdForStorageGet; + var walletIdForStorageSet; + walletMock.id = 'wallet789'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + cb(null, JSON.stringify(history.slice(2, 52))); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + var historyWithHigherConfirmations = []; + for (var i = 0; i < 50; i++) { + historyWithHigherConfirmations.push({ + confirmations: i >= 6 ? history[i].confirmations : history[i].confirmations + 1, + time: history[i].time, + txid: history[i].txid + }); + } + + cb(null, historyWithHigherConfirmations.slice(fetchSkip, fetchSkip + fetchLimit)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, false, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageGet).toBe('wallet789'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(walletIdForStorageSet).toBe('wallet789'); + expect(savedTxs.length).toBe(52); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(52); + expect(returnedHistory[2].confirmations).toBe(3); + + }); + + it('updateLocalTxHistoryByPage, some cachedTx, getLatest, do not flush cache, some new with insufficient overlap, so only new saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var savedTxs; + var walletIdForStorageGet; + var walletIdForStorageSet; + walletMock.id = 'wallet789'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + cb(null, JSON.stringify(history.slice(48, 78))); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(fetchSkip, fetchSkip + fetchLimit)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, false, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageGet).toBe('wallet789'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(walletIdForStorageSet).toBe('wallet789'); + expect(savedTxs.length).toBe(50); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(50); + + }); + + it('updateLocalTxHistoryByPage, some cachedTx, getLatest, do not flush cache, some new with sufficient overlap so all saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var savedTxs; + var walletIdForStorageGet; + var walletIdForStorageSet; + walletMock.id = 'wallet789'; + + storageServiceMock.getTxHistory.and.callFake(function(walletId, cb){ + walletIdForStorageGet = walletId; + cb(null, JSON.stringify(history.slice(42, 72))); + }); + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(fetchSkip, fetchSkip + fetchLimit)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, false, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageGet).toBe('wallet789'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(walletIdForStorageSet).toBe('wallet789'); + expect(savedTxs.length).toBe(72); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(72); + + }); + + it('updateLocalTxHistoryByPage, getLatest, flush cache, some new so saved.', function(){ + var fetchLimit; + var fetchSkip; + var returnedHistory; + var savedTxs; + var walletIdForStorageSet; + walletMock.id = 'wallet7890'; + + walletMock.getTxHistory.and.callFake(function(opts, cb){ + fetchSkip = opts.skip; + fetchLimit = opts.limit; + + cb(null, history.slice(0, fetchLimit)); + }); + + storageServiceMock.setTxHistory.and.callFake(function(txs, walletId, cb){ + savedTxs = txs; + walletIdForStorageSet = walletId; + cb(null); + }); + + walletHistoryService.updateLocalTxHistoryByPage(walletMock, true, true, function(err, txs){ + returnedErr = err; + returnedHistory = txs; + }); + + expect(walletIdForStorageSet).toBe('wallet7890'); + expect(fetchSkip).toBe(0); + expect(fetchLimit).toBe(50); + expect(savedTxs.length).toBe(50); + expect(returnedErr).toBeNull(); + expect(returnedHistory.length).toBe(50); + expect(storageServiceMock.getTxHistory.calls.any()).toBe(false); + }); + +}); \ No newline at end of file