This commit is contained in:
Yemel Jardi 2014-09-23 12:12:15 -03:00
commit 4b7c138877
86 changed files with 3511 additions and 2141 deletions

4
.gitignore vendored
View file

@ -77,3 +77,7 @@ dist/windows
dist/*.dmg dist/*.dmg
dist/*.tar.gz dist/*.tar.gz
dist/*.exe dist/*.exe
doc/
/node_modules
/*-cov

View file

@ -9,14 +9,37 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-angular-gettext'); grunt.loadNpmTasks('grunt-angular-gettext');
grunt.loadNpmTasks('grunt-jsdoc');
grunt.loadNpmTasks('grunt-release');
// Project Configuration // Project Configuration
grunt.initConfig({ grunt.initConfig({
release: {
options: {
bump: true,
file: 'package.json',
add: true,
commit: true,
tag: true,
push: true,
pushTags: true,
npm: false,
npmtag: true,
tagName: 'v<%= version %>',
commitMessage: 'New release v<%= version %>',
tagMessage: 'Version <%= version %>',
github: {
repo: 'bitpay/copay',
usernameVar: 'GITHUB_USERNAME', //ENVIRONMENT VARIABLE that contains Github username
passwordVar: 'GITHUB_PASSWORD' //ENVIRONMENT VARIABLE that contains Github password
}
}
},
shell: { shell: {
prod: { prod: {
options: { options: {
stdout: false, stdout: false,
stderr: false stderr: false
}, },
command: 'node ./util/build.js' command: 'node ./util/build.js'
}, },
@ -41,7 +64,9 @@ module.exports = function(grunt) {
}, },
scripts: { scripts: {
files: [ files: [
'js/models/**/*.js' 'js/models/**/*.js',
'js/models/*.js',
'plugins/*.js',
], ],
tasks: ['shell:dev'] tasks: ['shell:dev']
}, },
@ -51,14 +76,20 @@ module.exports = function(grunt) {
}, },
main: { main: {
files: [ files: [
'js/app.js', 'js/init.js',
'js/directives.js', 'js/app.js',
'js/filters.js', 'js/directives.js',
'js/routes.js', 'js/filters.js',
'js/services/*.js', 'js/routes.js',
'js/mobile.js',
'js/services/*.js',
'js/controllers/*.js' 'js/controllers/*.js'
], ],
tasks: ['concat:main'] tasks: ['concat:main']
},
config: {
files: ['config.js'],
tasks: ['shell:dev', 'concat:main']
} }
}, },
mochaTest: { mochaTest: {
@ -107,21 +138,23 @@ module.exports = function(grunt) {
'lib/ng-idle/angular-idle.min.js', 'lib/ng-idle/angular-idle.min.js',
'lib/angular-foundation/mm-foundation.min.js', 'lib/angular-foundation/mm-foundation.min.js',
'lib/angular-foundation/mm-foundation-tpls.min.js', 'lib/angular-foundation/mm-foundation-tpls.min.js',
'lib/angular-gettext/dist/angular-gettext.min.js' 'lib/angular-gettext/dist/angular-gettext.min.js',
'lib/angular-load/angular-load.min.js'
// If you add libs here, remember to add it too to karma.conf
], ],
dest: 'lib/angularjs-all.js' dest: 'lib/angularjs-all.js'
}, },
main: { main: {
src: [ src: [
'js/app.js', 'js/app.js',
'js/directives.js', 'js/directives.js',
'js/filters.js', 'js/filters.js',
'js/routes.js', 'js/routes.js',
'js/services/*.js', 'js/services/*.js',
'js/controllers/*.js', 'js/controllers/*.js',
'js/translations.js', 'js/translations.js',
'js/mobile.js', // PLACEHOLDER: CORDOVA SRIPT 'js/mobile.js', // PLACEHOLDER: CORDOVA SRIPT
'js/init.js' 'js/init.js',
], ],
dest: 'js/copayMain.js' dest: 'js/copayMain.js'
} }
@ -166,11 +199,23 @@ module.exports = function(grunt) {
'js/translations.js': ['po/*.po'] 'js/translations.js': ['po/*.po']
} }
}, },
},
jsdoc: {
dist : {
src: ['js/models/core/*.js', 'js/models/*.js', 'plugins/*.js'],
options: {
destination: 'doc',
configure: 'jsdoc.conf.json',
template: './node_modules/grunt-jsdoc/node_modules/ink-docstrap/template',
theme: 'flatly'
}
}
} }
}); });
grunt.registerTask('default', ['shell:dev', 'nggettext_compile', 'concat', 'cssmin']); grunt.registerTask('default', ['shell:dev', 'nggettext_compile', 'concat', 'cssmin']);
grunt.registerTask('prod', ['shell:prod', 'nggettext_compile', 'concat', 'cssmin', 'uglify']); grunt.registerTask('prod', ['shell:prod', 'nggettext_compile', 'concat', 'cssmin', 'uglify']);
grunt.registerTask('translate', ['nggettext_extract']); grunt.registerTask('translate', ['nggettext_extract']);
grunt.registerTask('docs', ['jsdoc']);
}; };

View file

