Merge branch 'wallet/sprint/21' into wallet/task/546

This commit is contained in:
Jean-Baptiste Dominguez 2018-09-26 15:35:00 +02:00 committed by GitHub
commit 7a78eb1261
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 3660 additions and 1001 deletions

View file

@ -1,5 +1,6 @@
ext {
ANDROID_SUPPORT_V4_VERSION = '26.1.0'
ANDROID_SUPPORT_ANNOTATIONS_VERSION = '26.1.0'
}
configurations.all {

View file

@ -19,6 +19,7 @@ var modules = [
'copayApp.controllers',
'copayApp.directives',
'copayApp.addons',
'bitcoincom.controllers',
'bitcoincom.directives',
'bitcoincom.services'
];
@ -30,5 +31,6 @@ angular.module('copayApp.services', []);
angular.module('copayApp.controllers', []);
angular.module('copayApp.directives', []);
angular.module('copayApp.addons', []);
angular.module('bitcoincom.controllers', []);
angular.module('bitcoincom.directives', []);
angular.module('bitcoincom.services', []);

View file

@ -1,16 +1,23 @@
'use strict';
angular.module('copayApp.controllers').controller('amountController', amountController);
(function(){
function amountController(configService, $filter, gettextCatalog, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, profileService, walletService, $window, ongoingProcess, popupService) {
angular
.module('bitcoincom.controllers')
.controller('amountController', amountController);
function amountController(configService, $filter, gettextCatalog, $ionicHistory, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, ongoingProcess, popupService, profileService, walletService, $window) {
var vm = this;
// Variables
vm.allowSend = false;
vm.altCurrencyList = [];
vm.alternativeAmount = '';
vm.alternativeUnit = '';
vm.amount = '0';
vm.availableFunds = '';
vm.canSendAllAvailableFunds = true;
vm.errorMessage = '';
// Use insufficient for logic, as when the amount is invalid, funds being
// either sufficent or insufficient doesn't make sense.
vm.fundsAreInsufficient = false;
@ -20,9 +27,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
vm.lastUsedPopularList = [];
vm.maxAmount = 0;
vm.minAmount = 0;
vm.sendableFunds = '';
vm.showSendMaxButton = false;
vm.showSendLimitMaxButton = false;
vm.thirdParty = false;
vm.unit = '';
// Functions
vm.changeUnit = changeUnit;
vm.close = close;
vm.findCurrency = findCurrency;
@ -35,7 +46,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
vm.removeDigit = removeDigit;
vm.save = save;
vm.sendMax = sendMax;
vm.errorMessage = '';
$scope.$on('$ionicView.beforeEnter', onBeforeEnter);
$scope.$on('$ionicView.leave', onLeave);
@ -46,10 +57,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
var altCurrencyModal = null;
var altUnitIndex = 0;
var availableFundsInCrypto = '';
var availableFundsInFiat = '';
var availableSatoshis = null;
var availableUnits = [];
var canSendMax = true;
var fiatCode;
var isNW = platformInfo.isNW;
var isAndroid = platformInfo.isAndroid;
@ -57,10 +66,18 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
var lastUsedAltCurrencyList = [];
var passthroughParams = {};
var satToUnit;
var transactionSendableAmount = {
crypto: '',
satoshis: null
};
var unitDecimals;
var unitIndex = 0;
var unitToSatoshi;
var useSendMax = false;
var walletSpendableAmount = {
crypto: '',
satoshis: null
};
function onLeave() {
angular.element($window).off('keydown');
@ -81,40 +98,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
vm.minAmount = parseFloat(passthroughParams.minAmount);
vm.maxAmount = parseFloat(passthroughParams.maxAmount);
if (passthroughParams.thirdParty) {
vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object
if (vm.thirdParty) {
if (vm.thirdParty.id === 'shapeshift') {
if (!vm.thirdParty.data) {
vm.thirdParty.data = {};
}
vm.thirdParty.data['fromWalletId'] = vm.fromWalletId;
vm.fromWallet = profileService.getWallet(vm.fromWalletId);
vm.toWallet = profileService.getWallet(vm.toWalletId);
ongoingProcess.set('connectingShapeshift', true);
shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(err, data) {
if (err) {
// Error stop here
ongoingProcess.set('connectingShapeshift', false);
popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.toString(), function () {
$ionicHistory.goBack();
});
} else {
vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum);
vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit);
ongoingProcess.set('connectingShapeshift', false);
}
});
}
}
}
vm.isRequestingSpecificAmount = !passthroughParams.fromWalletId;
vm.showSendMaxButton = !vm.isRequestingSpecificAmount;
var config = configService.getSync().wallet.settings;
unitToSatoshi = config.unitToSatoshi;
satToUnit = 1 / unitToSatoshi;
unitDecimals = config.unitDecimals;
setAvailableUnits();
updateUnitUI();
@ -146,10 +136,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
});
}
unitToSatoshi = config.unitToSatoshi;
satToUnit = 1 / unitToSatoshi;
unitDecimals = config.unitDecimals;
resetAmount();
processAmount();
@ -221,6 +208,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
var fromWallet = profileService.getWallet(passthroughParams.fromWalletId);
updateAvailableFundsFromWallet(fromWallet);
}
if (passthroughParams.thirdParty) {
vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object
if (vm.thirdParty) {
initShapeshift();
}
}
}
}
@ -228,6 +222,34 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
sendFlowService.router.goBack();
}
function initShapeshift() {
if (vm.thirdParty.id === 'shapeshift') {
vm.thirdParty.data = vm.thirdParty.data || {};
vm.fromWallet = profileService.getWallet(vm.fromWalletId);
vm.toWallet = profileService.getWallet(vm.toWalletId);
vm.showSendMaxButton = false;
vm.showSendLimitMaxButton = false;
vm.canSendAllAvailableFunds = false;
ongoingProcess.set('connectingShapeshift', true);
shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function onMarketData(err, data) {
ongoingProcess.set('connectingShapeshift', false);
if (err) {
// Error stop here
popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.message, function () {
goBack();
});
} else {
vm.thirdParty.data.minAmount = vm.minAmount = parseFloat(data.minimum);
vm.thirdParty.data.maxAmount = vm.maxAmount = parseFloat(data.maxLimit);
setMaximumButtonFromWallet(vm.fromWallet);
}
});
}
}
function paste(value) {
vm.amount = value;
processAmount();
@ -243,8 +265,28 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
}
function sendMax() {
useSendMax = true;
finish();
if (canSendMax) {
useSendMax = true;
finish();
} else {
var transactionSendableAmountInUnits = transactionSendableAmount.satoshis * satToUnit;
if (vm.minAmount && transactionSendableAmountInUnits < vm.minAmount) {
popupService.showAlert(
gettextCatalog.getString('Insufficient funds'),
gettextCatalog.getString('Amount below minimum allowed')
);
} else {
// Need to be precise, so use crypto directly rather than fiat with exchange rate
if (availableUnits[unitIndex].isFiat) {
var tempIndex = altUnitIndex;
altUnitIndex = unitIndex;
unitIndex = tempIndex;
}
vm.amount = transactionSendableAmountInUnits.toFixed(LENGTH_AFTER_COMMA_EXPRESSION_LIMIT);
useSendMax = true;
finish();
}
}
}
function updateUnitUI() {
@ -364,8 +406,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
amountInCrypto = a;
var amountInSatoshis = a * unitToSatoshi;
vm.fundsAreInsufficient = !!passthroughParams.fromWalletId
&& availableSatoshis !== null
&& availableSatoshis < amountInSatoshis;
&& walletSpendableAmount.satoshis !== null
&& walletSpendableAmount.satoshis < amountInSatoshis;
vm.alternativeAmount = txFormatService.formatAmount(amountInSatoshis, true);
vm.allowSend = lodash.isNumber(a)
@ -385,8 +427,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
} else {
amountInCrypto = result;
vm.fundsAreInsufficient = passthroughParams.fromWalletId
&& availableSatoshis !== null
&& availableSatoshis < result * unitToSatoshi;
&& walletSpendableAmount.satoshis !== null
&& walletSpendableAmount.satoshis < result * unitToSatoshi;
vm.alternativeAmount = $filter('formatFiatAmount')(toFiat(result));
vm.allowSend = lodash.isNumber(result)
@ -460,13 +502,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
var satoshis = 0;
if (unit.isFiat) {
satoshis = (fromFiat(uiAmount) * unitToSatoshi).toFixed(0);
satoshis = Math.floor(fromFiat(uiAmount) * unitToSatoshi);
} else {
satoshis = (uiAmount * unitToSatoshi).toFixed(0);
satoshis = Math.floor(uiAmount * unitToSatoshi);
}
var confirmData = {
amount: useSendMax ? undefined : satoshis,
amount: (useSendMax && canSendMax) ? undefined : satoshis,
displayAddress: passthroughParams.displayAddress,
fromWalletId: passthroughParams.fromWalletId,
sendMax: useSendMax,
@ -482,7 +524,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
$state.transitionTo('tabs.paymentRequest.confirm', confirmData);
} else {
sendFlowService.goNext(confirmData);
$scope.useSendMax = null;
useSendMax = false;
}
}
@ -600,18 +642,73 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
}
function updateAvailableFundsStringIfNeeded() {
if (passthroughParams.fromWalletId && availableSatoshis !== null) {
availableFundsInFiat = '';
vm.availableFunds = availableFundsInCrypto;
if (passthroughParams.fromWalletId && walletSpendableAmount.satoshis !== null) {
vm.availableFunds = walletSpendableAmount.crypto;
if (availableUnits[unitIndex].isFiat) {
var coin = availableUnits[altUnitIndex].id;
txFormatService.formatAlternativeStr(coin, availableSatoshis, function formatCallback(formatted){
if (formatted) {
availableFundsInFiat = formatted;
txFormatService.formatAlternativeStr(coin, walletSpendableAmount.satoshis, function formatCallback(formatted){
if (formatted) {
$scope.$apply(function() {
vm.availableFunds = availableFundsInFiat;
vm.availableFunds = formatted;
});
}
});
}
updateMaximumButtonIfNeeded();
}
}
function updateAvailableFundsFromWallet(wallet) {
console.log('amount updateAvailableFundsFromWallet()');
var availableFundsInFiat = '';
if (wallet.status && wallet.status.isValid) {
walletSpendableAmount.crypto = wallet.status.spendableBalanceStr;
walletSpendableAmount.satoshis = wallet.status.spendableAmount;
if (wallet.status.alternativeBalanceAvailable) {
availableFundsInFiat = wallet.status.spendableBalanceAlternative + ' ' + wallet.status.alternativeIsoCode;
} else {
availableFundsInFiat = '';
}
} else if (wallet.cachedStatus && wallet.cachedStatus.isValid) {
if (wallet.cachedStatus.alternativeBalanceAvailable) {
availableFundsInFiat = wallet.cachedStatus.spendableBalanceAlternative + ' ' + wallet.cachedStatus.alternativeIsoCode;
} else {
availableFundsInFiat = '';
}
walletSpendableAmount.crypto = wallet.cachedStatus.spendableBalanceStr;
walletSpendableAmount.satoshis = wallet.cachedStatus.spendableAmount;
} else {
walletSpendableAmount.crypto = '';
walletSpendableAmount.satoshis = null;
}
if (availableUnits[unitIndex].isFiat) {
vm.availableFunds = availableFundsInFiat || walletSpendableAmount.crypto;
} else {
vm.availableFunds = walletSpendableAmount.crypto;
}
setMaximumButtonFromWallet(wallet);
}
function updateMaximumButtonIfNeeded() {
console.log('sendmax updateMaximumButtonIfNeeded()');
if (vm.showSendMaxButton || vm.showSendLimitMaxButton) {
transactionSendableAmount.fiat = '';
vm.sendableFunds = transactionSendableAmount.crypto;
if (availableUnits[unitIndex].isFiat) {
var coin = availableUnits[altUnitIndex].id;
txFormatService.formatAlternativeStr(coin, transactionSendableAmount.satoshis, function formatCallback(formatted){
if (formatted) {
$scope.$apply(function onApply() {
vm.sendableFunds = formatted;
});
}
});
@ -619,37 +716,59 @@ function amountController(configService, $filter, gettextCatalog, $ionicModal, $
}
}
function updateAvailableFundsFromWallet(wallet) {
if (wallet.status && wallet.status.isValid) {
availableFundsInCrypto = wallet.status.spendableBalanceStr;
availableSatoshis = wallet.status.spendableAmount;
if (wallet.status.alternativeBalanceAvailable) {
availableFundsInFiat = wallet.status.spendableBalanceAlternative + ' ' + wallet.status.alternativeIsoCode;
function setMaximumButtonFromWallet(wallet) {
console.log('sendmax setMaximumButtonFromWallet()');
var minSatoshis = vm.minAmount * unitToSatoshi;
var maxSatoshis = vm.maxAmount * unitToSatoshi;
if (minSatoshis > walletSpendableAmount.satoshis) {
console.log('sendmax Hiding max buttons as minimum is too high.');
canSendMax = false;
vm.showSendMaxButton = true;
vm.showSendLimitMaxButton = false;
transactionSendableAmount.satoshis = walletSpendableAmount.satoshis;
} else if (maxSatoshis) {
if (walletSpendableAmount.satoshis > maxSatoshis) {
console.log('sendmax Showing max limit button as available is greater than max limit.');
canSendMax = false;
vm.showSendMaxButton = false;
vm.showSendLimitMaxButton = true;
transactionSendableAmount.satoshis = maxSatoshis;
} else {
availableFundsInFiat = '';
console.log('sendmax Showing sendmax as all available as less than max limit.');
// Enabling send max here is a little dangerous, if they receive funds between pressing
// this and the calculation in the Review screen.
canSendMax = false;
vm.showSendMaxButton = true;
vm.showSendLimitMaxButton = false;
transactionSendableAmount.satoshis = walletSpendableAmount.satoshis;
}
} else if (wallet.cachedStatus && wallet.status.isValid) {
} else {
console.log('sendmax Showing sendmax as all available because no limits.');
canSendMax = true;
vm.showSendMaxButton = true;
vm.showSendLimitMaxButton = false;
transactionSendableAmount.satoshis = walletSpendableAmount.satoshis;
}
if (wallet.cachedStatus.alternativeBalanceAvailable) {
availableFundsInFiat = wallet.cachedStatus.spendableBalanceAlternative + ' ' + wallet.cachedStatus.alternativeIsoCode;
} else {
availableFundsInFiat = '';
if (vm.showSendMaxButton || vm.showSendLimitMaxButton) {
console.log('sendmax Setting max button text');
transactionSendableAmount.crypto = txFormatService.formatAmountStr(wallet.coin, transactionSendableAmount.satoshis);
vm.sendableFunds = transactionSendableAmount.crypto;
if (availableUnits[unitIndex].isFiat) {
txFormatService.formatAlternativeStr(wallet.coin, transactionSendableAmount.satoshis, function onFormat(formatted){
if (formatted) {
$scope.$apply(function onApply() {
vm.sendableFunds = formatted;
});
}
});
}
availableFundsInCrypto = wallet.cachedStatus.spendableBalanceStr;
availableSatoshis = wallet.cachedStatus.spendableAmount;
} else {
availableFundsInFiat = '';
availableFundsInCrypto = '';
availableSatoshis = null;
}
if (availableUnits[unitIndex].isFiat) {
vm.availableFunds = availableFundsInFiat || availableFundsInCrypto;
} else {
vm.availableFunds = availableFundsInCrypto;
}
}
}
})();

View file

@ -1,14 +1,20 @@
describe('amountController', function(){
var configCache,
configService,
configService,
gettextCatalog,
$controller,
$ionicHistory,
$rootScope,
ongoingProcess,
platformInfo,
popupService,
profileService,
rateService,
sendFlowService,
shapeshiftService,
txFormatService,
$scope,
$state,
$stateParams;
@ -20,7 +26,7 @@ describe('amountController', function(){
configCache = {
wallet: {
settings: {
unitToSatoshi: 100000000
}
}
};
@ -33,20 +39,42 @@ describe('amountController', function(){
});
configService.getSync.and.returnValue(configCache);
gettextCatalog = jasmine.createSpyObj(['getString']);
gettextCatalog.getString.and.callFake(function(str){ return str; });
$ionicHistory = jasmine.createSpyObj(['backView']);
ongoingProcess = jasmine.createSpyObj(['set']);
platformInfo = {
isChromeApp: false,
isAndroid: false,
isIos: true
};
popupService = jasmine.createSpyObj(['showAlert']);
profileService = jasmine.createSpyObj(['getWallet', 'getWallets']);
rateService = jasmine.createSpyObj(['fromFiat', 'whenAvailable']);
sendFlowService = jasmine.createSpyObj(['getStateClone']);
shapeshiftService = jasmine.createSpyObj(['shiftIt']);
rateService = jasmine.createSpyObj(['fromFiat', 'listAlternatives', 'updateRates', 'whenAvailable']);
sendFlowService = jasmine.createSpyObj(['getStateClone', 'pushState']);
shapeshiftService = jasmine.createSpyObj(['getMarketData']);
txFormatService = jasmine.createSpyObj(['formatAlternativeStr', 'formatAmountStr']);
txFormatService.formatAlternativeStr.and.callFake(function(coin, satoshis, cb) {
if (typeof satoshis !== "number") {
throw "satoshis in formatAlternativeStr() is not a number."
}
var units = satoshis / 100000000;
var formatted = (units * 10000).toFixed(2) + ' USD';
cb(formatted);
});
txFormatService.formatAmountStr.and.callFake(function(coin, satoshis) {
if (typeof satoshis !== "number") {
throw "satoshis in formatAmountStr() is not a number."
}
return (satoshis * 100000000).toFixed(8) + ' ' + (coin || 'bch').toUpperCase();
});
$state = jasmine.createSpyObj(['transitionTo']);
$stateParams = {};
inject(function(_$controller_, _$rootScope_){
@ -67,7 +95,10 @@ describe('amountController', function(){
$ionicHistory.backView.and.returnValue(backView);
var wallet = {
status: {
isValid: true,
spendableAmount: 123456
}
};
profileService.getWallet.and.returnValue(wallet);
profileService.getWallets.and.returnValue([{}]);
@ -78,22 +109,22 @@ describe('amountController', function(){
var amountController = $controller('amountController', {
configService: configService,
gettextCatalog: {},
gettextCatalog: gettextCatalog,
$ionicHistory: $ionicHistory,
$ionicModal: {},
$ionicScrollDelegate: {},
nodeWebkitService: {},
ongoingProcess: {},
ongoingProcess: ongoingProcess,
platformInfo: platformInfo,
profileService: profileService,
popupService: {},
popupService: popupService,
rateService: rateService,
$scope: $scope,
sendFlowService: sendFlowService,
shapeshiftService: shapeshiftService,
$state: {},
$stateParams: $stateParams,
txFormatService: {},
txFormatService: txFormatService,
walletService: {}
});
@ -110,4 +141,464 @@ describe('amountController', function(){
//expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s');
});
describe('Shapeshift', function() {
var walletFrom;
var walletTo;
beforeEach(function(){
walletFrom = {};
walletTo = {};
profileService.getWallet.and.callFake(function(walletId){
if (walletId === '4cd7673e-7320-4dfa-86e5-d4edb51d460a') {
return walletFrom;
} else if (walletId === 'bf00af8f-0788-4b57-b30a-0390747407e9') {
return walletTo;
} else {
return null;
}
});
rateService.listAlternatives.and.returnValue([
{name: "Australian Dollar", isoCode: "AUD"},
{name: "United States Dollar", isoCode: "USD"}
]);
});
it ('with available balance below limit, shows sendMax for triggering alert', function() {
walletFrom.coin = 'btc';
walletFrom.status = {
isValid: true,
spendableAmount: 789
};
walletTo.coin = 'bch';
profileService.getWallets.and.returnValue([{}]);
rateService.fromFiat.and.returnValue(12);
var $scope = $rootScope.$new();
var amountController = $controller('amountController', {
configService: configService,
gettextCatalog: gettextCatalog,
$ionicHistory: $ionicHistory,
$ionicModal: {},
$ionicScrollDelegate: {},
nodeWebkitService: {},
ongoingProcess: ongoingProcess,
platformInfo: platformInfo,
profileService: profileService,
popupService: popupService,
rateService: rateService,
$scope: $scope,
sendFlowService: sendFlowService,
shapeshiftService: shapeshiftService,
$state: $state,
$stateParams: $stateParams,
txFormatService: txFormatService,
walletService: {}
});
rateService.whenAvailable.and.callFake(function(cb){
cb();
});
var sendFlowState = {
amount: '',
displayAddress: null,
fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a',
sendMax: false,
thirdParty: {
id: 'shapeshift',
data: {},
},
toAddress: '',
toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9'
};
sendFlowService.getStateClone.and.returnValue(sendFlowState);
var reqCoinIn = '';
var reqCoinOut = '';
shapeshiftService.getMarketData.and.callFake(function(coinIn, coinOut, cb){
reqCoinIn = coinIn;
reqCoinOut = coinOut;
cb({
maxLimit: '0.6846239',
minimum: '0.00013692'
});
});
$scope.$emit('$ionicView.beforeEnter', {});
expect(rateService.updateRates.calls.any()).toEqual(true);
expect(reqCoinIn).toBe('btc');
expect(reqCoinOut).toBe('bch');
expect(amountController.maxAmount).toBe(0.68462390);
expect(amountController.minAmount).toBe(0.00013692);
expect(amountController.showSendMaxButton).toEqual(true);
expect(amountController.showSendLimitMaxButton).toEqual(false);
expect(amountController.sendableFunds).toEqual('0.08 USD');
// Now hit the Send Max button
amountController.sendMax();
expect(popupService.showAlert.calls.argsFor(0)[0]).toEqual('Insufficient funds');
expect(popupService.showAlert.calls.argsFor(0)[1]).toEqual('Amount below minimum allowed');
expect(sendFlowService.pushState.calls.any()).toEqual(false);
expect($state.transitionTo.calls.any()).toEqual(false);
});
it ('with available balance between limits, uses sendMax', function() {
walletFrom.coin = 'btc';
walletFrom.status = {
isValid: true,
spendableAmount: 456789
};
walletTo.coin = 'bch';
profileService.getWallets.and.returnValue([{}]);
rateService.fromFiat.and.returnValue(12);
var $scope = $rootScope.$new();
var amountController = $controller('amountController', {
configService: configService,
gettextCatalog: {},
$ionicHistory: $ionicHistory,
$ionicModal: {},
$ionicScrollDelegate: {},
nodeWebkitService: {},
ongoingProcess: ongoingProcess,
platformInfo: platformInfo,
profileService: profileService,
popupService: {},
rateService: rateService,
$scope: $scope,
sendFlowService: sendFlowService,
shapeshiftService: shapeshiftService,
$state: $state,
$stateParams: $stateParams,
txFormatService: txFormatService,
walletService: {}
});
rateService.whenAvailable.and.callFake(function(cb){
cb();
});
var sendFlowState = {
amount: '',
displayAddress: null,
fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a',
sendMax: false,
thirdParty: {
id: 'shapeshift',
data: {},
},
toAddress: '',
toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9'
};
sendFlowService.getStateClone.and.returnValue(sendFlowState);
var reqCoinIn = '';
var reqCoinOut = '';
shapeshiftService.getMarketData.and.callFake(function(coinIn, coinOut, cb){
reqCoinIn = coinIn;
reqCoinOut = coinOut;
cb({
maxLimit: '0.6846239',
minimum: '0.00013692'
});
});
$scope.$emit('$ionicView.beforeEnter', {});
expect(rateService.updateRates.calls.any()).toEqual(true);
expect(reqCoinIn).toBe('btc');
expect(reqCoinOut).toBe('bch');
expect(amountController.maxAmount).toBe(0.68462390);
expect(amountController.minAmount).toBe(0.00013692);
expect(amountController.showSendMaxButton).toEqual(true);
expect(amountController.showSendLimitMaxButton).toEqual(false);
// Now hit the Send Max button
var pushedState = null;
sendFlowService.pushState.and.callFake(function (sendFlowState){
pushedState = sendFlowState;
});
amountController.sendMax();
expect(pushedState.amount).toBeUndefined();
expect(pushedState.fromWalletId).toEqual('4cd7673e-7320-4dfa-86e5-d4edb51d460a');
expect(pushedState.sendMax).toEqual(true);
expect(pushedState.toWalletId).toEqual('bf00af8f-0788-4b57-b30a-0390747407e9');
expect(pushedState.thirdParty.id).toEqual('shapeshift');
expect(pushedState.thirdParty.data.maxAmount).toEqual(0.6846239);
expect(pushedState.thirdParty.data.minAmount).toEqual(0.00013692);
expect($state.transitionTo.calls.count()).toEqual(1);
expect($state.transitionTo.calls.argsFor(0)[0]).toEqual('tabs.send.review');
});
it ('with available balance higher than max, uses send limit max instead of sendMax', function() {
walletFrom.coin = 'btc';
walletFrom.status = {
isValid: true,
spendableAmount: 123456789
};
walletTo.coin = 'bch';
profileService.getWallets.and.returnValue([{}]);
rateService.fromFiat.and.returnValue(12); // satoshis or coins?
var $scope = $rootScope.$new();
var amountController = $controller('amountController', {
configService: configService,
gettextCatalog: {},
$ionicHistory: $ionicHistory,
$ionicModal: {},
$ionicScrollDelegate: {},
nodeWebkitService: {},
ongoingProcess: ongoingProcess,
platformInfo: platformInfo,
profileService: profileService,
popupService: {},
rateService: rateService,
$scope: $scope,
sendFlowService: sendFlowService,
shapeshiftService: shapeshiftService,
$state: $state,
$stateParams: $stateParams,
txFormatService: txFormatService,
walletService: {}
});
rateService.whenAvailable.and.callFake(function(cb){
cb();
});
var sendFlowState = {
amount: '',
displayAddress: null,
fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a',
sendMax: false,
thirdParty: {
id: 'shapeshift',
data: {},
},
toAddress: '',
toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9'
};
sendFlowService.getStateClone.and.returnValue(sendFlowState);
var reqCoinIn = '';
var reqCoinOut = '';
shapeshiftService.getMarketData.and.callFake(function(coinIn, coinOut, cb){
reqCoinIn = coinIn;
reqCoinOut = coinOut;
cb({
maxLimit: '0.6846239',
minimum: '0.00013692'
});
});
$scope.$emit('$ionicView.beforeEnter', {});
expect(rateService.updateRates.calls.any()).toEqual(true);
expect(reqCoinIn).toBe('btc');
expect(reqCoinOut).toBe('bch');
expect(amountController.maxAmount).toBe(0.6846239);
expect(amountController.minAmount).toBe(0.00013692);
expect(amountController.showSendMaxButton).toEqual(false);
expect(amountController.showSendLimitMaxButton).toEqual(true);
// Now hit the Send Max button
var pushedState = null;
sendFlowService.pushState.and.callFake(function (sendFlowState){
pushedState = sendFlowState;
});
amountController.sendMax();
expect(pushedState.amount).toEqual(68462390);
expect(pushedState.fromWalletId).toEqual('4cd7673e-7320-4dfa-86e5-d4edb51d460a');
expect(pushedState.sendMax).toEqual(false);
expect(pushedState.toWalletId).toEqual('bf00af8f-0788-4b57-b30a-0390747407e9');
expect(pushedState.thirdParty.id).toEqual('shapeshift');
expect(pushedState.thirdParty.data.maxAmount).toEqual(0.6846239);
expect(pushedState.thirdParty.data.minAmount).toEqual(0.00013692);
expect($state.transitionTo.calls.count()).toEqual(1);
expect($state.transitionTo.calls.argsFor(0)[0]).toEqual('tabs.send.review');
});
});
describe('Wallet transfer', function() {
var walletFrom;
var walletTo;
beforeEach(function(){
walletFrom = {};
walletTo = {};
profileService.getWallet.and.callFake(function(walletId){
if (walletId === '4cd7673e-7320-4dfa-86e5-d4edb51d460a') {
return walletFrom;
} else if (walletId === 'bf00af8f-0788-4b57-b30a-0390747407e9') {
return walletTo;
} else {
return null;
}
});
rateService.listAlternatives.and.returnValue([
{name: "Australian Dollar", isoCode: "AUD"},
{name: "United States Dollar", isoCode: "USD"}
]);
});
it('wallet transfer send max.', function() {
walletFrom.coin = 'btc';
walletFrom.status = {
isValid: true,
spendableAmount: 123456789
};
profileService.getWallets.and.returnValue([{}]);
var $scope = $rootScope.$new();
var amountController = $controller('amountController', {
configService: configService,
gettextCatalog: gettextCatalog,
$ionicHistory: $ionicHistory,
$ionicModal: {},
$ionicScrollDelegate: {},
nodeWebkitService: {},
ongoingProcess: ongoingProcess,
platformInfo: platformInfo,
profileService: profileService,
popupService: popupService,
rateService: rateService,
$scope: $scope,
sendFlowService: sendFlowService,
shapeshiftService: shapeshiftService,
$state: $state,
$stateParams: $stateParams,
txFormatService: txFormatService,
walletService: {}
});
var sendFlowState = {
fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a',
toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9'
};
sendFlowService.getStateClone.and.returnValue(sendFlowState);
$scope.$emit('$ionicView.beforeEnter', {});
expect(amountController.showSendMaxButton).toEqual(true);
expect(amountController.showSendLimitMaxButton).toEqual(false);
expect(amountController.sendableFunds).toEqual('12345.68 USD');
// Now hit the Send Max button
var pushedState = null;
sendFlowService.pushState.and.callFake(function (sendFlowState){
pushedState = sendFlowState;
});
amountController.sendMax();
expect(pushedState.amount).toBeUndefined();
expect(pushedState.fromWalletId).toEqual('4cd7673e-7320-4dfa-86e5-d4edb51d460a');
expect(pushedState.sendMax).toEqual(true);
expect(pushedState.toWalletId).toEqual('bf00af8f-0788-4b57-b30a-0390747407e9');
expect($state.transitionTo.calls.count()).toEqual(1);
expect($state.transitionTo.calls.argsFor(0)[0]).toEqual('tabs.send.review');
});
// This situation was seen in real life
it('wallet transfer with valid cached status only.', function() {
walletFrom.coin = 'btc';
walletFrom.status = {
isValid: false,
};
walletFrom.cachedStatus = {
isValid: true,
spendableAmount: 5678
};
profileService.getWallets.and.returnValue([{}]);
var $scope = $rootScope.$new();
var amountController = $controller('amountController', {
configService: configService,
gettextCatalog: gettextCatalog,
$ionicHistory: $ionicHistory,
$ionicModal: {},
$ionicScrollDelegate: {},
nodeWebkitService: {},
ongoingProcess: ongoingProcess,
platformInfo: platformInfo,
profileService: profileService,
popupService: popupService,
rateService: rateService,
$scope: $scope,
sendFlowService: sendFlowService,
shapeshiftService: shapeshiftService,
$state: $state,
$stateParams: $stateParams,
txFormatService: txFormatService,
walletService: {}
});
var sendFlowState = {
fromWalletId: '4cd7673e-7320-4dfa-86e5-d4edb51d460a',
toWalletId: 'bf00af8f-0788-4b57-b30a-0390747407e9'
};
sendFlowService.getStateClone.and.returnValue(sendFlowState);
$scope.$emit('$ionicView.beforeEnter', {});
expect(amountController.showSendMaxButton).toEqual(true);
expect(amountController.showSendLimitMaxButton).toEqual(false);
expect(amountController.sendableFunds).toEqual('0.57 USD');
});
});
});

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tourController',
function($scope, $state, $log, $timeout, $filter, ongoingProcess, profileService, rateService, popupService, gettextCatalog, startupService, storageService, walletService, $q) {
function ($scope, $state, $log, $timeout, $filter, ongoingProcess, configService, profileService, rateService, popupService, gettextCatalog, lodash, startupService, storageService, uxLanguage, walletService, $q) {
$scope.data = {
index: 0
@ -46,62 +46,90 @@ angular.module('copayApp.controllers').controller('tourController',
creatingWallet = true;
ongoingProcess.set('creatingWallet', true);
$timeout(function() {
profileService.createDefaultWallet(function(err, walletClients) {
if (err) {
$log.warn(err);
uxLanguage.init(function(lang) {
var rateCode = uxLanguage.getRateCode(lang);
console.log("When Available: rateService");
rateService.whenAvailable(function() {
var alternatives = rateService.listAlternatives(true);
return $timeout(function() {
$log.warn('Retrying to create default wallet.....:' + ++retryCount);
if (retryCount > 3) {
ongoingProcess.set('creatingWallet', false);
popupService.showAlert(
gettextCatalog.getString('Cannot Create Wallet'), err,
function() {
retryCount = 0;
return $scope.createDefaultWallet();
}, gettextCatalog.getString('Retry'));
} else {
return $scope.createDefaultWallet();
}
}, 2000);
};
ongoingProcess.set('creatingWallet', false);
var bchWallet = walletClients[0];
var btcWallet = walletClients[1];
var bchWalletId = bchWallet.credentials.walletId;
var btcWalletId = btcWallet.credentials.walletId;
function createAddressPromise(wallet) {
return $q(function(resolve, reject) {
walletService.getAddress(wallet, true, function(e, addr) {
if (e) reject(e);
resolve(addr);
var newAltCurrency = lodash.find(alternatives, {
'isoCode': rateCode
});
configService.whenAvailable(function(config) {
var opts = {
wallet: {
settings: {
alternativeName: newAltCurrency.name,
alternativeIsoCode: newAltCurrency.isoCode,
}
}
};
configService.set(opts, function(err) {
if (err) $log.warn(err);
profileService.createDefaultWallet(function(err, walletClients) {
if (err) {
$log.warn(err);
return $timeout(function() {
$log.warn('Retrying to create default wallet.....:' + ++retryCount);
if (retryCount > 3) {
ongoingProcess.set('creatingWallet', false);
popupService.showAlert(
gettextCatalog.getString('Cannot Create Wallet'), err,
function() {
retryCount = 0;
return $scope.createDefaultWallet();
}, gettextCatalog.getString('Retry'));
} else {
return $scope.createDefaultWallet();
}
}, 2000);
}
;
ongoingProcess.set('creatingWallet', false);
var bchWallet = walletClients[0];
var btcWallet = walletClients[1];
var bchWalletId = bchWallet.credentials.walletId;
var btcWalletId = btcWallet.credentials.walletId;
function createAddressPromise(wallet) {
return $q(function (resolve, reject) {
walletService.getAddress(wallet, true, function (e, addr) {
if (e) reject(e);
resolve(addr);
});
});
}
function goToCollectEmail() {
$state.go('onboarding.collectEmail', {
bchWalletId: bchWalletId,
btcWalletId: btcWalletId
});
}
var bchAddressPromise = createAddressPromise(bchWallet);
var btcAddressPromise = createAddressPromise(btcWallet);
ongoingProcess.set('generatingNewAddress', true);
$q.all([bchAddressPromise, btcAddressPromise]).then(function (addresses) {
ongoingProcess.set('generatingNewAddress', false);
$state.go('tabs.home');
}, function (e) {
ongoingProcess.set('generatingNewAddress', false);
$log.warn(e);
popupService.showAlert(gettextCatalog.getString('Error'), e);
$state.go('tabs.home');
});
});
});
});
$log.debug('Setting default currency : ' + newAltCurrency);
});
}
function goToCollectEmail() {
$state.go('onboarding.collectEmail', {
bchWalletId: bchWalletId,
btcWalletId: btcWalletId
});
}
var bchAddressPromise = createAddressPromise(bchWallet);
var btcAddressPromise = createAddressPromise(btcWallet);
ongoingProcess.set('generatingNewAddress', true);
$q.all([bchAddressPromise, btcAddressPromise]).then(function(addresses) {
ongoingProcess.set('generatingNewAddress', false);
$state.go('tabs.home');
}, function(e) {
ongoingProcess.set('generatingNewAddress', false);
$log.warn(e);
popupService.showAlert(gettextCatalog.getString('Error'), e);
$state.go('tabs.home');
});
});
}, 300);
};
});
})
}, 300);
};
});

View file

@ -43,21 +43,20 @@ angular.module('copayApp.controllers').controller('tabHomeController',
});
}
if ($scope.isNW) {
latestReleaseService.checkLatestRelease(function(err, newRelease) {
if (err) {
$log.warn(err);
return;
}
if (newRelease) {
$scope.newRelease = true;
$scope.updateText = gettextCatalog.getString('There is a new version of {{appName}} available', {
appName: $scope.name
});
}
});
}
};
latestReleaseService.checkLatestRelease(function(err, newReleaseData) {
if (err) {
$log.warn(err);
return;
}
if (newReleaseData) {
$scope.newRelease = true;
$scope.newReleaseText = gettextCatalog.getString('There is a new version of {{appName}} available', {
appName: $scope.name
});
$scope.newReleaseNotes = newReleaseData.releaseNotes;
}
});
}
function onEnter(event, data) {
$ionicNavBarDelegate.showBar(true);
@ -109,13 +108,13 @@ angular.module('copayApp.controllers').controller('tabHomeController',
$scope.$apply();
}, 10);
});
};
}
function onLeave (event, data) {
lodash.each(listeners, function(x) {
x();
});
};
}
$scope.createdWithinPastDay = function(time) {
return timeService.withinPastDay(time);
@ -125,14 +124,8 @@ angular.module('copayApp.controllers').controller('tabHomeController',
sendFlowService.start();
}
$scope.openExternalLink = function() {
var url = 'https://github.com/Bitcoin-com/Wallet/releases/latest';
var optIn = true;
var title = gettextCatalog.getString('Update Available');
var message = gettextCatalog.getString('An update to this app is available. For your security, please update to the latest version.');
var okText = gettextCatalog.getString('View Update');
var cancelText = gettextCatalog.getString('Go Back');
externalLinkService.open(url, optIn, title, message, okText, cancelText);
$scope.showUpdatePopup = function() {
latestReleaseService.showUpdatePopup();
};
$scope.openBannerUrl = function() {

View file

@ -2,6 +2,7 @@
angular.module('copayApp.controllers').controller('tabReceiveController', function($rootScope, $scope, $timeout, $log, $ionicModal, $state, $ionicHistory, $ionicPopover, storageService, platformInfo, walletService, profileService, configService, lodash, gettextCatalog, popupService, bwcError, bitcoinCashJsService, $ionicNavBarDelegate, sendFlowService, txFormatService, soundService, clipboardService) {
var CLOSE_NORMAL = 1000;
var listeners = [];
$scope.bchAddressType = { type: 'cashaddr' };
var bchAddresses = {};
@ -10,12 +11,11 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
$scope.isCordova = platformInfo.isCordova;
$scope.isNW = platformInfo.isNW;
var currentAddressSocket = {};
var paymentSubscriptionObj = { op:"addr_sub" }
var config;
var currentAddressSocket = null;
var paymentSubscriptionObj = { op:'addr_sub' };
$scope.displayBalanceAsFiat = true;
$scope.$on('$ionicView.beforeLeave', onBeforeLeave);
$scope.requestSpecificAmount = function() {
sendFlowService.start({
@ -24,6 +24,50 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
});
};
function connectSocket() {
// Close existing socket if not connected with current address
if (currentAddressSocket) {
currentAddressSocket.close([CLOSE_NORMAL]);
}
if ($scope.wallet.coin === 'bch') {
// listen to bch address
currentAddressSocket = new WebSocket('wss://ws.blockchain.info/bch/inv');
paymentSubscriptionObj.addr = $scope.addrBchLegacy;
} else {
// listen to btc address
currentAddressSocket = new WebSocket('wss://ws.blockchain.info/inv');
paymentSubscriptionObj.addr = $scope.addr;
}
// create subscription to address
var msg = JSON.stringify(paymentSubscriptionObj);
currentAddressSocket.onopen = function (event) {
currentAddressSocket.send(msg);
};
// listen for response
currentAddressSocket.onmessage = function (event) {
//console.log("message received:" + event.data);
receivedPayment(event.data);
};
currentAddressSocket.onclose = function(event) {
if (event.code !== CLOSE_NORMAL) {
$log.debug('Socket was closed abnormally. Reconnect will be attempted in 1 second.');
$timeout(function() {
connectSocket();
}, 1000);
}
};
currentAddressSocket.onerror = function(err) {
console.error('Socket encountered error: ', err, 'Closing socket');
currentAddressSocket.close();
};
}
$scope.setAddress = function(newAddr, copyAddress) {
$scope.addr = null;
if (!$scope.wallet || $scope.generatingAddress || !$scope.wallet.isComplete()) return;
@ -36,49 +80,24 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
popupService.showAlert(err);
}
//close existing socket
if (currentAddressSocket.close === 'function') {
currentAddressSocket.close();
}
if ($scope.wallet.coin == 'bch') {
bchAddresses = bitcoinCashJsService.translateAddresses(addr);
$scope.addr = bchAddresses[$scope.bchAddressType.type];
$scope.addrBchLegacy = bchAddresses['legacy'];
// listen to bch address
currentAddressSocket = new WebSocket("wss://ws.blockchain.info/bch/inv");
paymentSubscriptionObj.addr = bchAddresses['legacy'];
if ($scope.wallet.coin === 'bch') {
bchAddresses = bitcoinCashJsService.translateAddresses(addr);
$scope.addr = bchAddresses[$scope.bchAddressType.type];
$scope.addrBchLegacy = bchAddresses['legacy'];
} else {
$scope.addr = addr;
// listen to btc address
currentAddressSocket = new WebSocket("wss://ws.blockchain.info/inv");
paymentSubscriptionObj.addr = $scope.addr
$scope.addr = addr;
}
connectSocket();
if (copyAddress === true) {
try {
clipboardService.copyToClipboard($scope.wallet.coin == 'bch' && $scope.bchAddressType.type == 'cashaddr' ? 'bitcoincash:' + $scope.addr : $scope.addr);
} catch (error) {
$log.debug("Error copying to clipboard:");
$log.debug('Error copying to clipboard:');
$log.debug(error);
}
}
// create subscription
var msg = JSON.stringify(paymentSubscriptionObj);
currentAddressSocket.onopen = function (event) {
//console.log("message sent: " + msg);
currentAddressSocket.send(msg);
}
// listen for response
currentAddressSocket.onmessage = function (event) {
//console.log("message received:" + event.data);
receivedPayment(event.data);
}
$timeout(function() {
$scope.$apply();
@ -164,7 +183,6 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
// Notify new tx
$scope.$emit('bwsEvent', $scope.wallet.id);
$scope.$apply(function () {
$scope.showingPaymentReceived = true;
});
@ -233,6 +251,10 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
}
};
function onBeforeLeave() {
currentAddressSocket.close([CLOSE_NORMAL]);
}
$scope.$on("$ionicView.beforeEnter", function(event, data) {
$scope.wallets = profileService.getWallets();
$scope.singleWallet = $scope.wallets.length == 1;
@ -258,7 +280,6 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
configService.whenAvailable(function(_config) {
$scope.displayBalanceAsFiat = _config.wallet.settings.priceDisplay === 'fiat';
config = _config;
});
});

View file

@ -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) {
@ -19,8 +55,6 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
var log = new window.BitAnalytics.LogEvent("wallet_details_open", [], [channel]);
window.BitAnalytics.LogEventHandlers.postEvent(log);
$scope.amountIsCollapsible = !$scope.isAndroid;
$scope.openExternalLink = function(url, target) {
externalLinkService.open(url, target);
};
@ -52,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,
@ -135,68 +170,97 @@ 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() {
$scope.vm.gettingCachedHistory = true;
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);
@ -235,24 +299,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() {
@ -262,97 +337,32 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
};
var prevPos;
$scope.txHistoryPaddingBottom = 0;
function getScrollPosition() {
var scrollPosition = $ionicScrollDelegate.getScrollPosition();
$timeout(function() {
getScrollPosition();
}, 200);
if (!scrollPosition) {
$window.requestAnimationFrame(function() {
getScrollPosition();
});
return;
}
var pos = scrollPosition.top;
if (pos > 0) {
$scope.txHistoryPaddingBottom = "200px";
}
if (pos === prevPos) {
$window.requestAnimationFrame(function() {
getScrollPosition();
});
return;
}
prevPos = pos;
refreshAmountSection(pos);
};
function refreshAmountSection(scrollPos) {
var AMOUNT_HEIGHT_BASE = 210;
$scope.showBalanceButton = false;
if ($scope.status) {
$scope.showBalanceButton = ($scope.status.totalBalanceSat != $scope.status.spendableAmount);
if ($scope.showBalanceButton) {
AMOUNT_HEIGHT_BASE = 270;
}
}
if (!$scope.amountIsCollapsible) {
var t = ($scope.showBalanceButton ? 15 : 45);
$scope.amountScale = 'translateY(' + t + 'px)';
return;
}
scrollPos = scrollPos || 0;
var amountHeight = AMOUNT_HEIGHT_BASE - scrollPos;
if (amountHeight < 80) {
amountHeight = 80;
}
var contentMargin = amountHeight;
if (contentMargin > AMOUNT_HEIGHT_BASE) {
contentMargin = AMOUNT_HEIGHT_BASE;
}
var amountScale = (amountHeight / AMOUNT_HEIGHT_BASE);
if (amountScale < 0.5) {
amountScale = 0.5;
}
if (amountScale > 1.1) {
amountScale = 1.1;
}
var s = amountScale;
// Make space for the balance button when it needs to display.
var TOP_NO_BALANCE_BUTTON = 115;
var TOP_BALANCE_BUTTON = 30;
var top = TOP_NO_BALANCE_BUTTON;
if ($scope.showBalanceButton) {
top = TOP_BALANCE_BUTTON;
}
var amountTop = ((amountScale - 0.80) / 0.80) * top;
if (amountTop < -2) {
amountTop = -2;
}
if (amountTop > top) {
amountTop = top;
}
var t = amountTop;
$scope.altAmountOpacity = (amountHeight - 100) / 80;
$scope.buttonsOpacity = (amountHeight - 140) / 70;
$window.requestAnimationFrame(function() {
$scope.amountHeight = amountHeight + 'px';
$scope.contentMargin = contentMargin + 'px';
$scope.amountScale = 'scale3d(' + s + ',' + s + ',' + s + ') translateY(' + t + 'px)';
$scope.$digest();
getScrollPosition();
});
$scope.scrollPosition = pos;
}
var scrollWatcherInitialized;
$scope.$on("$ionicView.enter", function(event, data) {
if ($scope.isCordova && $scope.isAndroid) setAndroidStatusBarColor();
if (scrollWatcherInitialized || !$scope.amountIsCollapsible) {
return;
}
scrollWatcherInitialized = true;
});
@ -370,7 +380,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);
@ -380,21 +390,25 @@ 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);
}),
];
});
var refreshInterval;
$scope.$on("$ionicView.afterEnter", function(event, data) {
$scope.updateAll();
refreshAmountSection();
$scope.$on("$ionicView.afterEnter", function onAfterEnter(event, data) {
updateTxHistoryFromCachedData();
$scope.updateAll(true, true);
// refreshAmountSection();
refreshInterval = $interval($scope.onRefresh, 10 * 1000);
$timeout(function() {
getScrollPosition();
}, 1000);
});
$scope.$on("$ionicView.afterLeave", function(event, data) {

View file

@ -1,189 +1,204 @@
'use strict';
angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $state, sendFlowService, configService, gettextCatalog, profileService, txFormatService) {
(function () {
var fromWalletId = '';
var priceDisplayAsFiat = false;
var unitDecimals = 0;
var unitsFromSatoshis = 0;
angular
.module('copayApp.controllers')
.controller('walletSelectorController', walletSelectorController);
$scope.$on("$ionicView.beforeEnter", onBeforeEnter);
$scope.$on("$ionicView.enter", onEnter);
function onBeforeEnter(event, data) {
if (data.direction == "back") {
sendFlowService.state.pop();
}
function walletSelectorController ($scope, $state, sendFlowService, configService, gettextCatalog, ongoingProcess, profileService, walletService, txFormatService) {
var fromWalletId = '';
var priceDisplayAsFiat = false;
var unitDecimals = 0;
var unitsFromSatoshis = 0;
$scope.params = sendFlowService.state.getClone();
console.log('walletSelector onBeforeEnter after back sendflow', $scope.params);
var config = configService.getSync().wallet.settings;
priceDisplayAsFiat = config.priceDisplay === 'fiat';
unitDecimals = config.unitDecimals;
unitsFromSatoshis = 1 / config.unitToSatoshi;
if ($scope.params.isWalletTransfer) {
$scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
} else if (!$scope.params.thirdParty) {
$scope.sendFlowTitle = gettextCatalog.getString('Send');
}
$scope.coin = false; // Wallets to show (for destination screen or contacts)
$scope.type = $scope.params['fromWalletId'] ? 'destination' : 'origin'; // origin || destination
fromWalletId = $scope.params['fromWalletId'];
if ($scope.type === 'destination' && $scope.params.toAddress) {
$state.transitionTo(getNextStep($scope.params));
}
if ($scope.params.coin) {
$scope.coin = $scope.params.coin; // Contacts have a coin embedded
}
if ($scope.params.amount) { // There is an amount, so presume that it is a payment request
$scope.sendFlowTitle = gettextCatalog.getString('Payment Request');
$scope.specificAmount = $scope.specificAlternativeAmount = '';
$scope.isPaymentRequest = true;
}
if ($scope.params.thirdParty) {
$scope.thirdParty = $scope.params.thirdParty;
}
};
function onEnter (event, data) {
configService.whenAvailable(function(config) {
$scope.selectedPriceDisplay = config.wallet.settings.priceDisplay;
});
if ($scope.thirdParty) {
// Third party services specific logic
handleThirdPartyIfShapeshift();
}
prepareWalletLists();
formatRequestedAmount();
};
function formatRequestedAmount() {
if ($scope.params.amount) {
var cryptoAmount = (unitsFromSatoshis * $scope.params.amount).toFixed(unitDecimals);
var cryptoCoin = $scope.coin.toUpperCase();
txFormatService.formatAlternativeStr($scope.coin, $scope.params.amount, function onFormatAlternativeStr(formatted){
if (formatted) {
var fiatParts = formatted.split(' ');
var fiatAmount = fiatParts[0];
var fiatCurrrency = fiatParts.length > 1 ? fiatParts[1] : '';
if (priceDisplayAsFiat) {
$scope.requestAmount = fiatAmount;
$scope.requestCurrency = fiatCurrrency;
$scope.requestAmountSecondary = cryptoAmount;
$scope.requestCurrencySecondary = cryptoCoin;
} else {
$scope.requestAmount = cryptoAmount;
$scope.requestCurrency = cryptoCoin;
$scope.requestAmountSecondary = fiatAmount;
$scope.requestCurrencySecondary = fiatCurrrency;
}
$scope.$apply();
}
});
}
}
function handleThirdPartyIfShapeshift() {
console.log($scope.thirdParty, $scope.coin);
if ($scope.thirdParty.id === 'shapeshift' && $scope.type === 'destination') { // Shapeshift wants to know the
$scope.coin = profileService.getWallet(fromWalletId).coin;
if ($scope.coin === 'bch') {
$scope.coin = 'btc';
} else {
$scope.coin = 'bch';
$scope.$on("$ionicView.beforeEnter", onBeforeEnter);
$scope.$on("$ionicView.enter", onEnter);
function onBeforeEnter(event, data) {
if (data.direction == "back") {
sendFlowService.state.pop();
}
}
}
function prepareWalletLists() {
var walletsAll = [];
var walletsSufficientFunds = [];
$scope.walletsInsufficientFunds = []; // For origin screen
$scope.params = sendFlowService.state.getClone();
if ($scope.type === 'origin') {
$scope.headerTitle = gettextCatalog.getString('Choose a wallet to send from');
console.log('walletSelector onBeforeEnter after back sendflow', $scope.params);
var config = configService.getSync().wallet.settings;
priceDisplayAsFiat = config.priceDisplay === 'fiat';
unitDecimals = config.unitDecimals;
unitsFromSatoshis = 1 / config.unitToSatoshi;
if ($scope.params.isWalletTransfer) {
$scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
} else if (!$scope.params.thirdParty) {
$scope.sendFlowTitle = gettextCatalog.getString('Send');
}
$scope.coin = false; // Wallets to show (for destination screen or contacts)
$scope.type = $scope.params['fromWalletId'] ? 'destination' : 'origin'; // origin || destination
fromWalletId = $scope.params['fromWalletId'];
if ($scope.type === 'destination' && $scope.params.toAddress) {
$state.transitionTo(getNextStep($scope.params));
}
if ($scope.params.coin) {
$scope.coin = $scope.params.coin; // Contacts have a coin embedded
}
if ($scope.params.amount) { // There is an amount, so presume that it is a payment request
$scope.sendFlowTitle = gettextCatalog.getString('Payment Request');
$scope.specificAmount = $scope.specificAlternativeAmount = '';
$scope.isPaymentRequest = true;
}
if ($scope.params.thirdParty) {
$scope.thirdParty = $scope.params.thirdParty;
}
};
function onEnter (event, data) {
configService.whenAvailable(function(config) {
$scope.selectedPriceDisplay = config.wallet.settings.priceDisplay;
});
if ($scope.thirdParty) {
// Third party services specific logic
handleThirdPartyIfShapeshift();
}
prepareWalletLists();
formatRequestedAmount();
};
function formatRequestedAmount() {
if ($scope.params.amount) {
var cryptoAmount = (unitsFromSatoshis * $scope.params.amount).toFixed(unitDecimals);
var cryptoCoin = $scope.coin.toUpperCase();
walletsAll = profileService.getWallets({coin: $scope.coin});
txFormatService.formatAlternativeStr($scope.coin, $scope.params.amount, function onFormatAlternativeStr(formatted){
if (formatted) {
var fiatParts = formatted.split(' ');
var fiatAmount = fiatParts[0];
var fiatCurrrency = fiatParts.length > 1 ? fiatParts[1] : '';
if (priceDisplayAsFiat) {
$scope.requestAmount = fiatAmount;
$scope.requestCurrency = fiatCurrrency;
$scope.requestAmountSecondary = cryptoAmount;
$scope.requestCurrencySecondary = cryptoCoin;
} else {
$scope.requestAmount = cryptoAmount;
$scope.requestCurrency = cryptoCoin;
$scope.requestAmountSecondary = fiatAmount;
$scope.requestCurrencySecondary = fiatCurrrency;
}
$scope.$apply();
}
});
}
}
function handleThirdPartyIfShapeshift() {
console.log($scope.thirdParty, $scope.coin);
if ($scope.thirdParty.id === 'shapeshift' && $scope.type === 'destination') { // Shapeshift wants to know the
$scope.coin = profileService.getWallet(fromWalletId).coin;
if ($scope.coin === 'bch') {
$scope.coin = 'btc';
} else {
$scope.coin = 'bch';
}
}
}
function prepareWalletLists() {
var walletsAll = [];
var walletsSufficientFunds = [];
$scope.walletsInsufficientFunds = []; // For origin screen
if ($scope.type === 'origin') {
$scope.headerTitle = gettextCatalog.getString('Choose a wallet to send from');
if ($scope.params.amount || $scope.coin) {
walletsAll = profileService.getWallets({coin: $scope.coin});
ongoingProcess.set('scanning', true);
walletsAll.forEach(function forWallet(wallet) {
if (!wallet.status && !wallet.cachedStatus) {
walletService.getStatus(wallet, {}, function(err, status) {
wallet.status = status;
if (status.availableBalanceSat > ($scope.params.amount ? $scope.params.amount : 0)) {
walletsSufficientFunds.push(wallet);
} else {
$scope.walletsInsufficientFunds.push(wallet);
}
if ($scope.coin === 'btc') { // As this is a promise
$scope.walletsBtc = walletsSufficientFunds;
} else {
$scope.walletsBch = walletsSufficientFunds;
}
ongoingProcess.set('scanning', false);
});
} else {
var walletStatus = null;
if (wallet.status && wallet.status.isValid) {
walletStatus = wallet.status;
} else if (wallet.cachedStatus && wallet.status.isValid) {
walletStatus = wallet.cachedStatus;
}
if (walletStatus && walletStatus.availableBalanceSat > ($scope.params.amount ? $scope.params.amount : 0)) {
walletsSufficientFunds.push(wallet);
} else {
$scope.walletsInsufficientFunds.push(wallet);
}
ongoingProcess.set('scanning', false);
}
});
if ($scope.coin === 'btc') {
$scope.walletsBtc = walletsSufficientFunds;
} else {
$scope.walletsBch = walletsSufficientFunds;
}
} else {
$scope.walletsBch = profileService.getWallets({coin: 'bch', hasFunds: true});
$scope.walletsBtc = profileService.getWallets({coin: 'btc', hasFunds: true});
$scope.walletsInsufficientFunds = profileService.getWallets({coin: $scope.coin, hasNoFunds: true});
}
walletsAll.forEach(function forWallet(wallet){
if (wallet.status.availableBalanceSat > $scope.params.amount) {
walletsSufficientFunds.push(wallet);
} else {
$scope.walletsInsufficientFunds.push(wallet);
}
});
if ($scope.coin === 'btc') {
$scope.walletsBtc = walletsSufficientFunds;
} else {
$scope.walletsBch = walletsSufficientFunds;
} else if ($scope.type === 'destination') {
if (!$scope.coin) { // Allow for the coin to be set by a third party
$scope.fromWallet = profileService.getWallet(fromWalletId);
$scope.coin = $scope.fromWallet.coin; // Only show wallets with the select origin wallet coin
}
$scope.headerTitle = gettextCatalog.getString('Choose a wallet to send to');
} else if ($scope.coin) {
walletsAll = profileService.getWallets({coin: $scope.coin});
walletsAll.forEach(function forWallet(wallet){
if (wallet.status.availableBalanceSat > 0) {
walletsSufficientFunds.push(wallet);
} else {
$scope.walletsInsufficientFunds.push(wallet);
}
});
if ($scope.coin === 'btc') {
$scope.walletsBtc = walletsSufficientFunds;
if ($scope.coin === 'btc') { // if no specific coin is set or coin is set btc
$scope.walletsBtc = profileService.getWallets({coin: $scope.coin});
} else {
$scope.walletsBch = walletsSufficientFunds;
$scope.walletsBch = profileService.getWallets({coin: $scope.coin});
}
} else {
$scope.walletsBch = profileService.getWallets({coin: 'bch', hasFunds: true});
$scope.walletsBtc = profileService.getWallets({coin: 'btc', hasFunds: true});
$scope.walletsInsufficientFunds = profileService.getWallets({coin: $scope.coin, hasNoFunds: true});
}
} else if ($scope.type === 'destination') {
if (!$scope.coin) { // Allow for the coin to be set by a third party
$scope.fromWallet = profileService.getWallet(fromWalletId);
$scope.coin = $scope.fromWallet.coin; // Only show wallets with the select origin wallet coin
}
$scope.headerTitle = gettextCatalog.getString('Choose a wallet to send to');
if ($scope.coin === 'btc') { // if no specific coin is set or coin is set btc
$scope.walletsBtc = profileService.getWallets({coin: $scope.coin});
} else {
$scope.walletsBch = profileService.getWallets({coin: $scope.coin});
}
}
}
$scope.useWallet = function(wallet) {
var params = sendFlowService.state.getClone();
if ($scope.type === 'origin') { // we're on the origin screen, set wallet to send from
params.fromWalletId = wallet.id;
} else { // we're on the destination screen, set wallet to send to
params.toWalletId = wallet.id;
$scope.useWallet = function(wallet) {
var params = sendFlowService.state.getClone();
if ($scope.type === 'origin') { // we're on the origin screen, set wallet to send from
params.fromWalletId = wallet.id;
} else { // we're on the destination screen, set wallet to send to
params.toWalletId = wallet.id;
}
sendFlowService.goNext(params);
};
$scope.goBack = function() {
sendFlowService.router.goBack();
}
sendFlowService.goNext(params);
};
$scope.goBack = function() {
sendFlowService.router.goBack();
}
});
})();

View file

@ -116,7 +116,8 @@ angular.module('copayApp.directives')
function getTransformStyle(translatePct) {
return {
'transform': 'translateX(' + translatePct + '%)'
'transform': 'translateX(' + translatePct + '%)',
'-webkit-transform': 'translateX(' + translatePct + '%)'
};
}

View file

@ -12,6 +12,7 @@
totalBalanceSat: '@',
// The Wallet object is sometimes not stringify()-able, so not interpolatable,
// so can't be passed to a directive.
walletCoin: '@',
walletStatus: '@',
walletCachedBalance: '@',
walletCachedBalanceUpdatedOn: '@',
@ -31,7 +32,6 @@
});
function displayCryptoBalance(walletStatus, walletCachedBalance, walletCachedBalanceUpdatedOn, walletCachedStatus) {
console.log('displayCryptoBalance()');
if (walletStatus && walletStatus.isValid && walletStatus.totalBalanceStr) {
setDisplay(walletStatus.totalBalanceStr, '');
@ -52,7 +52,7 @@
setDisplay('', '');
}
function displayFiatBalance(walletStatus, walletCachedStatus) {
function displayFiatBalance(walletStatus, walletCachedStatus, walletCoin) {
var displayAmount = '';
if (walletStatus && walletStatus.isValid && walletStatus.alternativeBalanceAvailable) {
displayAmount = walletStatus.totalBalanceAlternative + ' ' + walletStatus.alternativeIsoCode;
@ -66,7 +66,7 @@
return;
}
getFiatBalance(wallet);
getFiatBalance(walletStatus, walletCachedStatus, walletCoin);
}
function formatBalance() {
@ -94,19 +94,30 @@
}
if (displayAsFiat) {
displayFiatBalance(walletStatusObj, walletCachedStatusObj);
displayFiatBalance(walletStatusObj, walletCachedStatusObj, $scope.walletCoin);
}
}
function getFiatBalance(wallet) {
if (!(wallet.status && wallet.status.isValid)) {
$log.warn('Abandoning call to get fiat balance, because no valid wallet status.');
function getFiatBalance(walletStatus, walletCachedStatus, walletCoin) {
var totalBalanceSat = null;
if (walletStatus && walletStatus.isValid) {
totalBalanceSat = walletStatus.totalBalanceSat
} else if (walletCachedStatus && walletCachedStatus.isValid) {
totalBalanceSat = walletCachedStatus.totalBalanceSat
}
// 0 is valid
if (totalBalanceSat === null) {
$log.warn('Abandoning call to get fiat balance, because no valid wallet status (cached or otherwise).');
return;
}
txFormatService.formatAlternativeStr(wallet.coin, wallet.status.totalBalanceSat, function onFormatAlernativeStr(formatted) {
txFormatService.formatAlternativeStr(walletCoin, totalBalanceSat, function onFormatAlernativeStr(formatted) {
if (formatted) {
setDisplay(formatted, '');
} else {
$log.error('Failed to format fiat balance of wallet.');
}
});
}

View file

@ -84,13 +84,53 @@
return result;
}
function infoFromImport(data) {
function infoFromWalletImportText(data) {
var split = data.split('|');
// Copay seems to use extra parameter for coin.
if (split.length < 5 || split.length > 6) {
return null;
}
var type = parseInt(split[0], 10);
if (isNaN(type)) {
return null;
}
var data = split[1];
var network = split[2];
if (!(network === 'livenet' || network === 'testnet')) {
return null;
}
var isTestnet = network === 'testnet';
var derivationPath = split[3];
if (!/^m\/\d+'\/\d+'\/\d+'$/.test(derivationPath)) {
return null;
}
var hasPassphraseText = split[4];
if (!(hasPassphraseText === 'true' || hasPassphraseText === 'false')) {
return null;
}
var hasPassphrase = hasPassphraseText === 'true';
var coin; // Intentionally undefined as may not be present
if (split.length > 5) {
var coinText = split[5];
if (!(coinText === 'bch' || coinText === 'btc')) {
return null;
}
coin = coinText;
}
return {
type: type,
data: data,
isTestnet: isTestnet,
derivationPath: derivationPath,
hasPassphrase: hasPassphrase,
coin: coin
};
}
/*
@ -105,6 +145,13 @@
bareUrl: '',
coin: '',
copayInvitation: '',
import: { // testnet info in root, coin info in root if available
data: '',
derivationPath: '',
hasPassphrase: false,
type: 1,
},
isTestnet: false,
isValid: false,
label: '',
message: '',
@ -124,7 +171,6 @@
"req-param0": '',
"req-param1": ''
},
testnet: false,
url: '' // For BIP70
}
@ -158,7 +204,13 @@
} else if (/^(?:bitcoincash)|(?:bitcoin-cash)$/.test(preColonLower)) {
parsed.coin = 'bch';
parsed.test = false;
parsed.isTestnet = false;
addressAndParams = colonSplit[2].trim();
console.log('Is bch');
} else if (/^(?:bch)$/.test(preColonLower)) {
parsed.coin = 'bch';
parsed.isTestnet = false;
addressAndParams = colonSplit[2].trim();
console.log('Is bch');
@ -173,16 +225,19 @@
addressAndParams = colonSplit[1].trim();
console.log('No prefix.');
} else if (/^https?$/.test(colonSplit[1])) {
} else if (/^https?$/.test(colonSplit[1])) { // Plain URL
addressAndParams = trimmed;
} else if (colonSplit[2].indexOf('|') == 0) { // Import
addressAndParams = trimmed
} else {
// Something with a colon in the middle that we don't recognise
// Something we don't recognise
return parsed;
}
// Remove erroneous leading slashes
var leadingSlashes = /^\/*([^\/]+(?:.*))$/.exec(addressAndParams);
//var leadingSlashes = /^\/*([^\/]+(?:.*))$/.exec(addressAndParams);
var leadingSlashes = /^\/*(.*)$/.exec(addressAndParams);
if (!leadingSlashes) {
return parsed;
}
@ -262,7 +317,6 @@
var copayInvitationRe = /^[0-9A-HJ-NP-Za-km-z]{70,80}$/;
//var legacyRe = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
//var legacyTestnetRe = /^[mn][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
var importRe = /^[123]|$/;
var privateKeyEncryptedRe = /^6P[1-9A-HJ-NP-Za-km-z]{56}$/;
var privateKeyForUncompressedPublicKeyRe = /^5[1-9A-HJ-NP-Za-km-z]{50}$/;
var privateKeyForUncompressedPublicKeyTestnetRe = /^9[1-9A-HJ-NP-Za-km-z]{50}$/;
@ -273,6 +327,7 @@
var bitpayAddrMainnet = bitpayAddrOnMainnet(address);
var cashAddrTestnet = cashAddrOnTestnet(addressLowerCase);
var cashAddrMainnet = cashAddrOnMainnet(addressLowerCase);
var importInfo = infoFromWalletImportText(address);
var privateKey = '';
if (parsed.isTestnet && cashAddrTestnet) {
@ -342,6 +397,17 @@
} else if (urlRe.test(address)) {
parsed.bareUrl = trimmed;
parsed.isValid = true;
} else if (importInfo) {
parsed.import = {
type: importInfo.type,
data: importInfo.data,
derivationPath: importInfo.derivationPath,
hasPassphrase: importInfo.hasPassphrase
};
parsed.coin = importInfo.coin;
parsed.isTestnet = importInfo.isTestnet;
parsed.isValid = true;
}
} else {

View file

@ -30,7 +30,7 @@ describe('bitcoinUriService', function() {
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress).toBeUndefined();
expect(parsed.isTestnet).toBeUndefined();
expect(parsed.isTestnet).toBe(false);
expect(parsed.url).toBe('https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
});
@ -171,6 +171,16 @@ describe('bitcoinUriService', function() {
expect(parsed.isTestnet).toBe(false);
});
it('legacy address with bch prefix', function() {
var parsed = bitcoinUriService.parse('bch:19yUdM2H7sADrabR6Afu9zTpmwqr6WYprX');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.legacy).toBe('19yUdM2H7sADrabR6Afu9zTpmwqr6WYprX');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr testnet with prefix', function() {
var parsed = bitcoinUriService.parse('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
@ -191,6 +201,16 @@ describe('bitcoinUriService', function() {
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with bch prefix', function() {
var parsed = bitcoinUriService.parse('bch:qpqzqtjqqc00nsxj0e3kevz65ujg4yt5z5w99jap5f');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpqzqtjqqc00nsxj0e3kevz65ujg4yt5z5w99jap5f');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with dash', function() {
var parsed = bitcoinUriService.parse('bitcoin-cash:qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
@ -270,6 +290,41 @@ describe('bitcoinUriService', function() {
expect(parsed.copayInvitation).toBe('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch');
});
it ('import BCH wallet no password', function() {
var parsed = bitcoinUriService.parse("1|suggest route obvious broccoli good position hidden tone history around final lobster|livenet|m/44'/0'/0'|false");
expect(parsed.isValid).toBe(true);
expect(parsed.import.type).toBe(1);
expect(parsed.import.data).toBe('suggest route obvious broccoli good position hidden tone history around final lobster');
expect(parsed.isTestnet).toBe(false);
expect(parsed.import.derivationPath).toBe("m/44'/0'/0'");
expect(parsed.import.hasPassphrase).toBe(false);
});
it ('import BCH wallet with passphrase', function() {
var parsed = bitcoinUriService.parse("1|fringe hazard all hobby trap myth fire stand sock empty soon east|livenet|m/44'/0'/0'|true");
expect(parsed.isValid).toBe(true);
expect(parsed.import.type).toBe(1);
expect(parsed.import.data).toBe('fringe hazard all hobby trap myth fire stand sock empty soon east');
expect(parsed.isTestnet).toBe(false);
expect(parsed.import.derivationPath).toBe("m/44'/0'/0'");
expect(parsed.import.hasPassphrase).toBe(true);
});
it ('import BTC wallet testnet', function() {
// From copay
var parsed = bitcoinUriService.parse("1|cat wealth column firm wet sauce tornado era feature monster click eyebrow|testnet|m/44'/1'/0'|false|btc");
expect(parsed.isValid).toBe(true);
expect(parsed.import.type).toBe(1);
expect(parsed.import.data).toBe('cat wealth column firm wet sauce tornado era feature monster click eyebrow');
expect(parsed.isTestnet).toBe(true);
expect(parsed.import.derivationPath).toBe("m/44'/1'/0'");
expect(parsed.import.hasPassphrase).toBe(false);
});
// Invalid addresses from https://github.com/bitcoincashorg/bitcoincash.org/blob/master/spec/cashaddr.md
it('invalid cashAddr style 1', function() {
var parsed = bitcoinUriService.parse('prefix:x64nx6hz');

View file

@ -85,7 +85,9 @@ angular.module('copayApp.services').factory('bitcoincomService', function(gettex
};
var register = function() {
nextStepsService.register(cashGamesItem);
if (!platformInfo.isAndroid) { // To comply with Google Play policies
nextStepsService.register(cashGamesItem);
}
nextStepsService.register(newsItem);
nextStepsService.register(poolItem);
nextStepsService.register(toolsItem);

View file

@ -0,0 +1,180 @@
'use strict';
(function() {
angular
.module('bitcoincom.services')
.factory('latestReleaseService', latestReleaseService);
function latestReleaseService($log, $http, $ionicPopup, configService, externalLinkService, gettextCatalog, platformInfo) {
var service = {
// Functions
checkLatestRelease: checkLatestRelease,
requestLatestRelease: requestLatestRelease,
showUpdatePopup: showUpdatePopup
};
return service;
function checkLatestRelease(cb) {
var releaseURL = configService.getDefaults().release.url;
requestLatestRelease(releaseURL, function (err, releaseData) {
if (err) return cb(err);
var currentVersion = window.version;
var latestVersion = releaseData.tag_name;
if (!verifyTagFormat(currentVersion))
return cb('Cannot verify the format of version tag: ' + currentVersion);
if (!verifyTagFormat(latestVersion))
return cb('Cannot verify the format of latest release tag: ' + latestVersion);
var current = formatTagNumber(currentVersion);
var latest = formatTagNumber(latestVersion);
if (latest.major < current.major || (latest.major === current.major && latest.minor <= current.minor)) {
return cb(null, false);
}
var releaseSearchTerm = "";
if (platformInfo.isNW) { // XX SP: DESKTOP: Check if the latest release is already available for current OS
var platform = process.platform;
if (platform === "darwin") {
releaseSearchTerm = "osx";
} else if (platform === "win32") {
releaseSearchTerm = "win";
} else if (platform === "linux") {
releaseSearchTerm = "linux";
}
var foundNewVersion = false;
for (var i in releaseData.assets) {
if (releaseData.assets[i].name.indexOf(releaseSearchTerm) !== -1) {
foundNewVersion = true;
break;
}
}
}
$log.debug('A new version is available: ' + latestVersion);
var releaseNotes = false;
if (releaseData.body) {
var releaseLines = releaseData.body.split('\n');
for (var lineNum in releaseLines) {
if (releaseLines[lineNum].substring(0, 2) === "# ") {
releaseLines[lineNum] = "<strong>" + releaseLines[lineNum].substring(2) + "</strong>";
} else if (releaseLines[lineNum].substring(0, 2) === "- ") {
releaseLines[lineNum] = "&bull; " + releaseLines[lineNum].substring(2);
}
}
releaseNotes = releaseLines.join('\n');
}
return cb(null, {latestVersion: latestVersion, releaseNotes: releaseNotes});
});
function verifyTagFormat(tag) {
var regex = /^v?\d+\.\d+(\.\d+)?(-rc\d)?$/i;
return regex.exec(tag);
}
function formatTagNumber(tag) {
var label = false;
if (tag.split("-")[1]) { // Move postfixes like "-rc2" to a variable
label = tag.split("-")[1];
tag = tag.split("-")[0];
}
var formattedNumber = tag.replace(/^v/i, '').split('.');
return {
major: +(formattedNumber[0] ? +formattedNumber[0] : 0),
minor: +(formattedNumber[1] ? +formattedNumber[1] : 0),
patch: +(formattedNumber[2] ? +formattedNumber[2] : 0),
label: label /* XX SP: Maybe we can use this in a later stage (with for example 1.0.0-rc2 the value will be "rc2" and false if there is no label) */
};
}
}
function requestLatestRelease(releaseURL, cb) {
$log.debug('Retrieving latest release information...');
var request = {
url: releaseURL,
method: 'GET',
json: true
};
$http(request).then(function (release) {
$log.debug('Latest release: ' + release.data.name);
return cb(null, release.data);
}, function (err) {
return cb('Cannot get the release information: ' + err);
});
}
function showUpdatePopup() {
var buttons = [];
if (!platformInfo.isIOS) { // There is no GitHub-release for iPhone
buttons.push({
text: "GitHub",
type: 'button-positive',
onTap: function () {
var url = 'https://github.com/Bitcoin-com/Wallet/releases/latest';
externalLinkService.open(url, false);
}
});
}
if (platformInfo.isAndroid) {
buttons.unshift({
text: "Google Play Store",
type: 'button-positive',
onTap: function () {
var url = 'https://play.google.com/store/apps/details?id=com.bitcoin.mwallet';
externalLinkService.open(url, false);
}
});
}
if (platformInfo.isIOS) {
buttons.unshift({
text: "App Store",
type: 'button-positive',
onTap: function () {
var url = 'https://itunes.apple.com/app/id1252903728';
externalLinkService.open(url, false);
}
});
} else if (platformInfo.isNW) {
if (process.platform === 'darwin') {
buttons.unshift({
text: "Mac App Store",
type: 'button-positive',
onTap: function () {
var url = 'https://itunes.apple.com/app/bitcoin-com-wallet/id1383072453';
externalLinkService.open(url, false);
}
});
}
}
if (buttons.length === 1) { // There is only one source to download (probably on desktop, so open GitHub release page..)
buttons[0].onTap();
} else {
buttons.push({
text: gettextCatalog.getString('Go Back'),
type: 'button-positive',
onTap: function () {
return true;
}
});
$ionicPopup.show({
title: gettextCatalog.getString('Update Available'),
subTitle: gettextCatalog.getString('An update to this app is available. For your security, please update to the latest version.'),
cssClass: 'popup-update',
buttons: buttons
});
}
}
}
})();

View file

@ -1,63 +0,0 @@
'use strict';
angular.module('copayApp.services')
.factory('latestReleaseService', function latestReleaseServiceFactory($log, $http, configService) {
var root = {};
root.checkLatestRelease = function(cb) {
var releaseURL = configService.getDefaults().release.url;
requestLatestRelease(releaseURL, function(err, release) {
if (err) return cb(err);
var currentVersion = window.version;
var latestVersion = release.data.tag_name;
if (!verifyTagFormat(currentVersion))
return cb('Cannot verify the format of version tag: ' + currentVersion);
if (!verifyTagFormat(latestVersion))
return cb('Cannot verify the format of latest release tag: ' + latestVersion);
var current = formatTagNumber(currentVersion);
var latest = formatTagNumber(latestVersion);
if (latest.major < current.major || (latest.major == current.major && latest.minor <= current.minor))
return cb(null, false);
$log.debug('A new version is available: ' + latestVersion);
return cb(null, true);
});
function verifyTagFormat(tag) {
var regex = /^v?\d+\.\d+\.\d+$/i;
return regex.exec(tag);
};
function formatTagNumber(tag) {
var formattedNumber = tag.replace(/^v/i, '').split('.');
return {
major: +formattedNumber[0],
minor: +formattedNumber[1],
patch: +formattedNumber[2]
};
};
};
function requestLatestRelease(releaseURL, cb) {
$log.debug('Retrieving latest relsease information...');
var request = {
url: releaseURL,
method: 'GET',
json: true
};
$http(request).then(function(release) {
$log.debug('Latest release: ' + release.data.name);
return cb(null, release);
}, function(err) {
return cb('Cannot get the release information: ' + err);
});
};
return root;
});

View file

@ -11,7 +11,7 @@ angular
var service = {
// Variables
state: {
amount: '',
amount: 0,
displayAddress: null,
fromWalletId: '',
sendMax: false,
@ -67,7 +67,7 @@ angular
$log.debug("send-flow-state clearCurrent()");
service.state = {
amount: '',
amount: 0,
displayAddress: null,
fromWalletId: '',
sendMax: false,

View file

@ -10,7 +10,7 @@ angular.module('copayApp.services')
isoCode: 'en',
rateCode: 'USD'
}, {
name: 'català',
name: 'Català',
isoCode: 'ca',
rateCode: 'EUR'
},{
@ -59,10 +59,6 @@ angular.module('copayApp.services')
name: 'Português',
isoCode: 'pt',
rateCode: 'EUR'
}, {
name: 'русский язык',
isoCode: 'ru',
rateCode: 'RUB'
}, {
name: '한국어',
isoCode: 'ko',

View file

@ -0,0 +1,274 @@
'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 fraction 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_FRACTION = 0.5;
var SAFE_CONFIRMATIONS = 6;
var allTransactionsFetched = false;
var service = {
getCachedTxHistory: getCachedTxHistory,
updateLocalTxHistoryByPage: updateLocalTxHistoryByPage,
};
return service;
function addEarlyTransactions(walletId, cachedTxs, newTxs) {
var cachedTxIndexFromId = {};
cachedTxs.forEach(function forCachedTx(tx, txIndex){
cachedTxIndexFromId[tx.txid] = txIndex;
});
var confirmationsUpdated = false;
var someTransactionsWereNew = false;
var overlappingTxsCount = 0;
newTxs.forEach(function forNewTx(tx){
if (typeof cachedTxIndexFromId[tx.txid] === "undefined") {
someTransactionsWereNew = true;
cachedTxs.push(tx);
} else {
var txUpdated = updateCachedTx(cachedTxs, cachedTxIndexFromId, tx);
confirmationsUpdated = confirmationsUpdated || txUpdated;
overlappingTxsCount++;
}
});
var overlappingTxFraction = overlappingTxsCount / Math.min(cachedTxs.length, PAGE_OVERLAP);
console.log('overlappingTxFraction:', overlappingTxFraction);
console.log('overlappingTxsCount:', overlappingTxsCount);
if (overlappingTxFraction >= MIN_KNOWN_TX_OVERLAP_FRACTION || (someTransactionsWereNew && overlappingTxsCount === 0)) { // We are good
if (someTransactionsWereNew) {
saveTxHistory(walletId, cachedTxs);
} else if (confirmationsUpdated) {
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 cachedTxIndexFromId = {};
cachedTxs.forEach(function forCachedTx(tx, txIndex){
cachedTxIndexFromId[tx.txid] = txIndex;
});
var someTransactionsWereNew = false;
var confirmationsUpdated = false;
var overlappingTxsCount = 0;
var uniqueNewTxs = [];
newTxs.forEach(function forNewTx(tx){
if (typeof cachedTxIndexFromId[tx.txid] === "undefined") {
someTransactionsWereNew = true;
uniqueNewTxs.push(tx);
} else {
var txUpdated = updateCachedTx(cachedTxs, cachedTxIndexFromId, tx);
confirmationsUpdated = confirmationsUpdated || txUpdated;
overlappingTxsCount++;
}
});
var overlappingTxFraction = overlappingTxsCount / Math.min(cachedTxs.length, PAGE_OVERLAP);
if (overlappingTxFraction >= MIN_KNOWN_TX_OVERLAP_FRACTION || (someTransactionsWereNew && overlappingTxsCount === 0)) { // We are good
if (someTransactionsWereNew) {
var allTxs = uniqueNewTxs.concat(cachedTxs);
saveTxHistory(walletId, allTxs);
return allTxs;
} else {
if (confirmationsUpdated) {
saveTxHistory(walletId, cachedTxs);
}
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 wallet
* @param start
* @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);
}
});
}
/**
* Returns true if the cached tx was updated
* @param {*} cachedTxs
* @param {*} cachedTxIndexFromId - Indices for cachedTxs, based on txid
* @param {*} tx - The most recent tx info
*/
function updateCachedTx(cachedTxs, cachedTxIndexFromId, tx) {
var updated = false;
var txIndex = cachedTxIndexFromId[tx.txid];
var cachedTx = cachedTxs[txIndex];
if (cachedTx.confirmations < SAFE_CONFIRMATIONS && tx.confirmations > cachedTx.confirmations) {
cachedTxs[txIndex].confirmations = tx.confirmations;
updated = true;
}
return updated;
}
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);
});
});
}
}
}
})();

View file

@ -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 <HACK>
// do not sync all history, just looking for a single TX.
@ -492,14 +496,16 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
}
// </HACK>
shouldContinue = false;
skipped = skip;
if (!shouldContinue) {
$log.debug('Finished Sync: New / soft confirmed Txs: ' + newTxs.length);
console.log('pagination Finished Sync: New / soft confirmed Txs: ' + newTxs.length);
return next(null, newTxs);
}
requestLimit = LIMIT;
getNewTxs(newTxs, skip, next);
});
};
@ -535,6 +541,87 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
});
}
if (opts.getMoreTxs) {
var requestLimit = LIMIT;
getNewTxs([], skipped, function(err, txs) {
if (err) return cb(err);
console.log();
createReceivedEvents(txs);
var newHistory = lodash.uniq(lodash.compact(txs.concat(confirmedTxs)), function(x) {
return x.txid;
});
function updateNotes(cb2) {
if (!endingTs) return cb2();
$log.debug('Syncing notes from: ' + endingTs);
wallet.getTxNotes({
minTs: endingTs
}, function(err, notes) {
if (err) {
$log.warn(err);
return cb2();
};
lodash.each(notes, function(note) {
$log.debug('Note for ' + note.txid);
lodash.each(newHistory, function(tx) {
if (tx.txid == note.txid) {
$log.debug('...updating note for ' + note.txid);
tx.note = note;
}
});
});
return cb2();
});
}
function updateLowAmount(txs) {
if (!opts.lowAmount) return;
lodash.each(txs, function(tx) {
tx.lowAmount = tx.amount < opts.lowAmount;
});
};
updateLowAmount(txs);
updateNotes(function() {
// <HACK>
if (foundLimitTx) {
$log.debug('Tx history read until limitTx: ' + opts.limitTx);
return cb(null, newHistory);
}
// </HACK>
var historyToSave = JSON.stringify(newHistory);
lodash.each(txs, function(tx) {
tx.recent = true;
})
$log.debug('Tx History synced. Total Txs: ' + newHistory.length);
// Final update
if (walletId == wallet.credentials.walletId) {
wallet.completeHistory = newHistory;
}
return storageService.setTxHistory(historyToSave, walletId, function() {
$log.debug('Tx History saved.');
return cb(null, newHistory);
});
});
});
return;
}
skipped = 0;
getNewTxs([], 0, function(err, txs) {
if (err) return cb(err);
@ -603,14 +690,19 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
return storageService.setTxHistory(historyToSave, walletId, function() {
$log.debug('Tx History saved.');
return cb();
return cb(null, newHistory);
});
});
});
});
};
root.getMoreTxs = function(wallet, cb) {
var opts = {};
opts.getMoreTxs = true;
updateLocalTxHistory(wallet, opts, cb);
};
root.getTxNote = function(wallet, txid, cb) {
wallet.getTxNote({
txid: txid
@ -634,7 +726,6 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
};
root.getTx = function(wallet, txid, cb) {
function finish(list) {
var tx = lodash.find(list, {
txid: txid
@ -651,7 +742,6 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
limitTx: txid
}, function(err, txHistory) {
if (err) return cb(err);
finish(txHistory);
});
}
@ -675,16 +765,10 @@ angular.module('copayApp.services').factory('walletService', function($log, $tim
root.getTxHistory = function(wallet, opts, cb) {
opts = opts || {};
var walletId = wallet.credentials.walletId;
if (!wallet.isComplete()) return cb();
function isHistoryCached() {
return wallet.completeHistory && wallet.completeHistory.isValid;
};
// disable caching
//if (isHistoryCached() && !opts.force) return cb(null, wallet.completeHistory);
// var historyIsCached = wallet.completeHistory && wallet.completeHistory.isValid;
// if (historyIsCached && !opts.force) return cb(null, wallet.completeHistory);
$log.debug('Updating Transaction History');

View file

@ -1,5 +1,5 @@
%button-standard {
width: 85%;
width: 90%;
max-width: 300px;
margin-left: auto;
margin-right: auto;

View file

@ -1,11 +1,11 @@
.fee-summary {
position: relative;
background-color: #F2F2F2;
box-sizing: border-box;
display: flex;
flex-direction: column;
width: 100%;
padding: 5px 12px 15px;
box-sizing: border-box;
background-color: #F2F2F2;
position: relative;
width: 100%;
&:before {
content: '';
@ -18,12 +18,11 @@
}
.amount {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
.fee-fiat {
display: inline;
&.positive {
color: #70955F;
}
@ -35,6 +34,7 @@
.fee-crypto {
color: #A7A7A7;
float: right;
}
}
}

View file

@ -27,6 +27,7 @@
left: 13px;
top: 50%;
padding: 0;
-webkit-transform: translate(0,-50%);
transform: translate(0,-50%);
}
}

View file

@ -59,6 +59,8 @@
.button {
font-weight: bold;
font-size: 19px;
line-height: 26px;
padding: 8px 6px;
}
}
.button-first-contact img {

View file

@ -350,6 +350,7 @@
.primary-amount-display {
margin-right: 5px;
word-break: break-all;
width: 100%;
}
}

View file

@ -6,8 +6,10 @@
}
.fee-summary {
position: absolute;
bottom: 92px;
bottom: calc(92px + constant(safe-area-inset-bottom)); /* iOS 11.0 */
bottom: calc(92px + env(safe-area-inset-bottom)); /* iOS 11.2 */
position: absolute;
}
.shapeshift-banner, .bitpay-banner, .egifter-banner {
@ -17,4 +19,5 @@
.warning {
color: $v-warning-color-2;
}
}

View file

@ -83,14 +83,14 @@
.button {
border: 2px solid;
border-radius: 47px;
padding: 0 15px 0 15px;
padding: 8px 2px 8px 2px;
text-align: center;
width: 100%;
max-width: 300px;
font-size: 19px;
font-weight: bolder;
min-height: auto;
line-height: 36px;
min-height: 0;
line-height: 19px;
}
}
.wallet-coin-logo {
@ -173,6 +173,11 @@
font-weight: 700;
color: #444;
}
.release-notes {
white-space: pre;
white-space: pre-line;
text-align: left;
}
.button {
width: 100%;
border: none;
@ -190,3 +195,13 @@
top:11px;
}
}
.popup-update {
.popup-buttons {
display: block;
}
.popup-buttons .button{
display:block;
min-width: 100% !important;
margin-top: 4px;
}
}

View file

@ -84,6 +84,9 @@
width: 100%;
}
.payment-received-container {
svg {
max-height: 400px;
}
margin: 0 20px;
.payment-received-amount {
font-size: 1.8em;

View file

@ -105,7 +105,7 @@
width: auto;
margin: 2px 0 4px;
}
height: 60px;
min-height: 65px;
line-height: 16px;
margin-right: 0px;
width: 95%;

View file

@ -1,3 +1,4 @@
$wallet-details-collapse-transition: all 0.25s ease-in-out;
.wallet-details {
&__tx-amount {
font-size: 16px;
@ -137,6 +138,20 @@
margin-top: 20px;
margin-top: env(safe-area-inset-top);
}
&.collapse {
ion-content {
margin-top: 40px;
}
.amount {
&__scale, &__error {
-webkit-transform: scale3d(0.5, 0.5, 0.5) translateY(0px);
transform: scale3d(0.5, 0.5, 0.5) translateY(0px);
}
}
.amount-alternative, .send-receive-buttons, .wallet-details-wallet-info {
opacity: 0;
}
}
}
.bar-header {
border: 0;
@ -152,13 +167,14 @@
background-color: inherit !important;
}
ion-content {
&.collapsible {
margin-top: 230px;
}
padding-top: 0;
top: 0;
transition: $wallet-details-collapse-transition;
margin-top: 185px;
@media only screen and (max-height:500px) {
margin-top: 165px;
}
margin-bottom: 16px;
.scroll {
@ -199,7 +215,7 @@
width: 100%;
position: absolute;
bottom: 20px;
transition: $wallet-details-collapse-transition;
>.col {
padding: 5px 10px;
margin-bottom: 0;
@ -207,30 +223,37 @@
.button {
border: 2px solid;
border-radius: 47px;
padding: 0 15px 0 15px;
padding: 6px 2px 6px 2px;
text-align: center;
width: 100%;
max-width: 300px;
font-size: 19px;
font-weight: bolder;
min-height: auto;
line-height: 36px;
min-height: 0;
line-height: 19px;
}
}
}
.amount {
width: 100%;
text-align: center;
color: #fff;
height: 230px;
padding-top: 40px;
display: block;
align-items: center;
color: #fff;
display: block;
height: 230px;
@media only screen and (max-height:500px) {
height: 210px;
}
justify-content: center;
padding-top: 40px;
text-align: center;
transition: $wallet-details-collapse-transition;
width: 100%;
&__balance {
-webkit-transform: scale3d(1, 1, 1) translateY(45px);
transform: scale3d(1, 1, 1) translateY(45px);
transition: $wallet-details-collapse-transition;
}
&__updating {
@ -240,6 +263,7 @@
&-alternative {
line-height: 36px;
transition: $wallet-details-collapse-transition;
}
&__button-balance {
@ -255,6 +279,7 @@
&__error {
font-size: 14px;
padding: 35px 20px;
opacity: 1;
}
}
@ -341,4 +366,6 @@ a.item {
.loading-wallet svg {
margin-top: 0;
width: 16px;
height: 16px;
}

View file

@ -8,4 +8,56 @@ if (!ArrayBuffer['isView']) {
ArrayBuffer.isView = function(a) {
return a !== null && typeof(a) === "object" && a['buffer'] instanceof ArrayBuffer;
};
}
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
if (sameValueZero(o[k], searchElement)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}