This commit is contained in:
Jean-Baptiste Dominguez 2018-09-07 01:41:13 +09:00
commit 230b6e2228
83 changed files with 5830 additions and 2736 deletions

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

@ -21,28 +21,14 @@ angular.module('copayApp.controllers').controller('addressbookViewController', f
});
$scope.sendTo = function() {
$ionicHistory.removeBackView();
sendFlowService.clear();
$state.go('tabs.send');
$timeout(function() {
var to = '';
if ($scope.addressbookEntry.coin == 'bch') {
var a = 'bitcoincash:' + $scope.addressbookEntry.address;
to = bitcoinCashJsService.readAddress(a).legacy;
} else {
to = $scope.addressbookEntry.address;
}
var stateParams = {
data: $scope.addressbookEntry.address,
toName: $scope.addressbookEntry.name,
toEmail: $scope.addressbookEntry.email,
coin: $scope.addressbookEntry.coin
};
var stateParams = {
toAddress: to,
toName: $scope.addressbookEntry.name,
toEmail: $scope.addressbookEntry.email,
coin: $scope.addressbookEntry.coin
};
sendFlowService.pushState(stateParams);
$state.transitionTo('tabs.send.origin');
}, 100);
sendFlowService.start(stateParams);
};
$scope.remove = function(addressbookEntry) {

View file

@ -1,16 +1,23 @@
'use strict';
angular.module('copayApp.controllers').controller('amountController', amountController);
(function(){
function amountController(configService, $filter, gettextCatalog, $ionicHistory, $ionicModal, $ionicScrollDelegate, lodash, $log, nodeWebkitService, rateService, $scope, $state, $timeout, sendFlowService, shapeshiftService, txFormatService, platformInfo, profileService, walletService, $window) {
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, $ionicHistory,
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, $ionicHistory,
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, $ionicHistory,
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, $ionicHistory,
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');
@ -68,42 +85,26 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
function onBeforeEnter(event, data) {
if (data.direction == "back") {
sendFlowService.popState();
sendFlowService.state.pop();
}
console.log('amount onBeforeEnter after back sendflow ', sendFlowService.state);
initCurrencies();
passthroughParams = sendFlowService.getStateClone();
passthroughParams = sendFlowService.state.getClone();
console.log('amount onBeforeEnter after back sendflow ', passthroughParams);
vm.fromWalletId = passthroughParams.fromWalletId;
vm.toWalletId = passthroughParams.toWalletId;
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);
shapeshiftService.getMarketData(vm.fromWallet.coin, vm.toWallet.coin, function(data) {
vm.thirdParty.data['minAmount'] = vm.minAmount = parseFloat(data.minimum);
vm.thirdParty.data['maxAmount'] = vm.maxAmount = parseFloat(data.maxLimit);
});
}
}
}
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();
@ -112,7 +113,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
var reOp = /^[\*\+\-\/]$/;
if (!isAndroid && !isIos) {
var disableKeys = angular.element($window).on('keydown', function(e) {
angular.element($window).on('keydown', function(e) {
if (!e.key) return;
if (e.which === 8) { // you can add others here inside brackets.
if (!altCurrencyModal) {
@ -135,10 +136,7 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
});
}
unitToSatoshi = config.unitToSatoshi;
satToUnit = 1 / unitToSatoshi;
unitDecimals = config.unitDecimals;
resetAmount();
processAmount();
@ -210,13 +208,48 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
var fromWallet = profileService.getWallet(passthroughParams.fromWalletId);
updateAvailableFundsFromWallet(fromWallet);
}
if (passthroughParams.thirdParty) {
vm.thirdParty = passthroughParams.thirdParty; // Parse stringified JSON-object
if (vm.thirdParty) {
initShapeshift();
}
}
}
}
function goBack() {
$ionicHistory.goBack();
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();
@ -232,8 +265,28 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
}
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() {
@ -353,8 +406,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
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)
@ -374,8 +427,8 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
} 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)
@ -449,13 +502,13 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
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,
@ -467,12 +520,11 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
confirmData.thirdParty = vm.thirdParty;
}
sendFlowService.pushState(confirmData);
if (!confirmData.fromWalletId) {
$state.transitionTo('tabs.paymentRequest.confirm', confirmData);
} else {
$state.transitionTo('tabs.send.review', confirmData);
$scope.useSendMax = null;
sendFlowService.goNext(confirmData);
useSendMax = false;
}
}
@ -590,18 +642,73 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
}
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;
});
}
});
@ -609,37 +716,59 @@ function amountController(configService, $filter, gettextCatalog, $ionicHistory,
}
}
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,12 +1,20 @@
describe('amountController', function(){
var configCache,
configService,
configService,
gettextCatalog,
$controller,
$ionicHistory,
$rootScope,
ongoingProcess,
platformInfo,
popupService,
profileService,
rateService,
sendFlowService,
shapeshiftService,
txFormatService,
$scope,
$state,
$stateParams;
@ -18,7 +26,7 @@ describe('amountController', function(){
configCache = {
wallet: {
settings: {
unitToSatoshi: 100000000
}
}
};
@ -31,18 +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
};
profileService = jasmine.createSpyObj(['getWallets']);
popupService = jasmine.createSpyObj(['showAlert']);
profileService = jasmine.createSpyObj(['getWallet', 'getWallets']);
rateService = jasmine.createSpyObj(['fromFiat', 'whenAvailable']);
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_){
@ -61,6 +93,14 @@ describe('amountController', function(){
stateName: 'ignoreme'
};
$ionicHistory.backView.and.returnValue(backView);
var wallet = {
status: {
isValid: true,
spendableAmount: 123456
}
};
profileService.getWallet.and.returnValue(wallet);
profileService.getWallets.and.returnValue([{}]);
rateService.fromFiat.and.returnValue(12); // satoshis or coins?
@ -69,33 +109,496 @@ 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: {}
});
var data = {
stateParams: {
fromWalletId: 'fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b',
toAddress: 'qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'
}
var sendFlowState = {
fromWalletId: 'fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b',
toAddress: 'qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s'
};
$scope.$emit('$ionicView.beforeEnter', data);
expect($scope.fromWalletId).toBe('fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b');
expect($scope.toAddress).toBe('qrup46avn8t466xxwlzs4qelht7cnwvesv2e29wf7s');
sendFlowService.getStateClone.and.returnValue(sendFlowState);
$scope.$emit('$ionicView.beforeEnter', {});
//expect($scope.fromWalletId).toBe('fd56c1e7-e3ac-4fd9-8afc-27b9c1b3718b');
//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

@ -78,11 +78,15 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
function onBeforeEnter(event, data) {
console.log('walletSelector onBeforeEnter sendflow ', sendFlowService.state);
console.log('review onBeforeEnter sendflow ', sendFlowService.state);
defaults = configService.getDefaults();
sendFlowData = sendFlowService.getStateClone();
sendFlowData = sendFlowService.state.getClone();
originWalletId = sendFlowData.fromWalletId;
satoshis = parseInt(sendFlowData.amount, 10);
if (typeof sendFlowData.amount === 'string') {
satoshis = parseInt(sendFlowData.amount, 10);
} else {
satoshis = sendFlowData.amount;
}
toAddress = sendFlowData.toAddress;
destinationWalletId = sendFlowData.toWalletId;
@ -93,26 +97,48 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
if (sendFlowData.thirdParty) {
vm.thirdParty = sendFlowData.thirdParty;
handleThirdPartyInitIfBip70();
handleThirdPartyInitIfShapeshift();
switch (vm.thirdParty.id) {
case 'shapeshift':
initShapeshift(function (err) {
if (err) {
// Error stop here
ongoingProcess.set('connectingShapeshift', false);
popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.toString(), function () {
$ionicHistory.goBack();
});
} else {
_next(data);
}
});
break;
case 'bip70':
initBip70();
default:
_next(data);
break;
}
} else {
_next(data);
}
configService.get(function onConfig(err, configCache) {
if (err) {
$log.err('Error getting config.', err);
} else {
config = configCache;
priceDisplayIsFiat = config.wallet.settings.priceDisplay === 'fiat';
vm.origin.currencyColor = (vm.originWallet.coin === 'btc' ? defaults.bitcoinWalletColor : defaults.bitcoinCashWalletColor);
console.log("coin", vm.originWallet.coin, vm.origin.currencyColor, config.bitcoinWalletColor, vm.originWallet.coin === 'btc');
unitFromSat = 1 / config.wallet.settings.unitToSatoshi;
}
updateSendAmounts();
getOriginWalletBalance(vm.originWallet);
handleDestinationAsAddress(toAddress, coin);
handleDestinationAsWallet(sendFlowData.toWalletId);
createVanityTransaction(data);
});
function _next() {
configService.get(function onConfig(err, configCache) {
if (err) {
$log.err('Error getting config.', err);
} else {
config = configCache;
priceDisplayIsFiat = config.wallet.settings.priceDisplay === 'fiat';
vm.origin.currencyColor = (vm.originWallet.coin === 'btc' ? defaults.bitcoinWalletColor : defaults.bitcoinCashWalletColor);
console.log("coin", vm.originWallet.coin, vm.origin.currencyColor, config.bitcoinWalletColor, vm.originWallet.coin === 'btc');
unitFromSat = 1 / config.wallet.settings.unitToSatoshi;
}
updateSendAmounts();
getOriginWalletBalance(vm.originWallet);
handleDestinationAsAddress(toAddress, coin);
handleDestinationAsWallet(sendFlowData.toWalletId);
createVanityTransaction(data);
});
}
}
vm.approve = function() {
@ -403,7 +429,7 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
}
function goBack() {
$ionicHistory.goBack();
sendFlowService.router.goBack();
}
function handleDestinationAsAddress(address, originCoin) {
@ -458,72 +484,62 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
vm.destination.balanceCurrency = balanceText.currency;
}
function handleThirdPartyInitIfBip70() {
if (vm.thirdParty.id === 'bip70') {
vm.sendingTitle = gettextCatalog.getString('You are paying');
vm.memo = vm.thirdParty.memo;
vm.memoExpanded = !!vm.memo;
vm.destination.name = vm.thirdParty.name;
function initBip70() {
vm.sendingTitle = gettextCatalog.getString('You are paying');
vm.memo = vm.thirdParty.memo;
vm.memoExpanded = !!vm.memo;
vm.destination.name = vm.thirdParty.name;
txPayproData = {
caTrusted: vm.thirdParty.caTrusted,
domain: vm.thirdParty.domain,
expires: vm.thirdParty.expires,
toAddress: toAddress,
url: vm.thirdParty.url,
verified: vm.thirdParty.verified,
};
}
txPayproData = {
caTrusted: vm.thirdParty.caTrusted,
domain: vm.thirdParty.domain,
expires: vm.thirdParty.expires,
toAddress: toAddress,
url: vm.thirdParty.url,
verified: vm.thirdParty.verified,
};
}
function handleThirdPartyInitIfShapeshift() {
if (vm.thirdParty.id === 'shapeshift') {
vm.sendingTitle = gettextCatalog.getString('You are shifting');
if (!vm.thirdParty.data) {
vm.thirdParty.data = {};
}
function initShapeshift(cb) {
vm.sendingTitle = gettextCatalog.getString('You are shifting');
if (!vm.thirdParty.data) {
vm.thirdParty.data = {};
}
var toWallet = profileService.getWallet(destinationWalletId);
vm.destination.name = toWallet.name;
vm.destination.color = toWallet.color;
vm.destination.currency = toWallet.coin.toUpperCase();
var toWallet = profileService.getWallet(destinationWalletId);
vm.destination.name = toWallet.name;
vm.destination.color = toWallet.color;
vm.destination.currency = toWallet.coin.toUpperCase();
ongoingProcess.set('connectingShapeshift', true);
walletService.getAddress(vm.originWallet, false, function onReturnWalletAddress(err, returnAddr) {
ongoingProcess.set('connectingShapeshift', true);
walletService.getAddress(vm.originWallet, false, function onReturnWalletAddress(err, returnAddr) {
if (err) {
return cb(err);
}
walletService.getAddress(toWallet, false, function onWithdrawalWalletAddress(err, withdrawalAddr) {
if (err) {
ongoingProcess.set('connectingShapeshift', false);
popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.toString(), function () {
$ionicHistory.goBack();
});
return;
return cb(err);
}
walletService.getAddress(toWallet, false, function onWithdrawalWalletAddress(err, withdrawalAddr) {
if (err) {
ongoingProcess.set('connectingShapeshift', false);
popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.toString(), function () {
$ionicHistory.goBack();
});
return;
}
shapeshiftService.shiftIt(vm.originWallet.coin, toWallet.coin, withdrawalAddr, returnAddr, function onShiftIt(err, shapeshiftData) {
if (err && err != null) {
ongoingProcess.set('connectingShapeshift', false);
popupService.showAlert(gettextCatalog.getString('Shapeshift Error'), err.toString(), function () {
$ionicHistory.goBack();
});
} else {
vm.memo = 'ShapeShift Order:\nhttps://www.shapeshift.io/#/status/' + shapeshiftData.orderId;
vm.memoExpanded = !!vm.memo;
tx.toAddress = shapeshiftData.toAddress;
vm.destination.address = toAddress;
vm.destination.kind = 'shapeshift';
}
});
// Need to use the correct service to do it.
var amount = parseFloat(satoshis / 100000000);
shapeshiftService.shiftIt(vm.originWallet.coin, toWallet.coin, withdrawalAddr, returnAddr, amount, function onShiftIt(err, shapeshiftData) {
if (err) {
return cb(err);
} else {
vm.destination.kind = 'shapeshift';
vm.destination.address = toAddress;
tx.toAddress = shapeshiftData.toAddress;
vm.memo = 'ShapeShift Order:\nhttps://www.shapeshift.io/#/status/' + shapeshiftData.orderId;
vm.memoExpanded = !!vm.memo;
ongoingProcess.set('connectingShapeshift', false);
cb();
}
});
});
}
});
}
function onShareTransaction() {
@ -766,8 +782,12 @@ function reviewController(addressbookService, bitcoinCashJsService, bitcore, bit
((processName === 'signingTx') && vm.originWallet.m > 1) ||
(processName == 'sendingTx' && !vm.originWallet.canSign() && !vm.originWallet.isPrivKeyExternal())
) && !isOn) {
// Show the popup
vm.sendStatus = 'success';
// Clear the send flow service state
sendFlowService.state.clear();
if ($state.current.name === "tabs.send.review") { // XX SP: Otherwise all open wallets on other devices play this sound if you have been in a send flow before on that device.
soundService.play('misc/payment_sent.mp3');
}

View file

@ -6,22 +6,6 @@ angular.module('copayApp.controllers').controller('shapeshiftController', functi
$scope.showMyAddress = showMyAddress;
function generateAddress(wallet, cb) {
if (!wallet) return;
walletService.getAddress(wallet, false, function(err, addr) {
if (err) {
popupService.showAlert(err);
}
return cb(addr);
});
}
function showToWallets() {
$scope.toWallets = $scope.fromWallet.coin === 'btc' ? walletsBch : walletsBtc;
$scope.onToWalletSelect($scope.toWallets[0]);
$scope.singleToWallet = $scope.toWallets.length === 1;
}
$scope.$on("$ionicView.beforeEnter", function(event, data) {
walletsBtc = profileService.getWallets({coin: 'btc'});
walletsBch = profileService.getWallets({coin: 'bch'});
@ -62,18 +46,7 @@ angular.module('copayApp.controllers').controller('shapeshiftController', functi
id: 'shapeshift'
}
};
// Starting new send flow, so ensure everything is reset
sendFlowService.clear();
$state.go('tabs.home').then(function() {
$ionicHistory.clearHistory();
$state.go('tabs.send').then(function() {
$timeout(function () {
sendFlowService.pushState(stateParams);
$state.transitionTo('tabs.send.origin');
}, 60);
});
});
sendFlowService.start(stateParams);
}
function showMyAddress() {

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,31 +108,24 @@ 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);
};
$scope.startFreshSend = function() {
sendFlowService.clear();
$state.go('tabs.send');
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

@ -18,10 +18,10 @@ angular.module('copayApp.controllers').controller('tabReceiveController', functi
$scope.displayBalanceAsFiat = true;
$scope.requestSpecificAmount = function() {
sendFlowService.pushState({
toWalletId: $scope.wallet.credentials.walletId
sendFlowService.start({
toWalletId: $scope.wallet.credentials.walletId,
isRequestAmount: true
});
$state.go('tabs.paymentRequest.amount');
};
$scope.setAddress = function(newAddr, copyAddress) {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tabScanController', function($scope, $log, $timeout, scannerService, incomingData, $state, $ionicHistory, $rootScope, $ionicNavBarDelegate) {
angular.module('copayApp.controllers').controller('tabScanController', function(gettextCatalog, popupService, $scope, $log, $timeout, scannerService, incomingDataService, $state, $ionicHistory, $rootScope, $ionicNavBarDelegate) {
var scannerStates = {
unauthorized: 'unauthorized',
@ -111,7 +111,18 @@ angular.module('copayApp.controllers').controller('tabScanController', function(
// Sometimes (testing in Chrome, when reading QR Code) data is an object
// that has a string data.result.
contents = contents.result || contents;
incomingData.redir(contents);
incomingDataService.redir(contents, function onError(err) {
if (err) {
var title = gettextCatalog.getString('Scan Failed');
popupService.showAlert(title, err.message, function onAlertShown() {
// Enable another scan since we won't receive incomingDataMenu.menuHidden
activate();
});
} else {
scannerService.resumePreview();
}
});
}
$rootScope.$on('incomingDataMenu.menuHidden', function() {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('tabSendController', function($scope, $rootScope, $log, $timeout, $ionicScrollDelegate, $ionicLoading, addressbookService, profileService, lodash, $state, walletService, incomingData, popupService, platformInfo, sendFlowService, bwcError, gettextCatalog, scannerService, configService, bitcoinCashJsService, $ionicPopup, $ionicNavBarDelegate, clipboardService) {
angular.module('copayApp.controllers').controller('tabSendController', function(bitcoinUriService, $scope, $log, $timeout, $ionicScrollDelegate, addressbookService, profileService, lodash, $state, walletService, platformInfo, sendFlowService, gettextCatalog, configService, $ionicPopup, $ionicNavBarDelegate, clipboardService, incomingDataService) {
var clipboardHasAddress = false;
var clipboardHasContent = false;
var originalList;
@ -29,7 +29,7 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
$scope.$on("$ionicView.enter", function(event, data) {
var stateParams = sendFlowService.getStateClone();
var stateParams = sendFlowService.state.getClone();
$scope.fromWallet = profileService.getWallet(stateParams.fromWalletId);
clipboardService.readFromClipboard(function(text) {
@ -39,7 +39,9 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
$scope.clipboardHasAddress = false;
$scope.clipboardHasContent = false;
if ((text.indexOf('bitcoincash:') === 0 || text[0] === 'C' || text[0] === 'H' || text[0] === 'p' || text[0] === 'q') && text.replace('bitcoincash:', '').length === 42) { // CashAddr
var parsed = bitcoinUriService.parse(text);
console.log('parsed', parsed);
if (parsed.isValid && parsed.publicAddress && parsed.coin === 'bch' && !parsed.testnet) { // CashAddr
$scope.clipboardHasAddress = true;
} else if ((text[0] === "1" || text[0] === "3" || text.substring(0, 3) === "bc1") && text.length >= 26 && text.length <= 35) { // Legacy Addresses
$scope.clipboardHasAddress = true;
@ -60,11 +62,6 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
});
$scope.findContact = function(search) {
if (incomingData.redir(search)) {
return;
}
if (!search || search.length < 1) {
$scope.list = originalList;
$timeout(function() {
@ -73,12 +70,16 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
return;
}
var result = lodash.filter(originalList, function(item) {
var val = item.name;
return lodash.startsWith(val.toLowerCase(), search.toLowerCase());
var params = sendFlowService.state.getClone();
params.data = search;
sendFlowService.start(params, function onError() {
var result = lodash.filter(originalList, function(item) {
var val = item.name;
return lodash.startsWith(val.toLowerCase(), search.toLowerCase());
});
$scope.list = result;
});
$scope.list = result;
};
var hasWallets = function() {
@ -184,27 +185,18 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
$log.debug('Got toAddress:' + toAddress + ' | ' + item.name);
var stateParams = sendFlowService.getStateClone();
stateParams.toAddress = toAddress,
var stateParams = sendFlowService.state.getClone();
stateParams.toAddress = toAddress;
stateParams.coin = item.coin;
sendFlowService.pushState(stateParams);
if (!stateParams.fromWalletId) { // If we have no toAddress or fromWallet
$state.transitionTo('tabs.send.origin');
} else {
$state.transitionTo('tabs.send.amount');
}
sendFlowService.start(stateParams);
});
};
$scope.startWalletToWalletTransfer = function() {
console.log('startWalletToWalletTransfer()');
var params = sendFlowService.getStateClone();
sendFlowService.pushState(params);
$state.transitionTo('tabs.send.wallet-to-wallet', {
fromWalletId: sendFlowService.fromWalletId
});
var params = sendFlowService.state.getClone();
params.isWalletTransfer = true;
sendFlowService.start(params);
}
// This could probably be enhanced refactoring the routes abstract states
@ -238,7 +230,7 @@ angular.module('copayApp.controllers').controller('tabSendController', function(
});
if (data.direction == "back") {
sendFlowService.clear();
sendFlowService.state.clear();
}
});

View file

@ -1,11 +1,13 @@
'use strict';
angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingData, lodash, popupService, gettextCatalog, scannerService, sendFlowService) {
angular.module('copayApp.controllers').controller('tabsController', function($rootScope, $log, $scope, $state, $stateParams, $timeout, platformInfo, incomingDataService, lodash, popupService, gettextCatalog, scannerService, sendFlowService) {
$scope.onScan = function(data) {
if (!incomingData.redir(data)) {
popupService.showAlert(gettextCatalog.getString('Error'), gettextCatalog.getString('Invalid data'));
}
incomingDataService.redir(data, function onError(err) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err.message);
}
});
};
$scope.setScanFn = function(scanFn) {
@ -16,8 +18,7 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
};
$scope.startFreshSend = function() {
sendFlowService.clear();
$state.go('tabs.send');
sendFlowService.start();
};
$scope.importInit = function() {
@ -28,7 +29,6 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
};
$scope.chooseScanner = function() {
sendFlowService.clear();
var isWindowsPhoneApp = platformInfo.isCordova && platformInfo.isWP;
if (!isWindowsPhoneApp) {
@ -38,10 +38,14 @@ angular.module('copayApp.controllers').controller('tabsController', function($ro
scannerService.useOldScanner(function(err, contents) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
return;
popupService.showAlert(gettextCatalog.getString('Error'), err.message);
} else {
incomingDataService.redir(contents, function onError(err) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err.message);
}
});
}
incomingData.redir(contents);
});
};

View file

@ -55,34 +55,11 @@ 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);
};
var setPendingTxps = function(txps) {
/* Uncomment to test multiple outputs */
// var txp = {
// message: 'test multi-output',
// fee: 1000,
// createdOn: new Date() / 1000,
// outputs: [],
// wallet: $scope.wallet
// };
//
// function addOutput(n) {
// txp.outputs.push({
// amount: 600,
// toAddress: '2N8bhEwbKtMvR2jqMRcTCQqzHP6zXGToXcK',
// message: 'output #' + (Number(n) + 1)
// });
// };
// lodash.times(15, addOutput);
// txps.push(txp);
if (!txps) {
$scope.txps = [];
return;
@ -359,103 +336,36 @@ 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;
});
$scope.$on("$ionicView.beforeEnter", function(event, data) {
sendFlowService.clear();
configService.whenAvailable(function (config) {
$scope.selectedPriceDisplay = config.wallet.settings.priceDisplay;
@ -491,11 +401,12 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
var refreshInterval;
$scope.$on("$ionicView.afterEnter", function(event, data) {
updateTxHistoryFromCachedData();
$scope.updateAll(false, true);
refreshAmountSection();
$scope.updateAll();
// refreshAmountSection();
refreshInterval = $interval($scope.onRefresh, 10 * 1000);
//refreshInterval = $interval($scope.onRefresh, 120 * 1000); // For testing
$timeout(function() {
getScrollPosition();
}, 1000);
});
$scope.$on("$ionicView.afterLeave", function(event, data) {
@ -555,16 +466,10 @@ angular.module('copayApp.controllers').controller('walletDetailsController', fun
}
$scope.goToSend = function() {
sendFlowService.startSend({
sendFlowService.start({
fromWalletId: $scope.wallet.id
});
// Go home first so that the Home tab works properly
$state.go('tabs.home').then(function () {
$ionicHistory.clearHistory();
$state.go('tabs.send');
});
};
$scope.goToReceive = function() {
$state.go('tabs.home', {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $rootScope, $state, $log, $ionicHistory, sendFlowService, configService, gettextCatalog, profileService, txFormatService) {
angular.module('copayApp.controllers').controller('walletSelectorController', function($scope, $state, sendFlowService, configService, gettextCatalog, profileService, txFormatService) {
var fromWalletId = '';
var priceDisplayAsFiat = false;
@ -12,31 +12,22 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu
function onBeforeEnter(event, data) {
if (data.direction == "back") {
sendFlowService.popState();
sendFlowService.state.pop();
}
console.log('walletSelector onBeforeEnter after back sendflow', sendFlowService.state);
$scope.params = sendFlowService.getStateClone();
$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;
switch($state.current.name) {
case 'tabs.send.wallet-to-wallet':
$scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
break;
case 'tabs.send.destination':
if ($scope.params.fromWalletId && !$scope.params.thirdParty) {
$scope.sendFlowTitle = gettextCatalog.getString('Transfer between wallets');
}
break;
default:
if (!$scope.params.thirdParty) {
$scope.sendFlowTitle = gettextCatalog.getString('Send');
}
// nop
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)
@ -99,21 +90,12 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu
$scope.requestAmountSecondary = fiatAmount;
$scope.requestCurrencySecondary = fiatCurrrency;
}
$scope.$apply();
}
});
}
}
function getNextStep(params) {
if (!params.toWalletId && !params.toAddress) { // If we have no toAddress or fromWallet
return 'tabs.send.destination';
} else if (!params.amount) { // If we have no amount
return 'tabs.send.amount';
} else { // If we do have them
return 'tabs.send.review';
}
}
function handleThirdPartyIfShapeshift() {
console.log($scope.thirdParty, $scope.coin);
if ($scope.thirdParty.id === 'shapeshift' && $scope.type === 'destination') { // Shapeshift wants to know the
@ -191,20 +173,17 @@ angular.module('copayApp.controllers').controller('walletSelectorController', fu
$scope.useWallet = function(wallet) {
var params = sendFlowService.getStateClone();
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.pushState(params);
var nextStep = getNextStep(params);
console.log('walletSelector nextStep', nextStep);
$state.transitionTo(nextStep, $scope.params);
sendFlowService.goNext(params);
};
$scope.goBack = function() {
$ionicHistory.goBack();
sendFlowService.router.goBack();
}
});

View file

@ -1,23 +1,28 @@
'use strict';
angular.module('copayApp.directives')
.directive('incomingDataMenu', function($timeout, $rootScope, $state, externalLinkService) {
.directive('incomingDataMenu', function($timeout, $rootScope, $state, externalLinkService, sendFlowService, bitcoinCashJsService) {
return {
restrict: 'E',
templateUrl: 'views/includes/incomingDataMenu.html',
link: function(scope, element, attrs) {
$rootScope.$on('incomingDataMenu.showMenu', function(event, data) {
$timeout(function() {
scope.data = data.data;
scope.type = data.type;
scope.showMenu = true;
scope.https = false;
scope.data = data;
if (scope.type === 'url') {
if (scope.data.indexOf('https://') === 0) {
scope.https = true;
}
if (scope.data.parsed.privateKey) {
scope.type = "privateKey";
} else if (scope.data.parsed.url) {
scope.type = "url";
} else if (scope.data.parsed.publicAddress) {
scope.type = "bitcoinAddress";
var prefix = scope.data.parsed.isTestnet ? 'bchtest:' : 'bitcoincash:';
scope.data.toAddress = (prefix + scope.data.parsed.publicAddress.cashAddr) || scope.data.parsed.publicAddress.legacy || scope.data.parsed.publicAddress.bitpay;
} else {
scope.type = "text";
}
scope.showMenu = true;
});
});
scope.hide = function() {
@ -28,18 +33,9 @@ angular.module('copayApp.directives')
externalLinkService.open(url);
};
scope.sendPaymentToAddress = function(bitcoinAddress) {
var noPrefixInAddress = 0;
if (bitcoinAddress.toLowerCase().indexOf('bitcoin') < 0) {
noPrefixInAddress = 1;
}
scope.showMenu = false;
$state.go('tabs.send').then(function() {
$timeout(function() {
$state.transitionTo('tabs.send.amount', {
toAddress: bitcoinAddress,
noPrefix: noPrefixInAddress
});
}, 50);
sendFlowService.start({
data: bitcoinAddress
});
};
scope.addToAddressBook = function(bitcoinAddress) {

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService, incomingData, ongoingProcess) {
angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function($interval, shapeshiftApiService, profileService, incomingDataService, ongoingProcess) {
return {
restrict: 'E',
transclude: true,
@ -111,7 +111,8 @@ angular.module('copayApp.directives').directive('shapeshiftCoinTrader', function
orderId: $scope.depositInfo.orderId
};
if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) {
// How to handle this
if (incomingDataService.redir(sendAddress, 'shapeshift', shapeshiftData)) {
ongoingProcess.set('connectingShapeshift', false);
return;
}

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

@ -0,0 +1,416 @@
'use strict';
// https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0072.mediawiki
(function(){
angular
.module('bitcoincom.services')
.factory('bitcoinUriService', bitcoinUriService);
function bitcoinUriService(bitcoinCashJsService, bwcService, $log) {
var bch = bitcoinCashJsService.getBitcoinCashJs();
var bitcore = bwcService.getBitcore();
var service = {
parse: parse
};
return service;
function bitpayAddrOnMainnet(address) {
var Address = bch.Address;
var BitpayFormat = Address.BitpayFormat;
var mainnet = bch.Networks.mainnet;
var result = null;
if (address[0] == 'C') {
try {
result = Address.fromString(address, mainnet, 'pubkeyhash', BitpayFormat);
} catch (e) {};
} else if (address[0] == 'H') {
try {
result = Address.fromString(address, mainnet, 'scripthash', BitpayFormat);
} catch (e) {};
}
return result;
}
function cashAddrOnMainnet(address) {
var Address = bch.Address;
var CashAddrFormat = Address.CashAddrFormat;
var mainnet = bch.Networks.mainnet;
var prefixed = 'bitcoincash:' + address;
var result = null;
if (address[0] == 'q') {
try {
result = Address.fromString(prefixed, mainnet, 'pubkeyhash', CashAddrFormat);
} catch (e) {};
} else if (address[0] == 'p') {
try {
result = Address.fromString(prefixed, mainnet, 'scripthash', CashAddrFormat);
} catch (e) {};
}
return result;
}
function cashAddrOnTestnet(address) {
var Address = bch.Address;
var CashAddrFormat = Address.CashAddrFormat;
var testnet = bch.Networks.testnet;
var prefixed = 'bchtest:' + address;
var result = null;
if (address[0] == 'q') {
try {
result = Address.fromString(prefixed, testnet, 'pubkeyhash', CashAddrFormat);
} catch (e) {};
} else if (address[0] == 'p') {
try {
result = Address.fromString(prefixed, testnet, 'scripthash', CashAddrFormat);
} catch (e) {};
}
return result;
}
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
};
}
/*
For parsing:
BIP21
BIP72
returns:
{
amount: '',
amountInSatoshis: 0,
bareUrl: '',
coin: '',
copayInvitation: '',
import: { // testnet info in root, coin info in root if available
data: '',
derivationPath: '',
hasPassphrase: false,
type: 1,
},
isValid: false,
label: '',
message: '',
other: {
somethingIDontUnderstand: 'Its value'
},
privateKey: {
encrypted: '',
wif: ''
}'',
publicAddress: {
bitpay: '',
cashAddr: '',
legacy: '',
},
req: {
"req-param0": '',
"req-param1": ''
},
testnet: false,
url: '' // For BIP70
}
Only fields that are present in the data are defined in the returned object. Both privateKey and publicAddress only have 1 field defined, if they exist at all.
The exception to this is the coin property, which is determined from other data, such as the prefix or address type.
*/
function parse(data) {
var parsed = {
isValid: false
};
if (typeof data !== 'string') {
return parsed;
}
// Identify prefix
var trimmed = data.trim();
var colonSplit = /^([\w-]*):?(.*)$/.exec(trimmed);
if (!colonSplit) {
return parsed;
}
var addressAndParams = '';
var preColonLower = colonSplit[1].toLowerCase();
if (preColonLower === 'bitcoin') {
parsed.coin = 'btc';
addressAndParams = colonSplit[2].trim();
console.log('Is btc');
} else if (/^(?:bitcoincash)|(?:bitcoin-cash)$/.test(preColonLower)) {
parsed.coin = 'bch';
parsed.test = false;
addressAndParams = colonSplit[2].trim();
console.log('Is bch');
} else if (/^(?:bchtest)$/.test(preColonLower)) {
parsed.coin = 'bch';
parsed.isTestnet = true;
addressAndParams = colonSplit[2].trim();
console.log('Is bch');
} else if (colonSplit[2] === '') {
// No colon and no coin specifier.
addressAndParams = colonSplit[1].trim();
console.log('No prefix.');
} else if (/^https?$/.test(colonSplit[1])) { // Plain URL
addressAndParams = trimmed;
} else if (colonSplit[2].indexOf('|') == 0) { // Import
addressAndParams = trimmed
} else {
// Something we don't recognise
return parsed;
}
// Remove erroneous leading slashes
//var leadingSlashes = /^\/*([^\/]+(?:.*))$/.exec(addressAndParams);
var leadingSlashes = /^\/*(.*)$/.exec(addressAndParams);
if (!leadingSlashes) {
return parsed;
}
addressAndParams = leadingSlashes[1];
var questionMarkSplit = /^([^\?]*)\??([^\?]*)$/.exec(addressAndParams);
if (!questionMarkSplit) {
return parsed;
}
var address = questionMarkSplit[1];
var params = questionMarkSplit[2];
if (params.length > 0) {
var paramsSplit = params.split('&');
var others;
var req;
var paramCount = paramsSplit.length;
for(var i = 0; i < paramCount; i++) {
var param = paramsSplit[i];
var valueSplit = param.split('=');
if (valueSplit.length !== 2) {
return parsed;
}
var key = valueSplit[0];
var value = valueSplit[1];
var decodedValue = decodeURIComponent(value);
switch(key) {
case 'amount':
var amount = parseFloat(decodedValue);
if (amount) { // Checking for NaN, or no numbers at all etc. & convert to satoshi
parsed.amount = decodedValue; // Need to check if a currency is precised
parsed.amountInSatoshis = amount * 100000000
} else {
return parsed;
}
break;
case 'label':
parsed.label = decodedValue;
break;
case 'message':
parsed.message = decodedValue;
break;
case 'r':
// Could use a more comprehesive regex to test URL validity, but then how would we know
// which part of the validation it failed?
if (decodedValue.startsWith('https://')) {
parsed.url = decodedValue;
} else {
return parsed;
}
break;
default:
if (key.startsWith('req-')) {
req = req || {};
req[key] = decodedValue;
} else {
others = others || {};
others[key] = decodedValue;
}
}
};
}
parsed.others = others;
parsed.req = req;
if (address) {
var addressLowerCase = address.toLowerCase();
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 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}$/;
var privateKeyForCompressedPublicKeyRe = /^[KL][1-9A-HJ-NP-Za-km-z]{51}$/;
var privateKeyForCompressedPublicKeyTestnetRe = /^[c][1-9A-HJ-NP-Za-km-z]{51}$/;
var urlRe = /^https?:\/\/.+/;
var bitpayAddrMainnet = bitpayAddrOnMainnet(address);
var cashAddrTestnet = cashAddrOnTestnet(addressLowerCase);
var cashAddrMainnet = cashAddrOnMainnet(addressLowerCase);
var importInfo = infoFromWalletImportText(address);
var privateKey = '';
if (parsed.isTestnet && cashAddrTestnet) {
parsed.address = addressLowerCase;
parsed.coin = 'bch';
parsed.publicAddress = {
cashAddr: addressLowerCase
};
parsed.isValid = true;
} else if (cashAddrMainnet) {
parsed.coin = 'bch';
parsed.publicAddress = {
cashAddr: addressLowerCase
};
parsed.isTestnet = false;
parsed.isValid = true;
} else if (bitcore.Address.isValid(address, 'livenet')) {
parsed.publicAddress = {
legacy: address
};
parsed.isTestnet = false;
parsed.isValid = true;
} else if (bitcore.Address.isValid(address, 'testnet')) {
parsed.publicAddress = {
legacy: address
};
parsed.isTestnet = true;
parsed.isValid = true;
} else if (bitpayAddrMainnet) {
parsed.coin = 'bch';
parsed.publicAddress = {
bitpay: address
};
parsed.isTestnet = false;
parsed.isValid = true;
} else if (copayInvitationRe.test(address) ) {
parsed.copayInvitation = address;
parsed.isValid = true;
} else if (privateKeyForUncompressedPublicKeyRe.test(address) || privateKeyForCompressedPublicKeyRe.test(address)) {
privateKey = address;
try {
new bitcore.PrivateKey(privateKey, 'livenet');
parsed.privateKey = { wif: privateKey };
parsed.isTestnet = false;
parsed.isValid = true;
} catch (e) {}
} else if (privateKeyForUncompressedPublicKeyTestnetRe.test(address) || privateKeyForCompressedPublicKeyTestnetRe.test(address)) {
privateKey = address;
try {
new bitcore.PrivateKey(privateKey, 'testnet');
parsed.privateKey = { wif: privateKey };
parsed.isTestnet = true;
parsed.isValid = true;
} catch (e) {}
} else if (privateKeyEncryptedRe.test(address)) {
parsed.privateKey = { encrypted: address };
parsed.isValid = true;
} 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 {
parsed.isValid = !!parsed.url; // BIP72
}
return parsed;
}
}
})();

View file

@ -0,0 +1,429 @@
describe('bitcoinUriService', function() {
var bitcoinUriService;
beforeEach(function() {
module('bitcoinCashJsModule');
module('bitcoincom.services');
module('bwcModule');
inject(function($injector){
bitcoinUriService = $injector.get('bitcoinUriService');
});
});
it('Bitcoin BIP72', function() {
var parsed = bitcoinUriService.parse('bitcoin:?r=https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.isTestnet).toBeUndefined();
expect(parsed.publicAddress).toBeUndefined();
expect(parsed.url).toBe('https://bitpay.com/i/CwzbKP3k3JNgXJBfuoerDr');
});
it('Bitcoin Cash BIP72', function() {
var parsed = bitcoinUriService.parse('bitcoincash:?r=https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress).toBeUndefined();
expect(parsed.isTestnet).toBeUndefined();
expect(parsed.url).toBe('https://bitpay.com/i/SmHdie5dvBnG5kouZzEPzu');
});
it('Bitcoin Cash prefix with legacy address', function() {
var parsed = bitcoinUriService.parse('bitcoincash:1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.legacy).toBe('1G9FA9fFnHfTYxvmXeAbBD9FwzPAVMbd3j');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin Cash prefix with legacy address on testnet', function() {
var parsed = bitcoinUriService.parse('bitcoincash:mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.legacy).toBe('mkDQrKfSFD441JxrD1iPBsJFExgkvrPGQn');
expect(parsed.isTestnet).toBe(true);
});
it('Bitcoin Cash uri with extended params', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc?unknown=something&mystery=Melton%20probang&req-one=ichi&req-beta=Ni%20san');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.others.mystery).toBe('Melton probang');
expect(parsed.others.unknown).toBe('something');
expect(parsed.publicAddress.cashAddr).toBe('qr8v2vqnzntykakht43rqmxq8cdjzjp795fc3vsjgc');
expect(parsed.req['req-beta']).toBe('Ni san');
expect(parsed.req['req-one']).toBe('ichi');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin Cash uri with invalid amount', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qq0knhwj4d5zy3kdph24w6etq58vwzua6sm7lhcmuk?amount=three');
expect(parsed.isValid).toBe(false);
});
it('Bitcoin testnet address', function() {
var parsed = bitcoinUriService.parse('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBeUndefined();
expect(parsed.publicAddress.legacy).toBe('mtWcoToWhbtPoCby5fvs8xdBujT5GGenD4');
expect(parsed.isTestnet).toBe(true);
});
it('Bitcoin uri', function() {
var parsed = bitcoinUriService.parse('bitcoin:15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('15yCdKWVKRvfXMJpPYZBqMhiGKwjKzZdLN');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with encoded label', function() {
var parsed = bitcoinUriService.parse('bitcoin:1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT?label=Mr.%20Smith');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.label).toBe('Mr. Smith');
expect(parsed.publicAddress.legacy).toBe('1MxudKDEBWZ1yjizUSf6htacenNtb3DWbT');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with params', function() {
var parsed = bitcoinUriService.parse('bitcoin:12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu?amount=20.3&label=Luke-Jr&message=Donation%20for%20project%20xyz');
expect(parsed.isValid).toBe(true);
expect(parsed.amount).toBe('20.3');
expect(parsed.amountInSatoshis).toBe(2030000000);
expect(parsed.coin).toBe('btc');
expect(parsed.label).toBe('Luke-Jr');
expect(parsed.publicAddress.legacy).toBe('12nCRhMDfxVnuF3uYMXv2fNxBohNmacfWu');
expect(parsed.message).toBe('Donation for project xyz');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with slash', function() {
var parsed = bitcoinUriService.parse('bitcoin:/1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('1GhpYmbRaf73AZRxDwAGr6653iZBGzdgeA');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with slashes', function() {
var parsed = bitcoinUriService.parse('bitcoin://18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('18PCPhgZJjLxe9g3Q1BXLpL5aVut1fW3aX');
expect(parsed.isTestnet).toBe(false);
});
it('Bitcoin uri with space', function() {
var parsed = bitcoinUriService.parse('bitcoin: 19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('btc');
expect(parsed.publicAddress.legacy).toBe('19cPoKU5ZazY8NkLEsxK7drBqJnpGkax3d');
expect(parsed.isTestnet).toBe(false);
});
it('Bitpay without prefix', function() {
var parsed = bitcoinUriService.parse('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.bitpay).toBe('CJoRov8TirekvajiimQpb5Hk95evA7H2Yz');
expect(parsed.isTestnet).toBe(false);
});
it('legacy address', function() {
var parsed = bitcoinUriService.parse('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBeUndefined();
expect(parsed.publicAddress.legacy).toBe('1JXeGEu7bNEAYu6URT6dU6g1Ys6ffSAWYW');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr testnet with prefix', function() {
var parsed = bitcoinUriService.parse('bchtest:qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpcz6pmurq9ctg5848trzz9zmuuygj4q5qam7ph3gt');
expect(parsed.isTestnet).toBe(true);
});
it('cashAddr uppercase', function() {
var parsed = bitcoinUriService.parse('BITCOINCASH:QZZG9NMC5VX8GAP6XFATX3TWNSDN2YRMCSSULSMY44');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qzzg9nmc5vx8gap6xfatx3twnsdn2yrmcssulsmy44');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with dash', function() {
var parsed = bitcoinUriService.parse('bitcoin-cash:qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpshfu3dk5s3e7zdcgdcun6xgxtra6uyxs7g580js0');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with prefix', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qrq9p82a247lecv08ldk5p5h6ahtnjzpqcnh8yhq92');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with slash', function() {
var parsed = bitcoinUriService.parse('bitcoincash:/qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qzdectfmuw0xxztfx7mh045830dqcshj85hr44l35a');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with slashes', function() {
var parsed = bitcoinUriService.parse('bitcoincash://qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpj966w8utue75lqqq3rlgh20zkz3rmydqpq8syv9c');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with space', function() {
var parsed = bitcoinUriService.parse('bitcoincash: qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qpar9ldle8z6alcwgclejdhc24ha2xrg0szs5802ce');
expect(parsed.isTestnet).toBe(false);
});
it('cashAddr with space on testnet', function() {
var parsed = bitcoinUriService.parse('bchtest: qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qqjxkmtaxk4nv6w9h5ht2fjcj9c7ruh0fu7cnxsx5j');
expect(parsed.isTestnet).toBe(true);
});
it('cashAddr without prefix', function() {
var parsed = bitcoinUriService.parse('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq');
expect(parsed.isValid).toBe(true);
expect(parsed.coin).toBe('bch');
expect(parsed.publicAddress.cashAddr).toBe('qqen2y3l28dpk0dzsag8w027ds96u7z4pc0uxtl0nq');
expect(parsed.isTestnet).toBe(false);
});
it('copay invitation', function() {
var parsed = bitcoinUriService.parse('PD5B7rEEj72st9d5nFszyuKxJP6FAGS7idVC2SMqiMxUcWVd8JifZDJw1UgjUctxefUFE3Sz6qLbch');
expect(parsed.isValid).toBe(true);
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');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 2', function() {
var parsed = bitcoinUriService.parse('p:gpf8m4h7');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 3', function() {
var parsed = bitcoinUriService.parse('bitcoincash:qpzry9x8gf2tvdw0s3jn54khce6mua7lcw20ayyn');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 4', function() {
var parsed = bitcoinUriService.parse('bchtest:testnetaddress4d6njnut');
expect(parsed.isValid).toBe(false);
});
it('invalid cashAddr style 5', function() {
var parsed = bitcoinUriService.parse('bchreg:555555555555555555555555555555555555555555555udxmlmrz');
expect(parsed.isValid).toBe(false);
});
it('non-string', function() {
var parsed = bitcoinUriService.parse([1, 2, 3, 4]);
expect(parsed.isValid).toBe(false);
});
it('private key encrypted with BIP38', function() {
var parsed = bitcoinUriService.parse('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.encrypted).toBe('6PRN5nEDmX842gsBzJryPu8Tw5kcsaQq1GPLcjVQPcEStvbFAtz11JX9pX');
});
it('private key for compressed pubkey mainnet', function() {
var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ');
expect(parsed.isTestnet).toBe(false);
});
it('private key for compressed pubkey mainnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTu');
expect(parsed.isValid).toBe(false);
});
it('private key for compressed pubkey testnet', function() {
var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMm');
expect(parsed.isTestnet).toBe(true);
});
it('private key for compressed pubkey testnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('cNJFgo1driFnPcBdBX8BrJrpxchBWXwXCvNH5SoSkdcF6JXXwHMM');
expect(parsed.isValid).toBe(false);
});
it('private key for uncompressed pubkey mainnet', function() {
var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTMwx');
expect(parsed.isTestnet).toBe(false);
});
it('private key for uncompressed pubkey mainnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('L18V3rAhCKEioPnJ4BHLCCsaYa8eSNFrMjNQ2EdwgeAdmBSnTTwx');
expect(parsed.isValid).toBe(false);
});
it('private key for uncompressed pubkey testnet', function() {
var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc');
expect(parsed.isValid).toBe(true);
expect(parsed.privateKey.wif).toBe('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcc');
expect(parsed.isTestnet).toBe(true);
});
it('private key for uncompressed pubkey testnet with wrong checksum', function() {
var parsed = bitcoinUriService.parse('92Pg46rUhgTT7romnV7iGW6W1gbGdeezqdbJCzShkCsYNzyyNcC');
expect(parsed.isValid).toBe(false);
});
it('URL only, http', function() {
var parsed = bitcoinUriService.parse('http://paperwallet.bitcoin.com');
expect(parsed.isValid).toBe(true);
expect(parsed.bareUrl).toBe('http://paperwallet.bitcoin.com');
});
it('URL only, https with query', function() {
var parsed = bitcoinUriService.parse('https://purse.io/?one=two&three=four');
expect(parsed.isValid).toBe(true);
expect(parsed.bareUrl).toBe('https://purse.io/?one=two&three=four');
});
});

View file

@ -0,0 +1,79 @@
'use strict';
/**
* incomingDataService is an intermediate to redirect either to the sendFlow
* or to import/join a wallet.
*/
angular.module('copayApp.services').factory('incomingDataService', function(bitcoinUriService, $log, $state, $rootScope, scannerService, sendFlowService, gettextCatalog) {
var root = {};
root.showMenu = function(data) {
$rootScope.$broadcast('incomingDataMenu.showMenu', data);
};
root.redir = function(data, cbError) {
var parsed = bitcoinUriService.parse(data);
console.log(parsed);
$log.debug(parsed);
if (parsed.isValid) {
if (parsed.isTestnet) {
if (cbError) {
var errorMessage = gettextCatalog.getString('Testnet is not supported.');
cbError(new Error(errorMessage));
}
} else {
scannerService.pausePreview();
/**
* Strategy for the action
*/
if (parsed.copayInvitation) {
$state.go('tabs.home').then(function() {
$state.transitionTo('tabs.add.join', {
url: data
});
});
} else if (parsed.import) {
$state.go('tabs.home').then(function() {
$state.transitionTo('tabs.add.import', {
code: data
});
});
} else if (
!parsed.isValid
|| parsed.privateKey
|| (sendFlowService.state.isEmpty() && !parsed.url && !parsed.amount)
) {
root.showMenu({
original: data,
parsed: parsed
});
} else {
var state = sendFlowService.state.getClone();
state.data = data;
sendFlowService.start(state, function onError(err) {
/**
* OnError, open the menu (link not validated)
*/
root.showMenu({
original: data,
parsed: parsed
});
});
}
}
} else {
if (cbError) {
var errorMessage = gettextCatalog.getString('Data not recognised.');
cbError(new Error(errorMessage));
}
}
};
return root;
});

View file

@ -1,475 +0,0 @@
'use strict';
angular.module('copayApp.services').factory('incomingData', function($log, $state, $timeout, $ionicHistory, bitcore, bitcoreCash, $rootScope, payproService, scannerService, sendFlowService, appConfigService, popupService, gettextCatalog, bitcoinCashJsService) {
var root = {};
root.showMenu = function(data) {
$rootScope.$broadcast('incomingDataMenu.showMenu', data);
};
root.redir = function(data, serviceId, serviceData) {
var originalAddress = null;
var noPrefixInAddress = 0;
if (data.toLowerCase().indexOf('bitcoin') < 0) {
noPrefixInAddress = 1;
}
if (typeof(data) == 'string' && !(/^bitcoin(cash)?:\?r=[\w+]/).exec(data) && (data.toLowerCase().indexOf('bitcoincash:') >= 0 || data[0] == 'q' || data[0] == 'p' || data[0] == 'C' || data[0] == 'H')) {
try {
noPrefixInAddress = 0;
if (data[0] == 'p' || data[0] == 'q') {
data = 'bitcoincash:' + data;
}
var paramString = '';
if (data.indexOf('?') >= 0) {
paramString = data.substring(data.indexOf('?'));
data = data.substring(0, data.indexOf('?'));
}
if (data.indexOf('BITCOINCASH:') >= 0) {
data = data.toLowerCase();
}
originalAddress = data.replace('bitcoincash:', '');
var legacyAddress = bitcoinCashJsService.readAddress(data).legacy;
data = 'bitcoincash:' + legacyAddress + paramString;
} catch (ex) {}
}
$log.debug('Processing incoming data: ' + data);
function sanitizeUri(data) {
// Fixes when a region uses comma to separate decimals
var regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i;
var match = regex.exec(data);
if (!match || match.length === 0) {
return data;
}
var value = match[0].replace(',', '.');
var newUri = data.replace(regex, value);
// mobile devices, uris like copay://glidera
newUri.replace('://', ':');
return newUri;
}
function getParameterByName(name, url) {
if (!url) return;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
function checkPrivateKey(privateKey) {
try {
new bitcore.PrivateKey(privateKey, 'livenet');
} catch (err) {
return false;
}
return true;
}
function goSend(addr, amount, message, coin, serviceId, serviceData) {
$state.go('tabs.send', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.send' ? false : true
});
// Timeout is required to enable the "Back" button
$timeout(function() {
var params = sendFlowService.getStateClone();
if (amount) {
params.amount = amount;
}
if (addr) {
params.toAddress = addr;
params.displayAddress = originalAddress ? originalAddress : addr;
}
if (coin) {
params.coin = coin;
}
if (noPrefixInAddress) {
params.noPrefixInAddress = noPrefixInAddress;
}
if (serviceId) {
params.thirdParty = [];
params.thirdParty.id = serviceId;
params.thirdParty.data = serviceData;
sendFlowService.pushState(params);
$state.transitionTo('tabs.send.amount');
} else {
sendFlowService.pushState(params);
$state.transitionTo('tabs.send.origin');
}
}, 100);
}
// data extensions for Payment Protocol with non-backwards-compatible request
if ((/^bitcoin(cash)?:\?r=[\w+]/).exec(data)) {
var coin = data.indexOf('bitcoincash') >= 0 ? 'bch' : 'btc';
data = decodeURIComponent(data.replace(/bitcoin(cash)?:\?r=/, ''));
if (coin == 'bch') {
payproService.getPayProDetailsViaHttp(data, function onGetPayProDetailsViaHttp(err, details) {
if (err) {
var message = err.toString();
if (typeof err.data === 'string') {
// i.e. 'This invoice is no longer accepting payments'
message = gettextCatalog.getString(err.data);
}
popupService.showAlert(gettextCatalog.getString('Error'), message)
} else {
handlePayPro(details, coin);
}
});
} else {
payproService.getPayProDetails(data, coin, function onGetPayProDetails(err, details) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
} else {
handlePayPro(details, coin);
}
});
}
return true;
}
data = sanitizeUri(data);
// Bitcoin URL
if (bitcore.URI.isValid(data)) {
var coin = 'btc';
var parsed = new bitcore.URI(data);
var addr = parsed.address ? parsed.address.toString() : '';
var message = parsed.message;
var amount = parsed.amount ? parsed.amount : '';
if (parsed.r) {
payproService.getPayProDetails(parsed.r, coin, function(err, details) {
if (err) {
if (addr && amount) goSend(addr, amount, message, coin, serviceId, serviceData);
else popupService.showAlert(gettextCatalog.getString('Error'), err);
} else handlePayPro(details, coin);
});
} else {
goSend(addr, amount, message, coin, serviceId, serviceData);
}
return true;
// Cash URI
} else if (bitcoreCash.URI.isValid(data)) {
var coin = 'bch';
var parsed = new bitcoreCash.URI(data);
var addr = parsed.address ? parsed.address.toString() : '';
var message = parsed.message;
var amount = parsed.amount ? parsed.amount : '';
// paypro not yet supported on cash
if (parsed.r) {
payproService.getPayProDetails(parsed.r, coin, function(err, details) {
if (err) {
if (addr && amount)
goSend(addr, amount, message, coin, serviceId, serviceData);
else
popupService.showAlert(gettextCatalog.getString('Error'), err);
}
handlePayPro(details, coin);
});
} else {
goSend(addr, amount, message, coin, serviceId, serviceData);
}
return true;
// Cash URI with bitcoin (btc) address version number?
} else if (bitcore.URI.isValid(data.replace(/^bitcoincash:/,'bitcoin:'))) {
$log.debug('Handling bitcoincash URI with legacy address');
var coin = 'bch';
var parsed = new bitcore.URI(data.replace(/^bitcoincash:/,'bitcoin:'));
var oldAddr = parsed.address ? parsed.address.toString() : '';
if (!oldAddr) return false;
var addr = '';
var a = bitcore.Address(oldAddr).toObject();
addr = bitcoreCash.Address.fromObject(a).toString();
// Translate address
$log.debug('address transalated to:' + addr);
popupService.showConfirm(
gettextCatalog.getString('Bitcoin cash Payment'),
gettextCatalog.getString('Payment address was translated to new Bitcoin Cash address format: ' + addr),
gettextCatalog.getString('OK'),
gettextCatalog.getString('Cancel'),
function(ret) {
if (!ret) return false;
var message = parsed.message;
var amount = parsed.amount ? parsed.amount : '';
// paypro not yet supported on cash
if (parsed.r) {
payproService.getPayProDetails(parsed.r, coin, function(err, details) {
if (err) {
if (addr && amount)
goSend(addr, amount, message, coin, serviceId, serviceData);
else
popupService.showAlert(gettextCatalog.getString('Error'), err);
}
handlePayPro(details, coin);
});
} else {
goSend(addr, amount, message, coin, serviceId, serviceData);
}
}
);
return true;
// Plain URL
} else if (/^https?:\/\//.test(data)) {
payproService.getPayProDetails(data, coin, function(err, details) {
if (err) {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'url'
});
}
return;
}
handlePayPro(details);
return true;
});
// Plain Address
} else if (bitcore.Address.isValid(data, 'livenet') || bitcore.Address.isValid(data, 'testnet')) {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'bitcoinAddress'
});
} else {
goToAmountPage(data);
}
} else if (bitcoreCash.Address.isValid(data, 'livenet')) {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'bitcoinAddress',
coin: 'bch',
});
} else {
goToAmountPage(data, 'bch');
}
} else if (data && data.indexOf(appConfigService.name + '://glidera') === 0) {
var code = getParameterByName('code', data);
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.transitionTo('tabs.buyandsell.glidera', {
code: code
});
});
return true;
} else if (data && data.indexOf(appConfigService.name + '://coinbase') === 0) {
var code = getParameterByName('code', data);
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$ionicHistory.nextViewOptions({
disableAnimate: true
});
$state.transitionTo('tabs.buyandsell.coinbase', {
code: code
});
});
return true;
// BitPayCard Authentication
} else if (data && data.indexOf(appConfigService.name + '://') === 0) {
// Disable BitPay Card
if (!appConfigService._enabledExtensions.debitcard) return false;
var secret = getParameterByName('secret', data);
var email = getParameterByName('email', data);
var otp = getParameterByName('otp', data);
var reason = getParameterByName('r', data);
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
switch (reason) {
default:
case '0':
/* For BitPay card binding */
$state.transitionTo('tabs.bitpayCardIntro', {
secret: secret,
email: email,
otp: otp
});
break;
}
});
return true;
// Join
} else if (data && data.match(/^copay:[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$state.transitionTo('tabs.add.join', {
url: data
});
});
return true;
// Old join
} else if (data && data.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/)) {
$state.go('tabs.home', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.home' ? false : true
}).then(function() {
$state.transitionTo('tabs.add.join', {
url: data
});
});
return true;
} else if (data && (data.substring(0, 2) == '6P' || checkPrivateKey(data))) {
root.showMenu({
data: data,
type: 'privateKey'
});
} else if (data && ((data.substring(0, 2) == '1|') || (data.substring(0, 2) == '2|') || (data.substring(0, 2) == '3|'))) {
$state.go('tabs.home').then(function() {
$state.transitionTo('tabs.add.import', {
code: data
});
});
return true;
} else {
if ($state.includes('tabs.scan')) {
root.showMenu({
data: data,
type: 'text'
});
}
}
return false;
};
function goToAmountPage(toAddress, coin) {
$state.go('tabs.send', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.send' ? false : true
});
$timeout(function() {
var stateParams = {
toAddress: toAddress,
displayAddress: toAddress,
coin: coin,
noPrefix: 1
};
sendFlowService.pushState(stateParams);
$state.transitionTo('tabs.send.origin');
}, 100);
}
function handlePayPro(payProData, coin) {
console.log(payProData);
var toAddr = payProData.toAddress;
var amount = payProData.amount;
var paymentUrl = payProData.url;
var expires = payProData.expires;
var time = payProData.time;
if (coin === 'bch') {
var displayAddr = payProData.outputs[0].address;
toAddr = bitcoinCashJsService.readAddress('bitcoincash:' + displayAddr).legacy;
amount = payProData.outputs[0].amount;
paymentUrl = payProData.paymentUrl;
expires = Math.floor(new Date(expires).getTime() / 1000)
time = Math.ceil(new Date(time).getTime() / 1000)
}
var name = payProData.domain;
if (payProData.memo.indexOf('eGifter') > -1) {
name = 'eGifter'
} else if (paymentUrl.indexOf('https://bitpay.com') > -1) {
name = 'BitPay';
}
var thirdPartyData = {
id: 'bip70',
amount: amount,
caTrusted: true,
name: name,
domain: payProData.domain,
expires: expires,
memo: payProData.memo,
network: 'livenet',
requiredFeeRate: payProData.requiredFeeRate,
selfSigned: 0,
time: time,
displayAddress: displayAddr,
toAddress: toAddr,
url: paymentUrl,
verified: true
};
var stateParams = {
amount: thirdPartyData.amount,
toAddress: thirdPartyData.toAddress,
coin: coin,
thirdParty: thirdPartyData
};
// fee
if (thirdPartyData.requiredFeeRate) {
stateParams.requiredFeeRate = thirdPartyData.requiredFeeRate * 1024;
}
// This does not make sense, thirdPartyData gets added by stateParams below
//sendFlowService.pushState(thirdPartyData);
scannerService.pausePreview();
$state.go('tabs.send', {}, {
'reload': true,
'notify': $state.current.name == 'tabs.send' ? false : true
}).then(function() {
$timeout(function() {
sendFlowService.pushState(stateParams); // Need to do more here
$state.transitionTo('tabs.send.origin');
});
});
}
return root;
});

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

@ -52,11 +52,7 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
root.clear = function() {
ongoingProcess = {};
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.hide();
} else {
$ionicLoading.hide();
}
$ionicLoading.hide();
};
root.get = function(processName) {
@ -82,23 +78,14 @@ angular.module('copayApp.services').factory('ongoingProcess', function($log, $ti
if (customHandler) {
customHandler(processName, showName, isOn);
} else if (root.onGoingProcessName) {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.show(null, showName, root.clear);
} else {
var tmpl;
if (isWindowsPhoneApp) tmpl = '<div>' + showName + '</div>';
else tmpl = '<div class="item-icon-left">' + showName + '<ion-spinner class="spinner-stable" icon="lines"></ion-spinner></div>';
$ionicLoading.show({
template: tmpl
});
}
var tmpl;
if (isWindowsPhoneApp) tmpl = '<div>' + showName + '</div>';
else tmpl = '<div class="item-icon-left">' + showName + '<ion-spinner class="spinner-stable" icon="lines"></ion-spinner></div>';
$ionicLoading.show({
template: tmpl,
});
} else {
if (isCordova && !isWindowsPhoneApp) {
window.plugins.spinnerDialog.hide();
} else {
$ionicLoading.hide();
}
$ionicLoading.hide();
}
};

View file

@ -1,6 +1,6 @@
'use strict';
angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingData, appConfigService) {
angular.module('copayApp.services').factory('openURLService', function($rootScope, $ionicHistory, $document, $log, $state, platformInfo, lodash, profileService, incomingDataService, appConfigService) {
var root = {};
var handleOpenURL = function(args) {
@ -23,9 +23,12 @@ angular.module('copayApp.services').factory('openURLService', function($rootScop
document.addEventListener('handleopenurl', handleOpenURL, false);
if (!incomingData.redir(url)) {
$log.warn('Unknown URL! : ' + url);
}
incomingDataService.redir(url, function onError(err) {
if (err) {
$log.warn('Unknown URL! : ' + url);
popupService.showAlert(gettextCatalog.getString('Error'), err.message);
}
});
};
var handleResume = function() {

View file

@ -0,0 +1,85 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('sendFlowRouterService', sendFlowRouterService);
function sendFlowRouterService(
sendFlowStateService
, $state, $ionicHistory, $timeout
) {
var service = {
// Functions
start: start,
goNext: goNext,
goBack: goBack,
};
return service;
/**
* Start new send flow
*/
function start() {
var state = sendFlowStateService.state;
if (state.isRequestAmount) {
$state.go('tabs.paymentRequest.amount');
} else {
if ($state.current.name != 'tabs.send') {
$state.go('tabs.home').then(function () {
$ionicHistory.clearHistory();
$state.go('tabs.send').then(function () {
$timeout(function () {
goNext();
}, 60);
});
});
} else {
goNext();
}
}
}
/**
* Go to the next page
* Routing strategy : https://bitcoindotcom.atlassian.net/wiki/x/BQDWKQ
*/
function goNext() {
var state = sendFlowStateService.state;
var needsDestination = !state.toWalletId && !state.toAddress;
var needsOrigin = !state.fromWalletId;
var needsAmount = !state.amount && !state.sendMax;
if (needsDestination) {
if (!state.isWalletTransfer && !state.thirdParty) {
$state.go('tabs.send');
return;
} else if (!needsOrigin) {
$state.go('tabs.send.destination');
return;
}
}
if (needsOrigin) {
$state.go('tabs.send.origin');
} else if (needsAmount) {
$state.go('tabs.send.amount');
} else {
$state.go('tabs.send.review');
}
}
/**
* Go to the previous page
*/
function goBack() {
$ionicHistory.goBack();
}
};
})();

View file

@ -0,0 +1,142 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('sendFlowStateService', sendFlowStateService);
function sendFlowStateService($log) {
var service = {
// Variables
state: {
amount: 0,
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: '',
coin: '',
isRequestAmount: false,
isWalletTransfer: false
},
previousStates: [],
// Functions
init: init,
clear: clear,
getClone: getClone,
map: map,
pop: pop,
push: push,
isEmpty: isEmpty
};
return service;
/**
* Init state & stack
* @param {Object} params
*/
function init(params) {
$log.debug("send-flow-state init()");
clear();
if (params) {
push(params);
}
}
/**
* Clear a state & stack
*/
function clear() {
$log.debug("send-flow-state clear()");
clearCurrent();
service.previousStates = [];
}
/**
* Clear current state only
*/
function clearCurrent() {
$log.debug("send-flow-state clearCurrent()");
service.state = {
amount: 0,
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: '',
coin: '',
isRequestAmount: false,
isWalletTransfer: false
}
}
/**
* Get a clone of the current state
*/
function getClone() {
var currentState = {};
Object.keys(service.state).forEach(function forCurrentParam(key) {
if (typeof service.state[key] !== 'function' && key !== 'previousStates') {
currentState[key] = service.state[key];
}
});
return currentState;
}
/**
* Fill in the current state from the params
* @param {Object} params
*/
function map(params) {
Object.keys(params).forEach(function forNewParam(key) {
service.state[key] = params[key];
});
}
/**
* Pop state
*/
function pop() {
$log.debug('send-flow-state pop');
if (service.previousStates.length) {
var params = service.previousStates.pop();
clearCurrent();
map(params);
} else {
clear();
}
}
/**
* Push state
* @param {Object} params
*/
function push(params) {
$log.debug('send-flow-state push');
var currentParams = getClone();
service.previousStates.push(currentParams);
clearCurrent();
map(params);
}
/**
* Is empty stack
*/
function isEmpty() {
return service.previousStates.length == 0;
}
};
})();

View file

@ -0,0 +1,148 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('sendFlowService', sendFlowService);
function sendFlowService(
sendFlowStateService, sendFlowRouterService
, bitcoinUriService, payproService, bitcoinCashJsService
, popupService, gettextCatalog
, $state, $log
) {
var service = {
// Variables
state: sendFlowStateService,
router: sendFlowRouterService,
// Functions
start: start,
goNext: goNext,
goBack: goBack
};
return service;
/**
* Start a new send flow
* @param {Object} params
* @param {Function} onError
*/
function start(params, onError) {
$log.debug('send-flow start()');
if (params && params.data) {
var res = bitcoinUriService.parse(params.data);
if (res.isValid) {
// If BIP70 (url)
if (res.url) {
var url = res.url;
var coin = res.coin || '';
payproService.getPayProDetails(url, coin, function onGetPayProDetails(err, payProData) {
if (err) {
popupService.showAlert(gettextCatalog.getString('Error'), err);
} else {
var name = payProData.domain;
// Detect some merchant that we know
if (payProData.memo.indexOf('eGifter') > -1) {
name = 'eGifter'
} else if (paymentUrl.indexOf('https://bitpay.com') > -1) {
name = 'BitPay';
}
// Init thirdParty
var thirdPartyData = {
id: 'bip70',
caTrusted: true,
name: name,
domain: payProData.domain,
expires: payProData.expires,
memo: payProData.memo,
network: 'livenet',
requiredFeeRate: payProData.requiredFeeRate,
selfSigned: 0,
time: payProData.time,
url: payProData.url,
verified: true
};
// Fill in params
params.amount = payProData.amount,
params.toAddress = payProData.toAddress,
params.coin = coin,
params.thirdParty = thirdPartyData
}
// Resolve
_next();
});
} else {
if (res.coin) {
params.coin = res.coin;
}
if (res.amountInSatoshis) {
params.amount = res.amountInSatoshis;
}
if (res.publicAddress) {
var prefix = res.isTestnet ? 'bchtest:' : 'bitcoincash:';
params.displayAddress = res.publicAddress.cashAddr || res.publicAddress.legacy || res.publicAddress.bitpay;
var formatAddress = res.publicAddress.cashAddr ? prefix + params.displayAddress : params.displayAddress;
params.toAddress = bitcoinCashJsService.readAddress(formatAddress).legacy;
}
_next();
}
} else {
if (onError) {
onError();
}
}
} else {
_next();
}
// Next used for sync the async task
function _next() {
sendFlowStateService.init(params);
// Routing strategy to -> send-flow-router.service
sendFlowRouterService.start();
}
}
/**
* Go to the next step
* @param {Object} state
*/
function goNext(state) {
$log.debug('send-flow goNext()');
// Save the current route before leaving
state.route = $state.current.name;
// Save the state and redirect the user
sendFlowStateService.push(state);
sendFlowRouterService.goNext();
}
/**
* Go to the previous step
*/
function goBack() {
$log.debug('send-flow goBack()');
// Remove the state on top and redirect the user
sendFlowStateService.pop();
sendFlowRouterService.goBack();
}
}
})();

View file

@ -1,103 +0,0 @@
'use strict';
(function(){
angular
.module('copayApp.services')
.factory('sendFlowService', sendFlowService);
function sendFlowService($log) {
var service = {
// A separate state variable so we can ensure it is cleared of everything,
// even other properties added that this service does not know about. (such as "coin")
state: {
amount: '',
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: ''
},
previousStates: [],
// Functions
clear: clear,
getStateClone: getStateClone,
map: map,
popState: popState,
pushState: pushState,
startSend: startSend
};
return service;
function clear() {
console.log("sendFlow clear()");
clearCurrent();
service.previousStates = [];
}
function clearCurrent() {
console.log("sendFlow clearCurrent()");
service.state = {
amount: '',
displayAddress: null,
fromWalletId: '',
sendMax: false,
thirdParty: null,
toAddress: '',
toWalletId: ''
}
}
/**
* Handy for debugging
*/
function getStateClone() {
var currentState = {};
Object.keys(service.state).forEach(function forCurrentParam(key) {
if (typeof service.state[key] !== 'function' && key !== 'previousStates') {
currentState[key] = service.state[key];
}
});
return currentState;
}
/**
* Clears all previous state
*/
function startSend(params) {
console.log('startSend()');
clear();
map(params);
}
function map(params) {
Object.keys(params).forEach(function forNewParam(key) {
service.state[key] = params[key];
});
};
function popState() {
console.log('sendFlow pop');
if (service.previousStates.length) {
var params = service.previousStates.pop();
clearCurrent();
map(params);
} else {
clear();
}
};
function pushState(params) {
console.log('sendFlow push');
var currentParams = getStateClone();
service.previousStates.push(currentParams);
clearCurrent();
map(params);
};
};
})();

View file

@ -328,18 +328,23 @@ angular.module('copayApp.services').factory('shapeshiftApiService', function($q)
$scope.amount, $scope.withdrawalAddress,
$scope.coinIn, $scope.coinOut
);
console.log('shapeshiftApiService.FixedAmountTx()');
console.log(fixedTx);
SSA.FixedAmountTx(fixedTx, function (data) {
console.log(data)
return promise.resolve({ fixedTxData : data.success });
console.log(data);
promise.resolve(data);
});
return promise.promise;
},
NormalTx : function($scope){
var promise = $q.defer();
var normalTx = SSA.CreateNormalTx($scope.withdrawalAddress, $scope.coinIn, $scope.coinOut);
console.log('shapeshiftApiService.NormalTx()');
console.log(normalTx);
SSA.NormalTx(normalTx, function (data) {
promise.resolve({ normalTxData : data });
console.log(data);
promise.resolve(data);
});
return promise.promise;
},
@ -360,11 +365,12 @@ angular.module('copayApp.services').factory('shapeshiftApiService', function($q)
return promise.promise;
},
ValidateAddress : function(address, coin) {
var promise = $q.defer();
SSA.ValidateAdddress(address, coin, function(data){
promise.resolve(data);
});
return promise.promise;
var promise = $q.defer();
SSA.ValidateAdddress(address, coin, function onRequest(data){
console.log(data);
promise.resolve(data);
});
return promise.promise;
}
};
});

View file

@ -0,0 +1,112 @@
'use strict';
(function(){
angular
.module('bitcoincom.services')
.factory('shapeshiftService', shapeshiftService);
function shapeshiftService(shapeshiftApiService, gettextCatalog) {
var service = {
// Variables
coinIn: '',
coinOut: '',
withdrawalAddress: '',
returnAddress: '',
amount: '',
marketData: {},
coins: {
'BTC': {name: 'Bitcoin', symbol: 'BTC'},
'BCH': {name: 'Bitcoin Cash', symbol: 'BCH'}
},
// Functions
getMarketData: getMarketData,
shiftIt: shiftIt
};
return service;
function handleError(response, defaultMessage, cb) {
if (response && typeof response.error === "string") {
cb(new Error(response.error));
} else if (response && response.error && response.error.message) {
cb(new Error(response.error.message));
} else {
cb(new Error(defaultMessage));
}
}
function getMarketData(coinIn, coinOut, cb) {
service.coinIn = coinIn;
service.coinOut = coinOut;
shapeshiftApiService
.marketInfo(service.coinIn, service.coinOut)
.then(function (response) {
if (!response || response.error) {
handleError(response, 'Invalid response from Shapeshift', cb);
} else {
service.marketData = response;
service.rateString = service.marketData.rate.toString() + ' ' + coinOut.toUpperCase() + '/' + coinIn.toUpperCase();
cb(null, response);
}
});
}
function shiftIt(coinIn, coinOut, withdrawalAddress, returnAddress, amount, cb) {
// Test if the amount is correct depending on the min and max
if (!amount || typeof amount !== 'number') {
cb(new Error(gettextCatalog.getString('Amount is not defined')));
} else if (amount < service.marketData.minimum) {
cb(new Error(gettextCatalog.getString('Amount is below the minimun')));
} else if (amount > service.marketData.maxLimit) {
cb(new Error(gettextCatalog.getString('Amount is above the limit')));
} else {
// Init service data
service.withdrawalAddress = withdrawalAddress;
service.returnAddress = returnAddress;
service.coinIn = coinIn;
service.coinOut = coinOut;
service.amount = amount;
// Check the address
shapeshiftApiService
.ValidateAddress(withdrawalAddress, coinOut)
.then(function onSuccess(response) {
if (response && response.isvalid) {
// Prepare the transaction shapeshift side
shapeshiftApiService.NormalTx(service).then(function onResponse(response) {
// If error, return it
if (!response || response.error) {
handleError(response, gettextCatalog.getString('Invalid response from Shapeshift'), cb);
} else {
var txData = response;
// If the content is not that it was expected, get back an error
if (!txData || !txData.orderId || !txData.deposit) {
cb(new Error(gettextCatalog.getString('Invalid response from Shapeshift')));
} else {
// Get back the data
service.depositInfo = txData;
var shapeshiftData = {
coinIn: coinIn,
coinOut: coinOut,
toWalletId: service.toWalletId,
minAmount: service.marketData.minimum,
maxAmount: service.marketData.maxLimit,
orderId: txData.orderId,
toAddress: txData.deposit
};
cb(null, shapeshiftData);
}
}
});
} else {
cb(new Error(gettextCatalog.getString('Invalid address')));
}
});
}
}
}
})();

View file

@ -1,141 +0,0 @@
'use strict';
angular.module('copayApp.services').factory('shapeshiftService', function ($http, $interval, $log, lodash, moment, ongoingProcess, shapeshiftApiService, storageService, configService, incomingData, platformInfo, servicesService) {
var root = {};
root.ShiftState = 'Shift';
root.coinIn = '';
root.coinOut = '';
root.withdrawalAddress = '';
root.returnAddress = '';
root.amount = '';
root.marketData = {};
root.getMarketDataIn = function (coin) {
if (coin === root.coinOut) return root.getMarketData(root.coinOut, root.coinIn);
return root.getMarketData(coin, root.coinOut);
};
root.getMarketDataOut = function (coin) {
if (coin === root.coinIn) return root.getMarketData(root.coinOut, root.coinIn);
return root.getMarketData(root.coinIn, coin);
};
root.getMarketData = function (coinIn, coinOut, cb) {
root.coinIn = coinIn;
root.coinOut = coinOut;
if (root.coinIn === undefined || root.coinOut === undefined) return;
shapeshiftApiService
.marketInfo(root.coinIn, root.coinOut)
.then(function (marketData) {
root.marketData = marketData;
root.rateString = root.marketData.rate.toString() + ' ' + coinOut.toUpperCase() + '/' + coinIn.toUpperCase();
if (cb) {
cb(marketData);
}
});
};
/*shapeshiftApiService.coins().then(function(coins){
root.coins = coins;
root.coinIn = coins['BTC'].symbol;
root.coinOut = coins['BCH'].symbol;
root.getMarketData(root.coinIn, root.coinOut);
});*/
root.coins = {
'BTC': {name: 'Bitcoin', symbol: 'BTC'},
'BCH': {name: 'Bitcoin Cash', symbol: 'BCH'}
};
function checkForError(data) {
if (data.err) return true;
return false;
}
root.shiftIt = function (coinIn, coinOut, withdrawalAddress, returnAddress, cb) {
ongoingProcess.set('connectingShapeshift', true);
root.withdrawalAddress = withdrawalAddress;
root.returnAddress = returnAddress;
root.coinIn = coinIn;
root.coinOut = coinOut;
shapeshiftApiService.ValidateAddress(withdrawalAddress, coinOut).then(function (valid) {
var tx = ShapeShift();
var coin;
console.log("Starting");
tx.then(function (txData) {
console.log("Got txData", txData);
if (txData['fixedTxData']) {
txData = txData.fixedTxData;
if (checkForError(txData)) return cb(txData.err);
//console.log(txData)
var coinPair = txData.pair.split('_');
txData.depositType = coinPair[0].toUpperCase();
txData.withdrawalType = coinPair[1].toUpperCase();
coin = root.coins[txData.depositType].name.toLowerCase();
txData.depositQR = coin + ":" + txData.deposit + "?amount=" + txData.depositAmount;
root.txFixedPending = true;
} else if (txData['normalTxData']) {
txData = txData.normalTxData;
if (checkForError(txData)) return cb(txData.err);
coin = root.coins[txData.depositType.toUpperCase()].name.toLowerCase();
txData.depositQR = coin + ":" + txData.deposit;
} else if (txData['cancelTxData']) {
txData = txData.cancelTxData;
if (checkForError(txData)) return cb(txData.err);
if (root.txFixedPending) {
root.txFixedPending = false;
}
root.ShiftState = 'Shift';
}
root.depositInfo = txData;
//console.log(root.marketData);
//console.log(root.depositInfo);
var sendAddress = txData.depositQR;
if (sendAddress && sendAddress.indexOf('bitcoin cash') >= 0)
sendAddress = sendAddress.replace('bitcoin cash', 'bitcoincash');
ongoingProcess.set('connectingShapeshift', false);
root.ShiftState = 'Cancel';
//root.GetStatus();
//root.txInterval=$interval(root.GetStatus, 8000);
var shapeshiftData = {
coinIn: coinIn,
coinOut: coinOut,
toWalletId: root.toWalletId,
minAmount: root.marketData.minimum,
maxAmount: root.marketData.maxLimit,
orderId: root.depositInfo.orderId,
toAddress: txData.deposit
};
//
// if (incomingData.redir(sendAddress, 'shapeshift', shapeshiftData)) {
ongoingProcess.set('connectingShapeshift', false);
// return;
// }
cb(null, shapeshiftData);
});
})
};
function ShapeShift() {
if (parseFloat(root.amount) > 0) return shapeshiftApiService.FixedAmountTx(root);
return shapeshiftApiService.NormalTx(root);
}
root.GetStatus = function () {
var address = root.depositInfo.deposit
shapeshiftApiService.GetStatusOfDepositToAddress(address).then(function (data) {
root.DepositStatus = data;
if (root.DepositStatus.status === 'complete') {
$interval.cancel(root.txInterval);
root.depositInfo = null;
root.ShiftState = 'Shift'
}
});
};
return root;
});

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;
}
}

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;
}
});
}