diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index b5ecdd172..000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "lib" -} diff --git a/.ctags b/.ctags deleted file mode 100644 index f42671741..000000000 --- a/.ctags +++ /dev/null @@ -1,2 +0,0 @@ ---exclude=js/copayBundle.js ---exclude=lib/bitcore/browser/bundle.js diff --git a/.gitignore b/.gitignore index f075c785b..550fb9bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,85 +1,82 @@ -# from https://github.com/github/gitignore/blob/master/Node.gitignore -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz -*.sw* -*.sig -tags -pids -logs -results -build - -node_modules - -# extras -*.swp -*.swo -*~ -.project -peerdb.json - -npm-debug.log -.nodemonignore - -.DS_Store -public/lib/* -public/js/angularjs-all.js -public/js/main.js -public/js/vendors.js - -public/css/main.css - -README.html - -lib/* -!lib/socket.io.js -!lib/sjcl.js - -js/copayBundle.js -js/copayMain.js -css/copay.min.css -css/vendors.min.css - # translation po/* !po/*.po -js/translations.js +src/js/translations.js -webapp -browser-extensions/chrome/copay-chrome-extension -browser-extensions/chrome/copay-chrome-extension.zip -browser-extensions/firefox/firefox-addon -browser-extensions/firefox/data -browser-extensions/firefox/copay.xpi -version.js -!js/controllers/version.js +# version +src/js/version.js +# cordova cordova/project/* cordova/*.keystore -coverage/ +# Logs +logs +*.log -shell/bin/linux -shell/bin/darwin -shell/bin/win32 +# readme +README.html -shell/scripts/bin -shell/scripts/build +# Runtime data +pids +*.pid +*.seed -dist/darwin -dist/linux -dist/windows -dist/web -dist/*.dmg -dist/*.tar.gz -dist/*.exe +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov -doc/ -/node_modules -/*-cov +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules +bower_components + +# Users Environment Variables +.lock-wscript + +# OSX + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# VIM ignore + +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + +# copay public +public/icons/* +public/css/* +public/lib/* +public/js/* diff --git a/.jshint b/.jshint deleted file mode 100644 index 219ea40f2..000000000 --- a/.jshint +++ /dev/null @@ -1,15 +0,0 @@ -{ - "camelcase": true, - "curly": true, - "eqeqeq": true, - "freeze": true, - "indent": 2, - "newcap": true, - "quotmark": "single", - "maxdepth": 3, - "maxstatements": 15, - "maxlen": 80, - "eqnull": true, - "funcscope": true, - "node": true -} diff --git a/Gruntfile.js b/Gruntfile.js index 2cb1d039b..575fd0f64 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,9 +1,8 @@ module.exports = function(grunt) { - require('load-grunt-tasks')(grunt); - // Project Configuration grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), release: { options: { bump: true, @@ -26,11 +25,11 @@ module.exports = function(grunt) { } }, exec: { - prod: { - command: 'node ./util/build.js' + version: { + command: 'node ./util/version.js' }, - dev: { - command: 'node ./util/build.js -d' + clear: { + command: 'rm -Rf bower_components node_modules' } }, watch: { @@ -44,52 +43,22 @@ module.exports = function(grunt) { files: ['README.md'], tasks: ['markdown'] }, - scripts: { - files: [ - 'js/models/*.js', - 'js/util/*.js', - 'js/plugins/*.js', - 'js/*.js', - '!js/copayBundle.js', - '!js/copayMain.js' - ], - tasks: ['exec:dev'] - }, css: { - files: ['css/src/*.css'], - tasks: ['cssmin:desktop'] + files: ['src/css/*.css'], + tasks: ['concat:css'] }, main: { files: [ - 'js/init.js', - 'js/app.js', - 'js/directives.js', - 'js/filters.js', - 'js/routes.js', - 'js/services/*.js', - 'js/controllers/*.js' + 'src/js/init.js', + 'src/js/app.js', + 'src/js/directives/*.js', + 'src/js/filters/*.js', + 'src/js/routes.js', + 'src/js/services/*.js', + 'src/js/models/*.js', + 'src/js/controllers/*.js' ], - tasks: ['concat:main'] - }, - config: { - files: ['config.js'], - tasks: ['exec:dev', 'concat:main'] - }, - test: { - files: ['test/**/*.js'], - tasks: ['mochaTest'] - } - }, - mochaTest: { - tests: { - options: { - require: 'setup/node.js', - reporter: 'spec', - mocha: require('mocha') - }, - src: [ - 'test/*.js', - ] + tasks: ['concat:js'] } }, markdown: { @@ -97,72 +66,80 @@ module.exports = function(grunt) { files: [{ expand: true, src: 'README.md', - dest: '.', + dest: './doc', ext: '.html' }] } }, concat: { - vendors: { - src: [ - 'lib/moment/min/moment.min.js', - 'lib/qrcode-generator/js/qrcode.js', - 'lib/lodash/dist/lodash.js', - 'lib/bitcore.js', - 'lib/file-saver/FileSaver.js', - 'lib/socket.io-client/socket.io.js', - 'lib/sjcl.js', - 'lib/qrcode-decoder-js/lib/qrcode-decoder.min.js', - 'lib/moment/lang/*.js' - ], - dest: 'lib/vendors.js' + options: { + sourceMap: false, + sourceMapStyle: 'link' // embed, link, inline }, angular: { src: [ - 'lib/angular/angular.min.js', - 'lib/angular-route/angular-route.min.js', - 'lib/angular-moment/angular-moment.js', - 'lib/angular-qrcode/qrcode.js', - 'lib/ng-idle/angular-idle.min.js', - 'lib/angular-foundation/mm-foundation.min.js', - 'lib/angular-foundation/mm-foundation-tpls.min.js', - 'lib/angular-gettext/dist/angular-gettext.min.js', - 'lib/angular-load/angular-load.min.js', - 'lib/angular-gravatar/build/md5.min.js', - 'lib/angular-gravatar/build/angular-gravatar.min.js', - 'lib/angular-touch/angular-touch.min.js' - // If you add libs here, remember to add it too to karma.conf + 'bower_components/fastclick/lib/fastclick.js', + 'bower_components/qrcode-generator/js/qrcode.js', + 'bower_components/qrcode-decoder-js/lib/qrcode-decoder.js', + 'bower_components/moment/min/moment-with-locales.js', + 'bower_components/angular/angular.js', + 'bower_components/angular-ui-router/release/angular-ui-router.js', + 'bower_components/angular-local-storage/dist/angular-local-storage.js', + 'bower_components/angular-foundation/mm-foundation.js', + 'bower_components/angular-foundation/mm-foundation-tpls.js', + 'bower_components/angular-animate/angular-animate.js', + 'bower_components/angular-moment/angular-moment.js', + 'bower_components/ng-lodash/build/ng-lodash.js', + 'bower_components/angular-qrcode/qrcode.js', + 'bower_components/angular-gettext/dist/angular-gettext.js', + 'bower_components/angular-touch/angular-touch.js', + 'bower_components/angular-bitcore-wallet-client/angular-bitcore-wallet-client.js', + 'bower_components/angular-ui-switch/angular-ui-switch.js' ], - dest: 'lib/angularjs-all.js' + dest: 'public/lib/angular.js' }, - main: { + js: { src: [ - 'js/app.js', - 'js/directives.js', - 'js/filters.js', - 'js/routes.js', - 'js/services/*.js', - 'js/controllers/*.js', - 'js/translations.js', - 'js/init.js', + 'src/js/app.js', + 'src/js/routes.js', + 'src/js/directives/*.js', + 'src/js/filters/*.js', + 'src/js/models/*.js', + 'src/js/services/*.js', + 'src/js/controllers/*.js', + 'src/js/translations.js', + 'src/js/version.js', + 'src/js/init.js' ], - dest: 'js/copayMain.js' + dest: 'public/js/copay.js' + }, + css: { + src: ['src/css/*.css'], + dest: 'public/css/copay.css' + }, + foundation: { + src: [ + 'bower_components/angular/angular-csp.css', + 'bower_components/foundation/css/foundation.css', + 'bower_components/animate.css/animate.css', + 'bower_components/angular-ui-switch/angular-ui-switch.css' + ], + dest: 'public/css/foundation.css', } }, cssmin: { - desktop: { + copay: { files: { - 'css/copay.min.css': ['css/src/*.css'], + 'public/css/copay.css': ['src/css/*.css'], } }, - mobile: { + foundation: { files: { - 'css/copay.min.css': ['css/src/*.css', '!css/src/desktop.css', '!css/src/animation.css'], - } - }, - vendors: { - files: { - 'css/vendors.min.css': ['css/foundation.min.css', 'css/foundation-icons.css'] + 'public/css/foundation.css': [ + 'bower_components/angular/angular-csp.css', + 'bower_components/foundation/css/foundation.css', + 'bower_components/animate.css/animate.css' + ] } } }, @@ -172,16 +149,15 @@ module.exports = function(grunt) { }, prod: { files: { - 'js/copayMain.js': ['js/copayMain.js'], - 'lib/angularjs-all.js': ['lib/angularjs-all.js'], - 'lib/vendors.js': ['lib/vendors.js'] + 'public/js/copay.js': ['public/js/copay.js'], + 'public/lib/angular.js': ['public/lib/angular.js'] } } }, nggettext_extract: { pot: { files: { - 'po/template.pot': ['index.html', 'views/*.html', 'views/**/*.html'] + 'po/template.pot': ['public/index.html', 'public/views/*.html', 'public/views/**/*.html'] } }, }, @@ -191,70 +167,58 @@ module.exports = function(grunt) { module: 'copayApp' }, files: { - 'js/translations.js': ['po/*.po'] + 'src/js/translations.js': ['po/*.po'] } }, }, copy: { - dist: { - files: [ - { - src: [ - 'index.html', - 'init.js', - 'config.js', - 'popup.html', - 'css/vendors.min.css', - 'css/copay.min.css', - 'js/copayBundle.js', - 'js/copayMain.js', - 'lib/vendors.js', - 'lib/angularjs-all.js', - 'font/**', - 'img/**', - 'sound/**', - 'views/**' - ], - dest: 'dist/web/' - } - ], - }, + icons: { + expand: true, + flatten: true, + src: 'bower_components/foundation-icon-fonts/foundation-icons.*', + dest: 'public/icons/' + } }, - jsdoc: { - dist: { - src: ['js/models/*.js', 'js/plugins/*.js'], - options: { - destination: 'doc', - configure: 'jsdoc.conf.json', - template: './node_modules/grunt-jsdoc/node_modules/ink-docstrap/template', - theme: 'flatly' - } + karma: { + unit: { + configFile: 'test/karma.conf.js' + }, + prod: { + configFile: 'test/karma.conf.js', + singleRun: true + } + }, + coveralls: { + options: { + debug: false, + coverageDir: 'coverage/report-lcov', + dryRun: true, + force: true, + recursive: false } } }); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.loadNpmTasks('grunt-angular-gettext'); + grunt.loadNpmTasks('grunt-markdown'); + grunt.loadNpmTasks('grunt-release'); + grunt.loadNpmTasks('grunt-exec'); + grunt.loadNpmTasks('grunt-karma'); + grunt.loadNpmTasks('grunt-karma-coveralls'); + grunt.registerTask('default', [ - 'exec:dev', 'nggettext_compile', 'concat', 'cssmin:desktop', 'cssmin:vendors' - ]); - grunt.registerTask('mobile', [ - 'exec:dev', 'nggettext_compile', 'concat', 'cssmin:mobile', 'cssmin:vendors' - ]); - grunt.registerTask('dist', [ - 'exec:prod', 'nggettext_compile', 'concat', 'cssmin:desktop', 'cssmin:vendors', 'uglify', 'copy:dist' - ]); - grunt.registerTask('dist-dbg', [ - 'exec:prod', 'nggettext_compile', 'concat', 'cssmin:desktop', 'cssmin:vendors', 'copy:dist' - ]); - grunt.registerTask('dist-mobile', [ - 'exec:prod', 'nggettext_compile', 'concat', 'cssmin:mobile', 'cssmin:vendors', 'uglify', 'copy:dist' - ]); - grunt.registerTask('dist-mobile-dbg', [ - 'exec:dev', 'nggettext_compile', 'concat', 'cssmin:mobile', 'cssmin:vendors', 'copy:dist' + 'nggettext_compile', 'exec:version', 'concat', 'copy' ]); grunt.registerTask('prod', [ - 'exec:prod', 'nggettext_compile', 'concat', 'cssmin:desktop', 'cssmin:vendors', 'uglify' + 'default', 'uglify' ]); grunt.registerTask('translate', ['nggettext_extract']); - grunt.registerTask('docs', ['jsdoc']); + grunt.registerTask('test', ['karma:unit']); + grunt.registerTask('test-coveralls', ['karma:prod', 'coveralls']); }; diff --git a/Makefile b/Makefile index 25563803c..2fc90c1a7 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,8 @@ cordova-base: # release-android: cordova-base # make -C cordova release-android # -wp8: - cordova/build.sh WP8 +wp8-prod: + cordova/build.sh WP8 --clear cordova/wp/fix-svg.sh echo -e "\a" @@ -31,7 +31,7 @@ wp8-debug: cordova/wp/fix-svg.sh echo -e "\a" -ios: +ios-prod: cordova/build.sh IOS --clear cd cordova/project && cordova build ios open cordova/project/platforms/ios/Copay.xcodeproj @@ -41,10 +41,14 @@ ios-debug: cd cordova/project && cordova build ios open cordova/project/platforms/ios/Copay.xcodeproj -android: - cordova/build.sh ANDROID --dbgjs --clear - cd cordova/project && cordova run android - android-prod: cordova/build.sh ANDROID --clear cd cordova/project && cordova build android --release + +android-debug: + cordova/build.sh ANDROID --dbgjs --clear + cd cordova/project && cordova run android + +android-debug-fast: + cordova/build.sh ANDROID --dbgjs + cd cordova/project && cordova run android diff --git a/README.md b/README.md index fd57cd1d4..7052e0d26 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,17 @@ [![Coverage Status](https://img.shields.io/coveralls/bitpay/copay.svg)](https://coveralls.io/r/bitpay/copay?branch=master) [![Stories in Ready](https://badge.waffle.io/bitpay/copay.svg?label=in progress&title=In progress)](https://waffle.io/bitpay/copay) -*Copay* is an easy-to-use, open-source, multiplatform, multisignature, secure bitcoin wallet platform for both individuals and companies. With Copay, now everyone using bitcoin can enjoy a wallet with corporate-level security! +*Copay* is an easy-to-use, open-source, multiplatform, multisignature, secure bitcoin wallet platform for both individuals and companies. -When friends or company executives join a *Copay* wallet, more than one person must sign every transaction. If your computer is ever compromised and your private keys are stolen, the bitcoins are still safe if you used the multi-signature feature. This feature is in addition to state-of-the-art encrypted storage and communication. +*Copay* uses Bitcore-Wallet-Service (https://github.com/bitpay/bitcore-wallet-service) for peer syncronization and bitcore network interfacing. ## Before you start -**Note:** Please check the [Copay Known Issues](https://github.com/bitpay/copay/wiki/Copay-Known-issues) before using *Copay* with real Bitcoins. +Current Copay version is meant to be run on *mobile devices OR desktop application*, NOT browsers. + +When running Copay as a web page, and a browser extension is enabled, the browser extension +could have access to Copay internal data, and compromise the user's private key +and more. ## Installation @@ -48,6 +52,11 @@ npm start Then visit localhost:3000 in your browser. +## Bitcore Wallet Service + +Copay depends on Bitcore Wallet Service (BWS) for blockchain information, networking and copayer syncronization. BWS can be run within minutes or you can use a public instance. Switch between BWS instances is very simple and can be done with a click from Copay. BWS also allows Copay to interoperate with others wallet like Bitcore-Wallet CLI https://github.com/bitpay/bitcore-wallet + + ## Tests Open test/index.html in your browser to test models. Install [mocha](https://www.npmjs.com/package/mocha) and [karma](https://www.npmjs.com/package/karma-cli) to test the services and controllers. @@ -64,10 +73,6 @@ mocha karma start ``` -## Configuration - -The default configuration can be found in the config.js file - see [config.js](https://github.com/bitpay/copay/blob/master/config.js) for more info. This configuration could be partially overridden with the options set at the "Settings" tab. - ## Troubleshooting ### Building on Ubuntu 14.04, gyp, Python @@ -104,7 +109,6 @@ On success, the chrome extension will be located at: `browser-extensions/chrome/ To install it go to `chrome://extensions/` in your Chrome browser and ensure you have the 'developer mode' option enabled in the settings. Then click on "Load unpacked chrome extension" and choose the directory mentioned above. ## Firefox Add-on - The *CoPay* Firefox Extension has been deprecated and is no longer supported. # QA and Bug Reporting @@ -136,32 +140,18 @@ Platform: Android 4.3, Android 4.4, iOS ## General -*Copay* implements a multisig wallet using [p2sh](https://en.bitcoin.it/wiki/Pay_to_script_hash) addresses. It supports multiple wallet configurations, such as 3-of-5 (3 required signatures from 5 participant peers) or 2-of-3. To create a multisig wallet shared between multiple participants, *Copay* requires the public keys of all the wallet participants. Those public keys are then incorporated into the wallet configuration and combined to generate a payment address where funds can be sent into the wallet. Conversely, each participant manages their own private key and that private key is never transmitted anywhere. +*Copay* implements a multisig wallet using [p2sh](https://en.bitcoin.it/wiki/Pay_to_script_hash) addresses. It supports multiple wallet, with with its own configuration, such as 3-of-5 (3 required signatures from 5 participant peers) or 2-of-3. To create a multisig wallet shared between multiple participants, *Copay* requires the extended public keys of all the wallet participants. Those public keys are then incorporated into the wallet configuration and combined to generate a payment address where funds can be sent into the wallet. Conversely, each participant manages their own private key and that private key is never transmitted anywhere. -To unlock a payment and spend the wallet's funds, a quorum of participant signatures must be collected and assembled in the transaction. The funds cannot be spent without at least the minimum number of signatures required by the wallet configuration (2 of 3, 3 of 5, 6 of 6, etc). Once a transaction proposal is created, the proposal is distributed among the wallet participants for each to sign the transaction locally. Once the transaction is signed, the last signing participant will broadcast the transaction to the Bitcoin network using a public API (defaults to the [Insight API](https://github.com/bitpay/insight-api)). +To unlock a payment and spend the wallet's funds, a quorum of participant signatures must be collected and assembled in the transaction. The funds cannot be spent without at least the minimum number of signatures required by the wallet configuration (2 of 3, 3 of 5, 6 of 6, etc). Once a transaction proposal is created, the proposal is distributed among the wallet participants for each to sign the transaction locally. Once the transaction is signed, the last signing participant will broadcast the transaction to the Bitcoin network. *Copay* also implements [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) to generate new addresses for peers. The public key that each participant contributes to the wallet is a BIP32 extended public key. As additional public keys are needed for wallet operations (to produce new addresses to receive payments into the wallet, for example) new public keys can be derived from the participants' original extended public keys. Once again, it's important to stress that each participant keeps their own private keys locally - private keys are not shared - and are used to sign transaction proposals to make payments from the shared wallet. For more information regarding how addresses are generated using this procedure, see: [Structure for Deterministic P2SH Multisignature Wallets](https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki). -## Security model - -*Copay* peers encrypt and sign each message using the hybrid Elliptic Curve Integrated Encryption Scheme ([ECIES](http://en.wikipedia.org/wiki/Integrated_Encryption_Scheme)). - -The *identity key* is an ECDSA public key derived from the participant's extended public key using a specific BIP32 branch. This special public key is never used for Bitcoin address creation and should only be known by members of the WR. In *Copay* this special public key is named *copayerId*. A hash of the copayerId named *peerId* is used to register with a peerjs server. For more information on this hash value, please refer to the Bitcoin Wiki page entitled [Identity protocol v1](https://en.bitcoin.it/wiki/Identity_protocol_v1). - -Registering with a hash avoids disclosing the copayerId to parties outside of the WR. Peer discovery is accomplished using only the hashes of the WR members' copayerIds. All members of the WR know the full copayerId's of all the other members of the WR. - ## Bitcore Copay uses the powerful and feature-rich [Bitcore](https://github.com/bitpay/bitcore) Javascript library for Bitcoin-related functions. Bitcore should be built this way: -```sh -var cmd = `node util/build_bitcore.js` -cd -node $cmd -``` - ## Payment Protocol Copay currently supports [BIP70 (Payment Protocol)](https://github.com/bitcoin/bips/blob/master/bip-0070.mediawiki), with the following limitations: diff --git a/app.js b/app.js index dcdb86880..b4909fd2d 100644 --- a/app.js +++ b/app.js @@ -1,54 +1,13 @@ var express = require('express'); -var http = require('http'); +var path = require('path'); var app = express(); +app.use(express.static(__dirname + '/public')); -// This is not really necesarry, just to simulate -// Content Security Policy from Google Chrome Apps. -// -// app.use(function(req, res, next){ -// res.header("Content-Security-Policy", "script-src 'self';object-src 'none';media-src 'self';frame-src 'none';font-src 'self' data:"); -// next(); -// }); +var port = process.env.PORT || 3000; +app.listen(port); +console.log("App listening on port " + port); -app.use('/', express.static(__dirname + '/')); app.get('*', function(req, res) { - return res.sendFile(__dirname + '/' + 'index.html'); + res.sendFile(path.join(__dirname, 'public')); }); -app.start = function(port, callback) { - - app.set('port', port); - app.use(express.static(__dirname)); - - if (process.env.USE_HTTPS) { - var path = require('path'); - - var bc = path.dirname(require.resolve('bitcore/package.json')); - var pserver = require(bc + '/examples/PayPro/server.js'); - - pserver.removeListener('request', pserver.app); - - // pserver.options['no-tx'] = true; - // pserver.options['discovery'] = true; - - pserver.on('request', function(req, res) { - if (req.url.indexOf('/-/') === 0) { - return pserver.app(req, res); - } - return app(req, res); - }); - - pserver.listen(port, function() { - callback('https://localhost:' + port); - }); - - return; - } - - app.listen(port, function() { - callback('http://localhost:' + port); - }); -}; - -module.exports = app; - diff --git a/TODO.md b/atom-shell.md similarity index 98% rename from TODO.md rename to atom-shell.md index bc9a34c43..0807918fb 100644 --- a/TODO.md +++ b/atom-shell.md @@ -1,3 +1,8 @@ + +# Warning + +This NEEDS to be updated. + ## Running in the Native Shell Copay can be executed from within a "native" application shell, providing some diff --git a/bower.json b/bower.json index 31ac3bc02..46a905951 100644 --- a/bower.json +++ b/bower.json @@ -3,29 +3,27 @@ "keywords": [ "copay", "wallet", - "multisignature" + "multisignature", + "bircore" ], "dependencies": { - "angular": "~1.3.0", + "angular": "~1.3.13", "angular-foundation": "*", - "angular-route": "~1.3.0", - "angular-qrcode": "~3.1.0", - "angular-mocks": "~1.3.0", - "angular-gettext": "~1.1.0", - "mocha": "~1.18.2", - "chai": "~1.9.1", - "sjcl": "1.0.0", - "file-saver": "*", + "angular-animate": "~1.3.13", + "angular-touch": "~1.3.0", + "angular-qrcode": "~5.1.0", + "angular-gettext": "~2.0.5", + "animate.css": "~3.2.0", + "foundation": "zurb/bower-foundation#~5.5.1", + "foundation-icon-fonts": "*", + "ng-lodash": "~0.2.0", + "angular-moment": "~0.9.0", + "angular-bitcore-wallet-client": "^0.0.14", + "angular-ui-router": "~0.2.13", "qrcode-decoder-js": "*", - "angular-moment": "~0.7.1", - "socket.io-client": ">=1.0.0", - "ng-idle": "~1.0.2", - "inherits": "~0.0.1", - "lodash": "~2.4.1", - "angular-gravatar": "*", - "angular-touch": "~1.3.0" + "angular-ui-switch": "~0.1.0" }, "resolutions": { - "angular": "~1.3.0" + "qrcode-generator": "0.0.1" } } diff --git a/browser-extensions/chrome/README.md b/browser-extensions/chrome/README.md deleted file mode 100644 index 0eec88a19..000000000 --- a/browser-extensions/chrome/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Development - -* Just run: - -``` -$ sh chrome/build.sh -``` - -* The ZIP file is *chrome/copay-chrome-extension.zip* diff --git a/browser-extensions/chrome/build.sh b/browser-extensions/chrome/build.sh deleted file mode 100755 index 211de01f9..000000000 --- a/browser-extensions/chrome/build.sh +++ /dev/null @@ -1,64 +0,0 @@ -#! /bin/bash - -# Description: This script compiles and copy the needed files to later package the application for Chrome - -OpenColor="\033[" -Red="1;31m" -Yellow="1;33m" -Green="1;32m" -CloseColor="\033[0m" - -# Check function OK -checkOK() { - if [ $? != 0 ]; then - echo "${OpenColor}${Red}* ERROR. Exiting...${CloseColor}" - exit 1 - fi -} - -# Configs -BUILDDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -APPDIR="$BUILDDIR/copay-chrome-extension" -ZIPFILE="copay-chrome-extension.zip" -VERSION=`cut -d '"' -f2 $BUILDDIR/../../version.js|head -n 1` - -# Move to the build directory -cd $BUILDDIR - -# Create/Clean temp dir -echo "${OpenColor}${Green}* Checking temp dir...${CloseColor}" -if [ -d $APPDIR ]; then - rm -rf $APPDIR -fi - -mkdir -p $APPDIR - -# Re-compile copayBundle.js -echo "${OpenColor}${Green}* Generating copay bundle...${CloseColor}" -grunt prod -checkOK - -# Copy all chrome-extension files -echo "${OpenColor}${Green}* Copying all chrome-extension files...${CloseColor}" -sed "s/APP_VERSION/$VERSION/g" manifest.json > $APPDIR/manifest.json -checkOK - - -INCLUDE=`cat ../include` -cd $BUILDDIR/../.. -LIBS=`cat index.html |grep -o -E 'src="([^"#]+)"' | cut -d'"' -f2|grep lib` -echo "LIBS: $LIBS" - -CMD="rsync -rLRv --exclude-from $BUILDDIR/../exclude $INCLUDE $LIBS $APPDIR" -echo $CMD -$CMD -checkOK - -# Zipping chrome-extension -echo "${OpenColor}${Green}* Zipping all chrome-extension files...${CloseColor}" -cd $BUILDDIR -rm $ZIPFILE -zip -qr $ZIPFILE "`basename $APPDIR`" -checkOK - -echo "${OpenColor}${Yellow}\nThe Chrome Extension is ready at $BUILDDIR/copay-chrome-extension.zip${CloseColor}" diff --git a/browser-extensions/chrome/initial.js b/browser-extensions/chrome/initial.js deleted file mode 100644 index fc12301b5..000000000 --- a/browser-extensions/chrome/initial.js +++ /dev/null @@ -1,8 +0,0 @@ -chrome.app.runtime.onLaunched.addListener(function() { - chrome.app.window.create('index.html', { - 'bounds': { - 'width': 1200, - 'height': 800 - } - }); -}); diff --git a/browser-extensions/chrome/manifest.json b/browser-extensions/chrome/manifest.json deleted file mode 100644 index d5361b40e..000000000 --- a/browser-extensions/chrome/manifest.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "manifest_version": 2, - "name": "Copay", - "description": "A secure Bitcoin wallet for friends and companies", - "version": "APP_VERSION", - "permissions": [ - "storage", - "notifications", - "videoCapture" - ], - "app": { - "background": { - "scripts": ["initial.js"] - } - }, - "icons": { - "128": "img/icons/icon.png" - } -} diff --git a/browser-extensions/chrome/tile.png b/browser-extensions/chrome/tile.png deleted file mode 100644 index d457fa2bf..000000000 Binary files a/browser-extensions/chrome/tile.png and /dev/null differ diff --git a/browser-extensions/exclude b/browser-extensions/exclude deleted file mode 100644 index 3689c0576..000000000 --- a/browser-extensions/exclude +++ /dev/null @@ -1,19 +0,0 @@ -lib/socket.io -lib/*/test -lib/*/demo -lib/sjcl/ -lib/angular/angular.js -lib/moment/lang -lib/moment/min/*lang* -lib/moment/moment.js -lib/angular/angular.min.js.gzip -lib/bitcore/node_modules -lib/bitcore/.git -lib/bitcore/docs -lib/bitcore/lib -lib/bitcore/examples -lib/bitcore/coverage -lib/bitcore/build -.git -tests -.* diff --git a/browser-extensions/include b/browser-extensions/include deleted file mode 100644 index 5a43929ed..000000000 --- a/browser-extensions/include +++ /dev/null @@ -1,16 +0,0 @@ -css -font -img -js -sound -views -config.js -init.js -initial.js -version.js -index.html -popup.html -lib/angular/angular-csp.css -lib/angular/angular.min.js.map -lib/angular-route/angular-route.min.js.map -lib/angular-touch/angular-touch.min.js.map diff --git a/config.js b/config.js deleted file mode 100644 index 27f77de49..000000000 --- a/config.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict'; -var defaultConfig = { - // DEFAULT network (livenet or testnet) - networkName: 'livenet', - logLevel: 'info', - - - // wallet limits - limits: { - totalCopayers: 6, - mPlusN: 100, - }, - - // network layer config - network: { - testnet: { - url: 'https://test-insight.bitpay.com:443', - transports: ['polling'], - }, - livenet: { - url: 'https://insight.bitpay.com:443', - transports: ['polling'], - }, - }, - - // wallet default config - wallet: { - requiredCopayers: 2, - totalCopayers: 3, - spendUnconfirmed: true, - reconnectDelay: 5000, - idleDurationMin: 4, - settings: { - unitName: 'bits', - unitToSatoshi: 100, - unitDecimals: 2, - alternativeName: 'US Dollar', - alternativeIsoCode: 'USD', - } - }, - - // local encryption/security config - passphraseConfig: { - iterations: 5000, - storageSalt: 'mjuBtGybi/4=', - }, - - rates: { - url: 'https://insight.bitpay.com:443/api/rates', - }, - - verbose: 1, - - plugins: { - //LocalStorage: true, - // EncryptedLocalStorage: true, - //GoogleDrive: true, - //InsightStorage: true - EncryptedInsightStorage: true, - }, - - // This can be changed on the UX > Settings > Insight livenet - EncryptedInsightStorage: { - url: 'https://insight.bitpay.com:443/api/email', - //url: 'http://localhost:3001/api/email' - - // This KDF parameters are for the passphrase for Insight authentication - // Are not related to encryption itself. - // - // WARN: Changing this parameters would prevent accesing previously created profiles. - iterations: 1000, - salt: 'jBbYTj8zTrOt6V', - }, - - minPasswordStrength: 4, - - /* - 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') - module.exports = defaultConfig; diff --git a/copay.js b/copay.js deleted file mode 100644 index 462ad98da..000000000 --- a/copay.js +++ /dev/null @@ -1,26 +0,0 @@ -// core -module.exports.PublicKeyRing = require('./js/models/PublicKeyRing'); -module.exports.TxProposal = require('./js/models/TxProposal'); -module.exports.TxProposals = require('./js/models/TxProposals'); -module.exports.PrivateKey = require('./js/models/PrivateKey'); -module.exports.HDPath = require('./js/models/HDPath'); -module.exports.HDParams = require('./js/models/HDParams'); -module.exports.Async = require('./js/models/Async'); -module.exports.Insight = require('./js/models/Insight'); -module.exports.RateService = require('./js/models/RateService'); -module.exports.Identity = require('./js/models/Identity'); -module.exports.Wallet = require('./js/models/Wallet'); -module.exports.Compatibility = require('./js/models/Compatibility'); -module.exports.PluginManager = require('./js/models/PluginManager'); - - -module.exports.crypto = require('./js/util/crypto'); -module.exports.logger = require('./js/util/log'); -module.exports.csv = require('./js/util/csv'); - -module.exports.version = require('./version').version; -module.exports.commitHash = require('./version').commitHash; - - -module.exports.defaultConfig = require('./config'); - diff --git a/cordova/android/AndroidManifest.xml b/cordova/android/AndroidManifest.xml index c02f3e726..fb5de590d 100644 --- a/cordova/android/AndroidManifest.xml +++ b/cordova/android/AndroidManifest.xml @@ -1,5 +1,5 @@ - + @@ -9,7 +9,7 @@ - + diff --git a/cordova/build.sh b/cordova/build.sh index cc4b684ed..6ae36addc 100755 --- a/cordova/build.sh +++ b/cordova/build.sh @@ -15,7 +15,6 @@ checkOK() { # Configs BUILDDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT="$BUILDDIR/project" -VERSION=`cut -d '"' -f2 $BUILDDIR/../version.js` CURRENT_OS=$1 @@ -115,27 +114,39 @@ if [ ! -d $PROJECT ]; then cordova plugin add hu.dpal.phonegap.plugins.spinnerdialog checkOK + cordova plugin add org.apache.cordova.dialogs + checkOK + + cordova plugin add org.apache.cordova.network-information + checkOK + + cordova plugin add org.apache.cordova.console + checkok + + cordova plugin add hu.dpal.phonegap.plugins.uniquedeviceid + checkok + fi if $DBGJS then echo "${OpenColor}${Green}* Generating copay bundle (debug js)...${CloseColor}" cd $BUILDDIR/.. - grunt dist-mobile-dbg + grunt checkOK else echo "${OpenColor}${Green}* Generating copay bundle...${CloseColor}" cd $BUILDDIR/.. - grunt dist-mobile + grunt prod checkOK fi echo "${OpenColor}${Green}* Copying files...${CloseColor}" cd $BUILDDIR/.. -cp -af dist/web/** $PROJECT/www +cp -af public/** $PROJECT/www checkOK -sed "s/<\!-- PLACEHOLDER: CORDOVA SRIPT -->/ - - - - - - - - - - - - diff --git a/init.js b/init.js deleted file mode 100644 index c56005eb9..000000000 --- a/init.js +++ /dev/null @@ -1,29 +0,0 @@ -var isChromeApp = window.chrome && chrome.runtime && chrome.runtime.id; -var ld; -if (!isChromeApp) { - ld = (document.all); -} - -var ns4 = document.layers; -var ns6 = !isChromeApp && document.getElementById && !document.all; -var ie4 = !isChromeApp && document.all; -if (ns4) { - ld = document.loading; -} else if (ns6) { - ld = document.getElementById("loading").style; -} else if (ie4) { - ld = document.all.loading.style; -} - -function init() { - if (ns4) { - ld.visibility = "hidden"; - } else if (ns6 || ie4) { - ld.display = "none"; - } else { - ld = document.getElementById("loading").style; - ld.visibility = "hidden"; - ld.display = "none"; - } -} -init(); diff --git a/initial.js b/initial.js deleted file mode 100644 index fc12301b5..000000000 --- a/initial.js +++ /dev/null @@ -1,8 +0,0 @@ -chrome.app.runtime.onLaunched.addListener(function() { - chrome.app.window.create('index.html', { - 'bounds': { - 'width': 1200, - 'height': 800 - } - }); -}); diff --git a/js/app.js b/js/app.js deleted file mode 100644 index 0f8d9bbec..000000000 --- a/js/app.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -var copay = require('copay'); -var _ = require('lodash'); -var LS = require('../js/plugins/LocalStorage'); -var ls = new LS(); - - -// TODO move this to configService ! -var config = copay.defaultConfig; - -ls.getItem('config', function(err, data) { - var localConfig; - try { - localConfig = JSON.parse(data); - } catch(e) {}; - if (localConfig) { - var cmv = copay.version.split('.')[1]; - var lmv = localConfig.version ? localConfig.version.split('.')[1] : '-1'; - if (cmv === lmv) { - _.each(localConfig, function(value, key) { - config[key] = value; - }); - } - } -}); - -var modules = [ - 'ngRoute', - 'angularMoment', - 'mm.foundation', - 'monospaced.qrcode', - 'ngIdle', - 'gettext', - 'ui.gravatar', - 'ngTouch', - 'copayApp.filters', - 'copayApp.services', - 'copayApp.controllers', - 'copayApp.directives', -]; - -var copayApp = window.copayApp = angular.module('copayApp', modules); - -var defaults = JSON.parse(JSON.stringify(copay.defaultConfig)); -copayApp.value('defaults', defaults); - -copayApp.config(function($sceDelegateProvider) { - $sceDelegateProvider.resourceUrlWhitelist([ - 'self', - 'mailto:**' - ]); -}); - -angular.module('ui.gravatar').config([ - 'gravatarServiceProvider', - function(gravatarServiceProvider) { - gravatarServiceProvider.defaults = { - size: 35 - }; - // Use https endpoint - gravatarServiceProvider.secure = true; - } -]); - -angular.module('copayApp.filters', []); -angular.module('copayApp.services', []); -angular.module('copayApp.controllers', []); -angular.module('copayApp.directives', []); diff --git a/js/controllers/copayers.js b/js/controllers/copayers.js deleted file mode 100644 index 877efd6f0..000000000 --- a/js/controllers/copayers.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('CopayersController', - function($scope, $rootScope, $timeout, go, identityService, notification, isCordova) { - var w = $rootScope.wallet; - - - $scope.init = function() { - $rootScope.title = 'Share this secret with your copayers'; - $scope.loading = false; - $scope.secret = $rootScope.wallet.getSecret(); - $scope.isCordova = isCordova; - - w.on('publicKeyRingUpdated', $scope.updateList); - w.on('ready', $scope.updateList); - - $scope.updateList(); - }; - - $scope.updateList = function() { - var w = $rootScope.wallet; - - $scope.copayers = $rootScope.wallet.getRegisteredPeerIds(); - if (w.isComplete()) { - - w.removeListener('publicKeyRingUpdated', $scope.updateList); - w.removeListener('ready', $scope.updateList); - go.walletHome(); - } - $timeout(function() { - $rootScope.$digest(); - }, 1); - }; - - $scope.deleteWallet = function() { - $rootScope.starting = true; - $timeout(function() { - identityService.deleteWallet(w, function(err) { - $rootScope.starting = false; - if (err) { - $scope.error = err.message || err; - copay.logger.warn(err); - $timeout(function () { $scope.$digest(); }); - } else { - if ($rootScope.wallet) { - go.walletHome(); - } - $timeout(function() { - notification.success('Success', 'The wallet "' + (w.name || w.id) + '" was deleted'); - }); - } - }); - }, 100); - }; - - $scope.copySecret = function(secret) { - if (isCordova) { - window.cordova.plugins.clipboard.copy(secret); - window.plugins.toast.showShortCenter('Copied to clipboard'); - } - }; - - $scope.shareSecret = function(secret) { - if (isCordova) { - if (isMobile.Android() || isMobile.Windows()) { - window.ignoreMobilePause = true; - } - window.plugins.socialsharing.share(secret, null, null, null); - } - }; - - }); diff --git a/js/controllers/create.js b/js/controllers/create.js deleted file mode 100644 index 3e65e7d34..000000000 --- a/js/controllers/create.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('CreateController', - function($scope, $rootScope, $location, $timeout, identityService, backupService, notification, defaults, isMobile, isCordova) { - - $rootScope.fromSetup = true; - $scope.loading = false; - $scope.walletPassword = $rootScope.walletPassword; - $scope.isMobile = isMobile.any(); - $scope.hideAdv = true; - $scope.networkName = config.networkName; - $rootScope.title = 'Create new wallet'; - $rootScope.hideWalletNavigation = true; - $scope.isWindowsPhoneApp = isMobile.Windows() && isCordova; - - // ng-repeat defined number of times instead of repeating over array? - $scope.getNumber = function(num) { - return new Array(num); - } - - $scope.totalCopayers = config.wallet.totalCopayers; - $scope.TCValues = _.range(1, config.limits.totalCopayers + 1); - - var updateRCSelect = function(n) { - var maxReq = copay.Wallet.getMaxRequiredCopayers(n); - $scope.RCValues = _.range(1, maxReq + 1); - $scope.requiredCopayers = Math.min(parseInt(n / 2 + 1), maxReq); - }; - - updateRCSelect($scope.totalCopayers); - - $scope.$watch('totalCopayers', function(tc) { - updateRCSelect(tc); - }); - - $scope.$watch('networkName', function(tc) { - $scope.networkUrl = config.network[$scope.networkName].url; - }); - - $scope.showNetwork = function() { - return $scope.networkUrl != defaults.network.livenet.url && $scope.networkUrl != defaults.network.testnet.url; - }; - - - $scope.create = function(form) { - if (form && form.$invalid) { - $scope.error = 'Please enter the required fields'; - return; - } - var opts = { - requiredCopayers: $scope.requiredCopayers, - totalCopayers: $scope.totalCopayers, - name: $scope.walletName, - privateKeyHex: $scope.private, - networkName: $scope.networkName, - }; - $rootScope.starting = true; - identityService.createWallet(opts, function(err, wallet){ - $rootScope.starting = false; - if (err || !wallet) { - copay.logger.debug(err); - if (err.match('OVERQUOTA')){ - $scope.error = 'Could not create wallet: storage limits on remove server exceeded'; - } else { - $scope.error = 'Could not create wallet: ' + err; - } - } - - $timeout(function(){ - $rootScope.$digest(); - },1); - }); - }; - - $scope.$on("$destroy", function () { - $rootScope.hideWalletNavigation = false; - }); - }); diff --git a/js/controllers/createProfile.js b/js/controllers/createProfile.js deleted file mode 100644 index 1c54a2d08..000000000 --- a/js/controllers/createProfile.js +++ /dev/null @@ -1,202 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('CreateProfileController', function($scope, $rootScope, $location, $timeout, $window, notification, pluginManager, identityService, pinService, isMobile, isCordova, configService, go) { - - var _credentials; - - $scope.init = function() { - - if ($rootScope.wallet) - go.walletHome(); - - $scope.isMobile = isMobile.any(); - $scope.isWindowsPhoneApp = isMobile.Windows() && isCordova; - $scope.hideForWP = 0; - $scope.digits = []; - $scope.defined = []; - $scope.askForPin = 0; - - $scope.createStep = 'storage'; - $scope.useLocalstorage = false; - $scope.minPasswordStrength = _.isUndefined(config.minPasswordStrength) ? - 4 : config.minPasswordStrength; - - }; - - $scope.clear = function() { - pinService.clearPin($scope); - }; - - $scope.press = function(digit) { - pinService.pressPin($scope, digit, true); - }; - - $scope.skip = function () { - pinService.skipPin($scope, true); - }; - - $scope.formFocus = function() { - if (!$scope.isWindowsPhoneApp) return - $scope.hideForWP = true; - $timeout(function() { - $scope.$digest(); - }, 1); - }; - - $scope.createPin = function(pin) { - preconditions.checkArgument(pin); - preconditions.checkState($rootScope.iden); - preconditions.checkState(_credentials && _credentials.email); - $rootScope.starting = true; - - $timeout(function() { - pinService.save(pin, _credentials.email, _credentials.password, function(err) { - _credentials.password = ''; - _credentials = null; - $scope.askForPin = 0; - $rootScope.hasPin = true; - $scope.createDefaultWallet(); - }); - }, 100); - }; - - - $scope.setStep = function(step) { - $scope.error = null; - $scope.createStep = step; - $scope.hideForWP = false; - $timeout(function() { - $scope.$digest(); - }, 1); - }; - - $scope.selectStorage = function(storage) { - $scope.useLocalstorage = storage == 'local'; - $scope.hideForWP = false; - $timeout(function() { - $scope.$digest(); - }, 1); - }; - - $scope.goToEmail = function() { - $scope.createStep = 'email'; - $scope.useEmail = !$scope.useLocalstorage; - }; - - $scope.setEmailOrUsername = function(form) { - $scope.userOrEmail = $scope.useLocalstorage ? form.username.$modelValue : form.email.$modelValue; - preconditions.checkState($scope.userOrEmail); - - $scope.error = null; - $scope.hideForWP = false; - $scope.createStep = 'pass'; - $timeout(function() { - $scope.$digest(); - }, 1); - }; - - /* Last step. Will emit after creation so the UX gets updated */ - $scope.createDefaultWallet = function() { - $rootScope.hideNavigation = false; - $rootScope.starting = true; - identityService.createDefaultWallet(function(err) { - $scope.askForPin = 0; - $rootScope.starting = null; - - if (err) { - var msg = err.toString(); - $scope.error = msg; - } else { - if (!$scope.useLocalstorage) { - $rootScope.pleaseConfirmEmail = true; - } - } - - }); - }; - - $scope._doCreateProfile = function(emailOrUsername, password, cb) { - preconditions.checkArgument(_.isString(emailOrUsername)); - preconditions.checkArgument(_.isString(password)); - - $rootScope.hideNavigation = false; - - identityService.create(emailOrUsername, password, function(err) { - if (err) { - var msg = err.toString(); - $scope.createStep = 'email'; - if (msg.indexOf('EEXIST') >= 0 || msg.indexOf('BADC') >= 0) { - msg = 'This profile already exists' - } - if (msg.indexOf('EMAILERROR') >= 0) { - msg = 'Could not send verification email. Please check your email address.'; - } - return cb(msg); - } else { - // mobile - if ($scope.isMobile) { - $rootScope.starting = null; - _credentials = { - email: emailOrUsername, - password: password, - }; - $scope.askForPin = 1; - $scope.hideForWP = 0; - - $rootScope.hideNavigation = true; - $timeout(function() { - $rootScope.$digest(); - }, 1); - return; - } else { - $scope.createDefaultWallet(); - } - } - return cb(); - }); - }; - - - $scope.saveSettings = function(cb) { - var plugins = config.plugins; - - plugins.EncryptedLocalStorage = false; - plugins.EncryptedInsightStorage = false; - - var pluginName = $scope.useLocalstorage ? 'EncryptedLocalStorage' : 'EncryptedInsightStorage'; - plugins[pluginName] = true; - - configService.set({ - plugins: plugins - }, cb); - }; - - - $scope.createProfile = function(form) { - if (form && form.$invalid) { - $scope.error = 'Please enter the required fields'; - return; - } - - $scope.saveSettings(function(err) { - preconditions.checkState(!err, err); - $rootScope.starting = true; - - $scope._doCreateProfile($scope.userOrEmail, form.password.$modelValue, function(err) { - if (err) { - $scope.error = err; - $scope.passwordStrength = null; - $rootScope.starting = false; - } - form.password.$setViewValue(''); - form.password.$render(); - form.repeatpassword.$setViewValue(''); - form.repeatpassword.$render(); - form.$setPristine(); - $timeout(function() { - $rootScope.$digest(); - }, 1); - }); - }); - }; -}); diff --git a/js/controllers/head.js b/js/controllers/head.js deleted file mode 100644 index 6ca050314..000000000 --- a/js/controllers/head.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('HeadController', function($scope, $rootScope, $filter, $timeout, notification, identityService, balanceService) { - $scope.username = $rootScope.iden ? $rootScope.iden.getName() : ''; - $scope.hoverMenu = false; - - var isChromeApp = typeof window !== "undefined" && window.chrome && chrome.runtime && chrome.runtime.id; - - $scope.hoverIn = function() { - this.hoverMenu = true; - }; - - $scope.hoverOut = function() { - this.hoverMenu = false; - }; - - $scope.signout = function() { - identityService.signout(); - }; - - $scope.refresh = function() { - var w = $rootScope.wallet; - if (!w) return; - - if (w.isComplete()) { - w.sendWalletReady(); - balanceService.clearBalanceCache(w); - balanceService.update(w, function() { - $rootScope.$digest(); - }, true); - } - }; - - - //Ensures a graceful disconnect - window.onbeforeunload = function() { - $scope.signout(); - }; - - $scope.$on('$destroy', function() { - if (isChromeApp) return; - window.onbeforeunload = undefined; - }); - - $scope.init = function() { - if (!$rootScope.wallet) return; - - $scope.$on('IdleStart', function() {}); - $scope.$on('IdleWarn', function(a, countdown) { - $rootScope.countdown = countdown; - $rootScope.sessionExpired = true; - $rootScope.$apply(); - }); - $scope.$on('IdleEnd', function() { - $timeout(function() { - $rootScope.sessionExpired = null; - }, 500); - }); - $scope.$on('IdleTimeout', function() { - $rootScope.sessionExpired = null; - $scope.signout(); - notification.warning('Session closed', 'Session closed because a long time of inactivity'); - }); - $scope.$on('Keepalive', function() { - if ($rootScope.wallet) { - $rootScope.wallet.keepAlive(); - } - }); - $rootScope.$watch('title', function(newTitle, oldTitle) { - $scope.title = newTitle; - }); - }; -}); diff --git a/js/controllers/history.js b/js/controllers/history.js deleted file mode 100644 index f6effad96..000000000 --- a/js/controllers/history.js +++ /dev/null @@ -1,204 +0,0 @@ -'use strict'; -var bitcore = require('bitcore'); - -angular.module('copayApp.controllers').controller('HistoryController', - function($scope, $rootScope, $filter, $timeout, $modal, rateService, notification, go) { - var w = $rootScope.wallet; - - $rootScope.title = 'History'; - $scope.loading = false; - $scope.generating = false; - $scope.lastShowed = false; - - $scope.currentPage = 1; - $scope.itemsPerPage = 10; - $scope.nbPages = 0; - $scope.totalItems = 0; - $scope.blockchain_txs = []; - $scope.alternativeCurrency = []; - - $scope.selectPage = function(page) { - $scope.paging = true; - $scope.currentPage = page; - $scope.update(); - }; - - $scope.downloadHistory = function() { - var w = $rootScope.wallet; - if (!w) return; - - var filename = "copay_history.csv"; - var descriptor = { - columns: [{ - label: 'Date', - property: 'ts', - type: 'date' - }, { - label: 'Amount (' + w.settings.unitName + ')', - property: 'amount', - type: 'number' - }, { - label: 'Amount (' + w.settings.alternativeIsoCode + ')', - property: 'alternativeAmount' - }, { - label: 'Action', - property: 'action' - }, { - label: 'AddressTo', - property: 'addressTo' - }, { - label: 'Comment', - property: 'comment' - }, ], - }; - if (w.isShared()) { - descriptor.columns.push({ - label: 'Signers', - property: function(obj) { - if (!obj.actionList) return ''; - return _.map(obj.actionList, function(action) { - return w.publicKeyRing.nicknameForCopayer(action.cId); - }).join('|'); - } - }); - } - - $scope.generating = true; - - $scope._getTransactions(w, null, function(err, res) { - if (err) { - $scope.generating = false; - logger.error(err); - notification.error('Could not get transaction history'); - return; - } - $scope._addRates(w, res.items, function(err) { - copay.csv.toCsv(res.items, descriptor, function(err, res) { - if (err) { - $scope.generating = false; - logger.error(err); - notification.error('Could not generate csv file'); - return; - } - var csvContent = "data:text/csv;charset=utf-8," + res; - var encodedUri = encodeURI(csvContent); - var link = document.createElement("a"); - link.setAttribute("href", encodedUri); - link.setAttribute("download", filename); - link.click(); - $scope.generating = false; - $scope.$digest(); - }); - }); - }); - }; - - - $scope.update = function() { - $scope.getTransactions(); - }; - - $scope.show = function() { - $scope.loading = true; - setTimeout(function() { - $scope.update(); - }, 1); - }; - - $scope._getTransactions = function(w, opts, cb) { - w.getTransactionHistory(opts, function(err, res) { - if (err) return cb(err); - if (!res) return cb(); - - var now = new Date(); - var items = res.items; - _.each(items, function(tx) { - tx.ts = tx.minedTs || tx.sentTs; - tx.rateTs = Math.floor((tx.ts || now) / 1000); - tx.amount = $filter('noFractionNumber')(tx.amount); - }); - return cb(null, res); - }); - }; - - $scope._addRates = function(w, txs, cb) { - if (!txs || txs.length == 0) return cb(); - var index = _.groupBy(txs, 'rateTs'); - rateService.getHistoricRates(w.settings.alternativeIsoCode, _.keys(index), function(err, res) { - if (err || !res) return cb(err); - _.each(res, function(r) { - _.each(index[r.ts], function (tx) { - var alternativeAmount = (r.rate != null ? tx.amountSat * rateService.SAT_TO_BTC * r.rate : null); - tx.alternativeAmount = alternativeAmount ? $filter('noFractionNumber')(alternativeAmount, 2) : null; - }); - }); - return cb(); - }); - }; - - - $scope.openTxModal = function(btx) { - var ModalInstanceCtrl = function($scope, $modalInstance) { - $scope.btx = btx; - - $scope.getShortNetworkName = function() { - var w = $rootScope.wallet; - return w.getNetworkName().substring(0, 4); - }; - - $scope.cancel = function() { - $modalInstance.dismiss('cancel'); - }; - }; - - $modal.open({ - templateUrl: 'views/modals/tx-details.html', - windowClass: 'medium', - controller: ModalInstanceCtrl, - }); - }; - - $scope.getTransactions = function() { - var w = $rootScope.wallet; - if (!w) return; - - $scope.blockchain_txs = w.cached_txs || []; - $scope.loading = true; - - $scope._getTransactions(w, { - currentPage: $scope.currentPage, - itemsPerPage: $scope.itemsPerPage, - }, function(err, res) { - if (err) throw err; - - if (!res) { - $scope.loading = false; - $scope.lastShowed = false; - return; - } - - var items = res.items; - $scope._addRates(w, items, function(err) { - $timeout(function() { - $scope.$digest(); - }, 1); - }) - - $scope.blockchain_txs = w.cached_txs = items; - $scope.nbPages = res.nbPages; - $scope.totalItems = res.nbItems; - - $scope.loading = false; - $scope.paging = false; - setTimeout(function() { - $scope.$digest(); - }, 1); - }); - }; - - - $scope.hasAction = function(actions, action) { - return actions.hasOwnProperty('create'); - }; - - }); diff --git a/js/controllers/home.js b/js/controllers/home.js deleted file mode 100644 index 5dd6f1b2a..000000000 --- a/js/controllers/home.js +++ /dev/null @@ -1,216 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('HomeController', function($scope, $rootScope, $timeout, $window, go, notification, identityService, Compatibility, pinService, applicationService, isMobile, isCordova, localstorageService) { - - var KEY = 'CopayDisclaimer'; - var ls = localstorageService; - var _credentials; - - $scope.init = function() { - $scope.isMobile = isMobile.any(); - $scope.isWindowsPhoneApp = isMobile.Windows() && isCordova; - $scope.hideForWP = 0; - $scope.attempt = 0; - $scope.digits = []; - $scope.defined = []; - $scope.askForPin = 0; - - // This is only for backwards compat, insight api should link to #!/confirmed directly - if (getParam('confirmed')) { - var hashIndex = window.location.href.indexOf('/?'); - window.location = window.location.href.substr(0, hashIndex) + '#!/confirmed'; - return; - } - - if ($rootScope.fromEmailConfirmation) { - $scope.confirmedEmail = true; - $rootScope.fromEmailConfirmation = false; - } - - if ($rootScope.wallet) { - go.walletHome(); - } - - Compatibility.check($scope); - pinService.check(function(err, value) { - $rootScope.hasPin = value; - }); - $scope.usingLocalStorage = config.plugins.EncryptedLocalStorage; - - if (isCordova) { - ls.getItem(KEY, function(err, value) { - $scope.showDisclaimer = value ? null : true; - }); - } - }; - - $scope.clear = function() { - pinService.clearPin($scope); - }; - - $scope.press = function(digit) { - pinService.pressPin($scope, digit); - }; - - $scope.skip = function () { - pinService.skipPin($scope); - }; - - $scope.agreeDisclaimer = function() { - ls.setItem(KEY, true, function(err) { - $scope.showDisclaimer = null; - }); - }; - - $scope.formFocus = function() { - if ($scope.isWindowsPhoneApp) { - $scope.hideForWP = true; - $timeout(function() { - $scope.$digest(); - }, 1); - } - }; - - $scope.openWithPin = function(pin) { - - if (!pin) { - $scope.error = 'Please enter the required fields'; - return; - } - $rootScope.starting = true; - - $timeout(function() { - var credentials = pinService.get(pin, function(err, credentials) { - if (err || !credentials) { - $rootScope.starting = null; - $scope.error = 'Wrong PIN'; - $scope.clear(); - $timeout(function() { - $scope.error = null; - }, 2000); - return; - } - $scope.open(credentials.email, credentials.password); - }); - }, 100); - }; - - $scope.openWallets = function() { - preconditions.checkState($rootScope.iden); - var iden = $rootScope.iden; - $rootScope.hideNavigation = false; - $rootScope.starting = true; - iden.openWallets(); - }; - - $scope.createPin = function(pin) { - preconditions.checkArgument(pin); - preconditions.checkState($rootScope.iden); - preconditions.checkState(_credentials && _credentials.email); - $rootScope.starting = true; - - $timeout(function() { - pinService.save(pin, _credentials.email, _credentials.password, function(err) { - _credentials.password = ''; - _credentials = null; - $scope.askForPin = 0; - $rootScope.hasPin = true; - $rootScope.starting = null; - $scope.openWallets(); - }); - }, 100); - }; - - $scope.openWithCredentials = function(form) { - if (form && form.$invalid) { - $scope.error = 'Please enter the required fields'; - return; - } - - $timeout(function() { - $scope.open(form.email.$modelValue, form.password.$modelValue); - }, 100); - }; - - - $scope.pinLogout = function() { - pinService.clear(function() { - copay.logger.debug('PIN erased'); - delete $rootScope['hasPin']; - applicationService.restart(); - }); - }; - - $scope.open = function(email, password) { - $rootScope.starting = true; - identityService.open(email, password, function(err, iden) { - if (err) { - $rootScope.starting = false; - copay.logger.warn(err); - - var identifier = $scope.usingLocalStorage ? 'username' : 'email'; - if ((err.toString() || '').match('PNOTFOUND')) { - $scope.error = 'Invalid ' + identifier + ' or password'; - - if ($scope.attempt++ > 1) { - var storage = $scope.usingLocalStorage ? 'this device storage' : 'cloud storage'; - $scope.error = 'Invalid ' + identifier + ' or password. You are trying to sign in using ' + storage + '. Change it on settings if necessary.'; - }; - - $rootScope.hasPin = false; - pinService.clear(function() {}); - - } else if ((err.toString() || '').match('Connection')) { - $scope.error = 'Could not connect to Insight Server'; - } else if ((err.toString() || '').match('Unable')) { - $scope.error = 'Unable to read data from the Insight Server'; - } else { - $scope.error = 'Unknown error'; - } - $timeout(function() { - $rootScope.$digest(); - }, 1) - return; - } - - // Open successfully? - if (iden) { - $scope.error = null; - $scope.confirmedEmail = false; - - // mobile - if ($scope.isMobile && !$rootScope.hasPin) { - _credentials = { - email: email, - password: password, - }; - $scope.askForPin = 1; - $rootScope.starting = false; - $rootScope.hideNavigation = true; - $timeout(function() { - $rootScope.$digest(); - }, 1); - return; - } - // no mobile - else { - $scope.openWallets(); - } - } - }); - }; - - function getParam(sname) { - var params = location.search.substr(location.search.indexOf("?") + 1); - var sval = ""; - params = params.split("&"); - // split param and value into individual pieces - for (var i = 0; i < params.length; i++) { - var temp = params[i].split("="); - if ([temp[0]] == sname) { - sval = temp[1]; - } - } - return sval; - } -}); diff --git a/js/controllers/homeWallet.js b/js/controllers/homeWallet.js deleted file mode 100644 index 9b5df970f..000000000 --- a/js/controllers/homeWallet.js +++ /dev/null @@ -1,103 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('HomeWalletController', function($scope, $rootScope, $timeout, $filter, $modal, rateService, notification, txStatus, identityService, isCordova) { - - $scope.openTxModal = function(tx) { - var ModalInstanceCtrl = function($scope, $modalInstance) { - var w = $rootScope.wallet; - $scope.error = null; - $scope.tx = tx; - $scope.registeredCopayerIds = w.getRegisteredCopayerIds(); - $scope.loading = null; - - $scope.getShortNetworkName = function() { - var w = $rootScope.wallet; - return w.getNetworkName().substring(0, 4); - }; - - $scope.sign = function(ntxid) { - if (isCordova) { - window.plugins.spinnerDialog.show(null, 'Signing transaction...', true); - } - $scope.loading = true; - $scope.error = null; - $timeout(function() { - w.signAndSend(ntxid, function(err, id, status) { - if (isCordova) { - window.plugins.spinnerDialog.hide(); - } - $scope.loading = false; - if (err) { - $scope.error = 'Transaction could not send. Please try again.'; - $scope.$digest(); - } - else { - $modalInstance.close(status); - } - }); - }, 100); - }; - - $scope.reject = function(ntxid) { - if (isCordova) { - window.plugins.spinnerDialog.show(null, 'Rejecting transaction...', true); - } - $scope.loading = true; - $scope.error = null; - $timeout(function() { - w.reject(ntxid, function(err, status) { - if (isCordova) { - window.plugins.spinnerDialog.hide(); - } - $scope.loading = false; - if (err) { - $scope.error = err; - $scope.$digest(); - } - else { - $modalInstance.close(status); - } - }); - }, 100); - }; - - $scope.broadcast = function(ntxid) { - if (isCordova) { - window.plugins.spinnerDialog.show(null, 'Sending transaction...', true); - } - $scope.loading = true; - $scope.error = null; - $timeout(function() { - w.issueTx(ntxid, function(err, txid, status) { - if (isCordova) { - window.plugins.spinnerDialog.hide(); - } - $scope.loading = false; - if (err) { - $scope.error = 'Transaction could not send. Please try again.'; - $scope.$digest(); - } - else { - $modalInstance.close(status); - } - }); - }, 100); - }; - - $scope.cancel = function() { - $modalInstance.dismiss('cancel'); - }; - }; - - var modalInstance = $modal.open({ - templateUrl: 'views/modals/txp-details.html', - windowClass: 'medium', - controller: ModalInstanceCtrl, - }); - - modalInstance.result.then(function(status) { - txStatus.notify(status); - }); - - }; -}); diff --git a/js/controllers/import.js b/js/controllers/import.js deleted file mode 100644 index 933d7f16c..000000000 --- a/js/controllers/import.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('ImportController', - function($scope, $rootScope, $location, $timeout, identityService, notification, isMobile, isCordova, Compatibility) { - - $rootScope.title = 'Import wallet'; - $scope.importStatus = 'Importing wallet - Reading backup...'; - $scope.hideAdv = true; - $scope.isSafari = isMobile.Safari(); - $scope.isCordova = isCordova; - $scope.importOpts = {}; - $rootScope.hideWalletNavigation = true; - - - window.ignoreMobilePause = true; - $scope.$on('$destroy', function() { - $timeout(function(){ - window.ignoreMobilePause = false; - }, 100); - }); - - Compatibility.check($scope); - - var reader = new FileReader(); - - var updateStatus = function(status) { - $scope.importStatus = status; - } - - - $scope.getFile = function() { - // If we use onloadend, we need to check the readyState. - reader.onloadend = function(evt) { - if (evt.target.readyState == FileReader.DONE) { // DONE == 2 - var encryptedObj = evt.target.result; - updateStatus('Importing wallet - Procesing backup...'); - identityService.importWallet(encryptedObj, $scope.password, {}, function(err) { - if (err) { - $rootScope.starting = false; - $scope.error = 'Could not read wallet. Please check your password'; - $timeout(function() { - $rootScope.$digest(); - }, 1); - } - }); - } - } - }; - - $scope.import = function(form) { - - if (form.$invalid) { - $scope.error = 'There is an error in the form'; - return; - } - - var backupFile = $scope.file; - var backupText = form.backupText.$modelValue; - var backupOldWallet = form.backupOldWallet.$modelValue; - var password = form.password.$modelValue; - - if (backupOldWallet) { - backupText = backupOldWallet.value; - } - - if (!backupFile && !backupText) { - $scope.error = 'Please, select your backup file'; - return; - } - - $rootScope.starting = true; - - $timeout(function() { - - $scope.importOpts = {}; - - var skipFields = []; - - if ($scope.skipPublicKeyRing) - skipFields.push('publicKeyRing'); - - if ($scope.skipTxProposals) - skipFields.push('txProposals'); - - if (skipFields) - $scope.importOpts.skipFields = skipFields; - - if (backupFile) { - reader.readAsBinaryString(backupFile); - } else { - updateStatus('Importing wallet - Procesing backup...'); - identityService.importWallet(backupText, $scope.password, $scope.importOpts, function(err) { - if (err) { - $rootScope.starting = false; - $scope.error = 'Could not read wallet. Please check your password'; - $timeout(function() { - $rootScope.$digest(); - }, 1); - } - }); - } - }, 100); - }; - - - $scope.$on("$destroy", function () { - $rootScope.hideWalletNavigation = false; - }); - - }); diff --git a/js/controllers/index.js b/js/controllers/index.js deleted file mode 100644 index 3f4e85eac..000000000 --- a/js/controllers/index.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('IndexController', function($scope, $timeout, go, isCordova, identityService, notification) { - $scope.init = function() { - - }; - - $scope.resendVerificationEmail = function() { - $scope.loading = true; - identityService.resendVerificationEmail(function(err) { - if (err) { - notification.error('Could not send email', 'There was a problem sending the verification email.'); - } - else { - notification.success('Email sent', 'Check your inbox and confirms the email'); - $scope.hideReSendButton = true; - } - $scope.loading = null; - $timeout(function() { - $scope.$digest(); - }, 1); - return; - }); - }; - - $scope.openMenu = function() { - go.swipe(true); - }; - - $scope.closeMenu = function() { - go.swipe(); - }; - -}); diff --git a/js/controllers/join.js b/js/controllers/join.js deleted file mode 100644 index 3234e1bae..000000000 --- a/js/controllers/join.js +++ /dev/null @@ -1,160 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('JoinController', - function($scope, $rootScope, $timeout, isMobile, notification, identityService) { - $rootScope.fromSetup = false; - $scope.loading = false; - $scope.isMobile = isMobile.any(); - $rootScope.title = 'Join shared wallet'; - $rootScope.hideWalletNavigation = true; - - - // QR code Scanner - var cameraInput; - var video; - var canvas; - var $video; - var context; - var localMediaStream; - - $scope.hideAdv = true; - - - navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; - - if (!window.cordova && !navigator.getUserMedia) - $scope.disableScanner = 1; - - var _scan = function(evt) { - if (localMediaStream) { - context.drawImage(video, 0, 0, 300, 225); - - try { - qrcode.decode(); - } catch (e) { - //qrcodeError(e); - } - } - - $timeout(_scan, 500); - }; - - var _successCallback = function(stream) { - video.src = (window.URL && window.URL.createObjectURL(stream)) || stream; - localMediaStream = stream; - video.play(); - $timeout(_scan, 1000); - }; - - var _scanStop = function() { - $scope.showScanner = false; - if (!$scope.isMobile) { - if (localMediaStream && localMediaStream.stop) localMediaStream.stop(); - localMediaStream = null; - video.src = ''; - } - }; - - var _videoError = function(err) { - _scanStop(); - }; - - qrcode.callback = function(data) { - _scanStop(); - - $scope.$apply(function() { - $scope.connectionId = data; - $scope.joinForm.connectionId.$setViewValue(data); - $scope.joinForm.connectionId.$render(); - }); - }; - - $scope.cancelScanner = function() { - _scanStop(); - }; - - $scope.openScanner = function() { - if (window.cordova) return $scope.scannerIntent(); - - $scope.showScanner = true; - - // Wait a moment until the canvas shows - $timeout(function() { - canvas = document.getElementById('qr-canvas'); - context = canvas.getContext('2d'); - - if ($scope.isMobile) { - cameraInput = document.getElementById('qrcode-camera'); - cameraInput.addEventListener('change', _scan, false); - } else { - video = document.getElementById('qrcode-scanner-video'); - $video = angular.element(video); - canvas.width = 300; - canvas.height = 225; - context.clearRect(0, 0, 300, 225); - - navigator.getUserMedia({ - video: true - }, _successCallback, _videoError); - } - }, 500); - }; - - $scope.scannerIntent = function() { - window.ignoreMobilePause = true; - cordova.plugins.barcodeScanner.scan( - function onSuccess(result) { - $timeout(function(){ - window.ignoreMobilePause = false; - }, 100); - if (result.cancelled) return; - - $scope.connectionId = result.text; - $rootScope.$digest(); - }, - function onError(error) { - $timeout(function(){ - window.ignoreMobilePause = false; - }, 100); - alert('Scanning error'); - }); - } - - - $scope.join = function(form) { - if (form && form.$invalid) { - notification.error('Error', 'Please enter the required fields'); - return; - } - - $rootScope.starting = true; - identityService.joinWallet({ - secret: $scope.connectionId, - nickname: $scope.nickname, - privateHex: $scope.private, - }, function(err) { - $rootScope.starting = false; - if (err) { - if (err === 'joinError') - notification.error('Fatal error connecting to Insight server'); - else if (err === 'walletFull') - notification.error('The wallet is full'); - else if (err === 'walletAlreadyExists') - notification.error('Wallet already exists', 'Cannot join again from the same profile'); - else if (err === 'badNetwork') - notification.error('Network Error', 'Wallet network configuration missmatch'); - else if (err === 'badSecret') - notification.error('Bad secret', 'The secret string you entered is invalid'); - else { - notification.error('Error', err.message || err); - } - } - $timeout(function () { $scope.$digest(); }, 1); - }); - } - - - $scope.$on("$destroy", function () { - $rootScope.hideWalletNavigation = false; - }); - }); diff --git a/js/controllers/more.js b/js/controllers/more.js deleted file mode 100644 index c7d489889..000000000 --- a/js/controllers/more.js +++ /dev/null @@ -1,183 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('MoreController', - function($scope, $rootScope, $location, $filter, $timeout, balanceService, notification, rateService, backupService, identityService, isMobile, isCordova, go, pendingTxsService) { - var w = $rootScope.wallet; - var max = $rootScope.quotaPerItem; - $scope.isSafari = isMobile.Safari(); - $scope.isCordova = isCordova; - $scope.wallet = w; - $scope.error = null; - $scope.success = null; - - var bits = w.sizes().total; - w.kb = $filter('noFractionNumber')(bits / 1000, 1); - if (max) { - w.usage = $filter('noFractionNumber')(bits / max * 100, 0); - } - - $rootScope.title = 'Settings'; - - $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: 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.hideAdv = true; - $scope.hidePriv = true; - $scope.hideSecret = true; - if (w) { - $scope.priv = w.privateKey.toObj().extendedPrivateKeyString; - $scope.secret = w.getSecret(); - } - - setTimeout(function() { - $scope.$digest(); - }, 1); - - $scope.save = function() { - var w = $rootScope.wallet; - w.changeSettings({ - unitName: $scope.selectedUnit.shortName, - unitToSatoshi: $scope.selectedUnit.value, - unitDecimals: $scope.selectedUnit.decimals, - alternativeName: $scope.selectedAlternative.name, - alternativeIsoCode: $scope.selectedAlternative.isoCode, - }); - notification.success('Success', $filter('translate')('settings successfully updated')); - balanceService.update(w, function() { - pendingTxsService.update(); - $rootScope.$digest(); - }); - }; - - $scope.purge = function(deleteAll) { - var removed = w.purgeTxProposals(deleteAll); - if (removed) { - balanceService.update(w, function() { - $rootScope.$digest(); - }, true); - } - notification.info('Transactions Proposals Purged', removed + ' ' + $filter('translate')('transaction proposal purged')); - }; - - $scope.updateIndexes = function() { - var w = $rootScope.wallet; - notification.info('Scaning for transactions', 'Using derived addresses from your wallet'); - w.updateIndexes(function(err) { - notification.info('Scan Ended', 'Updating balance'); - if (err) { - notification.error('Error', $filter('translate')('Error updating indexes: ') + err); - } - balanceService.update(w, function() { - notification.info('Finished', 'The balance is updated using the derived addresses'); - w.sendIndexes(); - $rootScope.$digest(); - }, true); - }); - }; - - $scope.deleteWallet = function() { - $scope.loading = true; - $timeout(function() { - identityService.deleteWallet(w, function(err) { - $scope.loading = false; - if (err) { - $scope.error = err.message || err; - copay.logger.warn(err); - $timeout(function () { $scope.$digest(); }); - } else { - if ($rootScope.wallet) { - go.walletHome(); - } - $timeout(function() { - notification.success('Success', 'The wallet "' + (w.name || w.id) + '" was deleted'); - }); - } - }); - }, 100); - }; - - $scope.copyText = function(text) { - if (isCordova) { - window.cordova.plugins.clipboard.copy(text); - window.plugins.toast.showShortCenter('Copied to clipboard'); - } - }; - - $scope.downloadWalletBackup = function() { - backupService.walletDownload(w); - }; - - $scope.viewWalletBackup = function() { - $scope.loading = true; - $timeout(function() { - $scope.backupWalletPlainText = backupService.walletEncrypted(w); - }, 100); - }; - - $scope.copyWalletBackup = function() { - var ew = backupService.walletEncrypted(w); - window.cordova.plugins.clipboard.copy(ew); - window.plugins.toast.showShortCenter('Copied to clipboard'); - }; - - $scope.sendWalletBackup = function() { - if (isMobile.Android() || isMobile.Windows()) { - window.ignoreMobilePause = true; - } - window.plugins.toast.showShortCenter('Preparing backup...'); - var name = (w.name || w.id); - var ew = backupService.walletEncrypted(w); - var properties = { - subject: 'Copay Wallet Backup: ' + name, - body: 'Here is the encrypted backup of the wallet ' - + name + ': \n\n' + ew - + '\n\n To import this backup, copy all text between {...}, including the symbols {}', - isHtml: false - }; - window.plugin.email.open(properties); - }; - - }); diff --git a/js/controllers/paymentUri.js b/js/controllers/paymentUri.js deleted file mode 100644 index dd855cc8c..000000000 --- a/js/controllers/paymentUri.js +++ /dev/null @@ -1,20 +0,0 @@ -var bitcore = require('bitcore'); - -angular.module('copayApp.controllers').controller('paymentUriController', function($rootScope, $scope, $routeParams, $location, go) { - - // Build bitcoinURI with querystring - var query = []; - angular.forEach($location.search(), function(value, key) { - query.push(key + "=" + value); - }); - var queryString = query ? query.join("&") : null; - var bitcoinURI = $routeParams.data + ( queryString ? '?' + queryString : ''); - var uri = new bitcore.BIP21(bitcoinURI); - - if (uri && uri.address && (_.isString(uri.address) || uri.address.isValid()) ) { - copay.logger.debug('Payment Intent:', bitcoinURI); - $rootScope.pendingPayment = bitcoinURI; - } - - go.home(); -}); diff --git a/js/controllers/receive.js b/js/controllers/receive.js deleted file mode 100644 index cdaa0b868..000000000 --- a/js/controllers/receive.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('ReceiveController', - function($scope, $rootScope, $timeout, $modal, isCordova, isMobile) { - - $scope.newAddr = function() { - var w = $rootScope.wallet; - var lastAddr = w.generateAddress(null); - $scope.setAddressList(); - $scope.addr = lastAddr; - }; - - $scope.copyAddress = function(addr) { - if (isCordova) { - window.cordova.plugins.clipboard.copy('bitcoin:' + addr); - window.plugins.toast.showShortCenter('Copied to clipboard'); - } - }; - - $scope.shareAddress = function(addr) { - if (isCordova) { - if (isMobile.Android() || isMobile.Windows()) { - window.ignoreMobilePause = true; - } - window.plugins.socialsharing.share('bitcoin:' + addr, null, null, null); - } - }; - - $scope.init = function() { - $rootScope.title = 'Receive'; - $scope.showAll = false; - $scope.isCordova = isCordova; - - var w = $rootScope.wallet; - var lastAddr = _.first(w.getAddressesOrdered()); - var balance = w.balanceInfo.balanceByAddr; - $scope.setAddressList(); - - while (balance && balance[lastAddr] > 0) { - $scope.loading = true; - lastAddr = w.generateAddress(null); - }; - $scope.loading = false; - $scope.addr = lastAddr; - }; - - $scope.openAddressModal = function(address) { - var scope = $scope; - var ModalInstanceCtrl = function($scope, $modalInstance, address) { - $scope.address = address; - $scope.isCordova = isCordova; - $scope.copyAddress = function(addr) { - scope.copyAddress(addr); - }; - - $scope.cancel = function() { - $modalInstance.dismiss('cancel'); - }; - }; - - $modal.open({ - templateUrl: 'views/modals/qr-address.html', - windowClass: 'small', - controller: ModalInstanceCtrl, - resolve: { - address: function() { - return address; - } - } - }); - }; - - $scope.toggleShowAll = function() { - $scope.showAll = !$scope.showAll; - $scope.setAddressList(); - }; - - $scope.setAddressList = function() { - if ($scope.showAll) { - var w = $rootScope.wallet; - var balance = w.balanceInfo.balanceByAddr; - - var addresses = w.getAddressesOrdered(); - if (addresses) { - $scope.addrLength = addresses.length; - - if (!$scope.showAll) - addresses = addresses.slice(0, 3); - - var list = []; - _.each(addresses, function(address, index) { - list.push({ - 'index': index, - 'address': address, - 'balance': balance ? balance[address] : null, - 'isChange': w.addressIsChange(address), - }); - }); - $scope.addresses = list; - } - } else { - $scope.addresses = []; - } - }; - } -); diff --git a/js/controllers/send.js b/js/controllers/send.js deleted file mode 100644 index 2c7a372d8..000000000 --- a/js/controllers/send.js +++ /dev/null @@ -1,602 +0,0 @@ -'use strict'; -var bitcore = require('bitcore'); -var preconditions = require('preconditions').singleton(); - -angular.module('copayApp.controllers').controller('SendController', - function($scope, $rootScope, $window, $timeout, $modal, $filter, notification, isMobile, rateService, txStatus, isCordova) { - - $scope.init = function() { - var w = $rootScope.wallet; - preconditions.checkState(w); - - preconditions.checkState(w.settings.unitToSatoshi); - - $scope.isMobile = isMobile.any(); - $scope.isWindowsPhoneApp = isMobile.Windows() && isCordova; - $rootScope.wpInputFocused = false; - - $scope.isShared = w.isShared(); - $scope.requiresMultipleSignatures = w.requiresMultipleSignatures(); - $rootScope.title = $scope.requiresMultipleSignatures ? 'Send Proposal' : 'Send'; - $scope.loading = false; - $scope.error = $scope.success = null; - - $scope.alternativeName = w.settings.alternativeName; - $scope.alternativeIsoCode = w.settings.alternativeIsoCode; - - $scope.isRateAvailable = false; - $scope.rateService = rateService; - $scope.showScanner = false; - $scope.myId = w.getMyCopayerId(); - $scope.isMobile = isMobile.any(); - - if ($rootScope.pendingPayment) { - $timeout(function() { - $scope.setFromUri($rootScope.pendingPayment) - $rootScope.pendingPayment = null; - }, 100); - } - - $scope.setInputs(); - $scope.setScanner(); - - rateService.whenAvailable(function() { - $scope.isRateAvailable = true; - $scope.$digest(); - }); - }; - - if (isCordova) { - var openScannerCordova = $rootScope.$on('dataScanned', function(event, data) { - $scope.sendForm.address.$setViewValue(data); - $scope.sendForm.address.$render(); - }); - - $scope.$on('$destroy', function() { - openScannerCordova(); - }); - } - - $scope.formFocus = function(what) { - if (!$scope.isWindowsPhoneApp) return - - if (!what) { - $rootScope.wpInputFocused = false; - $scope.hideAddress = false; - $scope.hideAmount = false; - - } else { - $rootScope.wpInputFocused = true; - if (what == 'amount') { - $scope.hideAddress = true; - } else if (what == 'msg') { - $scope.hideAddress = true; - $scope.hideAmount = true; - } - - } - $timeout(function() { - $rootScope.$digest(); - }, 1); - }; - - $scope.setInputs = function() { - var w = $rootScope.wallet; - var unitToSat = w.settings.unitToSatoshi; - var satToUnit = 1 / unitToSat; - /** - * Setting the two related amounts as properties prevents an infinite - * recursion for watches while preserving the original angular updates - * - */ - Object.defineProperty($scope, - "_alternative", { - get: function() { - return this.__alternative; - }, - set: function(newValue) { - this.__alternative = newValue; - if (typeof(newValue) === 'number' && $scope.isRateAvailable) { - this._amount = parseFloat( - (rateService.fromFiat(newValue, $scope.alternativeIsoCode) * satToUnit).toFixed(w.settings.unitDecimals), 10); - } else { - this._amount = 0; - } - }, - enumerable: true, - configurable: true - }); - Object.defineProperty($scope, - "_amount", { - get: function() { - return this.__amount; - }, - set: function(newValue) { - this.__amount = newValue; - if (typeof(newValue) === 'number' && $scope.isRateAvailable) { - this.__alternative = parseFloat( - (rateService.toFiat(newValue * unitToSat, $scope.alternativeIsoCode)).toFixed(2), 10); - } else { - this.__alternative = 0; - } - }, - enumerable: true, - configurable: true - }); - - Object.defineProperty($scope, - "_address", { - get: function() { - return this.__address; - }, - set: function(newValue) { - this.__address = $scope.onAddressChange(newValue); - }, - enumerable: true, - configurable: true - }); - }; - - $scope.setScanner = function() { - navigator.getUserMedia = navigator.getUserMedia || - navigator.webkitGetUserMedia || navigator.mozGetUserMedia || - navigator.msGetUserMedia; - window.URL = window.URL || window.webkitURL || - window.mozURL || window.msURL; - - if (!window.cordova && !navigator.getUserMedia) - $scope.disableScanner = 1; - }; - - - $scope.setError = function(err) { - var w = $rootScope.wallet; - copay.logger.warn(err); - - var msg = err.toString(); - if (msg.match('BIG')) - msg = 'The transaction have too many inputs. Try creating many transactions for smaller amounts' - - if (msg.match('totalNeededAmount') || msg.match('unspent not set')) - msg = 'Insufficient funds' - - if (msg.match('expired')) - msg = 'The payment request has expired'; - - if (msg.match('XMLHttpRequest')) - msg = 'Error when sending to the blockchain. Resend it from Home'; - - var message = 'The transaction' + ($scope.requiresMultipleSignatures ? ' proposal' : '') + - ' could not be created: ' + msg; - - $scope.error = message; - - $timeout(function() { - $scope.$digest(); - }, 1); - }; - - $scope.submitForm = function(form) { - var w = $rootScope.wallet; - var unitToSat = w.settings.unitToSatoshi; - - if (form.$invalid) { - $scope.error = 'Unable to send transaction proposal'; - return; - } - - if (isCordova) { - window.plugins.spinnerDialog.show(null, 'Creating transaction...', true); - } - - $scope.loading = true; - if ($scope.isWindowsPhoneApp) - $rootScope.wpInputFocused = true; - - $timeout(function () { - var comment = form.comment.$modelValue; - var merchantData = $scope._merchantData; - var address, amount; - if (!merchantData) { - address = form.address.$modelValue; - amount = parseInt((form.amount.$modelValue * unitToSat).toFixed(0)); - } - - w.spend({ - merchantData: merchantData, - toAddress: address, - amountSat: amount, - comment: comment, - }, function (err, txid, status) { - if (isCordova) { - window.plugins.spinnerDialog.hide(); - } - $scope.loading = false; - if ($scope.isWindowsPhoneApp) - $rootScope.wpInputFocused = false; - - if (err) { - $scope.setError(err); - } - else { - txStatus.notify(status); - $scope.resetForm(); - } - }); - }, 100); - }; - - // QR code Scanner - var cameraInput; - var video; - var canvas; - var $video; - var context; - var localMediaStream; - - var _scan = function(evt) { - if ($scope.isMobile) { - $scope.scannerLoading = true; - var files = evt.target.files; - - if (files.length === 1 && files[0].type.indexOf('image/') === 0) { - var file = files[0]; - - var reader = new FileReader(); - reader.onload = (function(theFile) { - return function(e) { - var mpImg = new MegaPixImage(file); - mpImg.render(canvas, { - maxWidth: 200, - maxHeight: 200, - orientation: 6 - }); - - $timeout(function() { - qrcode.width = canvas.width; - qrcode.height = canvas.height; - qrcode.imagedata = context.getImageData(0, 0, qrcode.width, qrcode.height); - - try { - qrcode.decode(); - } catch (e) { - // error decoding QR - } - }, 1500); - }; - })(file); - - // Read in the file as a data URL - reader.readAsDataURL(file); - } - } else { - if (localMediaStream) { - context.drawImage(video, 0, 0, 300, 225); - - try { - qrcode.decode(); - } catch (e) { - //qrcodeError(e); - } - } - - $timeout(_scan, 500); - } - }; - - var _successCallback = function(stream) { - video.src = (window.URL && window.URL.createObjectURL(stream)) || stream; - localMediaStream = stream; - video.play(); - $timeout(_scan, 1000); - }; - - var _scanStop = function() { - $scope.scannerLoading = false; - $scope.showScanner = false; - if (!$scope.isMobile) { - if (localMediaStream && localMediaStream.stop) localMediaStream.stop(); - localMediaStream = null; - video.src = ''; - } - }; - - var _videoError = function(err) { - _scanStop(); - }; - - qrcode.callback = function(data) { - _scanStop(); - $scope.$apply(function() { - $scope.sendForm.address.$setViewValue(data); - $scope.sendForm.address.$render(); - }); - }; - - $scope.cancelScanner = function() { - _scanStop(); - }; - - $scope.openScanner = function() { - $scope.showScanner = true; - - // Wait a moment until the canvas shows - $timeout(function() { - canvas = document.getElementById('qr-canvas'); - context = canvas.getContext('2d'); - - if ($scope.isMobile) { - cameraInput = document.getElementById('qrcode-camera'); - cameraInput.addEventListener('change', _scan, false); - } else { - video = document.getElementById('qrcode-scanner-video'); - $video = angular.element(video); - canvas.width = 300; - canvas.height = 225; - context.clearRect(0, 0, 300, 225); - - navigator.getUserMedia({ - video: true - }, _successCallback, _videoError); - } - }, 500); - }; - - $scope.setTopAmount = function() { - var w = $rootScope.wallet; - var form = $scope.sendForm; - if (form) { - form.amount.$setViewValue(w.balanceInfo.topAmount); - form.amount.$render(); - form.amount.$isValid = true; - } - }; - - $scope.setForm = function(to, amount, comment) { - var form = $scope.sendForm; - if (to) { - form.address.$setViewValue(to); - form.address.$isValid = true; - form.address.$render(); - $scope.lockAddress = true; - } - - if (amount) { - form.amount.$setViewValue("" + amount); - form.amount.$isValid = true; - form.amount.$render(); - $scope.lockAmount = true; - } - - if (comment) { - form.comment.$setViewValue(comment); - form.comment.$isValid = true; - form.comment.$render(); - } - }; - - $scope.resetForm = function() { - var form = $scope.sendForm; - - $scope.fetchingURL = null; - $scope._merchantData = $scope._domain = null; - - $scope.lockAddress = false; - $scope.lockAmount = false; - - $scope._amount = $scope._address = null; - - form.amount.$pristine = true; - form.amount.$setViewValue(''); - form.amount.$render(); - - form.comment.$setViewValue(''); - form.comment.$render(); - form.$setPristine(); - - if (form.address) { - form.address.$pristine = true; - form.address.$setViewValue(''); - form.address.$render(); - } - $timeout(function() { - $rootScope.$digest(); - }, 1); - }; - - var $oscope = $scope; - $scope.openPPModal = function(merchantData) { - var ModalInstanceCtrl = function($scope, $modalInstance) { - var w = $rootScope.wallet; - var satToUnit = 1 / w.settings.unitToSatoshi; - $scope.md = merchantData; - $scope.alternative = $oscope._alternative; - $scope.alternativeIsoCode = $oscope.alternativeIsoCode; - $scope.isRateAvailable = $oscope.isRateAvailable; - $scope.unitTotal = (merchantData.total * satToUnit).toFixed(w.settings.unitDecimals); - - $scope.cancel = function() { - $modalInstance.dismiss('cancel'); - }; - }; - $modal.open({ - templateUrl: 'views/modals/paypro.html', - windowClass: 'medium', - controller: ModalInstanceCtrl, - }); - }; - - - $scope.setFromPayPro = function(uri) { - - var isChromeApp = window.chrome && chrome.runtime && chrome.runtime.id; - if (isChromeApp) { - $scope.error = 'Payment Protocol not yet supported on ChromeApp'; - return; - } - - var w = $rootScope.wallet; - var satToUnit = 1 / w.settings.unitToSatoshi; - $scope.fetchingURL = uri; - $scope.loading = true; - - - // Payment Protocol URI (BIP-72) - w.fetchPaymentRequest({ - url: uri - }, function(err, merchantData) { - $scope.loading = false; - $scope.fetchingURL = null; - - if (err) { - copay.logger.warn(err); - $scope.resetForm(); - var msg = err.toString(); - if (msg.match('HTTP')) { - msg = 'Could not fetch payment information'; - } - $scope.error = msg; - } else { - $scope._merchantData = merchantData; - $scope._domain = merchantData.domain; - $scope.setForm(null, (merchantData.total * satToUnit).toFixed(w.settings.unitDecimals)); - } - }); - }; - - $scope.setFromUri = function(uri) { - function sanitizeUri(uri) { - // Fixes when a region uses comma to separate decimals - var regex = /[\?\&]amount=(\d+([\,\.]\d+)?)/i; - var match = regex.exec(uri); - if (!match || match.length === 0) { - return uri; - } - var value = match[0].replace(',', '.'); - var newUri = uri.replace(regex, value); - return newUri; - }; - - var w = $rootScope.wallet; - var satToUnit = 1 / w.settings.unitToSatoshi; - var form = $scope.sendForm; - - uri = sanitizeUri(uri); - - var parsed = new bitcore.BIP21(uri); - if (!parsed.isValid() || !parsed.address.isValid()) { - $scope.error = 'Invalid bitcoin URL'; - form.address.$isValid = false; - return uri; - }; - - var addr = parsed.address.toString(); - if (parsed.data.merchant) - return $scope.setFromPayPro(parsed.data.merchant); - - var amount = (parsed.data && parsed.data.amount) ? - ((parsed.data.amount * 100000000).toFixed(0) * satToUnit).toFixed(w.settings.unitDecimals) : 0; - - $scope.setForm(addr, amount, parsed.data.message, true); - return addr; - }; - - $scope.onAddressChange = function(value) { - $scope.error = $scope.success = null; - if (!value) return ''; - - if (value.indexOf('bitcoin:') === 0) { - return $scope.setFromUri(value); - } else if (/^https?:\/\//.test(value)) { - return $scope.setFromPayPro(value); - } - - return value; - }; - - $scope.openAddressBook = function() { - var w = $rootScope.wallet; - var modalInstance = $modal.open({ - templateUrl: 'views/modals/address-book.html', - windowClass: 'large', - controller: function($scope, $modalInstance) { - - $scope.showForm = null; - $scope.addressBook = w.addressBook; - - $scope.hasEntry = function() { - return _.keys($scope.addressBook).length > 0 ? true : false; - }; - - $scope.toggleAddressBookEntry = function(key) { - w.toggleAddressBookEntry(key); - }; - - $scope.copyToSend = function(addr) { - $modalInstance.close(addr); - }; - - $scope.cancel = function(form) { - $scope.error = $scope.success = $scope.newaddress = $scope.newlabel = null; - clearForm(form); - $scope.toggleForm(); - }; - - $scope.toggleForm = function() { - $scope.showForm = !$scope.showForm; - }; - - var clearForm = function(form) { - form.newaddress.$pristine = true; - form.newaddress.$setViewValue(''); - form.newaddress.$render(); - - form.newlabel.$pristine = true; - form.newlabel.$setViewValue(''); - form.newlabel.$render(); - form.$setPristine(); - }; - - // TODO change to modal - $scope.submitAddressBook = function(form) { - if (form.$invalid) { - return; - } - $scope.loading = true; - $timeout(function() { - var errorMsg; - var entry = { - "address": form.newaddress.$modelValue, - "label": form.newlabel.$modelValue - }; - try { - w.setAddressBook(entry.address, entry.label); - } catch (e) { - copay.logger.warn(e); - errorMsg = e.message; - } - - if (errorMsg) { - $scope.error = errorMsg; - } else { - clearForm(form); - $scope.toggleForm(); - notification.success('Entry created', 'New addressbook entry created') - } - $scope.loading = false; - $rootScope.$digest(); - }, 100); - return; - }; - - $scope.close = function() { - $modalInstance.dismiss('cancel'); - }; - }, - }); - - modalInstance.result.then(function(addr) { - $scope.setForm(addr); - }); - }; - }); diff --git a/js/controllers/settings.js b/js/controllers/settings.js deleted file mode 100644 index d04a778cb..000000000 --- a/js/controllers/settings.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $route, $location, notification, configService) { - $scope.title = 'Settings'; - $scope.insightLivenet = config.network.livenet.url; - $scope.insightTestnet = config.network.testnet.url; - $scope.defaultLogLevel = config.logLevel || 'log'; - - var logLevels = copay.logger.getLevels(); - - $scope.availableLogLevels = []; - - for (var key in logLevels) { - $scope.availableLogLevels.push({ - 'name': key - }); - } - - $scope.availableStorages = [{ - name: 'In the cloud (Insight server)', - pluginName: 'EncryptedInsightStorage', - }, { - name: 'On this device (localstorage)', - pluginName: 'EncryptedLocalStorage', - }, - // { - // name: 'GoogleDrive', - // pluginName: 'GoogleDrive', - // } - ]; - - _.each($scope.availableStorages, function(v) { - if (config.plugins[v.pluginName]) - $scope.selectedStorage = v; - }); - - for (var ii in $scope.availableLogLevels) { - if ($scope.defaultLogLevel === $scope.availableLogLevels[ii].name) { - $scope.selectedLogLevel = $scope.availableLogLevels[ii]; - break; - } - } - - $scope.save = function() { - $scope.insightLivenet = copay.Insight.setCompleteUrl($scope.insightLivenet); - $scope.insightTestnet = copay.Insight.setCompleteUrl($scope.insightTestnet); - - var insightSettings = { - livenet: { - url: $scope.insightLivenet, - transports: ['polling'], - }, - testnet: { - url: $scope.insightTestnet, - transports: ['polling'], - }, - } - - var plugins = {}; - plugins[$scope.selectedStorage.pluginName] = true; - - configService.set({ - network: insightSettings, - plugins: plugins, - logLevel: $scope.selectedLogLevel.name, - EncryptedInsightStorage: _.extend(config.EncryptedInsightStorage, { - url: insightSettings.livenet.url + '/api/email' - }), - rates: _.extend(config.rates, { - url: insightSettings.livenet.url + '/api/rates' - }), - }, - function() { - notification.success('Settings saved',"Settings were saved"); - $location.path('/'); - }); - }; - - $scope.reset = function() { - configService.reset(function() { - notification.success('Settings reseted',"Settings were reseted"); - $location.path('/'); - }); - }; - -}); diff --git a/js/controllers/sidebar.js b/js/controllers/sidebar.js deleted file mode 100644 index 8904f7a55..000000000 --- a/js/controllers/sidebar.js +++ /dev/null @@ -1,121 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('SidebarController', function($scope, $rootScope, $location, $timeout, identityService, isMobile, isCordova, go) { - - $scope.isMobile = isMobile.any(); - $scope.isCordova = isCordova; - $scope.username = $rootScope.iden ? $rootScope.iden.getName() : ''; - - $scope.menu = [{ - 'title': 'Home', - 'icon': 'icon-home', - 'link': 'homeWallet' - }, { - 'title': 'Receive', - 'icon': 'icon-receive', - 'link': 'receive' - }, { - 'title': 'Send', - 'icon': 'icon-paperplane', - 'link': 'send' - }, { - 'title': 'History', - 'icon': 'icon-history', - 'link': 'history' - }]; - - $scope.signout = function() { - identityService.signout(); - }; - - $scope.isActive = function(item) { - return item.link && item.link == $location.path().split('/')[1]; - }; - - $scope.switchWallet = function(wid) { - $scope.walletSelection = false; - identityService.setFocusedWallet(wid); - go.walletHome(); - }; - - $scope.toggleWalletSelection = function() { - $scope.walletSelection = !$scope.walletSelection; - if (!$scope.walletSelection) return; - $scope.setWallets(); - }; - - $scope.openScanner = function() { - window.ignoreMobilePause = true; - cordova.plugins.barcodeScanner.scan( - function onSuccess(result) { - $timeout(function() { - window.ignoreMobilePause = false; - }, 100); - if (result.cancelled) return; - - $timeout(function() { - var data = result.text; - $scope.$apply(function() { - $rootScope.$emit('dataScanned', data); - }); - }, 1000); - }, - function onError(error) { - $timeout(function() { - window.ignoreMobilePause = false; - }, 100); - alert('Scanning error'); - } - ); - go.send(); - }; - - $scope.init = function() { - // This should be called only once. - - // focused wallet change - if ($rootScope.wallet) { - $rootScope.$watch('wallet', function() { - $scope.walletSelection = false; - $scope.setWallets(); - }); - } - - // wallet list change - if ($rootScope.iden) { - var iden = $rootScope.iden; - iden.on('newWallet', function() { - $scope.walletSelection = false; - $scope.setWallets(); - }); - iden.on('walletDeleted', function(wid) { - if (wid == $rootScope.wallet.id) { - copay.logger.debug('Deleted focused wallet:', wid); - - // new focus - var newWid = $rootScope.iden.getLastFocusedWalletId(); - if (newWid && $rootScope.iden.getWalletById(newWid)) { - identityService.setFocusedWallet(newWid); - } else { - copay.logger.debug('No wallets'); - identityService.noFocusedWallet(newWid); - } - } - $scope.walletSelection = false; - $scope.setWallets(); - }); - } - }; - - $scope.setWallets = function() { - if (!$rootScope.iden) return; - var ret = _.filter($rootScope.iden.getWallets(), function(w) { - return w; - }); - $scope.wallets = _.sortBy(ret, 'name'); - }; - - $scope.openMenu = function() { - go.swipe(true); - }; -}); diff --git a/js/controllers/unsupported.js b/js/controllers/unsupported.js deleted file mode 100644 index 2617a81df..000000000 --- a/js/controllers/unsupported.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('UnsupportedController', - function($scope, $location) { - if (localStorage && localStorage.length > 0) { - $location.path('/'); - } - } -); diff --git a/js/controllers/version.js b/js/controllers/version.js deleted file mode 100644 index 8c0cbc389..000000000 --- a/js/controllers/version.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('VersionController', - function($scope, $rootScope, $http, $filter, notification) { - - var w = $rootScope.wallet; - - $scope.version = copay.version; - $scope.commitHash = copay.commitHash; - $scope.networkName = w ? w.getNetworkName() : ''; - if (_.isUndefined($rootScope.checkVersion)) - $rootScope.checkVersion = true; - - if ($rootScope.checkVersion) { - $rootScope.checkVersion = false; - $http.get('https://api.github.com/repos/bitpay/copay/tags').success(function(data) { - var toInt = function(s) { - return parseInt(s); - }; - var latestVersion = data[0].name.replace('v', '').split('.').map(toInt); - var currentVersion = copay.version.split('.').map(toInt); - var title = 'Copay ' + data[0].name + ' ' + $filter('translate')('available.'); - var content; - if (currentVersion[0] < latestVersion[0]) { - content = 'It\'s important that you update your wallet at https://copay.io'; - notification.version(title, content, true); - } else if (currentVersion[0] == latestVersion[0] && currentVersion[1] < latestVersion[1]) { - var content = 'Please update your wallet at https://copay.io'; - notification.version(title, content, false); - } - }); - } - - }); diff --git a/js/models/Async.js b/js/models/Async.js deleted file mode 100644 index 421d83b32..000000000 --- a/js/models/Async.js +++ /dev/null @@ -1,432 +0,0 @@ -'use strict'; - -var EventEmitter = require('events').EventEmitter; -var bitcore = require('bitcore'); -var log = require('../util/log'); -var AuthMessage = bitcore.AuthMessage; -var util = bitcore.util; -var nodeUtil = require('util'); -var extend = nodeUtil._extend; -var io = require('socket.io-client'); -var preconditions = require('preconditions').singleton(); - -function Network(opts) { - preconditions.checkArgument(opts); - preconditions.checkArgument(opts.url); - opts = opts || {}; - this.maxPeers = opts.maxPeers || 12; - this.url = opts.url; - this.secretNumber = opts.secretNumber; - this.cleanUp(); - - this.socketOptions = { - reconnection: true, - 'force new connection': true, - 'secure': this.url.indexOf('https') === 0, - }; - - if (opts.transports) { - this.socketOptions['transports'] = opts.transports; - } -} - -nodeUtil.inherits(Network, EventEmitter); - -Network.prototype.cleanUp = function() { - this.started = false; - this.connectedPeers = []; - this.peerId = null; - this.privkey = null; - this.key = null; - this.copayerId = null; - this.allowedCopayerIds = null; - this.isInboundPeerAuth = []; - this.copayerForPeer = {}; - this.criticalErr = ''; - if (this.socket) { - log.info('Async DISCONNECT'); - this.socket.disconnect(); - this.socket.removeAllListeners(); - this.socket = null; - } - this.removeAllListeners(); -}; - -Network.parent = EventEmitter; - -// Array helpers -Network._inArray = function(el, array) { - return array.indexOf(el) > -1; -}; - -Network._arrayPushOnce = function(el, array) { - var ret = false; - if (!Network._inArray(el, array)) { - array.push(el); - ret = true; - } - return ret; -}; - -Network._arrayRemove = function(el, array) { - var pos = array.indexOf(el); - if (pos >= 0) array.splice(pos, 1); - return array; -}; - -Network.prototype.connectedCopayers = function() { - var ret = []; - for (var i in this.connectedPeers) { - var copayerId = this.copayerForPeer[this.connectedPeers[i]]; - if (copayerId) ret.push(copayerId); - } - return ret; -}; - -Network.prototype._sendHello = function(copayerId, secretNumber) { - - this.send(copayerId, { - type: 'hello', - copayerId: this.copayerId, - secretNumber: secretNumber - }); -}; - -Network.prototype._sendRejectConnection = function(copayerId) { - - this.send(copayerId, { - type: 'rejectConnection', - copayerId: this.copayerId, - }); -}; - -Network.prototype._deletePeer = function(peerId) { - delete this.isInboundPeerAuth[peerId]; - delete this.copayerForPeer[peerId]; - this.connectedPeers = Network._arrayRemove(peerId, this.connectedPeers); -}; - -Network.prototype._addCopayer = function(copayerId) { - var peerId = this.peerFromCopayer(copayerId); - this._addCopayerMap(peerId, copayerId); - Network._arrayPushOnce(peerId, this.connectedPeers); -}; - -Network.prototype._addConnectedCopayer = function(copayerId) { - this._addCopayer(copayerId); - this.emit('connect', copayerId); -}; - -Network.prototype.getKey = function() { - preconditions.checkState(this.privkey || this.key); - if (!this.key) { - var key = new bitcore.Key(); - key.private = new Buffer(this.privkey, 'hex'); - key.regenerateSync(); - this.key = key; - } - return this.key; -}; - -//hex version of one's own nonce -Network.prototype.setHexNonce = function(networkNonce) { - if (networkNonce) { - if (networkNonce.length !== 16) - throw new Error('incorrect length of hex nonce'); - this.networkNonce = new Buffer(networkNonce, 'hex'); - } else - this.iterateNonce(); -}; - -//hex version of copayers' nonces -Network.prototype.setHexNonces = function(networkNonces) { - for (var i in networkNonces) { - if (!this.networkNonces) - this.networkNonces = {}; - if (networkNonces[i].length === 16) - this.networkNonces[i] = new Buffer(networkNonces[i], 'hex'); - } -}; - -//for oneself -Network.prototype.getHexNonce = function() { - return this.networkNonce.toString('hex'); -}; - -//for copayers -Network.prototype.getHexNonces = function() { - var networkNoncesHex = []; - for (var i in this.networkNonces) { - networkNoncesHex[i] = this.networkNonces[i].toString('hex'); - } - return networkNoncesHex; -}; - -Network.prototype.iterateNonce = function() { - if (!this.networkNonce || this.networkNonce.length !== 8) { - this.networkNonce = new Buffer(8); - this.networkNonce.fill(0); - } - //the first 4 bytes of a nonce is a unix timestamp in seconds - //the second 4 bytes is just an iterated "sub" nonce - //the whole thing is interpreted as one big endian number - var noncep1 = this.networkNonce.slice(0, 4); - noncep1.writeUInt32BE(Math.floor(Date.now() / 1000), 0); - var noncep2uint = this.networkNonce.slice(4, 8).readUInt32BE(0); - var noncep2 = this.networkNonce.slice(4, 8); - noncep2.writeUInt32BE(noncep2uint + 1, 0); - this.networkNonce = Buffer.concat([noncep1, noncep2], 8); - return this.networkNonce; -}; - -Network.prototype.decode = function(enc) { - var sender = enc.pubkey; - var key = this.getKey(); - var prevnonce = this.networkNonces ? this.networkNonces[sender] : undefined; - var opts = { - prevnonce: prevnonce - }; - var decoded = AuthMessage.decode(key, enc, opts); - - //if no error thrown in the last step, we can set the copayer's nonce - if (!this.networkNonces) - this.networkNonces = {}; - this.networkNonces[sender] = decoded.nonce; - - var payload = decoded.payload; - return payload; -}; - -Network.prototype._onMessage = function(enc) { - var sender = enc.pubkey; - try { - var payload = this.decode(enc); - } catch (e) { - this._deletePeer(sender); - return; - } - - if (this.ignoreMessageFromTs && this.ignoreMessageFromTs === enc.ts) { - log.debug('Ignoring trailing message. Ts:', enc.ts); - return; - } - - var self = this; - switch (payload.type) { - case 'hello': - if (typeof payload.secretNumber === 'undefined' || payload.secretNumber !== this.secretNumber) { - this._sendRejectConnection(sender); - this._deletePeer(enc.pubkey, 'incorrect secret number'); - return; - } - // if we locked allowed copayers, check if it belongs - if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) { - this._sendRejectConnection(sender); - this._deletePeer(sender); - return; - } - //ensure claimed public key is actually the public key of the peer - //e.g., their public key should hash to be their peerId - if (sender !== payload.copayerId) { - this._sendRejectConnection(sender); - this._deletePeer(enc.pubkey, 'incorrect pubkey for peerId'); - return; - } - this._addConnectedCopayer(payload.copayerId); - break; - default: - this.emit('data', sender, payload, enc.ts); - } -}; - -Network.prototype._setupSocketHandlers = function(opts, cb) { - preconditions.checkState(this.socket); - log.debug('setting up connection', opts); - var self = this; - - self.socket.on('connect_error', function(m) { - - // If socket is not started, destroy it and emit and error - // If it is started, socket.io will try to reconnect. - if (!self.started) { - self.emit('connect_error'); - self.cleanUp(); - } - }); - - self.socket.on('subscribed', function(m) { - var fromTs = opts.syncedTimestamp || 0; - - // We ask for this message, and then ignore it, only to see if the - // server has erased our old messages. - - if (fromTs) { - self.ignoreMessageFromTs = fromTs; - } - log.info('Async: synchronizing from: ', fromTs); - self.socket.emit('sync', fromTs); - self.started = true; - }); - - - self.socket.on('message', function(m) { - // delay execution, to improve error handling - setTimeout(function() { - self._onMessage(m); - }, 1); - }); - self.socket.on('error', self._onError.bind(self)); - self.socket.on('no_messages', self.emit.bind(self, 'no_messages')); - self.socket.on('no messages', self.emit.bind(self, 'no_messages')); - self.socket.on('connect', function() { - var pubkey = self.getKey().public.toString('hex'); - log.debug('Async subscribing to pubkey:', pubkey); - - self.socket.emit('subscribe', pubkey); - - self.socket.on('disconnect', function() { - self.socket.emit('subscribe', pubkey); - }); - if (typeof cb === 'function') cb(); - }); -}; - -Network.prototype._onError = function(err) { - log.debug('RECV ERROR: ', err); - log.debug(err.stack); - this.criticalError = err.message; -}; - -Network.prototype.greet = function(copayerId, secretNumber) { - this._sendHello(copayerId, secretNumber); - var peerId = this.peerFromCopayer(copayerId); - this._addCopayerMap(peerId, copayerId); -}; - -Network.prototype._addCopayerMap = function(peerId, copayerId) { - if (!this.copayerForPeer[peerId]) { - if (Object.keys(this.copayerForPeer).length < this.maxPeers) { - this.copayerForPeer[peerId] = copayerId; - } - } -}; - -Network.prototype._setInboundPeerAuth = function(peerId) { - this.isInboundPeerAuth[peerId] = true; -}; - -Network.prototype.setCopayerId = function(copayerId) { - preconditions.checkState(!this.started, 'network already started: can not change peerId'); - - this.copayerId = copayerId; - this.copayerIdBuf = new Buffer(copayerId, 'hex'); - this.peerId = this.peerFromCopayer(this.copayerId); - this._addCopayerMap(this.peerId, copayerId); -}; - - -// TODO cache this. -Network.prototype.peerFromCopayer = function(hex) { - var SIN = bitcore.SIN; - return new SIN(new Buffer(hex, 'hex')).toString(); -}; - -Network.prototype.start = function(opts, openCallback) { - preconditions.checkArgument(opts); - preconditions.checkArgument(opts.privkey); - preconditions.checkArgument(opts.copayerId); - - if (this.started) { - log.debug('Async: Networing already started for this wallet.') - return openCallback(); - } - - this.privkey = opts.privkey; - this.setCopayerId(opts.copayerId); - this.maxPeers = opts.maxPeers || this.maxPeers; - - this.socket = this.createSocket(); - this._setupSocketHandlers(opts, openCallback); -}; - -Network.prototype.createSocket = function() { - log.debug('Async: Connecting to socket:', this.url); - return io.connect(this.url, this.socketOptions); -}; - -Network.prototype.getOnlinePeerIDs = function() { - return this.connectedPeers; -}; - - -Network.prototype.getCopayerIds = function() { - if (this.allowedCopayerIds) { - return Object.keys(this.allowedCopayerIds); - } else { - var copayerIds = []; - for (var peerId in this.copayerForPeer) { - copayerIds.push(this.copayerForPeer[peerId]); - } - return copayerIds; - } -}; - - -Network.prototype.send = function(dest, payload, cb) { - preconditions.checkState(this.socket); - preconditions.checkArgument(payload); - - var self = this; - if (!dest) { - dest = this.getCopayerIds(); - payload.isBroadcast = 1; - } - - if (typeof dest === 'string') - dest = [dest]; - - var l = dest.length; - var i = 0; - - for (var ii in dest) { - var to = dest[ii]; - if (to == this.copayerId) - continue; - - var message = this.encode(to, payload); - this.socket.emit('message', message); - } - - if (typeof cb === 'function') cb(); -}; - - -Network.prototype.encode = function(copayerId, payload, nonce) { - this.iterateNonce(); - var opts = { - nonce: nonce || this.networkNonce - }; - var copayerIdBuf = new Buffer(copayerId, 'hex'); - var message = AuthMessage.encode(copayerIdBuf, this.getKey(), payload, opts); - return message; -}; - -Network.prototype.isOnline = function() { - return !!this.socket; -}; - - -Network.prototype.lockIncommingConnections = function(allowedCopayerIdsArray) { - this.allowedCopayerIds = {}; - for (var i in allowedCopayerIdsArray) { - this.allowedCopayerIds[allowedCopayerIdsArray[i]] = true; - } -}; - -Network.prototype.setCopayers = function(copayersIdsArray) { - for (var i in copayersIdsArray) { - this._addCopayer(copayersIdsArray[i]); - } -}; - -module.exports = Network; diff --git a/js/models/Compatibility.js b/js/models/Compatibility.js deleted file mode 100644 index 94da67c59..000000000 --- a/js/models/Compatibility.js +++ /dev/null @@ -1,256 +0,0 @@ -'use strict'; - -var Identity = require('./Identity'); -var Wallet = require('./Wallet'); -var cryptoUtils = require('../util/crypto'); -var CryptoJS = require('node-cryptojs-aes').CryptoJS; -var sjcl = require('../../lib/sjcl'); -var log = require('../util/log'); -var preconditions = require('preconditions').instance(); -var _ = require('lodash'); - -var Compatibility = {}; -Compatibility.iterations = 100; -Compatibility.salt = 'mjuBtGybi/4='; - -/** - * Reads from localstorage wallets saved previously to 0.8 - */ -Compatibility._getWalletIds = function(cb) { - preconditions.checkArgument(cb); - var walletIds = []; - var uniq = {}; - var key; - for (key in localStorage) { - var split = key.split('::'); - if (split.length == 2) { - var walletId = split[0]; - - if (!walletId || walletId === 'nameFor' || walletId === 'lock' || walletId === 'wallet') { - continue; - } - - if (typeof uniq[walletId] === 'undefined') { - walletIds.push(walletId); - uniq[walletId] = 1; - } - } - } - return cb(walletIds); -}; - -/** - * @param {string} encryptedWallet - base64-encoded encrypted wallet - * @param {string} password - * @returns {Object} - */ -Compatibility.importLegacy = function(encryptedWallet, password) { - var passphrase = this.kdf(password); - var ret = Compatibility._decrypt(encryptedWallet, passphrase); - - if (!ret) return null; - return ret; -}; - -/** - * Decrypts using the CryptoJS library (unknown encryption schema) - * - * Don't use CryptoJS to encrypt. This still exists for compatibility reasons only. - */ -Compatibility._decrypt = function(base64, passphrase) { - var decryptedStr = null; - try { - var decrypted = CryptoJS.AES.decrypt(base64, passphrase); - if (decrypted) - decryptedStr = decrypted.toString(CryptoJS.enc.Utf8); - } catch (e) { - // Error while decrypting - return null; - } - return decryptedStr; -}; - -/** - * Reads an item from localstorage, decrypts it with passphrase - */ -Compatibility._read = function(k, passphrase, cb) { - preconditions.checkArgument(cb); - - var ret = localStorage.getItem(k); - if (!ret) return cb(null); - var ret = self._decrypt(ret, passphrase); - if (!ret) return cb(null); - - ret = ret.toString(CryptoJS.enc.Utf8); - ret = JSON.parse(ret); - return ret; -}; - -Compatibility.getWallets_Old = function(cb) { - preconditions.checkArgument(cb); - - var wallets = []; - var self = this; - - this._getWalletIds(function(ids) { - if (!ids.length) { - return cb([]); - } - - _.each(ids, function(id) { - var name = localStorage.getItem('nameFor::' + id); - if (name) { - wallets.push({ - id: id, - name: name, - }); - } - }); - return cb(wallets); - }); -}; - -Compatibility.getWallets2 = function(cb) { - var self = this; - var re = /wallet::([^_]+)(_?(.*))/; - var va = /^{+/; - - var key; - var keys = []; - for (key in localStorage) { - keys.push(key); - } - var wallets = _.compact(_.map(keys, function(key) { - if (key.indexOf('wallet::') !== 0) - return null; - var match = key.match(re); - var matchValue = localStorage[key].match(va); - if (match.length != 4) - return null; - if (matchValue) - return null; - return { - id: match[1], - name: match[3] ? match[3] : undefined, - value: localStorage[key] - }; - })); - - return cb(wallets); -}; - -/** - * Lists all wallets in localstorage - */ -Compatibility.listWalletsPre8 = function(cb) { - var self = this; - self.getWallets2(function(wallets) { - self.getWallets_Old(function(wallets2) { - var ids = _.pluck(wallets, 'id'); - _.each(wallets2, function(w) { - if (!_.contains(ids, w.id)) - wallets.push(w); - }); - return cb(wallets); - }); - }) -}; - -/** - * Retrieves a wallet that predates the 0.8 release - */ -Compatibility.readWalletPre8 = function(walletId, password, cb) { - var self = this; - var passphrase = cryptoUtils.kdf(password); - var obj = {}; - var key; - - for (key in localStorage) { - if (key.indexOf('wallet::' + walletId) !== -1) { - var ret = self._read(localStorage.getItem(key), passphrase); - if (err) return cb(err); - - _.each(Wallet.PERSISTED_PROPERTIES, function(p) { - obj[p] = ret[p]; - }); - - if (!_.any(_.values(obj))) - return cb(new Error('Wallet not found')); - - var w, err; - obj.id = walletId; - try { - w = self.fromObj(obj); - } 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); - } - } -}; - -Compatibility.importEncryptedWallet = function(identity, cypherText, password, opts, cb) { - var crypto = (opts && opts.cryptoUtil) || cryptoUtils; - - var obj = crypto.decrypt(password, cypherText); - if (!obj) { - // 0.7.3 broken KDF - log.debug('Trying legacy encryption 0.7.2...'); - var passphrase = crypto.kdf(password, 'mjuBtGybi/4=', 100); - obj = crypto.decrypt(passphrase, cypherText); - } - - if (!obj) { - log.info("Could not decrypt, trying legacy.."); - obj = Compatibility.importLegacy(cypherText, password); - }; - - if (!obj) { - return cb('Could not decrypt', null); - } - - - try { - obj = JSON.parse(obj); - } catch (e) { - return cb('Could not read encrypted wallet', null); - } - return identity.importWalletFromObj(obj, opts, cb); -}; - -/** - * @desc Generate a WordArray expanding a password - * - * @param {string} password - the password to expand - * @returns WordArray 512 bits with the expanded key generated from password - */ -Compatibility.kdf = function(password) { - var hash = sjcl.hash.sha256.hash(sjcl.hash.sha256.hash(password)); - var salt = sjcl.codec.base64.toBits(this.salt); - - var crypto2 = function(key, salt, iterations, length, alg) { - return sjcl.codec.hex.fromBits(sjcl.misc.pbkdf2(key, salt, iterations, length * 8, - alg == 'sha1' ? function(key) { - return new sjcl.misc.hmac(key, sjcl.hash.sha1) - } : null - )) - }; - - var key512 = crypto2(hash, salt, this.iterations, 64, 'sha1'); - var sbase64 = sjcl.codec.base64.fromBits(sjcl.codec.hex.toBits(key512)); - return sbase64; -}; - -Compatibility.deleteOldWallet = function(walletObj) { -console.log('[Compatibility.js:249]',walletObj); //TODO - localStorage.removeItem('wallet::' + walletObj.id + '_' + walletObj.name); - log.info('Old wallet ' + walletObj.name + ' deleted: ' + walletObj.id); -}; - - -module.exports = Compatibility; diff --git a/js/models/HDParams.js b/js/models/HDParams.js deleted file mode 100644 index dba208e28..000000000 --- a/js/models/HDParams.js +++ /dev/null @@ -1,274 +0,0 @@ -'use strict'; - -// 83.8% typed (by google's closure-compiler account) - -var preconditions = require('preconditions').singleton(); -var HDPath = require('./HDPath'); -var _ = require('lodash'); - -/** - * @desc - * HDParams is a class that encapsulates information about the current indexes - * of a copayer - * - * When a copayer creates a new wallet, his receiveIndex gets updated, and an - * address is generated from everybody's public key, using this BIP32 path: - *
 m/copay'/{copayer}/0/{index} 
- * - * When a copayer generates a transaction proposal, his changeIndex gets - * updated, and all funds from that transaction proposal go to the multisig - * address generated from this BIP32 path for all the copayers: - *
 m/copay'/{copayer}/1/{changeIndex} 
- * - * There's a shared index, HDPath.SHARED_INDEX, that serves to - * generate addresses common to everybody. - * - * @TODO: Should opts.cosigner go? - * - * @constructor - * - * @param {Object} opts - options for the construction of this object - * @param {number=} opts.cosigner - backwards compatible index of a copayer - * @param {number} opts.copayerIndex - the copayer that generated this branch - * of addresses - * @param {number} opts.receiveIndex - the current index for a the last receive - * address generated for this copayer - * @param {number} opts.changeIndex - the current index for a the last change - * address generated for this copayer - */ -function HDParams(opts) { - opts = opts || {}; - - //opts.cosigner is for backwards compatibility only - - /** - * @public - * @type number - */ - this.copayerIndex = _.isUndefined(opts.copayerIndex) ? opts.cosigner : opts.copayerIndex; - /** - * @public - * @type number - */ - this.changeIndex = opts.changeIndex || 0; - /** - * @public - * @type number - */ - this.receiveIndex = opts.receiveIndex || 0; - - if (_.isUndefined(this.copayerIndex)) { - this.copayerIndex = HDPath.SHARED_INDEX; - } -} - -/** - * @desc - * Creates a set of HDParams, with one HDParams structure for each copayer and - * a shared path. - * - * @static - * @param {number} totalCopayers - the number of copayers in a wallet - * @returns {HDParams[]} a list of HDParams generated for a new empty wallet - */ -HDParams.init = function(totalCopayers) { - preconditions.shouldBeNumber(totalCopayers); - - var ret = [new HDParams({receiveIndex: 1})]; - for (var i = 0 ; i < totalCopayers ; i++) { - ret.push(new HDParams({copayerIndex: i})); - } - return ret; -}; - -/** - * @desc - * Generates a set of HDParams from a list with a object-serialized version of - * HDParams. - * - * @static - * @param {Object[]} hdParams - a list with objects - * @returns {HDParams[]} builds a HDParams for each object literal found in the list - */ -HDParams.fromList = function(hdParams) { - return hdParams.map(function(i) { return HDParams.fromObj(i); }); -}; - -/** - * @desc - * Generate a HDParams from an object. - * - * This essentialy only calls new HDParams(data), but it's the interface being - * used everywhere to encode/decode. - * - * @TODO: This should be clarified - Or abstracted away - as it's a pattern - * used in multiple places. - * - * @static - * @param {Object} data - a serialized version of HDParams - * @return {HDParams} - * @throws {BADDATA} - when the parameter data already is an instance of - * HDParams. - */ -HDParams.fromObj = function(data) { - if (data instanceof HDParams) { - throw new Error('BADDATA', 'bad data format: Did you use .toObj()?'); - } - return new HDParams(data); -}; - -/** - * @desc - * Serializes a list of HDParams to a list of "plain" objects, according to each - * element's toObj() method. - * - * @TODO: There should be a list of Classes that share this behaviour. - * - * @static - * @param {HDParams[]} hdParams - a list of HDParams objects. - * @returns {Array} an array with the toObj() serialization of each - * element in hdParams - */ -HDParams.serialize = function(hdParams) { - return hdParams.map(function(i) { return i.toObj(); }); -}; - -/** - * @desc - * Creates a new HDParams set with totalCopayers+1 elements. - * - * Sets the first (corresponding to the parameters for the shared addresses) - * HDParams object to match shared's values. Returns a serialized - * version of this set - * - *
- * var updateResult = HDParams.update({changeIndex: 1, receiveIndex: 2}, 5);
- * // All the following asserts succeed
- * assert(_.isArray(updateResult));
- * assert(_.all(updateResult, function (hd) { return !(hd instanceOf HDParams); }));
- * assert(_.size(updateResult) === 6);
- * assert(updateResult[0].changeIndex === 1);
- * assert(updateResult[0].receiveIndex === 2);
- * 
- * - * @TODO: This method is badly coded, it does something that is very specific - * and kind of strange. I couldn't figure out why would it be needed. - * - * @static - * @param {HDParams} shared - an instance of HDParams - * @param {number} totalCopayers - * @return {Object[]} - */ -HDParams.update = function(shared, totalCopayers) { - var hdParams = this.init(totalCopayers); - hdParams[0].changeIndex = shared.changeIndex; - hdParams[0].receiveIndex = shared.receiveIndex; - return this.serialize(hdParams); -}; - -/** - * @desc - * Serializes this object - * - * @TODO: I couldn't realize why would this be needed - calling, for example, - * JSON.stringify would have the same result on this object than on the - * original instance - * - * @returns {Object} a serialized version that should be equal (in a deep object - * comparison) to the this instance if passed to the - * HDParams() constructor. - */ -HDParams.prototype.toObj = function() { - return { - copayerIndex: this.copayerIndex, - changeIndex: this.changeIndex, - receiveIndex: this.receiveIndex - }; -}; - -/** - * @desc - * Throws an error if a given index falls out of the range for known addresses. - * - * @TODO: This is not a good pattern, exceptions should be for exceptional things. - * - * @param {number} index the index to check for - * @param {boolean} isChange whether to check for the change index or the - * receive address index - */ -HDParams.prototype.checkRange = function(index, isChange) { - if ((isChange && index > this.changeIndex) || - (!isChange && index > this.receiveIndex)) { - throw new Error('Out of bounds at index ' + index + ' isChange: ' + isChange); - } -}; - -/** - * @desc - * Return this instance's changeIndex value - * - * @TODO: Somebody did a lot of java. Not sure if we need to be so verbose. If - * anything, let's just declare changeIndex as a read only variable - * @returns {number} this HDParams current index to generate a change address - */ -HDParams.prototype.getChangeIndex = function() { - return this.changeIndex; -}; - -/** - * @desc - * Return this instance's receiveIndex value - * - * @TODO: Somebody did a lot of java. Not sure if we need to be so verbose. If - * anything, let's just declare changeIndex as a read only variable - * @returns {number} this HDParams current index to generate a receive address - */ -HDParams.prototype.getReceiveIndex = function() { - return this.receiveIndex; -}; - -/** - * @desc - * Increment this instance's changeIndex or receiveIndex value - * - * @TODO: Somebody did a lot of java. Not sure if we need to be so verbose. - * - * @param {boolean} isChange - if true, change changeIndex - */ -HDParams.prototype.increment = function(isChange) { - if (isChange) { - this.changeIndex++; - } else { - this.receiveIndex++; - } -}; - -/** - * @desc - * Merge this instance with another HDParams instance. - * - * @TODO: Device a general approach to merges. - * - * @param {Object} inHDParams - the object to merge to - * @param {number} inHDParams.copayerIndex - the object to merge to - * @returns {boolean} true if this object has changed - */ -HDParams.prototype.merge = function(inHDParams) { - preconditions.shouldBeObject(inHDParams); - preconditions.checkArgument(this.copayerIndex == inHDParams.copayerIndex); - - var hasChanged = false; - - if (inHDParams.changeIndex > this.changeIndex) { - this.changeIndex = inHDParams.changeIndex; - hasChanged = true; - } - - if (inHDParams.receiveIndex > this.receiveIndex) { - this.receiveIndex = inHDParams.receiveIndex; - hasChanged = true; - } - return hasChanged; -}; - -module.exports = HDParams; diff --git a/js/models/HDPath.js b/js/models/HDPath.js deleted file mode 100644 index 7a03fa858..000000000 --- a/js/models/HDPath.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -// 90.2% typed (by google's closure-compiler account) - -var preconditions = require('preconditions').singleton(); -var _ = require('lodash'); - -/** - * @namespace - * @desc - * HDPath contains helper functions to handle BIP32 branches as - * Copay uses them. - * Based on https://github.com/maraoz/bips/blob/master/bip-NNNN.mediawiki - *
- * m / purpose' / copayerIndex / change:boolean / addressIndex
- * 
- */ -var HDPath = {}; - -/** - * @desc Copay's BIP45 purpose code - * @const - * @type number - */ -HDPath.PURPOSE = 45; - -/** - * @desc Maximum number for non-hardened values (BIP32) - * @const - * @type number - */ -HDPath.MAX_NON_HARDENED = 0x80000000 - 1; - -/** - * @desc Shared Index: used for creating addresses for no particular purpose - * @const - * @type number - */ -HDPath.SHARED_INDEX = HDPath.MAX_NON_HARDENED - 0; - -/** - * @desc ??? - * @const - * @type number - */ -HDPath.ID_INDEX = HDPath.MAX_NON_HARDENED - 1; - -/** - * @desc BIP45 prefix for COPAY - * @const - * @type string - */ -HDPath.BIP45_PUBLIC_PREFIX = 'm/' + HDPath.PURPOSE + '\''; - -/** - * @desc Retrieve a string to be used with bitcore representing a Copay branch - * @param {number} addressIndex - the last value of the HD derivation - * @param {boolean} isChange - whether this is a change address or a receive - * @param {number} copayerIndex - the index of the copayer in the pubkeyring - * @return {string} - the path for the HD derivation - */ -HDPath.Branch = function(addressIndex, isChange, copayerIndex) { - preconditions.checkArgument(_.isNumber(addressIndex)); - preconditions.checkArgument(_.isBoolean(isChange)); - - var ret = 'm/' + - (typeof copayerIndex !== 'undefined' ? copayerIndex : HDPath.SHARED_INDEX) + '/' + - (isChange ? 1 : 0) + '/' + - addressIndex; - return ret; -}; - -/** - * @desc ??? - * @param {number} addressIndex - the last value of the HD derivation - * @param {boolean} isChange - whether this is a change address or a receive - * @param {number} copayerIndex - the index of the copayer in the pubkeyring - * @return {string} - the path for the HD derivation - */ -HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) { - preconditions.checkArgument(_.isNumber(addressIndex)); - preconditions.checkArgument(_.isBoolean(isChange)); - - var sub = HDPath.Branch(addressIndex, isChange, copayerIndex); - sub = sub.substring(2); - return HDPath.BIP45_PUBLIC_PREFIX + '/' + sub; -}; - -/** - * @desc - * Decompose a string and retrieve its arguments as if it where a Copay address. - * @param {string} path - the HD path - * @returns {Object} an object with three keys: addressIndex, isChange, and - * copayerIndex - */ -HDPath.indexesForPath = function(path) { - preconditions.checkArgument(_.isString(path)); - - var s = path.split('/'); - return { - isChange: s[3] === '1', - addressIndex: parseInt(s[4], 10), - copayerIndex: parseInt(s[2], 10) - }; -}; - -/** - * @desc The ID for a shared branch - */ -HDPath.IdFullBranch = HDPath.FullBranch(0, false, HDPath.ID_INDEX); -/** - * @desc Partial ID for a shared branch - */ -HDPath.IdBranch = HDPath.Branch(0, false, HDPath.ID_INDEX); - -module.exports = HDPath; diff --git a/js/models/Identity.js b/js/models/Identity.js deleted file mode 100644 index 8cda88980..000000000 --- a/js/models/Identity.js +++ /dev/null @@ -1,905 +0,0 @@ -'use strict'; -var _ = require('lodash'); -var preconditions = require('preconditions').singleton(); -var inherits = require('inherits'); -var events = require('events'); -var async = require('async'); - -var bitcore = require('bitcore'); - -var TxProposals = require('./TxProposals'); -var PublicKeyRing = require('./PublicKeyRing'); -var PrivateKey = require('./PrivateKey'); -var Wallet = require('./Wallet'); -var PluginManager = require('./PluginManager'); -var Async = require('./Async'); -var cryptoUtil = require('../util/crypto'); -var log = require('../util/log'); -var version = require('../../version').version; - -/** - * @desc - * Identity - stores the state for a wallet in creation - * - * @param {Object} opts - configuration for this wallet - * @param {string} opts.fullName - * @param {string} opts.email - * @param {string} opts.password - * @param {string} opts.storage - * @param {string} opts.pluginManager - * @param {Object} opts.walletDefaults - * @param {string} opts.version - * @param {Object} opts.wallets - * @param {Object} opts.network - * @param {string} opts.network.testnet - * @param {string} opts.network.livenet - * @constructor - */ -function Identity(opts) { - preconditions.checkArgument(opts); - - opts = _.extend({}, opts); - this.networkOpts = { - 'livenet': opts.network.livenet, - 'testnet': opts.network.testnet, - }; - this.blockchainOpts = { - 'livenet': opts.network.livenet, - 'testnet': opts.network.testnet, - }; - - this.fullName = opts.fullName || opts.email; - this.email = opts.email; - this.password = opts.password; - - this.storage = opts.storage || opts.pluginManager.get('DB'); - this.storage.setCredentials(this.email, this.password, {}); - - this.walletDefaults = opts.walletDefaults || {}; - this.version = opts.version || version; - - this.walletIds = opts.walletIds || []; - this.wallets = opts.wallets || {}; - this.focusedTimestamps = opts.focusedTimestamps || {}; - this.backupNeeded = opts.backupNeeded || false; - -}; - - -inherits(Identity, events.EventEmitter); - -Identity.getStoragePrefix = function() { - return 'profile::'; -}; - -Identity.getKeyForEmail = function(email) { - return Identity.getStoragePrefix() + bitcore.util.sha256ripe160(email).toString('hex'); -}; - -Identity.prototype.getChecksumForStorage = function() { - return JSON.stringify(_.sortBy(this.walletIds)); -}; - -Identity.prototype.getId = function() { - return Identity.getKeyForEmail(this.email); -}; - -Identity.prototype.getName = function() { - return this.fullName || this.email; -}; - -/** - * Creates an Identity - * - * @param opts - * @param cb - * @return {undefined} - */ -Identity.create = function(opts, cb) { - opts = _.extend({ - backupNeeded: true - }, opts); - - var iden = new Identity(opts); - iden.store(_.extend(opts, { - failIfExists: true - }), function(err) { - if (err) return cb(err); - return cb(null, iden); - }); -}; - -Identity.prototype.resendVerificationEmail = function(cb) { - var self = this; - - preconditions.checkArgument(_.isFunction(cb)); - preconditions.checkState(_.isFunction(self.storage.resendVerificationEmail)); - - self.storage.resendVerificationEmail(cb); -}; - - -/** - * Open an Identity from the given storage. - * - * After opening a profile, and setting its wallet event handlers, - * the client must run .netStart on each - * (probably on iden's newWallet handler - * - * @param {Object} opts - * @param {Object} opts.storage - * @param {string} opts.email - * @param {string} opts.password - * @param {Function} cb - */ -Identity.open = function(opts, cb) { - preconditions.checkArgument(_.isObject(opts)); - preconditions.checkArgument(_.isFunction(cb)); - - var storage = opts.storage || opts.pluginManager.get('DB'); - storage.setCredentials(opts.email, opts.password, opts); - storage.getItem(Identity.getKeyForEmail(opts.email), function(err, data, headers) { - var exported; - if (err) { - return cb(err); - } - try { - exported = JSON.parse(data); - } catch (e) { - return cb(e); - } - return cb(null, new Identity(_.extend(opts, exported)), headers); - }); -}; - -Identity.prototype.verifyChecksum = function(cb) { - var self = this; - - self.storage.getItem(Identity.getKeyForEmail(self.email), function(err, data, headers) { - var iden; - if (err) return cb(err); - try { - iden = JSON.parse(data); - } catch (e) { - return cb(e); - } - return cb(null, self.getChecksumForStorage() == self.getChecksumForStorage.call(iden)); - }); -}; - - -/** - * @param {string} walletId - * @returns {Wallet} - */ -Identity.prototype.getWalletById = function(walletId) { - return this.wallets[walletId]; -}; - -/** - * @returns {Wallet[]} - */ -Identity.prototype.getWallets = function() { - return _.values(this.wallets); -}; - -/** - * addWallet - * - * @param w - */ -Identity.prototype.addWallet = function(w) { - this.wallets[w.getId()] = w; - this.walletIds = _.union(this.walletIds, [w.getId()]); -}; - -/** - * @desc Deletes a wallet. This involves removing it from the storage instance - * - * @param {string} walletId - * @callback cb - * @return {err} - */ -Identity.prototype.deleteWallet = function(walletId, cb) { - preconditions.checkArgument(_.isString(walletId)); - var self = this; - - self.verifyChecksum(function(err, match) { - if (err) return cb(err); - if (!match) return cb('The profile is out of sync. Please re-login to get the latest changes.'); - - var w = self.getWalletById(walletId); - w.close(); - - delete self.wallets[walletId]; - delete self.focusedTimestamps[walletId]; - self.walletIds = _.without(self.walletIds, walletId); - - self.storage.removeItem(Wallet.getStorageKey(walletId), function(err) { - if (err) return cb(err); - self.emitAndKeepAlive('walletDeleted', walletId); - if (!self.walletIds.length) { - self.emitAndKeepAlive('noWallets') - } - self.store({ - noWallets: true - }, cb); - }); - }); -}; - - -/** - * readAndBindWallet - * - * @param {string} wid walletId to be readed - * @param {function} cb - * - */ -Identity.prototype.readAndBindWallet = function(walletId, cb) { - var self = this; - self.retrieveWalletFromStorage(walletId, {}, function(error, wallet) { - if (!error) { - self.addWallet(wallet); - self.bindWallet(wallet); - } - return cb(error); - }); -}; - - -Identity.prototype.emitAndKeepAlive = function(args) { - var args = Array.prototype.slice.call(arguments); - log.debug('Ident Emitting:', args); - //this.keepAlive(); // TODO - this.emit.apply(this, arguments); -}; - - -/** - * @desc open profile's wallets. Call it AFTER setting - * the proper even listeners. no callback. - * - */ -Identity.prototype.openWallets = function() { - var self = this; - - - if (_.isEmpty(self.walletIds)) { - self.emitAndKeepAlive('noWallets') - return; - } - - // First read the lastFocused wallet - self.walletIds.sort(function(a, b) { - var va = self.focusedTimestamps[a] || 0; - var vb = self.focusedTimestamps[b] || 0; - - return va < vb ? 1 : (va === vb ? 0 : -1); - }); - - // opens the wallets, in the order they were last accessed. Emits open events (newWallet) - async.eachSeries(self.walletIds, function(walletId, a_cb) { - self.readAndBindWallet(walletId, a_cb); - }); -}; - -/** - * @param {string} walletId - * @param {} opts - * opts.importWallet - * @param {Function} cb - */ -Identity.prototype.retrieveWalletFromStorage = function(walletId, opts, cb) { - var self = this; - - var importFunction = opts.importWallet || Wallet.fromUntrustedObj; - - this.storage.getItem(Wallet.getStorageKey(walletId), function(error, walletData) { - if (error) { - return cb(error); - } - try { - log.info('## OPENING Wallet:', walletId); - if (_.isString(walletData)) { - walletData = JSON.parse(walletData); - } - var readOpts = { - networkOpts: self.networkOpts, - blockchainOpts: self.blockchainOpts, - skipFields: [] - }; - } catch (e) { - log.debug("ERROR: ", e.message); - if (e && e.message && e.message.indexOf('MISSOPTS') !== -1) { - return cb(new Error('WERROR: Could not read: ' + walletId + ': ' + e.message)); - } else { - return cb(e); - } - } - return cb(null, importFunction(walletData, readOpts)); - }); -}; - -/** - * @param {Wallet} wallet - * @param {Function} cb - */ -Identity.prototype.storeWallet = function(wallet, cb) { - var self = this; - - preconditions.checkArgument(wallet && _.isObject(wallet)); - - wallet.setVersion(this.version); - var val = wallet.toObj(); - var key = wallet.getStorageKey(); - log.debug('Storing wallet:' + wallet.getName()); - - this.storage.setItem(key, val, function(err) { - if (err) { - log.error('Wallet:' + wallet.getName() + ' could not be stored:', err); - log.error('Wallet:' + wallet.getName() + ' Size:', JSON.stringify(wallet.sizes())); - - if (err.match('OVERQUOTA')) { - self.emitAndKeepAlive('walletStorageError', wallet.getId(), 'Storage limits on remote server exceeded'); - } else { - self.emitAndKeepAlive('walletStorageError', wallet.getId(), err); - } - } - if (cb) - return cb(err); - }); -}; - - -/** - * @param {Identity} identity - * @param {Wallet} wallet - * @param {Function} cb - */ -Identity.storeWalletDebounced = _.debounce(function(identity, wallet, cb) { - identity.storeWallet(wallet, cb); -}, 3000); - - - -Identity.prototype.toObj = function() { - return _.pick(this, 'walletIds', 'version', 'fullName', 'password', 'email', 'backupNeeded', 'focusedTimestamps'); -}; - -Identity.prototype.exportEncryptedWithWalletInfo = function(opts) { - var crypto = opts.cryptoUtil || cryptoUtil; - - return crypto.encrypt(this.password, this.exportWithWalletInfo(opts)); -}; - -Identity.prototype.setBackupNeeded = function(backupNeeded) { - var self = this; - - self.backupNeeded = !!backupNeeded; - - self.verifyChecksum(function(err, match) { - if (err) { - log.error(err); - return; - } - if (!match) { - log.error('The profile is out of sync. Please re-login to get the latest changes.'); - return; - } - - self.store({ - noWallets: true - }, function() {}); - }); -} - -Identity.prototype.exportWithWalletInfo = function(opts) { - return _.extend({ - wallets: _.map(this.getWallets(), function(wallet) { - return wallet.toObj(); - }) - }, - _.pick(this, 'version', 'fullName', 'password', 'email', 'backupNeeded') - ); -}; - -/** - * @param {Object} opts - * @param {Function} cb - */ -Identity.prototype.store = function(opts, cb) { - var self = this; - opts = opts || {}; - - var storeFunction = opts.failIfExists ? self.storage.createItem : self.storage.setItem; - - storeFunction.call(self.storage, this.getId(), this.toObj(), function(err) { - if (err) return cb(err); - - if (opts.noWallets) return cb(); - - async.each(self.getWallets(), function(wallet, in_cb) { - self.storeWallet(wallet, in_cb); - }, cb); - }); -}; - -/** - * @param {Object} opts - * @param {Function} cb - */ -Identity.prototype.remove = function(opts, cb) { - log.debug('Deleting profile'); - - var self = this; - opts = opts || {}; - - async.each(self.getWallets(), function(w, cb) { - w.close(); - self.storage.removeItem(Wallet.getStorageKey(w.getId()), function(err) { - if (err) return cb(err); - cb(); - }); - }, function(err) { - if (err) return cb(err); - - self.storage.removeItem(self.getId(), function(err) { - if (err) return cb(err); - - self.storage.clear(function(err) { - if (err) return cb(err); - - self.emitAndKeepAlive('closed'); - return cb(); - }); - }); - }); -}; - -Identity.prototype._cleanUp = function() { - var self = this; - - _.each(this.getWallets(), function(w) { - w.close(); - delete self.wallets[w.getId()]; - }); -}; - -/** - * @desc Closes the wallet and disconnects all services - */ -Identity.prototype.close = function(cb) { - var self = this; - - function doClose() { - self._cleanUp(); - self.emitAndKeepAlive('closed'); - if (cb) return cb(); - }; - - self.verifyChecksum(function(err, match) { - if (!err && match) { - self.store({ - noWallets: true, - }, function(err) { - return doClose(); - }); - } else { - return doClose(); - } - }); -}; - - -Identity.prototype.importWalletFromObj = function(obj, opts, cb) { - var self = this; - preconditions.checkArgument(cb); - - self.verifyChecksum(function(err, match) { - if (err) return cb(err); - if (!match) return cb('The profile is out of sync. Please re-login to get the latest changes.'); - - var importFunction = opts.importWallet || Wallet.fromUntrustedObj; - - var readOpts = { - networkOpts: self.networkOpts, - blockchainOpts: self.blockchainOpts, - skipFields: opts.skipFields, - }; - - var w = importFunction(obj, readOpts); - if (!w) return cb(new Error('Could not decrypt')); - log.debug('Wallet decrypted:' + w.getName()); - - self._checkVersion(w.version); - log.debug('Updating Indexes for wallet:' + w.getName()); - w.updateIndexes(function(err) { - log.debug('Adding wallet to profile:' + w.getName()); - self.storeWallet(w, function(err) { - if (err) return cb(err); - - self.addWallet(w); - self.updateFocusedTimestamp(w.getId()); - self.bindWallet(w); - - self.backupNeeded = true; - self.store({ - noWallets: true, - }, function(err) { - return cb(err); - }); - }); - }); - }); -}; - - -Identity.prototype.importMultipleWalletsFromObj = function(objs, opts) { - var self = this; - opts = opts || {}; - - async.eachSeries(objs, function(walletData, cb) { - if (!walletData) - return cb(); - self.importWalletFromObj(walletData, opts, cb); - }); -}; - - -/** - * @param {Wallet} wallet - * @param {Function} cb - */ -Identity.prototype.closeWallet = function(wallet, cb) { - preconditions.checkState(wallet, 'Wallet not found'); - - var self = this; - wallet.close(function(err) { - return cb(err); - }); -}; - -Identity.importFromEncryptedFullJson = function(ejson, password, opts, cb) { - var crypto = opts.cryptoUtil || cryptoUtil; - - var str = crypto.decrypt(password, ejson); - if (!str) { - // 0.7.3 broken KDF - log.debug('Trying legacy encryption...'); - var passphrase = crypto.kdf(password, 'mjuBtGybi/4=', 100); - str = crypto.decrypt(passphrase, ejson); - } - - if (!str) - return cb('BADSTR'); - - return Identity.importFromFullJson(str, password, opts, cb); -}; - -Identity.importFromFullJson = function(str, password, opts, cb) { - preconditions.checkArgument(str); - var json; - try { - json = JSON.parse(str); - } catch (e) { - return cb('BADSTR: Unable to retrieve json from string', str); - } - - var email = json.email; - - opts.email = email; - opts.password = password; - - if (!email) - return cb('BADSTR'); - - var iden = new Identity(opts); - - opts.failIfExists = true; - - json.wallets = json.wallets || {}; - - iden.store(opts, function(err) { - if (err) return cb(err); //profile already exists - - return cb(null, iden, json.wallets); - }); -}; - - -/** - * @desc binds a wallet's events and emits 'newWallet' - * @param {string} walletId Wallet id to be binded - * @emits newWallet (walletId) - */ -Identity.prototype.bindWallet = function(w) { - preconditions.checkArgument(w && this.getWalletById(w.getId())); - - var self = this; - log.debug('Binding wallet:' + w.getName()); - - w.on('txProposalsUpdated', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('paymentAck', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('newAddresses', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('settingsUpdated', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('txProposalEvent', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('addressBookUpdated', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('publicKeyRingUpdated', function() { - Identity.storeWalletDebounced(self, w); - }); - w.on('ready', function() { - Identity.storeWalletDebounced(self, w); - }); - - this.emitAndKeepAlive('newWallet', w.getId()); -}; - -/** - * @desc This method prepares options for a new Wallet - * - * @param {Object} opts - * @param {string} opts.id - * @param {PrivateKey=} opts.privateKey - * @param {string=} opts.privateKeyHex - * @param {number} opts.requiredCopayers - * @param {number} opts.totalCopayers - * @param {PublicKeyRing=} opts.publicKeyRing - * @param {string} opts.nickname - * @param {string} opts.password - * @param {boolean} opts.spendUnconfirmed this.walletDefaults.spendUnconfirmed - * @param {number} opts.reconnectDelay time in milliseconds - * @param {number=} opts.version - * @param {callback} opts.version - * @return {Wallet} - */ -Identity.prototype.createWallet = function(opts, cb) { - preconditions.checkArgument(cb); - - var self = this; - - self.verifyChecksum(function(err, match) { - if (err) return cb(err); - if (!match) return cb('The profile is out of sync. Please re-login to get the latest changes.'); - - 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')); - - var privOpts = { - networkName: opts.networkName, - }; - - if (opts.privateKeyHex && opts.privateKeyHex.length > 1) { - privOpts.extendedPrivateKeyString = opts.privateKeyHex; - } - - opts.privateKey = opts.privateKey || new PrivateKey(privOpts); - - var requiredCopayers = opts.requiredCopayers || self.walletDefaults.requiredCopayers; - var totalCopayers = opts.totalCopayers || self.walletDefaults.totalCopayers; - opts.lockTimeoutMin = self.walletDefaults.idleDurationMin; - - opts.publicKeyRing = opts.publicKeyRing || new PublicKeyRing({ - networkName: opts.networkName, - requiredCopayers: requiredCopayers, - totalCopayers: totalCopayers, - }); - opts.publicKeyRing.addCopayer( - opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(), - opts.nickname || self.getName() - ); - log.debug('\t### PublicKeyRing Initialized'); - - opts.txProposals = opts.txProposals || new TxProposals({ - networkName: opts.networkName, - }); - var walletClass = opts.walletClass || Wallet; - - log.debug('\t### TxProposals Initialized'); - - - opts.networkOpts = self.networkOpts; - opts.blockchainOpts = self.blockchainOpts; - - opts.spendUnconfirmed = opts.spendUnconfirmed || self.walletDefaults.spendUnconfirmed; - opts.reconnectDelay = opts.reconnectDelay || self.walletDefaults.reconnectDelay; - opts.requiredCopayers = requiredCopayers; - opts.totalCopayers = totalCopayers; - opts.version = opts.version || self.version; - - var w = new walletClass(opts); - - if (self.getWalletById(w.getId())) { - return cb('walletAlreadyExists'); - } - - self.storeWallet(w, function(err) { - if (err) return cb(err); - - self.addWallet(w); - self.updateFocusedTimestamp(w.getId()); - self.bindWallet(w); - - self.backupNeeded = true; - self.store({ - noWallets: true, - }, function(err) { - return cb(err, w); - }); - }); - }); -}; - -/** - * @desc Checks if a version is compatible with the current version - * @param {string} inVersion - a version, with major, minor, and revision, period-separated (x.y.z) - * @throws {Error} if there's a major version difference - */ -Identity.prototype._checkVersion = function(inVersion) { - if (inVersion) { - var thisV = this.version.split('.'); - var thisV0 = parseInt(thisV[0]); - var inV = inVersion.split('.'); - var inV0 = parseInt(inV[0]); - } - - //We only check for major version differences - if (thisV0 < inV0) { - throw new Error('Major difference in software versions' + - '. Received:' + inVersion + - '. Current version:' + this.version + - '. Aborting.'); - } -}; - - -/** - * @desc Pass through to {@link Wallet#secret} - */ -Identity.prototype.decodeSecret = function(secret) { - try { - return Wallet.decodeSecret(secret); - } catch (e) { - return false; - } -}; - -/** - * getLastFocusedWalletId - * - * @return {string} walletId - */ -Identity.prototype.getLastFocusedWalletId = function() { - if (this.walletIds.length == 0) return undefined; - - var max = _.max(this.focusedTimestamps); - - if (!max) - return this.walletIds[0]; - - return _.findKey(this.focusedTimestamps, function(ts) { - return ts == max; - }) || this.walletIds[0]; -}; - -Identity.prototype.updateFocusedTimestamp = function(wid) { - preconditions.checkArgument(wid && this.getWalletById(wid)); - this.focusedTimestamps[wid] = Date.now(); -}; - -/** - * @callback walletCreationCallback - * @param {?} err - an error, if any, that happened during the wallet creation - * @param {Wallet=} wallet - the wallet created - */ - -/** - * @desc Start the network functionality. - * - * Start up the Network instance and try to join a wallet defined by the - * parameter secret using the parameter nickname. Encode - * information locally using passphrase. privateHex is the - * private extended master key. cb has two params: error and wallet. - * - * @param {object} opts - * @param {string} opts.secret - the wallet secret - * @param {string} opts.nickname - a nickname for the current user - * @param {string} opts.privateHex - the private extended master key - * @param {walletCreationCallback} cb - a callback - */ -Identity.prototype.joinWallet = function(opts, cb) { - preconditions.checkArgument(opts); - preconditions.checkArgument(opts.secret); - preconditions.checkArgument(cb); - var self = this; - var decodedSecret = this.decodeSecret(opts.secret); - if (!decodedSecret || !decodedSecret.networkName || !decodedSecret.pubKey) { - return cb('badSecret'); - } - - var privOpts = { - networkName: decodedSecret.networkName, - }; - - if (opts.privateHex && opts.privateHex.length > 1) { - privOpts.extendedPrivateKeyString = opts.privateHex; - } - - //Create our PrivateK - var privateKey = new PrivateKey(privOpts); - log.debug('\t### PrivateKey Initialized'); - var joinOpts = { - copayerId: privateKey.getId(), - privkey: privateKey.getIdPriv(), - key: privateKey.getIdKey(), - secretNumber: decodedSecret.secretNumber, - }; - - - var joinNetwork = opts.Async || new Async(this.networkOpts[decodedSecret.networkName]); - - // This is a hack to reconize if the connection was rejected or the peer wasn't there. - var connectedOnce = false; - joinNetwork.on('connected', function(sender, data) { - connectedOnce = true; - }); - - joinNetwork.on('connect_error', function() { - return cb('connectionError'); - }); - - joinNetwork.on('serverError', function() { - return cb('joinError'); - }); - - joinNetwork.start(joinOpts, function() { - - joinNetwork.greet(decodedSecret.pubKey, joinOpts.secretNumber); - joinNetwork.on('data', function(sender, data) { - if (data.type === 'walletId' && data.opts) { - if (!data.networkName || data.networkName !== decodedSecret.networkName) { - return cb('badNetwork'); - } - data.opts.networkName = data.networkName; - - var walletOpts = _.clone(data.opts); - walletOpts.id = data.walletId; - walletOpts.network = joinNetwork; - - walletOpts.privateKey = privateKey; - walletOpts.nickname = opts.nickname || self.getName(); - - if (opts.password) - walletOpts.password = opts.password; - - self.createWallet(walletOpts, function(err, w) { - if (w) { - w.sendWalletReady(decodedSecret.pubKey); - } else { - if (!err) { - err = 'walletFull'; - } - } - return cb(err, w); - }); - } - }); - }); -}; - - -module.exports = Identity; diff --git a/js/models/Insight.js b/js/models/Insight.js deleted file mode 100644 index b642a1c21..000000000 --- a/js/models/Insight.js +++ /dev/null @@ -1,346 +0,0 @@ -'use strict'; - -var util = require('util'); -var async = require('async'); -var request = require('request'); -var io = require('socket.io-client'); -var _ = require('lodash'); -var EventEmitter = require('events').EventEmitter; -var preconditions = require('preconditions').singleton(); - -var bitcore = require('bitcore'); - -var log = require('../util/log.js'); - - -/* - This class lets interface with the blockchain, making general queries and - subscribing to transactions on addresses and blocks. - - Opts: - - url - - reconnection (optional) - - reconnectionDelay (optional) - - Events: - - tx: activity on subscribed address. - - block: a new block that includes a subscribed address. - - connect: the connection with the blockchain is ready. - - disconnect: the connection with the blochckain is unavailable. -*/ - -var Insight = function(opts) { - preconditions.checkArgument(opts) - .shouldBeObject(opts) - .checkArgument(opts.url) - - this.status = this.STATUS.DISCONNECTED; - this.subscribed = {}; - this.listeningBlocks = false; - - this.url = opts.url; - this.opts = { - 'reconnection': opts.reconnection || true, - 'reconnectionDelay': opts.reconnectionDelay || 1000, - 'secure': opts.url.indexOf('https') === 0 - }; - - - if (opts.transports) { - this.opts['transports'] = opts.transports; - } - - this.socket = this.getSocket(); -} - -Insight.setCompleteUrl = function(uri) { - - if (!uri) return uri; - - var re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; - - var parts = [ - 'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor' - ]; - - function parseuri(str) { - var m = re.exec(str || ''), - uri = {}, - i = 14; - - while (i--) { - uri[parts[i]] = m[i] || ''; - } - - return uri; - }; - - var opts_host; - var opts_secure; - var opts_port; - var opts_protocol; - if (uri) { - uri = parseuri(uri); - opts_host = uri.host; - opts_protocol = uri.protocol; - opts_secure = uri.protocol == 'https' || uri.protocol == 'wss'; - opts_port = uri.port; - } - - var this_secure = null != opts_secure ? opts_secure : - ('https:' == location.protocol); - - var opts_hostname; - if (opts_host) { - var pieces = opts_host.split(':'); - opts_hostname = pieces.shift(); - if (pieces.length) opts_port = pieces.pop(); - } - - var this_port = opts_port || - (this_secure ? 443 : 80); - - var newUri = opts_protocol + '://' + opts_host + ':' + this_port; - - return newUri; -} - -util.inherits(Insight, EventEmitter); - -Insight.prototype.STATUS = { - CONNECTED: 'connected', - DISCONNECTED: 'disconnected', - DESTROYED: 'destroyed' -} - -/** @private */ -Insight.prototype.subscribeToBlocks = function() { - var socket = this.getSocket(); - if (this.listeningBlocks || !socket.connected) return; - - var self = this; - socket.on('block', function(blockHash) { - self.emit('block', blockHash); - }); - this.listeningBlocks = true; -} - -/** @private */ -Insight.prototype._getSocketIO = function(url, opts) { - log.debug('Insight: Connecting to socket:', this.url); - return io(this.url, this.opts); -}; - - -Insight.prototype._setMainHandlers = function(url, opts) { - // Emmit connection events - var self = this; - this.socket.on('connect', function() { - self.status = self.STATUS.CONNECTED; - self.subscribeToBlocks(); - self.emit('connect', 0); - }); - - this.socket.on('connect_error', function() { - if (self.status != self.STATUS.CONNECTED) return; - self.status = self.STATUS.DISCONNECTED; - self.emit('disconnect'); - }); - - this.socket.on('connect_timeout', function() { - if (self.status != self.STATUS.CONNECTED) return; - self.status = self.STATUS.DISCONNECTED; - self.emit('disconnect'); - }); - - this.socket.on('reconnect', function(attempt) { - if (self.status != self.STATUS.DISCONNECTED) return; - self.emit('reconnect', attempt); - self.reSubscribe(); - self.status = self.STATUS.CONNECTED; - }); -}; - - -/** @private */ -Insight.prototype.getSocket = function() { - - if (!this.socket) { - this.socket = this._getSocketIO(this.url, this.opts); - this._setMainHandlers(); - } - return this.socket; -} - -/** @private */ -Insight.prototype.request = function(path, cb) { - preconditions.checkArgument(path).shouldBeFunction(cb); - request(this.url + path, cb); -} - -/** @private */ -Insight.prototype.requestPost = function(path, data, cb) { - preconditions.checkArgument(path).checkArgument(data).shouldBeFunction(cb); - request({ - method: "POST", - url: this.url + path, - json: data - }, cb); -} - -Insight.prototype.destroy = function() { - var socket = this.getSocket(); - this.socket.disconnect(); - this.socket.removeAllListeners(); - this.socket = null; - this.subscribed = {}; - this.status = this.STATUS.DESTROYED; - this.removeAllListeners(); -}; - -Insight.prototype.subscribe = function(addresses) { - addresses = Array.isArray(addresses) ? addresses : [addresses]; - var self = this; - - function handlerFor(self, address) { - return function(txid) { - // verify the address is still subscribed - if (!self.subscribed[address]) return; - - self.emit('tx', { - address: address, - txid: txid - }); - } - } - - var s = self.getSocket(); - addresses.forEach(function(address) { - preconditions.checkArgument(new bitcore.Address(address).isValid()); - - // skip already subscibed - if (!self.subscribed[address]) { - var handler = handlerFor(self, address); - self.subscribed[address] = handler; -// log.debug('Subscribe to: ', address); - s.emit('subscribe', address); - s.on(address, handler); - } - }); -}; - -Insight.prototype.getSubscriptions = function(addresses) { - return this.subscribed; -} - - -Insight.prototype.reSubscribe = function() { - log.debug('insight reSubscribe'); - var allAddresses = Object.keys(this.subscribed); - this.subscribed = {}; - var s = this.socket; - if (s) { - s.removeAllListeners(); - this._setMainHandlers(); - this.subscribe(allAddresses); - this.subscribeToBlocks(); - } -}; - - -Insight.prototype.broadcast = function(rawtx, cb) { - preconditions.checkArgument(rawtx); - preconditions.shouldBeFunction(cb); - - this.requestPost('/api/tx/send', { - rawtx: rawtx - }, function(err, res, body) { - if (err || res.statusCode != 200) return cb(err || body); - return cb(null, body ? body.txid : null); - }); -}; - -Insight.prototype.getTransaction = function(txid, cb) { - preconditions.shouldBeFunction(cb); - this.request('/api/tx/' + txid, function(err, res, body) { - if (err || res.statusCode != 200 || !body) return cb(err || res); - cb(null, JSON.parse(body)); - }); -}; - -Insight.prototype.getTransactions = function(addresses, from, to, cb) { - preconditions.shouldBeArray(addresses); - preconditions.shouldBeFunction(cb); - - var qs = ''; - if (_.isNumber(from)) { - qs += '?from=' + from; - if (_.isNumber(to)) { - qs += '&to=' + to; - } - } - - this.requestPost('/api/addrs/txs' + qs, { - addrs: addresses.join(',') - }, function(err, res, txs) { - if (err || res.statusCode != 200) return cb(err || res); - cb(null, txs); - }); -}; - -Insight.prototype.getUnspent = function(addresses, cb) { - preconditions.shouldBeArray(addresses); - preconditions.shouldBeFunction(cb); - - this.requestPost('/api/addrs/utxo', { - addrs: addresses.join(',') - }, function(err, res, unspentRaw) { - if (err || res.statusCode != 200) return cb(err || res); - - // This filter out possible broken unspent, as reported on - // https://github.com/bitpay/copay/issues/1585 - // and later gitter conversation. - - var unspent = _.filter(unspentRaw, 'scriptPubKey'); - cb(null, unspent); - }); -}; - -Insight.prototype.getActivity = function(addresses, cb) { - preconditions.shouldBeArray(addresses); - - this.getTransactions(addresses, null, null, function then(err, txs) { - if (err) return cb(err); - - var flatArray = function(xss) { - return xss.reduce(function(r, xs) { - return r.concat(xs); - }, []); - }; - var getInputs = function(t) { - return t.vin.map(function(vin) { - return vin.addr - }); - }; - var getOutputs = function(t) { - return flatArray( - t.vout.map(function(vout) { - return vout.scriptPubKey.addresses; - }) - ); - }; - - var activityMap = new Array(addresses.length); - var activeAddress = flatArray(txs.map(function(t) { - return getInputs(t).concat(getOutputs(t)); - })); - activeAddress.forEach(function(addr) { - var index = addresses.indexOf(addr); - if (index != -1) activityMap[index] = true; - }); - - cb(null, activityMap); - }); -}; - -module.exports = Insight; diff --git a/js/models/PluginManager.js b/js/models/PluginManager.js deleted file mode 100644 index c4082ecbd..000000000 --- a/js/models/PluginManager.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; -var preconditions = require('preconditions').singleton(); -var log = require('../util/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; - if(config.pluginsPath){ - pluginClass = require(config.pluginsPath + pluginName); - } else { - pluginClass = require('../js/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['DB'] = 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, 'Unknown plugin 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; diff --git a/js/models/PrivateKey.js b/js/models/PrivateKey.js deleted file mode 100644 index 9ae522814..000000000 --- a/js/models/PrivateKey.js +++ /dev/null @@ -1,264 +0,0 @@ -'use strict'; - -// 62.9% typed (by google's closure-compiler account) - -var bitcore = require('bitcore'); -var HK = bitcore.HierarchicalKey; -var WalletKey = bitcore.WalletKey; -var networks = bitcore.networks; -var util = bitcore.util; -var _ = require('lodash'); -var preconditions = require('preconditions').instance(); -var HDPath = require('./HDPath'); - -/** - * @desc - * Wrapper for bitcore.HierarchicalKey to be used inside of Copay. - * - * @param {Object} opts - * @param {string} opts.networkName if set to 'testnet', use the test3 bitcoin - * network constants (livenet otherwise) - * @param {string} opts.extendedPrivateKeyString if set, use this private key - * string, othewise create a new - * private key - * @constructor - */ -function PrivateKey(opts) { - opts = opts || {}; - this.network = opts.networkName === 'testnet' ? networks.testnet : networks.livenet; - var init = opts.extendedPrivateKeyString || this.network.name; - this.bip = new HK(init); - this.privateKeyCache = {}; - this.publicHex = this.deriveBIP45Branch().eckey.public.toString('hex'); -}; - -/** - * @desc Retrieve this derivated private key's public key in hexa format - * - * The value returned is calculated using the path from PrivateKey's - * HDParams.IdFullBranch. This key is used to identify the copayer - * (signing messages mostly). - * - * @returns {string} the public key in a hexadecimal string - */ -PrivateKey.prototype.getId = function() { - if (!this.id) { - this.cacheId(); - } - return this.id; -}; - -/** - * @desc Retrieve this private key's private key in hex format - * - * The value returned is calculated using the path from PrivateKey's - * HDParams.IdFullBranch. This key is used to identify the copayer - * (signing messages mostly). - * - * @returns {string} the private key in a hexadecimal string - */ -PrivateKey.prototype.getIdPriv = function() { - if (!this.idpriv) { - this.cacheId(); - } - return this.idpriv; -}; - -/** - * @desc Retrieve this private key's private key - * - * The value returned is calculated using the path from PrivateKey's - * HDParams.IdFullBranch. This key is used to identify the copayer - * (signing messages mostly). - * - * @returns {bitcore.PrivateKey} the private key - */ -PrivateKey.prototype.getIdKey = function() { - if (!this.idkey) { - this.cacheId(); - } - return this.idkey; -}; - -/** - * @desc Caches the result of deriving IdFullBranch - * - * @private - */ -PrivateKey.prototype.cacheId = function() { - var path = HDPath.IdFullBranch; - var idhk = this.bip.derive(path); - this.idkey = idhk.eckey; - this.id = idhk.eckey.public.toString('hex'); - this.idpriv = idhk.eckey.private.toString('hex'); -}; - -/** - * @desc Derive the master branch for Copay. - */ -PrivateKey.prototype.deriveBIP45Branch = function() { - if (!this.bip45Branch) { - this.bip45Branch = this.bip.derive(HDPath.BIP45_PUBLIC_PREFIX); - } - return this.bip45Branch; -}; - -/** - * @desc Returns an object with information needed to rebuild a PrivateKey - * (as most of its properties are derived from the extended private key). - * - * @TODO: Figure out if this is the correct pattern - * This is a static method and is probably used for serialization. - * - * @static - * @param {Object} data - * @param {*} data.networkName - a name for a bitcoin network - * @param {*} data.extendedPrivateKeyString - a bip32 extended private key - * @returns {Object} an object with two properties: networkName and - * extendedPrivateKeyString, taken from the data - * parameter. - */ -PrivateKey.trim = function(data) { - var opts = {}; - ['networkName', 'extendedPrivateKeyString'].forEach(function(k){ - opts[k] = data[k]; - }); - return opts -}; - -/** - * @desc Generate a private Key from a serialized object - * - * @TODO: This method uses PrivateKey.trim but it's actually not needed... - * - * @param {Object} data - * @param {*} data.networkName - a name for a bitcoin network - * @param {*} data.extendedPrivateKeyString - a bip32 extended private key - * @returns {PrivateKey} - */ -PrivateKey.fromObj = function(obj) { - return new PrivateKey(PrivateKey.trim(obj)); -}; - -/** - * @desc Serialize a private key, keeping only the data necessary to rebuild it - * - * @returns {Object} - */ -PrivateKey.prototype.toObj = function() { - return { - extendedPrivateKeyString: this.getExtendedPrivateKeyString(), - networkName: this.network.name - }; -}; - -/** - * @desc Retrieve a BIP32 extended public key as generated by bitcore - * - * @returns {string} - */ -PrivateKey.prototype.getExtendedPublicKeyString = function() { - return this.bip.extendedPublicKeyString(); -}; - -/** - * @desc Retrieve a BIP32 extended private key as generated by bitcore - * - * @returns {string} - */ -PrivateKey.prototype.getExtendedPrivateKeyString = function() { - return this.bip.extendedPrivateKeyString(); -}; - -/** - * @desc - * Retrieve a HierarchicalKey derived from the given path as generated by - * bitcore - * @param {string} path - a string for derivation (something like "m/234'/1/2") - * @returns {bitcore.HierarchicalKey} - */ -PrivateKey.prototype._getHK = function(path) { - if (_.isUndefined(path)) { - return this.bip; - } - var ret = this.bip.derive(path); - return ret; -}; - -/** - * @desc - * Retrieve an array of WalletKey derived from given paths. {@see PrivateKey#getForPath} - * - * @param {string[]} paths - the paths to derive - * @returns {bitcore.WalletKey[]} - the derived keys - */ -PrivateKey.prototype.getForPaths = function(paths) { - return paths.map(this.getForPath.bind(this)); -}; - -/** - * @desc - * Retrieve a WalletKey derived from a path. - * - * @param {string} paths - the path to derive - * @returns {bitcore.WalletKey} - the derived key - */ -PrivateKey.prototype.getForPath = function(path) { - var pk = this.privateKeyCache[path]; - if (!pk) { - var derivedHK = this._getHK(path); - pk = this.privateKeyCache[path] = derivedHK.eckey.private.toString('hex'); - } - var wk = new WalletKey({ - network: this.network - }); - wk.fromObj({ - priv: pk - }); - return wk; -}; - -/** - * @desc - * Retrieve a Branch for Copay using the given path - * - * @TODO: Investigate when is this called and if this is really needed - * - * @param {number} index - the index of the key to generate - * @param {boolean} isChange - whether this is a change adderess or a receive - * @param {number} cosigner - the cosigner index - * @return {bitcore.HierarchicalKey} - */ -PrivateKey.prototype.get = function(index, isChange, cosigner) { - - // TODO: Add parameter validation? - - var path = HDPath.FullBranch(index, isChange, cosigner); - return this.getForPath(path); -}; - -/** - * @desc - * Retrieve multiple branches for Copay up to the received indexes - * - * @TODO: Investigate when is this called and if this is really needed - * - * @param {number} receiveIndex - the number of receive addresses to generate - * @param {number} changeIndex - the number of change addresses to generate - * @param {number} cosigner - the cosigner index - * @return {bitcore.HierarchicalKey} - */ -PrivateKey.prototype.getAll = function(receiveIndex, changeIndex, cosigner) { - preconditions.checkArgument(!_.isUndefined(receiveIndex) && !_.isUndefined(changeIndex)); - - var ret = []; - for (var i = 0; i < receiveIndex; i++) { - ret.push(this.get(i, false, cosigner)); - } - for (var i = 0; i < changeIndex; i++) { - ret.push(this.get(i, true, cosigner)); - } - return ret; -}; - -module.exports = PrivateKey; diff --git a/js/models/PublicKeyRing.js b/js/models/PublicKeyRing.js deleted file mode 100644 index ca48d08c8..000000000 --- a/js/models/PublicKeyRing.js +++ /dev/null @@ -1,803 +0,0 @@ -'use strict'; - -var preconditions = require('preconditions').instance(); -var _ = require('lodash'); -var log = require('../util/log'); -var bitcore = require('bitcore'); -var HK = bitcore.HierarchicalKey; -var Address = bitcore.Address; -var Script = bitcore.Script; -var PrivateKey = require('./PrivateKey'); -var HDPath = require('./HDPath'); -var HDParams = require('./HDParams'); - -/** - * @desc Represents a public key ring, the set of all public keys and the used indexes - * - * @constructor - * @param {Object} opts - * @param {string} opts.walletId - * @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.totalCopayers - defaults to 5 - * @param {Object[]} [opts.indexes] - an array to be deserialized using {@link HDParams#fromList} - * (defaults to all indexes in zero) - * @param {Object=} opts.nicknameFor - nicknames for other copayers - */ -function PublicKeyRing(opts) { - opts = opts || {}; - - this.walletId = opts.walletId; - - this.network = opts.networkName === 'livenet' ? - bitcore.networks.livenet : bitcore.networks.testnet; - - this.requiredCopayers = opts.requiredCopayers || 3; - this.totalCopayers = opts.totalCopayers || 5; - - this.copayersHK = []; - - this.indexes = opts.indexes ? HDParams.fromList(opts.indexes) : HDParams.init(this.totalCopayers); - - this.publicKeysCache = {}; - this.nicknameFor = opts.nicknameFor || {}; - this.copayerIds = []; - - this.resetCache(); -}; - -PublicKeyRing.prototype.resetCache = function() { - this.cache = {}; - this.cache.addressToPath = {}; - this.cache.pathToAddress = {}; - - // Non persistent cache - this._isChange = {}; -}; - - -/** - * @desc Returns an object with only the keys needed to rebuild a PublicKeyRing - * - * @TODO: Figure out if this is the correct pattern - * This is a static method and is probably used for serialization. - * - * @static - * @param {Object} data - * @param {string} data.walletId - a string to identify a wallet - * @param {string} data.networkName - the name of the bitcoin network - * @param {number} data.requiredCopayers - the number of required copayers - * @param {number} data.totalCopayers - the number of copayers in the ring - * @param {Object[]} data.indexes - an array of objects that can be turned into - * an array of HDParams - * @param {Object} data.nicknameFor - a registry of nicknames for other copayers - * @param {string[]} data.copayersExtPubKeys - the extended public keys of copayers - * @returns {Object} a trimmed down version of PublicKeyRing that can be used - * as a parameter - */ -PublicKeyRing.trim = function(data) { - preconditions.checkArgument(data); - var opts = {}; - ['walletId', 'networkName', 'requiredCopayers', 'totalCopayers', - 'indexes', 'nicknameFor', 'copayersExtPubKeys' - ].forEach(function(k) { - opts[k] = data[k]; - }); - return opts; -}; - -/** - * @desc Deserializes a PublicKeyRing from a plain object - * - * If the data parameter is an instance of PublicKeyRing already, - * it will fail, throwing an assertion error. - * - * @static - * @param {object} data - a serialized version of PublicKeyRing {@see PublicKeyRing#trim} - * @return {PublicKeyRing} - the deserialized object - */ -PublicKeyRing.fromObj = function(opts) { - preconditions.checkArgument(!(opts instanceof PublicKeyRing), 'bad opts format: Did you use .toObj()?'); - - // Support old indexes schema - if (!Array.isArray(opts.indexes)) { - opts.indexes = HDParams.update(opts.indexes, opts.totalCopayers); - } - - var pkr = new PublicKeyRing(opts); - - for (var k in opts.copayersExtPubKeys) { - pkr.addCopayer(opts.copayersExtPubKeys[k]); - } - - if (opts.cache && opts.cache.addressToPath) { - log.debug('PublicKeyRing: Using address cache'); - pkr.cache.addressToPath = opts.cache.addressToPath; - pkr.rebuildCache(); - } - - return pkr; -}; - - -PublicKeyRing.prototype.rebuildCache = function() { - if (!this.cache.addressToPath) - return; - - var self = this; - _.each(this.cache.addressToPath, function(path, address) { - self.cache.pathToAddress[path] = address; - }); -}; - -PublicKeyRing.fromUntrustedObj = function(opts) { - return PublicKeyRing.fromObj(PublicKeyRing.trim(opts)); -}; - -/** - * @desc Serialize this object to a plain object with all the data needed to - * rebuild it - * - * @return {Object} a serialized version of a PublicKeyRing - */ -PublicKeyRing.prototype.toObj = function() { - return { - walletId: this.walletId, - networkName: this.network.name, - requiredCopayers: this.requiredCopayers, - totalCopayers: this.totalCopayers, - indexes: HDParams.serialize(this.indexes), - - copayersExtPubKeys: this.copayersHK.map(function(b) { - return b.extendedPublicKeyString(); - }), - nicknameFor: this.nicknameFor, - - // We only store addressToPath and derive the reset from it - cache: { - addressToPath: this.cache.addressToPath - }, - }; -}; - - -PublicKeyRing.prototype.toTrimmedObj = function() { - return PublicKeyRing.trim(this.toObj()); -} - - -/** - * @desc - * Retrieve a copayer's public key as a hexadecimal encoded string - * - * @param {number} copayerId - the copayer id - * @returns {string} the extended public key of the i-th copayer - */ -PublicKeyRing.prototype.getCopayerId = function(copayerId) { - preconditions.checkArgument(!_.isUndefined(copayerId)) - preconditions.checkArgument(_.isNumber(copayerId)); - - return this.copayerIds[copayerId]; -}; - -/** - * @desc - * Get the amount of registered copayers in this PubKeyRing - * - * @returns {number} amount of copayers present - */ -PublicKeyRing.prototype.registeredCopayers = function() { - return this.copayersHK.length; -}; - -/** - * @desc - * Returns true if all the needed copayers have joined the public key ring - * - * @returns {boolean} - */ -PublicKeyRing.prototype.isComplete = function() { - return this.remainingCopayers() == 0; -}; - -/** - * @desc - * Returns the number of copayers yet to join to make the public key ring complete - * - * @returns {number} - */ -PublicKeyRing.prototype.remainingCopayers = function() { - return this.totalCopayers - this.registeredCopayers(); -}; - -/** - * @desc - * Returns an array of copayer's public keys - * - * @returns {string[]} a list of hexadecimal strings with the public keys for - * the copayers in this ring - */ -PublicKeyRing.prototype.getAllCopayerIds = function() { - return this.copayerIds; -}; - -/** - * @desc - * Gets the current user's copayerId - * - * @returns {string} the extended public key hexadecimal-encoded - */ -PublicKeyRing.prototype.myCopayerId = function() { - return this.getCopayerId(0); -}; - -/** - * @desc Throws an error if the public key ring isn't complete - */ -PublicKeyRing.prototype._checkKeys = function() { - if (!this.isComplete()) throw new Error('dont have required keys yet'); -}; - -/** - * @desc - * Updates the internal register of the public hex string for a copayer, based - * on the value of the hierarchical key stored in copayersHK - * - * @private - * @param {number} index - the index of the copayer to update - */ -PublicKeyRing.prototype._updateBip = function(index) { - var hk = this.copayersHK[index].derive(HDPath.IdBranch); - this.copayerIds[index] = hk.eckey.public.toString('hex'); -}; - -/** - * @desc - * Sets a nickname for one of the copayers - * - * @private - * @param {number} index - the index of the copayer to update - * @param {string} nickname - the new nickname for that copayer - */ -PublicKeyRing.prototype._setNicknameForIndex = function(index, nickname) { - this.nicknameFor[this.copayerIds[index]] = nickname; -}; - -/** - * @desc - * Fetch the name of a copayer - * - * @param {number} index - the index of the copayer - * @return {string} the nickname of the index-th copayer - */ -PublicKeyRing.prototype.nicknameForIndex = function(index) { - return this.nicknameFor[this.copayerIds[index]]; -}; - -/** - * @desc - * Fetch the name of a copayer using its public key - * - * @param {string} copayerId - the public key ring of a copayer, hex encoded - * @return {string} the nickname of the copayer with such pubkey - */ -PublicKeyRing.prototype.nicknameForCopayer = function(copayerId) { - return this.nicknameFor[copayerId] || 'NN'; -}; - -/** - * @desc - * Add a copayer into the public key ring. - * - * @param {string} newHexaExtendedPublicKey - an hex encoded string with the copayer's pubkey - * @param {string} nickname - a nickname for this copayer - * @return {string} the newHexaExtendedPublicKey parameter - */ -PublicKeyRing.prototype.addCopayer = function(newHexaExtendedPublicKey, nickname) { - preconditions.checkArgument(newHexaExtendedPublicKey && _.isString(newHexaExtendedPublicKey)); - preconditions.checkArgument(!this.isComplete()); - preconditions.checkArgument(!nickname || _.isString(nickname)); - preconditions.checkArgument(!_.any(this.copayersHK, - function(copayer) { - return copayer.extendedPublicKeyString === newHexaExtendedPublicKey; - } - )); - - var newCopayerIndex = this.copayersHK.length; - var hierarchicalKey = new HK(newHexaExtendedPublicKey); - - this.copayersHK.push(hierarchicalKey); - this._updateBip(newCopayerIndex); - - if (nickname) { - this._setNicknameForIndex(newCopayerIndex, nickname); - } - return newHexaExtendedPublicKey; -}; - -/** - * @desc - * Get all the public keys for the copayers in this ring, for a given branch of Copay - * - * @param {number} index - the index for the shared address - * @param {boolean} isChange - whether to derive a change address o receive address - * @param {number} copayerIndex - the index of the copayer that requested the derivation - * @return {Buffer[]} an array of derived public keys in hexa format - */ -PublicKeyRing.prototype.getPubKeys = function(index, isChange, copayerIndex) { - this._checkKeys(); - - log.warn('Slow pubkey derivation...'); - var path = HDPath.Branch(index, isChange, copayerIndex); - var pubKeys = _.map(this.copayersHK, function(hdKey) { - return hdKey.derive(path).eckey.public; - }); - - return pubKeys; -}; - -/** - * @desc - * Generate a new Script for a copay address generated by index, isChange, and copayerIndex - * - * @TODO this could be cached - * - * @param {number} index - the index for the shared address - * @param {boolean} isChange - whether to derive a change address o receive address - * @param {number} copayerIndex - the index of the copayer that requested the derivation - * @returns {bitcore.Script} - */ -PublicKeyRing.prototype.getRedeemScript = function(index, isChange, copayerIndex) { - var pubKeys = this.getPubKeys(index, isChange, copayerIndex); - var script = Script.createMultisig(this.requiredCopayers, pubKeys); - return script; -}; - - - -/** - * @desc - * Get the address for a multisig based on the given params. - * - * Caches the address to the branch in the member addressToPath - * - * @param {number} index - the index for the shared address - * @param {boolean} isChange - whether to derive a change address o receive address - * @param {number} copayerIndex - the index of the copayer that requested the derivation - * @returns {bitcore.Address} - */ -PublicKeyRing.prototype._getAddress = function(index, isChange, id) { - var copayerIndex = this.getCosigner(id); - var path = HDPath.FullBranch(index, isChange, copayerIndex); - if (this.cache.pathToAddress[path]) - return this.cache.pathToAddress[path]; - - log.info('Generating Address:', index, isChange, copayerIndex); - var script = this.getRedeemScript(index, isChange, copayerIndex); - var address = Address.fromScript(script, this.network.name).toString(); - - this._cacheAddress(address, path, isChange); - return address; -}; - -PublicKeyRing.prototype._cacheAddress = function(address, path, isChange) { - this.cache.addressToPath[address] = path; - this.cache.pathToAddress[path] = address; -}; - -/** - * @desc - * Get the parameters used to derive a pubkey or a cosigner index - * - * Overloaded to receive a PubkeyString or a consigner index - * - * @param {number|string} id public key in hex format, or the copayer's index - * @return ???? - */ -PublicKeyRing.prototype.getHDParams = function(id) { - var copayerIndex = this.getCosigner(id); - var index = this.indexes.filter(function(i) { - return i.copayerIndex == copayerIndex - }); - if (index.length != 1) throw new Error('no index for copayerIndex'); - - return index[0]; -}; - -/** - * @desc - * Get the path used to derive a pubkey or a cosigner index for an address - * - * @param {string} address a multisig p2sh address - * @return {HDPath} - */ -PublicKeyRing.prototype.pathForAddress = function(address) { - this._checkCache(); - var path = this.cache.addressToPath[address]; - if (!path) throw new Error('Couldn\'t find path for address ' + address); - return path; -}; - -/** - * @desc - * Generates a new address and updates the last index used - * - * @param {truthy} isChange - generate a change address if true, otherwise - * generates a receive - * @param {number|string} pubkey - the pubkey for the copayer that generates the - * address (or index in the keyring) - * @returns {bitpay.Address} - */ -PublicKeyRing.prototype.generateAddress = function(isChange, pubkey) { - isChange = !!isChange; - var hdParams = this.getHDParams(pubkey); - var index = isChange ? hdParams.getChangeIndex() : hdParams.getReceiveIndex(); - var ret = this._getAddress(index, isChange, hdParams.copayerIndex); - hdParams.increment(isChange); - return ret; -}; - -/** - * @desc Is an address is from this wallet? - * - * @param {string} address - * @return {boolean} - */ -PublicKeyRing.prototype.addressIsOwn = function(address) { - return !!this.cache.addressToPath[address]; -}; - -/** - * @desc Is an address is a change address? - * - * @param {string} address - * @return {boolean} - */ -PublicKeyRing.prototype.addressIsChange = function(address) { - this._checkCache(); - - var path = this.cache.addressToPath[address]; - if (!path) - return null; - - var p = HDPath.indexesForPath(path); - - //Memoization Only, never stored. - this._isChange[address] = p.isChange; - return !!this._isChange[address]; -}; - - -/** - * @desc - * Maps a copayer's public key to his index in the keyring - * - * @param {number|string|undefined} pubKey - if undefined, returns the SHARED_INDEX - * - if a number, just return it - * - if a string, assume is the hex encoded public key - * @returns {number} the index of the copayer with the given pubkey - */ -PublicKeyRing.prototype.getCosigner = function(pubKey) { - if (_.isUndefined(pubKey)) return HDPath.SHARED_INDEX; - if (_.isNumber(pubKey)) return pubKey; - - var sorted = this.copayersHK.map(function(h, i) { - return h.eckey.public.toString('hex'); - }).sort(function(h1, h2) { - return h1.localeCompare(h2); - }); - - var index = sorted.indexOf(pubKey); - if (index == -1) throw new Error('public key is not on the ring'); - - return index; -}; - - - -PublicKeyRing.prototype.buildAddressCache = function() { - var ret = []; - var self = this; - - _.each(this.indexes, function(index) { - for (var i = 0; i < index.receiveIndex; i++) { - self._getAddress(i, false, index.copayerIndex); - } - for (var i = 0; i < index.changeIndex; i++) { - self._getAddress(i, true, index.copayerIndex); - } - }); -}; - - -PublicKeyRing.prototype.size = function(opts) { - var self = this; - return _.reduce(this.indexes, function(sum, index) { - return sum + index.receiveIndex + index.changeIndex - }, 0); -}; - -PublicKeyRing.prototype._checkCache = function(opts) { - if (_.isEmpty(this.cache.addressToPath)) { - this.buildAddressCache(); - } - if (_.size(this.cache.addressToPath) !== this.size()) { - this.buildAddressCache(); - } -}; - - -/** - * @desc - * Gets information about addresses for a copayer - * - * @param {Object} opts - * @returns {AddressInfo[]} - */ -PublicKeyRing.prototype.getAddresses = function() { - this._checkCache(); - return _.keys(this.cache.addressToPath); -}; - -/** - * getAddressesOrdered - * {@link Wallet#getAddressesOrdered} - * - * @param pubkey - * @return {string[]} - */ -PublicKeyRing.prototype.getAddressesOrdered = function(pubkey) { - this._checkCache(); - - var info = _.map(this.cache.addressToPath, function(path, addr) { - var p = HDPath.indexesForPath(path); - p.address = addr; - return p; - }); - - var copayerIndex = this.getCosigner(pubkey); - var l = info.length; - - var sortedInfo = _.sortBy(info, function(i) { - var goodness = ((i.copayerIndex !== copayerIndex) ? 2 * l : 0) + (i.isChange ? l : 0) + l - i.addressIndex; - return goodness; - }); - - return _.pluck(sortedInfo, 'address'); -}; - - - -/** - * @desc - * Gets information about addresses for a copayer - * - * @param {Object} opts - * @returns {AddressInfo[]} - */ -PublicKeyRing.prototype.getReceiveAddresses = function() { - this._checkCache(); - - var self = this; - return _.filter(this.getAddresses(), function(addr) { - return !self.addressIsChange(addr); - }); -}; - - - -/** - * @desc - * Retrieve the public keys for all cosigners for a given path - * - * @param {string} path - the BIP32 path - * @return {Buffer[]} the public keys, in buffer format - */ -PublicKeyRing.prototype._getForPath = function(path) { - var p = HDPath.indexesForPath(path); - return this.getPubKeys(p.addressIndex, p.isChange, p.copayerIndex); -}; - - -/** - * @desc - * Retrieve the public keys for derived addresses and the public keys for copayers - * - * @TODO: Should this exist? A user should just call _getForPath(paths) - * - * @param {string[]} paths - the paths to be derived - * @return {Object} with keys pubKeys and copayerIds - */ -PublicKeyRing.prototype.forPaths = function(paths) { - return { - pubKeys: paths.map(this._getForPath.bind(this)), - copayerIds: this.copayerIds, - } -}; - -/** - * @desc - * Retrieve the public keys for all cosigners for multiple paths - * - * @param {string[]} paths - the BIP32 paths - * @return {Array[]} the public keys, in buffer format (matrix of Buffer, Buffer[][]) - */ -PublicKeyRing.prototype._getForPaths = function(paths) { - preconditions.checkArgument(!_.isUndefined(paths)); - preconditions.checkArgument(_.isArray(paths)); - preconditions.checkArgument(_.all(paths, _.isString)); - - return paths.map(this._getForPath.bind(this)); -}; - -/** - * @desc - * Returns a map from a pubkey of an address to the id that generated it - * - * @param {string[]} pubkeys - the pubkeys to query - * @param {string[]} paths - the paths to query - */ -PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { - preconditions.checkArgument(pubkeys); - preconditions.checkArgument(paths); - - var inKeyMap = {}, - ret = {}; - for (var i in pubkeys) { - inKeyMap[pubkeys[i]] = 1; - }; - - var keys = this._getForPaths(paths); - for (var i in keys) { - for (var copayerIndex in keys[i]) { - var kHex = keys[i][copayerIndex].toString('hex'); - if (inKeyMap[kHex]) { - ret[kHex] = this.copayerIds[copayerIndex]; - delete inKeyMap[kHex]; - } - } - } - - if (_.size(inKeyMap)) { - for (var i in inKeyMap) { - log.error('Pubkey ' + i + ' not identified'); - } - throw new Error('Pubkeys not identified'); - } - - return ret; -}; - -/** - * @desc - * Returns a map from address -> public key needed - * - * @param {HDPath[]} paths - paths to be solved - * @returns {Object} a map from addresses to Buffer with the hex pubkeys - */ -PublicKeyRing.prototype.getRedeemScriptMap = function(paths) { - var ret = {}; - for (var i = 0; i < paths.length; i++) { - var path = paths[i]; - var p = HDPath.indexesForPath(path); - var script = this.getRedeemScript(p.addressIndex, p.isChange, p.copayerIndex); - ret[Address.fromScript(script, this.network.name).toString()] = script.getBuffer().toString('hex'); - } - return ret; -}; - -/** - * @desc - * Check if another PubKeyRing is similar to this one (checks network name, - * requiredCopayers, and totalCopayers). If ignoreId is falsy, also check that - * both walletIds match. - * - * @private - * @param {PubKeyRing} inPKR - the other PubKeyRing - * @param {boolean} ignoreId - whether to ignore checking for equal walletId - * @throws {Error} if the wallets mismatch - * @return true - */ - -PublicKeyRing.prototype._checkInPKR = function(inPKR, ignoreId) { - preconditions.checkArgument(_.isObject(inPKR)); - - if (!ignoreId && this.walletId !== inPKR.walletId) - throw new Error('inPKR walletId mismatch'); - - if (this.network.name !== inPKR.network.name) - throw new Error('Network mismatch. Should be ' + this.network.name + - ' and found ' + inPKR.network.name); - - if (this.requiredCopayers && inPKR.requiredCopayers && - (this.requiredCopayers !== inPKR.requiredCopayers)) - throw new Error('inPKR requiredCopayers mismatch ' + this.requiredCopayers + - '!=' + inPKR.requiredCopayers); - - if (this.totalCopayers && inPKR.totalCopayers && - this.totalCopayers !== inPKR.totalCopayers) - throw new Error('inPKR totalCopayers mismatch' + this.totalCopayers + - '!=' + inPKR.requiredCopayers); - - return true; -}; - -/** - * @desc - * Merges the public keys of the wallet passed in as a parameter with ours. - * - * @param {PublicKeyRing} inPKR - * @return {boolean} true if there where changes in our internal state - */ -PublicKeyRing.prototype._mergePubkeys = function(inPKR) { - var self = this; - var hasChanged = false; - - if (self.isComplete()) - return; - - inPKR.copayersHK.forEach(function(b) { - var epk = b.extendedPublicKeyString(); - var haveIt = _.any(self.copayersHK, function(hk) { - return hk.extendedPublicKeyString() === epk; - }); - - if (!haveIt) { - if (self.isComplete()) { - throw new Error('trying to add more pubkeys, when PKR isComplete at merge'); - } - var l2 = self.copayersHK.length; - self.copayersHK.push(new HK(epk)); - self._updateBip(l2); - if (inPKR.nicknameFor[self.getCopayerId(l2)]) - self._setNicknameForIndex(l2, inPKR.nicknameFor[self.getCopayerId(l2)]); - hasChanged = true; - } - }); - return hasChanged; -}; - - -/** - * @desc - * Merges the indexes for addresses generated with another copy of a list of - * HDParams - * - * @param {HDParams[]} indexes - indexes as received from another sources - * @return {boolean} true if the internal state has changed - */ -PublicKeyRing.prototype.mergeIndexes = function(indexes) { - var self = this; - var hasChanged = false; - - indexes.forEach(function(theirs) { - var mine = self.getHDParams(theirs.copayerIndex); - hasChanged |= mine.merge(theirs); - }); - - return !!hasChanged -} - - -/** - * @desc - * Merges this public key ring with another one, optionally ignoring the - * wallet id - * - * @param {PublicKeyRing} inPkr - * @param {boolean} ignoreId - * @return {boolean} true if the internal state has changed - */ -PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { - - this._checkInPKR(inPKR, ignoreId); - var hasChanged = false; - hasChanged |= this.mergeIndexes(inPKR.indexes); - hasChanged |= this._mergePubkeys(inPKR); - - return !!hasChanged; -}; - - - -module.exports = PublicKeyRing; diff --git a/js/models/TxProposal.js b/js/models/TxProposal.js deleted file mode 100644 index ba3def705..000000000 --- a/js/models/TxProposal.js +++ /dev/null @@ -1,624 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var preconditions = require('preconditions').singleton(); - -var bitcore = require('bitcore'); -var util = bitcore.util; -var Transaction = bitcore.Transaction; -var TransactionBuilder = bitcore.TransactionBuilder; -var Script = bitcore.Script; -var Key = bitcore.Key; - -var log = require('../util/log'); - -var TX_MAX_SIZE_KB = 50; -var VERSION = 1; -var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL', 'paymentAckMemo']; - - -function TxProposal(opts) { - preconditions.checkArgument(opts); - preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); - preconditions.checkArgument(opts.builder, 'no builder'); - preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); - - this.inputChainPaths = opts.inputChainPaths; - this.version = opts.version; - this.builder = opts.builder; - this.createdTs = opts.createdTs; - - // Copayer Actions ( copayerId: timeStamp ) - this.signedBy = opts.signedBy || {}; - this.seenBy = opts.seenBy || {}; - this.rejectedBy = opts.rejectedBy || {}; - - this.sentTs = opts.sentTs || null; - this.sentTxid = opts.sentTxid || null; - this.comment = opts.comment || null; - this.readonly = opts.readonly || null; - this.merchant = opts.merchant || null; - this.paymentAckMemo = opts.paymentAckMemo || null; - this.paymentProtocolURL = opts.paymentProtocolURL || null; - - this.resetCache(); - - // New Tx Proposal - if (_.isEmpty(this.seenBy) && opts.creator) { - var now = Date.now(); - var me = {}; - me[opts.creator] = now; - - this.seenBy = me; - this.signedBy = {}; - this.creator = opts.creator; - this.createdTs = now; - if (opts.signWith) { - if (!this.sign(opts.signWith, opts.creator)) - throw new Error('Could not sign generated tx'); - } - } -} - -TxProposal.prototype._checkPayPro = function() { - if (!this.merchant) return; - - if (this.paymentProtocolURL !== this.merchant.request_url) - throw new Error('PayPro: Mismatch on Payment URLs'); - - if (!this.merchant.outs || this.merchant.outs.length !== 1) - throw new Error('PayPro: Unsopported number of outputs'); - - if (this.merchant.expires < (this.getSent() || Date.now() / 1000.)) - throw new Error('PayPro: Request expired'); - - if (!this.merchant.total || !this.merchant.outs[0].amountSatStr || !this.merchant.outs[0].address) - throw new Error('PayPro: Missing amount'); - - var outs = JSON.parse(this.builder.vanilla.outs); - if (_.size(outs) != 1) - throw new Error('PayPro: Wrong outs in Tx'); - - var ppOut = this.merchant.outs[0]; - var txOut = outs[0]; - - if (ppOut.address !== txOut.address) - throw new Error('PayPro: Wrong out address in Tx'); - - if (ppOut.amountSatStr !== txOut.amountSatStr + '') - throw new Error('PayPro: Wrong amount in Tx'); - -}; - - -TxProposal.prototype.isFullySigned = function() { - return this.builder && this.builder.isFullySigned(); -}; - - -TxProposal.prototype.getMySignatures = function() { - preconditions.checkState(this._mySignatures, 'Still no signatures from us'); - return _.clone(this._mySignatures); -}; - -TxProposal.prototype._setMySignatures = function(signaturesBefore) { - var mySigs = []; - _.each(this.getSignatures(), function(signatures, index) { - var diff = _.difference(signatures, signaturesBefore[index]); - preconditions.checkState(diff.length == 1, 'more that one signature added!'); - mySigs.push(diff[0].toString('hex')); - }) - this._mySignatures = mySigs; - return; -}; - -TxProposal.prototype.sign = function(keys, signerId) { - var before = this.countSignatures(); - var signaturesBefore = this.getSignatures(); - this.builder.sign(keys); - - var signaturesAdded = this.countSignatures() > before; - if (signaturesAdded) { - this.signedBy[signerId] = Date.now(); - this._setMySignatures(signaturesBefore); - } - return signaturesAdded; -}; - -TxProposal.prototype._check = function() { - - if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { - throw new Error('Invalid tx proposal'); - } - - // Should be able to build - var tx = this.builder.build(); - - var txSize = tx.getSize(); - if (txSize / 1024 > TX_MAX_SIZE_KB) - throw new Error('BIG: Invalid TX proposal. Too big: ' + txSize + ' bytes'); - - if (!tx.ins.length) - throw new Error('Invalid tx proposal: no ins'); - - _.each(tx.ins, function(value, index) { - var scriptSig = value.s; - if (!scriptSig || !scriptSig.length) { - throw new Error('Invalid tx proposal: no signatures'); - } - var hashType = tx.getHashType(index); - if (hashType && hashType !== Transaction.SIGHASH_ALL) - throw new Error('Invalid tx proposal: bad signatures'); - }); - this._checkPayPro(); -}; - - -TxProposal.prototype.addMerchantData = function(merchantData) { - preconditions.checkArgument(merchantData.pr); - preconditions.checkArgument(merchantData.request_url); - var m = _.clone(merchantData); - - if (!this.paymentProtocolURL) - this.paymentProtocolURL = m.request_url; - - // remove unneeded data - m.raw = m.pr.pki_data = m.pr.signature = undefined; - this.merchant = m; - this._checkPayPro(); -}; - -TxProposal.prototype.getSignatures = function() { - var ins = this.builder.build().ins; - var sigs = _.map(ins, function(value) { - var script = new bitcore.Script(value.s); - var nchunks = script.chunks.length; - return _.map(script.chunks.slice(1, nchunks - 1), function(buffer) { - return buffer.toString('hex'); - }); - }); - - return sigs; -}; - -TxProposal.prototype.rejectCount = function() { - return _.size(this.rejectedBy); -}; - - -TxProposal.prototype.isFinallyRejected = function(maxRejectCount) { - return this.rejectCount() > maxRejectCount; -}; - -TxProposal.prototype.isPending = function(maxRejectCount) { - preconditions.checkArgument(_.isNumber(maxRejectCount)); - - if (this.isFinallyRejected(maxRejectCount) || this.sentTxid) - return false; - - return true; -}; - -TxProposal.prototype._setSigned = function(copayerId) { - - // Sign powns rejected - if (this.rejectedBy[copayerId]) { - log.info("WARN: a previously rejected transaction was signed by:", copayerId); - delete this.rejectedBy[copayerId]; - } - - this.signedBy[copayerId] = Date.now(); - - return this; -}; - - - -/** - * - * @desc verify signatures of ONE copayer, using an array of signatures for each input - * - * @param {string[]} signatures, of the same copayer, one for each input - * @return {string[]} array for signing pubkeys for each input - */ -TxProposal.prototype._addSignatureAndVerify = function(signatures) { - var self = this; - - var ret = []; - var tx = self.builder.build(); - - var inputsFullySigned = 0; - var newScriptSigs = []; - - this.resetCache(); - _.each(tx.ins, function(input, index) { - var scriptSig = new Script(input.s); - - var info = TxProposal.infoFromRedeemScript(scriptSig); - var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); - var keys = TxProposal.formatKeys(info.keys); - var sig = new Buffer(signatures[index], 'hex'); - - var hashType = sig[sig.length - 1]; - if (hashType !== Transaction.SIGHASH_ALL) - throw new Error('BADSIG: Invalid signature: Bad hash type'); - - var sigRaw = new Buffer(sig.slice(0, sig.length - 1)); - var signingPubKeyHex = self._verifyOneSignature(keys, sigRaw, txSigHash); - if (!signingPubKeyHex) - throw new Error('BADSIG: Invalid signatures: invalid for input:' + index); - - // now insert it - var keysHex = _.pluck(keys, 'keyHex'); - var prio = _.indexOf(keysHex, signingPubKeyHex); - preconditions.checkState(prio >= 0); - - var currentKeys = self.getSignersPubKeys()[index]; - - if (_.indexOf(currentKeys, signingPubKeyHex) >= 0) - throw new Error('BADSIG: Already have this signature'); - - var currentPrios = _.map(currentKeys, function(key) { - var prio = _.indexOf(keysHex, key); - preconditions.checkState(prio >= 0); - return prio; - }); - - var insertAt = 0; - while (!_.isUndefined(currentPrios[insertAt]) && prio > currentPrios[insertAt]) - insertAt++; - - // Insert it! (1 is OP_0!) - scriptSig.chunks.splice(1 + insertAt, 0, sig); - scriptSig.updateBuffer(); - - if (info.nreq == currentKeys.length + 1) { - inputsFullySigned++; - } - newScriptSigs.push(scriptSig.buffer); - }); - preconditions.checkState(newScriptSigs.length === tx.ins.length); - - // If we reach here, all signatures are OK, let's update the TX. - _.each(tx.ins, function(input, index) { - input.s = newScriptSigs[index]; - - // just to keep TransactionBuilder updated - if (tx.ins.length == inputsFullySigned) - self.builder.inputsSigned++; - }); -}; - -TxProposal.prototype.resetCache = function() { - this.cache = { - pubkeysForScript: {}, - }; -}; - -/** - * addSignature - * - * @param {string[]} signatures from *ONE* copayer, one signature for each TX input. - * @return {boolean} true = signatures added - */ -TxProposal.prototype.addSignature = function(copayerId, signatures) { - preconditions.checkArgument(_.isArray(signatures)); - - if (this.isFullySigned()) - return false; - - var tx = this.builder.build(); - preconditions.checkArgument(signatures.length === tx.ins.length, 'Wrong number of signatures given'); - - this._addSignatureAndVerify(signatures); - - this._setSigned(copayerId); - return true; -}; - -/** - * - * getSignersPubKey - * @desc get Pubkeys of signers, for each input. this is CPU intensive - * - * @return {string[][]} array of hashes for signing pubkeys for each input - */ -TxProposal.prototype.getSignersPubKeys = function(forceUpdate) { - var self = this; - - - var signersPubKey = []; - - if (!self.cache.signersPubKey || forceUpdate) { - - log.debug('PERFORMANCE WARN: Verifying *all* TX signatures:', self.getId()); - - var tx = self.builder.build(); - _.each(tx.ins, function(input, index) { - - if (!self.cache.pubkeysForScript[input.s]) { - var scriptSig = new Script(input.s); - var signatureCount = scriptSig.countSignatures(); - - var info = TxProposal.infoFromRedeemScript(scriptSig); - var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL); - var inputSignersPubKey = self.verifySignatures(info.keys, scriptSig, txSigHash); - - // Does scriptSig has strings that are not signatures? - if (inputSignersPubKey.length !== signatureCount) - throw new Error('Invalid signature'); - - self.cache.pubkeysForScript[input.s] = inputSignersPubKey; - } - - signersPubKey[index] = self.cache.pubkeysForScript[input.s]; - }); - self.cache.signersPubKey = signersPubKey; - } else { - log.debug('Using signatures verification cache') - } - - return self.cache.signersPubKey; -}; - -TxProposal.prototype.getId = function() { - preconditions.checkState(this.builder); - - if (!this.ntxid) { - this.ntxid = this.builder.build().getNormalizedHash().toString('hex'); - } - return this.ntxid; -}; - -TxProposal.prototype.toObj = function() { - var o = JSON.parse(JSON.stringify(this)); - delete o['builder']; - delete o['cache']; - o.builderObj = this.builder.toObj(); - return o; -}; - - -TxProposal._trim = function(o) { - var ret = {}; - CORE_FIELDS.forEach(function(k) { - ret[k] = o[k]; - }); - return ret; -}; - -TxProposal.fromObj = function(o, forceOpts) { - preconditions.checkArgument(o.builderObj); - delete o['builder']; - forceOpts = forceOpts || {}; - o.builderObj.opts = o.builderObj.opts || {}; - - // force opts is requested. - _.each(forceOpts, function(value, key) { - o.builderObj.opts[key] = value; - }); - - // Handle undef fee options - if (_.isUndefined(forceOpts.fee) && _.isUndefined(forceOpts.feeSat)) { - o.builderObj.opts.fee = undefined; - o.builderObj.opts.feeSat = undefined; - } - - try { - o.builder = TransactionBuilder.fromObj(o.builderObj); - } catch (e) { - throw new Error(e); - return null; - } - return new TxProposal(o); -}; - -TxProposal.fromUntrustedObj = function(o, forceOpts) { - var trimmed = TxProposal._trim(o); - var txp = TxProposal.fromObj(trimmed, forceOpts); - if (!txp) - throw new Error('Invalid Transaction'); - - txp._check(); - return txp; -}; - -TxProposal.prototype.toObjTrim = function() { - return TxProposal._trim(this.toObj()); -}; - -TxProposal.formatKeys = function(keys) { - var ret = []; - for (var i in keys) { - if (!Buffer.isBuffer(keys[i])) - throw new Error('keys must be buffers'); - - var k = new Key(); - k.public = keys[i]; - ret.push({ - keyObj: k, - keyHex: keys[i].toString('hex'), - }); - } - return ret; -}; - - -/** - * @desc Verify a single signature, for a given hash, tested against a given list of public keys. - * @param keys - * @param sigRaw - * @param txSigHash - * @return {string?} on valid signature, return the signing public key hex representation - */ -TxProposal.prototype._verifyOneSignature = function(keys, sigRaw, txSigHash) { - preconditions.checkArgument(Buffer.isBuffer(txSigHash)); - preconditions.checkArgument(Buffer.isBuffer(sigRaw)); - preconditions.checkArgument(_.isArray(keys)); - preconditions.checkArgument(keys[0].keyObj); - - var signingKey = _.find(keys, function(key) { - var ret = false; - try { - ret = key.keyObj.verifySignatureSync(txSigHash, sigRaw); - } catch (e) {}; - return ret; - }); - - return signingKey ? signingKey.keyHex : null; -}; - - -/** - * @desc verify transaction signatures - * - * @param inKeys - * @param scriptSig - * @param txSigHash - * @return {string[]} signing pubkeys, in order of apperance - */ -TxProposal.prototype.verifySignatures = function(inKeys, scriptSig, txSigHash) { - preconditions.checkArgument(Buffer.isBuffer(txSigHash)); - preconditions.checkArgument(inKeys); - preconditions.checkState(Buffer.isBuffer(inKeys[0])); - var self = this; - - if (scriptSig.chunks[0] !== 0) - throw new Error('Invalid scriptSig'); - - var keys = TxProposal.formatKeys(inKeys); - var ret = []; - for (var i = 1; i <= scriptSig.countSignatures(); i++) { - var chunk = scriptSig.chunks[i]; - log.debug('\t Verifying CHUNK:', i); - var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1)); - - var signingPubKeyHex = self._verifyOneSignature(keys, sigRaw, txSigHash); - if (!signingPubKeyHex) - throw new Error('Found a signature that is invalid'); - - ret.push(signingPubKeyHex); - } - return ret; -}; - -TxProposal.infoFromRedeemScript = function(s) { - var redeemScript = new Script(s.chunks[s.chunks.length - 1]); - if (!redeemScript) - throw new Error('Bad scriptSig (no redeemscript)'); - - var nreq = nreq = redeemScript.chunks[0] - 80; //see OP_2-OP_16 - var pubkeys = redeemScript.capture(); - if (!pubkeys || !pubkeys.length) - throw new Error('Bad scriptSig (no pubkeys)'); - - return { - nreq: nreq, - keys: pubkeys, - script: redeemScript, - }; -}; - -TxProposal.prototype.getSeen = function(copayerId) { - return this.seenBy[copayerId]; -}; - -TxProposal.prototype.setSeen = function(copayerId) { - if (!this.seenBy[copayerId]) - this.seenBy[copayerId] = Date.now(); -}; - -TxProposal.prototype.setRejected = function(copayerId) { - - if (this.signedBy[copayerId]) - throw new Error('Can not reject a signed TX'); - - if (!this.rejectedBy[copayerId]) - this.rejectedBy[copayerId] = Date.now(); - - return this; -}; - -TxProposal.prototype.setSent = function(sentTxid) { - this.sentTxid = sentTxid; - this.sentTs = Date.now(); - return this; -}; - -TxProposal.prototype.getSent = function() { - return this.sentTs; -} - -TxProposal.prototype.setCopayers = function(pubkeyToCopayerMap) { - var newCopayer = {}, - oldCopayers = {}, - newSignedBy = {}, - readOnlyPeers = {}, - isNew = 1; - - for (var k in this.signedBy) { - oldCopayers[k] = 1; - isNew = 0; - }; - - if (isNew == 0) { - if (!this.creator || !this.createdTs) - throw new Error('Existing TX has no creator'); - - if (!this.signedBy[this.creator]) - throw new Error('Existing TX is not signed by creator'); - - - if (Object.keys(this.signedBy).length === 0) - throw new Error('Existing TX has no signatures'); - } - - - var iSig = this.getSignersPubKeys(); - for (var i in iSig) { - var copayerId = pubkeyToCopayerMap[iSig[i]]; - - if (!copayerId) - throw new Error('Found unknown signature') - - if (oldCopayers[copayerId]) { - //Already have it. Do nothing - } else { - newCopayer[copayerId] = Date.now(); - delete oldCopayers[i]; - } - } - - if (Object.keys(newCopayer).length > 1) - throw new Error('New TX must have only 1 new signature'); - - // Handler creator / createdTs. - // from senderId, and must be signed by senderId * DISABLED* - // - if (isNew) { - this.creator = Object.keys(newCopayer)[0]; - this.seenBy[this.creator] = this.createdTs = Date.now(); - } - - //Ended. Update this - _.extend(this.signedBy, newCopayer); - - // signedBy has preference over rejectedBy - for (var i in this.signedBy) { - delete this.rejectedBy[i]; - } - - return Object.keys(newCopayer); -}; - -//This should be on bitcore / Transaction -TxProposal.prototype.countSignatures = function() { - var tx = this.builder.build(); - var ret = 0; - for (var i in tx.ins) { - ret += tx.countInputSignatures(i); - } - return ret; -}; - -module.exports = TxProposal; diff --git a/js/models/TxProposals.js b/js/models/TxProposals.js deleted file mode 100644 index 77dbcbccd..000000000 --- a/js/models/TxProposals.js +++ /dev/null @@ -1,156 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var preconditions = require('preconditions').singleton(); - -var bitcore = require('bitcore'); -var util = bitcore.util; -var Transaction = bitcore.Transaction; -var Script = bitcore.Script; -var Key = bitcore.Key; -var buffertools = bitcore.buffertools; - -var log = require('../util/log'); -var TxProposal = require('./TxProposal');; - -function TxProposals(opts) { - opts = opts || {}; - this.walletId = opts.walletId; - this.network = opts.networkName === 'livenet' ? - bitcore.networks.livenet : bitcore.networks.testnet; - this.txps = {}; -} - -// fromObj => from a trusted source -TxProposals.fromObj = function(o, forceOpts) { - var ret = new TxProposals({ - networkName: o.networkName, - walletId: o.walletId, - }); - - o.txps.forEach(function(o2) { - try { - var t = TxProposal.fromObj(o2, forceOpts); - } catch (e) { - log.info('Ignoring corrupted TxProposal:', o2, e); - } - if (t && t.builder) { - var id = t.getId(); - ret.txps[id] = t; - } - - }); - return ret; -}; - -TxProposals.prototype.length = function() { - return Object.keys(this.txps).length; -}; - - -TxProposals.prototype.getNtxidsSince = function(sinceTs) { - preconditions.checkArgument(sinceTs); - var ret = []; - - for (var ii in this.txps) { - var txp = this.txps[ii]; - if (txp.createdTs >= sinceTs) - ret.push(ii); - } - return ret; -}; - - - -TxProposals.prototype.getNtxids = function() { - return Object.keys(this.txps); -}; - -TxProposals.prototype.deleteOne = function(ntxid) { - preconditions.checkState(this.txps[ntxid], 'Unknown TXP: ' + ntxid); - delete this.txps[ntxid]; -}; - -TxProposals.prototype.deleteAll = function() { - this.txps = {}; -}; - -TxProposals.prototype.deletePending = function(maxRejectCount) { - for (var ntxid in this.txps) { - if (this.txps[ntxid].isPending(maxRejectCount)) - delete this.txps[ntxid]; - }; -}; - -TxProposals.prototype.toObj = function() { - var ret = []; - for (var id in this.txps) { - var t = this.txps[id]; - if (!t.sent) - ret.push(t.toObj()); - } - return { - txps: ret, - walletId: this.walletId, - networkName: this.network.name, - }; -}; - - - -// Add a LOCALLY CREATED (trusted) tx proposal -TxProposals.prototype.add = function(txp) { - var ntxid = txp.getId(); - this.txps[ntxid] = txp; - return ntxid; -}; - - -TxProposals.prototype.exist = function(ntxid) { - return this.txps[ntxid] ? true : false; -}; - - -TxProposals.prototype.get = function(ntxid) { - var ret = this.txps[ntxid]; - if (!ret) - throw new Error('Unknown TXP: ' + ntxid); - - return ret; -}; - -//returns the unspent txid-vout used in PENDING Txs -TxProposals.prototype.getUsedUnspent = function(maxRejectCount) { - var ret = {}; - var self = this; - - _.each(this.txps, function(txp) { - if (!txp.isPending(maxRejectCount)) - return - - _.each(txp.builder.getSelectedUnspent(), function(u) { - ret[u.txid + ',' + u.vout] = 1; - }); - }); - return ret; -}; - -/** - * purge - * - * @param deleteAll - * @return {undefined} - */ -TxProposals.prototype.purge = function(deleteAll, maxRejectCount) { - var m = _.size(this.txps); - - if (deleteAll) { - this.deleteAll(); - } else { - this.deletePending(maxRejectCount); - } - var n = _.size(this.txps); - return m - n; -}; - -module.exports = TxProposals; diff --git a/js/models/Wallet.js b/js/models/Wallet.js deleted file mode 100644 index acfb49b34..000000000 --- a/js/models/Wallet.js +++ /dev/null @@ -1,2846 +0,0 @@ -'use strict'; - -var _ = require('lodash'); -var preconditions = require('preconditions').singleton(); -var inherits = require('inherits'); -var events = require('events'); -var async = require('async'); - -var bitcore = require('bitcore'); -var BIP21 = bitcore.BIP21; -var bignum = bitcore.Bignum; -var coinUtil = bitcore.util; -var buffertools = bitcore.buffertools; -var Builder = bitcore.TransactionBuilder; -var SecureRandom = bitcore.SecureRandom; -var Base58Check = bitcore.Base58.base58Check; -var Address = bitcore.Address; -var PayPro = bitcore.PayPro; -var Transaction = bitcore.Transaction; - -var log = require('../util/log'); -var cryptoUtil = require('../util/crypto'); -var httpUtil = require('../util/HTTP'); -var HDParams = require('./HDParams'); -var PublicKeyRing = require('./PublicKeyRing'); -var TxProposal = require('./TxProposal'); -var TxProposals = require('./TxProposals'); -var PrivateKey = require('./PrivateKey'); -var Async = require('./Async'); -var Insight = module.exports.Insight = require('./Insight'); -var copayConfig = require('../../config'); - -var TX_MAX_INS = 70; - - -/** - * @desc - * Wallet manages a private key for Copay, network and blockchain information. - * - * @TODO: Split this leviathan. - * - * @param {Object} opts - * @param {Network} opts.network - used to send and retrieve messages from - * copayers - * @param {Blockchain} opts.blockchain - source of truth for what happens in - * the blockchain (utxos, balances) - * @param {number} opts.requiredCopayers - number of required copayers to - * release funds - * @param {number} opts.totalCopayers - number of copayers in the wallet - * @param {boolean} opts.spendUnconfirmed - whether it's safe to spend - * unconfirmed outputs or not - * @param {PublicKeyRing} opts.publicKeyRing - an instance of {@link PublicKeyRing} - * @param {TxProposals} opts.txProposals - an instance of {@link TxProposals} - * @param {PrivateKey} opts.privateKey - an instance of {@link PrivateKey} - * @param {string} opts.version - the version of copay where this wallet was - * created - * @TODO: figure out if reconnectDelay is set in milliseconds - * @param {number} opts.reconnectDelay - amount of seconds to wait before - * attempting to reconnect - * @constructor - */ -function Wallet(opts) { - var self = this; - preconditions.checkArgument(opts); - - opts.reconnectDelay = opts.reconnectDelay || 500; - - var networkName = Wallet.obtainNetworkName(opts); - preconditions.checkState((opts.network && opts.blockchain) || networkName); - - opts.network = opts.network || Wallet._newAsync(opts.networkOpts[networkName]); - opts.blockchain = opts.blockchain || Wallet._newInsight(opts.blockchainOpts[networkName]);; - this.httpUtil = opts.httpUtil || httpUtil; - - //required params - ['network', 'blockchain', - 'requiredCopayers', 'totalCopayers', 'spendUnconfirmed', - 'publicKeyRing', 'txProposals', 'privateKey', 'version', - 'reconnectDelay' - ].forEach(function(k) { - preconditions.checkArgument(!_.isUndefined(opts[k]), 'MISSOPT: missing required option for Wallet: ' + k); - self[k] = opts[k]; - }); - - - this.id = opts.id || Wallet.getRandomId(); - this.secretNumber = opts.secretNumber || Wallet.getRandomSecretNumber(); - // TODO - // this.lock = new WalletLock(this.storage, this.id, opts.lockTimeOutMin); - this.settings = opts.settings || copayConfig.wallet.settings; - this.name = opts.name; - - this.publicKeyRing.walletId = this.id; - this.txProposals.walletId = this.id; - this.registeredPeerIds = []; - this.addressBook = opts.addressBook || {}; - this.publicKey = this.privateKey.publicHex; - this.syncedTimestamp = opts.syncedTimestamp || 0; - this.lastMessageFrom = {}; - - - var networkName = Wallet.obtainNetworkName(opts); - - preconditions.checkArgument(this.network.setHexNonce, 'Incorrect network parameter'); - preconditions.checkArgument(this.blockchain.getTransaction, 'Incorrect blockchain parameter'); - - - this.network.maxPeers = this.totalCopayers; - this.network.secretNumber = this.secretNumber; - - //network nonces are 8 byte buffers, representing a big endian number - //one nonce for oneself, and then one nonce for each copayer - this.network.setHexNonce(opts.networkNonce); - this.network.setHexNonces(opts.networkNonces); - - this.cache = { - paymentRequests: {}, - }; -} - -inherits(Wallet, events.EventEmitter); - - -Wallet.TX_NEW = 'txNew'; -Wallet.TX_PURGED = 'txPurged'; -Wallet.TX_ALL_PURGED = 'txAllPurged'; -Wallet.TX_REJECTED = 'txRejected'; -Wallet.TX_BROADCASTED = 'txBroadcasted'; -Wallet.TX_PROPOSAL_SENT = 'txProposalSent'; -Wallet.TX_SIGNED = 'txSigned'; -Wallet.TX_SIGNED_AND_BROADCASTED = 'txSignedAndBroadcasted'; - -Wallet.prototype.emitAndKeepAlive = function(args) { - var args = Array.prototype.slice.call(arguments); - log.debug('Wallet:' + this.getName() + ' Emitting:', args); - this.keepAlive(); - this.emit.apply(this, arguments); -}; - -/** - * @desc Fixed & Forced TransactionBuilder options, for genererating transactions. - * - * @static - * @property lockTime null - * @property signhash SIGHASH - * @property fee null (automatic) - * @property feeSat null - */ -Wallet.builderOpts = { - lockTime: null, - signhash: bitcore.Transaction.SIGHASH_ALL, - fee: 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', - 'syncedTimestamp', - 'secretNumber', -]; - -/* For compressed keys, m*73 + n*34 <= 496 */ -Wallet.COPAYER_PAIR_LIMITS = { - 1: 1, - 2: 2, - 3: 3, - 4: 4, - 5: 4, - 6: 4, - 7: 3, - 8: 3, - 9: 2, - 10: 2, - 11: 1, - 12: 1, -}; - -Wallet.getStoragePrefix = function() { - return 'wallet::'; -}; - -Wallet.getStorageKey = function(str) { - return Wallet.getStoragePrefix() + str; -}; - -Wallet.prototype.getStorageKey = function() { - return Wallet.getStorageKey(this.getId()); -}; - -/* for stubbing */ -Wallet._newInsight = function(opts) { - return new Insight(opts); -}; - -/* for stubbing */ -Wallet._newAsync = function(opts) { - return new Async(opts); -}; - -/** - * @desc Retrieve a random id for the wallet - * @TODO: Discuss changing to a UUID - * @return {string} 8 bytes, hexa encoded - */ -Wallet.getRandomId = function() { - var r = bitcore.SecureRandom.getPseudoRandomBuffer(8).toString('hex'); - return r; -}; - -/** - * @desc Retrieve a random secret number to secure wallet secret - * @return {string} 5 bytes, hexa encoded - */ -Wallet.getRandomSecretNumber = function() { - var r = bitcore.SecureRandom.getPseudoRandomBuffer(5).toString('hex') - return r; -}; - -/** - * @desc - * Get the maximum allowed number of required copayers. - * This is a limit imposed by the maximum allowed size of the scriptSig. - * @param {number} totalCopayers - the total number of copayers - * @return {number} - */ -Wallet.getMaxRequiredCopayers = function(totalCopayers) { - return Wallet.COPAYER_PAIR_LIMITS[totalCopayers]; -}; - -/** - * @desc obtain network name from serialized wallet - * @param {Object} wallet object - * @return {string} network name - */ -Wallet.obtainNetworkName = function(obj) { - return obj.networkName || - (obj.opts ? obj.opts.networkName : null) || - (obj.publicKeyRing ? (obj.publicKeyRing.networkName || obj.publicKeyRing.network.name) : null) || - (obj.privateKey ? obj.privateKey.networkName : null); -}; - - - -/** - * @desc Set the copayer id for the owner of this wallet - * @param {string} pubkey - the pubkey to set to the {@link Wallet#seededCopayerId} property - */ -Wallet.prototype.seedCopayer = function(pubKey) { - this.seededCopayerId = pubKey; -}; - - -/** - * @desc Handles an 'indexes' message. - * - * Processes the data using {@link HDParams#fromList} and merges it with the - * {@link Wallet#publicKeyRing}. - * - * @param {Object} data - the data received, {@see HDParams#fromList} - */ -Wallet.prototype._doOnIndexes = function(indexes, fromTxProposal) { - preconditions.checkArgument(indexes); - var inIndexes = HDParams.fromList(indexes); - var hasChanged = this.publicKeyRing.mergeIndexes(inIndexes); - if (hasChanged) { - - // If the new indexes come from TX proposals (change address) - // that addresses should be new. No need to clean cache. - if (!fromTxProposal) - this.clearUnspentCache(); - - this.subscribeToAddresses(); - this.emitAndKeepAlive('newAddresses'); - } -}; - - -Wallet.prototype._onIndexes = function(senderId, data) { - return this._doOnIndexes(data.indexes); -}; - -/** - * @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.emitAndKeepAlive('settingsUpdated'); -}; - -/** - * @desc Locks other sessions from connecting to the wallet - * @see Async#lockIncommingConnections - */ -Wallet.prototype._lockIncomming = function() { - this.network.lockIncommingConnections(this.publicKeyRing.getAllCopayerIds()); -}; - -/** - * @desc - * Handles a 'PUBLICKEYRING' message from senderId. - * - * data.publicKeyRing is expected to be processed correctly by - * {@link PublicKeyRing#fromObj}. - * - * After successful deserialization, {@link Wallet#publicKeyRing} is merged - * with the received data, a call to {@link Wallet#store} is performed if the - * internal state has changed. - * - * This locks new incoming connections in case the public key ring is completed - * - * @param {string} senderId - the sender id - * @param {Object} data - the data recived, {@see HDParams#fromList} - * @param {Object} data.publicKeyRing - data to be deserialized into a {@link PublicKeyRing} - * using {@link PublicKeyRing#fromObj} - * @emits connectionError - */ -Wallet.prototype._onPublicKeyRing = function(senderId, data) { - log.debug('Wallet:' + this.id + ' RECV PUBLICKEYRING:', data); - - var inPKR = PublicKeyRing.fromUntrustedObj(data.publicKeyRing); - var wasIncomplete = !this.publicKeyRing.isComplete(); - var hasChanged; - - try { - hasChanged = this.publicKeyRing.merge(inPKR, true); - } catch (e) { - log.warn('Wallet:' + this.id, e); - return; - } - if (hasChanged) { - - if (wasIncomplete) { - this.sendPublicKeyRing(); - } - if (this.publicKeyRing.isComplete()) { - this._lockIncomming(); - this.subscribeToAddresses(); - this.emitAndKeepAlive('ready'); - } else { - this.emitAndKeepAlive('publicKeyRingUpdated'); - } - } -}; - -/** - * @desc - * Retrieves a keymap from a transaction proposal set extracts a maps from - * public key to cosignerId for each signed input of the transaction proposal. - * - * @param {TxProposals} txp - the transaction proposals - * @return {Object} [pubkey] -> copayerId - */ -Wallet.prototype._getPubkeyToCopayerMap = function(txp) { - preconditions.checkArgument(txp); - var inSig0, keyMapAll = {}, - self = this; - - var signersPubKeys = txp.getSignersPubKeys(); - _.each(signersPubKeys, function(inputSignersPubKey, i) { - var keyMap = self.publicKeyRing.copayersForPubkeys(inputSignersPubKey, txp.inputChainPaths); - - if (_.size(keyMap) !== _.size(inputSignersPubKey)) - throw new Error('Signature does not match known copayers'); - - _.extend(keyMapAll, keyMap); - - // From here -> only to check that all inputs have the same sigs - var inSigArr = _.values(keyMap); - var inSig = JSON.stringify(inSigArr.sort()); - - if (!inSig0) { - inSig0 = inSig; - } else { - if (inSig !== inSig0) - throw new Error('found inputs with different signatures'); - } - }); - return keyMapAll; -}; - -/** - * @callback transactionCallback - * @param {error} error - * @param {number} transaction ID (if sent) - */ - -/** - * @desc - * Asynchronously check with the blockchain if a given transaction was sent. - * - * @param {string} ntxid - the transaction proposal - * @param {transactionCallback} cb - */ -Wallet.prototype._checkIfTxIsSent = function(ntxid, cb) { - var txp = this.txProposals.get(ntxid); - var tx = txp.builder.build(); - - //not used anymore - //var txHex = tx.serialize().toString('hex'); - - - //Use calcHash NOT getHash which could be cached. - var txid = bitcore.util.formatHashFull(tx.calcHash()); - - log.debug('Checking if transactions was broadcasted... ID:' + txid); - - this.blockchain.getTransaction(txid, function(err, tx) { - return cb(err, !err ? txid : null); - }); -}; - -/** - * - * @desc Set Incomming Transaction Proposal seen status - * and send `seen` messages to peers if aplicable. - * @param ntxid - */ -Wallet.prototype._setTxProposalSeen = function(txp) { - if (!txp.getSeen(this.getMyCopayerId())) { - txp.setSeen(this.getMyCopayerId()); - this.sendSeen(txp.getId()); - } -}; - - - -/** - * @desc updates Tx Proposal Sent status by checking the blockchain - * - * @param ntxid - * @param {transactionCallback} cb - */ -Wallet.prototype._updateTxProposalSent = function(txp, cb) { - var self = this; - this._checkIfTxIsSent(txp.getId(), function(err, txid) { - if (err) return cb(err); - - if (txid) { - txp.setSent(txid); - } - if (cb) - return cb(null, txid, txid ? Wallet.TX_BROADCASTED : null); - }); -}; - - -/** - * _processTxProposalPayPro - * - * @desc Process and incoming PayPro TX Proposal. Fetchs the payment request - * from the merchant. - * - * @param mergeInfo Proposals merge information, as returned by TxProposals.merge - * @return {fetchPaymentRequestCallback} - */ -Wallet.prototype._processTxProposalPayPro = function(txp, cb) { - var self = this; - - if (!txp.paymentProtocolURL) - return cb(); - - log.info('Received a Payment Protocol TX Proposal'); - self.fetchPaymentRequest({ - url: txp.paymentProtocolURL - }, function(err, merchantData) { - if (err) return cb(err); - - // This will verify current TXP data vs. merchantData (e.g., out addresses) - try { - txp.addMerchantData(merchantData); - } catch (e) { - log.error(e); - err = 'BADPAYPRO: ' + e.toString(); - } - return cb(err); - }); -}; - -/** - * @desc Process an NEW incoming transaction proposal. Runs safety and sanity checks on it. - * - * @param mergeInfo Proposals merge information, as returned by TxProposals.merge - * @return {errCallback} - */ -Wallet.prototype._processIncomingNewTxProposal = function(txp, cb) { - var self = this; - - var ntxid = txp.getId(); - self._processTxProposalPayPro(txp, function(err) { - if (err) return cb(err); - - // Disabled, not been shown on the UX now. - //self._setTxProposalSeen(txp); - - var tx = txp.builder.build(); - if (tx.isComplete() && !txp.getSent()) - self._updateTxProposalSent(txp); - return cb(); - }); -}; - - -/* only for stubbing */ -Wallet.prototype._txProposalFromUntrustedObj = function(data, opts) { - return TxProposal.fromUntrustedObj(data, opts); -}; - -/** - * @desc - * Handles a NEW 'TXPROPOSAL' network message - * - * @param {string} senderId - the id of the sender - * @param {Object} data - the data received - * @param {Object} data.txProposal - first parameter for {@link TxProposals#merge} - * @emits txProposalEvent - */ -Wallet.prototype._onTxProposal = function(senderId, data) { - preconditions.checkArgument(data.txProposal); - var self = this; - - if (data.indexes) { - this._doOnIndexes(data.indexes, true); - } - - try { - var incomingTx = self._txProposalFromUntrustedObj(data.txProposal, Wallet.builderOpts); - var incomingNtxid = incomingTx.getId(); - } catch (e) { - log.warn(e); - return; - } - - if (this.txProposals.exist(incomingNtxid)) { - log.warn('Ignoring existing tx Proposal:' + incomingNtxid); - return; - } - - self._processIncomingNewTxProposal(incomingTx, function(err) { - if (err) { - log.warn('Corrupt TX proposal received from:', senderId, err.toString()); - return; - } - - - var pubkeyToCopayerMap = self._getPubkeyToCopayerMap(incomingTx); - incomingTx.setCopayers(pubkeyToCopayerMap); - - self.txProposals.add(incomingTx); - self.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_NEW, - cId: senderId, - }); - }); -}; - - -Wallet.prototype._onSignature = function(senderId, data) { - var self = this; - try { - var localTx = this.txProposals.get(data.ntxid); - } catch (e) { - log.info('Ignoring signature for unknown tx Proposal:' + data.ntxid); - return; - }; - localTx.addSignature(senderId, data.signatures); - self.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_SIGNED, - cId: senderId, - }); - self.issueTxIfComplete(data.ntxid, function(err, txid) {}); -}; - -/** - * @desc - * Handle a REJECT message received - * - * @param {string} senderId - * @param {Object} data - * @param {string} data.ntxid - * @emits txProposalEvent - */ -Wallet.prototype._onReject = function(senderId, data) { - preconditions.checkState(data.ntxid); - log.debug('Wallet:' + this.id + ' RECV REJECT:', data); - - try { - var txp = this.txProposals.get(data.ntxid); - } catch (e) { - log.info(e); - }; - - if (txp) { - if (txp.signedBy[senderId]) - throw new Error('Received Reject for an already signed TX from:' + senderId); - - txp.setRejected(senderId); - this.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_REJECTED, - cId: senderId, - txId: data.ntxid - }); - } -}; - -/** - * @desc - * Handle a SEEN message received - * - * @param {string} senderId - * @param {Object} data - * @param {string} data.ntxid - * @emits txProposalEvent - */ -Wallet.prototype._onSeen = function(senderId, data) { - preconditions.checkState(data.ntxid); - try { - var txp = this.txProposals.get(data.ntxid); - } catch (e) {}; - if (txp) { - txp.setSeen(senderId); - this.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_SEEN, - cId: senderId, - txId: data.ntxid - }); - } -}; - -/** - * @desc - * Handle a ADDRESSBOOK message received - * - * @param {string} senderId - * @param {Object} data - * @param {Object} data.addressBook - * @emits addressBookUpdated - * @emits txProposalEvent - */ -Wallet.prototype._onAddressBook = function(senderId, data) { - if (!data.addressBook || !_.isObject(data.addressBook)) - return; - - var self = this, - hasChange; - _.each(data.addressBook, function(value, key) { - if (key && !self.addressBook[key] && _.isString(key) && Address.validate(key)) { - - self.addressBook[key] = _.pick(value, ['createdTs', 'label']); - - // Force author to senderId. - self.addressBook[key].copayerId = senderId; - - hasChange = true; - } - }); - - if (hasChange) { - this.emitAndKeepAlive('addressBookUpdated'); - } -}; - -Wallet.prototype.updateSyncedTimestamp = function(ts) { - preconditions.checkArgument(ts); - preconditions.checkArgument(_.isNumber(ts)); - preconditions.checkArgument(ts > 2999999999999, 'use microseconds'); - this.syncedTimestamp = ts; -}; - - -/** - * @desc Called when there are no messages in the server - * Triggers a call to {@link Wallet#sendWalletReady} - */ -Wallet.prototype._onNoMessages = function() { - if (this.isComplete()) { - log.debug('Wallet:' + this.getName() + ' No messages at the server. Requesting peer sync from: ' + (this.syncedTimestamp + 1)); - this.sendWalletReady(null, parseInt((this.syncedTimestamp + 1) / 1000000)); - } -}; - -/** - * @desc Demultiplexes a new message received through the wire - * - * @param {string} senderId - the sender id - * @param {Object} data - the received object - * @param {number} ts - the timestamp when this object was received - * - * @emits corrupt - */ -Wallet.prototype._onData = function(senderId, data, ts) { - preconditions.checkArgument(senderId); - preconditions.checkArgument(data); - preconditions.checkArgument(data.type); - preconditions.checkArgument(ts); - preconditions.checkArgument(_.isNumber(ts)); - - log.debug('Wallet:' + this.getName() + ' RECV:', data.type, data, senderId); - - this.updateSyncedTimestamp(ts); - - if (data.type !== 'walletId' && this.id !== data.walletId) { - log.debug('Wallet:' + this.id + ' Received corrupt message:', data) - this.emitAndKeepAlive('corrupt', senderId); - return; - } - - switch (data.type) { - // This handler is repeaded on WalletFactory (#join). TODO - case 'walletId': - this.sendWalletReady(senderId); - break; - case 'walletReady': - if (this.lastMessageFrom[senderId] !== 'walletReady') { - log.debug('Wallet:' + this.id + ' peer Sync received. since: ' + (data.sinceTs || 0)); - this.sendPublicKeyRing(senderId); - this.sendAddressBook(senderId); - this.sendAllTxProposals(senderId, data.sinceTs); // send old txps - } - break; - case 'publicKeyRing': - this._onPublicKeyRing(senderId, data); - break; - case 'reject': - this._onReject(senderId, data); - break; - case 'seen': - this._onSeen(senderId, data); - break; - case 'txProposal': - this._onTxProposal(senderId, data); - break; - case 'signature': - this._onSignature(senderId, data); - break; - case 'indexes': - this._onIndexes(senderId, data); - break; - case 'addressbook': - this._onAddressBook(senderId, data); - break; - // unused messages - case 'disconnect': - //case 'an other unused message': - break; - default: - throw new Error('unknown message type received: ' + data.type + ' from: ' + senderId) - } - this.lastMessageFrom[senderId] = data.type; - -}; - -/** - * @desc Handles a connect message - * @param {string} newCopayerId - the new copayer in the wallet - * @emits connect - */ -Wallet.prototype._onConnect = function(newCopayerId) { - if (newCopayerId) { - log.debug('Wallet:' + this.id + '#### Setting new COPAYER:', newCopayerId); - this.sendWalletId(newCopayerId); - } - - var peerID = this.network.peerFromCopayer(newCopayerId); - this.emitAndKeepAlive('connect', peerID); -}; - -/** - * @desc Returns the network name for this wallet ('testnet' or 'livenet') - * @return {string} - */ -Wallet.prototype.getNetworkName = function() { - return this.publicKeyRing.network.name; -}; - -/** - * @return {bool} - */ -Wallet.prototype.isTestnet = function() { - return this.publicKeyRing.network.name === 'testnet'; -}; - - - -/** - * @desc Serialize options into an object - * @return {Object} with keys id, spendUnconfirmed, - * requiredCopayers, totalCopayers, name, - * version - */ -Wallet.prototype._optsToObj = function() { - var obj = { - id: this.id, - spendUnconfirmed: this.spendUnconfirmed, - requiredCopayers: this.requiredCopayers, - totalCopayers: this.totalCopayers, - name: this.name, - version: this.version, - networkName: this.getNetworkName(), - }; - - return obj; -}; - -/** - * @desc Retrieve the copayerId pubkey for a given index - * @param {number=} index - the index of the copayer, ours by default - * @return {string} hex-encoded pubkey - */ -Wallet.prototype.getCopayerId = function(index) { - return this.publicKeyRing.getCopayerId(index || 0); -}; - -/** - * @desc Get my own pubkey - * @return {string} hex-encoded pubkey - */ -Wallet.prototype.getMyCopayerId = function() { - - if (!this._myId) - this._myId = this.getCopayerId(0); - - return this._myId; //copayer id is hex of a public key -}; - -/** - * @desc Retrieve my private key - * @return {string} hex-encoded private key - */ -Wallet.prototype.getMyCopayerIdPriv = function() { - 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 - * @return {string} my own pubkey, base58 encoded - */ -Wallet.prototype.getSecretNumber = function() { - return this.secretNumber; -}; - -/** - * @desc Returns the secret number used to prevent MitM attacks from Insight - * @return {string} - */ -Wallet.prototype.getSecret = function() { - var buf = new Buffer( - this.getMyCopayerId() + - this.getSecretNumber() + - (this.getNetworkName() === 'livenet' ? '00' : '01'), - 'hex'); - var str = Base58Check.encode(buf); - return str; -}; - -/** - * @desc Returns an object with a pubKey value, an hex representation - * of a public key - * @param {string} secretB - the secret to be base58-decoded - * @return {Object} - */ -Wallet.decodeSecret = function(secretB) { - try { - var secret = Base58Check.decode(secretB); - var pubKeyBuf = secret.slice(0, 33); - var secretNumber = secret.slice(33, 38); - var networkName = secret.slice(38, 39).toString('hex') === '00' ? 'livenet' : 'testnet'; - return { - pubKey: pubKeyBuf.toString('hex'), - secretNumber: secretNumber.toString('hex'), - networkName: networkName, - } - } catch (e) { - log.debug(e.message); - return false; - } -}; - - -Wallet.prototype._setupBlockchainHandlers = function() { - - var self = this; - self.blockchain.removeAllListeners(); - self.subscribeToAddresses(); - - log.debug('Setting Blockchain listeners for', this.getName()); - self.blockchain.on('reconnect', function(attempts) { - log.debug('Wallet:' + self.id + 'blockchain reconnect event'); - self.clearUnspentCache(); - self.emitAndKeepAlive('insightReconnected'); - }); - - self.blockchain.on('disconnect', function() { - log.debug('Wallet:' + self.id + 'blockchain disconnect event'); - self.emitAndKeepAlive('insightError'); - }); - - self.blockchain.on('tx', function(tx) { - log.debug('Wallet:' + self.id + ' blockchain tx event'); - var addresses = self.getAddresses(); - // This should always be >=0 - if (_.indexOf(addresses, tx.address) >= 0) { - self.clearUnspentCache(); - self.emitAndKeepAlive('tx', tx.address, self.addressIsChange(tx.address)); - } - }); - - if (!self.spendUnconfirmed) { - - // TODO HERE should only clean utxos if there are some wallet - // transactions waiting for confirmation (ie confirmation < min confirmation) - self.clearUnspentCache(); - - self.blockchain.on('block', self.emitAndKeepAlive.bind(self, 'balanceUpdated')); - } -} - -Wallet.prototype._setupNetworkHandlers = function() { - var self = this; - - var net = this.network; - net.removeAllListeners(); - net.on('connect', self._onConnect.bind(self)); - net.on('data', self._onData.bind(self)); - net.on('no_messages', self._onNoMessages.bind(self)); - net.on('connect_error', function() { - self.emitAndKeepAlive('connectionError'); - }); -}; - -/** - * @desc Sets up the networking with other peers. - * - * @emits connect - * @emits data - * - * @emits ready - * - */ -Wallet.prototype.netStart = function() { - var self = this; - - if (self.netStarted) - return; - - - self._setupBlockchainHandlers(); - self.netStarted = true; - - if (!this.isShared()) { - self.emitAndKeepAlive('ready'); - return; - } - - self._setupNetworkHandlers(); - - var myId = self.getMyCopayerId(); - var myIdPriv = self.getMyCopayerIdPriv(); - var startOpts = { - copayerId: myId, - privkey: myIdPriv, - maxPeers: self.totalCopayers, - syncedTimestamp: this.syncedTimestamp || 0, - secretNumber: self.secretNumber, - }; - - var wasIncomplete; - if (this.publicKeyRing.isComplete()) { - this._lockIncomming(this.publicKeyRing.getAllCopayerIds()); - } else { - //Partially complete wallet. - if (this.publicKeyRing.getAllCopayerIds().length > 1) { - this.network.setCopayers(this.publicKeyRing.getAllCopayerIds()); - wasIncomplete = true; - } - } - - log.debug('Wallet:' + self.id + ' Starting network.'); - this.network.start(startOpts, function() { - //Partially complete wallet. - if (wasIncomplete) { - log.debug('Incomplete wallet opened:' + self.getName() + '. forced peer sync from 0'); - self.sendWalletReady(null, 0); - } - self.emitAndKeepAlive(self.isComplete() ? 'ready' : 'waitingCopayers'); - }); -}; - -/** - * @desc Retrieves the public keys of all the copayers in the ring - * @return {string[]} hex-encoded public keys of copayers - */ -Wallet.prototype.getRegisteredCopayerIds = function() { - var l = this.publicKeyRing.registeredCopayers(); - var copayers = []; - for (var i = 0; i < l; i++) { - var cid = this.getCopayerId(i); - copayers.push(cid); - } - return copayers; -}; - -/** - * @desc Retrieves the public keys of all the peers in the network - * @TODO: Isn't this deprecated? Now that we don't use peerjs - * - * @return {string[]} hex-encoded public keys of copayers - */ -Wallet.prototype.getRegisteredPeerIds = function() { - var l = this.publicKeyRing.registeredCopayers(); - if (this.registeredPeerIds.length !== l) { - this.registeredPeerIds = []; - var copayers = this.getRegisteredCopayerIds(); - for (var i = 0; i < l; i++) { - var cid = copayers[i]; - var pid = this.network.peerFromCopayer(cid); - this.registeredPeerIds.push({ - peerId: pid, - copayerId: cid, - nick: this.publicKeyRing.nicknameForCopayer(cid), - index: i, - }); - } - } - return this.registeredPeerIds; -}; - -/** - * @TODO: Review design of this call - * @desc Send a keepalive to this wallet's {@link WalletLock} instance. - * - * @emits locked - in case the wallet is opened in another instance - */ -Wallet.prototype.keepAlive = function() { - var self = this; - - - // this.lock.keepAlive(function(err) { - // if (err) { - // log.debug(err); - // self.emitAndKeepAlive('locked', null, 'Wallet appears to be openned on other browser instance. Closing this one.'); - // } - // }); -}; - - - -Wallet.prototype.getId = function() { - return this.id; -}; - -/** - * @desc Serialize the wallet into a plain object. - * @return {Object} - */ -Wallet.prototype.toObj = function() { - var optsObj = this._optsToObj(); - - var walletObj = { - opts: optsObj, - settings: this.settings, - networkNonce: this.network.getHexNonce(), //yours - networkNonces: this.network.getHexNonces(), //copayers - publicKeyRing: this.publicKeyRing.toObj(), - txProposals: this.txProposals.toObj(), - privateKey: this.privateKey ? this.privateKey.toObj() : undefined, - addressBook: this.addressBook, - syncedTimestamp: this.syncedTimestamp || 0, - secretNumber: this.secretNumber, - }; - - return walletObj; -}; - -/** - * @desc: returns the sizes, by component, of a wallet, in bytes. - * - * @return {object} sizes by component name and 'total' for the total wallet size. - */ -Wallet.prototype.sizes = function() { - var obj = this.toObj(); - var sizes = {}, - total = 0; - _.each(obj, function(val, key) { - var s = JSON.stringify(val).length; - sizes[key] = s; - total += s; - }); - sizes.total = total; - return sizes; -}; - -Wallet.fromUntrustedObj = function(obj, readOpts) { - obj = _.clone(obj); - var o = {}; - _.each(Wallet.PERSISTED_PROPERTIES, function(p) { - o[p] = obj[p]; - }); - - return Wallet.fromObj(o, readOpts); -}; - -/** - * @desc Retrieve the wallet state from a trusted object - * - * @param {Object} o - * @param {Object[]} o.addressBook - Stores known associations of bitcoin addresses to names - * @param {Object} o.privateKey - Private key to be deserialized by {@link PrivateKey#fromObj} - * @param {string} o.networkName - 'livenet' or 'testnet' - * @param {Object} o.publicKeyRing - PublicKeyRing to be deserialized by {@link PublicKeyRing#fromObj} - * @param {number} o.syncedTimestamp - ts of the last synced message with insifht (in microseconds, as insight returns ts) - * @param {Object} o.txProposals - TxProposals to be deserialized by {@link TxProposals#fromObj} - * @param {string} o.nickname - user's nickname - * - * @param readOpts.network - * @param readOpts.blockchain - * @param readOpts.{string[]} skipFields - parameters to ignore when importing - */ -Wallet.fromObj = function(o, readOpts) { - - preconditions.checkArgument(readOpts.networkOpts); - preconditions.checkArgument(readOpts.blockchainOpts); - - var networkOpts = readOpts.networkOpts; - var blockchainOpts = readOpts.blockchainOpts; - var skipFields = readOpts.skipFields || []; - - - if (skipFields) { - _.each(skipFields, function(k) { - if (o[k]) { - delete o[k]; - } else { - throw new Error('unknown field:' + k); - } - }); - } - - var networkName = Wallet.obtainNetworkName(o); - - - // TODO Why moving everything to opts. This needs refactoring. - // - // clone opts - - if (!o.opts) { - return null; - } - - var opts = JSON.parse(JSON.stringify(o.opts)); - opts.addressBook = o.addressBook; - opts.settings = o.settings; - - - if (o.privateKey) { - opts.privateKey = PrivateKey.fromObj(o.privateKey); - } else { - opts.privateKey = new PrivateKey({ - networkName: networkName - }); - } - - opts.secretNumber = o.secretNumber; - - if (o.publicKeyRing) { - opts.publicKeyRing = PublicKeyRing.fromObj(o.publicKeyRing); - } else { - opts.publicKeyRing = new PublicKeyRing({ - networkName: networkName, - requiredCopayers: opts.requiredCopayers, - totalCopayers: opts.totalCopayers, - }); - opts.publicKeyRing.addCopayer( - opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(), - opts.nickname - ); - } - - if (o.txProposals) { - opts.txProposals = TxProposals.fromObj(o.txProposals, Wallet.builderOpts); - } else { - opts.txProposals = new TxProposals({ - networkName: networkName, - }); - } - - opts.syncedTimestamp = opts.publicKeyRing.isComplete() ? o.syncedTimestamp || 0 : 0; - opts.blockchainOpts = readOpts.blockchainOpts; - opts.networkOpts = readOpts.networkOpts; - - return new Wallet(opts); -}; - - -/** - * @desc sends a message to peers - * @param {string[]} recipients - the pubkey of the recipients of the message. Null for sending to all peers. - * @param {Object} obj - the data to be sent to them. - * @param {String} obj.type - Type of the message to be send - */ -Wallet.prototype._sendToPeers = function(recipients, obj) { - if (!this.isShared()) return; - log.info('Wallet:' + this.getName() + ' ### Sending ' + obj.type); - log.debug('Sending:', recipients, obj); - - this.network.send(recipients, obj); -}; - -/** - * @desc Send the set of TxProposals to peers - * @param {string[]} recipients - the pubkeys of the recipients - */ -Wallet.prototype.sendAllTxProposals = function(recipients, sinceTs) { - var ntxids = sinceTs ? this.txProposals.getNtxidsSince(sinceTs) : this.txProposals.getNtxids(); - var self = this; - _.each(ntxids, function(ntxid, key) { - self.sendTxProposal(ntxid, recipients); - }); -}; - -/** - * @desc Send a TxProposal identified by transaction id to a set of recipients - * @param {string} ntxid - the transaction proposal id - * @param {string[]} [recipients] - the pubkeys of the recipients - */ -Wallet.prototype.sendTxProposal = function(ntxid, recipients) { - preconditions.checkArgument(ntxid); - var indexes = HDParams.serialize(this.publicKeyRing.indexes); - this._sendToPeers(recipients, { - type: 'txProposal', - txProposal: this.txProposals.get(ntxid).toObjTrim(), - walletId: this.id, - indexes: indexes, - }); -}; - -/** - * @desc Notifies other peers that a transaction proposal was seen - * @param {string} ntxid - */ -Wallet.prototype.sendSeen = function(ntxid) { - preconditions.checkArgument(ntxid); - this._sendToPeers(null, { - type: 'seen', - ntxid: ntxid, - walletId: this.id, - }); -}; - -/** - * @desc Notifies other peers that a transaction proposal was rejected - * @param {string} ntxid - */ -Wallet.prototype.sendReject = function(ntxid) { - preconditions.checkArgument(ntxid); - - this._sendToPeers(null, { - type: 'reject', - ntxid: ntxid, - walletId: this.id, - }); -}; - - -/** - * @desc Send a signature for a TX Proposal - * @param {string} ntxid - */ -Wallet.prototype.sendSignature = function(ntxid) { - preconditions.checkArgument(ntxid); - - var txp = this.txProposals.get(ntxid); - var signatures = txp.getMySignatures(); - preconditions.checkState(signatures && signatures.length); - - this._sendToPeers(null, { - type: 'signature', - ntxid: ntxid, - signatures: signatures, - walletId: this.id, - }); -}; - - -/** - * @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 - */ -Wallet.prototype.sendWalletReady = function(recipients, sinceTs) { - this._sendToPeers(recipients, { - type: 'walletReady', - walletId: this.id, - sinceTs: sinceTs - }); -}; - -/** - * @desc Notify other peers of the walletId - * @TODO: Why is this needed? Can't everybody just calculate the walletId? - * @param {string[]} [recipients] - the pubkeys of the recipients - */ -Wallet.prototype.sendWalletId = function(recipients) { - this._sendToPeers(recipients, { - type: 'walletId', - walletId: this.id, - opts: this._optsToObj(), - networkName: this.getNetworkName(), - }); -}; - -/** - * @desc Send the current PublicKeyRing to other recipients - * @param {string[]} [recipients] - the pubkeys of the recipients - */ -Wallet.prototype.sendPublicKeyRing = function(recipients) { - var publicKeyRingObj = this.publicKeyRing.toTrimmedObj(); - - this._sendToPeers(recipients, { - type: 'publicKeyRing', - publicKeyRing: publicKeyRingObj, - walletId: this.id, - }); -}; - -/** - * @desc Send the current indexes of our public key ring to other peers - * @param {string[]} recipients - the pubkeys of the recipients - */ -Wallet.prototype.sendIndexes = function(recipients) { - var indexes = HDParams.serialize(this.publicKeyRing.indexes); - this._sendToPeers(recipients, { - type: 'indexes', - indexes: indexes, - walletId: this.id, - }); -}; - -/** - * sendAddressBook - * @desc Send our addressBook to other recipients - * - * @param {string[]} recipients - the pubkeys of the recipients - * @param onlyKey - * @return {undefined} - */ -Wallet.prototype.sendAddressBook = function(recipients, onlyKey) { - var toSend = [], - myId = this.getMyCopayerId(); - - if (onlyKey && this.addressBook[onlyKey]) { - toSend = {}; - toSend[onlyKey] = this.addressBook[onlyKey]; - } else { - toSend = _.filter(this.addressBook, function(entry) { - return entry.copayerId === myId; - }); - } - if (_.isEmpty(toSend)) return; - - this._sendToPeers(recipients, { - type: 'addressbook', - addressBook: toSend, - walletId: this.id, - }); -}; - -/** - * @desc Retrieve this wallet's name - * @return {string} - */ -Wallet.prototype.getName = function() { - return this.name || this.id; -}; - -/** - * @desc Generate a new address - * @param {boolean} isChange - whether to generate a change address or a receive address - * @return {string[]} a list of all the addresses generated so far for the wallet - */ -Wallet.prototype._doGenerateAddress = function(isChange) { - var addr = this.publicKeyRing.generateAddress(isChange, this.publicKey); - this.subscribeToAddresses(); - this.emitAndKeepAlive('newAddresses'); - return addr; -}; - -/** - * @desc Generate a new address - * @param {boolean} isChange - whether to generate a change address or a receive address - * @return {string[]} a list of all the addresses generated so far for the wallet - */ -Wallet.prototype.generateAddress = function(isChange) { - var addr = this._doGenerateAddress(isChange); - this.sendIndexes(); - return addr; -}; - - -/** - * @desc get list of actions (see {@link getPendingTxProposals}) - */ -Wallet.prototype._getActionList = function(txp) { - preconditions.checkArgument(txp); - - var self = this; - var peers = []; - - _.each(self.getRegisteredCopayerIds(), function(copayerId) { - var actions = { - rejected: txp.rejectedBy[copayerId], - sign: txp.signedBy[copayerId], - seen: txp.seenBy[copayerId], - create: (txp.creator === copayerId) ? txp.createdTs : null, - }; - peers.push({ - cId: copayerId, - actions: actions, - }); - }); - return peers; -}; - -/** - * @desc Retrieve Pendings Transaction proposals (see {@link TxProposals}) - * @return {Object[]} each object returned represents a transaction proposal - */ -Wallet.prototype.getPendingTxProposalsCount = function() { - var self = this; - var txps = this.txProposals.txps; - var maxRejectCount = this.maxRejectCount(); - var myId = this.getMyCopayerId(); - var pending = 0, - pendingForUs = 0; - - _.each(txps, function(inTxp, ntxid) { - if (!inTxp.isPending(maxRejectCount)) - return; - // TODO: are the uxtos availables? - // - pending++; - - if (!inTxp.signedBy[myId] && !inTxp.rejectedBy[myId]) - pendingForUs++ - }); - - - return { - pending: pending, - pendingForUs: pendingForUs, - }; -}; - - -/** - * @desc Retrieve Pendings Transaction proposals (see {@link TxProposals}) - * @return {Object[]} each object returned represents a transaction proposal - */ -Wallet.prototype.getPendingTxProposals = function() { - var self = this; - var ret = []; - var txps = this.txProposals.txps; - var maxRejectCount = this.maxRejectCount(); - var satToUnit = 1 / this.settings.unitToSatoshi; - - _.each(txps, function(inTxp, ntxid) { - if (!inTxp.isPending(maxRejectCount) || (inTxp.sentTs && inTxp.isFullySigned())) - return; - - var txp = _.clone(inTxp); - txp.ntxid = ntxid; - - var addresses = {}; - var outs = JSON.parse(txp.builder.vanilla.outs); - outs.forEach(function(o) { - if (!addresses[o.address]) addresses[o.address] = 0; - addresses[o.address] += (o.amountSatStr || Math.round(o.amount * bitcore.util.COIN)); - }); - txp.outs = []; - _.each(addresses, function(value, address) { - txp.outs.push({ - address: address, - value: value * satToUnit - }); - }); - // extra fields - txp.fee = txp.builder.feeSat * satToUnit; - txp.missingSignatures = txp.builder.build().countInputMissingSignatures(0); - txp.actionList = self._getActionList(txp); - ret.push(txp); - }); - - return ret; -}; - -/** - * @desc Removes old transactions - * @param {boolean} deleteAll - if true, remove all the transactions - * @return {number} the number of deleted proposals - */ -Wallet.prototype.purgeTxProposals = function(deleteAll) { - var deleted = this.txProposals.purge(deleteAll, this.maxRejectCount()); - if (deleted) { - this.emitAndKeepAlive('txProposalEvent', { - type: deleteAll ? Wallet.TX_PURGED : Wallet.TX_ALL_PURGED, - cId: this.getMyCopayerId(), - }); - } - return deleted; -}; - -/** - * @desc Reject a proposal - * @param {string} ntxid the id of the transaction proposal to reject - * @emits txProposalsEvent - */ -Wallet.prototype.reject = function(ntxid, cb) { - var txp = this.txProposals.get(ntxid); - txp.setRejected(this.getMyCopayerId()); - this.sendReject(ntxid); - - this.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_REJECTED, - cId: this.getMyCopayerId(), - }); - - // TODO this callback should be triggered by sendRejected, which is trully async - if (cb) - cb(null, Wallet.TX_REJECTED); -}; - -/** - * @callback signCallback - * @param {Error} error if any - * @param {number} Transaction ID or Transaction Proposal ID - * @param {status} Wallet.TX_* Status - */ - - -/** - * @desc Signs a transaction proposal - * @param {string} ntxid the id of the transaction proposal to sign - * @throws {Error} Could not sign proposal - * @throws {Error} Bad payment request - * @return {boolean} true if signing actually incremented the number of signatures - */ -Wallet.prototype.sign = function(ntxid) { - preconditions.checkState(!_.isUndefined(this.getMyCopayerId())); - - var txp = this.txProposals.get(ntxid); - var keys = this.privateKey.getForPaths(txp.inputChainPaths); - var signaturesAdded = txp.sign(keys, this.getMyCopayerId()); - if (!signaturesAdded) - return false; - - return true; -}; - -Wallet.prototype.issueTxIfComplete = function(ntxid, cb) { - var txp = this.txProposals.get(ntxid); - var tx = txp.builder.build(); - if (tx.isComplete()) { - this.issueTx(ntxid, cb); - } else { - return cb(); - } -}; - - -/** - * - * @desc signs and send or broadcast a transaction. - * In m-n wallets, - * if m==1 it will broadcast it to the Bitcoin Network - * if n>1 it will send the proposal to the peers - * - * @param ntxid Transaction Proposal Id - * @param {signCallback} cb - * @throws {Error} Could not sign proposal - * @emits txProposalEvent - */ -Wallet.prototype.signAndSend = function(ntxid, cb) { - var self = this; - - if (this.sign(ntxid)) { - - this.sendSignature(ntxid); - this.issueTxIfComplete(ntxid, function(err, txid, status) { - - // We did not broadcast the TX, only signed it. - if (!txid) { - self.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_SIGNED, - cId: self.getMyCopayerId(), - }); - } - - return cb(err, txid, status ? status : Wallet.TX_SIGNED); - }); - } else { - return cb(new Error('Could not sign the proposal')); - } -}; - - -/** - * @desc Broadcast a tx proposal. In case of failure, check if the resulting - * transactions is already on the blockchain. - * - * @param ntxid - * @param cb - * @return {undefined} - */ -Wallet.prototype.broadcastToBitcoinNetwork = function(ntxid, cb) { - var self = this; - var txp = this.txProposals.get(ntxid); - - var tx = txp.builder.build(); - preconditions.checkState(tx.isComplete(), 'tx is not complete'); - - var txHex = tx.serialize().toString('hex'); - - log.info('Wallet:' + this.id + ' Broadcasting Transaction ntxid:' + ntxid); - log.debug('\tRaw transaction: ', txHex); - - this.blockchain.broadcast(txHex, function(err, txid) { - if (err || !txid) { - log.debug('Wallet:' + self.getName() + ' Send failed:' + err); - - self._checkIfTxIsSent(ntxid, function(err, txid) { - self.emitAndKeepAlive('balanceUpdated'); - return cb(err, txid); - }); - } else { - log.info('Wallet:' + self.getName() + ' broadcasted a TX! TXID:', txid); - return cb(null, txid); - } - }); -}; - -/** - * @desc Broadcasts a transaction to the blockchain, updates tx transactions - * sent status. If the tx proposal is a payment protocol request,it will also - * send the payment message to the server,and process the response. - * - * @param {string} ntxid - the transaction proposal id - * @param {string} txid - the transaction id on the blockchain - * @param {signCallback} cb - */ -Wallet.prototype.issueTx = function(ntxid, cb) { - var self = this; - - self.broadcastToBitcoinNetwork(ntxid, function(err, txid) { - if (err) return cb(err); - preconditions.checkState(txid); - - var txp = self.txProposals.get(ntxid); - txp.setSent(txid); - self.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_BROADCASTED, - cId: self.getMyCopayerId(), - }); - - // PAYPRO: Payment message is optional, only if payment_url is set - // This is async. and will notify and update txp async. - if (txp.merchant && txp.merchant.pr.pd.payment_url) { - var data = self.createPayProPayment(txp); - self.sendPayProPayment(txp, data, function(err, data) { - if (err) return cb(err); - self.onPayProPaymentAck(ntxid, data); - }); - } - return cb(null, txid, Wallet.TX_BROADCASTED); - }); -}; - -/** - * @callback {fetchPaymentRequestCallback} - * @param {string=} err - an error, if any - * @param {Object} merchantData - object representing the payment request. Add described on BIP70 merchant_data - */ - -/** - * @desc Creates a Payment Protocol transaction - * @param {Object|string} options - if it's a string, parse it as the url - * @param {string} options.url the url for the transaction - * @return {fetchPaymentRequestCallback} cb - */ -Wallet.prototype.fetchPaymentRequest = function(options, cb) { - preconditions.checkArgument(_.isObject(options)); - preconditions.checkArgument(options.url); - preconditions.checkArgument(options.url.indexOf('http') == 0, 'Bad PayPro URL given:' + options.url); - var self = this; - - if (self.cache.paymentRequests[options.url]) - return cb(null, self.cache.paymentRequests[options.url]); - - this.httpUtil.request({ - method: 'GET', - url: options.url, - headers: { - 'Accept': PayPro.PAYMENT_REQUEST_CONTENT_TYPE - }, - responseType: 'arraybuffer' - }) - .success(function(rawData) { - log.info('PayPro Request done successfully. Parsing response') - - var merchantData, err; - try { - merchantData = self.parsePaymentRequest(options, rawData); - } catch (e) { - err = e - }; - - log.debug('PayPro request data', merchantData); - - self.cache.paymentRequests[options.url] = merchantData; - return cb(err, merchantData); - }) - .error(function(data, status) { - log.debug('Server did not return PaymentRequest.\nXHR status: ' + status); - return cb(new Error('Status: ' + status)); - }); -}; - - -/** - * _addOutputsToMerchantData - * - * @desc parses merchant_data internal output representation and stores - * the result in merchant_data.outs = [{address: xx, amountSatStr: xx}], - * to be compatible with TransactionBuilder. - *` - * @param merchantData BIP70 merchant_data (from the payment request) - * @throws {Error} PayPro: Unsupported inputs - * @return {undefined} - */ -Wallet.prototype._addOutputsToMerchantData = function(merchantData) { - - var total = bignum(0); - var outs = {}; - - _.each(merchantData.pr.pd.outputs, function(output) { - var amount = output.amount; - - // big endian - var v = new Buffer(8); - v[0] = (amount.high >> 24) & 0xff; - v[1] = (amount.high >> 16) & 0xff; - v[2] = (amount.high >> 8) & 0xff; - v[3] = (amount.high >> 0) & 0xff; - v[4] = (amount.low >> 24) & 0xff; - v[5] = (amount.low >> 16) & 0xff; - v[6] = (amount.low >> 8) & 0xff; - v[7] = (amount.low >> 0) & 0xff; - - var script = { - offset: output.script.offset, - limit: output.script.limit, - buffer: new Buffer(output.script.buffer, 'hex') - }; - var s = script.buffer.slice(script.offset, script.limit); - var network = merchantData.pr.pd.network === 'main' ? 'livenet' : 'testnet'; - var addr = bitcore.Address.fromScriptPubKey(new bitcore.Script(s), network); - - var a = addr[0].toString(); - outs[a] = bignum.fromBuffer(v, { - endian: 'big', - size: 1 - }).add(outs[a] || bignum(0)); - - total = total.add(bignum.fromBuffer(v, { - endian: 'big', - size: 1 - })); - }); - - // for now we only support PayPro with 1 output. - if (_.size(outs) !== 1) - throw new Error('PayPro: Unsupported outputs'); - - var out = _.pairs(outs)[0]; - - merchantData.outs = [{ - address: out[0], - amountSatStr: out[1].toString(10), - }]; - merchantData.total = total.toString(10); - - // If user is granted the privilege of choosing - // their own amount, add it to the tx. - if (merchantData.total == "0" && options.amount) { - merchant.outs[0].amountSatStr = merchantData.total = options.amount; - } -}; - -/** - * @desc Analyzes a payment request and generate merchantData - * @param {Object} options - * @param {string} options.url url where the pay request was acquired - * @param {string} options.amount Only used if pay requesst allow user to set the amount - * @param {PayProRequest} rawData - */ -Wallet.prototype.parsePaymentRequest = function(options, rawData) { - var self = this; - - var data = PayPro.PaymentRequest.decode(rawData); - var paypro = new PayPro(); - var pr = paypro.makePaymentRequest(data); - var ver = pr.get('payment_details_version'); - var pki_type = pr.get('pki_type'); - var pki_data = pr.get('pki_data'); - var details = pr.get('serialized_payment_details'); - var sig = pr.get('signature'); - - var certs = PayPro.X509Certificates.decode(pki_data); - certs = certs.certificate; - - // Verify Signature - var trust = pr.verify(true); - - if (!trust.verified) { - throw new Error('Server sent a bad signature.'); - } - - details = PayPro.PaymentDetails.decode(details); - var pd = new PayPro(); - pd = pd.makePaymentDetails(details); - - var network = pd.get('network'); - var outputs = pd.get('outputs'); - var time = pd.get('time'); - var expires = pd.get('expires'); - var memo = pd.get('memo'); - var payment_url = pd.get('payment_url'); - var merchant_data = pd.get('merchant_data'); - - var total = bignum('0', 10).toString(10); - var merchantData = { - pr: { - payment_details_version: ver, - pki_type: pki_type, - pki_data: certs, - pd: { - network: network, - outputs: outputs.map(function(output) { - return { - amount: output.get('amount'), - script: { - offset: output.get('script').offset, - limit: output.get('script').limit, - // NOTE: For some reason output.script.buffer - // is only an ArrayBuffer - buffer: new Buffer(new Uint8Array( - output.get('script').buffer)).toString('hex') - } - }; - }), - time: time, - expires: expires, - memo: memo || 'This server would like some BTC from you.', - payment_url: payment_url, - merchant_data: merchant_data ? merchant_data.toString('hex') : null - }, - signature: sig.toString('hex'), - ca: trust.caName, - untrusted: !trust.caTrusted, - selfSigned: trust.selfSigned - }, - expires: expires, - request_url: options.url, - domain: /^(?:https?)?:\/\/([^\/:]+).*$/.exec(options.url)[1], - total: total, - expirationDate: expires ? new Date(expires * 1000) : null, - }; - - this._addOutputsToMerchantData(merchantData, options.amount); - - return merchantData; -}; - -/** - * _getPayProRefundOutputs - * Create refund outputs for a PayPro Payment Message - * Uses current transaction's change address. - * - * @param txp - * @return {undefined} - */ -Wallet.prototype._getPayProRefundOutputs = function(txp) { - var pkr = this.publicKeyRing; - var amount = +txp.merchant.total.toString(10); - - var output = new PayPro.Output(); - var opts = JSON.parse(txp.builder.vanilla.opts); - if (!opts.remainderOut) { - log.warn('no remainder set. Not setting refund in PayPro'); - return; - } - var addrStr = opts.remainderOut.address; - var addr = new bitcore.Address(addrStr); - var script = bitcore.Script.createP2SH(addr.payload()).getBuffer(); - log.debug('PayPro refund address set to:' + addrStr); - - output.set('script', script); - output.set('amount', amount); - return [output]; -}; - - -/** - * - * @desc Creates a Payment Protocol Payment message for the given TX Proposal - * @param txp Transaction Proposal - * @param txHex - * @return {undefined} - */ -Wallet.prototype.createPayProPayment = function(txp) { - - var tx = txp.builder.build(); - var txBuf = tx.serialize(); - - - // We send this to the serve after receiving a PaymentRequest - var pay = new PayPro(); - pay = pay.makePayment(); - - var merchant_data = txp.merchant.pr.pd.merchant_data; - if (merchant_data) { - merchant_data = new Buffer(merchant_data, 'hex'); - pay.set('merchant_data', merchant_data); - } - pay.set('transactions', [txBuf]); - - var refund_outputs = this._getPayProRefundOutputs(txp); - if (refund_outputs) - pay.set('refund_to', refund_outputs); - - // Unused for now - // options.memo = ''; - // pay.set('memo', options.memo); - - pay = pay.serialize(); - var buf = new ArrayBuffer(pay.length); - var view = new Uint8Array(buf); - for (var i = 0; i < pay.length; i++) { - view[i] = pay[i]; - } - - return view; -}; - - -/** - * onPayProPaymentAck - * - * @desc parse and process a Payment Protocol Payment Ack. Updates - * given TX Proposal with merchant's memo and send it to copayers - * - * @param ntxid ID of the Transaction Proposal - * @param rawData of the Payment Ack - * @emits paymentACK - (merchants's memo) - */ -Wallet.prototype.onPayProPaymentAck = function(ntxid, rawData) { - var data = PayPro.PaymentACK.decode(rawData); - var paypro = new PayPro(); - var ack = paypro.makePaymentACK(data); - var memo = ack.get('memo'); - log.debug('Payment Acknowledged!: %s', memo); - - var txp = this.txProposals.get(ntxid); - txp.paymentAckMemo = memo; - this.sendTxProposal(ntxid); - this.emitAndKeepAlive('paymentACK', memo); -}; - - -/** - * @desc Send a payment transaction to a merchant, complying with BIP70 - * on Acknoledge, updates the TX Proposal with server's memo and send it - * to peers - * - * @param {string} ntxid - the transaction proposal ID for with the - * - */ -Wallet.prototype.sendPayProPayment = function(txp, data, cb) { - var self = this; - - log.debug('Sending Payment Message to merchant server'); - var postInfo = { - method: 'POST', - url: txp.merchant.pr.pd.payment_url, - headers: { - // BIP-71 - 'Accept': PayPro.PAYMENT_ACK_CONTENT_TYPE, - 'Content-Type': PayPro.PAYMENT_CONTENT_TYPE - // XHR does not allow these: - // 'Content-Length': (pay.byteLength || pay.length) + '', - // 'Content-Transfer-Encoding': 'binary' - }, - // Technically how this should be done via XHR (used to - // be the ArrayBuffer, now you send the View instead). - data: data, - responseType: 'arraybuffer' - }; - - this.httpUtil.request(postInfo) - .success(function(rawData) { - return cb(null, rawData); - }) - .error(function(data, status) { - log.error('Sending payment notification: XHR status: ' + status); - return cb(new Error(status)); - }); -}; - -/** - * @desc Mark that a user has seen a given TxProposal - * @return {boolean} true if the internal state has changed - */ -Wallet.prototype.addSeenToTxProposals = function() { - var ret = false; - var myId = this.getMyCopayerId(); - - for (var k in this.txProposals.txps) { - var txp = this.txProposals.txps[k]; - if (!txp.seenBy[myId]) { - - txp.seenBy[myId] = Date.now(); - ret = true; - } - } - return ret; -}; - -/** - * @desc Alias for {@link PublicKeyRing#getAddresses} - * @return {Buffer[]} - */ -Wallet.prototype.getAddresses = function() { - return this.publicKeyRing.getAddresses(); -}; - - -/** - * @desc gets the list of addresses, ordered for the caller: - * 1) himselfs first - * 2) receive address first - * 3) last created first - */ -Wallet.prototype.getAddressesOrdered = function() { - return this.publicKeyRing.getAddressesOrdered(this.publicKey); -}; - -/** - * @desc Alias for {@link PublicKeyRing#getAddresses} - * @return {Buffer[]} - */ -Wallet.prototype.getReceiveAddresses = function() { - return this.publicKeyRing.getReceiveAddresses(); -}; - - -Wallet.prototype.subscribeToAddresses = function() { - if (!this.publicKeyRing.isComplete()) return; - - var addresses = this.getAddresses(); - this.blockchain.subscribe(addresses); - log.debug('Wallet:' + this.getName() + ' Subscribed to:' + addresses.length + ' addresses'); -}; - -/** - * @desc Returns true if a given address was generated by deriving our master public key - * @return {boolean} - */ -Wallet.prototype.addressIsOwn = function(addrStr) { - return this.publicKeyRing.addressIsOwn(addrStr); -}; - - -/** - * @desc Returns true if a given address is a change address (remainder) - * @param addrStr - * @return {boolean} - */ -Wallet.prototype.addressIsChange = function(addrStr) { - return this.publicKeyRing.addressIsChange(addrStr); -}; - - - -/** - * Estimate a tx fee in satoshis given its input count - * (only used when spending all wallet funds) - */ -Wallet.estimatedFee = function(unspentCount) { - preconditions.checkArgument(_.isNumber(unspentCount)); - var estimatedSizeKb = Math.ceil((500 + unspentCount * 250) / 1024); - return parseInt(estimatedSizeKb * bitcore.TransactionBuilder.FEE_PER_1000B_SAT); -}; - - -/** - * @callback {getBalanceCallback} - * @param {string=} err - an error, if any - * @param {number} balance - total number of satoshis for all addresses - * @param {Object} balanceByAddr - maps string addresses to satoshis - * @param {number} safeBalance - total number of satoshis in UTXOs that are not part of any TxProposal - * @param {number} safeUnspentCount - total number of safe unspent Outputs that make this balance. - */ - -/** - * computeBalance - * - * @param safeUnspent - * @param unspent - * @param {getBalanceCallback} cb - */ -Wallet.prototype.computeBalance = function(safeUnspent, unspent, cb) { - var balance = 0; - var safeBalance = 0; - var balanceByAddr = {}; - var COIN = coinUtil.COIN; - - for (var i = 0; i < unspent.length; i++) { - var u = unspent[i]; - var amt = u.amount * COIN; - balance += amt; - balanceByAddr[u.address] = (balanceByAddr[u.address] || 0) + amt; - } - - // we multiply and divide by BIT to avoid rounding errors when adding - for (var a in balanceByAddr) { - balanceByAddr[a] = parseInt(balanceByAddr[a].toFixed(0), 10); - } - - balance = parseInt(balance.toFixed(0), 10); - - var safeUnspentCount = safeUnspent.length; - - for (var i = 0; i < safeUnspentCount; i++) { - var u = safeUnspent[i]; - var amt = u.amount * COIN; - safeBalance += amt; - } - - safeBalance = parseInt(safeBalance.toFixed(0), 10); - return cb(null, balance, balanceByAddr, safeBalance, safeUnspentCount); -}; - -/** - * @desc Returns the balances for all addresses in Satoshis - * @param {getBalanceCallback} cb - */ -Wallet.prototype.getBalance = function(cb) { - var self = this; - - this.getUnspent(function(err, safeUnspent, unspent) { - if (err) { - return cb(err); - } - self.computeBalance(safeUnspent, unspent, cb); - }); -}; - - -// See -// https://github.com/bitpay/copay/issues/1056 -// -// maxRejectCount should equal requiredCopayers -// strictly. -/** - * @desc Get the number of copayers that need to reject a transaction so it can't be signed - * @return {number} - */ -Wallet.prototype.maxRejectCount = function() { - return this.totalCopayers - this.requiredCopayers; -}; - - -Wallet.prototype.clearUnspentCache = function() { - log.debug('Cleaning unspent cache'); - this.cache.unspent = null; -}; - -/** - * @callback getUnspentCallback - * @desc Get a list of unspent transaction outputs - * @param {string} error - * @param {Object[]} safeUnspendList - * @param {Object[]} unspentList - * @param {getUnspentCallback} cb - */ - -Wallet.prototype.getUnspent = function(cb) { - var self = this; - - - if (self.cache.unspent != null) { - log.debug('Wallet ' + this.getName() + ': Get unspent cache hit'); - return self.computeUnspent(self.cache.unspent, cb); - } - - var addresses = this.getAddresses(); - log.debug('Wallet ' + this.getName() + ': Getting unspents from ' + addresses.length + ' addresses'); - this.blockchain.getUnspent(addresses, function(err, unspentList) { - if (err) - return cb(err); - - self.cache.unspent = unspentList; - return self.computeUnspent(self.cache.unspent, cb); - }); -}; - -/** - * - * @callback getUnspentCallback - * @param {string} error - * @param {Object[]} safeUnspendList - * @param {Object[]} unspentList - */ -/** - * computeUnspent - * - * @param unspentList List of unprocessed utxos. - * @param {getUnspentCallback} cb - */ -Wallet.prototype.computeUnspent = function(unspentList, cb) { - var self = this; - - var safeUnspendList = []; - var uu = this.txProposals.getUsedUnspent(this.maxRejectCount()); - - _.each(unspentList, function(u) { - var name = u.txid + ',' + u.vout; - if (!uu[name] && (self.spendUnconfirmed || u.confirmations >= 1)) - safeUnspendList.push(u); - }); - - return cb(null, safeUnspendList, unspentList); -}; - -/** - * spend - * - * @desc Spends coins from the wallet - * Create a Transaction Proposal and send it - * to copayers (broadcast it in a 1-x wallet) - * @param {object} opts - * @param {string} opts.toAddress address to send coins - * @param {number} opts.amountSat amount in satoshis - * @param {string} opts.comment optional transaction proposal private comment (for copayers) - * @param {string} opts.url optional (payment protocol URL). If this is given, toAddress will be ignored, and amount could be ignored or not, depending on the payment protocol request. - * @param {signCallback} cb - */ -Wallet.prototype.spend = function(opts, cb) { - preconditions.checkArgument(_.isObject(opts)); - log.debug('create Options', opts); - - var self = this; - var toAddress = opts.toAddress; - var amountSat = opts.amountSat; - var comment = opts.comment; - var merchantData = opts.merchantData; - - - // PayPro? With given merchant data - if (opts.merchantData && !opts.toAddress) { - if (!merchantData.outs[0].address) - return cb(new Error('BADPAYPRO')); - - opts.toAddress = merchantData.outs[0].address; - opts.amountSat = parseInt(merchantData.outs[0].amountSatStr); - return self.spend(opts, cb); - } - - // PayPro? Fetch payment data and recurse - var url = opts.url; - if (url && !opts.merchantData) { - return self.fetchPaymentRequest({ - url: url, - memo: comment, - amount: amountSat, - }, function(err, merchantData) { - if (err) return cb(err); - opts.merchantData = merchantData; - return self.spend(opts, cb); - }); - } - - preconditions.checkArgument(amountSat, 'no amount'); - preconditions.checkArgument(toAddress, 'no address'); - - this.getUnspent(function(err, safeUnspent) { - if (err) { - log.info(err); - return cb(new Error('Spend: Could not get list of UTXOs')); - } - - var ntxid, txp; - try { - txp = self._createTxProposal(toAddress, - amountSat, comment, safeUnspent, opts.builderOpts); - - if (opts.merchantData) { - txp.addMerchantData(opts.merchantData); - } - } catch (e) { - log.warn(e); - return cb(e); - } - - var ntxid = self.txProposals.add(txp); - if (!ntxid) { - return cb(new Error('Error creating the transaction')); - } - - log.debug('TXP Added: ', ntxid); - // Needs only one signature? Broadcast it! - if (!self.requiresMultipleSignatures()) - return self.issueTx(ntxid, cb); - - self.sendTxProposal(ntxid); - self.emitAndKeepAlive('txProposalEvent', { - type: Wallet.TX_PROPOSAL_SENT, - cId: self.getMyCopayerId(), - }); - return cb(null, ntxid, Wallet.TX_PROPOSAL_SENT); - }); -}; - -/** - * _getAddress - * Returns an Address object from an address string or a BIP21 URL.* - * @param address - * @return { bitcore.Address } - */ - -Wallet._getAddress = function(address) { - if (/ ^ bitcoin: /g.test(address)) { - return new BIP21(address).address; - } - return new Address(address); -}; - - -Wallet.prototype._getBuilder = function(opts) { - opts = opts || {}; - - if (!opts.remainderOut) { - opts.remainderOut = { - address: this._doGenerateAddress(true).toString() - }; - } - if (_.isUndefined(opts.spendUnconfirmed)) { - opts.spendUnconfirmed = this.spendUnconfirmed; - } - - for (var k in Wallet.builderOpts) { - opts[k] = Wallet.builderOpts[k]; - } - - return new Builder(opts); -}; - - -/* - * _createTxProposal - * Creates a transaction proposal and run many sanity checks - * - * @param toAddress - * @param amountSat - * @param comment (optional) - * @param utxos - * @param builderOpts bitcore.TransactionBuilder options(like spendUnconfirmed) - * @return {TxProposal} The newly created transaction proposal.* - * Throws errors on unexpected inputs. - */ - -Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utxos, builderOpts) { - preconditions.checkArgument(toAddress); - preconditions.checkArgument(amountSat); - preconditions.checkArgument(_.isArray(utxos)); - preconditions.checkArgument(!comment || comment.length <= 100, 'Comment too long'); - - var pkr = this.publicKeyRing; - var priv = this.privateKey; - var addr = Wallet._getAddress(toAddress); - - preconditions.checkState(addr && addr.data && addr.isValid(), 'Bad address:' + addr.toString()); - - preconditions.checkArgument(addr.network().name === this.getNetworkName(), 'networkname mismatch'); - preconditions.checkState(pkr.isComplete(), 'pubkey ring incomplete'); - preconditions.checkState(priv, 'no private key'); - - var b = this._getBuilder(builderOpts); - - b.setUnspent(utxos) - .setOutputs([{ - address: addr.data, - amountSatStr: amountSat, - }]); - - var selectedUtxos = b.getSelectedUnspent(); - - if (selectedUtxos.length > TX_MAX_INS) - throw new Error('BIG: Resulting TX is too big:' + selectedUtxos.length + - ' inputs. Aborting'); - - - var inputChainPaths = selectedUtxos.map(function(utxo) { - return pkr.pathForAddress(utxo.address); - }); - b.setHashToScriptMap(pkr.getRedeemScriptMap(inputChainPaths)); - - - var tx = b.build(); - var myId = this.getMyCopayerId(); - var keys = priv.getForPaths(inputChainPaths); - return new TxProposal({ - inputChainPaths: inputChainPaths, - comment: comment, - builder: b, - creator: myId, - signWith: keys, - }); - - return txp; -}; - - -/** - * @desc Updates all the indexes for the current publicKeyRing. This scans - * the blockchain looking for transactions on derived addresses. - * - * @param {Function} callback - called when all indexes have been updated. Receives an error, if any, as first argument - */ -Wallet.prototype.updateIndexes = function(callback) { - var self = this; - if (!self.isComplete()) - return callback(); - log.debug('Wallet:' + this.id + ' Updating indexes...'); - var tasks = this.publicKeyRing.indexes.map(function(index) { - return function(callback) { - self.updateIndex(index, callback); - }; - }); - - async.parallel(tasks, function(err) { - if (err) callback(err); - log.debug('Wallet:' + self.id + ' Indexes updated'); - self.clearUnspentCache(); - self.subscribeToAddresses(); - self.emitAndKeepAlive('newAddresses'); - callback(); - }); -}; - -/** - * @desc Updates the lastly used index - * @param {Object} index - an index, as used by {@link PublicKeyRing} - * @param {Function} callback - called with no arguments when done updating - */ -Wallet.prototype.updateIndex = function(index, callback) { - var self = this; - var SCANN_WINDOW = 20; - self.indexDiscovery(index.changeIndex, true, index.copayerIndex, SCANN_WINDOW, function(err, changeIndex) { - if (err) return callback(err); - if (changeIndex != -1) - index.changeIndex = changeIndex + 1; - - self.indexDiscovery(index.receiveIndex, false, index.copayerIndex, SCANN_WINDOW, function(err, receiveIndex) { - if (err) return callback(err); - if (receiveIndex != -1) - index.receiveIndex = receiveIndex + 1; - callback(); - }); - }); -}; - -/** - * @desc Derive addresses using the given parameters - * - * @param {number} index - the index to start with - * @param {number} amount - number of addresses to derive - * @param {boolean} isChange - derive change addresses or receive addresses - * @param {number} copayerIndex - the index of the copayer for whom to derive addresses - * @return {string[]} the result of calling {@link PublicKeyRing#getAddress} - */ -Wallet.prototype.deriveAddresses = function(index, amount, isChange, copayerIndex) { - preconditions.checkArgument(amount); - preconditions.shouldBeDefined(copayerIndex); - - var ret = new Array(amount); - for (var i = 0; i < amount; i++) { - // TODO - ret[i] = this.publicKeyRing._getAddress(index + i, isChange, copayerIndex).toString(); - } - return ret; -}; - -/** - * @callback {indexDiscoveryCallback} - * @param {?} err - * @param {number} lastActivityIndex - */ -/** - * @desc Scans the block chain for the last index with activity for a copayer - * - * This function scans the publicKeyRing branch starting at index @start and reports the index with last activity, - * using a scan window of @gap. The argument @change defines the branch to scan: internal or external. - * Returns -1 if no activity is found in range. - * @param {number} start - the number for which to start scanning - * @param {boolean} change - whether to search for in the change branch or the receive branch - * @param {number} copayerIndex - the index of the copayer - * @param {number} gap - the maximum number of addresses to scan after the last active address - * @param {indexDiscoveryCallback} cb - callback - * @return {number} -1 if there's no activity in the range provided - */ -Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) { - preconditions.shouldBeDefined(copayerIndex); - preconditions.checkArgument(gap); - var scanIndex = start; - var lastActive = -1; - var hasActivity = false; - - var self = this; - async.doWhilst( - function _do(next) { - // Optimize window to minimize the derivations. - var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; - var addresses = self.deriveAddresses(scanIndex, scanWindow, change, copayerIndex); - self.blockchain.getActivity(addresses, function(err, actives) { - if (err) throw err; - - // Check for new activities in the newlly scanned addresses - var recentActive = actives.reduce(function(r, e, i) { - return e ? scanIndex + i : r; - }, lastActive); - hasActivity = lastActive != recentActive; - lastActive = recentActive; - scanIndex += scanWindow; - next(); - }); - }, - function _while() { - return hasActivity; - }, - function _finally(err) { - if (err) return cb(err); - cb(null, lastActive); - } - ); -}; - -/** - * @desc Closes the wallet and disconnects all services - */ -Wallet.prototype.close = function(cb) { - log.debug('## CLOSING Wallet: ' + this.id); - this.network.removeAllListeners(); - this.network.cleanUp(); - this.blockchain.removeAllListeners(); - this.blockchain.destroy(); - - // TODO - // this.lock.release(function() { - if (cb) return cb(); - // }); -}; - -/** - * @desc Returns the name of the network ('livenet' or 'testnet') - * @return {string} - */ -Wallet.prototype.getNetwork = function() { - return this.network; -}; - -/** - * @desc Throws an error if an address already exists in the address book - * @private - */ -Wallet.prototype._checkAddressBook = function(key) { - if (this.addressBook[key] && this.addressBook[key].copayerId != -1) { - throw new Error('This address already exists in your Address Book'); - } -}; - -/** - * @desc Add an entry to the address book - * - * @param {string} key - the address to be added - * @param {string} label - a name for the address - */ -Wallet.prototype.setAddressBook = function(key, label) { - this._checkAddressBook(key); - var copayerId = this.getMyCopayerId(); - var ts = Date.now(); - var newEntry = { - hidden: false, - createdTs: ts, - copayerId: copayerId, - label: label, - }; - this.addressBook[key] = newEntry; - this.sendAddressBook(null, key); - this.emitAndKeepAlive('addressBookUpdated'); -}; - -/** - * @desc Hides or unhides an address book entry - * @param {string} key - the address in the addressbook - */ -Wallet.prototype.toggleAddressBookEntry = function(key) { - if (!key) throw new Error('Key is required'); - this.addressBook[key].hidden = !this.addressBook[key].hidden; - this.emitAndKeepAlive('addressBookUpdated', true); -}; - -/** - * @desc Returns true if there are more than one cosigners - * @return {boolean} - */ -Wallet.prototype.isShared = function() { - return this.totalCopayers > 1; -}; - -/** - * @desc Returns true if more than one signature is required - * @return {boolean} - */ -Wallet.prototype.requiresMultipleSignatures = function() { - return this.requiredCopayers > 1; -}; - -/** - * @desc Returns true if the keyring is complete - * @return {boolean} - */ -Wallet.prototype.isComplete = function() { - return this.publicKeyRing.isComplete(); -}; - - -/** - * @desc Sets the version of this wallet object - * - * @param {string} version - the new version for the wallet - */ -Wallet.prototype.setVersion = function(version) { - this.version = version; - if (this.opts) { - this.opts.version = version; - } -}; - -/** - * @desc Return a list of past transactions - * - * @param {number} opts.currentPage - the desired page in the dataset - * @param {number} opts.itemsPerPage - number of items per page - * @return {Object} the list of transactions - */ -Wallet.prototype.getTransactionHistory = function(opts, cb) { - var self = this; - - if (_.isFunction(opts)) { - cb = opts; - opts = {}; - } - opts = opts || {}; - - var addresses = self.getAddresses(); - var proposals = self.txProposals.txps; - var satToUnit = 1 / self.settings.unitToSatoshi; - - var indexedProposals = _.indexBy(proposals, 'sentTxid'); - - - function extractInsOuts(tx) { - // Inputs - var inputs = _.map(tx.vin, function(item) { - return { - type: 'in', - address: item.addr, - isMine: self.addressIsOwn(item.addr), - isChange: self.addressIsChange(item.addr), - amountSat: item.valueSat, - } - }); - var outputs = _.map(tx.vout, function(item) { - var itemAddr; - // If classic multisig, ignore - if (item.scriptPubKey && item.scriptPubKey.addresses.length == 1) { - itemAddr = item.scriptPubKey.addresses[0]; - } - - return { - type: 'out', - address: itemAddr, - isMine: self.addressIsOwn(itemAddr), - isChange: self.addressIsChange(itemAddr), - label: self.addressBook[itemAddr] ? self.addressBook[itemAddr].label : undefined, - amountSat: parseInt((item.value * bitcore.util.COIN).toFixed(0)), - } - }); - - return inputs.concat(outputs); - }; - - function sum(items, filter) { - return _.reduce(_.where(items, filter), - function(memo, item) { - return memo + item.amountSat; - }, 0); - }; - - function decorateTx(tx) { - var items = extractInsOuts(tx); - - var amountIn = sum(items, { - type: 'in', - isMine: true - }); - - var amountOut = sum(items, { - type: 'out', - isMine: true, - isChange: false, - }); - - var amountOutChange = sum(items, { - type: 'out', - isMine: true, - isChange: true, - }); - - var fees = parseInt((tx.fees * bitcore.util.COIN).toFixed(0)); - var amount; - - - if (amountIn == (amountOut + amountOutChange + (amountIn > 0 ? fees : 0))) { - tx.action = 'moved'; - amount = amountOut; - } else { - amount = amountIn - amountOut - amountOutChange - (amountIn > 0 ? fees : 0); - tx.action = amount > 0 ? 'sent' : 'received'; - } - - if (tx.action == 'sent' || tx.action == 'moved') { - var firstOut = _.findWhere(items, { - type: 'out' - }); - if (firstOut) { - tx.labelTo = firstOut.label; - tx.addressTo = firstOut.address; - } - }; - - tx.amountSat = Math.abs(amount); - tx.amount = tx.amountSat * satToUnit; - tx.minedTs = !_.isNaN(tx.time) ? tx.time * 1000 : undefined; - - var proposal = indexedProposals[tx.txid]; - if (proposal) { - tx.comment = proposal.comment; - tx.sentTs = proposal.sentTs; - tx.merchant = proposal.merchant; - tx.peerActions = proposal.peerActions; - tx.paymentAckMemo = proposal.paymentAckMemo; - tx.actionList = self._getActionList(proposal); - } - }; - - function paginate(res, currentPage, itemsPerPage) { - if (!res) { - res = { - totalItems: 0, - items: [], - }; - }; - - var r = { - itemsPerPage: itemsPerPage || res.totalItems, - currentPage: currentPage || 1, - nbItems: res.totalItems, - items: res.items, - }; - r.nbPages = r.itemsPerPage != 0 ? Math.ceil(r.nbItems / r.itemsPerPage) : 1; - return r; - }; - - if (addresses.length > 0) { - var from = (opts.currentPage - 1) * opts.itemsPerPage; - var to = opts.currentPage * opts.itemsPerPage; - if (!_.isNumber(from) || _.isNaN(from)) from = 0; - if (!_.isNumber(to) || _.isNaN(to)) to = null; - - self.blockchain.getTransactions(addresses, from, to, function(err, res) { - if (err) return cb(err); - - _.each(res.items, function(tx) { - if (tx) { - decorateTx(tx); - } - }); - - return cb(null, paginate(res, opts.currentPage, opts.itemsPerPage)); - }); - } else { - return cb(null, paginate(null, opts.currentPage, opts.itemsPerPage)); - } -}; - -Wallet.prototype.exportEncrypted = function(password, opts) { - opts = opts || {}; - var crypto = opts.cryptoUtil || cryptoUtil; - return crypto.encrypt(password, this.toObj()); -}; - -module.exports = Wallet; diff --git a/js/models/WalletLock.js b/js/models/WalletLock.js deleted file mode 100644 index 77514d7e0..000000000 --- a/js/models/WalletLock.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -var preconditions = require('preconditions').singleton(); - -function WalletLock(storage, walletId, timeoutMin) { - preconditions.checkArgument(storage); - preconditions.checkArgument(walletId); - - this.storage = storage; - this.timeoutMin = timeoutMin || 5; - this.key = WalletLock._keyFor(walletId); -} - -WalletLock.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); - }); - }); -}; - - -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) { - return 'lock' + '::' + walletId; -}; - -WalletLock.prototype._isLockedByOther = function(cb) { - var self = this; - - this.storage.getGlobal(this.key, function(json) { - 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); - - var isMyself = wl.sessionId === self.sessionId; - - if (isMyself) - return cb(false); - - // Seconds remainding - return cb(parseInt(-expiredSince / 1000)); - }); -}; - - -WalletLock.prototype._setLock = function(cb) { - preconditions.checkArgument(cb); - preconditions.checkState(this.sessionId); - var self = this; - - this.storage.setGlobal(this.key, { - sessionId: this.sessionId, - expireTs: Date.now() + this.timeoutMin * 60 * 1000, - }, function() { - - cb(null); - }); -}; - - -WalletLock.prototype._doKeepAlive = function(cb) { - 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); -}; - - -module.exports = WalletLock; diff --git a/js/plugins/EncryptedInsightStorage.js b/js/plugins/EncryptedInsightStorage.js deleted file mode 100644 index ce377796c..000000000 --- a/js/plugins/EncryptedInsightStorage.js +++ /dev/null @@ -1,59 +0,0 @@ -var cryptoUtil = require('../util/crypto'); -var InsightStorage = require('./InsightStorage'); -var inherits = require('inherits'); -var log = require('../util/log'); -var SEPARATOR = '%^#@'; - -function EncryptedInsightStorage(config) { - InsightStorage.apply(this, [config]); -} -inherits(EncryptedInsightStorage, InsightStorage); - -EncryptedInsightStorage.prototype._brokenDecrypt = function(body) { - var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100); - log.debug('Trying legacy decrypt') - var decryptedJson = cryptoUtil.decrypt(key, body); - return decryptedJson; -}; - -EncryptedInsightStorage.prototype.resendVerificationEmail = function(callback) { - InsightStorage.prototype.resendVerificationEmail.apply(this, [callback]); -}; - -EncryptedInsightStorage.prototype.getItem = function(name, callback) { - var self = this; - InsightStorage.prototype.getItem.apply(this, [name, - function(err, body, headers) { - if (err) { - return callback(err); - } - var decryptedJson = cryptoUtil.decrypt(self.email + SEPARATOR + self.password, body); - - if (!decryptedJson) { - log.debug('Could not decrypt value using current decryption schema'); - decryptedJson = self._brokenDecrypt(body); - } - - if (!decryptedJson) { - log.debug('Could not decrypt value.'); - return callback('PNOTFOUND'); - } - return callback(null, decryptedJson, headers); - } - ]); -}; - -EncryptedInsightStorage.prototype.setItem = function(name, value, callback) { - var record = cryptoUtil.encrypt(this.email + SEPARATOR + this.password, value); - InsightStorage.prototype.setItem.apply(this, [name, record, callback]); -}; - -EncryptedInsightStorage.prototype.removeItem = function(name, callback) { - InsightStorage.prototype.removeItem.apply(this, [name, callback]); -}; - -EncryptedInsightStorage.prototype.clear = function(callback) { - InsightStorage.prototype.clear.apply(this, [callback]); -}; - -module.exports = EncryptedInsightStorage; diff --git a/js/plugins/EncryptedLocalStorage.js b/js/plugins/EncryptedLocalStorage.js deleted file mode 100644 index c14cd4008..000000000 --- a/js/plugins/EncryptedLocalStorage.js +++ /dev/null @@ -1,77 +0,0 @@ -var cryptoUtil = require('../util/crypto'); -var log = require('../util/log'); -var LocalStorage = require('./LocalStorage'); -var inherits = require('inherits'); -var preconditions = require('preconditions').singleton(); - -var SEPARATOR = '@#$'; - -function EncryptedLocalStorage(config) { - LocalStorage.apply(this, [config]); -} -inherits(EncryptedLocalStorage, LocalStorage); - - -EncryptedLocalStorage.prototype._brokenDecrypt = function(body) { - var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100); - log.debug('Trying legacy decrypt') - var decryptedJson = cryptoUtil.decrypt(key, body); - return decryptedJson; -}; - - - -EncryptedLocalStorage.prototype._brokenDecryptUndef = function(body) { - var badkey = undefined + SEPARATOR + undefined; - return cryptoUtil.decrypt(badkey, body); -}; - - - - -EncryptedLocalStorage.prototype.getItem = function(name, callback) { - var self = this; - preconditions.checkState(self.email); - - LocalStorage.prototype.getItem.apply(this, [name, - function(err, body) { - - - var decryptedJson = cryptoUtil.decrypt(self.email + SEPARATOR + self.password, body); - if (!decryptedJson) { - log.debug('Could not decrypt value using current decryption schema'); - decryptedJson = self._brokenDecrypt(body); - } - - if (!decryptedJson) { - decryptedJson = self._brokenDecryptUndef(body); - } - - if (!decryptedJson) { - log.debug('Could not decrypt value.'); - return callback('PNOTFOUND'); - } - - return callback(null, decryptedJson); - } - ]); -}; - -EncryptedLocalStorage.prototype.setItem = function(name, value, callback) { - if (!_.isString(value)) { - value = JSON.stringify(value); - } - var record = cryptoUtil.encrypt(this.email + SEPARATOR + this.password, value); - LocalStorage.prototype.setItem.apply(this, [name, record, callback]); -}; - -EncryptedLocalStorage.prototype.removeItem = function(name, callback) { - LocalStorage.prototype.removeItem.apply(this, [name, callback]); -}; - -EncryptedLocalStorage.prototype.clear = function(callback) { - LocalStorage.prototype.clear.apply(this, [callback]); -}; - - -module.exports = EncryptedLocalStorage; diff --git a/js/plugins/GoogleDrive.js b/js/plugins/GoogleDrive.js deleted file mode 100644 index 08634a279..000000000 --- a/js/plugins/GoogleDrive.js +++ /dev/null @@ -1,301 +0,0 @@ -'use strict'; - -var preconditions = require('preconditions').singleton(); -var loaded = 0; -var SCOPES = 'https://www.googleapis.com/auth/drive'; -var log = require('../util/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 = 'DB'; - - 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)); -}; - -GoogleDrive.prototype.setCredentils = function(email, password, opts, callback) { -}; - -/** - * 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.createItem = function(name, value, callback) { - this.getItem(name, function(err, retrieved) { - if (err || !retrieved) { - return this.setItem(name, value, callback); - } else { - return callback('EEXISTS'); - } - }); -}; - -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); - }); -}; - -module.exports = GoogleDrive; diff --git a/js/plugins/InsightStorage.js b/js/plugins/InsightStorage.js deleted file mode 100644 index ab4b791ce..000000000 --- a/js/plugins/InsightStorage.js +++ /dev/null @@ -1,287 +0,0 @@ -var request = require('request'); -var cryptoUtil = require('../util/crypto'); -var bitcore = require('bitcore'); -var buffers = require('buffer'); -var querystring = require('querystring'); -var Identity = require('../models/Identity'); -var log = require('../util/log'); - -var SEPARATOR = '|'; - -function InsightStorage(config) { - this.type = 'DB'; - this.storeUrl = config.url || 'https://insight.bitpay.com:443/api/email', - this.request = config.request || request; - - this.iterations = config.iterations || 1000; - this.salt = config.salt || 'jBbYTj8zTrOt6V'; -} - -InsightStorage.prototype.init = function() {}; - -InsightStorage.prototype.setCredentials = function(email, password, opts) { - this.email = email; - this.password = password; - this._cachedKey = null; -}; - -InsightStorage.prototype.createItem = function(name, value, callback) { - var self = this; - - this.getItem(name, function(err, retrieved) { - if (err || !retrieved) { - return self.setItem(name, value, callback); - } else { - return callback('EEXISTS'); - } - }); -}; - -InsightStorage.prototype.resendVerificationEmail = function (callback) { - var passphrase = this.getPassphrase(); - var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); - var resendUrl = this.storeUrl + '/resend_email'; - - log.debug('Resending verification email: ' + this.email); - this.request.get({ - url: resendUrl, - headers: { - 'Authorization': authHeader - }, - body: null, - }, function(err, response, body) { - if (err) { - return callback('Connection error'); - } - if (response.statusCode === 409) { - return callback('BADCREDENTIALS: Invalid username or password'); - } else if (response.statusCode !== 200) { - return callback('Unable to process the request'); - } - return callback(); - }); -}; - -function mayBeOldPassword(password) { - // Test for base64 - return /^[a-zA-Z0-9\/=\+]+$/.test(password); -} - -InsightStorage.prototype.getItem = function(name, callback) { - var passphrase = this.getPassphrase(); - var self = this; - - this._makeGetRequest(passphrase, name, function(err, body, headers) { - if (err) log.warn(err); - if (err && err.indexOf('PNOTFOUND') !== -1 && mayBeOldPassword(self.password)) { - return self._brokenGetItem(name, callback); - } - return callback(err, body, headers); - }); -}; - -/* This key need to have DIFFERENT - * settings(salt,iterations) than the kdf for wallet/profile encryption - * in Encrpted*Storage. The user should be able - * to change the settings on config.js to modify salt / iterations - * for encryption, but - * mantain the same key & passphrase. This is why those settings are - * not shared with encryption - */ -InsightStorage.prototype.getKey = function() { - if (!this._cachedKey) { - this._cachedKey = cryptoUtil.kdf(this.password + SEPARATOR + this.email, this.salt, this.iterations); - } - return this._cachedKey; -}; - -InsightStorage.prototype.getPassphrase = function() { - return bitcore.util.twoSha256(this.getKey()).toString('base64'); -}; - -/** - * XmlHttpRequest's getAllResponseHeaders() method returns a string of response - * headers according to the format described here: - * http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders-method - * This method parses that string into a user-friendly key/value pair object. - */ -InsightStorage.parseResponseHeaders = function (headerStr) { - var headers = {}; - if (!headerStr) { - return headers; - } - var headerPairs = headerStr.split('\u000d\u000a'); - for (var i = 0, len = headerPairs.length; i < len; i++) { - var headerPair = headerPairs[i]; - var index = headerPair.indexOf('\u003a\u0020'); - if (index > 0) { - var key = headerPair.substring(0, index); - var val = headerPair.substring(index + 2); - headers[key] = val; - } - } - return headers; -} - -InsightStorage.prototype._makeGetRequest = function(passphrase, key, callback) { - var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); - var retrieveUrl = this.storeUrl + '/retrieve'; - var getParams = { - url: retrieveUrl + '?' + querystring.encode({ - key: key, - rand: Math.random() // prevent cache - }), - headers: { - 'Authorization': authHeader - } - }; - this.request.get(getParams, - function(err, response, body) { - if (err) { - return callback('Connection error'); - } - if (response.statusCode === 403) { - return callback('PNOTFOUND: Profile not found'); - } - if (response.statusCode !== 200) { - return callback('Unable to read item from insight'); - } - return callback(null, body, InsightStorage.parseResponseHeaders(response.getAllResponseHeaders())); - } - ); -}; - -InsightStorage.prototype._brokenGetItem = function(name, callback) { - var passphrase = this._makeBrokenSecret(); - var self = this; - log.debug('using legacy get'); - this._makeGetRequest(passphrase, name, function(err, body) { - if (!err) { - return self._changePassphrase(function(err) { - if (err) { - return callback(err); - } - return callback(null, body); - }); - } - return callback(err); - }); -}; - -InsightStorage.prototype._makeBrokenSecret = function() { - var key = cryptoUtil.kdf(this.password + this.email, 'mjuBtGybi/4=', 100); - return cryptoUtil.kdf(key, this.password, 100); -}; - -InsightStorage.prototype._changePassphrase = function(callback) { - var passphrase = this._makeBrokenSecret(); - var newPassphrase = this.getPassphrase(); - var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); - - var url = this.storeUrl + '/change_passphrase'; - this.request.post({ - url: url, - headers: { - 'Authorization': authHeader - }, - body: querystring.encode({ - newPassphrase: newPassphrase - }) - }, function(err, response, body) { - if (err) { - return callback('Connection error'); - } - if (response.statusCode === 409) { - return callback('BADCREDENTIALS: Invalid username or password'); - } - if (response.statusCode !== 200) { - return callback('Unable to store data on insight'); - } - return callback(); - }); -}; - -InsightStorage.prototype.setItem = function(name, value, callback) { - var passphrase = this.getPassphrase(); - var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); - var registerUrl = this.storeUrl + '/save'; - - log.debug('setItem ' + name + ' size:'+ (value.length/1024).toFixed(1) + 'kb' ); - this.request.post({ - url: registerUrl, - headers: { - 'Authorization': authHeader - }, - body: querystring.encode({ - key: name, - record: value - }) - }, function(err, response, body) { - if (err) { - return callback('Connection error'); - } - if (response.statusCode === 409) { - return callback('BADCREDENTIALS: Invalid username or password'); - } else if (response.statusCode === 406) { - return callback('OVERQUOTA: Quota exceeded'); - } else if (response.statusCode === 501) { - return callback('EMAILERROR: Error sending verification email'); - } else if (response.statusCode !== 200) { - return callback('Unable to store data on insight'); - } - return callback(); - }); -}; - -InsightStorage.prototype.removeItem = function(key, callback) { - var passphrase = this.getPassphrase(); - var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); - var deleteUrl = this.storeUrl + '/delete/item'; - var getParams = { - url: deleteUrl + '?' + querystring.encode({ - key: key - }), - headers: { - 'Authorization': authHeader - } - }; - log.debug('Erasing: ' + key); - this.request.get(getParams, function(err, response, body) { - if (err) { - return callback('Connection error'); - } - if (response.statusCode === 409) { - return callback('BADCREDENTIALS: Invalid username or password'); - } else if (response.statusCode !== 200) { - return callback('Unable to remove data on insight'); - } - return callback(); - }); -}; - -InsightStorage.prototype.clear = function(callback) { - var passphrase = this.getPassphrase(); - var authHeader = new buffers.Buffer(this.email + ':' + passphrase).toString('base64'); - var deleteUrl = this.storeUrl + '/delete/profile'; - - log.debug('Clearing storage for: ' + this.email); - this.request.post({ - url: deleteUrl, - headers: { - 'Authorization': authHeader - }, - body: null, - }, function(err, response, body) { - if (err) { - return callback('Connection error'); - } - if (response.statusCode === 409) { - return callback('BADCREDENTIALS: Invalid username or password'); - } else if (response.statusCode !== 200) { - return callback('Unable to remove data on insight'); - } - return callback(); - }); -}; - -module.exports = InsightStorage; diff --git a/js/plugins/LocalStorage.js b/js/plugins/LocalStorage.js deleted file mode 100644 index 3a7f5d82d..000000000 --- a/js/plugins/LocalStorage.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; -var _ = require('lodash'); -var preconditions = require('preconditions').singleton(); -var isChromeApp = typeof window !== "undefined" && window.chrome && chrome.runtime && chrome.runtime.id; - - -function LocalStorage(opts) { - this.type = 'DB'; - opts = opts || {}; - - - - - this.ls = opts.ls || - ((typeof localStorage !== "undefined") ? localStorage : null); - - if (isChromeApp && !this.ls) { - this.ls = localStorage = chrome.storage.local; - window.localStorage = chrome.storage.local; - } - - preconditions.checkState(this.ls, - 'localstorage not available, cannot run plugin'); -}; - -LocalStorage.prototype.init = function() {}; - -LocalStorage.prototype.setCredentials = function(email, password, opts) { - this.email = email; - this.password = password; -}; - -LocalStorage.prototype.getItem = function(k, cb) { - if (isChromeApp) { - chrome.storage.local.get(k, - function(data) { - //TODO check for errors - return cb(null, data[k]); - }); - } else { - return cb(null, this.ls.getItem(k)); - } -}; - -/** - * Same as setItem, but fails if an item already exists - */ -LocalStorage.prototype.createItem = function(name, value, callback) { - var self = this; - self.getItem(name, - function(err, data) { - if (data) { - return callback('EEXISTS'); - } else { - return self.setItem(name, value, callback); - } - }); -}; - -LocalStorage.prototype.setItem = function(k, v, cb) { - if (isChromeApp) { - var obj = {}; - obj[k] = v; - - chrome.storage.local.set(obj, cb); - } else { - this.ls.setItem(k, v); - return cb(); - } - -}; - -LocalStorage.prototype.removeItem = function(k, cb) { - if (isChromeApp) { - chrome.storage.local.remove(k, cb); - } else { - this.ls.removeItem(k); - return cb(); - } - -}; - -LocalStorage.prototype.clear = function(cb) { - // NOP - return cb(); -}; - -LocalStorage.prototype.allKeys = function(cb) { - if (isChromeApp) { - chrome.storage.local.get(null, function(items) { - return cb(null, _.keys(items)); - }); - } else { - var ret = []; - var l = this.ls.length; - - for (var i = 0; i < l; i++) - ret.push(this.ls.key(i)); - - return cb(null, ret); - } -}; - - - -module.exports = LocalStorage; diff --git a/js/routes.js b/js/routes.js deleted file mode 100644 index 93ebdc797..000000000 --- a/js/routes.js +++ /dev/null @@ -1,180 +0,0 @@ -'use strict'; - -var LS = require('../js/plugins/LocalStorage'); -var ls = new LS(); - -var unsupported = false; - -if (!ls || ls.length < 1) - unsupported = true; - - -if (window && window.navigator) { - var rxaosp = window.navigator.userAgent.match(/Android.*AppleWebKit\/([\d.]+)/); - var isaosp = (rxaosp && rxaosp[1] < 537); - if (!window.cordova && isaosp) - unsupported = true; -} - - -//Setting up route -angular - .module('copayApp') - .config(function($routeProvider) { - - $routeProvider - .when('/', { - templateUrl: 'views/home.html', - }) - .when('/createProfile', { - templateUrl: 'views/createProfile.html', - }) - .when('/unsupported', { - templateUrl: 'views/unsupported.html' - }) - .when('/confirmed', { - template: " ", // just fire controller - controller: 'EmailConfirmationController', - }) - // Payment intents come here. - .when('/uri-payment/:data', { - template: " ", // just fire controller - controller: 'paymentUriController', - }) - .when('/selectWalletForPayment', { - template: " ", // just fire controller - controller: 'walletForPaymentController', - logged: true - }) - .when('/join', { - templateUrl: 'views/join.html', - logged: true - }) - .when('/import', { - templateUrl: 'views/import.html', - logged: true - }) - .when('/importProfile', { - templateUrl: 'views/importProfile.html', - }) - .when('/create', { - templateUrl: 'views/create.html', - logged: true - }) - .when('/copayers', { - templateUrl: 'views/copayers.html', - logged: true - }) - .when('/homeWallet', { - templateUrl: 'views/homeWallet.html', - walletShouldBeComplete: true, - logged: true - }) - .when('/receive', { - templateUrl: 'views/receive.html', - walletShouldBeComplete: true, - logged: true - }) - .when('/history', { - templateUrl: 'views/history.html', - walletShouldBeComplete: true, - logged: true - }) - .when('/send', { - templateUrl: 'views/send.html', - walletShouldBeComplete: true, - logged: true - }) - .when('/more', { - templateUrl: 'views/more.html', - walletShouldBeComplete: true, - logged: true - }) - .when('/settings', { - templateUrl: 'views/settings.html', - walletShouldBeComplete: true, - logged: false - }) - .when('/warning', { - templateUrl: 'views/warning.html', - logged: true - }) - .when('/profile', { - templateUrl: 'views/profile.html', - logged: true - }) - .when('/add', { - templateUrl: 'views/add.html', - logged: true - }) - .when('/signout', { - template: " ", // just fire controller - controller: 'signOutController', - logged: true - }); - - if (config.developmentFeatures) { - $routeProvider.when('/devLogin/:mail/:password', { - templateUrl: 'views/devLogin.html', - logged: false - }); - } - - $routeProvider.otherwise({ - templateUrl: 'views/errors/404.html', - title: 'Error' - }); - }); - -//Setting HTML5 Location Mode -angular - .module('copayApp') - .config(function($locationProvider, IdleProvider, KeepaliveProvider) { - $locationProvider - .html5Mode(false) - .hashPrefix('!'); - // IDLE timeout - var timeout = config.wallet.idleDurationMin * 60 || 300; - IdleProvider.idle(timeout); // in seconds - IdleProvider.timeout(40); // in seconds - KeepaliveProvider.interval(30); // in seconds - }) - .run(function($rootScope, $location, Idle, gettextCatalog, uriHandler, isCordova, amMoment) { - - var userLang, androidLang; - - if (navigator && navigator.userAgent && (androidLang = navigator.userAgent.match(/android.*\W(\w\w)-(\w\w)\W/i))) { - userLang = androidLang[1]; - } else { - // works for iOS and Android 4.x - userLang = navigator.userLanguage || navigator.language; - } - - userLang = userLang ? (userLang.split('-', 1)[0] || 'en') : 'en'; - gettextCatalog.setCurrentLanguage(userLang); - amMoment.changeLanguage(userLang); - - // Register URI handler, not for mobileApp - if (!isCordova) { - Idle.watch(); - uriHandler.register(); - } - - $rootScope.$on('$routeChangeStart', function(event, next, current) { - if (unsupported) { - $location.path('unsupported'); - return; - } - - if (!$rootScope.iden && next.logged) { - Idle.unwatch(); - $location.path('/'); - } - if ($rootScope.wallet && !$rootScope.wallet.isComplete() && next.walletShouldBeComplete) { - $location.path('/copayers'); - } - }); - }) - .config(function($compileProvider) { - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|tel|chrome-extension|resource):/); - }); diff --git a/js/services/applicationService.js b/js/services/applicationService.js deleted file mode 100644 index 5df3b85be..000000000 --- a/js/services/applicationService.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; -angular.module('copayApp.services') - .factory('applicationService', function($rootScope, $location, $timeout, go, isCordova) { - var root = {}; - var isChromeApp = window.chrome && chrome.runtime && chrome.runtime.id; - - root.restart = function() { - if (isCordova) { - $rootScope.iden = $rootScope.wallet = undefined; - go.path('/'); - $timeout(function(){ - $rootScope.$digest(); - },1); - - } else { - - // Go home reloading the application - var hashIndex = window.location.href.indexOf('#!/'); - if (isChromeApp) { - chrome.runtime.reload(); - } else { - window.location = window.location.href.substr(0, hashIndex); - } - } - }; - - return root; - }); diff --git a/js/services/backupService.js b/js/services/backupService.js deleted file mode 100644 index 5caf3f3ce..000000000 --- a/js/services/backupService.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict'; - -var BackupService = function($rootScope, notification) { - this.$rootScope = $rootScope; - this.notifications = notification; -}; - -BackupService.prototype.getCopayer = function(wallet) { - return wallet.totalCopayers > 1 ? wallet.getMyCopayerNickname() : ''; -}; - - - - - -BackupService.prototype._download = function(ew, walletName, filename) { - - var NewBlob = function(data, datatype) { - var out; - - try { - out = new Blob([data], { - type: datatype - }); - console.debug("case 1"); - } catch (e) { - window.BlobBuilder = window.BlobBuilder || - window.WebKitBlobBuilder || - window.MozBlobBuilder || - window.MSBlobBuilder; - - if (e.name == 'TypeError' && window.BlobBuilder) { - var bb = new BlobBuilder(); - bb.append(data); - out = bb.getBlob(datatype); - console.debug("case 2"); - } else if (e.name == "InvalidStateError") { - // InvalidStateError (tested on FF13 WinXP) - out = new Blob([data], { - type: datatype - }); - console.debug("case 3"); - } else { - // We're screwed, blob constructor unsupported entirely - console.debug("Errore"); - } - } - return out; - }; - - var blob; - - blob = new NewBlob(ew, 'text/plain;charset=utf-8'); - - - this.notifications.success('Backup created', 'Encrypted backup file saved'); - - // otherwise lean on the browser implementation - saveAs(blob, filename); -}; - -BackupService.prototype.walletEncrypted = function(wallet) { - return wallet.exportEncrypted(this.$rootScope.iden.password); -} - -BackupService.prototype.walletDownload = function(wallet) { - var ew = this.walletEncrypted(wallet); - var walletName = wallet.getName(); - var copayerName = this.getCopayer(wallet); - var filename = (copayerName ? copayerName + '-' : '') + walletName + '-keybackup.json.aes'; - this._download(ew, walletName, filename) -}; - -BackupService.prototype.profileEncrypted = function(iden) { - iden.setBackupNeeded(false); - return iden.exportEncryptedWithWalletInfo(iden.password); -} - -BackupService.prototype.profileDownload = function(iden) { - var ew = this.profileEncrypted(iden); - var name = iden.fullName; - var filename = name + '-profile.json'; - this._download(ew, name, filename) -}; - -angular.module('copayApp.services').service('backupService', BackupService); diff --git a/js/services/compatibility.js b/js/services/compatibility.js deleted file mode 100644 index 904ccc501..000000000 --- a/js/services/compatibility.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('Compatibility', function($rootScope) { - var root = {}; - - root.check = function (scope) { - copay.Compatibility.listWalletsPre8(function(wallets) { - scope.anyWallet = wallets.length > 0 ? true : false; - scope.oldWallets = wallets; - }); - }; - return root; -}); diff --git a/js/services/configService.js b/js/services/configService.js deleted file mode 100644 index 55dae5068..000000000 --- a/js/services/configService.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('configService', function($timeout, localstorageService, gettextCatalog, defaults) { - var root = {}; - - root.set = function(opts, cb) { - - // Options that have runtime effects - if (opts.logLevel) - copay.logger.setLevel(opts.logLevel); - - // Set current version - opts.version = copay.version; - - localstorageService.getItem('config', function(err, oldOpsStr) { - var oldOpts = {}; - try { - oldOpts = JSON.parse(oldOpsStr); - } catch (e) {}; - - var newOpts = {}; - _.extend(newOpts, copay.defaultConfig, oldOpts, opts); - - // TODO remove this global variable. - config = newOpts; - localstorageService.setItem('config', JSON.stringify(newOpts), cb); - }); - }; - - root.reset = function(cb) { - config = defaults; - localstorageService.removeItem('config', cb); - }; - - return root; -}); diff --git a/js/services/identityService.js b/js/services/identityService.js deleted file mode 100644 index 96cf0ed19..000000000 --- a/js/services/identityService.js +++ /dev/null @@ -1,363 +0,0 @@ -'use strict'; -angular.module('copayApp.services') - .factory('identityService', function($rootScope, $location, $timeout, $filter, pluginManager, notification, pendingTxsService, balanceService, applicationService, go) { - - // TODO: - // * remove iden from rootScope - // * remove wallet from rootScope - // * create walletService - - var root = {}; - root.check = function(scope) { - copay.Identity.checkIfExistsAny({ - pluginManager: pluginManager.getInstance(config), - }, function(anyProfile) { - copay.Wallet.checkIfExistsAny({ - pluginManager: pluginManager.getInstance(config), - }, function(anyWallet) { - scope.loading = false; - scope.anyProfile = anyProfile ? true : false; - scope.anyWallet = anyWallet ? true : false; - - if (!scope.anyProfile) { - $location.path('/createProfile'); - } - }); - }); - }; - - root.create = function(email, password, cb) { - copay.Identity.create({ - email: email, - password: password, - pluginManager: pluginManager.getInstance(config), - network: config.network, - networkName: config.networkName, - walletDefaults: config.wallet, - passphraseConfig: config.passphraseConfig, - failIfExists: true, - }, function(err, iden) { - if (err) return cb(err); - preconditions.checkState(iden); - root.bind(iden); - - return cb(null); - }); - }; - - root.createDefaultWallet = function(cb) { - var iden = $rootScope.iden; - - var walletOptions = { - nickname: iden.fullName, - networkName: config.networkName, - requiredCopayers: 1, - totalCopayers: 1, - password: iden.password, - name: 'My wallet', - }; - iden.createWallet(walletOptions, function(err, wallet) { - return cb(err); - }); - }; - - root.resendVerificationEmail = function(cb) { - var iden = $rootScope.iden; - iden.resendVerificationEmail(cb); - }; - - root.setServerStatus = function(headers) { - if (!headers) - return; - - var customHeaders = {}; - _.each(_.keys(headers), function(headerKey) { - var hk = headerKey.toLowerCase(); - if (hk.indexOf('x-') === 0) { - customHeaders[hk] = headers[headerKey]; - } - }); - - if (customHeaders['x-email-needs-validation']) - $rootScope.needsEmailConfirmation = true; - else - $rootScope.needsEmailConfirmation = null; - - if (customHeaders['x-quota-per-item']) - $rootScope.quotaPerItem = parseInt(customHeaders['x-quota-per-item']); - - if (customHeaders['x-quota-items-limit']) - $rootScope.quotaItems = parseInt(customHeaders['x-quota-items-limit']); - }; - - root.open = function(email, password, cb) { - var opts = { - email: email, - password: password, - pluginManager: pluginManager.getInstance(config), - network: config.network, - networkName: config.networkName, - walletDefaults: config.wallet, - passphraseConfig: config.passphraseConfig, - }; - - copay.Identity.open(opts, function(err, iden, headers) { - if (err) return cb(err); - root.setServerStatus(headers); - root.bind(iden); - return cb(null, iden); - }); - }; - - root.deleteProfile = function(cb) { - $rootScope.iden.remove(null, cb); - }; - - root.deleteWallet = function(w, cb) { - $rootScope.iden.deleteWallet(w.getId(), cb); - }; - - root.isFocused = function(wid) { - return $rootScope.wallet && wid === $rootScope.wallet.getId(); - }; - - root.setupGlobalVariables = function(iden) { - $rootScope.reconnecting = false; - $rootScope.iden = iden; - }; - - - root.noFocusedWallet = function() { - $rootScope.wallet = null; - $timeout(function() { - $rootScope.$digest(); - }) - }; - - root.setFocusedWallet = function(w, dontUpdateIt) { - if (!_.isObject(w)) - w = $rootScope.iden.getWalletById(w); - preconditions.checkState(w && _.isObject(w)); - - copay.logger.debug('Set focus:', w.getName()); - $rootScope.wallet = w; - - if (!dontUpdateIt) - $rootScope.iden.updateFocusedTimestamp(w.getId()); - - pendingTxsService.update(); - $timeout(function() { - $rootScope.$digest(); - }, 1); - }; - - root.notifyTxProposalEvent = function(w, e) { - if (e.cId == w.getMyCopayerId()) - return; - - var user = w.publicKeyRing.nicknameForCopayer(e.cId); - var name = w.getName(); - switch (e.type) { - case copay.Wallet.TX_NEW: - notification.info('[' + name + '] New Transaction', - $filter('translate')('You received a transaction proposal from') + ' ' + user); - break; - case copay.Wallet.TX_SIGNED: - notification.success('[' + name + '] Transaction Signed', - $filter('translate')('A transaction was signed by') + ' ' + user); - break; - case copay.Wallet.TX_BROADCASTED: - notification.success('[' + name + '] Transaction Approved', - $filter('translate')('A transaction was broadcasted by') + ' ' + user); - break; - case copay.Wallet.TX_REJECTED: - notification.warning('[' + name + '] Transaction Rejected', - $filter('translate')('A transaction was rejected by') + ' ' + user); - break; - } - }; - - root.installWalletHandlers = function(w) { - var wid = w.getId(); - w.on('connectionError', function() { - if (root.isFocused(wid)) { - var message = "Could not connect to the Insight server. Check your settings and network configuration"; - notification.error('Networking Error', message); - } - }); - - w.on('corrupt', function(peerId) { - copay.logger.warn('Received corrupt message from ' + peerId); - }); - - w.on('publicKeyRingUpdated', function() { - $rootScope.$digest(); - }); - - w.on('ready', function() { - var isFocused = root.isFocused(wid); - copay.logger.debug('Wallet:' + w.getName() + - ' is ready. Focused:', isFocused); - - balanceService.update(w, function() { - $rootScope.$digest(); - }, isFocused); - }); - - w.on('tx', function(address, isChange) { - if (!isChange) { - notification.funds('Funds received on ' + w.getName(), address); - } - balanceService.update(w, function() { - $rootScope.$digest(); - }, root.isFocused(wid)); - }); - - w.on('balanceUpdated', function() { - balanceService.update(w, function() { - $rootScope.$digest(); - }, root.isFocused(wid)); - }); - - w.on('insightReconnected', function() { - $rootScope.reconnecting = false; - balanceService.update(w, function() { - $rootScope.$digest(); - }, root.isFocused(wid)); - }); - - w.on('insightError', function() { - if (root.isFocused(wid)) { - $rootScope.reconnecting = true; - $rootScope.$digest(); - } - }); - w.on('newAddresses', function() { - // Nothing yet - }); - - // Disabled for now, does not seens to have much value for the user - // w.on('paymentACK', function(memo) { - // notification.success('Payment Acknowledged', memo); - // }); - - w.on('txProposalEvent', function(ev) { - - if (root.isFocused(wid)) { - pendingTxsService.update(); - } - - // TODO aqui lo unico que cambia son los locked - // se puede optimizar - balanceService.update(w, function() { - $rootScope.$digest(); - }, root.isFocused(wid)); - - root.notifyTxProposalEvent(w, ev); - $timeout(function() { - $rootScope.$digest(); - }); - }); - - w.on('addressBookUpdated', function(dontDigest) { - if (root.isFocused(wid)) { - if (!dontDigest) { - $rootScope.$digest(); - } - } - }); - w.on('connect', function(peerID) { - $rootScope.$digest(); - }); - // TODO? - // w.on('close', ); - // w.on('locked',); - }; - - root.bind = function(iden) { - preconditions.checkArgument(_.isObject(iden)); - copay.logger.debug('Binding profile...'); - - var self = this; - root.setupGlobalVariables(iden); - iden.on('newWallet', function(wid) { - var w = iden.getWalletById(wid); - copay.logger.debug('newWallet:', - w.getName(), wid, iden.getLastFocusedWalletId()); - root.installWalletHandlers(w); - if (wid == iden.getLastFocusedWalletId()) { - copay.logger.debug('GOT Focused wallet:', w.getName()); - root.setFocusedWallet(w, true); - go.walletHome(); - } - - // At the end (after all handlers are in place)...start the wallet. - w.netStart(); - }); - - iden.on('noWallets', function() { - notification.warning('No Wallets', 'Your profile has no wallets. Create one here'); - $rootScope.starting = false; - $location.path('/add'); - $timeout(function() { - $rootScope.$digest(); - }, 1); - }); - - iden.on('walletDeleted', function(wid) { - // do nothing. this is handled 'on sync' on controller. - }); - - iden.on('walletStorageError', function(wid, message) { - notification.error('Error storing wallet', message); - }); - - iden.on('closed', function() { - delete $rootScope['wallet']; - delete $rootScope['iden']; - applicationService.restart(); - }); - }; - - root.signout = function() { - if ($rootScope.iden) { - $rootScope.signingOut = true; - $rootScope.iden.close(function() { // Will trigger 'closed' - $timeout(function() { - $rootScope.signingOut = null; - }, 100); - }); // Will trigger 'closed' - } - }; - - root.createWallet = function(opts, cb) { - $rootScope.iden.createWallet(opts, cb); - }; - - root.importWallet = function(encryptedObj, pass, opts, cb) { - copay.Compatibility.importEncryptedWallet($rootScope.iden, encryptedObj, pass, opts, cb); - }; - - root.joinWallet = function(opts, cb) { - $rootScope.iden.joinWallet(opts, function(err, w) { - return cb(err); - }); - }; - - root.importProfile = function(str, password, cb) { - copay.Identity.importFromEncryptedFullJson(str, password, { - pluginManager: pluginManager.getInstance(config), - network: config.network, - networkName: config.networkName, - walletDefaults: config.wallet, - passphraseConfig: config.passphraseConfig, - }, function(err, iden, walletObjs) { - if (err) return cb(err); - root.bind(iden); - iden.importMultipleWalletsFromObj(walletObjs); - return cb(); - }); - }; - - return root; - }); diff --git a/js/services/localstorageService.js b/js/services/localstorageService.js deleted file mode 100644 index 1dc19d04b..000000000 --- a/js/services/localstorageService.js +++ /dev/null @@ -1,9 +0,0 @@ - -'use strict'; - -angular.module('copayApp.services') - .factory('localstorageService', function($rootScope) { - var LS = require('../js/plugins/LocalStorage'); - var ls = new LS(); - return ls; - }); diff --git a/js/services/pendingTxsService.js b/js/services/pendingTxsService.js deleted file mode 100644 index c523392c0..000000000 --- a/js/services/pendingTxsService.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -angular.module('copayApp.services') - .factory('pendingTxsService', function($rootScope, $filter, rateService) { - var root = {}; - - root.setAlternativeAmount = function(w, tx, cb) { - var alternativeIsoCode = w.settings.alternativeIsoCode; - rateService.whenAvailable(function() { - _.each(tx.outs, function(out) { - var valueSat = out.valueSat * w.settings.unitToSatoshi; - out.alternativeAmount = $filter('noFractionNumber')( - rateService.toFiat(valueSat, alternativeIsoCode), 2); - out.alternativeIsoCode = alternativeIsoCode; - }); - if (cb) return cb(tx); - }); - }; - - root.getDecoratedTxProposals = function(w) { - var txps = w.getPendingTxProposals(); - - _.each(txps, function(tx) { - root.setAlternativeAmount(w, tx); - if (tx.outs) { - _.each(tx.outs, function(out) { - out.valueSat = out.value; - out.value = $filter('noFractionNumber')(out.value); - }); - } - }); - return txps; - }; - - /** - * @desc adds 2 fields to wallet: pendingTxProposalsCountForUs, pendingTxProposals. - * - * @param w wallet - */ - root.update = function(w) { - var w = $rootScope.wallet; - if (!w) return; - - //pendingTxCount - var ret = w.getPendingTxProposalsCount(); - w.pendingTxProposalsCountForUs = ret.pendingForUs; - w.pendingTxProposals = root.getDecoratedTxProposals(w); - }; - - return root; - }); diff --git a/js/services/pinService.js b/js/services/pinService.js deleted file mode 100644 index 90bacf55a..000000000 --- a/js/services/pinService.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -angular.module('copayApp.services') - .factory('pinService', function($rootScope, $timeout, localstorageService) { - - var KEY = 'pinDATA'; - var SALT = '4gllotIKguqi0EkIslC0'; - var ITER = 5000; - - var ls = localstorageService; - var root = {}; - - var _firstpin; - - root.check = function(cb) { - ls.getItem(KEY, function(err, value) { - return cb(err, value ? true : false); - }); - }; - - root.get = function(pin, cb) { - ls.getItem(KEY, function(err, value) { - if (!value) return cb(null); - var enc = value; - var data = copay.crypto.decrypt('' + parseInt(pin), enc); - var err = new Error('Could not decrypt'); - if (data) { - var obj; - try { - obj = JSON.parse(data); - err = null; - } catch (e) {}; - } - return cb(err, obj); - }); - }; - - root.save = function(pin, email, password, cb) { - var credentials = { - email: email, - password: password, - }; - var enc = copay.crypto.encrypt('' + parseInt(pin), credentials, SALT, ITER); - ls.setItem(KEY, enc, function(err) { - return cb(err); - }); - }; - - root.clear = function(cb) { - ls.removeItem(KEY, cb); - }; - - root.clearPin = function(scope) { - scope.digits = []; - scope.defined = []; - }; - - root.pressPin = function(scope, digit, skipOpenWithPin) { - scope.error = null; - scope.digits.push(digit); - scope.defined.push(true); - if (scope.digits.length == 4) { - var pin = scope.digits.join(''); - if (!$rootScope.hasPin) { - if (!_firstpin) { - _firstpin = pin; - scope.askForPin = 2; - $timeout(function() { - scope.clear(); - }, 100); - return; - } - else { - if (pin === _firstpin) { - _firstpin = null; - scope.askForPin = null; - scope.createPin(pin); - return; - } - else { - _firstpin = null; - scope.askForPin = 1; - $timeout(function() { - scope.clear(); - scope.error = 'Entered PINs were not equal. Try again'; - $timeout(function() { - scope.error = null; - }, 2000); - }, 100); - return; - } - } - } - if (!skipOpenWithPin) { - scope.openWithPin(pin); - } - } - }; - - root.skipPin = function(scope, creatingProfile) { - if (!$rootScope.hasPin) { - if (!creatingProfile) { - scope.openWallets(); - } - else { - scope.createDefaultWallet() - } - } - else { - scope.pinLogout(); - } - }; - - return root; - }); diff --git a/js/services/rate.js b/js/services/rate.js deleted file mode 100644 index 64dd100d4..000000000 --- a/js/services/rate.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('rateService', function(request) { - var cfg = _.extend(config.rates, { - request: request - }); - return copay.RateService.singleton(cfg); -}); diff --git a/js/services/request.js b/js/services/request.js deleted file mode 100644 index 5a92093c9..000000000 --- a/js/services/request.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('request', function() { - return require('request'); -}); - diff --git a/js/services/txStatus.js b/js/services/txStatus.js deleted file mode 100644 index 2bfcaf31d..000000000 --- a/js/services/txStatus.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -angular.module('copayApp.services').factory('txStatus', function($modal) { - var root = {}; - - root.notify = function(status) { - var msg; - if (status == copay.Wallet.TX_BROADCASTED) - msg = 'Transaction broadcasted'; - else if (status == copay.Wallet.TX_PROPOSAL_SENT) - msg = 'Transaction proposal created'; - else if (status == copay.Wallet.TX_SIGNED) - msg = 'Transaction proposal signed'; - else if (status == copay.Wallet.TX_REJECTED) - msg = 'Transaction was rejected'; - - if (msg) - root.openModal(msg); - }; - - root.openModal = function(statusStr) { - var ModalInstanceCtrl = function($scope, $modalInstance) { - $scope.statusStr = statusStr; - $scope.cancel = function() { - $modalInstance.dismiss('cancel'); - }; - }; - $modal.open({ - templateUrl: 'views/modals/tx-status.html', - windowClass: 'tiny', - controller: ModalInstanceCtrl, - }); - }; - - return root; -}); diff --git a/js/shell.js b/js/shell.js deleted file mode 100644 index 9e3a89ff0..000000000 --- a/js/shell.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - ** copay-shell integration - */ -(function() { - /* - ** This is a monkey patch for when Copay is running from - ** within Copay-Shell (atom-shell). Since the renderer (the frontend) - ** receives context from Node.js, we get a `module.exports` contruct - ** available to us. Because of this, some libs (specifically Moment.js) - ** attempt to assume their CommonJS form and bind to this. This causes - ** there to be no references in the window to these libs, so let's trick - ** the renderer into thinking that we are _not_ in a CommonJS environment. - */ - if (typeof module !== 'undefined') module = { - exports: false - }; - - // are we running in copay shell? - if (window.process && process.type === 'renderer') { - window.cshell = initCopayShellBindings(); - } else { - return; - } - - function controller(name) { - return angular.element( - document.querySelectorAll( - '[ng-controller="' + name + '"], [data-ng-controller="' + name + '"]' - ) - ).scope(); - }; - - function needsWalletLogin(ipc) { - ipc.send('alert', 'info', 'Please select a wallet.'); - }; - - function initCopayShellBindings() { - - var ipc = require('ipc'); - var clipb = require('clipboard'); - - // atom shell forces to implement the clipboard (on osx) on our own - thanks obama. - - Mousetrap.stopCallback = function() { - return false - }; - - Mousetrap.bind('command+c', function(e) { - clipb.writeText(window.getSelection().toString()); - }); - - Mousetrap.bind('command+v', function(e) { - if (document.activeElement) { - document.activeElement.value = clipb.readText(); - document.activeElement.dispatchEvent(new Event('change')); - } - }); - - // handle messages - ipc.on('address:create', function(data) { - location.href = '#/addresses'; - var ctrl = controller('AddressesController'); - if (!ctrl) return needsWalletLogin(ipc); - ctrl.newAddr(); - }); - - ipc.on('transactions:send', function(data) { - location.href = '#/send'; - var ctrl = controller('SendController'); - if (!ctrl) return needsWalletLogin(ipc); - }); - - ipc.on('transactions:all', function(data) { - location.href = '#/transactions'; - var ctrl = controller('TransactionsController'); - if (!ctrl) return needsWalletLogin(ipc); - ctrl.show(); - }); - - ipc.on('transactions:pending', function(data) { - location.href = '#/transactions'; - var ctrl = controller('TransactionsController'); - if (!ctrl) return needsWalletLogin(ipc); - ctrl.show(true); - }); - - ipc.on('backup:download', function(data) { - location.href = '#/backup'; - var ctrl = controller('BackupController'); - if (!ctrl) return needsWalletLogin(ipc); - ctrl.download(); - }); - - ipc.on('backup:email', function(data) { - location.href = '#/backup'; - var ctrl = controller('BackupController'); - if (!ctrl) return needsWalletLogin(ipc); - ctrl.openModal(); - }); - - ipc.on('backup:import:data', function(data) { - location.href = '#/import'; - var ctrl = controller('ImportController'); - if (!ctrl) return; - ctrl.backupText = data; - ctrl.openPasteArea(); - ctrl.$apply(); - }); - - ipc.on('backup:import', function(data) { - location.href = '#/import'; - }); - - // let the shell know when an error occurs - window.onerror = function(err) { - ipc.send('error', err); - }; - - return ipc; - }; - -})(); diff --git a/js/util/HTTP.js b/js/util/HTTP.js deleted file mode 100644 index 0c184fb8a..000000000 --- a/js/util/HTTP.js +++ /dev/null @@ -1,79 +0,0 @@ -var preconditions = require('preconditions').singleton(); - -module.exports = { - request: function(options, callback) { - preconditions.checkArgument(_.isObject(options)); - - options.method = options.method || 'GET'; - options.headers = options.headers || {}; - var ret = { - success: function(cb) { - this._success = cb; - return this; - }, - error: function(cb) { - this._error = cb; - return this; - }, - _success: function() {; - }, - _error: function(_, err) { - console.trace(err); - throw err; - } - }; - - var method = (options.method || 'GET').toUpperCase(); - var url = options.url; - var req = options; - - req.headers = req.headers || {}; - req.body = req.body || req.data || ''; - - var xhr = options.xhr || new XMLHttpRequest(); - xhr.open(method, url, true); - - Object.keys(req.headers).forEach(function(key) { - var val = req.headers[key]; - if (key === 'Content-Length') return; - if (key === 'Content-Transfer-Encoding') return; - xhr.setRequestHeader(key, val); - }); - - if (req.responseType) { - xhr.responseType = req.responseType; - } - - xhr.onload = function(event) { - var response = xhr.response; - var buf = new Uint8Array(response); - var headers = {}; - (xhr.getAllResponseHeaders() || '').replace( - /(?:\r?\n|^)([^:\r\n]+): *([^\r\n]+)/g, - function($0, $1, $2) { - headers[$1.toLowerCase()] = $2; - } - ); - - return ret._success(buf, xhr.status, headers, options); - }; - - xhr.onerror = function(event) { - 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) { - xhr.send(req.body); - } else { - xhr.send(null); - } - - return ret; - }, -}; diff --git a/js/util/crypto.js b/js/util/crypto.js deleted file mode 100644 index 88c821363..000000000 --- a/js/util/crypto.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Small module for some helpers that wrap sjcl with some good practices. - */ -var sjcl = require('sjcl'); -var _ = require('lodash'); - -var log = require('../util/log.js'); -var config = require('../../config'); - -var defaultSalt = (config && config.passphraseConfig && config.passphraseConfig.storageSalt) || 'mjuBtGybi/4='; -var defaultIterations = (config && config.passphraseConfig && config.passphraseConfig.iterations) || 1000; - -module.exports = { - - /** - * @param {string} password - * @param {string} salt - base64 encoded, defaults to 'mjuBtGybi/4=' - * @param {number} iterations - defaults to 100 - * @param {number} length - bits, defaults to 512 bits - * @returns {string} base64 encoded pbkdf2 derivation using sha1 for hmac - */ - kdf: function(password, salt, iterations, length) { - return sjcl.codec.base64.fromBits( - this.kdfbinary(password, salt, iterations, length) - ); - }, - - /** - * @param {string} key - * @param {string} data - * @return {string} base64 encoded hmac - */ - hmac: function(key, data) { - return sjcl.codec.base64.fromBits( - new sjcl.misc.hmac(key, sjcl.hash.sha256).encrypt(data) - ); - }, - - /** - * @param {string} password - * @param {string} salt - base64 encoded, defaults to 'mjuBtGybi/4=' - * @param {number} iterations - defaults to 100 - * @param {number} length - bits, defaults to 512 bits - * @returns {string} base64 encoded pbkdf2 derivation using sha1 for hmac - */ - kdfbinary: function(password, salt, iterations, length) { - iterations = iterations || defaultIterations; - length = length || 512; - salt = sjcl.codec.base64.toBits(salt || defaultSalt); - - var hash = sjcl.hash.sha256.hash(sjcl.hash.sha256.hash(password)); - var prff = function(key) { - return new sjcl.misc.hmac(hash, sjcl.hash.sha1); - }; - - return sjcl.misc.pbkdf2(hash, salt, iterations, length, prff); - }, - - /** - * Encrypts symmetrically using a passphrase - */ - encrypt: function(key, message, salt, iter) { - if (!_.isString(message)) { - message = JSON.stringify(message); - } - sjcl.json.defaults.salt = salt || defaultSalt; - sjcl.json.defaults.iter = iter || defaultIterations; - return sjcl.encrypt(key, message); - }, - - /** - * Decrypts symmetrically using a passphrase - */ - decrypt: function(key, sjclEncryptedJson) { - var output = {}; - try { - return sjcl.decrypt(key, sjclEncryptedJson); - } catch (e) { - log.debug('Decryption failed due to error: ' + e.message); - return null; - } - } -}; diff --git a/js/util/csv.js b/js/util/csv.js deleted file mode 100644 index b573ea816..000000000 --- a/js/util/csv.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Small module for exporting data to CSV. - */ -var _ = require('lodash'); -var preconditions = require('preconditions').singleton(); -var moment = require('moment'); - -var logger = require('../util/log.js'); -var config = require('../../config'); - - -var COL_DELIMITER = ','; -var ROW_DELIMITER = '\r\n'; - -function getValue(obj, property) { - if (_.isFunction(property)) { - try { - return property(obj); - } catch (err) { - if (_.isString(err)) return err; - return undefined; - } - } - if (!_.isObject(obj)) return undefined; - return obj.hasOwnProperty(property) ? obj[property] : undefined; -}; - -function formatValue(value, type, format) { - if (_.isUndefined(value) || _.isNull(value)) return ''; - - var r; - switch (type) { - default: - case 'string': - r = value.toString(); - r.replace('"', '\\"'); - break; - case 'date': - r = moment(value).format(format); - break; - case 'number': - r = value.toString(); - break; - } - - // escape when commas in values - if (r.indexOf(',') !== -1) { - r = '"' + r + '"'; - } - return r; -}; - -function getHeader(descriptor) { - return _.map(descriptor.columns, function (col) { - return col.label || (_.isString(col.property) ? col.property : '') || ''; - }); -}; - -function processDataRow(data, descriptor) { - return _.map(descriptor.columns, function (col) { - var value = getValue(data, col.property); - var formatted = formatValue(value, col.type, col.format); - return formatted; - }); -}; - -/** - * @desc Convert json object to csv based on a descriptor - * - * @param {array} data - the array of json objects to convert to csv - * @param {object} descriptor - an object that parameterizes the conversion - * @param {function} cb - called with the resulting csv - */ -module.exports.toCsv = function(data, descriptor, cb) { - preconditions.shouldBeArray(data); - preconditions.shouldBeObject(descriptor); - preconditions.shouldBeArray(descriptor.columns); - preconditions.shouldBeFunction(cb); - - var colDelimiter = descriptor.colDelimiter || COL_DELIMITER; - var rowDelimiter = descriptor.rowDelimiter || ROW_DELIMITER; - - var rows = _.map(data, function (dataRow) { - return processDataRow(dataRow, descriptor); - }); - - var header = getHeader(descriptor); - rows.unshift(header); - - var csv = _.reduce(rows, function (memo, row) { - return memo + row.join(colDelimiter) + rowDelimiter; - }, ''); - - return cb(null, csv); -}; diff --git a/js/util/log.js b/js/util/log.js deleted file mode 100644 index c0e7a13a9..000000000 --- a/js/util/log.js +++ /dev/null @@ -1,150 +0,0 @@ -var config = config || require('../../config'); -var _ = require('lodash'); -var ls; - -try { - var LS = require('../js/plugins/LocalStorage'); - ls = new LS(); -} catch (e) {}; - -/** - * @desc - * A simple logger that wraps the console.log methods when available. - * - * Usage: - *
- *   log = new Logger('copay');
- *   log.setLevel('info');
- *   log.debug('Message!'); // won't show
- *   log.setLevel('debug');
- *   log.debug('Message!', 1); // will show '[debug] copay: Message!, 1'
- * 
- * - * @param {string} name - a name for the logger. This will show up on every log call - * @constructor - */ -var Logger = function(name) { - this.name = name || 'log'; - this.level = 2; -}; - -Logger.prototype.getLevels = function() { - return levels; -}; - - -var levels = { - 'debug': 0, - 'info': 1, - 'log': 2, - 'warn': 3, - 'error': 4, - 'fatal': 5 -}; - -_.each(levels, function(level, levelName) { - Logger.prototype[levelName] = function() { - if (level >= levels[this.level]) { - - if (Error.stackTraceLimit && this.level == 'debug') { - var old = Error.stackTraceLimit; - Error.stackTraceLimit = 2; - var stack; - - // this hack is to be compatible with IE11 - try { - anerror(); - } catch (e) { - stack = e.stack; - } - var lines = stack.split('\n'); - var caller = lines[2]; - caller = ':' + caller.substr(6); - Error.stackTraceLimit = old; - } - - var str = '[' + levelName + (caller || '') + '] ' + arguments[0], - extraArgs, - extraArgs = [].slice.call(arguments, 1); - if (console[levelName]) { - extraArgs.unshift(str); - console[levelName].apply(console, extraArgs); - } else { - if (extraArgs.length) { - str += JSON.stringify(extraArgs); - } - console.log(str); - } - } - }; -}); - -/** - * @desc - * Sets the level of a logger. A level can be any bewteen: 'debug', 'info', 'log', - * 'warn', 'error', and 'fatal'. That order matters: if a logger's level is set to - * 'warn', calling level.debug won't have any effect. - * - * @param {number} level - the name of the logging level - */ -Logger.prototype.setLevel = function(level) { - this.level = level; -}; - -/** - * @class Logger - * @method debug - * @desc Log messages at the debug level. - * @param {*} args - the arguments to be logged. - */ -/** - * @class Logger - * @method info - * @desc Log messages at the info level. - * @param {*} args - the arguments to be logged. - */ -/** - * @class Logger - * @method log - * @desc Log messages at an intermediary level called 'log'. - * @param {*} args - the arguments to be logged. - */ -/** - * @class Logger - * @method warn - * @desc Log messages at the warn level. - * @param {*} args - the arguments to be logged. - */ -/** - * @class Logger - * @method error - * @desc Log messages at the error level. - * @param {*} args - the arguments to be logged. - */ -/** - * @class Logger - * @method fatal - * @desc Log messages at the fatal level. - * @param {*} args - the arguments to be logged. - */ - -var logger = new Logger('copay'); -var error = new Error(); - -var logLevel = config.logLevel || 'info'; - - - -if (ls && ls.getItem) { - ls.getItem("config", function(err, value) { - if (err) return; - var localConfig = JSON.parse(value); - if (localConfig && localConfig.logLevel) - logLevel = localConfig.logLevel; - logger.setLevel(logLevel); - }); -} else { - logger.setLevel(logLevel); -} - -module.exports = logger; diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 0390efd96..000000000 --- a/karma.conf.js +++ /dev/null @@ -1,118 +0,0 @@ -// Karma configuration -// Generated on Fri Mar 21 2014 17:52:41 GMT-0300 (ART) - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['mocha', 'chai'], - - - // list of files / patterns to load in the browser - files: [ - //Configs - 'config.js', - - 'lib/angular/angular.min.js', - 'lib/angular-mocks/angular-mocks.js', - 'lib/moment/moment.js', - 'lib/ng-idle/angular-idle.min.js', - 'lib/angular-moment/angular-moment.js', - 'lib/qrcode-generator/js/qrcode.js', - 'lib/angular-qrcode/qrcode.js', - 'lib/angular-route/angular-route.min.js', - 'lib/angular-foundation/mm-foundation.min.js', - 'lib/angular-foundation/mm-foundation-tpls.min.js', - 'lib/angular-load/angular-load.min.js', - 'lib/angular-gravatar/build/md5.min.js', - 'lib/angular-gravatar/build/angular-gravatar.min.js', - 'lib/angular-touch/angular-touch.min.js', - 'lib/angular-gettext/dist/angular-gettext.min.js', - 'lib/inherits/inherits.js', - 'lib/lodash/dist/lodash.js', - 'lib/file-saver/FileSaver.js', - 'lib/socket.io-client/socket.io.js', - 'lib/sjcl.js', - 'lib/qrcode-decoder-js/lib/qrcode-decoder.min.js', - - 'lib/bitcore.js', - 'js/copayBundle.js', - - //App-specific Code - 'js/app.js', - 'js/routes.js', - 'js/services/*.js', - 'js/directives.js', - 'js/filters.js', - 'js/controllers/*.js', - 'js/translations.js', - 'js/init.js', - - 'test/mocks/FakeBlockchainSocket.js', - - 'test/mocha.conf.js', - - //test files - 'setup/karma.js', - 'test/unit/**/*.js', - 'test/*.js', - ], - - - // list of files to exclude - exclude: [], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'js/controllers/*.js': ['coverage'] - }, - - coverageReporter: { - type: 'html', - dir: 'coverage/' - }, - - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', 'coverage'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome', 'Firefox'], - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: false, - - // if browser doesn't capture output in given timeout (ms), kill it - captureTimeout: 60000 - }); -}; diff --git a/launch.js b/launch.js deleted file mode 100755 index 5b9d9c2ab..000000000 --- a/launch.js +++ /dev/null @@ -1,34 +0,0 @@ -#! /usr/bin/node - -'use strict'; - - - -var sys = require('sys') -var exec = require('child_process').exec; - -function puts(error, stdout, stderr) { - sys.puts(stdout) -} - -function isNumber(n) { - return !isNaN(parseInt(n)) && isFinite(n); -} - -var args = process.argv.slice(2); -var n_str = args[0]; -if (!isNumber(n_str)) { - console.log('Program requires one numeric argument for the amount of copayers'); - process.exit(1); -} - -var N = parseInt(n_str); -var DEFAULT_PORT = process.env.DEFAULT_PORT || 3000; - - -for (var i = 0; i < N; i++) { - var port = (i + DEFAULT_PORT); - console.log('Simulating copayer #' + (i + 1) + ' at http://localhost:' + port); - var command = 'PORT=' + port + ' npm start &' - exec(command, puts); -} diff --git a/lib/bitcore.js b/lib/bitcore.js deleted file mode 100644 index be555e03b..000000000 --- a/lib/bitcore.js +++ /dev/null @@ -1,636 +0,0 @@ -require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oi;i++)n[i]^=r[i];var o=t.fromUncompressedPubKey(e);o=t.multiply(o,n);var u=o.toUncompressedPubKey();return u},r.prototype.next=function(){var e=this.generatePubKey();return new r(this.chaincode,e)},r.fromMasterPublicKey=function(e){var n=e.substr(0,130),t=e.substr(130,e.length);return new r(t,n)},r.decodeSeed=function(t){for(var i=t.trim().split("\n"),o=[],c=0;ct)return!0;if(t>n)return!1;var o=r.slice(4,8).readUInt32BE(0),i=e.slice(4,8).readUInt32BE(0);return o>i?!0:!1},a._encrypt=function(r,e,t,o){var i=n.encrypt(r,e,t,o);return i},a._decrypt=function(r,e){var t=n.decrypt(r,e);return t},a._sign=function(r,n){var t=e.sign(n,r);return t},a._verify=function(r,n,t){var o=e.verifyWithPubKey(r,t,n);return o},module.exports=a}).call(this,require("buffer").Buffer); -},{"./ECIES":"0Qraa1","./Key":"ALJ4PS","./Message":"CBDCgz","buffer":111,"preconditions":180}],"./lib/AuthMessage":[function(require,module,exports){ -module.exports=require('cBnJMk'); -},{}],"Rpcuro":[function(require,module,exports){ -"use strict";var URL=require("url"),Address=require("./Address"),BIP21=function(t){if(this.data={},this.address=void 0,"string"==typeof t)this.parse(t);else if("object"==typeof t)this.fromObj(t);else if("undefined"!=typeof t)throw new Error("Invalid argument")};BIP21.prototype.fromObj=function(t){for(var r in t)this.data[r]=t[r];t.address&&(delete this.data.address,this.setAddress(t.address))},BIP21.prototype.parse=function(t){var r=URL.parse(t,!0);if("bitcoin:"!=r.protocol)throw new Error("Invalid protocol");var e=/[^:]*:\/?\/?([^?]*)/.exec(t);this.setAddress(e&&e[1]);for(var s in r.query){var d=r.query[s];"amount"===s&&(d=Number(d)),"r"===s&&(this.data.merchant=d),this.data[s]=d}},BIP21.prototype.isValid=function(t){var r=t||[],e=!0;"undefined"!=typeof this.data.amount&&(e&=!isNaN(this.data.amount)),this.address&&(e&="object"==typeof this.address&&this.address.isValid()),e&=!(!this.address&&!this.data.r);for(var s in this.data)0==s.indexOf("req-")&&(e&=-1!=r.indexOf(s));return!!e},BIP21.prototype.setAddress=function(t){return t&&(this.address=Address.validate(t)?new Address(t):t),this},BIP21.prototype.getURI=function(){return URL.format({protocol:"bitcoin:",host:this.address,query:this.data})},module.exports=BIP21; -},{"./Address":"G+CcXD","url":142}],"./lib/BIP21":[function(require,module,exports){ -module.exports=require('Rpcuro'); -},{}],"./lib/BIP39":[function(require,module,exports){ -module.exports=require('82LilS'); -},{}],"82LilS":[function(require,module,exports){ -(function(e){var n=require("../util"),r=require("./sjcl"),t=require("./SecureRandom"),i=function(e){var n=new r.misc.hmac(e,r.hash.sha512);this.encrypt=function(){return n.encrypt.apply(n,arguments)}},o=function(e,n,t){var o=r.misc.pbkdf2(e,n,t,512,i);return r.codec.hex.fromBits(o)},l=function(){};l.mnemonic=function(e,n){if(n||(n=128),n%32!=0)throw new Error("bits must be multiple of 32");var r=t.getRandomBuffer(n/8);return l.entropy2mnemonic(e,r)},l.entropy2mnemonic=function(e,r){for(var t=n.sha256(r),i="",o=8*r.length,l=0;lc)return!1;o+=("00000000000"+c.toString(2)).slice(-11)}if(o.length%11!=0)throw new Error("internal error - entropy not an even multiple of 11 bits - "+o.length);for(var u=o.length/33,s=o.slice(-u),a=o.slice(0,o.length-u),f=new e(a.length/8),l=0;l0){var i=new e(t);return i.fill(0),t==r.length?i:(n=n.toBuffer(),e.concat([i,n],t+n.length))}return n.toBuffer()}},s={encode:function(r){var t=new e(r.length+4),o=n(r);return r.copy(t),o.copy(t,r.length),l.encode(t)},decode:function(e){var r=l.decode(e);if(r.length<4)throw new Error("invalid input: too short");var t=r.slice(0,-4),o=r.slice(-4),i=n(t),c=i.slice(0,4);if(o.toString("hex")!==c.toString("hex"))throw new Error("checksum mismatch");return t}};exports.setBuffer=function(e){i=e},exports.base58=l,exports.base58Check=s,exports.encode=l.encode,exports.decode=l.decode}).call(this,require("buffer").Buffer); -},{"bignum":63,"buffer":111,"crypto":115}],"./lib/Block":[function(require,module,exports){ -module.exports=require('pJEQEB'); -},{}],"pJEQEB":[function(require,module,exports){ -(function(t){function e(t){"object"!=typeof t&&(t={}),this.hash=t.hash||null,this.prev_hash=t.prev_hash||r.NULL_HASH,this.merkle_root=t.merkle_root||r.NULL_HASH,this.timestamp=t.timestamp||0,this.bits=t.bits||0,this.nonce=t.nonce||0,this.version=t.version||0,this.height=t.height||0,this.size=t.size||0,this.active=t.active||!1,this.chainWork=t.chainWork||r.EMPTY_BUFFER,this.txs=t.txs||[]}var r=require("../util"),i=require("./Script"),o=require("bignum"),h=require("buffertools"),s=require("./Transaction"),n=s.In,a=s.Out,c=s.COINBASE_OP,u=require("../util/error").VerificationError,l={maxTimeOffset:7200,largestHash:new o("10000000000000000000000000000000000000000000000000000000000000000",16)};e.prototype.getHeader=function(){var e=new t(80),r=0;return e.writeUInt32LE(this.version,r),r+=4,this.prev_hash.copy(e,r),r+=32,this.merkle_root.copy(e,r),r+=32,e.writeUInt32LE(this.timestamp,r),r+=4,e.writeUInt32LE(this.bits,r),r+=4,e.writeUInt32LE(this.nonce,r),r+=4,e},e.prototype.parse=function(t,e){if(this.version=t.word32le(),this.prev_hash=t.buffer(32),this.merkle_root=t.buffer(32),this.timestamp=t.word32le(),this.bits=t.word32le(),this.nonce=t.word32le(),this.txs=[],this.size=0,!e)for(var r=t.varInt(),i=0;r>i;i++){var o=new s;o.parse(t),this.txs.push(o)}},e.prototype.calcHash=function(){var t=this.getHeader();return r.twoSha256(t)},e.prototype.checkHash=function(){return this.hash&&this.hash.length?0==h.compare(this.calcHash(),this.hash):!1},e.prototype.getHash=function(){return this.hash&&this.hash.length||(this.hash=this.calcHash()),this.hash},e.prototype.checkProofOfWork=function(){var t=r.decodeDiffBits(this.bits),e=h.reverse(this.hash);if(h.compare(e,t)>0)throw new u("Difficulty target not met");return!0},e.prototype.getWork=function(){var t=r.decodeDiffBits(this.bits,!0);return l.largestHash.div(t.add(1))},e.prototype.checkTimestamp=function(){var t=(new Date).getTime()/1e3;if(this.timestamp>t+l.maxTimeOffset)throw new u("Timestamp too far into the future");return!0},e.prototype.checkTransactions=function(t){if(!Array.isArray(t)||t.length<=0)throw new u("No transactions");if(!t[0].isCoinBase())throw new u("First tx must be coinbase");for(var e=1;e1;h=Math.floor((h+1)/2)){for(var n=0;h>n;n+=2){var a=Math.min(n+1,h-1),c=i[o+n],u=i[o+a];i.push(r.twoSha256(t.concat([c,u])))}o+=h}return i},e.prototype.calcMerkleRoot=function(t){var e=this.getMerkleTree(t);return e[e.length-1]},e.prototype.checkMerkleRoot=function(e){if(!this.merkle_root||!this.merkle_root.length)throw new u("No merkle root");if(0!==h.compare(this.calcMerkleRoot(e),new t(this.merkle_root)))throw new u("Merkle root incorrect");return!0},e.prototype.checkBlock=function(t){if(!this.checkHash())throw new u("Block hash invalid");if(this.checkProofOfWork(),this.checkTimestamp(),t&&(this.checkTransactions(t),!this.checkMerkleRoot(t)))throw new u("Merkle hash invalid");return!0},e.getBlockValue=function(t){var e=50*r.COIN;return e/=Math.pow(2,Math.floor(t/21e4)),e=Math.floor(e),e=new o(e)},e.prototype.getBlockValue=function(){return e.getBlockValue(this.height)},e.prototype.toString=function(){return""},e.prototype.createCoinbaseTx=function(t){var e=new s;return e.ins.push(new n({s:r.EMPTY_BUFFER,q:4294967295,o:c})),e.outs.push(new a({v:r.bigIntToValue(this.getBlockValue()),s:i.createPubKeyOut(t).getBuffer()})),e},e.prototype.solve=function(t,e){var i=this.getHeader(),o=r.decodeDiffBits(this.bits);t.solve(i,o,e)},e.prototype.getStandardizedObject=function(t){var e={hash:r.formatHashFull(this.getHash()),version:this.version,prev_block:r.formatHashFull(this.prev_hash),mrkl_root:r.formatHashFull(this.merkle_root),time:this.timestamp,bits:this.bits,nonce:this.nonce,height:this.height};if(t){var i=this.getMerkleTree(t).map(function(t){return r.formatHashFull(t)});e.mrkl_root=i[i.length-1],e.n_tx=t.length;var o=80;o+=r.getVarIntSize(t.length),t=t.map(function(t){return t=t.getStandardizedObject(),o+=t.size,t}),e.size=o,e.tx=t,e.mrkl_tree=i}else e.size=this.size;return e},module.exports=e}).call(this,require("buffer").Buffer); -},{"../util":207,"../util/error":206,"./Script":"hQ0t76","./Transaction":"LJhYtm","bignum":63,"buffer":111,"buffertools":"fugeBw"}],"./lib/Bloom":[function(require,module,exports){ -module.exports=require('KifRG4'); -},{}],"KifRG4":[function(require,module,exports){ -function Bloom(){this.data="",this.hashFuncs=0}function ROTL32(t,n){return t<>32-n}function getBlockU32(t,n){var o=4*t,s=n[o+0]<<0|n[o+1]<<8|n[o+2]<<16|n[o+3]<<24;return s}function toInt(t){return~~t}function min(t,n){return n>t?t:n}var MAX_BLOOM_FILTER_SIZE=36e3,MAX_HASH_FUNCS=50,LN2SQUARED=.48045301391820144,LN2=.6931471805599453,bit_mask=[1,2,4,8,16,32,64,128];Bloom.prototype.hash=function(t,n){for(var o=t*(4294967295/(this.hashFuncs-1)),s=3432918353,a=461845907,h=n.length/4,i=-h;i;i++){var r=getBlockU32(i);r*=s,r=ROTLF32(r,15),r*=a,o^=r,o=ROTFL(o,13),o=5*o+3864292196}var e=n.slice(4*h),r=0;switch(3&n.length){case 3:r^=e[2]<<16;case 2:r^=e[1]<<8;case 1:r^=e[0],r*=s,r=ROTL32(r,15),r*=a,o^=r}return o^=n.length,o^=o>>16,o*=2246822507,o^=o>>13,o*=3266489909,o^=o>>16,o%(8*this.data.length)},Bloom.prototype.insert=function(t){for(var n=0;n>3]|=bit_mask[7&o]}},Bloom.prototype.contains=function(t){for(var n=0;n>3]&bit_mask[7&o]))return!1}return!0},Bloom.prototype.sizeOk=function(){return this.data.length<=MAX_BLOOM_FILTER_SIZE&&this.hashFuncs<=MAX_HASH_FUNCS},Bloom.prototype.init=function(t,n){var o=min(toInt(-1/LN2SQUARED*t*Math.log(n)),8*MAX_BLOOM_FILTER_SIZE)/8;this.data[o]=0,this.hashFuncs=min(toInt(8*this.data.length/t*LN2),MAX_HASH_FUNCS)},module.exports=Bloom; -},{}],"./lib/Connection":[function(require,module,exports){ -module.exports=require('DB/p3X'); -},{}],"DB/p3X":[function(require,module,exports){ -(function(e){function t(e,t,r){if(this.config=r||a,this.network=h[this.config.network]||h.livenet,this.socket=e,this.peer=t,this.config.proxy){var s=require("socks5-client");this.socket=new s(this.config.proxy.host,this.config.proxy.port)}this.active=!1,this.recvVer=0,this.sendVer=0,this.bestHeight=0,this.inbound=!!this.socket.server,this.getaddr=!1,this.buffers=new o,(new Date).getTime()>1329696e6&&(this.recvVer=209,this.sendVer=209),this.setupHandlers()}var r=require("../util/log"),s=1e7,n=7e4,i=require("bufferput"),o=require("buffers");require("../patches/Buffers.monkey").patch(o);var a=require("../config"),h=require("../networks"),c=require("./Block"),d=require("./Transaction"),f=require("../util"),u=require("../util/BinaryParser"),p=require("buffertools"),g=f.twoSha256,l=require("./SecureRandom"),v=l.getPseudoRandomBuffer(8),b=require("util"),k=require("events").EventEmitter,m=6e4;b.inherits(t,k),t.prototype.open=function(e){return"function"==typeof e&&this.once("connect",e),this.socket.connect(this.peer.port,this.peer.host),this},t.prototype.setupHandlers=function(){this.socket.addListener("connect",this.handleConnect.bind(this)),this.socket.addListener("error",this.handleError.bind(this)),this.socket.addListener("end",this.handleDisconnect.bind(this)),this.socket.addListener("data",function(e){var t=35;r.debug("["+this.peer+"] Recieved "+e.length+" bytes of data:"),r.debug("... "+p.toHex(e.slice(0,t>e.length?e.length:t))+(e.length>t?"...":""))}.bind(this)),this.socket.addListener("data",this.handleData.bind(this))},t.prototype.handleConnect=function(){this.inbound||this.sendVersion(),this.emit("connect",{conn:this,socket:this.socket,peer:this.peer})},t.prototype.handleError=function(e){110==e.errno||"ETIMEDOUT"==e.errno?r.info("connection timed out for "+this.peer):111==e.errno||"ECONNREFUSED"==e.errno?r.info("connection refused for "+this.peer):r.warn("connection with "+this.peer+" "+e.toString()),this.emit("error",{conn:this,socket:this.socket,peer:this.peer,err:e})},t.prototype.handleDisconnect=function(){this.emit("disconnect",{conn:this,socket:this.socket,peer:this.peer})},t.prototype.handleMessage=function(t){if(t){try{switch(t.command){case"version":if(0===p.compare(v,t.nonce))return void this.socket.end();this.inbound&&this.sendVersion(),t.version>=209&&this.sendMessage("verack",new e([])),this.sendVer=Math.min(t.version,n),t.version<209?this.recvVer=Math.min(t.version,n):this.once("verack",function(){this.recvVer=t.version}.bind(this)),this.bestHeight=t.start_height;break;case"verack":this.recvVer=Math.min(t.version,n),this.active=!0;break;case"ping":"object"==typeof t.nonce&&this.sendPong(t.nonce)}}catch(s){return void r.err('Error while handling "'+t.command+'" message from '+this.peer+":\n"+(s.stack?s.stack:s.toString()))}this.emit(t.command,{conn:this,socket:this.socket,peer:this.peer,message:t})}},t.prototype.sendPong=function(e){this.sendMessage("pong",e)},t.prototype.sendVersion=function(){var t="/BitcoinX:0.1/",r=new i;r.word32le(n),r.word64le(1),r.word64le(Math.round((new Date).getTime()/1e3)),r.pad(26),r.pad(26),r.put(v),r.varint(t.length),r.put(new e(t,"ascii")),r.word32le(0),this.sendMessage("version",r.buffer())},t.prototype.sendGetBlocks=function(t,r,s){r=r||f.NULL_HASH;var n=new i;n.word32le(this.sendVer),n.varint(t.length);for(var o=0;o12)throw"Command name too long";var a;a=this.sendVer>=209?g(s).slice(0,4):new e([]);var h=new i;h.put(n),h.put(o),h.pad(12-o.length),h.word32le(s.length),h.put(a),h.put(s);var c=h.buffer();r.debug("["+this.peer+"] Sending message "+t+" ("+s.length+" bytes)"),this.socket.write(c)}catch(d){r.err("Error while sending message to peer "+this.peer+": "+(d.stack?d.stack:d.toString()))}},t.prototype.handleData=function(e){return this.buffers.push(e),this.buffers.length>s?(r.err("Peer "+this.peer+" exceeded maxreceivebuffer, disconnecting."+(err.stack?err.stack:err.toString())),void this.socket.destroy()):void this.processData()},t.prototype.processData=function(){if(!(this.buffers.length<20)){for(var e=this.network.magic,t=0;;){if(this.buffers.get(t)===e[0]&&this.buffers.get(t+1)===e[1]&&this.buffers.get(t+2)===e[2]&&this.buffers.get(t+3)===e[3]){0!==t&&(r.debug("["+this.peer+"] Received "+t+" bytes of inter-message garbage: "),r.debug("... "+this.buffers.slice(0,t)),this.buffers.skip(t));break}if(t>this.buffers.length-4)return void this.buffers.skip(t);t++}var s=this.buffers.get(16)+(this.buffers.get(17)<<8)+(this.buffers.get(18)<<16)+(this.buffers.get(19)<<24),n=this.recvVer>=209?24:20,i=n+s;if(!(this.buffers.length=209?this.buffers.slice(20,24):null;if(r.debug("["+this.peer+"] Received message "+o+" ("+s+" bytes)"),null!==h){var c=g(a).slice(0,4);if(0!==p.compare(c,h))return void r.err("["+this.peer+"] Checksum failed",{cmd:o,expected:c.toString("hex"),actual:h.toString("hex")})}var d;try{d=this.parseMessage(o,a)}catch(f){r.err("Error while parsing message "+o+" from "+this.peer+":\n"+(f.stack?f.stack:f.toString()))}d&&this.handleMessage(d),this.buffers.skip(i),this.processData()}}},t.prototype.parseMessage=function(e,t){var s,n=new u(t),i={command:e};switch(e){case"version":i.version=n.word32le(),i.services=n.word64le(),i.timestamp=n.word64le(),i.addr_me=n.buffer(26),i.addr_you=n.buffer(26),i.nonce=n.buffer(8),i.subversion=n.varStr(),i.start_height=n.word32le();break;case"inv":case"getdata":for(i.count=n.varInt(),i.invs=[],s=0;ss;s++)i.starts.push(n.buffer(32));i.stop=n.buffer(32);break;case"addr":var p=n.varInt();for(p>1e3&&(p=1e3),i.addrs=[],s=0;p>s;s++)i.addrs.push({time:n.word32le(),services:n.word64le(),ip:n.buffer(16),port:n.word16be()});break;case"alert":i.payload=n.varStr(),i.signature=n.varStr();break;case"ping":this.recvVer>m&&(i.nonce=n.buffer(8));break;case"getaddr":case"verack":case"reject":break;default:return r.err("Connection.parseMessage(): Command not implemented",{cmd:e}),null}return i},module.exports=t}).call(this,require("buffer").Buffer); -},{"../config":"4itQ50","../networks":"ULNIu2","../patches/Buffers.monkey":"kytKTK","../util":207,"../util/BinaryParser":"b3ZSD7","../util/log":"AdF7pF","./Block":"pJEQEB","./SecureRandom":"p4SiC2","./Transaction":"LJhYtm","buffer":111,"bufferput":"aXRuS6","buffers":"OBo3aV","buffertools":"fugeBw","events":"T9Wsc/","socks5-client":189,"util":144}],"ez/meX":[function(require,module,exports){ -exports.intFromCompact=function(r){var t=(r>>>24&255)>>>0,n=(16777215&r)<<8*(t-3)>>>0;return n}; -},{}],"./lib/Deserialize":[function(require,module,exports){ -module.exports=require('ez/meX'); -},{}],"./lib/Electrum":[function(require,module,exports){ -module.exports=require('hdzBvq'); -},{}],"hdzBvq":[function(require,module,exports){ -(function(e){function r(r){this.mpk=new e(r,"hex")}var t=require("./Key"),u=require("./Point"),n=require("../util").twoSha256,i=(require("buffertools"),require("bignum"));r.prototype.getSequence=function(r,t){var u=r?1:0,o=e.concat([new e(t+":"+u+":","utf8"),this.mpk]);return i.fromBuffer(n(o))},r.prototype.generatePubKey=function(r,n){var o=i.fromBuffer(this.mpk.slice(0,32),{size:32}),f=i.fromBuffer(this.mpk.slice(32,64),{size:32}),c=new u(o,f),p=this.getSequence(n,r),s=new t;s.private=p.toBuffer(),s.regenerateSync(),s.compressed=!1;var a=u.fromUncompressedPubKey(s.public);pt=u.add(c,a);var m=pt.x.toBuffer({size:32}),b=pt.y.toBuffer({size:32}),h=new e([4]),l=new t;return l.compressed=!1,l.public=e.concat([h,m,b]),l.public},r.prototype.generateChangePubKey=function(e){return this.generatePubKey(e,!0)},module.exports=r}).call(this,require("buffer").Buffer); -},{"../util":207,"./Key":"ALJ4PS","./Point":"6tXgqr","bignum":63,"buffer":111,"buffertools":"fugeBw"}],"x1O6JW":[function(require,module,exports){ -(function(e){function i(e,i){if(e.lengthn;n++)t*=256,t+=e[n];return t}function t(e){return i(e,1)}function n(e){return i(e,4)}var r=require("./Base58").base58,s=require("../util"),h=require("./Key"),a=require("./Point"),c=require("./SecureRandom"),o=require("bignum"),d=require("../networks"),l=new o("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",16),u=(new o("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",16),function(i){if("undefined"==typeof i||"mainnet"==i||"livenet"==i?(i="livenet",this.version=d.livenet.hkeyPrivateVersion):"testnet"==i&&(this.version=d.testnet.hkeyPrivateVersion),"livenet"==i||"testnet"==i)return this.depth=0,this.parentFingerprint=new e([0,0,0,0]),this.childIndex=new e([0,0,0,0]),this.chainCode=c.getRandomBuffer(32),this.eckey=h.generateSync(),this.hasPrivateKey=!0,this.pubKeyHash=s.sha256ripe160(this.eckey.public),this.buildExtendedPublicKey(),void this.buildExtendedPrivateKey();if("string"==typeof i){var t=r.decode(i);if(82!=t.length)throw new Error("Not enough data, expected 82 and received "+t.length);var n=t.slice(78,82);i=t.slice(0,78);var a=s.sha256(s.sha256(i));if(a[0]!=n[0]||a[1]!=n[1]||a[2]!=n[2]||a[3]!=n[3])throw new Error("Invalid checksum")}void 0!==i&&null!==i&&this.initFromBytes(i)});u.seed=function(i,t){if(t||(t="livenet"),e.isBuffer(i)||(i=new e(i,"hex")),i.length<16)return!1;if(i.length>64)return!1;var n=s.sha512hmac(i,new e("Bitcoin seed")),r=new u(null);return r.depth=0,r.parentFingerprint=new e([0,0,0,0]),r.childIndex=new e([0,0,0,0]),r.chainCode=n.slice(32,64),r.version=d[t].hkeyPrivateVersion,r.eckey=new h,r.eckey.private=n.slice(0,32),r.eckey.regenerateSync(),r.hasPrivateKey=!0,r.pubKeyHash=s.sha256ripe160(r.eckey.public),r.buildExtendedPublicKey(),r.buildExtendedPrivateKey(),r},u.prototype.initFromBytes=function(e){if(78!=e.length)throw new Error("not enough data");this.version=n(e.slice(0,4)),this.depth=t(e.slice(4,5)),this.parentFingerprint=e.slice(5,9),this.childIndex=n(e.slice(9,13)),this.chainCode=e.slice(13,45);var i=e.slice(45,78),r=this.version==d.livenet.hkeyPrivateVersion||this.version==d.testnet.hkeyPrivateVersion,a=this.version==d.livenet.hkeyPublicVersion||this.version==d.testnet.hkeyPublicVersion;if(r&&0==i[0])this.eckey=new h,this.eckey.private=i.slice(1,33),this.eckey.compressed=!0,this.eckey.regenerateSync(),this.pubKeyHash=s.sha256ripe160(this.eckey.public),this.hasPrivateKey=!0;else{if(!a||2!=i[0]&&3!=i[0])throw new Error("Invalid key");this.eckey=new h,this.eckey.public=i,this.pubKeyHash=s.sha256ripe160(this.eckey.public),this.hasPrivateKey=!1}this.buildExtendedPublicKey(),this.buildExtendedPrivateKey()},u.prototype.buildExtendedPublicKey=function(){this.extendedPublicKey=new e([]);var i=null;switch(this.version){case d.livenet.hkeyPublicVersion:case d.livenet.hkeyPrivateVersion:i=d.livenet.hkeyPublicVersion;break;case d.testnet.hkeyPublicVersion:case d.testnet.hkeyPrivateVersion:i=d.testnet.hkeyPublicVersion;break;default:throw new Error("Unknown version")}this.extendedPublicKey=e.concat([new e([i>>24]),new e([i>>16&255]),new e([i>>8&255]),new e([255&i]),new e([this.depth]),this.parentFingerprint,new e([this.childIndex>>>24]),new e([this.childIndex>>>16&255]),new e([this.childIndex>>>8&255]),new e([255&this.childIndex]),this.chainCode,this.eckey.public])},u.prototype.extendedPublicKeyString=function(i){if(void 0===i||"base58"===i){var t=s.sha256(s.sha256(this.extendedPublicKey)),n=t.slice(0,4),h=e.concat([this.extendedPublicKey,n]);return r.encode(h)}if("hex"===i)return this.extendedPublicKey.toString("hex");throw new Error("bad format")},u.prototype.buildExtendedPrivateKey=function(){if(this.hasPrivateKey){this.extendedPrivateKey=new e([]);var i=this.version;this.extendedPrivateKey=e.concat([new e([i>>24]),new e([i>>16&255]),new e([i>>8&255]),new e([255&i]),new e([this.depth]),this.parentFingerprint,new e([this.childIndex>>>24]),new e([this.childIndex>>>16&255]),new e([this.childIndex>>>8&255]),new e([255&this.childIndex]),this.chainCode,new e([0]),this.eckey.private])}},u.prototype.extendedPrivateKeyString=function(i){if(void 0===i||"base58"===i){var t=s.sha256(s.sha256(this.extendedPrivateKey)),n=t.slice(0,4),h=e.concat([this.extendedPrivateKey,n]);return r.encode(h)}if("hex"===i)return this.extendedPrivateKey.toString("hex");throw new Error("bad format")},u.prototype.derive=function(e){var i=e.split("/");if("m"==e||"M"==e||"m'"==e||"M'"==e)return this;var t=this;for(var n in i){var r=i[n];if(0!=n){var s=r.length>1&&"'"==r[r.length-1],h=2147483647&parseInt(s?r.slice(0,r.length-1):r);s&&(h+=2147483648),t=t.deriveChild(h)}else if("m"!=r)throw new Error("invalid path")}return t},u.prototype.deriveChild=function(i){var t=[];t.push(i>>24&255),t.push(i>>16&255),t.push(i>>8&255),t.push(255&i),t=new e(t);var n=0!=(2147483648&i),r=this.version==d.livenet.hkeyPrivateVersion||this.version==d.testnet.hkeyPrivateVersion;if(n&&(!this.hasPrivateKey||!r))throw new Error("Cannot do private key derivation without private key");var c=null;if(this.hasPrivateKey){var y=null;y=e.concat(n?[new e([0]),this.eckey.private,t]:[this.eckey.public,t]);var v=s.sha512hmac(y,this.chainCode),p=o.fromBuffer(v.slice(0,32),{size:32}),w=v.slice(32,64),b=o.fromBuffer(this.eckey.private,{size:32}),f=p.add(b).mod(l);c=new u(null),c.chainCode=w,c.eckey=new h,c.eckey.private=f.toBuffer({size:32}),c.eckey.regenerateSync(),c.hasPrivateKey=!0}else{var y=e.concat([this.eckey.public,t]),v=s.sha512hmac(y,this.chainCode),p=v.slice(0,32),w=v.slice(32,64),k=new h;k.private=p,k.regenerateSync(),k.compressed=!1;var P=a.fromUncompressedPubKey(k.public),F=new h;F.public=this.eckey.public,F.compressed=!1;var x=a.fromUncompressedPubKey(F.public),K=a.add(P,x).toUncompressedPubKey();c=new u(null),c.chainCode=new e(w);var g=new h;g.public=K,g.compressed=!0,c.eckey=g,c.hasPrivateKey=!1}return c.childIndex=i,c.parentFingerprint=this.pubKeyHash.slice(0,4),c.version=this.version,c.depth=this.depth+1,c.eckey.compressed=!0,c.pubKeyHash=s.sha256ripe160(c.eckey.public),c.buildExtendedPublicKey(),c.buildExtendedPrivateKey(),c},module.exports=u}).call(this,require("buffer").Buffer); -},{"../networks":"ULNIu2","../util":207,"./Base58":"6VqyzY","./Key":"ALJ4PS","./Point":"6tXgqr","./SecureRandom":"p4SiC2","bignum":63,"buffer":111}],"./lib/HierarchicalKey":[function(require,module,exports){ -module.exports=require('x1O6JW'); -},{}],"CBDCgz":[function(require,module,exports){ -(function(e){"use strict";var r=require("../util"),i=require("./Key"),n=require("bignum"),r=require("../util"),t=function(){};t.sign=function(e,r){var i=t.magicHash(e),n=r.signSync(i);return n},t.verifyWithPubKey=function(e,r,n){var u=t.magicHash(r),a=new i;return 65==e.length&&(a.compressed=!1),a.public=e,a.verifySignatureSync(u,n)},t.signMessage=function(e,r){var u=t.magicHash(e),a=n.fromBuffer(r.private),s=i.signCompressed(u,a);return s},t.verifyMessage=function(e,r,n){var u=t.magicHash(r);return i.verifyCompressed(u,n,e)},t.magicBytes=new e("Bitcoin Signed Message:\n"),t.magicHash=function(i){var n=t.magicBytes,u=r.varIntBuf(n.length),a=new e(i),s=r.varIntBuf(a.length),c=e.concat([u,n,s,a]),f=r.twoSha256(c);return f},module.exports=t}).call(this,require("buffer").Buffer); -},{"../util":207,"./Key":"ALJ4PS","bignum":63,"buffer":111}],"./lib/Message":[function(require,module,exports){ -module.exports=require('CBDCgz'); -},{}],"qYkfjX":[function(require,module,exports){ -var log=require("../util/log"),networks=require("../networks"),Address=require("./Address"),Peer=require("./Peer"),PeerManager=require("./PeerManager"),util=require("util"),EventEmitter=require("events").EventEmitter,preconditions=require("preconditions").singleton(),NetworkMonitor=function(e){preconditions.checkArgument(e),this.peerman=e,this.networkName=e.config.network,this.init()};util.inherits(NetworkMonitor,EventEmitter),NetworkMonitor.create=function(e){this.config=e;var t=new PeerManager({network:e.networkName});return t.addPeer(new Peer(e.host,e.port)),new NetworkMonitor(t)},NetworkMonitor.prototype.init=function(){var e=this,t=function(e){var t=e.message.invs;e.conn.sendGetData(t)},n=function(t){e.emit("block",t.message)},o=function(t){var n=t.message.tx;e.emit("tx",n);for(var o=n.getSendingAddresses(e.networkName),r=0;r=31402||this.peers.length<1e3)&&(e.conn.sendGetAddr(),e.conn.getaddr=!0)},PeerManager.prototype.handleReady=function(e){log.info("connected to "+e.conn.peer.host+":"+e.conn.peer.port),this.emit("connect",{pm:this,conn:e.conn,socket:e.socket,peer:e.peer}),0==this.isConnected&&(this.emit("netConnected",e),this.isConnected=!0)},PeerManager.prototype.handleAddr=function(e){if(this.peerDiscovery){var n=GetAdjustedTime();e.message.addrs.forEach(function(e){try{(e.time<=1e8||e.time>n+600)&&(e.time=n-432e3);var t=new Peer(e.ip,e.port,e.services);t.lastSeen=e.time,this.peers.push(t)}catch(r){log.warn("Invalid addr received: "+r.message)}}.bind(this)),e.message.addrs.length<1e3&&(e.conn.getaddr=!1)}},PeerManager.prototype.handleGetAddr=function(){},PeerManager.prototype.handleError=function(e){log.err("unkown error with peer "+e.peer+" (disconnecting): "+e.err),this.handleDisconnect.apply(this,[].slice.call(arguments))},PeerManager.prototype.handleDisconnect=function(e){log.info("disconnected from peer "+e.peer);var n=this.connections.indexOf(e.conn);-1!=n&&this.connections.splice(n,1),this.removePeer(e.peer),this.pool.length&&(log.info("replacing peer using the pool of "+this.pool.length+" seeds"),this.addPeer(this.pool.pop())),this.connections.length||(this.emit("netDisconnected"),this.isConnected=!1)},PeerManager.prototype.getActiveConnection=function(){var e=this.connections.filter(function(e){return e.active});if(e.length){var n=Math.floor(Math.random()*e.length),t=e[n];return t.socket.writable?t:(e.splice(n,1),this.getActiveConnection())}return null},PeerManager.prototype.getActiveConnections=function(){return this.connections.slice(0)},PeerManager.prototype.discover=function(e,n){var t=this,r=networks[t.config.network].dnsSeeds;t.limit=e.limit||12;var i=r.map(function(e){return function(n){return~t.seeds.resolved.indexOf(e)?n(null,t.seeds.results[e]):~t.seeds.failed.indexOf(e)?n(null,[]):(log.info("resolving dns seed "+e),void dns.resolve(e,function(r,i){return r?(log.err("failed to resolve dns seed "+e,r),t.seeds.failed.push(e),n(null,[])):(log.info("found "+i.length+" peers from "+e),t.seeds.resolved.push(e),i=i.map(function(e){return new Peer(e,networks[t.config.network].defaultClientPort)}),i.forEach(function(e){t.peers.length33&&!this.compressed()||34==this.data.length&&1!=this.data[33]||this.data.length>34)throw new Error("invalid data length")}),"undefined"==typeof this.network())throw new Error("invalid network")},i.prototype.payload=function(t){if(t)return this.doAsBinary(function(){t.copy(this.data,1)}),t;var i=this.as("binary");return 34==i.length?i.slice(1,33):33==i.length?i.slice(1):void 0},i.prototype.compressed=function(i){if(void 0===i){var e=34,r=this.as("binary");if(r.length==e&&1==r[e-1])return!0;if(r.length==e-1)return!1;throw new Error("invalid private key")}this.doAsBinary(function(){var e=34;if(i){var r=new t(e);this.data.copy(r),this.data=r,this.data[e-1]=1}else this.data=this.data.slice(0,e-1)})},i.prototype.network=function(){var t,i=this.version(),e=n.livenet,r=n.testnet;return i===e.privKeyVersion?t=e:i===r.privKeyVersion&&(t=r),t},module.exports=i}).call(this,require("buffer").Buffer); -},{"../networks":"ULNIu2","../util/EncodedData":"eLfUFE","../util/VersionedData":"QLzNQg","buffer":111,"util":144}],"./lib/PrivateKey":[function(require,module,exports){ -module.exports=require('izTl9z'); -},{}],"7siE1N":[function(require,module,exports){ -(function(t){function e(t){t=t||{},this.host=t.host||"127.0.0.1",this.port=t.port||8332,this.user=t.user||"user",this.pass=t.pass||"pass",this.protocol="http"==t.protocol?o:n,this.batchedCalls=null,this.disableAgent=t.disableAgent||!1,this.rejectUnauthorized=t.rejectUnauthorized||!1}function r(t,e,r){function s(t,e){return function(){var s=arguments.length-1;if(this.batchedCalls)var s=arguments.length;for(var o=0;s>o;o++)e[o]&&(arguments[o]=e[o](arguments[o]));this.batchedCalls?this.batchedCalls.push({jsonrpc:"2.0",method:t,params:l(arguments)}):r.call(this,{method:t,params:l(arguments,0,arguments.length-1)},arguments[arguments.length-1])}}var o={str:function(t){return t.toString()},"int":function(t){return parseFloat(t)},"float":function(t){return parseFloat(t)},bool:function(t){return t===!0||"1"==t||"true"==t||"true"==t.toString().toLowerCase()}};for(var n in e)if(e.hasOwnProperty(n)){for(var i=e[n].split(" "),a=0;a=h.map.OP_1&&t<=h.map.OP_16}function n(t){return t=t?2:65535>=t?3:5}function s(e){var r=void 0;return e=e?(r=new t(2),r.writeUInt8(h.map.OP_PUSHDATA1,0),r.writeUInt8(e,1)):65535>=e?(r=new t(3),r.writeUInt8(h.map.OP_PUSHDATA2,0),r.writeUInt16LE(e,1)):(r=new t(5),r.writeUInt8(h.map.OP_PUSHDATA4,0),r.writeUInt32LE(e,1)),r}var u=(require("../config"),require("../util/log")),h=require("./Opcode"),o=require("buffertools"),f=require("../util/util"),c=require("../util/BinaryParser"),p=require("bufferput"),a=0,l=1,g=2,k=3,y=4,S=5,P=["unknown","pubkey","pubkeyhash","multisig","scripthash","return"];e.TX_UNKNOWN=a,e.TX_PUBKEY=l,e.TX_PUBKEYHASH=g,e.TX_MULTISIG=k,e.TX_SCRIPTHASH=y,e.TX_RETURN=S,e.prototype.parse=function(){this.chunks=[];for(var t=new c(this.buffer);!t.eof();){var e,r,n=t.word8();n>0&&nh.map.OP_16)return!1}return!0},e.prototype.isP2SH=function(){return 3==this.chunks.length&&this.chunks[0]==h.map.OP_HASH160&&t.isBuffer(this.chunks[1])&&20==this.chunks[1].length&&this.chunks[2]==h.map.OP_EQUAL},e.prototype.isPubkey=function(){return 2==this.chunks.length&&t.isBuffer(this.chunks[0])&&this.chunks[1]==h.map.OP_CHECKSIG},e.prototype.isReturn=function(){return 2==this.chunks.length&&t.isBuffer(this.chunks[1])&&this.chunks[0]==h.map.OP_RETURN},e.prototype.isPubkeyHash=function(){return 5==this.chunks.length&&this.chunks[0]==h.map.OP_DUP&&this.chunks[1]==h.map.OP_HASH160&&t.isBuffer(this.chunks[2])&&20==this.chunks[2].length&&this.chunks[3]==h.map.OP_EQUALVERIFY&&this.chunks[4]==h.map.OP_CHECKSIG},e.prototype.isMultiSig=function(){return this.chunks.length>3&&r(this.chunks[0])&&this.chunks.slice(1,this.chunks.length-2).every(function(e){return t.isBuffer(e)})&&r(this.chunks[this.chunks.length-2])&&this.chunks[this.chunks.length-1]==h.map.OP_CHECKMULTISIG},e.prototype.isPubkeyHashScriptSig=function(){return 2==this.chunks.length&&t.isBuffer(this.chunks[0])&&t.isBuffer(this.chunks[1])},e.prototype.isP2shScriptSig=function(){if(!r(this.chunks[0])||0!==this.chunks[0])return!1;var t=new e(this.chunks[this.chunks.length-1]),n=t.classify();return n!==a},e.prototype.isMultiSigScriptSig=function(){return r(this.chunks[0])&&0===this.chunks[0]?!this.isP2shScriptSig():!1},e.prototype.isPubkeyScriptSig=function(){return 1==this.chunks.length&&t.isBuffer(this.chunks[0])},e.prototype.countSignatures=function(){var t=0,e=this.chunks.length;return t=this.isMultiSigScriptSig()?e-1:this.isP2shScriptSig()?e-2:this.isPubkeyHashScriptSig()?1:0},e.prototype.getSignatures=function(){ret=[];var t=this.chunks.length;if(this.isMultiSigScriptSig())for(var e=1;t>e;e++)ret.push(this.chunks[e]);else if(this.isP2shScriptSig())for(var e=1;t-1>e;e++)ret.push(this.chunks[e]);else this.isPubkeyHashScriptSig()&&ret.push(this.chunks[0]);return ret},e.prototype.getHashType=function(){for(var t=this.getSignatures(),e=null,r=0;ri;i++){var u=this.chunks[i];if(i>0&&(n+=" "),n+=t.isBuffer(u)?"0x"+f.formatBuffer(u,e?null:0):h.reverseMap[u],r&&i>r){n+=" ...";break}}return n},e.prototype.toString=function(t,e){var r=" + + + + + + + + diff --git a/public/views/add.html b/public/views/add.html new file mode 100644 index 000000000..c6285dc1a --- /dev/null +++ b/public/views/add.html @@ -0,0 +1,32 @@ + +
diff --git a/public/views/backup.html b/public/views/backup.html new file mode 100644 index 000000000..a0d185ddc --- /dev/null +++ b/public/views/backup.html @@ -0,0 +1,62 @@ + +
+
+ +
+ + This wallet have its private key encrypted. Exporting a backup will keep the private key encrypted on the backup. + +
+ + +
+ +
+ + +
+ +
+ + + +
+

Backup options

+ + +
+ + +
+
+

Copy backup in a safe place

+
+ + +
+
+ + Copy this text as it is in a safe place (notepad or email) +
+
+
+ +
+ * You can safely install your backup on other device and + use your wallet from many devices at the same time. +
+ +
+ diff --git a/public/views/copayers.html b/public/views/copayers.html new file mode 100644 index 000000000..c2e337d04 --- /dev/null +++ b/public/views/copayers.html @@ -0,0 +1,57 @@ +
+ +
+
+
+

Share this secret with your copayers

+
+
+ +
+
+ +
+ {{index.walletSecret || ('Loading...'|translate)}} +
+
+
+
+ + + Share secret + +
+

+ Waiting for copayers + + [ {{index.m}} of {{index.n}} ] + +

+
+
+

+ + Waiting... +

+
+
+ +
+
+
+

Wallet incomplete and broken

+

Delete it and create a new one

+
+ +
+
+ +
+ + +
+ diff --git a/public/views/create.html b/public/views/create.html new file mode 100644 index 000000000..d07a3d212 --- /dev/null +++ b/public/views/create.html @@ -0,0 +1,80 @@ +
+
Creating wallet...
+
+
+
+
+
+ +
+ + {{create.error|translate}} + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ + + +
+
+
+
+
diff --git a/public/views/createProfile.html b/public/views/createProfile.html new file mode 100644 index 000000000..ca58a33d1 --- /dev/null +++ b/public/views/createProfile.html @@ -0,0 +1,22 @@ +
+ +
+ Copay +
+
+
+
+

Warning

+
+

You don’t have an internet connection active. Copay needs connection to create a wallet

+
+
+
+ +
+ Creating wallet... +
+
+
+ diff --git a/views/devLogin.html b/public/views/devLogin.html similarity index 100% rename from views/devLogin.html rename to public/views/devLogin.html diff --git a/views/dummy-translations.html b/public/views/dummy-translations.html similarity index 100% rename from views/dummy-translations.html rename to public/views/dummy-translations.html diff --git a/public/views/history.html b/public/views/history.html new file mode 100644 index 000000000..cf1c54ffc --- /dev/null +++ b/public/views/history.html @@ -0,0 +1,56 @@ +
+
+
+
+ Getting transactions... + No transactions yet +
+
+
+
+
+
+ Received + Sent + Moved +
+ +
+ + + + - + {{history.formatAmount(btx.amount)}} + {{history.getUnitName()}} + +
+
+
+ + + Unconfirmed + +
+
+
+ {{btx.message || btx.addressTo}} +
+
+
+
+ +
+
+
+
+
diff --git a/public/views/import.html b/public/views/import.html new file mode 100644 index 000000000..906d08c40 --- /dev/null +++ b/public/views/import.html @@ -0,0 +1,45 @@ +
+
Importing wallet...
+
+
+
+
+
+ +
+ + {{import.error|translate}} + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +
+
+
+
+ +
diff --git a/public/views/importLegacy.html b/public/views/importLegacy.html new file mode 100644 index 000000000..9faeae7fa --- /dev/null +++ b/public/views/importLegacy.html @@ -0,0 +1,75 @@ +
+
+
+
+ Copay +
+
+
+
+

Importing...

+
    +
  • + {{m.message}} +
+
+ +
+
+ +
+ + {{importLegacy.error|translate}} + +
+ +
+
+ + + + + + + + + + +
+ +
+
+
+ +
+ + {{error|translate}} + +
+
+
+ + +
+
+
+ diff --git a/views/importProfile.html b/public/views/importProfile.html similarity index 77% rename from views/importProfile.html rename to public/views/importProfile.html index 269df3db5..ff6b4ef30 100644 --- a/views/importProfile.html +++ b/public/views/importProfile.html @@ -1,5 +1,5 @@ -
+
@@ -11,8 +11,8 @@
-

Import Profile

-
+

Import a profile

+
@@ -25,23 +25,24 @@
+ placeholder="{{'Select a backup file'|translate}}" name="backupFile" ng-model="importProfile.backupFile" ng-file-select>
-
+
- +
@@ -53,7 +54,7 @@