@ -24,7 +24,8 @@
"zeroclipboard": "~1.3.5", "zeroclipboard": "~1.3.5",
"ng-idle": "*", "ng-idle": "*",
"underscore": "~1.7.0", "underscore": "~1.7.0",
"inherits": "~0.0.1" "inherits": "~0.0.1",
"angular-load": "0.2.0"
}, },
"resolutions": { "resolutions": {
"angular": "=1.2.19" "angular": "=1.2.19"

View file

@ -2,15 +2,9 @@
var defaultConfig = { var defaultConfig = {
defaultLanguage: 'en', defaultLanguage: 'en',
// DEFAULT network (livenet or testnet) // DEFAULT network (livenet or testnet)
networkName: 'testnet', networkName: 'livenet',
forceNetwork: false,
logLevel: 'info', logLevel: 'info',
// DEFAULT unit: Bit
unitName: 'bits',
unitToSatoshi: 100,
alternativeName: 'US Dollar',
alternativeIsoCode: 'USD',
// wallet limits // wallet limits
limits: { limits: {
@ -20,9 +14,12 @@ var defaultConfig = {
// network layer config // network layer config
network: { network: {
host: 'test-insight.bitpay.com', testnet: {
port: 443, url: 'https://test-insight.bitpay.com:443'
schema: 'https' },
livenet: {
url: 'https://insight.bitpay.com:443'
},
}, },
// wallet default config // wallet default config
@ -30,25 +27,15 @@ var defaultConfig = {
requiredCopayers: 2, requiredCopayers: 2,
totalCopayers: 3, totalCopayers: 3,
spendUnconfirmed: true, spendUnconfirmed: true,
verbose: 1,
// will duplicate itself after each try
reconnectDelay: 5000, reconnectDelay: 5000,
idleDurationMin: 4 idleDurationMin: 4,
}, settings: {
unitName: 'bits',
// blockchain service API config unitToSatoshi: 100,
blockchain: { unitDecimals: 2,
schema: 'https', alternativeName: 'US Dollar',
host: 'test-insight.bitpay.com', alternativeIsoCode: 'USD',
port: 443, }
retryDelay: 1000,
},
// socket service API config
socket: {
schema: 'https',
host: 'test-insight.bitpay.com',
port: 443,
reconnectDelay: 1000,
}, },
// local encryption/security config // local encryption/security config
@ -63,6 +50,27 @@ var defaultConfig = {
}, },
verbose: 1, verbose: 1,
plugins: {
LocalStorage: true,
//GoogleDrive: true,
},
GoogleDrive: {
home: 'copay',
/*
* This clientId was generated at:
* https://console.developers.google.com/project
* To run Copay with Google Drive at your domain you need
* to generata your own Id.
*/
// for localhost:3001 you can use you can:
clientId: '232630733383-a35gcnovnkgka94394i88gq60vtjb4af.apps.googleusercontent.com',
// for copay.io:
// clientId: '1036948132229-biqm3b8sirik9lt5rtvjo9kjjpotn4ac.apps.googleusercontent.com',
},
}; };
if (typeof module !== 'undefined') if (typeof module !== 'undefined')
module.exports = defaultConfig; module.exports = defaultConfig;

View file

@ -11,11 +11,12 @@ module.exports.HDParams = require('./js/models/core/HDParams');
// components // components
var Async = module.exports.Async = require('./js/models/network/Async'); var Async = module.exports.Async = require('./js/models/network/Async');
var Insight = module.exports.Insight = require('./js/models/blockchain/Insight'); var Insight = module.exports.Insight = require('./js/models/blockchain/Insight');
var StorageLocalEncrypted = module.exports.StorageLocalEncrypted = require('./js/models/storage/LocalEncrypted'); var Storage = module.exports.Storage = require('./js/models/Storage');
module.exports.WalletFactory = require('./js/models/core/WalletFactory'); module.exports.WalletFactory = require('./js/models/core/WalletFactory');
module.exports.Wallet = require('./js/models/core/Wallet'); module.exports.Wallet = require('./js/models/core/Wallet');
module.exports.WalletLock = require('./js/models/core/WalletLock'); module.exports.WalletLock = require('./js/models/core/WalletLock');
module.exports.PluginManager = require('./js/models/core/PluginManager');
module.exports.version = require('./version').version; module.exports.version = require('./version').version;
module.exports.commitHash = require('./version').commitHash; module.exports.commitHash = require('./version').commitHash;

View file

@ -280,7 +280,6 @@ a:hover {
.last-transactions-header { .last-transactions-header {
padding: 1rem 0; padding: 1rem 0;
height: 50px;
} }
.last-transactions-footer { .last-transactions-footer {
@ -385,6 +384,7 @@ a:hover {
.size-48 { font-size: 48px; } .size-48 { font-size: 48px; }
.size-60 { font-size: 60px; } .size-60 { font-size: 60px; }
.size-72 { font-size: 72px; } .size-72 { font-size: 72px; }
.m3r {margin-right: 3px;}
.m5t {margin-top: 5px;} .m5t {margin-top: 5px;}
.m10t {margin-top: 10px;} .m10t {margin-top: 10px;}
.m5b {margin-bottom: 5px;} .m5b {margin-bottom: 5px;}
@ -725,6 +725,14 @@ button.radius, .button.radius {
} }
/* SECONDARY */ /* SECONDARY */
input[type='submit']
{
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
button.secondary, button.secondary,
.button.secondary { .button.secondary {
background-color: #4A90E2; background-color: #4A90E2;
@ -758,6 +766,7 @@ button.primary,
.button.primary { .button.primary {
background-color: #1ABC9C; background-color: #1ABC9C;
color: #fff; color: #fff;
border-radius: 0;
} }
button.primary:hover, button.primary:hover,
button.primary:focus, button.primary:focus,
@ -1046,7 +1055,7 @@ a.text-warning:hover {color: #FD7262;}
background: #2C3E50; background: #2C3E50;
-moz-box-shadow: 0px 0px 0px 0px rgba(255,255,255,0.09), inset 0px 0px 2px 0px rgba(0,0,0,0.20); -moz-box-shadow: 0px 0px 0px 0px rgba(255,255,255,0.09), inset 0px 0px 2px 0px rgba(0,0,0,0.20);
box-shadow: 0px 0px 0px 0px rgba(255,255,255,0.09), inset 0px 0px 2px 0px rgba(0,0,0,0.20); box-shadow: 0px 0px 0px 0px rgba(255,255,255,0.09), inset 0px 0px 2px 0px rgba(0,0,0,0.20);
margin-bottom: 5px; margin-bottom: 15px;
} }
.box-setup-copay-required { .box-setup-copay-required {

View file

@ -122,13 +122,13 @@
display: block; display: block;
float: none; float: none;
margin: 0 auto; margin: 0 auto;
width: 210px; width: 160px;
height: 210px; height: 160px;
} }
.panel qrcode canvas { .panel qrcode canvas {
width: 200px; width: 150px;
height: 200px; height: 150px;
} }
.addresses .panel { .addresses .panel {

View file

@ -17,18 +17,21 @@
<i class="fi-loop icon-rotate m15r"></i> <i class="fi-loop icon-rotate m15r"></i>
<span translate> Network Error. Attempting to reconnect...</span> <span translate> Network Error. Attempting to reconnect...</span>
</span> </span>
<nav class="tab-bar" ng-class="{'hide-tab-bar' : !$root.wallet || <nav class="tab-bar" ng-if="$root.wallet &&
!$root.wallet.isReady() || $root.wallet.isLocked}"> $root.wallet.isReady() && !$root.wallet.isLocked">
<section class="left-small"> <section class="left-small">
<a class="left-off-canvas-toggle menu-icon" ><span></span></a> <a class="left-off-canvas-toggle menu-icon" ><span></span></a>
</section> </section>
<section ng-controller="SidebarController" class="right-small text-center">
<a href="#" ng-click="signout()"><i class="size-24 fi-power"></i></a>
</section>
<section class="middle tab-bar-section"> <section class="middle tab-bar-section">
<h1 class="right"> <h1 class="right">
{{totalBalance || 0 |noFractionNumber}} {{$root.unitName}} <span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span>
<span class="size-14" ng-if="!$root.updatingBalance">
{{totalBalance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}}
</span>
</h1> </h1>
<h1 class="title ellipsis"> <h1 class="title ellipsis">
{{$root.wallet.getName()}} {{$root.wallet.getName()}}

View file

@ -10,15 +10,16 @@ if (localConfig) {
var lmv = localConfig.version ? localConfig.version.split('.')[1] : '-1'; var lmv = localConfig.version ? localConfig.version.split('.')[1] : '-1';
if (cmv === lmv) { if (cmv === lmv) {
_.each(localConfig, function(value, key) { _.each(localConfig, function(value, key) {
if (key === 'networkName' && config['forceNetwork']) {
return;
}
config[key] = value; config[key] = value;
}); });
} }
} }
var copayApp = window.copayApp = angular.module('copayApp', [ var log = function() {
if (config.verbose) console.log(arguments);
}
var modules = [
'ngRoute', 'ngRoute',
'angularMoment', 'angularMoment',
'mm.foundation', 'mm.foundation',
@ -29,7 +30,13 @@ var copayApp = window.copayApp = angular.module('copayApp', [
'copayApp.services', 'copayApp.services',
'copayApp.controllers', 'copayApp.controllers',
'copayApp.directives', 'copayApp.directives',
]); ];
if (Object.keys(config.plugins).length)
modules.push('angularLoad');
var copayApp = window.copayApp = angular.module('copayApp', modules);
copayApp.config(function($sceDelegateProvider) { copayApp.config(function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([ $sceDelegateProvider.resourceUrlWhitelist([

View file

@ -5,6 +5,12 @@ angular.module('copayApp.controllers').controller('CopayersController',
$scope.hideAdv = true; $scope.hideAdv = true;
$scope.skipBackup = function() {
var w = $rootScope.wallet;
w.setBackupReady(true);
};
$scope.backup = function() { $scope.backup = function() {
var w = $rootScope.wallet; var w = $rootScope.wallet;
w.setBackupReady(); w.setBackupReady();

View file

@ -41,6 +41,7 @@ angular.module('copayApp.controllers').controller('CreateController',
$scope.walletPassword = $rootScope.walletPassword; $scope.walletPassword = $rootScope.walletPassword;
$scope.isMobile = !!window.cordova; $scope.isMobile = !!window.cordova;
$scope.hideAdv = true; $scope.hideAdv = true;
$scope.networkName = config.networkName;
// ng-repeat defined number of times instead of repeating over array? // ng-repeat defined number of times instead of repeating over array?
$scope.getNumber = function(num) { $scope.getNumber = function(num) {
@ -83,9 +84,11 @@ angular.module('copayApp.controllers').controller('CreateController',
nickname: $scope.myNickname, nickname: $scope.myNickname,
passphrase: passphrase, passphrase: passphrase,
privateKeyHex: $scope.private, privateKeyHex: $scope.private,
networkName: $scope.networkName,
}; };
var w = walletFactory.create(opts); walletFactory.create(opts, function(err, w) {
controllerUtils.startNetwork(w, $scope); controllerUtils.startNetwork(w, $scope);
});
}); });
}; };

View file

@ -1,10 +1,12 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('HomeController', angular.module('copayApp.controllers').controller('HomeController', function($scope, $rootScope, $location, walletFactory, notification, controllerUtils) {
function($scope, $rootScope, $location, walletFactory, notification, controllerUtils) {
controllerUtils.redirIfLogged();
$scope.loading = false; controllerUtils.redirIfLogged();
$scope.hasWallets = (walletFactory.getWallets() && walletFactory.getWallets().length > 0) ? true : false;
$scope.retreiving = true;
walletFactory.getWallets(function(err,ret) {
$scope.retreiving = false;
$scope.hasWallets = (ret && ret.length > 0) ? true : false;
}); });
});

View file

@ -2,12 +2,11 @@
angular.module('copayApp.controllers').controller('ImportController', angular.module('copayApp.controllers').controller('ImportController',
function($scope, $rootScope, $location, walletFactory, controllerUtils, Passphrase, notification) { function($scope, $rootScope, $location, walletFactory, controllerUtils, Passphrase, notification) {
controllerUtils.redirIfLogged(); controllerUtils.redirIfLogged();
$scope.title = 'Import a backup'; $scope.title = 'Import a backup';
$scope.importStatus = 'Importing wallet - Reading backup...'; $scope.importStatus = 'Importing wallet - Reading backup...';
$scope.hideAdv=true; $scope.hideAdv = true;
var reader = new FileReader(); var reader = new FileReader();
@ -59,7 +58,7 @@ angular.module('copayApp.controllers').controller('ImportController',
$rootScope.wallet = w; $rootScope.wallet = w;
controllerUtils.startNetwork($rootScope.wallet, $scope); controllerUtils.startNetwork($rootScope.wallet, $scope);
}); });
}); });
}; };
@ -98,13 +97,13 @@ angular.module('copayApp.controllers').controller('ImportController',
if (!backupFile) { if (!backupFile) {
$scope.loading = false; $scope.loading = false;
notification.error('Error', 'Please, select your backup file or paste the file contents'); notification.error('Error', 'Please, select your backup file');
$scope.loading = false; $scope.loading = false;
return; return;
} }
if (backupFile) { if (backupFile) {
reader.readAsBinaryString(backupFile); reader.readAsBinaryString(backupFile);
} }
}; };
}); });

View file

@ -7,7 +7,7 @@ angular.module('copayApp.controllers').controller('JoinController',
$scope.loading = false; $scope.loading = false;
$scope.isMobile = !!window.cordova; $scope.isMobile = !!window.cordova;
// QR code Scanner // QR code Scanner
var cameraInput; var cameraInput;
var video; var video;
var canvas; var canvas;
@ -15,14 +15,13 @@ angular.module('copayApp.controllers').controller('JoinController',
var context; var context;
var localMediaStream; var localMediaStream;
$scope.hideAdv=true; $scope.hideAdv = true;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
if (!window.cordova && !navigator.getUserMedia) if (!window.cordova && !navigator.getUserMedia)
$scope.disableScanner =1; $scope.disableScanner = 1;
var _scan = function(evt) { var _scan = function(evt) {
if (localMediaStream) { if (localMediaStream) {
@ -118,10 +117,14 @@ angular.module('copayApp.controllers').controller('JoinController',
} }
$scope.loading = true; $scope.loading = true;
walletFactory.network.on('badSecret', function() {});
Passphrase.getBase64Async($scope.joinPassword, function(passphrase) { Passphrase.getBase64Async($scope.joinPassword, function(passphrase) {
walletFactory.joinCreateSession($scope.connectionId, $scope.nickname, passphrase, $scope.private, function(err, w) { walletFactory.joinCreateSession({
secret: $scope.connectionId,
nickname: $scope.nickname,
passphrase: passphrase,
privateHex: $scope.private,
}, function(err, w) {
$scope.loading = false; $scope.loading = false;
if (err || !w) { if (err || !w) {
if (err === 'joinError') if (err === 'joinError')
@ -129,9 +132,11 @@ angular.module('copayApp.controllers').controller('JoinController',
else if (err === 'walletFull') else if (err === 'walletFull')
notification.error('The wallet is full'); notification.error('The wallet is full');
else if (err === 'badNetwork') else if (err === 'badNetwork')
notification.error('Network Error', 'The wallet your are trying to join uses a different Bitcoin Network. Check your settings.'); notification.error('Network Error', 'Wallet network configuration missmatch');
else if (err === 'badSecret') else if (err === 'badSecret')
notification.error('Bad secret', 'The secret string you entered is invalid'); notification.error('Bad secret', 'The secret string you entered is invalid');
else if (err === 'connectionError')
notification.error('Networking Error', 'Could not connect to the Insight server. Check your settings and network configuration');
else else
notification.error('Unknown error'); notification.error('Unknown error');
controllerUtils.onErrorDigest(); controllerUtils.onErrorDigest();

View file

@ -1,45 +1,98 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('MoreController', angular.module('copayApp.controllers').controller('MoreController',
function($scope, $rootScope, $location, backupService, walletFactory, controllerUtils, notification) { function($scope, $rootScope, $location, $filter, backupService, walletFactory, controllerUtils, notification, rateService) {
var w = $rootScope.wallet; var w = $rootScope.wallet;
$scope.hideAdv=true; $scope.unitOpts = [{
$scope.hidePriv=true; name: 'Satoshis (100,000,000 satoshis = 1BTC)',
shortName: 'SAT',
value: 1,
decimals: 0
}, {
name: 'bits (1,000,000 bits = 1BTC)',
shortName: 'bits',
value: 100,
decimals: 2
}, {
name: 'mBTC (1,000 mBTC = 1BTC)',
shortName: 'mBTC',
value: 100000,
decimals: 5
}, {
name: 'BTC',
shortName: 'BTC',
value: 100000000,
decimals: 8
}];
$scope.selectedAlternative = {
name: w.settings.alternativeName,
isoCode: w.settings.alternativeIsoCode
};
$scope.alternativeOpts = rateService.isAvailable ?
rateService.listAlternatives() : [$scope.selectedAlternative];
rateService.whenAvailable(function() {
$scope.alternativeOpts = rateService.listAlternatives();
for (var ii in $scope.alternativeOpts) {
if (w.settings.alternativeIsoCode === $scope.alternativeOpts[ii].isoCode) {
$scope.selectedAlternative = $scope.alternativeOpts[ii];
}
}
});
for (var ii in $scope.unitOpts) {
if (w.settings.unitName === $scope.unitOpts[ii].shortName) {
$scope.selectedUnit = $scope.unitOpts[ii];
break;
}
}
$scope.save = function() {
w.changeSettings({
unitName: $scope.selectedUnit.shortName,
unitToSatoshi: $scope.selectedUnit.value,
unitDecimals: $scope.selectedUnit.decimals,
alternativeName: $scope.selectedAlternative.name,
alternativeIsoCode: $scope.selectedAlternative.isoCode,
});
controllerUtils.updateBalance();
};
$scope.hideAdv = true;
$scope.hidePriv = true;
if (w) if (w)
$scope.priv = w.privateKey.toObj().extendedPrivateKeyString; $scope.priv = w.privateKey.toObj().extendedPrivateKeyString;
$scope.downloadBackup = function() { $scope.downloadBackup = function() {
var w = $rootScope.wallet;
backupService.download(w); backupService.download(w);
} }
$scope.deleteWallet = function() { $scope.deleteWallet = function() {
var w = $rootScope.wallet;
walletFactory.delete(w.id, function() { walletFactory.delete(w.id, function() {
controllerUtils.logout(); controllerUtils.logout();
}); });
}; };
$scope.purge = function(deleteAll) { $scope.purge = function(deleteAll) {
var w = $rootScope.wallet;
var removed = w.purgeTxProposals(deleteAll); var removed = w.purgeTxProposals(deleteAll);
if (removed){ if (removed) {
controllerUtils.updateBalance(); controllerUtils.updateBalance();
} }
notification.info('Tx Proposals Purged', removed + ' transaction proposal purged'); notification.info('Transactions Proposals Purged', removed + ' ' + $filter('translate')('transaction proposal purged'));
}; };
$scope.updateIndexes = function() { $scope.updateIndexes = function() {
var w = $rootScope.wallet; notification.info('Scaning for transactions', 'Using derived addresses from your wallet');
notification.info('Scaning for transactions','Using derived addresses from your wallet');
w.updateIndexes(function(err) { w.updateIndexes(function(err) {
notification.info('Scan Ended', 'Updating balance'); notification.info('Scan Ended', 'Updating balance');
if (err) { if (err) {
notification.error('Error', 'Error updating indexes: ' + err); notification.error('Error', $filter('translate')('Error updating indexes: ') + err);
} }
controllerUtils.updateAddressList(); controllerUtils.updateAddressList();
controllerUtils.updateBalance(function(){ controllerUtils.updateBalance(function() {
notification.info('Finished', 'The balance is updated using the derived addresses'); notification.info('Finished', 'The balance is updated using the derived addresses');
w.sendIndexes(); w.sendIndexes();
}); });

View file

@ -14,15 +14,32 @@ angular.module('copayApp.controllers').controller('OpenController', function($sc
}; };
$rootScope.fromSetup = false; $rootScope.fromSetup = false;
$scope.loading = false; $scope.loading = false;
$scope.wallets = walletFactory.getWallets().sort(cmp); $scope.retreiving = true;
$scope.selectedWalletId = walletFactory.storage.getLastOpened() || ($scope.wallets[0] && $scope.wallets[0].id);
walletFactory.getWallets(function(err, wallets) {
if (err || !wallets || !wallets.length) {
$location.path('/');
} else {
$scope.retreiving = false;
$scope.wallets = wallets.sort(cmp);
walletFactory.storage.getLastOpened(function(ret) {
if (ret && _.indexOf(_.pluck($scope.wallets, 'id')) == -1)
ret = null;
$scope.selectedWalletId = ret || ($scope.wallets[0] && $scope.wallets[0].id);
setTimeout(function() {
$rootScope.$digest();
}, 0);
});
}
});
$scope.openPassword = ''; $scope.openPassword = '';
$scope.isMobile = !!window.cordova; $scope.isMobile = !!window.cordova;
if (!$scope.wallets.length){
$location.path('/');
}
$scope.open = function(form) { $scope.open = function(form) {
if (form && form.$invalid) { if (form && form.$invalid) {
notification.error('Error', 'Please enter the required fields'); notification.error('Error', 'Please enter the required fields');
@ -34,19 +51,16 @@ angular.module('copayApp.controllers').controller('OpenController', function($sc
Passphrase.getBase64Async(password, function(passphrase) { Passphrase.getBase64Async(password, function(passphrase) {
var w, errMsg; var w, errMsg;
try { walletFactory.open($scope.selectedWalletId, passphrase, function(err, w) {
w = walletFactory.open($scope.selectedWalletId, passphrase); if (!w) {
} catch (e) { $scope.loading = false;
errMsg = e.message; notification.error('Error', err.errMsg || 'Wrong password');
}; $rootScope.$digest();
if (!w) { } else {
$scope.loading = false; $rootScope.updatingBalance = true;
notification.error('Error', errMsg || 'Wrong password'); controllerUtils.startNetwork(w, $scope);
$rootScope.$digest(); }
return; });
}
$rootScope.updatingBalance = true;
controllerUtils.startNetwork(w, $scope);
}); });
}; };

View file

@ -1,17 +1,22 @@
'use strict'; 'use strict';
var bitcore = require('bitcore'); var bitcore = require('bitcore');
var preconditions = require('preconditions').singleton();
angular.module('copayApp.controllers').controller('SendController', angular.module('copayApp.controllers').controller('SendController',
function($scope, $rootScope, $window, $timeout, $anchorScroll, $modal, isMobile, notification, controllerUtils, rateService) { function($scope, $rootScope, $window, $timeout, $anchorScroll, $modal, isMobile, notification, controllerUtils, rateService) {
var w = $rootScope.wallet;
preconditions.checkState(w);
preconditions.checkState(w.settings.unitToSatoshi);
$scope.title = 'Send'; $scope.title = 'Send';
$scope.loading = false; $scope.loading = false;
var satToUnit = 1 / config.unitToSatoshi; var satToUnit = 1 / w.settings.unitToSatoshi;
$scope.defaultFee = bitcore.TransactionBuilder.FEE_PER_1000B_SAT * satToUnit; $scope.defaultFee = bitcore.TransactionBuilder.FEE_PER_1000B_SAT * satToUnit;
$scope.unitToBtc = config.unitToSatoshi / bitcore.util.COIN; $scope.unitToBtc = w.settings.unitToSatoshi / bitcore.util.COIN;
$scope.unitToSatoshi = config.unitToSatoshi; $scope.unitToSatoshi = w.settings.unitToSatoshi;
$scope.alternativeName = config.alternativeName; $scope.alternativeName = w.settings.alternativeName;
$scope.alternativeIsoCode = config.alternativeIsoCode; $scope.alternativeIsoCode = w.settings.alternativeIsoCode;
$scope.isRateAvailable = false; $scope.isRateAvailable = false;
$scope.rateService = rateService; $scope.rateService = rateService;
@ -36,7 +41,7 @@ angular.module('copayApp.controllers').controller('SendController',
this._alternative = newValue; this._alternative = newValue;
if (typeof(newValue) === 'number' && $scope.isRateAvailable) { if (typeof(newValue) === 'number' && $scope.isRateAvailable) {
this._amount = parseFloat( this._amount = parseFloat(
(rateService.fromFiat(newValue, config.alternativeIsoCode) * satToUnit).toFixed(config.unitDecimals), 10); (rateService.fromFiat(newValue, w.settings.alternativeIsoCode) * satToUnit).toFixed(w.settings.unitDecimals), 10);
} else { } else {
this._amount = 0; this._amount = 0;
} }
@ -53,7 +58,7 @@ angular.module('copayApp.controllers').controller('SendController',
this._amount = newValue; this._amount = newValue;
if (typeof(newValue) === 'number' && $scope.isRateAvailable) { if (typeof(newValue) === 'number' && $scope.isRateAvailable) {
this._alternative = parseFloat( this._alternative = parseFloat(
(rateService.toFiat(newValue * config.unitToSatoshi, config.alternativeIsoCode)).toFixed(2), 10); (rateService.toFiat(newValue * w.settings.unitToSatoshi, w.settings.alternativeIsoCode)).toFixed(2), 10);
} else { } else {
this._alternative = 0; this._alternative = 0;
} }
@ -75,7 +80,6 @@ angular.module('copayApp.controllers').controller('SendController',
} }
$scope.showAddressBook = function() { $scope.showAddressBook = function() {
var w = $rootScope.wallet;
var flag; var flag;
if (w) { if (w) {
for (var k in w.addressBook) { for (var k in w.addressBook) {
@ -91,7 +95,7 @@ angular.module('copayApp.controllers').controller('SendController',
if ($rootScope.pendingPayment) { if ($rootScope.pendingPayment) {
var pp = $rootScope.pendingPayment; var pp = $rootScope.pendingPayment;
$scope.address = pp.address + ''; $scope.address = pp.address + '';
var amount = pp.data.amount / config.unitToSatoshi * 100000000; var amount = pp.data.amount / w.settings.unitToSatoshi * 100000000;
$scope.amount = amount; $scope.amount = amount;
$scope.commentText = pp.data.message; $scope.commentText = pp.data.message;
} }
@ -105,7 +109,7 @@ angular.module('copayApp.controllers').controller('SendController',
$scope.submitForm = function(form) { $scope.submitForm = function(form) {
if (form.$invalid) { if (form.$invalid) {
var message = 'Unable to send transaction proposal.'; var message = 'Unable to send transaction proposal';
notification.error('Error', message); notification.error('Error', message);
return; return;
} }
@ -113,11 +117,9 @@ angular.module('copayApp.controllers').controller('SendController',
$scope.loading = true; $scope.loading = true;
var address = form.address.$modelValue; var address = form.address.$modelValue;
var amount = parseInt((form.amount.$modelValue * config.unitToSatoshi).toFixed(0)); var amount = parseInt((form.amount.$modelValue * w.settings.unitToSatoshi).toFixed(0));
var commentText = form.comment.$modelValue; var commentText = form.comment.$modelValue;
var w = $rootScope.wallet;
function done(err, ntxid, merchantData) { function done(err, ntxid, merchantData) {
if (err) { if (err) {
var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created'; var message = 'The transaction' + (w.isShared() ? ' proposal' : '') + ' could not be created';
@ -150,7 +152,7 @@ angular.module('copayApp.controllers').controller('SendController',
message += ' Message from server: ' + merchantData.ack.memo; message += ' Message from server: ' + merchantData.ack.memo;
message += ' For merchant: ' + merchantData.pr.pd.payment_url; message += ' For merchant: ' + merchantData.pr.pd.payment_url;
} }
notification.success('Success!', message); notification.success('Success', message);
$scope.loadTxs(); $scope.loadTxs();
} else { } else {
w.sendTx(ntxid, function(txid, merchantData) { w.sendTx(ntxid, function(txid, merchantData) {
@ -163,9 +165,9 @@ angular.module('copayApp.controllers').controller('SendController',
message += ' Message from server: ' + merchantData.ack.memo; message += ' Message from server: ' + merchantData.ack.memo;
message += ' For merchant: ' + merchantData.pr.pd.payment_url; message += ' For merchant: ' + merchantData.pr.pd.payment_url;
} }
notification.success('Transaction broadcast', message); notification.success('Transaction broadcasted', message);
} else { } else {
notification.error('Error', 'There was an error sending the transaction.'); notification.error('Error', 'There was an error sending the transaction');
} }
$scope.loading = false; $scope.loading = false;
$scope.loadTxs(); $scope.loadTxs();
@ -344,7 +346,6 @@ angular.module('copayApp.controllers').controller('SendController',
} }
$scope.toggleAddressBookEntry = function(key) { $scope.toggleAddressBookEntry = function(key) {
var w = $rootScope.wallet;
w.toggleAddressBookEntry(key); w.toggleAddressBookEntry(key);
}; };
@ -379,7 +380,6 @@ angular.module('copayApp.controllers').controller('SendController',
}); });
modalInstance.result.then(function(entry) { modalInstance.result.then(function(entry) {
var w = $rootScope.wallet;
$timeout(function() { $timeout(function() {
$scope.loading = false; $scope.loading = false;
@ -403,7 +403,7 @@ angular.module('copayApp.controllers').controller('SendController',
}; };
$scope.getAvailableAmount = function() { $scope.getAvailableAmount = function() {
var amount = ((($rootScope.availableBalance * config.unitToSatoshi).toFixed(0) - bitcore.TransactionBuilder.FEE_PER_1000B_SAT) / config.unitToSatoshi); var amount = ((($rootScope.availableBalance * w.settings.unitToSatoshi).toFixed(0) - bitcore.TransactionBuilder.FEE_PER_1000B_SAT) / w.settings.unitToSatoshi);
return amount > 0 ? amount : 0; return amount > 0 ? amount : 0;
}; };
@ -416,13 +416,12 @@ angular.module('copayApp.controllers').controller('SendController',
$scope.send = function(ntxid, cb) { $scope.send = function(ntxid, cb) {
$scope.loading = true; $scope.loading = true;
$rootScope.txAlertCount = 0; $rootScope.txAlertCount = 0;
var w = $rootScope.wallet;
w.sendTx(ntxid, function(txid, merchantData) { w.sendTx(ntxid, function(txid, merchantData) {
if (!txid) { if (!txid) {
notification.error('Error', 'There was an error sending the transaction'); notification.error('Error', 'There was an error sending the transaction');
} else { } else {
if (!merchantData) { if (!merchantData) {
notification.success('Transaction broadcast', 'Transaction id: ' + txid); notification.success('Transaction broadcasted', 'Transaction id: ' + txid);
} else { } else {
var message = 'Transaction ID: ' + txid; var message = 'Transaction ID: ' + txid;
if (merchantData.pr.ca) { if (merchantData.pr.ca) {
@ -441,7 +440,6 @@ angular.module('copayApp.controllers').controller('SendController',
$scope.sign = function(ntxid) { $scope.sign = function(ntxid) {
$scope.loading = true; $scope.loading = true;
var w = $rootScope.wallet;
w.sign(ntxid, function(ret) { w.sign(ntxid, function(ret) {
if (!ret) { if (!ret) {
notification.error('Error', 'There was an error signing the transaction'); notification.error('Error', 'There was an error signing the transaction');
@ -461,13 +459,46 @@ angular.module('copayApp.controllers').controller('SendController',
$scope.reject = function(ntxid) { $scope.reject = function(ntxid) {
$scope.loading = true; $scope.loading = true;
$rootScope.txAlertCount = 0; $rootScope.txAlertCount = 0;
var w = $rootScope.wallet;
w.reject(ntxid); w.reject(ntxid);
notification.warning('Transaction rejected', 'You rejected the transaction successfully'); notification.warning('Transaction rejected', 'You rejected the transaction successfully');
$scope.loading = false; $scope.loading = false;
$scope.loadTxs(); $scope.loadTxs();
}; };
$scope.clearMerchant = function(callback) {
var scope = $scope;
// TODO: Find a better way of detecting
// whether we're in the Send scope or not.
if (!scope.sendForm || !scope.sendForm.address) {
delete $rootScope.merchant;
if (callback) callback();
return;
}
var val = scope.sendForm.address.$viewValue || '';
var uri;
// If we're setting the domain, ignore the change.
if ($rootScope.merchant && $rootScope.merchant.domain && val === $rootScope.merchant.domain) {
uri = {
merchant: $rootScope.merchant.request_url
};
}
if (val.indexOf('bitcoin:') === 0) {
uri = new bitcore.BIP21(val).data;
} else if (/^https?:\/\//.test(val)) {
uri = {
merchant: val
};
}
if (!uri || !uri.merchant) {
delete $rootScope.merchant;
scope.sendForm.amount.$setViewValue('');
scope.sendForm.amount.$render();
if (callback) callback();
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
$rootScope.$apply();
}
}
};
$scope.onChanged = function() { $scope.onChanged = function() {
var scope = $scope; var scope = $scope;
@ -497,7 +528,7 @@ angular.module('copayApp.controllers').controller('SendController',
// Payment Protocol URI (BIP-72) // Payment Protocol URI (BIP-72)
scope.wallet.fetchPaymentTx(uri.merchant, function(err, merchantData) { scope.wallet.fetchPaymentTx(uri.merchant, function(err, merchantData) {
var balance = $rootScope.availableBalance; var balance = $rootScope.availableBalance;
var available = +(balance * config.unitToSatoshi).toFixed(0); var available = +(balance * w.settings.unitToSatoshi).toFixed(0);
if (merchantData && available < +merchantData.total) { if (merchantData && available < +merchantData.total) {
err = new Error('No unspent outputs available.'); err = new Error('No unspent outputs available.');
@ -508,7 +539,7 @@ angular.module('copayApp.controllers').controller('SendController',
scope.sendForm.address.$isValid = false; scope.sendForm.address.$isValid = false;
if (err.amount) { if (err.amount) {
scope.sendForm.amount.$setViewValue(+err.amount / config.unitToSatoshi); scope.sendForm.amount.$setViewValue(+err.amount / w.settings.unitToSatoshi);
scope.sendForm.amount.$render(); scope.sendForm.amount.$render();
scope.sendForm.amount.$isValid = false; scope.sendForm.amount.$isValid = false;
scope.notEnoughAmount = true; scope.notEnoughAmount = true;
@ -538,7 +569,7 @@ angular.module('copayApp.controllers').controller('SendController',
var url = merchantData.request_url; var url = merchantData.request_url;
var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1]; var domain = /^(?:https?)?:\/\/([^\/:]+).*$/.exec(url)[1];
merchantData.unitTotal = (+merchantData.total / config.unitToSatoshi) + ''; merchantData.unitTotal = (+merchantData.total / w.settings.unitToSatoshi) + '';
merchantData.expiration = new Date( merchantData.expiration = new Date(
merchantData.pr.pd.expires * 1000).toISOString(); merchantData.pr.pd.expires * 1000).toISOString();
merchantData.domain = domain; merchantData.domain = domain;
@ -555,31 +586,8 @@ angular.module('copayApp.controllers').controller('SendController',
// If the address changes to a non-payment-protocol one, // If the address changes to a non-payment-protocol one,
// delete the `merchant` property from the scope. // delete the `merchant` property from the scope.
var unregister = scope.$watch('address', function() { var unregister = $rootScope.$watch(function() {
var val = scope.sendForm.address.$viewValue || ''; $scope.clearMerchant(unregister);
var uri;
// If we're setting the domain, ignore the change.
if ($rootScope.merchant && $rootScope.merchant.domain && val === $rootScope.merchant.domain) {
uri = {
merchant: $rootScope.merchant.request_url
};
}
if (val.indexOf('bitcoin:') === 0) {
uri = new bitcore.BIP21(val).data;
} else if (/^https?:\/\//.test(val)) {
uri = {
merchant: val
};
}
if (!uri || !uri.merchant) {
delete $rootScope.merchant;
scope.sendForm.amount.$setViewValue('');
scope.sendForm.amount.$render();
unregister();
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
$rootScope.$apply();
}
}
}); });
if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') { if ($rootScope.$$phase !== '$apply' && $rootScope.$$phase !== '$digest') {
@ -587,8 +595,10 @@ angular.module('copayApp.controllers').controller('SendController',
} }
notification.info('Payment Request', notification.info('Payment Request',
'Server is requesting ' + merchantData.unitTotal + ' ' + config.unitName + '.' + ' Message: ' + merchantData.pr.pd.memo); 'Server is requesting ' + merchantData.unitTotal +
' ' + w.settings.unitName +
'.' + ' Message: ' + merchantData.pr.pd.memo);
}); });
}; };
}); });

View file

@ -1,15 +1,12 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $location, controllerUtils, rateService) { angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $location, controllerUtils) {
controllerUtils.redirIfLogged(); controllerUtils.redirIfLogged();
$scope.title = 'Settings'; $scope.title = 'Settings';
$scope.networkName = config.networkName;
$scope.insightHost = config.blockchain.host;
$scope.insightPort = config.blockchain.port;
$scope.insightSecure = config.blockchain.schema === 'https';
$scope.forceNetwork = config.forceNetwork;
$scope.defaultLanguage = config.defaultLanguage || 'en'; $scope.defaultLanguage = config.defaultLanguage || 'en';
$scope.insightLivenet = config.network.livenet.url;
$scope.insightTestnet = config.network.testnet.url;
$scope.availableLanguages = [{ $scope.availableLanguages = [{
name: 'English', name: 'English',
@ -26,86 +23,18 @@ angular.module('copayApp.controllers').controller('SettingsController', function
} }
} }
$scope.unitOpts = [{
name: 'Satoshis (100,000,000 satoshis = 1BTC)',
shortName: 'SAT',
value: 1,
decimals: 0
}, {
name: 'bits (1,000,000 bits = 1BTC)',
shortName: 'bits',
value: 100,
decimals: 2
}, {
name: 'mBTC (1,000 mBTC = 1BTC)',
shortName: 'mBTC',
value: 100000,
decimals: 5
}, {
name: 'BTC',
shortName: 'BTC',
value: 100000000,
decimals: 8
}];
$scope.selectedAlternative = {
name: config.alternativeName,
isoCode: config.alternativeIsoCode
};
$scope.alternativeOpts = rateService.isAvailable ?
rateService.listAlternatives() : [$scope.selectedAlternative];
rateService.whenAvailable(function() {
$scope.alternativeOpts = rateService.listAlternatives();
for (var ii in $scope.alternativeOpts) {
if (config.alternativeIsoCode === $scope.alternativeOpts[ii].isoCode) {
$scope.selectedAlternative = $scope.alternativeOpts[ii];
}
}
});
for (var ii in $scope.unitOpts) {
if (config.unitName === $scope.unitOpts[ii].shortName) {
$scope.selectedUnit = $scope.unitOpts[ii];
break;
}
}
$scope.changeNetwork = function() {
$scope.insightHost = $scope.networkName !== 'testnet' ? 'test-insight.bitpay.com' : 'insight.bitpay.com';
};
$scope.changeInsightSSL = function() {
$scope.insightPort = $scope.insightSecure ? 80 : 443;
};
$scope.save = function() { $scope.save = function() {
var network = config.network; var insightSettings = {
network.host = $scope.insightHost; livenet: {
network.port = $scope.insightPort; url: $scope.insightLivenet,
network.schema = $scope.insightSecure ? 'https' : 'http'; },
testnet: {
url: $scope.insightTestnet,
},
}
localStorage.setItem('config', JSON.stringify({ localStorage.setItem('config', JSON.stringify({
networkName: $scope.networkName, network: insightSettings,
blockchain: {
host: $scope.insightHost,
port: $scope.insightPort,
schema: $scope.insightSecure ? 'https' : 'http',
},
socket: {
host: $scope.insightHost,
port: $scope.insightPort,
schema: $scope.insightSecure ? 'https' : 'http',
},
network: network,
unitName: $scope.selectedUnit.shortName,
unitToSatoshi: $scope.selectedUnit.value,
unitDecimals: $scope.selectedUnit.decimals,
alternativeName: $scope.selectedAlternative.name,
alternativeIsoCode: $scope.selectedAlternative.isoCode,
version: copay.version, version: copay.version,
defaultLanguage: $scope.selectedLanguage.isoCode defaultLanguage: $scope.selectedLanguage.isoCode
})); }));

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('SidebarController', function($scope, $rootScope, $sce, $location, $http, notification, controllerUtils) { angular.module('copayApp.controllers').controller('SidebarController', function($scope, $rootScope, $sce, $location, $http, $filter, notification, controllerUtils) {
$scope.menu = [{ $scope.menu = [{
'title': 'Receive', 'title': 'Receive',
@ -60,7 +60,7 @@ angular.module('copayApp.controllers').controller('SidebarController', function(
if ($rootScope.wallet) { if ($rootScope.wallet) {
$scope.$on('$idleWarn', function(a,countdown) { $scope.$on('$idleWarn', function(a,countdown) {
if (!(countdown%5)) if (!(countdown%5))
notification.warning('Session will be closed', 'Your session is about to expire due to inactivity in ' + countdown + ' seconds'); notification.warning('Session will be closed', $filter('translate')('Your session is about to expire due to inactivity in') + ' ' + countdown + ' ' + $filter('translate')('seconds'));
}); });
$scope.$on('$idleTimeout', function() { $scope.$on('$idleTimeout', function() {

View file

@ -4,6 +4,8 @@ var bitcore = require('bitcore');
angular.module('copayApp.controllers').controller('TransactionsController', angular.module('copayApp.controllers').controller('TransactionsController',
function($scope, $rootScope, $timeout, controllerUtils, notification) { function($scope, $rootScope, $timeout, controllerUtils, notification) {
var w = $rootScope.wallet;
$scope.title = 'Transactions'; $scope.title = 'Transactions';
$scope.loading = false; $scope.loading = false;
$scope.lastShowed = false; $scope.lastShowed = false;
@ -12,7 +14,7 @@ angular.module('copayApp.controllers').controller('TransactionsController',
$scope.txpItemsPerPage = 4; $scope.txpItemsPerPage = 4;
$scope.blockchain_txs = []; $scope.blockchain_txs = [];
var satToUnit = 1 / config.unitToSatoshi; var satToUnit = 1 / w.settings.unitToSatoshi;
$scope.update = function() { $scope.update = function() {
$scope.loading = true; $scope.loading = true;
@ -139,7 +141,7 @@ angular.module('copayApp.controllers').controller('TransactionsController',
} }
$scope.getShortNetworkName = function() { $scope.getShortNetworkName = function() {
return config.networkName.substring(0, 4); return w.getNetworkName().substring(0, 4);
}; };
// Autoload transactions on 1-of-1 // Autoload transactions on 1-of-1

View file

@ -1,12 +1,13 @@
'use strict'; 'use strict';
angular.module('copayApp.controllers').controller('VersionController', angular.module('copayApp.controllers').controller('VersionController',
function($scope, $rootScope, $http, notification) { function($scope, $rootScope, $http, $filter, notification) {
var w = $rootScope.wallet;
$scope.version = copay.version; $scope.version = copay.version;
$scope.commitHash = copay.commitHash; $scope.commitHash = copay.commitHash;
$scope.networkName = config.networkName; $scope.networkName = w ? w.getNetworkName() : '';
$scope.defaultLanguage = config.defaultLanguage;
if (_.isUndefined($rootScope.checkVersion)) if (_.isUndefined($rootScope.checkVersion))
$rootScope.checkVersion = true; $rootScope.checkVersion = true;
@ -18,7 +19,7 @@ angular.module('copayApp.controllers').controller('VersionController',
}; };
var latestVersion = data[0].name.replace('v', '').split('.').map(toInt); var latestVersion = data[0].name.replace('v', '').split('.').map(toInt);
var currentVersion = copay.version.split('.').map(toInt); var currentVersion = copay.version.split('.').map(toInt);
var title = 'Copay ' + data[0].name + ' available.'; var title = 'Copay ' + data[0].name + ' ' + $filter('translate')('available.');
var content; var content;
if (currentVersion[0] < latestVersion[0]) { if (currentVersion[0] < latestVersion[0]) {
content = 'It\'s important that you update your wallet at https://copay.io'; content = 'It\'s important that you update your wallet at https://copay.io';
@ -30,4 +31,4 @@ angular.module('copayApp.controllers').controller('VersionController',
}); });
} }
}); });

View file

@ -1,20 +1,21 @@
'use strict'; 'use strict';
angular.module('copayApp.directives') var bitcore = require('bitcore');
.directive('validAddress', ['$rootScope', function($rootScope) { var Address = bitcore.Address;
var bitcore = require('bitcore'); var bignum = bitcore.Bignum;
var Address = bitcore.Address; var preconditions = require('preconditions').singleton();
var bignum = bitcore.Bignum;
angular.module('copayApp.directives')
.directive('validAddress', ['$rootScope',
function($rootScope) {
return { return {
require: 'ngModel', require: 'ngModel',
link: function(scope, elem, attrs, ctrl) { link: function(scope, elem, attrs, ctrl) {
var validator = function(value) { var validator = function(value) {
// If we're setting the domain, ignore the change. // If we're setting the domain, ignore the change.
if ($rootScope.merchant if ($rootScope.merchant && $rootScope.merchant.domain && value === $rootScope.merchant.domain) {
&& $rootScope.merchant.domain
&& value === $rootScope.merchant.domain) {
ctrl.$setValidity('validAddress', true); ctrl.$setValidity('validAddress', true);
return value; return value;
} }
@ -25,35 +26,41 @@ angular.module('copayApp.directives')
return value; return value;
} }
// Bip21 uri // Bip21 uri
if (/^bitcoin:/.test(value)) { if (/^bitcoin:/.test(value)) {
var uri = new bitcore.BIP21(value); var uri = new bitcore.BIP21(value);
var hasAddress = uri.address && uri.isValid() && uri.address.network().name === config.networkName; var hasAddress = uri.address && uri.isValid() && uri.address.network().name === $rootScope.wallet.getNetworkName();
ctrl.$setValidity('validAddress', uri.data.merchant || hasAddress); ctrl.$setValidity('validAddress', uri.data.merchant || hasAddress);
return value; return value;
} }
// Regular Address // Regular Address
var a = new Address(value); var a = new Address(value);
ctrl.$setValidity('validAddress', a.isValid() && a.network().name === config.networkName); ctrl.$setValidity('validAddress', a.isValid() && a.network().name === $rootScope.wallet.getNetworkName());
return value; return value;
}; };
ctrl.$parsers.unshift(validator); ctrl.$parsers.unshift(validator);
ctrl.$formatters.unshift(validator); ctrl.$formatters.unshift(validator);
} }
}; };
}]) }
])
.directive('enoughAmount', ['$rootScope', .directive('enoughAmount', ['$rootScope',
function($rootScope) { function($rootScope) {
var bitcore = require('bitcore'); var w = $rootScope.wallet;
preconditions.checkState(w);
preconditions.checkState(w.settings.unitToSatoshi);
var feeSat = Number(bitcore.TransactionBuilder.FEE_PER_1000B_SAT); var feeSat = Number(bitcore.TransactionBuilder.FEE_PER_1000B_SAT);
return { return {
require: 'ngModel', require: 'ngModel',
link: function(scope, element, attrs, ctrl) { link: function(scope, element, attrs, ctrl) {
var val = function(value) { var val = function(value) {
var availableBalanceNum = Number(($rootScope.availableBalance * config.unitToSatoshi).toFixed(0)); var availableBalanceNum = Number(($rootScope.availableBalance * w.settings.unitToSatoshi).toFixed(0));
var vNum = Number((value * config.unitToSatoshi).toFixed(0)); var vNum = Number((value * w.settings.unitToSatoshi).toFixed(0));
if (typeof vNum == "number" && vNum > 0) { if (typeof vNum == "number" && vNum > 0) {
vNum = vNum + feeSat; vNum = vNum + feeSat;
@ -81,7 +88,8 @@ angular.module('copayApp.directives')
require: 'ngModel', require: 'ngModel',
link: function(scope, elem, attrs, ctrl) { link: function(scope, elem, attrs, ctrl) {
var validator = function(value) { var validator = function(value) {
ctrl.$setValidity('walletSecret', Boolean(walletFactory.decodeSecret(value))); var a = new Address(value);
ctrl.$setValidity('walletSecret', !a.isValid() && Boolean(walletFactory.decodeSecret(value)));
return value; return value;
}; };
@ -270,7 +278,7 @@ angular.module('copayApp.directives')
client.on('datarequested', function(client) { client.on('datarequested', function(client) {
client.setText(scope.clipCopy); client.setText(scope.clipCopy);
} ); });
client.on('complete', function(client, args) { client.on('complete', function(client, args) {
elm.removeClass('btn-copy').addClass('btn-copied').html('Copied!'); elm.removeClass('btn-copy').addClass('btn-copied').html('Copied!');

View file

@ -44,43 +44,44 @@ angular.module('copayApp.filters', [])
return addrs; return addrs;
}; };
}) })
.filter('noFractionNumber', .filter('noFractionNumber', ['$filter', '$locale', '$rootScope',
[ '$filter', '$locale', function(filter, locale, $rootScope) {
function(filter, locale) { var numberFilter = filter('number');
var numberFilter = filter('number'); var formats = locale.NUMBER_FORMATS;
var formats = locale.NUMBER_FORMATS; return function(amount, n) {
return function(amount, n) { if (typeof(n) === 'undefined' && !$rootScope.wallet) return amount;
var fractionSize = (typeof(n) != 'undefined') ? n : config.unitToSatoshi.toString().length - 1;
var value = numberFilter(amount, fractionSize); var fractionSize = (typeof(n) !== 'undefined') ?
var sep = value.indexOf(formats.DECIMAL_SEP); n : $rootScope.wallet.settings.unitToSatoshi.toString().length - 1;
var group = value.indexOf(formats.GROUP_SEP); var value = numberFilter(amount, fractionSize);
if(amount >= 0) { var sep = value.indexOf(formats.DECIMAL_SEP);
if (group > 0) { var group = value.indexOf(formats.GROUP_SEP);
if (sep < 0) { 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 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 { return 0;
value = parseFloat(value); };
if(value % 1 === 0) { }
value = value.toFixed(0); ]);
}
return value;
}
}
return 0;
};
} ]);

View file

@ -101,5 +101,4 @@ Logger.prototype.setLevel = function(level) {
var logger = new Logger('copay'); var logger = new Logger('copay');
logger.setLevel(config.logLevel); logger.setLevel(config.logLevel);
logger.log('Log level:' + config.logLevel);
module.exports = logger; module.exports = logger;

320
js/models/Storage.js Normal file
View file

@ -0,0 +1,320 @@
'use strict';
var preconditions = require('preconditions').singleton();
var CryptoJS = require('node-cryptojs-aes').CryptoJS;
var bitcore = require('bitcore');
var preconditions = require('preconditions').instance();
var _ = require('underscore');
var CACHE_DURATION = 1000 * 60 * 5;
var id = 0;
function Storage(opts) {
opts = opts || {};
this.wListCache = {};
this.__uniqueid = ++id;
if (opts.password)
this.setPassphrase(opts.password);
try {
this.storage = opts.storage || localStorage;
this.sessionStorage = opts.sessionStorage || sessionStorage;
} catch (e) {
console.log('Error in storage:', e); //TODO
};
preconditions.checkState(this.storage, 'No storage defined');
preconditions.checkState(this.sessionStorage, 'No sessionStorage defined');
}
var pps = {};
Storage.prototype._getPassphrase = function() {
if (!pps[this.__uniqueid])
throw new Error('NOPASSPHRASE: No passphrase set');
return pps[this.__uniqueid];
}
Storage.prototype.setPassphrase = function(password) {
pps[this.__uniqueid] = password;
}
Storage.prototype._encrypt = function(string) {
var encrypted = CryptoJS.AES.encrypt(string, this._getPassphrase());
var encryptedBase64 = encrypted.toString();
return encryptedBase64;
};
Storage.prototype._decrypt = function(base64) {
var decryptedStr = null;
try {
var decrypted = CryptoJS.AES.decrypt(base64, this._getPassphrase());
if (decrypted)
decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
} catch (e) {
// Error while decrypting
return null;
}
return decryptedStr;
};
Storage.prototype._read = function(k, cb) {
preconditions.checkArgument(cb);
var self = this;
this.storage.getItem(k, function(ret) {
if (!ret) return cb(null);
var ret = self._decrypt(ret);
if (!ret) return cb(null);
ret = ret.toString(CryptoJS.enc.Utf8);
ret = JSON.parse(ret);
return cb(ret);
});
};
Storage.prototype._write = function(k, v, cb) {
preconditions.checkArgument(cb);
v = JSON.stringify(v);
v = this._encrypt(v);
this.storage.setItem(k, v, cb);
};
// get value by key
Storage.prototype.getGlobal = function(k, cb) {
preconditions.checkArgument(cb);
this.storage.getItem(k, function(item) {
cb(item == 'undefined' ? undefined : item);
});
};
// set value for key
Storage.prototype.setGlobal = function(k, v, cb) {
preconditions.checkArgument(cb);
this.storage.setItem(k, typeof v === 'object' ? JSON.stringify(v) : v, cb);
};
// remove value for key
Storage.prototype.removeGlobal = function(k, cb) {
preconditions.checkArgument(cb);
this.storage.removeItem(k, cb);
};
Storage.prototype.getSessionId = function(cb) {
preconditions.checkArgument(cb);
var self = this;
self.sessionStorage.getItem('sessionId', function(sessionId) {
if (sessionId)
return cb(sessionId);
sessionId = bitcore.SecureRandom.getRandomBuffer(8).toString('hex');
self.sessionStorage.setItem('sessionId', sessionId, function() {
return cb(sessionId);
});
});
};
Storage.prototype.setSessionId = function(sessionId, cb) {
this.sessionStorage.setItem('sessionId', sessionId, cb);
};
Storage.prototype._key = function(walletId, k) {
return walletId + '::' + k;
};
// get value by key
Storage.prototype.get = function(walletId, k, cb) {
preconditions.checkArgument(walletId, k, cb);
this._read(this._key(walletId, k), cb);
};
Storage.prototype._readHelper = function(walletId, k, cb) {
var wk = this._key(walletId, k);
this._read(wk, function(v) {
return cb(v, k);
});
};
Storage.prototype.getMany = function(walletId, keys, cb) {
preconditions.checkArgument(cb);
var self = this;
var ret = {};
var l = keys.length,
i = 0;
for (var ii in keys) {
this._readHelper(walletId, keys[ii], function(v, k) {
ret[k] = v;
if (++i == l) {
return cb(ret);
}
});
}
};
// set value for key
Storage.prototype.set = function(walletId, k, v, cb) {
preconditions.checkArgument(walletId && k && cb);
if (_.isUndefined(v)) return cb();
this._write(this._key(walletId, k), v, cb);
};
// remove value for key
Storage.prototype.remove = function(walletId, k, cb) {
preconditions.checkArgument(walletId && k && cb);
this.removeGlobal(this._key(walletId, k), cb);
};
Storage.prototype.setName = function(walletId, name, cb) {
preconditions.checkArgument(walletId && name && cb);
this.setGlobal('nameFor::' + walletId, name, cb);
};
Storage.prototype.getName = function(walletId, cb) {
preconditions.checkArgument(walletId && cb);
this.getGlobal('nameFor::' + walletId, cb);
};
Storage.prototype.getWalletIds = function(cb) {
preconditions.checkArgument(cb);
var walletIds = [];
var uniq = {};
this.storage.allKeys(function(keys) {
for (var ii in keys) {
var key = keys[ii];
var split = key.split('::');
if (split.length == 2) {
var walletId = split[0];
if (!walletId || walletId === 'nameFor' || walletId === 'lock')
continue;
if (typeof uniq[walletId] === 'undefined') {
walletIds.push(walletId);
uniq[walletId] = 1;
}
}
}
return cb(walletIds);
});
};
Storage.prototype.getWallets = function(cb) {
preconditions.checkArgument(cb);
if (this.wListCache.ts > Date.now())
return cb(this.wListCache.data)
var wallets = [];
var self = this;
this.getWalletIds(function(ids) {
var l = ids.length,
i = 0;
if (!l)
return cb([]);
_.each(ids, function(id) {
self.getName(id, function(name) {
wallets.push({
id: id,
name: name,
});
if (++i == l) {
self.wListCache.data = wallets;
self.wListCache.ts = Date.now() + CACHE_DURATION;
return cb(wallets);
}
});
});
});
};
Storage.prototype.deleteWallet = function(walletId, cb) {
preconditions.checkArgument(walletId);
preconditions.checkArgument(cb);
var err;
var self = this;
var toDelete = {};
this.storage.allKeys(function(allKeys) {
for (var ii in allKeys) {
var key = allKeys[ii];
var split = key.split('::');
if (split.length == 2 && split[0] === walletId) {
toDelete[key] = 1;
};
}
var l = Object.keys(toDelete).length,
j = 0;
if (!l)
return cb(new Error('WNOTFOUND: Wallet not found'));
toDelete['nameFor::' + walletId] = 1;
l++;
for (var i in toDelete) {
self.removeGlobal(i, function() {
if (++j == l)
return cb(err);
});
}
});
};
Storage.prototype.setLastOpened = function(walletId, cb) {
this.setGlobal('lastOpened', walletId, cb);
}
Storage.prototype.getLastOpened = function(cb) {
this.getGlobal('lastOpened', cb);
}
//obj contains keys to be set
Storage.prototype.setFromObj = function(walletId, obj, cb) {
preconditions.checkArgument(cb);
var self = this;
var l = Object.keys(obj).length,
i = 0;
for (var k in obj) {
self.set(walletId, k, obj[k], function() {
if (++i == l) {
if (obj.opts.name)
self.setName(walletId, obj.opts.name, cb);
else
return cb();
}
});
}
};
// remove all values
Storage.prototype.clearAll = function(cb) {
this.storage.clear(cb);
};
Storage.prototype.import = function(base64) {
var decryptedStr = this._decrypt(base64);
return JSON.parse(decryptedStr);
};
Storage.prototype.export = function(obj) {
var string = JSON.stringify(obj);
return this._encrypt(string);
};
module.exports = Storage;

View file

@ -15,9 +15,7 @@ var preconditions = require('preconditions').singleton();
subscribing to transactions on adressess and blocks. subscribing to transactions on adressess and blocks.
Opts: Opts:
- host - url
- port
- schema
- reconnection (optional) - reconnection (optional)
- reconnectionDelay (optional) - reconnectionDelay (optional)
@ -29,22 +27,22 @@ var preconditions = require('preconditions').singleton();
*/ */
var Insight = function(opts) { var Insight = function(opts) {
preconditions.checkArgument(opts)
.shouldBeObject(opts)
.checkArgument(opts.url)
this.status = this.STATUS.DISCONNECTED; this.status = this.STATUS.DISCONNECTED;
this.subscribed = {}; this.subscribed = {};
this.listeningBlocks = false; this.listeningBlocks = false;
preconditions.checkArgument(opts).shouldBeObject(opts) this.url = opts.url;
.checkArgument(opts.host)
.checkArgument(opts.port)
.checkArgument(opts.schema);
this.url = opts.schema + '://' + opts.host + ':' + opts.port;
this.opts = { this.opts = {
'reconnection': opts.reconnection || true, 'reconnection': opts.reconnection || true,
'reconnectionDelay': opts.reconnectionDelay || 1000, 'reconnectionDelay': opts.reconnectionDelay || 1000,
'secure': opts.schema === 'https' 'secure': opts.url.indexOf('https') === 0
}; };
this.socket = this.getSocket();
} }
util.inherits(Insight, EventEmitter); util.inherits(Insight, EventEmitter);
@ -105,7 +103,7 @@ Insight.prototype._setMainHandlers = function(url, opts) {
/** @private */ /** @private */
Insight.prototype.getSocket = function(url, opts) { Insight.prototype.getSocket = function() {
if (!this.socket) { if (!this.socket) {
this.socket = this._getSocketIO(this.url, this.opts); this.socket = this._getSocketIO(this.url, this.opts);
@ -148,7 +146,6 @@ Insight.prototype.subscribe = function(addresses) {
return function(txid) { return function(txid) {
// verify the address is still subscribed // verify the address is still subscribed
if (!self.subscribed[address]) return; if (!self.subscribed[address]) return;
log.debug('insight tx event'); log.debug('insight tx event');
self.emit('tx', { self.emit('tx', {

View file

@ -7,10 +7,9 @@ var _ = require('underscore');
/** /**
* @namespace * @namespace
* * @desc
* HDPath contains helper functions to handle BIP32 branches as * HDPath contains helper functions to handle BIP32 branches as
* Copay uses them. * Copay uses them.
*
* Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki * Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki
* <pre> * <pre>
* m / purpose' / copayerIndex / change:boolean / addressIndex * m / purpose' / copayerIndex / change:boolean / addressIndex

View file

@ -0,0 +1,52 @@
'use strict';
var preconditions = require('preconditions').singleton();
var log = require('../../log');
function PluginManager(config) {
this.registered = {};
this.scripts = [];
for (var ii in config.plugins) {
var pluginName = ii;
if (!config.plugins[pluginName])
continue;
log.info('Loading plugin: ' + pluginName);
var pluginClass = require('../plugins/' + pluginName);
var pluginObj = new pluginClass(config[pluginName]);
pluginObj.init();
this._register(pluginObj, pluginName);
}
};
var KIND_UNIQUE = PluginManager.KIND_UNIQUE = 1;
var KIND_MULTIPLE = PluginManager.KIND_MULTIPLE = 2;
PluginManager.TYPE = {};
PluginManager.TYPE['STORAGE'] = KIND_UNIQUE;
PluginManager.prototype._register = function(obj, name) {
preconditions.checkArgument(obj.type, 'Plugin has not type:' + name);
var type = obj.type;
var kind = PluginManager.TYPE[type];
preconditions.checkArgument(kind, 'Plugin has unknown type' + name);
preconditions.checkState(kind !== PluginManager.KIND_UNIQUE || !this.registered[type], 'Plugin kind already registered: ' + name);
if (kind === PluginManager.KIND_UNIQUE) {
this.registered[type] = obj;
} else {
this.registered[type] = this.registered[type] || [];
this.registered[type].push(obj);
}
this.scripts = this.scripts.concat(obj.scripts || []);
};
PluginManager.prototype.get = function(type) {
return this.registered[type];
};
module.exports = PluginManager;

View file

@ -21,6 +21,7 @@ var HDPath = require('./HDPath');
* @param {string} opts.extendedPrivateKeyString if set, use this private key * @param {string} opts.extendedPrivateKeyString if set, use this private key
* string, othewise create a new * string, othewise create a new
* private key * private key
* @constructor
*/ */
function PrivateKey(opts) { function PrivateKey(opts) {
opts = opts || {}; opts = opts || {};

View file

@ -12,7 +12,7 @@ var HDPath = require('./HDPath');
var HDParams = require('./HDParams'); var HDParams = require('./HDParams');
/** /**
* @desc * @desc Represents a public key ring, the set of all public keys and the used indexes
* *
* @constructor * @constructor
* @param {Object} opts * @param {Object} opts
@ -20,10 +20,10 @@ var HDParams = require('./HDParams');
* @param {string} opts.network 'livenet' to signal the bitcoin main network, all others are testnet * @param {string} opts.network 'livenet' to signal the bitcoin main network, all others are testnet
* @param {number=} opts.requiredCopayers - defaults to 3 * @param {number=} opts.requiredCopayers - defaults to 3
* @param {number=} opts.totalCopayers - defaults to 5 * @param {number=} opts.totalCopayers - defaults to 5
* @param {Object[]=} opts.indexes - an array to be deserialized using {@link HDParams#fromList} * @param {Object[]} [opts.indexes] - an array to be deserialized using {@link HDParams#fromList}
* (defaults to all indexes in zero) * (defaults to all indexes in zero)
* @param {Object=} opts.nicknameFor - nicknames for other copayers * @param {Object=} opts.nicknameFor - nicknames for other copayers
* @param {boolean[]=} opts.copayersBackup - whether other copayers have backed up their wallets * @param {boolean[]} [opts.copayersBackup] - whether other copayers have backed up their wallets
*/ */
function PublicKeyRing(opts) { function PublicKeyRing(opts) {
opts = opts || {}; opts = opts || {};
@ -527,7 +527,7 @@ PublicKeyRing.prototype.getForPath = function(path) {
* @see PublicKeyRing#getForPath * @see PublicKeyRing#getForPath
* *
* @param {string[]} paths - the BIP32 paths * @param {string[]} paths - the BIP32 paths
* @return {Buffer[][]} the public keys, in buffer format * @return {Array[]} the public keys, in buffer format (matrix of Buffer, Buffer[][])
*/ */
PublicKeyRing.prototype.getForPaths = function(paths) { PublicKeyRing.prototype.getForPaths = function(paths) {
preconditions.checkArgument(!_.isUndefined(paths)); preconditions.checkArgument(!_.isUndefined(paths));

View file

@ -50,7 +50,6 @@ TxProposals.prototype.getNtxidsSince = function(sinceTs) {
if (txp.createdTs >= sinceTs) if (txp.createdTs >= sinceTs)
ret.push(ii); ret.push(ii);
} }
console.log('[TxProposals.js.52:ret:]',ret); //TODO
return ret; return ret;
}; };

View file

@ -53,6 +53,7 @@ var copayConfig = require('../../../config');
* @TODO: figure out if reconnectDelay is set in milliseconds * @TODO: figure out if reconnectDelay is set in milliseconds
* @param {number} opts.reconnectDelay - amount of seconds to wait before * @param {number} opts.reconnectDelay - amount of seconds to wait before
* attempting to reconnect * attempting to reconnect
* @constructor
*/ */
function Wallet(opts) { function Wallet(opts) {
var self = this; var self = this;
@ -63,19 +64,16 @@ function Wallet(opts) {
'publicKeyRing', 'txProposals', 'privateKey', 'version', 'publicKeyRing', 'txProposals', 'privateKey', 'version',
'reconnectDelay' 'reconnectDelay'
].forEach(function(k) { ].forEach(function(k) {
preconditions.checkArgument(!_.isUndefined(opts[k]), 'missing required option for Wallet: ' + k); preconditions.checkArgument(!_.isUndefined(opts[k]), 'MISSOPT: missing required option for Wallet: ' + k);
self[k] = opts[k]; self[k] = opts[k];
}); });
preconditions.checkArgument(!copayConfig.forceNetwork || this.getNetworkName() === copayConfig.networkName,
'Network forced to ' + copayConfig.networkName +
' and tried to create a Wallet with network ' + this.getNetworkName());
this.id = opts.id || Wallet.getRandomId(); this.id = opts.id || Wallet.getRandomId();
this.secretNumber = opts.secretNumber || Wallet.getRandomNumber(); this.secretNumber = opts.secretNumber || Wallet.getRandomNumber();
this.lock = new WalletLock(this.storage, this.id, opts.lockTimeOutMin); this.lock = new WalletLock(this.storage, this.id, opts.lockTimeOutMin);
this.settings = opts.settings || copayConfig.wallet.settings;
this.name = opts.name; this.name = opts.name;
this.verbose = opts.verbose;
this.publicKeyRing.walletId = this.id; this.publicKeyRing.walletId = this.id;
this.txProposals.walletId = this.id; this.txProposals.walletId = this.id;
this.network.maxPeers = this.totalCopayers; this.network.maxPeers = this.totalCopayers;
@ -86,6 +84,13 @@ function Wallet(opts) {
this.lastTimestamp = opts.lastTimestamp || undefined; this.lastTimestamp = opts.lastTimestamp || undefined;
this.lastMessageFrom = {}; this.lastMessageFrom = {};
//to avoid confirmation of copayer's backups if is imported from a file
this.isImported = opts.isImported || false;
//to avoid waiting others copayers to make a backup and login immediatly
this.forcedLogin = opts.forcedLogin || false;
this.paymentRequests = opts.paymentRequests || {}; this.paymentRequests = opts.paymentRequests || {};
//network nonces are 8 byte buffers, representing a big endian number //network nonces are 8 byte buffers, representing a big endian number
@ -112,6 +117,21 @@ Wallet.builderOpts = {
feeSat: undefined, feeSat: undefined,
}; };
/**
* @desc static list with persisted properties of a wallet.
* These are the properties that get stored/read from localstorage
*/
Wallet.PERSISTED_PROPERTIES = [
'opts',
'settings',
'publicKeyRing',
'txProposals',
'privateKey',
'addressBook',
'backupOffered',
'lastTimestamp',
];
/** /**
* @desc Retrieve a random id for the wallet * @desc Retrieve a random id for the wallet
* @TODO: Discuss changing to a UUID * @TODO: Discuss changing to a UUID
@ -149,7 +169,7 @@ Wallet.prototype.seedCopayer = function(pubKey) {
* *
* @param {string} senderId - the sender id * @param {string} senderId - the sender id
* @param {Object} data - the data recived, {@see HDParams#fromList} * @param {Object} data - the data recived, {@see HDParams#fromList}
* @emits {publicKeyRingUpdated} * @emits publicKeyRingUpdated
*/ */
Wallet.prototype._onIndexes = function(senderId, data) { Wallet.prototype._onIndexes = function(senderId, data) {
log.debug('RECV INDEXES:', data); log.debug('RECV INDEXES:', data);
@ -161,6 +181,22 @@ Wallet.prototype._onIndexes = function(senderId, data) {
} }
}; };
/**
* @desc
* Changes wallet settings. The settings format is:
*
* var settings = {
* unitName: 'bits',
* unitToSatoshi: 100,
* alternativeName: 'US Dollar',
* alternativeIsoCode: 'USD',
* };
*/
Wallet.prototype.changeSettings = function(settings) {
this.settings = settings;
this.store();
};
/** /**
* @desc * @desc
* Handles a 'PUBLICKEYRING' message from <tt>senderId</tt>. * Handles a 'PUBLICKEYRING' message from <tt>senderId</tt>.
@ -178,8 +214,8 @@ Wallet.prototype._onIndexes = function(senderId, data) {
* @param {Object} data - the data recived, {@see HDParams#fromList} * @param {Object} data - the data recived, {@see HDParams#fromList}
* @param {Object} data.publicKeyRing - data to be deserialized into a {@link PublicKeyRing} * @param {Object} data.publicKeyRing - data to be deserialized into a {@link PublicKeyRing}
* using {@link PublicKeyRing#fromObj} * using {@link PublicKeyRing#fromObj}
* @emits {publicKeyRingUpdated} * @emits publicKeyRingUpdated
* @emits {connectionError} * @emits connectionError
*/ */
Wallet.prototype._onPublicKeyRing = function(senderId, data) { Wallet.prototype._onPublicKeyRing = function(senderId, data) {
log.debug('RECV PUBLICKEYRING:', data); log.debug('RECV PUBLICKEYRING:', data);
@ -214,7 +250,7 @@ Wallet.prototype._onPublicKeyRing = function(senderId, data) {
* *
* @param {string} senderId - the copayer that sent this event * @param {string} senderId - the copayer that sent this event
* @param {Object} m - the data received * @param {Object} m - the data received
* @emits {txProposalEvent} * @emits txProposalEvent
*/ */
Wallet.prototype._processProposalEvents = function(senderId, m) { Wallet.prototype._processProposalEvents = function(senderId, m) {
var ev; var ev;
@ -481,7 +517,6 @@ Wallet.prototype._onData = function(senderId, data, ts) {
preconditions.checkArgument(data.type); preconditions.checkArgument(data.type);
preconditions.checkArgument(ts); preconditions.checkArgument(ts);
preconditions.checkArgument(_.isNumber(ts)); preconditions.checkArgument(_.isNumber(ts));
log.debug('RECV', senderId, data); log.debug('RECV', senderId, data);
if (data.type !== 'walletId' && this.id !== data.walletId) { if (data.type !== 'walletId' && this.id !== data.walletId) {
@ -490,7 +525,6 @@ Wallet.prototype._onData = function(senderId, data, ts) {
return; return;
} }
switch (data.type) { switch (data.type) {
// This handler is repeaded on WalletFactory (#join). TODO // This handler is repeaded on WalletFactory (#join). TODO
case 'walletId': case 'walletId':
@ -570,6 +604,7 @@ Wallet.prototype._optsToObj = function() {
totalCopayers: this.totalCopayers, totalCopayers: this.totalCopayers,
name: this.name, name: this.name,
version: this.version, version: this.version,
networkName: this.getNetworkName(),
}; };
return obj; return obj;
@ -600,6 +635,14 @@ Wallet.prototype.getMyCopayerIdPriv = function() {
return this.privateKey.getIdPriv(); //copayer idpriv is hex of a private key return this.privateKey.getIdPriv(); //copayer idpriv is hex of a private key
}; };
/**
* @desc Get my own nickname
* @return {string} copayer nickname
*/
Wallet.prototype.getMyCopayerNickname = function() {
return this.publicKeyRing.nicknameForCopayer(this.getMyCopayerId());
};
/** /**
* @desc Returns the secret value for other users to join this wallet * @desc Returns the secret value for other users to join this wallet
* @return {string} my own pubkey, base58 encoded * @return {string} my own pubkey, base58 encoded
@ -615,7 +658,11 @@ Wallet.prototype.getSecretNumber = function() {
* @return {string} * @return {string}
*/ */
Wallet.prototype.getSecret = function() { Wallet.prototype.getSecret = function() {
var buf = new Buffer(this.getMyCopayerId() + this.getSecretNumber(), 'hex'); var buf = new Buffer(
this.getMyCopayerId() +
this.getSecretNumber() +
(this.getNetworkName() === 'livenet' ? '00' : '01'),
'hex');
var str = Base58Check.encode(buf); var str = Base58Check.encode(buf);
return str; return str;
}; };
@ -630,9 +677,11 @@ Wallet.decodeSecret = function(secretB) {
var secret = Base58Check.decode(secretB); var secret = Base58Check.decode(secretB);
var pubKeyBuf = secret.slice(0, 33); var pubKeyBuf = secret.slice(0, 33);
var secretNumber = secret.slice(33, 38); var secretNumber = secret.slice(33, 38);
var networkName = secret.slice(38, 39).toString('hex') === '00' ? 'livenet' : 'testnet';
return { return {
pubKey: pubKeyBuf.toString('hex'), pubKey: pubKeyBuf.toString('hex'),
secretNumber: secretNumber.toString('hex') secretNumber: secretNumber.toString('hex'),
networkName: networkName,
} }
}; };
@ -661,7 +710,13 @@ Wallet.prototype._setBlockchainListeners = function() {
}); });
this.blockchain.on('tx', function(tx) { this.blockchain.on('tx', function(tx) {
log.debug('blockchain tx event'); log.debug('blockchain tx event');
self.emit('tx', tx.address); var addresses = self.getAddressesInfo();
var addr = _.findWhere(addresses, {
addressStr: tx.address
});
if (addr) {
self.emit('tx', tx.address, addr.isChange);
}
}); });
if (!self.spendUnconfirmed) { if (!self.spendUnconfirmed) {
@ -767,23 +822,28 @@ Wallet.prototype.getRegisteredPeerIds = function() {
* @emits locked - in case the wallet is opened in another instance * @emits locked - in case the wallet is opened in another instance
*/ */
Wallet.prototype.keepAlive = function() { Wallet.prototype.keepAlive = function() {
try { var self = this;
this.lock.keepAlive();
} catch (e) { this.lock.keepAlive(function(err) {
log.debug(e); if (err) {
this.emit('locked', null, 'Wallet appears to be openned on other browser instance. Closing this one.'); log.debug(err);
} self.emit('locked', null, 'Wallet appears to be openned on other browser instance. Closing this one.');
}
});
}; };
/** /**
* @desc Store the wallet's state * @desc Store the wallet's state
* @param {function} callback (err)
*/ */
Wallet.prototype.store = function() { Wallet.prototype.store = function(cb) {
var self = this;
this.keepAlive(); this.keepAlive();
this.storage.setFromObj(this.id, this.toObj(), function(err) {
var wallet = this.toObj(); log.debug('Wallet stored');
this.storage.setFromObj(this.id, wallet); if (cb)
log.debug('Wallet stored'); cb(err);
});
}; };
/** /**
@ -793,13 +853,11 @@ Wallet.prototype.store = function() {
Wallet.prototype.toObj = function() { Wallet.prototype.toObj = function() {
var optsObj = this._optsToObj(); var optsObj = this._optsToObj();
var networkNonce = this.network.getHexNonce();
var networkNonces = this.network.getHexNonces();
var walletObj = { var walletObj = {
opts: optsObj, opts: optsObj,
networkNonce: networkNonce, //yours settings: this.settings,
networkNonces: networkNonces, //copayers networkNonce: this.network.getHexNonce(), //yours
networkNonces: this.network.getHexNonces(), //copayers
publicKeyRing: this.publicKeyRing.toObj(), publicKeyRing: this.publicKeyRing.toObj(),
txProposals: this.txProposals.toObj(), txProposals: this.txProposals.toObj(),
privateKey: this.privateKey ? this.privateKey.toObj() : undefined, privateKey: this.privateKey ? this.privateKey.toObj() : undefined,
@ -827,10 +885,11 @@ Wallet.prototype.toObj = function() {
*/ */
Wallet.fromObj = function(o, storage, network, blockchain) { Wallet.fromObj = function(o, storage, network, blockchain) {
// TODO: What is this supposed to do? // clone opts
var opts = JSON.parse(JSON.stringify(o.opts)); var opts = JSON.parse(JSON.stringify(o.opts));
opts.addressBook = o.addressBook; opts.addressBook = o.addressBook;
opts.settings = o.settings;
if (o.privateKey) { if (o.privateKey) {
opts.privateKey = PrivateKey.fromObj(o.privateKey); opts.privateKey = PrivateKey.fromObj(o.privateKey);
@ -867,6 +926,7 @@ Wallet.fromObj = function(o, storage, network, blockchain) {
opts.storage = storage; opts.storage = storage;
opts.network = network; opts.network = network;
opts.blockchain = blockchain; opts.blockchain = blockchain;
opts.isImported = true;
return new Wallet(opts); return new Wallet(opts);
}; };
@ -896,7 +956,6 @@ Wallet.prototype.send = function(recipients, obj) {
Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) { Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) {
var ntxids = sinceTs ? this.txProposals.getNtxidsSince(sinceTs) : this.txProposals.getNtxids(); var ntxids = sinceTs ? this.txProposals.getNtxidsSince(sinceTs) : this.txProposals.getNtxids();
var self = this; var self = this;
_.each(ntxids, function(ntxid, key) { _.each(ntxids, function(ntxid, key) {
self.sendTxProposal(ntxid, recipients); self.sendTxProposal(ntxid, recipients);
}); });
@ -905,7 +964,7 @@ Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) {
/** /**
* @desc Send a TxProposal identified by transaction id to a set of recipients * @desc Send a TxProposal identified by transaction id to a set of recipients
* @param {string} ntxid - the transaction proposal id * @param {string} ntxid - the transaction proposal id
* @param {string[]=} recipients - the pubkeys of the recipients * @param {string[]} [recipients] - the pubkeys of the recipients
*/ */
Wallet.prototype.sendTxProposal = function(ntxid, recipients) { Wallet.prototype.sendTxProposal = function(ntxid, recipients) {
preconditions.checkArgument(ntxid); preconditions.checkArgument(ntxid);
@ -947,7 +1006,7 @@ Wallet.prototype.sendReject = function(ntxid) {
/** /**
* @desc Notify other peers that a wallet has been backed up and it's ready to be used * @desc Notify other peers that a wallet has been backed up and it's ready to be used
* @param {string[]=} recipients - the pubkeys of the recipients * @param {string[]} [recipients] - the pubkeys of the recipients
*/ */
Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { Wallet.prototype.sendWalletReady = function(recipients, sinceTs) {
log.debug('### SENDING WalletReady TO:', recipients || 'All'); log.debug('### SENDING WalletReady TO:', recipients || 'All');
@ -962,7 +1021,7 @@ Wallet.prototype.sendWalletReady = function(recipients, sinceTs) {
/** /**
* @desc Notify other peers of the walletId * @desc Notify other peers of the walletId
* @TODO: Why is this needed? Can't everybody just calculate the walletId? * @TODO: Why is this needed? Can't everybody just calculate the walletId?
* @param {string[]=} recipients - the pubkeys of the recipients * @param {string[]} [recipients] - the pubkeys of the recipients
*/ */
Wallet.prototype.sendWalletId = function(recipients) { Wallet.prototype.sendWalletId = function(recipients) {
log.debug('### SENDING walletId TO:', recipients || 'All', this.id); log.debug('### SENDING walletId TO:', recipients || 'All', this.id);
@ -977,7 +1036,7 @@ Wallet.prototype.sendWalletId = function(recipients) {
/** /**
* @desc Send the current PublicKeyRing to other recipients * @desc Send the current PublicKeyRing to other recipients
* @param {string[]=} recipients - the pubkeys of the recipients * @param {string[]} [recipients] - the pubkeys of the recipients
*/ */
Wallet.prototype.sendPublicKeyRing = function(recipients) { Wallet.prototype.sendPublicKeyRing = function(recipients) {
log.debug('### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj()); log.debug('### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj());
@ -992,7 +1051,7 @@ Wallet.prototype.sendPublicKeyRing = function(recipients) {
/** /**
* @desc Send the current indexes of our public key ring to other peers * @desc Send the current indexes of our public key ring to other peers
* @param {string[]=} recipients - the pubkeys of the recipients * @param {string[]} recipients - the pubkeys of the recipients
*/ */
Wallet.prototype.sendIndexes = function(recipients) { Wallet.prototype.sendIndexes = function(recipients) {
var indexes = HDParams.serialize(this.publicKeyRing.indexes); var indexes = HDParams.serialize(this.publicKeyRing.indexes);
@ -1007,7 +1066,7 @@ Wallet.prototype.sendIndexes = function(recipients) {
/** /**
* @desc Send our addressBook to other recipients * @desc Send our addressBook to other recipients
* @param {string[]=} recipients - the pubkeys of the recipients * @param {string[]} recipients - the pubkeys of the recipients
*/ */
Wallet.prototype.sendAddressBook = function(recipients) { Wallet.prototype.sendAddressBook = function(recipients) {
log.debug('### SENDING addressBook TO:', recipients || 'All', this.addressBook); log.debug('### SENDING addressBook TO:', recipients || 'All', this.addressBook);
@ -1244,7 +1303,7 @@ Wallet.prototype.createPaymentTx = function(options, cb) {
return self.receivePaymentRequest(options, pr, cb); return self.receivePaymentRequest(options, pr, cb);
}) })
.error(function(data, status, headers, config) { .error(function(data, status, headers, config) {
return cb(new Error('Status: ' + JSON.stringify(status))); return cb(new Error('Status: ' + status));
}); });
}; };
@ -1357,7 +1416,9 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) {
expires: expires, expires: expires,
memo: memo || 'This server would like some BTC from you.', memo: memo || 'This server would like some BTC from you.',
payment_url: payment_url, payment_url: payment_url,
merchant_data: merchant_data.toString('hex') merchant_data: merchant_data ? merchant_data.toString('hex')
// : new Buffer('none', 'utf8').toString('hex')
: '00'
}, },
signature: sig.toString('hex'), signature: sig.toString('hex'),
ca: trust.caName, ca: trust.caName,
@ -1521,7 +1582,7 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) {
return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb); return self.receivePaymentRequestACK(ntxid, tx, txp, ack, cb);
}) })
.error(function(data, status, headers, config) { .error(function(data, status, headers, config) {
return cb(new Error('Status: ' + JSON.stringify(status))); return cb(new Error('Status: ' + status));
}); });
}; };
@ -1891,21 +1952,16 @@ Wallet.prototype.getAddressesStr = function(opts) {
}); });
}; };
Wallet.prototype.subscribeToAddresses = function() {
var addrInfo = this.publicKeyRing.getAddressesInfo();
this.blockchain.subscribe(_.pluck(addrInfo, 'addressStr'));
};
/** /**
* @desc Alias for {@link PublicKeyRing#getAddressesInfo} * @desc Alias for {@link PublicKeyRing#getAddressesInfo}
*/ */
Wallet.prototype.getAddressesInfo = function(opts) { Wallet.prototype.getAddressesInfo = function(opts) {
var addrInfo = this.publicKeyRing.getAddressesInfo(opts, this.publicKey); return this.publicKeyRing.getAddressesInfo(opts, this.publicKey);
var currentAddrs = this.blockchain.getSubscriptions();
var newAddrs = [];
for (var i in addrInfo) {
var a = addrInfo[i];
if (!currentAddrs[a.addressStr] && !a.isChange)
newAddrs.push(a.addressStr);
}
this.blockchain.subscribe(newAddrs);
return addrInfo;
}; };
/** /**
* @desc Returns true if a given address was generated by deriving our master public key * @desc Returns true if a given address was generated by deriving our master public key
@ -2293,11 +2349,14 @@ Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb)
/** /**
* @desc Closes the wallet and disconnects all services * @desc Closes the wallet and disconnects all services
*/ */
Wallet.prototype.close = function() { Wallet.prototype.close = function(cb) {
var self =this;
log.debug('## CLOSING'); log.debug('## CLOSING');
this.lock.release(); this.lock.release(function() {
this.network.cleanUp(); self.network.cleanUp();
this.blockchain.destroy(); self.blockchain.destroy();
if (cb) return cb();
});
}; };
/** /**
@ -2389,7 +2448,7 @@ Wallet.prototype.isShared = function() {
* @return {boolean} * @return {boolean}
*/ */
Wallet.prototype.isReady = function() { Wallet.prototype.isReady = function() {
var ret = this.publicKeyRing.isComplete() && this.publicKeyRing.isFullyBackup(); var ret = this.publicKeyRing.isComplete() && (this.publicKeyRing.isFullyBackup() || this.isImported || this.forcedLogin);
return ret; return ret;
}; };
@ -2398,7 +2457,8 @@ Wallet.prototype.isReady = function() {
* *
* Also backs up the wallet * Also backs up the wallet
*/ */
Wallet.prototype.setBackupReady = function() { Wallet.prototype.setBackupReady = function(forcedLogin) {
this.forcedLogin = forcedLogin;
this.publicKeyRing.setBackupReady(); this.publicKeyRing.setBackupReady();
this.sendPublicKeyRing(); this.sendPublicKeyRing();
this.store(); this.store();
@ -2436,15 +2496,6 @@ Wallet.prototype.verifySignedJson = function(senderId, payload, signature) {
return v; return v;
} }
// NOTE: Angular $http module does not send ArrayBuffers correctly, so we're
// not going to use it. We'll have to write our own. Otherwise, we could
// hex-encoded our messages and decode them on the other side, but that
// deviates from BIP-70.
// if (typeof angular !== 'undefined') {
// var $http = angular.bootstrap().get('$http');
// }
/** /**
* @desc Create a HTTP request * @desc Create a HTTP request
* @TODO: This shouldn't be a wallet responsibility * @TODO: This shouldn't be a wallet responsibility
@ -2510,7 +2561,13 @@ Wallet.request = function(options, callback) {
}; };
xhr.onerror = function(event) { xhr.onerror = function(event) {
return ret._error(null, new Error(event.message), null, options); var status;
if (xhr.status === 0 || !xhr.statusText) {
status = 'HTTP Request Error: This endpoint likely does not support cross-origin requests.';
} else {
status = xhr.statusText;
}
return ret._error(null, status, null, options);
}; };
if (req.body) { if (req.body) {
@ -2522,4 +2579,4 @@ Wallet.request = function(options, callback) {
return ret; return ret;
}; };
module.exports = Wallet; module.exports = Wallet;

View file

@ -1,4 +1,5 @@
'use strict'; 'use strict';
var preconditions = require('preconditions').singleton();
var TxProposals = require('./TxProposals'); var TxProposals = require('./TxProposals');
var PublicKeyRing = require('./PublicKeyRing'); var PublicKeyRing = require('./PublicKeyRing');
@ -6,9 +7,11 @@ var PrivateKey = require('./PrivateKey');
var Wallet = require('./Wallet'); var Wallet = require('./Wallet');
var _ = require('underscore'); var _ = require('underscore');
var log = require('../../log'); var log = require('../../log');
var PluginManager = require('./PluginManager');
var Async = module.exports.Async = require('../network/Async'); var Async = module.exports.Async = require('../network/Async');
var Insight = module.exports.Insight = require('../blockchain/Insight'); var Insight = module.exports.Insight = require('../blockchain/Insight');
var StorageLocalEncrypted = module.exports.StorageLocalEncrypted = require('../storage/LocalEncrypted'); var preconditions = require('preconditions').singleton();
var Storage = module.exports.Storage = require('../Storage');
/** /**
* @desc * @desc
@ -23,97 +26,103 @@ var StorageLocalEncrypted = module.exports.StorageLocalEncrypted = require('../s
* @param {Storage} config.Storage - the class to instantiate to store the wallet (StorageLocalEncrypted by default) * @param {Storage} config.Storage - the class to instantiate to store the wallet (StorageLocalEncrypted by default)
* @param {Object} config.storage - the configuration to be sent to the Storage constructor * @param {Object} config.storage - the configuration to be sent to the Storage constructor
* @param {Network} config.Network - the class to instantiate to make network requests to copayers (the Async module by default) * @param {Network} config.Network - the class to instantiate to make network requests to copayers (the Async module by default)
* @param {Object} config.network - the configuration to be sent to the Network constructor * @param {Object} config.network - the configurations to be sent to the Network and Blockchain constructors
* @param {Blockchain} config.Blockchain - the class to instantiate to get information about the blockchain (Insight by default) * @param {Blockchain} config.Blockchain - the class to instantiate to get information about the blockchain (Insight by default)
* @param {Object} config.blockchain - the configuration to be sent to the Blockchain constructor
* @param {string} config.networkName - the name of the bitcoin network to use ('testnet' or 'livenet')
* @TODO: Investigate what parameters go inside this object * @TODO: Investigate what parameters go inside this object
* @param {Object} config.wallet - default configuration for the wallet * @param {Object} config.wallet - default configuration for the wallet
* @TODO: put `version` inside of the config object * @TODO: put `version` inside of the config object
* @param {string} version - the version of copay for which this wallet was generated (for example, 0.4.7) * @param {string} version - the version of copay for which this wallet was generated (for example, 0.4.7)
* @constructor
*/ */
function WalletFactory(config, version) {
var self = this;
config = config || {};
this.Storage = config.Storage || StorageLocalEncrypted; function WalletFactory(config, version, pluginManager) {
var self = this;
preconditions.checkArgument(config);
preconditions.checkArgument(config.network);
this.Storage = config.Storage || Storage;
this.Network = config.Network || Async; this.Network = config.Network || Async;
this.Blockchain = config.Blockchain || Insight; this.Blockchain = config.Blockchain || Insight;
this.storage = new this.Storage(config.storage); var storageOpts = {};
this.network = new this.Network(config.network);
this.blockchain = new this.Blockchain(config.blockchain);
this.networkName = config.networkName; if (pluginManager) {
this.walletDefaults = config.wallet; storageOpts = {
storage: pluginManager.get('STORAGE')
};
}
this.storage = new this.Storage(storageOpts);
this.networks = {
'livenet': new this.Network(config.network.livenet),
'testnet': new this.Network(config.network.testnet),
};
this.blockchains = {
'livenet': new this.Blockchain(config.network.livenet),
'testnet': new this.Blockchain(config.network.testnet),
};
this.walletDefaults = config.wallet || {};
this.version = version; this.version = version;
}; };
/** /**
* @desc * @desc obtain network name from serialized wallet
* Returns true if the storage instance can retrieve the following keys using a given walletId * @param {Object} wallet object
* <ul> * @return {string} network name
* <li><tt>publicKeyRing</tt></li>
* <li><tt>txProposals</tt></li>
* <li><tt>opts</tt></li>
* <li><tt>privateKey</tt></li>
* </ul>
* @param {string} walletId
* @return {boolean} true if all the keys are present in the storage instance
*/ */
WalletFactory.prototype._checkRead = function(walletId) { WalletFactory.prototype.obtainNetworkName = function(obj) {
var s = this.storage; return obj.networkName ||
var ret = obj.opts.networkName ||
s.get(walletId, 'publicKeyRing') && obj.publicKeyRing.networkName ||
s.get(walletId, 'txProposals') && obj.privateKey.networkName;
s.get(walletId, 'opts') &&
s.get(walletId, 'privateKey');
return !!ret;
}; };
/** /**
* @desc Deserialize an object to a Wallet * @desc Deserialize an object to a Wallet
* @param {Object} obj * @param {Object} wallet object
* @param {string[]} skipFields - fields to skip when importing * @param {string[]} skipFields - fields to skip when importing
* @return {Wallet} * @return {Wallet}
*/ */
WalletFactory.prototype.fromObj = function(obj, skipFields) { WalletFactory.prototype.fromObj = function(inObj, skipFields) {
var networkName = this.obtainNetworkName(inObj);
preconditions.checkState(networkName);
preconditions.checkArgument(inObj);
var obj = JSON.parse(JSON.stringify(inObj));
// not stored options // not stored options
obj.opts = obj.opts || {};
obj.opts.reconnectDelay = this.walletDefaults.reconnectDelay; obj.opts.reconnectDelay = this.walletDefaults.reconnectDelay;
// this is only used if private key or public key ring is skipped
obj.opts.networkName = this.networkName;
skipFields = skipFields || []; skipFields = skipFields || [];
skipFields.forEach(function(k){ skipFields.forEach(function(k) {
if (obj[k]) { if (obj[k]) {
delete obj[k]; delete obj[k];
} else } else
throw new Error('unknown field:' + k); throw new Error('unknown field:' + k);
}); });
var w = Wallet.fromObj(obj, this.storage, this.network, this.blockchain); var w = Wallet.fromObj(obj, this.storage, this.networks[networkName], this.blockchains[networkName]);
if (!w) return false; if (!w) return false;
w.verbose = this.verbose;
this._checkVersion(w.version); this._checkVersion(w.version);
this._checkNetwork(w.getNetworkName());
return w; return w;
}; };
/** /**
* @desc Imports a wallet from an encrypted base64 object * @desc Imports a wallet from an encrypted base64 object
* @param {string} base64 - the base64 encoded object * @param {string} base64 - the base64 encoded object
* @param {string} password - password to decrypt it * @param {string} passphrase - passphrase to decrypt it
* @param {string[]} skipFields - fields to ignore when importing * @param {string[]} skipFields - fields to ignore when importing
* @return {Wallet} * @return {Wallet}
*/ */
WalletFactory.prototype.fromEncryptedObj = function(base64, password, skipFields) { WalletFactory.prototype.fromEncryptedObj = function(base64, passphrase, skipFields) {
this.storage._setPassphrase(password); this.storage.setPassphrase(passphrase);
var walletObj = this.storage.import(base64); var walletObj = this.storage.import(base64);
if (!walletObj) return false; if (!walletObj) return false;
var w = this.fromObj(walletObj, skipFields); return this.fromObj(walletObj, skipFields);
return w;
}; };
/** /**
@ -121,15 +130,15 @@ WalletFactory.prototype.fromEncryptedObj = function(base64, password, skipFields
* @TODO: this is essentialy the same method as {@link WalletFactory#fromEncryptedObj}! * @TODO: this is essentialy the same method as {@link WalletFactory#fromEncryptedObj}!
* @desc Imports a wallet from an encrypted base64 object * @desc Imports a wallet from an encrypted base64 object
* @param {string} base64 - the base64 encoded object * @param {string} base64 - the base64 encoded object
* @param {string} password - password to decrypt it * @param {string} passphrase - passphrase to decrypt it
* @param {string[]} skipFields - fields to ignore when importing * @param {string[]} skipFields - fields to ignore when importing
* @return {Wallet} * @return {Wallet}
*/ */
WalletFactory.prototype.import = function(base64, password, skipFields) { WalletFactory.prototype.import = function(base64, passphrase, skipFields) {
var self = this; var self = this;
var w = self.fromEncryptedObj(base64, password, skipFields); var w = self.fromEncryptedObj(base64, passphrase, skipFields);
if (!w) throw new Error('Wrong password'); if (!w) throw new Error('Wrong passphrase');
return w; return w;
}; };
@ -137,30 +146,52 @@ WalletFactory.prototype.import = function(base64, password, skipFields) {
* @desc Retrieve a wallet from storage * @desc Retrieve a wallet from storage
* @param {string} walletId - the wallet id * @param {string} walletId - the wallet id
* @param {string[]} skipFields - parameters to ignore when importing * @param {string[]} skipFields - parameters to ignore when importing
* @return {Wallet} * @param {function} callback - {err, Wallet}
*/ */
WalletFactory.prototype.read = function(walletId, skipFields) { WalletFactory.prototype.read = function(walletId, skipFields, cb) {
if (!this._checkRead(walletId)) var self = this,
return false; err;
var obj = {}; var obj = {};
var s = this.storage;
obj.id = walletId; this.storage.getMany(walletId, Wallet.PERSISTED_PROPERTIES, function(ret) {
obj.opts = s.get(walletId, 'opts'); for (var ii in ret) {
obj.publicKeyRing = s.get(walletId, 'publicKeyRing'); obj[ii] = ret[ii];
obj.txProposals = s.get(walletId, 'txProposals'); }
obj.privateKey = s.get(walletId, 'privateKey');
obj.addressBook = s.get(walletId, 'addressBook');
obj.backupOffered = s.get(walletId, 'backupOffered');
obj.lastTimestamp = s.get(walletId, 'lastTimestamp');
var w = this.fromObj(obj, skipFields); if (!_.any(_.values(obj)))
return w; return cb(new Error('Wallet not found'));
var w, err;
obj.id = walletId;
try {
w = self.fromObj(obj, skipFields);
} catch (e) {
if (e && e.message && e.message.indexOf('MISSOPTS')) {
err = new Error('Could not read: ' + walletId);
} else {
err = e;
}
w = null;
}
return cb(err, w);
});
};
/**
* @desc This method instantiates a wallet. Usefull for stubbing.
*
* @param {opts} opts, ready for new Wallet(opts)
*
*/
WalletFactory.prototype._getWallet = function(opts) {
return new Wallet(opts);
}; };
/** /**
* @desc This method instantiates a wallet * @desc This method prepares options for a new Wallet
* *
* @param {Object} opts * @param {Object} opts
* @param {string} opts.id * @param {string} opts.id
@ -176,17 +207,22 @@ WalletFactory.prototype.read = function(walletId, skipFields) {
* @TODO: Figure out in what unit is this reconnect delay. * @TODO: Figure out in what unit is this reconnect delay.
* @param {number} opts.reconnectDelay milliseconds? * @param {number} opts.reconnectDelay milliseconds?
* @param {number=} opts.version * @param {number=} opts.version
* @param {callback} opts.version
* @return {Wallet} * @return {Wallet}
*/ */
WalletFactory.prototype.create = function(opts) { WalletFactory.prototype.create = function(opts, cb) {
preconditions.checkArgument(cb);
opts = opts || {}; opts = opts || {};
opts.networkName = opts.networkName || 'testnet';
log.debug('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')); log.debug('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey'));
var privOpts = { var privOpts = {
networkName: this.networkName, networkName: opts.networkName,
}; };
if (opts.privateKeyHex && opts.privateKeyHex.length>1) { if (opts.privateKeyHex && opts.privateKeyHex.length > 1) {
privOpts.extendedPrivateKeyString = opts.privateKeyHex; privOpts.extendedPrivateKeyString = opts.privateKeyHex;
} }
@ -197,7 +233,7 @@ WalletFactory.prototype.create = function(opts) {
opts.lockTimeoutMin = this.walletDefaults.idleDurationMin; opts.lockTimeoutMin = this.walletDefaults.idleDurationMin;
opts.publicKeyRing = opts.publicKeyRing || new PublicKeyRing({ opts.publicKeyRing = opts.publicKeyRing || new PublicKeyRing({
networkName: this.networkName, networkName: opts.networkName,
requiredCopayers: requiredCopayers, requiredCopayers: requiredCopayers,
totalCopayers: totalCopayers, totalCopayers: totalCopayers,
}); });
@ -208,16 +244,14 @@ WalletFactory.prototype.create = function(opts) {
log.debug('\t### PublicKeyRing Initialized'); log.debug('\t### PublicKeyRing Initialized');
opts.txProposals = opts.txProposals || new TxProposals({ opts.txProposals = opts.txProposals || new TxProposals({
networkName: this.networkName, networkName: opts.networkName,
}); });
log.debug('\t### TxProposals Initialized'); log.debug('\t### TxProposals Initialized');
this.storage._setPassphrase(opts.passphrase);
opts.storage = this.storage; opts.storage = this.storage;
opts.network = this.network; opts.network = this.networks[opts.networkName];
opts.blockchain = this.blockchain; opts.blockchain = this.blockchains[opts.networkName];
opts.verbose = this.verbose;
opts.spendUnconfirmed = opts.spendUnconfirmed || this.walletDefaults.spendUnconfirmed; opts.spendUnconfirmed = opts.spendUnconfirmed || this.walletDefaults.spendUnconfirmed;
opts.reconnectDelay = opts.reconnectDelay || this.walletDefaults.reconnectDelay; opts.reconnectDelay = opts.reconnectDelay || this.walletDefaults.reconnectDelay;
@ -225,10 +259,15 @@ WalletFactory.prototype.create = function(opts) {
opts.totalCopayers = totalCopayers; opts.totalCopayers = totalCopayers;
opts.version = opts.version || this.version; opts.version = opts.version || this.version;
var w = new Wallet(opts); this.storage.setPassphrase(opts.passphrase);
w.store(); var w = this._getWallet(opts);
this.storage.setLastOpened(w.id); var self = this;
return w; w.store(function(err) {
if (err) return cb(err);
self.storage.setLastOpened(w.id, function(err) {
return cb(err, w);
});
});
}; };
/** /**
@ -245,20 +284,9 @@ WalletFactory.prototype._checkVersion = function(inVersion) {
//We only check for major version differences //We only check for major version differences
if (thisV0 < inV0) { if (thisV0 < inV0) {
throw new Error('Major difference in software versions' + throw new Error('Major difference in software versions' +
'. Received:' + inVersion + '. Received:' + inVersion +
'. Current version:' + this.version + '. Current version:' + this.version +
'. Aborting.'); '. Aborting.');
}
};
/**
* @desc Throw an error if the network name is different to {@link WalletFactory#networkName}
* @param {string} inNetworkName - the network name to check
* @throws {Error}
*/
WalletFactory.prototype._checkNetwork = function(inNetworkName) {
if (this.networkName !== inNetworkName) {
throw new Error('This Wallet is configured for ' + inNetworkName + ' while currently Copay is configured for: ' + this.networkName + '. Check your settings.');
} }
}; };
@ -266,28 +294,31 @@ WalletFactory.prototype._checkNetwork = function(inNetworkName) {
* @desc Retrieve a wallet from the storage * @desc Retrieve a wallet from the storage
* @param {string} walletId - the id of the wallet * @param {string} walletId - the id of the wallet
* @param {string} passphrase - the passphrase to decode it * @param {string} passphrase - the passphrase to decode it
* @return {Wallet} * @param {function} callback (err, {Wallet})
* @return
*/ */
WalletFactory.prototype.open = function(walletId, passphrase) { WalletFactory.prototype.open = function(walletId, passphrase, cb) {
this.storage._setPassphrase(passphrase); preconditions.checkArgument(cb);
var w = this.read(walletId); var self = this;
if (w) self.storage.setPassphrase(passphrase);
w.store(); self.read(walletId, null, function(err, w) {
if (err) return cb(err);
this.storage.setLastOpened(walletId); w.store(function(err) {
return w; self.storage.setLastOpened(walletId, function() {
return cb(err, w);
});
});
});
}; };
/** WalletFactory.prototype.getWallets = function(cb) {
* @desc Retrieve all wallets stored without encription in the storage instance this.storage.getWallets(function(ret) {
* @returns {Wallet[]} ret.forEach(function(i) {
*/ i.show = i.name ? ((i.name + ' <' + i.id + '>')) : i.id;
WalletFactory.prototype.getWallets = function() { });
var ret = this.storage.getWallets(); return cb(null, ret);
ret.forEach(function(i) {
i.show = i.name ? ((i.name + ' <' + i.id + '>')) : i.id;
}); });
return ret;
}; };
/** /**
@ -300,9 +331,12 @@ WalletFactory.prototype.getWallets = function() {
*/ */
WalletFactory.prototype.delete = function(walletId, cb) { WalletFactory.prototype.delete = function(walletId, cb) {
var s = this.storage; var s = this.storage;
s.deleteWallet(walletId); s.deleteWallet(walletId, function(err) {
s.setLastOpened(undefined); if (err) return cb(err);
return cb(); s.setLastOpened(null, function(err) {
return cb(err);
});
});
}; };
/** /**
@ -318,7 +352,7 @@ WalletFactory.prototype.decodeSecret = function(secret) {
/** /**
* @callback walletCreationCallback * @callback walletCreationCallback
* @param {?=} err - an error, if any, that happened during the wallet creation * @param {?} err - an error, if any, that happened during the wallet creation
* @param {Wallet=} wallet - the wallet created * @param {Wallet=} wallet - the wallet created
*/ */
@ -330,64 +364,86 @@ WalletFactory.prototype.decodeSecret = function(secret) {
* information locally using <tt>passphrase</tt>. <tt>privateHex</tt> is the * information locally using <tt>passphrase</tt>. <tt>privateHex</tt> is the
* private extended master key. <tt>cb</tt> has two params: error and wallet. * private extended master key. <tt>cb</tt> has two params: error and wallet.
* *
* @param {string} secret - the wallet secret * @param {object} opts
* @param {string} nickname - a nickname for the current user * @param {string} opts.secret - the wallet secret
* @param {string} passphrase - a passphrase to use to encrypt the wallet for persistance * @param {string} opts.passphrase - a passphrase to use to encrypt the wallet for persistance
* @param {string} privateHex - the private extended master key * @param {string} opts.nickname - a nickname for the current user
* @param {string} opts.privateHex - the private extended master key
* @param {walletCreationCallback} cb - a callback * @param {walletCreationCallback} cb - a callback
*/ */
WalletFactory.prototype.joinCreateSession = function(secret, nickname, passphrase, privateHex, cb) { WalletFactory.prototype.joinCreateSession = function(opts, cb) {
preconditions.checkArgument(opts);
preconditions.checkArgument(opts.secret);
preconditions.checkArgument(opts.passphrase);
preconditions.checkArgument(opts.nickname);
preconditions.checkArgument(cb);
var self = this; var self = this;
var s = self.decodeSecret(secret); var decodedSecret = this.decodeSecret(opts.secret);
if (!s) return cb('badSecret'); if (!decodedSecret || !decodedSecret.networkName || !decodedSecret.pubKey) {
return cb('badSecret');
}
var privOpts = { var privOpts = {
networkName: this.networkName, networkName: decodedSecret.networkName,
}; };
if (privateHex && privateHex.length>1) { if (opts.privateHex && opts.privateHex.length > 1) {
privOpts.extendedPrivateKeyString = privateHex; privOpts.extendedPrivateKeyString = privateHex;
} }
//Create our PrivateK //Create our PrivateK
var privateKey = new PrivateKey(privOpts); var privateKey = new PrivateKey(privOpts);
log.debug('\t### PrivateKey Initialized'); log.debug('\t### PrivateKey Initialized');
var opts = { var joinOpts = {
copayerId: privateKey.getId(), copayerId: privateKey.getId(),
privkey: privateKey.getIdPriv(), privkey: privateKey.getIdPriv(),
key: privateKey.getIdKey(), key: privateKey.getIdKey(),
secretNumber : s.secretNumber, secretNumber: decodedSecret.secretNumber,
}; };
self.network.cleanUp();
var joinNetwork = this.networks[decodedSecret.networkName];
joinNetwork.cleanUp();
// This is a hack to reconize if the connection was rejected or the peer wasn't there. // This is a hack to reconize if the connection was rejected or the peer wasn't there.
var connectedOnce = false; var connectedOnce = false;
self.network.on('connected', function(sender, data) { joinNetwork.on('connected', function(sender, data) {
connectedOnce = true; connectedOnce = true;
}); });
self.network.on('serverError', function() { joinNetwork.on('connect_error', function() {
return cb('connectionError');
});
joinNetwork.on('serverError', function() {
return cb('joinError'); return cb('joinError');
}); });
self.network.start(opts, function() { joinNetwork.start(joinOpts, function() {
self.network.greet(s.pubKey,opts.secretNumber);
self.network.on('data', function(sender, data) { joinNetwork.greet(decodedSecret.pubKey, joinOpts.secretNumber);
if (data.type === 'walletId') { joinNetwork.on('data', function(sender, data) {
if (data.networkName !== self.networkName) { if (data.type === 'walletId' && data.opts) {
if (data.networkName !== decodedSecret.networkName) {
return cb('badNetwork'); return cb('badNetwork');
} }
data.opts.privateKey = privateKey; var walletOpts = _.clone(data.opts);
data.opts.nickname = nickname; walletOpts.id = data.walletId;
data.opts.passphrase = passphrase;
data.opts.id = data.walletId; walletOpts.privateKey = privateKey;
var w = self.create(data.opts); walletOpts.nickname = opts.nickname;
w.sendWalletReady(s.pubKey); walletOpts.passphrase = opts.passphrase;
//w.seedCopayer(s.pubKey);
return cb(null, w); self.create(walletOpts, function(err, w) {
} else {
return cb('walletFull', w); if (w) {
w.sendWalletReady(decodedSecret.pubKey);
} else {
if (!err) err = 'walletFull';
log.info(err);
}
return cb(err, w);
});
} }
}); });
}); });

View file

@ -6,50 +6,95 @@ function WalletLock(storage, walletId, timeoutMin) {
preconditions.checkArgument(storage); preconditions.checkArgument(storage);
preconditions.checkArgument(walletId); preconditions.checkArgument(walletId);
this.sessionId = storage.getSessionId();
this.storage = storage; this.storage = storage;
this.timeoutMin = timeoutMin || 5; this.timeoutMin = timeoutMin || 5;
this.key = WalletLock._keyFor(walletId); this.key = WalletLock._keyFor(walletId);
this._setLock();
} }
WalletLock.prototype.init = function(cb) {
preconditions.checkArgument(cb);
var self = this;
self.storage.getSessionId(function(sid) {
preconditions.checkState(sid);
self.sessionId = sid;
cb();
});
};
WalletLock._keyFor = function(walletId) { WalletLock._keyFor = function(walletId) {
return 'lock' + '::' + walletId; return 'lock' + '::' + walletId;
}; };
WalletLock.prototype._isLockedByOther = function() { WalletLock.prototype._isLockedByOther = function(cb) {
var json = this.storage.getGlobal(this.key); var self = this;
var wl = json ? JSON.parse(json) : null;
var t = wl ? (Date.now() - wl.expireTs) : false;
// is not locked?
if (!wl || t > 0 || wl.sessionId === this.sessionId)
return false;
// Seconds remainding this.storage.getGlobal(this.key, function(json) {
return parseInt(-t/1000.); var wl = json ? JSON.parse(json) : null;
}; if (!wl || !wl.expireTs)
return cb(false);
var expiredSince = Date.now() - wl.expireTs;
if (expiredSince >= 0)
return cb(false);
WalletLock.prototype._setLock = function() { var isMyself = wl.sessionId === self.sessionId;
this.storage.setGlobal(this.key, {
sessionId: this.sessionId, if (isMyself)
expireTs: Date.now() + this.timeoutMin * 60 * 1000, return cb(false);
// Seconds remainding
return cb(parseInt(-expiredSince / 1000));
}); });
}; };
WalletLock.prototype.keepAlive = function() { WalletLock.prototype._setLock = function(cb) {
preconditions.checkArgument(cb);
preconditions.checkState(this.sessionId); preconditions.checkState(this.sessionId);
var self = this;
var t = this._isLockedByOther(); this.storage.setGlobal(this.key, {
if (t) sessionId: this.sessionId,
throw new Error('Wallet is already open. Close it to proceed or wait '+ t + ' seconds if you close it already' ); expireTs: Date.now() + this.timeoutMin * 60 * 1000,
this._setLock(); }, function() {
cb(null);
});
}; };
WalletLock.prototype.release = function() { WalletLock.prototype._doKeepAlive = function(cb) {
this.storage.removeGlobal(this.key); preconditions.checkArgument(cb);
preconditions.checkState(this.sessionId);
var self = this;
this._isLockedByOther(function(t) {
if (t)
return cb(new Error('LOCKED: Wallet is locked for ' + t + ' srcs'));
self._setLock(cb);
});
};
WalletLock.prototype.keepAlive = function(cb) {
var self = this;
if (!self.sessionId) {
return self.init(self._doKeepAlive.bind(self, cb));
};
return this._doKeepAlive(cb);
};
WalletLock.prototype.release = function(cb) {
this.storage.removeGlobal(this.key, cb);
}; };

View file

@ -11,12 +11,11 @@ var io = require('socket.io-client');
var preconditions = require('preconditions').singleton(); var preconditions = require('preconditions').singleton();
function Network(opts) { function Network(opts) {
var self = this; preconditions.checkArgument(opts);
preconditions.checkArgument(opts.url);
opts = opts || {}; opts = opts || {};
this.maxPeers = opts.maxPeers || 12; this.maxPeers = opts.maxPeers || 12;
this.host = opts.host || 'localhost'; this.url = opts.url;
this.port = opts.port || 3001;
this.schema = opts.schema || 'https';
this.secretNumber = opts.secretNumber; this.secretNumber = opts.secretNumber;
this.cleanUp(); this.cleanUp();
} }
@ -74,12 +73,12 @@ Network.prototype.connectedCopayers = function() {
return ret; return ret;
}; };
Network.prototype._sendHello = function(copayerId,secretNumber) { Network.prototype._sendHello = function(copayerId, secretNumber) {
this.send(copayerId, { this.send(copayerId, {
type: 'hello', type: 'hello',
copayerId: this.copayerId, copayerId: this.copayerId,
secretNumber : secretNumber secretNumber: secretNumber
}); });
}; };
@ -197,11 +196,10 @@ Network.prototype._onMessage = function(enc) {
var self = this; var self = this;
switch (payload.type) { switch (payload.type) {
case 'hello': case 'hello':
if (typeof payload.secretNumber === 'undefined' || payload.secretNumber !== this.secretNumber) if (typeof payload.secretNumber === 'undefined' || payload.secretNumber !== this.secretNumber) {
{
this._sendRejectConnection(sender); this._sendRejectConnection(sender);
this._deletePeer(enc.pubkey, 'incorrect secret number'); this._deletePeer(enc.pubkey, 'incorrect secret number');
return; return;
} }
// if we locked allowed copayers, check if it belongs // if we locked allowed copayers, check if it belongs
if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) { if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) {
@ -274,8 +272,8 @@ Network.prototype._onError = function(err) {
this.criticalError = err.message; this.criticalError = err.message;
}; };
Network.prototype.greet = function(copayerId,secretNumber) { Network.prototype.greet = function(copayerId, secretNumber) {
this._sendHello(copayerId,secretNumber); this._sendHello(copayerId, secretNumber);
var peerId = this.peerFromCopayer(copayerId); var peerId = this.peerFromCopayer(copayerId);
this._addCopayerMap(peerId, copayerId); this._addCopayerMap(peerId, copayerId);
}; };
@ -326,11 +324,10 @@ Network.prototype.start = function(opts, openCallback) {
}; };
Network.prototype.createSocket = function() { Network.prototype.createSocket = function() {
var hostPort = this.schema + '://' + this.host + ':' + this.port; return io.connect(this.url, {
return io.connect(hostPort, {
reconnection: true, reconnection: true,
'force new connection': true, 'force new connection': true,
'secure': this.schema === 'https', 'secure': this.url.indexOf('https') === 0,
}); });
}; };

View file

@ -1,208 +0,0 @@
'use strict';
var CryptoJS = require('node-cryptojs-aes').CryptoJS;
var bitcore = require('bitcore');
var preconditions = require('preconditions').instance();
var id = 0;
function Storage(opts) {
opts = opts || {};
this.__uniqueid = ++id;
if (opts.password)
this._setPassphrase(opts.password);
try {
this.localStorage = opts.localStorage || localStorage;
this.sessionStorage = opts.sessionStorage || sessionStorage;
} catch (e) {}
preconditions.checkState(this.localStorage, 'No localstorage found');
preconditions.checkState(this.sessionStorage, 'No sessionStorage found');
}
var pps = {};
Storage.prototype._getPassphrase = function() {
if (!pps[this.__uniqueid])
throw new Error('No passprase set');
return pps[this.__uniqueid];
}
Storage.prototype._setPassphrase = function(password) {
pps[this.__uniqueid] = password;
}
Storage.prototype._encrypt = function(string) {
var encrypted = CryptoJS.AES.encrypt(string, this._getPassphrase());
var encryptedBase64 = encrypted.toString();
return encryptedBase64;
};
Storage.prototype._decrypt = function(base64) {
var decryptedStr = null;
try {
var decrypted = CryptoJS.AES.decrypt(base64, this._getPassphrase());
if (decrypted)
decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
} catch (e) {
// Error while decrypting
return null;
}
return decryptedStr;
};
Storage.prototype._read = function(k) {
var ret;
ret = this.localStorage.getItem(k);
if (!ret) return null;
ret = this._decrypt(ret);
if (!ret) return null;
ret = ret.toString(CryptoJS.enc.Utf8);
ret = JSON.parse(ret);
return ret;
};
Storage.prototype._write = function(k, v) {
v = JSON.stringify(v);
v = this._encrypt(v);
this.localStorage.setItem(k, v);
};
// get value by key
Storage.prototype.getGlobal = function(k) {
var item = this.localStorage.getItem(k);
return item == 'undefined' ? undefined : item;
};
// set value for key
Storage.prototype.setGlobal = function(k, v) {
this.localStorage.setItem(k, typeof v === 'object' ? JSON.stringify(v) : v);
};
// remove value for key
Storage.prototype.removeGlobal = function(k) {
this.localStorage.removeItem(k);
};
Storage.prototype.getSessionId = function() {
var sessionId = this.sessionStorage.getItem('sessionId');
if (!sessionId) {
sessionId = bitcore.SecureRandom.getRandomBuffer(8).toString('hex');
this.sessionStorage.setItem('sessionId', sessionId);
}
return sessionId;
};
Storage.prototype._key = function(walletId, k) {
return walletId + '::' + k;
};
// get value by key
Storage.prototype.get = function(walletId, k) {
var ret = this._read(this._key(walletId, k));
return ret;
};
// set value for key
Storage.prototype.set = function(walletId, k, v) {
this._write(this._key(walletId, k), v);
};
// remove value for key
Storage.prototype.remove = function(walletId, k) {
this.removeGlobal(this._key(walletId, k));
};
Storage.prototype.setName = function(walletId, name) {
this.setGlobal('nameFor::' + walletId, name);
};
Storage.prototype.getName = function(walletId) {
var ret = this.getGlobal('nameFor::' + walletId);
return ret;
};
Storage.prototype.getWalletIds = function() {
var walletIds = [];
var uniq = {};
for (var i = 0; i < this.localStorage.length; i++) {
var key = this.localStorage.key(i);
var split = key.split('::');
if (split.length == 2) {
var walletId = split[0];
if (!walletId || walletId === 'nameFor' || walletId === 'lock')
continue;
if (typeof uniq[walletId] === 'undefined') {
walletIds.push(walletId);
uniq[walletId] = 1;
}
}
}
return walletIds;
};
Storage.prototype.getWallets = function() {
var wallets = [];
var ids = this.getWalletIds();
for (var i in ids) {
wallets.push({
id: ids[i],
name: this.getName(ids[i]),
});
}
return wallets;
};
Storage.prototype.deleteWallet = function(walletId) {
var toDelete = {};
toDelete['nameFor::' + walletId] = 1;
for (var i = 0; i < this.localStorage.length; i++) {
var key = this.localStorage.key(i);
var split = key.split('::');
if (split.length == 2 && split[0] === walletId) {
toDelete[key] = 1;
}
}
for (var i in toDelete) {
this.removeGlobal(i);
}
};
Storage.prototype.setLastOpened = function(walletId) {
this.setGlobal('lastOpened', walletId);
}
Storage.prototype.getLastOpened = function() {
return this.getGlobal('lastOpened');
}
//obj contains keys to be set
Storage.prototype.setFromObj = function(walletId, obj) {
for (var k in obj) {
this.set(walletId, k, obj[k]);
}
this.setName(walletId, obj.opts.name);
};
// remove all values
Storage.prototype.clearAll = function() {
this.localStorage.clear();
};
Storage.prototype.import = function(base64) {
var decryptedStr = this._decrypt(base64);
return JSON.parse(decryptedStr);
};
Storage.prototype.export = function(obj) {
var string = JSON.stringify(obj);
return this._encrypt(string);
};
module.exports = Storage;

View file

@ -76,8 +76,8 @@ angular
// IDLE timeout // IDLE timeout
var timeout = config.wallet.idleDurationMin * 60 || 300; var timeout = config.wallet.idleDurationMin * 60 || 300;
$idleProvider.idleDuration(timeout); // in seconds $idleProvider.idleDuration(timeout); // in seconds
$idleProvider.warningDuration(20); // in seconds $idleProvider.warningDuration(40); // in seconds
$keepaliveProvider.interval(2); // in seconds $keepaliveProvider.interval(30); // in seconds
}) })
.run(function($rootScope, $location, $idle, gettextCatalog) { .run(function($rootScope, $location, $idle, gettextCatalog) {
gettextCatalog.currentLanguage = config.defaultLanguage; gettextCatalog.currentLanguage = config.defaultLanguage;

View file

@ -9,14 +9,17 @@ BackupService.prototype.getName = function(wallet) {
return (wallet.name ? (wallet.name + '-') : '') + wallet.id; return (wallet.name ? (wallet.name + '-') : '') + wallet.id;
}; };
BackupService.prototype.getCopayer = function(wallet) {
return wallet.totalCopayers > 1 ? wallet.getMyCopayerNickname() : '';
};
BackupService.prototype.download = function(wallet) { BackupService.prototype.download = function(wallet) {
var ew = wallet.toEncryptedObj(); var ew = wallet.toEncryptedObj();
var partial = !wallet.publicKeyRing.isComplete(); var walletName = this.getName(wallet);
var walletName = this.getName(wallet) + (partial ? '-Partial' : ''); var copayerName = this.getCopayer(wallet);
var filename = walletName + '-keybackup.json.aes'; var filename = (copayerName ? copayerName + '-' : '') + walletName + '-keybackup.json.aes';
var notify = partial ? 'Partial backup created' : 'Backup created'; this.notifications.success('Backup created', 'Encrypted backup file saved');
this.notifications.success(notify, 'Encrypted backup file saved.');
var blob = new Blob([ew], { var blob = new Blob([ew], {
type: 'text/plain;charset=utf-8' type: 'text/plain;charset=utf-8'
}); });
@ -32,9 +35,8 @@ BackupService.prototype.download = function(wallet) {
// throw an email intent if we are in the mobile version // throw an email intent if we are in the mobile version
if (window.cordova) { if (window.cordova) {
var name = wallet.name ? wallet.name + ' ' : ''; var name = wallet.name ? wallet.name + ' ' : '';
var partial = partial ? 'Partial ' : '';
return window.plugin.email.open({ return window.plugin.email.open({
subject: 'Copay - ' + name + 'Wallet ' + partial + 'Backup', subject: 'Copay - ' + name + 'Wallet ' + 'Backup',
body: 'Here is the encrypted backup of the wallet ' + wallet.id, body: 'Here is the encrypted backup of the wallet ' + wallet.id,
attachments: ['base64:' + filename + '//' + btoa(ew)] attachments: ['base64:' + filename + '//' + btoa(ew)]
}); });

View file

@ -2,7 +2,7 @@
var bitcore = require('bitcore'); var bitcore = require('bitcore');
angular.module('copayApp.services') angular.module('copayApp.services')
.factory('controllerUtils', function($rootScope, $sce, $location, notification, $timeout, uriHandler, rateService) { .factory('controllerUtils', function($rootScope, $sce, $location, $filter, notification, $timeout, uriHandler, rateService) {
var root = {}; var root = {};
root.redirIfLogged = function() { root.redirIfLogged = function() {
@ -50,16 +50,18 @@ angular.module('copayApp.services')
$scope.loading = false; $scope.loading = false;
}); });
w.on('corrupt', function(peerId) { w.on('corrupt', function(peerId) {
notification.error('Error', 'Received corrupt message from ' + peerId); notification.error('Error', $filter('translate')('Received corrupt message from ') + peerId);
}); });
w.on('ready', function(myPeerID) { w.on('ready', function(myPeerID) {
$rootScope.wallet = w; $rootScope.wallet = w;
if ($rootScope.pendingPayment) { if ($rootScope.initialConnection) {
$location.path('send'); $rootScope.initialConnection = false;
} else { if ($rootScope.pendingPayment) {
$location.path('receive'); $location.path('send');
} else {
$location.path('receive');
}
} }
}); });
@ -70,8 +72,10 @@ angular.module('copayApp.services')
} }
}); });
w.on('tx', function(address) { w.on('tx', function(address, isChange) {
notification.funds('Funds received!', address); if (!isChange) {
notification.funds('Funds received!', address);
}
root.updateBalance(function() { root.updateBalance(function() {
$rootScope.$digest(); $rootScope.$digest();
}); });
@ -108,17 +112,17 @@ angular.module('copayApp.services')
}, 3000); }, 3000);
}); });
w.on('txProposalEvent', function(e) { w.on('txProposalEvent', function(e) {
var user = w.publicKeyRing.nicknameForCopayer(e.cId); var user = w.publicKeyRing.nicknameForCopayer(e.cId);
switch (e.type) { switch (e.type) {
case 'signed': case 'signed':
notification.info('Transaction Update', 'A transaction was signed by ' + user); notification.info('Transaction Update', $filter('translate')('A transaction was signed by') + ' ' + user);
break; break;
case 'rejected': case 'rejected':
notification.info('Transaction Update', 'A transaction was rejected by ' + user); notification.info('Transaction Update', $filter('translate')('A transaction was rejected by') + ' ' + user);
break; break;
case 'corrupt': case 'corrupt':
notification.error('Transaction Error', 'Received corrupt transaction from ' + user); notification.error('Transaction Error', $filter('translate')('Received corrupt transaction from') + ' ' + user);
break; break;
} }
}); });
@ -138,12 +142,13 @@ angular.module('copayApp.services')
uriHandler.register(); uriHandler.register();
$rootScope.unitName = config.unitName; $rootScope.unitName = config.unitName;
$rootScope.txAlertCount = 0; $rootScope.txAlertCount = 0;
$rootScope.initialConnection = true;
$rootScope.reconnecting = false; $rootScope.reconnecting = false;
$rootScope.isCollapsed = true; $rootScope.isCollapsed = true;
$rootScope.$watch('txAlertCount', function(txAlertCount) { $rootScope.$watch('txAlertCount', function(txAlertCount) {
if (txAlertCount && txAlertCount > 0) { if (txAlertCount && txAlertCount > 0) {
notification.info('New Transaction', ($rootScope.txAlertCount == 1) ? 'You have a pending transaction proposal' : 'You have ' + $rootScope.txAlertCount + ' pending transaction proposals', txAlertCount); notification.info('New Transaction', ($rootScope.txAlertCount == 1) ? 'You have a pending transaction proposal' : $filter('translate')('You have') + ' ' + $rootScope.txAlertCount + ' ' + $filter('translate')('pending transaction proposals'), txAlertCount);
} }
}); });
}; };
@ -159,8 +164,10 @@ angular.module('copayApp.services')
// TODO movie this to wallet // TODO movie this to wallet
root.updateAddressList = function() { root.updateAddressList = function() {
var w = $rootScope.wallet; var w = $rootScope.wallet;
if (w && w.isReady()) if (w && w.isReady()) {
w.subscribeToAddresses();
$rootScope.addrInfos = w.getAddressesInfo(); $rootScope.addrInfos = w.getAddressesInfo();
}
}; };
root.updateBalance = function(cb) { root.updateBalance = function(cb) {
@ -176,7 +183,7 @@ angular.module('copayApp.services')
w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat) { w.getBalance(function(err, balanceSat, balanceByAddrSat, safeBalanceSat) {
if (err) throw err; if (err) throw err;
var satToUnit = 1 / config.unitToSatoshi; var satToUnit = 1 / w.settings.unitToSatoshi;
var COIN = bitcore.util.COIN; var COIN = bitcore.util.COIN;
$rootScope.totalBalance = balanceSat * satToUnit; $rootScope.totalBalance = balanceSat * satToUnit;
@ -196,11 +203,10 @@ angular.module('copayApp.services')
$rootScope.updatingBalance = false; $rootScope.updatingBalance = false;
rateService.whenAvailable(function() { rateService.whenAvailable(function() {
$rootScope.totalBalanceAlternative = rateService.toFiat(balanceSat, config.alternativeIsoCode); $rootScope.totalBalanceAlternative = rateService.toFiat(balanceSat, w.settings.alternativeIsoCode);
$rootScope.alternativeIsoCode = config.alternativeIsoCode; $rootScope.alternativeIsoCode = w.settings.alternativeIsoCode;
$rootScope.lockedBalanceAlternative = rateService.toFiat(balanceSat - safeBalanceSat, config.alternativeIsoCode); $rootScope.lockedBalanceAlternative = rateService.toFiat(balanceSat - safeBalanceSat, w.settings.alternativeIsoCode);
$rootScope.alternativeConversionRate = rateService.toFiat(100000000, w.settings.alternativeIsoCode);
return cb ? cb() : null; return cb ? cb() : null;
}); });
}); });
@ -211,7 +217,7 @@ angular.module('copayApp.services')
if (!w) return; if (!w) return;
opts = opts || $rootScope.txsOpts || {}; opts = opts || $rootScope.txsOpts || {};
var satToUnit = 1 / config.unitToSatoshi; var satToUnit = 1 / w.settings.unitToSatoshi;
var myCopayerId = w.getMyCopayerId(); var myCopayerId = w.getMyCopayerId();
var pendingForUs = 0; var pendingForUs = 0;
var inT = w.getTxProposals().sort(function(t1, t2) { var inT = w.getTxProposals().sort(function(t1, t2) {
@ -235,7 +241,7 @@ angular.module('copayApp.services')
var tx = i.builder.build(); var tx = i.builder.build();
var outs = []; var outs = [];
tx.outs.forEach(function(o) { tx.outs.forEach(function(o) {
var addr = bitcore.Address.fromScriptPubKey(o.getScript(), config.networkName)[0].toString(); var addr = bitcore.Address.fromScriptPubKey(o.getScript(), w.getNetworkName())[0].toString();
if (!w.addressIsOwn(addr, { if (!w.addressIsOwn(addr, {
excludeMain: true excludeMain: true
})) { })) {

View file

@ -0,0 +1,18 @@
'use strict';
angular.module('copayApp.services').factory('pluginManager', function(angularLoad){
var pm = new copay.PluginManager(config);
var scripts = pm.scripts;
for(var ii in scripts){
var src = scripts[ii].src;
console.log('\tLoading ',src); //TODO
angularLoad.loadScript(src)
.then(scripts[ii].then || null)
.catch(function() {
throw new Error('Loading ' + src);
})
}
return pm;
});

View file

@ -4,6 +4,7 @@ var RateService = function(request) {
this.isAvailable = false; this.isAvailable = false;
this.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable or use service.whenAvailable'; this.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable or use service.whenAvailable';
this.SAT_TO_BTC = 1 / 1e8; this.SAT_TO_BTC = 1 / 1e8;
this.BTC_TO_SAT = 1e8;
var MINS_IN_HOUR = 60; var MINS_IN_HOUR = 60;
var MILLIS_IN_SECOND = 1000; var MILLIS_IN_SECOND = 1000;
var rateServiceConfig = config.rate; var rateServiceConfig = config.rate;
@ -62,7 +63,7 @@ RateService.prototype.fromFiat = function(amount, code) {
if (!this.isAvailable) { if (!this.isAvailable) {
throw new Error(this.UNAVAILABLE_ERROR); throw new Error(this.UNAVAILABLE_ERROR);
} }
return amount / this.rates[code] / this.SAT_TO_BTC; return amount / this.rates[code] * this.BTC_TO_SAT;
}; };
RateService.prototype.listAlternatives = function() { RateService.prototype.listAlternatives = function() {

View file

@ -1,3 +1,5 @@
'use strict'; 'use strict';
angular.module('copayApp.services').factory('walletFactory', function(pluginManager){
return new copay.WalletFactory(config, copay.version, pluginManager);
});
angular.module('copayApp.services').value('walletFactory', new copay.WalletFactory(config, copay.version));

18
jsdoc.conf.json Normal file
View file

@ -0,0 +1,18 @@
{
"tags": {
"allowUnknownTags": true
},
"source": {
"includePattern": ".+\\.js(doc)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": [],
"templates": {
"cleverLinks": false,
"monospaceLinks": false,
"default": {
"outputSourceFiles": true
},
"theme": "flatly"
}
}

View file

@ -28,6 +28,7 @@ module.exports = function(config) {
'lib/angular-route/angular-route.min.js', 'lib/angular-route/angular-route.min.js',
'lib/angular-foundation/mm-foundation.min.js', 'lib/angular-foundation/mm-foundation.min.js',
'lib/angular-foundation/mm-foundation-tpls.min.js', 'lib/angular-foundation/mm-foundation-tpls.min.js',
'lib/angular-load/angular-load.min.js',
'lib/angular-gettext/dist/angular-gettext.min.js', 'lib/angular-gettext/dist/angular-gettext.min.js',
'lib/inherits/inherits.js', 'lib/inherits/inherits.js',
'lib/bitcore.js', 'lib/bitcore.js',
@ -60,6 +61,7 @@ module.exports = function(config) {
'test/mocks/FakeWallet.js', 'test/mocks/FakeWallet.js',
'test/mocks/FakeBlockchainSocket.js', 'test/mocks/FakeBlockchainSocket.js',
'test/mocks/FakePayProServer.js', 'test/mocks/FakePayProServer.js',
'test/mocks/FakeLocalStorage.js',
'test/mocha.conf.js', 'test/mocha.conf.js',

View file

@ -9,7 +9,7 @@
"bugs": { "bugs": {
"url": "https://github.com/bitpay/copay/issues" "url": "https://github.com/bitpay/copay/issues"
}, },
"version": "0.5.0", "version": "0.6.1",
"dependencies": { "dependencies": {
"browser-request": "^0.3.2", "browser-request": "^0.3.2",
"inherits": "^2.0.1", "inherits": "^2.0.1",
@ -42,6 +42,7 @@
], ],
"devDependencies": { "devDependencies": {
"async": "0.9.0", "async": "0.9.0",
"bitcore": "0.1.36",
"blanket": "1.1.6", "blanket": "1.1.6",
"browser-pack": "2.0.1", "browser-pack": "2.0.1",
"browser-request": "0.3.2", "browser-request": "0.3.2",
@ -55,17 +56,18 @@
"express": "4.0.0", "express": "4.0.0",
"github-releases": "0.2.0", "github-releases": "0.2.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-angular-gettext": "^0.2.15",
"grunt-browserify": "2.0.8", "grunt-browserify": "2.0.8",
"grunt-cli": "^0.1.13", "grunt-cli": "^0.1.13",
"grunt-contrib-concat": "0.5.0", "grunt-contrib-concat": "0.5.0",
"grunt-contrib-cssmin": "0.10.0", "grunt-contrib-cssmin": "0.10.0",
"grunt-contrib-uglify": "^0.5.1", "grunt-contrib-uglify": "^0.5.1",
"grunt-contrib-watch": "0.5.3", "grunt-contrib-watch": "0.5.3",
"grunt-jsdoc": "^0.5.7",
"grunt-markdown": "0.5.0", "grunt-markdown": "0.5.0",
"bitcore": "0.1.36",
"grunt-mocha-test": "0.8.2", "grunt-mocha-test": "0.8.2",
"grunt-release": "^0.7.0",
"grunt-shell": "0.6.4", "grunt-shell": "0.6.4",
"grunt-angular-gettext": "^0.2.15",
"istanbul": "0.2.10", "istanbul": "0.2.10",
"karma": "0.12.9", "karma": "0.12.9",
"karma-chrome-launcher": "0.1.3", "karma-chrome-launcher": "0.1.3",

322
plugins/GoogleDrive.js Normal file
View file

@ -0,0 +1,322 @@
'use strict';
var preconditions = require('preconditions').singleton();
var loaded = 0;
var SCOPES = 'https://www.googleapis.com/auth/drive';
var log = require('../js/log');
function GoogleDrive(config) {
preconditions.checkArgument(config && config.clientId, 'No clientId at GoogleDrive config');
this.clientId = config.clientId;
this.home = config.home || 'copay';
this.idCache = {};
this.type = 'STORAGE';
this.scripts = [{
then: this.initLoaded.bind(this),
src: 'https://apis.google.com/js/client.js?onload=InitGoogleDrive'
}];
this.isReady = false;
this.useImmediate = true;
this.ts = 100;
};
window.InitGoogleDrive = function() {
log.debug('googleDrive loadeded'); //TODO
loaded = 1;
};
GoogleDrive.prototype.init = function() {};
/**
* Called when the client library is loaded to start the auth flow.
*/
GoogleDrive.prototype.initLoaded = function() {
if (!loaded) {
window.setTimeout(this.initLoaded.bind(this), 500);
} else {
window.setTimeout(this.checkAuth.bind(this), 1);
}
}
/**
* Check if the current user has authorized the application.
*/
GoogleDrive.prototype.checkAuth = function() {
log.debug('Google Drive: Checking Auth');
gapi.auth.authorize({
'client_id': this.clientId,
'scope': SCOPES,
'immediate': this.useImmediate,
},
this.handleAuthResult.bind(this));
};
/**
* Called when authorization server replies.
*/
GoogleDrive.prototype.handleAuthResult = function(authResult) {
var self = this;
log.debug('Google Drive: authResult', authResult); //TODO
if (authResult.error) {
if (authResult.error) {
self.useImmediate = false;
return this.checkAuth();
};
throw new Error(authResult.error);
}
gapi.client.load('drive', 'v2', function() {
self.isReady = true;
});
}
GoogleDrive.prototype.checkReady = function() {
if (!this.isReady)
throw new Error('goggle drive is not ready!');
};
GoogleDrive.prototype._httpGet = function(theUrl) {
var accessToken = gapi.auth.getToken().access_token;
var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", theUrl, false);
xmlHttp.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xmlHttp.send(null);
return xmlHttp.responseText;
}
GoogleDrive.prototype.getItem = function(k, cb) {
//console.log('[googleDrive.js.95:getItem:]', k); //TODO
var self = this;
self.checkReady();
self._idForName(k, function(kId) {
// console.log('[googleDrive.js.89:kId:]', kId); //TODO
if (!kId)
return cb(null);
var args = {
'path': '/drive/v2/files/' + kId,
'method': 'GET',
};
// console.log('[googleDrive.js.95:args:]', args); //TODO
var request = gapi.client.request(args);
request.execute(function(res) {
// console.log('[googleDrive.js.175:res:]', res); //TODO
if (!res || !res.downloadUrl)
return cb(null);
return cb(self._httpGet(res.downloadUrl));
});
});
};
GoogleDrive.prototype.setItem = function(k, v, cb) {
// console.log('[googleDrive.js.111:setItem:]', k, v); //TODO
var self = this;
self.checkReady();
self._idForName(this.home, function(parentId) {
preconditions.checkState(parentId);
// console.log('[googleDrive.js.118:parentId:]', parentId); //TODO
self._idForName(k, function(kId) {
// console.log('[googleDrive.js.105]', parentId, kId); //TODO
var boundary = '-------314159265358979323846';
var delimiter = "\r\n--" + boundary + "\r\n";
var close_delim = "\r\n--" + boundary + "--";
var metadata = {
'title': k,
'mimeType': 'application/octet-stream',
'parents': [{
'id': parentId
}],
};
var base64Data = btoa(v);
var multipartRequestBody =
delimiter +
'Content-Type: application/json\r\n\r\n' +
JSON.stringify(metadata) +
delimiter +
'Content-Type: application/octet-stream \r\n' +
'Content-Transfer-Encoding: base64\r\n' +
'\r\n' +
base64Data +
close_delim;
var args = {
'path': '/upload/drive/v2/files' + (kId ? '/' + kId : ''),
'method': kId ? 'PUT' : 'POST',
'params': {
'uploadType': 'multipart',
},
'headers': {
'Content-Type': 'multipart/mixed; boundary="' + boundary + '"'
},
'body': multipartRequestBody
}
// console.log('[googleDrive.js.148:args:]', args); //TODO
var request = gapi.client.request(args);
request.execute(function(ret) {
return cb(ret.kind === 'drive#file' ? null : new Error('error saving file on drive'));
});
});
});
};
GoogleDrive.prototype.removeItem = function(k, cb) {
var self = this;
self.checkReady();
self._idForName(this.home, function(parentId) {
preconditions.checkState(parentId);
self._idForName(k, function(kId) {
var args = {
'path': '/drive/v2/files/' + kId,
'method': 'DELETE',
};
var request = gapi.client.request(args);
request.execute(function() {
if (cb)
cb();
});
});
});
};
GoogleDrive.prototype.clear = function() {
this.checkReady();
throw new Error('clear not implemented');
};
GoogleDrive.prototype._mkdir = function(cb) {
preconditions.checkArgument(cb);
var self = this;
log.debug('Creating drive folder ' + this.home);
var request = gapi.client.request({
'path': '/drive/v2/files',
'method': 'POST',
'body': JSON.stringify({
'title': this.home,
'mimeType': "application/vnd.google-apps.folder",
}),
});
request.execute(function() {
self._idForName(self.home, cb);
});
};
GoogleDrive.prototype._idForName = function(name, cb) {
// console.log('[googleDrive.js.199:_idForName:]', name); //TODO
preconditions.checkArgument(name);
preconditions.checkArgument(cb);
var self = this;
if (!self.isReady) {
log.debug('Waiting for Google Drive');
self.ts = self.ts * 1.5;
return setTimeout(self._idForName.bind(self, name, cb), self.ts);
}
if (self.idCache[name]) {
// console.log('[googleDrive.js.212:] FROM CACHE', name, self.idCache[name]); //TODO
return cb(self.idCache[name]);
}
log.debug('GoogleDrive Querying for: ', name); //TODO
var args;
var idParent = name == this.home ? 'root' : self.idCache[this.home];
if (!idParent) {
return self._mkdir(function() {
self._idForName(name, cb);
});
}
// console.log('[googleDrive.js.177:idParent:]', idParent); //TODO
preconditions.checkState(idParent);
args = {
'path': '/drive/v2/files',
'method': 'GET',
'params': {
'q': "title='" + name + "' and trashed = false and '" + idParent + "' in parents",
}
};
var request = gapi.client.request(args);
request.execute(function(res) {
var i = res.items && res.items[0] ? res.items[0].id : false;
if (i)
self.idCache[name] = i;
// console.log('[googleDrive.js.238] CACHING ' + name + ':' + i); //TODO
return cb(self.idCache[name]);
});
};
GoogleDrive.prototype._checkHomeDir = function(cb) {
var self = this;
this._idForName(this.home, function(homeId) {
if (!homeId)
return self._mkdir(cb);
return cb(homeId);
});
};
GoogleDrive.prototype.allKeys = function(cb) {
var self = this;
this._checkHomeDir(function(homeId) {
preconditions.checkState(homeId);
var request = gapi.client.request({
'path': '/drive/v2/files',
'method': 'GET',
'params': {
'q': "'" + homeId + "' in parents and trashed = false",
'fields': 'items(id,title)'
},
});
request.execute(function(res) {
// console.log('[googleDrive.js.152:res:]', res); //TODO
if (res.error)
throw new Error(res.error.message);
var ret = [];
for (var ii in res.items) {
ret.push(res.items[ii].title);
}
return cb(ret);
});
});
};
GoogleDrive.prototype.key = function(k) {
var v = localStorage.key(k);
return v;
};
module.exports = GoogleDrive;

41
plugins/LocalStorage.js Normal file
View file

@ -0,0 +1,41 @@
'use strict';
function LocalStorage() {
this.type = 'STORAGE';
};
LocalStorage.prototype.init = function() {
};
LocalStorage.prototype.getItem = function(k,cb) {
return cb(localStorage.getItem(k));
};
LocalStorage.prototype.setItem = function(k,v,cb) {
localStorage.setItem(k,v);
return cb();
};
LocalStorage.prototype.removeItem = function(k,cb) {
localStorage.removeItem(k);
return cb();
};
LocalStorage.prototype.clear = function(cb) {
localStorage.clear();
return cb();
};
LocalStorage.prototype.allKeys = function(cb) {
var l = localStorage.length;
var ret = [];
for(var i=0; i<l; i++)
ret.push(localStorage.key(i));
return cb(ret);
};
module.exports = LocalStorage;

419
po/es.po
View file

@ -16,35 +16,33 @@ msgstr ""
msgid "(*) The limits are imposed by the bitcoin network." msgid "(*) The limits are imposed by the bitcoin network."
msgstr "(*) Los límites son impuestos por la red de bitcoin." msgstr "(*) Los límites son impuestos por la red de bitcoin."
#: views/dummy-translations.html
msgid "A transaction was rejected by"
msgstr "Una transacción fue rechazada por"
#: views/dummy-translations.html
msgid "A transaction was signed by"
msgstr "Una transacción fue firmada por"
#: views/more.html #: views/more.html
msgid "" msgid ""
"ALL Transactions Proposals will be discarted. This need to be done on " "ALL Transactions Proposals will be discarted. This needs to be done on "
"<b>ALL<b> peers of a wallet, to prevent the old proposals to be resynced " "<b>ALL</b> peers of a wallet, to prevent the old proposals to be resynced "
"again.\n" "again."
" </b></b>"
msgstr "" msgstr ""
"TODAS las Propuestas de Transacciones serán descartadas. Es necesario que lo " "TODAS las Propuestas de Transacciones serán descartadas. Es necesario que lo "
"hagan <b>TODOS<b> los compañeros del monedero, para prevenir que las viejas " "hagan <b>TODOS</b> los compañeros del monedero, para prevenir que las viejas "
"propuestas sean re sincronizadas de nuevo.\n" "propuestas sean re sincronizadas de nuevo."
" </b></b>"
#: views/modals/address-book.html #: views/send.html views/modals/address-book.html
msgid "Add Address" msgid "Add"
msgstr "Agregar Dirección" msgstr "Agregar"
#: views/modals/address-book.html
msgid "Add Address Book Entry"
msgstr "Nueva entrada"
#: views/send.html
msgid "Add New Entry"
msgstr "Nueva Entrada"
#: views/send.html views/modals/address-book.html #: views/send.html views/modals/address-book.html
msgid "Address" msgid "Address"
msgstr "Dirección" msgstr "Dirección"
#: views/send.html #: views/send.html views/modals/address-book.html
msgid "Address Book" msgid "Address Book"
msgstr "Libreta de Direcciones" msgstr "Libreta de Direcciones"
@ -52,7 +50,7 @@ msgstr "Libreta de Direcciones"
msgid "Addresses" msgid "Addresses"
msgstr "Direcciones" msgstr "Direcciones"
#: views/settings.html #: views/more.html
msgid "Alternative Currency" msgid "Alternative Currency"
msgstr "Moneda Alternativa" msgstr "Moneda Alternativa"
@ -81,6 +79,10 @@ msgstr "Volver"
msgid "Backup" msgid "Backup"
msgstr "Copia de Seguridad" msgstr "Copia de Seguridad"
#: views/dummy-translations.html
msgid "Backup created"
msgstr "Copia de Seguridad creada"
#: views/copayers.html #: views/copayers.html
msgid "Backup wallet" msgid "Backup wallet"
msgstr "Hacer copia de seguridad" msgstr "Hacer copia de seguridad"
@ -93,9 +95,9 @@ msgstr "Balance"
msgid "Balance locked in pending transaction proposals" msgid "Balance locked in pending transaction proposals"
msgstr "Balance bloqueado en las propuestas de transacción pendientes" msgstr "Balance bloqueado en las propuestas de transacción pendientes"
#: views/settings.html #: views/send.html
msgid "Bitcoin Network" msgid "Bitcoin address"
msgstr "Red Bitcoin" msgstr "Dirección bitcoin"
#: views/includes/transaction.html #: views/includes/transaction.html
msgid "Broadcast Transaction" msgid "Broadcast Transaction"
@ -113,11 +115,15 @@ msgstr "Cancelar"
msgid "Certificate:" msgid "Certificate:"
msgstr "Certificado:" msgstr "Certificado:"
#: views/create.html
msgid "Choose a password"
msgstr "Escribe una contraseña"
#: views/import.html #: views/import.html
msgid "Choose backup file from your computer" msgid "Choose backup file from your computer"
msgstr "Seleccione el archivo backup de su computadora" msgstr "Seleccione el archivo backup de su computadora"
#: views/create.html views/join.html #: views/join.html
msgid "Choose your password" msgid "Choose your password"
msgstr "Escribe tu contraseña" msgstr "Escribe tu contraseña"
@ -141,10 +147,21 @@ msgstr "Continuar de todas maneras"
msgid "Copayers" msgid "Copayers"
msgstr "Compañeros" msgstr "Compañeros"
#: views/dummy-translations.html
msgid "Copied to clipboard"
msgstr "Copiado al portapapeles"
#: views/modals/qr-address.html #: views/modals/qr-address.html
msgid "Copy to clipboard" msgid "Copy to clipboard"
msgstr "Copiar al portapapeles" msgstr "Copiar al portapapeles"
#: views/dummy-translations.html
msgid ""
"Could not connect to the Insight server. Check your settings and network "
"configuration"
msgstr ""
"No se pudo conectar con el servidor Insight. Verifica la configuración de red"
#: views/home.html #: views/home.html
msgid "Create a new wallet" msgid "Create a new wallet"
msgstr "Crear un nuevo monedero" msgstr "Crear un nuevo monedero"
@ -161,6 +178,12 @@ msgstr "Crear nuevo monedero"
msgid "Create {{requiredCopayers}}-of-{{totalCopayers}} wallet" msgid "Create {{requiredCopayers}}-of-{{totalCopayers}} wallet"
msgstr "Crea monedero {{requiredCopayers}}-de-{{totalCopayers}}" msgstr "Crea monedero {{requiredCopayers}}-de-{{totalCopayers}}"
#: views/copayers.html
msgid "Creating and storing a backup will allow you to recover wallet funds"
msgstr ""
"Crear y guardar una copia de seguridad le permitirá recuperar el dinero de "
"su monedero"
#: views/create.html #: views/create.html
msgid "Creating wallet..." msgid "Creating wallet..."
msgstr "Creando monedero..." msgstr "Creando monedero..."
@ -181,10 +204,6 @@ msgstr "Eliminar"
msgid "Delete Wallet" msgid "Delete Wallet"
msgstr "Borrar Monedero" msgstr "Borrar Monedero"
#: views/copayers.html
msgid "Delete wallet"
msgstr "Borrar monedero"
#: views/copayers.html #: views/copayers.html
msgid "Download Backup" msgid "Download Backup"
msgstr "Descargar Copia de Seguridad" msgstr "Descargar Copia de Seguridad"
@ -193,28 +212,48 @@ msgstr "Descargar Copia de Seguridad"
msgid "Download File" msgid "Download File"
msgstr "Descargar Archivo" msgstr "Descargar Archivo"
#: views/copayers.html
msgid "Download seed backup"
msgstr "Descargar copia de seguridad"
#: views/send.html #: views/send.html
msgid "Empty. Create an alias for your addresses" msgid "Empty. Create an alias for your addresses"
msgstr "Vacío. Crea una etiqueta para tus direcciones" msgstr "Vacío. Crea una etiqueta para tus direcciones"
#: views/dummy-translations.html
msgid "Encrypted backup file saved"
msgstr "Archivo de copia de seguridad encriptado guardado"
#: views/dummy-translations.html
msgid "Error updating indexes:"
msgstr "Error al actualizar índices:"
#: views/create.html #: views/create.html
msgid "Family vacation funds" msgid "Family vacation funds"
msgstr "Fondos para vacaciones en familia" msgstr "Fondos para vacaciones en familia"
#: views/dummy-translations.html
msgid "Fatal error connecting to Insight server"
msgstr "Error fatal al conectar con el servidor Insight"
#: views/transactions.html views/includes/transaction.html #: views/transactions.html views/includes/transaction.html
msgid "Fee" msgid "Fee"
msgstr "Tasa" msgstr "Tasa"
#: views/dummy-translations.html
msgid "Finished"
msgstr "Finalizado"
#: views/dummy-translations.html
msgid "Form Error"
msgstr "Error en formulario"
#: views/dummy-translations.html
msgid "Funds received!"
msgstr "¡Fondos recibidos!"
#: views/join.html views/send.html #: views/join.html views/send.html
msgid "Get QR code" msgid "Get QR code"
msgstr "Obtener código QR" msgstr "Obtener código QR"
#: views/copayers.html views/create.html views/import.html views/join.html #: views/create.html views/import.html views/join.html views/more.html
#: views/more.html views/transactions.html #: views/transactions.html
msgid "Hide" msgid "Hide"
msgstr "Ocultar" msgstr "Ocultar"
@ -230,29 +269,41 @@ msgstr ""
"Si todos los fondos fueron removidos de tu monedero y no deseas tener los " "Si todos los fondos fueron removidos de tu monedero y no deseas tener los "
"datos guardados en tu computadora, puedes eliminar tu monedero." "datos guardados en tu computadora, puedes eliminar tu monedero."
#: views/home.html #: views/dummy-translations.html views/home.html
msgid "Import a backup" msgid "Import a backup"
msgstr "Importar backup" msgstr "Importar una copia de seguridad"
#: views/import.html #: views/import.html
msgid "Import backup" msgid "Import backup"
msgstr "Importar copia de seguridad" msgstr "Importar copia de seguridad"
#: views/dummy-translations.html
msgid "Importing wallet - Reading backup..."
msgstr "Importando monedero - Leyendo archivo..."
#: views/dummy-translations.html
msgid "Importing wallet - Setting things up..."
msgstr "Importando monedero - Configurando..."
#: views/dummy-translations.html
msgid "Importing wallet - We are almost there..."
msgstr "Importando monedero - Finalizando..."
#: views/send.html #: views/send.html
msgid "Including fee of" msgid "Including fee of"
msgstr "Incluye tasa de" msgstr "Incluye tasa de"
#: views/settings.html #: views/settings.html
msgid "Insight API server" msgid "Insight API server"
msgstr "Servidor API Insight" msgstr "Servidor de API Insight"
#: views/settings.html #: views/settings.html
msgid "" msgid ""
"Insight API server is open-source software. You can run your own instance, " "Insight API server is open-source software. You can run your own instances, "
"check <a href=\"http://insight.is\" target=\"_blank\">Insight API Homepage</" "check <a href=\"http://insight.is\" target=\"_blank\">Insight API Homepage</"
"a>" "a>"
msgstr "" msgstr ""
"Servidor API de insight es un software código-abierto. Puedes correr tu " "Servidor de API insight es un software código-abierto. Puedes correr tu "
"propia instancia en <a href=\"http://insight.is\" target=\"_blank\">Insight " "propia instancia en <a href=\"http://insight.is\" target=\"_blank\">Insight "
"API Homepage</a>" "API Homepage</a>"
@ -260,13 +311,17 @@ msgstr ""
msgid "Insufficient funds" msgid "Insufficient funds"
msgstr "Fondos insuficientes" msgstr "Fondos insuficientes"
#: views/dummy-translations.html
msgid "It's important that you update your wallet at https://copay.io"
msgstr "Es importante que actualices tu monedero en https://copay.io"
#: views/more.html #: views/more.html
msgid "" msgid ""
"It's important to backup your wallet so that you can recover it in case of " "It's important to backup your wallet so that you can recover it in case of "
"disaster" "disaster"
msgstr "" msgstr ""
"Es importante hacer copia de seguridad de tu monedero para que puedas " "Es importante hacer copia de seguridad de tu monedero para que puedas "
"recuperarlo en caso de pérdidas" "recuperarlo en caso de pérdidas de datos de tu computadora"
#: views/join.html #: views/join.html
msgid "Join" msgid "Join"
@ -296,6 +351,14 @@ msgstr "Dejar mensaje privado a tus compañeros"
msgid "Locked" msgid "Locked"
msgstr "Bloqueado" msgstr "Bloqueado"
#: views/dummy-translations.html
msgid "Login Required"
msgstr "Inicio de Sesión Requerido"
#: views/includes/sidebar.html
msgid "Manual Update"
msgstr "Actualización Manual"
#: views/more.html #: views/more.html
msgid "Master Private Key" msgid "Master Private Key"
msgstr "Master Private Key" msgstr "Master Private Key"
@ -316,20 +379,22 @@ msgstr "Nombre"
msgid "Network Error. Attempting to reconnect..." msgid "Network Error. Attempting to reconnect..."
msgstr "Error de Red. Intentando reconectar..." msgstr "Error de Red. Intentando reconectar..."
#: views/settings.html #: views/dummy-translations.html
msgid "" msgid "Networking Error"
"Network has been fixed to <strong>{{networkName}}</strong> in this setup. " msgstr "Error de Red"
"See <a href=\"https://copay.io\">copay.io</a> for options to use Copay on "
"both livenet and testnet." #: views/dummy-translations.html
msgstr "" msgid "New Transaction"
"La red fue fijada a <strong>{{networkName}}</strong> para esta " msgstr "Nueva Transacción"
"configuración. Ver <a href=\"https://copay.io\">copay.io</a> para más "
"opciones de uso de Copay en livenet y testnet."
#: views/copayers.html #: views/copayers.html
msgid "New Wallet Created" msgid "New Wallet Created"
msgstr "Nuevo Monedero Creado" msgstr "Nuevo Monedero Creado"
#: views/dummy-translations.html
msgid "New entry has been created"
msgstr "Nueva entrada fue creada"
#: views/create.html #: views/create.html
msgid "Next" msgid "Next"
msgstr "Siguiente" msgstr "Siguiente"
@ -389,6 +454,10 @@ msgstr "Página no encontrada"
msgid "Password" msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
#: views/create.html views/join.html
msgid "Passwords must match"
msgstr "Las contraseñas deben coincidir"
#: views/join.html #: views/join.html
msgid "Paste wallet secret here" msgid "Paste wallet secret here"
msgstr "Pegar código secreto del monedero aquí" msgstr "Pegar código secreto del monedero aquí"
@ -399,25 +468,39 @@ msgstr "Vencimiento de Pago:"
#: views/more.html #: views/more.html
msgid "" msgid ""
"Pending Transactions Proposals will be discarted. This need to be done on " "Pending Transactions Proposals will be discarted. This needs to be done on "
"<b>ALL<b> peers of a wallet, to prevent the old proposals to be resynced " "<b>ALL</b> peers of a wallet, to prevent the old proposals to be resynced "
"again.\n" "again."
" </b></b>"
msgstr "" msgstr ""
"Las Propuestas de Transacciones Pendientes serán descartadas. Esto es " "Las Propuestas de Transacciones Pendientes serán descartadas. Esto es "
"necesario hacerlo con <b>TODOS</b> los compañeros del monedero, para " "necesario hacerlo con <b>TODOS</b> los compañeros del monedero, para "
"prevenir que viejas propuestas sean re sincronizadas de nuevo.\n" "prevenir que viejas propuestas sean re sincronizadas de nuevo."
" </b></b>"
#: views/settings.html #: views/dummy-translations.html
msgid "Port" msgid "Please complete required fields"
msgstr "Puerto" msgstr "Por favor complete los campos requeridos"
#: views/dummy-translations.html
msgid "Please enter the required fields"
msgstr "Por favor ingrese los campos requeridos"
#: views/dummy-translations.html
msgid "Please open wallet to complete payment"
msgstr "Por favor abrir un monedero para completar el pago"
#: views/dummy-translations.html
msgid "Please update your wallet at https://copay.io"
msgstr "Por favor actualiza tu monedero de https://copay.io"
#: views/dummy-translations.html
msgid "Please, select your backup file"
msgstr "Por favor, selecciona el archivo de copia de seguridad"
#: views/uri-payment.html #: views/uri-payment.html
msgid "Preparing payment..." msgid "Preparing payment..."
msgstr "Preparando pago..." msgstr "Preparando pago..."
#: views/create.html views/join.html #: views/join.html
msgid "Private Key (Hex)" msgid "Private Key (Hex)"
msgstr "Clave Privada (Hex)" msgstr "Clave Privada (Hex)"
@ -449,6 +532,14 @@ msgstr "Listo"
msgid "Receive" msgid "Receive"
msgstr "Recibir" msgstr "Recibir"
#: views/dummy-translations.html
msgid "Received corrupt message from"
msgstr "Se recibió un mensaje corrupto de"
#: views/dummy-translations.html
msgid "Received corrupt transaction from"
msgstr "Se recibió una transacción corrupta de"
#: views/includes/transaction.html #: views/includes/transaction.html
msgid "Reject" msgid "Reject"
msgstr "Rechazar" msgstr "Rechazar"
@ -457,11 +548,11 @@ msgstr "Rechazar"
msgid "Repeat password" msgid "Repeat password"
msgstr "Repite la contraseña" msgstr "Repite la contraseña"
#: views/create.html views/import.html views/join.html #: views/import.html views/join.html views/modals/address-book.html
msgid "Required" msgid "Required"
msgstr "Requerido" msgstr "Requerido"
#: views/settings.html #: views/more.html views/settings.html
msgid "Save" msgid "Save"
msgstr "Guardar" msgstr "Guardar"
@ -473,6 +564,10 @@ msgstr "Explorar"
msgid "Scan Wallet Addresses" msgid "Scan Wallet Addresses"
msgstr "Explorar Direcciones del Monedero" msgstr "Explorar Direcciones del Monedero"
#: views/dummy-translations.html
msgid "Scaning for transactions"
msgstr "Explorando transacciones"
#: views/import.html #: views/import.html
msgid "Select a backup file" msgid "Select a backup file"
msgstr "Seleccionar el archivo de copia de seguridad" msgstr "Seleccionar el archivo de copia de seguridad"
@ -485,6 +580,10 @@ msgstr "Seleccione las firmas requeridas (*)"
msgid "Select total number of copayers (*)" msgid "Select total number of copayers (*)"
msgstr "Seleccione el total de compañeros (*)" msgstr "Seleccione el total de compañeros (*)"
#: views/dummy-translations.html
msgid "Send"
msgstr "Enviar"
#: views/send.html #: views/send.html
msgid "Send Proposals" msgid "Send Proposals"
msgstr "Enviar Propuestas" msgstr "Enviar Propuestas"
@ -505,6 +604,18 @@ msgstr "Enviado"
msgid "Server Says:" msgid "Server Says:"
msgstr "Mensaje del Servidor:" msgstr "Mensaje del Servidor:"
#: views/dummy-translations.html
msgid "Session closed"
msgstr "Sesión cerrada"
#: views/dummy-translations.html
msgid "Session closed because a long time of inactivity"
msgstr "La sesión fue cerrada por mucho tiempo de inactividad"
#: views/dummy-translations.html
msgid "Session will be closed"
msgstr "La sesión se cerrará"
#: views/home.html views/more.html #: views/home.html views/more.html
msgid "Settings" msgid "Settings"
msgstr "Configuración" msgstr "Configuración"
@ -513,8 +624,8 @@ msgstr "Configuración"
msgid "Share this secret with your other copayers" msgid "Share this secret with your other copayers"
msgstr "Compartir el código secreto con tus otros compañeros" msgstr "Compartir el código secreto con tus otros compañeros"
#: views/copayers.html views/create.html views/import.html views/join.html #: views/create.html views/import.html views/join.html views/more.html
#: views/more.html views/transactions.html #: views/transactions.html
msgid "Show" msgid "Show"
msgstr "Mostrar" msgstr "Mostrar"
@ -530,6 +641,10 @@ msgstr "Ver menos"
msgid "Sign" msgid "Sign"
msgstr "Firmar" msgstr "Firmar"
#: views/copayers.html
msgid "Skip Backup"
msgstr "Saltear Copia de Seguridad"
#: views/import.html #: views/import.html
msgid "Skip public keys from peers" msgid "Skip public keys from peers"
msgstr "Ignorar claves pública de los compañeros" msgstr "Ignorar claves pública de los compañeros"
@ -542,6 +657,34 @@ msgstr "Ignorar propuestas de transacciones desde la Copia de Seguridad"
msgid "Skipping fields: {{skipFields}}" msgid "Skipping fields: {{skipFields}}"
msgstr "Saltear campos: {{skipFields}}" msgstr "Saltear campos: {{skipFields}}"
#: views/dummy-translations.html
msgid "Success"
msgstr "Listo"
#: views/dummy-translations.html
msgid "The balance is updated using the derived addresses"
msgstr "El balance es actualizado utilizando direcciones derivadas"
#: views/dummy-translations.html
msgid "The secret string you entered is invalid"
msgstr "La palabra secreta ingresada no es válida"
#: views/dummy-translations.html
msgid "The transaction proposal has been created"
msgstr "La propuesta de transacción fue creada"
#: views/dummy-translations.html
msgid "The wallet is full"
msgstr "El monedero esta completo"
#: views/dummy-translations.html
msgid "There was an error sending the transaction"
msgstr "Hubo un error al enviar la transacción"
#: views/dummy-translations.html
msgid "There was an error signing the transaction"
msgstr "Hubo un error al firmar la transacción"
#: views/warning.html #: views/warning.html
msgid "This wallet appears to be currently open." msgid "This wallet appears to be currently open."
msgstr "Este monedero parece estar actualmente abierto." msgstr "Este monedero parece estar actualmente abierto."
@ -559,8 +702,8 @@ msgstr ""
"sincronización de direcciones a los demás compañeros conectados." "sincronización de direcciones a los demás compañeros conectados."
#: views/send.html #: views/send.html
msgid "To address" msgid "To"
msgstr "Dirección" msgstr "A"
#: views/transactions.html #: views/transactions.html
msgid "Total" msgid "Total"
@ -570,30 +713,62 @@ msgstr "Total"
msgid "Total amount for this transaction:" msgid "Total amount for this transaction:"
msgstr "Cantidad total de esta transacción:" msgstr "Cantidad total de esta transacción:"
#: views/dummy-translations.html
msgid "Transaction Error"
msgstr "Error en Transacción"
#: views/includes/transaction.html #: views/includes/transaction.html
msgid "Transaction ID" msgid "Transaction ID"
msgstr "ID Transacción" msgstr "ID de Transacción"
#: views/transactions.html #: views/transactions.html
msgid "Transaction Proposals" msgid "Transaction Proposals"
msgstr "Propuestas de Transacción" msgstr "Propuestas de Transacción"
#: views/dummy-translations.html
msgid "Transaction Update"
msgstr "Actualización de una Transacción"
#: views/dummy-translations.html
msgid "Transaction broadcasted"
msgstr "Transacción transmitida"
#: views/includes/transaction.html #: views/includes/transaction.html
msgid "Transaction finally rejected" msgid "Transaction finally rejected"
msgstr "Transacción finalmente rechazada"
#: views/dummy-translations.html
msgid "Transaction rejected"
msgstr "Transacción rechazada" msgstr "Transacción rechazada"
#: views/settings.html #: views/dummy-translations.html
msgid "Use SSL" msgid "Transactions Proposals Purged"
msgstr "Usar SSL" msgstr "Propuestas de Transacciones Purgadas"
#: views/dummy-translations.html
msgid "Unable to send transaction proposal"
msgstr "No se puede enviar propuesta de transacción"
#: views/dummy-translations.html
msgid "Updating balance"
msgstr "Actualizando balance"
#: views/send.html #: views/send.html
msgid "Use all funds" msgid "Use all funds"
msgstr "Todos los fondos" msgstr "Todos los fondos"
#: views/create.html
msgid "Use test network"
msgstr "Red de prueba"
#: views/join.html #: views/join.html
msgid "User information" msgid "User information"
msgstr "Información de Usuario" msgstr "Información de Usuario"
#: views/dummy-translations.html
msgid "Using derived addresses from your wallet"
msgstr "Usando direcciones derivadas de tu monedero"
#: views/modals/address-book.html #: views/modals/address-book.html
msgid "Valid" msgid "Valid"
msgstr "Válido" msgstr "Válido"
@ -622,7 +797,7 @@ msgstr "Código Secreto del Monedero"
msgid "Wallet Secret is not valid!" msgid "Wallet Secret is not valid!"
msgstr "¡El código secreto no es válido!" msgstr "¡El código secreto no es válido!"
#: views/settings.html #: views/more.html
msgid "Wallet Unit" msgid "Wallet Unit"
msgstr "Unidad del monedero" msgstr "Unidad del monedero"
@ -630,13 +805,29 @@ msgstr "Unidad del monedero"
msgid "Wallet name" msgid "Wallet name"
msgstr "Nombre del monedero" msgstr "Nombre del monedero"
#: views/dummy-translations.html
msgid "Wallet network configuration missmatch"
msgstr "Configuración de la Red del monedero no coinciden"
#: views/warning.html #: views/warning.html
msgid "Warning!" msgid "Warning!"
msgstr "¡Advertencia!" msgstr "¡Advertencia!"
#: views/create.html #: views/dummy-translations.html
msgid "Your Wallet Password" msgid "Wrong password"
msgstr "Contraseña de tu Monedero" msgstr "Contraseña incorrecta"
#: views/dummy-translations.html
msgid "You have"
msgstr "Tienes"
#: views/dummy-translations.html
msgid "You have a pending transaction proposal"
msgstr "Tienes una propuesta de transacción pendiente"
#: views/dummy-translations.html
msgid "You rejected the transaction successfully"
msgstr "Rechazaste la transacción con éxito"
#: views/more.html #: views/more.html
msgid "" msgid ""
@ -654,19 +845,26 @@ msgstr "Tu nombre"
msgid "Your name (optional)" msgid "Your name (optional)"
msgstr "Tu nombre (opcional)" msgstr "Tu nombre (opcional)"
#: views/open.html #: views/create.html views/open.html
msgid "Your password" msgid "Your password"
msgstr "Tu contraseña" msgstr "Tu contraseña"
#: views/dummy-translations.html
msgid "Your session is about to expire due to inactivity in"
msgstr "Tu sesión está va a expirar por inactividad en"
#: views/import.html #: views/import.html
msgid "Your wallet password" msgid "Your wallet password"
msgstr "Contraseña de tu monedero" msgstr "Contraseña de tu monedero"
#: views/copayers.html views/create.html views/import.html views/join.html #: views/create.html views/import.html views/join.html views/more.html
#: views/more.html
msgid "advanced options" msgid "advanced options"
msgstr "opciones avanzadas" msgstr "opciones avanzadas"
#: views/dummy-translations.html
msgid "available."
msgstr "disponible."
#: views/addresses.html #: views/addresses.html
msgid "change" msgid "change"
msgstr "vuelto" msgstr "vuelto"
@ -676,8 +874,8 @@ msgid "first seen at"
msgstr "Visto el" msgstr "Visto el"
#: views/transactions.html #: views/transactions.html
msgid "mined at" msgid "mined"
msgstr "Minado el" msgstr "minado el"
#: views/send.html #: views/send.html
msgid "not valid" msgid "not valid"
@ -691,18 +889,30 @@ msgstr "de"
msgid "optional" msgid "optional"
msgstr "opcional" msgstr "opcional"
#: views/dummy-translations.html
msgid "pending transaction proposals"
msgstr "propuestas de transacciones pendientes"
#: views/copayers.html #: views/copayers.html
msgid "people have" msgid "people have"
msgstr "personas" msgstr "personas"
#: views/send.html views/modals/address-book.html #: views/send.html
msgid "required" msgid "required"
msgstr "requerido" msgstr "requerido"
#: views/dummy-translations.html
msgid "seconds"
msgstr "segundos"
#: views/send.html #: views/send.html
msgid "too long!" msgid "too long!"
msgstr "¡demasiado largo!" msgstr "¡demasiado largo!"
#: views/dummy-translations.html
msgid "transaction proposal purged"
msgstr "propuestas de transacciones purgadas"
#: views/send.html #: views/send.html
msgid "valid!" msgid "valid!"
msgstr "¡válido!" msgstr "¡válido!"
@ -719,8 +929,55 @@ msgstr "deben unirse"
msgid "{{tx.missingSignatures}} signatures missing" msgid "{{tx.missingSignatures}} signatures missing"
msgstr "Faltan {{tx.missingSignatures}} firmas" msgstr "Faltan {{tx.missingSignatures}} firmas"
#~ msgid "Send" #~ msgid "Scan Ended"
#~ msgstr "Enviar" #~ msgstr "Búsqueda Finalizada"
#~ msgid "There is an error in the form."
#~ msgstr "Hubo un error en el formulario."
#, fuzzy
#~ msgid "Wrong password que parece"
#~ msgstr "Contraseña incorrecta"
#~ msgid "Add Address"
#~ msgstr "Agregar Dirección"
#~ msgid "Add Address Book Entry"
#~ msgstr "Nueva entrada"
#~ msgid "Add New Entry"
#~ msgstr "Nueva Entrada"
#, fuzzy
#~ msgid "Your Password"
#~ msgstr "Tu contraseña"
#~ msgid "Bitcoin Network"
#~ msgstr "Red Bitcoin"
#~ msgid "Delete wallet"
#~ msgstr "Borrar monedero"
#~ msgid "Download seed backup"
#~ msgstr "Descargar copia de seguridad"
#~ msgid ""
#~ "Network has been fixed to <strong>{{networkName}}</strong> in this setup. "
#~ "See <a href=\"https://copay.io\">copay.io</a> for options to use Copay on "
#~ "both livenet and testnet."
#~ msgstr ""
#~ "La red fue fijada a <strong>{{networkName}}</strong> para esta "
#~ "configuración. Ver <a href=\"https://copay.io\">copay.io</a> para más "
#~ "opciones de uso de Copay en livenet y testnet."
#~ msgid "Port"
#~ msgstr "Puerto"
#~ msgid "Use SSL"
#~ msgstr "Usar SSL"
#~ msgid "Your Wallet Password"
#~ msgstr "Contraseña de tu Monedero"
#~ msgid "" #~ msgid ""
#~ "{{$root.wallet.requiredCopayers}}-of-{{$root.wallet.totalCopayers}} wallet" #~ "{{$root.wallet.requiredCopayers}}-of-{{$root.wallet.totalCopayers}} wallet"

View file

@ -11,7 +11,7 @@ FakeBlockchain.prototype.getTransaction = function(txid, cb) {
}; };
FakeBlockchain.prototype.getTransactions = function(addresses, cb) { FakeBlockchain.prototype.getTransactions = function(addresses, cb) {
return cb(null, []); cb(null, []);
}; };

View file

@ -1,27 +1,33 @@
//localstorage Mock //localstorage Mock
ls = {};
function LocalStorage(opts) {}
FakeLocalStorage = {}; function FakeLocalStorage() {
FakeLocalStorage.length = 0; this.ls = {};
FakeLocalStorage.removeItem = function(key) { };
delete ls[key]; FakeLocalStorage.prototype.removeItem = function(key, cb) {
this.length = Object.keys(ls).length; delete this.ls[key];
cb();
}; };
FakeLocalStorage.getItem = function(k) { FakeLocalStorage.prototype.getItem = function(k, cb) {
return ls[k]; return cb(this.ls[k]);
}; };
FakeLocalStorage.key = function(i) { FakeLocalStorage.prototype.allKeys = function(cb) {
return Object.keys(ls)[i]; return cb(Object.keys(this.ls));
}; };
FakeLocalStorage.setItem = function(k, v) { FakeLocalStorage.prototype.setItem = function(k, v, cb) {
ls[k] = v; this.ls[k] = v;
this.key[this.length] = k; return cb();
this.length = Object.keys(ls).length;
}; };
FakeLocalStorage.prototype.clear = function() {
this.ls = {};
}
module.exports = FakeLocalStorage; module.exports = FakeLocalStorage;
module.exports.storageParams = {
storage: new FakeLocalStorage(),
sessionStorage: new FakeLocalStorage(),
};

View file

@ -1,131 +0,0 @@
var FakeStorage = function() {
this.reset();
};
FakeStorage.prototype.reset = function(password) {
this.storage = {};
};
FakeStorage.prototype._setPassphrase = function(password) {
this.storage.passphrase = password;
};
FakeStorage.prototype.setGlobal = function(id, v) {
this.storage[id] = typeof v === 'object' ? JSON.stringify(v) : v;
};
FakeStorage.prototype.getGlobal = function(id) {
return this.storage[id];
};
FakeStorage.prototype.setLastOpened = function(val) {
this.storage['lastOpened'] = val;
};
FakeStorage.prototype.getLastOpened = function() {
return this.storage['lastOpened'];
};
FakeStorage.prototype.setLock = function(id) {
this.storage[id + '::lock'] = true;
}
FakeStorage.prototype.getLock = function(id) {
return this.storage[id + '::lock'];
}
FakeStorage.prototype.getSessionId = function() {
return this.sessionId || 'aSessionId';
};
FakeStorage.prototype.removeLock = function(id) {
delete this.storage[id + '::lock'];
}
FakeStorage.prototype.removeGlobal = function(id) {
delete this.storage[id];
};
FakeStorage.prototype.set = function(wid, id, payload) {
this.storage[wid + '::' + id] = payload;
};
FakeStorage.prototype.get = function(wid, id) {
return this.storage[wid + '::' + id];
};
FakeStorage.prototype.clear = function() {
delete this['storage'];
};
FakeStorage.prototype.getWalletIds = function() {
var walletIds = [];
var uniq = {};
for (var ii in this.storage) {
var split = ii.split('::');
if (split.length == 2) {
var walletId = split[0];
if (!walletId || walletId === 'nameFor' || walletId ==='lock')
continue;
if (typeof uniq[walletId] === 'undefined') {
walletIds.push(walletId);
uniq[walletId] = 1;
}
}
}
return walletIds;
};
FakeStorage.prototype.deleteWallet = function(walletId) {
var toDelete = {};
toDelete['nameFor::' + walletId] = 1;
for (var key in this.storage) {
var split = key.split('::');
if (split.length == 2 && split[0] === walletId) {
toDelete[key] = 1;
}
}
for (var i in toDelete) {
this.removeGlobal(i);
}
};
FakeStorage.prototype.getName = function(walletId) {
return this.getGlobal('nameFor::' + walletId);
};
FakeStorage.prototype.setName = function(walletId, name) {
this.setGlobal('nameFor::' + walletId, name);
};
FakeStorage.prototype.getWallets = function() {
var wallets = [];
var ids = this.getWalletIds();
for (var i in ids) {
wallets.push({
id: ids[i],
name: this.getName(ids[i]),
});
}
return wallets;
};
FakeStorage.prototype.setFromObj = function(walletId, obj) {
for (var k in obj) {
this.set(walletId, k, obj[k]);
}
this.setName(walletId, obj.opts.name);
};
module.exports = FakeStorage;

View file

@ -6,11 +6,10 @@ if (is_browser) {
} }
var Wallet = copay.Wallet; var Wallet = copay.Wallet;
var FakePrivateKey = function () { var FakePrivateKey = function() {};
};
FakePrivateKey.prototype.toObj = function() { FakePrivateKey.prototype.toObj = function() {
return extendedPublicKeyString = 'privHex'; return extendedPublicKeyString = 'privHex';
}; };
var FakeWallet = function() { var FakeWallet = function() {
@ -24,6 +23,7 @@ var FakeWallet = function() {
'1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC': 1000 '1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC': 1000
}; };
this.name = 'myTESTwullet'; this.name = 'myTESTwullet';
this.nickname = 'myNickname';
this.addressBook = { this.addressBook = {
'2NFR2kzH9NUdp8vsXTB4wWQtTtzhpKxsyoJ': { '2NFR2kzH9NUdp8vsXTB4wWQtTtzhpKxsyoJ': {
label: 'John', label: 'John',
@ -37,11 +37,21 @@ var FakeWallet = function() {
} }
}; };
this.blockchain = { this.blockchain = {
getSubscriptions: function(){ return []; }, getSubscriptions: function() {
subscribe: function(){} return [];
},
subscribe: function() {},
getTransactions: function() {}
}; };
this.privateKey = new FakePrivateKey(); this.privateKey = new FakePrivateKey();
this.settings = {
unitName: 'bits',
unitToSatoshi: 100,
unitDecimals: 2,
alternativeName: 'US Dollar',
alternativeIsoCode: 'USD',
};
}; };
FakeWallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) { FakeWallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts, cb) {
@ -52,6 +62,9 @@ FakeWallet.prototype.createTx = function(toAddress, amountSatStr, comment, opts,
FakeWallet.prototype.sendTx = function(ntxid, cb) { FakeWallet.prototype.sendTx = function(ntxid, cb) {
cb(8); cb(8);
} }
FakeWallet.prototype.getAddressesStr = function() {
return ['2Mw2YXxyMD7fhtPhHYY39X6BVWiBRaez5Zn'];
};
FakeWallet.prototype.set = function(balance, safeBalance, balanceByAddr) { FakeWallet.prototype.set = function(balance, safeBalance, balanceByAddr) {
this.balance = balance; this.balance = balance;
@ -72,6 +85,12 @@ FakeWallet.prototype.getAddressesInfo = function() {
return ret; return ret;
}; };
FakeWallet.prototype.subscribeToAddresses = function() {};
FakeWallet.prototype.getMyCopayerNickname = function() {
return this.nickname;
};
FakeWallet.prototype.isShared = function() { FakeWallet.prototype.isShared = function() {
return this.totalCopayers > 1; return this.totalCopayers > 1;
} }
@ -98,8 +117,7 @@ FakeWallet.prototype.getBalance = function(cb) {
return cb(null, this.balance, this.balanceByAddr, this.safeBalance); return cb(null, this.balance, this.balanceByAddr, this.safeBalance);
}; };
FakeWallet.prototype.removeTxWithSpentInputs = function (cb) { FakeWallet.prototype.removeTxWithSpentInputs = function(cb) {};
};
FakeWallet.prototype.setEnc = function(enc) { FakeWallet.prototype.setEnc = function(enc) {
this.enc = enc; this.enc = enc;
@ -109,7 +127,10 @@ FakeWallet.prototype.toEncryptedObj = function() {
return this.enc; return this.enc;
}; };
FakeWallet.prototype.close = function() { FakeWallet.prototype.close = function() {};
FakeWallet.prototype.getNetworkName = function() {
return 'testnet';
}; };
// TODO a try catch was here // TODO a try catch was here

View file

@ -1,249 +0,0 @@
'use strict';
var chai = chai || require('chai');
var should = chai.should();
var is_browser = typeof process == 'undefined' || typeof process.versions === 'undefined';
var copay = copay || require('../copay');
var LocalEncrypted = copay.StorageLocalEncrypted;
var fakeWallet = 'fake-wallet-id';
var timeStamp = Date.now();
var localMock = require('./mocks/FakeLocalStorage');
var sessionMock = require('./mocks/FakeLocalStorage');
describe('Storage/LocalEncrypted model', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
});
s._setPassphrase('mysupercoolpassword');
it('should create an instance', function() {
var s2 = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
});
should.exist(s2);
});
it('should fail when encrypting without a password', function() {
var s2 = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
});
(function() {
s2.set(fakeWallet, timeStamp, 1);
}).should.throw();
});
it('should be able to encrypt and decrypt', function() {
s._write(fakeWallet + timeStamp, 'value');
s._read(fakeWallet + timeStamp).should.equal('value');
localMock.removeItem(fakeWallet + timeStamp);
});
it('should be able to set a value', function() {
s.set(fakeWallet, timeStamp, 1);
localMock.removeItem(fakeWallet + '::' + timeStamp);
});
var getSetData = [
1, 1000, -15, -1000,
0.1, -0.5, -0.5e-10, Math.PI,
'hi', 'auydoaiusyodaisudyoa', '0b5b8556a0c2ce828c9ccfa58b3dd0a1ae879b9b',
'1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC', 'OP_DUP OP_HASH160 80ad90d4035', [1, 2, 3, 4, 5, 6], {
x: 1,
y: 2
}, {
x: 'hi',
y: null
}, {
a: {},
b: [],
c: [1, 2, 'hi']
},
null
];
getSetData.forEach(function(obj) {
it('should be able to set a value and get it for ' + JSON.stringify(obj), function() {
s.set(fakeWallet, timeStamp, obj);
var obj2 = s.get(fakeWallet, timeStamp);
JSON.stringify(obj2).should.equal(JSON.stringify(obj));
localMock.removeItem(fakeWallet + '::' + timeStamp);
});
});
describe('#export', function() {
it('should export the encrypted wallet', function() {
var storage = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password',
});
storage.set(fakeWallet, timeStamp, 'testval');
var obj = {
test: 'testval'
};
var encrypted = storage.export(obj);
encrypted.length.should.be.greaterThan(10);
localMock.removeItem(fakeWallet + '::' + timeStamp);
//encrypted.slice(0,6).should.equal("53616c");
});
});
describe('#remove', function() {
it('should remove an item', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.set('1', "hola", 'juan');
s.get('1', 'hola').should.equal('juan');
s.remove('1', 'hola');
should.not.exist(s.get('1', 'hola'));
});
});
describe('#getWalletIds', function() {
it('should get wallet ids', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.set('1', "hola", 'juan');
s.set('2', "hola", 'juan');
s.getWalletIds().should.deep.equal(['1', '2']);
});
});
describe('#getName #setName', function() {
it('should get/set names', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.setName(1, 'hola');
s.getName(1).should.equal('hola');
});
});
describe('#getLastOpened #setLastOpened', function() {
it('should get/set names', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.setLastOpened('hey');
s.getLastOpened().should.equal('hey');
});
});
if (is_browser) {
describe('#getSessionId', function() {
it('should get SessionId', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
var sid = s.getSessionId();
should.exist(sid);
var sid2 = s.getSessionId();
sid2.should.equal(sid);
});
});
}
describe('#getWallets', function() {
it('should retreive wallets from storage', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.set('1', "hola", 'juan');
s.set('2', "hola", 'juan');
s.setName(1, 'hola');
s.getWallets()[0].should.deep.equal({
id: '1',
name: 'hola',
});
s.getWallets()[1].should.deep.equal({
id: '2',
name: undefined
});
});
});
describe('#deleteWallet', function() {
it('should delete a wallet', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.set('1', "hola", 'juan');
s.set('2', "hola", 'juan');
s.setName(1, 'hola');
s.deleteWallet('1');
s.getWallets().length.should.equal(1);
s.getWallets()[0].should.deep.equal({
id: '2',
name: undefined
});
});
});
describe('#setFromObj', function() {
it('set localstorage from an object', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.setFromObj('id1', {
'key': 'val',
'opts': {
'name': 'nameid1'
},
});
s.get('id1', 'key').should.equal('val');
});
});
describe('#globals', function() {
it('should set, get and remove keys', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.setGlobal('a', {
b: 1
});
JSON.parse(s.getGlobal('a')).should.deep.equal({
b: 1
});
s.removeGlobal('a');
should.not.exist(s.getGlobal('a'));
});
});
describe('session storage', function() {
it('should get a session ID', function() {
var s = new LocalEncrypted({
localStorage: localMock,
sessionStorage: sessionMock,
password: 'password'
});
s.getSessionId().length.should.equal(16);
(new Buffer(s.getSessionId(),'hex')).length.should.equal(8);
});
});
});

View file

@ -36,5 +36,4 @@ describe('Passphrase model', function() {
done(); done();
}); });
}); });
}); });

View file

@ -11,7 +11,6 @@ if (is_browser) {
} }
var Wallet = copay.Wallet; var Wallet = copay.Wallet;
var PrivateKey = copay.PrivateKey; var PrivateKey = copay.PrivateKey;
var Storage = require('./mocks/FakeStorage');
var Network = require('./mocks/FakeNetwork'); var Network = require('./mocks/FakeNetwork');
var Blockchain = require('./mocks/FakeBlockchain'); var Blockchain = require('./mocks/FakeBlockchain');
var bitcore = bitcore || require('bitcore'); var bitcore = bitcore || require('bitcore');
@ -21,6 +20,10 @@ var Address = bitcore.Address;
var PayPro = bitcore.PayPro; var PayPro = bitcore.PayPro;
var bignum = bitcore.Bignum; var bignum = bitcore.Bignum;
var startServer = copay.FakePayProServer; // TODO should be require('./mocks/FakePayProServer'); var startServer = copay.FakePayProServer; // TODO should be require('./mocks/FakePayProServer');
var localMock = require('./mocks/FakeLocalStorage');
var sessionMock = require('./mocks/FakeLocalStorage');
var Storage = copay.Storage;
var server; var server;
@ -30,6 +33,7 @@ var walletConfig = {
spendUnconfirmed: true, spendUnconfirmed: true,
reconnectDelay: 100, reconnectDelay: 100,
networkName: 'testnet', networkName: 'testnet',
storage: require('./mocks/FakeLocalStorage').storageParams,
}; };
var getNewEpk = function() { var getNewEpk = function() {
@ -41,6 +45,7 @@ var getNewEpk = function() {
}; };
describe('PayPro (in Wallet) model', function() { describe('PayPro (in Wallet) model', function() {
if (!is_browser) { if (!is_browser) {
var createW = function(N, conf) { var createW = function(N, conf) {
var c = JSON.parse(JSON.stringify(conf || walletConfig)); var c = JSON.parse(JSON.stringify(conf || walletConfig));
@ -64,6 +69,7 @@ describe('PayPro (in Wallet) model', function() {
}); });
var storage = new Storage(walletConfig.storage); var storage = new Storage(walletConfig.storage);
storage.setPassphrase('xxx');
var network = new Network(walletConfig.network); var network = new Network(walletConfig.network);
var blockchain = new Blockchain(walletConfig.blockchain); var blockchain = new Blockchain(walletConfig.blockchain);
c.storage = storage; c.storage = storage;
@ -86,7 +92,6 @@ describe('PayPro (in Wallet) model', function() {
}; };
c.networkName = walletConfig.networkName; c.networkName = walletConfig.networkName;
c.verbose = walletConfig.verbose;
c.version = '0.0.1'; c.version = '0.0.1';
return new Wallet(c); return new Wallet(c);

321
test/test.Storage.js Normal file
View file

@ -0,0 +1,321 @@
'use strict';
var chai = chai || require('chai');
var sinon = require('sinon');
var should = chai.should();
var is_browser = typeof process == 'undefined' || typeof process.versions === 'undefined';
var copay = copay || require('../copay');
var Storage = copay.Storage;
var fakeWallet = 'fake-wallet-id';
var timeStamp = Date.now();
describe('Storage model', function() {
var s;
beforeEach(function() {
s = new Storage(require('./mocks/FakeLocalStorage').storageParams);
s.setPassphrase('mysupercoolpassword');
s.storage.clear();
s.sessionStorage.clear();
});
it('should create an instance', function() {
var s2 = new Storage(require('./mocks/FakeLocalStorage').storageParams);
should.exist(s2);
});
it('should fail when encrypting without a password', function() {
var s2 = new Storage(require('./mocks/FakeLocalStorage').storageParams);
(function() {
s2.set(fakeWallet, timeStamp, 1, function() {});
}).should.throw('NOPASSPHRASE');
});
it('should be able to encrypt and decrypt', function(done) {
s._write(fakeWallet + timeStamp, 'value', function() {
s._read(fakeWallet + timeStamp, function(v) {
v.should.equal('value');
done();
});
});
});
it('should be able to set a value', function(done) {
s.set(fakeWallet, timeStamp, 1, function() {
done();
});
});
var getSetData = [
1, 1000, -15, -1000,
0.1, -0.5, -0.5e-10, Math.PI,
'hi', 'auydoaiusyodaisudyoa', '0b5b8556a0c2ce828c9ccfa58b3dd0a1ae879b9b',
'1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC', 'OP_DUP OP_HASH160 80ad90d4035', [1, 2, 3, 4, 5, 6], {
x: 1,
y: 2
}, {
x: 'hi',
y: null
}, {
a: {},
b: [],
c: [1, 2, 'hi']
},
null
];
getSetData.forEach(function(obj) {
it('should be able to set a value and get it for ' + JSON.stringify(obj), function(done) {
s.set(fakeWallet, timeStamp, obj, function() {
s.get(fakeWallet, timeStamp, function(obj2) {
JSON.stringify(obj2).should.equal(JSON.stringify(obj));
done();
});
});
});
});
describe('#export', function() {
it('should export the encrypted wallet', function(done) {
s.set(fakeWallet, timeStamp, 'testval', function() {
var obj = {
test: 'testval'
};
var encrypted = s.export(obj);
encrypted.length.should.be.greaterThan(10);
done();
});
});
});
describe('#remove', function() {
it('should remove an item', function(done) {
s.set('1', "hola", 'juan', function() {
s.get('1', 'hola', function(v) {
v.should.equal('juan');
s.remove('1', 'hola', function() {
s.get('1', 'hola', function(v) {
should.not.exist(v);
done();
});
});
})
})
});
});
describe('#getWalletIds', function() {
it('should get wallet ids', function(done) {
s.set('1', "hola", 'juan', function() {
s.set('2', "hola", 'juan', function() {
s.getWalletIds(function(v) {
v.should.deep.equal(['1', '2']);
done();
});
});
});
});
});
describe('#getName #setName', function() {
it('should get/set names', function(done) {
s.setName(1, 'hola', function() {
s.getName(1, function(v) {
v.should.equal('hola');
done();
});
});
});
});
describe('#getLastOpened #setLastOpened', function() {
it('should get/set last opened', function() {
s.setLastOpened('hey', function() {
s.getLastOpened(function(v) {
v.should.equal('hey');
});
});
});
});
if (is_browser) {
describe('#getSessionId', function() {
it('should get SessionId', function(done) {
s.getSessionId(function(sid) {
should.exist(sid);
s.getSessionId(function(sid2) {
sid2.should.equal(sid);
done();
});
});
});
});
}
describe('#getWallets', function() {
it('should retreive wallets from storage', function(done) {
s.set('1', "hola", 'juan', function() {
s.set('2', "hola", 'juan', function() {
s.setName(1, 'hola', function() {
s.getWallets(function(ws) {
ws[0].should.deep.equal({
id: '1',
name: 'hola',
});
ws[1].should.deep.equal({
id: '2',
name: undefined
});
done();
});
});
});
});
});
it('should retreive wallets from storage (with delay)', function(done) {
s.set('1', "hola", 'juan', function() {
s.set('2', "hola", 'juan', function() {
s.setName(1, 'hola', function() {
var orig = s.getName.bind(s);
s.getName = function(wid, cb) {
setTimeout(function() {
orig(wid, cb);
},1);
};
s.getWallets(function(ws) {
ws[0].should.deep.equal({
id: '1',
name: 'hola',
});
ws[1].should.deep.equal({
id: '2',
name: undefined
});
done();
});
});
});
});
});
});
describe('#deleteWallet', function() {
it('should fail to delete a unexisting wallet', function(done) {
s.set('1', "hola", 'juan', function() {
s.set('2', "hola", 'juan', function() {
s.deleteWallet('3', function(err) {
err.toString().should.include('WNOTFOUND');
done();
});
});
});
});
it('should delete a wallet', function(done) {
s.set('1', "hola", 'juan', function() {
s.set('2', "hola", 'juan', function() {
s.deleteWallet('1', function(err) {
should.not.exist(err);
s.getWallets(function(ws) {
ws.length.should.equal(1);
ws[0].should.deep.equal({
id: '2',
name: undefined
});
done();
});
});
});
});
});
});
describe('#setFromObj', function() {
it('set localstorage from an object', function(done) {
s.setFromObj('id1', {
'key': 'val',
'opts': {
'name': 'nameid1'
},
}, function() {
s.get('id1', 'key', function(v) {
v.should.equal('val');
done();
});
});
});
});
describe('#globals', function() {
it('should set, get and remove keys', function(done) {
s.setGlobal('a', {
b: 1
}, function() {
s.getGlobal('a', function(v) {
JSON.parse(v).should.deep.equal({
b: 1
});
s.removeGlobal('a', function() {
s.getGlobal('a', function(v) {
should.not.exist(v);
done();
});
});
});
});
});
});
describe('session storage', function() {
it('should get a session ID', function(done) {
s.getSessionId(function(s) {
should.exist(s);
s.length.should.equal(16);
(new Buffer(s, 'hex')).length.should.equal(8);
done();
});
});
});
describe('#import', function() {
it('should not be able to decrypt with wrong password', function() {
s.setPassphrase('xxx');
var wo = s.import(encryptedLegacy1);
should.not.exist(wo);
});
it('should be able to decrypt an old backup', function() {
s.setPassphrase(legacyPassword1);
var wo = s.import(encryptedLegacy1);
should.exist(wo);
wo.opts.id.should.equal('48ba2f1ffdfe9708');
wo.opts.spendUnconfirmed.should.equal(true);
wo.opts.requiredCopayers.should.equal(1);
wo.opts.totalCopayers.should.equal(1);
wo.opts.name.should.equal('pepe wallet');
wo.opts.version.should.equal('0.4.7');
wo.publicKeyRing.walletId.should.equal('48ba2f1ffdfe9708');
wo.publicKeyRing.networkName.should.equal('testnet');
wo.publicKeyRing.requiredCopayers.should.equal(1);
wo.publicKeyRing.totalCopayers.should.equal(1);
wo.publicKeyRing.indexes.length.should.equal(2);
JSON.stringify(wo.publicKeyRing.indexes[0]).should.equal('{"copayerIndex":2147483647,"changeIndex":0,"receiveIndex":1}');
JSON.stringify(wo.publicKeyRing.indexes[1]).should.equal('{"copayerIndex":0,"changeIndex":0,"receiveIndex":1}');
wo.publicKeyRing.copayersBackup.length.should.equal(1);
wo.publicKeyRing.copayersBackup[0].should.equal('0298f65b2694c55f9048bc05f10368242727c7f9d2065cbd788c3ecde1ec57f33f');
wo.publicKeyRing.copayersExtPubKeys.length.should.equal(1);
wo.publicKeyRing.copayersExtPubKeys[0].should.equal('tpubD9SGoP7CXsqSKTiQxCZSCpicDcophqnE4yuqjfw5M9tAR3fSjT9GDGwPEUFCN7SSmRKGDLZgKQePYFaLWyK32akeSan45TNTd8sgef9Ymh6');
wo.privateKey.extendedPrivateKeyString.should.equal('tprv8ZgxMBicQKsPfQCscb7CtJKzixxcVSyrCVcfr3WCFbtT8kYTzNubhjQ5R7AuYJgPCcSH4R8T34YVxeohKGhAB9wbB4eFBbQFjUpjGCqptHm');
wo.privateKey.networkName.should.equal('testnet');
});
});
});
var legacyPassword1 = '1DUpLRbuVpgLkcEY8gY8iod/SmA7+OheGZJ9PtvmTlvNE0FkEWpCKW9STdzXYJqbn0wiAapE4ojHNYj2hjYYAQ==';
var encryptedLegacy1 = 'U2FsdGVkX19yGM1uBAIzQa8Po/dvUicmxt1YyRk/S97PcZ6I6rHMp9dMagIrehg4Qd6JHn/ustmFHS7vmBYj0EBpf6rdXiQezaWnVAJS9/xYjAO36EFUbl+NmUanuwujAxgYdSP/sNssRLeInvExmZYW993EEclxkwL6YUyX66kKsxGQo2oWng0NreBJNhFmrbOEWeFje2PiWP57oUjKsurFzwpluAAarUTYSLud+nXeabC7opzOP5yqniWBMJz0Ou8gpNCWCMhG/P9F9ccVPY7juyd0Hf41FVse8nd2++axKB57+paozLdO+HRfV6zkMqC3h8gWY7LkS75j3bvqcTw9LhXmzE0Sz21n9yDnRpA4chiAvtwQvvBGgj1pFMKhNQU6Obac9ZwKYzUTgdDn3Uzg1UlDzgyOh9S89rbRTV84WB+hXwhuVluWzbNNYV3vXe5PFrocVktIrtS3xQh+k/7my4A6/gRRrzNYpKrUASJqDS/9u9WBkG35xD63J/qXjtG2M0YPwbI57BK1IK4K510b8V72lz5U2XQrIC4ldBwni1rpSavwCJV9xF6hUdOmNV8fZsVHP0NeN1PYlLkSb2QgfuoWnkcsJerwuFR7GZC/i6efrswtpO0wMEQr/J0CLbeXlHAru6xxjCBhWoJvZpMGw72zgnDLoyMNsEVglNhx/VlV9ZMYkkdaEYAxPOEIyZdQ5MS+2jEAlXf818n/xzJSVrniCn9be8EPePvkw35pivprvy09vbW4cKsWBKvgIyoT6A3OhUOCCS8E9cg0WAjjav2EymrbKmGWRHaiD+EoJqaDg6s20zhHn1YEa/YwvGGSB5+Hg8baLHD8ZASvxz4cFFAAVZrBUedRFgHzqwaMUlFXLgueivWUj7RXlIw6GuNhLoo1QkhZMacf23hrFxxQYvGBRw1hekBuDmcsGWljA28udBxBd5f9i+3gErttMLJ6IPaud590uvrxRIclu0Sz9R2EQX64YJxqDtLpMY0PjddSMu8vaDRpK9/ZSrnz/xrXsyabaafz4rE/ItFXjwFUFkvtmuauHTz6nmuKjVfxvNLNAiKb/gI7vQyUhnTbKIApe7XyJsjedNDtZqsPoJRIzdDmrZYxGStbAZ7HThqFJlSJ9NPNhH+E2jm3TwL5mwt0fFZ5h+p497lHMtIcKffESo7KNa2juSVNMDREk0NcyxGXGiVB2FWl4sLdvyhcsVq0I7tmW6OGZKRf8W49GCJXq6Ie69DJ9LB1DO67NV1jsYbsLx9uhE2yEmpWZ3jkoCV/Eas4grxt0CGN6EavzQ==';

View file

@ -1,5 +1,6 @@
'use strict'; 'use strict';
var _ = require('underscore');
var chai = chai || require('chai'); var chai = chai || require('chai');
var should = chai.should(); var should = chai.should();
var sinon = require('sinon'); var sinon = require('sinon');
@ -12,7 +13,7 @@ if (is_browser) {
var copayConfig = require('../config'); var copayConfig = require('../config');
var Wallet = copay.Wallet; var Wallet = copay.Wallet;
var PrivateKey = copay.PrivateKey; var PrivateKey = copay.PrivateKey;
var Storage = require('./mocks/FakeStorage'); var Storage = copay.Storage;
var Network = require('./mocks/FakeNetwork'); var Network = require('./mocks/FakeNetwork');
var Blockchain = require('./mocks/FakeBlockchain'); var Blockchain = require('./mocks/FakeBlockchain');
var Builder = require('./mocks/FakeBuilder'); var Builder = require('./mocks/FakeBuilder');
@ -27,6 +28,7 @@ var walletConfig = {
spendUnconfirmed: true, spendUnconfirmed: true,
reconnectDelay: 100, reconnectDelay: 100,
networkName: 'testnet', networkName: 'testnet',
storage: require('./mocks/FakeLocalStorage').storageParams,
}; };
var getNewEpk = function() { var getNewEpk = function() {
@ -80,6 +82,7 @@ describe('Wallet model', function() {
}); });
var storage = new Storage(walletConfig.storage); var storage = new Storage(walletConfig.storage);
storage.setPassphrase('xxx');
var network = new Network(walletConfig.network); var network = new Network(walletConfig.network);
var blockchain = new Blockchain(walletConfig.blockchain); var blockchain = new Blockchain(walletConfig.blockchain);
c.storage = storage; c.storage = storage;
@ -102,7 +105,6 @@ describe('Wallet model', function() {
}; };
c.networkName = walletConfig.networkName; c.networkName = walletConfig.networkName;
c.verbose = walletConfig.verbose;
c.version = '0.0.1'; c.version = '0.0.1';
@ -341,8 +343,10 @@ describe('Wallet model', function() {
// non stored options // non stored options
o.opts.reconnectDelay = 100; o.opts.reconnectDelay = 100;
var s = new Storage(walletConfig.storage);
s.setPassphrase('xxx');
var w2 = Wallet.fromObj(o, var w2 = Wallet.fromObj(o,
new Storage(walletConfig.storage), s,
new Network(walletConfig.network), new Network(walletConfig.network),
new Blockchain(walletConfig.blockchain)); new Blockchain(walletConfig.blockchain));
should.exist(w2); should.exist(w2);
@ -363,7 +367,18 @@ describe('Wallet model', function() {
var s = Wallet.decodeSecret(sb); var s = Wallet.decodeSecret(sb);
s.pubKey.should.equal(id); s.pubKey.should.equal(id);
s.secretNumber.should.equal(secretNumber); s.secretNumber.should.equal(secretNumber);
s.networkName.should.equal(w.getNetworkName());
});
it('#getSecret decodeSecret livenet', function() {
var w = cachedCreateW2();
var stub = sinon.stub(w, 'getNetworkName');
stub.returns('livenet');
var sb = w.getSecret();
should.exist(sb);
var s = Wallet.decodeSecret(sb);
s.networkName.should.equal('livenet');
stub.restore();
}); });
@ -391,6 +406,37 @@ describe('Wallet model', function() {
}); });
describe('#_onData', function() {
var w = cachedCreateW();
var sender = '025c046aaf505a6d23203edd343132e9d4d21818b962d1e9a9c98573cc2031bfc9';
var ts = 1410810974778246;
it('should fail on message unknown', function() {
var data = {
type: "xxx",
walletId: w.id
};
(function() {
w._onData(sender, data, ts);
}).should.
throw('unknown message type received: xxx from: 025c046aaf505a6d23203edd343132e9d4d21818b962d1e9a9c98573cc2031bfc9');
});
it('should call sendWalletReady', function() {
var data = {
type: "walletId",
walletId: w.id
};
var spy = sinon.spy(w, 'sendWalletReady');
w._onData(sender, data, ts);
sinon.assert.callCount(spy, 1);
});
});
describe('#purgeTxProposals', function() { describe('#purgeTxProposals', function() {
it('should delete all', function() { it('should delete all', function() {
var w = cachedCreateW(); var w = cachedCreateW();
@ -904,6 +950,22 @@ describe('Wallet model', function() {
}); });
}); });
describe('#subscribeToAddresses', function() {
it('should subscribe successfully', function() {
var w = cachedCreateW2();
var addr1 = w.getAddresses()[0].toString();
var addr2 = w.generateAddress().toString();
var addr3 = w.generateAddress(true).toString();
chai.expect(w.getAddresses().length).to.equal(3);
w.blockchain.subscribe = sinon.spy();
w.subscribeToAddresses();
w.blockchain.subscribe.calledOnce.should.equal(true);
var arg = w.blockchain.subscribe.getCall(0).args[0];
chai.expect(_.difference(arg, [addr1, addr2, addr3]).length).to.equal(0);
});
});
describe('#send', function() { describe('#send', function() {
it('should call this.network.send', function() { it('should call this.network.send', function() {
var w = cachedCreateW2(); var w = cachedCreateW2();
@ -1194,6 +1256,15 @@ describe('Wallet model', function() {
}); });
}); });
describe('#getMyCopayerNickname', function() {
it('should call publicKeyRing.nicknameForCopayer', function() {
var w = cachedCreateW2();
w.publicKeyRing.nicknameForCopayer = sinon.spy();
w.getMyCopayerNickname();
w.publicKeyRing.nicknameForCopayer.calledOnce.should.equal(true);
});
});
describe('#netStart', function() { describe('#netStart', function() {
it('should call Network.start', function() { it('should call Network.start', function() {
var w = cachedCreateW2(); var w = cachedCreateW2();
@ -1211,15 +1282,6 @@ describe('Wallet model', function() {
}); });
describe('#forceNetwork in config', function() {
it('should throw if network is different', function() {
var backup = copayConfig.forceNetwork;
copayConfig.forceNetwork = true;
walletConfig.networkName = 'livenet';
createW2.should.throw(Error);
copayConfig.forceNetwork = backup;
});
});
describe('_getKeymap', function() { describe('_getKeymap', function() {
var w = cachedCreateW(); var w = cachedCreateW();
@ -1533,4 +1595,27 @@ describe('Wallet model', function() {
should.exist(n.networkNonce); should.exist(n.networkNonce);
}); });
}); it('should emit notification when tx received', function(done) {
var w = cachedCreateW2();
w.blockchain.removeAllListeners = sinon.stub();
var spy = sinon.spy(w, 'emit');
w.generateAddress(false, function(addr1) {
w.generateAddress(true, function(addr2) {
w.blockchain.on = sinon.stub().withArgs('tx').yields({
address: addr1.toString(),
});
w._setBlockchainListeners();
spy.calledWith('tx', addr1.toString(), false).should.be.true;
w.blockchain.on = sinon.stub().withArgs('tx').yields({
address: addr2.toString(),
});
w._setBlockchainListeners();
spy.calledWith('tx', addr2.toString(), true).should.be.true;
done();
});
});
});
});

File diff suppressed because one or more lines are too long

View file

@ -11,12 +11,20 @@ if (is_browser) {
} }
var copayConfig = require('../config'); var copayConfig = require('../config');
var WalletLock = copay.WalletLock; var WalletLock = copay.WalletLock;
var PrivateKey = copay.PrivateKey; var PrivateKey = copay.PrivateKey;
var Storage = require('./mocks/FakeStorage'); var Storage = copay.Storage;
var storage;
describe('WalletLock model', function() { describe('WalletLock model', function() {
var storage = new Storage();
beforeEach(function() {
storage = new Storage(require('./mocks/FakeLocalStorage').storageParams);
storage.setPassphrase('mysupercoolpassword');
storage.storage.clear();
storage.sessionStorage.clear();
});
it('should fail with missing args', function() { it('should fail with missing args', function() {
(function() { (function() {
@ -36,45 +44,68 @@ describe('WalletLock model', function() {
should.exist(w); should.exist(w);
}); });
it('should NOT fail if locked already', function() {
it('should generate a sessionId with init', function(done) {
var w = new WalletLock(storage, 'id');
var spy = sinon.spy(storage, 'getSessionId');
w.init(function() {
spy.calledOnce.should.equal(true);
done();
});
});
it('#keepAlive should call getsessionId if not called before', function(done) {
var w = new WalletLock(storage, 'id');
var spy = sinon.spy(storage, 'getSessionId');
w.keepAlive(function() {
spy.calledOnce.should.equal(true);
done();
});
});
it('should NOT fail if locked already by me', function(done) {
var w = new WalletLock(storage, 'walletId2');
w.keepAlive(function() {
var w2 = new WalletLock(storage, 'walletId2');
w2.init(function() {
w2.keepAlive(function() {
w.sessionId.should.equal(w2.sessionId);
should.exist(w2);
done();
});
});
})
});
it('should FAIL if locked by someone else', function(done) {
var w = new WalletLock(storage, 'walletId'); var w = new WalletLock(storage, 'walletId');
storage.sessionId = 'xxx'; w.keepAlive(function() {
var w2= new WalletLock(storage, 'walletId'); storage.setSessionId('session2', function() {
should.exist(w2); var w2 = new WalletLock(storage, 'walletId');
}); w2.keepAlive(function(locked) {
should.exist(locked);
locked.message.should.contain('LOCKED');
done();
});
});
});
})
it('should change status of previously openned wallet', function() { it('should FAIL if locked by someone else but expired', function(done) {
storage.sessionId = 'session1';
var w = new WalletLock(storage, 'walletId'); var w = new WalletLock(storage, 'walletId');
storage.sessionId = 'xxx'; w.keepAlive(function() {
var w2= new WalletLock(storage, 'walletId'); storage.setSessionId('session2', function() {
w2.keepAlive();
(function() {w.keepAlive();}).should.throw('already open');
});
it('should not fail if locked by me', function() {
var s = new Storage();
var w = new WalletLock(s, 'walletId');
var w2 = new WalletLock(s, 'walletId')
w2.keepAlive();
should.exist(w2);
});
it('should not fail if expired', function() {
var s = new Storage();
var w = new WalletLock(s, 'walletId');
var k = Object.keys(s.storage)[0];
var v = JSON.parse(s.storage[k]);
v.expireTs = Date.now() - 60 * 6 * 1000;
s.storage[k] = JSON.stringify(v);
s.sessionId = 'xxx';
var w2 = new WalletLock(s, 'walletId')
should.exist(w2);
});
var json = JSON.parse(storage.storage.ls['lock::walletId']);
json.expireTs -= 3600 * 1000;
storage.storage.ls['lock::walletId'] = JSON.stringify(json);
var w2 = new WalletLock(storage, 'walletId');
w2.keepAlive(function(locked) {
w2.sessionId.should.equal('session2');
should.not.exist(locked);
done();
});
});
});
})
}); });

View file

@ -40,9 +40,7 @@ var UNSPENT = [{
}]; }];
var FAKE_OPTS = { var FAKE_OPTS = {
host: 'something.com', url: 'http://something.com:123',
port: 123,
schema: 'http'
} }
describe('Insight model', function() { describe('Insight model', function() {
@ -348,7 +346,7 @@ describe('Insight model', function() {
}); });
describe('Events', function() { describe('Events', function() {
it('should emmit event on a new block', function(done) { it('should emit event on a new block', function(done) {
var blockchain = new Insight(FAKE_OPTS); var blockchain = new Insight(FAKE_OPTS);
var socket = blockchain.getSocket(); var socket = blockchain.getSocket();
blockchain.on('connect', function() { blockchain.on('connect', function() {
@ -362,7 +360,7 @@ describe('Insight model', function() {
}); });
}); });
it('should emmit event on a transaction for subscribed addresses', function(done) { it('should emit event on a transaction for subscribed addresses', function(done) {
var blockchain = new Insight(FAKE_OPTS); var blockchain = new Insight(FAKE_OPTS);
var socket = blockchain.getSocket(); var socket = blockchain.getSocket();
blockchain.subscribe('2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY'); blockchain.subscribe('2NFjCBFZSsxiwWAD7CKQ3hzWFtf9DcqTucY');
@ -378,7 +376,7 @@ describe('Insight model', function() {
}); });
}); });
it('should\'t emmit event on a transaction for non subscribed addresses', function(done) { it('should\'t emit event on a transaction for non subscribed addresses', function(done) {
var blockchain = new Insight(FAKE_OPTS); var blockchain = new Insight(FAKE_OPTS);
var socket = blockchain.getSocket(); var socket = blockchain.getSocket();
blockchain.on('connect', function() { blockchain.on('connect', function() {
@ -392,7 +390,7 @@ describe('Insight model', function() {
}); });
}); });
it('should emmit event on connection', function(done) { it('should emit event on connection', function(done) {
var blockchain = new Insight(FAKE_OPTS); var blockchain = new Insight(FAKE_OPTS);
var socket = blockchain.getSocket(); var socket = blockchain.getSocket();
blockchain.on('connect', function() { blockchain.on('connect', function() {
@ -400,7 +398,7 @@ describe('Insight model', function() {
}); });
}); });
it('should emmit event on disconnection', function(done) { it('should emit event on disconnection', function(done) {
var blockchain = new Insight(FAKE_OPTS); var blockchain = new Insight(FAKE_OPTS);
var socket = blockchain.getSocket(); var socket = blockchain.getSocket();
blockchain.on('connect', function() { blockchain.on('connect', function() {

View file

@ -13,7 +13,9 @@ describe('Network / Async', function() {
var createN = function(pk) { var createN = function(pk) {
var n = new Async(); var n = new Async({
url: 'http://insight.example.com:1234'
});
var fakeSocket = {}; var fakeSocket = {};
fakeSocket.emit = function() {}; fakeSocket.emit = function() {};
fakeSocket.on = function() {}; fakeSocket.on = function() {};

View file

@ -5,24 +5,29 @@
var sinon = require('sinon'); var sinon = require('sinon');
// Replace saveAs plugin // Replace saveAs plugin
saveAsLastCall = null; saveAs = function(blob, filename) {
saveAs = function(o) { saveAsLastCall = {
saveAsLastCall = o; blob: blob,
filename: filename
};
}; };
var startServer = require('../../mocks/FakePayProServer'); var startServer = require('../../mocks/FakePayProServer');
describe("Unit: Controllers", function() { describe("Unit: Controllers", function() {
config.plugins.LocalStorage=true;
config.plugins.GoogleDrive=null;
var invalidForm = { var invalidForm = {
$invalid: true $invalid: true
}; };
var scope; var scope;
var server; var server;
beforeEach(module('copayApp.services')); beforeEach(module('copayApp.services'));
beforeEach(module('copayApp.controllers')); beforeEach(module('copayApp.controllers'));
beforeEach(angular.mock.module('copayApp'));
var walletConfig = { var walletConfig = {
requiredCopayers: 3, requiredCopayers: 3,
@ -34,11 +39,6 @@ describe("Unit: Controllers", function() {
alternativeIsoCode: 'LOL' alternativeIsoCode: 'LOL'
}; };
it('Copay config should be binded', function() {
should.exist(config);
should.exist(config.unitToSatoshi);
});
describe('More Controller', function() { describe('More Controller', function() {
var ctrl; var ctrl;
@ -50,21 +50,32 @@ describe("Unit: Controllers", function() {
$scope: scope, $scope: scope,
$modal: {}, $modal: {},
}); });
saveAsLastCall = null;
})); }));
it('Backup controller #download', function() { it('Backup controller #download', function() {
scope.wallet.setEnc('1234567'); scope.wallet.setEnc('1234567');
expect(saveAsLastCall).equal(null); expect(saveAsLastCall).equal(null);
scope.downloadBackup(); scope.downloadBackup();
expect(saveAsLastCall.size).equal(7); expect(saveAsLastCall.blob.size).equal(7);
expect(saveAsLastCall.type).equal('text/plain;charset=utf-8'); expect(saveAsLastCall.blob.type).equal('text/plain;charset=utf-8');
}); });
it('Backup controller #delete', function() { it('Backup controller should name backup correctly for multiple copayers', function() {
expect(scope.wallet).not.equal(undefined); scope.wallet.setEnc('1234567');
scope.deleteWallet(); expect(saveAsLastCall).equal(null);
expect(scope.wallet).equal(undefined); scope.downloadBackup();
expect(saveAsLastCall.filename).equal('myNickname-myTESTwullet-testID-keybackup.json.aes');
}); });
it('Backup controller should name backup correctly for 1-1 wallet', function() {
scope.wallet.setEnc('1234567');
expect(saveAsLastCall).equal(null);
scope.wallet.totalCopayers = 1;
scope.downloadBackup();
expect(saveAsLastCall.filename).equal('myTESTwullet-testID-keybackup.json.aes');
});
}); });
describe('Create Controller', function() { describe('Create Controller', function() {
@ -110,6 +121,7 @@ describe("Unit: Controllers", function() {
var transactionsCtrl; var transactionsCtrl;
beforeEach(inject(function($controller, $rootScope) { beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new(); scope = $rootScope.$new();
$rootScope.wallet = new FakeWallet(walletConfig);
transactionsCtrl = $controller('TransactionsController', { transactionsCtrl = $controller('TransactionsController', {
$scope: scope, $scope: scope,
}); });
@ -131,7 +143,11 @@ describe("Unit: Controllers", function() {
beforeEach(module(function($provide) { beforeEach(module(function($provide) {
$provide.value('request', { $provide.value('request', {
'get': function(_, cb) { 'get': function(_, cb) {
cb(null, null, [{name: 'lol currency', code: 'LOL', rate: 2}]); cb(null, null, [{
name: 'lol currency',
code: 'LOL',
rate: 2
}]);
} }
}); });
})); }));
@ -139,8 +155,8 @@ describe("Unit: Controllers", function() {
scope = $rootScope.$new(); scope = $rootScope.$new();
scope.rateService = rateService; scope.rateService = rateService;
$rootScope.wallet = new FakeWallet(walletConfig); $rootScope.wallet = new FakeWallet(walletConfig);
config.alternativeName = 'lol currency'; $rootScope.wallet.settings.alternativeName = 'lol currency';
config.alternativeIsoCode = 'LOL'; $rootScope.wallet.settings.alternativeIsoCode = 'LOL';
var element = angular.element( var element = angular.element(
'<form name="form">' + '<form name="form">' +
'<input type="text" id="newaddress" name="newaddress" ng-disabled="loading" placeholder="Address" ng-model="newaddress" valid-address required>' + '<input type="text" id="newaddress" name="newaddress" ng-disabled="loading" placeholder="Address" ng-model="newaddress" valid-address required>' +
@ -224,35 +240,35 @@ describe("Unit: Controllers", function() {
sinon.assert.callCount(spy2, 0); sinon.assert.callCount(spy2, 0);
sinon.assert.callCount(scope.loadTxs, 1); sinon.assert.callCount(scope.loadTxs, 1);
spy.getCall(0).args[0].should.equal('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); spy.getCall(0).args[0].should.equal('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy');
spy.getCall(0).args[1].should.equal(1000 * config.unitToSatoshi); spy.getCall(0).args[1].should.equal(1000 * scope.wallet.settings.unitToSatoshi);
(typeof spy.getCall(0).args[2]).should.equal('undefined'); (typeof spy.getCall(0).args[2]).should.equal('undefined');
}); });
it('should handle big values in 100 BTC', function() { it('should handle big values in 100 BTC', function() {
var old = config.unitToSatoshi; var old = scope.wallet.settings.unitToSatoshi;
config.unitToSatoshi = 100000000;; scope.wallet.settings.unitToSatoshi = 100000000;;
sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy');
sendForm.amount.$setViewValue(100); sendForm.amount.$setViewValue(100);
var spy = sinon.spy(scope.wallet, 'createTx'); var spy = sinon.spy(scope.wallet, 'createTx');
scope.loadTxs = sinon.spy(); scope.loadTxs = sinon.spy();
scope.submitForm(sendForm); scope.submitForm(sendForm);
spy.getCall(0).args[1].should.equal(100 * config.unitToSatoshi); spy.getCall(0).args[1].should.equal(100 * scope.wallet.settings.unitToSatoshi);
config.unitToSatoshi = old; scope.wallet.settings.unitToSatoshi = old;
}); });
it('should handle big values in 5000 BTC', function() { it('should handle big values in 5000 BTC', inject(function($rootScope) {
var old = config.unitToSatoshi; var old = $rootScope.wallet.settings.unitToSatoshi;
config.unitToSatoshi = 100000000;; $rootScope.wallet.settings.unitToSatoshi = 100000000;;
sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy');
sendForm.amount.$setViewValue(5000); sendForm.amount.$setViewValue(5000);
var spy = sinon.spy(scope.wallet, 'createTx'); var spy = sinon.spy(scope.wallet, 'createTx');
scope.loadTxs = sinon.spy(); scope.loadTxs = sinon.spy();
scope.submitForm(sendForm); scope.submitForm(sendForm);
spy.getCall(0).args[1].should.equal(5000 * config.unitToSatoshi); spy.getCall(0).args[1].should.equal(5000 * $rootScope.wallet.settings.unitToSatoshi);
config.unitToSatoshi = old; $rootScope.wallet.settings.unitToSatoshi = old;
}); }));
it('should convert bits amount to fiat', function(done) { it('should convert bits amount to fiat', function(done) {
scope.rateService.whenAvailable(function() { scope.rateService.whenAvailable(function() {
@ -302,18 +318,19 @@ describe("Unit: Controllers", function() {
describe("Unit: Version Controller", function() { describe("Unit: Version Controller", function() {
var scope, $httpBackendOut; var scope, $httpBackendOut;
var GH = 'https://api.github.com/repos/bitpay/copay/tags'; var GH = 'https://api.github.com/repos/bitpay/copay/tags';
beforeEach(angular.mock.module('copayApp'));
beforeEach(inject(function($controller, $injector) { beforeEach(inject(function($controller, $injector) {
$httpBackend = $injector.get('$httpBackend'); $httpBackend = $injector.get('$httpBackend');
$httpBackend.when('GET', GH) $httpBackend.when('GET', GH)
.respond([{ .respond([{
name: "v100.1.6", name: "v100.1.6",
zipball_url: "https://api.github.com/repos/bitpay/copay/zipball/v0.0.6", zipball_url: "https://api.github.com/repos/bitpay/copay/zipball/v0.0.6",
tarball_url: "https://api.github.com/repos/bitpay/copay/tarball/v0.0.6", tarball_url: "https://api.github.com/repos/bitpay/copay/tarball/v0.0.6",
commit: { commit: {
sha: "ead7352bf2eca705de58d8b2f46650691f2bc2c7", sha: "ead7352bf2eca705de58d8b2f46650691f2bc2c7",
url: "https://api.github.com/repos/bitpay/copay/commits/ead7352bf2eca705de58d8b2f46650691f2bc2c7" url: "https://api.github.com/repos/bitpay/copay/commits/ead7352bf2eca705de58d8b2f46650691f2bc2c7"
} }
}]); }]);
})); }));
var rootScope; var rootScope;
@ -358,11 +375,6 @@ describe("Unit: Controllers", function() {
scope.$apply(); scope.$apply();
}); });
it('should return networkName', function() {
$httpBackend.flush(); // need flush
var networkName = scope.networkName;
expect(networkName).equal('testnet');
});
}); });
describe("Unit: Sidebar Controller", function() { describe("Unit: Sidebar Controller", function() {
@ -390,6 +402,7 @@ describe("Unit: Controllers", function() {
beforeEach(inject(function($compile, $rootScope, $controller) { beforeEach(inject(function($compile, $rootScope, $controller) {
scope = $rootScope.$new(); scope = $rootScope.$new();
$rootScope.availableBalance = 123456; $rootScope.availableBalance = 123456;
$rootScope.wallet = new FakeWallet(walletConfig);
var element = angular.element( var element = angular.element(
'<form name="form">' + '<form name="form">' +

View file

@ -8,17 +8,23 @@ describe("Unit: Testing Directives", function() {
beforeEach(module('copayApp.directives')); beforeEach(module('copayApp.directives'));
beforeEach(function() { var walletConfig = {
config.unitToSatoshi = 100; requiredCopayers: 3,
config.unitName = 'bits'; totalCopayers: 5,
}); spendUnconfirmed: 1,
reconnectDelay: 100,
networkName: 'testnet',
alternativeName: 'lol currency',
alternativeIsoCode: 'LOL'
};
describe('Check config', function() {
it('unit should be set to BITS in config.js', function() { beforeEach(inject(function($rootScope) {
expect(config.unitToSatoshi).to.equal(100); $rootScope.wallet = new FakeWallet(walletConfig);
expect(config.unitName).to.equal('bits'); var w = $rootScope.wallet;
}); w.settings.unitToSatoshi = 100;
}); w.settings.unitName = 'bits';
}));
describe('Validate Address', function() { describe('Validate Address', function() {
beforeEach(inject(function($compile, $rootScope) { beforeEach(inject(function($compile, $rootScope) {
@ -36,16 +42,16 @@ describe("Unit: Testing Directives", function() {
form = $scope.form; form = $scope.form;
})); }));
it('should validate with network', function() { it('should validate with network', inject(function($rootScope) {
config.networkName = 'testnet'; $rootScope.wallet.getNetworkName = sinon.stub().returns('testnet');
form.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); form.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy');
expect(form.address.$invalid).to.equal(false); expect(form.address.$invalid).to.equal(false);
}); }));
it('should not validate with other network', function() { it('should not validate with other network', inject(function($rootScope) {
config.networkName = 'livenet'; $rootScope.wallet.getNetworkName = sinon.stub().returns('livenet');
form.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); form.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy');
expect(form.address.$invalid).to.equal(true); expect(form.address.$invalid).to.equal(true);
}); }));
it('should not validate random', function() { it('should not validate random', function() {
form.address.$setViewValue('thisisaninvalidaddress'); form.address.$setViewValue('thisisaninvalidaddress');
expect(form.address.$invalid).to.equal(true); expect(form.address.$invalid).to.equal(true);
@ -94,9 +100,12 @@ describe("Unit: Testing Directives", function() {
describe('Unit: BTC', function() { describe('Unit: BTC', function() {
beforeEach(inject(function($compile, $rootScope) { beforeEach(inject(function($compile, $rootScope) {
config.unitToSatoshi = 100000000;
config.unitName = 'BTC';
$scope = $rootScope; $scope = $rootScope;
var w = new FakeWallet(walletConfig);
w.settings.unitToSatoshi = 100000000;
w.settings.unitName = 'BTC';
$rootScope.wallet = w;
$rootScope.availableBalance = 0.04; $rootScope.availableBalance = 0.04;
var element = angular.element( var element = angular.element(
'<form name="form">' + '<form name="form">' +

View file

@ -5,6 +5,15 @@
describe('Unit: Testing Filters', function() { describe('Unit: Testing Filters', function() {
beforeEach(module('copayApp.filters')); beforeEach(module('copayApp.filters'));
var walletConfig = {
requiredCopayers: 3,
totalCopayers: 5,
spendUnconfirmed: 1,
reconnectDelay: 100,
networkName: 'testnet',
alternativeName: 'lol currency',
alternativeIsoCode: 'LOL'
};
describe('limitAddress', function() { describe('limitAddress', function() {
@ -103,68 +112,76 @@ describe('Unit: Testing Filters', function() {
})); }));
}); });
describe('noFractionNumber bits', function() { describe('noFractionNumber', function() {
beforeEach(function() { describe('noFractionNumber bits', function() {
config.unitToSatoshi = 100; beforeEach(inject(function($rootScope) {
config.unitName = 'bits'; $rootScope.wallet = new FakeWallet(walletConfig);
var w = $rootScope.wallet;
w.settings.unitToSatoshi = 100;
w.settings.unitName = 'bits';
}));
it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber');
expect(noFraction(3100)).to.equal('3,100');
expect(noFraction(3100200)).to.equal('3,100,200');
expect(noFraction(3)).to.equal('3');
expect(noFraction(0.3)).to.equal(0.3);
expect(noFraction(0.30000000)).to.equal(0.3);
expect(noFraction(3200.01)).to.equal('3,200.01');
expect(noFraction(3200890.010000)).to.equal('3,200,890.01');
}));
}); });
it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber');
expect(noFraction(3100)).to.equal('3,100');
expect(noFraction(3100200)).to.equal('3,100,200');
expect(noFraction(3)).to.equal('3');
expect(noFraction(0.3)).to.equal(0.3);
expect(noFraction(0.30000000)).to.equal(0.3);
expect(noFraction(3200.01)).to.equal('3,200.01');
expect(noFraction(3200890.010000)).to.equal('3,200,890.01');
}));
});
describe('noFractionNumber BTC', function() { describe('noFractionNumber BTC', function() {
beforeEach(function() { beforeEach(inject(function($rootScope) {
config.unitToSatoshi = 100000000; $rootScope.wallet = new FakeWallet(walletConfig);
config.unitName = 'BTC'; var w = $rootScope.wallet;
w.settings.unitToSatoshi = 100000000;
w.settings.unitName = 'BTC';
}));
it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber');
expect(noFraction(0.30000000)).to.equal(0.3);
expect(noFraction(0.00302000)).to.equal(0.00302);
expect(noFraction(1.00000001)).to.equal(1.00000001);
expect(noFraction(3.10000012)).to.equal(3.10000012);
expect(noFraction(0.00100000)).to.equal(0.001);
expect(noFraction(0.00100009)).to.equal(0.00100009);
expect(noFraction(2000.00312011)).to.equal('2,000.00312011');
expect(noFraction(2000998.00312011)).to.equal('2,000,998.00312011');
}));
}); });
it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber');
expect(noFraction(0.30000000)).to.equal(0.3);
expect(noFraction(0.00302000)).to.equal(0.00302);
expect(noFraction(1.00000001)).to.equal(1.00000001);
expect(noFraction(3.10000012)).to.equal(3.10000012);
expect(noFraction(0.00100000)).to.equal(0.001);
expect(noFraction(0.00100009)).to.equal(0.00100009);
expect(noFraction(2000.00312011)).to.equal('2,000.00312011');
expect(noFraction(2000998.00312011)).to.equal('2,000,998.00312011');
}));
});
describe('noFractionNumber mBTC', function() { describe('noFractionNumber mBTC', function() {
beforeEach(function() { beforeEach(inject(function($rootScope) {
config.unitToSatoshi = 100000; $rootScope.wallet = new FakeWallet(walletConfig);
config.unitName = 'mBTC'; var w = $rootScope.wallet;
w.settings.unitToSatoshi = 100000;
w.settings.unitName = 'mBTC';
}));
it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber');
expect(noFraction(0.30000)).to.equal(0.3);
expect(noFraction(0.00302)).to.equal(0.00302);
expect(noFraction(1.00001)).to.equal(1.00001);
expect(noFraction(3.10002)).to.equal(3.10002);
expect(noFraction(0.00100000)).to.equal(0.001);
expect(noFraction(0.00100009)).to.equal(0.001);
expect(noFraction(2000.00312)).to.equal('2,000.00312');
expect(noFraction(2000998.00312)).to.equal('2,000,998.00312');
}));
}); });
it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber');
expect(noFraction(0.30000)).to.equal(0.3);
expect(noFraction(0.00302)).to.equal(0.00302);
expect(noFraction(1.00001)).to.equal(1.00001);
expect(noFraction(3.10002)).to.equal(3.10002);
expect(noFraction(0.00100000)).to.equal(0.001);
expect(noFraction(0.00100009)).to.equal(0.001);
expect(noFraction(2000.00312)).to.equal('2,000.00312');
expect(noFraction(2000998.00312)).to.equal('2,000,998.00312');
}));
});
describe('noFractionNumber:custom fractionSize', function() { describe('noFractionNumber:custom fractionSize', function() {
it('should format number to display correctly', inject(function($filter) { it('should format number to display correctly', inject(function($filter) {
var noFraction = $filter('noFractionNumber'); var noFraction = $filter('noFractionNumber');
expect(noFraction(0.30000, 0)).to.equal('0'); expect(noFraction(0.30000, 0)).to.equal('0');
expect(noFraction(1.00001, 0)).to.equal('1'); expect(noFraction(1.00001, 0)).to.equal('1');
expect(noFraction(3.10002, 0)).to.equal('3'); expect(noFraction(3.10002, 0)).to.equal('3');
expect(noFraction(2000.00312, 0)).to.equal('2,000'); expect(noFraction(2000.00312, 0)).to.equal('2,000');
expect(noFraction(2000998.00312, 0)).to.equal('2,000,998'); expect(noFraction(2000998.00312, 0)).to.equal('2,000,998');
})); }));
}); });
});
}); });

View file

@ -4,19 +4,9 @@
// //
// //
var sinon = require('sinon'); var sinon = require('sinon');
var preconditions = require('preconditions').singleton();
beforeEach(function() { beforeEach(angular.mock.module('copayApp'));
config.unitToSatoshi = 100;
config.unitName = 'bits';
});
describe('Check config', function() {
it('unit should be set to BITS in config.js', function() {
expect(config.unitToSatoshi).to.equal(100);
expect(config.unitName).to.equal('bits');
});
});
describe("Unit: Walletfactory Service", function() { describe("Unit: Walletfactory Service", function() {
beforeEach(angular.mock.module('copayApp.services')); beforeEach(angular.mock.module('copayApp.services'));
@ -36,7 +26,7 @@ describe("Unit: controllerUtils", function() {
var Waddr = Object.keys($rootScope.wallet.balanceByAddr)[0]; var Waddr = Object.keys($rootScope.wallet.balanceByAddr)[0];
var a = {}; var a = {};
a[Waddr] = 100; a[Waddr] = 100;
//SATs //SATs
$rootScope.wallet.set(100000001, 90000002, a); $rootScope.wallet.set(100000001, 90000002, a);
//retuns values in DEFAULT UNIT(bits) //retuns values in DEFAULT UNIT(bits)
@ -125,22 +115,32 @@ describe('Unit: Rate Service', function() {
beforeEach(module(function($provide) { beforeEach(module(function($provide) {
$provide.value('request', { $provide.value('request', {
'get': function(_, cb) { 'get': function(_, cb) {
cb(null, null, [{name: 'lol currency', code: 'LOL', rate: 2}]); cb(null, null, [{
name: 'lol currency',
code: 'LOL',
rate: 2
}]);
} }
}); });
})); }));
it('should be possible to ask for conversion from fiat', it('should be possible to ask for conversion from fiat',
inject(function(rateService) { function(done) {
rateService.whenAvailable(function() { inject(function(rateService) {
(1).should.equal(rateService.fromFiat(2, 'LOL')); rateService.whenAvailable(function() {
}); (1e8).should.equal(rateService.fromFiat(2, 'LOL'));
}) done();
});
})
}
); );
it('should be possible to ask for conversion to fiat', it('should be possible to ask for conversion to fiat',
inject(function(rateService) { function(done) {
rateService.whenAvailable(function() { inject(function(rateService) {
(2).should.equal(rateService.toFiat(1e8, 'LOL')); rateService.whenAvailable(function() {
}); (2).should.equal(rateService.toFiat(1e8, 'LOL'));
}) done();
});
})
}
); );
}); });

View file

@ -15,7 +15,9 @@ var getCommitHash = function() {
//exec git command to get the hash of the current commit //exec git command to get the hash of the current commit
//git rev-parse HEAD //git rev-parse HEAD
var hash = shell.exec('git rev-parse HEAD',{silent:true}).output.trim().substr(0,7); var hash = shell.exec('git rev-parse HEAD', {
silent: true
}).output.trim().substr(0, 7);
return hash; return hash;
} }
@ -23,7 +25,7 @@ var createVersion = function() {
var json = JSON.parse(fs.readFileSync('./package.json', 'utf8')); var json = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
var content = 'module.exports.version="' + json.version + '";'; var content = 'module.exports.version="' + json.version + '";';
content = content + '\nmodule.exports.commitHash="' + getCommitHash() + '";'; content = content + '\nmodule.exports.commitHash="' + getCommitHash() + '";';
fs.writeFileSync("./version.js", content); fs.writeFileSync("./version.js", content);
}; };
@ -43,9 +45,9 @@ var createBundle = function(opts) {
b.require('browser-request', { b.require('browser-request', {
expose: 'request' expose: 'request'
}); });
b.require('underscore', { b.require('underscore');
expose: 'underscore' b.require('assert');
}); b.require('preconditions');
b.require('./copay', { b.require('./copay', {
expose: 'copay' expose: 'copay'
@ -81,6 +83,19 @@ var createBundle = function(opts) {
b.require('./js/models/core/HDPath', { b.require('./js/models/core/HDPath', {
expose: '../js/models/core/HDPath' expose: '../js/models/core/HDPath'
}); });
b.require('./js/models/core/PluginManager', {
expose: '../js/models/core/PluginManager'
});
if (!opts.disablePlugins) {
b.require('./plugins/GoogleDrive', {
expose: '../plugins/GoogleDrive'
});
b.require('./plugins/LocalStorage', {
expose: '../plugins/LocalStorage'
});
}
b.require('./config', { b.require('./config', {
expose: '../config' expose: '../config'
}); });
@ -89,9 +104,6 @@ var createBundle = function(opts) {
//include dev dependencies //include dev dependencies
b.require('sinon'); b.require('sinon');
b.require('blanket'); b.require('blanket');
b.require('./test/mocks/FakeStorage', {
expose: './mocks/FakeStorage'
});
b.require('./test/mocks/FakeLocalStorage', { b.require('./test/mocks/FakeLocalStorage', {
expose: './mocks/FakeLocalStorage' expose: './mocks/FakeLocalStorage'
}); });
@ -130,10 +142,10 @@ if (require.main === module) {
}; };
var program = require('commander'); var program = require('commander');
program program
.version('0.0.1') .version('0.0.1')
.option('-d, --debug', 'Development. Don\'t minify the codem and include debug packages.') .option('-d, --debug', 'Development. Don\'t minify the codem and include debug packages.')
.option('-o, --stdout', 'Specify output as stdout') .option('-o, --stdout', 'Specify output as stdout')
.parse(process.argv); .parse(process.argv);
createVersion(); createVersion();
var copayBundle = createBundle(program); var copayBundle = createBundle(program);

View file

@ -4,39 +4,53 @@
<span translate>Addresses</span> <span translate>Addresses</span>
<span class="button primary small side-bar" ng-click="newAddr()" ng-disabled="loading"><i class="fi-plus"></i></span> <span class="button primary small side-bar" ng-click="newAddr()" ng-disabled="loading"><i class="fi-plus"></i></span>
</h1> </h1>
<div class="large-12 medium-12" ng-if="!!(addresses|removeEmpty).length"> <div class="large-12 medium-12" ng-if="!!(addresses|removeEmpty).length">
<div class="large-12 medium-12" ng-init="showAll=0"> <div class="large-12 medium-12" ng-init="showAll=0">
<div class="panel radius oh" ng-repeat="addr in addresses|removeEmpty|limitAddress:showAll"> <div class="oh" ng-repeat="addr in addresses|removeEmpty|limitAddress:showAll">
<div class="row collapse"> <div class="panel radius row show-for-large-up">
<div class="large-10 medium-9 small-8 column" > <div class="large-9 medium-9 column">
<div class="ellipsis list-addr"> <div class="list-addr">
<i class="fi-thumbnails size-48 show-for-large-up" ng-click="openAddressModal(addr)">&nbsp;</i> <i class="fi-thumbnails size-48" ng-click="openAddressModal(addr)">&nbsp;</i>
<span> <span>
<contact address="{{addr.address}}" tooltip-popup-delay="500" tooltip tooltip-placement="right"/> <contact address="{{addr.address}}" tooltip-popup-delay="500" tooltip tooltip-placement="right">
</span> </span>
<span class="btn-copy" clip-copy="addr.address"> </span> <span class="btn-copy" clip-copy="addr.address"> </span>
<small translate class="label" ng-if="addr.isChange">change</small> <small translate class="label" ng-if="addr.isChange">change</small>
</div>
</div>
<div class="large-2 medium-3 small-4 column text-right">
<span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span>
<span class="size-12" ng-if="!$root.updatingBalance">
{{addr.balance || 0|noFractionNumber}} {{$root.unitName}}
</span>
</div> </div>
</div> </div>
</div> <div class="large-3 medium-3 column text-right">
<span ng-if="$root.updatingBalance">
<a class="secondary radius" ng-click="showAll=!showAll" ng-show="(addresses|removeEmpty).length != (addresses|removeEmpty|limitAddress).length"> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
<span translate ng-if="!showAll">Show all</span> </span>
<span translate ng-if="showAll">Show less</span> <p class="size-14" ng-if="!$root.updatingBalance">
</a> <b>{{addr.balance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}}</b>
</p>
</div>
</div> <!-- end of panel large screen -->
<a class="db text-black panel radius row hide-for-large-up list-addr" ng-click="openAddressModal(addr)">
<div class="ellipsis m5b">
<span><contact address="{{addr.address}}"></span>
<small translate class="m0 label" ng-if="addr.isChange">change</small>
</div>
<div class="text-left">
<p class="small-12 columns m15t" ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i>
</p>
<p class="size-14" ng-if="!$root.updatingBalance">
<b>{{addr.balance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}}</b>
</p>
</div>
</a> <!-- end of panel mobile -->
</div>
</div> </div>
</div> </div>
<a class="secondary radius" ng-click="showAll=!showAll" ng-show="(addresses|removeEmpty).length != (addresses|removeEmpty|limitAddress).length">
<span translate ng-if="!showAll">Show all</span>
<span translate ng-if="showAll">Show less</span>
</a>
</div> </div>
</div> </div>

View file

@ -51,6 +51,10 @@
</div> </div>
<div class="box-setup-copayers p20"> <div class="box-setup-copayers p20">
<p class="text-primary m10b"
ng-show="$root.wallet && $root.wallet.publicKeyRing.isComplete()" translate>
Creating and storing a backup will allow you to recover wallet funds
</p>
<div class="oh"> <div class="oh">
<div ng-include="'views/includes/copayer.html'"></div> <div ng-include="'views/includes/copayer.html'"></div>
<div ng-if="!$root.wallet.publicKeyRing.isComplete()"> <div ng-if="!$root.wallet.publicKeyRing.isComplete()">
@ -74,23 +78,7 @@
</div> </div>
</div> </div>
</div> </div>
<button class="button primary right m0"
<div class="text-right">
<div class="left text-left m10b">
<a class="expand small" ng-click="hideAdv=!hideAdv">
<span translate ng-hide="!hideAdv">Show</span>
<span translate ng-hide="hideAdv">Hide</span>
<span translate>advanced options</span>
</a>
<div ng-hide="hideAdv">
<a translate class="text-warning" ng-really-click="deleteWallet()"
ng-really-message="Are you sure to delete this wallet from this computer?">Delete wallet</a>
<span class="text-gray">|</span>
<a translate class="text-primary m20r" ng-click="downloadBackup()"
ng-show="!$root.wallet.publicKeyRing.isComplete()">Download seed backup</a>
</div>
</div>
<button class="button primary m0"
ng-click="backup()" ng-click="backup()"
ng-show="!$root.wallet.publicKeyRing.isBackupReady()" ng-show="!$root.wallet.publicKeyRing.isBackupReady()"
ng-disabled="!$root.wallet.publicKeyRing.isComplete()"> ng-disabled="!$root.wallet.publicKeyRing.isComplete()">
@ -108,6 +96,13 @@
<span translate>yet to join.</span> <span translate>yet to join.</span>
</span> </span>
</button> </button>
<a class="text-primary m15t m20r right" ng-click="skipBackup()"
ng-show="!$root.wallet.publicKeyRing.isBackupReady()"
ng-disabled="!$root.wallet.publicKeyRing.isComplete()">
<span class="size-12" translate ng-show="$root.wallet.publicKeyRing.isComplete()" >
Skip Backup
</span>
</a>
<button class="button primary" <button class="button primary"
disabled="disabled" disabled="disabled"
ng-show="$root.wallet.publicKeyRing.isBackupReady()"> ng-show="$root.wallet.publicKeyRing.isBackupReady()">

View file

@ -24,43 +24,39 @@
<input id="Name" type="text" placeholder="{{'Name'|translate}}" class="form-control" ng-model="$parent.myNickname"> <input id="Name" type="text" placeholder="{{'Name'|translate}}" class="form-control" ng-model="$parent.myNickname">
</div> </div>
<div> <div>
<label for="walletPassword"><span translate>Your Wallet Password</span> <label translate for="walletPassword">
<small translate data-options="disable_for_touch:true" class="has-tip text-gray" tooltip="doesn't need to be shared" >Required</small> Your password
</label> </label>
<input id="walletPassword" type="password" <input id="walletPassword" type="password" placeholder="{{'Choose a password'|translate}}" class="form-control" ng-model="$parent.walletPassword" name="walletPassword" check-strength="passwordStrength" tooltip-html-unsafe="Password strength:
placeholder="{{'Choose your password'|translate}}" class="form-control"
ng-model="$parent.walletPassword"
name="walletPassword"
check-strength="passwordStrength"
tooltip-html-unsafe="Password strength:
<i>{{passwordStrength}}</i><br/><span <i>{{passwordStrength}}</i><br/><span
class='size-12'>Tip: Use lower and uppercase, numbers and class='size-12'>Tip: Use lower and uppercase, numbers and
symbols</span>" symbols</span>" tooltip-trigger="focus" required tooltip-placement="top">
tooltip-trigger="focus" required <div class="pr">
tooltip-placement="top"> <input type="password" placeholder="{{'Repeat password'|translate}}" name="walletPasswordConfirm" ng-model="walletPasswordConfirm" match="walletPassword" required>
<small class="icon-input" ng-show="setupForm.walletPasswordConfirm.$dirty && setupForm.$invalid"><i class="fi-x"></i></small>
<input type="password" <p class="m15b text-gray size-12" ng-show="setupForm.walletPasswordConfirm.$dirty && setupForm.$invalid">
placeholder="{{'Repeat password'|translate}}" <i class="fi-x m5r"></i>
name="walletPasswordConfirm" {{'Passwords must match'|translate}}
ng-model="walletPasswordConfirm" </p>
match="walletPassword" </div>
required>
<div class="text-left line-sidebar-t">
<input id="network-name" type="checkbox" ng-model="networkName" ng-true-value="testnet" ng-false-value="livenet" class="form-control" ng-click="changeNetwork()" ng-checked="networkName == 'testnet' ? true : false">
<label for="network-name" translate>Use test network</label>
</div>
</div> </div>
<a class="expand small" ng-click="hideAdv=!hideAdv"> <a class="expand small" ng-click="hideAdv=!hideAdv">
<i class="fi-widget m3r"></i>
<span translate ng-hide="!hideAdv">Show</span> <span translate ng-hide="!hideAdv">Show</span>
<span translate ng-hide="hideAdv">Hide</span> <span translate ng-hide="hideAdv">Hide</span>
<span translate>advanced options</span> <span translate>advanced options</span>
</a> </a>
<div ng-hide="hideAdv"> <div ng-hide="hideAdv" class="m10t">
<p> <p>
<input type="text" <input type="text" placeholder="BIP32 master extended private key (hex)" name="private" ng-model="private">
placeholder="{{'Private Key (Hex)'|translate}}"
name="private"
ng-model="private"
>
</div> </div>
</div> </div>
<div class="row" ng-show="!isSetupWalletPage"> <div class="row" ng-show="!isSetupWalletPage">
<div class="large-6 medium-6 columns"> <div class="large-6 medium-6 columns">
@ -78,24 +74,17 @@
</div> </div>
<div class="box-setup-copayers" ng-show="!isSetupWalletPage"> <div class="box-setup-copayers" ng-show="!isSetupWalletPage">
<div class="box-setup-copayers p10"> <div class="box-setup-copayers p10">
<img class="br100 oh box-setup-copay m10" ng-repeat="i in getNumber(totalCopayers) track by $index" <img class="br100 oh box-setup-copay m10" ng-repeat="i in getNumber(totalCopayers) track by $index" src="./img/satoshi.gif" title="Copayer {{$index+1}}-{{totalCopayers}}" ng-class="{'box-setup-copay-required': ($index+1) <= requiredCopayers}" width="50px">
src="./img/satoshi.gif"
title="Copayer {{$index+1}}-{{totalCopayers}}"
ng-class="{'box-setup-copay-required': ($index+1) <= requiredCopayers}"
width="50px">
</div> </div>
</div> </div>
<p translate class="comment" ng-show="totalCopayers>1 && !isSetupWalletPage">(*) The limits are imposed by the bitcoin network.</p> <p translate class="comment" ng-show="totalCopayers>1 && !isSetupWalletPage">(*) The limits are imposed by the bitcoin network.</p>
<div class="text-right"> <div class="text-right">
<a ng-show="!isSetupWalletPage" class="back-button m20r" <a ng-show="!isSetupWalletPage" class="back-button m20r" href="#!/">&laquo; <span translate>Back</span></a>
href="#!/">&laquo; <span translate>Back</span></a> <a ng-show="isSetupWalletPage" class="back-button m20r" ng-click="setupWallet()">&laquo; <span translate>Back</span></a>
<a ng-show="isSetupWalletPage" class="back-button m20r"
ng-click="setupWallet()">&laquo; <span translate>Back</span></a>
<button translate ng-show="isSetupWalletPage" type="submit" class="button secondary m0" ng-disabled="setupForm.$invalid || loading"> <button translate ng-show="isSetupWalletPage" type="submit" class="button secondary m0" ng-disabled="setupForm.$invalid || loading">
Create {{requiredCopayers}}-of-{{totalCopayers}} wallet Create {{requiredCopayers}}-of-{{totalCopayers}} wallet
</button> </button>
<a translate class="button secondary m0" ng-show="!isSetupWalletPage" <a translate class="button secondary m0" ng-show="!isSetupWalletPage" ng-click="setupWallet()">Next</a>
ng-click="setupWallet()">Next</a>
</div> </div>
</div> </div>
</div> </div>
@ -103,4 +92,3 @@
</form> </form>
</div> </div>
</div> </div>

View file

@ -1,3 +1,60 @@
<span translate>Receive</span> {{'Receive'|translate}}
<span translate>History</span> {{'History'|translate}}
{{'Wrong password'|translate}}
{{'Copied to clipboard'|translate}}
{{'Please enter the required fields'|translate}}
{{'Import a backup'|translate}}
{{'Importing wallet - Reading backup...'|translate}}
{{'Importing wallet - Setting things up...'|translate}}
{{'Importing wallet - We are almost there...'|translate}}
{{'Error updating indexes:'|translate}}
{{'Please, select your backup file'|translate}}
{{'Please enter the required fields'|translate}}
{{'Fatal error connecting to Insight server'|translate}}
{{'The wallet is full'|translate}}
{{'Wallet network configuration missmatch'|translate}}
{{'The secret string you entered is invalid'|translate}}
{{'Transactions Proposals Purged'|translate}}
{{'transaction proposal purged'|translate}}
{{'Updating balance'|translate}}
{{'Scaning for transactions'|translate}}
{{'Using derived addresses from your wallet'|translate}}
{{'Finished'|translate}}
{{'The balance is updated using the derived addresses'|translate}}
{{'Login Required'|translate}}
{{'Please open wallet to complete payment'|translate}}
{{'Send'|translate}}
{{'Unable to send transaction proposal'|translate}}
{{'The transaction proposal has been created'|translate}}
{{'Form Error'|translate}}
{{'Please complete required fields'|translate}}
{{'Success'|translate}}
{{'New entry has been created'|translate}}
{{'There was an error sending the transaction'|translate}}
{{'Transaction rejected'|translate}}
{{'You rejected the transaction successfully'|translate}}
{{'There was an error signing the transaction'|translate}}
{{'Session will be closed'|translate}}
{{'Your session is about to expire due to inactivity in'|translate}}
{{'seconds'|translate}}
{{'Session closed'|translate}}
{{'Session closed because a long time of inactivity'|translate}}
{{'available.'|translate}}
{{'It\'s important that you update your wallet at https://copay.io'|translate}}
{{'Please update your wallet at https://copay.io'|translate}}
{{'Backup created'|translate}}
{{'Encrypted backup file saved'|translate}}
{{'Networking Error'|translate}}
{{'Could not connect to the Insight server. Check your settings and network configuration'|translate}}
{{'Received corrupt message from '|translate}}
{{'Transaction Update'|translate}}
{{'A transaction was signed by'|translate}}
{{'A transaction was rejected by'|translate}}
{{'Transaction Error'|translate}}
{{'Received corrupt transaction from'|translate}}
{{'New Transaction'|translate}}
{{'You have a pending transaction proposal'|translate}}
{{'You have'|translate}}
{{'pending transaction proposals'|translate}}
{{'Funds received!'|translate}}
{{'Transaction broadcasted'|translate}}

View file

@ -1,5 +1,9 @@
<div class="home" ng-controller="HomeController"> <div class="home" ng-controller="HomeController">
<div class="row"> <div data-alert class="loading-screen" ng-show="retreiving">
<i class="size-60 fi-bitcoin-circle icon-rotate spinner"></i>
Retreiving information from storage...
</div>
<div class="row" ng-show="!loading && !retreiving">
<div class="large-4 columns logo-setup"> <div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay" width="146" height="59"> <img src="img/logo-negative-beta.svg" alt="Copay" width="146" height="59">
<div ng-include="'views/includes/version.html'"></div> <div ng-include="'views/includes/version.html'"></div>

View file

@ -1,7 +1,7 @@
<div class="import" ng-controller="ImportController"> <div class="import" ng-controller="ImportController">
<div data-alert class="loading-screen" ng-show="loading"> <div data-alert class="loading-screen" ng-show="loading">
<i class="size-60 fi-bitcoin-circle icon-rotate spinner"></i> <i class="size-60 fi-bitcoin-circle icon-rotate spinner"></i>
{{ importStatus }} {{ importStatus|translate }}
</div> </div>
<div class="row" ng-init="choosefile=0; pastetext=0" ng-show="!loading"> <div class="row" ng-init="choosefile=0; pastetext=0" ng-show="!loading">
@ -25,13 +25,14 @@
placeholder="{{'Your wallet password'|translate}}" name="password" ng-model="password" required> placeholder="{{'Your wallet password'|translate}}" name="password" ng-model="password" required>
<a class="expand small" ng-click="hideAdv=!hideAdv"> <a class="expand small" ng-click="hideAdv=!hideAdv">
<i class="fi-widget m3r"></i>
<span translate ng-hide="!hideAdv">Show</span> <span translate ng-hide="!hideAdv">Show</span>
<span translate ng-hide="hideAdv">Hide</span> <span translate ng-hide="hideAdv">Hide</span>
<span translate>advanced options</span> <span translate>advanced options</span>
</a> </a>
<div ng-hide="hideAdv"> <div ng-hide="hideAdv" class="m10t">
<label> <label>
<input type="checkbox" class="form-control" <input type="checkbox" class="form-control"
name="skipPublicKeyRing" ng-model="skipPublicKeyRing"> name="skipPublicKeyRing" ng-model="skipPublicKeyRing">

View file

@ -10,12 +10,11 @@
class="ellipsis" class="ellipsis"
tooltip="ID: {{copayer.peerId}}" tooltip="ID: {{copayer.peerId}}"
tooltip-placement="bottom"> tooltip-placement="bottom">
<small class="text-gray" ng-show="copayer.index == 0"><i class="fi-check m5r"></i><span translate>Me</span></small> <small class="text-gray" ng-show="copayer.index == 0">
<i class="fi-check m5r"></i>{{'Me'|translate}}</small>
<small class="text-gray" ng-show="copayer.index > 0"><i class="fi-check m5r"></i>{{copayer.nick}}</small> <small class="text-gray" ng-show="copayer.index > 0"><i class="fi-check m5r"></i>{{copayer.nick}}</small>
</div> </div>
<div translate class="success label m10t" ng-show="isBackupReady(copayer)"> <div translate class="success label m10t" ng-show="isBackupReady(copayer)">Ready</div>
Ready
</div>
</div> </div>
</div> </div>

View file

@ -15,9 +15,7 @@
width="30"> width="30">
<div class="ellipsis" tooltip-placement="top" tooltip="{{copayer.nick}}"> <div class="ellipsis" tooltip-placement="top" tooltip="{{copayer.nick}}">
<small class="text-gray" ng-show="copayer.index == 0"> <small class="text-gray" ng-show="copayer.index == 0">{{'Me'|translate}}</small>
<span translate>Me</span>
</small>
<small class="text-gray" ng-show="copayer.index > 0">{{copayer.nick}}</small> <small class="text-gray" ng-show="copayer.index > 0">{{copayer.nick}}</small>
</div> </div>
</div> </div>

View file

@ -6,29 +6,30 @@
</a> </a>
<div ng-include="'views/includes/version.html'"></div> <div ng-include="'views/includes/version.html'"></div>
</div> </div>
</header> </header>
<div class="line-sidebar-b"></div> <div class="line-sidebar-b"></div>
<div class="founds size-12 box-founds p15" ng-disabled="$root.loading" ng-click="refresh()"> <div ng-if="$root.wallet" class="founds size-12 box-founds p15" ng-disabled="$root.loading" ng-click="refresh()">
<p class="text-gray"> <p class="text-gray">
<span>{{$root.wallet.getName()}}</span> <span>{{$root.wallet.getName()}}</span>
<span class="size-12 right">{{$root.wallet.requiredCopayers}}-of-{{$root.wallet.totalCopayers}}</span> <span class="size-12 right">{{$root.wallet.requiredCopayers}}-of-{{$root.wallet.totalCopayers}}</span>
</p> </p>
<div class="line-sidebar-t"> <div class="line-sidebar-t">
<span translate>Balance</span> {{'Balance'|translate}}
<span class="gray small side-bar right" title="Manual Refresh"><i class="size-16 fi-refresh"></i></span> <span class="gray small side-bar right" title="Manual Refresh"><i class="size-16 fi-refresh"></i></span>
<span ng-if="$root.updatingBalance"> <span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span> </span>
<span ng-if="!$root.updatingBalance">{{totalBalance || 0 <span ng-if="!$root.updatingBalance">{{totalBalance || 0
|noFractionNumber}} {{$root.unitName}} |noFractionNumber}} {{$root.wallet.settings.unitName}}
</span> </span>
</div> </div>
<div class="m10t" ng-show="lockedBalance"> <div class="m10t" ng-show="lockedBalance">
<span translate>Locked</span> {{'Locked'|translate}}
<span ng-if="$root.updatingBalance"> <span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span> </span>
<span ng-show="!$root.updatingBalance">{{lockedBalance || 0|noFractionNumber}} {{$root.unitName}} <span ng-show="!$root.updatingBalance">{{lockedBalance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}}
</span> &nbsp;<i class="fi-info medium" tooltip="{{'Balance locked in pending transaction proposals'|translate}}" tooltip-placement="bottom"></i> </span> &nbsp;<i class="fi-info medium" tooltip="{{'Balance locked in pending transaction proposals'|translate}}" tooltip-placement="bottom"></i>
</div> </div>
</div> </div>
@ -42,7 +43,7 @@
</li> </li>
<li> <li>
<a href="#" class="db p20h" title="Close" <a href="#" class="db p20h" title="Close"
ng-click="signout()"><i class="size-24 m20r fi-power"></i> <span translate>Close</span></a> ng-click="signout()"><i class="size-24 m20r fi-power"></i> {{'Close'|translate}}</a>
</li> </li>
</ul> </ul>

View file

@ -11,35 +11,35 @@
<a href="#!/receive" class="name-wallet" tooltip-placement="bottom" tooltip="ID: {{$root.wallet.id}}"> <a href="#!/receive" class="name-wallet" tooltip-placement="bottom" tooltip="ID: {{$root.wallet.id}}">
<span>{{$root.wallet.getName()}}</span> <span>{{$root.wallet.getName()}}</span>
</a> </a>
<a class="button gray small side-bar right" title="Manual Refresh" <a class="button gray small side-bar right" title="{{'Manual Update'|translate}}"
ng-disabled="$root.loading" ng-disabled="$root.loading"
ng-click="refresh()"><i class="size-16 fi-refresh"></i></a> ng-click="refresh()"><i class="size-16 fi-refresh"></i></a>
</div> </div>
<div class="founds size-14 m10v"> <div class="founds size-14 m10v">
<span translate>Balance</span> {{'Balance'|translate}}
<span ng-if="$root.updatingBalance"> <span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span> </span>
<span ng-if="!$root.updatingBalance" <span ng-if="$root.wallet && !$root.updatingBalance"
class="has-tip" class="has-tip size-18"
data-options="disable_for_touch:true" data-options="disable_for_touch:true"
tooltip-popup-delay='500' tooltip-popup-delay='500'
tooltip="{{totalBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}}" tooltip="{{totalBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}}"
tooltip-trigger="mouseenter" tooltip-trigger="mouseenter"
tooltip-placement="bottom">{{totalBalance || 0 |noFractionNumber}} {{$root.unitName}} tooltip-placement="bottom">{{totalBalance || 0 |noFractionNumber}} {{$root.wallet.settings.unitName}}
</span> </span>
<div class="m10t" ng-show="lockedBalance"> <div class="m10t" ng-show="lockedBalance">
<span translate>Locked</span> &nbsp; {{'Locked'|translate}} &nbsp;
<span ng-if="$root.updatingBalance"> <span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span> </span>
<span ng-show="!$root.updatingBalance" <span ng-if="$root.wallet && !$root.updatingBalance"
class="has-tip" class="has-tip"
data-options="disable_for_touch:true" data-options="disable_for_touch:true"
tooltip-popup-delay='500' tooltip-popup-delay='500'
tooltip="{{lockedBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}}" tooltip="{{lockedBalanceAlternative |noFractionNumber:2}} {{alternativeIsoCode}}"
tooltip-trigger="mouseenter" tooltip-trigger="mouseenter"
tooltip-placement="bottom">{{lockedBalance || 0|noFractionNumber}} {{$root.unitName}} tooltip-placement="bottom">{{lockedBalance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}}
</span> &nbsp;<i class="fi-info medium" tooltip="{{'Balance locked in pending transaction proposals'|translate}}" tooltip-placement="bottom"></i> </span> &nbsp;<i class="fi-info medium" tooltip="{{'Balance locked in pending transaction proposals'|translate}}" tooltip-placement="bottom"></i>
</div> </div>
</div> </div>
@ -56,7 +56,7 @@
</li> </li>
<li> <li>
<a href="#!/" class="db p20h" title="Close" <a href="#!/" class="db p20h" title="Close"
ng-click="signout()"><i class="size-21 m20r fi-power"></i> <span translate>Close</span></a> ng-click="signout()"><i class="size-21 m20r fi-power"></i> {{'Close'|translate}}</a>
</li> </li>
</ul> </ul>

View file

@ -1,4 +1,4 @@
<div class="last-transactions-header"> <div class="last-transactions-header oh">
<div class="hide-for-small-only large-1 medium-1 columns"> <div class="hide-for-small-only large-1 medium-1 columns">
<a class="text-black" ng-show="tx.comment"> <a class="text-black" ng-show="tx.comment">
<i class="fi-comment-quotes size-24" Popover-animation="true" popover="{{$root.wallet.publicKeyRing.nicknameForCopayer(tx.creator)}}" popover-title="{{tx.comment}}" popover-placement="right" popover-trigger="mouseenter"></i> <i class="fi-comment-quotes size-24" Popover-animation="true" popover="{{$root.wallet.publicKeyRing.nicknameForCopayer(tx.creator)}}" popover-title="{{tx.comment}}" popover-placement="right" popover-trigger="mouseenter"></i>
@ -8,24 +8,23 @@
</a> </a>
</div> </div>
<div class="show-for-small-only small-12 columns m10b" ng-show="tx.comment"> <div class="show-for-small-only small-12 columns m10b" ng-show="tx.comment">
<p class="size-14 label" > <p class="size-14 label">
{{tx.comment}} - {{tx.comment}} Created by <strong>{{$root.wallet.publicKeyRing.nicknameForCopayer(tx.creator)}}</strong>
{{$root.wallet.publicKeyRing.nicknameForCopayer(tx.creator)}}
</p> </p>
</div> </div>
<div class="large-8 medium-8 small-8 columns"> <div class="large-8 medium-8 small-9 columns">
<div ng-repeat="out in tx.outs"> <div ng-repeat="out in tx.outs">
<div class="large-3 medium-3 small-3 columns"> <div class="large-3 medium-3 small-4 columns">
<p class="size-14 hide-for-small-only">{{out.value | noFractionNumber}} {{$root.unitName}}</p> <p class="size-14 hide-for-small-only">{{out.value | noFractionNumber}} {{$root.wallet.settings.unitName}}</p>
<p class="size-12 show-for-small-only">{{out.value | noFractionNumber}} {{$root.unitName}}</p> <p class="size-12 show-for-small-only">{{out.value | noFractionNumber}} {{$root.wallet.settings.unitName}}</p>
</div> </div>
<div class="large-1 medium-1 small-2 columns fi-arrow-right"> </div> <div class="large-1 medium-1 small-2 columns fi-arrow-right"></div>
<div class="large-8 medium-8 small-7 columns ellipsis"> <div class="large-8 medium-8 small-7 columns ellipsis">
<contact address="{{out.address}}" tooltip-popup-delay="500" tooltip tooltip-placement="right"/> <contact address="{{out.address}}" tooltip-popup-delay="500" tooltip tooltip-placement="right" />
</div> </div>
</div> </div>
</div> </div>
<div class="large-3 medium-3 small-4 columns text-right"> <div class="large-3 medium-3 small-3 columns text-right">
<p class="size-12">{{tx.createdTs | amCalendar}}</p> <p class="size-12">{{tx.createdTs | amCalendar}}</p>
</div> </div>
</div> </div>
@ -37,25 +36,25 @@
</a> </a>
<div class="box-status"> <div class="box-status">
<a ng-if="c.actions.create" tooltip-popup-delay="1000" tooltip="Created {{c.actions.create | amTimeAgo}}"> <a ng-if="c.actions.create" tooltip-popup-delay="1000" tooltip="Created {{c.actions.create | amTimeAgo}}">
<i class="fi-crown icon-status icon-active"></i> <i class="fi-crown icon-status icon-active"></i>
</a> </a>
<a ng-if="!c.actions.create"><i class="fi-crown icon-status"></i></a> <a ng-if="!c.actions.create"><i class="fi-crown icon-status"></i></a>
<a ng-if="c.actions.seen" tooltip-popup-delay="1000" tooltip="Seen {{c.actions.seen | amTimeAgo}}"> <a ng-if="c.actions.seen" tooltip-popup-delay="1000" tooltip="Seen {{c.actions.seen | amTimeAgo}}">
<i class="fi-eye icon-status icon-active"></i> <i class="fi-eye icon-status icon-active"></i>
</a> </a>
<a ng-if="!c.actions.seen"><i class="fi-eye icon-status"></i></a> <a ng-if="!c.actions.seen"><i class="fi-eye icon-status"></i></a>
<a ng-if="c.actions.rejected" tooltip-popup-delay="1000" tooltip="Rejected {{c.actions.rejected | amTimeAgo}}"> <a ng-if="c.actions.rejected" tooltip-popup-delay="1000" tooltip="Rejected {{c.actions.rejected | amTimeAgo}}">
<i class="fi-x icon-status icon-active-x"></i> <i class="fi-x icon-status icon-active-x"></i>
</a> </a>
<a ng-if="c.actions.sign" tooltip-popup-delay="1000" tooltip="Signed {{c.actions.sign | amTimeAgo}}"> <a ng-if="c.actions.sign" tooltip-popup-delay="1000" tooltip="Signed {{c.actions.sign | amTimeAgo}}">
<i class="fi-check icon-status icon-active-check"></i> <i class="fi-check icon-status icon-active-check"></i>
</a> </a>
<a ng-if="!c.actions.sign && !c.actions.rejected && tx.missingSignatures" class="icon-status"> <a ng-if="!c.actions.sign && !c.actions.rejected && tx.missingSignatures" class="icon-status">
<i class="fi-loop icon-rotate"></i> <i class="fi-loop icon-rotate"></i>
</a> </a>
</div> </div>
@ -100,7 +99,7 @@
</div> </div>
<div ng-show="!tx.missingSignatures && tx.sentTs"> <div ng-show="!tx.missingSignatures && tx.sentTs">
<div class="is-valid m10b"> <div class="is-valid m10b">
<strong translate>Sent</strong> <span class="text-gray" am-time-ago="tx.sentTs"></span> <strong translate>Sent</strong> <span class="text-gray" am-time-ago="tx.sentTs"></span>
</div> </div>
<div class="ellipsis small"> <div class="ellipsis small">
<span translate>Transaction ID</span>: <span translate>Transaction ID</span>:
@ -110,12 +109,12 @@
</div> </div>
</div> </div>
<p translate class="text-gray m5b" ng-show="!tx.finallyRejected && tx.missingSignatures==1"> <p translate class="text-gray m5b" ng-show="!tx.finallyRejected && tx.missingSignatures==1">
One signature missing One signature missing
</p> </p>
<p translate class="text-gray m5b" ng-show="!tx.finallyRejected && tx.missingSignatures>1"> <p translate class="text-gray m5b" ng-show="!tx.finallyRejected && tx.missingSignatures>1">
{{tx.missingSignatures}} signatures missing</p> {{tx.missingSignatures}} signatures missing</p>
<div class="ellipsis small text-gray"> <div class="ellipsis small text-gray">
<strong translate>Fee</strong>: {{tx.fee|noFractionNumber}} {{$root.unitName}} <strong translate>Fee</strong>: {{tx.fee|noFractionNumber}} {{$root.wallet.settings.unitName}}
<strong translate>Proposal ID</strong>: {{tx.ntxid}} <strong translate>Proposal ID</strong>: {{tx.ntxid}}
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
<div ng-controller="VersionController"> <div ng-controller="VersionController">
<small>v{{version}} ({{defaultLanguage}})</small> <small>v{{version}}</small>
<small>#{{commitHash}}</small> <small>#{{commitHash}}</small>
<small ng-if="networkName=='testnet'">[ {{networkName}} ]</small> <small ng-if="networkName ==='testnet' || networkName ==='livenet'">[ {{networkName}} ]</small>
</div> </div>

View file

@ -22,9 +22,12 @@
</label> </label>
<div class="row collapse"> <div class="row collapse">
<div class="large-10 medium-10 small-10 columns"> <div class="large-10 medium-10 small-10 columns pr">
<input id="connectionId" type="text" class="small-9 columns" <input id="connectionId" type="text" class="small-9 columns"
placeholder="{{'Paste wallet secret here'|translate}}" name="connectionId" ng-model="connectionId" wallet-secret required> placeholder="{{'Paste wallet secret here'|translate}}" name="connectionId" ng-model="connectionId" wallet-secret required>
<small class="icon-input" ng-show="joinForm.connectionId.$invalid && !joinForm.connectionId.$pristine"><i class="fi-x"></i></small>
<small class="icon-input" ng-show="joinForm.connectionId.$valid
&& !joinForm.connectionId.$pristine"><i class="fi-check"></i></small>
</div> </div>
<div class="small-2 columns" ng-hide="showScanner || disableScanner"> <div class="small-2 columns" ng-hide="showScanner || disableScanner">
<a class="postfix button primary" ng-click="openScanner()"><i class="fi-camera">&nbsp;</i></a> <a class="postfix button primary" ng-click="openScanner()"><i class="fi-camera">&nbsp;</i></a>
@ -64,20 +67,29 @@
numbers and symbols</span>" tooltip-trigger="focus" numbers and symbols</span>" tooltip-trigger="focus"
tooltip-placement="top" required> tooltip-placement="top" required>
<input type="password" <div class="pr line-sidebar-b">
placeholder="{{'Repeat password'|translate}}" <input type="password"
name="joinPasswordConfirm" placeholder="{{'Repeat password'|translate}}"
ng-model="joinPasswordConfirm" name="joinPasswordConfirm"
match="joinPassword" required> ng-model="joinPasswordConfirm"
match="joinPassword" required>
<small class="icon-input" ng-show="joinForm.joinPasswordConfirm.$dirty && joinForm.joinPasswordConfirm.$invalid"><i class="fi-x"></i></small>
<p class="m15b text-gray size-12" ng-show="joinForm.joinPasswordConfirm.$dirty && joinForm.joinPasswordConfirm.$invalid">
<i class="fi-x m5r"></i>
{{'Passwords must match'|translate}}
</p>
</div>
<a class="expand small" ng-click="hideAdv=!hideAdv"> <a class="expand small" ng-click="hideAdv=!hideAdv">
<i class="fi-widget m3r"></i>
<span translate ng-hide="!hideAdv">Show</span> <span translate ng-hide="!hideAdv">Show</span>
<span translate ng-hide="hideAdv">Hide</span> <span translate ng-hide="hideAdv">Hide</span>
<span translate>advanced options</span> <span translate>advanced options</span>
</a> </a>
<div ng-hide="hideAdv"> <div ng-hide="hideAdv" class="m10t">
<p> <p>
<input type="text" <input type="text"
placeholder="{{'Private Key (Hex)'|translate}}" placeholder="BIP32 master extended private key (hex)"
name="private" name="private"
ng-model="$parent.private" ng-model="$parent.private"
> >

View file

@ -1,22 +1,21 @@
<h2 translate>Add Address Book Entry</h2> <h2 translate>Address Book</h2>
<form name="addressBookForm" ng-submit="submitAddressBook(addressBookForm)" novalidate> <form name="addressBookForm" ng-submit="submitAddressBook(addressBookForm)" novalidate>
<label for="newaddress"><span translate>Address</span> <label for="newaddress"><span translate>Address</span>
<small translate ng-hide="!addressBookForm.newaddress.$pristine || newaddress">required</small> <small translate ng-hide="!addressBookForm.newaddress.$pristine || newaddress">Required</small>
<small translate class="is-valid" ng-show="!addressBookForm.newaddress.$invalid && newaddress">Valid</small> <small translate class="is-valid" ng-show="!addressBookForm.newaddress.$invalid && newaddress">Valid</small>
<small translate class="has-error" ng-show="addressBookForm.newaddress.$invalid && newaddress"> <small translate class="has-error" ng-show="addressBookForm.newaddress.$invalid && newaddress">Not valid</small>
Not valid</small>
<input type="text" id="newaddress" name="newaddress" ng-disabled="loading" <input type="text" id="newaddress" name="newaddress" ng-disabled="loading"
placeholder="{{'Address'|translate}}" ng-model="newaddress" valid-address required> placeholder="{{'Address'|translate}}" ng-model="newaddress" valid-address required>
</label> </label>
<label for="newlabel"><span translate>Label</span> <label for="newlabel"><span translate>Label</span>
<small translate ng-hide="!addressBookForm.newlabel.$pristine || newlabel">required</small> <small translate ng-hide="!addressBookForm.newlabel.$pristine || newlabel">Required</small>
<input type="text" id="newlabel" name="newlabel" ng-disabled="loading" <input type="text" id="newlabel" name="newlabel" ng-disabled="loading"
placeholder="{{'Label'|translate}}" ng-model="newlabel" required> placeholder="{{'Label'|translate}}" ng-model="newlabel" required>
</label> </label>
<a translate class="button warning small default" ng-click="cancel()">Cancel</a> <a translate class="button warning small default" ng-click="cancel()">Cancel</a>
<input type="submit" class="button small primary right" <input type="submit" class="button small primary right"
ng-disabled="addressBookForm.$invalid || loading" ng-disabled="addressBookForm.$invalid || loading"
value="{{'Add Address'|translate}}"> value="{{'Add'|translate}}">
</form> </form>
<a class="close-reveal-modal" ng-click="cancel()">&#215;</a> <a class="close-reveal-modal" ng-click="cancel()">&#215;</a>

View file

@ -1,20 +1,22 @@
<div class="text-center"> <div class="text-center">
<qrcode size="250" data="bitcoin:{{address.address}}"></qrcode> <qrcode size="220" data="bitcoin:{{address.address}}"></qrcode>
<div class="m10t"> <div class="m10t">
<h4>{{address.address}} <span class="btn-copy" clip-copy="address.address"></span></h4> <h4 class="size-12">{{address.address}} <span class="btn-copy" clip-copy="address.address"></span></h4>
<span ng-if="$root.updatingBalance"> <span ng-if="$root.updatingBalance">
<i class="fi-bitcoin-circle icon-rotate spinner"></i> <i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span> </span>
<p class="m15b" ng-if="!$root.updatingBalance"> <p class="m15b size-18" ng-if="!$root.updatingBalance">
{{address.balance || 0|noFractionNumber}} {{$root.unitName}} {{address.balance || 0|noFractionNumber}} {{$root.wallet.settings.unitName}}
</p> </p>
<button class="m15t button secondary" open-external address="{{address.address}}"> <div class="small-10 columns small-centered">
<i class="fi-link">&nbsp;</i> <span translate>Open in external application</span> <button class="m15t button secondary hide-for-large-up" ng-show="isMobile" ng-click="mobileCopy(address.address)">
</button><br><br> <i class="fi-link">&nbsp;</i> <span translate>Copy to clipboard</span>
<button class="m15t button secondary" ng-show="isMobile" ng-click="mobileCopy(address.address)"> </button>
<i class="fi-link">&nbsp;</i> <span translate>Copy to clipboard</span> <a class="m15t secondary" open-external address="{{address.address}}">
</button> <span translate>Open in external application</span>
</a>
</div>
</div> </div>
</div> </div>
<a class="close-reveal-modal" ng-click="cancel()">&#215;</a> <a class="close-reveal-modal" ng-click="cancel()">&#215;</a>

View file

@ -1,62 +1,80 @@
<div class="backup" ng-controller="MoreController"> <div class="backup" ng-controller="MoreController">
<h1 translate>Settings </h1> <h1 translate>Settings </h1>
<div class="oh large-12 columns panel"> <div class="oh large-12 columns panel">
<h3><i class="fi-download m10r"></i> <span translate>Backup</span> </h3> <h3><i class="fi-download m10r"></i> <span translate>Backup</span> </h3>
<p translate class="large-8 columns text-gray">It's important to backup your wallet so that you can recover it in case of disaster</p> <p translate class="large-8 columns text-gray">It's important to backup your wallet so that you can recover it in case of disaster</p>
<div class="large-4 columns"> <div class="large-4 columns">
<a translate class="button primary expand" ng-click="downloadBackup()">Download File</a> <a translate class="button primary expand" ng-click="downloadBackup()">Download File</a>
</div>
</div> </div>
<div class="large-12 columns line-dashed-h m15b"> </div> </div>
<div> <div class="large-12 columns line-dashed-h m15b"></div>
<div class="oh large-12 columns panel"> <div class="row collapse">
<h3><i class="fi-minus-circle m10r"></i> <form name="settingsForm" class="large-6 small-12 columns">
<span translate>Delete Wallet</span> </h3> <fieldset>
<p translate class="large-8 columns text-gray">If all funds have been removed from your wallet and you do not wish to have the wallet data stored on your computer anymore, you can delete your wallet.</p> <legend translate>Wallet Unit</legend>
<div class="large-4 columns"> <select class="form-control" ng-model="selectedUnit" ng-options="o.name for o in unitOpts" required>
<a translate class="button warning expand" </select>
ng-really-message="'Are you sure to delete this wallet from this computer?'|translate" ng-really-click="deleteWallet()"> Delete</a> </fieldset>
<fieldset>
<legend translate>Alternative Currency</legend>
<select class="form-control" ng-model="selectedAlternative" ng-options="alternative.name for alternative in alternativeOpts" required>
</select>
</fieldset>
<div class="text-left">
<button translate type="submit" class="large-6 small-12 columns button primary m0 ng-binding" ng-disabled="setupForm.$invalid || loading" disabled="disabled" ng-click="save()">
Save
</button>
</div> </div>
</form>
</div>
<div class="large-12 columns line-dashed-h m15b"></div>
<div class="oh large-12 columns panel">
<h3><i class="fi-minus-circle m10r"></i> <span translate> Delete Wallet </span></h3>
<p translate class="large-8 columns text-gray">If all funds have been removed from your wallet and you do not wish to have the wallet data stored on your computer anymore, you can delete your wallet.</p>
<div class="large-4 columns">
<a translate class="button warning expand" ng-really-message="{{'Are you sure to delete this wallet from this computer?'|translate}}" ng-really-click="deleteWallet()"> Delete</a>
</div> </div>
</div> </div>
<p> <p>
<a class="expand small" ng-click="hideAdv=!hideAdv"> <a class="expand small" ng-click="hideAdv=!hideAdv">
<span translate ng-hide="!hideAdv">Show</span> <i class="fi-widget m3r"></i>
<span translate ng-hide="hideAdv">Hide</span> <span translate ng-hide="!hideAdv">Show</span>
<span translate>advanced options</span> <span translate ng-hide="hideAdv">Hide</span>
</a> <span translate>advanced options</span>
</a>
<div ng-hide="hideAdv"> <div ng-hide="hideAdv" class="m10t">
<div class="oh large-12 columns panel"> <div class="oh large-12 columns panel">
<h3><i class="fi-minus-circle m10r"></i> <h3><i class="fi-minus-circle m10r"></i>
<span translate>Master Private Key</span> </h3> <span translate>Master Private Key</span> </h3>
<p translate class="large-8 columns text-gray"> <p translate class="large-8 columns text-gray">
Your master private key contains the information to sign <b>any</b> transaction on this wallet. Handle with care. Your master private key contains the information to sign <b>any</b> transaction on this wallet. Handle with care.
</p> </p>
<div class="large-4 columns"> <div class="large-4 columns">
<a class="button primary expand" ng-click="hidePriv=!hidePriv"> <a class="button primary expand" ng-click="hidePriv=!hidePriv">
<span translate ng-hide="!hidePriv">Show</span> <span translate ng-hide="!hidePriv">Show</span>
<span translate ng-hide="hidePriv">Hide</span> <span translate ng-hide="hidePriv">Hide</span>
</a> </a>
</div> </div>
<textarea ng-hide="hidePriv" readonly>{{priv}}</textarea> <textarea ng-hide="hidePriv" readonly>{{priv}}</textarea>
</div> </div>
<div class="oh large-12 columns panel"> <div class="oh large-12 columns panel">
<h3><i class="fi-minus-circle m10r"></i> <span translate>Scan Wallet Addresses</span> </h3> <h3><i class="fi-minus-circle m10r"></i> <span translate>Scan Wallet Addresses</span> </h3>
<p translate class="large-8 columns text-gray"> <p translate class="large-8 columns text-gray">
This will scan the blockchain looking for addresses derived from your wallet, in case you have funds in addresses not yet generated (e.g.: you restored an old backup). This will also trigger a syncronization of addresses to other connected peers. This will scan the blockchain looking for addresses derived from your wallet, in case you have funds in addresses not yet generated (e.g.: you restored an old backup). This will also trigger a syncronization of addresses to other connected peers.
</p> </p>
<div class="large-4 columns"> <div class="large-4 columns">
<a translate class="button primary expand" ng-click="updateIndexes()"> <a translate class="button primary expand" ng-click="updateIndexes()">
Scan Scan
</a> </a>
</div> </div>
</div> </div>
<div class="oh large-12 columns panel"> <div class="oh large-12 columns panel">
<h3><i class="fi-minus-circle m10r"></i> <span translate>Purge Pending Transaction Proposals</span> </h3> <h3><i class="fi-minus-circle m10r"></i> <span translate>Purge Pending Transaction Proposals</span> </h3>
<p translate class="large-8 columns text-gray"> <p translate class="large-8 columns text-gray">
Pending Transactions Proposals will be discarted. This need to be done on <b>ALL<b> peers of a wallet, to prevent the old proposals to be resynced again. Pending Transactions Proposals will be discarted. This needs to be done on <b>ALL</b> peers of a wallet, to prevent the old proposals to be resynced again.
</p> </p>
<div class="large-4 columns"> <div class="large-4 columns">
<a translate class="button warning expand" ng-click="purge()"> <a translate class="button warning expand" ng-click="purge()">
@ -67,7 +85,7 @@
<div class="oh large-12 columns panel"> <div class="oh large-12 columns panel">
<h3><i class="fi-minus-circle m10r"></i> <span translate>Purge ALL Transaction Proposals</span> </h3> <h3><i class="fi-minus-circle m10r"></i> <span translate>Purge ALL Transaction Proposals</span> </h3>
<p translate class="large-8 columns text-gray"> <p translate class="large-8 columns text-gray">
ALL Transactions Proposals will be discarted. This need to be done on <b>ALL<b> peers of a wallet, to prevent the old proposals to be resynced again. ALL Transactions Proposals will be discarted. This needs to be done on <b>ALL</b> peers of a wallet, to prevent the old proposals to be resynced again.
</p> </p>
<div class="large-4 columns"> <div class="large-4 columns">
<a translate class="button warning expand" ng-click="purge(true)"> <a translate class="button warning expand" ng-click="purge(true)">
@ -78,4 +96,3 @@
</div> </div>
</div> </div>

View file

@ -1,9 +1,13 @@
<div class="open" ng-controller="OpenController"> <div class="open" ng-controller="OpenController">
<div data-alert class="loading-screen" ng-show="loading && !failure"> <div data-alert class="loading-screen" ng-show="retreiving">
<i class="size-60 fi-bitcoin-circle icon-rotate spinner"></i>
Retreiving information from storage...
</div>
<div data-alert class="loading-screen" ng-show="loading && !failure && !retreiving">
<i class="size-60 fi-bitcoin-circle icon-rotate spinner"></i> <i class="size-60 fi-bitcoin-circle icon-rotate spinner"></i>
<span translate>Connecting...</span> <span translate>Connecting...</span>
</div> </div>
<div class="row" ng-show="!loading"> <div class="row" ng-show="!loading && !retreiving">
<div class="large-4 columns logo-setup"> <div class="large-4 columns logo-setup">
<img src="img/logo-negative-beta.svg" alt="Copay" width="146" height="59"> <img src="img/logo-negative-beta.svg" alt="Copay" width="146" height="59">
<div ng-include="'views/includes/version.html'"></div> <div ng-include="'views/includes/version.html'"></div>

View file

@ -14,14 +14,14 @@
<div class="row collapse"> <div class="row collapse">
<div class="large-12 columns"> <div class="large-12 columns">
<div class="row collapse"> <div class="row collapse">
<label for="address"><span translate>To address</span> <label for="address"><span translate>To</span>
<small translate ng-hide="!sendForm.address.$pristine || address">required</small> <small translate ng-hide="!sendForm.address.$pristine || address">required</small>
<small translate class="is-valid" ng-show="!sendForm.address.$invalid && address">valid!</small> <small translate class="is-valid" ng-show="!sendForm.address.$invalid && address">valid!</small>
<small translate class="has-error" ng-show="sendForm.address.$invalid && address">not valid</small> <small translate class="has-error" ng-show="sendForm.address.$invalid && address">not valid</small>
</label> </label>
<div class="small-10 columns"> <div class="small-10 columns">
<input type="text" id="address" name="address" ng-disabled="loading || !!$root.merchant" <input type="text" id="address" name="address" ng-disabled="loading || !!$root.merchant"
placeholder="{{'Send to'|translate}}" ng-model="address" ng-change="onChanged()" valid-address required> placeholder="{{'Bitcoin address'|translate}}" ng-model="address" ng-change="onChanged()" valid-address required>
<small class="icon-input" ng-show="!sendForm.address.$invalid && address"><i class="fi-check"></i></small> <small class="icon-input" ng-show="!sendForm.address.$invalid && address"><i class="fi-check"></i></small>
<small class="icon-input" ng-show="sendForm.address.$invalid && address"><i class="fi-x"></i></small> <small class="icon-input" ng-show="sendForm.address.$invalid && address"><i class="fi-x"></i></small>
</div> </div>
@ -53,7 +53,7 @@
</div> </div>
<div class="row collapse"> <div class="row collapse">
<div class="large-5 medium-5 columns"> <div class="large-6 medium-6 columns">
<div class="row collapse"> <div class="row collapse">
<label for="amount"><span translate>Amount</span> <label for="amount"><span translate>Amount</span>
<small translate ng-hide="!sendForm.amount.$pristine">required</small> <small translate ng-hide="!sendForm.amount.$pristine">required</small>
@ -77,11 +77,11 @@
<a class="small input-note" title="{{'Send all funds'|translate}}" <a class="small input-note" title="{{'Send all funds'|translate}}"
ng-show="$root.availableBalance > 0 && (!$root.merchant || +$root.merchant.total === 0)" ng-show="$root.availableBalance > 0 && (!$root.merchant || +$root.merchant.total === 0)"
ng-click="topAmount(sendForm)"> ng-click="topAmount(sendForm)">
<span translate>Use all funds</span> ({{getAvailableAmount()}} {{$root.unitName}}) <span translate>Use all funds</span> ({{getAvailableAmount()}} {{$root.wallet.settings.unitName}})
</a> </a>
</div> </div>
<div class="small-3 columns"> <div class="small-3 columns">
<span class="postfix">{{$root.unitName}}</span> <span class="postfix">{{$root.wallet.settings.unitName}}</span>
</div> </div>
</div> </div>
</div> </div>
@ -117,9 +117,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row collapse"> <div class="row collapse">
<div class="large-5 medium-3 small-4 columns"> <span ng-if="!$root.alternativeConversionRate">
<i class="fi-bitcoin-circle icon-rotate spinner"></i>
</span>
<span class="left m5t text-gray size-14" ng-if="$root.alternativeConversionRate > 0">1 BTC = {{alternativeConversionRate|noFractionNumber:2}} {{alternativeIsoCode}}
</span>
<div class="large-5 medium-3 small-6 columns right">
<button type="submit" class="button primary expand text-center" ng-disabled="sendForm.$invalid || loading"> <button type="submit" class="button primary expand text-center" ng-disabled="sendForm.$invalid || loading">
Send Send
</button> </button>
@ -138,13 +144,13 @@
</p> </p>
<h6 translate>Total amount for this transaction:</h6> <h6 translate>Total amount for this transaction:</h6>
<p class="text-gray" ng-class="{'hidden': sendForm.amount.$invalid || !amount > 0}"> <p class="text-gray" ng-class="{'hidden': sendForm.amount.$invalid || !amount > 0}">
<b>{{amount + defaultFee |noFractionNumber}}</b> {{$root.unitName}} <b>{{amount + defaultFee |noFractionNumber}}</b> {{$root.wallet.settings.unitName}}
<small ng-if="isRateAvailable"> <small ng-if="isRateAvailable">
{{ rateService.toFiat((amount + defaultFee) * unitToSatoshi, alternativeIsoCode) | noFractionNumber: 2 }} {{ alternativeIsoCode }} {{ rateService.toFiat((amount + defaultFee) * unitToSatoshi, alternativeIsoCode) | noFractionNumber: 2 }} {{ alternativeIsoCode }}
<br> <br>
</small> </small>
<small> <small>
<span translate>Including fee of</span> {{defaultFee|noFractionNumber}} {{$root.unitName}} <span translate>Including fee of</span> {{defaultFee|noFractionNumber}} {{$root.wallet.settings.unitName}}
</small> </small>
</p> </p>
<div ng-show="wallet.isShared()"> <div ng-show="wallet.isShared()">
@ -181,9 +187,9 @@
<tr> <tr>
<th translate>Label</th> <th translate>Label</th>
<th translate>Address</th> <th translate>Address</th>
<th translate>Creator</th> <th ng-class="{'hide-for-small-only' : $root.wallet.isShared()}" ng-show="$root.wallet.isShared()" translate>Creator</th>
<th translate>Date</th> <th class="hide-for-small-only" translate>Date</th>
<th>&nbsp;</th> <th class="hide-for-small-only">&nbsp;</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -191,15 +197,15 @@
ng-repeat="(addr, info) in $root.wallet.addressBook" ng-repeat="(addr, info) in $root.wallet.addressBook"
ng-class="{'addressbook-disabled': info.hidden}"> ng-class="{'addressbook-disabled': info.hidden}">
<td><a ng-click="copyAddress(addr)" title="Copy address">{{info.label}}</a></td> <td><a ng-click="copyAddress(addr)" title="Copy address">{{info.label}}</a></td>
<td class="ellipsis">{{addr}} <span class="btn-copy" clip-copy="addr"></span></td> <td class="size-12">{{addr}} <span class="btn-copy" clip-copy="addr"></span></td>
<td>{{$root.wallet.publicKeyRing.nicknameForCopayer(info.copayerId)}}</td> <td ng-show="$root.wallet.isShared()" ng-class="{'hide-for-small-only' : $root.wallet.isShared()}">{{$root.wallet.publicKeyRing.nicknameForCopayer(info.copayerId)}}</td>
<td><time>{{info.createdTs | amCalendar}}</time></td> <td class="hide-for-small-only"><time>{{info.createdTs | amCalendar}}</time></td>
<td width="5"><a ng-click="toggleAddressBookEntry(addr)">{{info.hidden ? <td class="hide-for-small-only" width="5"><a ng-click="toggleAddressBookEntry(addr)">{{info.hidden ?
'Enable' : 'Disable'}}</a></td> 'Enable' : 'Disable'}}</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<button translate class="button tiny primary text-center" ng-click="openAddressBookModal()">Add New Entry</button> <button translate class="button tiny primary text-center" ng-click="openAddressBookModal()">Add</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,45 +10,20 @@
<form name="settingsForm"> <form name="settingsForm">
<fieldset> <fieldset>
<legend translate>Language</legend> <legend translate>Language</legend>
<select class="form-control" ng-model="selectedLanguage" <select class="form-control" ng-model="selectedLanguage" ng-options="o.name for o in availableLanguages" required>
ng-options="o.name for o in availableLanguages" required>
</select>
</fieldset>
<fieldset>
<legend translate>Bitcoin Network</legend>
<input id="network-name" type="checkbox" ng-model="networkName"
ng-true-value="livenet" ng-false-value="testnet" class="form-control" ng-click="changeNetwork()"
ng-disabled="forceNetwork"
ng-checked="networkName == 'livenet' ? true : false">
<label for="network-name">Livenet</label>
<div translate ng-show="forceNetwork">
Network has been fixed to <strong>{{networkName}}</strong> in this setup. See <a href="https://copay.io">copay.io</a> for options to use Copay on both livenet and testnet.
</div>
</fieldset>
<fieldset>
<legend translate>Wallet Unit</legend>
<select class="form-control" ng-model="selectedUnit" ng-options="o.name for o in unitOpts" required>
</select>
</fieldset>
<fieldset>
<legend translate>Alternative Currency</legend>
<select class="form-control" ng-model="selectedAlternative" ng-options="alternative.name for alternative in alternativeOpts" required>
</select> </select>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend translate>Insight API server</legend> <legend translate>Insight API server</legend>
<label for="insight-host">Host</label> <label for="insight-livenet">Livenet</label>
<input type="text" ng-model="insightHost" class="form-control" name="insight-host"> <input type="text" ng-model="insightLivenet" class="form-control" name="insight-livenet">
<label for="insight-port" translate>Port</label> <label for="insight-testnet">Testnet</label>
<input type="number" ng-model="insightPort" class="form-control" name="insight-port"> <input type="text" ng-model="insightTestnet" class="form-control" name="insight-testnet">
<input id="insight-secure" type="checkbox" ng-model="insightSecure" class="form-control" ng-click="changeInsightSSL()">
<label for="insight-secure" translate>Use SSL</label>
<p translate class="small"> <p translate class="small">
Insight API server is open-source software. You can run your own instance, check <a href="http://insight.is" target="_blank">Insight API Homepage</a></p> Insight API server is open-source software. You can run your own instances, check <a href="http://insight.is" target="_blank">Insight API Homepage</a>
</p>
</fieldset> </fieldset>
<div class="text-right"> <div class="text-right">
<a class="back-button text-white m20r" href="#!/">&laquo; <span translate>Back</span></a> <a class="back-button text-white m20r" href="#!/">&laquo; <span translate>Back</span></a>
<button translate type="submit" class="button primary m0 ng-binding" ng-disabled="setupForm.$invalid || loading" disabled="disabled" ng-click="save()"> <button translate type="submit" class="button primary m0 ng-binding" ng-disabled="setupForm.$invalid || loading" disabled="disabled" ng-click="save()">
@ -60,4 +35,3 @@
</div> </div>
</div> </div>
</div> </div>

View file

@ -7,7 +7,8 @@
<div class="last-transactions" ng-repeat="tx in txs | paged"> <div class="last-transactions" ng-repeat="tx in txs | paged">
<div ng-include="'views/includes/transaction.html'"></div> <div ng-include="'views/includes/transaction.html'"></div>
</div> </div>
<p ng-show="txs.length == 0"><span translate>No transactions proposals yet.</span></p> <p ng-show="txs.length == 0"><span translate>No transactions proposals yet.</span>
</p>
<pagination ng-show="txs.length > txpItemsPerPage" total-items="txs.length" items-per-page="txpItemsPerPage" page="txpCurrentPage" on-select-page="show()" class="pagination-small primary"></pagination> <pagination ng-show="txs.length > txpItemsPerPage" total-items="txs.length" items-per-page="txpItemsPerPage" page="txpCurrentPage" on-select-page="show()" class="pagination-small primary"></pagination>
</div> </div>
@ -20,18 +21,15 @@
<div class="large-12"> <div class="large-12">
<div class="m10b size-12" ng-hide="wallet.totalCopayers == 1"> <div class="m10b size-12" ng-hide="wallet.totalCopayers == 1">
<a class="text-gray active" ng-click="toogleLast()" <a class="text-gray active" ng-click="toogleLast()" ng-disabled="loading" loading="Updating" ng-hide="lastShowed && !loading">[ <span translate>Show</span> ]</a>
ng-disabled="loading" loading="Updating" ng-hide="lastShowed && !loading">[ <span translate>Show</span> ]</a> <a class="text-gray" ng-click="toogleLast()" ng-disabled="loading" loading="Updating" ng-show="lastShowed && !loading">[ <span translate>Hide</span> ]</a>
<a class="text-gray" ng-click="toogleLast()" ng-disabled="loading"
loading="Updating" ng-show="lastShowed && !loading">[ <span translate>Hide</span> ]</a>
</div> </div>
<div class="btransactions" ng-if="lastShowed"> <div class="btransactions" ng-if="lastShowed">
<div translate ng-if="!blockchain_txs[0].txid && !loading"> <div translate ng-if="!blockchain_txs[0].txid && !loading">
No transactions yet. <em><strong>No transactions yet.</strong></em> </div>
</div>
<div class="last-transactions" ng-repeat="btx in blockchain_txs | orderBy: 'time':true"> <div class="last-transactions" ng-repeat="btx in blockchain_txs | orderBy: 'time':true">
<div class="last-transactions-header size-14"> <div class="last-transactions-header oh size-14">
<div class="large-8 medium-7 small-4 columns ellipsis"> <div class="large-8 medium-7 small-4 columns ellipsis">
<a href="http://{{getShortNetworkName()}}.insight.is/tx/{{btx.txid}}" target="_blank"> <a href="http://{{getShortNetworkName()}}.insight.is/tx/{{btx.txid}}" target="_blank">
{{btx.txid}} {{btx.txid}}
@ -44,7 +42,7 @@
<time>{{btx.firstSeenTs * 1000 | amCalendar}}</time> <time>{{btx.firstSeenTs * 1000 | amCalendar}}</time>
</div> </div>
<div data-ng-show="btx.time && !btx.firstSeenTs"> <div data-ng-show="btx.time && !btx.firstSeenTs">
<span translate>mined at</span> <span translate>mined</span>
<time>{{btx.time * 1000 | amCalendar}}</time> <time>{{btx.time * 1000 | amCalendar}}</time>
</div> </div>
</div> </div>
@ -52,9 +50,9 @@
<div class="last-transactions-content"> <div class="last-transactions-content">
<div class="large-5 medium-5 small-12 columns"> <div class="large-5 medium-5 small-12 columns">
<div ng-repeat="vin in btx.vinSimple"> <div ng-repeat="vin in btx.vinSimple">
<small class="right m5t">{{vin.value| noFractionNumber}} {{$root.unitName}}</small> <small class="right m5t">{{vin.value| noFractionNumber}} {{$root.wallet.settings.unitName}}</small>
<p class="ellipsis text-gray size-12"> <p class="ellipsis text-gray size-12">
<contact address="{{vin.addr}}" tooltip-popup-delay="500" tooltip tooltip-placement="right"/> <contact address="{{vin.addr}}" tooltip-popup-delay="500" tooltip tooltip-placement="right" />
</p> </p>
</div> </div>
</div> </div>
@ -66,20 +64,20 @@
</div> </div>
<div class="large-6 medium-6 small-12 columns"> <div class="large-6 medium-6 small-12 columns">
<div ng-repeat="vout in btx.voutSimple"> <div ng-repeat="vout in btx.voutSimple">
<small class="right m5t">{{vout.value| noFractionNumber}} {{$root.unitName}}</small> <small class="right m5t">{{vout.value| noFractionNumber}} {{$root.wallet.settings.unitName}}</small>
<p class="ellipsis text-gray size-12"> <p class="ellipsis text-gray size-12">
<contact address="{{vout.addr}}" tooltip-popup-delay="500" tooltip tooltip-placement="right"/> <contact address="{{vout.addr}}" tooltip-popup-delay="500" tooltip tooltip-placement="right" />
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="last-transactions-footer"> <div class="last-transactions-footer">
<div class="large-6 medium-6 small-6 columns"> <div class="large-6 medium-6 small-6 columns">
<p class="size-12"><span translate>Fee</span>: {{btx.fees | noFractionNumber}} {{$root.unitName}}</p> <p class="size-12"><span translate>Fee</span>: {{btx.fees | noFractionNumber}} {{$root.wallet.settings.unitName}}</p>
<p class="size-12"><span translate>Confirmations</span>: {{btx.confirmations || 0}}</p> <p class="size-12"><span translate>Confirmations</span>: {{btx.confirmations || 0}}</p>
</div> </div>
<div class="large-6 medium-6 small-6 columns text-right"> <div class="large-6 medium-6 small-6 columns text-right">
<p class="label size-14"><span translate>Total</span>: {{btx.valueOut| noFractionNumber}} {{$root.unitName}}</p> <p class="label size-14"><span translate>Total</span>: {{btx.valueOut| noFractionNumber}} {{$root.wallet.settings.unitName}}</p>
</div> </div>
</div> </div>
</div> </div>
@ -87,4 +85,3 @@
</div> </div>
</div> </div>
</div> </div>