Wallet/public/js/copay.js

9892 lines
791 KiB
JavaScript
Raw Normal View History

2016-11-22 11:37:19 -03:00
'use strict';
var modules = [
'ui.router',
'angularMoment',
'mm.foundation',
'monospaced.qrcode',
'gettext',
'ngLodash',
'uiSwitch',
'bwcModule',
'copayApp.filters',
'copayApp.services',
'copayApp.controllers',
'copayApp.directives',
'copayApp.addons'
];
var copayApp = window.copayApp = angular.module('copayApp', modules);
angular.module('copayApp.filters', []);
angular.module('copayApp.services', []);
angular.module('copayApp.controllers', []);
angular.module('copayApp.directives', []);
angular.module('copayApp.addons', []);
'use strict';
var unsupported, isaosp;
if (window && window.navigator) {
var rxaosp = window.navigator.userAgent.match(/Android.*AppleWebKit\/([\d.]+)/);
isaosp = (rxaosp && rxaosp[1] < 537);
if (!window.cordova && isaosp)
unsupported = true;
if (unsupported) {
window.location = '#/unsupported';
}
}
//Setting up route
angular
.module('copayApp')
.config(function(historicLogProvider, $provide, $logProvider, $stateProvider, $urlRouterProvider, $compileProvider) {
$urlRouterProvider.otherwise('/');
$logProvider.debugEnabled(true);
$provide.decorator('$log', ['$delegate',
function($delegate) {
var historicLog = historicLogProvider.$get();
['debug', 'info', 'warn', 'error', 'log'].forEach(function(level) {
var orig = $delegate[level];
$delegate[level] = function() {
if (level == 'error')
console.log(arguments);
var args = [].slice.call(arguments);
if (!Array.isArray(args)) args = [args];
args = args.map(function(v) {
try {
if (typeof v == 'undefined') v = 'undefined';
if (!v) v = 'null';
if (typeof v == 'object') {
if (v.message)
v = v.message;
else
v = JSON.stringify(v);
}
// Trim output in mobile
if (window.cordova) {
v = v.toString();
if (v.length > 1000) {
v = v.substr(0, 997) + '...';
}
}
} catch (e) {
console.log('Error at log decorator:', e);
v = 'undefined';
}
return v;
});
try {
if (window.cordova)
console.log(args.join(' '));
historicLog.add(level, args.join(' '));
orig.apply(null, args);
} catch (e) {
console.log('ERROR (at log decorator):', e, args[0]);
}
};
});
return $delegate;
}
]);
// whitelist 'chrome-extension:' for chromeApp to work with image URLs processed by Angular
// link: http://stackoverflow.com/questions/15606751/angular-changes-urls-to-unsafe-in-extension-page?lq=1
$compileProvider.imgSrcSanitizationWhitelist(/^\s*((https?|ftp|file|blob|chrome-extension):|data:image\/)/);
$stateProvider
.state('splash', {
url: '/splash',
needProfile: false,
views: {
'main': {
templateUrl: 'views/splash.html',
}
}
});
$stateProvider
.state('translators', {
url: '/translators',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/translators.html'
}
}
})
.state('disclaimer', {
url: '/disclaimer',
needProfile: false,
views: {
'main': {
templateUrl: 'views/disclaimer.html',
}
}
})
.state('walletHome', {
url: '/',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/walletHome.html',
},
}
})
.state('unsupported', {
url: '/unsupported',
needProfile: false,
views: {
'main': {
templateUrl: 'views/unsupported.html'
}
}
})
.state('payment', {
url: '/uri-payment/:data',
templateUrl: 'views/paymentUri.html',
views: {
'main': {
templateUrl: 'views/paymentUri.html',
},
},
needProfile: true
})
.state('selectWalletForPayment', {
url: '/selectWalletForPayment',
controller: 'walletForPaymentController',
needProfile: true
})
.state('join', {
url: '/join',
needProfile: true,
views: {
'main': {
templateUrl: 'views/join.html'
},
}
})
.state('import', {
url: '/import',
needProfile: true,
views: {
'main': {
templateUrl: 'views/import.html'
},
}
})
.state('importProfile', {
url: '/importProfile',
templateUrl: 'views/importProfile.html',
needProfile: false
})
.state('importLegacy', {
url: '/importLegacy',
needProfile: true,
views: {
'main': {
templateUrl: 'views/importLegacy.html',
},
}
})
.state('create', {
url: '/create',
templateUrl: 'views/create.html',
needProfile: true,
views: {
'main': {
templateUrl: 'views/create.html'
},
}
})
.state('copayers', {
url: '/copayers',
needProfile: true,
views: {
'main': {
templateUrl: 'views/copayers.html'
},
}
})
.state('preferences', {
url: '/preferences',
templateUrl: 'views/preferences.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferences.html',
},
}
})
.state('preferencesLanguage', {
url: '/preferencesLanguage',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesLanguage.html'
},
}
})
.state('preferencesUnit', {
url: '/preferencesUnit',
templateUrl: 'views/preferencesUnit.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesUnit.html'
},
}
})
.state('preferencesFee', {
url: '/preferencesFee',
templateUrl: 'views/preferencesFee.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesFee.html'
},
}
})
.state('uriglidera', {
url: '/uri-glidera?code',
needProfile: true,
views: {
'main': {
templateUrl: 'views/glideraUri.html'
},
}
})
.state('glidera', {
url: '/glidera',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/glidera.html'
},
}
})
.state('buyGlidera', {
url: '/buy',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/buyGlidera.html'
},
}
})
.state('sellGlidera', {
url: '/sell',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/sellGlidera.html'
},
}
})
.state('preferencesGlidera', {
url: '/preferencesGlidera',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesGlidera.html'
},
}
})
.state('preferencesAdvanced', {
url: '/preferencesAdvanced',
templateUrl: 'views/preferencesAdvanced.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesAdvanced.html'
},
}
})
.state('preferencesColor', {
url: '/preferencesColor',
templateUrl: 'views/preferencesColor.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesColor.html'
},
}
})
.state('preferencesAltCurrency', {
url: '/preferencesAltCurrency',
templateUrl: 'views/preferencesAltCurrency.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesAltCurrency.html'
},
}
})
.state('preferencesAlias', {
url: '/preferencesAlias',
templateUrl: 'views/preferencesAlias.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesAlias.html'
},
}
})
.state('preferencesEmail', {
url: '/preferencesEmail',
templateUrl: 'views/preferencesEmail.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesEmail.html'
},
}
})
.state('preferencesBwsUrl', {
url: '/preferencesBwsUrl',
templateUrl: 'views/preferencesBwsUrl.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesBwsUrl.html'
},
}
})
.state('delete', {
url: '/delete',
templateUrl: 'views/preferencesDeleteWallet.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesDeleteWallet.html'
},
}
})
.state('information', {
url: '/information',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesInformation.html'
},
}
})
.state('about', {
url: '/about',
templateUrl: 'views/preferencesAbout.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesAbout.html'
},
}
})
.state('logs', {
url: '/logs',
templateUrl: 'views/preferencesLogs.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/preferencesLogs.html'
},
}
})
.state('export', {
url: '/export',
templateUrl: 'views/export.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/export.html'
},
}
})
.state('paperWallet', {
url: '/paperWallet',
templateUrl: 'views/paperWallet.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/paperWallet.html'
},
}
})
.state('backup', {
url: '/backup',
templateUrl: 'views/backup.html',
walletShouldBeComplete: true,
needProfile: true,
views: {
'main': {
templateUrl: 'views/backup.html'
},
}
})
.state('settings', {
url: '/settings',
controller: 'settingsController',
templateUrl: 'views/settings.html',
needProfile: false
})
.state('warning', {
url: '/warning',
controller: 'warningController',
templateUrl: 'views/warning.html',
needProfile: false
})
.state('add', {
url: '/add',
needProfile: true,
views: {
'main': {
templateUrl: 'views/add.html'
},
}
})
.state('cordova', {
url: '/cordova/:status/:isHome',
views: {
'main': {
controller: function($rootScope, $state, $stateParams, $timeout, go, isCordova) {
switch ($stateParams.status) {
case 'resume':
$rootScope.$emit('Local/Resume');
break;
case 'backbutton':
if (isCordova && $stateParams.isHome == 'true' && !$rootScope.modalOpened) {
navigator.app.exitApp();
} else {
$rootScope.$emit('closeModal');
}
break;
};
$timeout(function() {
$rootScope.$emit('Local/SetTab', 'walletHome', true);
}, 100);
go.walletHome();
}
}
},
needProfile: false
});
})
.run(function($rootScope, $state, $log, uriHandler, isCordova, profileService, $timeout, nodeWebkit, uxLanguage, animationService) {
FastClick.attach(document.body);
uxLanguage.init();
// Register URI handler, not for mobileApp
if (!isCordova) {
uriHandler.register();
}
if (nodeWebkit.isDefined()) {
var gui = require('nw.gui');
var win = gui.Window.get();
var nativeMenuBar = new gui.Menu({
type: "menubar"
});
try {
nativeMenuBar.createMacBuiltin("Copay");
} catch (e) {
$log.debug('This is not OSX');
}
win.menu = nativeMenuBar;
}
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if (!profileService.profile && toState.needProfile) {
// Give us time to open / create the profile
event.preventDefault();
// Try to open local profile
profileService.loadAndBindProfile(function(err) {
if (err) {
if (err.message.match('NOPROFILE')) {
$log.debug('No profile... redirecting');
$state.transitionTo('splash');
} else if (err.message.match('NONAGREEDDISCLAIMER')) {
$log.debug('Display disclaimer... redirecting');
$state.transitionTo('disclaimer');
} else {
throw new Error(err); // TODO
}
} else {
$log.debug('Profile loaded ... Starting UX.');
$state.transitionTo(toState.name || toState, toParams);
}
});
}
if (profileService.focusedClient && !profileService.focusedClient.isComplete() && toState.walletShouldBeComplete) {
$state.transitionTo('copayers');
event.preventDefault();
}
if (!animationService.transitionAnimated(fromState, toState)) {
event.preventDefault();
// Time for the backpane to render
setTimeout(function() {
$state.transitionTo(toState);
}, 50);
}
});
});
'use strict';
function selectText(element) {
var doc = document;
if (doc.body.createTextRange) { // ms
var range = doc.body.createTextRange();
range.moveToElementText(element);
range.select();
} else if (window.getSelection) {
var selection = window.getSelection();
var range = doc.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
}
angular.module('copayApp.directives')
.directive('validAddress', ['$rootScope', 'bitcore', 'profileService',
function($rootScope, bitcore, profileService) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var URI = bitcore.URI;
var Address = bitcore.Address
var validator = function(value) {
if (!profileService.focusedClient)
return;
var networkName = profileService.focusedClient.credentials.network;
// Regular url
if (/^https?:\/\//.test(value)) {
ctrl.$setValidity('validAddress', true);
return value;
}
// Bip21 uri
if (/^bitcoin:/.test(value)) {
var uri, isAddressValid;
var isUriValid = URI.isValid(value);
if (isUriValid) {
uri = new URI(value);
isAddressValid = Address.isValid(uri.address.toString(), networkName)
}
ctrl.$setValidity('validAddress', isUriValid && isAddressValid);
return value;
}
if (typeof value == 'undefined') {
ctrl.$pristine = true;
return;
}
// Regular Address
ctrl.$setValidity('validAddress', Address.isValid(value, networkName));
return value;
};
ctrl.$parsers.unshift(validator);
ctrl.$formatters.unshift(validator);
}
};
}
])
.directive('validUrl', [
function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var validator = function(value) {
// Regular url
if (/^https?:\/\//.test(value)) {
ctrl.$setValidity('validUrl', true);
return value;
} else {
ctrl.$setValidity('validUrl', false);
return value;
}
};
ctrl.$parsers.unshift(validator);
ctrl.$formatters.unshift(validator);
}
};
}
])
.directive('validAmount', ['configService',
function(configService) {
return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
var val = function(value) {
var settings = configService.getSync().wallet.settings;
var vNum = Number((value * settings.unitToSatoshi).toFixed(0));
if (typeof value == 'undefined' || value == 0) {
ctrl.$pristine = true;
}
if (typeof vNum == "number" && vNum > 0) {
var decimals = Number(settings.unitDecimals);
var sep_index = ('' + value).indexOf('.');
var str_value = ('' + value).substring(sep_index + 1);
if (sep_index > 0 && str_value.length > decimals) {
ctrl.$setValidity('validAmount', false);
} else {
ctrl.$setValidity('validAmount', true);
}
} else {
ctrl.$setValidity('validAmount', false);
}
return value;
}
ctrl.$parsers.unshift(val);
ctrl.$formatters.unshift(val);
}
};
}
])
.directive('walletSecret', function(bitcore) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var validator = function(value) {
if (value.length > 0) {
var m = value.match(/^[0-9A-HJ-NP-Za-km-z]{70,80}$/);
ctrl.$setValidity('walletSecret', m ? true : false);
}
return value;
};
ctrl.$parsers.unshift(validator);
}
};
})
.directive('loading', function() {
return {
restrict: 'A',
link: function($scope, element, attr) {
var a = element.html();
var text = attr.loading;
element.on('click', function() {
element.html('<i class="size-21 fi-bitcoin-circle icon-rotate spinner"></i> ' + text + '...');
});
$scope.$watch('loading', function(val) {
if (!val) {
element.html(a);
}
});
}
}
})
.directive('ngFileSelect', function() {
return {
link: function($scope, el) {
el.bind('change', function(e) {
$scope.file = (e.srcElement || e.target).files[0];
$scope.getFile();
});
}
}
})
.directive('contact', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
if (!scope.wallet) return;
var address = attrs.address;
var contact = scope.wallet.addressBook[address];
if (contact && !contact.hidden) {
element.append(contact.label);
element.attr('tooltip', attrs.address);
} else {
element.append(address);
}
element.bind('click', function() {
selectText(element[0]);
});
}
};
})
.directive('highlightOnChange', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.highlightOnChange, function(newValue, oldValue) {
element.addClass('highlight');
setTimeout(function() {
element.removeClass('highlight');
}, 500);
});
}
}
})
.directive('checkStrength', function() {
return {
replace: false,
restrict: 'EACM',
require: 'ngModel',
link: function(scope, element, attrs) {
var MIN_LENGTH = 8;
var MESSAGES = ['Very Weak', 'Very Weak', 'Weak', 'Medium', 'Strong', 'Very Strong'];
var COLOR = ['#dd514c', '#dd514c', '#faa732', '#faa732', '#16A085', '#16A085'];
function evaluateMeter(password) {
var passwordStrength = 0;
var text;
if (password.length > 0) passwordStrength = 1;
if (password.length >= MIN_LENGTH) {
if ((password.match(/[a-z]/)) && (password.match(/[A-Z]/))) {
passwordStrength++;
} else {
text = ', add mixed case';
}
if (password.match(/\d+/)) {
passwordStrength++;
} else {
if (!text) text = ', add numerals';
}
if (password.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/)) {
passwordStrength++;
} else {
if (!text) text = ', add punctuation';
}
if (password.length > 12) {
passwordStrength++;
} else {
if (!text) text = ', add characters';
}
} else {
text = ', that\'s short';
}
if (!text) text = '';
return {
strength: passwordStrength,
message: MESSAGES[passwordStrength] + text,
color: COLOR[passwordStrength]
}
}
scope.$watch(attrs.ngModel, function(newValue, oldValue) {
if (newValue && newValue !== '') {
var info = evaluateMeter(newValue);
scope[attrs.checkStrength] = info;
}
});
}
};
})
.directive('showFocus', function($timeout) {
return function(scope, element, attrs) {
scope.$watch(attrs.showFocus,
function(newValue) {
$timeout(function() {
newValue && element[0].focus();
});
}, true);
};
})
.directive('match', function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
match: '='
},
link: function(scope, elem, attrs, ctrl) {
scope.$watch(function() {
return (ctrl.$pristine && angular.isUndefined(ctrl.$modelValue)) || scope.match === ctrl.$modelValue;
}, function(currentValue) {
ctrl.$setValidity('match', currentValue);
});
}
};
})
.directive('clipCopy', function() {
return {
restrict: 'A',
scope: {
clipCopy: '=clipCopy'
},
link: function(scope, elm) {
// TODO this does not work (FIXME)
elm.attr('tooltip', 'Press Ctrl+C to Copy');
elm.attr('tooltip-placement', 'top');
elm.bind('click', function() {
selectText(elm[0]);
});
}
};
})
.directive('menuToggle', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'views/includes/menu-toggle.html'
}
})
.directive('logo', function() {
return {
restrict: 'E',
scope: {
width: "@",
negative: "="
},
controller: function($scope) {
$scope.logo_url = $scope.negative ? 'img/logo-negative.png' : 'img/logo.png';
},
replace: true,
template: '<img ng-src="{{ logo_url }}" alt="Copay">'
}
})
.directive('availableBalance', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'views/includes/available-balance.html'
}
});
'use strict';
/*
* This is a modification from https://github.com/angular/angular.js/blob/master/src/ngTouch/swipe.js
*/
function makeSwipeDirective(directiveName, direction, eventName) {
angular.module('copayApp.directives')
.directive(directiveName, ['$parse', '$swipe',
function($parse, $swipe) {
// The maximum vertical delta for a swipe should be less than 75px.
var MAX_VERTICAL_DISTANCE = 75;
// Vertical distance should not be more than a fraction of the horizontal distance.
var MAX_VERTICAL_RATIO = 0.4;
// At least a 30px lateral motion is necessary for a swipe.
var MIN_HORIZONTAL_DISTANCE = 30;
return function(scope, element, attr) {
var swipeHandler = $parse(attr[directiveName]);
var startCoords, valid;
function validSwipe(coords) {
// Check that it's within the coordinates.
// Absolute vertical distance must be within tolerances.
// Horizontal distance, we take the current X - the starting X.
// This is negative for leftward swipes and positive for rightward swipes.
// After multiplying by the direction (-1 for left, +1 for right), legal swipes
// (ie. same direction as the directive wants) will have a positive delta and
// illegal ones a negative delta.
// Therefore this delta must be positive, and larger than the minimum.
if (!startCoords) return false;
var deltaY = Math.abs(coords.y - startCoords.y);
var deltaX = (coords.x - startCoords.x) * direction;
return valid && // Short circuit for already-invalidated swipes.
deltaY < MAX_VERTICAL_DISTANCE &&
deltaX > 0 &&
deltaX > MIN_HORIZONTAL_DISTANCE &&
deltaY / deltaX < MAX_VERTICAL_RATIO;
}
var pointerTypes = ['touch'];
$swipe.bind(element, {
'start': function(coords, event) {
startCoords = coords;
valid = true;
},
'move': function(coords, event) {
if (validSwipe(coords)) {
scope.$apply(function() {
element.triggerHandler(eventName);
swipeHandler(scope, {
$event: event
});
});
}
}
}, pointerTypes);
};
}
]);
}
// Left is negative X-coordinate, right is positive.
makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft');
makeSwipeDirective('ngSwipeRight', 1, 'swiperight');
'use strict';
angular.module('copayApp.directives')
.directive('qrScanner', ['$rootScope', '$timeout', '$modal', 'isCordova', 'gettextCatalog',
function($rootScope, $timeout, $modal, isCordova, gettextCatalog) {
var controller = function($scope) {
$scope.cordovaOpenScanner = function() {
window.ignoreMobilePause = true;
window.plugins.spinnerDialog.show(null, gettextCatalog.getString('Preparing camera...'), true);
$timeout(function() {
cordova.plugins.barcodeScanner.scan(
function onSuccess(result) {
$timeout(function() {
window.plugins.spinnerDialog.hide();
window.ignoreMobilePause = false;
}, 100);
if (result.cancelled) return;
$timeout(function() {
var data = result.text;
$scope.onScan({ data: data });
}, 1000);
},
function onError(error) {
$timeout(function() {
window.ignoreMobilePause = false;
window.plugins.spinnerDialog.hide();
}, 100);
alert('Scanning error');
}
);
if ($scope.beforeScan) {
$scope.beforeScan();
}
}, 100);
};
$scope.modalOpenScanner = function() {
var parentScope = $scope;
var ModalInstanceCtrl = function($scope, $rootScope, $modalInstance) {
// QR code Scanner
var video;
var canvas;
var $video;
var context;
var localMediaStream;
var prevResult;
var _scan = function(evt) {
if (localMediaStream) {
context.drawImage(video, 0, 0, 300, 225);
try {
qrcode.decode();
} catch (e) {
//qrcodeError(e);
}
}
$timeout(_scan, 800);
};
var _scanStop = function() {
if (localMediaStream && localMediaStream.active) {
var localMediaStreamTrack = localMediaStream.getTracks();
for (var i = 0; i < localMediaStreamTrack.length; i++) {
localMediaStreamTrack[i].stop();
}
}
localMediaStream = null;
video.src = '';
};
qrcode.callback = function(data) {
if (prevResult != data) {
prevResult = data;
return;
}
_scanStop();
$modalInstance.close(data);
};
var _successCallback = function(stream) {
video.src = (window.URL && window.URL.createObjectURL(stream)) || stream;
localMediaStream = stream;
video.play();
$timeout(_scan, 1000);
};
var _videoError = function(err) {
$scope.cancel();
};
var setScanner = function() {
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
window.URL = window.URL || window.webkitURL ||
window.mozURL || window.msURL;
};
$scope.init = function() {
setScanner();
$timeout(function() {
if (parentScope.beforeScan) {
parentScope.beforeScan();
}
canvas = document.getElementById('qr-canvas');
context = canvas.getContext('2d');
video = document.getElementById('qrcode-scanner-video');
$video = angular.element(video);
canvas.width = 300;
canvas.height = 225;
context.clearRect(0, 0, 300, 225);
navigator.getUserMedia({
video: true
}, _successCallback, _videoError);
}, 500);
};
$scope.cancel = function() {
_scanStop();
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/scanner.html',
windowClass: 'full',
controller: ModalInstanceCtrl,
backdrop : 'static',
keyboard: false
});
modalInstance.result.then(function(data) {
parentScope.onScan({ data: data });
});
};
$scope.openScanner = function() {
if (isCordova) {
$scope.cordovaOpenScanner();
}
else {
$scope.modalOpenScanner();
}
};
};
return {
restrict: 'E',
scope: {
onScan: "&",
beforeScan: "&"
},
controller: controller,
replace: true,
template: '<a id="camera-icon" class="p10" ng-click="openScanner()"><i class="icon-scan size-21"></i></a>'
}
}
]);
'use strict';
angular.module('copayApp.filters', [])
.filter('amTimeAgo', ['amMoment',
function(amMoment) {
return function(input) {
return amMoment.preprocessDate(input).fromNow();
};
}
])
.filter('paged', function() {
return function(elements) {
if (elements) {
return elements.filter(Boolean);
}
return false;
};
})
.filter('removeEmpty', function() {
return function(elements) {
elements = elements || [];
// Hide empty change addresses from other copayers
return elements.filter(function(e) {
return !e.isChange || e.balance > 0;
});
}
})
.filter('noFractionNumber', ['$filter', '$locale', 'configService',
function(filter, locale, configService) {
var numberFilter = filter('number');
var formats = locale.NUMBER_FORMATS;
var config = configService.getSync().wallet.settings;
return function(amount, n) {
if (typeof(n) === 'undefined' && !config) return amount;
var fractionSize = (typeof(n) !== 'undefined') ?
n : config.unitToSatoshi.toString().length - 1;
var value = numberFilter(amount, fractionSize);
var sep = value.indexOf(formats.DECIMAL_SEP);
var group = value.indexOf(formats.GROUP_SEP);
if (amount >= 0) {
if (group > 0) {
if (sep < 0) {
return value;
}
var intValue = value.substring(0, sep);
var floatValue = parseFloat(value.substring(sep));
if (floatValue === 0) {
floatValue = '';
} else {
if (floatValue % 1 === 0) {
floatValue = floatValue.toFixed(0);
}
floatValue = floatValue.toString().substring(1);
}
var finalValue = intValue + floatValue;
return finalValue;
} else {
value = parseFloat(value);
if (value % 1 === 0) {
value = value.toFixed(0);
}
return value;
}
}
return 0;
};
}
]);
'use strict';
/**
* Profile
*
* credential: array of OBJECTS
*/
function Profile() {
this.version = '1.0.0';
};
Profile.create = function(opts) {
opts = opts || {};
var x = new Profile();
x.createdOn = Date.now();
x.credentials = opts.credentials || [];
return x;
};
Profile.fromObj = function(obj) {
var x = new Profile();
x.createdOn = obj.createdOn;
x.credentials = obj.credentials;
if (x.credentials[0] && typeof x.credentials[0] != 'object')
throw ("credentials should be an object");
return x;
};
Profile.fromString = function(str) {
return Profile.fromObj(JSON.parse(str));
};
Profile.prototype.toObj = function() {
return JSON.stringify(this);
};
'use strict';
angular.module('copayApp.services').service('addonManager', function (lodash) {
var addons = [];
this.registerAddon = function (addonSpec) {
addons.push(addonSpec);
};
this.addonMenuItems = function () {
return lodash.map(addons, function (addonSpec) {
return addonSpec.menuItem;
});
};
this.addonViews = function () {
return lodash.map(addons, function (addonSpec) {
return addonSpec.view;
});
};
this.formatPendingTxp = function (txp) {
lodash.each(addons, function (addon) {
if (addon.formatPendingTxp) {
addon.formatPendingTxp(txp);
}
});
};
this.txTemplateUrl = function() {
var addon = lodash.find(addons, 'txTemplateUrl');
return addon ? addon.txTemplateUrl() : null;
}
});
'use strict';
'use strict';
angular.module('copayApp.services')
.factory('addressService', function(storageService, profileService, $log, $timeout, lodash, bwsError, gettext) {
var root = {};
root.expireAddress = function(walletId,cb) {
$log.debug('Cleaning Address ' + walletId );
storageService.clearLastAddress(walletId, function(err) {
return cb(err);
});
};
root.isUsed = function(walletId, byAddress, cb) {
storageService.getLastAddress(walletId, function(err, addr) {
var used = lodash.find(byAddress, {
address: addr
});
return cb(null, used);
});
};
root._createAddress = function(walletId, cb) {
var client = profileService.getClient(walletId);
$log.debug('Creating address for wallet:', walletId);
client.createAddress(function(err, addr) {
if (err) {
if (err.error && err.error.match(/locked/gi)) {
$log.debug(err.error);
return $timeout(function() {
root._createAddress(walletId, cb);
}, 5000);
}
return bwsError.cb(err, gettext('Could not create address'), cb);
}
return cb(null, addr.address);
});
};
root.getAddress = function(walletId, forceNew, cb) {
var firstStep;
if (forceNew) {
firstStep = storageService.clearLastAddress;
} else {
firstStep = function(walletId, cb) {
return cb();
};
}
firstStep(walletId, function(err) {
if (err) return cb(err);
storageService.getLastAddress(walletId, function(err, addr) {
if (err) return cb(err);
if (addr) return cb(null, addr);
root._createAddress(walletId, function(err, addr) {
if (err) return cb(err);
storageService.storeLastAddress(walletId, addr, function() {
if (err) return cb(err);
return cb(null, addr);
});
});
});
});
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('animationService', function(isCordova) {
var root = {};
var cachedTransitionState, cachedBackPanel;
// DISABLE ANIMATION ON DESKTOP
root.modalAnimated = {
slideUp : isCordova ? 'full animated slideInUp' : 'full',
slideRight : isCordova ? 'full animated slideInRight' : 'full',
slideOutDown : isCordova ? 'slideOutDown' : 'hideModal',
slideOutRight : isCordova ? 'slideOutRight' : 'hideModal',
};
var pageWeight = {
walletHome: 0,
copayers: -1,
cordova: -1,
payment: -1,
uriglidera: -1,
preferences: 11,
glidera: 11,
preferencesColor: 12,
backup: 12,
preferencesAdvanced: 12,
buyGlidera: 12,
sellGlidera: 12,
preferencesGlidera: 12,
about: 12,
delete: 13,
preferencesLanguage: 12,
preferencesUnit: 12,
preferencesFee: 12,
preferencesAltCurrency: 12,
preferencesBwsUrl: 12,
preferencesAlias: 12,
preferencesEmail: 12,
export: 13,
logs: 13,
information: 13,
translators: 13,
disclaimer: 13,
add: 11,
create: 12,
join: 12,
import: 12,
importLegacy: 13
};
function cleanUpLater(e, e2) {
var cleanedUp = false, timeoutID;
var cleanUp = function() {
if (cleanedUp) return;
cleanedUp = true;
e2.parentNode.removeChild(e2);
e2.innerHTML = "";
e.className = '';
cachedBackPanel = null;
cachedTransitionState = '';
if (timeoutID) {
timeoutID = null;
window.clearTimeout(timeoutID);
}
};
e.addEventListener("animationend", cleanUp, true);
e2.addEventListener("animationend", cleanUp, true);
e.addEventListener("webkitAnimationEnd", cleanUp, true);
e2.addEventListener("webkitAnimationEnd", cleanUp, true);
timeoutID = setTimeout(cleanUp, 500);
};
root.transitionAnimated = function (fromState, toState, event) {
if (isaosp)
return true;
// Animation in progress?
var x = document.getElementById('mainSectionDup');
if (x && !cachedTransitionState) {
console.log('Anim in progress');
return true;
}
var fromName = fromState.name;
var toName = toState.name;
if (!fromName || !toName)
return true;
var fromWeight = pageWeight[fromName];
var toWeight = pageWeight[toName];
var entering = null,
leaving = null;
// Horizontal Slide Animation?
if (isCordova && fromWeight && toWeight) {
if (fromWeight > toWeight) {
leaving = 'CslideOutRight';
} else {
entering = 'CslideInRight';
}
// Vertical Slide Animation?
} else if (isCordova && fromName && fromWeight >= 0 && toWeight >= 0) {
if (toWeight) {
entering = 'CslideInUp';
} else {
leaving = 'CslideOutDown';
}
// no Animation ?
} else {
return true;
}
var e = document.getElementById('mainSection');
var desiredTransitionState = (fromName || '-') + ':' + (toName || '-');
if (desiredTransitionState == cachedTransitionState) {
e.className = entering || '';
cachedBackPanel.className = leaving || '';
cleanUpLater(e, cachedBackPanel);
//console.log('USing animation', cachedTransitionState);
return true;
} else {
var sc;
// Keep prefDiv scroll
var contentDiv = e.getElementsByClassName('content');
if (contentDiv && contentDiv[0])
sc = contentDiv[0].scrollTop;
cachedBackPanel = e.cloneNode(true);
cachedBackPanel.id = 'mainSectionDup';
var c = document.getElementById('sectionContainer');
c.appendChild(cachedBackPanel);
if (sc)
cachedBackPanel.getElementsByClassName('content')[0].scrollTop = sc;
cachedTransitionState = desiredTransitionState;
return false;
}
}
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('applicationService', function($rootScope, $timeout, isCordova, isChromeApp, nodeWebkit, go) {
var root = {};
root.restart = function() {
var hashIndex = window.location.href.indexOf('#/');
if (isCordova) {
window.location = window.location.href.substr(0, hashIndex);
$timeout(function() {
$rootScope.$digest();
}, 1);
} else {
// Go home reloading the application
if (isChromeApp) {
if (nodeWebkit.isDefined()) {
go.walletHome();
$timeout(function() {
var win = require('nw.gui').Window.get();
win.reload(3);
//or
win.reloadDev();
}, 100);
} else {
chrome.runtime.reload();
}
} else {
window.location = window.location.href.substr(0, hashIndex);
}
}
};
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('backupService', function backupServiceFactory($log, $timeout, profileService, sjcl) {
var root = {};
var _download = function(ew, filename, cb) {
var NewBlob = function(data, datatype) {
var out;
try {
out = new Blob([data], {
type: datatype
});
$log.debug("case 1");
} catch (e) {
window.BlobBuilder = window.BlobBuilder ||
window.WebKitBlobBuilder ||
window.MozBlobBuilder ||
window.MSBlobBuilder;
if (e.name == 'TypeError' && window.BlobBuilder) {
var bb = new BlobBuilder();
bb.append(data);
out = bb.getBlob(datatype);
$log.debug("case 2");
} else if (e.name == "InvalidStateError") {
// InvalidStateError (tested on FF13 WinXP)
out = new Blob([data], {
type: datatype
});
$log.debug("case 3");
} else {
// We're screwed, blob constructor unsupported entirely
$log.debug("Errore");
}
}
return out;
};
var a = document.createElement("a");
document.body.appendChild(a);
a.style.display = "none";
var blob = new NewBlob(ew, 'text/plain;charset=utf-8');
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
a.click();
$timeout(function() {
window.URL.revokeObjectURL(url);
}, 250);
return cb();
};
root.walletExport = function(password, opts) {
if (!password) {
return null;
}
var fc = profileService.focusedClient;
try {
opts = opts || {};
var b = fc.export(opts);
var e = sjcl.encrypt(password, b, {
iter: 10000
});
return e;
} catch (err) {
$log.debug('Error exporting wallet: ', err);
return null;
};
};
root.walletDownload = function(password, opts, cb) {
var fc = profileService.focusedClient;
var ew = root.walletExport(password, opts);
if (!ew) return cb('Could not create backup');
var walletName = (fc.alias || '') + (fc.alias ? '-' : '') + fc.credentials.walletName;
if (opts.noSign) walletName = walletName + '-noSign'
var filename = walletName + '-Copaybackup.aes.json';
_download(ew, filename, cb)
};
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('bitcore', function bitcoreFactory(bwcService) {
var bitcore = bwcService.getBitcore();
return bitcore;
});
'use strict';
angular.module('copayApp.services')
.factory('bwsError', function bwcErrorService($log, gettextCatalog) {
var root = {};
root.msg = function(err, prefix) {
var body = '';
prefix = prefix || '';
if (err && err.code) {
switch(err.code) {
case 'CONNECTION_ERROR':
body = gettextCatalog.getString('Network connection error');
break;
case 'NOT_FOUND':
body = gettextCatalog.getString('Wallet service not found');
break;
case 'BAD_SIGNATURES':
body = gettextCatalog.getString('Signatures rejected by server');
break;
case 'COPAYER_DATA_MISMATCH':
body = gettextCatalog.getString('Copayer data mismatch');
break;
case 'COPAYER_IN_WALLET':
body = gettextCatalog.getString('Copayer already in this wallet');
break;
case 'COPAYER_REGISTERED':
body = gettextCatalog.getString('Wallet already registered');
break;
case 'COPAYER_VOTED':
body = gettextCatalog.getString('Copayer already voted on this spend proposal');
break;
case 'DUST_AMOUNT':
body = gettextCatalog.getString('Amount below dust threshold');
break;
case 'INCORRECT_ADDRESS_NETWORK':
body = gettextCatalog.getString('Incorrect address network');
break;
case 'INSUFFICIENT_FUNDS':
body = gettextCatalog.getString('Insufficient funds');
break;
case 'INSUFFICIENT_FUNDS_FOR_FEE':
body = gettextCatalog.getString('Insufficient funds for fee');
break;
case 'INVALID_ADDRESS':
body = gettextCatalog.getString('Invalid address');
break;
case 'LOCKED_FUNDS':
body = gettextCatalog.getString('Funds are locked by pending spend proposals');
break;
case 'NOT_AUTHORIZED':
body = gettextCatalog.getString('Not authorized');
break;
case 'TX_ALREADY_BROADCASTED':
body = gettextCatalog.getString('Transaction already broadcasted');
break;
case 'TX_CANNOT_CREATE':
body = gettextCatalog.getString('Locktime in effect. Please wait to create a new spend proposal');
break;
case 'TX_CANNOT_REMOVE':
body = gettextCatalog.getString('Locktime in effect. Please wait to remove this spend proposal');
break;
case 'TX_NOT_ACCEPTED':
body = gettextCatalog.getString('Spend proposal is not accepted');
break;
case 'TX_NOT_FOUND':
body = gettextCatalog.getString('Spend proposal not found');
break;
case 'TX_NOT_PENDING':
body = gettextCatalog.getString('The spend proposal is not pending');
break;
case 'UPGRADE_NEEDED':
body = gettextCatalog.getString('Please upgrade Copay to perform this action');
break;
case 'WALLET_ALREADY_EXISTS':
body = gettextCatalog.getString('Wallet already exists');
break;
case 'WALLET_FULL':
body = gettextCatalog.getString('Wallet is full');
break;
case 'WALLET_NOT_COMPLETE':
body = gettextCatalog.getString('Wallet is not complete');
break;
case 'WALLET_NOT_FOUND':
body = gettextCatalog.getString('Wallet not found');
break;
case 'SERVER_COMPROMISED':
body = gettextCatalog.getString('Server response could not be verified');
break;
case 'WALLET_DOES_NOT_EXIST':
body = gettextCatalog.getString('Wallet not registed at the Wallet Service. Recreate it from "Create Wallet" using "Advanced Options" to set your seed');
break;
case 'INVALID_BACKUP':
body = gettextCatalog.getString('Wallet seed is invalid');
break;
case 'ERROR':
body = (err.message || err.error);
break;
default:
$log.warn('Unknown error type:', err.code);
body = err.message || err.code;
break;
}
} else if (err.message) {
body = gettextCatalog.getString(err.message);
} else {
body = gettextCatalog.getString(err);
}
var msg = prefix + ( body ? ': ' + body : '');
return msg;
};
root.cb = function (err,prefix, cb) {
return cb(root.msg(err, prefix))
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('configService', function(storageService, lodash, $log) {
var root = {};
var defaultConfig = {
// wallet limits
limits: {
totalCopayers: 6,
mPlusN: 100,
},
// Bitcore wallet service URL
bws: {
url: 'https://bws.bitpay.com/bws/api',
},
// wallet default config
wallet: {
requiredCopayers: 2,
totalCopayers: 3,
spendUnconfirmed: true,
reconnectDelay: 5000,
idleDurationMin: 4,
settings: {
unitName: 'bits',
unitToSatoshi: 100,
unitDecimals: 2,
unitCode: 'bit',
alternativeName: 'US Dollar',
alternativeIsoCode: 'USD',
}
},
// External services
glidera: {
enabled: true,
testnet: false
},
rates: {
url: 'https://insight.bitpay.com:443/api/rates',
},
};
var configCache = null;
root.getSync = function() {
if (!configCache)
throw new Error('configService#getSync called when cache is not initialized');
return configCache;
};
root.get = function(cb) {
storageService.getConfig(function(err, localConfig) {
if (localConfig) {
configCache = JSON.parse(localConfig);
//these ifs are to avoid migration problems
if (!configCache.bws) {
configCache.bws = defaultConfig.bws;
}
if (!configCache.wallet) {
configCache.wallet = defaultConfig.wallet;
}
if (!configCache.wallet.settings.unitCode) {
configCache.wallet.settings.unitCode = defaultConfig.wallet.settings.unitCode;
}
if (!configCache.glidera) {
configCache.glidera = defaultConfig.glidera;
}
} else {
configCache = lodash.clone(defaultConfig);
};
$log.debug('Preferences read:', configCache)
return cb(err, configCache);
});
};
root.set = function(newOpts, cb) {
var config = defaultConfig;
storageService.getConfig(function(err, oldOpts) {
if (lodash.isString(oldOpts)) {
oldOpts = JSON.parse(oldOpts);
}
if (lodash.isString(config)) {
config = JSON.parse(config);
}
if (lodash.isString(newOpts)) {
newOpts = JSON.parse(newOpts);
}
lodash.merge(config, oldOpts, newOpts);
configCache = config;
storageService.storeConfig(JSON.stringify(config), cb);
});
};
root.reset = function(cb) {
configCache = lodash.clone(defaultConfig);
storageService.removeConfig(cb);
};
root.getDefaults = function() {
return lodash.clone(defaultConfig);
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('confirmDialog', function($log, $timeout, profileService, configService, gettextCatalog, isCordova, isChromeApp) {
var root = {};
var acceptMsg = gettextCatalog.getString('Accept');
var cancelMsg = gettextCatalog.getString('Cancel');
var confirmMsg = gettextCatalog.getString('Confirm');
root.show = function(msg, cb) {
if (isCordova) {
navigator.notification.confirm(
msg,
function(buttonIndex) {
if (buttonIndex == 1) {
$timeout(function() {
return cb(true);
}, 1);
} else {
return cb(false);
}
},
confirmMsg, [acceptMsg, cancelMsg]
);
} else if (isChromeApp) {
// No feedback, alert/confirm not supported.
return cb(true);
} else {
return cb(confirm(msg));
}
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('feeService', function($log, profileService, configService, gettextCatalog, lodash) {
var root = {};
// Constant fee options to translate
root.feeOpts = {
priority: gettextCatalog.getString('Priority'),
normal: gettextCatalog.getString('Normal'),
economy: gettextCatalog.getString('Economy')
};
root.getCurrentFeeValue = function(currentSendFeeLevel, cb) {
var fc = profileService.focusedClient;
var config = configService.getSync().wallet.settings;
var feeLevel = currentSendFeeLevel || config.feeLevel || 'normal';
// static fee
var fee = 10000;
fc.getFeeLevels(fc.credentials.network, function(err, levels) {
if (err) {
return cb({message: 'Could not get dynamic fee. Using static 10000sat'}, fee);
}
else {
fee = lodash.find(levels, { level: feeLevel }).feePerKB;
$log.debug('Dynamic fee: ' + feeLevel + ' ' + fee + ' SAT');
return cb(null, fee);
}
});
};
root.getFeeLevels = function(cb) {
var fc = profileService.focusedClient;
var config = configService.getSync().wallet.settings;
var unitName = config.unitName;
fc.getFeeLevels('livenet', function(errLivenet, levelsLivenet) {
fc.getFeeLevels('testnet', function(errTestnet, levelsTestnet) {
if (errLivenet || errTestnet) $log.debug('Could not get dynamic fee');
else {
for (var i = 0; i < 3; i++) {
levelsLivenet[i]['feePerKBUnit'] = profileService.formatAmount(levelsLivenet[i].feePerKB) + ' ' + unitName;
levelsTestnet[i]['feePerKBUnit'] = profileService.formatAmount(levelsTestnet[i].feePerKB) + ' ' + unitName;
}
}
return cb({
'livenet': levelsLivenet,
'testnet': levelsTestnet
});
});
});
};
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('fileStorageService', function(lodash, $log) {
var root = {},
_fs, _dir;
root.init = function(cb) {
if (_dir) return cb(null, _fs, _dir);
function onFileSystemSuccess(fileSystem) {
console.log('File system started: ', fileSystem.name, fileSystem.root.name);
_fs = fileSystem;
root.getDir(function(err, newDir) {
if (err || !newDir.nativeURL) return cb(err);
_dir = newDir
$log.debug("Got main dir:", _dir.nativeURL);
return cb(null, _fs, _dir);
});
}
function fail(evt) {
var msg = 'Could not init file system: ' + evt.target.error.code;
console.log(msg);
return cb(msg);
};
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFileSystemSuccess, fail);
};
root.get = function(k, cb) {
root.init(function(err, fs, dir) {
if (err) return cb(err);
dir.getFile(k, {
create: false,
}, function(fileEntry) {
if (!fileEntry) return cb();
fileEntry.file(function(file) {
var reader = new FileReader();
reader.onloadend = function(e) {
if (this.result)
$log.debug("Read: ", this.result);
return cb(null, this.result)
}
reader.readAsText(file);
});
}, function(err) {
// Not found
if (err.code == 1) return cb();
else return cb(err);
});
})
};
root.set = function(k, v, cb) {
root.init(function(err, fs, dir) {
if (err) return cb(err);
dir.getFile(k, {
create: true,
}, function(fileEntry) {
// Create a FileWriter object for our FileEntry (log.txt).
fileEntry.createWriter(function(fileWriter) {
fileWriter.onwriteend = function(e) {
console.log('Write completed.');
return cb();
};
fileWriter.onerror = function(e) {
var err = e.error ? e.error : JSON.stringify(e);
console.log('Write failed: ' + err);
return cb('Fail to write:' + err);
};
if (lodash.isObject(v))
v = JSON.stringify(v);
if (!lodash.isString(v)){
v = v.toString();
}
$log.debug('Writing:', k, v);
fileWriter.write(v);
}, cb);
});
});
};
// See https://github.com/apache/cordova-plugin-file/#where-to-store-files
root.getDir = function(cb) {
if (!cordova.file) {
return cb('Could not write on device storage');
}
var url = cordova.file.dataDirectory;
// This could be needed for windows
// if (cordova.file === undefined) {
// url = 'ms-appdata:///local/';
window.resolveLocalFileSystemURL(url, function(dir) {
return cb(null, dir);
}, function(err) {
$log.warn(err);
return cb(err || 'Could not resolve filesystem:' + url);
});
};
root.remove = function(k, cb) {
root.init(function(err, fs, dir) {
if (err) return cb(err);
dir.getFile(k, {
create: false,
}, function(fileEntry) {
// Create a FileWriter object for our FileEntry (log.txt).
fileEntry.remove(function() {
console.log('File removed.');
return cb();
}, cb, cb);
});
});
};
/**
* Same as setItem, but fails if an item already exists
*/
root.create = function(name, value, callback) {
root.get(name,
function(err, data) {
if (data) {
return callback('EEXISTS');
} else {
return root.set(name, value, callback);
}
});
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('glideraService', function($http, $log, isCordova) {
var root = {};
var credentials = {};
root.setCredentials = function(network) {
if (network == 'testnet') {
credentials.HOST = 'https://sandbox.glidera.io';
if (isCordova) {
credentials.REDIRECT_URI = 'bitcoin://glidera';
credentials.CLIENT_ID = 'dfc56e4336e32bb8ba46dde34f3d7d6d';
credentials.CLIENT_SECRET = '5eb679058f6c7eb81123162323d4fba5';
}
else {
credentials.REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
credentials.CLIENT_ID = '9915b6ffa6dc3baffb87135ed3873d49';
credentials.CLIENT_SECRET = 'd74eda05b9c6a228fd5c85cfbd0eb7eb';
}
}
else {
credentials.HOST = 'https://glidera.io';
if (isCordova) {
credentials.REDIRECT_URI = 'bitcoin://glidera';
credentials.CLIENT_ID = '9c8023f0ac0128235b7b27a6f2610c83';
credentials.CLIENT_SECRET = '30431511407b47f25a83bffd72881d55';
}
else {
credentials.REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob';
credentials.CLIENT_ID = '8a9e8a9cf155db430c1ea6c7889afed1';
credentials.CLIENT_SECRET = '24ddec578f38d5488bfe13601933c05f';
}
};
};
root.getOauthCodeUrl = function() {
return credentials.HOST
+ '/oauth2/auth?response_type=code&client_id='
+ credentials.CLIENT_ID
+ '&redirect_uri='
+ credentials.REDIRECT_URI;
};
root.getToken = function(code, cb) {
var req = {
method: 'POST',
url: credentials.HOST + '/api/v1/oauth/token',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
data: {
grant_type : 'authorization_code',
code: code,
client_id : credentials.CLIENT_ID,
client_secret: credentials.CLIENT_SECRET,
redirect_uri: credentials.REDIRECT_URI
}
};
$http(req).then(function(data) {
$log.info('Glidera Authorization Access Token: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Authorization Access Token: ERROR ' + data.statusText);
return cb('Glidera Authorization Access Token: ERROR ' + data.statusText);
});
};
var _get = function(endpoint, token) {
return {
method: 'GET',
url: credentials.HOST + '/api/v1' + endpoint,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
}
};
};
root.getAccessTokenPermissions = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/oauth/token', token)).then(function(data) {
$log.info('Glidera Access Token Permissions: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Access Token Permissions: ERROR ' + data.statusText);
return cb('Glidera Access Token Permissions: ERROR ' + data.statusText);
});
};
root.getEmail = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/user/email', token)).then(function(data) {
$log.info('Glidera Get Email: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Get Email: ERROR ' + data.statusText);
return cb('Glidera Get Email: ERROR ' + data.statusText);
});
};
root.getPersonalInfo = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/user/personalinfo', token)).then(function(data) {
$log.info('Glidera Get Personal Info: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Get Personal Info: ERROR ' + data.statusText);
return cb('Glidera Get Personal Info: ERROR ' + data.statusText);
});
};
root.getStatus = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/user/status', token)).then(function(data) {
$log.info('Glidera User Status: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera User Status: ERROR ' + data.statusText);
return cb('Glidera User Status: ERROR ' + data.statusText);
});
};
root.getLimits = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/user/limits', token)).then(function(data) {
$log.info('Glidera Transaction Limits: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Transaction Limits: ERROR ' + data.statusText);
return cb('Glidera Transaction Limits: ERROR ' + data.statusText);
});
};
root.getTransactions = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/transaction', token)).then(function(data) {
$log.info('Glidera Transactions: SUCCESS');
return cb(null, data.data.transactions);
}, function(data) {
$log.error('Glidera Transactions: ERROR ' + data.statusText);
return cb('Glidera Transactions: ERROR ' + data.statusText);
});
};
root.getTransaction = function(token, txid, cb) {
if (!token) return cb('Invalid Token');
if (!txid) return cb('TxId required');
$http(_get('/transaction/' + txid, token)).then(function(data) {
$log.info('Glidera Transaction: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Transaction: ERROR ' + data.statusText);
return cb('Glidera Transaction: ERROR ' + data.statusText);
});
};
root.getSellAddress = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/user/create_sell_address', token)).then(function(data) {
$log.info('Glidera Create Sell Address: SUCCESS');
return cb(null, data.data.sellAddress);
}, function(data) {
$log.error('Glidera Create Sell Address: ERROR ' + data.statusText);
return cb('Glidera Create Sell Address: ERROR ' + data.statusText);
});
};
root.get2faCode = function(token, cb) {
if (!token) return cb('Invalid Token');
$http(_get('/authentication/get2faCode', token)).then(function(data) {
$log.info('Glidera Sent 2FA code by SMS: SUCCESS');
return cb(null, data.status == 200 ? true : false);
}, function(data) {
$log.error('Glidera Sent 2FA code by SMS: ERROR ' + data.statusText);
return cb('Glidera Sent 2FA code by SMS: ERROR ' + data.statusText);
});
};
var _post = function(endpoint, token, twoFaCode, data) {
return {
method: 'POST',
url: credentials.HOST + '/api/v1' + endpoint,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + token,
'2FA_CODE': twoFaCode
},
data: data
};
};
root.sellPrice = function(token, price, cb) {
var data = {
qty: price.qty,
fiat: price.fiat
};
$http(_post('/prices/sell', token, null, data)).then(function(data) {
$log.info('Glidera Sell Price: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Sell Price: ERROR ' + data.statusText);
return cb('Glidera Sell Price: ERROR ' + data.statusText);
});
};
root.sell = function(token, twoFaCode, data, cb) {
var data = {
refundAddress: data.refundAddress,
signedTransaction: data.signedTransaction,
priceUuid: data.priceUuid,
useCurrentPrice: data.useCurrentPrice,
ip: data.ip
};
$http(_post('/sell', token, twoFaCode, data)).then(function(data) {
$log.info('Glidera Sell: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Sell Request: ERROR ' + data.statusText);
return cb('Glidera Sell Request: ERROR ' + data.statusText);
});
};
root.buyPrice = function(token, price, cb) {
var data = {
qty: price.qty,
fiat: price.fiat
};
$http(_post('/prices/buy', token, null, data)).then(function(data) {
$log.info('Glidera Buy Price: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Buy Price: ERROR ' + data.statusText);
return cb('Glidera Buy Price: ERROR ' + data.statusText);
});
};
root.buy = function(token, twoFaCode, data, cb) {
var data = {
destinationAddress: data.destinationAddress,
qty: data.qty,
priceUuid: data.priceUuid,
useCurrentPrice: data.useCurrentPrice,
ip: data.ip
};
$http(_post('/buy', token, twoFaCode, data)).then(function(data) {
$log.info('Glidera Buy: SUCCESS');
return cb(null, data.data);
}, function(data) {
$log.error('Glidera Buy Request: ERROR ' + data.statusText);
return cb('Glidera Buy Request: ERROR ' + data.statusText);
});
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('go', function($window, $rootScope, $location, $state, profileService, nodeWebkit) {
var root = {};
var hideSidebars = function() {
if (typeof document === 'undefined')
return;
var elem = document.getElementById('off-canvas-wrap');
elem.className = 'off-canvas-wrap';
};
var toggleSidebar = function(invert) {
if (typeof document === 'undefined')
return;
var elem = document.getElementById('off-canvas-wrap');
var leftbarActive = elem.className.indexOf('move-right') >= 0;
if (invert) {
if (profileService.profile && !$rootScope.hideNavigation) {
elem.className = 'off-canvas-wrap move-right';
}
} else {
if (leftbarActive) {
hideSidebars();
}
}
};
root.openExternalLink = function(url, target) {
if (nodeWebkit.isDefined()) {
nodeWebkit.openExternalLink(url);
}
else {
target = target || '_blank';
var ref = window.open(url, target, 'location=no');
}
};
root.path = function(path, cb) {
$state.transitionTo(path)
.then(function() {
if (cb) return cb();
}, function() {
if (cb) return cb('animation in progress');
});
hideSidebars();
};
root.swipe = function(invert) {
toggleSidebar(invert);
};
root.walletHome = function() {
var fc = profileService.focusedClient;
if (fc && !fc.isComplete()) {
root.path('copayers');
} else {
root.path('walletHome', function() {
$rootScope.$emit('Local/SetTab', 'walletHome', true);
});
}
};
root.send = function() {
root.path('walletHome', function() {
$rootScope.$emit('Local/SetTab', 'send');
});
};
root.addWallet = function() {
$state.go('add');
};
root.preferences = function() {
$state.go('preferences');
};
root.reload = function() {
$state.reload();
};
// Global go. This should be in a better place TODO
// We dont do a 'go' directive, to use the benefits of ng-touch with ng-click
$rootScope.go = function(path) {
root.path(path);
};
$rootScope.openExternalLink = function(url, target) {
root.openExternalLink(url, target);
};
return root;
});
'use strict';
var logs = [];
angular.module('copayApp.services')
.factory('historicLog', function historicLog() {
var root = {};
root.add = function(level, msg) {
logs.push({
level: level,
msg: msg,
});
};
root.get = function() {
return logs;
};
return root;
});
'use strict';
angular.module('copayApp.services').value('isChromeApp', !!(window.chrome && chrome.runtime && chrome.runtime.id));
'use strict';
angular.module('copayApp.services').value('isCordova', window.cordova ? true : false);
'use strict';
// Detect mobile devices
var isMobile = {
Android: function() {
return !!navigator.userAgent.match(/Android/i);
},
BlackBerry: function() {
return !!navigator.userAgent.match(/BlackBerry/i);
},
iOS: function() {
return !!navigator.userAgent.match(/iPhone|iPad|iPod/i);
},
Opera: function() {
return !!navigator.userAgent.match(/Opera Mini/i);
},
Windows: function() {
return !!navigator.userAgent.match(/IEMobile/i);
},
Safari: function() {
return Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
},
any: function() {
return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Opera() || isMobile.Windows());
}
};
angular.module('copayApp.services').value('isMobile', isMobile);
'use strict';
angular.module('copayApp.services')
.factory('ledger', function($log, bwcService, gettext) {
var root = {};
var LEDGER_CHROME_ID = "kkdpmhnladdopljabkgpacgpliggeeaf";
// Ledger magic number to get xPub without user confirmation
root.ENTROPY_INDEX_PATH = "0xb11e/";
root.callbacks = {};
root.hasSession = function() {
root._message({
command: "has_session"
});
}
root.getEntropySource = function(account, callback) {
var path = root.ENTROPY_INDEX_PATH + account + "'";
var xpub = root.getXPubKey(path, function(data) {
if (!data.success) {
$log.warn(data.message);
return callback(data);
}
var b = bwcService.getBitcore();
var x = b.HDPublicKey(data.xpubkey);
data.entropySource = x.publicKey.toString();
return callback(data);
});
};
root.getXPubKeyForAddresses = function(account, callback) {
return root.getXPubKey(root._getPath(account), callback);
};
root.getXPubKey = function(path, callback) {
$log.debug('Ledger deriving xPub path:', path);
root.callbacks["get_xpubkey"] = callback;
root._messageAfterSession({
command: "get_xpubkey",
path: path
})
};
root.getInfoForNewWallet = function(account, callback) {
var opts = {};
root.getEntropySource(account, function(data) {
if (!data.success) {
$log.warn(data.message);
return callback(data.message);
}
opts.entropySource = data.entropySource;
root.getXPubKeyForAddresses(account, function(data) {
if (!data.success) {
$log.warn(data.message);
return callback(data);
}
opts.extendedPublicKey = data.xpubkey;
opts.externalSource = 'ledger';
opts.externalIndex = account;
return callback(null, opts);
});
});
};
root._signP2SH = function(txp, account, callback) {
root.callbacks["sign_p2sh"] = callback;
var redeemScripts = [];
var paths = [];
var tx = bwcService.getUtils().buildTx(txp);
for (var i = 0; i < tx.inputs.length; i++) {
redeemScripts.push(new ByteString(tx.inputs[i].redeemScript.toBuffer().toString('hex'), GP.HEX).toString());
paths.push(root._getPath(account) + txp.inputs[i].path.substring(1));
}
var splitTransaction = root._splitTransaction(new ByteString(tx.toString(), GP.HEX));
var inputs = [];
for (var i = 0; i < splitTransaction.inputs.length; i++) {
var input = splitTransaction.inputs[i];
inputs.push([
root._reverseBytestring(input.prevout.bytes(0, 32)).toString(),
root._reverseBytestring(input.prevout.bytes(32)).toString()
]);
}
$log.debug('Ledger signing paths:', paths);
root._messageAfterSession({
command: "sign_p2sh",
inputs: inputs,
scripts: redeemScripts,
outputs_number: splitTransaction.outputs.length,
outputs_script: splitTransaction.outputScript.toString(),
paths: paths
});
};
root.signTx = function(txp, account, callback) {
if (txp.addressType == 'P2PKH') {
var msg = 'P2PKH wallets are not supported with ledger';
$log.error(msg);
return callback(msg);
} else {
root._signP2SH(txp, account, callback);
}
}
root._message = function(data) {
chrome.runtime.sendMessage(
LEDGER_CHROME_ID, {
request: data
},
function(response) {
root._callback(response);
}
);
}
root._messageAfterSession = function(data) {
root._after_session = data;
root._message({
command: "launch"
});
root._should_poll_session = true;
root._do_poll_session();
}
root._do_poll_session = function() {
root.hasSession();
if (root._should_poll_session) {
setTimeout(root._do_poll_session, 500);
}
}
root._callback = function(data) {
if (typeof data == "object") {
if (data.command == "has_session" && data.success) {
root._message(root._after_session);
root._after_session = null;
root._should_poll_session = false;
} else if (typeof root.callbacks[data.command] == "function") {
root.callbacks[data.command](data);
}
} else {
root._should_poll_session = false;
Object.keys(root.callbacks).forEach(function(key) {
root.callbacks[key]({
success: false,
message: gettext("The Ledger Chrome application is not installed"),
});
});
}
}
root._getPath = function(account) {
return "44'/0'/" + account + "'";
}
root._splitTransaction = function(transaction) {
var result = {};
var inputs = [];
var outputs = [];
var offset = 0;
var version = transaction.bytes(offset, 4);
offset += 4;
var varint = root._getVarint(transaction, offset);
var numberInputs = varint[0];
offset += varint[1];
for (var i = 0; i < numberInputs; i++) {
var input = {};
input['prevout'] = transaction.bytes(offset, 36);
offset += 36;
varint = root._getVarint(transaction, offset);
offset += varint[1];
input['script'] = transaction.bytes(offset, varint[0]);
offset += varint[0];
input['sequence'] = transaction.bytes(offset, 4);
offset += 4;
inputs.push(input);
}
varint = root._getVarint(transaction, offset);
var numberOutputs = varint[0];
offset += varint[1];
var outputStartOffset = offset;
for (var i = 0; i < numberOutputs; i++) {
var output = {};
output['amount'] = transaction.bytes(offset, 8);
offset += 8;
varint = root._getVarint(transaction, offset);
offset += varint[1];
output['script'] = transaction.bytes(offset, varint[0]);
offset += varint[0];
outputs.push(output);
}
var locktime = transaction.bytes(offset, 4);
result['version'] = version;
result['inputs'] = inputs;
result['outputs'] = outputs;
result['locktime'] = locktime;
result['outputScript'] = transaction.bytes(outputStartOffset, offset - outputStartOffset);
return result;
}
root._getVarint = function(data, offset) {
if (data.byteAt(offset) < 0xfd) {
return [data.byteAt(offset), 1];
}
if (data.byteAt(offset) == 0xfd) {
return [((data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 3];
}
if (data.byteAt(offset) == 0xfe) {
return [((data.byteAt(offset + 4) << 24) + (data.byteAt(offset + 3) << 16) +
(data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 5];
}
}
root._reverseBytestring = function(x) {
var res = "";
for (var i = x.length - 1; i >= 0; i--) {
res += Convert.toHexByte(x.byteAt(i));
}
return new ByteString(res, GP.HEX);
}
return root;
});
var Convert = {};
/**
* Convert a binary string to his hexadecimal representation
* @param {String} src binary string
* @static
* @returns {String} hexadecimal representation
*/
Convert.stringToHex = function(src) {
var r = "";
var hexes = new Array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f");
for (var i = 0; i < src.length; i++) {
r += hexes[src.charCodeAt(i) >> 4] + hexes[src.charCodeAt(i) & 0xf];
}
return r;
}
/**
* Convert an hexadecimal string to its binary representation
* @param {String} src hexadecimal string
* @static
* @return {Array} byte array
* @throws {InvalidString} if the string isn't properly formatted
*/
Convert.hexToBin = function(src) {
var result = "";
var digits = "0123456789ABCDEF";
if ((src.length % 2) != 0) {
throw "Invalid string";
}
src = src.toUpperCase();
for (var i = 0; i < src.length; i += 2) {
var x1 = digits.indexOf(src.charAt(i));
if (x1 < 0) {
return "";
}
var x2 = digits.indexOf(src.charAt(i + 1));
if (x2 < 0) {
return "";
}
result += String.fromCharCode((x1 << 4) + x2);
}
return result;
}
/**
* Convert a double digit hexadecimal number to an integer
* @static
* @param {String} data buffer containing the digit to parse
* @param {Number} offset offset to the digit (default is 0)
* @returns {Number} converted digit
*/
Convert.readHexDigit = function(data, offset) {
var digits = '0123456789ABCDEF';
if (typeof offset == "undefined") {
offset = 0;
}
return (digits.indexOf(data.substring(offset, offset + 1).toUpperCase()) << 4) + (digits.indexOf(data.substring(offset + 1, offset + 2).toUpperCase()));
}
/**
* Convert a number to a two digits hexadecimal string (deprecated)
* @static
* @param {Number} number number to convert
* @returns {String} converted number
*/
Convert.toHexDigit = function(number) {
var digits = '0123456789abcdef';
return digits.charAt(number >> 4) + digits.charAt(number & 0x0F);
}
/**
* Convert a number to a two digits hexadecimal string (similar to toHexDigit)
* @static
* @param {Number} number number to convert
* @returns {String} converted number
*/
Convert.toHexByte = function(number) {
return Convert.toHexDigit(number);
}
/**
* Convert a BCD number to a two digits hexadecimal string
* @static
* @param {Number} number number to convert
* @returns {String} converted number
*/
Convert.toHexByteBCD = function(numberBCD) {
var number = ((numberBCD / 10) * 16) + (numberBCD % 10);
return Convert.toHexDigit(number);
}
/**
* Convert a number to an hexadecimal short number
* @static
* @param {Number} number number to convert
* @returns {String} converted number
*/
Convert.toHexShort = function(number) {
return Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff);
}
/**
* Convert a number to an hexadecimal int number
* @static
* @param {Number} number number to convert
* @returns {String} converted number
*/
Convert.toHexInt = function(number) {
return Convert.toHexDigit((number >> 24) & 0xff) + Convert.toHexDigit((number >> 16) & 0xff) +
Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff);
}
var GP = {};
GP.ASCII = 1;
GP.HEX = 5;
/**
* @class GPScript ByteString implementation
* @param {String} value initial value
* @param {HEX|ASCII} encoding encoding to use
* @property {Number} length length of the ByteString
* @constructs
*/
var ByteString = function(value, encoding) {
this.encoding = encoding;
this.hasBuffer = (typeof Buffer != 'undefined');
if (this.hasBuffer && (value instanceof Buffer)) {
this.value = value;
this.encoding = GP.HEX;
} else {
switch (encoding) {
case GP.HEX:
if (!this.hasBuffer) {
this.value = Convert.hexToBin(value);
} else {
this.value = new Buffer(value, 'hex');
}
break;
case GP.ASCII:
if (!this.hasBuffer) {
this.value = value;
} else {
this.value = new Buffer(value, 'ascii');
}
break;
default:
throw "Invalid arguments";
}
}
this.length = this.value.length;
}
/**
* Retrieve the byte value at the given index
* @param {Number} index index
* @returns {Number} byte value
*/
ByteString.prototype.byteAt = function(index) {
if (arguments.length < 1) {
throw "Argument missing";
}
if (typeof index != "number") {
throw "Invalid index";
}
if ((index < 0) || (index >= this.value.length)) {
throw "Invalid index offset";
}
if (!this.hasBuffer) {
return Convert.readHexDigit(Convert.stringToHex(this.value.substring(index, index + 1)));
} else {
return this.value[index];
}
}
/**
* Retrieve a subset of the ByteString
* @param {Number} offset offset to start at
* @param {Number} [count] size of the target ByteString (default : use the remaining length)
* @returns {ByteString} subset of the original ByteString
*/
ByteString.prototype.bytes = function(offset, count) {
var result;
if (arguments.length < 1) {
throw "Argument missing";
}
if (typeof offset != "number") {
throw "Invalid offset";
}
//if ((offset < 0) || (offset >= this.value.length)) {
if (offset < 0) {
throw "Invalid offset";
}
if (typeof count == "number") {
if (count < 0) {
throw "Invalid count";
}
if (!this.hasBuffer) {
result = new ByteString(this.value.substring(offset, offset + count), GP.ASCII);
} else {
result = new Buffer(count);
this.value.copy(result, 0, offset, offset + count);
}
} else
if (typeof count == "undefined") {
if (!this.hasBuffer) {
result = new ByteString(this.value.substring(offset), GP.ASCII);
} else {
result = new Buffer(this.value.length - offset);
this.value.copy(result, 0, offset, this.value.length);
}
} else {
throw "Invalid count";
}
if (!this.hasBuffer) {
result.encoding = this.encoding;
return result;
} else {
return new ByteString(result, GP.HEX);
}
}
/**
* Appends two ByteString
* @param {ByteString} target ByteString to append
* @returns {ByteString} result of the concatenation
*/
ByteString.prototype.concat = function(target) {
if (arguments.length < 1) {
throw "Not enough arguments";
}
if (!(target instanceof ByteString)) {
throw "Invalid argument";
}
if (!this.hasBuffer) {
var result = this.value + target.value;
var x = new ByteString(result, GP.ASCII);
x.encoding = this.encoding;
return x;
} else {
var result = Buffer.concat([this.value, target.value]);
return new ByteString(result, GP.HEX);
}
}
/**
* Check if two ByteString are equal
* @param {ByteString} target ByteString to check against
* @returns {Boolean} true if the two ByteString are equal
*/
ByteString.prototype.equals = function(target) {
if (arguments.length < 1) {
throw "Not enough arguments";
}
if (!(target instanceof ByteString)) {
throw "Invalid argument";
}
if (!this.hasBuffer) {
return (this.value == target.value);
} else {
return Buffer.equals(this.value, target.value);
}
}
/**
* Convert the ByteString to a String using the given encoding
* @param {HEX|ASCII|UTF8|BASE64|CN} encoding encoding to use
* @return {String} converted content
*/
ByteString.prototype.toString = function(encoding) {
var targetEncoding = this.encoding;
if (arguments.length >= 1) {
if (typeof encoding != "number") {
throw "Invalid encoding";
}
switch (encoding) {
case GP.HEX:
case GP.ASCII:
targetEncoding = encoding;
break;
default:
throw "Unsupported arguments";
}
targetEncoding = encoding;
}
switch (targetEncoding) {
case GP.HEX:
if (!this.hasBuffer) {
return Convert.stringToHex(this.value);
} else {
return this.value.toString('hex');
}
case GP.ASCII:
if (!this.hasBuffer) {
return this.value;
} else {
return this.value.toString();
}
default:
throw "Unsupported";
}
}
ByteString.prototype.toStringIE = function(encoding) {
return this.toString(encoding);
}
ByteString.prototype.toBuffer = function() {
return this.value;
}
'use strict';
angular.module('copayApp.services')
.factory('legacyImportService', function($rootScope, $log, $timeout, $http, lodash, bitcore, bwcService, sjcl, profileService, isChromeApp) {
var root = {};
var wc = bwcService.getClient();
root.getKeyForEmail = function(email) {
var hash = bitcore.crypto.Hash.sha256ripemd160(new bitcore.deps.Buffer(email)).toString('hex');
$log.debug('Storage key:' + hash);
return 'profile::' + hash;
};
root.getKeyForWallet = function(id) {
return 'wallet::' + id;
};
root._importOne = function(user, pass, walletId, get, cb) {
get(root.getKeyForWallet(walletId), function(err, blob) {
if (err) {
$log.warn('Could not fetch wallet: ' + walletId + ":" + err);
return cb('Could not fetch ' + walletId);
}
profileService.importLegacyWallet(user, pass, blob, cb);
});
};
root._doImport = function(user, pass, get, cb) {
var self = this;
get(root.getKeyForEmail(user), function(err, p) {
if (err || !p)
return cb(err || ('Could not find profile for ' + user));
var ids = wc.getWalletIdsFromOldCopay(user, pass, p);
if (!ids)
return cb('Could not find wallets on the profile');
$rootScope.$emit('Local/ImportStatusUpdate',
'Found ' + ids.length + ' wallets to import:' + ids.join());
$log.info('Importing Wallet Ids:', ids);
var i = 0;
var okIds = [];
var toScanIds = [];
lodash.each(ids, function(walletId) {
$timeout(function() {
$rootScope.$emit('Local/ImportStatusUpdate',
'Importing wallet ' + walletId + ' ... ');
self._importOne(user, pass, walletId, get, function(err, id, name, existed) {
if (err) {
$rootScope.$emit('Local/ImportStatusUpdate',
'Failed to import wallet ' + (name || walletId));
} else {
okIds.push(walletId);
$rootScope.$emit('Local/ImportStatusUpdate',
'Wallet ' + id + '[' + name + '] imported successfully');
if (!existed) {
$log.info('Wallet ' + walletId + ' was created. need to be scanned');
toScanIds.push(id);
}
}
if (++i == ids.length) {
return cb(null, okIds, toScanIds);
}
});
}, 100);
});
});
};
root.import = function(user, pass, serverURL, fromCloud, cb) {
var insightGet = function(key, cb) {
var kdfbinary = function(password, salt, iterations, length) {
iterations = iterations || defaultIterations;
length = length || 512;
salt = sjcl.codec.base64.toBits(salt || defaultSalt);
var hash = sjcl.hash.sha256.hash(sjcl.hash.sha256.hash(password));
var prff = function(key) {
return new sjcl.misc.hmac(hash, sjcl.hash.sha1);
};
return sjcl.misc.pbkdf2(hash, salt, iterations, length, prff);
};
var salt = 'jBbYTj8zTrOt6V';
var iter = 1000;
var SEPARATOR = '|';
var kdfb = kdfbinary(pass + SEPARATOR + user, salt, iter);
var kdfb64 = sjcl.codec.base64.fromBits(kdfb);
var keyBuf = new bitcore.deps.Buffer(kdfb64);
var passphrase = bitcore.crypto.Hash.sha256sha256(keyBuf).toString('base64');
var authHeader = new bitcore.deps.Buffer(user + ':' + passphrase).toString('base64');
var retrieveUrl = serverURL + '/retrieve';
var getParams = {
method: 'GET',
url: retrieveUrl + '?key=' + encodeURIComponent(key) + '&rand=' + Math.random(),
headers: {
'Authorization': authHeader,
},
};
$log.debug('Insight GET', getParams);
$http(getParams)
.success(function(data) {
data = JSON.stringify(data);
$log.info('Fetch from insight OK:' + getParams.url);
return cb(null, data);
})
.error(function() {
$log.warn('Failed to fetch from insight');
return cb('PNOTFOUND: Profile not found');
});
};
var localStorageGet = function(key, cb) {
if (isChromeApp) {
chrome.storage.local.get(key,
function(data) {
return cb(null, data[key]);
});
} else {
var v = localStorage.getItem(key);
return cb(null, v);
}
};
var get = fromCloud ? insightGet : localStorageGet;
root._doImport(user, pass, get, cb);
};
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('localStorageService', function(isChromeApp, nodeWebkit, $timeout) {
var root = {};
var ls = ((typeof window.localStorage !== "undefined") ? window.localStorage : null);
if (isChromeApp && !nodeWebkit.isDefined() && !ls) {
ls = localStorage = chrome.storage.local;
window.localStorage = chrome.storage.local;
}
if (!ls)
throw new Error('localstorage not available');
root.get = function(k, cb) {
if (isChromeApp && !nodeWebkit.isDefined()) {
chrome.storage.local.get(k,
function(data) {
//TODO check for errors
return cb(null, data[k]);
});
} else {
return cb(null, ls.getItem(k));
}
};
/**
* Same as setItem, but fails if an item already exists
*/
root.create = function(name, value, callback) {
root.get(name,
function(err, data) {
if (data) {
return callback('EEXISTS');
} else {
return root.set(name, value, callback);
}
});
};
root.set = function(k, v, cb) {
if (isChromeApp && !nodeWebkit.isDefined()) {
var obj = {};
obj[k] = v;
chrome.storage.local.set(obj, cb);
} else {
ls.setItem(k, v);
return cb();
}
};
root.remove = function(k, cb) {
if (isChromeApp && !nodeWebkit.isDefined()) {
chrome.storage.local.remove(k, cb);
} else {
ls.removeItem(k);
return cb();
}
};
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('logHeader', function($log, isChromeApp, isCordova, nodeWebkit) {
$log.info('Starting Copay v' + window.version + ' #' + window.commitHash);
$log.info('Client: isCordova:', isCordova, 'isChromeApp:', isChromeApp, 'isNodeWebkit:', nodeWebkit.isDefined());
$log.info('Navigator:', navigator.userAgent);
return {};
});
'use strict';
angular.module('copayApp.services').factory('nodeWebkit', function nodeWebkitFactory() {
var root = {};
var isNodeWebkit = function() {
var isNode = (typeof process !== "undefined" && typeof require !== "undefined");
if(isNode) {
try {
return (typeof require('nw.gui') !== "undefined");
} catch(e) {
return false;
}
}
};
root.isDefined = function() {
return isNodeWebkit();
};
root.readFromClipboard = function() {
if (!isNodeWebkit()) return;
var gui = require('nw.gui');
var clipboard = gui.Clipboard.get();
return clipboard.get();
};
root.writeToClipboard = function(text) {
if (!isNodeWebkit()) return;
var gui = require('nw.gui');
var clipboard = gui.Clipboard.get();
return clipboard.set(text);
};
root.openExternalLink = function(url) {
if (!isNodeWebkit()) return;
var gui = require('nw.gui');
return gui.Shell.openExternal(url);
};
return root;
});
'use strict';
angular.module('copayApp.services').
factory('notification', ['$timeout',
function($timeout) {
var notifications = [];
/*
ls.getItem('notifications', function(err, data) {
if (data) {
notifications = JSON.parse(data);
}
});
*/
var queue = [];
var settings = {
info: {
duration: 6000,
enabled: true
},
funds: {
duration: 7000,
enabled: true
},
version: {
duration: 60000,
enabled: true
},
warning: {
duration: 7000,
enabled: true
},
error: {
duration: 7000,
enabled: true
},
success: {
duration: 5000,
enabled: true
},
progress: {
duration: 0,
enabled: true
},
custom: {
duration: 35000,
enabled: true
},
details: true,
localStorage: false,
html5Mode: false,
html5DefaultIcon: 'img/favicon.ico'
};
function html5Notify(icon, title, content, ondisplay, onclose) {
if (window.webkitNotifications && window.webkitNotifications.checkPermission() === 0) {
if (!icon) {
icon = 'img/favicon.ico';
}
var noti = window.webkitNotifications.createNotification(icon, title, content);
if (typeof ondisplay === 'function') {
noti.ondisplay = ondisplay;
}
if (typeof onclose === 'function') {
noti.onclose = onclose;
}
noti.show();
} else {
settings.html5Mode = false;
}
}
return {
/* ========== SETTINGS RELATED METHODS =============*/
disableHtml5Mode: function() {
settings.html5Mode = false;
},
disableType: function(notificationType) {
settings[notificationType].enabled = false;
},
enableHtml5Mode: function() {
// settings.html5Mode = true;
settings.html5Mode = this.requestHtml5ModePermissions();
},
enableType: function(notificationType) {
settings[notificationType].enabled = true;
},
getSettings: function() {
return settings;
},
toggleType: function(notificationType) {
settings[notificationType].enabled = !settings[notificationType].enabled;
},
toggleHtml5Mode: function() {
settings.html5Mode = !settings.html5Mode;
},
requestHtml5ModePermissions: function() {
if (window.webkitNotifications) {
if (window.webkitNotifications.checkPermission() === 0) {
return true;
} else {
window.webkitNotifications.requestPermission(function() {
if (window.webkitNotifications.checkPermission() === 0) {
settings.html5Mode = true;
} else {
settings.html5Mode = false;
}
});
return false;
}
} else {
return false;
}
},
/* ============ QUERYING RELATED METHODS ============*/
getAll: function() {
// Returns all notifications that are currently stored
return notifications;
},
getQueue: function() {
return queue;
},
/* ============== NOTIFICATION METHODS ==============*/
info: function(title, content, userData) {
return this.awesomeNotify('info', 'fi-info', title, content, userData);
},
funds: function(title, content, userData) {
return this.awesomeNotify('funds', 'icon-receive', title, content, userData);
},
version: function(title, content, severe) {
return this.awesomeNotify('version', severe ? 'fi-alert' : 'fi-flag', title, content);
},
error: function(title, content, userData) {
return this.awesomeNotify('error', 'fi-x', title, content, userData);
},
success: function(title, content, userData) {
return this.awesomeNotify('success', 'fi-check', title, content, userData);
},
warning: function(title, content, userData) {
return this.awesomeNotify('warning', 'fi-alert', title, content, userData);
},
new: function(title, content, userData) {
return this.awesomeNotify('warning', 'fi-plus', title, content, userData);
},
sent: function(title, content, userData) {
return this.awesomeNotify('warning', 'icon-paperplane', title, content, userData);
},
awesomeNotify: function(type, icon, title, content, userData) {
/**
* Supposed to wrap the makeNotification method for drawing icons using font-awesome
* rather than an image.
*
* Need to find out how I'm going to make the API take either an image
* resource, or a font-awesome icon and then display either of them.
* Also should probably provide some bits of color, could do the coloring
* through classes.
*/
// image = '<i class="icon-' + image + '"></i>';
return this.makeNotification(type, false, icon, title, content, userData);
},
notify: function(image, title, content, userData) {
// Wraps the makeNotification method for displaying notifications with images
// rather than icons
return this.makeNotification('custom', image, true, title, content, userData);
},
makeNotification: function(type, image, icon, title, content, userData) {
var notification = {
'type': type,
'image': image,
'icon': icon,
'title': title,
'content': content,
'timestamp': +new Date(),
'userData': userData
};
notifications.push(notification);
if (settings.html5Mode) {
html5Notify(image, title, content, function() {
// inner on display function
}, function() {
// inner on close function
});
}
//this is done because html5Notify() changes the variable settings.html5Mode
if (!settings.html5Mode) {
queue.push(notification);
$timeout(function removeFromQueueTimeout() {
queue.splice(queue.indexOf(notification), 1);
}, settings[type].duration);
}
// Mobile notification
if (window && window.navigator && window.navigator.vibrate) {
window.navigator.vibrate([200, 100, 200]);
};
if (document.hidden && (type == 'info' || type == 'funds')) {
new window.Notification(title, {
body: content,
icon: 'img/notification.png'
});
}
this.save();
return notification;
},
/* ============ PERSISTENCE METHODS ============ */
save: function() {
// Save all the notifications into localStorage
if (settings.localStorage) {
localStorage.setItem('notifications', JSON.stringify(notifications));
}
},
restore: function() {
// Load all notifications from localStorage
},
clear: function() {
notifications = [];
this.save();
}
};
}
]).directive('notifications', function(notification, $compile) {
/**
*
* It should also parse the arguments passed to it that specify
* its position on the screen like "bottom right" and apply those
* positions as a class to the container element
*
* Finally, the directive should have its own controller for
* handling all of the notifications from the notification service
*/
function link(scope, element, attrs) {
var position = attrs.notifications;
position = position.split(' ');
element.addClass('dr-notification-container');
for (var i = 0; i < position.length; i++) {
element.addClass(position[i]);
}
}
return {
restrict: 'A',
scope: {},
templateUrl: 'views/includes/notifications.html',
link: link,
controller: ['$scope',
function NotificationsCtrl($scope) {
$scope.queue = notification.getQueue();
$scope.removeNotification = function(noti) {
$scope.queue.splice($scope.queue.indexOf(noti), 1);
};
}
]
};
});
'use strict';
angular.module('copayApp.services')
.factory('notificationService', function profileServiceFactory($filter, notification, lodash, configService, gettext) {
var root = {};
var groupingTime = 5000;
var lastNotificationOnWallet = {};
root.getLast = function(walletId) {
var last = lastNotificationOnWallet[walletId];
if (!last) return null;
return Date.now() - last.ts < groupingTime ? last : null;
};
root.storeLast = function(notificationData, walletId) {
if (notificationData.type == 'NewAddress')
return;
lastNotificationOnWallet[walletId] = {
creatorId: notificationData.creatorId,
type: notificationData.type,
ts: Date.now(),
};
};
root.shouldSkip = function(notificationData, last) {
if (!last) return false;
// rules...
if (last.type === 'NewTxProposal'
&& notificationData.type === 'TxProposalAcceptedBy')
return true;
if (last.type === 'TxProposalFinallyAccepted'
&& notificationData.type === 'NewOutgoingTx')
return true;
if (last.type === 'TxProposalRejectedBy'
&& notificationData.type === 'TxProposalFinallyRejected')
return true;
return false;
};
root.newBWCNotification = function(notificationData, walletId, walletName) {
var last = root.getLast(walletId);
root.storeLast(notificationData, walletId);
if (root.shouldSkip(notificationData, last))
return;
var config = configService.getSync();
config.colorFor = config.colorFor || {};
var color = config.colorFor[walletId] || '#7A8C9E';
var name = config.aliasFor[walletId] || walletName;
switch (notificationData.type) {
case 'NewTxProposal':
notification.new(gettext('New Payment Proposal'),
name, {color: color} );
break;
case 'TxProposalAcceptedBy':
notification.success(gettext('Payment Proposal Signed by Copayer'),
name, {color: color} );
break;
case 'TxProposalRejectedBy':
notification.error(gettext('Payment Proposal Rejected by Copayer'),
name, {color: color} );
break;
case 'TxProposalFinallyRejected':
notification.error(gettext('Payment Proposal Rejected'),
name, {color: color} );
break;
case 'NewOutgoingTx':
notification.sent(gettext('Payment Sent'),
name, {color: color} );
break;
case 'NewIncomingTx':
notification.funds(gettext('Funds received'),
name, {color: color} );
break;
case 'ScanFinished':
notification.success(gettext('Scan Finished'),
name, {color: color} );
break;
case 'NewCopayer':
// No UX notification
break;
}
};
return root;
});
'use strict';
angular.module('copayApp.services')
.factory('profileService', function profileServiceFactory($rootScope, $location, $timeout, $filter, $log, lodash, storageService, bwcService, configService, notificationService, isChromeApp, isCordova, gettext, gettextCatalog, nodeWebkit, bwsError, uxLanguage, ledger, bitcore) {
var root = {};
root.profile = null;
root.focusedClient = null;
root.walletClients = {};
root.getUtils = function() {
return bwcService.getUtils();
};
root.formatAmount = function(amount) {
var config = configService.getSync().wallet.settings;
if (config.unitCode == 'sat') return amount;
//TODO : now only works for english, specify opts to change thousand separator and decimal separator
return this.getUtils().formatAmount(amount, config.unitCode);
};
root._setFocus = function(walletId, cb) {
$log.debug('Set focus:', walletId);
// Set local object
if (walletId)
root.focusedClient = root.walletClients[walletId];
else
root.focusedClient = [];
if (lodash.isEmpty(root.focusedClient)) {
root.focusedClient = root.walletClients[lodash.keys(root.walletClients)[0]];
}
// Still nothing?
if (lodash.isEmpty(root.focusedClient)) {
$rootScope.$emit('Local/NoWallets');
} else {
$rootScope.$emit('Local/NewFocusedWallet');
}
return cb();
};
root.setAndStoreFocus = function(walletId, cb) {
root._setFocus(walletId, function() {
storageService.storeFocusedWalletId(walletId, cb);
});
};
root.setWalletClients = function() {
lodash.each(root.profile.credentials, function(credentials) {
if (root.walletClients[credentials.walletId] &&
root.walletClients[credentials.walletId].started) {
return;
}
var client = bwcService.getClient(JSON.stringify(credentials));
root.walletClients[credentials.walletId] = client;
client.removeAllListeners();
client.on('reconnect', function() {
if (root.focusedClient.credentials.walletId == client.credentials.walletId) {
$log.debug('### Online');
}
});
client.on('reconnecting', function() {
if (root.focusedClient.credentials.walletId == client.credentials.walletId) {
$log.debug('### Offline');
}
});
client.on('notification', function(n) {
$log.debug('BWC Notification:', n);
notificationService.newBWCNotification(n,
client.credentials.walletId, client.credentials.walletName);
if (root.focusedClient.credentials.walletId == client.credentials.walletId) {
$rootScope.$emit(n.type);
} else {
$rootScope.$apply();
}
});
client.on('walletCompleted', function() {
$log.debug('Wallet completed');
root.updateCredentialsFC(function() {
$rootScope.$emit('Local/WalletCompleted')
});
});
root.walletClients[credentials.walletId].started = true;
root.walletClients[credentials.walletId].doNotVerifyPayPro = isChromeApp;
client.initNotifications(function(err) {
if (err) {
$log.error('Could not init notifications err:', err);
return;
}
});
});
$rootScope.$emit('Local/WalletListUpdated');
};
root.applyConfig = function() {
var config = configService.getSync();
$log.debug('Applying preferences');
bwcService.setBaseUrl(config.bws.url);
bwcService.setTransports(['polling']);
};
root.bindProfile = function(profile, cb) {
root.profile = profile;
configService.get(function(err) {
$log.debug('Preferences read');
if (err) return cb(err);
root.applyConfig();
root.setWalletClients();
storageService.getFocusedWalletId(function(err, focusedWalletId) {
if (err) return cb(err);
root._setFocus(focusedWalletId, function() {
$rootScope.$emit('Local/ProfileBound');
return cb();
});
});
});
};
root.loadAndBindProfile = function(cb) {
storageService.getCopayDisclaimerFlag(function(err, val) {
if (!val) {
return cb(new Error('NONAGREEDDISCLAIMER: Non agreed disclaimer'));
} else {
storageService.getProfile(function(err, profile) {
if (err) {
$rootScope.$emit('Local/DeviceError', err);
return cb(err);
}
if (!profile) {
// Migration??
storageService.tryToMigrate(function(err, migratedProfile) {
if (err) return cb(err);
if (!migratedProfile)
return cb(new Error('NOPROFILE: No profile'));
profile = migratedProfile;
return root.bindProfile(profile, cb);
})
} else {
$log.debug('Profile read');
return root.bindProfile(profile, cb);
}
});
}
});
};
root._seedWallet = function(opts, cb) {
opts = opts || {};
var walletClient = bwcService.getClient();
var network = opts.networkName || 'livenet';
if (opts.mnemonic) {
try {
opts.mnemonic = root._normalizeMnemonic(opts.mnemonic);
walletClient.seedFromMnemonic(opts.mnemonic, opts.passphrase, network);
} catch (ex) {
$log.info(ex);
return cb(gettext('Could not create: Invalid wallet seed'));
}
} else if (opts.extendedPrivateKey) {
try {
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
} catch (ex) {
$log.warn(ex);
return cb(gettext('Could not create using the specified extended private key'));
}
} else if (opts.extendedPublicKey) {
try {
walletClient.seedFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource);
} catch (ex) {
$log.warn("Creating wallet from Extended Public Key Arg:", ex, opts);
return cb(gettext('Could not create using the specified extended public key'));
}
} else {
var lang = uxLanguage.getCurrentLanguage();
try {
walletClient.seedFromRandomWithMnemonic(network, opts.passphrase, lang);
} catch (e) {
$log.info('Error creating seed: ' + e.message);
if (e.message.indexOf('language') > 0) {
$log.info('Using default language for mnemonic');
walletClient.seedFromRandomWithMnemonic(network, opts.passphrase);
} else {
return cb(e);
}
}
}
return cb(null, walletClient);
};
root._createNewProfile = function(opts, cb) {
if (opts.noWallet) {
return cb(null, Profile.create());
}
root._seedWallet({}, function(err, walletClient) {
if (err) return cb(err);
var walletName = gettextCatalog.getString('Personal Wallet');
var me = gettextCatalog.getString('me');
walletClient.createWallet(walletName, me, 1, 1, {
network: 'livenet'
}, function(err) {
if (err) return bwsError.cb(err, gettext('Error creating wallet'), cb);
var p = Profile.create({
credentials: [JSON.parse(walletClient.export())],
});
return cb(null, p);
});
})
};
root.createWallet = function(opts, cb) {
$log.debug('Creating Wallet:', opts);
root._seedWallet(opts, function(err, walletClient) {
if (err) return cb(err);
walletClient.createWallet(opts.name, opts.myName || 'me', opts.m, opts.n, {
network: opts.networkName
}, function(err, secret) {
if (err) return bwsError.cb(err, gettext('Error creating wallet'), cb);
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb(null, secret, walletClient.credentials.walletId);
});
});
})
});
};
root.joinWallet = function(opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Joining Wallet:', opts);
try {
var walletData = this.getUtils().fromSecret(opts.secret);
// check if exist
if (lodash.find(root.profile.credentials, {
'walletId': walletData.walletId
})) {
return cb(gettext('Cannot join the same wallet more that once'));
}
} catch (ex) {
return cb(gettext('Bad wallet invitation'));
}
opts.networkName = walletData.network;
$log.debug('Joining Wallet:', opts);
root._seedWallet(opts, function(err, walletClient) {
if (err) return cb(err);
walletClient.joinWallet(opts.secret, opts.myName || 'me', function(err) {
if (err) return bwsError.cb(err, gettext('Could not join wallet'), cb);
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb();
});
});
})
})
};
root.getClient = function(walletId) {
return root.walletClients[walletId];
};
root.deleteWalletFC = function(opts, cb) {
var fc = root.focusedClient;
$log.debug('Deleting Wallet:', fc.credentials.walletName);
fc.removeAllListeners();
root.profile.credentials = lodash.reject(root.profile.credentials, {
walletId: fc.credentials.walletId
});
delete root.walletClients[fc.credentials.walletId];
root.focusedClient = null;
$timeout(function() {
root.setWalletClients();
root.setAndStoreFocus(null, function() {
storageService.storeProfile(root.profile, function(err) {
if (err) return cb(err);
return cb();
});
});
});
};
root._addWalletClient = function(walletClient, cb) {
var walletId = walletClient.credentials.walletId;
// check if exist
var w = lodash.find(root.profile.credentials, {
'walletId': walletId
});
if (w) {
return cb(gettext('Wallet already in Copay' + ": ") + w.walletName );
}
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb(null, walletId);
});
});
};
root.importWallet = function(str, opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Importing Wallet:', opts);
try {
walletClient.import(str, {
compressed: opts.compressed,
password: opts.password
});
} catch (err) {
return cb(gettext('Could not import. Check input file and password'));
}
root._addWalletClient(walletClient, cb);
};
root.importExtendedPrivateKey = function(xPrivKey, cb) {
var walletClient = bwcService.getClient();
$log.debug('Importing Wallet xPrivKey');
walletClient.importFromExtendedPrivateKey(xPrivKey, function(err) {
if (err)
return bwsError.cb(err, gettext('Could not import'), cb);
root._addWalletClient(walletClient, cb);
});
};
root._normalizeMnemonic = function(words) {
var isJA = words.indexOf('\u3000') > -1;
var wordList = words.split(/[\u3000\s]+/);
return wordList.join(isJA ? '\u3000' : ' ');
};
root.importMnemonic = function(words, opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Importing Wallet Mnemonic');
words = root._normalizeMnemonic(words);
walletClient.importFromMnemonic(words, {
network: opts.networkName,
passphrase: opts.passphrase,
}, function(err) {
if (err)
return bwsError.cb(err, gettext('Could not import'), cb);
root._addWalletClient(walletClient, cb);
});
};
root.importExtendedPublicKey = function(opts, cb) {
var walletClient = bwcService.getClient();
$log.debug('Importing Wallet XPubKey');
walletClient.importFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, function(err) {
if (err) {
// in HW wallets, req key is always the same. They can't addAccess.
if (err.code == 'NOT_AUTHORIZED')
err.code = 'WALLET_DOES_NOT_EXIST';
return bwsError.cb(err, gettext('Could not import'), cb);
}
root._addWalletClient(walletClient, cb);
});
};
root.create = function(opts, cb) {
$log.info('Creating profile');
configService.get(function(err) {
root.applyConfig();
root._createNewProfile(opts, function(err, p) {
if (err) return cb(err);
root.bindProfile(p, function(err) {
storageService.storeNewProfile(p, function(err) {
return cb(err);
});
});
});
});
};
root.importLegacyWallet = function(username, password, blob, cb) {
var walletClient = bwcService.getClient();
walletClient.createWalletFromOldCopay(username, password, blob, function(err, existed) {
if (err) return cb(gettext('Error importing wallet: ') + err);
if (root.walletClients[walletClient.credentials.walletId]) {
$log.debug('Wallet:' + walletClient.credentials.walletName + ' already imported');
return cb(gettext('Wallet Already Imported: ') + walletClient.credentials.walletName);
};
$log.debug('Creating Wallet:', walletClient.credentials.walletName);
root.profile.credentials.push(JSON.parse(walletClient.export()));
root.setWalletClients();
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
storageService.storeProfile(root.profile, function(err) {
return cb(null, walletClient.credentials.walletId, walletClient.credentials.walletName, existed);
});
});
});
};
root.updateCredentialsFC = function(cb) {
var fc = root.focusedClient;
var newCredentials = lodash.reject(root.profile.credentials, {
walletId: fc.credentials.walletId
});
newCredentials.push(JSON.parse(fc.export()));
root.profile.credentials = newCredentials;
storageService.storeProfile(root.profile, cb);
};
root.setPrivateKeyEncryptionFC = function(password, cb) {
var fc = root.focusedClient;
$log.debug('Encrypting private key for', fc.credentials.walletName);
fc.setPrivateKeyEncryption(password);
root.lockFC();
root.updateCredentialsFC(function() {
$log.debug('Wallet encrypted');
return cb();
});
};
root.disablePrivateKeyEncryptionFC = function(cb) {
var fc = root.focusedClient;
$log.debug('Disabling private key encryption for', fc.credentials.walletName);
try {
fc.disablePrivateKeyEncryption();
} catch (e) {
return cb(e);
}
root.updateCredentialsFC(function() {
$log.debug('Wallet encryption disabled');
return cb();
});
};
root.lockFC = function() {
var fc = root.focusedClient;
try {
fc.lock();
} catch (e) {};
};
root.unlockFC = function(cb) {
var fc = root.focusedClient;
$log.debug('Wallet is encrypted');
$rootScope.$emit('Local/NeedsPassword', false, function(err2, password) {
if (err2 || !password) {
return cb({
message: (err2 || gettext('Password needed'))
});
}
try {
fc.unlock(password);
} catch (e) {
$log.debug(e);
return cb({
message: gettext('Wrong password')
});
}
$timeout(function() {
if (fc.isPrivKeyEncrypted()) {
$log.debug('Locking wallet automatically');
root.lockFC();
};
}, 2000);
return cb();
});
};
root.getWallets = function(network) {
if (!root.profile) return [];
var config = configService.getSync();
config.colorFor = config.colorFor || {};
config.aliasFor = config.aliasFor || {};
var ret = lodash.map(root.profile.credentials, function(c) {
return {
m: c.m,
n: c.n,
name: config.aliasFor[c.walletId] || c.walletName,
id: c.walletId,
network: c.network,
color: config.colorFor[c.walletId] || '#2C3E50'
};
});
ret = lodash.filter(ret, function(w) {
return (w.network == network);
});
return lodash.sortBy(ret, 'name');
};
root._signWithLedger = function(txp, cb) {
var fc = root.focusedClient;
$log.info('Requesting Ledger Chrome app to sign the transaction');
ledger.signTx(txp, 0, function(result) {
if (!result.success)
return cb(result);
txp.signatures = lodash.map(result.signatures, function(s) {
return s.substring(0, s.length - 2);
});
return fc.signTxProposal(txp, cb);
});
};
root.signTxProposal = function(txp, cb) {
var fc = root.focusedClient;
if (fc.isPrivKeyExternal()) {
if (fc.getPrivKeyExternalSourceName() != 'ledger') {
var msg = 'Unsupported External Key:' + fc.getPrivKeyExternalSourceName();
$log.error(msg);
return cb(msg);
}
return root._signWithLedger(txp, cb);
} else {
return fc.signTxProposal(txp, function(err, signedTxp) {
root.lockFC();
return cb(err, signedTxp);
});
}
};
return root;
});
'use strict';
//var util = require('util');
//var _ = require('lodash');
//var log = require('../util/log');
//var preconditions = require('preconditions').singleton();
//var request = require('request');
/*
This class lets interfaces with BitPay's exchange rate API.
*/
var RateService = function(opts) {
var self = this;
opts = opts || {};
self.httprequest = opts.httprequest; // || request;
self.lodash = opts.lodash;
self.SAT_TO_BTC = 1 / 1e8;
self.BTC_TO_SAT = 1e8;
self.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable() or use service.whenAvailable()';
self.UNSUPPORTED_CURRENCY_ERROR = 'Currency not supported';
self._url = opts.url || 'https://insight.bitpay.com:443/api/rates';
self._isAvailable = false;
self._rates = {};
self._alternatives = [];
self._queued = [];
self._fetchCurrencies();
};
var _instance;
RateService.singleton = function(opts) {
if (!_instance) {
_instance = new RateService(opts);
}
return _instance;
};
RateService.prototype._fetchCurrencies = function() {
var self = this;
var backoffSeconds = 5;
var updateFrequencySeconds = 5 * 60;
var rateServiceUrl = 'https://bitpay.com/api/rates';
var retrieve = function() {
//log.info('Fetching exchange rates');
self.httprequest.get(rateServiceUrl).success(function(res) {
self.lodash.each(res, function(currency) {
self._rates[currency.code] = currency.rate;
self._alternatives.push({
name: currency.name,
isoCode: currency.code,
rate: currency.rate
});
});
self._isAvailable = true;
self.lodash.each(self._queued, function(callback) {
setTimeout(callback, 1);
});
setTimeout(retrieve, updateFrequencySeconds * 1000);
}).error(function(err) {
//log.debug('Error fetching exchange rates', err);
setTimeout(function() {
backoffSeconds *= 1.5;
retrieve();
}, backoffSeconds * 1000);
return;
});
};
retrieve();
};
RateService.prototype.getRate = function(code) {
return this._rates[code];
};
RateService.prototype.getHistoricRate = function(code, date, cb) {
var self = this;
self.httprequest.get(self._url + '/' + code + '?ts=' + date)
.success(function(body) {
return cb(null, body.rate)
})
.error(function(err) {
return cb(err)
});
};
RateService.prototype.getHistoricRates = function(code, dates, cb) {
var self = this;
var tsList = dates.join(',');
self.httprequest.get(self._url + '/' + code + '?ts=' + tsList)
.success(function(body) {
if (!self.lodash.isArray(body)) {
body = [{
ts: dates[0],
rate: body.rate
}];
}
return cb(null, body);
})
.error(function(err) {
return cb(err)
});
};
RateService.prototype.getAlternatives = function() {
return this._alternatives;
};
RateService.prototype.isAvailable = function() {
return this._isAvailable;
};
RateService.prototype.whenAvailable = function(callback) {
if (this.isAvailable()) {
setTimeout(callback, 1);
} else {
this._queued.push(callback);
}
};
RateService.prototype.toFiat = function(satoshis, code) {
if (!this.isAvailable()) {
return null;
}
return satoshis * this.SAT_TO_BTC * this.getRate(code);
};
RateService.prototype.toFiatHistoric = function(satoshis, code, date, cb) {
var self = this;
self.getHistoricRate(code, date, function(err, rate) {
if (err) return cb(err);
return cb(null, satoshis * self.SAT_TO_BTC * rate);
});
};
RateService.prototype.fromFiat = function(amount, code) {
if (!this.isAvailable()) {
return null;
}
return amount / this.getRate(code) * this.BTC_TO_SAT;
};
RateService.prototype.listAlternatives = function() {
var self = this;
if (!this.isAvailable()) {
return [];
}
return self.lodash.map(this.getAlternatives(), function(item) {
return {
name: item.name,
isoCode: item.isoCode
}
});
};
angular.module('copayApp.services').factory('rateService', function($http, lodash) {
// var cfg = _.extend(config.rates, {
// httprequest: $http
// });
var cfg = {
httprequest: $http,
lodash: lodash
};
return RateService.singleton(cfg);
});
'use strict';
angular.module('copayApp.services')
.factory('sjcl', function bitcoreFactory(bwcService) {
var sjcl = bwcService.getSJCL();
return sjcl;
});
'use strict';
angular.module('copayApp.services')
.factory('storageService', function(logHeader, fileStorageService, localStorageService, sjcl, $log, lodash, isCordova) {
var root = {};
// File storage is not supported for writting according to
// https://github.com/apache/cordova-plugin-file/#supported-platforms
var shouldUseFileStorage = isCordova && !isMobile.Windows();
$log.debug('Using file storage:', shouldUseFileStorage);
var storage = shouldUseFileStorage ? fileStorageService : localStorageService;
var getUUID = function(cb) {
// TO SIMULATE MOBILE
//return cb('hola');
if (!window || !window.plugins || !window.plugins.uniqueDeviceID)
return cb(null);
window.plugins.uniqueDeviceID.get(
function(uuid) {
return cb(uuid);
}, cb);
};
var encryptOnMobile = function(text, cb) {
getUUID(function(uuid) {
if (uuid) {
$log.debug('Encrypting profile');
text = sjcl.encrypt(uuid, text);
}
return cb(null, text);
});
};
var decryptOnMobile = function(text, cb) {
var json;
try {
json = JSON.parse(text);
} catch (e) {};
if (!json.iter || !json.ct)
return cb(null, text);
$log.debug('Profile is encrypted');
getUUID(function(uuid) {
if (!uuid)
return cb(new Error('Could not decrypt localstorage profile'));
text = sjcl.decrypt(uuid, text);
return cb(null, text);
});
};
root.tryToMigrate = function(cb) {
if (!shouldUseFileStorage) return cb();
localStorageService.get('profile', function(err, str) {
if (err) return cb(err);
if (!str) return cb();
$log.info('Starting Migration profile to File storage...');
fileStorageService.create('profile', str, function(err) {
if (err) cb(err);
$log.info('Profile Migrated successfully');
localStorageService.get('config', function(err, c) {
if (err) return cb(err);
if (!c) return root.getProfile(cb);
fileStorageService.create('config', c, function(err) {
if (err) {
$log.info('Error migrating config: ignoring', err);
return root.getProfile(cb);
}
$log.info('Config Migrated successfully');
return root.getProfile(cb);
});
});
});
});
};
root.storeNewProfile = function(profile, cb) {
encryptOnMobile(profile.toObj(), function(err, x) {
storage.create('profile', x, cb);
});
};
root.storeProfile = function(profile, cb) {
encryptOnMobile(profile.toObj(), function(err, x) {
storage.set('profile', x, cb);
});
};
root.getProfile = function(cb) {
storage.get('profile', function(err, str) {
if (err || !str)
// Migrate ?
return cb(err);
decryptOnMobile(str, function(err, str) {
if (err) return cb(err);
var p, err;
try {
p = Profile.fromString(str);
} catch (e) {
err = new Error('Could not read profile:' + p);
}
return cb(err, p);
});
});
};
root.deleteProfile = function(cb) {
storage.remove('profile', cb);
};
root.storeFocusedWalletId = function(id, cb) {
storage.set('focusedWalletId', id||'', cb);
};
root.getFocusedWalletId = function(cb) {
storage.get('focusedWalletId', cb);
};
root.getLastAddress = function(walletId, cb) {
storage.get('lastAddress-' + walletId, cb);
};
root.storeLastAddress = function(walletId, address, cb) {
storage.set('lastAddress-' + walletId, address, cb);
};
root.clearLastAddress = function(walletId, cb) {
storage.remove('lastAddress-' + walletId, cb);
};
root.setBackupFlag = function(walletId, cb) {
storage.set('backup-' + walletId, Date.now(), cb);
};
root.getBackupFlag = function(walletId, cb) {
storage.get('backup-' + walletId, cb);
};
root.setCleanAndScanAddresses = function(cb) {
storage.set('CleanAndScanAddresses', Date.now(), cb);
};
root.getCleanAndScanAddresses = function(cb) {
storage.get('CleanAndScanAddresses', cb);
};
root.removeCleanAndScanAddresses = function(cb) {
storage.remove('CleanAndScanAddresses', cb);
};
root.getConfig = function(cb) {
storage.get('config', cb);
};
root.storeConfig = function(val, cb) {
$log.debug('Storing Preferences', val);
storage.set('config', val, cb);
};
root.clearConfig = function(cb) {
storage.remove('config', cb);
};
root.setCopayDisclaimerFlag = function(cb) {
storage.set('agreeDisclaimer', true, cb);
};
root.getCopayDisclaimerFlag = function(cb) {
storage.get('agreeDisclaimer', cb);
};
root.setRemotePrefsStoredFlag = function(cb) {
storage.set('remotePrefStored', true, cb);
};
root.getRemotePrefsStoredFlag = function(cb) {
storage.get('remotePrefStored', cb);
};
root.setGlideraToken = function(network, token, cb) {
storage.set('glideraToken-' + network, token, cb);
};
root.getGlideraToken = function(network, cb) {
storage.get('glideraToken-' + network, cb);
};
root.removeGlideraToken = function(network, cb) {
storage.remove('glideraToken-' + network, cb);
};
return root;
});
'use strict';
/*
* This is a modification from https://github.com/angular/angular.js/blob/master/src/ngTouch/swipe.js
*/
angular.module('copayApp.services')
.factory('$swipe', [
function() {
// The total distance in any direction before we make the call on swipe vs. scroll.
var MOVE_BUFFER_RADIUS = 10;
var POINTER_EVENTS = {
'touch': {
start: 'touchstart',
move: 'touchmove',
end: 'touchend',
cancel: 'touchcancel'
}
};
function getCoordinates(event) {
var originalEvent = event.originalEvent || event;
var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
var e = (originalEvent.changedTouches && originalEvent.changedTouches[0]) || touches[0];
return {
x: e.clientX,
y: e.clientY
};
}
function getEvents(pointerTypes, eventType) {
var res = [];
angular.forEach(pointerTypes, function(pointerType) {
var eventName = POINTER_EVENTS[pointerType][eventType];
if (eventName) {
res.push(eventName);
}
});
return res.join(' ');
}
return {
/**
* @ngdoc method
* @name $swipe#bind
*
* @description
* The main method of `$swipe`. It takes an element to be watched for swipe motions, and an
* object containing event handlers.
* The pointer types that should be used can be specified via the optional
* third argument, which is an array of strings `'mouse'` and `'touch'`. By default,
* `$swipe` will listen for `mouse` and `touch` events.
*
* The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end`
* receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`.
*
* `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is
* watching for `touchmove` or `mousemove` events. These events are ignored until the total
* distance moved in either dimension exceeds a small threshold.
*
* Once this threshold is exceeded, either the horizontal or vertical delta is greater.
* - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow.
* - If the vertical distance is greater, this is a scroll, and we let the browser take over.
* A `cancel` event is sent.
*
* `move` is called on `mousemove` and `touchmove` after the above logic has determined that
* a swipe is in progress.
*
* `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`.
*
* `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling
* as described above.
*
*/
bind: function(element, eventHandlers, pointerTypes) {
// Absolute total movement, used to control swipe vs. scroll.
var totalX, totalY;
// Coordinates of the start position.
var startCoords;
// Last event's position.
var lastPos;
// Whether a swipe is active.
var active = false;
pointerTypes = pointerTypes || ['touch'];
element.on(getEvents(pointerTypes, 'start'), function(event) {
startCoords = getCoordinates(event);
active = true;
totalX = 0;
totalY = 0;
lastPos = startCoords;
eventHandlers['start'] && eventHandlers['start'](startCoords, event);
});
var events = getEvents(pointerTypes, 'cancel');
if (events) {
element.on(events, function(event) {
active = false;
eventHandlers['cancel'] && eventHandlers['cancel'](event);
});
}
element.on(getEvents(pointerTypes, 'move'), function(event) {
if (!active) return;
// Android will send a touchcancel if it thinks we're starting to scroll.
// So when the total distance (+ or - or both) exceeds 10px in either direction,
// we either:
// - On totalX > totalY, we send preventDefault() and treat this as a swipe.
// - On totalY > totalX, we let the browser handle it as a scroll.
if (!startCoords) return;
var coords = getCoordinates(event);
totalX += Math.abs(coords.x - lastPos.x);
totalY += Math.abs(coords.y - lastPos.y);
lastPos = coords;
if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) {
return;
}
// One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll.
if (totalY > totalX) {
// Allow native scrolling to take over.
active = false;
eventHandlers['cancel'] && eventHandlers['cancel'](event);
return;
} else {
// Prevent the browser from scrolling.
event.preventDefault();
eventHandlers['move'] && eventHandlers['move'](coords, event);
}
});
element.on(getEvents(pointerTypes, 'end'), function(event) {
if (!active) return;
active = false;
eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event);
});
}
};
}
]);
'use strict';
angular.module('copayApp.services').factory('txFormatService', function(profileService, rateService, configService, lodash) {
var root = {};
var formatAmountStr = function(amount) {
if (!amount) return;
var config = configService.getSync().wallet.settings;
return profileService.formatAmount(amount) + ' ' + config.unitName;
};
var formatAlternativeStr = function(amount) {
if (!amount) return;
var config = configService.getSync().wallet.settings;
return (rateService.toFiat(amount, config.alternativeIsoCode) ? rateService.toFiat(amount, config.alternativeIsoCode).toFixed(2) : 'N/A') + ' ' + config.alternativeIsoCode;
};
var formatFeeStr = function(fee) {
if (!fee) return;
var config = configService.getSync().wallet.settings;
return profileService.formatAmount(fee) + ' ' + config.unitName;
};
root.processTx = function(tx) {
if (!tx) return;
var outputs = tx.outputs ? tx.outputs.length : 0;
if (outputs > 1 && tx.action != 'received') {
tx.hasMultiplesOutputs = true;
tx.recipientCount = outputs;
tx.amount = lodash.reduce(tx.outputs, function(total, o) {
o.amountStr = formatAmountStr(o.amount);
o.alternativeAmountStr = formatAlternativeStr(o.amount);
return total + o.amount;
}, 0);
}
tx.amountStr = formatAmountStr(tx.amount);
tx.alternativeAmountStr = formatAlternativeStr(tx.amount);
tx.feeStr = formatFeeStr(tx.fee || tx.fees);
return tx;
};
return root;
});
'use strict';
angular.module('copayApp.services').factory('txStatus', function($modal, lodash, profileService, $timeout) {
var root = {};
root.notify = function(txp, cb) {
var fc = profileService.focusedClient;
var status = txp.status;
var type;
var INMEDIATE_SECS = 10;
if (status == 'broadcasted') {
type = 'broadcasted';
} else {
var n = txp.actions.length;
var action = lodash.find(txp.actions, {
copayerId: fc.credentials.copayerId
});
if (!action) {
type = 'created';
} else if (action.type == 'accept') {
// created and accepted at the same time?
if ( n == 1 && action.createdOn - txp.createdOn < INMEDIATE_SECS ) {
type = 'created';
} else {
type = 'accepted';
}
} else if (action.type == 'reject') {
type = 'rejected';
} else {
throw new Error('Unknown type:' + type);
}
}
openModal(type, txp, cb);
};
root._templateUrl = function(type, txp) {
return 'views/modals/tx-status.html';
};
var openModal = function(type, txp, cb) {
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.type = type;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
if (cb) $timeout(cb, 100);
};
var modalInstance = $modal.open({
templateUrl: root._templateUrl(type, txp),
windowClass: 'popup-tx-status full',
controller: ModalInstanceCtrl,
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass('hideModal');
});
};
return root;
});
'use strict';
var UriHandler = function() {};
UriHandler.prototype.register = function() {
var base = window.location.origin + '/';
var url = base + '#/uri-payment/%s';
if(navigator.registerProtocolHandler) {
navigator.registerProtocolHandler('bitcoin', url, 'Copay');
}
};
angular.module('copayApp.services').value('uriHandler', new UriHandler());
'use strict';
angular.module('copayApp.services')
.factory('uxLanguage', function languageService($log, lodash, gettextCatalog, amMoment, configService) {
var root = {};
root.availableLanguages = [{
name: 'English',
isoCode: 'en',
}, {
name: 'Français',
isoCode: 'fr',
}, {
name: 'Italiano',
isoCode: 'it',
}, {
name: 'Deutsch',
isoCode: 'de',
}, {
name: 'Español',
isoCode: 'es',
}, {
name: 'Português',
isoCode: 'pt',
}, {
name: 'Ελληνικά',
isoCode: 'el',
}, {
name: '日本語',
isoCode: 'ja',
useIdeograms: true,
}, {
name: 'Pусский',
isoCode: 'ru',
}, {
name: 'Türk',
isoCode: 'tr',
}];
root.currentLanguage = null;
root._detect = function() {
// Auto-detect browser language
var userLang, androidLang;
if (navigator && navigator.userAgent && (androidLang = navigator.userAgent.match(/android.*\W(\w\w)-(\w\w)\W/i))) {
userLang = androidLang[1];
} else {
// works for iOS and Android 4.x
userLang = navigator.userLanguage || navigator.language;
}
userLang = userLang ? (userLang.split('-', 1)[0] || 'en') : 'en';
return userLang;
};
root._set = function(lang) {
$log.debug('Setting default language: ' + lang);
gettextCatalog.setCurrentLanguage(lang);
amMoment.changeLocale(lang);
root.currentLanguage = lang;
};
root.getCurrentLanguage = function() {
return root.currentLanguage;
};
root.getCurrentLanguageName = function() {
return root.getName(root.currentLanguage);
};
root.getCurrentLanguageInfo = function() {
return lodash.find(root.availableLanguages, {
'isoCode': root.currentLanguage
});
};
root.getLanguages = function() {
return root.availableLanguages;
};
root.init = function() {
root._set(root._detect());
};
root.update = function() {
var userLang = configService.getSync().wallet.settings.defaultLanguage;
if (!userLang) {
userLang = root._detect();
}
if (userLang != gettextCatalog.getCurrentLanguage()) {
root._set(userLang);
}
return userLang;
};
root.getName = function(lang) {
return lodash.result(lodash.find(root.availableLanguages, {
'isoCode': lang
}), 'name');
};
return root;
});
'use strict';
angular.module('copayApp.controllers').controller('wordsController',
function($rootScope, $scope, $timeout, profileService, go, gettext, confirmDialog, notification, bwsError, $log) {
var msg = gettext('Are you sure you want to delete the backup words?');
var successMsg = gettext('Backup words deleted');
this.show = false;
var self = this;
this.toggle = function() {
this.show = !this.show;
if (this.show)
$rootScope.$emit('Local/BackupDone');
$timeout(function(){
$scope.$apply();
}, 1);
};
this.delete = function() {
var fc = profileService.focusedClient;
confirmDialog.show(msg, function(ok) {
if (ok) {
fc.clearMnemonic();
profileService.updateCredentialsFC(function() {
notification.success(successMsg);
go.walletHome();
});
}
});
};
$scope.$on('$destroy', function() {
profileService.lockFC();
});
function setWords(words) {
if (words) {
self.mnemonicWords = words.split(/[\u3000\s]+/);
self.mnemonicHasPassphrase = fc.mnemonicHasPassphrase();
self.useIdeograms = words.indexOf("\u3000") >= 0;
}
};
var fc = profileService.focusedClient;
try {
setWords(fc.getMnemonic());
} catch (e) {
if (e.message && e.message.match(/encrypted/) && fc.isPrivKeyEncrypted()) {
self.credentialsEncrypted = true;
$timeout(function(){
$scope.$apply();
}, 1);
profileService.unlockFC(function(err) {
if (err) {
self.error = bwsError.msg(err, gettext('Could not decrypt'));
$log.warn('Error decrypting credentials:',self.error); //TODO
return;
}
self.credentialsEncrypted = false;
setWords(fc.getMnemonic());
});
}
}
});
'use strict';
angular.module('copayApp.controllers').controller('buyGlideraController',
function($scope, $timeout, $modal, profileService, addressService, glideraService, bwsError, lodash, isChromeApp, animationService) {
var self = this;
this.show2faCodeInput = null;
this.error = null;
this.success = null;
this.loading = null;
this.otherWallets = function(testnet) {
var network = testnet ? 'testnet' : 'livenet';
return lodash.filter(profileService.getWallets(network), function(w) {
return w.network == network;
});
};
$scope.openWalletsModal = function(wallets) {
self.error = null;
self.selectedWalletId = null;
self.selectedWalletName = null;
self.selectedWalletAddr = null;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.wallets = wallets;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
$scope.selectWallet = function(walletId, walletName) {
if (!profileService.getClient(walletId).isComplete()) {
self.error = bwsError.msg({'code': 'WALLET_NOT_COMPLETE'}, 'Could not choose the wallet');
$modalInstance.dismiss('cancel');
return;
}
addressService.getAddress(walletId, false, function(err, walletAddr) {
if (err) {
self.error = bwsError.cb(err, 'Could not create address');
$modalInstance.dismiss('cancel');
return;
}
$modalInstance.close({
'walletId': walletId,
'walletName': walletName,
'walletAddr': walletAddr
});
});
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/wallets.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
modalInstance.result.then(function(obj) {
$timeout(function() {
self.selectedWalletId = obj.walletId;
self.selectedWalletName = obj.walletName;
self.selectedWalletAddr = obj.walletAddr;
$scope.$apply();
}, 100);
});
};
this.getBuyPrice = function(token, price) {
var self = this;
this.error = null;
if (!price || (price && !price.qty && !price.fiat)) {
this.buyPrice = null;
return;
}
this.gettingBuyPrice = true;
glideraService.buyPrice(token, price, function(err, buyPrice) {
self.gettingBuyPrice = false;
if (err) {
self.error = 'Could not get exchange information. Please, try again.';
}
else {
self.buyPrice = buyPrice;
}
});
};
this.get2faCode = function(token) {
var self = this;
this.loading = 'Sending 2FA code...';
$timeout(function() {
glideraService.get2faCode(token, function(err, sent) {
self.loading = null;
if (err) {
self.error = 'Could not send confirmation code to your phone';
}
else {
self.error = null;
self.show2faCodeInput = sent;
}
});
}, 100);
};
this.sendRequest = function(token, permissions, twoFaCode) {
var self = this;
self.error = null;
self.loading = 'Buying bitcoin...';
var data = {
destinationAddress: self.selectedWalletAddr,
qty: self.buyPrice.qty,
priceUuid: self.buyPrice.priceUuid,
useCurrentPrice: false,
ip: null
};
$timeout(function() {
glideraService.buy(token, twoFaCode, data, function(err, data) {
self.loading = null;
if (err) {
self.error = err;
}
else {
self.success = data;
$scope.$emit('Local/GlideraTx');
}
});
}, 100);
};
});
'use strict';
angular.module('copayApp.controllers').controller('copayersController',
function($scope, $rootScope, $timeout, $log, $modal, profileService, go, notification, isCordova, gettext, gettextCatalog, animationService) {
var self = this;
var delete_msg = gettextCatalog.getString('Are you sure you want to delete this wallet?');
var accept_msg = gettextCatalog.getString('Accept');
var cancel_msg = gettextCatalog.getString('Cancel');
var confirm_msg = gettextCatalog.getString('Confirm');
self.init = function() {
var fc = profileService.focusedClient;
if (fc.isComplete()) {
$log.debug('Wallet Complete...redirecting')
go.walletHome();
return;
}
self.loading = false;
self.isCordova = isCordova;
};
var _modalDeleteWallet = function() {
var ModalInstanceCtrl = function($scope, $modalInstance, gettext) {
$scope.title = delete_msg;
$scope.loading = false;
$scope.ok = function() {
$scope.loading = true;
$modalInstance.close(accept_msg);
};
$scope.cancel = function() {
$modalInstance.dismiss(cancel_msg);
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/confirmation.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
modalInstance.result.then(function(ok) {
if (ok) {
_deleteWallet();
}
});
};
var _deleteWallet = function() {
var fc = profileService.focusedClient;
$timeout(function() {
var fc = profileService.focusedClient;
var walletName = fc.credentials.walletName;
profileService.deleteWalletFC({}, function(err) {
if (err) {
this.error = err.message || err;
console.log(err);
$timeout(function() {
$scope.$digest();
});
} else {
go.walletHome();
$timeout(function() {
notification.success(gettextCatalog.getString('Success'), gettextCatalog.getString('The wallet "{{walletName}}" was deleted', {walletName: walletName}));
});
}
});
}, 100);
};
self.deleteWallet = function() {
var fc = profileService.focusedClient;
if (isCordova) {
navigator.notification.confirm(
delete_msg,
function(buttonIndex) {
if (buttonIndex == 1) {
_deleteWallet();
}
},
confirm_msg, [accept_msg, cancel_msg]
);
} else {
_modalDeleteWallet();
}
};
self.copySecret = function(secret) {
if (isCordova) {
window.cordova.plugins.clipboard.copy(secret);
window.plugins.toast.showShortCenter(gettextCatalog.getString('Copied to clipboard'));
}
};
self.shareSecret = function(secret) {
if (isCordova) {
if (isMobile.Android() || isMobile.Windows()) {
window.ignoreMobilePause = true;
}
var message = gettextCatalog.getString('Join my Copay wallet. Here is the invitation code: {{secret}} You can download Copay for your phone or desktop at https://copay.io', {secret: secret});
window.plugins.socialsharing.share(message, gettextCatalog.getString('Invitation to share a Copay Wallet'), null, null);
}
};
});
'use strict';
angular.module('copayApp.controllers').controller('createController',
function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isMobile, isCordova, gettext, isChromeApp, ledger) {
var self = this;
var defaults = configService.getDefaults();
this.isWindowsPhoneApp = isMobile.Windows() && isCordova;
/* For compressed keys, m*73 + n*34 <= 496 */
var COPAYER_PAIR_LIMITS = {
1: 1,
2: 2,
3: 3,
4: 4,
5: 4,
6: 4,
7: 3,
8: 3,
9: 2,
10: 2,
11: 1,
12: 1,
};
// ng-repeat defined number of times instead of repeating over array?
this.getNumber = function(num) {
return new Array(num);
}
var updateRCSelect = function(n) {
$scope.totalCopayers = n;
var maxReq = COPAYER_PAIR_LIMITS[n];
self.RCValues = lodash.range(1, maxReq + 1);
$scope.requiredCopayers = Math.min(parseInt(n / 2 + 1), maxReq);
};
this.TCValues = lodash.range(2, defaults.limits.totalCopayers + 1);
$scope.totalCopayers = defaults.wallet.totalCopayers;
this.setTotalCopayers = function(tc) {
updateRCSelect(tc);
};
this.isChromeApp = function() {
return isChromeApp;
};
this.create = function(form) {
if (form && form.$invalid) {
this.error = gettext('Please enter the required fields');
return;
}
var opts = {
m: $scope.requiredCopayers,
n: $scope.totalCopayers,
name: form.walletName.$modelValue,
myName: $scope.totalCopayers > 1 ? form.myName.$modelValue : null,
networkName: form.isTestnet.$modelValue ? 'testnet' : 'livenet',
};
var setSeed = form.setSeed.$modelValue;
if (setSeed) {
var words = form.privateKey.$modelValue;
if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) {
opts.extendedPrivateKey = words;
} else {
opts.mnemonic = words;
}
opts.passphrase = form.passphrase.$modelValue;
} else {
opts.passphrase = form.createPassphrase.$modelValue;
}
if (setSeed && !opts.mnemonic && !opts.extendedPrivateKey) {
this.error = gettext('Please enter the wallet seed');
return;
}
if (form.hwLedger.$modelValue) {
self.ledger = true;
// TODO : account
ledger.getInfoForNewWallet(0, function(err, lopts) {
self.ledger = false;
if (err) {
self.error = err;
$scope.$apply();
return;
}
opts = lodash.assign(lopts, opts);
self._create(opts);
});
} else {
self._create(opts);
}
};
this._create = function(opts) {
self.loading = true;
$timeout(function() {
profileService.createWallet(opts, function(err, secret, walletId) {
self.loading = false;
if (err) {
$log.warn(err);
self.error = err;
$timeout(function() {
$rootScope.$apply();
});
return;
}
if (opts.mnemonic || opts.externalSource || opts.extendedPrivateKey) {
if (opts.n == 1) {
$rootScope.$emit('Local/WalletImported', walletId);
}
}
go.walletHome();
});
}, 100);
}
this.formFocus = function(what) {
if (!this.isWindowsPhoneApp) return
if (what && what == 'my-name') {
this.hideWalletName = true;
this.hideTabs = true;
} else if (what && what == 'wallet-name') {
this.hideTabs = true;
} else {
this.hideWalletName = false;
this.hideTabs = false;
}
$timeout(function() {
$rootScope.$digest();
}, 1);
};
$scope.$on("$destroy", function() {
$rootScope.hideWalletNavigation = false;
});
});
'use strict';
angular.module('copayApp.controllers').controller('DevLoginController', function($scope, $rootScope, $routeParams, identityService) {
var mail = $routeParams.mail;
var password = $routeParams.password;
var form = {};
form.email = {};
form.password = {};
form.email.$modelValue = mail;
form.password.$modelValue = password;
identityService.open($scope, form);
});
'use strict';
angular.module('copayApp.controllers').controller('disclaimerController',
function($scope, $timeout, storageService, applicationService, gettextCatalog, isCordova, uxLanguage) {
$scope.agree = function() {
if (isCordova) {
window.plugins.spinnerDialog.show(null, gettextCatalog.getString('Loading...'), true);
}
$scope.loading = true;
$timeout(function() {
storageService.setCopayDisclaimerFlag(function(err) {
$timeout(function() {
if (isCordova) {
window.plugins.spinnerDialog.hide();
}
applicationService.restart();
}, 1000);
});
}, 100);
};
$scope.init = function() {
storageService.getCopayDisclaimerFlag(function(err, val) {
$scope.lang = uxLanguage.currentLanguage;
$scope.agreed = val;
$timeout(function() {
$scope.$digest();
}, 1);
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('backupController',
function($rootScope, $scope, $timeout, backupService, profileService, isMobile, isCordova, notification, go, gettext, gettextCatalog) {
this.isSafari = isMobile.Safari();
this.isCordova = isCordova;
this.error = null;
this.success = null;
var fc = profileService.focusedClient;
this.isEncrypted = fc.isPrivKeyEncrypted();
this.downloadWalletBackup = function() {
var self = this;
var opts = {
noSign: $scope.noSign,
};
backupService.walletDownload(this.password, opts, function(err) {
if (err) {
self.error = true;
return ;
}
$rootScope.$emit('Local/BackupDone');
notification.success(gettext('Success'), gettext('Encrypted export file saved'));
go.walletHome();
});
};
this.getBackup = function() {
var opts = {
noSign: $scope.noSign,
};
var ew = backupService.walletExport(this.password, opts);
if (!ew) {
this.error = true;
} else {
this.error = false;
}
return ew;
};
this.viewWalletBackup = function() {
var self = this;
$timeout(function() {
var ew = self.getBackup();
if (!ew) return;
self.backupWalletPlainText = ew;
$rootScope.$emit('Local/BackupDone');
}, 100);
};
this.copyWalletBackup = function() {
var ew = this.getBackup();
if (!ew) return;
window.cordova.plugins.clipboard.copy(ew);
window.plugins.toast.showShortCenter(gettextCatalog.getString('Copied to clipboard'));
$rootScope.$emit('Local/BackupDone');
};
this.sendWalletBackup = function() {
var fc = profileService.focusedClient;
if (isMobile.Android() || isMobile.Windows()) {
window.ignoreMobilePause = true;
}
window.plugins.toast.showShortCenter(gettextCatalog.getString('Preparing backup...'));
var name = (fc.credentials.walletName || fc.credentials.walletId);
if (fc.alias) {
name = fc.alias + ' [' + name + ']';
}
var ew = this.getBackup();
if (!ew) return;
if( $scope.noSign)
name = name + '(No Private Key)';
var properties = {
subject: 'Copay Wallet Backup: ' + name,
body: 'Here is the encrypted backup of the wallet ' + name + ': \n\n' + ew + '\n\n To import this backup, copy all text between {...}, including the symbols {}',
isHtml: false
};
$rootScope.$emit('Local/BackupDone');
window.plugin.email.open(properties);
};
});
'use strict';
angular.module('copayApp.controllers').controller('glideraController',
function($scope, $timeout, $modal, profileService, configService, storageService, glideraService, isChromeApp) {
var config = configService.getSync().wallet.settings;
this.getAuthenticateUrl = function() {
return glideraService.getOauthCodeUrl();
};
this.submitOauthCode = function(code) {
var fc = profileService.focusedClient;
var self = this;
this.loading = true;
this.error = null;
$timeout(function() {
glideraService.getToken(code, function(err, data) {
self.loading = null;
if (err) {
self.error = err;
$timeout(function() {
$scope.$apply();
}, 100);
}
else if (data && data.access_token) {
storageService.setGlideraToken(fc.credentials.network, data.access_token, function() {
$scope.$emit('Local/GlideraUpdated', data.access_token);
$timeout(function() {
$scope.$apply();
}, 100);
});
}
});
}, 100);
};
// DISABLE ANIMATION ON CHROMEAPP
if (isChromeApp) {
var animatedSlideRight = 'full';
}
else {
var animatedSlideRight = 'full animated slideInRight';
}
this.openTxModal = function(token, tx) {
var self = this;
var fc = profileService.focusedClient;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.tx = tx;
$scope.settings = config;
$scope.color = fc.backgroundColor;
glideraService.getTransaction(token, tx.transactionUuid, function(error, tx) {
$scope.tx = tx;
});
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/glidera-tx-details.html',
windowClass: animatedSlideRight,
controller: ModalInstanceCtrl,
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass('slideOutRight');
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('glideraUriController',
function($scope, $stateParams, $timeout, profileService, glideraService, storageService, go) {
this.submitOauthCode = function(code) {
var fc = profileService.focusedClient;
var self = this;
this.loading = true;
this.error = null;
$timeout(function() {
glideraService.getToken(code, function(err, data) {
self.loading = null;
if (err) {
self.error = err;
$timeout(function() {
$scope.$apply();
}, 100);
}
else if (data && data.access_token) {
storageService.setGlideraToken(fc.credentials.network, data.access_token, function() {
$scope.$emit('Local/GlideraUpdated', data.access_token);
$timeout(function() {
go.path('glidera');
$scope.$apply();
}, 100);
});
}
});
}, 100);
};
this.checkCode = function() {
this.code = $stateParams.code;
this.submitOauthCode(this.code);
};
});
'use strict';
angular.module('copayApp.controllers').controller('importController',
function($scope, $rootScope, $location, $timeout, $log, profileService, notification, go, isMobile, isCordova, sjcl, gettext, lodash, ledger) {
var self = this;
this.isSafari = isMobile.Safari();
this.isCordova = isCordova;
var reader = new FileReader();
window.ignoreMobilePause = true;
$scope.$on('$destroy', function() {
$timeout(function() {
window.ignoreMobilePause = false;
}, 100);
});
this.setType = function(type) {
$scope.type = type;
this.error = null;
$timeout(function() {
$rootScope.$apply();
});
};
var _importBlob = function(str, opts) {
var str2, err;
try {
str2 = sjcl.decrypt(self.password, str);
} catch (e) {
err = gettext('Could not decrypt file, check your password');
$log.warn(e);
};
if (err) {
self.error = err;
$timeout(function() {
$rootScope.$apply();
});
return;
}
self.loading = true;
$timeout(function() {
profileService.importWallet(str2, {
compressed: null,
password: null
}, function(err, walletId) {
self.loading = false;
if (err) {
self.error = err;
} else {
$rootScope.$emit('Local/WalletImported', walletId);
go.walletHome();
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
}
});
}, 100);
};
var _importExtendedPrivateKey = function(xPrivKey) {
self.loading = true;
$timeout(function() {
profileService.importExtendedPrivateKey(xPrivKey, function(err, walletId) {
self.loading = false;
if (err) {
self.error = err;
return $timeout(function() {
$scope.$apply();
});
}
$rootScope.$emit('Local/WalletImported', walletId);
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
go.walletHome();
});
}, 100);
};
var _importMnemonic = function(words, opts) {
self.loading = true;
$timeout(function() {
profileService.importMnemonic(words, opts, function(err, walletId) {
self.loading = false;
if (err) {
self.error = err;
return $timeout(function() {
$scope.$apply();
});
}
$rootScope.$emit('Local/WalletImported', walletId);
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
go.walletHome();
});
}, 100);
};
$scope.getFile = function() {
// If we use onloadend, we need to check the readyState.
reader.onloadend = function(evt) {
if (evt.target.readyState == FileReader.DONE) { // DONE == 2
_importBlob(evt.target.result);
}
}
};
this.importBlob = function(form) {
if (form.$invalid) {
this.error = gettext('There is an error in the form');
$timeout(function() {
$scope.$apply();
});
return;
}
var backupFile = $scope.file;
var backupText = form.backupText.$modelValue;
var password = form.password.$modelValue;
if (!backupFile && !backupText) {
this.error = gettext('Please, select your backup file');
$timeout(function() {
$scope.$apply();
});
return;
}
if (backupFile) {
reader.readAsBinaryString(backupFile);
} else {
_importBlob(backupText);
}
};
this.importMnemonic = function(form) {
if (form.$invalid) {
this.error = gettext('There is an error in the form');
$timeout(function() {
$scope.$apply();
});
return;
}
var opts = {};
var passphrase = form.passphrase.$modelValue;
var words = form.words.$modelValue;
this.error = null;
if (!words) {
this.error = gettext('Please enter the seed words');
} else if (words.indexOf('xprv') == 0 || words.indexOf('tprv') == 0) {
return _importExtendedPrivateKey(words)
} else {
var wordList = words.split(/[\u3000\s]+/);
if ((wordList.length % 3) != 0)
this.error = gettext('Wrong number of seed words:') + wordList.length;
}
if (this.error) {
$timeout(function() {
$scope.$apply();
});
return;
}
opts.passphrase = form.passphrase.$modelValue || null;
opts.networkName = form.isTestnet.$modelValue ? 'testnet' : 'livenet';
_importMnemonic(words, opts);
};
this.importLedger = function(form) {
var self = this;
if (form.$invalid) {
this.error = gettext('There is an error in the form');
$timeout(function() {
$scope.$apply();
});
return;
}
self.ledger = true;
// TODO account
ledger.getInfoForNewWallet(0, function(err, lopts) {
self.ledger = false;
if (err) {
self.error = err;
$scope.$apply();
return;
}
lopts.externalIndex = $scope.externalIndex;
lopts.externalSource = 'ledger';
self.loading = true;
$log.debug('Import opts', lopts);
profileService.importExtendedPublicKey(lopts, function(err, walletId) {
self.loading = false;
if (err) {
self.error = err;
return $timeout(function() {
$scope.$apply();
});
}
$rootScope.$emit('Local/WalletImported', walletId);
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
go.walletHome();
});
}, 100);
};
});
'use strict';
angular.module('copayApp.controllers').controller('importLegacyController',
function($rootScope, $scope, $log, $timeout, notification, legacyImportService, profileService, go, lodash, bitcore, gettext, gettextCatalog) {
var self = this;
self.messages = [];
self.fromCloud = true;
self.server = "https://insight.bitpay.com:443/api/email";
$rootScope.$on('Local/ImportStatusUpdate', function(event, status) {
$timeout(function() {
$log.debug(status);
self.messages.unshift({
message: status,
});
var op = 1;
lodash.each(self.messages, function(m) {
if (op < 0.1) op = 0.1;
m.opacity = op;
op = op - 0.15;
});
}, 100);
});
self.scan = function(ids) {
$log.debug('### Scaning: ' + ids)
var i = 0;
lodash.each(ids, function(id) {
$rootScope.$emit('Local/WalletImported', id);
if (++i == ids.length) {
go.walletHome();
};
});
};
self.import = function(form) {
var username = form.username.$modelValue;
var password = form.password.$modelValue;
var serverURL = form.server.$modelValue;
var fromCloud = form.fromCloud.$modelValue;
self.error = null;
self.importing = true;
$timeout(function() {
legacyImportService.import(username, password, serverURL, fromCloud, function(err, ids, toScanIds) {
if (err || !ids || !ids.length) {
self.importing = false;
self.error = err || gettext('Failed to import wallets');
return;
}
notification.success( gettextCatalog.getString('{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance', {len: ids.length}));
self.scan(toScanIds);
});
}, 100);
};
// TODO destroy event...
});
'use strict';
angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, amMoment, nodeWebkit, addonManager, feeService, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService) {
var self = this;
self.isCordova = isCordova;
self.isChromeApp = isChromeApp;
self.onGoingProcess = {};
self.limitHistory = 5;
function strip(number) {
return (parseFloat(number.toPrecision(12)));
};
self.goHome = function() {
go.walletHome();
};
self.menu = [{
'title': gettext('Home'),
'icon': 'icon-home',
'link': 'walletHome'
}, {
'title': gettext('Receive'),
'icon': 'icon-receive2',
'link': 'receive'
}, {
'title': gettext('Send'),
'icon': 'icon-paperplane',
'link': 'send'
}, {
'title': gettext('History'),
'icon': 'icon-history',
'link': 'history'
}];
self.addonViews = addonManager.addonViews();
self.menu = self.menu.concat(addonManager.addonMenuItems());
self.menuItemSize = self.menu.length > 5 ? 2 : 3;
self.txTemplateUrl = addonManager.txTemplateUrl() || 'views/includes/transaction.html';
self.tab = 'walletHome';
self.feeOpts = feeService.feeOpts;
self.setOngoingProcess = function(processName, isOn) {
$log.debug('onGoingProcess', processName, isOn);
self[processName] = isOn;
self.onGoingProcess[processName] = isOn;
var name;
self.anyOnGoingProcess = lodash.any(self.onGoingProcess, function(isOn, processName) {
if (isOn)
name = name || processName;
return isOn;
});
// The first one
self.onGoingProcessName = name;
$timeout(function() {
$rootScope.$apply();
});
};
self.setFocusedWallet = function() {
var fc = profileService.focusedClient;
if (!fc) return;
// Clean status
self.totalBalanceSat = null;
self.lockedBalanceSat = null;
self.availableBalanceSat = null;
self.pendingAmount = null;
self.spendUnconfirmed = null;
self.totalBalanceStr = null;
self.availableBalanceStr = null;
self.lockedBalanceStr = null;
self.alternativeBalanceAvailable = false;
self.totalBalanceAlternative = null;
self.currentFeeLevel = null;
self.notAuthorized = false;
self.txHistory = [];
self.balanceByAddress = null;
self.txHistoryPaging = false;
self.pendingTxProposalsCountForUs = null;
self.setSpendUnconfirmed();
$timeout(function() {
self.hasProfile = true;
self.noFocusedWallet = false;
self.onGoingProcess = {};
// Credentials Shortcuts
self.m = fc.credentials.m;
self.n = fc.credentials.n;
self.network = fc.credentials.network;
self.copayerId = fc.credentials.copayerId;
self.copayerName = fc.credentials.copayerName;
self.requiresMultipleSignatures = fc.credentials.m > 1;
self.isShared = fc.credentials.n > 1;
self.walletName = fc.credentials.walletName;
self.walletId = fc.credentials.walletId;
self.isComplete = fc.isComplete();
self.canSign = fc.canSign();
self.isPrivKeyExternal = fc.isPrivKeyExternal();
self.externalSource = fc.getPrivKeyExternalSourceName();
self.txps = [];
self.copayers = [];
self.updateColor();
self.updateAlias();
// DISABLED
//self.initGlidera();
if (fc.isPrivKeyExternal()) {
self.needsBackup = false;
self.openWallet();
} else {
storageService.getBackupFlag(self.walletId, function(err, val) {
self.needsBackup = self.network == 'testnet' ? false : !val;
self.openWallet();
});
}
});
};
self.setTab = function(tab, reset, tries, switchState) {
tries = tries || 0;
// check if the whole menu item passed
if (typeof tab == 'object') {
if (tab.open) {
if (tab.link) {
self.tab = tab.link;
}
tab.open();
return;
} else {
return self.setTab(tab.link, reset, tries, switchState);
}
}
if (self.tab === tab && !reset)
return;
if (!document.getElementById('menu-' + tab) && ++tries < 5) {
return $timeout(function() {
self.setTab(tab, reset, tries, switchState);
}, 300);
}
if (!self.tab || !$state.is('walletHome'))
self.tab = 'walletHome';
var changeTab = function() {
if (document.getElementById(self.tab)) {
document.getElementById(self.tab).className = 'tab-out tab-view ' + self.tab;
var old = document.getElementById('menu-' + self.tab);
if (old) {
old.className = '';
}
}
if (document.getElementById(tab)) {
document.getElementById(tab).className = 'tab-in tab-view ' + tab;
var newe = document.getElementById('menu-' + tab);
if (newe) {
newe.className = 'active';
}
}
self.tab = tab;
$rootScope.$emit('Local/TabChanged', tab);
};
if (switchState && !$state.is('walletHome')) {
go.path('walletHome', function() {
changeTab();
});
return;
}
changeTab();
};
self._updateRemotePreferencesFor = function(clients, prefs, cb) {
var client = clients.shift();
if (!client)
return cb();
$log.debug('Saving remote preferences', client.credentials.walletName, prefs);
client.savePreferences(prefs, function(err) {
// we ignore errors here
if (err) $log.warn(err);
self._updateRemotePreferencesFor(clients, prefs, cb);
});
};
self.updateRemotePreferences = function(opts, cb) {
var prefs = opts.preferences || {};
var fc = profileService.focusedClient;
// Update this JIC.
var config = configService.getSync().wallet.settings;
//prefs.email (may come from arguments)
prefs.language = self.defaultLanguageIsoCode;
prefs.unit = config.unitCode;
var clients = [];
if (opts.saveAll) {
clients = lodash.values(profileService.walletClients);
} else {
clients = [fc];
};
self._updateRemotePreferencesFor(clients, prefs, function(err) {
if (err) return cb(err);
if (!fc) return cb();
fc.getPreferences(function(err, preferences) {
if (err) {
return cb(err);
}
self.preferences = preferences;
return cb();
});
});
};
var _walletStatusHash = function(walletStatus) {
var bal;
if (walletStatus) {
bal = walletStatus.balance.totalAmount;
} else {
bal = self.totalBalanceSat;
}
return bal;
};
self.updateAll = function(opts, initStatusHash, tries) {
tries = tries || 0;
opts = opts || {};
if (opts.untilItChanges && lodash.isUndefined(initStatusHash)) {
initStatusHash = _walletStatusHash();
$log.debug('Updating status until it changes. initStatusHash:' + initStatusHash)
}
var get = function(cb) {
if (opts.walletStatus)
return cb(null, opts.walletStatus);
else {
self.updateError = false;
return fc.getStatus({}, function(err, ret) {
if (err) {
self.updateError = bwsError.msg(err, gettext('Could not update Wallet'));
} else {
if (!opts.quiet)
self.setOngoingProcess('scanning', ret.wallet.scanning);
}
return cb(err, ret);
});
}
};
var fc = profileService.focusedClient;
if (!fc) return;
$timeout(function() {
if (!opts.quiet)
self.setOngoingProcess('updatingStatus', true);
$log.debug('Updating Status:', fc.credentials.walletName, tries);
get(function(err, walletStatus) {
var currentStatusHash = _walletStatusHash(walletStatus);
$log.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries);
if (!err && opts.untilItChanges && initStatusHash == currentStatusHash && tries < 7) {
return $timeout(function() {
$log.debug('Retrying update... Try:' + tries)
return self.updateAll({
walletStatus: null,
untilItChanges: true,
triggerTxUpdate: opts.triggerTxUpdate,
}, initStatusHash, ++tries);
}, 1400 * tries);
}
if (!opts.quiet)
self.setOngoingProcess('updatingStatus', false);
if (err) {
self.handleError(err);
return;
}
$log.debug('Wallet Status:', walletStatus);
self.setPendingTxps(walletStatus.pendingTxps);
self.setFeesOpts();
// Status Shortcuts
self.walletName = walletStatus.wallet.name;
self.walletSecret = walletStatus.wallet.secret;
self.walletStatus = walletStatus.wallet.status;
self.walletScanStatus = walletStatus.wallet.scanStatus;
self.copayers = walletStatus.wallet.copayers;
self.preferences = walletStatus.preferences;
self.setBalance(walletStatus.balance);
self.otherWallets = lodash.filter(profileService.getWallets(self.network), function(w) {
return w.id != self.walletId;
});
// Notify external addons or plugins
$rootScope.$emit('Local/BalanceUpdated', walletStatus.balance);
$rootScope.$apply();
if (opts.triggerTxUpdate) {
$timeout(function() {
self.updateTxHistory();
}, 1);
}
});
});
};
self.setSpendUnconfirmed = function() {
self.spendUnconfirmed = configService.getSync().wallet.spendUnconfirmed;
};
self.setSendMax = function() {
// Set Send max
if (self.currentFeeLevel && self.totalBytesToSendMax) {
feeService.getCurrentFeeValue(self.currentFeeLevel, function(err, feePerKb) {
// KB to send max
if (self.totalBytesToSendMax) {
var feeToSendMaxSat = parseInt(((self.totalBytesToSendMax * feePerKb) / 1000.).toFixed(0));
self.feeRateToSendMax = feePerKb;
self.availableMaxBalance = strip((self.availableBalanceSat - feeToSendMaxSat) * self.satToUnit);
self.feeToSendMaxStr = profileService.formatAmount(feeToSendMaxSat) + ' ' + self.unitName;
} else {
self.feeToSendMaxStr = null;
self.feeRateToSendMax = null;
}
});
}
};
self.setCurrentFeeLevel = function(level) {
self.currentFeeLevel = level || configService.getSync().wallet.settings.feeLevel || 'normal';
self.setSendMax();
};
self.setFeesOpts = function() {
var fc = profileService.focusedClient;
if (!fc) return;
$timeout(function() {
feeService.getFeeLevels(function(levels) {
self.feeLevels = levels;
$rootScope.$apply();
});
});
};
self.updateBalance = function() {
var fc = profileService.focusedClient;
$timeout(function() {
self.setOngoingProcess('updatingBalance', true);
$log.debug('Updating Balance');
fc.getBalance(function(err, balance) {
self.setOngoingProcess('updatingBalance', false);
if (err) {
self.handleError(err);
return;
}
$log.debug('Wallet Balance:', balance);
self.setBalance(balance);
});
});
};
self.updatePendingTxps = function() {
var fc = profileService.focusedClient;
$timeout(function() {
self.setOngoingProcess('updatingPendingTxps', true);
$log.debug('Updating PendingTxps');
fc.getTxProposals({}, function(err, txps) {
self.setOngoingProcess('updatingPendingTxps', false);
if (err) {
self.handleError(err);
} else {
$log.debug('Wallet PendingTxps:', txps);
self.setPendingTxps(txps);
}
$rootScope.$apply();
});
});
};
self.updateTxHistory = function(skip) {
var fc = profileService.focusedClient;
if (!fc || !fc.isComplete()) return;
if (!skip) {
self.txHistory = [];
}
self.skipHistory = skip || 0;
$log.debug('Updating Transaction History');
self.txHistoryError = false;
self.updatingTxHistory = true;
self.txHistoryPaging = false;
$timeout(function() {
fc.getTxHistory({
skip: self.skipHistory,
limit: self.limitHistory + 1
}, function(err, txs) {
self.updatingTxHistory = false;
if (err) {
$log.debug('TxHistory ERROR:', err);
// We do not should errors here, since history is usually
// fetched AFTER others requests (if skip=0)
if (skip)
self.handleError(err);
self.txHistoryError = true;
} else {
$log.debug('Wallet Transaction History:', txs);
self.skipHistory = self.skipHistory + self.limitHistory;
self.setTxHistory(txs);
}
$rootScope.$apply();
});
});
};
self.debouncedUpdateHistory = lodash.throttle(function() {
self.updateTxHistory();
}, 5000);
// This handles errors from BWS/index with are nomally
// trigger from async events (like updates)
self.handleError = function(err) {
$log.warn('Client ERROR:', err);
if (err.code === 'NOT_AUTHORIZED') {
self.notAuthorized = true;
go.walletHome();
} else if (err.code === 'NOT_FOUND') {
self.showErrorPopup(gettext('Could not access Wallet Service: Not found'));
} else {
var msg = ""
$scope.$emit('Local/ClientError', (err.error ? err.error : err));
var msg = bwsError.msg(err, gettext('Error at Wallet Service'));
self.showErrorPopup(msg);
}
};
self.openWallet = function() {
var fc = profileService.focusedClient;
$timeout(function() {
$rootScope.$apply();
self.setOngoingProcess('openingWallet', true);
self.updateError = false;
fc.openWallet(function(err, walletStatus) {
self.setOngoingProcess('openingWallet', false);
if (err) {
self.updateError = true;
self.handleError(err);
return;
}
$log.debug('Wallet Opened');
self.updateAll(lodash.isObject(walletStatus) ? {
walletStatus: walletStatus
} : null);
$rootScope.$apply();
});
});
};
self.setPendingTxps = function(txps) {
self.pendingTxProposalsCountForUs = 0;
var now = Math.floor(Date.now() / 1000);
lodash.each(txps, function(tx) {
tx = txFormatService.processTx(tx);
// no future transactions...
if (tx.createdOn > now)
tx.createdOn = now;
var action = lodash.find(tx.actions, {
copayerId: self.copayerId
});
if (!action && tx.status == 'pending') {
tx.pendingForUs = true;
}
if (action && action.type == 'accept') {
tx.statusForUs = 'accepted';
} else if (action && action.type == 'reject') {
tx.statusForUs = 'rejected';
} else {
tx.statusForUs = 'pending';
}
if (!tx.deleteLockTime)
tx.canBeRemoved = true;
if (tx.creatorId != self.copayerId) {
self.pendingTxProposalsCountForUs = self.pendingTxProposalsCountForUs + 1;
}
addonManager.formatPendingTxp(tx);
});
self.txps = txps;
};
var SAFE_CONFIRMATIONS = 6;
self.setTxHistory = function(txs) {
var config = configService.getSync().wallet.settings;
var now = Math.floor(Date.now() / 1000);
var c = 0;
self.txHistoryPaging = txs[self.limitHistory] ? true : false;
self.hasUnsafeConfirmed = false;
lodash.each(txs, function(tx) {
tx = txFormatService.processTx(tx);
// no future transactions...
if (tx.time > now)
tx.time = now;
if (tx.confirmations >= SAFE_CONFIRMATIONS) {
tx.safeConfirmed = SAFE_CONFIRMATIONS + '+';
} else {
tx.safeConfirmed = false;
self.hasUnsafeConfirmed = true;
}
if (c < self.limitHistory) {
self.txHistory.push(tx);
c++;
}
});
};
self.updateAlias = function() {
var config = configService.getSync();
config.aliasFor = config.aliasFor || {};
self.alias = config.aliasFor[self.walletId];
var fc = profileService.focusedClient;
fc.alias = self.alias;
};
self.updateColor = function() {
var config = configService.getSync();
config.colorFor = config.colorFor || {};
self.backgroundColor = config.colorFor[self.walletId] || '#4A90E2';
var fc = profileService.focusedClient;
fc.backgroundColor = self.backgroundColor;
};
self.setBalance = function(balance) {
if (!balance) return;
var config = configService.getSync().wallet.settings;
var COIN = 1e8;
// Address with Balance
self.balanceByAddress = balance.byAddress;
// SAT
if (self.spendUnconfirmed) {
self.totalBalanceSat = balance.totalAmount;
self.lockedBalanceSat = balance.lockedAmount;
self.availableBalanceSat = balance.availableAmount;
self.pendingAmount = null;
} else {
self.totalBalanceSat = balance.totalConfirmedAmount;
self.lockedBalanceSat = balance.lockedConfirmedAmount;
self.availableBalanceSat = balance.availableConfirmedAmount;
self.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount;
}
// Selected unit
self.unitToSatoshi = config.unitToSatoshi;
self.satToUnit = 1 / self.unitToSatoshi;
self.unitName = config.unitName;
//STR
self.totalBalanceStr = profileService.formatAmount(self.totalBalanceSat) + ' ' + self.unitName;
self.lockedBalanceStr = profileService.formatAmount(self.lockedBalanceSat) + ' ' + self.unitName;
self.availableBalanceStr = profileService.formatAmount(self.availableBalanceSat) + ' ' + self.unitName;
if (self.pendingAmount) {
self.pendingAmountStr = profileService.formatAmount(self.pendingAmount) + ' ' + self.unitName;
} else {
self.pendingAmountStr = null;
}
self.alternativeName = config.alternativeName;
self.alternativeIsoCode = config.alternativeIsoCode;
// Other
self.totalBytesToSendMax = balance.totalBytesToSendMax;
self.setCurrentFeeLevel();
// Check address
addressService.isUsed(self.walletId, balance.byAddress, function(err, used) {
if (used) {
$log.debug('Address used. Creating new');
$rootScope.$emit('Local/NeedNewAddress');
}
});
rateService.whenAvailable(function() {
var totalBalanceAlternative = rateService.toFiat(self.totalBalanceSat, self.alternativeIsoCode);
var lockedBalanceAlternative = rateService.toFiat(self.lockedBalanceSat, self.alternativeIsoCode);
var alternativeConversionRate = rateService.toFiat(100000000, self.alternativeIsoCode);
self.totalBalanceAlternative = $filter('noFractionNumber')(totalBalanceAlternative, 2);
self.lockedBalanceAlternative = $filter('noFractionNumber')(lockedBalanceAlternative, 2);
self.alternativeConversionRate = $filter('noFractionNumber')(alternativeConversionRate, 2);
self.alternativeBalanceAvailable = true;
self.updatingBalance = false;
self.isRateAvailable = true;
$rootScope.$apply();
});
if (!rateService.isAvailable()) {
$rootScope.$apply();
}
};
this.csvHistory = function() {
function saveFile(name, data) {
var chooser = document.querySelector(name);
chooser.addEventListener("change", function(evt) {
var fs = require('fs');
fs.writeFile(this.value, data, function(err) {
if (err) {
$log.debug(err);
}
});
}, false);
chooser.click();
}
function formatDate(date) {
var dateObj = new Date(date);
if (!dateObj) {
$log.debug('Error formating a date');
return 'DateError'
}
if (!dateObj.toJSON()) {
return '';
}
return dateObj.toJSON();
}
function formatString(str) {
if (!str) return '';
if (str.indexOf('"') !== -1) {
//replace all
str = str.replace(new RegExp('"', 'g'), '\'');
}
//escaping commas
str = '\"' + str + '\"';
return str;
}
var step = 6;
function getHistory(skip, cb) {
skip = skip || 0;
fc.getTxHistory({
skip: skip,
limit: step,
}, function(err, txs) {
if (err) return cb(err);
if (txs && txs.length > 0) {
allTxs.push(txs);
return getHistory(skip + step, cb);
} else {
return cb(null, lodash.flatten(allTxs));
}
});
};
if (isCordova) {
$log.info('Not available on mobile');
return;
}
var isNode = nodeWebkit.isDefined();
var fc = profileService.focusedClient;
if (!fc.isComplete()) return;
var self = this;
var allTxs = [];
$log.debug('Generating CSV from History');
self.setOngoingProcess('generatingCSV', true);
$timeout(function() {
getHistory(null, function(err, txs) {
self.setOngoingProcess('generatingCSV', false);
if (err) {
self.handleError(err);
} else {
$log.debug('Wallet Transaction History:', txs);
self.satToUnit = 1 / self.unitToSatoshi;
var data = txs;
var satToBtc = 1 / 100000000;
var filename = 'Copay-' + (self.alias || self.walletName) + '.csv';
var csvContent = '';
if (!isNode) csvContent = 'data:text/csv;charset=utf-8,';
csvContent += 'Date,Destination,Note,Amount,Currency,Spot Value,Total Value,Tax Type,Category\n';
var _amount, _note;
var dataString;
data.forEach(function(it, index) {
var amount = it.amount;
if (it.action == 'moved')
amount = 0;
_amount = (it.action == 'sent' ? '-' : '') + (amount * satToBtc).toFixed(8);
_note = formatString((it.message ? it.message : '') + ' TxId: ' + it.txid + ' Fee:' + (it.fees * satToBtc).toFixed(8));
if (it.action == 'moved')
_note += ' Moved:' + (it.amount * satToBtc).toFixed(8)
dataString = formatDate(it.time * 1000) + ',' + formatString(it.addressTo) + ',' + _note + ',' + _amount + ',BTC,,,,';
csvContent += dataString + "\n";
if (it.fees && (it.action == 'moved' || it.action == 'sent')) {
var _fee = (it.fees * satToBtc).toFixed(8)
csvContent += formatDate(it.time * 1000) + ',Bitcoin Network Fees,, -' + _fee + ',BTC,,,,' + "\n";
}
});
if (isNode) {
saveFile('#export_file', csvContent);
} else {
var encodedUri = encodeURI(csvContent);
var link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", filename);
link.click();
}
}
$rootScope.$apply();
});
});
};
self.showErrorPopup = function(msg, cb) {
$log.warn('Showing err popup:' + msg);
self.showAlert = {
msg: msg,
close: function(err) {
self.showAlert = null;
if (cb) return cb(err);
},
};
$timeout(function() {
$rootScope.$apply();
});
};
self.recreate = function(cb) {
var fc = profileService.focusedClient;
self.setOngoingProcess('recreating', true);
fc.recreateWallet(function(err) {
self.notAuthorized = false;
self.setOngoingProcess('recreating', false);
if (err) {
self.handleError(err);
$rootScope.$apply();
return;
}
profileService.setWalletClients();
$timeout(function() {
$rootScope.$emit('Local/WalletImported', self.walletId);
}, 100);
});
};
self.openMenu = function() {
go.swipe(true);
};
self.closeMenu = function() {
go.swipe();
};
self.retryScan = function() {
var self = this;
self.startScan(self.walletId);
}
self.startScan = function(walletId) {
$log.debug('Scanning wallet ' + walletId);
var c = profileService.walletClients[walletId];
if (!c.isComplete()) return;
if (self.walletId == walletId)
self.setOngoingProcess('scanning', true);
c.startScan({
includeCopayerBranches: true,
}, function(err) {
if (err && self.walletId == walletId) {
self.setOngoingProcess('scanning', false);
self.handleError(err);
$rootScope.$apply();
}
});
};
self.setUxLanguage = function() {
var userLang = uxLanguage.update();
self.defaultLanguageIsoCode = userLang;
self.defaultLanguageName = uxLanguage.getName(userLang);
};
self.initGlidera = function(accessToken) {
self.glideraEnabled = configService.getSync().glidera.enabled;
self.glideraTestnet = configService.getSync().glidera.testnet;
var network = self.glideraTestnet ? 'testnet' : 'livenet';
self.glideraToken = null;
self.glideraError = null;
self.glideraPermissions = null;
self.glideraEmail = null;
self.glideraPersonalInfo = null;
self.glideraTxs = null;
self.glideraStatus = null;
if (!self.glideraEnabled || self.isShared) return;
glideraService.setCredentials(network);
var getToken = function(cb) {
if (accessToken) {
cb(null, accessToken);
} else {
storageService.getGlideraToken(network, cb);
}
};
getToken(function(err, accessToken) {
if (err || !accessToken) return;
else {
self.glideraLoading = 'Connecting to Glidera...';
glideraService.getAccessTokenPermissions(accessToken, function(err, p) {
self.glideraLoading = null;
if (err) {
self.glideraError = err;
} else {
self.glideraToken = accessToken;
self.glideraPermissions = p;
self.updateGlidera({
fullUpdate: true
});
}
});
}
});
};
self.updateGlidera = function(opts) {
if (!self.glideraToken || !self.glideraPermissions) return;
var accessToken = self.glideraToken;
var permissions = self.glideraPermissions;
opts = opts || {};
glideraService.getStatus(accessToken, function(err, data) {
self.glideraStatus = data;
});
glideraService.getLimits(accessToken, function(err, limits) {
self.glideraLimits = limits;
});
if (permissions.transaction_history) {
self.glideraLoadingHistory = 'Getting Glidera transactions...';
glideraService.getTransactions(accessToken, function(err, data) {
self.glideraLoadingHistory = null;
self.glideraTxs = data;
});
}
if (permissions.view_email_address && opts.fullUpdate) {
self.glideraLoadingEmail = 'Getting Glidera Email...';
glideraService.getEmail(accessToken, function(err, data) {
self.glideraLoadingEmail = null;
self.glideraEmail = data.email;
});
}
if (permissions.personal_info && opts.fullUpdate) {
self.glideraLoadingPersonalInfo = 'Getting Glidera Personal Information...';
glideraService.getPersonalInfo(accessToken, function(err, data) {
self.glideraLoadingPersonalInfo = null;
self.glideraPersonalInfo = data;
});
}
};
// UX event handlers
$rootScope.$on('Local/ColorUpdated', function(event) {
self.updateColor();
$timeout(function() {
$rootScope.$apply();
});
});
$rootScope.$on('Local/AliasUpdated', function(event) {
self.updateAlias();
$timeout(function() {
$rootScope.$apply();
});
});
$rootScope.$on('Local/SpendUnconfirmedUpdated', function(event) {
self.setSpendUnconfirmed();
self.updateAll();
});
$rootScope.$on('Local/FeeLevelUpdated', function(event, level) {
self.setCurrentFeeLevel(level);
});
$rootScope.$on('Local/ProfileBound', function() {
storageService.getRemotePrefsStoredFlag(function(err, val) {
if (err || val) return;
self.updateRemotePreferences({
saveAll: true
}, function() {
$log.debug('Remote preferences saved')
storageService.setRemotePrefsStoredFlag(function() {});
});
});
});
$rootScope.$on('Local/NewFocusedWallet', function() {
self.setUxLanguage();
});
$rootScope.$on('Local/LanguageSettingUpdated', function() {
self.setUxLanguage();
self.updateRemotePreferences({
saveAll: true
}, function() {
$log.debug('Remote preferences saved')
});
});
$rootScope.$on('Local/GlideraUpdated', function(event, accessToken) {
self.initGlidera(accessToken);
});
$rootScope.$on('Local/GlideraTx', function(event, accessToken, permissions) {
self.updateGlidera();
});
$rootScope.$on('Local/GlideraError', function(event) {
self.debouncedUpdate();
});
$rootScope.$on('Local/UnitSettingUpdated', function(event) {
self.updateAll();
self.updateTxHistory();
self.updateRemotePreferences({
saveAll: true
}, function() {
$log.debug('Remote preferences saved')
});
});
$rootScope.$on('Local/EmailSettingUpdated', function(event, email, cb) {
self.updateRemotePreferences({
preferences: {
email: email || null
},
}, cb);
});
$rootScope.$on('Local/BWSUpdated', function(event) {
profileService.applyConfig();
storageService.setCleanAndScanAddresses(function() {});
});
$rootScope.$on('Local/WalletCompleted', function(event) {
self.setFocusedWallet();
go.walletHome();
});
self.debouncedUpdate = lodash.throttle(function() {
self.updateAll({
quiet: true
});
self.updateTxHistory();
}, 4000, {
leading: false,
trailing: true
});
$rootScope.$on('Local/Resume', function(event) {
$log.debug('### Resume event');
self.debouncedUpdate();
});
$rootScope.$on('Local/BackupDone', function(event) {
self.needsBackup = false;
$log.debug('Backup done');
storageService.setBackupFlag(self.walletId, function(err) {
if (err) root.showErrorPopup(err);
$log.debug('Backup done stored');
});
});
$rootScope.$on('Local/DeviceError', function(event, err) {
root.showErrorPopup(err);
});
$rootScope.$on('Local/WalletImported', function(event, walletId) {
self.needsBackup = false;
storageService.setBackupFlag(walletId, function() {
$log.debug('Backup done stored');
addressService.expireAddress(walletId, function(err) {
$timeout(function() {
self.startScan(walletId);
}, 500);
});
});
});
$rootScope.$on('NewIncomingTx', function() {
self.updateAll({
walletStatus: null,
untilItChanges: true,
triggerTxUpdate: true,
});
});
$rootScope.$on('NewBlock', function() {
if (self.pendingAmount) {
self.updateAll({
walletStatus: null,
untilItChanges: null,
triggerTxUpdate: true,
});
} else if (self.hasUnsafeConfirmed) {
$log.debug('Wallet has transactions with few confirmations. Updating.')
if (self.network == 'testnet') {
self.debouncedUpdateHistory();
} else {
self.updateTxHistory();
}
}
});
$rootScope.$on('NewOutgoingTx', function() {
self.updateAll({
walletStatus: null,
untilItChanges: true,
triggerTxUpdate: true,
});
});
lodash.each(['NewTxProposal', 'TxProposalFinallyRejected', 'TxProposalRemoved', 'NewOutgoingTxByThirdParty',
'Local/NewTxProposal', 'Local/TxProposalAction', 'ScanFinished', 'Local/GlideraTx'
], function(eventName) {
$rootScope.$on(eventName, function(event, untilItChanges) {
self.updateAll({
walletStatus: null,
untilItChanges: untilItChanges,
triggerTxUpdate: true,
});
});
});
lodash.each(['TxProposalRejectedBy', 'TxProposalAcceptedBy'], function(eventName) {
$rootScope.$on(eventName, function() {
var f = function() {
if (self.updatingStatus) {
return $timeout(f, 200);
};
self.updatePendingTxps();
};
f();
});
});
$rootScope.$on('Local/NoWallets', function(event) {
$timeout(function() {
self.hasProfile = true;
self.noFocusedWallet = true;
self.isComplete = null;
self.walletName = null;
go.path('import');
});
});
$rootScope.$on('Local/NewFocusedWallet', function() {
self.setFocusedWallet();
self.updateTxHistory();
go.walletHome();
storageService.getCleanAndScanAddresses(function(err, val) {
if (val) {
$log.debug('Clear last address cache and Scan');
lodash.each(lodash.keys(profileService.walletClients), function(walletId) {
addressService.expireAddress(walletId, function(err) {
self.startScan(walletId);
});
});
storageService.removeCleanAndScanAddresses(function() {});
}
});
});
$rootScope.$on('Local/SetTab', function(event, tab, reset) {
self.setTab(tab, reset);
});
$rootScope.$on('Local/ShowAlert', function(event, msg, cb) {
self.showErrorPopup(msg, cb);
});
$rootScope.$on('Local/NeedsPassword', function(event, isSetup, cb) {
self.askPassword = {
isSetup: isSetup,
callback: function(err, pass) {
self.askPassword = null;
return cb(err, pass);
},
};
});
lodash.each(['NewCopayer', 'CopayerUpdated'], function(eventName) {
$rootScope.$on(eventName, function() {
// Re try to open wallet (will triggers)
self.setFocusedWallet();
});
});
});
'use strict';
angular.module('copayApp.controllers').controller('joinController',
function($scope, $rootScope, $timeout, go, isMobile, notification, profileService, isCordova, isChromeApp, $modal, gettext, lodash, ledger) {
var self = this;
this.isChromeApp = function() {
return isChromeApp;
};
this.onQrCodeScanned = function(data) {
$scope.secret = data;
$scope.joinForm.secret.$setViewValue(data);
$scope.joinForm.secret.$render();
};
this.join = function(form) {
if (form && form.$invalid) {
self.error = gettext('Please enter the required fields');
return;
}
self.loading = true;
var opts = {
secret: form.secret.$modelValue,
myName: form.myName.$modelValue,
}
var setSeed = form.setSeed.$modelValue;
if (setSeed) {
var words = form.privateKey.$modelValue;
if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) {
opts.extendedPrivateKey = words;
} else {
opts.mnemonic = words;
}
opts.passphrase = form.passphrase.$modelValue;
} else {
opts.passphrase = form.createPassphrase.$modelValue;
}
if (setSeed && !opts.mnemonic && !opts.extendedPrivateKey) {
this.error = gettext('Please enter the wallet seed');
return;
}
if (form.hwLedger.$modelValue) {
self.ledger = true;
// TODO account
ledger.getInfoForNewWallet(0, function(err, lopts) {
self.ledger = false;
if (err) {
self.error = err;
$scope.$apply();
return;
}
opts = lodash.assign(lopts, opts);
self._join(opts);
});
} else {
self._join(opts);
}
};
this._join = function(opts) {
$timeout(function() {
profileService.joinWallet(opts, function(err) {
if (err) {
self.loading = false;
self.error = err;
$rootScope.$apply();
return
}
$timeout(function() {
var fc = profileService.focusedClient;
if ( fc.isComplete() && (opts.mnemonic || opts.externalSource || opts.extendedPrivateKey)) {
$rootScope.$emit('Local/WalletImported', fc.credentials.walletId);
} else {
go.walletHome();
}
}, 2000);
});
}, 100);
};
});
angular.module('copayApp.controllers').controller('paperWalletController',
function($scope, $http, $timeout, $rootScope, profileService, go, addressService, isCordova, gettext) {
self = this;
var fc = profileService.focusedClient;
var rawTx;
if (isCordova) self.message = "Decrypting a paper wallet could take around 5 minutes on this device. please be patient and keep the app open."
self.onQrCodeScanned = function(data) {
$scope.privateKey = data;
}
self.createTx = function(privateKey, passphrase) {
self.error = null;
self.scanning = true;
$timeout(function() {
self.getRawTx(privateKey, passphrase, function(err, rawtx, utxos) {
self.scanning = false;
if (err)
self.error = err.toString();
else {
self.balance = (utxos / 1e8).toFixed(8);
rawTx = rawtx;
}
$timeout(function() {
$scope.$apply();
}, 1);
});
}, 100);
};
self.getRawTx = function(privateKey, passphrase, cb) {
if (privateKey.charAt(0) == 6) {
fc.decryptBIP38PrivateKey(privateKey, passphrase, null, function(err, privateKey) {
if (err) return cb(err);
fc.getBalanceFromPrivateKey(privateKey, function(err, utxos) {
if (err) return cb(err);
addressService.getAddress(fc.credentials.walletId, true, function(err, destinationAddress) {
if (err) return cb(err);
fc.buildTxFromPrivateKey(privateKey, destinationAddress, null, function(err, tx) {
if (err) return cb(err);
return cb(null, tx.serialize(), utxos);
});
});
});
});
} else {
fc.getBalanceFromPrivateKey(privateKey, function(err, utxos) {
if (err) return cb(err)
addressService.getAddress(fc.credentials.walletId, true, function(err, destinationAddress) {
if (err) return cb(err);
fc.buildTxFromPrivateKey(privateKey, destinationAddress, null, function(err, tx) {
if (err) return cb(err);
return cb(null, tx.serialize(), utxos);
});
});
});
}
};
self.transaction = function() {
self.doTransaction(rawTx).then(function(err, response) {
self.goHome();
},
function(err) {
self.error = err.toString();
});
};
self.goHome = function() {
go.walletHome();
};
self.doTransaction = function(rawTx) {
return $http.post('https://insight.bitpay.com/api/tx/send', {
rawtx: rawTx
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('passwordController',
function($rootScope, $scope, $timeout, profileService, notification, go, gettext) {
var self = this;
var pass1;
self.isVerification = false;
document.getElementById("passwordInput").focus();
self.close = function(cb) {
return cb('No password given');
};
self.set = function(isSetup, cb) {
self.error = false;
if (isSetup && !self.isVerification) {
document.getElementById("passwordInput").focus();
self.isVerification = true;
pass1 = self.password;
self.password = null;
$timeout(function() {
$rootScope.$apply();
})
return;
}
if (isSetup) {
if (pass1 != self.password) {
self.error = gettext('Passwords do not match');
self.isVerification = false;
self.password = null;
pass1 = null;
return;
}
}
return cb(null, self.password);
};
});
'use strict';
angular.module('copayApp.controllers').controller('paymentUriController',
function($rootScope, $stateParams, $location, $timeout, profileService, configService, lodash, bitcore, go) {
function strip(number) {
return (parseFloat(number.toPrecision(12)));
};
// Build bitcoinURI with querystring
this.checkBitcoinUri = function() {
var query = [];
angular.forEach($location.search(), function(value, key) {
query.push(key + "=" + value);
});
var queryString = query ? query.join("&") : null;
this.bitcoinURI = $stateParams.data + (queryString ? '?' + queryString : '');
var URI = bitcore.URI;
var isUriValid = URI.isValid(this.bitcoinURI);
if (!URI.isValid(this.bitcoinURI)) {
this.error = true;
return;
}
var uri = new URI(this.bitcoinURI);
if (uri && uri.address) {
var config = configService.getSync().wallet.settings;
var unitToSatoshi = config.unitToSatoshi;
var satToUnit = 1 / unitToSatoshi;
var unitName = config.unitName;
if (uri.amount) {
uri.amount = strip(uri.amount * satToUnit) + ' ' + unitName;
}
uri.network = uri.address.network.name;
this.uri = uri;
}
};
this.getWallets = function(network) {
return profileService.getWallets(network);
};
this.selectWallet = function(wid) {
var self = this;
if (wid != profileService.focusedClient.credentials.walletId) {
profileService.setAndStoreFocus(wid, function() {});
}
go.send();
$timeout(function() {
$rootScope.$emit('paymentUri', self.bitcoinURI);
}, 100);
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesController',
function($scope, $rootScope, $filter, $timeout, $modal, $log, lodash, configService, profileService, uxLanguage) {
var config = configService.getSync();
this.unitName = config.wallet.settings.unitName;
this.bwsurl = config.bws.url;
this.currentLanguageName = uxLanguage.getCurrentLanguageName();
this.selectedAlternative = {
name: config.wallet.settings.alternativeName,
isoCode: config.wallet.settings.alternativeIsoCode
};
$scope.spendUnconfirmed = config.wallet.spendUnconfirmed;
$scope.glideraEnabled = config.glidera.enabled;
$scope.glideraTestnet = config.glidera.testnet;
var fc = profileService.focusedClient;
if (fc) {
$scope.encrypt = fc.hasPrivKeyEncrypted();
this.externalSource = fc.getPrivKeyExternalSourceName() == 'ledger' ? "Ledger" : null;
// TODO externalAccount
//this.externalIndex = fc.getExternalIndex();
}
var unwatchSpendUnconfirmed = $scope.$watch('spendUnconfirmed', function(newVal, oldVal) {
if (newVal == oldVal) return;
var opts = {
wallet: {
spendUnconfirmed: newVal
}
};
configService.set(opts, function(err) {
$rootScope.$emit('Local/SpendUnconfirmedUpdated');
if (err) $log.debug(err);
});
});
var unwatch = $scope.$watch('encrypt', function(val) {
var fc = profileService.focusedClient;
if (!fc) return;
if (val && !fc.hasPrivKeyEncrypted()) {
$rootScope.$emit('Local/NeedsPassword', true, function(err, password) {
if (err || !password) {
$scope.encrypt = false;
return;
}
profileService.setPrivateKeyEncryptionFC(password, function() {
$scope.encrypt = true;
});
});
} else {
if (!val && fc.hasPrivKeyEncrypted()) {
profileService.unlockFC(function(err){
if (err) {
$scope.encrypt = true;
return;
}
profileService.disablePrivateKeyEncryptionFC(function(err) {
if (err) {
$scope.encrypt = true;
$log.error(err);
return;
}
$scope.encrypt = false;
});
});
}
}
});
var unwatchGlideraEnabled = $scope.$watch('glideraEnabled', function(newVal, oldVal) {
if (newVal == oldVal) return;
var opts = {
glidera: {
enabled: newVal
}
};
configService.set(opts, function(err) {
$rootScope.$emit('Local/GlideraUpdated');
if (err) $log.debug(err);
});
});
var unwatchGlideraTestnet = $scope.$watch('glideraTestnet', function(newVal, oldVal) {
if (newVal == oldVal) return;
var opts = {
glidera: {
testnet: newVal
}
};
configService.set(opts, function(err) {
$rootScope.$emit('Local/GlideraUpdated');
if (err) $log.debug(err);
});
});
$scope.$on('$destroy', function() {
unwatch();
unwatchSpendUnconfirmed();
unwatchGlideraEnabled();
unwatchGlideraTestnet();
});
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesAbout',
function() {});
'use strict';
angular.module('copayApp.controllers').controller('preferencesAdvancedController',
function($scope) {
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesAliasController',
function($scope, $timeout, configService, profileService, go) {
var config = configService.getSync();
var fc = profileService.focusedClient;
var walletId = fc.credentials.walletId;
var config = configService.getSync();
config.aliasFor = config.aliasFor || {};
this.alias = config.aliasFor[walletId] || fc.credentials.walletName;
this.save = function() {
var self = this;
var opts = {
aliasFor: {}
};
opts.aliasFor[walletId] = self.alias;
configService.set(opts, function(err) {
if (err) {
$scope.$emit('Local/DeviceError', err);
return;
}
$scope.$emit('Local/AliasUpdated');
$timeout(function(){
go.path('preferences');
}, 50);
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesAltCurrencyController',
function($scope, $rootScope, configService, go, rateService, lodash) {
this.hideAdv = true;
this.hidePriv = true;
this.hideSecret = true;
this.error = null;
this.success = null;
var config = configService.getSync();
this.selectedAlternative = {
name: config.wallet.settings.alternativeName,
isoCode: config.wallet.settings.alternativeIsoCode
};
this.alternativeOpts = [this.selectedAlternative]; //default value
var self = this;
rateService.whenAvailable(function() {
self.alternativeOpts = rateService.listAlternatives();
lodash.remove(self.alternativeOpts, function(n) {
return n.isoCode == 'BTC';
});
for (var ii in self.alternativeOpts) {
if (config.wallet.settings.alternativeIsoCode === self.alternativeOpts[ii].isoCode) {
self.selectedAlternative = self.alternativeOpts[ii];
}
}
$scope.$digest();
});
this.save = function(newAltCurrency) {
var opts = {
wallet: {
settings: {
alternativeName: newAltCurrency.name,
alternativeIsoCode: newAltCurrency.isoCode,
}
}
};
this.selectedAlternative = {
name: newAltCurrency.name,
isoCode: newAltCurrency.isoCode,
};
configService.set(opts, function(err) {
if (err) console.log(err);
$scope.$emit('Local/UnitSettingUpdated');
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesBwsUrlController',
function($scope,$log, configService, isMobile, isCordova, go, applicationService ) {
this.isSafari = isMobile.Safari();
this.isCordova = isCordova;
this.error = null;
this.success = null;
var config = configService.getSync();
this.bwsurl = config.bws.url;
this.save = function() {
var bws;
switch (this.bwsurl) {
case 'prod':
case 'production':
bws = 'https://bws.bitpay.com/bws/api'
break;
case 'sta':
case 'staging':
bws = 'https://bws-staging.b-pay.net/bws/api'
break;
case 'loc':
case 'local':
bws = 'http://localhost:3232/bws/api'
break;
};
if (bws) {
$log.info('Using BWS URL Alias to ' + bws);
this.bwsurl = bws;
}
var opts = {
bws: {
url: this.bwsurl,
}
};
configService.set(opts, function(err) {
if (err) console.log(err);
$scope.$emit('Local/BWSUpdated');
applicationService.restart();
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesColorController',
function($scope, configService, profileService, go) {
var config = configService.getSync();
this.colorOpts = [
'#DD4B39',
'#F38F12',
'#FAA77F',
'#FADA58',
'#9EDD72',
'#77DADA',
'#4A90E2',
'#484ED3',
'#9B59B6',
'#E856EF',
'#FF599E',
'#7A8C9E',
];
var fc = profileService.focusedClient;
var walletId = fc.credentials.walletId;
var config = configService.getSync();
config.colorFor = config.colorFor || {};
this.color = config.colorFor[walletId] || '#4A90E2';
this.save = function(color) {
var self = this;
var opts = {
colorFor: {}
};
opts.colorFor[walletId] = color;
configService.set(opts, function(err) {
if (err) {
$scope.$emit('Local/DeviceError', err);
return;
}
self.color = color;
$scope.$emit('Local/ColorUpdated');
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesDeleteWalletController',
function($scope, $rootScope, $filter, $timeout, $modal, $log, notification, profileService, isCordova, go, gettext, gettextCatalog, animationService) {
this.isCordova = isCordova;
this.error = null;
var delete_msg = gettextCatalog.getString('Are you sure you want to delete this wallet?');
var accept_msg = gettextCatalog.getString('Accept');
var cancel_msg = gettextCatalog.getString('Cancel');
var confirm_msg = gettextCatalog.getString('Confirm');
var _modalDeleteWallet = function() {
var ModalInstanceCtrl = function($scope, $modalInstance, gettext) {
$scope.title = delete_msg;
$scope.loading = false;
$scope.ok = function() {
$scope.loading = true;
$modalInstance.close(accept_msg);
};
$scope.cancel = function() {
$modalInstance.dismiss(cancel_msg);
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/confirmation.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
modalInstance.result.then(function(ok) {
if (ok) {
_deleteWallet();
}
});
};
var _deleteWallet = function() {
var fc = profileService.focusedClient;
var name = fc.credentials.walletName;
var walletName = (fc.alias||'') + ' [' + name + ']';
var self = this;
profileService.deleteWalletFC({}, function(err) {
if (err) {
self.error = err.message || err;
} else {
notification.success(gettextCatalog.getString('Success'), gettextCatalog.getString('The wallet "{{walletName}}" was deleted', {walletName: walletName}));
}
});
};
this.deleteWallet = function() {
if (isCordova) {
navigator.notification.confirm(
delete_msg,
function(buttonIndex) {
if (buttonIndex == 1) {
_deleteWallet();
}
},
confirm_msg, [accept_msg, cancel_msg]
);
} else {
_modalDeleteWallet();
}
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesEmailController',
function($scope, go, profileService, gettext, $log) {
this.save = function(form) {
var self = this;
this.error = null;
var fc = profileService.focusedClient;
this.saving = true;
$scope.$emit('Local/EmailSettingUpdated', self.email, function() {
self.saving = false;
go.path('preferences');
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesFeeController',
function($rootScope, configService) {
this.save = function(newFee) {
var opts = {
wallet: {
settings: {
feeLevel: newFee
}
}
};
$rootScope.$emit('Local/FeeLevelUpdated', newFee);
configService.set(opts, function(err) {
if (err) $log.debug(err);
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesGlideraController',
function($scope, $modal, $timeout, profileService, applicationService, glideraService, storageService, isChromeApp, animationService) {
this.getEmail = function(token) {
var self = this;
glideraService.getEmail(token, function(error, data) {
self.email = data;
});
};
this.getPersonalInfo = function(token) {
var self = this;
glideraService.getPersonalInfo(token, function(error, info) {
self.personalInfo = info;
});
};
this.getStatus = function(token) {
var self = this;
glideraService.getStatus(token, function(error, data) {
self.status = data;
});
};
this.getLimits = function(token) {
var self = this;
glideraService.getLimits(token, function(error, limits) {
self.limits = limits;
});
};
this.revokeToken = function(testnet) {
var network = testnet ? 'testnet' : 'livenet';
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.ok = function() {
$modalInstance.close(true);
};
$scope.cancel = function() {
$modalInstance.dismiss();
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/glidera-confirmation.html',
windowClass: animationService.modalAnimated.slideRight,
controller: ModalInstanceCtrl
});
modalInstance.result.then(function(ok) {
if (ok) {
storageService.removeGlideraToken(network, function() {
$timeout(function() {
applicationService.restart();
}, 100);
});
}
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutRight);
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesInformation',
function($scope, $log, $timeout, lodash, profileService) {
var fc = profileService.focusedClient;
var c = fc.credentials;
var base = 'xpub';
var basePath = profileService.getUtils().PATHS.BASE_ADDRESS_DERIVATION[c.derivationStrategy][c.network];
$scope.walletName = c.walletName;
$scope.walletId = c.walletId;
$scope.network = c.network;
$scope.addressType = c.addressType || 'P2SH';
$scope.derivationStrategy = c.derivationStrategy || 'BIP45';
$scope.basePath = basePath;
$scope.M = c.m;
$scope.N = c.n;
$scope.pubKeys = lodash.pluck(c.publicKeyRing, 'xPubKey');
$scope.addrs = null;
fc.getMainAddresses({
doNotVerify: true
}, function(err, addrs) {
if (err) {
$log.warn(err);
return;
};
var last10 = [],
i = 0,
e = addrs.pop();
while (i++ < 10 && e) {
e.path = base + e.path.substring(1);
last10.push(e);
e = addrs.pop();
}
$scope.addrs = last10;
$timeout(function() {
$scope.$apply();
});
});
this.sendAddrs = function() {
function formatDate(ts) {
var dateObj = new Date(ts * 1000);
if (!dateObj) {
$log.debug('Error formating a date');
return 'DateError';
}
if (!dateObj.toJSON()) {
return '';
}
return dateObj.toJSON();
};
fc.getMainAddresses({
doNotVerify: true
}, function(err, addrs) {
if (err) {
$log.warn(err);
return;
};
var body = 'Copay Wallet' + fc.walletName + ' Addresses\n Only Main Addresses are shown.\n\n';
body += '\n\n';
body += addrs.map(function(v) {
return addrs.address, base + addrs.path.substring(1), formatDate(addrs.createdOn);
}).join('\n');
var properties = {
subject: 'Copay Addresses',
body: body,
isHtml: false
};
window.plugin.email.open(properties);
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesLanguageController',
function($scope, $log, $timeout, configService, go, uxLanguage) {
this.availableLanguages = uxLanguage.getLanguages();
this.save = function(newLang) {
var opts = {
wallet: {
settings: {
defaultLanguage: newLang
}
}
};
configService.set(opts, function(err) {
if (err) $log.warn(err);
$scope.$emit('Local/LanguageSettingUpdated');
$timeout(function() {
go.preferences();
}, 100);
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesLogs',
function(historicLog, isCordova) {
this.logs = historicLog.get();
this.isCordova = isCordova;
this.sendLogs = function() {
var body = 'Copay Session Logs\n Be careful, this could contain sensitive private data\n\n';
body += '\n\n';
body += this.logs.map(function(v) {
return v.msg;
}).join('\n');
var properties = {
subject: 'Copay Logs',
body: body,
isHtml: false
};
window.plugin.email.open(properties);
};
});
'use strict';
angular.module('copayApp.controllers').controller('preferencesUnitController',
function($rootScope, $scope, $log, configService, go) {
var config = configService.getSync();
this.unitName = config.wallet.settings.unitName;
this.unitOpts = [
// TODO : add Satoshis to bitcore-wallet-client formatAmount()
// {
// name: 'Satoshis (100,000,000 satoshis = 1BTC)',
// shortName: 'SAT',
// value: 1,
// decimals: 0,
// code: 'sat',
// },
{
name: 'bits (1,000,000 bits = 1BTC)',
shortName: 'bits',
value: 100,
decimals: 2,
code: 'bit',
}
// TODO : add mBTC to bitcore-wallet-client formatAmount()
// ,{
// name: 'mBTC (1,000 mBTC = 1BTC)',
// shortName: 'mBTC',
// value: 100000,
// decimals: 5,
// code: 'mbtc',
// }
, {
name: 'BTC',
shortName: 'BTC',
value: 100000000,
decimals: 8,
code: 'btc',
}
];
this.save = function(newUnit) {
var opts = {
wallet: {
settings: {
unitName: newUnit.shortName,
unitToSatoshi: newUnit.value,
unitDecimals: newUnit.decimals,
unitCode: newUnit.code,
}
}
};
this.unitName = newUnit.shortName;
configService.set(opts, function(err) {
if (err) $log.warn(err);
$scope.$emit('Local/UnitSettingUpdated');
go.preferences();
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('sellGlideraController',
function($scope, $timeout, $log, $modal, configService, profileService, addressService, feeService, glideraService, bwsError, lodash, isChromeApp, animationService) {
var self = this;
var config = configService.getSync();
this.data = {};
this.show2faCodeInput = null;
this.success = null;
this.error = null;
this.loading = null;
this.currentSpendUnconfirmed = config.wallet.spendUnconfirmed;
this.currentFeeLevel = config.wallet.settings.feeLevel || 'normal';
var fc;
this.otherWallets = function(testnet) {
var network = testnet ? 'testnet' : 'livenet';
return lodash.filter(profileService.getWallets(network), function(w) {
return w.network == network && w.m == 1;
});
};
$scope.openWalletsModal = function(wallets) {
self.error = null;
self.selectedWalletId = null;
self.selectedWalletName = null;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.wallets = wallets;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
$scope.selectWallet = function(walletId, walletName) {
if (!profileService.getClient(walletId).isComplete()) {
self.error = bwsError.msg({'code': 'WALLET_NOT_COMPLETE'}, 'Could not choose the wallet');
$modalInstance.dismiss('cancel');
return;
}
$modalInstance.close({
'walletId': walletId,
'walletName': walletName,
});
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/wallets.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
modalInstance.result.finally(function() {
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
modalInstance.result.then(function(obj) {
$timeout(function() {
self.selectedWalletId = obj.walletId;
self.selectedWalletName = obj.walletName;
fc = profileService.getClient(obj.walletId);
$scope.$apply();
}, 100);
});
};
this.getSellPrice = function(token, price) {
var self = this;
this.error = null;
if (!price || (price && !price.qty && !price.fiat)) {
this.sellPrice = null;
return;
}
this.gettingSellPrice = true;
glideraService.sellPrice(token, price, function(err, sellPrice) {
self.gettingSellPrice = false;
if (err) {
self.error = 'Could not get exchange information. Please, try again.';
}
else {
self.error = null;
self.sellPrice = sellPrice;
}
});
};
this.get2faCode = function(token) {
var self = this;
this.loading = 'Sending 2FA code...';
$timeout(function() {
glideraService.get2faCode(token, function(err, sent) {
self.loading = null;
if (err) {
self.error = 'Could not send confirmation code to your phone';
}
else {
self.show2faCodeInput = sent;
}
});
}, 100);
};
this.createTx = function(token, permissions, twoFaCode) {
var self = this;
self.error = null;
this.loading = 'Selling Bitcoin...';
$timeout(function() {
addressService.getAddress(fc.credentials.walletId, null, function(err, refundAddress) {
if (!refundAddress) {
self.loading = null;
self.error = bwsError.msg(err, 'Could not create address');
return;
}
glideraService.getSellAddress(token, function(error, sellAddress) {
if (!sellAddress) {
self.loading = null;
self.error = 'Could not get the destination bitcoin address';
return;
}
var amount = parseInt((self.sellPrice.qty * 100000000).toFixed(0));
feeService.getCurrentFeeValue(self.currentFeeLevel, function(err, feePerKb) {
if (err) $log.debug(err);
fc.sendTxProposal({
toAddress: sellAddress,
amount: amount,
message: 'Glidera transaction',
customData: {'glideraToken': token},
payProUrl: null,
feePerKb: feePerKb,
excludeUnconfirmedUtxos: self.currentSpendUnconfirmed ? false : true
}, function(err, txp) {
if (err) {
profileService.lockFC();
$log.error(err);
$timeout(function() {
self.loading = null;
self.error = bwsError.msg(err, 'Error');
}, 1);
return;
}
if (!fc.canSign()) {
self.loading = null;
$log.info('No signing proposal: No private key');
return;
}
_signTx(txp, function(err, txp, rawTx) {
profileService.lockFC();
if (err) {
self.loading = null;
self.error = err;
$scope.$apply();
}
else {
var data = {
refundAddress: refundAddress,
signedTransaction: rawTx,
priceUuid: self.sellPrice.priceUuid,
useCurrentPrice: self.sellPrice.priceUuid ? false : true,
ip: null
};
glideraService.sell(token, twoFaCode, data, function(err, data) {
self.loading = null;
if (err) {
self.error = err;
fc.removeTxProposal(txp, function(err, txpb) {
$timeout(function() {
$scope.$emit('Local/GlideraError');
}, 100);
});
}
else {
self.success = data;
$scope.$emit('Local/GlideraTx');
}
});
}
});
});
});
});
});
}, 100);
};
var _signTx = function(txp, cb) {
var self = this;
fc.signTxProposal(txp, function(err, signedTx) {
profileService.lockFC();
if (err) {
err = bwsError.msg(err, 'Could not accept payment');
return cb(err);
}
else {
if (signedTx.status == 'accepted') {
return cb(null, txp, signedTx.raw);
} else {
return cb('The transaction could not be signed');
}
}
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('sidebarController',
function($rootScope, $timeout, lodash, profileService, configService, go, isMobile, isCordova) {
var self = this;
self.isWindowsPhoneApp = isMobile.Windows() && isCordova;
self.walletSelection = false;
// wallet list change
$rootScope.$on('Local/WalletListUpdated', function(event) {
self.walletSelection = false;
self.setWallets();
});
$rootScope.$on('Local/ColorUpdated', function(event) {
self.setWallets();
});
$rootScope.$on('Local/AliasUpdated', function(event) {
self.setWallets();
});
self.signout = function() {
profileService.signout();
};
self.switchWallet = function(selectedWalletId, currentWalletId) {
if (selectedWalletId == currentWalletId) return;
self.walletSelection = false;
profileService.setAndStoreFocus(selectedWalletId, function() {
});
};
self.toggleWalletSelection = function() {
self.walletSelection = !self.walletSelection;
if (!self.walletSelection) return;
self.setWallets();
};
self.setWallets = function() {
if (!profileService.profile) return;
var config = configService.getSync();
config.colorFor = config.colorFor || {};
config.aliasFor = config.aliasFor || {};
var ret = lodash.map(profileService.profile.credentials, function(c) {
return {
m: c.m,
n: c.n,
name: config.aliasFor[c.walletId] || c.walletName,
id: c.walletId,
color: config.colorFor[c.walletId] || '#4A90E2',
};
});
self.wallets = lodash.sortBy(ret, 'name');
};
self.setWallets();
});
'use strict';
angular.module('copayApp.controllers').controller('splashController',
function($scope, $timeout, $log, profileService, storageService, go) {
$scope.create = function(noWallet) {
$scope.creatingProfile = true;
$timeout(function() {
profileService.create({
noWallet: noWallet
}, function(err) {
if (err) {
$scope.creatingProfile = false;
$log.warn(err);
$scope.error = err;
$scope.$apply();
$timeout(function() {
$scope.create(noWallet);
}, 3000);
}
});
}, 100);
};
$scope.init = function() {
storageService.getCopayDisclaimerFlag(function(err, val) {
if (!val) go.path('disclaimer');
if (profileService.profile) {
go.walletHome();
}
});
};
});
'use strict';
angular.module('copayApp.controllers').controller('topbarController', function($scope, $rootScope, go) {
this.onQrCodeScanned = function(data) {
$rootScope.$emit('dataScanned', data);
};
this.openSendScreen = function() {
go.send();
};
this.goHome = function() {
go.walletHome();
};
});
'use strict';
angular.module('copayApp.controllers').controller('versionController', function() {
this.version = window.version;
this.commitHash = window.commitHash;
});
'use strict';
angular.module('copayApp.controllers').controller('walletHomeController', function($scope, $rootScope, $timeout, $filter, $modal, $log, notification, txStatus, isCordova, profileService, lodash, configService, rateService, storageService, bitcore, isChromeApp, gettext, gettextCatalog, nodeWebkit, addressService, ledger, feeService, bwsError, confirmDialog, txFormatService, animationService) {
var self = this;
$rootScope.hideMenuBar = false;
$rootScope.wpInputFocused = false;
$scope.currentSpendUnconfirmed = configService.getSync().wallet.spendUnconfirmed;
// INIT
var config = configService.getSync().wallet.settings;
this.unitToSatoshi = config.unitToSatoshi;
this.satToUnit = 1 / this.unitToSatoshi;
this.unitName = config.unitName;
this.alternativeIsoCode = config.alternativeIsoCode;
this.alternativeName = config.alternativeName;
this.alternativeAmount = 0;
this.unitDecimals = config.unitDecimals;
this.isCordova = isCordova;
this.addresses = [];
this.isMobile = isMobile.any();
this.isWindowsPhoneApp = isMobile.Windows() && isCordova;
this.blockUx = false;
this.isRateAvailable = false;
this.showScanner = false;
this.isMobile = isMobile.any();
this.addr = {};
var disableScannerListener = $rootScope.$on('dataScanned', function(event, data) {
self.setForm(data);
$rootScope.$emit('Local/SetTab', 'send');
var form = $scope.sendForm;
if (form.address.$invalid && !self.blockUx) {
self.resetForm();
self.error = gettext('Could not recognize a valid Bitcoin QR Code');
}
});
var disablePaymentUriListener = $rootScope.$on('paymentUri', function(event, uri) {
$timeout(function() {
$rootScope.$emit('Local/SetTab', 'send');
self.setForm(uri);
}, 100);
});
var disableAddrListener = $rootScope.$on('Local/NeedNewAddress', function() {
self.setAddress(true);
});
var disableFocusListener = $rootScope.$on('Local/NewFocusedWallet', function() {
self.addr = {};
self.resetForm();
});
var disableResumeListener = $rootScope.$on('Local/Resume', function() {
// This is needed then the apps go to sleep
self.bindTouchDown();
});
var disableTabListener = $rootScope.$on('Local/TabChanged', function(e, tab) {
// This will slow down switch, do not add things here!
switch (tab) {
case 'receive':
// just to be sure we have an address
self.setAddress();
break;
case 'send':
self.resetError();
};
});
var disableOngoingProcessListener = $rootScope.$on('Addon/OngoingProcess', function(e, name) {
self.setOngoingProcess(name);
});
$scope.$on('$destroy', function() {
disableAddrListener();
disableScannerListener();
disablePaymentUriListener();
disableTabListener();
disableFocusListener();
disableResumeListener();
disableOngoingProcessListener();
$rootScope.hideMenuBar = false;
});
rateService.whenAvailable(function() {
self.isRateAvailable = true;
$rootScope.$digest();
});
var accept_msg = gettextCatalog.getString('Accept');
var cancel_msg = gettextCatalog.getString('Cancel');
var confirm_msg = gettextCatalog.getString('Confirm');
$scope.openCopayersModal = function(copayers, copayerId) {
$rootScope.modalOpened = true;
var fc = profileService.focusedClient;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.copayers = copayers;
$scope.copayerId = copayerId;
$scope.color = fc.backgroundColor;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/copayers.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
};
$scope.openWalletsModal = function(wallets) {
$rootScope.modalOpened = true;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.wallets = wallets;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
$scope.selectWallet = function(walletId, walletName) {
$scope.gettingAddress = true;
$scope.selectedWalletName = walletName;
$timeout(function() {
$scope.$apply();
});
addressService.getAddress(walletId, false, function(err, addr) {
$scope.gettingAddress = false;
if (err) {
self.error = err;
$modalInstance.dismiss('cancel');
return;
}
$modalInstance.close(addr);
});
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/wallets.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
modalInstance.result.then(function(addr) {
if (addr) {
self.setForm(addr);
}
});
};
var GLIDERA_LOCK_TIME = 6 * 60 * 60;
// isGlidera flag is a security mesure so glidera status is not
// only determined by the tx.message
this.openTxpModal = function(tx, copayers, isGlidera) {
$rootScope.modalOpened = true;
var fc = profileService.focusedClient;
var refreshUntilItChanges = false;
var currentSpendUnconfirmed = $scope.currentSpendUnconfirmed;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.error = null;
$scope.copayers = copayers
$scope.copayerId = fc.credentials.copayerId;
$scope.canSign = fc.canSign() || fc.isPrivKeyExternal();
$scope.loading = null;
$scope.color = fc.backgroundColor;
// ToDo: use tx.customData instead of tx.message
if (tx.message === 'Glidera transaction' && isGlidera) {
tx.isGlidera = true;
if (tx.canBeRemoved) {
tx.canBeRemoved = (Date.now() / 1000 - (tx.ts || tx.createdOn)) > GLIDERA_LOCK_TIME;
}
}
$scope.tx = tx;
refreshUntilItChanges = false;
$scope.currentSpendUnconfirmed = currentSpendUnconfirmed;
$scope.getShortNetworkName = function() {
return fc.credentials.networkName.substring(0, 4);
};
lodash.each(['TxProposalRejectedBy', 'TxProposalAcceptedBy', 'transactionProposalRemoved', 'TxProposalRemoved', 'NewOutgoingTx', 'UpdateTx'], function(eventName) {
$rootScope.$on(eventName, function() {
fc.getTx($scope.tx.id, function(err, tx) {
if (err) {
if (err.code && err.code == 'TX_NOT_FOUND' &&
(eventName == 'transactionProposalRemoved' || eventName == 'TxProposalRemoved')) {
$scope.tx.removed = true;
$scope.tx.canBeRemoved = false;
$scope.tx.pendingForUs = false;
$scope.$apply();
return;
}
return;
}
var action = lodash.find(tx.actions, {
copayerId: fc.credentials.copayerId
});
$scope.tx = txFormatService.processTx(tx);
if (!action && tx.status == 'pending')
$scope.tx.pendingForUs = true;
$scope.updateCopayerList();
$scope.$apply();
});
});
});
$scope.updateCopayerList = function() {
lodash.map($scope.copayers, function(cp) {
lodash.each($scope.tx.actions, function(ac) {
if (cp.id == ac.copayerId) {
cp.action = ac.type;
}
});
});
};
$scope.sign = function(txp) {
var fc = profileService.focusedClient;
if (!fc.canSign() && !fc.isPrivKeyExternal())
return;
if (fc.isPrivKeyEncrypted()) {
profileService.unlockFC(function(err) {
if (err) {
$scope.error = bwsError.msg(err);
return;
}
return $scope.sign(txp);
});
return;
};
self._setOngoingForSigning();
$scope.loading = true;
$scope.error = null;
$timeout(function() {
profileService.signTxProposal(txp, function(err, txpsi) {
self.setOngoingProcess();
if (err) {
$scope.$emit('UpdateTx');
$scope.loading = false;
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not accept payment'));
$scope.$digest();
} else {
//if txp has required signatures then broadcast it
var txpHasRequiredSignatures = txpsi.status == 'accepted';
if (txpHasRequiredSignatures) {
self.setOngoingProcess(gettext('Broadcasting transaction'));
$scope.loading = true;
fc.broadcastTxProposal(txpsi, function(err, txpsb, memo) {
self.setOngoingProcess();
$scope.loading = false;
if (err) {
$scope.$emit('UpdateTx');
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not broadcast payment'));
$scope.$digest();
} else {
$log.debug('Transaction signed and broadcasted')
if (memo)
$log.info(memo);
refreshUntilItChanges = true;
$modalInstance.close(txpsb);
}
});
} else {
$scope.loading = false;
$modalInstance.close(txpsi);
}
}
});
}, 100);
};
$scope.reject = function(txp) {
self.setOngoingProcess(gettext('Rejecting payment'));
$scope.loading = true;
$scope.error = null;
$timeout(function() {
fc.rejectTxProposal(txp, null, function(err, txpr) {
self.setOngoingProcess();
$scope.loading = false;
if (err) {
$scope.$emit('UpdateTx');
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not reject payment'));
$scope.$digest();
} else {
$modalInstance.close(txpr);
}
});
}, 100);
};
$scope.remove = function(txp) {
self.setOngoingProcess(gettext('Deleting payment'));
$scope.loading = true;
$scope.error = null;
$timeout(function() {
fc.removeTxProposal(txp, function(err, txpb) {
self.setOngoingProcess();
$scope.loading = false;
// Hacky: request tries to parse an empty response
if (err && !(err.message && err.message.match(/Unexpected/))) {
$scope.$emit('UpdateTx');
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not delete payment proposal'));
$scope.$digest();
return;
}
$modalInstance.close();
});
}, 100);
};
$scope.broadcast = function(txp) {
self.setOngoingProcess(gettext('Broadcasting Payment'));
$scope.loading = true;
$scope.error = null;
$timeout(function() {
fc.broadcastTxProposal(txp, function(err, txpb, memo) {
self.setOngoingProcess();
$scope.loading = false;
if (err) {
$scope.error = bwsError.msg(err, gettextCatalog.getString('Could not broadcast payment'));
$scope.$digest();
} else {
if (memo)
$log.info(memo);
refreshUntilItChanges = true;
$modalInstance.close(txpb);
}
});
}, 100);
};
$scope.copyAddress = function(addr) {
if (!addr) return;
self.copyAddress(addr);
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/txp-details.html',
windowClass: animationService.modalAnimated.slideRight,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutRight);
});
modalInstance.result.then(function(txp) {
self.setOngoingProcess();
if (txp) {
txStatus.notify(txp, function() {
$scope.$emit('Local/TxProposalAction', refreshUntilItChanges);
});
} else {
$timeout(function() {
$scope.$emit('Local/TxProposalAction', refreshUntilItChanges);
}, 100);
}
});
};
this.setAddress = function(forceNew) {
self.addrError = null;
var fc = profileService.focusedClient;
if (!fc)
return;
// Address already set?
if (!forceNew && self.addr[fc.credentials.walletId]) {
return;
}
self.generatingAddress = true;
$timeout(function() {
addressService.getAddress(fc.credentials.walletId, forceNew, function(err, addr) {
self.generatingAddress = false;
if (err) {
self.addrError = err;
} else {
if (addr)
self.addr[fc.credentials.walletId] = addr;
}
$scope.$digest();
});
});
};
this.copyAddress = function(addr) {
if (isCordova) {
window.cordova.plugins.clipboard.copy(addr);
window.plugins.toast.showShortCenter(gettextCatalog.getString('Copied to clipboard'));
} else if (nodeWebkit.isDefined()) {
nodeWebkit.writeToClipboard(addr);
}
};
this.shareAddress = function(addr) {
if (isCordova) {
if (isMobile.Android() || isMobile.Windows()) {
window.ignoreMobilePause = true;
}
window.plugins.socialsharing.share('bitcoin:' + addr, null, null, null);
}
};
this.openCustomizedAmountModal = function(addr) {
$rootScope.modalOpened = true;
var self = this;
var fc = profileService.focusedClient;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.addr = addr;
$scope.color = fc.backgroundColor;
$scope.unitName = self.unitName;
$scope.alternativeAmount = self.alternativeAmount;
$scope.alternativeName = self.alternativeName;
$scope.alternativeIsoCode = self.alternativeIsoCode;
$scope.isRateAvailable = self.isRateAvailable;
$scope.unitToSatoshi = self.unitToSatoshi;
$scope.unitDecimals = self.unitDecimals;
var satToUnit = 1 / self.unitToSatoshi;
$scope.showAlternative = false;
$scope.isCordova = isCordova;
Object.defineProperty($scope,
"_customAlternative", {
get: function() {
return $scope.customAlternative;
},
set: function(newValue) {
$scope.customAlternative = newValue;
if (typeof(newValue) === 'number' && $scope.isRateAvailable) {
$scope.customAmount = parseFloat((rateService.fromFiat(newValue, $scope.alternativeIsoCode) * satToUnit).toFixed($scope.unitDecimals), 10);
} else {
$scope.customAmount = null;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty($scope,
"_customAmount", {
get: function() {
return $scope.customAmount;
},
set: function(newValue) {
$scope.customAmount = newValue;
if (typeof(newValue) === 'number' && $scope.isRateAvailable) {
$scope.customAlternative = parseFloat((rateService.toFiat(newValue * $scope.unitToSatoshi, $scope.alternativeIsoCode)).toFixed(2), 10);
} else {
$scope.customAlternative = null;
}
$scope.alternativeAmount = $scope.customAlternative;
},
enumerable: true,
configurable: true
});
$scope.submitForm = function(form) {
var satToBtc = 1 / 100000000;
var amount = form.amount.$modelValue;
var amountSat = parseInt((amount * $scope.unitToSatoshi).toFixed(0));
$timeout(function() {
$scope.customizedAmountUnit = amount + ' ' + $scope.unitName;
$scope.customizedAlternativeUnit = $filter('noFractionNumber')(form.alternative.$modelValue, 2) + ' ' + $scope.alternativeIsoCode;
if ($scope.unitName == 'bits') {
amount = (amountSat * satToBtc).toFixed(8);
}
$scope.customizedAmountBtc = amount;
}, 1);
};
$scope.toggleAlternative = function() {
$scope.showAlternative = !$scope.showAlternative;
};
$scope.shareAddress = function(uri) {
if (isCordova) {
if (isMobile.Android() || isMobile.Windows()) {
window.ignoreMobilePause = true;
}
window.plugins.socialsharing.share(uri, null, null, null);
}
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/customized-amount.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
};
// Send
var unwatchSpendUnconfirmed = $scope.$watch('currentSpendUnconfirmed', function(newVal, oldVal) {
if (newVal == oldVal) return;
$scope.currentSpendUnconfirmed = newVal;
});
$scope.$on('$destroy', function() {
unwatchSpendUnconfirmed();
});
this.canShowAlternative = function() {
return $scope.showAlternative;
};
this.showAlternative = function() {
$scope.showAlternative = true;
};
this.hideAlternative = function() {
$scope.showAlternative = false;
};
this.resetError = function() {
this.error = this.success = null;
};
this.bindTouchDown = function(tries) {
var self = this;
tries = tries || 0;
if (tries > 5) return;
var e = document.getElementById('menu-walletHome');
if (!e) return $timeout(function() {
self.bindTouchDown(++tries);
}, 500);
// on touchdown elements
$log.debug('Binding touchstart elements...');
['hamburger', 'menu-walletHome', 'menu-send', 'menu-receive', 'menu-history'].forEach(function(id) {
var e = document.getElementById(id);
if (e) e.addEventListener('touchstart', function() {
try {
event.preventDefault();
} catch (e) {};
angular.element(e).triggerHandler('click');
}, true);
});
}
this.hideMenuBar = lodash.debounce(function(hide) {
if (hide) {
$rootScope.hideMenuBar = true;
this.bindTouchDown();
} else {
$rootScope.hideMenuBar = false;
}
$rootScope.$digest();
}, 100);
this.formFocus = function(what) {
if (isCordova && !this.isWindowsPhoneApp) {
this.hideMenuBar(what);
}
if (!this.isWindowsPhoneApp) return
if (!what) {
this.hideAddress = false;
this.hideAmount = false;
} else {
if (what == 'amount') {
this.hideAddress = true;
} else if (what == 'msg') {
this.hideAddress = true;
this.hideAmount = true;
}
}
$timeout(function() {
$rootScope.$digest();
}, 1);
};
this.setSendFormInputs = function() {
var unitToSat = this.unitToSatoshi;
var satToUnit = 1 / unitToSat;
/**
* Setting the two related amounts as properties prevents an infinite
* recursion for watches while preserving the original angular updates
*
*/
Object.defineProperty($scope,
"_alternative", {
get: function() {
return $scope.__alternative;
},
set: function(newValue) {
$scope.__alternative = newValue;
if (typeof(newValue) === 'number' && self.isRateAvailable) {
$scope._amount = parseFloat((rateService.fromFiat(newValue, self.alternativeIsoCode) * satToUnit).toFixed(self.unitDecimals), 10);
} else {
$scope.__amount = null;
}
},
enumerable: true,
configurable: true
});
Object.defineProperty($scope,
"_amount", {
get: function() {
return $scope.__amount;
},
set: function(newValue) {
$scope.__amount = newValue;
if (typeof(newValue) === 'number' && self.isRateAvailable) {
$scope.__alternative = parseFloat((rateService.toFiat(newValue * self.unitToSatoshi, self.alternativeIsoCode)).toFixed(2), 10);
} else {
$scope.__alternative = null;
}
self.alternativeAmount = $scope.__alternative;
self.resetError();
},
enumerable: true,
configurable: true
});
Object.defineProperty($scope,
"_address", {
get: function() {
return $scope.__address;
},
set: function(newValue) {
$scope.__address = self.onAddressChange(newValue);
},
enumerable: true,
configurable: true
});
var fc = profileService.focusedClient;
// ToDo: use a credential's (or fc's) function for this
this.hideNote = !fc.credentials.sharedEncryptingKey;
};
this.setSendError = function(err) {
var fc = profileService.focusedClient;
var prefix =
fc.credentials.m > 1 ? gettextCatalog.getString('Could not create payment proposal') : gettextCatalog.getString('Could not send payment');
this.error = bwsError.msg(err, prefix);
$timeout(function() {
$scope.$digest();
}, 1);
};
this.setOngoingProcess = function(name) {
var self = this;
self.blockUx = !!name;
if (isCordova) {
if (name) {
window.plugins.spinnerDialog.hide();
window.plugins.spinnerDialog.show(null, name + '...', true);
} else {
window.plugins.spinnerDialog.hide();
}
} else {
self.onGoingProcess = name;
$timeout(function() {
$rootScope.$apply();
});
};
};
this.setFee = function(level) {
this.currentSendFeeLevel = level;
};
this.submitForm = function() {
var fc = profileService.focusedClient;
var unitToSat = this.unitToSatoshi;
if (isCordova && this.isWindowsPhoneApp) {
this.hideAddress = false;
this.hideAmount = false;
}
var form = $scope.sendForm;
if (form.$invalid) {
this.error = gettext('Unable to send transaction proposal');
return;
}
if (fc.isPrivKeyEncrypted()) {
profileService.unlockFC(function(err) {
if (err) return self.setSendError(err);
return self.submitForm();
});
return;
};
var comment = form.comment.$modelValue;
// ToDo: use a credential's (or fc's) function for this
if (comment && !fc.credentials.sharedEncryptingKey) {
var msg = 'Could not add message to imported wallet without shared encrypting key';
$log.warn(msg);
return self.setSendError(gettext(msg));
}
self.setOngoingProcess(gettext('Creating transaction'));
$timeout(function() {
var paypro = self._paypro;
var address, amount;
address = form.address.$modelValue;
amount = parseInt((form.amount.$modelValue * unitToSat).toFixed(0));
var getFee = function(cb) {
if (form.feePerKb) {
cb(null, form.feePerKb);
} else {
feeService.getCurrentFeeValue(self.currentSendFeeLevel, cb);
}
};
getFee(function(err, feePerKb) {
if (err) $log.debug(err);
fc.sendTxProposal({
toAddress: address,
amount: amount,
message: comment,
payProUrl: paypro ? paypro.url : null,
feePerKb: feePerKb,
excludeUnconfirmedUtxos: $scope.currentSpendUnconfirmed ? false : true
}, function(err, txp) {
if (err) {
self.setOngoingProcess();
profileService.lockFC();
return self.setSendError(err);
}
if (!fc.canSign() && !fc.isPrivKeyExternal()) {
$log.info('No signing proposal: No private key')
self.setOngoingProcess();
self.resetForm();
txStatus.notify(txp, function() {
return $scope.$emit('Local/TxProposalAction');
});
return;
}
self.signAndBroadcast(txp, function(err) {
self.setOngoingProcess();
self.resetForm();
if (err) {
self.error = err.message ? err.message : gettext('The payment was created but could not be completed. Please try again from home screen');
$scope.$emit('Local/TxProposalAction');
$timeout(function() {
$scope.$digest();
}, 1);
}
});
});
});
}, 100);
};
this._setOngoingForSigning = function() {
var fc = profileService.focusedClient;
if (fc.isPrivKeyExternal() && fc.getPrivKeyExternalSourceName() == 'ledger') {
self.setOngoingProcess(gettext('Requesting Ledger Wallet to sign'));
} else {
self.setOngoingProcess(gettext('Signing payment'));
}
};
this.signAndBroadcast = function(txp, cb) {
var fc = profileService.focusedClient;
this._setOngoingForSigning();
profileService.signTxProposal(txp, function(err, signedTx) {
self.setOngoingProcess();
if (err) {
err.message = bwsError.msg(err, gettextCatalog.getString('The payment was created but could not be signed. Please try again from home screen'));
return cb(err);
}
if (signedTx.status == 'accepted') {
self.setOngoingProcess(gettext('Broadcasting transaction'));
fc.broadcastTxProposal(signedTx, function(err, btx, memo) {
self.setOngoingProcess();
if (err) {
err.message = bwsError.msg(err, gettextCatalog.getString('The payment was signed but could not be broadcasted. Please try again from home screen'));
return cb(err);
}
if (memo)
$log.info(memo);
txStatus.notify(btx, function() {
$scope.$emit('Local/TxProposalAction', true);
return cb();
});
});
} else {
self.setOngoingProcess();
txStatus.notify(signedTx, function() {
$scope.$emit('Local/TxProposalAction');
return cb();
});
}
});
};
this.setForm = function(to, amount, comment, feeRate) {
var form = $scope.sendForm;
if (to) {
form.address.$setViewValue(to);
form.address.$isValid = true;
form.address.$render();
this.lockAddress = true;
}
if (amount) {
form.amount.$setViewValue("" + amount);
form.amount.$isValid = true;
form.amount.$render();
this.lockAmount = true;
}
if (comment) {
form.comment.$setViewValue(comment);
form.comment.$isValid = true;
form.comment.$render();
}
if (feeRate) {
form.feeRate = feeRate;
}
};
this.resetForm = function() {
this.resetError();
this._paypro = null;
this.lockAddress = false;
this.lockAmount = false;
this.currentSendFeeLevel = null;
this.hideAdvSend = true;
$scope.currentSpendUnconfirmed = configService.getSync().wallet.spendUnconfirmed;
this._amount = this._address = null;
var form = $scope.sendForm;
if (form && form.feeRate) {
form.feeRate = null;
}
if (form && form.amount) {
form.amount.$pristine = true;
form.amount.$setViewValue('');
form.amount.$render();
form.comment.$setViewValue('');
form.comment.$render();
form.$setPristine();
if (form.address) {
form.address.$pristine = true;
form.address.$setViewValue('');
form.address.$render();
}
}
$timeout(function() {
$rootScope.$digest();
}, 1);
};
this.openPPModal = function(paypro) {
$rootScope.modalOpened = true;
var ModalInstanceCtrl = function($scope, $modalInstance) {
var fc = profileService.focusedClient;
var satToUnit = 1 / self.unitToSatoshi;
$scope.paypro = paypro;
$scope.alternative = self.alternativeAmount;
$scope.alternativeIsoCode = self.alternativeIsoCode;
$scope.isRateAvailable = self.isRateAvailable;
$scope.unitTotal = (paypro.amount * satToUnit).toFixed(self.unitDecimals);
$scope.unitName = self.unitName;
$scope.color = fc.backgroundColor;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/paypro.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
};
this.setFromPayPro = function(uri, cb) {
if (!cb) cb = function() {};
var fc = profileService.focusedClient;
if (isChromeApp) {
this.error = gettext('Payment Protocol not supported on Chrome App');
return cb(true);
}
var satToUnit = 1 / this.unitToSatoshi;
var self = this;
/// Get information of payment if using Payment Protocol
self.setOngoingProcess(gettext('Fetching Payment Information'));
$log.debug('Fetch PayPro Request...', uri);
$timeout(function() {
fc.fetchPayPro({
payProUrl: uri,
}, function(err, paypro) {
self.setOngoingProcess();
if (err) {
$log.warn('Could not fetch payment request:', err);
self.resetForm();
var msg = err.toString();
if (msg.match('HTTP')) {
msg = gettext('Could not fetch payment information');
}
self.error = msg;
$timeout(function() {
$rootScope.$digest();
}, 1);
return cb(true);
}
if (!paypro.verified) {
self.resetForm();
$log.warn('Failed to verified payment protocol signatured');
self.error = gettext('Payment Protocol Invalid');
$timeout(function() {
$rootScope.$digest();
}, 1);
return cb(true);
}
self._paypro = paypro;
self.setForm(paypro.toAddress, (paypro.amount * satToUnit).toFixed(self.unitDecimals), paypro.memo);
return cb();
});
}, 1);
};
this.setFromUri = function(uri) {
var self = this;
function sanitizeUri(uri) {
// Fixes when a region uses comma to separate decimals
var regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i;
var match = regex.exec(uri);
if (!match || match.length === 0) {
return uri;
}
var value = match[0].replace(',', '.');
var newUri = uri.replace(regex, value);
return newUri;
};
var satToUnit = 1 / this.unitToSatoshi;
// URI extensions for Payment Protocol with non-backwards-compatible request
if ((/^bitcoin:\?r=[\w+]/).exec(uri)) {
uri = decodeURIComponent(uri.replace('bitcoin:?r=', ''));
this.setFromPayPro(uri, function(err) {
if (err) {
return err;
}
});
} else {
uri = sanitizeUri(uri);
if (!bitcore.URI.isValid(uri)) {
return uri;
}
var parsed = new bitcore.URI(uri);
var addr = parsed.address ? parsed.address.toString() : '';
var message = parsed.message;
var amount = parsed.amount ?
(parsed.amount.toFixed(0) * satToUnit).toFixed(this.unitDecimals) : 0;
if (parsed.r) {
this.setFromPayPro(parsed.r, function(err) {
if (err && addr && amount) {
self.setForm(addr, amount, message);
return addr;
}
});
} else {
this.setForm(addr, amount, message);
return addr;
}
}
};
this.onAddressChange = function(value) {
this.resetError();
if (!value) return '';
if (this._paypro)
return value;
if (value.indexOf('bitcoin:') === 0) {
return this.setFromUri(value);
} else if (/^https?:\/\//.test(value)) {
return this.setFromPayPro(value);
} else {
return value;
}
};
// History
function strip(number) {
return (parseFloat(number.toPrecision(12)));
}
this.getUnitName = function() {
return this.unitName;
};
this.getAlternativeIsoCode = function() {
return this.alternativeIsoCode;
};
this.openTxModal = function(btx) {
$rootScope.modalOpened = true;
var self = this;
var fc = profileService.focusedClient;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.btx = btx;
$scope.settings = config;
$scope.color = fc.backgroundColor;
$scope.copayerId = fc.credentials.copayerId;
$scope.isShared = fc.credentials.n > 1;
$scope.getAmount = function(amount) {
return self.getAmount(amount);
};
$scope.getUnitName = function() {
return self.getUnitName();
};
$scope.getShortNetworkName = function() {
var n = fc.credentials.network;
return n.substring(0, 4);
};
$scope.copyAddress = function(addr) {
if (!addr) return;
self.copyAddress(addr);
};
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/tx-details.html',
windowClass: animationService.modalAnimated.slideRight,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutRight);
});
};
this.hasAction = function(actions, action) {
return actions.hasOwnProperty('create');
};
this._doSendAll = function(amount, feeRate) {
this.setForm(null, amount, null, feeRate);
};
// TODO: showPopup alike
this.confirmDialog = function(msg, cb) {
if (isCordova) {
navigator.notification.confirm(
msg,
function(buttonIndex) {
if (buttonIndex == 1) {
$timeout(function() {
return cb(true);
}, 1);
} else {
return cb(false);
}
},
confirm_msg, [accept_msg, cancel_msg]
);
} else if (isChromeApp) {
// No feedback, alert/confirm not supported.
return cb(true);
} else {
return cb(confirm(msg));
}
};
this.sendAll = function(amount, feeStr, feeRate) {
var self = this;
var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees", {
fee: feeStr
});
this.confirmDialog(msg, function(confirmed) {
if (confirmed)
self._doSendAll(amount, feeRate);
});
};
/* Start setup */
this.bindTouchDown();
if (profileService.focusedClient) {
this.setAddress();
this.setSendFormInputs();
}
});
angular.module('copayApp').run(['gettextCatalog', function (gettextCatalog) {
/* jshint -W100 */
gettextCatalog.setStrings('cs', {"(possible double spend)":"(pravděpodobná dvojitá platba)","(Trusted)":"(Věrohodný)","[Balance Hidden]":"[skrytý zůstatek]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} bude odečteno jako poplatek bitcoinové síti","{{feeRateStr}} of the transaction":"{{feeRateStr}} z transakce","{{index.m}}-of-{{index.n}}":"{{index.m}} z {{index.n}}","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} transakce stažena","{{item.m}}-of-{{item.n}}":"{{item.m}} z {{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Návrh k platbě může být odstraněn pokud 1) jste jej vytvořil(a) a žádný spoluplátce jej nepodepsal 2) Uběhlo 24 hodin od vytvoření návrhu.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>POKUD ZTRATÍTE PŘÍSTUP K VAŠI SPOLUPLÁTCOVSKÉ PENĚŽENCE NEBO VAŠÍM ŠIFROVANÝM KLÍČŮM A NEMÁTE ULOŽENOU ZÁLOHU VAŠI PENĚŽENKY A HESLEM ZVLÁŠTĚ, BERETE NA VĚDOMÍ ŽE VŠECHNY BITCOINY ULOŽENÉ V TÉTO SPOLUPLÁTCOVSKÉ PENĚŽENCE NEBUDOU DOSTUPNÉ. </b>","A multisignature bitcoin wallet":"A vícepodpisová bitcoin peněženka","About Copay":"O Copay","Accept":"Přijmout","Account":"Účet","Account Number":"Číslo účtu","Activity":"Aktivita","Add a new entry":"Přidat nový záznam","Add wallet":"Přidat peněženku","Address":"Adresa","Address Type":"Typ adresy","Advanced":"Pokročilé","Alias":"Název","Alias for <i>{{index.walletName}}</i>":"Název pro <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Všichni spoluúčastníci překladů Copay jsou vítání. Přihlaště se na crowdin.com a přidejte se k projektu Copay na","All transaction requests are irreversible.":"Všechny žádosti o platbu jsou nevratné.","Alternative Currency":"Alternativní měna","Amount":"Částka","Amount in":"Částka v","Are you sure you want to delete this wallet?":"Opravdu si přejete odstranit tuto peněženku?","Available Balance":"Dostupný zůstatek","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Průměrný čas potvrzení je: {{fee.nbBlocks * 10}} minut","Back":"Zpět","Backup":"Záloha","Backup failed":"Chyba zálohování","Backup Needed":"Vyžadována záloha","Backup now":"Vytvořit zálohu","Bad wallet invitation":"Chybný požadavek do peněženky","Balance By Address":"Zůstatek adres","BIP32 path for address derivation":"BIP32 cesta pro derivaci adres","Bitcoin address":"Bitcoin adresa","Bitcoin Network Fee Policy":"Zásady poplatků bitcoinové sítě","Bitcoin URI is NOT valid!":"Bitcoin URI neni platná!","Broadcast Payment":"Vysílání platby","Broadcasting transaction":"Vysílání transakce","Browser unsupported":"Nepodporovaný prohlížeč","Calculating fee":"Vypočítávám poplatek","Cancel":"Zrušit","Cancel and delete the wallet":"Zrušit a odstranit peněženku","Cannot create transaction. Insufficient funds":"Nelze vytvořit transakci. Nedostatek prostředků","Cannot join the same wallet more that once":"Nelze spojit stejnou peněženku více než jednou","Cannot sign: The payment request has expired":"Chyba podpisu: Návrh platby vypršel","Certified by":"Ověřeno od","Changing wallet alias only affects the local wallet name.":"Změna názvu peněženky bude aktualizovat pouze název na tomto zařízení.","Choose a backup file from your computer":"Vyberte zálohu z PC","Clear cache":"Vymazat cache","Close":"Zavřít","Color":"Barva","Commit hash":"Hash softwaru","Confirm":"Potvrdit","Confirmations":"Potvrzení","Congratulations!":"Gratulujeme!","Connection reset by peer":"Spojení obnoveno uzlem","Continue":"Pokračovat","Copayer already in this walle
gettextCatalog.setStrings('de', {"(possible double spend)":"(mögliche Doppelausgabe)","(Trusted)":"(Vertraut)","[Balance Hidden]":"[Guthaben versteckt]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} wird als Netzwerkgebühr abgezogen","{{feeRateStr}} of the transaction":"{{feeRateStr}} der Transaktion","{{index.m}}-of-{{index.n}}":"{{index.m}}-von-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} weitere","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} Transaktionen werden heruntergeladen","{{item.m}}-of-{{item.n}}":"{{item.m}}-von-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Ein Zahlungsvorschlag kann gelöscht werden, wenn 1) Du diesen erzeugt hast und noch kein anderer Copayer unterschrieben hat, oder 2) 24 Stunden vergangen sind, seit der Vorschlag erstellt wurde.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>WENN DER ZUGRIFF AUF DAS COPAY WALLET ODER DEN VERSCHLÜSSELTEN PRIVATEN SCHLÜSSELN VERLOREN GEHT UND KEINE SICHERUNG DES WALLETS UND KORRESPONDIERENDEM PASSWORT EXISTIERT, DANN WIRD BESTÄTIGT UND AKZEPTIERT, DASS AUF ALLE MIT DIESEM WALLET VERBUNDENEN BITCOIN KEIN ZUGRIFF MEHR MÖGLICH IST.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>ODER</b> 1 Exportdatei des Wallets und und die noch benötigten Wallet-Wiederherstellungsphrasen (z.B. für ein 3-5 Wallet: 1 Exportdatei + 2 Wallet-Wiederherstellungsphrasen anderer Copayer).","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>ODER</b> die Wallet-Wiederherstellungsphrasen <b>aller</b> Copayer des Wallets","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>ODER</b> die Wallet-Wiederherstellungsphrasen <b>aller</b> Copayer des Wallets","A multisignature bitcoin wallet":"Ein Bitcoin Wallet mit Mehrfachunterschriften","About Copay":"Über Copay","Accept":"Akzeptieren","Account":"Benutzerkonto","Account Number":"Kontonummer","Activity":"Aktivität","Add a new entry":"Einen neuen Eintrag hinzufügen","Add a Password":"Passwort festlegen","Add an optional password to secure the recovery phrase":"Ein optionales Passwort zur Sicherung der Wiederherstellungsphrase hinzufügen","Add comment":"Kommentar hinzufügen","Add wallet":"Wallet hinzufügen","Address":"Adresse","Address Type":"Adresstyp","Advanced":"Erweitert","Alias":"Alias","Alias for <i>{{index.walletName}}</i>":"Alias für <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Alle Beiträge zur Übersetzung von Copay sind willkommen. Melde Dich bei crowdin.com an verbinde Dich mit dem Copay-Projekt über","All transaction requests are irreversible.":"Transaktionen können unmöglich rückgängig gemacht werden.","Alternative Currency":"Alternative Währung","Amount":"Betrag","Amount below minimum allowed":"Betrag unter zulässigem Minimum","Amount in":"Betrag in","Are you sure you want to delete the recovery phrase?":"Sind Sie sicher, dass Sie die Wiederherstellungsphrase löschen möchten?","Are you sure you want to delete this wallet?":"Soll das Wallet wirklich gelöscht werden?","Auditable":"Prüffähig","Available Balance":"Verfügbarer Gesamtbetrag","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Durchschnittliche Zeit für die Bestätigung der Transaktion: {{fee.nbBlocks * 10}} Minuten","Back":"Zurück","Backup":"Sicherung","Backup failed":"Backup ist fehlgeschlagen","Backup Needed":"Backup wird benötigt","
gettextCatalog.setStrings('el', {"(possible double spend)":"(πιθανό διπλό ξόδεμα)","(Trusted)":"(Εμπιστευτικό)","[Balance Hidden]":"[Υπόλοιπο Κρυμένο]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}}, θα προεξοφληθεί ώς τέλος του δικτύου bitcoin","{{feeRateStr}} of the transaction":"{{feeRateStr}} της συναλλαγής","{{index.m}}-of-{{index.n}}":"{{index.m}}-του-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} περισσότερα","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} οι συναλλαγές μεταφορτώθηκαν","{{item.m}}-of-{{item.n}}":"{{item.m}}-του-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"Μια πρόταση πληρωμής μπορεί να διαγραφεί εάν 1) είστε ο δημιουργός, και κανένας άλλος χρήστης του copay δεν έχει υπογράψει, ή 2) έχουν περάσει 24 ώρες απο την ώρα που η πρόταση δημιουργήθηκε.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>ΕΑΝ ΧΑΣΕΤΕ ΤΗΝ ΠΡΟΣΒΑΣΗ ΝΑ ΣΑΣ ΣΤΟ ΠΟΡΤΟΦΌΛΙ COPAY Ή ΣΤΑ ΚΡΥΠΤΟΓΡΑΦΗΜΕΝΑ ΙΔΙΩΤΙΚΑ ΣΑΣ ΚΛΕΙΔΙΑ ΚΑΙ ΔΕΝ ΑΠΟΘΗΚΕΥΣΑΤΕ ΧΩΡΙΣΤΆ ΕΝΑ ΑΝΤΙΓΡΑΦΟ ΑΣΦΑΛΕΙΑΣ ΤΟΥ ΠΟΡΤΟΦΟΛΙΟΥ ΚΑΙ ΤΟΥ ΑΝΤΙΣΤΟΙΧΟΥ ΚΩΔΙΚΟΥ ΠΡΌΣΒΑΣΗΣ, ΑΠΟΔΕΧΕΣΤΕ ΚΑΙ ΣΥΜΦΩΝΕΙΤΕ ΟΤΙ ΟΠΟΙΑΔΗΠΟΤΕ ΠΟΣΟΤΗΤΑ BITCOIN ΠΟΥ ΕΧΕΤΕ ΣΥΣΧΕΤΙΣΕΙ ΜΕ ΤΟ ΠΟΡΤΟΦΟΛΙ ΤΟΥ COPAY ΘΑ ΓΙΝΟΥΝ ΑΠΡΟΣΠΕΛΑΣΤΑ.</b>","A multisignature bitcoin wallet":"Ένα πορτοφόλι bitcoin με δυνατότητα πολλαπλών υπογραφών","About Copay":"Σχετικά με το Copay","Accept":"Αποδοχή","Account":"Λογαριασμός","Account Number":"Αριθμός λογαριασμού","Activity":"Δραστηριότητα","Add a new entry":"Προσθέστε Καταχώρηση","Add a Password":"Προσθέστε Κωδικό","Add an optional password to secure the recovery phrase":"Προσθέστε προαιρετικό κωδικό για να ασφαλίσετε τη φράση επαναφοράς","Add comment":"Προσθήκη σχολίου","Add wallet":"Προσθήκη Πορτοφολιού","Address":"Διεύθυνση","Address Type":"Τύπος Διεύθυνσης","Advanced":"Για προχωρημένους","Alias":"Ψευδώνυμο","Alias for <i>{{index.walletName}}</i>":"Ψευδώνυμο για <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Όλες οι εισηγήσεις στην μετάφραση του Copay είναι ευπρόσδεκτες. Εγγραφείτε στο crowdin.com για να συμμετάσχετε στο έργο Copay","All transaction requests are irreversible.":"Όλες οι αιτήσεις για συναλλαγές είναι αμετάκλητες.","Alternative Currency":"Εναλλακτικό Νόμισμα","Amount":"Ποσό","Amount below minimum allowed":"Ποσό χαμηλότερο από το κατώτερο επιτρεπόμενο","Amount in":"Ποσό εισόδου","Are you sure you want to delete the recovery phrase?":"Σίγουρα θέλετε να σβήσετε τη φράση επαναφοράς;","Are you sure you want to delete this wallet?":"Είσ<EFBFBD>
gettextCatalog.setStrings('es', {"(possible double spend)":"(Posible doble gasto)","(Trusted)":"(De confianza)","[Balance Hidden]":"[Balance Oculto]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} se descontará por comisión de la red bitcoin","{{feeRateStr}} of the transaction":"{{feeRateStr}} de la transacción","{{index.m}}-of-{{index.n}}":"{{index.m}}-de-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} más","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} transacciones descargadas","{{item.m}}-of-{{item.n}}":"{{item.m}}-de-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Una propuesta de pago puede ser eliminada si 1) Ud. es el creador, y ningún otro copayer la haya firmado, o 2) hayan transcurrido 24 horas desde la creación de la propuesta.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>SI UD. PIERDE ACCESO A SU MONEDERO COPAY O A SUS CLAVES PRIVADAS ENCRIPTADAS Y NO HA GUARDADO POR SEPARADO UNA COPIA DE SEGURIDAD DE SU MONEDERO Y CONTRASEÑA CORRESPONDIENTES, USTED RECONOCE Y ACEPTA QUE CUALQUIER BITCOIN QUE HA ASOCIADO CON ESE MONEDERO COPAY SERÁ INACCESIBLE.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>O</b> 1 archivo exportado del monedero y el quórum restante de la frase de recuperación (por ejemplo en un monedero 3-5: 1 archivo exportado + 2 frases de recuperación del monedero de cualquiera de los otros copayers).","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>O</b> la frase de recuperación de <b>todos</b> los copayers del monedero","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>O</b> las frases de recuperación de <b>todos</b> los copayers del monedero","A multisignature bitcoin wallet":"Monedero multifirma de bitcoin","About Copay":"Acerca de Copay","Accept":"Aceptar","Account":"Cuenta","Account Number":"Número de cuenta","Activity":"Actividad","Add a new entry":"Agregar una nueva entrada","Add a Password":"Agregar una contraseña","Add an optional password to secure the recovery phrase":"Agregar una contraseña opcional para asegurar la frase de recuperación","Add comment":"Añadir comentario","Add wallet":"Agregar monedero","Address":"Dirección","Address Type":"Tipo de Dirección","Advanced":"Avanzado","Alias":"Alias","Alias for <i>{{index.walletName}}</i>":"Alias de <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Todas las contribuciones a la traducción de Copay son bienvenidas. Regístrese en crowdin.com y únase al proyecto Copay en","All transaction requests are irreversible.":"Todas las solicitudes de transacciones son irreversibles.","Alternative Currency":"Moneda Alternativa","Amount":"Importe","Amount below minimum allowed":"Cantidad por debajo del mínimo permitido","Amount in":"Importe en","Are you sure you want to delete the recovery phrase?":"¿Está seguro que quiere eliminar la frase de recuperación?","Are you sure you want to delete this wallet?":"¿Estas seguro de borrar este monedero?","Auditable":"Auditables","Available Balance":"Balance disponible","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Tiempo promedio de confirmación: {{fee.nbBlocks * 10}} minutos","Back":"Volver","Backup":"Copia de seguridad","Backup failed":"Falló la copia de seguridad","Backup Needed":"Se requiere hacer copia de seguridad","Backup now":"Realizar copia de seguridad ahora","Bad wallet invi
gettextCatalog.setStrings('fr', {"(possible double spend)":"(double dépense possible)","(Trusted)":"(Fiable)","[Balance Hidden]":"[Solde masqué]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} seront déduits pour les frais de réseau Bitcoin","{{feeRateStr}} of the transaction":"{{feeRateStr}} de la transaction","{{index.m}}-of-{{index.n}}":"{{index.m}}-sur-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} de plus","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} transactions téléchargées","{{item.m}}-of-{{item.n}}":"{{item.m}}-sur-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Une proposition de paiement peut être supprimée si vous en êtes le créateur et qu'aucun des autres copayers n'a signé, ou si 24 heures sont passées depuis la création de la proposition.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>SI VOUS PERDEZ L'ACCÈS À VOTRE PORTEFEUILLE COPAY OU À VOS CLÉS PRIVÉES CHIFFRÉES ET QUE VOUS N'AVEZ PAS ENTREPOSÉ SÉPARÉMENT UNE SAUVEGARDE DE VOTRE PORTEFEUILLE ET LES MOTS DE PASSE CORRESPONDANT, VOUS RECONNAISSEZ ET ACCEPTEZ QUE LES BITCOINS QUE VOUS AVEZ ASSOCIÉ À CE PORTEFEUILLE COPAY DEVIENNENT INACCESSIBLES.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>OU</b> 1 fichier d'exportation de portefeuille et le quorum restant en phrases de récupération de portefeuille (ex. dans un portefeuille 3-5 : 1 fichier d'exportation du portefeuille + 2 phrases de récupération du portefeuille de n'importe quels autres copayers).","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>OU</b> la phrase de récupération de portefeuille de <b>tous</b> les copayers du portefeuille","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>OU</b> les phrases de récupération de portefeuille de <b>tous</b> les copayers du portefeuille","A multisignature bitcoin wallet":"Un portefeuille bitcoin multi-signatures","About Copay":"À propos de Copay","Accept":"Accepter","Account":"Compte","Account Number":"Numéro de compte","Activity":"Activité","Add a new entry":"Ajouter une nouvelle entrée","Add a Password":"Ajouter un mot de passe","Add an optional password to secure the recovery phrase":"Ajouter un mot de passe optionnel pour sécuriser la phrase de récupération","Add comment":"Ajouter un commentaire","Add wallet":"Ajouter portefeuille","Address":"Adresse","Address Type":"Type d'adresse","Advanced":"Paramètres avancés","Alias":"Alias","Alias for <i>{{index.walletName}}</i>":"Alias pour <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Toutes les contributions à la traduction de Copay sont les bienvenues. Inscrivez-vous sur crowdin.com et rejoignez le projet Copay sur","All transaction requests are irreversible.":"Toutes les transactions sont irréversibles.","Alternative Currency":"Devise alternative","Amount":"Montant","Amount below minimum allowed":"Montant en dessous du minimum autorisé","Amount in":"Montant en","Are you sure you want to delete the recovery phrase?":"Êtes-vous sûr(e) de vouloir supprimer la phrase de récupération ?","Are you sure you want to delete this wallet?":"Êtes-vous certain(e) de vouloir supprimer ce portefeuille ?","Auditable":"Vérifiable","Available Balance":"Solde disponible","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Temps de confirmation moyen : {{fee.nbBlocks * 10}} minu
gettextCatalog.setStrings('it', {"(possible double spend)":"(possibile doppia spesa)","(Trusted)":"(Fidato)","[Balance Hidden]":"[Fondi Nascosti]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} verranno detratti come commissione del network","{{feeRateStr}} of the transaction":"{{feeRateStr}} della transazione","{{index.m}}-of-{{index.n}}":"{{index.m}}-di-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} altre","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} transazioni scaricate","{{item.m}}-of-{{item.n}}":"{{item.m}}-di-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Una proposta di pagamento può essere eliminata se 1) Tu sei il creatore e nessun altro copayer ha firmato, oppure 2) Sono passate 24 ore da quando la proposta e' stata creata.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>Se perdi l'accesso al tuo portafoglio COPAY o tuo crittografato chiavi PRIVATE e non hai archiviato separatamente una copia di BACKUP del vostro portafoglio e la corrispondente PASSWORD, tu riconosci e accetti che qualsiasi BITCOIN associato con quel portafoglio COPAY diventerà inaccessibile.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>O</b> 1 file di portafoglio esportato e il restante quorum di frasi di recupero portafoglio (ad esempio in un 3-5 portafogli: 1 file di portafoglio esportato + 2 frasi di recupero portafoglio di qualsiasi degli altri copayers).","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>O</b> la frase di recupero di portafoglio di <b>tutti</b> i copayers nel portafoglio","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>O</b> le frasi di recupero di portafoglio di <b>tutti</b> i copayers nel portafoglio","A multisignature bitcoin wallet":"Un portafoglio bitcoin multifirma","About Copay":"Circa Copay","Accept":"Accetta","Account":"Conto","Account Number":"Numero del Conto","Activity":"Attività","Add a new entry":"Aggiungi una nuova voce","Add a Password":"Aggiungi una Password","Add an optional password to secure the recovery phrase":"Aggiungere una password facoltativa per proteggere la frase di recupero","Add comment":"Aggiungi commento","Add wallet":"Aggiungi un portafoglio","Address":"Indirizzo","Address Type":"Tipo di indirizzo","Advanced":"Avanzato","Alias":"Alias","Alias for <i>{{index.walletName}}</i>":"Alias per <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Tutti i contributori alla traduzione di Copay sono i benvenuti. Iscriviti a crowdin e unisciti al progetto Copay presso","All transaction requests are irreversible.":"Tutte le richieste di transazione sono irreversibili.","Alternative Currency":"Valuta alternativa","Amount":"Ammontare","Amount below minimum allowed":"Importo inferiore al minimo consentito","Amount in":"Importo in","Are you sure you want to delete the recovery phrase?":"Sei sicuro di voler cancellare la frase di recupero?","Are you sure you want to delete this wallet?":"Sei sicuro di voler eliminare questo portafoglio?","Auditable":"Controllabile","Available Balance":"Saldo disponibile","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Tempo medio di conferma: {{fee.nbBlocks * 10}} minuti","Back":"Indietro","Backup":"Backup","Backup failed":"Backup non riuscito","Backup Needed":"Backup necessario","Backup now":"Esegui backup ora","Bad wallet invitation":"Invito al wallet non corretto",
gettextCatalog.setStrings('ja', {"(possible double spend)":"(二重払い可能性あり)","(Trusted)":"(信頼済み)","[Balance Hidden]":"[残高非表示中]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} のビットコインネットワーク手数料が差し引かれます。","{{feeRateStr}} of the transaction":"{{feeRateStr}} のレート","{{index.m}}-of-{{index.n}}":"{{index.m}}-of-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"あと {{index.result.length - index.txHistorySearchResults.length}}","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} 個の取引ダウンロード済み","{{item.m}}-of-{{item.n}}":"{{item.m}}-of-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* 送金の提案の取下げは①他のウォレット参加者に署名されていなかった場合、提案者に提案を取り下げることができます。②提案の起案から24時間が経っても解決しなかった場合、全員に取り下げることができます。","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>Copayウォレットとその中にある秘密鍵の情報を紛失してしまい、尚且つバックアップが無い、若しくはそのバックアップを暗号化した際のパスワードが分からないなどの状況に陥ってしまえば、そのウォレットに含まれた全てのビットコインが永久送金不可能となってしまうことを認識し、同意するものとします。</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>または</b> 従来ウォレットエクスポートファイル1つに加えて残りの必須人数の復元フレーズ (例: 3-of-5 ウォレットでは従来ウォレットバックアップ1つに加え、他の参加者2人分の復元フレーズさえあればウォレットは復元できます)","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>または</b> 参加者 <b>全員</b> のウォレット復元フレーズ","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>または</b> 参加者 <b>全員</b> のウォレット復元フレーズ","A multisignature bitcoin wallet":"マルチシグネチャビットコインウォレット","About Copay":"Copayについて","Accept":"承諾","Account":"ポケット","Account Number":"ポケット番号","Activity":"履歴","Add a new entry":"新規追加","Add a Password":"パスワードを追加","Add an optional password to secure the recovery phrase":"フレーズを守るために任意のパスワードをかけて下さい","Add comment":"コメントを追加","Add wallet":"ウォレットを追加","Address":"アドレス","Address Type":"アドレスの種類","Advanced":"上級者向け","Alias":"通称","Alias for <i>{{index.walletName}}</i>":"<i>{{index.walletName}}</i> の通称設定","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Copayの翻訳は簡単に投稿することができます。crowdin.comのアカウント作成の後、自由にご参加いただけるプロジェクトページはこちら","All transaction requests are irreversible.":"署名が完了してしまった取引は取り消しが不可能となります。","Alternative Currency":"表示通貨","Amount":"金額","Amount below minimum allowed":"送金可能最少額を下回っています","Amount in":"換算済金額","Are you sure you want to delete the recovery phrase?":"復元フレーズ<EFBFBD>
gettextCatalog.setStrings('ko', {"(possible double spend)":"(이중 사용 가능성 있음)","(Trusted)":"(Trusted)","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} will be deducted for bitcoin networking fees","{{index.m}}-of-{{index.n}}":"{{index.m}}-of-{{index.n}}","{{item.m}}-of-{{item.n}}":"{{item.m}}-of-{{item.n}}","{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance":"{{len}} 개의 지갑을 가져왔습니다. 잔액을 조회하고 있습니다. 갱신된 잔액을 확인하려면 기다려 주세요","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* 지불제안은 다음 조건이 만족할 때 지울 수 있습니다. 1) 당신이 작성자이고, 다른 지갑 참여자가 사인하지 않았을 때, 또는 2) 제안이 작성된 지 24시간 이상이 지났을 때.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).":"<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).","<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet","<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet","A multisignature bitcoin wallet":"다중서명 비트코인 지갑","About Copay":"Copay에 대하여","Accept":"승인","Add a Seed Passphrase":"Add a Seed Passphrase","Add an optional passphrase to secure the seed":"Add an optional passphrase to secure the seed","Add wallet":"지갑 추가","Address":"주소","Address Type":"Address Type","Advanced":"고급","Advanced Send":"Advanced Send","Agree":"동의","Alias for <i>{{index.walletName}}</i>":"<i>{{index.walletName}}</i>의 별명","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at","All transaction requests are irreversible.":"All transaction requests are irreversible.","Already have a wallet?":"이미 지갑을 가지고 있나요?","Alternative Currency":"표시 통화","Amount":"금액","Amount below dust threshold":"Amount below dust threshold","Amount in":"Amount in","Applying changes":"변경 사항 적용 중","Are you sure you want to delete the backup words?":"Are you sure you want to delete the backup words?","Are you sure you want to delete this wallet?":"정말로 지갑을 삭제하시겠습니까?","Available Balance":"사용 가능한 잔액","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Average confirmation time: {{fee.nbBlocks * 10}} minutes","Back":"뒤로","Backup":"백업","Backup now":"지금 백업","Backup words deleted":"Backup words deleted","Bad wallet invitation":"Bad wallet invitation","Balance By Address":"Balance By Address","Before receiving funds, it is highly recommended you backup your wallet keys.":"비트코인을 받기 전에 지갑의 키를 백업하길 강력히 권장합니다.","Bitcoin address":"비트코인 주소","Bitcoin Network Fee Policy":"비트코인 네트워크 수수료 설정","Bitcoin transactions may include a fee collected by miners on the network. The higher t
gettextCatalog.setStrings('nl', {"(possible double spend)":"(mogelijk dubbel besteed)","(Trusted)":"(Trusted)","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} will be deducted for bitcoin networking fees","{{index.m}}-of-{{index.n}}":"{{index.m}}-of-{{index.n}}","{{item.m}}-of-{{item.n}}":"{{item.m}}-of-{{item.n}}","{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance":"{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Een betalingsvoorstel kan worden verwijderd als 1) u de aanmaker bent, en geen andere medebetaler heeft ondertekend, of 2) 24 uur zijn verstreken sinds het voorstel werd aangemaakt.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).":"<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).","<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet","<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet","A multisignature bitcoin wallet":"A multisignature bitcoin wallet","About Copay":"About Copay","Accept":"Accept","Add a Seed Passphrase":"Add a Seed Passphrase","Add an optional passphrase to secure the seed":"Add an optional passphrase to secure the seed","Add wallet":"Add wallet","Address":"Address","Address Type":"Address Type","Advanced":"Advanced","Advanced Send":"Advanced Send","Agree":"Agree","Alias for <i>{{index.walletName}}</i>":"Alias for <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at","All transaction requests are irreversible.":"All transaction requests are irreversible.","Already have a wallet?":"Already have a wallet?","Alternative Currency":"Alternative Currency","Amount":"Amount","Amount below dust threshold":"Amount below dust threshold","Amount in":"Amount in","Applying changes":"Applying changes","Are you sure you want to delete the backup words?":"Are you sure you want to delete the backup words?","Are you sure you want to delete this wallet?":"Are you sure you want to delete this wallet?","Available Balance":"Available Balance","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Average confirmation time: {{fee.nbBlocks * 10}} minutes","Back":"Back","Backup":"Backup","Backup now":"Backup now","Backup words deleted":"Backup words deleted","Bad wallet invitation":"Bad wallet invitation","Balance By Address":"Balance By Address","Before receiving funds, it is highly recommended you backup your wallet keys.":"Before receiving funds, it is highly recommended you backup your wallet keys.","Bitcoin address":"Bitcoin address","Bitcoin Network Fee Policy":"Bitcoin Network Fee Policy","Bitcoin transactions may include a fee collected by miners on the network. The higher the fee, the greater the incentive a miner has to include that transaction in a block. Actual fees are determined based on network load and the selected policy.":"Bitcoin
gettextCatalog.setStrings('pl', {"(possible double spend)":"(możliwa podwójna wypłata)","(Trusted)":"(Zaufany)","[Balance Hidden]":"[Balans Ukryty]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} zostanie potrącone jako prowizja sieci bitcoin","{{feeRateStr}} of the transaction":"{{feeRateStr}} transakcji","{{index.m}}-of-{{index.n}}":"{{index.m}}-z-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} więcej","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} transakcji pobrane","{{item.m}}-of-{{item.n}}":"{{item.m}}-z-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Wniosek wypłaty może być usunięty jeśli: 1) Po utworzeniu nie zatwierdził go żaden inny współwłaściciel portfela lub 2) minęły 24 godziny od kiedy wniosek został utworzony.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>JEŚLI UŻYTKOWNIK STRACI DOSTĘP DO PORTFELA COPAY LUB ZASZYFROWANYCH KLUCZY PRYWATNYCH, A NIE MA ZAPISANEJ KOPII ZAPASOWEJ PORTFELA I HASŁA, PRZYJMUJE DO WIADOMOŚCI, ŻE JAKIEKOLWIEK POSIADANE BITCOINY ZWIĄZANE Z TYM PORTFELEM COPAY BĘDĄ NIEDOSTĘPNE.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>LUB</b> 1 plik eksportu portfela i reszta wymaganych fraz odzyskiwania portfela (np. w portfelu 3-5: 1 plik eksportu portfela + 2 frazy odzyskiwania któregokolwiek z pozostałych współwłaścicieli portfela).","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>LUB</b> frazy odzyskiwania <b>wszystkich</b> współwłaścicieli portfela","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>LUB</b> fraz odzyskiwania <b>wszystkich</b> współwłaścicieli portfela","A multisignature bitcoin wallet":"Portfel bitcoin z multipodpisami","About Copay":"Informacje o Copay","Accept":"Akceptuj","Account":"Konto","Account Number":"Numer konta","Activity":"Transakcje","Add a new entry":"Dodaj nowy wpis","Add a Password":"Dodaj hasło","Add an optional password to secure the recovery phrase":"Dodaj opcjonalne hasło do bezpiecznego odzyskiwania frazy","Add comment":"Dodaj komentarz","Add wallet":"Dodaj portfel","Address":"Adres","Address Type":"Rodzaj adresu","Advanced":"Zaawansowane","Alias":"Nazwa","Alias for <i>{{index.walletName}}</i>":"Nazwa dla <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Wkład do tłumaczenia Copay mile widziany. Zapisz się na crowdin.com i dołącz do projektu Copay na","All transaction requests are irreversible.":"Transakcji nie można wycofać.","Alternative Currency":"Alternatywna waluta","Amount":"Kwota","Amount below minimum allowed":"Kwota poniżej minimum dozwolona","Amount in":"Kwota w","Are you sure you want to delete the recovery phrase?":"Czy na pewno chcesz usunąć frazę?","Are you sure you want to delete this wallet?":"Czy na pewno chcesz usunąć ten portfel?","Auditable":"Weryfikowalny","Available Balance":"Dostępne saldo","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Średni czas potwierdzenia: {{fee.nbBlocks * 10}} minut","Back":"Powrót","Backup":"Kopia zapasowa","Backup failed":"Tworzenie kopii zapasowej nie powiodło się","Backup Needed":"Potrzebna kopia zapasowa","Backup now":"Utwórz kopię zapasową teraz","Bad wallet invitation":"Nieprawidłowe zaproszenie","Balance By Address":"Saldo wg adresu","Before receiving funds, you must backup your wallet. If this device is lost, it is impossible to
gettextCatalog.setStrings('pt', {"(possible double spend)":"(possible double spend)","(Trusted)":"(Trusted)","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} will be deducted for bitcoin networking fees","{{index.m}}-of-{{index.n}}":"{{index.m}}-of-{{index.n}}","{{item.m}}-of-{{item.n}}":"{{item.m}}-of-{{item.n}}","{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance":"{{len}} carteiras importadas. Recursos de digitalização em andamento. Espere para ver o saldo atualizado","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).":"<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).","<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet","<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet","A multisignature bitcoin wallet":"Uma carteira de bitcoin multi-assinada","About Copay":"Sobre a Copay","Accept":"Aceitar","Add a Seed Passphrase":"Add a Seed Passphrase","Add an optional passphrase to secure the seed":"Add an optional passphrase to secure the seed","Add wallet":"Adicionar carteira","Address":"Endereço","Address Type":"Address Type","Advanced":"Avançado","Advanced Send":"Advanced Send","Agree":"Concordar","Alias for <i>{{index.walletName}}</i>":"Alias for <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at","All transaction requests are irreversible.":"All transaction requests are irreversible.","Already have a wallet?":"Já tem uma carteira?","Alternative Currency":"Moeda Alternativa","Amount":"Valor","Amount below dust threshold":"Amount below dust threshold","Amount in":"Montante em","Applying changes":"Aplicar alterações","Are you sure you want to delete the backup words?":"Are you sure you want to delete the backup words?","Are you sure you want to delete this wallet?":"Tem certeza que deseja excluir esta carteira?","Available Balance":"Saldo Disponível","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Average confirmation time: {{fee.nbBlocks * 10}} minutes","Back":"Voltar","Backup":"Backup","Backup now":"Backup agora","Backup words deleted":"Backup words deleted","Bad wallet invitation":"Bad wallet invitation","Balance By Address":"Balance By Address","Before receiving funds, it is highly recommended you backup your wallet keys.":"Antes de receber fundos, é altamente recomendável que você faça backup de suas chaves de carteira.","Bitcoin address":"Endereço Bitcoin","Bitcoin Network Fee Policy":"Bitcoin Network Fee Policy","Bitcoin transactions may include a fee collected by miners on the network. The higher the fee, the greater the incentive a miner has to include that transaction in a block. Actual fees are determined base
gettextCatalog.setStrings('ru', {"(possible double spend)":"(возможна двойная трата)","(Trusted)":"(Доверенный)","[Balance Hidden]":"[Баланс скрыт]","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} будет использовано для оплаты комиссии","{{feeRateStr}} of the transaction":"{{feeRateStr}} транзакции","{{index.m}}-of-{{index.n}}":"{{index.m}}-из-{{index.n}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} больше","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} транзакций скачено","{{item.m}}-of-{{item.n}}":"{{item.m}}-из-{{item.n}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Предложенный платёж может быть удалён если 1) вы создали этот платёж и никто его еще не подписал, или если 2) прошло более 24 часов с момента его создания.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>ЕСЛИ ВЫ ПОТЕРЯЕТЕ ДОСТУП К ВАШЕМУ КОШЕЛЬКУ COPAY ИЛИ ВАШИМ ЗАШИФРОВАННЫМ ЗАКРЫТЫМ КЛЮЧАМ, ПРИ ТОМ ЧТО У ВАС НЕТ ОТДЕЛЬНОЙ РЕЗЕРВНОЙ КОПИИ ВАШЕГО КОШЕЛЬКА И СООТВЕТСТВУЮЩЕМУ ЕМУ ПАРОЛЯ, ВЫ ПРИЗНАЁТЕ И СОГЛАШАЕТЕСЬ С ТЕМ ЧТО ВСЕ БИТКОЙНЫ АССОЦИИРОВАННЫЕ С ЭТИМ КОШЕЛЬКОМ СТАНУТ НЕДОСТУПНЫ.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>ИЛИ</b> один экспортный файл и кворум остальных ключевых словосочетаний (например в кошельке 3-5: экспортный файл и два ключевых словосочетания двух любых совладельцев).","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>ИЛИ</b> ключевое словосочетание <b>всех</b> совладельцев кошелька","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>ИЛИ</b> ключевые словосочетания <b>всех</b> совладельцев кошелька","A multisignature bitcoin wallet":"Биткойн-кошелёк с мультиподписью","About Copay":"О Copay","Accept":"Принять","Account":"Аккаунт","Account Number":"Номер аккаунта","Activity":"Активность","Add a new entry":"Добавить новую запись","Add a Password":"Защитить паролем","Add an optional password to secure the recovery phrase":"Добавьте необязательный пароль для защиты ключевого словосочетания","Add comment":"Добавить комментарий","Add wallet":"Добавить кошелёк","Address":"Адрес","Address Type":"Тип адреса","Advanced":"Дополнительные возможности","Alias":"Псевдоним","Alias for <i>{{index.walletName}}</i>":"Псевдоним для <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Любой вклад в перевод Copay приветствуются. Регистрируйтесь на crowdin.com и присоединяйтесь к
gettextCatalog.setStrings('sq', {"(possible double spend)":"(possible double spend)","(Trusted)":"(Trusted)","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} will be deducted for bitcoin networking fees","{{index.m}}-of-{{index.n}}":"{{index.m}}-of-{{index.n}}","{{item.m}}-of-{{item.n}}":"{{item.m}}-of-{{item.n}}","{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance":"{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).":"<b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).","<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seed of <b>all</b> copayers in the wallet","<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet":"<b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet","A multisignature bitcoin wallet":"A multisignature bitcoin wallet","About Copay":"Rreth Copay","Accept":"Prano","Add a Seed Passphrase":"Add a Seed Passphrase","Add an optional passphrase to secure the seed":"Add an optional passphrase to secure the seed","Add wallet":"Shto kuletë","Address":"Adresa","Address Type":"Address Type","Advanced":"Avancuar","Advanced Send":"Dërgim i avancuar","Agree":"Pranoj","Alias for <i>{{index.walletName}}</i>":"Nofka për <i>{{index.walletName}}</i>","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at","All transaction requests are irreversible.":"All transaction requests are irreversible.","Already have a wallet?":"Tashmë keni një kuletë?","Alternative Currency":"Monedhë alternative","Amount":"Shuma","Amount below dust threshold":"Amount below dust threshold","Amount in":"Shuma në","Applying changes":"Duke aplikuar ndryshimet","Are you sure you want to delete the backup words?":"Are you sure you want to delete the backup words?","Are you sure you want to delete this wallet?":"Jeni i sigurtë që doni të fshini këtë kuletë?","Available Balance":"Shuma në dispozicion","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Koha mesatare e konfirmimit: {{fee.nbBlocks * 10}} minuta","Back":"Prapa","Backup":"Kopje rezervë","Backup now":"Krijo kopjen rezervë tani","Backup words deleted":"Backup words deleted","Bad wallet invitation":"Bad wallet invitation","Balance By Address":"Balance By Address","Before receiving funds, it is highly recommended you backup your wallet keys.":"Before receiving funds, it is highly recommended you backup your wallet keys.","Bitcoin address":"Bitcoin adresa","Bitcoin Network Fee Policy":"Bitcoin Network Fee Policy","Bitcoin transactions may include a fee collected by miners on the network. The higher the fee, the greater the incentive a miner has to include that transaction in a block. Actual fees are determined based on network load and the sel
gettextCatalog.setStrings('tr', {"(possible double spend)":"(olası çift harcama)","(Trusted)":"(Güvenilir)","{{fee}} will be deducted for bitcoin networking fees":"{{fee}} bitcoin ağ ücreti olarak düşülecektir","{{index.m}}-of-{{index.n}}":"{{index.m}} te {{index.n}}","{{item.m}}-of-{{item.n}}":"{{item.n}} te {{item.m}}","{{len}} wallets imported. Funds scanning in progress. Hold on to see updated balance":"{{len}} cüzdanı içe aktarıldı. Fonlar taranırken ve güncellenirken bekleyiniz","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* Ödeme isteği; 1) kaynağı sizseniz, başka bir kullanıcı tarafından imzalanmamışsa veya 2) üzerinden 24 saat geçmişse silinebilir.","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>COPAY CÜZDANINIZA ERİŞİMİNİZİ KAYBEDER VEYA GEREKLİ ŞİFRESİ İLE BİRLİKTE CÜZDANINIZI ÖZEL ANAHTARLA BİRLİKTE YEDEKLEMEZSENİZ, COPAY CÜZDANINIZDAKİ BİTCOİN'LERE ERİŞİMİNİZİN KALMAYACAĞINI KABUL EDER VE ONAYLIYORSUNUZ DEMEKTİR.</b>","A multisignature bitcoin wallet":"Çoklu imzalı bitcoin cüzdanı","About Copay":"Copay Hakkında","Accept":"Onay","Add an optional passphrase to secure the seed":"Kurtarma sözcüklerinin güvenliği için opsiyonel parola ekle","Add wallet":"Cüzdan ekle","Address":"Adres","Address Type":"Adres Türü","Advanced":"Gelişmiş","Advanced Send":"Gelişmiş Gönderme","Agree":"Kabul","Alias for <i>{{index.walletName}}</i>":"<i>{{index.walletName}}</i> için takma ad","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"Copay'nın çevirisi için tüm katkılarınızı bekliyoruz. Crowdin.com sitesine kayıt olun ve Copay Projesi'ne katılın","All transaction requests are irreversible.":"Işlem isteklerinin hiç biri geri alınamaz.","Already have a wallet?":"Zaten bir cüzdanınız var mı?","Alternative Currency":"Alternatif Para Birimi","Amount":"Tutar","Amount below dust threshold":"Kabul edilenden düşük miktar","Amount in":"Tutar","Are you sure you want to delete the backup words?":"Yedekleme kelimelerini silmek istediğinizden emin misiniz?","Are you sure you want to delete this wallet?":"Bu cüzdanı silmek istediğinizden emin misiniz?","Available Balance":"Kullanılabilir Bakiye","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"Ortalama onay süresi: {{fee.nbBlocks * 10}} dakika","Back":"Geri","Backup":"Yedekleme","Backup now":"Şimdi yedekle","Backup words deleted":"Yedekleme kelimeleri silindi","Bad wallet invitation":"Geçersiz cüzdan daveti","Balance By Address":"Adrese göre Bakiye","Before receiving funds, it is highly recommended you backup your wallet keys.":"Para almadan önce cüzdanınızı mutlaka yedeklemeniz önerilir.","Bitcoin address":"Bitcoin adresi","Bitcoin Network Fee Policy":"Bitcoin ağ ücret politikası","Bitcoin transactions may include a fee collected by miners on the network. The higher the fee, the greater the incentive a miner has to include that transaction in a block. Actual fees are determined based on network load and the selected policy.":"Bitcoin işlemlerine ağda madenciler tarafından toplanan ücret dahildir. Daha yüksek ücret, madenciler için işleminizi bloklarına eklemek için daha teşvik edicidir. Gerçek ücretler ağ yüküne ve seçili ilkeye göre belirlenir.","Bitcoin URI is NOT valid!":"Bitcoin URI geçerli değil!","Broadcast Payment":"Ödemeyi Yayınla","Broadcasting Payment":"Ödeme Yayınlanıyor","Broadcasting transaction":"İşlem yayınlanıyor","Browser unsupported":"Desteklenmeyen tarayıcı","Cancel":"İptal","CANCEL":"İPTAL","Cannot join the same wallet more that once":"Aynı cüzdana birden fazla kez girilemez","Certified by":"Tarafından sert
gettextCatalog.setStrings('zh', {"(possible double spend)":"(重复支付)","(Trusted)":"(可信的)","[Balance Hidden]":"[隐藏余额]","{{fee}} will be deducted for bitcoin networking fees":"扣除比特币网络费 {{fee}}","{{feeRateStr}} of the transaction":"交易的{{feeRateStr}}","{{index.m}}-of-{{index.n}}":"{{index.n}} 分之 {{index.m}}","{{index.result.length - index.txHistorySearchResults.length}} more":"{{index.result.length - index.txHistorySearchResults.length}} 更多","{{index.txProgress}} transactions downloaded":"{{index.txProgress}} 条交易已下载","{{item.m}}-of-{{item.n}}":"{{item.n}} 分之 {{item.m}}","* A payment proposal can be deleted if 1) you are the creator, and no other copayer has signed, or 2) 24 hours have passed since the proposal was created.":"* 如果 1) 你是创造者,及没有其他 copayer 签名,或 2) 24 小时已经过去,支付提议将被删除。","<b>IF YOU LOSE ACCESS TO YOUR COPAY WALLET OR YOUR ENCRYPTED PRIVATE KEYS AND YOU HAVE NOT SEPARATELY STORED A BACKUP OF YOUR WALLET AND CORRESPONDING PASSWORD, YOU ACKNOWLEDGE AND AGREE THAT ANY BITCOIN YOU HAVE ASSOCIATED WITH THAT COPAY WALLET WILL BECOME INACCESSIBLE.</b>":"<b>如果你无法访问你的 COPAY 钱包或加密私钥,及你没有分开储存钱包备份和相应密码,你承认并同意有关 COPAY 钱包里的任何比特币将不可被存取。</b>","<b>OR</b> 1 wallet export file and the remaining quorum of wallet recovery phrases (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet recovery phrases of any of the other copayers).":"<b>或</b> 1 钱包导出文件和钱包恢复短语的剩余法定人数 (例如在 3-5 钱包1 钱包导出文件 + 任何其他 copayers 的 2 钱包恢复短语)。","<b>OR</b> the wallet recovery phrase of <b>all</b> copayers in the wallet":"<b>或</b> 钱包里的 <b>所有</b> copayers 的钱包恢复短语","<b>OR</b> the wallet recovery phrases of <b>all</b> copayers in the wallet":"<b>或</b> 钱包里的 <b>所有</b> copayers 的钱包恢复短语","A multisignature bitcoin wallet":"多重签名比特币钱包","About Copay":"Copay 简介","Accept":"同意","Account":"帐户","Account Number":"帐号","Activity":"活动","Add a new entry":"添加新条目","Add a Password":"添加密码","Add an optional password to secure the recovery phrase":"添加可选的密码,以保护恢复短语","Add comment":"添加评论","Add wallet":"添加钱包","Address":"地址","Address Type":"地址类型","Advanced":"進階","Alias":"别名","Alias for <i>{{index.walletName}}</i>":"<i>{{index.walletName}}</i>别名","All contributions to Copay's translation are welcome. Sign up at crowdin.com and join the Copay project at":"欢迎大家为 Copay 提供翻译,注册 crowdin.com 并加入 Copay 项目","All transaction requests are irreversible.":"所有交易请求均不可逆。","Alternative Currency":"替代货币","Amount":"数额","Amount below minimum allowed":"数额低于最低允许值","Amount in":"已转换的数额","Are you sure you want to delete the recovery phrase?":"你确定要删除恢复短语吗?","Are you sure you want to delete this wallet?":"确定要删除这钱包?","Auditable":"可审核","Available Balance":"可用余额","Average confirmation time: {{fee.nbBlocks * 10}} minutes":"平均确认时间: {{fee.nbBlocks * 10}} 分钟","Back":"返回","Backup":"备份","Backup failed":"备份失败","Backup Needed":"需要备份","Backup now":"现在备份","Bad wallet invitation":"坏钱包邀请","Balance By Address":"地址余额","Before receiving funds, you must backup your wallet. If this device is lost, it is impossible to access your funds without a backup.":"接收资金前, 务必备份你的钱包。如果你遗失此设备,就无法在没有备份的情况下找回资金。","BETA: Android Key Derivation Test:":"BETA: Android 密钥衍生测试︰","BIP32 path for address derivation":"BIP32 路径的地址衍生","Bitcoin address":"比特币地址","Bitcoin Network Fee Policy":"比特币网络手续费策略
/* jshint +W100 */
}]);
'use strict';
angular.element(document).ready(function() {
// Run copayApp after device is ready.
var startAngular = function() {
angular.bootstrap(document, ['copayApp']);
};
var handleBitcoinURI = function(url) {
if (!url) return;
if (url.indexOf('glidera') != -1) {
url = '#/uri-glidera' + url.replace('bitcoin://glidera', '');
}
else {
url = '#/uri-payment/' + url;
}
setTimeout(function() {
window.location = url;
}, 1000);
};
/* Cordova specific Init */
if (window.cordova !== undefined) {
document.addEventListener('deviceready', function() {
document.addEventListener('pause', function() {
if (!window.ignoreMobilePause) {
window.location = '#/';
}
}, false);
document.addEventListener('resume', function() {
if (!window.ignoreMobilePause) {
window.location = '#/cordova/resume';
}
setTimeout(function() {
window.ignoreMobilePause = false;
}, 100);
}, false);
// Back button event
document.addEventListener('backbutton', function() {
var loc = window.location;
var isHome = loc.toString().match(/index\.html#\/$/) ? 'true' : '';
if (!window.ignoreMobilePause) {
window.location = '#/cordova/backbutton/'+isHome;
}
setTimeout(function() {
window.ignoreMobilePause = false;
}, 100);
}, false);
document.addEventListener('menubutton', function() {
window.location = '#/preferences';
}, false);
setTimeout(function() {
navigator.splashscreen.hide();
}, 2000);
window.plugins.webintent.getUri(handleBitcoinURI);
window.plugins.webintent.onNewIntent(handleBitcoinURI);
window.handleOpenURL = handleBitcoinURI;
startAngular();
}, false);
} else {
try {
window.handleOpenURL = handleBitcoinURI;
window.plugins.webintent.getUri(handleBitcoinURI);
window.plugins.webintent.onNewIntent(handleBitcoinURI);
} catch (e) {}
startAngular();
}
});