diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index ca25d7c12..000000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,123 +0,0 @@ -module.exports = (grunt) -> - - grunt.initConfig - pkg: grunt.file.readJSON("package.json") - - clean: - build: ["build"] - dist: ["dist"] - test: ["coverage", "coverage-lcov"] - testjs: ["tests/*js"] - - coveralls: - options: - debug: true - coverageDir: 'coverage-lcov' - dryRun: false - force: true - recursive: true - - concat: - options: - separator: ";" - - mywallet: - src: [ - 'build/blockchain.js' - ] - dest: "dist/my-wallet.js" - - replace: - # monkey patch deps - bitcoinjs: - # comment out value validation in fromBuffer to speed up node - # creation from cached xpub/xpriv values - src: ['node_modules/bitcoinjs-lib/src/hdnode.js'], - overwrite: true, - replacements: [{ - from: /\n curve\.validate\(Q\)/g - to: '\n // curve.validate(Q)' - }] - - uglify: - options: - banner: "/*! <%= pkg.name %> <%= grunt.template.today(\"yyyy-mm-dd\") %> */\n" - mangle: false - - mywallet: - src: "dist/my-wallet.js" - dest: "dist/my-wallet.min.js" - - browserify: - options: - debug: true - browserifyOptions: { standalone: "Blockchain" } - - build: - src: ['index.js'] - dest: 'build/blockchain.js' - - production: - options: - debug: false - src: '<%= browserify.build.src %>' - dest: 'build/blockchain.js' - - watch: - scripts: - files: [ - 'src/**/*.js' - ] - tasks: ['build'] - - env: - build: - DEBUG: "1" - PRODUCTION: "0" - - production: - PRODUCTION: "1" - - preprocess: - js: - expand: true - cwd: 'src/' - src: '**/*.js' - dest: 'build' - ext: '.processed.js' - - grunt.loadNpmTasks 'grunt-browserify' - grunt.loadNpmTasks 'grunt-contrib-clean' - grunt.loadNpmTasks 'grunt-contrib-concat' - grunt.loadNpmTasks 'grunt-contrib-uglify' - grunt.loadNpmTasks 'grunt-contrib-watch' - grunt.loadNpmTasks 'grunt-env' - grunt.loadNpmTasks 'grunt-preprocess' - grunt.loadNpmTasks 'grunt-text-replace' - grunt.loadNpmTasks 'grunt-karma-coveralls' - - grunt.registerTask "default", [ - "build" - "watch" - ] - - grunt.registerTask "build", [ - "env:build" - "preprocess" - "replace:bitcoinjs" - "browserify:build" - "concat:mywallet" - ] - - # You must run grunt clean and grunt build first - grunt.registerTask "dist", () => - grunt.task.run [ - "env:production" - "preprocess" - "replace:bitcoinjs" - "browserify:production" - "concat:mywallet" - "uglify:mywallet" - ] - - return diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..c9b46d23f --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,156 @@ +module.exports = function (grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + clean: { + build: ['build'], + dist: ['dist'], + test: ['coverage', 'coverage-lcov'], + testjs: ['tests/*js'] + }, + + coveralls: { + options: { + debug: true, + coverageDir: 'coverage-lcov', + dryRun: false, + force: true, + recursive: true + } + }, + + concat: { + options: { + separator: ';' + }, + + mywallet: { + src: [ + 'build/blockchain.js' + ], + dest: 'dist/my-wallet.js' + } + }, + + replace: { + // monkey patch deps + bitcoinjs: { + // comment out value validation in fromBuffer to speed up node + // creation from cached xpub/xpriv values + src: ['node_modules/bitcoinjs-lib/src/hdnode.js'], + overwrite: true, + replacements: [{ + from: /\n{4}curve\.validate\(Q\)/g, + to: '\n // curve.validate(Q)' + }] + } + }, + + uglify: { + options: { + banner: '/*! <%= pkg.name %> <%= grunt.template.today(\'yyyy-mm-dd\') %> */\n', + mangle: false + }, + + mywallet: { + src: 'dist/my-wallet.js', + dest: 'dist/my-wallet.min.js' + } + }, + + browserify: { + options: { + debug: true, + browserifyOptions: { + standalone: 'Blockchain', + transform: [ + ['babelify', { + presets: ['es2015'], + global: true, + ignore: [ + '/src/blockchain-socket.js', + '/src/ws-browser.js' + ] + }] + ] + } + }, + + build: { + src: ['index.js'], + dest: 'build/blockchain.js' + }, + + production: { + options: { + debug: false + }, + src: '<%= browserify.build.src %>', + dest: 'build/blockchain.js' + } + }, + + watch: { + scripts: { + files: 'src/**/*.js', + tasks: ['build'] + } + }, + + env: { + build: { + DEBUG: '1', + PRODUCTION: '0' + }, + + production: { + PRODUCTION: '1' + } + }, + + preprocess: { + js: { + expand: true, + cwd: 'src/', + src: '**/*.js', + dest: 'build', + ext: '.processed.js' + } + } + }); + + grunt.loadNpmTasks('grunt-browserify'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-env'); + grunt.loadNpmTasks('grunt-preprocess'); + grunt.loadNpmTasks('grunt-text-replace'); + grunt.loadNpmTasks('grunt-karma-coveralls'); + + grunt.registerTask('default', [ + 'build', + 'watch' + ]); + + grunt.registerTask('build', [ + 'env:build', + 'preprocess', + 'replace:bitcoinjs', + 'browserify:build', + 'concat:mywallet' + ]); + + // You must run grunt clean and grunt build first + grunt.registerTask('dist', () => { + return grunt.task.run([ + 'env:production', + 'preprocess', + 'replace:bitcoinjs', + 'browserify:production', + 'concat:mywallet', + 'uglify:mywallet' + ]); + }); +}; diff --git a/index.js b/index.js index f7090ec95..745c097f4 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ 'use strict'; -require('es6-promise').polyfill(); require('isomorphic-fetch'); +require('es6-promise').polyfill(); + +global.Symbol = require('core-js/es6/symbol'); var Buffer = require('buffer').Buffer; @@ -31,7 +33,6 @@ module.exports = { Helpers: require('./src/helpers'), API: require('./src/api'), Tx: require('./src/wallet-transaction'), - Shared: require('./src/shared'), WalletTokenEndpoints: require('./src/wallet-token-endpoints'), WalletNetwork: require('./src/wallet-network'), RNG: require('./src/rng'), @@ -40,5 +41,11 @@ module.exports = { Metadata: require('./src/metadata'), Bitcoin: require('bitcoinjs-lib'), External: require('./src/external'), - BuySell: require('./src/buy-sell') + BuySell: require('./src/buy-sell'), + constants: require('./src/constants'), + BigInteger: require('bigi/lib'), + BIP39: require('bip39'), + Networks: require('bitcoinjs-lib/src/networks'), + ECDSA: require('bitcoinjs-lib/src/ecdsa'), + R: require('ramda') }; diff --git a/karma.conf.js b/karma.conf.js index 971eff041..33ad9a280 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,25 +1,16 @@ module.exports = function (config) { var configuration = { basePath: './', - frameworks: ['jasmine', 'browserify'], - browsers: ['PhantomJS'], - browserNoActivityTimeout: 60000, - // reportSlowerThan: 50, - logLevel: config.LOG_WARN, - client: { captureConsole: false }, - autoWatch: true, - // logLevel: karma.LOG_DEBUG, - reporters: ['progress', 'coverage'], coverageReporter: { @@ -27,69 +18,62 @@ module.exports = function (config) { { type: 'html', dir: 'coverage/' }, { type: 'lcov', dir: 'coverage-lcov/' } ], - subdir: '.' }, preprocessors: { - 'tests/**/*.coffee': ['browserify'] + 'src/**/*.js': ['browserify'], + 'tests/**/*.js': ['browserify'] }, browserify: { - configure: function (bundle) { + configure (bundle) { bundle.once('prebundle', function () { - bundle.transform('coffeeify'); - bundle.transform('browserify-istanbul'); + bundle.transform('browserify-istanbul'); // Must go first + bundle.transform('babelify', { + presets: ['es2015'], + ignore: [ + 'src/ws-browser.js', // undefined is not an object (evaluating 'global.WebSocket') + /\/node_modules\/(?!bitcoin-(coinify|exchange|sfox)-client\/)/ + ], + global: true, + sourceMap: 'inline' + }); bundle.plugin('proxyquireify/plugin'); }); }, debug: true }, - coffeePreprocessor: { - // options passed to the coffee compiler - options: { - bare: true, - sourceMap: true - }, - // transforming the filenames - transformPath: function (path) { - return path.replace(/\.coffee$/, '.js'); - } - }, - files: [ 'node_modules/babel-polyfill/dist/polyfill.js', 'node_modules/jasmine-es6-promise-matchers/jasmine-es6-promise-matchers.js', - 'tests/wallet_token_endpoints.js.coffee', - 'tests/wallet_network_spec.js.coffee', - 'tests/api_spec.js.coffee', - 'tests/helpers_spec.js.coffee', - 'tests/blockchain_socket.js.coffee', - // 'tests/**/*.coffee', - // Or specify individual test files: - 'tests/mocks/*.coffee', - 'tests/transaction_spend_spec.js.coffee', - 'tests/wallet_spec.js.coffee', - 'tests/bip38_spec.js.coffee', - 'tests/address_spec.js.coffee', - 'tests/external_spec.js.coffee', - 'tests/coinify/*_spec.js.coffee', - 'tests/keychain_spec.js.coffee', - 'tests/keyring_spec.js.coffee', - 'tests/hdaccount_spec.js.coffee', - 'tests/hdwallet_spec.js.coffee', - 'tests/blockchain_wallet_spec.js.coffee', - 'tests/rng_spec.js.coffee', - 'tests/payment_spec.js.coffee', - 'tests/wallet_transaction_spec.js.coffee', - 'tests/transaction_list_spec.js.coffee', - 'tests/wallet_crypto_spec.js.coffee', - 'tests/wallet_signup_spec.js.coffee', - 'tests/blockchain_settings_api_spec.js.coffee', - 'tests/account_info_spec.js.coffee', - 'tests/metadata_spec.js.coffee', - 'tests/exchange_delegate_spec.js.coffee' + 'tests/mocks/*.js', + 'tests/wallet_token_endpoints.js', + 'tests/wallet_network_spec.js', + 'tests/api_spec.js', + 'tests/helpers_spec.js', + 'tests/blockchain_socket.js', + 'tests/transaction_spend_spec.js', + 'tests/wallet_spec.js', + 'tests/bip38_spec.js', + 'tests/address_spec.js', + 'tests/external_spec.js', + 'tests/keychain_spec.js', + 'tests/keyring_spec.js', + 'tests/hdaccount_spec.js', + 'tests/hdwallet_spec.js', + 'tests/blockchain_wallet_spec.js', + 'tests/rng_spec.js', + 'tests/payment_spec.js', + 'tests/wallet_transaction_spec.js', + 'tests/transaction_list_spec.js', + 'tests/wallet_crypto_spec.js', + 'tests/wallet_signup_spec.js', + 'tests/blockchain_settings_api_spec.js', + 'tests/account_info_spec.js', + 'tests/metadata_spec.js', + 'tests/exchange_delegate_spec.js' ] }; diff --git a/package.json b/package.json index 31de705e7..7691163b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockchain-wallet-client", - "version": "3.22.18", + "version": "3.27.0", "description": "Blockchain.info JavaScript Wallet", "homepage": "https://github.com/blockchain/my-wallet-v3", "bugs": { @@ -42,19 +42,23 @@ "bs58": "2.0.*", "es6-promise": "^3.0.2", "isomorphic-fetch": "^2.2.0", + "pbkdf2": "3.0.4", + "ramda": "^0.22.1", "randombytes": "^2.0.1", "sjcl": "1.0.3", "unorm": "^1.4.1", "ws": "1.1.*", - "pbkdf2": "3.0.4" + "pbkdf2": "3.0.4", + "bitcoin-coinify-client": "0.3.*", + "bitcoin-sfox-client": "0.1.*" }, "devDependencies": { - "babel-polyfill": "^6.7.2", + "babel-polyfill": "6.16.*", + "babel-preset-es2015": "6.16.*", + "babelify": "7.3.*", "browserify": "13.*", - "browserify-istanbul": "^0.2.1", + "browserify-istanbul": "2.0.*", "bs58check": "^1.0.5", - "coffee-script": "~1.8.0", - "coffeeify": "^1.0.0", "eslint": "2.*", "fetch-mock": "^5.1.2", "git-changelog": "1.0.*", @@ -69,27 +73,38 @@ "grunt-karma-coveralls": "^2.5.4", "grunt-preprocess": "^4.1.0", "grunt-text-replace": "^0.4.0", + "istanbul": "~0.4.5", "jasmine-core": "^2.2.0", "jasmine-es6-promise-matchers": "^2.0.1", "karma": "0.13.*", - "karma-browserify": "^4.1.2", - "karma-coffee-preprocessor": "~0.2.1", - "karma-coverage": "0.2.6", - "karma-jasmine": "0.3.*", - "karma-osx-reporter": "^0.2.0", + "karma-browserify": "5.1.*", + "karma-coverage": "1.1.*", + "karma-jasmine": "1.0.*", + "karma-osx-reporter": "0.2.*", "karma-phantomjs-launcher": "1.0.*", "phantomjs-prebuilt": "2.1.*", - "proxyquireify": "^2.0.0", + "proxyquireify": "3.2.*", "semistandard": "8.*" }, "semistandard": { - "ignore": [ - "tests/" - ], + "ignore": [], "globals": [ "fetch", "XMLHttpRequest", - "Audio" + "Audio", + "assert", + "describe", + "it", + "xit", + "beforeEach", + "afterEach", + "expect", + "except", + "spyOn", + "jasmine", + "JasminePromiseMatchers", + "pending", + "localStorage" ] }, "engines": { diff --git a/src/account-info.js b/src/account-info.js index 46e2a2ad0..b64a9213c 100644 --- a/src/account-info.js +++ b/src/account-info.js @@ -8,14 +8,7 @@ module.exports = AccountInfo; function AccountInfo (object) { this._email = object.email; - if (object.sms_number && object.sms_number.length > 5) { - this._mobile = { - countryCode: object.sms_number.split(' ')[0].substr(1), - number: object.sms_number.split(' ')[1] - }; - } else { - this._mobile = null; - } + this.mobile = object.sms_number; this._countryCodeGuess = object.country_code; // Country guess by the backend this._dialCode = object.dial_code; // Dialcode guess by the backend @@ -58,6 +51,16 @@ Object.defineProperties(AccountInfo.prototype, { return this._mobile == null ? null : '+' + this._mobile.countryCode + this._mobile.number.replace(/^0*/, ''); + }, + set: function (value) { + if (value && value.length > 5) { + this._mobile = { + countryCode: value.split(' ')[0].substr(1), + number: value.split(' ')[1] + }; + } else { + this._mobile = null; + } } }, 'countryCodeGuess': { @@ -78,7 +81,11 @@ Object.defineProperties(AccountInfo.prototype, { }, 'isMobileVerified': { configurable: false, - get: function () { return this._isMobileVerified; } + get: function () { return this._isMobileVerified; }, + set: function (value) { + assert(Helpers.isBoolean(value), 'Boolean'); + this._isMobileVerified = value; + } }, 'currency': { configurable: false, diff --git a/src/address.js b/src/address.js index bf8f6d165..6a09824f7 100644 --- a/src/address.js +++ b/src/address.js @@ -4,12 +4,13 @@ module.exports = Address; var Base58 = require('bs58'); var RNG = require('./rng'); +var API = require('./api'); var Bitcoin = require('bitcoinjs-lib'); var Helpers = require('./helpers'); var MyWallet = require('./wallet'); // This cyclic import should be avoided once the refactor is complete -var shared = require('./shared'); var ImportExport = require('./import-export'); var WalletCrypto = require('./wallet-crypto'); +var constants = require('./constants'); // Address class function Address (object) { @@ -116,13 +117,9 @@ Object.defineProperties(Address.prototype, { get: function () { return this._tag === 2; }, set: function (value) { if (Helpers.isBoolean(value)) { - if (value) { // Archive: - this._tag = 2; - } else { // Unarchive: - this._tag = 0; - MyWallet.wallet.getHistory(); - } + this._tag = value ? 2 : 0; MyWallet.syncWallet(); + MyWallet.wallet.getHistory(); } else { throw new Error('address.archived must be a boolean'); } @@ -149,10 +146,9 @@ Address.import = function (key, label) { addr: null, priv: null, created_time: Date.now(), - created_device_name: shared.APP_NAME, - created_device_version: shared.APP_VERSION + created_device_name: constants.APP_NAME, + created_device_version: constants.APP_VERSION }; - switch (true) { case Helpers.isBitcoinAddress(key): object.addr = key; @@ -163,7 +159,7 @@ Address.import = function (key, label) { object.priv = Base58.encode(key.d.toBuffer(32)); break; case Helpers.isBitcoinPrivateKey(key): - key = Bitcoin.ECPair.fromWIF(key); + key = Bitcoin.ECPair.fromWIF(key, constants.getNetwork()); object.addr = key.getAddress(); object.priv = Base58.encode(key.d.toBuffer(32)); break; @@ -179,36 +175,66 @@ Address.import = function (key, label) { }; Address.fromString = function (keyOrAddr, label, bipPass) { - var asyncParse = function (resolve, reject) { - if (Helpers.isBitcoinAddress(keyOrAddr)) { - return resolve(Address.import(keyOrAddr, label)); - } else { - // Import private key - var format = Helpers.detectPrivateKeyFormat(keyOrAddr); - var okFormats = ['base58', 'base64', 'hex', 'mini', 'sipa', 'compsipa']; + if (Helpers.isBitcoinAddress(keyOrAddr)) { + return Promise.resolve(Address.import(keyOrAddr, label)); + } else { + // Import private key + var format = Helpers.detectPrivateKeyFormat(keyOrAddr); + var okFormats = ['base58', 'base64', 'hex', 'mini', 'sipa', 'compsipa']; + if (format === 'bip38') { + if (bipPass === undefined || bipPass === null || bipPass === '') { + return Promise.reject('needsBip38'); + } - if (format === 'bip38') { - if (bipPass === undefined || bipPass === null || bipPass === '') { - return reject('needsBip38'); - } + var parseBIP38Wrapper = function (resolve, reject) { ImportExport.parseBIP38toECPair(keyOrAddr, bipPass, function (key) { resolve(Address.import(key, label)); }, function () { reject('wrongBipPass'); }, function () { reject('importError'); } ); - } else if (okFormats.indexOf(format) > -1) { - var k = Helpers.privateKeyStringToKey(keyOrAddr, format); - return resolve(Address.import(k, label)); - } else { reject('unknown key format'); } + }; + return new Promise(parseBIP38Wrapper); + } else if (format === 'mini' || format === 'base58') { + try { + var myk = Helpers.privateKeyStringToKey(keyOrAddr, format); + } catch (e) { + return Promise.reject(e); + } + myk.compressed = true; + var cad = myk.getAddress(); + myk.compressed = false; + var uad = myk.getAddress(); + return API.getBalances([cad, uad]).then( + function (o) { + var compBalance = o[cad].final_balance; + var ucompBalance = o[uad].final_balance; + if (compBalance === 0 && ucompBalance > 0) { + myk.compressed = false; + } else { + myk.compressed = true; + } + return Address.import(myk, label); + } + ).catch( + function (e) { + myk.compressed = true; + return Promise.resolve(Address.import(myk, label)); + } + ); + } else if (okFormats.indexOf(format) > -1) { + var k = Helpers.privateKeyStringToKey(keyOrAddr, format); + return Promise.resolve(Address.import(k, label)); + } else { + return Promise.reject('unknown key format'); } - }; - return new Promise(asyncParse); + } }; Address.new = function (label) { var key = Bitcoin.ECPair.makeRandom({ rng: RNG.run.bind(RNG), - compressed: true + compressed: true, + network: constants.getNetwork() }); return Address.import(key, label); }; @@ -243,7 +269,7 @@ Address.prototype.signMessage = function (message, secondPassword) { var keyPair = Helpers.privateKeyStringToKey(priv, 'base58'); if (keyPair.getAddress() !== this.address) keyPair.compressed = false; - return Bitcoin.message.sign(keyPair, message).toString('base64'); + return Bitcoin.message.sign(keyPair, message, constants.getNetwork()).toString('base64'); }; Address.prototype.encrypt = function (cipher) { diff --git a/src/api.js b/src/api.js index 17051fb76..2d5f7f228 100644 --- a/src/api.js +++ b/src/api.js @@ -7,8 +7,6 @@ var Helpers = require('./helpers'); var WalletStore = require('./wallet-store'); var WalletCrypto = require('./wallet-crypto'); var MyWallet = require('./wallet'); -var Bitcoin = require('bitcoinjs-lib'); -var ECPair = Bitcoin.ECPair; // API class function API () { @@ -139,24 +137,10 @@ API.prototype.getTransaction = function (txhash) { return this.retry(this.request.bind(this, 'GET', transaction, data)); }; -API.prototype.getBalanceForRedeemCode = function (privatekey) { - var format = Helpers.detectPrivateKeyFormat(privatekey); - if (format == null) { return Promise.reject('Unknown private key format'); } - var privateKeyToSweep = Helpers.privateKeyStringToKey(privatekey, format); - var aC = new ECPair(privateKeyToSweep.d, null, {compressed: true}).getAddress(); - var aU = new ECPair(privateKeyToSweep.d, null, {compressed: false}).getAddress(); - var totalBalance = function (data) { - return Object.keys(data) - .map(function (a) { return data[a].final_balance; }) - .reduce(Helpers.add, 0); - }; - return this.getBalances([aC, aU]).then(totalBalance); -}; - API.prototype.getFiatAtTime = function (time, value, currencyCode) { var data = { value: value, - currency: currencyCode, + currency: currencyCode.toUpperCase(), time: time, textual: false, nosavecurrency: true @@ -289,3 +273,8 @@ API.prototype.exportHistory = function (active, currency, options) { if (options.end) data.end = options.end; return this.request('POST', 'v2/export-history', data); }; + +API.prototype.incrementSecPassStats = function (activeBool) { + var active = activeBool ? 1 : 0; + return fetch(this.ROOT_URL + 'event?name=wallet_login_second_password_' + active); +}; diff --git a/src/blockchain-settings-api.js b/src/blockchain-settings-api.js index 471cc332c..10efd5994 100644 --- a/src/blockchain-settings-api.js +++ b/src/blockchain-settings-api.js @@ -99,6 +99,10 @@ function updatePasswordHint2 (value, success, error) { isBad ? error(isBad) : updateKV('update-password-hint2', value, success, error); } +function sendConfirmationCode (success, error) { + updateKV('send-verify-email-mail', null, success, error); +} + function changeEmail (email, successCallback, error) { var success = function (res) { MyWallet.wallet.accountInfo.email = email; @@ -108,8 +112,13 @@ function changeEmail (email, successCallback, error) { updateKV('update-email', email, success, error); } -function changeMobileNumber (val, success, error) { - updateKV('update-sms', val, success, error); +function changeMobileNumber (mobile, successCallback, error) { + var success = function (res) { + MyWallet.wallet.accountInfo.mobile = mobile; + MyWallet.wallet.accountInfo.isMobileVerified = false; + if (typeof successCallback === 'function') successCallback(res); + }; + updateKV('update-sms', mobile, success, error); } // Logging levels: @@ -194,13 +203,13 @@ function resendEmailConfirmation (email, success, error) { * @param {function ()} error Error callback function. */ function verifyEmail (code, success, error) { - API.securePostCallbacks('wallet', { payload: code, length: code.length, method: 'verify-email' }, function (data) { + API.securePostCallbacks('wallet', { payload: code, length: code.length, method: 'verify-email-code' }, function (data) { WalletStore.sendEvent('msg', {type: 'success', message: data}); MyWallet.wallet.accountInfo.isEmailVerified = true; typeof (success) === 'function' && success(data); }, function (data) { WalletStore.sendEvent('msg', {type: 'error', message: data}); - typeof (error) === 'function' && error(); + typeof (error) === 'function' && error(data); }); } @@ -213,6 +222,7 @@ function verifyEmail (code, success, error) { function verifyMobile (code, success, error) { API.securePostCallbacks('wallet', { payload: code, length: code.length, method: 'verify-sms' }, function (data) { WalletStore.sendEvent('msg', {type: 'success', message: data}); + MyWallet.wallet.accountInfo.isMobileVerified = true; typeof (success) === 'function' && success(data); }, function (data) { WalletStore.sendEvent('msg', {type: 'error', message: data}); @@ -336,6 +346,7 @@ module.exports = { setTwoFactorGoogleAuthenticator: setTwoFactorGoogleAuthenticator, confirmTwoFactorGoogleAuthenticator: confirmTwoFactorGoogleAuthenticator, resendEmailConfirmation: resendEmailConfirmation, + sendConfirmationCode: sendConfirmationCode, verifyEmail: verifyEmail, verifyMobile: verifyMobile, getActivityLogs: getActivityLogs, diff --git a/src/blockchain-socket.js b/src/blockchain-socket.js index 500d29549..61f220a97 100644 --- a/src/blockchain-socket.js +++ b/src/blockchain-socket.js @@ -3,7 +3,7 @@ var WebSocket = require('ws'); var Helpers = require('./helpers'); function BlockchainSocket () { - this.wsUrl = 'wss://blockchain.info/inv'; + this.wsUrl = 'wss://ws.blockchain.info/inv'; this.headers = { 'Origin': 'https://blockchain.info' }; this.socket; this.reconnect = null; diff --git a/src/blockchain-wallet.js b/src/blockchain-wallet.js index f96d797da..6099eb95c 100644 --- a/src/blockchain-wallet.js +++ b/src/blockchain-wallet.js @@ -14,13 +14,14 @@ var Address = require('./address'); var Helpers = require('./helpers'); var MyWallet = require('./wallet'); // This cyclic import should be avoided once the refactor is complete var API = require('./api'); -var shared = require('./shared'); var BlockchainSettingsAPI = require('./blockchain-settings-api'); var KeyRing = require('./keyring'); var TxList = require('./transaction-list'); var Block = require('./bitcoin-block'); var External = require('./external'); var AccountInfo = require('./account-info'); +var Metadata = require('./metadata'); +var constants = require('./constants'); // Wallet @@ -35,10 +36,11 @@ function Wallet (object) { this._double_encryption = obj.double_encryption || false; this._dpasswordhash = obj.dpasswordhash; // options - this._pbkdf2_iterations = obj.options.pbkdf2_iterations; - this._fee_per_kb = obj.options.fee_per_kb == null ? 10000 : obj.options.fee_per_kb; - this._html5_notifications = obj.options.html5_notifications; - this._logout_time = obj.options.logout_time; + let options = Object.assign(constants.getDefaultWalletOptions(), obj.options); + this._pbkdf2_iterations = options.pbkdf2_iterations; + this._fee_per_kb = options.fee_per_kb; + this._html5_notifications = options.html5_notifications; + this._logout_time = options.logout_time; // legacy addresses list this._addresses = obj.keys ? obj.keys.reduce(Address.factory, {}) : undefined; @@ -326,12 +328,6 @@ Object.defineProperties(Wallet.prototype, { // update-wallet-balances after multiaddr call Wallet.prototype._updateWalletInfo = function (obj) { if (obj.info) { - if (obj.info.symbol_local) { - shared.setLocalSymbol(obj.info.symbol_local); - } - if (obj.info.symbol_btc) { - shared.setBTCSymbol(obj.info.symbol_btc); - } if (obj.info.notice) { WalletStore.sendEvent('msg', {type: 'error', message: obj.info.notice}); } @@ -694,12 +690,7 @@ Wallet.new = function (guid, sharedKey, mnemonic, bip39Password, firstAccountLab guid: guid, sharedKey: sharedKey, double_encryption: false, - options: { - pbkdf2_iterations: 5000, - html5_notifications: false, - fee_per_kb: 10000, - logout_time: 600000 - } + options: constants.getDefaultWalletOptions() }; MyWallet.wallet = new Wallet(object); var label = firstAccountLabel || 'My Bitcoin Wallet'; @@ -878,3 +869,34 @@ Wallet.prototype.loadExternal = function () { return this._external.fetch(); } }; + +Wallet.prototype.metadata = function (typeId) { + var masterhdnode = this.hdwallet.getMasterHDNode(); + return Metadata.fromMasterHDNode(masterhdnode, typeId); +}; + +Wallet.prototype.incStats = function () { + API.incrementSecPassStats(this.isDoubleEncrypted); + return true; +}; + +Wallet.prototype.saveGUIDtoMetadata = function () { + var setOrCheckGuid = function (res) { + if (res === null) { + return m.create({ guid: MyWallet.wallet.guid }); + } else if (!res || !res.guid || res.guid !== MyWallet.wallet.guid) { + return Promise.reject(); + } else { + return res.guid; + } + }; + + // backupGUID not enabled if secondPassword active + if (!this.isDoubleEncrypted && this.isUpgradedToHD) { + var GUID_METADATA_TYPE = 0; + var m = this.metadata(GUID_METADATA_TYPE); + return m.fetch().then(setOrCheckGuid); + } else { + return Promise.reject(); + } +}; diff --git a/src/buy-sell.js b/src/buy-sell.js index 35859e5f7..beadbc13e 100644 --- a/src/buy-sell.js +++ b/src/buy-sell.js @@ -26,10 +26,8 @@ function BuySell (wallet, debug) { return; } - // Add Coinify if not already added: - if (!this._wallet.external.coinify) this._wallet.external.addCoinify(); - this._wallet.external.coinify.debug = this.debug; + this._wallet.external.sfox.debug = this.debug; } Object.defineProperties(BuySell.prototype, { @@ -49,7 +47,8 @@ Object.defineProperties(BuySell.prototype, { !this._wallet.external.loaded ) return; return { - coinify: this._wallet.external.coinify + coinify: this._wallet.external.coinify, + sfox: this._wallet.external.sfox }; } } diff --git a/src/coinify/address.js b/src/coinify/address.js deleted file mode 100644 index afcfb22e7..000000000 --- a/src/coinify/address.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -module.exports = Address; - -function Address (obj) { - this._street = obj.street; - this._city = obj.city; - this._state = obj.state; - this._zipcode = obj.zipcode; - this._country = obj.country; -} - -Object.defineProperties(Address.prototype, { - 'city': { - configurable: false, - get: function () { - return this._city; - } - }, - 'country': { - configurable: false, - get: function () { - return this._country; - } - }, - 'state': { // ISO 3166-2, the part after the dash - configurable: false, - get: function () { - return this._state; - } - }, - 'street': { - configurable: false, - get: function () { - return this._street; - } - }, - 'zipcode': { - configurable: false, - get: function () { - return this._zipcode; - } - } -}); diff --git a/src/coinify/api.js b/src/coinify/api.js deleted file mode 100644 index ad1a83e99..000000000 --- a/src/coinify/api.js +++ /dev/null @@ -1,156 +0,0 @@ -var assert = require('assert'); - -module.exports = API; - -function API () { - this._offlineToken = null; - this._rootURL = 'https://app-api.coinify.com/'; - this._loginExpiresAt = null; -} - -Object.defineProperties(API.prototype, { - 'isLoggedIn': { - configurable: false, - get: function () { - // Debug: + 60 * 19 * 1000 expires the login after 1 minute - var tenSecondsFromNow = new Date(new Date().getTime() + 10000); - return Boolean(this._access_token) && this._loginExpiresAt > tenSecondsFromNow; - } - }, - 'offlineToken': { - configurable: false, - get: function () { - return this._offlineToken; - } - }, - 'hasAccount': { - configurable: false, - get: function () { - return Boolean(this.offlineToken); - } - } -}); - -API.prototype.login = function () { - var self = this; - - var promise = new Promise(function (resolve, reject) { - assert(self._offlineToken, 'Offline token required'); - - var loginSuccess = function (res) { - self._access_token = res.access_token; - self._loginExpiresAt = new Date(new Date().getTime() + res.expires_in * 1000); - resolve(); - }; - - var loginFailed = function (e) { - reject(e); - }; - self.POST('auth', { - grant_type: 'offline_token', - offline_token: self._offlineToken - }).then(loginSuccess).catch(loginFailed); - }); - - return promise; -}; - -API.prototype.GET = function (endpoint, data) { - return this._request('GET', endpoint, data); -}; - -API.prototype.authGET = function (endpoint, data) { - var doGET = function () { - return this._request('GET', endpoint, data, true); - }; - - if (this.isLoggedIn) { - return doGET.bind(this)(); - } else { - return this.login().then(doGET.bind(this)); - } -}; - -API.prototype.POST = function (endpoint, data) { - return this._request('POST', endpoint, data); -}; - -API.prototype.authPOST = function (endpoint, data) { - var doPOST = function () { - return this._request('POST', endpoint, data, true); - }; - - if (this.isLoggedIn) { - return doPOST.bind(this)(); - } else { - return this.login().then(doPOST.bind(this)); - } -}; - -API.prototype.PATCH = function (endpoint, data) { - return this._request('PATCH', endpoint, data); -}; - -API.prototype.authPATCH = function (endpoint, data) { - var doPATCH = function () { - return this._request('PATCH', endpoint, data, true); - }; - - if (this.isLoggedIn) { - return doPATCH.bind(this)(); - } else { - return this.login().then(doPATCH.bind(this)); - } -}; - -API.prototype._request = function (method, endpoint, data, authorized) { - assert(!authorized || this.isLoggedIn, "Can't make authorized request if not logged in"); - - var url = this._rootURL + endpoint; - - var options = { - headers: { 'Content-Type': 'application/json' }, - credentials: 'omit' - }; - - if (authorized) { - options.headers['Authorization'] = 'Bearer ' + this._access_token; - } - - // encodeFormData :: Object -> url encoded params - var encodeFormData = function (data) { - if (!data) return ''; - var encoded = Object.keys(data).map(function (k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]); - }).join('&'); - return encoded; - }; - - if (data) { - if (method === 'GET') { - url += '?' + encodeFormData(data); - } else { - options.body = JSON.stringify(data); - } - } - - options.method = method; - - var handleNetworkError = function (e) { - return Promise.reject({ error: 'COINIFY_CONNECT_ERROR', message: e }); - }; - - var checkStatus = function (response) { - if (response.status === 204) { - return; - } else if (response.status >= 200 && response.status < 300) { - return response.json(); - } else { - return response.text().then(Promise.reject.bind(Promise)); - } - }; - - return fetch(url, options) - .catch(handleNetworkError) - .then(checkStatus); -}; diff --git a/src/coinify/bank-account.js b/src/coinify/bank-account.js deleted file mode 100644 index 2e443aaae..000000000 --- a/src/coinify/bank-account.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -var Address = require('./address'); - -module.exports = BankAccount; - -function BankAccount (obj) { - this._id = obj.id; // Not used in buy - this._type = obj.account.type; // Missing in API - this._currency = obj.account.currency; // Missing in API - this._bic = obj.account.bic; - this._number = obj.account.number; - this._bank_name = obj.bank.name; - this._bank_address = new Address(obj.bank.address); - this._holder_name = obj.holder.name; - this._holder_address = new Address(obj.holder.address); - this._referenceText = obj.referenceText; - this._updated_at = obj.updateTime; // Not used in buy - this._created_at = obj.createTime; // Not used in buy -} - -Object.defineProperties(BankAccount.prototype, { - // 'id': { - // configurable: false, - // get: function () { - // return this._id; - // } - // }, - 'type': { - configurable: false, - get: function () { - return this._type; - } - }, - 'currency': { - configurable: false, - get: function () { - return this._currency; - } - }, - 'bic': { - configurable: false, - get: function () { - return this._bic; - } - }, - 'number': { - configurable: false, - get: function () { - return this._number; - } - }, - 'bankName': { - configurable: false, - get: function () { - return this._bank_name; - } - }, - 'bankAddress': { - configurable: false, - get: function () { - return this._bank_address; - } - }, - 'holderName': { - configurable: false, - get: function () { - return this._holder_name; - } - }, - 'holderAddress': { - configurable: false, - get: function () { - return this._holder_address; - } - }, - 'referenceText': { - configurable: false, - get: function () { - return this._referenceText; - } - } - // 'createdAt': { - // configurable: false, - // get: function () { - // return this._created_at; - // } - // }, - // 'updatedAt': { - // configurable: false, - // get: function () { - // return this._updated_at; - // } - // } -}); diff --git a/src/coinify/coinify.js b/src/coinify/coinify.js deleted file mode 100644 index 3189b8ecb..000000000 --- a/src/coinify/coinify.js +++ /dev/null @@ -1,382 +0,0 @@ -'use strict'; - -/* To use this class, three things are needed: -1 - a delegate object with functions that provide the following: - save() -> e.g. function () { return JSON.stringify(this._coinify); } - email() -> String : the users email address - isEmailVerified() -> Boolean : whether the users email is verified - getEmailToken() -> stringify : JSON web token {email: 'me@example.com'} - monitorAddress(address, callback) : callback(amount) if btc received - checkAddress(address) : look for existing transaction at address - getReceiveAddress(trade) : return the trades receive address - reserveReceiveAddress() - commitReceiveAddress() - releaseReceiveAddress() - serializeExtraFields(obj, trade) : e.g. obj.account_index = ... - deserializeExtraFields(obj, trade) - -2 - a Coinify parner identifier - -var object = {user: 1, offline_token: 'token'}; -var coinify = new Coinify(object, delegate); -coinify.partnerId = ...; -coinify.delegate.save.bind(coinify.delegate)() -// "{"user":1,"offline_token":"token"}" -*/ - -var CoinifyProfile = require('./profile'); -var CoinifyTrade = require('./trade'); -var CoinifyKYC = require('./kyc'); -var PaymentMethod = require('./payment-method'); -var ExchangeRate = require('./exchange-rate'); -var Quote = require('./quote'); -var API = require('./api'); - -var assert = require('assert'); - -var isBoolean = function (value) { - return typeof (value) === 'boolean'; -}; - -var isString = function (str) { - return typeof str === 'string' || str instanceof String; -}; - -module.exports = Coinify; - -function Coinify (object, delegate) { - assert(delegate, 'ExchangeDelegate required'); - this._delegate = delegate; - var obj = object || {}; - this._partner_id = null; - this._user = obj.user; - this._auto_login = obj.auto_login; - this._offlineToken = obj.offline_token; - - this._api = new API(); - this._api._offlineToken = this._offlineToken; - - this._profile = new CoinifyProfile(this._api); - this._lastQuote = null; - - this._buyCurrencies = null; - this._sellCurrencies = null; - - this._trades = []; - if (obj.trades) { - for (var i = 0; i < obj.trades.length; i++) { - var trade = new CoinifyTrade(obj.trades[i], this._api, delegate, this); - trade.debug = this._debug; - this._trades.push(trade); - } - } - - this._kycs = []; - - this.exchangeRate = new ExchangeRate(this._api); -} - -Object.defineProperties(Coinify.prototype, { - 'debug': { - configurable: false, - get: function () { return this._debug; }, - set: function (value) { - this._debug = Boolean(value); - this._delegate.debug = Boolean(value); - for (var i = 0; i < this.trades.length; i++) { - this.trades[i].debug = Boolean(value); - } - } - }, - 'delegate': { - configurable: false, - get: function () { return this._delegate; } - }, - 'user': { - configurable: false, - get: function () { return this._user; } - }, - 'autoLogin': { - configurable: false, - get: function () { return this._auto_login; }, - set: function (value) { - assert( - isBoolean(value), - 'Boolean' - ); - this._auto_login = value; - this.delegate.save.bind(this.delegate)(); - } - }, - 'profile': { - configurable: false, - get: function () { - if (!this._profile._did_fetch) { - return null; - } else { - return this._profile; - } - } - }, - 'trades': { - configurable: false, - get: function () { - return this._trades; - } - }, - 'kycs': { - configurable: false, - get: function () { - return this._kycs; - } - }, - 'hasAccount': { - configurable: false, - get: function () { - return Boolean(this._offlineToken); - } - }, - 'partnerId': { - configurable: false, - get: function () { - return this._partner_id; - }, - set: function (value) { - this._partner_id = value; - } - }, - 'buyCurrencies': { - configurable: false, - get: function () { - return this._buyCurrencies; - } - }, - 'sellCurrencies': { - configurable: false, - get: function () { - return this._sellCurrencies; - } - } -}); - -Coinify.prototype.toJSON = function () { - var coinify = { - user: this._user, - offline_token: this._offlineToken, - auto_login: this._auto_login, - trades: CoinifyTrade.filteredTrades(this._trades) - }; - - return coinify; -}; -// Country and default currency must be set -// Email must be set and verified -Coinify.prototype.signup = function (countryCode, currencyCode) { - var self = this; - var runChecks = function () { - assert(!self.user, 'Already signed up'); - - assert(self.delegate, 'ExchangeDelegate required'); - - assert( - countryCode && - isString(countryCode) && - countryCode.length === 2 && - countryCode.match(/[a-zA-Z]{2}/), - 'ISO 3166-1 alpha-2' - ); - - assert(currencyCode, 'currency required'); - - assert(self.delegate.email(), 'email required'); - assert(self.delegate.isEmailVerified(), 'email must be verified'); - }; - - var doSignup = function (emailToken) { - assert(emailToken, 'email token missing'); - return this._api.POST('signup/trader', { - email: self.delegate.email(), - partnerId: self.partnerId, - defaultCurrency: currencyCode, // ISO 4217 - profile: { - address: { - country: countryCode.toUpperCase() - } - }, - trustedEmailValidationToken: emailToken, - generateOfflineToken: true - }); - }; - - var saveMetadata = function (res) { - this._user = res.trader.id; - this._offlineToken = res.offlineToken; - this._api._offlineToken = this._offlineToken; - return this._delegate.save.bind(this._delegate)().then(function () { return res; }); - }; - - return Promise.resolve().then(runChecks.bind(this)) - .then(this.delegate.getEmailToken.bind(this.delegate)) - .then(doSignup.bind(this)) - .then(saveMetadata.bind(this)); -}; - -Coinify.prototype.fetchProfile = function () { - return this._profile.fetch(); -}; - -Coinify.prototype.getBuyQuote = function (amount, baseCurrency, quoteCurrency) { - assert(baseCurrency, 'Specify base currency'); - assert(baseCurrency !== 'BTC' || quoteCurrency, 'Specify quote currency'); - if (baseCurrency !== 'BTC') { - quoteCurrency = 'BTC'; - } - return Quote.getQuote(this._api, -amount, baseCurrency, quoteCurrency) - .then(this.setLastQuote.bind(this)); -}; - -Coinify.prototype.setLastQuote = function (quote) { - this._lastQuote = quote; - return quote; -}; - -Coinify.prototype.buy = function (amount, baseCurrency, medium) { - assert(this.delegate, 'ExchangeDelegate required'); - assert(this._lastQuote !== null, 'You must first obtain a quote'); - assert(this._lastQuote.baseAmount === -amount, 'LAST_QUOTE_AMOUNT_DOES_NOT_MATCH'); - assert(this._lastQuote.baseCurrency === baseCurrency, 'Currency must match last quote'); - assert(this._lastQuote.expiresAt > new Date(), 'LAST_QUOTE_EXPIRED'); - assert(medium === 'bank' || medium === 'card', 'Specify bank or card'); - - var addTrade = function (trade) { - trade.debug = this._debug; - this._trades.push(trade); - return this.delegate.save.bind(this.delegate)().then(function () { return trade; }); - }; - - return CoinifyTrade.buy( - this._lastQuote, - medium, - this._api, - this.delegate, - this - ).then(addTrade.bind(this)); -}; - -Coinify.prototype.updateList = function (list, items, ListClass) { - var item; - for (var i = 0; i < items.length; i++) { - item = undefined; - for (var k = 0; k < list.length; k++) { - if (list[k]._id === items[i].id) { - item = list[k]; - item.debug = this.debug; - item.set.bind(item)(items[i]); - } - } - if (item === undefined) { - item = new ListClass(items[i], this._api, this.delegate, this); - item.debug = this.debug; - list.push(item); - } - } -}; - -Coinify.prototype.getTrades = function () { - var self = this; - var save = function () { - return this.delegate.save.bind(this.delegate)().then(function () { return self._trades; }); - }; - var update = function (trades) { - this.updateList(this._trades, trades, CoinifyTrade); - }; - var process = function () { - for (var i = 0; i < this._trades.length; i++) { - var trade = this._trades[i]; - trade.process(this._trades); - } - }; - return CoinifyTrade.fetchAll(this._api) - .then(update.bind(this)) - .then(process.bind(this)) - .then(save.bind(this)); -}; - -Coinify.prototype.triggerKYC = function () { - var addKYC = function (kyc) { - this._kycs.push(kyc); - return kyc; - }; - - return CoinifyKYC.trigger(this._api).then(addKYC.bind(this)); -}; - -Coinify.prototype.getKYCs = function () { - var self = this; - var save = function () { - return this.delegate.save.bind(this.delegate)().then(function () { return self._kycs; }); - }; - var update = function (kycs) { - this.updateList(this._kycs, kycs, CoinifyKYC); - }; - return CoinifyKYC.fetchAll(this._api, this) - .then(update.bind(this)) - .then(save.bind(this)); -}; - -Coinify.prototype.getBuyMethods = function () { - return PaymentMethod.fetchAll(undefined, 'BTC', this._api); -}; - -Coinify.prototype.getSellMethods = function () { - return PaymentMethod.fetchAll('BTC', undefined, this._api); -}; - -Coinify.prototype.getBuyCurrencies = function () { - var getCurrencies = function (paymentMethods) { - var currencies = []; - for (var i = 0; i < paymentMethods.length; i++) { - var paymentMethod = paymentMethods[i]; - for (var j = 0; j < paymentMethod.inCurrencies.length; j++) { - var inCurrency = paymentMethod.inCurrencies[j]; - if (currencies.indexOf(inCurrency) === -1) { - currencies.push(paymentMethod.inCurrencies[j]); - } - } - } - this._buyCurrencies = JSON.parse(JSON.stringify(currencies)); - return currencies; - }; - return this.getBuyMethods().then(getCurrencies.bind(this)); -}; - -Coinify.prototype.getSellCurrencies = function () { - var getCurrencies = function (paymentMethods) { - var currencies = []; - for (var i = 0; i < paymentMethods.length; i++) { - var paymentMethod = paymentMethods[i]; - for (var j = 0; j < paymentMethod.outCurrencies.length; j++) { - var outCurrency = paymentMethod.outCurrencies[j]; - if (currencies.indexOf(outCurrency) === -1) { - currencies.push(paymentMethod.outCurrencies[j]); - } - } - } - this._sellCurrencies = JSON.parse(JSON.stringify(currencies)); - return currencies; - }; - return this.getSellMethods().then(getCurrencies.bind(this)); -}; - -Coinify.prototype.monitorPayments = function () { - CoinifyTrade.monitorPayments(this._trades, this.delegate); -}; - -Coinify.new = function (delegate) { - assert(delegate, 'Coinify.new requires delegate'); - var object = { - auto_login: true - }; - var coinify = new Coinify(object, delegate); - return coinify; -}; diff --git a/src/coinify/exchange-rate.js b/src/coinify/exchange-rate.js deleted file mode 100644 index 7a4467f81..000000000 --- a/src/coinify/exchange-rate.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -var assert = require('assert'); - -module.exports = ExchangeRate; - -function ExchangeRate (coinify) { - this._coinify = coinify; -} - -ExchangeRate.prototype.get = function (baseCurrency, quoteCurrency) { - var self = this; - var performChecks = function () { - assert(baseCurrency, 'Base currency required'); - assert(quoteCurrency, 'Quote currency required'); - }; - var getRate = function () { - return self._coinify.GET('rates/approximate', { - baseCurrency: baseCurrency, - quoteCurrency: quoteCurrency - }); - }; - var processRate = function (res) { - return res.rate; - }; - return Promise.resolve() - .then(performChecks) - .then(getRate) - .then(processRate); -}; diff --git a/src/coinify/helpers.js b/src/coinify/helpers.js deleted file mode 100644 index 234557115..000000000 --- a/src/coinify/helpers.js +++ /dev/null @@ -1,28 +0,0 @@ -var Helpers = {}; - -Helpers.isNumber = function (num) { - return typeof num === 'number' && !isNaN(num); -}; -Helpers.isInteger = function (num) { - return Helpers.isNumber(num) && num % 1 === 0; -}; -Helpers.isPositiveNumber = function (num) { - return Helpers.isNumber(num) && num >= 0; -}; -Helpers.isPositiveInteger = function (num) { - return Helpers.isPositiveNumber(num) && num % 1 === 0; -}; -Helpers.toCents = function (fiat) { - return Math.round((parseFloat(fiat) || 0) * 100); -}; -Helpers.toSatoshi = function (fiat) { - return Math.round((parseFloat(fiat) || 0) * 100000000); -}; -Helpers.fromCents = function (cents) { - return parseFloat((cents / 100).toFixed(2)); -}; -Helpers.fromSatoshi = function (satoshi) { - return parseFloat((satoshi / 100000000).toFixed(8)); -}; - -module.exports = Helpers; diff --git a/src/coinify/kyc.js b/src/coinify/kyc.js deleted file mode 100644 index b280c9830..000000000 --- a/src/coinify/kyc.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -module.exports = CoinifyKYC; - -function CoinifyKYC (obj, api, delegate, coinify) { - // delegate and coinify are not used - this._api = api; - this._id = obj.id; - this._createdAt = new Date(obj.createTime); - this._updatedAt = new Date(obj.updateTime); - this.set(obj); -} - -CoinifyKYC.prototype.set = function (obj) { - if ([ - 'pending', - 'rejected', - 'failed', - 'expired', - 'completed', - 'reviewing', - 'documentsRequested' - ].indexOf(obj.state) === -1) { - console.warn('Unknown state:', obj.state); - } - this._state = obj.state; - this._iSignThisID = obj.externalId; - this._updatedAt = new Date(obj.updateTime); - return this; -}; - -Object.defineProperties(CoinifyKYC.prototype, { - 'id': { - configurable: false, - get: function () { - return this._id; - } - }, - 'state': { - configurable: false, - get: function () { - return this._state; - } - }, - 'iSignThisID': { - configurable: false, - get: function () { - return this._iSignThisID; - } - }, - 'createdAt': { - configurable: false, - get: function () { - return this._createdAt; - } - }, - 'updatedAt': { - configurable: false, - get: function () { - return this._updatedAt; - } - } -}); - -CoinifyKYC.prototype.refresh = function () { - return this._api.authGET('kyc/' + this._id) - .then(this.set.bind(this)); -}; - -CoinifyKYC.trigger = function (api) { - var processKYC = function (res) { - var kyc = new CoinifyKYC(res, api, null, null); - return kyc; - }; - - return api.authPOST('traders/me/kyc').then(processKYC); -}; - -CoinifyKYC.fetchAll = function (api) { - return api.authGET('kyc'); -}; diff --git a/src/coinify/level.js b/src/coinify/level.js deleted file mode 100644 index 1fd477d1e..000000000 --- a/src/coinify/level.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -module.exports = Level; -var Limits = require('./limits'); - -function Level (obj) { - this._currency = obj.currency; - this._feePercentage = obj.feePercentage; - this._limits = new Limits(obj.limits); - this._requirements = obj.requirements; - this._name = obj.name; -} - -Object.defineProperties(Level.prototype, { - 'currency': { - configurable: false, - get: function () { - return this._currency; - } - }, - 'feePercentage': { - configurable: false, - get: function () { - return this._feePercentage; - } - }, - 'limits': { - configurable: false, - get: function () { - return this._limits; - } - }, - 'name': { - configurable: false, - get: function () { - return this._name; - } - }, - 'requirements': { - configurable: false, - get: function () { - return this._requirements; - } - } -}); diff --git a/src/coinify/limit.js b/src/coinify/limit.js deleted file mode 100644 index dc8379a4c..000000000 --- a/src/coinify/limit.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -var Helpers = require('./helpers'); - -module.exports = Limit; - -function Limit (obj) { - // Is this the amount remaining at this moment, or the daily/weekly limit? - if ((obj.in && Helpers.isPositiveNumber(obj.in.daily)) || (obj.out && Helpers.isPositiveNumber(obj.out.daily))) { - if (obj.in) { - this._inDaily = obj.in.daily; - } - if (obj.out) { - this._outDaily = obj.in.daily; - } - } else { - this._inRemaining = obj.in; - this._outRemaining = obj.out; - } -} - -Object.defineProperties(Limit.prototype, { - 'inRemaining': { - configurable: false, - get: function () { - return this._inRemaining; - } - }, - 'outRemaining': { - configurable: false, - get: function () { - return this._inRemaining; - } - }, - 'inDaily': { - configurable: false, - get: function () { - return this._inDaily; - } - }, - 'outDaily': { - configurable: false, - get: function () { - return this._outDaily; - } - } -}); diff --git a/src/coinify/limits.js b/src/coinify/limits.js deleted file mode 100644 index a6692ab8b..000000000 --- a/src/coinify/limits.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -var Limit = require('./limit'); - -module.exports = Limits; - -function Limits (obj) { - this._card = new Limit(obj.card); - this._bank = new Limit(obj.bank); -} - -Object.defineProperties(Limits.prototype, { - 'card': { - configurable: false, - get: function () { - return this._card; - } - }, - 'bank': { - configurable: false, - get: function () { - return this._bank; - } - } -}); diff --git a/src/coinify/payment-method.js b/src/coinify/payment-method.js deleted file mode 100644 index 11fa68bc9..000000000 --- a/src/coinify/payment-method.js +++ /dev/null @@ -1,127 +0,0 @@ -'use strict'; - -module.exports = PaymentMethod; - -function PaymentMethod (obj, api) { - this._api = api; - this._inMedium = obj.inMedium; - this._outMedium = obj.outMedium; - this._name = obj.name; - - this._inCurrencies = obj.inCurrencies; - this._outCurrencies = obj.outCurrencies; - - this._inCurrency = obj.inCurrency; - this._outCurrency = obj.outCurrency; - - if (this._inCurrency === 'BTC') { - this._inFixedFee = Math.round(obj.inFixedFee * 100000000); - this._outFixedFee = Math.round(obj.outFixedFee * 100); - } else { - this._inFixedFee = Math.round(obj.inFixedFee * 100); - this._outFixedFee = Math.round(obj.outFixedFee * 100000000); - } - this._inPercentageFee = obj.inPercentageFee; - this._outPercentageFee = obj.outPercentageFee; -} - -Object.defineProperties(PaymentMethod.prototype, { - 'inMedium': { - configurable: false, - get: function () { - return this._inMedium; - } - }, - 'outMedium': { - configurable: false, - get: function () { - return this._outMedium; - } - }, - 'name': { - configurable: false, - get: function () { - return this._name; - } - }, - 'inCurrencies': { - configurable: false, - get: function () { - return this._inCurrencies; - } - }, - 'outCurrencies': { - configurable: false, - get: function () { - return this._outCurrencies; - } - }, - 'inCurrency': { - configurable: false, - get: function () { - return this._inCurrency; - } - }, - 'outCurrency': { - configurable: false, - get: function () { - return this._outCurrency; - } - }, - 'inFixedFee': { - configurable: false, - get: function () { - return this._inFixedFee; - } - }, - 'outFixedFee': { - configurable: false, - get: function () { - return this._outFixedFee; - } - }, - 'inPercentageFee': { - configurable: false, - get: function () { - return this._inPercentageFee; - } - }, - 'outPercentageFee': { - configurable: false, - get: function () { - return this._outPercentageFee; - } - }, - 'fee': { - configurable: false, - get: function () { - return this._fee; - } - }, - 'total': { - configurable: false, - get: function () { - return this._total; - } - } -}); - -PaymentMethod.fetchAll = function (inCurrency, outCurrency, api) { - var params = {}; - if (inCurrency) { params.inCurrency = inCurrency; } - if (outCurrency) { params.outCurrency = outCurrency; } - - var output = []; - return api.authGET('trades/payment-methods', params).then(function (res) { - output.length = 0; - for (var i = 0; i < res.length; i++) { - output.push(new PaymentMethod(res[i], api)); - } - return Promise.resolve(output); - }); -}; - -PaymentMethod.prototype.calculateFee = function (quote) { - this._fee = Math.round(this.inFixedFee + -quote.baseAmount * (this.inPercentageFee / 100)); - this._total = -quote.baseAmount + this._fee; -}; diff --git a/src/coinify/profile.js b/src/coinify/profile.js deleted file mode 100644 index db4ef0ca4..000000000 --- a/src/coinify/profile.js +++ /dev/null @@ -1,184 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var Limits = require('./limits'); -var Level = require('./level'); - -module.exports = CoinifyProfile; - -function CoinifyProfile (api) { - this._api = api; - this._did_fetch; -} - -Object.defineProperties(CoinifyProfile.prototype, { - 'fullName': { - configurable: false, - get: function () { - return this._full_name; - } - }, - 'defaultCurrency': { // read-only - configurable: false, - get: function () { - return this._default_currency; - } - }, - 'email': { // ready-only - configurable: false, - get: function () { - return this._email; - } - }, - 'gender': { - configurable: false, - get: function () { - return this._gender; - } - }, - 'mobile': { // setter not implemented yet - configurable: false, - get: function () { - return this._mobile; - } - }, - 'city': { - configurable: false, - get: function () { - return this._city; - } - }, - 'country': { - configurable: false, - get: function () { - return this._country; - } - }, - 'state': { // ISO 3166-2, the part after the dash - configurable: false, - get: function () { - return this._state; - } - }, - 'street': { - configurable: false, - get: function () { - return this._street; - } - }, - 'zipcode': { - configurable: false, - get: function () { - return this._zipcode; - } - }, - 'level': { - configurable: false, - get: function () { - return this._level; - } - }, - 'nextLevel': { - configurable: false, - get: function () { - return this._nextLevel; - } - }, - 'currentLimits': { - configurable: false, - get: function () { - return this._currentLimits; - } - } -}); - -CoinifyProfile.prototype.fetch = function () { - var parentThis = this; - return this._api.authGET('traders/me').then(function (res) { - parentThis._full_name = res.profile.name; - parentThis._gender = res.profile.gender; - - parentThis._email = res.email; - - if (res.profile.mobile.countryCode) { - parentThis._mobile = '+' + res.profile.mobile.countryCode + res.profile.mobile.number.replace('-', ''); - } - - parentThis._default_currency = res.defaultCurrency; - - // TODO: use new Address(res.profile.address); - parentThis._street = res.profile.address.street; - parentThis._city = res.profile.address.city; - parentThis._state = res.profile.address.state; - parentThis._zipcode = res.profile.address.zipcode; - parentThis._country = res.profile.address.country; - - parentThis._level = new Level(res.level); - parentThis._nextLevel = new Level(res.nextLevel); - parentThis._currentLimits = new Limits(res.currentLimits); - - parentThis._did_fetch = true; - - return parentThis; - }); -}; - -CoinifyProfile.prototype.setFullName = function (value) { - var parentThis = this; - - return this.update({profile: {name: value}}).then(function (res) { - parentThis._full_name = res.profile.name; - }); -}; - -CoinifyProfile.prototype.setGender = function (value) { - assert(value === null || value === 'male' || value === 'female', 'invalid gender'); - var parentThis = this; - - return this.update({profile: {gender: value}}).then(function (res) { - parentThis._gender = res.profile.gender; - }); -}; - -CoinifyProfile.prototype.setCity = function (value) { - var parentThis = this; - - return this.update({profile: {address: {city: value}}}).then(function (res) { - parentThis._city = res.profile.address.city; - }); -}; - -CoinifyProfile.prototype.setCountry = function (value) { - var parentThis = this; - - return this.update({profile: {address: {country: value}}}).then(function (res) { - parentThis._country = res.profile.address.country; - }); -}; - -CoinifyProfile.prototype.setState = function (value) { - var parentThis = this; - - return this.update({profile: {address: {state: value}}}).then(function (res) { - parentThis._state = res.profile.address.state; - }); -}; - -CoinifyProfile.prototype.setStreet = function (value) { - var parentThis = this; - - return this.update({profile: {address: {street: value}}}).then(function (res) { - parentThis._street = res.profile.address.street; - }); -}; - -CoinifyProfile.prototype.setZipcode = function (value) { - var parentThis = this; - return this.update({profile: {address: {zipcode: value}}}).then(function (res) { - parentThis._zipcode = res.profile.address.zipcode; - }); -}; - -CoinifyProfile.prototype.update = function (values) { - return this._api.authPATCH('traders/me', values); -}; diff --git a/src/coinify/quote.js b/src/coinify/quote.js deleted file mode 100644 index 2c4105f02..000000000 --- a/src/coinify/quote.js +++ /dev/null @@ -1,148 +0,0 @@ -'use strict'; - -var PaymentMethod = require('./payment-method'); -var Helpers = require('./helpers'); -var assert = require('assert'); - -module.exports = Quote; - -function Quote (obj, api) { - this._api = api; - - var expiresAt = new Date(obj.expiryTime); - - // Debug, make quote expire in 15 seconds: - // expiresAt = new Date(new Date().getTime() + 15 * 1000); - - this._id = obj.id; - this._baseCurrency = obj.baseCurrency; - this._quoteCurrency = obj.quoteCurrency; - this._expiresAt = expiresAt; - - if (this._baseCurrency === 'BTC') { - this._baseAmount = Math.round(obj.baseAmount * 100000000); - this._quoteAmount = Math.round(obj.quoteAmount * 100); - } else { - this._baseAmount = Math.round(obj.baseAmount * 100); - this._quoteAmount = Math.round(obj.quoteAmount * 100000000); - } - - obj.baseAmount; -} - -Object.defineProperties(Quote.prototype, { - 'id': { - configurable: false, - get: function () { - return this._id; - } - }, - 'baseCurrency': { - configurable: false, - get: function () { - return this._baseCurrency; - } - }, - 'quoteCurrency': { - configurable: false, - get: function () { - return this._quoteCurrency; - } - }, - 'baseAmount': { - configurable: false, - get: function () { - return this._baseAmount; - } - }, - 'quoteAmount': { - configurable: false, - get: function () { - return this._quoteAmount; - } - }, - 'expiresAt': { - configurable: false, - get: function () { - return this._expiresAt; - } - } -}); - -Quote.getQuote = function (api, amount, baseCurrency, quoteCurrency) { - assert(Helpers.isInteger(amount), 'amount must be in cents or satoshi'); - - var supportedCurrencies = ['BTC', 'EUR', 'GBP', 'USD', 'DKK']; - - if (supportedCurrencies.indexOf(baseCurrency) === -1) { - return Promise.reject('base_currency_not_supported'); - } - - if (supportedCurrencies.indexOf(quoteCurrency) === -1) { - return Promise.reject('quote_currency_not_supported'); - } - - if (baseCurrency === 'CNY' || quoteCurrency === 'CNY') { - console.warn('CNY has only 1 decimal place'); - } - - var baseAmount; - if (baseCurrency === 'BTC') { - baseAmount = (amount / 100000000).toFixed(8); - } else { - baseAmount = (amount / 100).toFixed(2); - } - - var processQuote = function (quote) { - quote = new Quote(quote, api); - return quote; - }; - - var getAnonymousQuote = function () { - return api.POST('trades/quote', { - baseCurrency: baseCurrency, - quoteCurrency: quoteCurrency, - baseAmount: parseFloat(baseAmount) - }); - }; - - var getQuote = function () { - return api.authPOST('trades/quote', { - baseCurrency: baseCurrency, - quoteCurrency: quoteCurrency, - baseAmount: parseFloat(baseAmount) - }); - }; - - if (!api.hasAccount) { - return getAnonymousQuote().then(processQuote); - } else { - return getQuote().then(processQuote); - } -}; - -Quote.prototype.getPaymentMethods = function () { - var self = this; - - var setPaymentMethods = function (paymentMethods) { - self.paymentMethods = {}; - for (var i = 0; i < paymentMethods.length; i++) { - var paymentMethod = paymentMethods[i]; - self.paymentMethods[paymentMethod.inMedium] = paymentMethod; - paymentMethod.calculateFee.bind(paymentMethod)(self); - } - return self.paymentMethods; - }; - - if (this.paymentMethods) { - return Promise.resolve(this.paymentMethods); - } else { - return PaymentMethod.fetchAll(this.baseCurrency, this.quoteCurrency, this._api) - .then(setPaymentMethods); - } -}; - -// QA tool -Quote.prototype.expire = function () { - this._expiresAt = new Date(new Date().getTime() + 3 * 1000); -}; diff --git a/src/coinify/trade.js b/src/coinify/trade.js deleted file mode 100644 index 697e2a647..000000000 --- a/src/coinify/trade.js +++ /dev/null @@ -1,567 +0,0 @@ -'use strict'; - -var assert = require('assert'); - -var BankAccount = require('./bank-account'); -var Helpers = require('./helpers'); -var Quote = require('./quote'); - -module.exports = CoinifyTrade; - -function CoinifyTrade (obj, api, coinifyDelegate) { - assert(obj, 'JSON missing'); - assert(api, 'Coinify API missing'); - assert(coinifyDelegate, 'coinifyDelegate missing'); - assert(typeof coinifyDelegate.getReceiveAddress === 'function', 'delegate requires getReceiveAddress()'); - this._coinifyDelegate = coinifyDelegate; - this._api = api; - this._id = obj.id; - this.set(obj); -} - -Object.defineProperties(CoinifyTrade.prototype, { - 'debug': { - configurable: false, - get: function () { return this._debug; }, - set: function (value) { - this._debug = Boolean(value); - } - }, - 'id': { - configurable: false, - get: function () { - return this._id; - } - }, - 'iSignThisID': { - configurable: false, - get: function () { - return this._iSignThisID; - } - }, - 'quoteExpireTime': { - configurable: false, - get: function () { - return this._quoteExpireTime; - } - }, - 'bankAccount': { - configurable: false, - get: function () { - return this._bankAccount; - } - }, - 'createdAt': { - configurable: false, - get: function () { - return this._createdAt; - } - }, - 'updatedAt': { - configurable: false, - get: function () { - return this._updatedAt; - } - }, - 'inCurrency': { - configurable: false, - get: function () { - return this._inCurrency; - } - }, - 'outCurrency': { - configurable: false, - get: function () { - return this._outCurrency; - } - }, - 'inAmount': { - configurable: false, - get: function () { - return this._inAmount; - } - }, - 'medium': { - configurable: false, - get: function () { - return this._medium; - } - }, - 'state': { - configurable: false, - get: function () { - return this._state; - } - }, - 'sendAmount': { - configurable: false, - get: function () { - return this._sendAmount; - } - }, - 'outAmount': { - configurable: false, - get: function () { - return this._outAmount; - } - }, - 'outAmountExpected': { - configurable: false, - get: function () { - return this._outAmountExpected; - } - }, - 'receiptUrl': { - configurable: false, - get: function () { - return this._receiptUrl; - } - }, - 'receiveAddress': { - configurable: false, - get: function () { - return this._receiveAddress; - } - }, - 'accountIndex': { - configurable: false, - get: function () { - return this._account_index; - } - }, - 'bitcoinReceived': { - configurable: false, - get: function () { - return Boolean(this._txHash); - } - }, - 'confirmed': { - configurable: false, - get: function () { - return this._confirmed || this._confirmations >= 3; - } - }, - 'isBuy': { - configurable: false, - get: function () { - if (Boolean(this._is_buy) === this._is_buy) { - return this._is_buy; - } else if (this._is_buy === undefined && this.outCurrency === undefined) { - return true; // For older test wallets, can be safely removed later. - } else { - return this.outCurrency === 'BTC'; - } - } - }, - 'txHash': { - configurable: false, - get: function () { return this._txHash || null; } - } -}); - -CoinifyTrade.prototype.set = function (obj) { - if ([ - 'awaiting_transfer_in', - 'processing', - 'reviewing', - 'completed', - 'completed_test', - 'cancelled', - 'rejected', - 'expired' - ].indexOf(obj.state) === -1) { - console.warn('Unknown state:', obj.state); - } - if (this._isDeclined && obj.state === 'awaiting_transfer_in') { - // Coinify API may lag a bit behind the iSignThis iframe. - this._state = 'rejected'; - } else { - this._state = obj.state; - } - this._is_buy = obj.is_buy; - - this._inCurrency = obj.inCurrency; - this._outCurrency = obj.outCurrency; - - if (obj.transferIn) { - this._medium = obj.transferIn.medium; - this._sendAmount = this._inCurrency === 'BTC' - ? Helpers.toSatoshi(obj.transferIn.sendAmount) - : Helpers.toCents(obj.transferIn.sendAmount); - } - - if (this._inCurrency === 'BTC') { - this._inAmount = Helpers.toSatoshi(obj.inAmount); - this._outAmount = Helpers.toCents(obj.outAmount); - this._outAmountExpected = Helpers.toCents(obj.outAmountExpected); - } else { - this._inAmount = Helpers.toCents(obj.inAmount); - this._outAmount = Helpers.toSatoshi(obj.outAmount); - this._outAmountExpected = Helpers.toSatoshi(obj.outAmountExpected); - } - - if (obj.confirmed === Boolean(obj.confirmed)) { - this._coinifyDelegate.deserializeExtraFields(obj, this); - this._receiveAddress = this._coinifyDelegate.getReceiveAddress(this); - this._confirmed = obj.confirmed; - this._txHash = obj.tx_hash; - } else { // Contructed from Coinify API - /* istanbul ignore if */ - if (this.debug) { - // This log only happens if .set() is called after .debug is set. - console.info('Trade ' + this.id + ' from Coinify API'); - } - this._createdAt = new Date(obj.createTime); - this._updatedAt = new Date(obj.updateTime); - this._quoteExpireTime = new Date(obj.quoteExpireTime); - this._receiptUrl = obj.receiptUrl; - - if (this._inCurrency !== 'BTC') { - // NOTE: this field is currently missing in the Coinify API: - if (obj.transferOut && obj.transferOutdetails && obj.transferOutdetails.transaction) { - this._txHash = obj.transferOutdetails.transaction; - } - - if (this._medium === 'bank') { - this._bankAccount = new BankAccount(obj.transferIn.details); - } - - this._receiveAddress = obj.transferOut.details.account; - this._iSignThisID = obj.transferIn.details.paymentId; - } - } - - return this; -}; - -CoinifyTrade.prototype.cancel = function () { - var self = this; - - var processCancel = function (trade) { - self._state = trade.state; - - self._coinifyDelegate.releaseReceiveAddress(self); - - return self._coinifyDelegate.save.bind(self._coinifyDelegate)(); - }; - - return self._api.authPATCH('trades/' + self._id + '/cancel').then(processCancel); -}; - -// Checks the balance for the receive address and monitors the websocket if needed: -// Call this method long before the user completes the purchase: -// trade.watchAddress.then(() => ...); -CoinifyTrade.prototype.watchAddress = function () { - /* istanbul ignore if */ - if (this.debug) { - console.info('Watch ' + this.receiveAddress + ' for ' + this.state + ' trade ' + this.id); - } - var self = this; - var promise = new Promise(function (resolve, reject) { - self._watchAddressResolve = resolve; - }); - return promise; -}; - -CoinifyTrade.prototype.btcExpected = function () { - var self = this; - if (this.isBuy) { - if ([ - 'completed', - 'completed_test', - 'cancelled', - 'rejected' - ].indexOf(this.state) > -1) { - return Promise.resolve(this.outAmountExpected); - } - - var oneMinuteAgo = new Date(new Date().getTime() - 60 * 1000); - if (this.quoteExpireTime > new Date()) { - // Quoted price still valid - return Promise.resolve(this.outAmountExpected); - } else { - // Estimate BTC expected based on current exchange rate: - if (this._lastBtcExpectedGuessAt > oneMinuteAgo) { - return Promise.resolve(this._lastBtcExpectedGuess); - } else { - var processQuote = function (quote) { - self._lastBtcExpectedGuess = quote.quoteAmount; - self._lastBtcExpectedGuessAt = new Date(); - return self._lastBtcExpectedGuess; - }; - return Quote.getQuote(this._api, -this.inAmount, this.inCurrency).then(processQuote); - } - } - } else { - return Promise.reject(); - } -}; - -// QA tool: -CoinifyTrade.prototype.fakeBankTransfer = function () { - var self = this; - - return self._api.authPOST('trades/' + self._id + '/test/bank-transfer', { - sendAmount: parseFloat((self.inAmount / 100).toFixed(2)), - currency: self.inCurrency - }); -}; - -// QA tool: -CoinifyTrade.prototype.expireQuote = function () { - this._quoteExpireTime = new Date(new Date().getTime() + 3000); -}; - -CoinifyTrade.buy = function (quote, medium, api, coinifyDelegate, debug) { - assert(quote, 'Quote required'); - - /* istanbul ignore if */ - if (debug) { - console.info('Reserve receive address for new trade'); - } - var reservation = coinifyDelegate.reserveReceiveAddress(); - - var processTrade = function (res) { - var trade = new CoinifyTrade(res, api, coinifyDelegate); - trade.debug = debug; - - /* istanbul ignore if */ - if (debug) { - console.info('Commit receive address for new trade'); - } - reservation.commit(trade); - - /* istanbul ignore if */ - if (debug) { - console.info('Monitor trade', trade.receiveAddress); - } - trade._monitorAddress.bind(trade)(); - return trade; - }; - - var error = function (e) { - console.error(e); - return Promise.reject(e); - }; - - return api.authPOST('trades', { - priceQuoteId: quote.id, - transferIn: { - medium: medium - }, - transferOut: { - medium: 'blockchain', - details: { - account: reservation.receiveAddress - } - } - }).then(processTrade).catch(error); -}; - -CoinifyTrade.fetchAll = function (api) { - return api.authGET('trades'); -}; - -CoinifyTrade.prototype.self = function () { - return this; -}; - -CoinifyTrade.prototype.process = function () { - if (['rejected', 'cancelled', 'expired'].indexOf(this.state) > -1) { - /* istanbul ignore if */ - if (this.debug) { - console.info('Check if address for ' + this.state + ' trade ' + this.id + ' can be released'); - } - this._coinifyDelegate.releaseReceiveAddress(this); - } -}; - -CoinifyTrade.prototype.refresh = function () { - /* istanbul ignore if */ - if (this.debug) { - console.info('Refresh ' + this.state + ' trade ' + this.id); - } - return this._api.authGET('trades/' + this._id) - .then(this.set.bind(this)) - .then(this._coinifyDelegate.save.bind(this._coinifyDelegate)) - .then(this.self.bind(this)); -}; - -// Call this if the iSignThis iframe says the card is declined. It may take a -// while before Coinify API reflects this change -CoinifyTrade.prototype.declined = function () { - this._state = 'rejected'; - this._isDeclined = true; -}; - -CoinifyTrade.prototype._monitorAddress = function () { - var self = this; - - var tradeWasPaid = function (amount) { - /* istanbul ignore if */ - if (self.debug) { - console.info(amount + ' paid for trade ' + self.id); - } - var resolve = function () { - self._watchAddressResolve && self._watchAddressResolve(amount); - }; - self._coinifyDelegate.save.bind(self._coinifyDelegate)().then(resolve); - }; - - self._coinifyDelegate.monitorAddress(self.receiveAddress, function (hash, amount) { - var updateTrade = function () { - /* istanbul ignore if */ - if (self.debug) { - console.info('Transaction ' + hash + ' detected, considering ' + self.state + ' trade ' + self.id); - } - if (self.state === 'completed_test' && !self.confirmations && !self._txHash) { - // For test trades, there is no real transaction, so trade._txHash is not - // set. Instead use the hash for the incoming transaction. This will not - // work correctly with address reuse. - /* istanbul ignore if */ - if (self.debug) { - console.info('Test trade, not matched before, unconfirmed transaction, assuming match'); - } - self._txHash = hash; - tradeWasPaid(amount); - } else if (self.state === 'completed' || self.state === 'processing') { - if (self._txHash) { - // Multiple trades may reuse the same address if e.g. one is - // cancelled of if we reach the gap limit. - /* istanbul ignore if */ - if (self.debug) { - console.info('Trade already matched, ignoring transaction'); - } - if (self._txHash !== hash) return; - } else { - // transferOut.details.transaction is not implemented and might be - // missing if in the processing state. - /* istanbul ignore if */ - if (self.debug) { - console.info('Trade not matched yet, assuming match'); - } - self._txHash = hash; - } - tradeWasPaid(amount); - } else { - /* istanbul ignore if */ - if (self.debug) { - console.info('Not calling tradeWasPaid()'); - } - } - }; - - if (self.state === 'completed' || self.state === 'processing' || self.state === 'completed_test') { - updateTrade(); - } else { - self.refresh().then(updateTrade); - } - }); -}; - -CoinifyTrade._checkOnce = function (unfilteredTrades, tradeFilter, coinifyDelegate) { - assert(coinifyDelegate, '_checkOnce needs delegate'); - - var getReceiveAddress = function (obj) { return obj.receiveAddress; }; - - var trades = unfilteredTrades.filter(tradeFilter); - - var receiveAddresses = trades.map(getReceiveAddress); - - if (receiveAddresses.length === 0) { - return Promise.resolve(); - } - - var promises = []; - - for (var i = 0; i < trades.length; i++) { - promises.push(CoinifyTrade._setTransactionHash(trades[i], coinifyDelegate)); - } - - return Promise.all(promises).then(coinifyDelegate.save.bind(coinifyDelegate)); -}; - -// -CoinifyTrade._setTransactionHash = function (trade, coinifyDelegate) { - assert(coinifyDelegate, '_setTransactionHash needs delegate'); - return coinifyDelegate.checkAddress(trade.receiveAddress) - .then(function (tx) { - if (tx) { - if (trade.state === 'completed_test' && !trade._txHash) { - // See remarks below - trade._txHash = tx.hash; - } else if (trade.state === 'processing' || trade.state === 'completed') { - if (trade._txHash) { - if (trade._txHash !== tx.hash) return; - } else { - trade._txHash = tx.hash; - } - } else { - return; - } - trade._confirmations = tx.confirmations; - // TODO: refactor - if (trade.confirmed) { - trade._confirmed = true; - } - } - }); -}; - -CoinifyTrade._monitorWebSockets = function (unfilteredTrades, tradeFilter) { - var trades = unfilteredTrades - .filter(tradeFilter); - - for (var i = 0; i < trades.length; i++) { - var trade = trades[i]; - trade._monitorAddress.bind(trade)(); - } -}; - -// Monitor the receive addresses for pending and completed trades. -CoinifyTrade.monitorPayments = function (trades, coinifyDelegate) { - // TODO: refactor, apply filter here instead of passing it on - assert(coinifyDelegate, '_monitorPayments needs delegate'); - - var tradeFilter = function (trade) { - return [ - 'awaiting_transfer_in', - 'reviewing', - 'processing', - 'completed', - 'completed_test' - ].indexOf(trade.state) > -1 && !trade.confirmed; - }; - - CoinifyTrade._checkOnce(trades, tradeFilter, coinifyDelegate).then(function () { - CoinifyTrade._monitorWebSockets(trades, tradeFilter); - }); -}; - -CoinifyTrade.prototype.toJSON = function () { - var serialized = { - id: this._id, - state: this._state, - tx_hash: this._txHash, - confirmed: this.confirmed, - is_buy: this.isBuy - }; - - this._coinifyDelegate.serializeExtraFields(serialized, this); - - return serialized; -}; - -CoinifyTrade.filteredTrades = function (trades) { - return trades.filter(function (trade) { - // Only consider transactions that are complete or that we're still - // expecting payment for: - return [ - 'awaiting_transfer_in', - 'processing', - 'reviewing', - 'completed', - 'completed_test' - ].indexOf(trade.state) > -1; - }); -}; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 000000000..be50f3356 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,19 @@ + +var Bitcoin = require('bitcoinjs-lib'); + +module.exports = { + NETWORK: 'bitcoin', + APP_NAME: 'javascript_web', + APP_VERSION: '3.0', + getNetwork: function () { + return Bitcoin.networks[this.NETWORK]; + }, + getDefaultWalletOptions: function () { + return { + pbkdf2_iterations: 5000, + html5_notifications: false, + fee_per_kb: 10000, + logout_time: 600000 + }; + } +}; diff --git a/src/exchange-delegate.js b/src/exchange-delegate.js index 965436aa8..b90131959 100644 --- a/src/exchange-delegate.js +++ b/src/exchange-delegate.js @@ -25,6 +25,15 @@ Object.defineProperties(ExchangeDelegate.prototype, { set: function (value) { this._trades = value; } + }, + 'labelBase': { + configurable: false, + get: function () { + return this._labelBase || 'Exchange order'; + }, + set: function (value) { + this._labelBase = value; + } } }); @@ -36,24 +45,42 @@ ExchangeDelegate.prototype.email = function () { return this._wallet.accountInfo.email; }; +ExchangeDelegate.prototype.mobile = function () { + return this._wallet.accountInfo.mobile; +}; + ExchangeDelegate.prototype.isEmailVerified = function () { return this._wallet.accountInfo.isEmailVerified; }; -ExchangeDelegate.prototype.getEmailToken = function () { - var self = this; +ExchangeDelegate.prototype.isMobileVerified = function () { + return this._wallet.accountInfo.isMobileVerified; +}; + +ExchangeDelegate.prototype.getToken = function (partner, options) { + options = options || {}; + // assert(partner, 'Specify exchange partner'); + + let fields = { + // partner: partner, // Coinify doesn't support this yet + guid: this._wallet.guid, + sharedKey: this._wallet.sharedKey, + fields: `email${options.mobile ? '|mobile' : ''}${options.walletAge ? '|wallet_age' : ''}` + }; + + if (partner) { + fields.partner = partner; + } + return API.request( 'GET', - 'wallet/signed-email-token', - { - guid: self._wallet.guid, - sharedKey: self._wallet.sharedKey - } + 'wallet/signed-token', + fields ).then(function (res) { if (res.success) { return res.token; } else { - throw new Error('Unable to obtain email verification proof'); + throw new Error('Unable to obtain email & mobile verification proof'); } }); }; @@ -118,12 +145,11 @@ ExchangeDelegate.prototype.reserveReceiveAddress = function () { } function commitAddressLabel (trade) { - var labelBase = 'Coinify order'; var ids = self._trades .filter(Helpers.propEq('receiveAddress', receiveAddress)) .map(Helpers.pluck('id')).concat(trade.id); - var label = labelBase + ' #' + ids.join(', #'); + var label = self.labelBase + ' #' + ids.join(', #'); /* istanbul ignore if */ if (self.debug) { console.info('Set label for receive index', receiveAddressIndex, label); diff --git a/src/external.js b/src/external.js index 1998ca040..c77a7e45c 100644 --- a/src/external.js +++ b/src/external.js @@ -1,8 +1,8 @@ 'use strict'; -var Coinify = require('./coinify/coinify'); +var Coinify = require('bitcoin-coinify-client'); +var SFOX = require('bitcoin-sfox-client'); var Metadata = require('./metadata'); -var assert = require('assert'); var ExchangeDelegate = require('./exchange-delegate'); var METADATA_TYPE_EXTERNAL = 3; @@ -10,21 +10,49 @@ var METADATA_TYPE_EXTERNAL = 3; module.exports = External; function External (wallet) { - this._metadata = new Metadata(METADATA_TYPE_EXTERNAL); + var masterhdnode = wallet.hdwallet.getMasterHDNode(); + this._metadata = Metadata.fromMasterHDNode(masterhdnode, METADATA_TYPE_EXTERNAL); this._coinify = undefined; + this._sfox = undefined; this._wallet = wallet; } Object.defineProperties(External.prototype, { 'coinify': { configurable: false, - get: function () { return this._coinify; } + get: function () { + if (!this._coinify) { + var delegate = new ExchangeDelegate(this._wallet); + this._coinify = Coinify.new(delegate); + delegate.trades = this._coinify.trades; + } + return this._coinify; + } + }, + 'sfox': { + configurable: false, + get: function () { + if (!this._sfox) { + var delegate = new ExchangeDelegate(this._wallet); + this._sfox = SFOX.new(delegate); + delegate.trades = this._sfox.trades; + } + return this._sfox; + } + }, + 'hasExchangeAccount': { + configurable: false, + get: function () { + return (this._coinify && this._coinify.hasAccount) || + (this._sfox && this._sfox.hasAccount); + } } }); External.prototype.toJSON = function () { var external = { - coinify: this._coinify + coinify: this._coinify, + sfox: this._sfox }; return external; }; @@ -34,9 +62,14 @@ External.prototype.fetch = function () { this.loaded = true; if (object !== null) { if (object.coinify) { - var delegate = new ExchangeDelegate(this._wallet); - this._coinify = new Coinify(object.coinify, delegate); - delegate.trades = this._coinify.trades; + var coinifyDelegate = new ExchangeDelegate(this._wallet); + this._coinify = new Coinify(object.coinify, coinifyDelegate); + coinifyDelegate.trades = this._coinify.trades; + } + if (object.sfox) { + var sfoxDelegate = new ExchangeDelegate(this._wallet); + this._sfox = new SFOX(object.sfox, sfoxDelegate); + sfoxDelegate.trades = this._sfox.trades; } } return this; @@ -60,12 +93,5 @@ External.prototype.save = function () { External.prototype.wipe = function () { this._metadata.update({}).then(this.fetch.bind(this)); this._coinify = undefined; -}; - -External.prototype.addCoinify = function () { - assert(!this._coinify, 'Already added'); - - var delegate = new ExchangeDelegate(this._wallet); - this._coinify = Coinify.new(delegate); - delegate.trades = this._coinify.trades; + this._sfox = undefined; }; diff --git a/src/hd-account.js b/src/hd-account.js index 29a786a27..24b4cd4cd 100644 --- a/src/hd-account.js +++ b/src/hd-account.js @@ -9,6 +9,7 @@ var KeyRing = require('./keyring'); var MyWallet = require('./wallet'); // This cyclic import should be avoided once the refactor is complete var TxList = require('./transaction-list'); var API = require('./api'); +var constants = require('./constants'); // HDAccount Class @@ -87,10 +88,7 @@ Object.defineProperties(HDAccount.prototype, { if (Helpers.isBoolean(value)) { this._archived = value; MyWallet.syncWallet(); - if (!value) { // Unarchive - // we should define a way to update only the account, not the whole wallet - MyWallet.wallet.getHistory(); - } + MyWallet.wallet.getHistory(); } else { throw new Error('account.archived must be a boolean'); } @@ -230,7 +228,7 @@ HDAccount.fromWalletMasterKey = function (masterkey, index, label) { HDAccount.fromExtPublicKey = function (extPublicKey, index, label) { // this is creating a read-only account assert(Helpers.isXpubKey(extPublicKey), 'Extended public key must be given to create an account.'); - var accountZero = Bitcoin.HDNode.fromBase58(extPublicKey); + var accountZero = Bitcoin.HDNode.fromBase58(extPublicKey, constants.getNetwork()); var a = HDAccount.fromAccountMasterKey(accountZero, index, label); a._xpriv = null; return a; @@ -238,7 +236,7 @@ HDAccount.fromExtPublicKey = function (extPublicKey, index, label) { HDAccount.fromExtPrivateKey = function (extPrivateKey, index, label) { assert(Helpers.isXprivKey(extPrivateKey), 'Extended private key must be given to create an account.'); - var accountZero = Bitcoin.HDNode.fromBase58(extPrivateKey); + var accountZero = Bitcoin.HDNode.fromBase58(extPrivateKey, constants.getNetwork()); return HDAccount.fromAccountMasterKey(accountZero, index, label); }; diff --git a/src/hd-wallet.js b/src/hd-wallet.js index 84a9206c2..0a78c75a0 100644 --- a/src/hd-wallet.js +++ b/src/hd-wallet.js @@ -8,6 +8,7 @@ var Helpers = require('./helpers'); var HDAccount = require('./hd-account'); var BIP39 = require('bip39'); var MyWallet = require('./wallet'); // This cyclic import should be avoided once the refactor is complete +var constants = require('./constants'); function HDWallet (object) { function addAccount (o, index) { @@ -182,7 +183,7 @@ HDWallet.prototype.getMasterHDNode = function (cipher) { } var masterhex = HDWallet.getMasterHex(this._seedHex, this._bip39Password, dec); - var network = Bitcoin.networks.bitcoin; + var network = constants.getNetwork(); return Bitcoin.HDNode.fromSeedBuffer(masterhex, network); }; diff --git a/src/helpers.js b/src/helpers.js index 4e7d527d4..d408c8843 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -5,8 +5,9 @@ var BigInteger = require('bigi'); var Buffer = require('buffer').Buffer; var Base58 = require('bs58'); var BIP39 = require('bip39'); -var shared = require('./shared'); var ImportExport = require('./import-export'); +var constants = require('./constants'); +var WalletCrypo = require('./wallet-crypto'); var Helpers = {}; Math.log2 = function (x) { return Math.log(x) / Math.LN2; }; @@ -23,13 +24,13 @@ Helpers.isInstanceOf = function (object, theClass) { Helpers.isBitcoinAddress = function (candidate) { try { var d = Bitcoin.address.fromBase58Check(candidate); - var n = Bitcoin.networks.bitcoin; + var n = constants.getNetwork(); return d.version === n.pubKeyHash || d.version === n.scriptHash; } catch (e) { return false; } }; Helpers.isBitcoinPrivateKey = function (candidate) { try { - Bitcoin.ECPair.fromWIF(candidate); + Bitcoin.ECPair.fromWIF(candidate, constants.getNetwork()); return true; } catch (e) { return false; } }; @@ -37,10 +38,10 @@ Helpers.isBase58Key = function (str) { return Helpers.isString(str) && /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{40,44}$/.test(str); }; Helpers.isXprivKey = function (k) { - return Helpers.isString(k) && k.substring(0, 4) === 'xprv'; + return Helpers.isString(k) && (/^(x|t)prv/).test(k); }; Helpers.isXpubKey = function (k) { - return Helpers.isString(k) && k.substring(0, 4) === 'xpub'; + return Helpers.isString(k) && (/^(x|t)pub/).test(k); }; Helpers.isAlphaNum = function (str) { return Helpers.isString(str) && /^[\-+,._\w\d\s]+$/.test(str); @@ -130,12 +131,14 @@ Helpers.isEmptyArray = function (x) { Helpers.asyncOnce = function (f, milliseconds, before) { var timer = null; var oldArguments = []; - return function () { + + trigger.cancel = function () { + clearTimeout(timer); + }; + + function trigger () { + trigger.cancel(); before && before(); - if (timer) { - clearTimeout(timer); - timer = null; - } var myArgs = []; // this is needed because arguments is not an 'Array' instance for (var i = 0; i < arguments.length; i++) { myArgs[i] = arguments[i]; } @@ -145,7 +148,9 @@ Helpers.asyncOnce = function (f, milliseconds, before) { f.apply(this, myArgs); oldArguments = []; }, milliseconds); - }; + } + + return trigger; }; Helpers.exponentialBackoff = function (f, maxTime) { @@ -333,17 +338,31 @@ Helpers.privateKeyStringToKey = function (value, format) { throw new Error('Unsupported Key Format'); } - return new Bitcoin.ECPair(new BigInteger.fromByteArrayUnsigned(keyBytes), null, {compressed: format !== 'sipa'}); // eslint-disable-line new-cap + return new Bitcoin.ECPair( + new BigInteger.fromByteArrayUnsigned(keyBytes), // eslint-disable-line new-cap + null, + { compressed: format !== 'sipa', network: constants.getNetwork() } + ); }; Helpers.detectPrivateKeyFormat = function (key) { - // 51 characters base58, always starts with a '5' - if (/^5[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}$/.test(key)) { + var isTestnet = constants.NETWORK === 'testnet'; + + // 51 characters base58, always starts with 5 (or 9, for testnet) + var sipaRegex = isTestnet + ? (/^[9][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}$/) + : (/^[5][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{50}$/); + + if (sipaRegex.test(key)) { return 'sipa'; } - // 52 character compressed starts with L or K - if (/^[LK][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/.test(key)) { + // 52 character compressed starts with L or K (or c, for testnet) + var compsipaRegex = isTestnet + ? (/^[c][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/) + : (/^[LK][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{51}$/); + + if (compsipaRegex.test(key)) { return 'compsipa'; } @@ -419,31 +438,8 @@ Helpers.privateKeyCorrespondsToAddress = function (address, priv, bipPass) { return new Promise(asyncParse).then(predicate); }; -function parseValueBitcoin (valueString) { - valueString = valueString.toString(); - // TODO: Detect other number formats (e.g. comma as decimal separator) - var valueComp = valueString.split('.'); - var integralPart = valueComp[0]; - var fractionalPart = valueComp[1] || '0'; - while (fractionalPart.length < 8) fractionalPart += '0'; - fractionalPart = fractionalPart.replace(/^0+/g, ''); - var value = BigInteger.valueOf(parseInt(integralPart, 10)); - value = value.multiply(BigInteger.valueOf(100000000)); - value = value.add(BigInteger.valueOf(parseInt(fractionalPart, 10))); - return value; -} - -// The current 'shift' value - BTC = 1, mBTC = 3, uBTC = 6 -function sShift (symbol) { - return (shared.satoshi / symbol.conversion).toString().length - 1; -} - -Helpers.precisionToSatoshiBN = function (x) { - return parseValueBitcoin(x).divide(BigInteger.valueOf(Math.pow(10, sShift(shared.getBTCSymbol())).toString())); -}; - Helpers.verifyMessage = function (address, signature, message) { - return Bitcoin.message.verify(address, signature, message); + return Bitcoin.message.verify(address, signature, message, constants.getNetwork()); }; Helpers.getMobileOperatingSystem = function () { @@ -458,4 +454,17 @@ Helpers.getMobileOperatingSystem = function () { } }; +Helpers.isEmailInvited = function (email, fraction) { + if (!email) { + return false; + } + if (!Helpers.isPositiveNumber(fraction)) { + return false; + } + if (fraction > 1) { + return false; + } + return WalletCrypo.sha256(email)[0] / 256 >= 1 - fraction; +}; + module.exports = Helpers; diff --git a/src/import-export.js b/src/import-export.js index ebc5a81b8..c96cb6875 100644 --- a/src/import-export.js +++ b/src/import-export.js @@ -6,6 +6,7 @@ var Base58 = require('bs58'); var Unorm = require('unorm'); var WalletCrypto = require('./wallet-crypto'); var Buffer = require('buffer').Buffer; +var constants = require('./constants'); var hash256 = Bitcoin.crypto.hash256; @@ -68,7 +69,7 @@ var ImportExport = new function () { var AESopts = { mode: WalletCrypto.AES.ECB, padding: WalletCrypto.pad.NoPadding }; var verifyHashAndReturn = function () { - var tmpkey = new Bitcoin.ECPair(decrypted, null, {compressed: isCompPoint}); + var tmpkey = new Bitcoin.ECPair(decrypted, null, {compressed: isCompPoint, network: constants.getNetwork()}); var base58Address = tmpkey.getAddress(); @@ -108,7 +109,7 @@ var ImportExport = new function () { passfactor = hash256(prefactorB); } - var kp = new Bitcoin.ECPair(BigInteger.fromBuffer(passfactor)); + var kp = new Bitcoin.ECPair(BigInteger.fromBuffer(passfactor), null, {network: constants.getNetwork()}); var passpoint = kp.getPublicKeyBuffer(); diff --git a/src/keychain.js b/src/keychain.js index 64a550ce2..e8165e81a 100644 --- a/src/keychain.js +++ b/src/keychain.js @@ -5,6 +5,7 @@ module.exports = KeyChain; var Bitcoin = require('bitcoinjs-lib'); var assert = require('assert'); var Helpers = require('./helpers'); +var constants = require('./constants'); // keychain function KeyChain (extendedKey, index, cache) { @@ -40,10 +41,10 @@ KeyChain.prototype.init = function (extendedKey, index, cache) { // if cache is defined we use it to recreate the chain // otherwise we generate it using extendedKey and index if (cache) { - this._chainRoot = Bitcoin.HDNode.fromBase58(cache); + this._chainRoot = Bitcoin.HDNode.fromBase58(cache, constants.getNetwork()); } else { this._chainRoot = extendedKey && Helpers.isPositiveInteger(index) && index >= 0 - ? Bitcoin.HDNode.fromBase58(extendedKey).derive(index) : undefined; + ? Bitcoin.HDNode.fromBase58(extendedKey, constants.getNetwork()).derive(index) : undefined; } return this; }; diff --git a/src/metadata.js b/src/metadata.js index 04a0144e7..2a2f5eac9 100644 --- a/src/metadata.js +++ b/src/metadata.js @@ -4,188 +4,43 @@ var WalletCrypto = require('./wallet-crypto'); var Bitcoin = require('bitcoinjs-lib'); var API = require('./api'); var Helpers = require('./helpers'); - -var MyWallet = require('./wallet'); - -module.exports = Metadata; - -function Metadata (payloadType, cipher) { - this.VERSION = 1; - this._payloadTypeId = payloadType; - this._magicHash = null; - this._value = null; - this._sequence = Promise.resolve(); - - // BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number - // we take the first 31 bits of the SHA256 hash of a reverse domain. - var hash = WalletCrypto.sha256('info.blockchain.metadata'); - var purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7FFFFFFF; // 510742 - - var masterHDNode = MyWallet.wallet.hdwallet.getMasterHDNode(cipher); - var metaDataHDNode = masterHDNode.deriveHardened(purpose); - - // Payload types: - // 0: reserved - // 1: reserved - // 2: whats-new - - var payloadTypeNode = metaDataHDNode.deriveHardened(payloadType); - - // purpose' / type' / 0' : https://meta.blockchain.info/{address} - // signature used to authenticate - // purpose' / type' / 1' : sha256(private key) used as 256 bit AES key - - var node = payloadTypeNode.deriveHardened(0); - - this._address = node.getAddress(); - this._signatureKeyPair = node.keyPair; - - var privateKeyBuffer = payloadTypeNode.deriveHardened(1).keyPair.d.toBuffer(); - this._encryptionKey = WalletCrypto.sha256(privateKeyBuffer); -} - -Metadata.prototype.setMagicHash = function (encryptedPayload) { - this._magicHash = Bitcoin.message.magicHash(encryptedPayload, Bitcoin.networks.bitcoin); -}; - -Object.defineProperties(Metadata.prototype, { - 'existsOnServer': { - configurable: false, - get: function () { return Boolean(this._magicHash); } +var constants = require('./constants'); +var R = require('ramda'); + +class Metadata { + constructor (ecPair, encKeyBuffer, typeId) { + // ecPair :: ECPair object - bitcoinjs-lib + // encKeyBuffer :: Buffer (nullable = no encrypted save) + // TypeId :: Int (nullable = default -1) + this.VERSION = 1; + this._typeId = typeId || -1; + this._magicHash = null; + this._address = ecPair.getAddress(); + this._signKey = ecPair; + this._encKeyBuffer = encKeyBuffer; + this._sequence = Promise.resolve(); } -}); -/* -metadata = new Blockchain.Metadata(2); -metadata.create({ - lastViewed: Date.now() -}); -*/ -Metadata.prototype.create = function (data) { - var self = this; - return this.next(function () { - var payload = JSON.stringify(data); - - var encryptedPayload = WalletCrypto.encryptDataWithKey(payload, self._encryptionKey); - - var encryptedPayloadSignature = Bitcoin.message.sign( - self._signatureKeyPair, - encryptedPayload - ); - - var serverPayload = { - version: 1, - payload_type_id: self._payloadTypeId, - payload: encryptedPayload, - signature: encryptedPayloadSignature.toString('base64') - }; - - return self.POST(self._address, serverPayload).then(function () { - self._value = data; - self.setMagicHash(encryptedPayload); - }); - }); -}; - -Metadata.prototype.fetch = function () { - var self = this; - return this.next(function () { - return self.GET(self._address).then(function (serverPayload) { - if (serverPayload === null) { - return null; - } - - var decryptedPayload = WalletCrypto.decryptDataWithKey(serverPayload.payload, self._encryptionKey); - - var verified = Bitcoin.message.verify( - self._address, - Buffer(serverPayload.signature, 'base64'), - serverPayload.payload - ); - - if (verified) { - self._previousPayload = decryptedPayload; - self._value = JSON.parse(decryptedPayload); - self.setMagicHash(serverPayload.payload); - return self._value; - } else { - throw new Error('METADATA_SIGNATURE_VERIFICATION_ERROR'); - } - }).catch(function (e) { - console.error(e); - return Promise.reject('METADATA_FETCH_FAILED'); - }); - }); -}; - -/* -metadata.update({ - lastViewed: Date.now() -}); -*/ -Metadata.prototype.update = function (data) { - var self = this; - return this.next(function () { - var payload = JSON.stringify(data); - if (payload === self._previousPayload) { - return Promise.resolve(); - } - self._previousPayload = payload; - var encryptedPayload = WalletCrypto.encryptDataWithKey(payload, self._encryptionKey); - var encryptedPayloadSignature = Bitcoin.message.sign( - self._signatureKeyPair, - encryptedPayload - ); - - var serverPayload = { - version: 1, - payload_type_id: self._payloadTypeId, - prev_magic_hash: self._magicHash.toString('hex'), - payload: encryptedPayload, - signature: encryptedPayloadSignature.toString('base64') - }; - - return self.PUT(self._address, serverPayload).then(function () { - self._value = data; - self.setMagicHash(encryptedPayload); - }); - }); -}; - -Metadata.prototype.GET = function (endpoint, data) { - // if (this._payloadTypeId === 3) { - // return Promise.reject('DEBUG: simulate meta data service failure'); - // } - return this.request('GET', endpoint, data); -}; - -Metadata.prototype.POST = function (endpoint, data) { - return this.request('POST', endpoint, data); -}; - -Metadata.prototype.PUT = function (endpoint, data) { - return this.request('PUT', endpoint, data); -}; - -Metadata.prototype.request = function (method, endpoint, data) { - var url = API.API_ROOT_URL + 'metadata/' + endpoint; + get existsOnServer () { + return Boolean(this._magicHash); + } +} - var options = { +// network +Metadata.request = function (method, endpoint, data) { + const url = API.API_ROOT_URL + 'metadata/' + endpoint; + let options = { headers: { 'Content-Type': 'application/json' }, credentials: 'omit' }; - if (method !== 'GET') { options.body = JSON.stringify(data); } - options.method = method; + const handleNetworkError = (e) => + Promise.reject({ error: 'METADATA_CONNECT_ERROR', message: e }); - var handleNetworkError = function (e) { - return Promise.reject({ error: 'METADATA_CONNECT_ERROR', message: e }); - }; - - var checkStatus = function (response) { + const checkStatus = (response) => { if (response.status >= 200 && response.status < 300) { return response.json(); } else if (method === 'GET' && response.status === 404) { @@ -194,14 +49,166 @@ Metadata.prototype.request = function (method, endpoint, data) { return response.text().then(Promise.reject.bind(Promise)); } }; - return fetch(url, options) .catch(handleNetworkError) .then(checkStatus); }; +Metadata.GET = function (e, d) { return Metadata.request('GET', e, d); }; +Metadata.PUT = function (e, d) { return Metadata.request('PUT', e, d); }; +Metadata.read = (address) => Metadata.request('GET', address) + .then(Metadata.extractResponse(null)); + +// ////////////////////////////////////////////////////////////////////////////// +Metadata.encrypt = R.curry((key, data) => WalletCrypto.encryptDataWithKey(data, key)); +Metadata.decrypt = R.curry((key, data) => WalletCrypto.decryptDataWithKey(data, key)); +Metadata.B64ToBuffer = (base64) => Buffer.from(base64, 'base64'); +Metadata.BufferToB64 = (buff) => buff.toString('base64'); +Metadata.StringToBuffer = (base64) => Buffer.from(base64); +Metadata.BufferToString = (buff) => buff.toString(); + +// Metadata.message :: Buffer -> Buffer -> Base64String +Metadata.message = R.curry( + function (payload, prevMagic) { + if (prevMagic) { + const hash = WalletCrypto.sha256(payload); + const buff = Buffer.concat([prevMagic, hash]); + return buff.toString('base64'); + } else { + return payload.toString('base64'); + } + } +); + +// Metadata.magic :: Buffer -> Buffer -> Buffer +Metadata.magic = R.curry( + function (payload, prevMagic) { + const msg = this.message(payload, prevMagic); + return Bitcoin.message.magicHash(msg, constants.getNetwork()); + } +); + +Metadata.verify = (address, signature, hash) => + Bitcoin.message.verify(address, signature, hash); + +// Metadata.sign :: keyPair -> msg -> Buffer +Metadata.sign = (keyPair, msg) => Bitcoin.message.sign(keyPair, msg, Bitcoin.networks.bitcoin); + +// Metadata.computeSignature :: keypair -> buffer -> buffer -> base64 +Metadata.computeSignature = (key, payloadBuff, magicHash) => + Metadata.sign(key, Metadata.message(payloadBuff, magicHash)); + +Metadata.verifyResponse = R.curry((address, res) => { + if (res === null) return res; + const M = Metadata; + const sB = res.signature ? Buffer.from(res.signature, 'base64') : undefined; + const pB = res.payload ? Buffer.from(res.payload, 'base64') : undefined; + const mB = res.prev_magic_hash ? Buffer.from(res.prev_magic_hash, 'hex') : undefined; + const verified = Metadata.verify(address, sB, M.message(pB, mB)); + if (!verified) throw new Error('METADATA_SIGNATURE_VERIFICATION_ERROR'); + return R.assoc('compute_new_magic_hash', M.magic(pB, mB), res); +}); + +Metadata.extractResponse = R.curry((encKey, res) => { + const M = Metadata; + if (res === null) { + return res; + } else { + return encKey + ? R.compose(JSON.parse, M.decrypt(encKey), R.prop('payload'))(res) + : R.compose(JSON.parse, M.BufferToString, M.B64ToBuffer, R.prop('payload'))(res); + } +}); + +Metadata.toImmutable = R.compose(Object.freeze, JSON.parse, JSON.stringify); + +Metadata.prototype.create = function (payload) { + const M = Metadata; + payload = M.toImmutable(payload); + return this.next(() => { + const encPayloadBuffer = this._encKeyBuffer + ? R.compose(M.B64ToBuffer, M.encrypt(this._encKeyBuffer), JSON.stringify)(payload) + : R.compose(M.StringToBuffer, JSON.stringify)(payload); + const signatureBuffer = M.computeSignature(this._signKey, encPayloadBuffer, this._magicHash); + const body = { + 'version': this.VERSION, + 'payload': encPayloadBuffer.toString('base64'), + 'signature': signatureBuffer.toString('base64'), + 'prev_magic_hash': this._magicHash ? this._magicHash.toString('hex') : null, + 'type_id': this._typeId + }; + return M.PUT(this._address, body).then( + (response) => { + this._value = payload; + this._magicHash = M.magic(encPayloadBuffer, this._magicHash); + return payload; + } + ); + }); +}; + +Metadata.prototype.update = function (payload) { + if (JSON.stringify(payload) === JSON.stringify(this._value)) { + return this.next(() => Promise.resolve(Metadata.toImmutable(payload))); + } else { + return this.create(payload); + } +}; + +Metadata.prototype.fetch = function () { + return this.next(() => { + const M = Metadata; + const saveMagicHash = (res) => { + if (res === null) return res; + this._magicHash = R.prop('compute_new_magic_hash', res); + return res; + }; + const saveValue = (res) => { + if (res === null) return res; + this._value = Metadata.toImmutable(res); + return res; + }; + return M.GET(this._address).then(M.verifyResponse(this._address)) + .then(saveMagicHash) + .then(M.extractResponse(this._encKeyBuffer)) + .then(saveValue) + .catch((e) => Promise.reject('METADATA_FETCH_FAILED')); + }); +}; + Metadata.prototype.next = function (f) { var nextInSeq = this._sequence.then(f); this._sequence = nextInSeq.then(Helpers.noop, Helpers.noop); return nextInSeq; }; + +// CONSTRUCTORS +// used to restore metadata from purpose xpriv (second password) +Metadata.fromMetadataHDNode = function (metadataHDNode, typeId) { + // Payload types: + // 0: reserved (guid) + // 1: reserved + // 2: whats-new + // 3: buy-sell + // 4: contacts + const payloadTypeNode = metadataHDNode.deriveHardened(typeId); + // purpose' / type' / 0' : https://meta.blockchain.info/{address} + // signature used to authenticate + // purpose' / type' / 1' : sha256(private key) used as 256 bit AES key + const node = payloadTypeNode.deriveHardened(0); + const privateKeyBuffer = payloadTypeNode.deriveHardened(1).keyPair.d.toBuffer(); + const encryptionKey = WalletCrypto.sha256(privateKeyBuffer); + return new Metadata(node.keyPair, encryptionKey, typeId); +}; + +// used to create a new metadata entry from wallet master hd node +Metadata.fromMasterHDNode = function (masterHDNode, typeId) { + // BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number + // we take the first 31 bits of the SHA256 hash of a reverse domain. + var hash = WalletCrypto.sha256('info.blockchain.metadata'); + var purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7FFFFFFF; // 510742 + var metadataHDNode = masterHDNode.deriveHardened(purpose); + return Metadata.fromMetadataHDNode(metadataHDNode, typeId); +}; + +module.exports = Metadata; diff --git a/src/payment.js b/src/payment.js index ca320983a..adbee5bcf 100644 --- a/src/payment.js +++ b/src/payment.js @@ -9,6 +9,7 @@ var Helpers = require('./helpers'); var KeyRing = require('./keyring'); var EventEmitter = require('events'); var util = require('util'); +var constants = require('./constants'); // Payment Class @@ -364,7 +365,7 @@ Payment.updateFees = function () { Payment.prebuild = function (absoluteFee) { return function (payment) { - var dust = Bitcoin.networks.bitcoin.dustThreshold; + var dust = constants.getNetwork().dustThreshold; var usableCoins = Transaction.filterUsableCoins(payment.coins, payment.feePerKb); var max = Transaction.maxAvailableAmount(usableCoins, payment.feePerKb); @@ -490,8 +491,9 @@ function getUnspentCoins (addressList, notify) { function getKey (priv, addr) { var format = Helpers.detectPrivateKeyFormat(priv); var key = Helpers.privateKeyStringToKey(priv, format); - var ckey = new Bitcoin.ECPair(key.d, null, {compressed: true}); - var ukey = new Bitcoin.ECPair(key.d, null, {compressed: false}); + var network = constants.getNetwork(); + var ckey = new Bitcoin.ECPair(key.d, null, {compressed: true, network: network}); + var ukey = new Bitcoin.ECPair(key.d, null, {compressed: false, network: network}); if (ckey.getAddress() === addr) { return ckey; } else if (ukey.getAddress() === addr) { diff --git a/src/shared.js b/src/shared.js deleted file mode 100644 index c506c2a1f..000000000 --- a/src/shared.js +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable camelcase */ -var satoshi = 100000000; // One satoshi -var symbol_btc = {code: 'BTC', symbol: 'BTC', name: 'Bitcoin', conversion: satoshi, symbolAppearsAfter: true, local: false}; // Default BTC Currency Symbol object -var symbol_local = {'conversion': 0, 'symbol': '$', 'name': 'U.S. dollar', 'symbolAppearsAfter': false, 'local': true, 'code': 'USD'}; // Users local currency object -var symbol = symbol_btc; // Active currency object -var resource = 'Resources/'; - -module.exports = { - APP_NAME: 'javascript_web', - APP_VERSION: '3.0', - getBTCSymbol: getBTCSymbol, - getLocalSymbol: getLocalSymbol, - satoshi: satoshi, - setLocalSymbol: setLocalSymbol, - setBTCSymbol: setBTCSymbol, - playSound: playSound -}; - -function setLocalSymbol (new_symbol) { - if (!new_symbol) { - return; - } - - if (symbol === symbol_local) { - symbol_local = new_symbol; - symbol = symbol_local; - } else { - symbol_local = new_symbol; - } -} - -function getLocalSymbol () { - return symbol_local; -} - -function setBTCSymbol (new_symbol) { - if (!new_symbol) { - return; - } - - if (symbol === symbol_btc) { - symbol_btc = new_symbol; - symbol = symbol_btc; - } else { - symbol_btc = new_symbol; - } -} - -function getBTCSymbol () { - return symbol_btc; -} -// used iOS -var _sounds = {}; -function playSound (id) { - try { - if (!_sounds[id]) { - _sounds[id] = new Audio('/' + resource + id + '.wav'); - } - - _sounds[id].play(); - } catch (e) { } -} - -// Ignore Console -try { - if (!window.console) { - var names = ['log', 'debug', 'info', 'warn', 'error', 'assert', 'dir', 'dirxml', - 'group', 'groupEnd', 'time', 'timeEnd', 'count', 'trace', 'profile', 'profileEnd']; - - window.console = {}; - for (var i = 0; i < names.length; ++i) { - window.console[names[i]] = function () {}; - } - } -} catch (e) { -} -/* eslint-enable camelcase */ diff --git a/src/transaction.js b/src/transaction.js index a80e6ecf3..9f73c48fc 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -4,6 +4,7 @@ var assert = require('assert'); var Bitcoin = require('bitcoinjs-lib'); var Helpers = require('./helpers'); var Buffer = require('buffer').Buffer; +var constants = require('./constants'); // Error messages that can be seen by the user should take the form of: // {error: "NOT_GOOD", some_param: 1} @@ -15,7 +16,7 @@ var Transaction = function (payment, emitter) { var amounts = payment.amounts; var fee = payment.finalFee; var changeAddress = payment.change; - var BITCOIN_DUST = Bitcoin.networks.bitcoin.dustThreshold; + var BITCOIN_DUST = constants.getNetwork().dustThreshold; if (!Array.isArray(toAddresses) && toAddresses != null) { toAddresses = [toAddresses]; } if (!Array.isArray(amounts) && amounts != null) { amounts = [amounts]; } @@ -33,7 +34,7 @@ var Transaction = function (payment, emitter) { assert(toAddresses.length === amounts.length, 'The number of destiny addresses and destiny amounts should be the same.'); assert(this.amount >= BITCOIN_DUST, {error: 'BELOW_DUST_THRESHOLD', amount: this.amount, threshold: BITCOIN_DUST}); assert(unspentOutputs && unspentOutputs.length > 0, {error: 'NO_UNSPENT_OUTPUTS'}); - var transaction = new Bitcoin.TransactionBuilder(); + var transaction = new Bitcoin.TransactionBuilder(constants.getNetwork()); // add all outputs function addOutput (e, i) { transaction.addOutput(toAddresses[i], amounts[i]); } toAddresses.map(addOutput); @@ -49,7 +50,7 @@ var Transaction = function (payment, emitter) { // Generate address from output script and add to private list so we can check if the private keys match the inputs later var scriptBuffer = Buffer(output.script, 'hex'); assert.notEqual(Bitcoin.script.classifyOutput(scriptBuffer), 'nonstandard', {error: 'STRANGE_SCRIPT'}); - var address = Bitcoin.address.fromOutputScript(scriptBuffer).toString(); + var address = Bitcoin.address.fromOutputScript(scriptBuffer, constants.getNetwork()).toString(); assert(address, {error: 'CANNOT_DECODE_OUTPUT_ADDRESS', tx_hash: output.tx_hash}); this.addressesOfInputs.push(address); diff --git a/src/wallet-store.js b/src/wallet-store.js index 428d48ea2..bc0123942 100644 --- a/src/wallet-store.js +++ b/src/wallet-store.js @@ -17,7 +17,7 @@ var WalletStore = (function () { var counter = 0; var syncPubkeys = false; var isSynchronizedWithServer = true; - var eventListeners = []; // Emits Did decrypt wallet event (used on claim page) + var eventListeners = []; return { setPbkdf2Iterations: function (iterations) { diff --git a/src/wallet-transaction.js b/src/wallet-transaction.js index 9b7adf885..c367afc04 100644 --- a/src/wallet-transaction.js +++ b/src/wallet-transaction.js @@ -29,6 +29,18 @@ function Tx (object) { this.confirmations = Tx.setConfirmations(this.block_height); // computed properties + var initialIn = { + taggedIns: [], + fromWatchOnly: false, + totalIn: 0, + internalSpend: 0 + }; + var pins = this.inputs.reduce(procIns.bind(this), initialIn); + this.processedInputs = pins.taggedIns; + this.totalIn = pins.totalIn; + this.internalSpend = pins.internalSpend; + this.fromWatchOnly = pins.fromWatchOnly; + var initialOut = { taggedOuts: [], toWatchOnly: false, @@ -36,25 +48,13 @@ function Tx (object) { internalReceive: 0, changeAmount: 0 }; - var pouts = this.out.reduce(procOuts, initialOut); + var pouts = this.out.reduce(procOuts.bind(this), initialOut); this.processedOutputs = pouts.taggedOuts; this.toWatchOnly = pouts.toWatchOnly; this.totalOut = pouts.totalOut; this.internalReceive = pouts.internalReceive; this.changeAmount = pouts.changeAmount; - var initialIn = { - taggedIns: [], - fromWatchOnly: false, - totalIn: 0, - internalSpend: 0 - }; - var pins = this.inputs.reduce(procIns.bind(this), initialIn); - this.processedInputs = pins.taggedIns; - this.totalIn = pins.totalIn; - this.internalSpend = pins.internalSpend; - this.fromWatchOnly = pins.fromWatchOnly; - this.fee = isCoinBase(this.inputs[0]) ? 0 : this.totalIn - this.totalOut; this.result = this._result ? this._result : this.internalReceive - this.internalSpend; this.txType = computeTxType(this); @@ -96,6 +96,11 @@ function procOuts (acc, output) { acc.toWatchOnly = acc.toWatchOnly || tagOut.isWatchOnly || false; if (tagOut.coinType !== 'external') { acc.internalReceive = acc.internalReceive + tagOut.amount; + if (tagOut.coinType === 'legacy' && !tagOut.change) { + tagOut.change = this.processedInputs.some(function (input) { + return input.address === tagOut.address; + }); + } if (tagOut.change === true) { acc.changeAmount = acc.changeAmount + tagOut.amount; } @@ -189,7 +194,8 @@ function tagCoin (x) { function unpackInput (input) { if (isCoinBase(input)) { - return {addr: 'Coinbase', value: this.totalOut}; + var totalOut = this.out.reduce(function (sum, out) { return sum + out.value; }, 0); + return {addr: 'Coinbase', value: totalOut}; } else { return input.prev_out; } @@ -294,7 +300,9 @@ Tx.IOSfactory = function (tx) { to: tx.to, from: tx.from, fee: tx.fee, - note: tx.note + note: tx.note, + double_spend: tx.double_spend, + rbf: tx.rbf }; }; diff --git a/src/wallet.js b/src/wallet.js index 99aa40662..ee31f49eb 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -12,11 +12,11 @@ var API = require('./api'); var Wallet = require('./blockchain-wallet'); var Helpers = require('./helpers'); var BlockchainSocket = require('./blockchain-socket'); -var BlockchainSettingsAPI = require('./blockchain-settings-api'); var RNG = require('./rng'); var BIP39 = require('bip39'); var Bitcoin = require('bitcoinjs-lib'); var pbkdf2 = require('pbkdf2'); +var constants = require('./constants'); var isInitialized = false; MyWallet.wallet = undefined; @@ -70,7 +70,9 @@ MyWallet.getSocketOnMessage = function (message, lastOnChange) { if (lastOnChange.checksum !== newChecksum && oldChecksum !== newChecksum) { lastOnChange.checksum = newChecksum; - MyWallet.getWallet(); + MyWallet.getWallet(function () { + WalletStore.sendEvent('on_change'); + }); } } else if (obj.op === 'utx') { WalletStore.sendEvent('on_tx_received', obj.x); @@ -94,6 +96,8 @@ MyWallet.getSocketOnMessage = function (message, lastOnChange) { } else if (obj.op === 'email_verified') { MyWallet.wallet.accountInfo.isEmailVerified = Boolean(obj.x); WalletStore.sendEvent('on_email_verified', obj.x); + } else if (obj.op === 'wallet_logout') { + WalletStore.sendEvent('wallet_logout', obj.x); } }; @@ -354,7 +358,16 @@ MyWallet.initializeWallet = function (pw, decryptSuccess, buildHdSuccess) { return MyWallet.wallet.loadExternal.bind(MyWallet.wallet)().catch(loadExternalFailed); }; - return Promise.resolve().then(doInitialize).then(tryLoadExternal); + var p = Promise.resolve().then(doInitialize); + var incStats = function () { + return MyWallet.wallet.incStats.bind(MyWallet.wallet)(); + }; + var saveGUID = function () { + return MyWallet.wallet.saveGUIDtoMetadata(); + }; + p.then(incStats); + p.then(saveGUID); + return p.then(tryLoadExternal); }; // used on iOS @@ -504,11 +517,6 @@ MyWallet.createNewWallet = function (inputedEmail, inputedPassword, firstAccount var success = function (createdGuid, createdSharedKey, createdPassword, sessionToken) { if (languageCode) { WalletStore.setLanguage(languageCode); - BlockchainSettingsAPI.changeLanguage(languageCode, function () {}); - } - - if (currencyCode) { - BlockchainSettingsAPI.changeLocalCurrency(currencyCode, function () {}); } WalletStore.unsafeSetPassword(createdPassword); @@ -518,7 +526,17 @@ MyWallet.createNewWallet = function (inputedEmail, inputedPassword, firstAccount var saveWallet = function (wallet) { // Generate a session token to facilitate future login attempts: WalletNetwork.establishSession(null).then(function (sessionToken) { - WalletNetwork.insertWallet(wallet.guid, wallet.sharedKey, inputedPassword, {email: inputedEmail}, undefined, sessionToken).then(function () { + var extra = {email: inputedEmail}; + + if (languageCode) { + extra.language = languageCode; + } + + if (currencyCode) { + extra.currency = currencyCode; + } + + WalletNetwork.insertWallet(wallet.guid, wallet.sharedKey, inputedPassword, extra, undefined, sessionToken).then(function () { success(wallet.guid, wallet.sharedKey, inputedPassword, sessionToken); }).catch(function (e) { errorCallback(e); @@ -555,22 +573,24 @@ MyWallet.recoverFromMnemonic = function (inputedEmail, inputedPassword, mnemonic }; // used frontend and mywallet -MyWallet.logout = function (sessionToken, force) { +MyWallet.logout = function (force) { if (!force && WalletStore.isLogoutDisabled()) { + console.log('logout_prevented'); return; } - var reload = function () { - try { window.location.reload(); } catch (e) { - console.log(e); - } - }; - var data = { format: 'plain' }; - WalletStore.sendEvent('logging_out'); - - var headers = {sessionToken: sessionToken}; + try { + WalletStore.sendEvent('logging_out'); + window.location.reload(); + } catch (e) { + console.log(e); + } +}; - API.request('GET', 'wallet/logout', data, headers).then(reload).catch(reload); +MyWallet.endSession = function (sessionToken) { + var data = { format: 'plain' }; + var headers = { sessionToken: sessionToken }; + return API.request('GET', 'wallet/logout', data, headers); }; // In case of a non-mainstream browser, ensure it correctly implements the @@ -578,16 +598,18 @@ MyWallet.logout = function (sessionToken, force) { MyWallet.browserCheck = function () { var mnemonic = 'daughter size twenty place alter glass small bid purse october faint beyond'; var seed = BIP39.mnemonicToSeed(mnemonic, ''); - var masterkey = Bitcoin.HDNode.fromSeedBuffer(seed); + var masterkey = Bitcoin.HDNode.fromSeedBuffer(seed, constants.getNetwork()); var account = masterkey.deriveHardened(44).deriveHardened(0).deriveHardened(0); var address = account.derive(0).derive(0).getAddress(); - return address === '1QBWUDG4AFL2kFmbqoZ9y4KsSpQoCTZKRw'; + var answer = constants.NETWORK === 'testnet' ? 'n4hTmGM2yGmHXNFDZNXXnyYCJp1W8qoMw4' : '1QBWUDG4AFL2kFmbqoZ9y4KsSpQoCTZKRw'; + return address === answer; }; // Takes about 100 ms on a Macbook Pro MyWallet.browserCheckFast = function () { var mnemonic = 'daughter size twenty place alter glass small bid purse october faint beyond'; + var network = constants.getNetwork(); var seed = pbkdf2.pbkdf2Sync(mnemonic, 'mnemonic', 100, 64, 'sha512'); var seedString = seed.toString('hex'); @@ -598,11 +620,26 @@ MyWallet.browserCheckFast = function () { seed = Buffer('9f3ad67c5f1eebbffcc8314cb8a3aacbfa28046fd4b3d0af6965a8c804a603e57f5b551320eca4017267550e5b01e622978c133f2085c5999f7ef57a340d0ae2', 'hex'); + var vectors = { + 'bitcoin': { + priv: 'xprv9s21ZrQH143K44XyzPUorz65tsvifDFiWZRoqeM69iTeYXd5KbSrz4WEAbWwB2CY6jCGJ2pKdXgw66oQPePPifrpxhWuGoDkumMGCZQwduP', + pub: 'xpub682P4gW4PvBwugpzCZoKp3QMZRcj6KTakpZ5jwZXZuv1nvYkPbcT84PNVk1vSKnf1XtLRfTzuwqRH6y7T2HYKRWohWHLDpEv2sfeqPCAFkH', + address: '1MGULYKjmADKfZG6BpWwQQ3qVw622HqhCR', + pubChild: 'xpub6BQQYoWs7yyp2oNXYABTjjfmcJNJN1vHogwZ9qFdRPAfYhh5EDrBH63MHdjv5uvaawU3E3HTDGZ4SWDhwDjtnmP2S7A3EyYoQiZdFaFju5e' + }, + 'testnet': { + priv: 'tprv8ZgxMBicQKsPesmWexLK2di5D1LvtjHir7Lvi4mYdgx8L8NAJxncVosg5mgbBParUAj3J8S5ntGjYxM9WrjLXj8RVLjCw9wops6geJMEiVB', + pub: 'tpubD8Q1avKk7C9QBV1qeknR1xjitYiP5hxeJT9P6VcfupfuYDDTTpTYGW4Cjn6hxpjtoSRF3EysT3fTyHTiqt8nCry6m72duLu9cqG4bRT3wcX', + address: 'n1nRdbQiaBeaSfjhuPVKEKGAMvgiuxaDHY', + pubChild: 'tpubDBn353LYqFwGJbZNzMAYwf18wRTxMQRMMKXrWPJmmHvZHzMnJShGRXiBXfphcQspNqzwqcoKkNP78giKL5b8gCqKVhuLvWD2zgA31b1vXZE' + } + }[constants.NETWORK]; + // master node -> xpriv (1 ms) - var masterkey = Bitcoin.HDNode.fromSeedBuffer(seed); - var xpriv = masterkey.toString(); + var masterkey = Bitcoin.HDNode.fromSeedBuffer(seed, network); + var priv = masterkey.toString(); - if (xpriv !== 'xprv9s21ZrQH143K44XyzPUorz65tsvifDFiWZRoqeM69iTeYXd5KbSrz4WEAbWwB2CY6jCGJ2pKdXgw66oQPePPifrpxhWuGoDkumMGCZQwduP') { + if (priv !== vectors.priv) { return false; } @@ -619,17 +656,17 @@ MyWallet.browserCheckFast = function () { // return false; // } - var xpub = Bitcoin.HDNode.fromBase58('xpub682P4gW4PvBwugpzCZoKp3QMZRcj6KTakpZ5jwZXZuv1nvYkPbcT84PNVk1vSKnf1XtLRfTzuwqRH6y7T2HYKRWohWHLDpEv2sfeqPCAFkH'); + var pub = Bitcoin.HDNode.fromBase58(vectors.pub, network); // xpub -> address // 2 ms - if (xpub.getAddress() !== '1MGULYKjmADKfZG6BpWwQQ3qVw622HqhCR') { + if (pub.getAddress() !== vectors.address) { return false; } // xpub -> xpub' // 100 ms - var xpubChild = xpub.derive(0); + var pubChild = pub.derive(0); - if (xpubChild.toString() !== 'xpub6BQQYoWs7yyp2oNXYABTjjfmcJNJN1vHogwZ9qFdRPAfYhh5EDrBH63MHdjv5uvaawU3E3HTDGZ4SWDhwDjtnmP2S7A3EyYoQiZdFaFju5e') { + if (pubChild.toString() !== vectors.pubChild) { return false; } diff --git a/tests/_prepare_spec.js b/tests/_prepare_spec.js new file mode 100644 index 000000000..28a74d342 --- /dev/null +++ b/tests/_prepare_spec.js @@ -0,0 +1 @@ +JasminePromiseMatchers.install(); diff --git a/tests/account_info_spec.js b/tests/account_info_spec.js new file mode 100644 index 000000000..c47fa775b --- /dev/null +++ b/tests/account_info_spec.js @@ -0,0 +1,128 @@ +let proxyquire = require('proxyquireify')(require); + +let Helpers = { + isBoolean: function () { + return true; + } +}; + +let stubs = { + './helpers': Helpers +}; + +let AccountInfo = proxyquire('../src/account-info', stubs); + +describe('AccountInfo', () => { + describe('parse get-info', () => { + let o = { + btc_currency: 'BTC', + notifications_type: [32], + language: 'nl', + notifications_on: 2, + ip_lock_on: 0, + dial_code: '420', + block_tor_ips: 0, + currency: 'EUR', + notifications_confirmations: 0, + auto_email_backup: 0, + never_save_auth_type: 0, + email: 'support@blockchain.info', + sms_verified: 1, + is_api_access_enabled: 0, + auth_type: 0, + my_ip: '188.175.127.229', + email_verified: 0, + languages: { + de: 'German', + no: 'Norwegian', + en: 'English' + }, + country_code: 'CZ', + logging_level: 0, + guid: '1234', + ip_lock: '192.168.0.1', + btc_currencies: { + BTC: 'Bitcoin', + UBC: 'Bits (uBTC)', + MBC: 'MilliBit (mBTC)' + }, + sms_number: '+1 055512345', + currencies: { + ISK: 'Icelandic Króna', + HKD: 'Hong Kong Dollar', + EUR: 'Euro', + DKK: 'Danish Krone', + USD: 'U.S. dollar' + } + }; + + it('should get email', () => { + let i = new AccountInfo(o); + expect(i.email).toEqual('support@blockchain.info'); + }); + + it('should remove space and leading zero from mobile', () => { + let i = new AccountInfo(o); + expect(i.mobile).toEqual('+155512345'); + }); + + it('should split mobile into object with country and number', () => { + let i = new AccountInfo(o); + expect(i.mobileObject).toEqual({ + countryCode: '1', + number: '055512345' + }); + }); + + it('should get email verification status', () => { + let i = new AccountInfo(o); + expect(i.isEmailVerified).toEqual(false); + }); + + it('should update email verification status', () => { + o.email_verified = false; + let i = new AccountInfo(o); + i.isEmailVerified = true; + expect(i.isEmailVerified).toEqual(true); + }); + + it('should get mobile verification status', () => { + let i = new AccountInfo(o); + expect(i.isMobileVerified).toEqual(true); + }); + + it("should get the current country's dial code", () => { + let i = new AccountInfo(o); + expect(i.dialCode).toEqual('420'); + }); + + it('should get the users currency', () => { + let i = new AccountInfo(o); + expect(i.currency).toEqual('EUR'); + }); + + it('should get the notification status', () => { + let i = new AccountInfo(o); + expect(i.notifications.email).toEqual(false); + expect(i.notifications.sms).toEqual(true); + }); + + it('should have email notifications enabled', () => { + o.notifications_type = [1]; + let i = new AccountInfo(o); + expect(i.notifications.email).toEqual(true); + }); + + it('should have SMS notifications enabled', () => { + o.notifications_type = [32]; + let i = new AccountInfo(o); + expect(i.notifications.sms).toEqual(true); + }); + + it('should have HTTP notifications enabled', () => { + o.notifications_type = [4]; + let i = new AccountInfo(o); + expect(i.notifications.http).toEqual(true); + }); + }); +}); diff --git a/tests/account_info_spec.js.coffee b/tests/account_info_spec.js.coffee deleted file mode 100644 index 515239c0b..000000000 --- a/tests/account_info_spec.js.coffee +++ /dev/null @@ -1,109 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -Helpers = { - isBoolean: () -> true -} - -stubs = { - './helpers' : Helpers -} - -AccountInfo = proxyquire('../src/account-info', stubs) - -describe "AccountInfo", -> - - beforeEach -> - - describe "parse get-info", -> - # Typical result from get-info: - o = - btc_currency: "BTC" - notifications_type: [32] - language: "nl" - notifications_on: 2, - ip_lock_on:0, - dial_code:"420" - block_tor_ips:0, - currency:"EUR" - notifications_confirmations:0 - auto_email_backup:0 - never_save_auth_type:0 - email: "support@blockchain.info" - sms_verified:1 - is_api_access_enabled:0, - auth_type:0 - my_ip: "188.175.127.229" - email_verified:0 - languages: - de:"German" - no:"Norwegian" - en:"English" - country_code:"CZ" - logging_level:0 - guid:"1234" - ip_lock:"192.168.0.1", - btc_currencies: - BTC:"Bitcoin", - UBC:"Bits (uBTC)" - MBC:"MilliBit (mBTC)" - sms_number:"+1 055512345" - currencies: - ISK: "Icelandic Króna" - HKD: "Hong Kong Dollar" - EUR: "Euro" - DKK: "Danish Krone" - USD: "U.S. dollar" - - it "should get email", -> - i = new AccountInfo(o) - expect(i.email).toEqual("support@blockchain.info") - - it "should remove space and leading zero from mobile", -> - i = new AccountInfo(o) - expect(i.mobile).toEqual("+155512345") - - it "should split mobile into object with country and number", -> - i = new AccountInfo(o) - expect(i.mobileObject).toEqual({countryCode: "1", number: "055512345"}) - - it "should get email verification status", -> - i = new AccountInfo(o) - expect(i.isEmailVerified).toEqual(false) - - it "should update email verification status", -> - o.email_verified = false - i = new AccountInfo(o) - i.isEmailVerified = true - expect(i.isEmailVerified).toEqual(true) - - it "should get mobile verification status", -> - i = new AccountInfo(o) - expect(i.isMobileVerified).toEqual(true) - - it "should get the current country's dial code", -> - i = new AccountInfo(o) - expect(i.dialCode).toEqual("420") - - it "should get the users currency", -> - i = new AccountInfo(o) - expect(i.currency).toEqual("EUR") - - it "should get the notification status", -> - i = new AccountInfo(o) - expect(i.notifications.email).toEqual(false) - expect(i.notifications.sms).toEqual(true) - - it "should have email notifications enabled", -> - o.notifications_type = [1] - i = new AccountInfo(o) - expect(i.notifications.email).toEqual(true) - - it "should have SMS notifications enabled", -> - o.notifications_type = [32] - i = new AccountInfo(o) - expect(i.notifications.sms).toEqual(true) - - it "should have HTTP notifications enabled", -> - o.notifications_type = [4] - i = new AccountInfo(o) - expect(i.notifications.http).toEqual(true) diff --git a/tests/address_spec.js b/tests/address_spec.js new file mode 100644 index 000000000..0d444514e --- /dev/null +++ b/tests/address_spec.js @@ -0,0 +1,707 @@ +let Bitcoin = require('bitcoinjs-lib'); + +let proxyquire = require('proxyquireify')(require); + +let MyWallet = { + wallet: { + sharedKey: 'shared_key', + pbkdf2_iterations: 5000, + getHistory: function () {}, + syncWallet: function () {} + } +}; + +Bitcoin = { + ECPair: { + makeRandom: function (options) { + let pk; + pk = options.rng(32); + return { + getAddress: function () { + return 'random_address'; + }, + pub: {}, + d: { + toBuffer: function () { + return pk; + } + } + }; + }, + fromWIF: function (wif) { + return { + getAddress: function () { + return `pub_key_for_${wif}`; + }, + d: { + toBuffer: function () { + return `${wif}_private_key_buffer`; + } + } + }; + } + }, + message: { + sign: function (keyPair, message) { + return `${message}_signed`; + } + } +}; + +let Base58 = { + encode: function (v) { + return v; + } +}; + +let API = { + getBalances: function (l) { + let ad1; + let ad2; + let o; + ad1 = l[0]; + ad2 = l[1]; + o = {}; + if (ad1 === 'mini_2') { + o[ad1] = { + final_balance: 0 + }; + o[ad2] = { + final_balance: 10 + }; + } else { + o[ad1] = { + final_balance: 10 + }; + o[ad2] = { + final_balance: 0 + }; + } + return Promise.resolve(o); + } +}; + +let Helpers = { + isBitcoinAddress: function () { + return false; + }, + isKey: function () { + return true; + }, + isBitcoinPrivateKey: function () { + return false; + }, + privateKeyStringToKey: function (priv, format) { + return { + priv, + getAddress: function () { + return '1HaxXWGa5cZBUKNLzSWWtyDyRiYLWff8FN'; + } + }; + } +}; + +let RNG = { + run: function (input) { + if (RNG.shouldThrow) { + throw new Error('Connection failed'); + } + return '1111111111111111111111111111111H'; + } +}; + +let ImportExport = { + parseBIP38toECPair: function (b58, pass, succ, wrong, error) { + if (pass === 'correct') { + return succ('5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU'); + } else if (pass === 'wrong') { + return wrong(); + } else if (pass === 'fail') { + return error(); + } + } +}; + +let WalletCrypto = { + decryptSecretWithSecondPassword: function (data, pw) { + return `${data}_decrypted_with_${pw}`; + } +}; + +let stubs = { + './wallet': MyWallet, + './rng': RNG, + './api': API, + './import-export': ImportExport, + './wallet-crypto': WalletCrypto, + './helpers': Helpers, + 'bitcoinjs-lib': Bitcoin, + 'bs58': Base58 +}; + +let Address = proxyquire('../src/address', stubs); + +describe('Address', () => { + let object = { + 'addr': '1HaxXWGa5cZBUKNLzSWWtyDyRiYLWff8FN', + 'priv': 'GFZrKdb4tGWBWrvkjwRymnhGX8rfrWAGYadfHSJz36dF', + 'label': 'my label', + 'tag': 0, + 'created_time': 0, + 'created_device_name': 'javascript-web', + 'created_device_version': '1.0' + }; + + beforeEach(() => { + spyOn(MyWallet, 'syncWallet'); + spyOn(MyWallet.wallet, 'getHistory'); + }); + + describe('class', () => { + describe('new Address()', () => { + it('should create an empty Address with default options', () => { + let a = new Address(); + expect(a.balance).toEqual(null); + expect(a.archived).not.toBeTruthy(); + expect(a.active).toBeTruthy(); + expect(a.isWatchOnly).toBeTruthy(); + }); + + it('should transform an Object to an Address', () => { + let a = new Address(object); + expect(a.address).toEqual(object.addr); + expect(a.priv).toEqual(object.priv); + expect(a.label).toEqual(object.label); + expect(a.created_time).toEqual(object.created_time); + expect(a.created_device_name).toEqual(object.created_device_name); + expect(a.created_device_version).toEqual(object.created_device_version); + expect(a.active).toBeTruthy(); + expect(a.archived).not.toBeTruthy(); + expect(a.isWatchOnly).not.toBeTruthy(); + }); + }); + + describe('Address.new()', () => { + beforeEach(() => { + spyOn(Bitcoin.ECPair, 'makeRandom').and.callThrough(); + spyOn(RNG, 'run').and.callThrough(); + Helpers.isBitcoinAddress = () => false; + Helpers.isKey = () => true; + Helpers.isBitcoinPrivateKey = () => false; + }); + + it('should return an address', () => { + let a = Address['new']('My New Address'); + expect(a.label).toEqual('My New Address'); + }); + + it('should generate a random private key', () => { + let a = Address['new']('My New Address'); + expect(a.priv).toBe('1111111111111111111111111111111H'); + }); + + it('should generate a random address', () => { + let a = Address['new']('My New Address'); + expect(a.address).toBe('random_address'); + }); + + it('should call Bitcoin.ECPair.makeRandom with our RNG', () => { + Address.new('My New Address'); + expect(Bitcoin.ECPair.makeRandom).toHaveBeenCalled(); + expect(RNG.run).toHaveBeenCalled(); + }); + + it('should throw if RNG throws', () => { + RNG.shouldThrow = true; + expect(() => Address['new']('My New Address')).toThrow(Error('Connection failed')); + }); + }); + }); + + describe('instance', () => { + let a; + let SETTER_TYPE_ERROR = new TypeError('setting a property that has only a getter'); + + beforeEach(() => { + a = new Address(object); + }); + + describe('Setter', () => { + it('archived should archive the address and sync wallet', () => { + a.archived = true; + expect(a.archived).toBeTruthy(); + expect(a.active).not.toBeTruthy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('archived should unArchive the address and sync wallet', () => { + a.archived = false; + expect(a.archived).not.toBeTruthy(); + expect(a.active).toBeTruthy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + expect(MyWallet.wallet.getHistory).toHaveBeenCalled(); + }); + + it('archived should throw exception if is non-boolean set', () => { + let wrongSet; + wrongSet = () => { a.archived = 'failure'; }; + expect(wrongSet).toThrow(); + }); + + it('balance should be set and not sync wallet', () => { + a.balance = 100; + expect(a.balance).toEqual(100); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('balance should throw exception if is non-Number set', () => { + let wrongSet; + wrongSet = () => { a.balance = 'failure'; }; + expect(wrongSet).toThrow(); + }); + + it('label should be set and sync wallet', () => { + a.label = 'my label'; + expect(a.label).toEqual('my label'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('label should be alphanumerical', () => { + let invalid = () => { a.label = 1; }; + expect(invalid).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('label should be undefined if set to empty string', () => { + a.label = ''; + expect(a.label).toEqual(void 0); + }); + + it('totalSent must be a number', () => { + let invalid = () => { a.totalSent = '1'; }; + let valid = () => { a.totalSent = 1; }; + expect(invalid).toThrow(); + expect(a.totalSent).toEqual(null); + expect(valid).not.toThrow(); + expect(a.totalSent).toEqual(1); + }); + + it('totalReceived must be a number', () => { + let invalid = () => { a.totalReceived = '1'; }; + let valid = () => { a.totalReceived = 1; }; + expect(invalid).toThrow(); + expect(a.totalReceived).toEqual(null); + expect(valid).not.toThrow(); + expect(a.totalReceived).toEqual(1); + }); + + it('active shoud toggle archived', () => { + a.active = false; + expect(a.archived).toBeTruthy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + a.active = true; + expect(a.archived).toBeFalsy(); + }); + + it('private key is read only', () => { + expect(() => { a.priv = 'not allowed'; }).toThrow(SETTER_TYPE_ERROR); + expect(a.priv).toEqual('GFZrKdb4tGWBWrvkjwRymnhGX8rfrWAGYadfHSJz36dF'); + }); + + it('address is read only', () => { + expect(() => { a.address = 'not allowed'; }).toThrow(SETTER_TYPE_ERROR); + expect(a.address).toEqual('1HaxXWGa5cZBUKNLzSWWtyDyRiYLWff8FN'); + }); + }); + + describe('.signMessage', () => { + it('should sign a message', () => { + expect(a.signMessage('message')).toEqual('message_signed'); + }); + + it('should sign a message with the second password', () => { + a._priv = 'encpriv'; + spyOn(WalletCrypto, 'decryptSecretWithSecondPassword'); + expect(a.signMessage('message', 'secpass')).toEqual('message_signed'); + expect(WalletCrypto.decryptSecretWithSecondPassword).toHaveBeenCalledWith('encpriv', 'secpass', 'shared_key', 5000); + }); + + it('should fail when not passed a bad message', () => { + expect(a.signMessage.bind(a)).toThrow(Error('Expected message to be a string')); + }); + + it('should fail when encrypted and second pw is not provided', () => { + a._priv = 'encpriv'; + expect(a.signMessage.bind(a, 'message')).toThrow(Error('Second password needed to decrypt key')); + }); + + it('should fail when called on a watch only address', () => { + a._priv = null; + expect(a.signMessage.bind(a, 'message')).toThrow(Error('Private key needed for message signing')); + }); + + it('should convert to base64', () => { + let spy = jasmine.createSpy('toString'); + spyOn(Bitcoin.message, 'sign').and.returnValue({ + toString: spy + }); + a.signMessage('message'); + expect(spy).toHaveBeenCalledWith('base64'); + }); + + it('should try compressed format if the address does not match', () => { + let keyPair = { + getAddress () { + return 'uncomp_address'; + }, + compressed: true + }; + spyOn(Helpers, 'privateKeyStringToKey').and.returnValue(keyPair); + a.signMessage('message'); + expect(keyPair.compressed).toEqual(false); + }); + }); + + describe('.encrypt', () => { + it('should fail when encryption fails', () => { + let wrongEnc = () => a.encrypt(() => null); + expect(wrongEnc).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should write in a temporary field and let the original key intact', () => { + let originalKey = a.priv; + a.encrypt(() => 'encrypted key'); + expect(a._temporal_priv).toEqual('encrypted key'); + expect(a.priv).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if watch only address', () => { + a._priv = null; + a.encrypt(() => 'encrypted key'); + expect(a.priv).toEqual(null); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if no cipher provided', () => { + let originalKey = a.priv; + a.encrypt(void 0); + expect(a.priv).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('.decrypt', () => { + it('should fail when decryption fails', () => { + let wrongEnc = () => a.decrypt(() => null); + expect(wrongEnc).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should write in a temporary field and let the original key intact', () => { + let originalKey = a.priv; + a.decrypt(() => 'decrypted key'); + expect(a._temporal_priv).toEqual('decrypted key'); + expect(a.priv).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if watch only address', () => { + a._priv = null; + a.decrypt(() => 'decrypted key'); + expect(a.priv).toEqual(null); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if no cipher provided', () => { + let originalKey = a.priv; + a.decrypt(void 0); + expect(a.priv).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('.persist', () => { + it('should do nothing if temporary is empty', () => { + let originalKey = a.priv; + a.persist(); + expect(a.priv).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should swap and delete if we have a temporary value', () => { + a._temporal_priv = 'encrypted key'; + let temp = a._temporal_priv; + a.persist(); + expect(a.priv).toEqual(temp); + expect(a._temporal_priv).not.toBeDefined(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('JSON serializer', () => { + it('should hold: fromJSON . toJSON = id', () => { + let json = JSON.stringify(a, null, 2); + let b = JSON.parse(json, Address.reviver); + expect(a).toEqual(b); + }); + + it('should hold: fromJSON . toJSON = id for watchOnly addresses', () => { + a._priv = null; + let json = JSON.stringify(a, null, 2); + let b = JSON.parse(json, Address.reviver); + expect(a).toEqual(b); + }); + + it('should not serialize non-expected fields', () => { + a.rarefield = 'I am an intruder'; + let json = JSON.stringify(a, null, 2); + let b = JSON.parse(json); + expect(b.addr).toBeDefined(); + expect(b.priv).toBeDefined(); + expect(b.tag).toBeDefined(); + expect(b.label).toBeDefined(); + expect(b.created_time).toBeDefined(); + expect(b.created_device_name).toBeDefined(); + expect(b.created_device_version).toBeDefined(); + expect(b.rarefield).not.toBeDefined(); + expect(b._temporary_priv).not.toBeDefined(); + }); + + it('should not deserialize non-expected fields', () => { + let json = JSON.stringify(a, null, 2); + let b = JSON.parse(json); + b.rarefield = 'I am an intruder'; + let bb = new Address(b); + expect(bb).toEqual(a); + }); + }); + + describe('.fromString', () => { + beforeEach(() => { + Helpers.isBitcoinAddress = candidate => { + return candidate === 'address'; + }; + + Helpers.detectPrivateKeyFormat = candidate => { + if (candidate === 'unknown_format') { + return null; + } + if (candidate === 'bip_38') { + return 'bip38'; + } + if (candidate.indexOf('mini_') === 0) { + return 'mini'; + } + return 'sipa'; + }; + + let miniAddress = { + getAddress: function () { + return this.compressed + ? 'mini_address' + : 'mini_address_uncompressed'; + }, + compressed: true + }; + + let miniInvalid = { + getAddress: function () { + return 'mini_address'; + }, + compressed: true + }; + + let mini2 = { + getAddress: function () { + return this.compressed + ? 'mini_2' + : 'mini_2_uncompressed'; + }, + compressed: true + }; + + let validAddress = { + getAddress: function () { + return 'address'; + }, + compressed: true + }; + + Helpers.privateKeyStringToKey = (address, format) => { + if (address === 'mini_address') { + return miniAddress; + } + if (address === 'mini_2') { + return mini2; + } + if (address === 'address') { + return validAddress; + } + if (address === 'mini_invalid') { + throw miniInvalid; + } + }; + + spyOn(Address, 'import').and.callFake(address => { + if (Helpers.isString(address)) { + return { + _addr: address + }; + } + if (address) { + return { + _addr: address.getAddress() + }; + } + if (!address) { + return { + _addr: address + }; + } + }); + }); + + it('should not import unknown formats', done => { + let promise = Address.fromString('unknown_format', null, null); + expect(promise).toBeRejectedWith('unknown key format', done); + }); + + it('should not import BIP-38 format without a password', done => { + let promise = Address.fromString('bip_38', null, null, done); + expect(promise).toBeRejectedWith('needsBip38', done); + }); + + it('should not import BIP-38 format with an empty password', done => { + let promise = Address.fromString('bip_38', null, '', done); + expect(promise).toBeRejectedWith('needsBip38', done); + }); + + it('should not import BIP-38 format with a bad password', done => { + let promise = Address.fromString('bip_38', null, 'wrong', done); + expect(promise).toBeRejectedWith('wrongBipPass', done); + }); + + it('should not import BIP-38 format if the decryption fails', done => { + let promise = Address.fromString('bip_38', null, 'fail', done); + expect(promise).toBeRejectedWith('importError', done); + }); + + it('should import BIP-38 format with a correct password', done => { + let promise = Address.fromString('bip_38', null, 'correct', done); + expect(promise).toBeResolved(done); + }); + + it('should import valid addresses string', done => { + let promise = Address.fromString('address', null, null); + let match = jasmine.objectContaining({ + _addr: 'address' + }); + expect(promise).toBeResolvedWith(match, done); + }); + + it('should import private keys using mini format string', done => { + let promise = Address.fromString('mini_address', null, null); + let match = jasmine.objectContaining({ + _addr: 'mini_address' + }); + expect(promise).toBeResolvedWith(match, done); + }); + + it('should import uncompressed private keys using mini format string', done => { + let promise = Address.fromString('mini_2', null, null); + let match = jasmine.objectContaining({ + _addr: 'mini_2_uncompressed' + }); + expect(promise).toBeResolvedWith(match, done); + }); + + it('should not import private keys using an invalid mini format string', done => { + let promise; + promise = Address.fromString('mini_invalid', null, null); + expect(promise).toBeRejected(done); + }); + }); + + describe('Address import', () => { + beforeEach(() => { + Helpers.isKey = () => false; + Helpers.isBitcoinAddress = () => true; + }); + + it('should not import unknown formats', () => { + Helpers.isBitcoinAddress = () => false; + expect(() => Address['import']('abcd', null)).toThrow(); + }); + + it('should not import invalid addresses', () => { + Helpers.isBitcoinAddress = () => false; + expect(() => Address['import']('19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd66', null)).toThrow(); + }); + + it('should import WIF keys', () => { + Helpers.isBitcoinAddress = () => false; + Helpers.isBitcoinPrivateKey = () => true; + let addr = Address['import']('5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU', null); + expect(addr.address).toEqual('pub_key_for_5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU'); + }); + + it('should import valid addresses', () => { + let addr = Address['import']('19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q', null); + expect(addr.address).toEqual('19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q'); + }); + }); + + describe('Address factory', () => { + beforeEach(() => { + Helpers.isKey = () => false; + Helpers.isBitcoinAddress = () => true; + }); + + it('should not touch an already existing object', () => { + let addr = Address['import']('19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q', null); + let fromFactory = Address.factory({}, addr); + expect(fromFactory['19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q']).toEqual(addr); + }); + }); + + describe('isEncrypted', () => { + it('should be false if the address has been encrypted but not persisted', () => { + expect(a.isEncrypted).toBeFalsy(); + a.encrypt(() => 'ZW5jcnlwdGVk'); + expect(a.isEncrypted).toBeFalsy(); + }); + + it('should be true if the address has been encrypted and persisted', () => { + expect(a.isEncrypted).toBeFalsy(); + a.encrypt(() => 'ZW5jcnlwdGVk'); + a.persist(); + expect(a.isEncrypted).toBeTruthy(); + }); + }); + + describe('isUnEncrypted', () => { + it('should be false if the address has been decrypted but not persisted', () => { + expect(a.isUnEncrypted).toBeTruthy(); + expect(a.isEncrypted).toBeFalsy(); + a.encrypt(() => 'ZW5jcnlwdGVk'); + a.persist(); + expect(a.isUnEncrypted).toBeFalsy(); + a.decrypt(() => '5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU'); + expect(a.isUnEncrypted).toBeFalsy(); + }); + + it('should be true if the address has been decrypted and persisted', () => { + expect(a.isEncrypted).toBeFalsy(); + a.encrypt(() => 'ZW5jcnlwdGVk'); + a.persist(); + expect(a.isUnEncrypted).toBeFalsy(); + a.decrypt(() => 'GFZrKdb4tGWBWrvkjwRymnhGX8rfrWAGYadfHSJz36dF'); + expect(a.isUnEncrypted).toBeFalsy(); + a.persist(); + expect(a.isUnEncrypted).toBeTruthy(); + }); + }); + }); +}); diff --git a/tests/address_spec.js.coffee b/tests/address_spec.js.coffee deleted file mode 100644 index 2669f5c7d..000000000 --- a/tests/address_spec.js.coffee +++ /dev/null @@ -1,508 +0,0 @@ -Bitcoin = require('bitcoinjs-lib') - -proxyquire = require('proxyquireify')(require) -MyWallet = { - wallet: { - sharedKey: 'shared_key' - pbkdf2_iterations: 5000 - getHistory: () -> - syncWallet: () -> - } -} - -Bitcoin = { - ECPair: { - makeRandom: (options) -> - pk = options.rng(32) - { - getAddress: () -> - "random_address" - pub: { - - } - d: - toBuffer: () -> - pk - } - fromWIF: (wif) -> - { - getAddress: () -> - "pub_key_for_" + wif - d: - toBuffer: () -> - wif + "_private_key_buffer" - } - } - message: { - sign: (keyPair, message) -> message + '_signed' - } -} - -Base58 = { - encode: (v) -> - v -} - -Helpers = { - isBitcoinAddress: () -> false - isKey: () -> true - isBitcoinPrivateKey: () -> false - privateKeyStringToKey: (priv, format) -> - priv: priv - getAddress: () -> '1HaxXWGa5cZBUKNLzSWWtyDyRiYLWff8FN' -} - -RNG = { - run: (input) -> - if RNG.shouldThrow - throw new Error('Connection failed'); - "1111111111111111111111111111111H" -} - -ImportExport = - parseBIP38toECPair: (b58, pass, succ, wrong, error) -> - if pass == "correct" - succ('5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU') - else if pass == "wrong" - wrong() - else if pass == "fail" - error() - -WalletCrypto = - decryptSecretWithSecondPassword: (data, pw) -> data + '_decrypted_with_' + pw - -stubs = { - './wallet': MyWallet, - './rng' : RNG, - './import-export': ImportExport, - './wallet-crypto': WalletCrypto, - './helpers' : Helpers, - 'bitcoinjs-lib': Bitcoin, - 'bs58' : Base58 -} - -Address = proxyquire('../src/address', stubs) - -describe "Address", -> - - a = undefined - object = - "addr": "1HaxXWGa5cZBUKNLzSWWtyDyRiYLWff8FN" - "priv": "GFZrKdb4tGWBWrvkjwRymnhGX8rfrWAGYadfHSJz36dF" - "label": "my label" - "tag": 0 - "created_time": 0 - "created_device_name": "javascript-web" - "created_device_version": "1.0" - - beforeEach -> - spyOn(MyWallet, "syncWallet") - spyOn(MyWallet.wallet, "getHistory") - - describe "class", -> - describe "new Address()", -> - - it "should create an empty Address with default options", -> - a = new Address() - expect(a.balance).toEqual(null) - expect(a.archived).not.toBeTruthy() - expect(a.active).toBeTruthy() - expect(a.isWatchOnly).toBeTruthy() - - it "should transform an Object to an Address", -> - a = new Address(object) - expect(a.address).toEqual(object.addr) - expect(a.priv).toEqual(object.priv) - expect(a.label).toEqual(object.label) - expect(a.created_time).toEqual(object.created_time) - expect(a.created_device_name).toEqual(object.created_device_name) - expect(a.created_device_version).toEqual(object.created_device_version) - expect(a.active).toBeTruthy() - expect(a.archived).not.toBeTruthy() - expect(a.isWatchOnly).not.toBeTruthy() - - describe "Address.new()", -> - beforeEach -> - spyOn(Bitcoin.ECPair, "makeRandom").and.callThrough() - spyOn(RNG, "run").and.callThrough() - Helpers.isBitcoinAddress = () -> false - Helpers.isKey = () -> true - Helpers.isBitcoinPrivateKey = () -> false - - it "should return an address", -> - a = Address.new("My New Address") - expect(a.label).toEqual("My New Address") - - it "should generate a random private key", -> - a = Address.new("My New Address") - expect(a.priv).toBe("1111111111111111111111111111111H") - - it "should generate a random address", -> - a = Address.new("My New Address") - expect(a.address).toBe("random_address") - - it "should call Bitcoin.ECPair.makeRandom with our RNG", -> - a = Address.new("My New Address") - expect(Bitcoin.ECPair.makeRandom).toHaveBeenCalled() - expect(RNG.run).toHaveBeenCalled() - - it "should throw if RNG throws", -> - # E.g. because there was a network failure. - # This assumes BitcoinJS ECPair.makeRandom does not rescue a throw - # inside the RNG, which is the case in version 2.1.4 - RNG.shouldThrow = true - expect(() -> Address.new("My New Address")).toThrow(Error('Connection failed')) - - describe "instance", -> - beforeEach -> - a = new Address(object) - - describe "Setter", -> - - it "archived should archive the address and sync wallet", -> - a.archived = true - expect(a.archived).toBeTruthy() - expect(a.active).not.toBeTruthy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "archived should unArchive the address and sync wallet", -> - a.archived = false - expect(a.archived).not.toBeTruthy() - expect(a.active).toBeTruthy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - expect(MyWallet.wallet.getHistory).toHaveBeenCalled() - - it "archived should throw exception if is non-boolean set", -> - wrongSet = () -> a.archived = "failure" - expect(wrongSet).toThrow() - - it "balance should be set and not sync wallet", -> - a.balance = 100 - expect(a.balance).toEqual(100) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "balance should throw exception if is non-Number set", -> - wrongSet = () -> a.balance = "failure" - expect(wrongSet).toThrow() - - it "label should be set and sync wallet", -> - a.label = "my label" - expect(a.label).toEqual("my label") - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "label should be alphanumerical", -> - invalid = () -> - a.label = 1 - - expect(invalid).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "label should be undefined if set to empty string", -> - a.label = '' - - expect(a.label).toEqual(undefined) - - it "totalSent must be a number", -> - invalid = () -> - a.totalSent = "1" - - valid = () -> - a.totalSent = 1 - - expect(invalid).toThrow() - expect(a.totalSent).toEqual(null) - expect(valid).not.toThrow() - expect(a.totalSent).toEqual(1) - - it "totalReceived must be a number", -> - invalid = () -> - a.totalReceived = "1" - - valid = () -> - a.totalReceived = 1 - - expect(invalid).toThrow() - expect(a.totalReceived).toEqual(null) - expect(valid).not.toThrow() - expect(a.totalReceived).toEqual(1) - - it "active shoud toggle archived", -> - a.active = false - expect(a.archived).toBeTruthy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - a.active = true - expect(a.archived).toBeFalsy() - - it "private key is read only", -> - a.priv = "not allowed" - expect(a.priv).toEqual("GFZrKdb4tGWBWrvkjwRymnhGX8rfrWAGYadfHSJz36dF") - - it "address is read only", -> - a.address = "not allowed" - expect(a.address).toEqual("1HaxXWGa5cZBUKNLzSWWtyDyRiYLWff8FN") - - describe ".signMessage", -> - - it 'should sign a message', -> - expect(a.signMessage('message')).toEqual('message_signed') - - it 'should sign a message with the second password', -> - a._priv = 'encpriv' - spyOn(WalletCrypto, 'decryptSecretWithSecondPassword') - expect(a.signMessage('message', 'secpass')).toEqual('message_signed') - expect(WalletCrypto.decryptSecretWithSecondPassword).toHaveBeenCalledWith('encpriv', 'secpass', 'shared_key', 5000) - - it 'should fail when not passed a bad message', -> - expect(a.signMessage.bind(a)).toThrow(Error('Expected message to be a string')) - - it 'should fail when encrypted and second pw is not provided', -> - a._priv = 'encpriv' - expect(a.signMessage.bind(a, 'message')).toThrow(Error('Second password needed to decrypt key')) - - it 'should fail when called on a watch only address', -> - a._priv = null - expect(a.signMessage.bind(a, 'message')).toThrow(Error('Private key needed for message signing')) - - it 'should convert to base64', -> - spy = jasmine.createSpy('toString') - spyOn(Bitcoin.message, 'sign').and.returnValue({ toString: spy }) - a.signMessage('message') - expect(spy).toHaveBeenCalledWith('base64') - - it 'should try compressed format if the address does not match', -> - keyPair = { getAddress: (-> 'uncomp_address'), compressed: true } - spyOn(Helpers, 'privateKeyStringToKey').and.returnValue(keyPair) - a.signMessage('message') - expect(keyPair.compressed).toEqual(false) - - describe ".encrypt", -> - - it 'should fail when encryption fails', -> - wrongEnc = () -> a.encrypt(() -> null) - expect(wrongEnc).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should write in a temporary field and let the original key intact', -> - originalKey = a.priv - a.encrypt(() -> "encrypted key") - expect(a._temporal_priv).toEqual("encrypted key") - expect(a.priv).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if watch only address', -> - a._priv = null - a.encrypt(() -> "encrypted key") - expect(a.priv).toEqual(null) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if no cipher provided', -> - originalKey = a.priv - a.encrypt(undefined) - expect(a.priv).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe ".decrypt", -> - - it 'should fail when decryption fails', -> - wrongEnc = () -> a.decrypt(() -> null) - expect(wrongEnc).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should write in a temporary field and let the original key intact', -> - originalKey = a.priv - a.decrypt(() -> "decrypted key") - expect(a._temporal_priv).toEqual("decrypted key") - expect(a.priv).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if watch only address', -> - a._priv = null - a.decrypt(() -> "decrypted key") - expect(a.priv).toEqual(null) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if no cipher provided', -> - originalKey = a.priv - a.decrypt(undefined) - expect(a.priv).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe ".persist", -> - - it 'should do nothing if temporary is empty', -> - originalKey = a.priv - a.persist() - expect(a.priv).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should swap and delete if we have a temporary value', -> - a._temporal_priv = "encrypted key" - temp = a._temporal_priv - a.persist() - expect(a.priv).toEqual(temp) - expect(a._temporal_priv).not.toBeDefined() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe "JSON serializer", -> - - it 'should hold: fromJSON . toJSON = id', -> - json = JSON.stringify(a, null, 2) - b = JSON.parse(json, Address.reviver) - expect(a).toEqual(b) - - it 'should hold: fromJSON . toJSON = id for watchOnly addresses', -> - a._priv = null - json = JSON.stringify(a, null, 2) - b = JSON.parse(json, Address.reviver) - expect(a).toEqual(b) - - it 'should not serialize non-expected fields', -> - a.rarefield = "I am an intruder" - json = JSON.stringify(a, null, 2) - b = JSON.parse(json) - expect(b.addr).toBeDefined() - expect(b.priv).toBeDefined() - expect(b.tag).toBeDefined() - expect(b.label).toBeDefined() - expect(b.created_time).toBeDefined() - expect(b.created_device_name).toBeDefined() - expect(b.created_device_version).toBeDefined() - expect(b.rarefield).not.toBeDefined() - expect(b._temporary_priv).not.toBeDefined() - - it 'should not deserialize non-expected fields', -> - json = JSON.stringify(a, null, 2) - b = JSON.parse(json) - b.rarefield = "I am an intruder" - bb = new Address(b) - expect(bb).toEqual(a) - - describe ".fromString", -> - beforeEach -> - Helpers.isBitcoinAddress = (candidate) -> - return true if candidate == "address" - false - - Helpers.detectPrivateKeyFormat = (candidate) -> - return null if candidate == "unknown_format" - return "bip38" if candidate == "bip_38" - return "mini" if candidate.indexOf("mini_") == 0 - "sipa" - - Helpers.privateKeyStringToKey = (address, format) -> - return "mini_address" if address == "mini_address" - throw "invalid mini" if address == "mini_invalid" - - - spyOn(Address, "import").and.callFake((address) -> - { - _addr: address - } - ) - - - it "should not import unknown formats", (done) -> - promise = Address.fromString("unknown_format", null, null) - expect(promise).toBeRejectedWith('unknown key format', done) - - it "should not import BIP-38 format without a password", (done) -> - promise = Address.fromString("bip_38", null, null, done) - expect(promise).toBeRejectedWith('needsBip38', done) - - it "should not import BIP-38 format with an empty password", (done) -> - promise = Address.fromString("bip_38", null, "", done) - expect(promise).toBeRejectedWith('needsBip38', done) - - it "should not import BIP-38 format with a bad password", (done) -> - promise = Address.fromString("bip_38", null, "wrong", done) - expect(promise).toBeRejectedWith('wrongBipPass', done) - - it "should not import BIP-38 format if the decryption fails", (done) -> - promise = Address.fromString("bip_38", null, "fail", done) - expect(promise).toBeRejectedWith('importError', done) - - it "should import BIP-38 format with a correct password", (done) -> - promise = Address.fromString("bip_38", null, "correct", done) - expect(promise).toBeResolved(done) - - it "should import valid addresses string", (done) -> - promise = Address.fromString("address", null, null) - match = jasmine.objectContaining({_addr: "address"}) - expect(promise).toBeResolvedWith(match, done) - - it "should import private keys using mini format string", (done) -> - promise = Address.fromString("mini_address", null, null) - match = jasmine.objectContaining({_addr: "mini_address"}) - expect(promise).toBeResolvedWith(match, done) - - it "should not import private keys using an invalid mini format string", (done) -> - promise = Address.fromString("mini_invalid", null, null) - expect(promise).toBeRejected(done) - - describe "Address import", -> - beforeEach -> - Helpers.isKey = () -> false - Helpers.isBitcoinAddress = () -> true - - it "should not import unknown formats", -> - Helpers.isBitcoinAddress = () -> false - expect(() -> Address.import("abcd", null)).toThrow() - - it "should not import invalid addresses", -> - Helpers.isBitcoinAddress = () -> false - expect(() -> Address.import("19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd66", null)).toThrow() - - it "should import WIF keys", -> - Helpers.isBitcoinAddress = () -> false - Helpers.isBitcoinPrivateKey = () -> true - addr = Address.import("5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU", null) - expect(addr.address).toEqual("pub_key_for_5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU") - - it "should import valid addresses", -> - addr = Address.import("19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q", null) - expect(addr.address).toEqual("19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q") - - - describe "Address factory", -> - beforeEach -> - Helpers.isKey = () -> false - Helpers.isBitcoinAddress = () -> true - - it 'should not touch an already existing object', -> - addr = Address.import("19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q", null) - fromFactory = Address.factory({}, addr) - expect(fromFactory['19p7ktDbdJnmV4YLC7zQ37RsYczMZJmd6q']).toEqual(addr) - - describe "isEncrypted", -> - - it 'should be false if the address has been encrypted but not persisted', -> - expect(a.isEncrypted).toBeFalsy() - a.encrypt(() -> 'ZW5jcnlwdGVk') - expect(a.isEncrypted).toBeFalsy() - - it 'should be true if the address has been encrypted and persisted', -> - expect(a.isEncrypted).toBeFalsy() - a.encrypt(() -> 'ZW5jcnlwdGVk') - a.persist() - expect(a.isEncrypted).toBeTruthy() - - describe "isUnEncrypted", -> - - it 'should be false if the address has been decrypted but not persisted', -> - expect(a.isUnEncrypted).toBeTruthy() - expect(a.isEncrypted).toBeFalsy() - a.encrypt(() -> 'ZW5jcnlwdGVk') - a.persist() - expect(a.isUnEncrypted).toBeFalsy() - a.decrypt(() -> '5KUwyCzLyDjAvNGN4qmasFqnSimHzEYVTuHLNyME63JKfVU4wiU') - expect(a.isUnEncrypted).toBeFalsy() - - it 'should be true if the address has been decrypted and persisted', -> - expect(a.isEncrypted).toBeFalsy() - a.encrypt(() -> 'ZW5jcnlwdGVk') - a.persist() - expect(a.isUnEncrypted).toBeFalsy() - a.decrypt(() -> 'GFZrKdb4tGWBWrvkjwRymnhGX8rfrWAGYadfHSJz36dF') - expect(a.isUnEncrypted).toBeFalsy() - a.persist() - expect(a.isUnEncrypted).toBeTruthy() diff --git a/tests/api_spec.js b/tests/api_spec.js new file mode 100644 index 000000000..6921d0d75 --- /dev/null +++ b/tests/api_spec.js @@ -0,0 +1,20 @@ +let proxyquire = require('proxyquireify')(require); +let API = proxyquire('../src/api', {}); + +describe('API', () => { + describe('encodeFormData', () => { + it('should encode a flat list', () => { + let data = { foo: 'bar', alice: 'bob' }; + expect(API.encodeFormData(data)).toEqual('foo=bar&alice=bob'); + }); + + it('should encode a nested list', () => { + pending(); + let data = { + foo: 'bar', + name: { first: 'bob' } + }; + expect(API.encodeFormData(data)).toEqual('...'); + }); + }); +}); diff --git a/tests/api_spec.js.coffee b/tests/api_spec.js.coffee deleted file mode 100644 index fbdf5b79a..000000000 --- a/tests/api_spec.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -API = proxyquire('../src/api', stubs) - -describe "API", -> - describe "encodeFormData", -> - it "should encode a flat list", -> - data = { - foo: "bar", - alice: "bob" - } - expect(API.encodeFormData(data)).toEqual("foo=bar&alice=bob") - - it "should encode a nested list", -> - # Currently results in [object object] - pending() - data = { - foo: "bar", - name: { - first: "bob" - } - } - expect(API.encodeFormData(data)).toEqual("...") diff --git a/tests/bip38_spec.js b/tests/bip38_spec.js new file mode 100644 index 000000000..364aa8575 --- /dev/null +++ b/tests/bip38_spec.js @@ -0,0 +1,262 @@ + +let Bitcoin = require('bitcoinjs-lib'); +let BigInteger = require('bigi'); +let ImportExport = require('../src/import-export'); +let WalletCrypto = require('../src/wallet-crypto'); + +describe('BIP38', () => { + let observer = { + success (key) {}, + wrong_password () {}, + error (e) {} + }; + + beforeEach(() => { + window.setTimeout = myFunction => myFunction(); + localStorage.clear(); + spyOn(WalletCrypto, 'scrypt').and.callFake((password, salt, N, r, p, dkLen, callback) => { + let wrongPassword = 'WRONG_PASSWORD' + 'e957a24a' + '16384' + '8' + '8' + '64'; + let testVector1 = 'TestingOneTwoThree' + 'e957a24a' + '16384' + '8' + '8' + '64'; + let testVector2 = 'Satoshi' + '572e117e' + '16384' + '8' + '8' + '64'; + let testVector3 = 'ϓ\u0000𐐀💩' + 'f4e775a8' + '16384' + '8' + '8' + '64'; + let testVector4 = 'TestingOneTwoThree' + '43be4179' + '16384' + '8' + '8' + '64'; + let testVector5 = 'Satoshi' + '26e017d2' + '16384' + '8' + '8' + '64'; + let testVector6 = 'TestingOneTwoThree' + 'a50dba6772cb9383' + '16384' + '8' + '8' + '32'; + let testVector61 = '020eac136e97ce6bf3e2bceb65d906742f7317b6518c54c64353c43dcc36688c47' + '62b5b722a50dba6772cb9383' + '1024' + '1' + '1' + '64'; + let testVector7 = 'Satoshi' + '67010a9573418906' + '16384' + '8' + '8' + '32'; + let testVector71 = '022413a674b5bceab5abe0b14ce44dfa7fc6b55ecdbed88e7c50c0b4e953f1e05e' + '059a548167010a9573418906' + '1024' + '1' + '1' + '64'; + let testVector8 = 'MOLON LABE' + '4fca5a97' + '16384' + '8' + '8' + '32'; + let testVector81 = '02a6bf1824208903aa344833d614f7fa3ba46f4f8d57b2e219a1cfac961a9b7395' + 'bb458cef4fca5a974040f001' + '1024' + '1' + '1' + '64'; + let testVector9 = 'ΜΟΛΩΝ ΛΑΒΕ' + 'c40ea76f' + '16384' + '8' + '8' + '32'; + let testVector91 = '030a7a6f6536951f1cdf450e9ef6c1f615b904af58f7c17598cec3274e6769d3ef' + '494af136c40ea76fc501a001' + '1024' + '1' + '1' + '64'; + let CryptoScryptCache = {}; + CryptoScryptCache[wrongPassword] = Buffer('e39fc025591c26f6ebd47077b869958fedcb88df623fd6743fab116fefac0a4e1d13216d5e4294d15fd79772b8a91da612a030935ec30aa4f97c0adee73539a6', 'hex'); + CryptoScryptCache[testVector1] = Buffer('f87648a6b42fdd86ef6837a249cde15318f264d43a859b610e78ea63d51cb2d3e60bf44bfb29d543bba24afcccfadbfc6ef9312fcccf589fa5ea1366ec21e4c0', 'hex'); + CryptoScryptCache[testVector2] = Buffer('02d4a6b94240bd1cdaa6773f430e43a0d9a8cbc9a83b044998f7ef2e3f31a4de7f2436fede417c46b988879f4ef0595b75a55bcaec27848ef94e9f4b4d684cb9', 'hex'); + CryptoScryptCache[testVector3] = Buffer('981726c732b25e1eede74a32ba72fd113144c52d2eadc0f4bb12ec9ccb2e05cfc2579a6c3280d21cee2e2e6b4bf23d3b8cf2a39574b942e6f9f4381659db4c6f', 'hex'); + CryptoScryptCache[testVector4] = Buffer('731ef3c737b55df4998b44fa8a547a3f38df424da240de389b11d1875ba477672f2fe81b0532b5950e3ea6fff92c65d467aa7d054969821de2344f7a86d42569', 'hex'); + CryptoScryptCache[testVector5] = Buffer('0478e3e18d96ae2fbe033e3261944670c0ead16336890e4af46f55851ae211d22c97d288383bfd14983e5c574dafeb66f31b16bad037d40a6467019840ffa323', 'hex'); + CryptoScryptCache[testVector6] = Buffer('c8ff7a1c8c8898a0361e477fa8f0f05c00d07c5d9626f00b03c0140a307c98f4', 'hex'); + CryptoScryptCache[testVector61] = Buffer('da2d320e2ca088575369601e94dd71f210fc69c047a3d0f48bdbaab595916dc7b8d083ea2678b5a71558c0fb0efa58b565227d05adf0c25fa0b9a74755477827', 'hex'); + CryptoScryptCache[testVector7] = Buffer('e8a9722cf7988c31f929bd656085ca6470595e068bae22858ea7d84fb4197a99', 'hex'); + CryptoScryptCache[testVector71] = Buffer('dc7d942ea3c6c8953b30ee010c147a3222f6f5c52923e28185832f64d86781bc5120c42e25509460892ac9fec45e1bc52613238e1b5c1ead9d41bdeea8892c5c', 'hex'); + CryptoScryptCache[testVector8] = Buffer('7f5aee1a080d9e84f4ddf31f8b78356f472c03ac95bff320789d50137e66f279', 'hex'); + CryptoScryptCache[testVector81] = Buffer('a8bc4ad35fb69cc37f129abb458245e4523c97133b22a5cad88035f99d0b1d50ffc8317a1eaea330e1e17305539ec5c5ce36168a35d6d13fefa21d5e2cb1c1e9', 'hex'); + CryptoScryptCache[testVector9] = Buffer('147562b11c3a361365d89c1a2e5bed186c43c3c2d964632d17b2a56f96fc3110', 'hex'); + CryptoScryptCache[testVector91] = Buffer('1471d24b21c21e164f48237e9a5f0926493bf6118373a0d2e18387a7c345646d6889d2c30be9721874f10844fb98794de1caba62bb659a51492d4f33ca3237d7', 'hex'); + let keyTest = password.toString('hex') + salt.toString('hex') + N.toString() + r.toString() + p.toString() + dkLen.toString(); + let Image = CryptoScryptCache[keyTest]; + if (Image != null) { + return callback(Image); + } else { + throw new Error('Input not cached in crypto_scrypt mock function'); + } + }); + }); + + describe('parseBIP38toECPair()', () => { + beforeEach(() => { + Bitcoin.ECPair.originalFromWIF = Bitcoin.ECPair.fromWIF; + Bitcoin.OriginalECPair = Bitcoin.ECPair; + + spyOn(Bitcoin.ECPair, 'fromWIF').and.callFake(wif => { + let key; + let hex = hex = localStorage.getItem(`Bitcoin.ECPair.fromWIF ${wif}`); + if (hex) { + let buffer = new Buffer(hex, 'hex'); + key = { d: BigInteger.fromBuffer(buffer) }; + } else { + key = Bitcoin.ECPair.originalFromWIF(wif); + hex = key.d.toBuffer().toString('hex'); + localStorage.setItem(`Bitcoin.ECPair.fromWIF ${wif}`, hex); + } + return key; + }); + + spyOn(Bitcoin, 'ECPair').and.callFake((d, Q, options = {}) => { + let key; + let cacheKey = `Bitcoin.ECPair ${d.toBuffer().toString('hex')} ${options.compressed}`; + let hex = localStorage.getItem(cacheKey); + if (hex) { + console.log('Cache hit: ', hex); + key = Bitcoin.OriginalECPair.fromPublicKeyBuffer(Buffer(hex)); + key.d = d; + } else { + key = new Bitcoin.OriginalECPair(d, null, { + compressed: options.compressed + }); + hex = key.getPublicKeyBuffer().toString('hex'); + localStorage.setItem(cacheKey, hex); + } + let address = key.getAddress(); + let wif = key.toWIF(); + let pubKeyBuffer = key.getPublicKeyBuffer(); + return { + d, + getPublicKeyBuffer: function () { + return pubKeyBuffer; + }, + toWIF: function () { + return wif; + }, + getAddress: function () { + return address; + } + }; + }); + }); + + it('when called with correct password should fire success with the right params', () => { + let pw = 'TestingOneTwoThree'; + let pk = '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg'; + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let k = Bitcoin.ECPair.fromWIF('5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR'); + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + expect(WalletCrypto.scrypt).toHaveBeenCalled(); + expect(observer.success).toHaveBeenCalled(); + expect(observer.success.calls.argsFor(0)[0].d).toEqual(k.d); + expect(observer.wrong_password).not.toHaveBeenCalled(); + }); + + it('when called with wrong password should fire wrong_password', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let pw = 'WRONG_PASSWORD'; + let pk = '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + expect(observer.wrong_password).toHaveBeenCalled(); + }); + + it('(testvector1) No compression, no EC multiply, Test 1 , should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = '5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR'; + let expectedCompression = false; + let pw = 'TestingOneTwoThree'; + let pk = '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + it('(testvector2) No compression, no EC multiply, Test 2, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = '5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5'; + let expectedCompression = false; + let pw = 'Satoshi'; + let pk = '6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + xit('(testvector3) No compression, no EC multiply, Test 3, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let pw = String.fromCodePoint(0x03d2, 0x0301, 0x0000, 0x00010400, 0x0001f4a9); + let pk = '6PRW5o9FLp4gJDDVqJQKJFTpMvdsSGJxMYHtHaQBF3ooa8mwD69bapcDQn'; + let k = Bitcoin.ECPair.fromWIF('5Jajm8eQ22H3pGWLEVCXyvND8dQZhiQhoLJNKjYXk9roUFTMSZ4'); + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(observer.success.calls.argsFor(0)[0].d).toEqual(k.d); + }); + + it('(testvector4) Compression, no EC multiply, Test 1, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = 'L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP'; + let expectedCompression = true; + let pw = 'TestingOneTwoThree'; + let pk = '6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + it('(testvector5) Compression, no EC multiply, Test 2, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = 'KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7'; + let expectedCompression = true; + let pw = 'Satoshi'; + let pk = '6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + it('(testvector6) No compression, EC multiply, no lot/sequence numbers, Test 1, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = '5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2'; + let expectedCompression = false; + let pw = 'TestingOneTwoThree'; + let pk = '6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + it('(testvector7) No compression, EC multiply, no lot/sequence numbers, Test 2, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = '5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH'; + let expectedCompression = false; + let pw = 'Satoshi'; + let pk = '6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + it('(testvector8) No compression, EC multiply, lot/sequence numbers, Test 1, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = '5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8'; + let expectedCompression = false; + let pw = 'MOLON LABE'; + let pk = '6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + + it('(testvector9) No compression, EC multiply, lot/sequence numbers, Test 2, should work', () => { + spyOn(observer, 'success'); + spyOn(observer, 'wrong_password'); + let expectedWIF = '5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D'; + let expectedCompression = false; + let pw = 'ΜΟΛΩΝ ΛΑΒΕ'; + let pk = '6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH'; + ImportExport.parseBIP38toECPair(pk, pw, observer.success, observer.wrong_password); + let computedWIF = observer.success.calls.argsFor(0)[0].toWIF(); + let computedCompression = observer.success.calls.argsFor(0)[1]; + expect(observer.wrong_password).not.toHaveBeenCalled(); + expect(computedWIF).toEqual(expectedWIF); + expect(computedCompression).toEqual(expectedCompression); + }); + }); +}); diff --git a/tests/bip38_spec.js.coffee b/tests/bip38_spec.js.coffee deleted file mode 100644 index 42d0cf387..000000000 --- a/tests/bip38_spec.js.coffee +++ /dev/null @@ -1,331 +0,0 @@ -Bitcoin = require('bitcoinjs-lib') -ECPair = Bitcoin.ECPair -BigInteger = require('bigi') - -ImportExport = require('../src/import-export') -WalletCrypto = require('../src/wallet-crypto') - - -describe "BIP38", -> - - observer = - success: (key) -> - wrong_password: () -> - error: (err) -> - - beforeEach -> - # overrride as a temporary solution - window.setTimeout = (myFunction) -> myFunction() - - # TODO: figure out how to use ECPair.fromPublicKeyBuffer and reenable - # public key caching in tests - localStorage.clear() - - # mock used inside parseBIP38toECPair - spyOn(WalletCrypto, "scrypt").and.callFake( - (password, salt, N, r, p, dkLen, callback) -> - # preimages of Crypto_scrypt - wrongPassword = "WRONG_PASSWORD" + "e957a24a" + "16384" + "8" + "8" + "64" - testVector1 = "TestingOneTwoThree" + "e957a24a" + "16384" + "8" + "8" + "64" - testVector2 = "Satoshi" + "572e117e" + "16384" + "8" + "8" + "64" - # testVector3 = "ϓ␀𐐀💩" + "f4e775a8" + "16384" + "8" + "8" + "64" - testVector3 = 'ϓ\u0000𐐀💩' + "f4e775a8" + "16384" + "8" + "8" + "64" - testVector4 = "TestingOneTwoThree" + "43be4179" + "16384" + "8" + "8" + "64" - testVector5 = "Satoshi" + "26e017d2" + "16384" + "8" + "8" + "64" - testVector6 = "TestingOneTwoThree" + "a50dba6772cb9383" + "16384" + "8" + "8" + "32" - testVector61 = "020eac136e97ce6bf3e2bceb65d906742f7317b6518c54c64353c43dcc36688c47" + - "62b5b722a50dba6772cb9383" + "1024"+"1"+"1"+"64" - testVector7 = "Satoshi" + "67010a9573418906" + "16384" + "8" + "8" + "32" - testVector71 = "022413a674b5bceab5abe0b14ce44dfa7fc6b55ecdbed88e7c50c0b4e953f1e05e" + - "059a548167010a9573418906" + "1024"+"1"+"1"+"64" - testVector8 = "MOLON LABE" + "4fca5a97" + "16384" + "8" + "8" + "32" - testVector81 = "02a6bf1824208903aa344833d614f7fa3ba46f4f8d57b2e219a1cfac961a9b7395" + - "bb458cef4fca5a974040f001" + "1024"+"1"+"1"+"64" - testVector9 = "ΜΟΛΩΝ ΛΑΒΕ" + "c40ea76f" + "16384" + "8" + "8" + "32" - testVector91 = "030a7a6f6536951f1cdf450e9ef6c1f615b904af58f7c17598cec3274e6769d3ef" + - "494af136c40ea76fc501a001" + "1024"+"1"+"1"+"64" - # images of Crypto_scrypt - Crypto_scrypt_cache = {} - Crypto_scrypt_cache[wrongPassword] = - Buffer "e39fc025591c26f6ebd47077b869958fedcb88df623fd6743fab116fefac0a4e1d\ - 13216d5e4294d15fd79772b8a91da612a030935ec30aa4f97c0adee73539a6","hex" - Crypto_scrypt_cache[testVector1] = - Buffer "f87648a6b42fdd86ef6837a249cde15318f264d43a859b610e78ea63d51cb2d3e6\ - 0bf44bfb29d543bba24afcccfadbfc6ef9312fcccf589fa5ea1366ec21e4c0","hex" - Crypto_scrypt_cache[testVector2] = - Buffer "02d4a6b94240bd1cdaa6773f430e43a0d9a8cbc9a83b044998f7ef2e3f31a4de7f\ - 2436fede417c46b988879f4ef0595b75a55bcaec27848ef94e9f4b4d684cb9","hex" - # Crypto_scrypt_cache[testVector3] = - # Buffer "0e271b33f58006bbdc84850456f508cffc661f26909462995b041d31ad35ef87a1\ - # 2871a5c5e9a6b3bc8e8c3f2eb195d17e2559f38b71cba32337d742d7761f5c","hex" - Crypto_scrypt_cache[testVector3] = - Buffer "981726c732b25e1eede74a32ba72fd113144c52d2eadc0f4bb12ec9ccb2e05cfc257\ - 9a6c3280d21cee2e2e6b4bf23d3b8cf2a39574b942e6f9f4381659db4c6f","hex" - Crypto_scrypt_cache[testVector4] = - Buffer "731ef3c737b55df4998b44fa8a547a3f38df424da240de389b11d1875ba477672f\ - 2fe81b0532b5950e3ea6fff92c65d467aa7d054969821de2344f7a86d42569","hex" - Crypto_scrypt_cache[testVector5] = - Buffer "0478e3e18d96ae2fbe033e3261944670c0ead16336890e4af46f55851ae211d22c\ - 97d288383bfd14983e5c574dafeb66f31b16bad037d40a6467019840ffa323","hex" - Crypto_scrypt_cache[testVector6] = - Buffer "c8ff7a1c8c8898a0361e477fa8f0f05c00d07c5d9626f00b03c0140a307c98f4","hex" - Crypto_scrypt_cache[testVector61] = - Buffer "da2d320e2ca088575369601e94dd71f210fc69c047a3d0f48bdbaab595916dc7b8\ - d083ea2678b5a71558c0fb0efa58b565227d05adf0c25fa0b9a74755477827","hex" - Crypto_scrypt_cache[testVector7] = - Buffer "e8a9722cf7988c31f929bd656085ca6470595e068bae22858ea7d84fb4197a99","hex" - Crypto_scrypt_cache[testVector71] = - Buffer "dc7d942ea3c6c8953b30ee010c147a3222f6f5c52923e28185832f64d86781bc51\ - 20c42e25509460892ac9fec45e1bc52613238e1b5c1ead9d41bdeea8892c5c","hex" - Crypto_scrypt_cache[testVector8] = - Buffer "7f5aee1a080d9e84f4ddf31f8b78356f472c03ac95bff320789d50137e66f279","hex" - Crypto_scrypt_cache[testVector81] = - Buffer "a8bc4ad35fb69cc37f129abb458245e4523c97133b22a5cad88035f99d0b1d50ff\ - c8317a1eaea330e1e17305539ec5c5ce36168a35d6d13fefa21d5e2cb1c1e9","hex" - Crypto_scrypt_cache[testVector9] = - Buffer "147562b11c3a361365d89c1a2e5bed186c43c3c2d964632d17b2a56f96fc3110","hex" - Crypto_scrypt_cache[testVector91] = - Buffer "1471d24b21c21e164f48237e9a5f0926493bf6118373a0d2e18387a7c345646d68\ - 89d2c30be9721874f10844fb98794de1caba62bb659a51492d4f33ca3237d7","hex" - keyTest = password.toString("hex") + salt.toString("hex") + (N).toString() + - (r).toString() + (p).toString() + (dkLen).toString() - Image = Crypto_scrypt_cache[keyTest] - if Image? - callback Image - else - throw "Input not cached in crypto_scrypt mock function" - callback null - ) - - describe "parseBIP38toECPair()", -> - beforeEach -> - Bitcoin.ECPair.originalFromWIF = Bitcoin.ECPair.fromWIF - spyOn(Bitcoin.ECPair, "fromWIF").and.callFake((wif)-> - key = undefined - - if hex = localStorage.getItem("Bitcoin.ECPair.fromWIF " + wif) - buffer = new Buffer(hex, "hex") - key = { - d: BigInteger.fromBuffer(buffer) - } - else - key = Bitcoin.ECPair.originalFromWIF(wif) - hex = key.d.toBuffer().toString('hex') - localStorage.setItem("Bitcoin.ECPair.fromWIF " + wif, hex) - - key - ) - - Bitcoin.originalECPair = Bitcoin.ECPair - spyOn(Bitcoin, "ECPair").and.callFake((d, Q, options={}) -> - cacheKey = "Bitcoin.ECPair " + d.toBuffer().toString('hex') + " " + options.compressed - pubKey = undefined - if hex = localStorage.getItem(cacheKey) - console.log("Cache hit: ", hex) - key = Bitcoin.originalECPair.fromPublicKeyBuffer(Buffer(hex)) - key.d = d - else - key = new Bitcoin.originalECPair(d, null, {compressed: options.compressed}) - hex = key.getPublicKeyBuffer().toString("hex") - # console.log("Cache miss: ", hex) - localStorage.setItem(cacheKey, hex) - - address = key.getAddress() - wif = key.toWIF() - pubKeyBuffer = key.getPublicKeyBuffer() - - { - d: d - getPublicKeyBuffer: () -> pubKeyBuffer - toWIF: () -> wif - getAddress: () -> address - } - ) - - it "when called with correct password should fire success with the right params", -> - - pw = "TestingOneTwoThree" - pk = "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg" - spyOn(observer, "success") - spyOn(observer, "wrong_password") - - k = Bitcoin.ECPair - .fromWIF "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR" - - # Not needed: - # k.pub.Q._zInv = k.pub.Q.z.modInverse k.pub.Q.curve.p unless k.pub.Q._zInv? - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success, observer.wrong_password - - expect(WalletCrypto.scrypt).toHaveBeenCalled() - - # Doesn't work: - # expect(observer.success).toHaveBeenCalledWith(k) - - expect(observer.success).toHaveBeenCalled() - - expect(observer.success.calls.argsFor(0)[0].d).toEqual(k.d) - - expect(observer.wrong_password).not.toHaveBeenCalled() - - it "when called with wrong password should fire wrong_password", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - pw = "WRONG_PASSWORD" - pk = "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - - expect(observer.wrong_password).toHaveBeenCalled() - - it "(testvector1) No compression, no EC multiply, Test 1 , should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR" - expectedCompression = false; - pw = "TestingOneTwoThree" - pk = "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - it "(testvector2) No compression, no EC multiply, Test 2, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5" - expectedCompression = false; - pw = "Satoshi" - pk = "6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - xit "(testvector3) No compression, no EC multiply, Test 3, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - - pw = String.fromCodePoint(0x03d2, 0x0301, 0x0000, 0x00010400, 0x0001f4a9) - pk = "6PRW5o9FLp4gJDDVqJQKJFTpMvdsSGJxMYHtHaQBF3ooa8mwD69bapcDQn" - k = Bitcoin.ECPair - .fromWIF "5Jajm8eQ22H3pGWLEVCXyvND8dQZhiQhoLJNKjYXk9roUFTMSZ4" - # k.pub.Q._zInv = k.pub.Q.z.modInverse k.pub.Q.curve.p unless k.pub.Q._zInv? - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(observer.success.calls.argsFor(0)[0].d).toEqual(k.d) - - it "(testvector4) Compression, no EC multiply, Test 1, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP" - expectedCompression = true; - pw = "TestingOneTwoThree" - pk = "6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - it "(testvector5) Compression, no EC multiply, Test 2, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7" - expectedCompression = true; - pw = "Satoshi" - pk = "6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - it "(testvector6) No compression, EC multiply, no lot/sequence numbers, Test 1, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2" - expectedCompression = false; - pw = "TestingOneTwoThree" - pk = "6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - it "(testvector7) No compression, EC multiply, no lot/sequence numbers, Test 2, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH" - expectedCompression = false; - pw = "Satoshi" - pk = "6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - it "(testvector8) No compression, EC multiply, lot/sequence numbers, Test 1, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8" - expectedCompression = false; - pw = "MOLON LABE" - pk = "6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) - - it "(testvector9) No compression, EC multiply, lot/sequence numbers, Test 2, should work", -> - - spyOn(observer, "success") - spyOn(observer, "wrong_password") - expectedWIF = "5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D" - expectedCompression = false; - pw = "ΜΟΛΩΝ ΛΑΒΕ" - pk = "6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH" - - ImportExport.parseBIP38toECPair pk ,pw ,observer.success ,observer.wrong_password - computedWIF = observer.success.calls.argsFor(0)[0].toWIF() - computedCompression = observer.success.calls.argsFor(0)[1] - - expect(observer.wrong_password).not.toHaveBeenCalled() - expect(computedWIF).toEqual(expectedWIF) - expect(computedCompression).toEqual(expectedCompression) diff --git a/tests/blockchain_settings_api_spec.js b/tests/blockchain_settings_api_spec.js new file mode 100644 index 000000000..2ae6febf9 --- /dev/null +++ b/tests/blockchain_settings_api_spec.js @@ -0,0 +1,978 @@ +let proxyquire = require('proxyquireify')(require); + +let MyWallet = { + doubleEncrypted: false, + wallet: { + validateSecondPassword: function (pass) { + return pass === 'second password'; + }, + isDoubleEncrypted: function () { + return MyWallet.doubleEncrypted; + }, + accountInfo: { + email: 'a@b.com', + isEmailVerified: false + } + }, + syncWallet: function () {} +}; + +let API = { + callFailWithResponseText: false, + callFailWithoutResponseText: false, + securePost: function (endpoint, {method}) { + let promise = { + then: function (success, error) { + if (API.callFailWithoutResponseText) { + error('call failed'); + } else if (API.callFailWithResponseText) { + error({ responseText: 'call failed' }); + } else { + success('call succeeded'); + } + }, + 'catch': function () {} + }; + if (method === 'update-notifications-type') { + return { + then: function (success) { + if (!API.callFailWithoutResponseText && !API.callFailWithResponseText) { + success(); + } + return promise; + } + }; + } else { + return promise; + } + }, + securePostCallbacks: function (endpoint, data, success, error) { + if (API.callFailWithoutResponseText) { + error('call failed'); + } else if (API.callFailWithResponseText) { + error({ responseText: 'call failed' }); + } else { + success('call succeeded'); + } + } +}; + +let WalletStore = { + sendEvent: function () {}, + getPassword: function () { return 'password'; }, + setRealAuthType: function () {}, + setSyncPubKeys: function () {} +}; + +let stubs = { + './wallet.js': MyWallet, + './api': API, + './wallet-store.js': WalletStore +}; + +let SettingsAPI = proxyquire('../src/blockchain-settings-api', stubs); + +describe('SettingsAPI', () => { + let observers = { + success: function () {}, + error: function () {} + }; + + beforeEach(() => { + spyOn(WalletStore, 'sendEvent'); + spyOn(WalletStore, 'setRealAuthType'); + spyOn(API, 'securePost').and.callThrough(); + spyOn(API, 'securePostCallbacks').and.callThrough(); + spyOn(observers, 'success').and.callThrough(); + spyOn(observers, 'error').and.callThrough(); + }); + + describe('boolean settings', () => { + let booleanSettingsField = [ + { + func: 'updateIPlockOn', + endpoint: 'update-ip-lock-on' + }, { + func: 'toggleSave2FA', + endpoint: 'update-never-save-auth-type' + } + ]; + + booleanSettingsField.forEach(({func, endpoint}) => { + describe(`${func}`, () => { + it('should work without any callbacks', () => { + SettingsAPI[func](true); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 4, + payload: 'true', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: `${endpoint}-success: call succeeded` + }); + }); + + it('should work with callbacks', () => { + SettingsAPI[func](false, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 5, + payload: 'false', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: `${endpoint}-success: call succeeded` + }); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func](1, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 4, + payload: 'true', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: `${endpoint}-error: call failed` + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + }); + + describe('TOR block', () => { + it('should work without any callbacks', () => { + SettingsAPI.updateTorIpBlock(true); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 1, + payload: '1', + method: 'update-block-tor-ips' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-block-tor-ips-success: call succeeded' + }); + }); + + it('should work with callbacks', () => { + SettingsAPI.updateTorIpBlock(false, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 1, + payload: '0', + method: 'update-block-tor-ips' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-block-tor-ips-success: call succeeded' + }); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI.updateTorIpBlock(1, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 1, + payload: '1', + method: 'update-block-tor-ips' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'update-block-tor-ips-error: call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + }); + + describe('string settings', () => { + let stringSettingsField = [ + { + func: 'changeLanguage', + endpoint: 'update-language' + }, { + func: 'updateIPlock', + endpoint: 'update-ip-lock' + }, { + func: 'changeLocalCurrency', + endpoint: 'update-currency' + }, { + func: 'changeBtcCurrency', + endpoint: 'update-btc-currency' + }, { + func: 'changeEmail', + endpoint: 'update-email' + }, { + func: 'changeMobileNumber', + endpoint: 'update-sms' + }, { + func: 'resendEmailConfirmation', + endpoint: 'update-email' + } + ]; + + stringSettingsField.forEach(({func, endpoint}) => { + describe(`${func}`, () => { + it('should work without any callbacks', () => { + SettingsAPI[func]('string'); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 6, + payload: 'string', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: `${endpoint}-success: call succeeded` + }); + }); + + it('should work with callbacks', () => { + SettingsAPI[func]('string', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 6, + payload: 'string', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: `${endpoint}-success: call succeeded` + }); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func]('string', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 6, + payload: 'string', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: `${endpoint}-error: call failed` + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('code verification settings', () => { + let codeFields = [ + { + func: 'verifyMobile', + endpoint: 'verify-sms' + }, { + func: 'verifyEmail', + endpoint: 'verify-email-code' + } + ]; + + codeFields.forEach(({func, endpoint}) => ['asder', 'sfdsrsetertetrte'].forEach(code => { + describe(`${func} with code ${code}`, () => { + it('should work with callbacks', () => { + SettingsAPI[func](code, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: code.length, + payload: code, + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func](code, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: code.length, + payload: code, + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + })); + }); + + describe('getters', () => { + let getterFields = [ + { + func: 'getActivityLogs', + endpoint: 'list-logs', + errorMessage: 'Error Downloading Activity Logs' + }, { + func: 'getAccountInfo', + endpoint: 'get-info', + errorMessage: 'Error Downloading Account Settings' + } + ]; + + getterFields.forEach(({func, endpoint, errorMessage}) => describe(`${func}`, () => { + it('should work without callbacks', () => { + SettingsAPI[func](); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: endpoint, + format: 'json' + }, jasmine.anything(), jasmine.anything()); + }); + + it('should work with callbacks', () => { + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: endpoint, + format: 'json' + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: endpoint, + format: 'json' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: errorMessage + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + + it('should fail with error message if the API call fails with an error message', () => { + API.callFailWithResponseText = true; + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: endpoint, + format: 'json' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + })); + }); + + describe('custom settings', () => { + let customSettingsFields = [ + { + func: 'disableAllNotifications', + endpoint: 'update-notifications-type', + length: 1, + payload: 0, + errorMessage: 'Error Disabling Receive Notifications' + }, { + func: 'enableReceiveNotifications', + endpoint: 'update-notifications-on', + length: 1, + payload: 2, + errorMessage: 'Error Enabling Receive Notifications' + }, { + func: 'enableEmailNotifications', + endpoint: 'update-notifications-type', + length: 1, + payload: 1, + errorMessage: 'Error Enabling Email Notifications' + } + ]; + + customSettingsFields.forEach(setting => { + describe(`${setting.func}`, () => { + it('should work with callbacks', () => { + SettingsAPI[setting.func](observers.success, observers.error); + expect(API.securePost).toHaveBeenCalledWith('wallet', { + length: setting.length, + payload: setting.payload, + method: setting.endpoint + }); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[setting.func](observers.success, observers.error); + expect(API.securePost).toHaveBeenCalledWith('wallet', { + length: setting.length, + payload: setting.payload, + method: setting.endpoint + }); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: setting.errorMessage + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + + it('should fail with error message if the API call fails with an error message', () => { + API.callFailWithResponseText = true; + SettingsAPI[setting.func](observers.success, observers.error); + expect(API.securePost).toHaveBeenCalledWith('wallet', { + length: setting.length, + payload: setting.payload, + method: setting.endpoint + }); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('updateNotificationsType', () => { + let method = 'update-notifications-type'; + let vectors = [ + { + args: [ + { + email: false, + sms: false + } + ], + length: 1, + payload: 0 + }, { + args: [ + { + email: true, + sms: false + } + ], + length: 1, + payload: 1 + }, { + args: [ + { + email: false, + sms: true + } + ], + length: 2, + payload: 32 + }, { + args: [ + { + email: true, + sms: true + } + ], + length: 2, + payload: 33 + } + ]; + + beforeEach(() => { + spyOn(MyWallet, 'syncWallet'); + spyOn(WalletStore, 'setSyncPubKeys'); + }); + + vectors.forEach(v => + it(`should send ${v.payload} when email is ${v.email} and sms is ${v.sms}`, () => { + SettingsAPI.updateNotificationsType.apply(null, v.args); + expect(API.securePost).toHaveBeenCalledWith('wallet', { + method, + length: v.length, + payload: v.payload + }); + }) + ); + + it('should call syncWallet if successful', () => { + SettingsAPI.updateNotificationsType({}); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('should not call syncWallet if unsuccessful', () => { + API.callFailWithoutResponseText = true; + SettingsAPI.updateNotificationsType({}); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should set syncPubKeys to true if enabling notifications', () => { + SettingsAPI.updateNotificationsType({ email: true }); + expect(WalletStore.setSyncPubKeys).toHaveBeenCalledWith(true); + }); + + it('should set syncPubKeys to false if disabling notifications', () => { + SettingsAPI.updateNotificationsType({}); + expect(WalletStore.setSyncPubKeys).toHaveBeenCalledWith(false); + }); + }); + + describe('updateNotificationsOn', () => { + let method = 'update-notifications-on'; + let vectors = [ + { + args: [ + { + send: true, + receive: true + } + ], + length: 1, + payload: 0 + }, { + args: [ + { + send: true, + receive: false + } + ], + length: 1, + payload: 1 + }, { + args: [ + { + send: false, + receive: true + } + ], + length: 1, + payload: 2 + } + ]; + + vectors.forEach(v => + it(`should send ${v.payload} when email is ${v.email} and sms is ${v.sms}`, () => { + SettingsAPI.updateNotificationsOn.apply(null, v.args); + expect(API.securePost).toHaveBeenCalledWith('wallet', { + method, + length: v.length, + payload: v.payload + }); + }) + ); + }); + + describe('alias', () => + it('should remove', () => { + SettingsAPI.removeAlias(); + expect(API.securePost).toHaveBeenCalledWith('wallet', { + method: 'remove-alias', + length: 0, + payload: '' + }); + }) + ); + + describe('password hints', () => { + let passwordHintFields = [ + { + func: 'updatePasswordHint1', + endpoint: 'update-password-hint1' + }, { + func: 'updatePasswordHint2', + endpoint: 'update-password-hint2' + } + ]; + + passwordHintFields.forEach(({func, endpoint}) => + describe(`${func}`, () => { + it('should not work without error callback', () => + expect(() => SettingsAPI[func]('hint', observers.success)).toThrow() + ); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func]('hint', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 4, + payload: 'hint', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: `${endpoint}-error: call failed` + }); + }); + + it('should fail if the hint is not extended ascii', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func]('hįnt', observers.success, observers.error); + expect(API.securePostCallbacks).not.toHaveBeenCalled(); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalledWith(101); + expect(WalletStore.sendEvent).not.toHaveBeenCalled(); + }); + + it('should fail if the hint is the same as the password', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func]('password', observers.success, observers.error); + expect(API.securePostCallbacks).not.toHaveBeenCalled(); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalledWith(102); + expect(WalletStore.sendEvent).not.toHaveBeenCalled(); + }); + + it('should fail if the hint is the same as the second password', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func]('second password', observers.success, observers.error); + expect(API.securePostCallbacks).not.toHaveBeenCalled(); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalledWith(103); + expect(WalletStore.sendEvent).not.toHaveBeenCalled(); + }); + + it('should work otherwise', () => { + SettingsAPI[func]('hint', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 4, + payload: 'hint', + method: endpoint + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: `${endpoint}-success: call succeeded` + }); + }); + }) + ); + }); + + describe('auth types', () => { + let authTypeFields = [ + { + func: 'setTwoFactorEmail', + authType: 2 + }, { + func: 'unsetTwoFactor', + authType: 0 + }, { + func: 'setTwoFactorSMS', + authType: 5 + } + ]; + + authTypeFields.forEach(({func, authType}) => + describe(`${func}`, () => { + it('should work without callbacks', () => { + SettingsAPI[func](); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-auth-type', + payload: `${authType}`, + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-auth-type-success: call succeeded' + }); + expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(authType); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-auth-type', + payload: `${authType}`, + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'update-auth-type-error: call failed' + }); + }); + + it('should work with callbacks', () => { + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-auth-type', + payload: `${authType}`, + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-auth-type-success: call succeeded' + }); + expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(authType); + }); + }) + ); + }); + + describe('confirmTwoFactorGoogleAuthenticator', () => { + it('should work without callbacks', () => { + SettingsAPI.confirmTwoFactorGoogleAuthenticator('abc121'); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet?code=abc121', { + method: 'update-auth-type', + payload: '4', + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-auth-type-success: call succeeded' + }); + expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(4); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI.confirmTwoFactorGoogleAuthenticator('abc121', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet?code=abc121', { + method: 'update-auth-type', + payload: '4', + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'update-auth-type-error: call failed' + }); + }); + + it('should work with callbacks', () => { + SettingsAPI.confirmTwoFactorGoogleAuthenticator('abc121', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet?code=abc121', { + method: 'update-auth-type', + payload: '4', + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-auth-type-success: call succeeded' + }); + expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(4); + }); + }); + + describe('auth types', () => { + let authTypeFields = [ + { + func: 'setTwoFactorEmail', + authType: 2 + }, { + func: 'unsetTwoFactor', + authType: 0 + }, { + func: 'setTwoFactorSMS', + authType: 5 + } + ]; + + authTypeFields.forEach(({func, authType}) => + describe(`${func}`, () => { + it('should work without callbacks', () => { + SettingsAPI[func](); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-auth-type', + payload: `${authType}`, + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-auth-type-success: call succeeded' + }); + expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(authType); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-auth-type', + payload: `${authType}`, + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'update-auth-type-error: call failed' + }); + }); + + it('should work with callbacks', () => { + SettingsAPI[func](observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-auth-type', + payload: `${authType}`, + length: 1 + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-auth-type-success: call succeeded' + }); + expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(authType); + }); + }) + ); + }); + + describe('enableEmailReceiveNotifications', () => { + it("shouldn't work without callbacks", () => + expect(() => SettingsAPI.enableEmailReceiveNotifications()).toThrow() + ); + + it("shouldn't work without error callback", () => + expect(() => SettingsAPI.enableEmailReceiveNotifications(observers.success)).toThrow() + ); + + it('should work with both callbacks set', () => { + SettingsAPI.enableEmailReceiveNotifications(observers.success, observers.error); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + expect(API.securePost).toHaveBeenCalledTimes(2); + }); + + it('should fail if any API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI.enableEmailReceiveNotifications(observers.success, observers.error); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + expect(WalletStore.sendEvent).toHaveBeenCalledTimes(1); + }); + }); + + describe('setTwoFactorGoogleAuthenticator', () => { + it('should work without callbacks', () => { + SettingsAPI.setTwoFactorGoogleAuthenticator(); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'generate-google-secret' + }, jasmine.anything(), jasmine.anything()); + }); + + it('should work with callbacks', () => { + SettingsAPI.setTwoFactorGoogleAuthenticator(observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'generate-google-secret' + }, jasmine.anything(), jasmine.anything()); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail with error message if the API call fails with an error message', () => { + API.callFailWithResponseText = true; + SettingsAPI.setTwoFactorGoogleAuthenticator(observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'generate-google-secret' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + + describe('setTwoFactorYubiKey', () => { + it('should not work without callbacks', () => + expect(() => SettingsAPI.setTwoFactorYubiKey('cdsfe')).toThrow() + ); + + it('should work with callbacks', () => { + SettingsAPI.setTwoFactorYubiKey('cdsfd', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledTimes(2); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail with error message if the API call fails with an error message', () => { + API.callFailWithoutResponseText = true; + SettingsAPI.setTwoFactorYubiKey('cdsfd', observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + method: 'update-yubikey', + payload: 'cdsfd', + length: 5 + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'update-yubikey-error: call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + + describe('updateLoggingLevel', () => { + it('should work without any callbacks', () => { + SettingsAPI.updateLoggingLevel(0); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 1, + payload: '0', + method: 'update-logging-level' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-logging-level-success: call succeeded' + }); + }); + + it('should work with callbacks', () => { + SettingsAPI.updateLoggingLevel(1, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 1, + payload: '1', + method: 'update-logging-level' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'success', + message: 'update-logging-level-success: call succeeded' + }); + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + }); + + it('should fail if the API call fails', () => { + API.callFailWithoutResponseText = true; + SettingsAPI.updateLoggingLevel(2, observers.success, observers.error); + expect(API.securePostCallbacks).toHaveBeenCalledWith('wallet', { + length: 1, + payload: '2', + method: 'update-logging-level' + }, jasmine.anything(), jasmine.anything()); + expect(WalletStore.sendEvent).toHaveBeenCalledWith('msg', { + type: 'error', + message: 'update-logging-level-error: call failed' + }); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + }); + + afterEach(() => { + API.callFailWithoutResponseText = false; + API.callFailWithResponseText = false; + MyWallet.doubleEncrypted = false; + }); +}); diff --git a/tests/blockchain_settings_api_spec.js.coffee b/tests/blockchain_settings_api_spec.js.coffee deleted file mode 100644 index 084a51d77..000000000 --- a/tests/blockchain_settings_api_spec.js.coffee +++ /dev/null @@ -1,683 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -MyWallet = - doubleEncrypted: false - wallet: - validateSecondPassword: (pass) -> pass == "second password" - isDoubleEncrypted: () -> MyWallet.doubleEncrypted - accountInfo: - email: "a@b.com" - isEmailVerified: false - syncWallet: -> - -API = - callFailWithResponseText: false - callFailWithoutResponseText: false - securePost: (endpoint, data) -> - promise = - then: (success, error) -> - if API.callFailWithoutResponseText - error('call failed') - else if API.callFailWithResponseText - error({responseText: 'call failed'}) - else - success('call succeeded') - catch: -> - if data.method == 'update-notifications-type' - then: (success) -> - if !API.callFailWithoutResponseText && !API.callFailWithResponseText - success() - promise - else - return promise - securePostCallbacks: (endpoint, data, success, error) -> - if API.callFailWithoutResponseText - error('call failed') - else if API.callFailWithResponseText - error({responseText: 'call failed'}) - else - success('call succeeded') - -WalletStore = - sendEvent: () -> - getPassword: () -> "password" - setRealAuthType: () -> - setSyncPubKeys: -> - -AccountInfo = { - email: "a@b.com" -} - -stubs = { - './wallet.js': MyWallet, - './api': API, - './wallet-store.js': WalletStore -} - -SettingsAPI = proxyquire('../src/blockchain-settings-api', stubs) - -describe "SettingsAPI", -> - - observers = - success: () -> - error: () -> - - beforeEach -> - spyOn(WalletStore, "sendEvent") - spyOn(WalletStore, "setRealAuthType") - spyOn(API, "securePost").and.callThrough() - spyOn(API, "securePostCallbacks").and.callThrough() - spyOn(observers, "success").and.callThrough() - spyOn(observers, "error").and.callThrough() - - describe "boolean settings", -> - booleanSettingsField = [ - { - func: "updateIPlockOn", - endpoint: "update-ip-lock-on" - }, - { - func: "toggleSave2FA", - endpoint: "update-never-save-auth-type" - } - ] - - - booleanSettingsField.forEach (setting) -> - describe "#{setting.func}", -> - - it "should work without any callbacks", -> - SettingsAPI[setting.func](true) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 4, payload: 'true', method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: setting.endpoint + '-success: call succeeded'}) - - it "should work with callbacks", -> - SettingsAPI[setting.func](false, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 5, payload: 'false', method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: setting.endpoint + '-success: call succeeded'}) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func](1, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 4, payload: 'true', method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: setting.endpoint + '-error: call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "TOR block", -> - it "should work without any callbacks", -> - SettingsAPI.updateTorIpBlock(true) - - # Payload must be 0 or 1, not false or true - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 1, payload: '1', method : "update-block-tor-ips" }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-block-tor-ips-success: call succeeded'}) - - it "should work with callbacks", -> - SettingsAPI.updateTorIpBlock(false, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 1, payload: '0', method : "update-block-tor-ips" }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-block-tor-ips-success: call succeeded'}) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI.updateTorIpBlock(1, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 1, payload: '1', method : 'update-block-tor-ips' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'update-block-tor-ips-error: call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "string settings", -> - stringSettingsField = [ - { - func: "changeLanguage", - endpoint: "update-language" - }, - { - func: "updateIPlock", - endpoint: "update-ip-lock" - }, - { - func: "changeLocalCurrency", - endpoint: "update-currency" - }, - { - func: "changeBtcCurrency", - endpoint: "update-btc-currency" - }, - { - func: "changeEmail", - endpoint: "update-email" - }, - { - func: "changeMobileNumber", - endpoint: "update-sms" - }, - { - func: "resendEmailConfirmation", - endpoint: "update-email" - } - ] - - - stringSettingsField.forEach (setting) -> - describe "#{setting.func}", -> - - it "should work without any callbacks", -> - SettingsAPI[setting.func]("string") - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 6, payload: 'string', method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: setting.endpoint + '-success: call succeeded'}) - - it "should work with callbacks", -> - SettingsAPI[setting.func]("string", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 6, payload: 'string', method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: setting.endpoint + '-success: call succeeded'}) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func]("string", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 6, payload: 'string', method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: setting.endpoint + '-error: call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "code verification settings", -> - codeFields = [ - { - func: "verifyMobile", - endpoint: "verify-sms" - }, - { - func: "verifyEmail", - endpoint: "verify-email" - } - ] - - - codeFields.forEach (setting) -> - ['asder', 'sfdsrsetertetrte'].forEach (code) -> - describe "#{setting.func} with code #{code}", -> - - it "should work with callbacks", -> - SettingsAPI[setting.func](code, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: code.length, payload: code, method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func](code, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: code.length, payload: code, method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "getters", -> - getterFields = [ - { - func: "getActivityLogs", - endpoint: "list-logs", - errorMessage: 'Error Downloading Activity Logs' - }, - { - func: "getAccountInfo", - endpoint: "get-info", - errorMessage: 'Error Downloading Account Settings' - } - ] - - - getterFields.forEach (setting) -> - describe "#{setting.func}", -> - - it "should work without callbacks", -> - SettingsAPI[setting.func]() - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : setting.endpoint, format: 'json' }, jasmine.anything(), jasmine.anything()) - - it "should work with callbacks", -> - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : setting.endpoint, format: 'json' }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : setting.endpoint, format: 'json' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: setting.errorMessage}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - it "should fail with error message if the API call fails with an error message", -> - API.callFailWithResponseText = true - - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : setting.endpoint, format: 'json' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "custom settings", -> - - customSettingsFields = [ - { - func: "disableAllNotifications", - endpoint: "update-notifications-type", - length: 1, - payload: 0, - errorMessage: 'Error Disabling Receive Notifications' - }, - { - func: "enableReceiveNotifications", - endpoint: "update-notifications-on", - length: 1, - payload: 2, - errorMessage: 'Error Enabling Receive Notifications' - }, - { - func: "enableEmailNotifications", - endpoint: "update-notifications-type", - length: 1, - payload: 1, - errorMessage: 'Error Enabling Email Notifications' - } - ] - - - customSettingsFields.forEach (setting) -> - describe "#{setting.func}", -> - - it "should work with callbacks", -> - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePost).toHaveBeenCalledWith("wallet", { length: setting.length, payload: setting.payload, method : setting.endpoint }) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePost).toHaveBeenCalledWith("wallet", { length: setting.length, payload: setting.payload, method : setting.endpoint }) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: setting.errorMessage}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - it "should fail with error message if the API call fails with an error message", -> - API.callFailWithResponseText = true - - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePost).toHaveBeenCalledWith("wallet", { length: setting.length, payload: setting.payload, method : setting.endpoint }) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "updateNotificationsType", -> - method = 'update-notifications-type' - - vectors = [{ - args: [{ email: false, sms: false }] - length: 1, - payload: 0 - }, { - args: [{ email: true, sms: false }] - length: 1, - payload: 1 - }, { - args: [{ email: false, sms: true }] - length: 2, - payload: 32 - }, { - args: [{ email: true, sms: true }] - length: 2, - payload: 33 - }] - - beforeEach -> - spyOn(MyWallet, 'syncWallet') - spyOn(WalletStore, 'setSyncPubKeys') - - vectors.forEach (v) -> - it "should send #{v.payload} when email is #{v.email} and sms is #{v.sms}", -> - SettingsAPI.updateNotificationsType.apply(null, v.args) - expect(API.securePost).toHaveBeenCalledWith('wallet', { method: method, length: v.length, payload: v.payload }) - - it "should call syncWallet if successful", -> - SettingsAPI.updateNotificationsType({}) - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "should not call syncWallet if unsuccessful", -> - API.callFailWithoutResponseText = true - SettingsAPI.updateNotificationsType({}) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "should set syncPubKeys to true if enabling notifications", -> - SettingsAPI.updateNotificationsType({ email: true }) - expect(WalletStore.setSyncPubKeys).toHaveBeenCalledWith(true) - - it "should set syncPubKeys to false if disabling notifications", -> - SettingsAPI.updateNotificationsType({}) - expect(WalletStore.setSyncPubKeys).toHaveBeenCalledWith(false) - - describe "updateNotificationsOn", -> - method = 'update-notifications-on' - - vectors = [{ - args: [{ send: true, receive: true }] - length: 1, - payload: 0 - }, { - args: [{ send: true, receive: false }] - length: 1, - payload: 1 - }, { - args: [{ send: false, receive: true }] - length: 1, - payload: 2 - }] - - vectors.forEach (v) -> - it "should send #{v.payload} when email is #{v.email} and sms is #{v.sms}", -> - SettingsAPI.updateNotificationsOn.apply(null, v.args) - expect(API.securePost).toHaveBeenCalledWith('wallet', { method: method, length: v.length, payload: v.payload }) - - describe "alias", -> - - it "should remove", -> - SettingsAPI.removeAlias() - expect(API.securePost).toHaveBeenCalledWith('wallet', { method: 'remove-alias', length: 0, payload: '' }) - - describe "password hints", -> - passwordHintFields = [ - { - func: "updatePasswordHint1", - endpoint: "update-password-hint1" - }, - { - func: "updatePasswordHint2", - endpoint: "update-password-hint2" - } - ] - - passwordHintFields.forEach (setting) -> - describe "#{setting.func}", -> - - it "should not work without error callback", -> - expect(() -> SettingsAPI[setting.func]("hint", observers.success)).toThrow() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func]("hint", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 4, payload: "hint", method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: setting.endpoint + '-error: call failed'}) - - it "should fail if the hint is not extended ascii", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func]("hįnt", observers.success, observers.error) - - expect(API.securePostCallbacks).not.toHaveBeenCalled() - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalledWith(101) - expect(WalletStore.sendEvent).not.toHaveBeenCalled() - - it "should fail if the hint is the same as the password", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func]("password", observers.success, observers.error) - - expect(API.securePostCallbacks).not.toHaveBeenCalled() - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalledWith(102) - expect(WalletStore.sendEvent).not.toHaveBeenCalled() - - it "should fail if the hint is the same as the second password", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func]("second password", observers.success, observers.error) - - expect(API.securePostCallbacks).not.toHaveBeenCalled() - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalledWith(103) - expect(WalletStore.sendEvent).not.toHaveBeenCalled() - - it "should work otherwise", -> - SettingsAPI[setting.func]("hint", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 4, payload: "hint", method : setting.endpoint }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: setting.endpoint + '-success: call succeeded'}) - - describe "auth types", -> - authTypeFields = [ - { - func: "setTwoFactorEmail", - authType: 2 - }, - { - func: "unsetTwoFactor", - authType: 0 - }, - { - func: "setTwoFactorSMS", - authType: 5 - }, - ] - - authTypeFields.forEach (setting) -> - describe "#{setting.func}", -> - - it "should work without callbacks", -> - SettingsAPI[setting.func]() - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-auth-type", payload: '' + setting.authType, length: 1 }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-auth-type-success: call succeeded'}) - expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(setting.authType) - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-auth-type", payload: '' + setting.authType, length: 1 }, jasmine.anything(), jasmine.anything()) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'update-auth-type-error: call failed'}) - - it "should work with callbacks", -> - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-auth-type", payload: '' + setting.authType, length: 1 }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-auth-type-success: call succeeded'}) - expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(setting.authType) - - describe "confirmTwoFactorGoogleAuthenticator", -> - - it "should work without callbacks", -> - SettingsAPI.confirmTwoFactorGoogleAuthenticator("abc121") - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet?code=abc121", { method : "update-auth-type", payload: '4', length: 1 }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-auth-type-success: call succeeded'}) - expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(4) - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI.confirmTwoFactorGoogleAuthenticator("abc121", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet?code=abc121", { method : "update-auth-type", payload: '4', length: 1 }, jasmine.anything(), jasmine.anything()) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'update-auth-type-error: call failed'}) - - it "should work with callbacks", -> - SettingsAPI.confirmTwoFactorGoogleAuthenticator("abc121", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet?code=abc121", { method : "update-auth-type", payload: '4', length: 1 }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-auth-type-success: call succeeded'}) - expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(4) - - describe "auth types", -> - authTypeFields = [ - { - func: "setTwoFactorEmail", - authType: 2 - }, - { - func: "unsetTwoFactor", - authType: 0 - }, - { - func: "setTwoFactorSMS", - authType: 5 - }, - ] - - authTypeFields.forEach (setting) -> - describe "#{setting.func}", -> - - it "should work without callbacks", -> - SettingsAPI[setting.func]() - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-auth-type", payload: '' + setting.authType, length: 1 }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-auth-type-success: call succeeded'}) - expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(setting.authType) - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-auth-type", payload: '' + setting.authType, length: 1 }, jasmine.anything(), jasmine.anything()) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'update-auth-type-error: call failed'}) - - it "should work with callbacks", -> - SettingsAPI[setting.func](observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-auth-type", payload: '' + setting.authType, length: 1 }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-auth-type-success: call succeeded'}) - expect(WalletStore.setRealAuthType).toHaveBeenCalledWith(setting.authType) - - describe "enableEmailReceiveNotifications", -> - - it "shouldn't work without callbacks", -> - expect(() -> SettingsAPI.enableEmailReceiveNotifications()).toThrow() - - it "shouldn't work without error callback", -> - expect(() -> SettingsAPI.enableEmailReceiveNotifications(observers.success)).toThrow() - - it "should work with both callbacks set", -> - SettingsAPI.enableEmailReceiveNotifications(observers.success, observers.error) - - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - expect(API.securePost).toHaveBeenCalledTimes(2) - - it "should fail if any API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI.enableEmailReceiveNotifications(observers.success, observers.error) - - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - expect(WalletStore.sendEvent).toHaveBeenCalledTimes(1) - - describe "setTwoFactorGoogleAuthenticator", -> - - it "should work without callbacks", -> - SettingsAPI.setTwoFactorGoogleAuthenticator() - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method: 'generate-google-secret' }, jasmine.anything(), jasmine.anything()) - - it "should work with callbacks", -> - SettingsAPI.setTwoFactorGoogleAuthenticator(observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method: 'generate-google-secret' }, jasmine.anything(), jasmine.anything()) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail with error message if the API call fails with an error message", -> - API.callFailWithResponseText = true - - SettingsAPI.setTwoFactorGoogleAuthenticator(observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method: 'generate-google-secret' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "setTwoFactorYubiKey", -> - - it "should not work without callbacks", -> - expect(() -> SettingsAPI.setTwoFactorYubiKey("cdsfe")).toThrow() - - it "should work with callbacks", -> - SettingsAPI.setTwoFactorYubiKey("cdsfd", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledTimes(2) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail with error message if the API call fails with an error message", -> - API.callFailWithoutResponseText = true - - SettingsAPI.setTwoFactorYubiKey("cdsfd", observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { method : "update-yubikey", payload: 'cdsfd', length: 5 }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'update-yubikey-error: call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - describe "updateLoggingLevel", -> - - it "should work without any callbacks", -> - SettingsAPI.updateLoggingLevel(0) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 1, payload: '0', method : 'update-logging-level' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-logging-level-success: call succeeded'}) - - it "should work with callbacks", -> - SettingsAPI.updateLoggingLevel(1, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 1, payload: '1', method : 'update-logging-level' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "success", message: 'update-logging-level-success: call succeeded'}) - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - - it "should fail if the API call fails", -> - API.callFailWithoutResponseText = true - SettingsAPI.updateLoggingLevel(2, observers.success, observers.error) - - expect(API.securePostCallbacks).toHaveBeenCalledWith("wallet", { length: 1, payload: '2', method : 'update-logging-level' }, jasmine.anything(), jasmine.anything()) - expect(WalletStore.sendEvent).toHaveBeenCalledWith("msg", {type: "error", message: 'update-logging-level-error: call failed'}) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - afterEach -> - API.callFailWithoutResponseText = false - API.callFailWithResponseText = false - MyWallet.doubleEncrypted = false diff --git a/tests/blockchain_socket.js b/tests/blockchain_socket.js new file mode 100644 index 000000000..eaf57bcf9 --- /dev/null +++ b/tests/blockchain_socket.js @@ -0,0 +1,219 @@ +let proxyquire = require('proxyquireify')(require); + +describe('Websocket', () => { + let ws = (url, array, options) => ({ + on (event, callback) {}, + send (message) {}, + close () {}, + readyState: 1, + url + }); + + let Helpers = { + tor () { return false; } + }; + + let BlockchainSocket = proxyquire('../src/blockchain-socket', { + 'ws': ws, + './helpers': Helpers + }); + + describe('new', () => it('should have a URL', () => { + ws = new BlockchainSocket(); + expect(ws.wsUrl).toBeDefined(); + expect(ws.wsUrl.indexOf('wss://')).toEqual(0); + })); + + describe('instance', () => { + beforeEach(() => { + ws = new BlockchainSocket(); + }); + + describe('connect()', () => { + it('should open a socket', () => { + ws.connect(); + expect(ws.socket).toBeDefined(); + expect(ws.socket.url.indexOf('wss://')).toEqual(0); + }); + + describe('on TOR', () => { + beforeEach(() => + spyOn(Helpers, 'tor').and.returnValue(true) + ); + + it('should not open a socket', () => { + ws.connect(); + expect(ws.socket).not.toBeDefined(); + }); + }); + }); + + describe('send()', () => { + beforeEach(() => + ws.connect() + ); + + it('should pass the message on', () => { + let message = '{"op":"addr_sub", "addr": "1btc"}'; + spyOn(ws.socket, 'send'); + ws.send(message); + expect(ws.socket.send).toHaveBeenCalledWith(message); + }); + + describe('on TOR', () => { + let message = '{"op":"addr_sub", "addr": "1btc"}'; + + beforeEach(() => { + ws.socket = void 0; + spyOn(Helpers, 'tor').and.returnValue(true); + ws.connect(); + }); + + it('should not reconnect', () => { + ws.send(message); + expect(ws.socket).not.toBeDefined(); + }); + + it('should do nothing', () => + expect(() => ws.send(message)).not.toThrow() + ); + }); + }); + + describe('close()', () => { + beforeEach(() => + ws.connect() + ); + + it('should clear interval and timeout', () => { + ws.close(); + expect(ws.pingTimeoutPID).toEqual(null); + expect(ws.socket).toEqual(null); + }); + }); + + describe('ping()', () => { + beforeEach(() => + ws.connect() + ); + + it('should clear interval and timeout', () => { + spyOn(ws, 'send'); + ws.ping(); + let expected = JSON.stringify({ op: 'ping' }); + expect(ws.send).toHaveBeenCalledWith(expected); + }); + }); + + describe('msgWalletSub()', () => { + it('should subscribe to a guid', () => { + let res = ws.msgWalletSub('1234'); + let expected = JSON.stringify({ op: 'wallet_sub', guid: '1234' }); + expect(res).toEqual(expected); + }); + + it('should return an empty string if guid is missing', () => { + let res = ws.msgWalletSub(null); + let expected = ''; + expect(res).toEqual(expected); + }); + }); + + describe('msgBlockSub()', () => + it('should subscribe to new blocks', () => { + let res = ws.msgBlockSub(); + let expected = JSON.stringify({ op: 'blocks_sub' }); + expect(res).toEqual(expected); + }) + ); + + describe('msgAddrSub()', () => { + it('should return an empty string if addresses are missing', () => { + let res = ws.msgAddrSub(null); + let expected = ''; + expect(res).toEqual(expected); + }); + + it('should subscribe to one adddress', () => { + let res = ws.msgAddrSub('1abc'); + let expected = JSON.stringify({ op: 'addr_sub', addr: '1abc' }); + expect(res).toEqual(expected); + }); + + it('should subscribe to array of adddresses', () => { + let res = ws.msgAddrSub(['1abc', '1def']); + let expected = JSON.stringify({ + op: 'addr_sub', + addr: '1abc' + }) + JSON.stringify({ + op: 'addr_sub', + addr: '1def' + }); + expect(res).toEqual(expected); + }); + }); + + describe('msgXPUBSub()', () => { + it('should return an empty string if xpub is missing', () => { + let res = ws.msgXPUBSub(null); + let expected = ''; + expect(res).toEqual(expected); + }); + + it('should return an empty string if xpub is []', () => { + let res = ws.msgXPUBSub([]); + let expected = ''; + expect(res).toEqual(expected); + }); + + it('should subscribe to one xpub', () => { + let res = ws.msgXPUBSub('1abc'); + let expected = JSON.stringify({ op: 'xpub_sub', xpub: '1abc' }); + expect(res).toEqual(expected); + }); + + it('should subscribe to array of adddresses', () => { + let res = ws.msgXPUBSub(['1abc', '1def']); + let expected = JSON.stringify({ + op: 'xpub_sub', + xpub: '1abc' + }) + JSON.stringify({ + op: 'xpub_sub', + xpub: '1def' + }); + expect(res).toEqual(expected); + }); + }); + + describe('msgPing()', () => + it('should ping', () => { + let res = ws.msgPing(); + let expected = JSON.stringify({ op: 'ping' }); + expect(res).toEqual(expected); + }) + ); + + describe('msgOnOpen()', () => it('should subscribe to blocks, guid, addresses and xpubs', () => { + let guid = '1234'; + let addresses = ['123a', '1bcd']; + let xpubs = '1eff'; + let res = ws.msgOnOpen(guid, addresses, xpubs); + let expected = JSON.stringify({ + op: 'blocks_sub' + }) + JSON.stringify({ + op: 'wallet_sub', + guid: '1234' + }) + JSON.stringify({ + op: 'addr_sub', + addr: '123a' + }) + JSON.stringify({ + op: 'addr_sub', + addr: '1bcd' + }) + JSON.stringify({ + op: 'xpub_sub', + xpub: '1eff' + }); + expect(res).toEqual(expected); + })); + }); +}); diff --git a/tests/blockchain_socket.js.coffee b/tests/blockchain_socket.js.coffee deleted file mode 100644 index ae4363856..000000000 --- a/tests/blockchain_socket.js.coffee +++ /dev/null @@ -1,169 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -describe "Websocket", -> - - ws = (url, array, options) -> - { - on: (event, callback) -> - send: (message) -> - close: () -> - readyState: 1 - url: url - } - - Helpers = { - tor: () -> false - } - - BlockchainSocket = proxyquire('../src/blockchain-socket', { - 'ws': ws, - './helpers': Helpers - }) - - describe "new", -> - - it "should have a URL", -> - ws = new BlockchainSocket() - expect(ws.wsUrl).toBeDefined() - expect(ws.wsUrl.indexOf("wss://")).toEqual(0) - - describe "instance", -> - ws = undefined - - beforeEach -> - ws = new BlockchainSocket() - - describe "connect()", -> - it "should open a socket", -> - ws.connect() - expect(ws.socket).toBeDefined() - # The mock websocket has a URL method: - expect(ws.socket.url.indexOf("wss://")).toEqual(0) - - describe "on TOR", -> - beforeEach -> - spyOn(Helpers, "tor").and.returnValue true - - it "should not open a socket", -> - ws.connect() - expect(ws.socket).not.toBeDefined() - - describe "send()", -> - beforeEach -> - ws.connect() - - it "should pass the message on", -> - message = '{"op":"addr_sub", "addr": "1btc"}' - spyOn(ws.socket, "send") - ws.send(message) - expect(ws.socket.send).toHaveBeenCalledWith(message) - - describe "on TOR", -> - message = '{"op":"addr_sub", "addr": "1btc"}' - - beforeEach -> - ws.socket = undefined - spyOn(Helpers, "tor").and.returnValue true - ws.connect() - - it "should not reconnect", -> - ws.send(message) - expect(ws.socket).not.toBeDefined() - - it "should do nothing", -> - # ws.socket is not defined, so nothing to spy on - expect(() -> ws.send(message)).not.toThrow() - - describe "close()", -> - beforeEach -> - ws.connect() - - it "should clear interval and timeout", -> - ws.close() - expect(ws.pingTimeoutPID).toEqual(null) - expect(ws.socket).toEqual(null) - - describe "ping()", -> - beforeEach -> - ws.connect() - - it "should clear interval and timeout", -> - spyOn(ws, "send") - ws.ping() - expected = JSON.stringify({op: "ping"}) - expect(ws.send).toHaveBeenCalledWith(expected) - - describe "msgWalletSub()", -> - it "should subscribe to a guid", -> - res = ws.msgWalletSub("1234") - expected = JSON.stringify({op: "wallet_sub", guid: "1234"}) - expect(res).toEqual(expected) - - it "should return an empty string if guid is missing", -> - res = ws.msgWalletSub(null) - expected = "" - expect(res).toEqual(expected) - - describe "msgBlockSub()", -> - it "should subscribe to new blocks", -> - res = ws.msgBlockSub() - expected = JSON.stringify({op: "blocks_sub"}) - expect(res).toEqual(expected) - - describe "msgAddrSub()", -> - it "should return an empty string if addresses are missing", -> - res = ws.msgAddrSub(null) - expected = "" - expect(res).toEqual(expected) - - it "should subscribe to one adddress", -> - res = ws.msgAddrSub("1abc") - expected = JSON.stringify({op: "addr_sub", addr: "1abc"}) - expect(res).toEqual(expected) - - it "should subscribe to array of adddresses", -> - res = ws.msgAddrSub(["1abc", "1def"]) - expected = JSON.stringify({op: "addr_sub", addr: "1abc"}) + - JSON.stringify({op: "addr_sub", addr: "1def"}) - expect(res).toEqual(expected) - - describe "msgXPUBSub()", -> - it "should return an empty string if xpub is missing", -> - res = ws.msgXPUBSub(null) - expected = "" - expect(res).toEqual(expected) - - it "should return an empty string if xpub is []", -> - res = ws.msgXPUBSub([]) - expected = "" - expect(res).toEqual(expected) - - it "should subscribe to one xpub", -> - res = ws.msgXPUBSub("1abc") - expected = JSON.stringify({op: "xpub_sub", xpub: "1abc"}) - expect(res).toEqual(expected) - - it "should subscribe to array of adddresses", -> - res = ws.msgXPUBSub(["1abc", "1def"]) - expected = JSON.stringify({op: "xpub_sub", xpub: "1abc"}) + - JSON.stringify({op: "xpub_sub", xpub: "1def"}) - expect(res).toEqual(expected) - - describe "msgPing()", -> - it "should ping", -> - res = ws.msgPing() - expected = JSON.stringify({op: "ping"}) - expect(res).toEqual(expected) - - describe "msgOnOpen()", -> - it "should subscribe to blocks, guid, addresses and xpubs", -> - guid = "1234" - addresses = ["123a", "1bcd"] - xpubs = "1eff" - res = ws.msgOnOpen(guid, addresses, xpubs) - expected = JSON.stringify({op: "blocks_sub"}) + - JSON.stringify({op: "wallet_sub", guid: "1234"}) + - JSON.stringify({op: "addr_sub", addr: "123a"}) + - JSON.stringify({op: "addr_sub", addr: "1bcd"}) + - JSON.stringify({op: "xpub_sub", xpub: "1eff"}) - expect(res).toEqual(expected) diff --git a/tests/blockchain_wallet_spec.js b/tests/blockchain_wallet_spec.js new file mode 100644 index 000000000..8b94d09cd --- /dev/null +++ b/tests/blockchain_wallet_spec.js @@ -0,0 +1,912 @@ +let proxyquire = require('proxyquireify')(require); +let MyWallet; +let Address; +let Wallet; +let HDWallet; +let WalletStore; +let BlockchainSettingsAPI; +let BIP39; +let RNG; + +describe('Blockchain-Wallet', () => { + let wallet; + let object = { + 'guid': 'c8d9fe67-2ba0-4c15-a2be-0d17981d3c0a', + 'sharedKey': '981b98e8-03f5-48fa-b369-038e2a7fdc09', + 'double_encryption': false, + 'options': { + 'pbkdf2_iterations': 5000, + 'fee_per_kb': 10000, + 'html5_notifications': false, + 'logout_time': 600000 + }, + 'address_book': [{'address': '1dice8EMZmqKvrGE4Qc9bUFf9PX3xaYDp', 'label': 'SatoshiDice'}], + 'tx_notes': {}, + 'tx_names': [], + 'keys': [ + { + 'addr': '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv', + 'priv': 'HUFhy1SvLBzzdAYpwD3quUN9kxqmm9U3Y1ZDdwBhHjPH', + 'tag': 0, + 'created_time': 1437494028974, + 'created_device_name': 'javascript_web', + 'created_device_version': '1.0' + }, { + 'addr': '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9', + 'priv': null + }, + { + 'addr': '1H8Cwvr3Vq9rJBGEoudG1AeyeAezr38j8h', + 'priv': '5KHY1QhUx8BYrdZPV6GcRw5rVKyAHbjZxz9KLYkaoL16JuFBZv8', + 'tag': 2, + 'created_time': 1437494028974, + 'created_device_name': 'javascript_web', + 'created_device_version': '1.0' + } + ], + 'hd_wallets': [ { + 'seed_hex': '7e061ca8e579e5e70e9989ca40d342fe', + 'passphrase': '', + 'mnemonic_verified': false, + 'default_account_idx': 0, + 'accounts': [ { + 'label': 'My Bitcoin Wallet', + 'archived': false, + 'xpriv': 'xprv9yko4kDvhYSdUcqK5e8naLwtGE1Ca57mwJ6JMB8WxeYq8t1w3PpiZfGGvLN6N6GEwLF8XuHnp8HeNLrWWviAjXxb2BFEiLaW2UgukMZ3Zva', + 'xpub': 'xpub6Ck9UFkpXuzvh6unBffnwUtcpFqgyXqdJX1u9ZY8Wz5p1gM5aw8y7TakmcEWLA9rJkc59BJzn61p3qqKSaqFkSPMbbhGA9YDNmphj9SKBVJ', + 'address_labels': [{'index': 170, 'label': 'Utilities'}], + 'cache': { + 'receiveAccount': 'xpub6FD59hfbH1UWQA9B8NP1C8bh3jc6i2tpM6b8f4Wi9gHWQttZbBBtEsDDZAiPsw7e3427SzvQsFu2sdubjbZHDQdqYXN6x3hTDCrG5bZFEhB', + 'changeAccount': 'xpub6FD59hfbH1UWRrY38bVLPPLPLxcA1XBqsQgB95AgsSWngxbwqPBMd5Z3of8PNicLwE9peQ9g4SeWWtBTzUKLwfjSioAg73RRh7dJ5rWYxM7' + } + } ] + } ] + }; + + beforeEach(() => { + MyWallet = { + syncWallet (success, error) { + if (success) { + return success(); + } + }, + get_history () {} + }; + + Address = { + new (label) { + if (Address.shouldThrow) { + throw new Error(''); + } + let addr = { + label, + encrypt () { + return { + persist () {} + }; + } + }; + spyOn(addr, 'encrypt').and.callThrough(); + return addr; + } + }; + + HDWallet = { + new (cipher) { + if (HDWallet.shouldThrow) { + throw new Error(''); + } + return { + newAccount () {} + }; + } + }; + + let Helpers = { + isInstanceOf (candidate, theClass) { + return (candidate.label !== undefined) || (typeof (candidate) === 'object'); + } + }; + + let walletStoreTxs = []; + WalletStore = { + pushTransaction (tx) { return walletStoreTxs.push(tx); }, + getTransactions () { return walletStoreTxs; }, + getTransaction (hash) { + return walletStoreTxs.filter(tx => tx.hash === hash)[0]; + }, + setSyncPubKeys (boolean) {} + }; + + BlockchainSettingsAPI = { + shouldFail: false, + enableEmailReceiveNotifications (success, error) { + if (BlockchainSettingsAPI.shouldFail) { + return error(); + } else { + return success(); + } + }, + + disableAllNotifications (success, error) { + if (BlockchainSettingsAPI.shouldFail) { + return error(); + } else { + return success(); + } + } + + }; + + BIP39 = { + generateMnemonic (str, rng, wlist) { + let mnemonic = 'bicycle balcony prefer kid flower pole goose crouch century lady worry flavor'; + let seed = rng(32); + return seed === 'random' ? mnemonic : 'failure'; + } + }; + RNG = { + run (input) { + if (RNG.shouldThrow) { + throw new Error('Connection failed'); + } + return 'random'; + } + }; + + let stubs = { + './wallet': MyWallet, + './address': Address, + './helpers': Helpers, + './hd-wallet': HDWallet, + './wallet-store': WalletStore, + './blockchain-settings-api': BlockchainSettingsAPI, + 'bip39': BIP39, + './rng': RNG + }; + + Wallet = proxyquire('../src/blockchain-wallet', stubs); + + spyOn(MyWallet, 'syncWallet').and.callThrough(); + spyOn(MyWallet, 'get_history').and.callThrough(); + }); + + describe('Constructor', () => { + it('should create an empty Wallet with default options', () => { + wallet = new Wallet(); + expect(wallet.double_encryption).toBeFalsy(); + expect(wallet._totalSent).toEqual(0); + expect(wallet._totalReceived).toEqual(0); + expect(wallet._finalBalance).toEqual(0); + }); + + it('should transform an Object to a Wallet', () => { + Wallet = proxyquire('../src/blockchain-wallet', {}); + wallet = new Wallet(object); + + expect(wallet._guid).toEqual(object.guid); + expect(wallet._sharedKey).toEqual(object.sharedKey); + expect(wallet._double_encryption).toEqual(object.double_encryption); + expect(wallet._dpasswordhash).toEqual(object.dpasswordhash); + expect(wallet._pbkdf2_iterations).toEqual(object.options.pbkdf2_iterations); + expect(wallet._logout_time).toEqual(object.options.logout_time); + expect(wallet._address_book['1dice8EMZmqKvrGE4Qc9bUFf9PX3xaYDp']).toEqual('SatoshiDice'); + }); + }); + + describe('instance', () => { + beforeEach(() => { + wallet = new Wallet(object); + }); + + describe('Setter', () => { + it('guid is read only', () => { + expect(() => { wallet.guid = 'not allowed'; }).toThrow(); + expect(wallet.guid).not.toEqual('not allowed'); + }); + + it('sharedKey is read only', () => { + expect(() => { wallet.sharedKey = 'not allowed'; }).toThrow(); + expect(wallet.sharedKey).not.toEqual('not allowed'); + }); + + it('isDoubleEncrypted is read only', () => { + expect(() => { wallet.isDoubleEncrypted = 'not allowed'; }).toThrow(); + expect(wallet.isDoubleEncrypted).not.toEqual('not allowed'); + }); + + it('dpasswordhash is read only', () => { + expect(() => { wallet.dpasswordhash = 'not allowed'; }).toThrow(); + expect(wallet.dpasswordhash).not.toEqual('not allowed'); + }); + + it('fee_per_kb should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.fee_per_kb = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('fee_per_kb should throw expection if set to high', () => { + let invalid = () => { wallet.fee_per_kb = 100000000; }; + expect(invalid).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('fee_per_kb should be set to the value sent', () => { + let invalid = () => { wallet.fee_per_kb = 10000; }; + expect(invalid).not.toThrow(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + expect(wallet.fee_per_kb).toEqual(10000); + }); + + it('pbkdf2_iterations is read only', () => { + expect(() => { wallet.pbkdf2_iterations = 'not allowed'; }).toThrow(); + expect(wallet.pbkdf2_iterations).not.toEqual('not allowed'); + }); + + it('totalSent should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.totalSent = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('totalReceived should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.totalReceived = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('finalBalance should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.finalBalance = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('numberTxTotal should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.numberTxTotal = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('addresses is read only', () => { + expect(() => { wallet.addresses = 'not allowed'; }).toThrow(); + expect(wallet.addresses).not.toEqual('not allowed'); + }); + + it('activeAddresses is read only', () => { + expect(() => { wallet.activeAddresses = 'not allowed'; }).toThrow(); + expect(wallet.activeAddresses).not.toEqual('not allowed'); + }); + + it('key is read only', () => { + expect(() => { wallet.key = 'not allowed'; }).toThrow(); + expect(wallet.key).not.toEqual('not allowed'); + }); + + it('activeKey is read only', () => { + expect(() => { wallet.activeKey = 'not allowed'; }).toThrow(); + expect(wallet.activeKey).not.toEqual('not allowed'); + }); + + it('keys is read only', () => { + expect(() => { wallet.keys = 'not allowed'; }).toThrow(); + expect(wallet.keys).not.toEqual('not allowed'); + }); + + it('activeKeys is read only', () => { + expect(() => { wallet.activeKeys = 'not allowed'; }).toThrow(); + expect(wallet.activeKeys).not.toEqual('not allowed'); + }); + + it('hdwallet is read only', () => { + expect(() => { wallet.hdwallet = 'not allowed'; }).toThrow(); + expect(wallet.hdwallet).not.toEqual('not allowed'); + }); + + it('isUpgradedToHD is read only', () => { + expect(() => { wallet.isUpgradedToHD = 'not allowed'; }).toThrow(); + expect(wallet.isUpgradedToHD).not.toEqual('not allowed'); + }); + + it('balanceActiveLegacy is read only', () => { + expect(() => { wallet.balanceActiveLegacy = 'not allowed'; }).toThrow(); + expect(wallet.balanceActiveLegacy).not.toEqual('not allowed'); + }); + + it('addressBook is read only', () => { + expect(() => { wallet.addressBook = 'not allowed'; }).toThrow(); + expect(wallet.addressBook).not.toEqual('not allowed'); + }); + + it('logoutTime should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.logoutTime = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('logoutTime should throw exception if is out of range set', () => { + let wrongSet = () => { wallet.logoutTime = 59000; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('logoutTime should throw exception if is out of range set', () => { + let wrongSet = () => { wallet.logoutTime = 86400002; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('logoutTime should be set and sync', () => { + wallet.logoutTime = 100000; + expect(wallet.logoutTime).toEqual(100000); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + }); + + describe('Getter', () => { + it('guid', () => expect(wallet.guid).toEqual(object.guid)); + + it('sharedKey', () => expect(wallet.sharedKey).toEqual(object.sharedKey)); + + it('isDoubleEncrypted', () => expect(wallet.isDoubleEncrypted).toEqual(object.double_encryption)); + + it('dpasswordhash', () => expect(wallet.dpasswordhash).toEqual(object.dpasswordhash)); + + it('pbkdf2_iterations', () => expect(wallet.pbkdf2_iterations).toEqual(object.options.pbkdf2_iterations)); + + it('totalSent', () => { + wallet.totalSent = 101; + expect(wallet.totalSent).toEqual(101); + }); + + it('totalReceived', () => { + wallet.totalReceived = 101; + expect(wallet.totalReceived).toEqual(101); + }); + + it('finalBalance', () => { + wallet.finalBalance = 101; + expect(wallet.finalBalance).toEqual(101); + }); + + it('numberTxTotal', () => { + wallet.numberTxTotal = 101; + expect(wallet.numberTxTotal).toEqual(101); + }); + + it('addresses', () => expect(wallet.addresses).toEqual(['1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9', '1H8Cwvr3Vq9rJBGEoudG1AeyeAezr38j8h'])); + + it('activeAddresses', () => expect(wallet.activeAddresses).toEqual(['1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9'])); + + it('keys', () => { + let ad = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv'; + expect(wallet.keys[0].address).toEqual(ad); + }); + + it('key', () => { + let ad = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv'; + expect(wallet.key(ad).address).toEqual(ad); + }); + + it('activeKeys', () => expect(wallet.activeKeys.length).toEqual(2)); + + it('activeKey', () => { + let ad = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv'; + expect(wallet.activeKey(ad).address).toEqual(ad); + + let archived = '1H8Cwvr3Vq9rJBGEoudG1AeyeAezr38j8h'; + expect(wallet.activeKey(archived)).toEqual(null); + }); + + it('hdwallet', () => expect(wallet.hdwallet).toBeDefined()); + + it('isUpgradedToHD', () => expect(wallet.isUpgradedToHD).toBeTruthy()); + + it('balanceActiveLegacy with active', () => { + wallet.keys[0].balance = 101; + expect(wallet.balanceActiveLegacy).toEqual(101); + }); + + it('balanceActiveLegacy without active', () => { + wallet.keys[0].balance = 101; + wallet.keys[0]._tag = 2; + expect(wallet.balanceActiveLegacy).toEqual(0); + }); + + it('defaultPbkdf2Iterations', () => expect(wallet.defaultPbkdf2Iterations).toEqual(5000)); + + it('spendableActiveAddresses', () => expect(wallet.spendableActiveAddresses.length).toEqual(1)); + }); + + describe('Method', () => { + it('.containsLegacyAddress should find address', () => { + let adr = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv'; + expect(wallet.containsLegacyAddress(adr)).toBeTruthy(); + }); + + it('.containsLegacyAddress should find key', () => { + let key = wallet.keys[0]; + expect(wallet.containsLegacyAddress(key)).toBeTruthy(); + }); + + it('.containsLegacyAddress should not find address or key', () => { + let adr = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCXXX'; + let find1 = wallet.containsLegacyAddress(adr); + expect(find1).toBeFalsy(); + }); + + it('.importLegacyAddress', () => pending()); + + describe('.new', () => { + let cb = { + success () {}, + error () {} + }; + + beforeEach(() => { + spyOn(cb, 'success'); + spyOn(cb, 'error'); + }); + + it('should successCallback', () => { + Wallet.new('GUID', 'SHARED-KEY', 'water cow drink milk powder', undefined, 'ACC-LABEL', cb.success, cb.error); + expect(cb.success).toHaveBeenCalled(); + }); + + describe('(error control)', () => + + it('should errorCallback if HD seed generation fail', () => { + HDWallet.shouldThrow = true; + Wallet.new('GUID', 'SHARED-KEY', 'ACC-LABEL', undefined, undefined, cb.success, cb.error); + expect(cb.error).toHaveBeenCalled(); + }) + ); + }); + + describe('.newLegacyAddress', () => { + let callbacks = { + success () {}, + error () {} + }; + + beforeEach(() => { + spyOn(callbacks, 'success'); + spyOn(callbacks, 'error'); + }); + + describe('without second password', () => { + it('should add the address and sync', () => { + wallet.newLegacyAddress('label'); + let newAdd = wallet.keys[wallet.keys.length - 1]; + expect(newAdd).toBeDefined(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('should successCallback', () => { + wallet.newLegacyAddress('label', null, callbacks.success, callbacks.error); + expect(callbacks.success).toHaveBeenCalled(); + }); + + it('should errorCallback if Address.new throws', () => { + // E.g. when there is a network error RNG throws, + // which in turn causes Address.new to throw. + Address.shouldThrow = true; + wallet.newLegacyAddress('label', null, callbacks.success, callbacks.error); + expect(callbacks.error).toHaveBeenCalled(); + }); + }); + + describe('with second password', () => { + beforeEach(() => { + wallet._double_encryption = true; + }); + + it('should require the 2nd pwd', () => expect(() => wallet.newLegacyAddress('label')).toThrow()); + + it('should call encrypt', () => { + wallet.newLegacyAddress('label', '1234'); + let newAdd = wallet.keys[wallet.keys.length - 1]; + expect(newAdd.encrypt).toHaveBeenCalled(); + }); + + it('should add the address and sync', () => { + wallet.newLegacyAddress('label', '1234'); + let newAdd = wallet.keys[wallet.keys.length - 1]; + expect(newAdd).toBeDefined(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + }); + }); + + describe('.deleteLegacyAddress', () => { + it('should delete existing legacy addresses', () => { + expect(wallet.deleteLegacyAddress(wallet.keys[0])).toBeTruthy(); + expect(wallet.keys.length).toEqual(2); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('should do nothing when trying to delete non existing legacy addresses', () => { + expect(wallet.keys.length).toEqual(3); + expect(wallet.deleteLegacyAddress(Address.new('testing'))).toBeFalsy(); + expect(wallet.keys.length).toEqual(3); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing with bad arguments', () => { + expect(wallet.keys.length).toEqual(3); + expect(wallet.deleteLegacyAddress('1KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEb')).toBeFalsy(); + expect(wallet.keys.length).toEqual(3); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + it('.validateSecondPassword', () => { + wallet.encrypt('batteryhorsestaple'); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + expect(wallet.validateSecondPassword('batteryhorsestaple')).toBeTruthy(); + }); + + describe('.encrypt', () => { + let cb = { + success () {}, + error () {}, + encrypting () {}, + syncing () {} + }; + + beforeEach(() => { + spyOn(cb, 'success'); + spyOn(cb, 'error'); + spyOn(cb, 'encrypting'); + spyOn(cb, 'syncing'); + }); + + it('should encrypt a non encrypted wallet', () => { + wallet.encrypt('batteryhorsestaple', cb.success, cb.error, cb.encrypting, cb.syncing); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + expect(cb.success).toHaveBeenCalled(); + expect(cb.syncing).toHaveBeenCalled(); + expect(cb.encrypting).toHaveBeenCalled(); + expect(cb.error).not.toHaveBeenCalled(); + }); + + it('should not encrypt an already encrypted wallet', () => { + wallet.encrypt('batteryhorsestaple'); + wallet.encrypt('batteryhorsestaple', cb.success, cb.error, cb.encrypting, cb.syncing); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + expect(cb.success).not.toHaveBeenCalled(); + expect(cb.syncing).not.toHaveBeenCalled(); + expect(cb.encrypting).toHaveBeenCalled(); + expect(cb.error).not.toHaveBeenCalled(); + }); + }); + + it('.decrypt', () => { + let cb = { + success () {}, + error () {}, + decrypting () {}, + syncing () {} + }; + + beforeEach(() => { + spyOn(cb, 'success'); + spyOn(cb, 'error'); + spyOn(cb, 'decrypting'); + spyOn(cb, 'syncing'); + }); + + it('should decrypt an encrypted wallet', () => { + wallet.encrypt('batteryhorsestaple'); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + wallet.decrypt('batteryhorsestaple', cb.success, cb.error, cb.decrypting, cb.syncing); + expect(cb.success).toHaveBeenCalled(); + expect(cb.syncing).toHaveBeenCalled(); + expect(cb.decrypting).toHaveBeenCalled(); + expect(cb.error).not.toHaveBeenCalled(); + expect(wallet.isDoubleEncrypted).toBeFalsy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('should not decrypt an already decrypted wallet', () => { + expect(wallet.isDoubleEncrypted).toBeFalsy(); + wallet.decrypt('batteryhorsestaple', cb.success, cb.error, cb.decrypting, cb.syncing); + expect(cb.success).not.toHaveBeenCalled(); + expect(cb.syncing).not.toHaveBeenCalled(); + expect(cb.decrypting).toHaveBeenCalled(); + expect(cb.error).not.toHaveBeenCalled(); + }); + }); + + it('.restoreHDWallet', () => pending()); + + describe('.upgradeToV3', () => { + let cb = { + success () {}, + error () {} + }; + + beforeEach(() => { + spyOn(cb, 'success'); + spyOn(cb, 'error'); + spyOn(wallet, 'newAccount').and.callFake(() => {}); + spyOn(BIP39, 'generateMnemonic').and.callThrough(); + spyOn(RNG, 'run').and.callThrough(); + spyOn(wallet, 'loadExternal').and.callFake(() => {}); + }); + + it('should successCallback', () => { + wallet.upgradeToV3('ACC-LABEL', null, cb.success, cb.error); + expect(cb.success).toHaveBeenCalled(); + }); + + it('should call BIP39.generateMnemonic with our RNG', () => { + wallet.upgradeToV3('ACC-LABEL', null, cb.success, cb.error); + expect(BIP39.generateMnemonic).toHaveBeenCalled(); + expect(RNG.run).toHaveBeenCalled(); + }); + + it('should call loadExternal', () => { + wallet.upgradeToV3('ACC-LABEL', null, cb.success, cb.error); + expect(wallet.loadExternal).toHaveBeenCalled(); + }); + + it('should throw if RNG throws', () => { + // E.g. because there was a network failure. + // This assumes BIP39.generateMnemonic does not rescue a throw + // inside the RNG + RNG.shouldThrow = true; + wallet.upgradeToV3('ACC-LABEL', null, cb.success, cb.error); + expect(cb.error).toHaveBeenCalledWith(Error('Connection failed')); + }); + }); + + describe('.newAccount', () => { + let cb = + {success () {}}; + + beforeEach(() => spyOn(cb, 'success')); + + it("should do nothing for a wallet that hasn't been upgraded", () => { + wallet = new Wallet(); + expect(wallet.newAccount('Coffee fund')).toBeFalsy(); + }); + + it('should use the first hdwallet if no index is provided', () => { + expect(wallet.newAccount('Coffee fund')).toEqual(wallet._hd_wallets[0].lastAccount); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('should call the success callback if provided', () => { + wallet.newAccount('Coffee fund', undefined, 0, cb.success); + expect(cb.success).toHaveBeenCalled(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('should not call syncWallet if nosave is set to true', () => { + wallet.newAccount('Coffee fund', undefined, 0, cb.success, true); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should work with encrypted wallets', () => { + wallet.encrypt('batteryhorsestaple'); + wallet.newAccount('Coffee fund', 'batteryhorsestaple', 0, cb.success); + expect(cb.success).toHaveBeenCalled(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + }); + + describe('addressbook label', () => + + it('should be set, persisted and deleted', () => { + expect(wallet.getAddressBookLabel('1hash')).toEqual(undefined); + + wallet.addAddressBookEntry('1hash', 'Rent payment'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + + expect(wallet.getAddressBookLabel('1hash')).toEqual('Rent payment'); + + wallet.removeAddressBookEntry('1hash'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + + expect(wallet.getAddressBookLabel('hash')).toEqual(undefined); + }) + ); + + describe('notes', () => { + it('should be set, persisted and deleted', () => { + expect(wallet.getNote('hash')).toEqual(undefined); + + wallet.setNote('hash', 'Rent payment'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + + expect(wallet.getNote('hash')).toEqual('Rent payment'); + + wallet.deleteNote('hash'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + + expect(wallet.getNote('hash')).toEqual(undefined); + }); + + it('should have a placeholder given a received transaction and account filter', () => { + let outputs = [ + {'identity': 0, 'label': 'Car', 'coinType': '0/0/171'}, + {'identity': 0, 'label': 'Utilities', 'coinType': '0/0/170'} + ]; + + expect(wallet.getNotePlaceholder(0, {'txType': 'received', 'processedOutputs': outputs})).toEqual('Utilities'); + + expect(wallet.getNotePlaceholder(0, {'txType': 'sent', 'processedOutputs': outputs})).toEqual(''); + + expect(wallet.getNotePlaceholder(0, {'txType': 'transferred', 'processedOutputs': outputs})).toEqual(''); + }); + }); + + describe('.getMnemonic', () => { + it('should return the mnemonic if the wallet is not encrypted', () => expect(wallet.getMnemonic()).toEqual('lawn couch clay slab oxygen vicious denial couple ski alley spawn wisdom')); + + it('should fail to return the mnemonic if the wallet is encrypted and the provided password is wrong', () => { + wallet.encrypt('test'); + expect(() => wallet.getMnemonic('nottest')).toThrow(); + }); + + it('should return the mnemonic if the wallet is encrypted', () => { + wallet.encrypt('test'); + expect(wallet.getMnemonic('test')).toEqual('lawn couch clay slab oxygen vicious denial couple ski alley spawn wisdom'); + }); + }); + + describe('.changePbkdf2Iterations', () => { + it('should be change the number of iterations when called correctly', () => { + wallet.changePbkdf2Iterations(10000, null); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + expect(wallet.pbkdf2_iterations).toEqual(10000); + }); + + it('should do nothing when called with the number of iterations it already has', () => { + wallet.changePbkdf2Iterations(5000, null); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should work with a double encrypted wallet', () => { + wallet.encrypt('batteryhorsestaple'); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + wallet.changePbkdf2Iterations(10000, 'batteryhorsestaple'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + wallet.decrypt('batteryhorsestaple'); + expect(wallet.pbkdf2_iterations).toEqual(10000); + }); + }); + + describe('.getPrivateKeyForAddress', () => { + it('should work for non double encrypted wallets', () => { + expect(wallet.isDoubleEncrypted).toBeFalsy(); + expect(wallet.getPrivateKeyForAddress(wallet.keys[0])).toEqual('HUFhy1SvLBzzdAYpwD3quUN9kxqmm9U3Y1ZDdwBhHjPH'); + }); + + it('should work for double encrypted wallets', () => { + wallet.encrypt('batteryhorsestaple'); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + expect(wallet.getPrivateKeyForAddress(wallet.keys[0], 'batteryhorsestaple')).toEqual('HUFhy1SvLBzzdAYpwD3quUN9kxqmm9U3Y1ZDdwBhHjPH'); + }); + + it('should return null for watch-only addresses in non double encrypted wallets', () => { + expect(wallet.isDoubleEncrypted).toBeFalsy(); + expect(wallet.keys[1].isWatchOnly).toBeTruthy(); + expect(wallet.getPrivateKeyForAddress(wallet.keys[1])).toEqual(null); + }); + + it('should return null for watch-only addresses in for double encrypted wallets', () => { + wallet.encrypt('batteryhorsestaple'); + expect(wallet.isDoubleEncrypted).toBeTruthy(); + expect(wallet.keys[1].isWatchOnly).toBeTruthy(); + expect(wallet.getPrivateKeyForAddress(wallet.keys[1], 'batteryhorsestaple')).toEqual(null); + }); + }); + }); + + describe('_updateWalletInfo()', () => { + let multiaddr = { // eslint-disable-line no-unused-vars + wallet: { + total_sent: 1, + total_received: 0, + final_balance: 0, + n_tx: 1 + }, + addresses: [ + { + address: '1CzYCAAi46b8CFiybd3CcGbUykCAeqaocj', + n_tx: 1, + total_received: 0, + total_sent: 1, + final_balance: 0 + } + ], + txs: [ + { + hash: '1234' + } + ], + info: { + latest_block: 300000 + } + }; + beforeEach(() => spyOn(WalletStore, 'pushTransaction').and.callThrough()); + + it('should add a new transaction', () => + // should watch for txlist pushtxs + pending() + ); + // wallet._updateWalletInfo(multiaddr) + // expect(WalletStore.pushTransaction).toHaveBeenCalled() + + it('should not add a duplicate transaction', () => pending()); + }); + // missing mocks and probably this should be tested on txList object + // wallet._updateWalletInfo(multiaddr) + // wallet._updateWalletInfo(multiaddr) + // expect(wallet.txList.fetched).toEqual(1) + + describe('JSON serialization', () => + + it('should hold: fromJSON . toJSON = id', () => { + let json1 = JSON.stringify(wallet, null, 2); + let rwall = JSON.parse(json1, Wallet.reviver); + let json2 = JSON.stringify(rwall, null, 2); + expect(json1).toEqual(json2); + }) + ); + + describe('_getPrivateKey', () => { + it('should not compute private keys for non existent accounts', () => expect(() => wallet._getPrivateKey(-1, 'm/0/1')).toThrow()); + + it('should not compute private keys for invalid paths accounts', () => expect(() => wallet._getPrivateKey(0, 10)).toThrow()); + + it('should compute correct private keys with an unencrypted wallet', () => expect(wallet._getPrivateKey(0, 'M/0/14')).toEqual('KzP1z5HqMg5KAjgoqnkpszjXHo3bCnjxGAdb59fnE2bkSqfCTdyR')); + + it('should fail if encrypted and second password false', () => { + wallet.encrypt('batteryhorsestaple'); + + expect(() => wallet._getPrivateKey(0, 'M/0/14', 'batteryhorsestaple0')).toThrow(); + }); + + it('should compute correct private keys with an encrypted wallet', () => { + wallet.encrypt('batteryhorsestaple'); + + expect(wallet._getPrivateKey(0, 'M/0/14', 'batteryhorsestaple')).toEqual('KzP1z5HqMg5KAjgoqnkpszjXHo3bCnjxGAdb59fnE2bkSqfCTdyR'); + }); + }); + + describe('notifications', () => { + let cb = { + success () {}, + error () {} + }; + + beforeEach(() => { + spyOn(cb, 'success'); + spyOn(cb, 'error'); + spyOn(WalletStore, 'setSyncPubKeys'); + BlockchainSettingsAPI.shouldFail = false; + }); + + describe('.enableNotifications', () => + + it('should require success and error callbacks', () => { + expect(() => wallet.enableNotifications()).toThrow(); + expect(() => wallet.enableNotifications(cb.success)).toThrow(); + expect(() => wallet.enableNotifications(cb.success, cb.error)).not.toThrow(); + }) + ); + + describe('.disableNotifications', () => + + it('should require success and error callbacks', () => { + expect(() => wallet.disableNotifications()).toThrow(); + expect(() => wallet.disableNotifications(cb.success)).toThrow(); + expect(() => wallet.disableNotifications(cb.success, cb.error)).not.toThrow(); + }) + ); + }); + }); +}); diff --git a/tests/blockchain_wallet_spec.js.coffee b/tests/blockchain_wallet_spec.js.coffee deleted file mode 100644 index 726b283b8..000000000 --- a/tests/blockchain_wallet_spec.js.coffee +++ /dev/null @@ -1,793 +0,0 @@ -proxyquire = require('proxyquireify')(require) -MyWallet = undefined -Address = undefined -Wallet = undefined -HDWallet = undefined -WalletStore = undefined -BlockchainSettingsAPI = undefined -BIP39 = undefined -RNG = undefined - -describe "Blockchain-Wallet", -> - wallet = undefined - object = - 'guid': 'c8d9fe67-2ba0-4c15-a2be-0d17981d3c0a' - 'sharedKey': '981b98e8-03f5-48fa-b369-038e2a7fdc09' - 'double_encryption': false - 'options': - 'pbkdf2_iterations': 5000 - 'fee_per_kb': 10000 - 'html5_notifications': false - 'logout_time': 600000 - 'address_book': [{'address': '1dice8EMZmqKvrGE4Qc9bUFf9PX3xaYDp', 'label': 'SatoshiDice'}] - 'tx_notes': {} - 'tx_names': [] - 'keys': [ - { - 'addr': '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv' - 'priv': 'HUFhy1SvLBzzdAYpwD3quUN9kxqmm9U3Y1ZDdwBhHjPH' - 'tag': 0 - 'created_time': 1437494028974 - 'created_device_name': 'javascript_web' - 'created_device_version': '1.0' - }, { - 'addr': '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9' - 'priv': null - }, - { - 'addr': '1H8Cwvr3Vq9rJBGEoudG1AeyeAezr38j8h' - 'priv': '5KHY1QhUx8BYrdZPV6GcRw5rVKyAHbjZxz9KLYkaoL16JuFBZv8' - 'tag': 2 - 'created_time': 1437494028974 - 'created_device_name': 'javascript_web' - 'created_device_version': '1.0' - }, - ] - 'hd_wallets': [ { - 'seed_hex': '7e061ca8e579e5e70e9989ca40d342fe' - 'passphrase': '' - 'mnemonic_verified': false - 'default_account_idx': 0 - 'accounts': [ { - 'label': 'My Bitcoin Wallet' - 'archived': false - 'xpriv': 'xprv9yko4kDvhYSdUcqK5e8naLwtGE1Ca57mwJ6JMB8WxeYq8t1w3PpiZfGGvLN6N6GEwLF8XuHnp8HeNLrWWviAjXxb2BFEiLaW2UgukMZ3Zva' - 'xpub': 'xpub6Ck9UFkpXuzvh6unBffnwUtcpFqgyXqdJX1u9ZY8Wz5p1gM5aw8y7TakmcEWLA9rJkc59BJzn61p3qqKSaqFkSPMbbhGA9YDNmphj9SKBVJ' - 'address_labels': [{'index': 170, 'label': 'Utilities'}] - 'cache': - 'receiveAccount': 'xpub6FD59hfbH1UWQA9B8NP1C8bh3jc6i2tpM6b8f4Wi9gHWQttZbBBtEsDDZAiPsw7e3427SzvQsFu2sdubjbZHDQdqYXN6x3hTDCrG5bZFEhB' - 'changeAccount': 'xpub6FD59hfbH1UWRrY38bVLPPLPLxcA1XBqsQgB95AgsSWngxbwqPBMd5Z3of8PNicLwE9peQ9g4SeWWtBTzUKLwfjSioAg73RRh7dJ5rWYxM7' - } ] - } ] - - beforeEach -> - MyWallet = - syncWallet: (success, error) -> - if success - success() - get_history: () -> - - Address = - new: (label) -> - if Address.shouldThrow - throw "" - addr = { - label: label - encrypt: () -> - { - persist: () -> - } - } - spyOn(addr, "encrypt").and.callThrough() - addr - - HDWallet = - new: (cipher) -> - if HDWallet.shouldThrow - throw "" - { - newAccount: () -> - } - - Helpers = - isInstanceOf: (candidate, theClass) -> - candidate.label != undefined || typeof(candidate) == "object" - - walletStoreTxs = [] - WalletStore = { - pushTransaction: (tx) -> walletStoreTxs.push(tx) - getTransactions: () -> walletStoreTxs - getTransaction: (hash) -> walletStoreTxs.filter( - (tx) -> tx.hash == hash - )[0] - setSyncPubKeys: (boolean) -> - } - - BlockchainSettingsAPI = { - shouldFail: false - enableEmailReceiveNotifications: (success, error) -> - if BlockchainSettingsAPI.shouldFail - error() - else - success() - - disableAllNotifications: (success, error) -> - if BlockchainSettingsAPI.shouldFail - error() - else - success() - - } - - BIP39 = { - generateMnemonic: (str, rng, wlist) -> - mnemonic = "bicycle balcony prefer kid flower pole goose crouch century lady worry flavor" - seed = rng(32) - if seed = "random" then mnemonic else "failure" - } - RNG = { - run: (input) -> - if RNG.shouldThrow - throw new Error('Connection failed'); - "random" - } - - stubs = { - './wallet' : MyWallet, - './address' : Address, - './helpers' : Helpers, - './hd-wallet': HDWallet, - './wallet-store' : WalletStore, - './blockchain-settings-api': BlockchainSettingsAPI, - 'bip39': BIP39, - './rng' : RNG - } - - Wallet = proxyquire('../src/blockchain-wallet', stubs) - - spyOn(MyWallet, "syncWallet").and.callThrough() - spyOn(MyWallet, "get_history").and.callThrough() - - describe "Constructor", -> - - it "should create an empty Wallet with default options", -> - wallet = new Wallet() - expect(wallet.double_encryption).toBeFalsy() - expect(wallet._totalSent).toEqual(0) - expect(wallet._totalReceived).toEqual(0) - expect(wallet._finalBalance).toEqual(0) - - it "should transform an Object to a Wallet", -> - Wallet = proxyquire('../src/blockchain-wallet', {}) - wallet = new Wallet(object) - - expect(wallet._guid).toEqual(object.guid) - expect(wallet._sharedKey).toEqual(object.sharedKey) - expect(wallet._double_encryption).toEqual(object.double_encryption) - expect(wallet._dpasswordhash).toEqual(object.dpasswordhash) - expect(wallet._pbkdf2_iterations).toEqual(object.options.pbkdf2_iterations) - expect(wallet._logout_time).toEqual(object.options.logout_time) - expect(wallet._address_book['1dice8EMZmqKvrGE4Qc9bUFf9PX3xaYDp']).toEqual('SatoshiDice') - - describe "instance", -> - beforeEach -> - wallet = new Wallet(object) - - describe "Setter", -> - - it "guid is read only", -> - wallet.guid = "not allowed" - expect(wallet.guid).not.toEqual("not allowed") - - it "sharedKey is read only", -> - wallet.sharedKey = "not allowed" - expect(wallet.sharedKey).not.toEqual("not allowed") - - it "isDoubleEncrypted is read only", -> - wallet.isDoubleEncrypted = "not allowed" - expect(wallet.isDoubleEncrypted).not.toEqual("not allowed") - - it "dpasswordhash is read only", -> - wallet.dpasswordhash = "not allowed" - expect(wallet.dpasswordhash).not.toEqual("not allowed") - - it "fee_per_kb should throw exception if is non-number set", -> - wrongSet = () -> wallet.fee_per_kb = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "fee_per_kb should throw expection if set to high", -> - invalid = () -> wallet.fee_per_kb = 100000000 - expect(invalid).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "fee_per_kb should be set to the value sent", -> - invalid = () -> wallet.fee_per_kb = 10000 - expect(invalid).not.toThrow() - expect(MyWallet.syncWallet).toHaveBeenCalled() - expect(wallet.fee_per_kb).toEqual(10000) - - it "pbkdf2_iterations is read only", -> - wallet.pbkdf2_iterations = "not allowed" - expect(wallet.pbkdf2_iterations).not.toEqual("not allowed") - - it "totalSent should throw exception if is non-number set", -> - wrongSet = () -> wallet.totalSent = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "totalReceived should throw exception if is non-number set", -> - wrongSet = () -> wallet.totalReceived = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "finalBalance should throw exception if is non-number set", -> - wrongSet = () -> wallet.finalBalance = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "numberTxTotal should throw exception if is non-number set", -> - wrongSet = () -> wallet.numberTxTotal = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "addresses is read only", -> - wallet.addresses = "not allowed" - expect(wallet.addresses).not.toEqual("not allowed") - - it "activeAddresses is read only", -> - wallet.activeAddresses = "not allowed" - expect(wallet.activeAddresses).not.toEqual("not allowed") - - it "key is read only", -> - wallet.key = "not allowed" - expect(wallet.key).not.toEqual("not allowed") - - it "activeKey is read only", -> - wallet.activeKey = "not allowed" - expect(wallet.activeKey).not.toEqual("not allowed") - - it "keys is read only", -> - wallet.keys = "not allowed" - expect(wallet.keys).not.toEqual("not allowed") - - it "activeKeys is read only", -> - wallet.activeKeys = "not allowed" - expect(wallet.activeKeys).not.toEqual("not allowed") - - it "hdwallet is read only", -> - wallet.hdwallet = "not allowed" - expect(wallet.hdwallet).not.toEqual("not allowed") - - it "isUpgradedToHD is read only", -> - wallet.isUpgradedToHD = "not allowed" - expect(wallet.isUpgradedToHD).not.toEqual("not allowed") - - it "balanceActiveLegacy is read only", -> - wallet.balanceActiveLegacy = "not allowed" - expect(wallet.balanceActiveLegacy).not.toEqual("not allowed") - - it "addressBook is read only", -> - wallet.addressBook = "not allowed" - expect(wallet.addressBook).not.toEqual("not allowed") - - it "logoutTime should throw exception if is non-number set", -> - wrongSet = () -> wallet.logoutTime = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "logoutTime should throw exception if is out of range set", -> - wrongSet = () -> wallet.logoutTime = 59000 - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "logoutTime should throw exception if is out of range set", -> - wrongSet = () -> wallet.logoutTime = 86400002 - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "logoutTime should be set and sync", -> - wallet.logoutTime = 100000 - expect(wallet.logoutTime).toEqual(100000) - expect(MyWallet.syncWallet).toHaveBeenCalled() - - describe "Getter", -> - - it "guid", -> - expect(wallet.guid).toEqual(object.guid) - - it "sharedKey", -> - expect(wallet.sharedKey).toEqual(object.sharedKey) - - it "isDoubleEncrypted", -> - expect(wallet.isDoubleEncrypted).toEqual(object.double_encryption) - - it "dpasswordhash", -> - expect(wallet.dpasswordhash).toEqual(object.dpasswordhash) - - it "pbkdf2_iterations", -> - expect(wallet.pbkdf2_iterations).toEqual(object.options.pbkdf2_iterations) - - it "totalSent", -> - wallet.totalSent = 101 - expect(wallet.totalSent).toEqual(101) - - it "totalReceived", -> - wallet.totalReceived = 101 - expect(wallet.totalReceived).toEqual(101) - - it "finalBalance", -> - wallet.finalBalance = 101 - expect(wallet.finalBalance).toEqual(101) - - it "numberTxTotal", -> - wallet.numberTxTotal = 101 - expect(wallet.numberTxTotal).toEqual(101) - - it "addresses", -> - expect(wallet.addresses).toEqual(['1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9', '1H8Cwvr3Vq9rJBGEoudG1AeyeAezr38j8h']) - - it "activeAddresses", -> - expect(wallet.activeAddresses).toEqual(['1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9']) - - it "keys", -> - ad = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv' - expect(wallet.keys[0].address).toEqual(ad) - - it "key", -> - ad = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv' - expect(wallet.key(ad).address).toEqual(ad) - - it "activeKeys", -> - expect(wallet.activeKeys.length).toEqual(2) - - it "activeKey", -> - ad = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv' - expect(wallet.activeKey(ad).address).toEqual(ad) - - archived = '1H8Cwvr3Vq9rJBGEoudG1AeyeAezr38j8h' - expect(wallet.activeKey(archived)).toEqual(null) - - it "hdwallet", -> - expect(wallet.hdwallet).toBeDefined() - - it "isUpgradedToHD", -> - expect(wallet.isUpgradedToHD).toBeTruthy() - - it "balanceActiveLegacy with active", -> - wallet.keys[0].balance = 101 - expect(wallet.balanceActiveLegacy).toEqual(101) - - it "balanceActiveLegacy without active", -> - wallet.keys[0].balance = 101 - wallet.keys[0]._tag = 2; - expect(wallet.balanceActiveLegacy).toEqual(0) - - it "defaultPbkdf2Iterations", -> - expect(wallet.defaultPbkdf2Iterations).toEqual(5000) - - it "spendableActiveAddresses", -> - expect(wallet.spendableActiveAddresses.length).toEqual(1) - - describe "Method", -> - it ".containsLegacyAddress should find address", -> - adr = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCv' - expect(wallet.containsLegacyAddress(adr)).toBeTruthy() - - it ".containsLegacyAddress should find key", -> - key = wallet.keys[0] - expect(wallet.containsLegacyAddress(key)).toBeTruthy() - - it ".containsLegacyAddress should not find address or key", -> - adr = '1ASqDXsKYqcx7dkKZ74bKBBggpd5HDtjCXXX' - find1 = wallet.containsLegacyAddress(adr) - expect(find1).toBeFalsy() - - it ".importLegacyAddress", -> - pending() - - describe ".new", -> - cb = - success: () -> - error: () -> - - beforeEach -> - spyOn(cb, "success") - spyOn(cb, "error") - - it "should successCallback", -> - Wallet.new("GUID","SHARED-KEY", "water cow drink milk powder", undefined, "ACC-LABEL", cb.success, cb.error) - expect(cb.success).toHaveBeenCalled() - - describe "(error control)", -> - - it "should errorCallback if HD seed generation fail", -> - HDWallet.shouldThrow = true - Wallet.new("GUID","SHARED-KEY","ACC-LABEL", undefined, undefined, cb.success, cb.error) - expect(cb.error).toHaveBeenCalled() - - describe ".newLegacyAddress", -> - callbacks = - success: () -> - error: () -> - - beforeEach -> - spyOn(callbacks, "success") - spyOn(callbacks, "error") - - describe "without second password", -> - it "should add the address and sync", -> - wallet.newLegacyAddress("label") - newAdd = wallet.keys[wallet.keys.length - 1] - expect(newAdd).toBeDefined() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "should successCallback", -> - wallet.newLegacyAddress("label", null, callbacks.success, callbacks.error) - expect(callbacks.success).toHaveBeenCalled() - - it "should errorCallback if Address.new throws", -> - # E.g. when there is a network error RNG throws, - # which in turn causes Address.new to throw. - Address.shouldThrow = true - wallet.newLegacyAddress("label", null, callbacks.success, callbacks.error) - expect(callbacks.error).toHaveBeenCalled() - - describe "with second password", -> - beforeEach -> - wallet._double_encryption = true - - it "should require the 2nd pwd", -> - expect(() -> wallet.newLegacyAddress("label")).toThrow() - - it "should call encrypt", -> - wallet.newLegacyAddress("label", "1234") - newAdd = wallet.keys[wallet.keys.length - 1] - expect(newAdd.encrypt).toHaveBeenCalled() - - it "should add the address and sync", -> - wallet.newLegacyAddress("label", "1234") - newAdd = wallet.keys[wallet.keys.length - 1] - expect(newAdd).toBeDefined() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - - describe ".deleteLegacyAddress", -> - - it "should delete existing legacy addresses", -> - expect(wallet.deleteLegacyAddress(wallet.keys[0])).toBeTruthy() - expect(wallet.keys.length).toEqual(2) - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "should do nothing when trying to delete non existing legacy addresses", -> - expect(wallet.keys.length).toEqual(3) - expect(wallet.deleteLegacyAddress(Address.new("testing"))).toBeFalsy() - expect(wallet.keys.length).toEqual(3) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "should do nothing with bad arguments", -> - expect(wallet.keys.length).toEqual(3) - expect(wallet.deleteLegacyAddress("1KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEb")).toBeFalsy() - expect(wallet.keys.length).toEqual(3) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it ".validateSecondPassword", -> - wallet.encrypt("batteryhorsestaple") - expect(wallet.isDoubleEncrypted).toBeTruthy() - expect(wallet.validateSecondPassword("batteryhorsestaple")).toBeTruthy() - - describe ".encrypt", -> - cb = - success: () -> - error: () -> - encrypting: () -> - syncing: () -> - - beforeEach -> - spyOn(cb, "success") - spyOn(cb, "error") - spyOn(cb, "encrypting") - spyOn(cb, "syncing") - - it "should encrypt a non encrypted wallet", -> - wallet.encrypt("batteryhorsestaple", cb.success, cb.error, cb.encrypting, cb.syncing) - expect(wallet.isDoubleEncrypted).toBeTruthy() - expect(cb.success).toHaveBeenCalled() - expect(cb.syncing).toHaveBeenCalled() - expect(cb.encrypting).toHaveBeenCalled() - expect(cb.error).not.toHaveBeenCalled() - - it "should not encrypt an already encrypted wallet", -> - wallet.encrypt("batteryhorsestaple") - wallet.encrypt("batteryhorsestaple", cb.success, cb.error, cb.encrypting, cb.syncing) - expect(wallet.isDoubleEncrypted).toBeTruthy() - expect(cb.success).not.toHaveBeenCalled() - expect(cb.syncing).not.toHaveBeenCalled() - expect(cb.encrypting).toHaveBeenCalled() - expect(cb.error).not.toHaveBeenCalled() - - it ".decrypt", -> - cb = - success: () -> - error: () -> - decrypting: () -> - syncing: () -> - - beforeEach -> - spyOn(cb, "success") - spyOn(cb, "error") - spyOn(cb, "decrypting") - spyOn(cb, "syncing") - - it "should decrypt an encrypted wallet", -> - wallet.encrypt("batteryhorsestaple") - expect(wallet.isDoubleEncrypted).toBeTruthy() - wallet.decrypt("batteryhorsestaple", cb.success, cb.error, cb.decrypting, cb.syncing) - expect(cb.success).toHaveBeenCalled() - expect(cb.syncing).toHaveBeenCalled() - expect(cb.decrypting).toHaveBeenCalled() - expect(cb.error).not.toHaveBeenCalled() - expect(wallet.isDoubleEncrypted).toBeFalsy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "should not decrypt an already decrypted wallet", -> - expect(wallet.isDoubleEncrypted).toBeFalsy() - wallet.decrypt("batteryhorsestaple", cb.success, cb.error, cb.decrypting, cb.syncing) - expect(cb.success).not.toHaveBeenCalled() - expect(cb.syncing).not.toHaveBeenCalled() - expect(cb.decrypting).toHaveBeenCalled() - expect(cb.error).not.toHaveBeenCalled() - - it ".restoreHDWallet", -> - pending() - - describe ".upgradeToV3", -> - cb = - success: () -> - error: () -> - - beforeEach -> - spyOn(cb, "success") - spyOn(cb, "error") - spyOn(wallet, "newAccount").and.callFake(()->) - spyOn(BIP39, "generateMnemonic").and.callThrough() - spyOn(RNG, "run").and.callThrough() - spyOn(wallet, "loadExternal").and.callFake(()->) - - it "should successCallback", -> - wallet.upgradeToV3("ACC-LABEL", null, cb.success, cb.error) - expect(cb.success).toHaveBeenCalled() - - it "should call BIP39.generateMnemonic with our RNG", -> - wallet.upgradeToV3("ACC-LABEL", null, cb.success, cb.error) - expect(BIP39.generateMnemonic).toHaveBeenCalled() - expect(RNG.run).toHaveBeenCalled() - - it "should call loadExternal", -> - wallet.upgradeToV3("ACC-LABEL", null, cb.success, cb.error) - expect(wallet.loadExternal).toHaveBeenCalled() - - it "should throw if RNG throws", -> - # E.g. because there was a network failure. - # This assumes BIP39.generateMnemonic does not rescue a throw - # inside the RNG - RNG.shouldThrow = true - wallet.upgradeToV3("ACC-LABEL", null, cb.success, cb.error) - expect(cb.error).toHaveBeenCalledWith(Error('Connection failed')) - - describe ".newAccount", -> - cb = - success: () -> - - beforeEach -> - spyOn(cb, "success") - - it "should do nothing for a wallet that hasn't been upgraded", -> - wallet = new Wallet() - expect(wallet.newAccount("Coffee fund")).toBeFalsy() - - it "should use the first hdwallet if no index is provided", -> - expect(wallet.newAccount("Coffee fund")).toEqual(wallet._hd_wallets[0].lastAccount) - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "should call the success callback if provided", -> - wallet.newAccount("Coffee fund", undefined, 0, cb.success) - expect(cb.success).toHaveBeenCalled() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "should not call syncWallet if nosave is set to true", -> - wallet.newAccount("Coffee fund", undefined, 0, cb.success, true) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "should work with encrypted wallets", -> - wallet.encrypt("batteryhorsestaple") - wallet.newAccount("Coffee fund", "batteryhorsestaple", 0, cb.success) - expect(cb.success).toHaveBeenCalled() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - describe "addressbook label", -> - - it "should be set, persisted and deleted", -> - expect(wallet.getAddressBookLabel("1hash")).toEqual(undefined) - - wallet.addAddressBookEntry("1hash", "Rent payment") - expect(MyWallet.syncWallet).toHaveBeenCalled() - - expect(wallet.getAddressBookLabel("1hash")).toEqual("Rent payment") - - wallet.removeAddressBookEntry("1hash") - expect(MyWallet.syncWallet).toHaveBeenCalled() - - expect(wallet.getAddressBookLabel("hash")).toEqual(undefined) - - describe "notes", -> - - it "should be set, persisted and deleted", -> - expect(wallet.getNote("hash")).toEqual(undefined) - - wallet.setNote("hash", "Rent payment") - expect(MyWallet.syncWallet).toHaveBeenCalled() - - expect(wallet.getNote("hash")).toEqual("Rent payment") - - wallet.deleteNote("hash") - expect(MyWallet.syncWallet).toHaveBeenCalled() - - expect(wallet.getNote("hash")).toEqual(undefined) - - it "should have a placeholder given a received transaction and account filter", -> - outputs = [ - {'identity':0, 'label':'Car', 'coinType':'0/0/171'}, - {'identity':0, 'label':'Utilities', 'coinType':'0/0/170'} - ] - - expect(wallet.getNotePlaceholder(0, {'txType':'received', 'processedOutputs':outputs})).toEqual('Utilities') - - expect(wallet.getNotePlaceholder(0, {'txType':'sent', 'processedOutputs':outputs})).toEqual('') - - expect(wallet.getNotePlaceholder(0, {'txType':'transferred', 'processedOutputs':outputs})).toEqual('') - - describe ".getMnemonic", -> - it "should return the mnemonic if the wallet is not encrypted", -> - expect(wallet.getMnemonic()).toEqual("lawn couch clay slab oxygen vicious denial couple ski alley spawn wisdom") - - it "should fail to return the mnemonic if the wallet is encrypted and the provided password is wrong", -> - wallet.encrypt("test") - expect(() -> wallet.getMnemonic("nottest")).toThrow() - - it "should return the mnemonic if the wallet is encrypted", -> - wallet.encrypt("test") - expect(wallet.getMnemonic("test")).toEqual("lawn couch clay slab oxygen vicious denial couple ski alley spawn wisdom") - - describe ".changePbkdf2Iterations", -> - it "should be change the number of iterations when called correctly", -> - wallet.changePbkdf2Iterations(10000, null) - expect(MyWallet.syncWallet).toHaveBeenCalled() - expect(wallet.pbkdf2_iterations).toEqual(10000) - - it "should do nothing when called with the number of iterations it already has", -> - wallet.changePbkdf2Iterations(5000, null) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "should work with a double encrypted wallet", -> - wallet.encrypt("batteryhorsestaple") - expect(wallet.isDoubleEncrypted).toBeTruthy() - wallet.changePbkdf2Iterations(10000, "batteryhorsestaple") - expect(MyWallet.syncWallet).toHaveBeenCalled() - wallet.decrypt("batteryhorsestaple") - expect(wallet.pbkdf2_iterations).toEqual(10000) - - describe ".getPrivateKeyForAddress", -> - it "should work for non double encrypted wallets", -> - expect(wallet.isDoubleEncrypted).toBeFalsy() - expect(wallet.getPrivateKeyForAddress(wallet.keys[0])).toEqual('HUFhy1SvLBzzdAYpwD3quUN9kxqmm9U3Y1ZDdwBhHjPH') - - it "should work for double encrypted wallets", -> - wallet.encrypt("batteryhorsestaple") - expect(wallet.isDoubleEncrypted).toBeTruthy() - expect(wallet.getPrivateKeyForAddress(wallet.keys[0], "batteryhorsestaple")).toEqual('HUFhy1SvLBzzdAYpwD3quUN9kxqmm9U3Y1ZDdwBhHjPH') - - it "should return null for watch-only addresses in non double encrypted wallets", -> - expect(wallet.isDoubleEncrypted).toBeFalsy() - expect(wallet.keys[1].isWatchOnly).toBeTruthy() - expect(wallet.getPrivateKeyForAddress(wallet.keys[1])).toEqual(null) - - it "should return null for watch-only addresses in for double encrypted wallets", -> - wallet.encrypt("batteryhorsestaple") - expect(wallet.isDoubleEncrypted).toBeTruthy() - expect(wallet.keys[1].isWatchOnly).toBeTruthy() - expect(wallet.getPrivateKeyForAddress(wallet.keys[1], "batteryhorsestaple")).toEqual(null) - - describe "_updateWalletInfo()", -> - multiaddr = { - wallet: - total_sent: 1 - total_received: 0 - final_balance: 0 - n_tx: 1 - addresses: [ - { - address:"1CzYCAAi46b8CFiybd3CcGbUykCAeqaocj", - n_tx:1, - total_received: 0, - total_sent: 1, - final_balance: 0 - } - ] - txs: [ - { - hash: "1234" - } - ] - info: - latest_block: 300000 - } - beforeEach -> - spyOn(WalletStore, "pushTransaction").and.callThrough() - - it "should add a new transaction", -> - # should watch for txlist pushtxs - pending() - # wallet._updateWalletInfo(multiaddr) - # expect(WalletStore.pushTransaction).toHaveBeenCalled() - - it "should not add a duplicate transaction", -> - pending() - # missing mocks and probably this should be tested on txList object - # wallet._updateWalletInfo(multiaddr) - # wallet._updateWalletInfo(multiaddr) - # expect(wallet.txList.fetched).toEqual(1) - - describe "JSON serialization", -> - - it 'should hold: fromJSON . toJSON = id', -> - json1 = JSON.stringify(wallet, null, 2) - rwall = JSON.parse(json1, Wallet.reviver) - json2 = JSON.stringify(rwall, null, 2) - expect(json1).toEqual(json2) - - describe "_getPrivateKey", -> - - it "should not compute private keys for non existent accounts", -> - expect(() -> wallet._getPrivateKey(-1, 'm/0/1')).toThrow() - - it "should not compute private keys for invalid paths accounts", -> - expect(() -> wallet._getPrivateKey(0, 10)).toThrow() - - it "should compute correct private keys with an unencrypted wallet", -> - expect(wallet._getPrivateKey(0, 'M/0/14')).toEqual('KzP1z5HqMg5KAjgoqnkpszjXHo3bCnjxGAdb59fnE2bkSqfCTdyR') - - it "should fail if encrypted and second password false", -> - wallet.encrypt("batteryhorsestaple") - - expect(() -> wallet._getPrivateKey(0, 'M/0/14', 'batteryhorsestaple0')).toThrow() - - it "should compute correct private keys with an encrypted wallet", -> - wallet.encrypt("batteryhorsestaple") - - expect(wallet._getPrivateKey(0, 'M/0/14', 'batteryhorsestaple')).toEqual('KzP1z5HqMg5KAjgoqnkpszjXHo3bCnjxGAdb59fnE2bkSqfCTdyR') - - describe "notifications", -> - cb = - success: () -> - error: () -> - - beforeEach -> - spyOn(cb, "success") - spyOn(cb, "error") - spyOn(WalletStore, "setSyncPubKeys") - BlockchainSettingsAPI.shouldFail = false - - describe ".enableNotifications", -> - - it "should require success and error callbacks", -> - expect(() -> wallet.enableNotifications()).toThrow() - expect(() -> wallet.enableNotifications(cb.success)).toThrow() - expect(() -> wallet.enableNotifications(cb.success, cb.error)).not.toThrow() - - describe ".disableNotifications", -> - - it "should require success and error callbacks", -> - expect(() -> wallet.disableNotifications()).toThrow() - expect(() -> wallet.disableNotifications(cb.success)).toThrow() - expect(() -> wallet.disableNotifications(cb.success, cb.error)).not.toThrow() diff --git a/tests/coinify/coinify_address_spec.js.coffee b/tests/coinify/coinify_address_spec.js.coffee deleted file mode 100644 index f96c5aaec..000000000 --- a/tests/coinify/coinify_address_spec.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -Address = proxyquire('../../src/coinify/address', stubs) - -describe "CoinifyAddress", -> - - aObj = undefined - a = undefined - - beforeEach -> - - aObj = { - street: "221B Baker Street" - city: "London" - state: "England" - zipcode: "NW1 6XE" - country: "United Kingdom" - } - a = new Address(aObj) - - describe "class", -> - describe "new Address()", -> - it "should construct an Address", -> - expect(a._street).toBe(aObj.street) - expect(a._city).toBe(aObj.city) - expect(a._state).toBe(aObj.state) - expect(a._zipcode).toBe(aObj.zipcode) - expect(a._country).toBe(aObj.country) - - describe "instance", -> - describe "getters", -> - it "should work", -> - - expect(a.street).toBe(aObj.street) - expect(a.city).toBe(aObj.city) - expect(a.state).toBe(aObj.state) - expect(a.zipcode).toBe(aObj.zipcode) - expect(a.country).toBe(aObj.country) diff --git a/tests/coinify/coinify_api_spec.js.coffee b/tests/coinify/coinify_api_spec.js.coffee deleted file mode 100644 index a470c5762..000000000 --- a/tests/coinify/coinify_api_spec.js.coffee +++ /dev/null @@ -1,238 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -require('isomorphic-fetch') - -fetchMock = require('fetch-mock') - -stubs = { -} - -API = proxyquire('../../src/coinify/api', stubs) - -describe "API", -> - - api = undefined - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "class", -> - describe "new API()", -> - it "should have a root URL", -> - api = new API() - expect(api._rootURL).toBeDefined() - - describe "instance", -> - beforeEach -> - api = new API() - api._offlineToken = "offline-token" - - describe "Getter", -> - describe "hasAccount", -> - it "should use offline_token to see if user has account", -> - api._offlineToken = undefined - expect(api.hasAccount).toEqual(false) - - api._offlineToken = "token" - expect(api.hasAccount).toEqual(true) - - describe "isLoggedIn", -> - beforeEach -> - api._access_token = "access_token" - api._loginExpiresAt = new Date(new Date().getTime() + 100000) - - it "checks if there is an access token", -> - expect(api.isLoggedIn).toEqual(true) - - api._access_token = undefined - expect(api.isLoggedIn).toEqual(false) - - it "checks if the token hasn't expired", -> - expect(api.isLoggedIn).toEqual(true) - - api._loginExpiresAt = new Date(new Date().getTime() - 100000) - expect(api.isLoggedIn).toEqual(false) - - - it "should be a few seconds on the safe side", -> - expect(api.isLoggedIn).toEqual(true) - - api._loginExpiresAt = new Date(new Date().getTime()) - expect(api.isLoggedIn).toEqual(false) - - describe 'login', -> - beforeEach -> - api._user = "user-1" - api._offlineToken = "offline-token" - - spyOn(api, "POST").and.callFake((endpoint, data) -> - if endpoint == "auth" - if data.offline_token == 'invalid-offline-token' - Promise.reject({"error":"offline_token_not_found"}) - else if data.offline_token == 'random-fail-offline-token' - Promise.reject() - else - Promise.resolve({access_token: "access-token", token_type: "bearer"}) - else - Promise.reject("Unknown endpoint") - ) - - it 'requires an offline token', -> - api._offlineToken = undefined - promise = api.login() - expect(promise).toBeRejectedWith("NO_OFFLINE_TOKEN") - - it 'should POST the offline token to /auth', -> - promise = api.login() - expect(api.POST).toHaveBeenCalled() - expect(api.POST.calls.argsFor(0)[1].offline_token).toEqual('offline-token') - - it 'should store the access token', (done) -> - checks = () -> - expect(api._access_token).toEqual("access-token") - - promise = api.login().then(checks) - expect(promise).toBeResolved(done) - - it 'should handle token not found error', (done) -> - api._offlineToken = 'invalid-offline-token' - promise = api.login() - expect(promise).toBeRejectedWith(jasmine.objectContaining({error: 'offline_token_not_found'}), done) - - it 'should handle generic failure', (done) -> - api._offlineToken = 'random-fail-offline-token' - promise = api.login() - expect(promise).toBeRejected(done) - - describe '_request', -> - beforeEach -> - api._rootURL = '/' - fetchMock.get('*', {}) - fetchMock.post('*', {}) - - afterEach -> - fetchMock.restore() - - it "should use fetch()", -> - api._request('GET', 'trades') - expect(fetchMock.lastUrl()).toEqual('/trades') - - it "should URL encode parameters for GET requests", -> - api._request('GET', 'trades', {param: 1}) - expect(fetchMock.lastUrl()).toEqual('/trades?param=1') - expect(fetchMock.lastOptions().method).toEqual('GET') - - it "should JSON encode POST data", -> - api._request('POST', 'trades', {param: 1}) - expect(fetchMock.lastUrl()).toEqual('/trades') - expect(fetchMock.lastOptions().method).toEqual('POST') - expect(JSON.parse(fetchMock.lastOptions().body)).toEqual({param: 1}) - - it "should add Authorization header if asked", -> - api._access_token = 'session-token' - api._loginExpiresAt = new Date(new Date().getTime() + 15000) - api._request('GET', 'trades', undefined, true) - expect(fetchMock.lastOptions().headers.Authorization).toEqual('Bearer session-token') - - describe 'REST', -> - beforeEach -> - spyOn(api, '_request') - - describe 'GET', -> - it "should make a GET request", -> - api.GET('/trades') - expect(api._request).toHaveBeenCalled() - expect(api._request.calls.argsFor(0)[0]).toEqual('GET') - - describe 'POST', -> - it "should make a POST request", -> - api.POST('/trades') - expect(api._request).toHaveBeenCalled() - expect(api._request.calls.argsFor(0)[0]).toEqual('POST') - - describe 'PATCH', -> - it "should make a PATCH request", -> - api.PATCH('/trades') - expect(api._request).toHaveBeenCalled() - expect(api._request.calls.argsFor(0)[0]).toEqual('PATCH') - - describe "authenticated", -> - beforeEach -> - api._access_token = 'session-token' - api._loginExpiresAt = new Date(new Date().getTime() + 15000) - spyOn(api, "login").and.callFake(() -> - api._access_token = 'session-token' - api._loginExpiresAt = new Date(new Date().getTime() + 15000) - Promise.resolve() - ) - - it "should not login again if access token is valid", -> - api.authGET('/trades') - api.authPOST('/trades') - api.authPATCH('/trades') - expect(api.login).not.toHaveBeenCalled() - - it "should refuse if no offline token is present for GET", -> - api._access_token = null - api._offlineToken = null - api.authGET('/trades') - - expect(api._request).not.toHaveBeenCalled() - - it "should refuse if no offline token is present for POST", -> - api._access_token = null - api._offlineToken = null - api.authPOST('/trades') - - expect(api._request).not.toHaveBeenCalled() - - it "should refuse if no offline token is present for PATCH", -> - api._access_token = null - api._offlineToken = null - api.authPATCH('/trades') - - expect(api._request).not.toHaveBeenCalled() - - it "should login first before GET if access token is absent", -> - api._access_token = null - api.authGET('/trades') - expect(api.login).toHaveBeenCalled() - - it "should login first before POST if access token is absent", -> - api._access_token = null - api.authPOST('/trades') - expect(api.login).toHaveBeenCalled() - - it "should login first before PATCH if access token is absent", -> - api._access_token = null - api.authPATCH('/trades') - expect(api.login).toHaveBeenCalled() - - it "should login first if access token is expired", -> - api._loginExpiresAt = new Date(new Date().getTime() - 1) - api.authGET('/trades') - expect(api.login).toHaveBeenCalled() - - describe 'GET', -> - it "should make a GET request", -> - api.authGET('/trades') - expect(api._request).toHaveBeenCalled() - expect(api._request.calls.argsFor(0)[0]).toEqual('GET') - expect(api._request.calls.argsFor(0)[3]).toEqual(true) - - describe 'POST', -> - it "should make a POST request", -> - api.authPOST('/trades') - expect(api._request).toHaveBeenCalled() - expect(api._request.calls.argsFor(0)[0]).toEqual('POST') - expect(api._request.calls.argsFor(0)[3]).toEqual(true) - - describe 'PATCH', -> - it "should make a PATCH request", -> - api.authPATCH('/trades') - expect(api._request).toHaveBeenCalled() - expect(api._request.calls.argsFor(0)[0]).toEqual('PATCH') - expect(api._request.calls.argsFor(0)[3]).toEqual(true) diff --git a/tests/coinify/coinify_bank_account_spec.js.coffee b/tests/coinify/coinify_bank_account_spec.js.coffee deleted file mode 100644 index a69ec15b7..000000000 --- a/tests/coinify/coinify_bank_account_spec.js.coffee +++ /dev/null @@ -1,80 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -Address = (obj) -> - {street: obj.street} - -stubs = { - './address' : Address -} - -BankAccount = proxyquire('../../src/coinify/bank-account', stubs) -o = undefined -address = undefined - -beforeEach -> - JasminePromiseMatchers.install() - - address = { - street: "221B Baker Street" - } - - o = { - id: "id" - account: { - type: 'type' - currency: "currency" - bic: "bic" - number: "number" - } - bank: { - name: "Banky McBankface" - address: address - } - holder: { - address: address - } - referenceText: "referenceText" - updateTime: "updateTime" - createTime: "createTime" - } - -afterEach -> - JasminePromiseMatchers.uninstall() - -describe "Coinify: Bank account", -> - - describe "constructor", -> - it "coinify reference must be preserved", -> - b = new BankAccount(o) - expect(b._id).toBe(o.id) - expect(b._type).toBe(o.account.type) - expect(b._currency).toBe(o.account.currency) - expect(b._bic).toBe(o.account.bic) - expect(b._number).toBe(o.account.number) - - expect(b._bank_name).toBe(o.bank.name) - expect(b._holder_name).toBe(o.holder.name) - expect(b._bank_address.street).toEqual(o.bank.address.street) - expect(b._holder_name).toBe(o.holder.name) - expect(b._holder_address.street).toEqual(o.holder.address.street) - expect(b._referenceText).toBe(o.referenceText) - - expect(b._updated_at).toBe(o.updateTime) - expect(b._created_at).toBe(o.createTime) - - describe "instance", -> - b = undefined - beforeEach -> - b = new BankAccount(o) - - it "has getters", -> - expect(b.type).toBe(o.account.type) - expect(b.currency).toBe(o.account.currency) - expect(b.bic).toBe(o.account.bic) - expect(b.number).toBe(o.account.number) - - expect(b.bankName).toBe(o.bank.name) - expect(b.bankAddress.street).toEqual(o.bank.address.street) - expect(b.holderName).toBe(o.holder.name) - expect(b.holderAddress.street).toEqual(o.holder.address.street) - expect(b.referenceText).toBe(o.referenceText) diff --git a/tests/coinify/coinify_exchange_rate_spec.js.coffee b/tests/coinify/coinify_exchange_rate_spec.js.coffee deleted file mode 100644 index 200a6c4af..000000000 --- a/tests/coinify/coinify_exchange_rate_spec.js.coffee +++ /dev/null @@ -1,59 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -ExchangeRate = proxyquire('../../src/coinify/exchange-rate', stubs) - -describe "Coinify: Exchange Rate", -> - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "constructor", -> - it "coinify reference must be preserved", -> - fakeCoinify = {} - e = new ExchangeRate(fakeCoinify) - expect(e._coinify).toBe(fakeCoinify) - - describe "get", -> - it "must obtain the right rate", (done) -> - - coinify = { - GET: (method, object) -> { - then: (cb) -> - cb({ rate: 1000 }) - } - } - - baseC = 'litecoin' - quoteC = 'dogecoin' - e = new ExchangeRate(coinify) - promise = e.get(baseC, quoteC) - expect(promise).toBeResolvedWith(1000, done) - - describe "get", -> - it "coinify.GET must be called", (done) -> - coinify = { - GET: (method, object) -> { - then: (cb) -> - cb({ rate: 1000 }) - } - } - spyOn(coinify, "GET").and.callThrough() - baseC = 'litecoin' - quoteC = 'dogecoin' - e = new ExchangeRate(coinify) - promise = e.get(baseC, quoteC) - argument = { - baseCurrency: baseC, - quoteCurrency: quoteC - } - testCalls = () -> - expect(coinify.GET).toHaveBeenCalledWith('rates/approximate', argument) - promise - .then(testCalls) - .then(done) diff --git a/tests/coinify/coinify_helpers_spec.js.coffee b/tests/coinify/coinify_helpers_spec.js.coffee deleted file mode 100644 index 0df26ab48..000000000 --- a/tests/coinify/coinify_helpers_spec.js.coffee +++ /dev/null @@ -1,54 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -Helpers = proxyquire('../../src/coinify/helpers', stubs) - -describe "CoinifyHelpers", -> - describe "isNumber", -> - it "should be true for 1.1", -> - expect(Helpers.isNumber(1.1)).toBeTruthy() - - it "should be false for 'a'", -> - expect(Helpers.isNumber('a')).toBeFalsy() - - describe "isInteger", -> - it "should be true for 1", -> - expect(Helpers.isInteger(1)).toBeTruthy() - - it "should be true for -1", -> - expect(Helpers.isInteger(-1)).toBeTruthy() - - it "should be false for 1.1", -> - expect(Helpers.isInteger(1.1)).toBeFalsy() - - it "should be true for 1.0", -> - expect(Helpers.isInteger(1.0)).toBeTruthy() - - describe "isPositiveInteger", -> - it "should be true for 1", -> - expect(Helpers.isPositiveInteger(1)).toBeTruthy() - - it "should be false for -1", -> - expect(Helpers.isPositiveInteger(-1)).toBeFalsy() - - it "should be true for 0", -> - expect(Helpers.isPositiveInteger(1)).toBeTruthy() - - describe "toCents", -> - it "should multiply by 100", -> - expect(Helpers.toCents(1.03)).toEqual(103) - - describe "toSatoshi", -> - it "should multiply by 100,000,000", -> - expect(Helpers.toSatoshi(0.001)).toEqual(100000) - - describe "fromCents", -> - it "should divide by 100", -> - expect(Helpers.fromCents(103)).toEqual(1.03) - - describe "fromSatoshi", -> - it "should divide by 100,000,000", -> - expect(Helpers.fromSatoshi(100000)).toEqual(0.001) diff --git a/tests/coinify/coinify_kyc_spec.js.coffee b/tests/coinify/coinify_kyc_spec.js.coffee deleted file mode 100644 index a3ad652fe..000000000 --- a/tests/coinify/coinify_kyc_spec.js.coffee +++ /dev/null @@ -1,106 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -CoinifyKYC = proxyquire('../../src/coinify/kyc', stubs) -o = undefined -coinify = undefined - -beforeEach -> - o = { - id: "id" - state: "pending" - externalId: "externalId" - createTime: "2016-07-07T12:10:19Z" - updateTime: "2016-07-07T12:11:36Z" - } - JasminePromiseMatchers.install() - -afterEach -> - JasminePromiseMatchers.uninstall() - -describe "KYC", -> - - describe "constructor", -> - api = {} - - it "must put everything in place", -> - k = new CoinifyKYC(o, api, null, coinify) - expect(k._api).toBe(api) - expect(k._id).toBe(o.id) - expect(k._iSignThisID).toBe(o.externalId) - - it "should warn if there is an unknown state type", -> - o.state = "unknown" - spyOn(window.console, 'warn') - new CoinifyKYC(o, api, null, coinify) - expect(window.console.warn).toHaveBeenCalled() - expect(window.console.warn.calls.argsFor(0)[1]).toEqual('unknown') - - describe "fetch all", -> - it "should call authGET with the correct arguments ", (done) -> - - coinify = { - _kycs: [] - authGET: (method, params) -> Promise.resolve([o,o,o,o]), - } - spyOn(coinify, "authGET").and.callThrough() - - promise = CoinifyKYC.fetchAll(coinify) - testCalls = () -> - expect(coinify.authGET).toHaveBeenCalledWith('kyc') - promise - .then(testCalls) - .then(done) - .catch(console.log) - - describe "refresh", -> - it "should call authGET with the correct arguments ", (done) -> - - coinify = { - _kycs: [] - } - api = { - authGET: (method) -> Promise.resolve([o,o,o,o]), - } - spyOn(api, "authGET").and.callThrough() - k = new CoinifyKYC(o, api, null, coinify) - - promise = k.refresh() - testCalls = () -> - expect(api.authGET).toHaveBeenCalledWith('kyc/' + k._id) - promise - .then(testCalls) - .then(done) - .catch(console.log) - - describe "trigger", -> - it "should authPOST traders/me/kyc with the correct arguments ", (done) -> - - coinify = { - _kycs: [] - authPOST: (method) -> Promise.resolve(o), - } - spyOn(coinify, "authPOST").and.callThrough() - - promise = CoinifyKYC.trigger(coinify) - testCalls = () -> - expect(coinify.authPOST).toHaveBeenCalledWith('traders/me/kyc') - promise - .then(testCalls) - .then(done) - .catch(console.log) - - describe "instance", -> - k = undefined - beforeEach -> - api = {} - k = new CoinifyKYC(o, api, null, coinify) - - it "should have getters", -> - expect(k.id).toBe(o.id) - expect(k.state).toBe(o.state) - expect(k.iSignThisID).toBe(o.externalId) - expect(k.createdAt).toEqual(new Date(o.createTime)) - expect(k.updatedAt).toEqual(new Date(o.updateTime)) diff --git a/tests/coinify/coinify_level_spec.js.coffee b/tests/coinify/coinify_level_spec.js.coffee deleted file mode 100644 index 5cf458216..000000000 --- a/tests/coinify/coinify_level_spec.js.coffee +++ /dev/null @@ -1,47 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -Limits = () -> - {limits: 'limits_mock'} - -stubs = { - './limits' : Limits -} - -Level = proxyquire('../../src/coinify/level', stubs) - -describe "CoinifyLevel", -> - obj = undefined - level = undefined - - beforeEach -> - obj = - currency: "EUR" - feePercentage: 3 - limits: {} - requirements: {} - name: "1" - - describe "class", -> - describe "constructor", -> - it "should set properties", -> - level = new Level(obj) - expect(level._name).toEqual('1') - expect(level._requirements).toEqual({}) - expect(level._feePercentage).toEqual(3) - expect(level._currency).toEqual('EUR') - - it "should create a Limits object", -> - level = new Level(obj) - expect(level._limits).toEqual({limits: 'limits_mock'}) - - describe "instance", -> - beforeEach -> - level = new Level(obj) - - it "should have getters", -> - expect(level.currency).toEqual('EUR') - expect(level.name).toEqual('1') - expect(level.requirements).toEqual({}) - expect(level.limits).toEqual({limits: 'limits_mock'}) - expect(level.feePercentage).toEqual(3) diff --git a/tests/coinify/coinify_limits_spec.js.coffee b/tests/coinify/coinify_limits_spec.js.coffee deleted file mode 100644 index 79b70cfe7..000000000 --- a/tests/coinify/coinify_limits_spec.js.coffee +++ /dev/null @@ -1,52 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -Limits = proxyquire('../../src/coinify/limits', stubs) - -# Tests both limits.js and limit.js -describe "CoinifyLimits", -> - remainingObj = undefined - limitsObj = undefined - - remaining = undefined - limits = undefined - - beforeEach -> - - remainingObj = { - card: - in: 100 - bank: - in: 1000 - out: 1000 - } - - limitsObj = { - card: - in: - daily: 100 - bank: - in: - daily: 1000 - out: - daily: 1000 - } - - remaining = new Limits(remainingObj) - limits = new Limits(limitsObj) - - - describe "class", -> - describe "new Limits()", -> - it "should process remaining amounts", -> - expect(remaining.card.inRemaining).toBe(100) - expect(remaining.bank.inRemaining).toBe(1000) - expect(remaining.bank.outRemaining).toBe(1000) - - it "should process daily limit", -> - expect(limits.card.inDaily).toBe(100) - expect(limits.bank.inDaily).toBe(1000) - expect(limits.bank.outDaily).toBe(1000) diff --git a/tests/coinify/coinify_payment_method_spec.js.coffee b/tests/coinify/coinify_payment_method_spec.js.coffee deleted file mode 100644 index 59c4a8e06..000000000 --- a/tests/coinify/coinify_payment_method_spec.js.coffee +++ /dev/null @@ -1,106 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -stubs = { -} - -PaymentMethod = proxyquire('../../src/coinify/payment-method', stubs) -o = undefined -coinify = undefined -b = undefined - -beforeEach -> - o = { - inMedium: "inMedium" - outMedium: "outMedium" - name: "name" - inCurrencies: "inCurrencies" - outCurrencies: "outCurrencies" - inCurrency: "inCurrency" - outCurrency: "outCurrency" - inFixedFee: 0.01 - outFixedFee: 0 - inPercentageFee: 3 - outPercentageFee: 0 - } - JasminePromiseMatchers.install() - -afterEach -> - JasminePromiseMatchers.uninstall() - -describe "Payment method", -> - - describe "constructor", -> - it "must put everything on place", -> - b = new PaymentMethod(o) - expect(b._inMedium).toBe(o.inMedium) - expect(b._outMedium).toBe(o.outMedium) - expect(b._name).toBe(o.name) - expect(b._inCurrencies).toBe(o.inCurrencies) - expect(b._outCurrencies).toBe(o.outCurrencies) - expect(b._inCurrency).toBe(o.inCurrency) - expect(b._outCurrency).toBe(o.outCurrency) - expect(b._inFixedFee).toBe(o.inFixedFee * 100) - expect(b._outFixedFee).toBe(o.outFixedFee * 100) - expect(b._inPercentageFee).toBe(o.inPercentageFee) - expect(b._outPercentageFee).toBe(o.outPercentageFee) - - it "must correctly round the fixed fee for fiat to BTC", -> - o.inFixedFee = 35.05 # 35.05 * 100 = 3504.9999999999995 in javascript - o.outFixedFee = 35.05 # 35.05 * 100 = 3504.9999999999995 in javascript - b = new PaymentMethod(o) - expect(b.inFixedFee).toEqual(3505) - expect(b.outFixedFee).toEqual(3505000000) - - it "must correctly round the fixed fee for BTC to fiat", -> - o.inCurrency = "BTC" - o.outCurrency = "EUR" - o.inFixedFee = 35.05 - o.outFixedFee = 35.05 - b = new PaymentMethod(o) - expect(b.inFixedFee).toEqual(3505000000) - expect(b.outFixedFee).toEqual(3505) - - describe "fetch all", -> - it "should authGET trades/payment-methods with the correct arguments", (done) -> - coinify = { - authGET: (method, params) -> Promise.resolve([o,o,o,o]), - } - spyOn(coinify, "authGET").and.callThrough() - - promise = PaymentMethod.fetchAll('EUR', 'BTC', coinify) - argument = { - inCurrency: 'EUR', - outCurrency: 'BTC' - } - testCalls = () -> - expect(coinify.authGET).toHaveBeenCalledWith('trades/payment-methods', argument) - promise - .then(testCalls) - .then(done) - - describe "instance", -> - it "should have getters", -> - b = new PaymentMethod(o) - expect(b.inMedium).toBe(o.inMedium) - expect(b.outMedium).toBe(o.outMedium) - expect(b.name).toBe(o.name) - expect(b.inCurrencies).toBe(o.inCurrencies) - expect(b.outCurrencies).toBe(o.outCurrencies) - expect(b.inCurrency).toBe(o.inCurrency) - expect(b.outCurrency).toBe(o.outCurrency) - expect(b.inFixedFee).toBe(o.inFixedFee * 100) - expect(b.outFixedFee).toBe(o.outFixedFee * 100) - expect(b.inPercentageFee).toBe(o.inPercentageFee) - expect(b.outPercentageFee).toBe(o.outPercentageFee) - - describe "calculateFee()", -> - beforeEach -> - b = new PaymentMethod(o) - - it "should set fee, given a quote", -> - b.calculateFee({baseAmount: -1000}) - expect(b.fee).toEqual(30 + 1) - - it "should set total, given a quote", -> - b.calculateFee({baseAmount: -1000}) - expect(b.total).toEqual(1000 + 30 + 1) diff --git a/tests/coinify/coinify_profile_spec.js.coffee b/tests/coinify/coinify_profile_spec.js.coffee deleted file mode 100644 index 952e872fa..000000000 --- a/tests/coinify/coinify_profile_spec.js.coffee +++ /dev/null @@ -1,188 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -Level = (obj) -> - { - name: obj.name - } - - -Limits = (obj) -> - { - card: - in: obj.card.in - } - -stubs = { - './level' : Level, - './limits' : Limits -} - -CoinifyProfile = proxyquire('../../src/coinify/profile', stubs) - -describe "CoinifyProfile", -> - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "class", -> - describe "new CoinifyProfile()", -> - - it "should keep a reference to API object", -> - api = {} - p = new CoinifyProfile(api) - expect(p._api).toBe(api) - - describe "instance", -> - p = undefined - coinify = undefined - profile = undefined - - beforeEach -> - profile = { - name: "John Do" - gender: 'male' - mobile: - countryCode: "1" - number: "1234" - address: - street: "Hoofdstraat 1" - city: "Amsterdam" - zipcode: "1111 AA" - state: "NH" - country: "NL" - } - coinify = { - authGET: (method) -> { - then: (cb) -> - cb({ - id: 1 - defaultCurrency: 'EUR' - email: "john@do.com" - profile: profile - feePercentage: 3 - currentLimits: - card: - in: 100 - bank: - in: 0 - - requirements: [] - level: {name: '1'} - nextLevel: {name: '2'} - }) - { - catch: () -> - } - } - authPATCH: () -> - } - spyOn(coinify, "authGET").and.callThrough() - spyOn(coinify, "authPATCH").and.callThrough() - p = new CoinifyProfile(coinify) - - describe "fetch()", -> - it "calls /traders/me", -> - p.fetch() - expect(coinify.authGET).toHaveBeenCalledWith('traders/me') - - it "populates the profile", -> - p.fetch() - expect(p.fullName).toEqual('John Do') - expect(p.defaultCurrency).toEqual('EUR') - expect(p.email).toEqual('john@do.com') - expect(p.gender).toEqual('male') - expect(p.mobile).toEqual('+11234') - expect(p.city).toEqual('Amsterdam') - expect(p.country).toEqual('NL') - expect(p.state).toEqual('NH') - expect(p.street).toEqual('Hoofdstraat 1') - expect(p.zipcode).toEqual('1111 AA') - expect(p.level.name).toEqual('1') - expect(p.nextLevel.name).toEqual('2') - expect(p.currentLimits.card.in).toEqual(100) - - describe "update()", -> - it "should update", -> - p.update({profile: {name: 'Jane Do'}}) - expect(coinify.authPATCH).toHaveBeenCalledWith('traders/me', {profile: { name: 'Jane Do' }}) - - describe "Setter", -> - beforeEach -> - spyOn(p, "update").and.callFake((values) -> - then: (cb) -> - if values.profile - profile.name = values.profile.name || profile.name - if values.profile.gender != undefined # can be null - profile.gender = values.profile.gender - if values.profile.address - profile.address.city = values.profile.address.city || profile.city - profile.address.country = values.profile.address.country || profile.country - profile.address.state = values.profile.address.state || profile.state - profile.address.street = values.profile.address.street || profile.street - profile.address.zipcode = values.profile.address.zipcode || profile.zipcode - - cb({profile: profile}) - { - catch: () -> - } - ) - - describe "setFullName", -> - it "should update", -> - p.setFullName('Jane Do') - expect(p.update).toHaveBeenCalledWith({profile: { name: 'Jane Do' }}) - expect(p.fullName).toEqual('Jane Do') - - describe "setGender", -> - it "should update", -> - p.setGender('female') - expect(p.update).toHaveBeenCalledWith({profile: { gender: 'female' }}) - expect(p.gender).toEqual('female') - - p.setGender('male') - expect(p.gender).toEqual('male') - - it "can be unset", -> - p.setGender(null) - expect(p.gender).toEqual(null) - - it "should only accept male, female or null", -> - try - p.setGender('wrong') - catch e - expect(e.toString()).toEqual('AssertionError: invalid gender') - - expect(p.update).not.toHaveBeenCalled() - - describe "setCity", -> - it "should update", -> - p.setCity('London') - expect(p.update).toHaveBeenCalledWith({profile: { address: { city: 'London'} }}) - expect(p.city).toEqual('London') - - describe "setCountry", -> - it "should update", -> - p.setCountry('GB') - expect(p.update).toHaveBeenCalledWith({profile: { address: { country: 'GB'} }}) - expect(p.country).toEqual('GB') - - describe "setState", -> - it "should update", -> - p.setState('LND') - expect(p.update).toHaveBeenCalledWith({profile: { address: { state: 'LND'} }}) - expect(p.state).toEqual('LND') - - describe "setStreet", -> - it "should update", -> - p.setStreet('Main St 1') - expect(p.update).toHaveBeenCalledWith({profile: { address: { street: 'Main St 1'} }}) - expect(p.street).toEqual('Main St 1') - - describe "setZipcode", -> - it "should update", -> - p.setZipcode('1234') - expect(p.update).toHaveBeenCalledWith({profile: { address: { zipcode: '1234'} }}) - expect(p.zipcode).toEqual('1234') diff --git a/tests/coinify/coinify_quote_spec.js.coffee b/tests/coinify/coinify_quote_spec.js.coffee deleted file mode 100644 index 04fad6bf7..000000000 --- a/tests/coinify/coinify_quote_spec.js.coffee +++ /dev/null @@ -1,175 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -PaymentMethod = { - fetchAll: () -> Promise.resolve([ - { - inMedium: 'bank' - calculateFee: this.prototype.calculateFee - } - ]) -} - -stubs = { - './payment-method' : PaymentMethod -} - -Quote = proxyquire('../../src/coinify/quote', stubs) - -describe "CoinifyQuote", -> - - obj = undefined - q = undefined - - beforeEach -> - - obj = { - id:353482, - baseCurrency:"EUR", - quoteCurrency:"BTC", - baseAmount:-12, - quoteAmount:0.02287838, - issueTime:"2016-09-02T13:50:55.648Z", - expiryTime:"2016-09-02T14:05:55.000Z" - } - q = new Quote(obj) - - describe "class", -> - describe "new Quote()", -> - it "should construct a Quote", -> - expect(q._expiresAt).toEqual(new Date(obj.expiryTime)) - expect(q._baseCurrency).toBe(obj.baseCurrency) - expect(q._quoteCurrency).toBe(obj.quoteCurrency) - expect(q._baseAmount).toBe(obj.baseAmount * 100) - expect(q._quoteAmount).toBe(obj.quoteAmount * 100000000) - expect(q._id).toBe(obj.id) - - it "must correctly round the fixed fee, fiat to BTC", -> - obj.baseAmount = 35.05 # 35.05 * 100 = 3504.9999999999995 in javascript - obj.quoteAmount = 0.00003505 - q = new Quote(obj) - expect(q.baseAmount).toEqual(3505) - expect(q.quoteAmount).toEqual(3505) - - it "must correctly round the fixed fee, BTC to fait", -> - obj.baseCurrency = "BTC" - obj.quoteCurrency = "EUR" - obj.baseAmount = 0.00003505 - obj.quoteAmount = 35.05 - q = new Quote(obj) - expect(q.baseAmount).toEqual(3505) - expect(q.quoteAmount).toEqual(3505) - - describe "getQuote()", -> - coinify = { - POST: (endpoint, data) -> - console.log(endpoint, data) - if endpoint == 'trades/quote' - Promise.resolve(data) - else - Promise.reject() - - authPOST: (endpoint, data) -> - console.log(endpoint, data) - if endpoint == 'trades/quote' - rate = undefined - if data.baseCurrency == 'BTC' - rate = 500 - else - rate = 0.002 - - Promise.resolve({ - baseCurrency: data.baseCurrency - quoteCurrency: data.quoteCurrency - baseAmount: data.baseAmount - quoteAmount: data.baseAmount * rate - }) - else - Promise.reject() - } - - beforeEach -> - spyOn(coinify, "POST").and.callThrough() - spyOn(coinify, "authPOST").and.callThrough() - - describe "without an account", -> - it "should POST /trades/quote", -> - Quote.getQuote(coinify, 1000, 'EUR', 'BTC') - expect(coinify.POST).toHaveBeenCalled() - expect(coinify.POST.calls.argsFor(0)[0]).toEqual('trades/quote') - - describe "with an account", -> - beforeEach -> - coinify.hasAccount = true - - it "should POST /trades/quote with credentials", -> - Quote.getQuote(coinify, 1000, 'EUR', 'BTC') - expect(coinify.authPOST).toHaveBeenCalled() - expect(coinify.authPOST.calls.argsFor(0)[0]).toEqual('trades/quote') - - it "should convert cents", -> - Quote.getQuote(coinify, 1000, 'EUR', 'BTC') - expect(coinify.authPOST.calls.argsFor(0)[1].baseAmount).toEqual(10) - - it "should convert satoshis", -> - Quote.getQuote(coinify, 100000000, 'BTC', 'EUR') - expect(coinify.authPOST.calls.argsFor(0)[1].baseAmount).toEqual(1) - - it "should check if the base currency is supported", -> - promise = Quote.getQuote(coinify, 100000000, 'XXX', 'BTC') - expect(promise).toBeRejected() - - it "should check if the quote currency is supported", -> - promise = Quote.getQuote(coinify, 100000000, 'EUR', 'DOGE') - expect(promise).toBeRejected() - - it "should resolve with the quote", (done) -> - checks = (res) -> - expect(res.quoteAmount).toEqual(50000) - done() - - promise = Quote.getQuote(coinify, 100000000, 'BTC', 'EUR') - .then(checks) - - expect(promise).toBeResolved() - - describe "instance", -> - describe "getters", -> - it "should work", -> - expect(q.expiresAt).toBe(q._expiresAt) - expect(q.baseCurrency).toBe(q._baseCurrency) - expect(q.quoteCurrency).toBe(q._quoteCurrency) - expect(q.baseAmount).toBe(q._baseAmount) - expect(q.quoteAmount).toBe(q._quoteAmount) - expect(q.id).toBe(q._id) - - describe "getPaymentMethods", -> - it "should cache the result", -> - q.paymentMethods = {} - spyOn(PaymentMethod, "fetchAll") - q.getPaymentMethods() - expect(PaymentMethod.fetchAll).not.toHaveBeenCalled() - - it "should set .bank for the bank method", (done) -> - checks = (res) -> - expect(res.bank).toBeDefined() - done() - - q.getPaymentMethods().then(checks) - - it "should calculate fees", (done) -> - spyOn(PaymentMethod.prototype, "calculateFee").and.callFake( - () -> - ) - - checks = (res) -> - expect(PaymentMethod.prototype.calculateFee).toHaveBeenCalled() - done() - - q.getPaymentMethods().then(checks) - - describe "QA expire()", -> - it "should set expiration time to 3 seconds in the future", -> - originalExpiration = q.expiresAt - q.expire() - expect(q.expiresAt).not.toEqual(originalExpiration) diff --git a/tests/coinify/coinify_spec.js.coffee b/tests/coinify/coinify_spec.js.coffee deleted file mode 100644 index c0c629f15..000000000 --- a/tests/coinify/coinify_spec.js.coffee +++ /dev/null @@ -1,539 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -Quote = { - getQuote: (coinify, amount, baseCurrency, quoteCurrency) -> - Promise.resolve({ - baseAmount: amount, - baseCurrency: baseCurrency, - quoteCurrency: quoteCurrency - }) -} - -API = () -> - { - GET: () -> - POST: () -> - PATCH: () -> - } - -PaymentMethod = { - fetchAll: () -> -} - -CoinifyTrade = (obj) -> - obj -CoinifyTrade.spyableProcessTrade = () -> -tradesJSON = [ - { - id: 1 - state: "awaiting_transfer_in" - } -] -CoinifyTrade.fetchAll = () -> - Promise.resolve([ - { - id: tradesJSON[0].id - state: tradesJSON[0].state - process: CoinifyTrade.spyableProcessTrade - } - ]) -CoinifyTrade.monitorPayments = () -> -CoinifyTrade.buy = (quote) -> - Promise.resolve({amount: quote.baseAmount}) - -CoinifyProfile = () -> - fetch: () -> - this._did_fetch = true - -kycsJSON = [ - { - id: 1 - state: "pending" - } -] -CoinifyKYC = (obj) -> - obj -CoinifyKYC.fetchAll = () -> - Promise.resolve([ - { - id: kycsJSON[0].id - state: kycsJSON[0].state - } - ]) -CoinifyKYC.trigger = () -> - Promise.resolve() - -ExchangeDelegate = () -> - { - save: () -> Promise.resolve() - } - -stubs = { - './api' : API, - './quote' : Quote, - './payment-method' : PaymentMethod, - './trade' : CoinifyTrade, - './kyc' : CoinifyKYC, - './profile' : CoinifyProfile, - './exchange-delegate' : ExchangeDelegate -} - -Coinify = proxyquire('../../src/coinify/coinify', stubs) - -describe "Coinify", -> - - c = undefined - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "class", -> - describe "new Coinify()", -> - - it "should transform an Object to a Coinify", -> - c = new Coinify({auto_login: true}, {}) - expect(c.constructor.name).toEqual("Coinify") - - it "should use fields", -> - c = new Coinify({auto_login: true}, {}) - expect(c._auto_login).toEqual(true) - - it "should require a delegate", -> - expect(() -> new Coinify({auto_login: true})).toThrow() - - it "should deserialize trades", -> - c = new Coinify({ - auto_login: true, - trades: [{}] - }, {}) - expect(c.trades.length).toEqual(1) - - - describe "Coinify.new()", -> - it "sets autoLogin to true", -> - c = Coinify.new({}) - expect(c._auto_login).toEqual(true) - - it "should require a delegate", -> - expect(() -> Coinify.new()).toThrow() - - describe "instance", -> - beforeEach -> - c = Coinify.new({ - email: () -> "info@blockchain.com" - isEmailVerified: () -> true - getEmailToken: () -> "json-web-token" - save: () -> Promise.resolve() - }) - c.partnerId = 18 - c._debug = false - - spyOn(c._api, "POST").and.callFake((endpoint, data) -> - if endpoint == "signup/trader" - console.log("singup/trader") - if data.email == "duplicate@blockchain.com" - console.log("Duplicate, reject!") - Promise.reject("DUPLICATE_EMAIL") - else if data.email == "fail@blockchain.com" - Promise.reject("ERROR_MESSAGE") - else - Promise.resolve({ - trader: {id: "1"} - offlineToken: "offline-token" - }) - else - Promise.reject("Unknown endpoint") - ) - - describe "Getter", -> - describe "hasAccount", -> - it "should use offline_token to see if user has account", -> - c._offlineToken = undefined - expect(c.hasAccount).toEqual(false) - - c._offlineToken = "token" - expect(c.hasAccount).toEqual(true) - - describe "Setter", -> - - describe "autoLogin", -> - beforeEach -> - spyOn(c.delegate, "save").and.callThrough() - - it "should update", -> - c.autoLogin = false - expect(c.autoLogin).toEqual(false) - - it "should save", -> - c.autoLogin = false - expect(c.delegate.save).toHaveBeenCalled() - - it "should check the input", -> - expect(() -> c.autoLogin = "1").toThrow() - - describe "debug", -> - it "should set debug", -> - c.debug = true - expect(c.debug).toEqual(true) - - it "should set debug flag on the delegate", -> - c._delegate = {debug: false} - c.debug = true - expect(c.delegate.debug).toEqual(true) - - it "should set debug flag on trades", -> - c._trades = [{debug: false}] - c.debug = true - expect(c.trades[0].debug).toEqual(true) - - describe "JSON serializer", -> - obj = - user: 1 - offline_token: "token" - auto_login: true - - p = new Coinify(obj, {}) - - it 'should serialize the right fields', -> - json = JSON.stringify(p, null, 2) - d = JSON.parse(json) - expect(d.user).toEqual(1) - expect(d.offline_token).toEqual("token") - expect(d.auto_login).toEqual(true) - - it 'should serialize trades', -> - p.trades = [] - json = JSON.stringify(p, null, 2) - d = JSON.parse(json) - expect(d.trades).toEqual([]) - - it 'should hold: fromJSON . toJSON = id', -> - json = JSON.stringify(c, null, 2) - b = new Coinify(JSON.parse(json), {}) - expect(json).toEqual(JSON.stringify(b, null, 2)) - - it 'should not serialize non-expected fields', -> - expectedJSON = JSON.stringify(c, null, 2) - c.rarefield = "I am an intruder" - json = JSON.stringify(c, null, 2) - expect(json).toEqual(expectedJSON) - - describe "signup", -> - it 'sets a user and offline token', (done) -> - checks = () -> - expect(c.user).toEqual("1") - expect(c._offlineToken).toEqual("offline-token") - - promise = c.signup('NL', 'EUR').then(checks) - - expect(promise).toBeResolved(done) - - it 'requires the country', -> - expect(c.signup('NL', 'EUR')).toBeResolved() - expect(c.signup(undefined, 'EUR')).toBeRejected() - - it 'requires the currency', -> - expect(c.signup('NL', 'EUR')).toBeResolved() - expect(c.signup('NL')).toBeRejected() - - it 'requires email', -> - c.delegate.email = () -> null - expect(c.signup('NL', 'EUR')).toBeRejected() - - it 'requires verified email', -> - c.delegate.isEmailVerified = () -> false - expect(c.signup('NL', 'EUR')).toBeRejected() - - it 'lets the user know if email is already registered', ((done) -> - c.delegate.email = () -> "duplicate@blockchain.com" - promise = c.signup('NL', 'EUR') - expect(promise).toBeRejectedWith("DUPLICATE_EMAIL", done) - ) - - it 'might fail for an unexpected reason', ((done) -> - c.delegate.email = () -> "fail@blockchain.com" - promise = c.signup('NL', 'EUR') - expect(promise).toBeRejectedWith("ERROR_MESSAGE", done) - ) - - describe 'getBuyQuote', -> - it 'should use Quote.getQuote', -> - spyOn(Quote, "getQuote").and.callThrough() - - c.getBuyQuote(1000, 'EUR', 'BTC') - - expect(Quote.getQuote).toHaveBeenCalled() - - it 'should use a negative amount', (done) -> - checks = (quote) -> - expect(quote.baseAmount).toEqual(-1000) - - promise = c.getBuyQuote(1000, 'EUR', 'BTC').then(checks) - - expect(promise).toBeResolved(done) - - it 'should set the quote currency to BTC for fiat base currency', (done) -> - checks = (quote) -> - expect(quote.quoteCurrency).toEqual('BTC') - - promise = c.getBuyQuote(1000, 'EUR').then(checks) - - expect(promise).toBeResolved(done) - - - it 'should set _lastQuote', (done) -> - checks = (quote) -> - expect(c._lastQuote.baseAmount).toEqual(-1000) - - promise = c.getBuyQuote(1000, 'EUR').then(checks) - - expect(promise).toBeResolved(done) - - describe 'buy()', -> - beforeEach -> - c._lastQuote = { - baseCurrency: 'EUR' - baseAmount: -1000 - expiresAt: new Date(new Date().getTime() + 100000) - } - - it 'should use CoinifyTrade.buy', -> - spyOn(CoinifyTrade, "buy").and.callThrough() - - c.buy(1000, 'EUR', 'card') - - expect(CoinifyTrade.buy).toHaveBeenCalled() - - it 'should check for a matching quote', -> - expect(() -> c.buy(1001, 'EUR', 'card')).toThrow() - expect(() -> c.buy(1000, 'USD', 'card')).toThrow() - - it 'should check for a quote that is still valid', -> - c._lastQuote.expiresAt = new Date(new Date().getTime() - 100000) - expect(() -> c.buy(1000, 'EUR', 'card')).toThrow() - - it 'should return the trade', (done) -> - checks = (res) -> - expect(res).toEqual({amount: -1000, debug: false}) - done() - - promise = c.buy(1000, 'EUR', 'card').then(checks) - expect(promise).toBeResolved() - - it "should save", (done) -> - spyOn(c.delegate, "save").and.callThrough() - - checks = () -> - expect(c.delegate.save).toHaveBeenCalled() - done() - - promise = c.buy(1000, 'EUR', 'card').then(checks) - expect(promise).toBeResolved() - - describe 'getBuyMethods()', -> - beforeEach -> - spyOn(PaymentMethod, 'fetchAll') - - it 'should get payment methods with BTC as out currency', -> - c.getBuyMethods() - expect(PaymentMethod.fetchAll).toHaveBeenCalled() - expect(PaymentMethod.fetchAll.calls.argsFor(0)[0]).not.toBeDefined() - expect(PaymentMethod.fetchAll.calls.argsFor(0)[1]).toEqual('BTC') - - describe 'getSellMethods()', -> - beforeEach -> - spyOn(PaymentMethod, 'fetchAll') - - it 'should get payment methods with BTC as in currency', -> - c.getSellMethods() - expect(PaymentMethod.fetchAll).toHaveBeenCalled() - expect(PaymentMethod.fetchAll.calls.argsFor(0)[0]).toEqual('BTC') - expect(PaymentMethod.fetchAll.calls.argsFor(0)[1]).not.toBeDefined() - - describe 'getBuyCurrencies()', -> - beforeEach -> - spyOn(c, 'getBuyMethods').and.callFake(() -> - Promise.resolve([ - { - inCurrencies: ["EUR","USD"], - }, - { - inCurrencies: ["EUR"], - } - ]) - ) - - it 'should return a list of currencies', (done) -> - checks = (res) -> - expect(res).toEqual(['EUR', 'USD']) - - promise = c.getBuyCurrencies().then(checks) - - expect(promise).toBeResolved(done) - - it 'should store the list', (done) -> - checks = (res) -> - expect(c.buyCurrencies).toEqual(['EUR', 'USD']) - done() - - promise = c.getBuyCurrencies().then(checks) - - describe 'getSellCurrencies()', -> - beforeEach -> - spyOn(c, 'getSellMethods').and.callFake(() -> - Promise.resolve([ - { - outCurrencies: ["EUR","USD"], - }, - { - outCurrencies: ["EUR"], - } - ]) - ) - - it 'should return a list of currencies', (done) -> - checks = (res) -> - expect(res).toEqual(['EUR', 'USD']) - - promise = c.getSellCurrencies().then(checks) - - expect(promise).toBeResolved(done) - - it 'should store the list', (done) -> - checks = (res) -> - expect(c.sellCurrencies).toEqual(['EUR', 'USD']) - done() - - promise = c.getSellCurrencies().then(checks) - - describe 'fetchProfile()', -> - it 'should call fetch() on profile', -> - spyOn(c._profile, "fetch") - c.fetchProfile() - expect(c._profile.fetch).toHaveBeenCalled() - - it 'profile should be null before', -> - expect(c.profile).toBeNull() - - it 'should set .profile', -> - c.fetchProfile() - expect(c.profile).not.toBeNull() - - describe 'monitorPayments()', -> - it 'should call CoinifyTrade.monitorPayments', -> - spyOn(CoinifyTrade, 'monitorPayments') - c.monitorPayments() - expect(CoinifyTrade.monitorPayments).toHaveBeenCalled() - - describe 'getTrades()', -> - it 'should call CoinifyTrade.fetchAll', -> - spyOn(CoinifyTrade, 'fetchAll').and.callThrough() - c.getTrades() - expect(CoinifyTrade.fetchAll).toHaveBeenCalled() - - it 'should store the trades', (done) -> - checks = (res) -> - expect(c._trades.length).toEqual(1) - - promise = c.getTrades().then(checks) - expect(promise).toBeResolved(done) - - it 'should resolve the trades', (done) -> - checks = (res) -> - expect(res.length).toEqual(1) - done() - - promise = c.getTrades().then(checks) - - it 'should call process on each trade', (done) -> - spyOn(CoinifyTrade, 'spyableProcessTrade') - - checks = (res) -> - expect(CoinifyTrade.spyableProcessTrade).toHaveBeenCalled() - done() - - c.getTrades().then(checks) - - it "should update existing trades", (done) -> - c._trades = [ - { - _id: 1 - process: () -> - state: 'awaiting_transfer_in' - set: (obj) -> - this.state = obj.state - }, - { - _id: 2 - process: () -> - state: 'awaiting_transfer_in' - set: () -> - this.state = obj.state - } - ] - - tradesJSON[0].state = "completed_test" - - checks = () -> - expect(c._trades.length).toBe(2) - expect(c._trades[0].state).toEqual('completed_test') - done() - - c.getTrades().then(checks) - - describe 'getKYCs()', -> - it 'should call CoinifyKYC.fetchAll', -> - spyOn(CoinifyKYC, 'fetchAll').and.callThrough() - c.getKYCs() - expect(CoinifyKYC.fetchAll).toHaveBeenCalled() - - it 'should store the kycs', (done) -> - checks = (res) -> - expect(c.kycs.length).toEqual(1) - - promise = c.getKYCs().then(checks) - expect(promise).toBeResolved(done) - - it 'should resolve the kycs', (done) -> - checks = (res) -> - expect(res.length).toEqual(1) - done() - - promise = c.getKYCs().then(checks) - - it "should update existing kycs", (done) -> - c._kycs = [ - { - _id: 1 - process: () -> - state: 'pending' - set: (obj) -> - this.state = obj.state - }, - { - _id: 2 - process: () -> - state: 'pending' - set: () -> - this.state = obj.state - } - ] - - kycsJSON[0].state = "completed_test" - - checks = () -> - expect(c.kycs.length).toBe(2) - expect(c.kycs[0].state).toEqual('completed_test') - done() - - c.getKYCs().then(checks) - - - describe 'triggerKYC()', -> - it 'should call CoinifyKYC.trigger', -> - spyOn(CoinifyKYC, 'trigger').and.callThrough() - c.triggerKYC() - expect(CoinifyKYC.trigger).toHaveBeenCalled() diff --git a/tests/coinify/coinify_trade_spec.js.coffee b/tests/coinify/coinify_trade_spec.js.coffee deleted file mode 100644 index 3840fa1f2..000000000 --- a/tests/coinify/coinify_trade_spec.js.coffee +++ /dev/null @@ -1,681 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -BankAccount = () -> - {mock: "bank-account"} - -Quote = { - getQuote: (api, amount, currency) -> - Promise.resolve({quoteAmount: 0.071}) -} - -stubs = { - './bank-account' : BankAccount - './quote' : Quote -} - -CoinifyTrade = proxyquire('../../src/coinify/trade', stubs) - -describe "CoinifyTrade", -> - - tradeJSON = undefined - tradeJSON2 = undefined - - api = undefined - - - beforeEach -> - jasmine.clock().uninstall(); - jasmine.clock().install() - - tradeJSON = { - id: 1142 - inCurrency: "USD" - outCurrency: "BTC" - inAmount: 40 - transferIn: { - medium: "card" - details: { - paymentId: "05e18928-7b29-4b70-b29e-84cfe9fbc5ac" - } - } - transferOut: { - details: { - account: "19g1YFsoR5duHgTFcs4HKnjKHH7PgNqBJM" - } - } - outAmountExpected: 0.06454481 - state: "awaiting_transfer_in" - receiptUrl: "my url" - createTime: "2016-08-26T14:53:26.650Z" - updateTime: "2016-08-26T14:54:00.000Z" - quoteExpireTime: "2016-08-26T15:10:00.000Z" - } - - tradeJSON2 = JSON.parse(JSON.stringify(tradeJSON)) - tradeJSON2.id = 1143 - - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - jasmine.clock().uninstall() - - describe "class", -> - describe "new CoinifyTrade()", -> - delegate = { - getReceiveAddress: () -> - } - - it "should keep a reference to the API", -> - api = {} - - t = new CoinifyTrade(tradeJSON, api, delegate) - expect(t._api).toBe(api) - expect(t._id).toBe(tradeJSON.id) - expect(t._inCurrency).toBe(tradeJSON.inCurrency) - expect(t._outCurrency).toBe(tradeJSON.outCurrency) - expect(t._medium).toBe(tradeJSON.transferIn.medium) - expect(t._receiveAddress).toBe(tradeJSON.transferOut.details.account) - expect(t._state).toBe(tradeJSON.state) - expect(t._iSignThisID).toBe(tradeJSON.transferIn.details.paymentId) - expect(t._receiptUrl).toBe(tradeJSON.receiptUrl) - - it "should warn if there is an unknown state type", -> - tradeJSON.state = "unknown" - spyOn(window.console, 'warn') - new CoinifyTrade(tradeJSON, api, delegate) - expect(window.console.warn).toHaveBeenCalled() - expect(window.console.warn.calls.argsFor(0)[1]).toEqual('unknown') - - describe "_checkOnce()", -> - _setTransactionHashCalled = undefined - trade = {} - - beforeEach -> - spyOn(CoinifyTrade, '_setTransactionHash').and.callFake(() -> - _setTransactionHashCalled = true - ) - - it "should resolve immedidatley if there are no transactions", (done) -> - coinifyDelegate = { - save: () -> Promise.resolve() - getReceiveAddress: () -> - } - - filter = () -> true - - promise = CoinifyTrade._checkOnce([], filter, coinifyDelegate).catch(console.log) - - expect(promise).toBeResolved(done) - - - it "should call _setTransactionHash", (done) -> - coinifyDelegate = { - save: () -> Promise.resolve() - getReceiveAddress: () -> - } - - filter = () -> true - - promise = CoinifyTrade._checkOnce([trade], filter, coinifyDelegate).catch(console.log) - - expect(promise).toBeResolved(done) - - expect(_setTransactionHashCalled).toEqual(true) - - describe "filteredTrades", -> - it "should return transactions that might still receive payment", -> - trades = [ - {state: "awaiting_transfer_in"} # might receive payment - {state: "cancelled"} # will never receive payment - ] - expected = [ - {state: "awaiting_transfer_in"}, - ] - expect(CoinifyTrade.filteredTrades(trades)).toEqual(expected) - - describe "_setTransactionHash", -> - trade = undefined - delegate = undefined - tx = {hash: 'tx-hash', confirmations: 0} - - beforeEach -> - delegate = - checkAddress: (address) -> - Promise.resolve(tx) - - trade = { - receiveAddress: "trade-address" - _coinifyDelegate: delegate - state: 'completed' - debug: true - _txHash: null - } - - it "should ask the delegate if a transaction exists for the trade address", -> - spyOn(delegate, "checkAddress").and.callThrough() - CoinifyTrade._setTransactionHash(trade, delegate) - expect(delegate.checkAddress).toHaveBeenCalledWith('trade-address') - - describe "for a test trade", -> - it "should set the hash if trade is completed", (done) -> - trade.state = 'completed_test' - - checks = () -> - expect(trade._txHash).toEqual('tx-hash') - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - it "should not override the hash if set earlier", (done) -> - trade.state = 'completed_test' - trade._txHash = 'tx-hash-before' - - checks = () -> - expect(trade._txHash).toEqual('tx-hash-before') - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - describe "for a real trade", -> - it "should set the hash if trade is completed", (done) -> - trade.state = 'completed' - - checks = () -> - expect(trade._txHash).toEqual('tx-hash') - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - it "should set the hash if trade is processing", (done) -> - trade.state = 'processing' - - checks = () -> - expect(trade._txHash).toEqual('tx-hash') - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - it "should not override the hash if set earlier", (done) -> - trade.state = 'completed' - trade._txHash = 'tx-hash-before' - - checks = () -> - expect(trade._txHash).toEqual('tx-hash-before') - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - it "should set the number of confirmations", (done) -> - trade.state = 'completed' - - checks = () -> - expect(trade._confirmations).toEqual(0) - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - it "should set _confirmed to true so it gets serialized", -> - trade.state = 'completed' - tx.confirmations = 6 - trade.confirmed = true # mock getter, the real one checks trade._confirmations - - checks = () -> - expect(trade._confirmed).toEqual(true) - done() - - CoinifyTrade._setTransactionHash(trade, delegate).then(checks) - - describe "_monitorWebSockets", -> - it "should call _monitorAddress() on each trade", -> - trades = [{ - _monitorAddress: () -> - }] - spyOn(trades[0], "_monitorAddress") - filter = () -> true - CoinifyTrade._monitorWebSockets(trades, filter) - expect(trades[0]._monitorAddress).toHaveBeenCalled() - - describe "monitorPayments", -> - delegate = { - } - - trade1 = { - state: "cancelled" - delegate: delegate - } - trade2 = { - state: "awaiting_transfer_in" - delegate: delegate - } - trades = [trade1, trade2] - - - beforeEach -> - spyOn(CoinifyTrade, "_checkOnce").and.callFake(() -> - Promise.resolve() - ) - - it "should call _checkOnce with trades", -> - CoinifyTrade.monitorPayments(trades, delegate) - expect(CoinifyTrade._checkOnce).toHaveBeenCalled() - expect(CoinifyTrade._checkOnce.calls.argsFor(0)[0]).toEqual(trades) - - it "should call _monitorWebSockets with relevant trades", (done) -> - spyOn(CoinifyTrade , '_monitorWebSockets').and.callFake(() -> done()) - CoinifyTrade.monitorPayments(trades, delegate) - - describe "_monitorAddress", -> - it "pending", -> - - describe "instance", -> - profile = undefined - trade = undefined - exchangeDelegate = undefined - - beforeEach -> - profile = { - name: "John Do" - gender: 'male' - mobile: - countryCode: "1" - number: "1234" - address: - street: "Hoofdstraat 1" - city: "Amsterdam" - zipcode: "1111 AA" - state: "NH" - country: "NL" - } - - exchangeDelegate = { - reserveReceiveAddress: () -> { receiveAddress: "1abcd", commit: -> } - removeLabeledAddress: () -> - releaseReceiveAddress: () -> - commitReceiveAddress: () -> - save: () -> Promise.resolve() - deserializeExtraFields: () -> - getReceiveAddress: () -> - serializeExtraFields: () -> - } - - api = { - authGET: (method) -> - Promise.resolve({ - id: 1 - defaultCurrency: 'EUR' - email: "john@do.com" - profile: profile - feePercentage: 3 - currentLimits: - card: - in: - daily:100 - bank: - in: - daily:0 - yearly:0 - out: - daily: 100 - yearly:1000 - - requirements: [] - level: {name: '1'} - nextLevel: {name: '2'} - state: 'awaiting_transfer_in' - }) - authPOST: () -> Promise.resolve('something') - } - spyOn(api, "authGET").and.callThrough() - spyOn(api, "authPOST").and.callThrough() - trade = new CoinifyTrade(tradeJSON, api, exchangeDelegate) - - describe "getters", -> - it "should have some simple ones restored from trades JSON", -> - trade = new CoinifyTrade({ - id: 1142 - state: 'awaiting_transfer_in' - tx_hash: null - confirmed: false - is_buy: true - }, api, exchangeDelegate) - - expect(trade.id).toEqual(1142) - expect(trade.state).toEqual('awaiting_transfer_in') - expect(trade.confirmed).toEqual(false) - expect(trade.isBuy).toEqual(true) - expect(trade.txHash).toEqual(null) - - it "should have more simple ones loaded from API", -> - trade = new CoinifyTrade(tradeJSON, api, exchangeDelegate) - expect(trade.id).toEqual(1142) - expect(trade.iSignThisID).toEqual('05e18928-7b29-4b70-b29e-84cfe9fbc5ac') - expect(trade.quoteExpireTime).toEqual(new Date('2016-08-26T15:10:00.000Z')) - expect(trade.createdAt).toEqual(new Date('2016-08-26T14:53:26.650Z')) - expect(trade.updatedAt).toEqual(new Date('2016-08-26T14:54:00.000Z')) - expect(trade.inCurrency).toEqual('USD') - expect(trade.outCurrency).toEqual('BTC') - expect(trade.inAmount).toEqual(4000) - expect(trade.medium).toEqual('card') - expect(trade.state).toEqual('awaiting_transfer_in') - expect(trade.sendAmount).toEqual(0) - expect(trade.outAmount).toEqual(0) - expect(trade.outAmountExpected).toEqual(6454481) - expect(trade.receiptUrl).toEqual('my url') - expect(trade.receiveAddress).toEqual('19g1YFsoR5duHgTFcs4HKnjKHH7PgNqBJM') - expect(trade.bitcoinReceived).toEqual(false) - expect(trade.confirmed).toEqual(false) - expect(trade.isBuy).toEqual(true) - expect(trade.txHash).toEqual(null) - - it "should have a bank account for a bank trade", -> - tradeJSON.transferIn = { - medium: "bank" - details: {} # Bank account details are mocked - } - trade = new CoinifyTrade(tradeJSON, api, exchangeDelegate) - expect(trade.bankAccount).toEqual({mock: "bank-account"}) - - describe "deserialize from trades JSON", -> - beforeEach -> - tradeJSON = { - id: 1142 - state: 'awaiting_transfer_in' - tx_hash: 'hash' - confirmed: false - is_buy: true - } - - it "should ask the delegate to deserialize extra fields", -> - - spyOn(exchangeDelegate, "deserializeExtraFields") - new CoinifyTrade(tradeJSON, api, exchangeDelegate) - expect(exchangeDelegate.deserializeExtraFields).toHaveBeenCalled() - - it "should pass in self, so delegate can set extra fields", -> - tradeJSON.extra = "test" - exchangeDelegate.deserializeExtraFields = (deserialized, t) -> - t.extra = deserialized.extra - - trade = new CoinifyTrade(tradeJSON, api, exchangeDelegate) - expect(trade.extra).toEqual('test') - - describe "serialize", -> - it "should store several fields", -> - trade._txHash = "hash" - expect(JSON.stringify(trade)).toEqual(JSON.stringify({ - id: 1142 - state: 'awaiting_transfer_in' - tx_hash: 'hash' - confirmed: false - is_buy: true - })) - - it "should ask the delegate to store more fields", -> - spyOn(trade._coinifyDelegate, "serializeExtraFields") - JSON.stringify(trade) - expect(trade._coinifyDelegate.serializeExtraFields).toHaveBeenCalled() - - it "should serialize any fields added by the delegate", -> - trade._coinifyDelegate.serializeExtraFields = (t) -> - t.extra_field = 'test' - - s = JSON.stringify(trade) - expect(JSON.parse(s).extra_field).toEqual('test') - - describe "debug", -> - it "can be set", -> - trade.debug = true - expect(trade.debug).toEqual(true) - - describe "isBuy", -> - it "should equal _is_buy if set", -> - trade._is_buy = false - expect(trade.isBuy).toEqual(false) - - trade._is_buy = true - expect(trade.isBuy).toEqual(true) - - it "should default to true for older test wallets", -> - trade._is_buy = undefined - trade._outCurrency = undefined - expect(trade.isBuy).toEqual(true) - - it "should be true if out currency is BTC", -> - trade._is_buy = undefined - trade._outCurrency = 'BTC' - expect(trade.isBuy).toEqual(true) - - trade._outCurrency = 'EUR' - expect(trade.isBuy).toEqual(false) - - describe "set(obj)", -> - it "set new object and does not change id or date", -> - oldId = tradeJSON.id - oldTimeStamp = trade._createdAt - tradeJSON.id = 100 - tradeJSON.inCurrency = "monopoly" - trade.set(tradeJSON) - expect(trade._id).toBe(oldId) - expect(trade._createdAt).toEqual(oldTimeStamp) - expect(trade._inCurrency).toBe(tradeJSON.inCurrency) - - it "should round correctly for buy", -> - tradeJSON.inAmount = 35.05 - tradeJSON.transferIn.sendAmount = 35.05 - tradeJSON.outAmount = 0.00003505 - tradeJSON.outAmountExpected = 0.00003505 - - trade.set(tradeJSON) - expect(trade.inAmount).toEqual(3505) - expect(trade.sendAmount).toEqual(3505) - expect(trade.outAmount).toEqual(3505) - expect(trade.outAmountExpected).toEqual(3505) - - it "should round correctly for sell", -> - tradeJSON.inCurrency = 'BTC' - tradeJSON.outCurrency = 'EUR' - tradeJSON.inAmount = 0.00003505 - tradeJSON.transferIn.sendAmount = 0.00003505 - tradeJSON.outAmount = 35.05 - tradeJSON.outAmountExpected = 35.05 - - trade.set(tradeJSON) - expect(trade.inAmount).toEqual(3505) - expect(trade.sendAmount).toEqual(3505) - expect(trade.outAmount).toEqual(3505) - expect(trade.outAmountExpected).toEqual(3505) - - it "state should stay 'rejected' after card decline", -> - trade._isDeclined = true - trade.set(tradeJSON) # {state: 'awaiting_transfer_in'} - expect(trade.state).toEqual('rejected') - - describe "declined()", -> - beforeEach -> - trade.set(tradeJSON) - - it "should change state to rejected and set _isDeclined", -> - trade.declined() - expect(trade.state).toEqual('rejected') - expect(trade._isDeclined).toEqual(true) - - - describe "cancel()", -> - beforeEach -> - api.authPATCH = () -> - then: (cb) -> - cb({state: "cancelled"}) - spyOn(api, "authPATCH").and.callThrough() - - it "should cancel a trade and update its state", -> - trade.cancel() - expect(api.authPATCH).toHaveBeenCalledWith('trades/' + trade._id + '/cancel') - expect(trade._state).toBe('cancelled') - - it "should notifiy the delegate the receive address is no longer needed", -> - spyOn(exchangeDelegate, "releaseReceiveAddress") - trade.cancel() - expect(exchangeDelegate.releaseReceiveAddress).toHaveBeenCalled() - - describe "watchAddress()", -> - it "should set this._watchAddressResolve() and return promise with the total received", (done) -> - testBitcoinReceived = (bit_rec) -> - expect(bit_rec).toBe(1714) - - trade.watchAddress() - .then(testBitcoinReceived) - .catch(console.log) - .then(done) - - trade._watchAddressResolve(1714) - - describe "fakeBankTransfer()", -> - it "should POST a fake bank-transfer", () -> - trade.fakeBankTransfer() - expect(api.authPOST).toHaveBeenCalledWith('trades/1142/test/bank-transfer', { - sendAmount: 40 - currency: 'USD' - }) - - describe "buy()", -> - quote = undefined - - beforeEach -> - spyOn(CoinifyTrade.prototype, "_monitorAddress").and.callFake(() ->) - quote = { id: 101 } - api.authPOST = () -> - Promise.resolve(tradeJSON) - - it "should POST the quote and resolve the trade", (done) -> - spyOn(api, "authPOST").and.callThrough() - testTrade = (t) -> - expect(api.authPOST).toHaveBeenCalled() - expect(t.id).toEqual(1142) - - promise = CoinifyTrade.buy(quote, 'bank', api, exchangeDelegate) - .then(testTrade) - - expect(promise).toBeResolved(done) - - it "should watch the address", (done) -> - checks = (trade) -> - expect(trade._monitorAddress).toHaveBeenCalled() - - promise = CoinifyTrade.buy(quote, 'bank', api, exchangeDelegate) - .then(checks) - - expect(promise).toBeResolved(done) - - - describe "fetchAll()", -> - beforeEach -> - spyOn(exchangeDelegate, "releaseReceiveAddress").and.callThrough() - - it "should fetch all the trades", (done) -> - api.authGET = () -> - then: (cb) -> - cb([tradeJSON,tradeJSON2]) - - check = (res) -> - expect(res.length).toBe(2) - done() - - promise = CoinifyTrade.fetchAll(api).then(check) - expect(promise).toBeResolved() - - describe "process", -> - beforeEach -> - spyOn(trade._coinifyDelegate, "releaseReceiveAddress") - - it "should ask delegate to release addresses for cancelled trades", -> - trade._state = 'cancelled' - trade.process() - expect(trade._coinifyDelegate.releaseReceiveAddress).toHaveBeenCalled() - - it "should not ask to release addresses for awaiting_transfer_in trades", -> - trade._state = 'awaiting_transfer_in' - trade.process() - expect(trade._coinifyDelegate.releaseReceiveAddress).not.toHaveBeenCalled() - - describe "btcExpected", -> - beforeEach -> - now = new Date(2016, 9, 25, 12, 10, 0) # 12:10:00 - jasmine.clock().mockDate(now); - trade.quoteExpireTime = new Date(2016, 9, 25, 12, 15, 0) # 12:15:00 - - it "should use the quote if that's still valid", -> - promise = trade.btcExpected() - expect(promise).toBeResolvedWith(0.06454481) - - describe "when quote expired", -> - beforeEach -> - trade._lastBtcExpectedGuessAt = new Date(2016, 9, 25, 12, 15, 15) # 12:15:15 - trade._lastBtcExpectedGuess = 0.07 - - it "should use the last value if quote expired less than a minute ago", -> - jasmine.clock().mockDate(new Date(2016, 9, 25, 12, 15, 45)) # 12:15:45 - - promise = trade.btcExpected() - expect(promise).toBeResolvedWith(0.07) - - it "should get and store quote", (done) -> - now = new Date(2016, 9, 25, 12, 16, 15) - jasmine.clock().mockDate(now) # 12:16:15 - spyOn(Quote, "getQuote").and.callThrough() - - checks = () -> - expect(trade._lastBtcExpectedGuessAt).toEqual(now) - expect(trade._lastBtcExpectedGuess).toEqual(0.071) - done() - - promise = trade.btcExpected().then(checks) - expect(promise).toBeResolvedWith(0.071) - - describe "expireQuote", -> - it "should expire the quote sooner", -> - now = new Date(2016, 9, 25, 11, 50, 0) - threeSeconds = new Date(2016, 9, 25, 11, 50, 3) - trade._quoteExpireTime = new Date(2016, 9, 25, 12, 0) - jasmine.clock().mockDate(now); - trade.expireQuote() - expect(trade.quoteExpireTime).toEqual(threeSeconds) - - describe "refresh()", -> - beforeEach -> - api.authGET = () -> - Promise.resolve({}) - - spyOn(api , "authGET").and.callThrough() - - it "should authGET /trades/:id and update the trade object", (done) -> - checks = () -> - expect(api.authGET).toHaveBeenCalledWith('trades/' + trade._id) - expect(trade.set).toHaveBeenCalled() - - trade.set = () -> Promise.resolve(trade) - spyOn(trade, "set").and.callThrough() - - promise = trade.refresh().then(checks) - - expect(promise).toBeResolved(done) - - - it "should save metadata", (done) -> - checks = () -> - expect(trade._coinifyDelegate.save).toHaveBeenCalled() - - trade.set = () -> Promise.resolve(trade) - spyOn(trade._coinifyDelegate, "save").and.callThrough() - promise = trade.refresh().then(checks) - - expect(promise).toBeResolved(done) - - it "should resolve with trade object", (done) -> - checks = (res) -> - expect(res).toEqual(trade) - - trade.set = () -> Promise.resolve(trade) - promise = trade.refresh().then(checks) - - expect(promise).toBeResolved(done) diff --git a/tests/data/transactions.json b/tests/data/transactions.json index 69d1c0d27..9285fbea1 100644 --- a/tests/data/transactions.json +++ b/tests/data/transactions.json @@ -148,5 +148,53 @@ "hash":"c5ea97247f6dc0a40a2c6d56a075f48ef7d034b0ae34bb51738d64ac8b0e2f46", "vout_sz":1, "result": -10000 + }, + "from_imported": { + "ver":1, + "inputs":[ + { + "sequence":4294967295, + "prev_out":{ + "spent":true, + "tx_index":178173345, + "type":0, + "addr":"1GpQRQAAMGuQ3AMcivFHDCPnV69qtQ65RZ", + "value":82786, + "n":0, + "script":"76a914ad80fcd0914b6c1b277e4b4acb8874a6ed26974d88ac" + }, + "script":"47304402202c894814a329bb07d9f0bd01d1317529255640fb04fb4bf9e0d176689c15fafd02204a2597edd2bc2b9e970bec3fdf67b7517c96f4f30157b8af0d41d29470b5cc860141046cd1423d7fea290740ac7eca4e298c8012b3911e38607931dea5eb3b0137431d75d857835122caaa34d2e1d23e31a6ffc9da460d9e3f1887856d6cb7edfe0b41" + } + ], + "block_height":431964, + "relayed_by":"127.0.0.1", + "out":[ + { + "spent":false, + "tx_index":178173457, + "type":0, + "addr":"16GJsXtwq5JsYDAFaTXGMgj4W178nT3Pqj", + "value":4968, + "n":0, + "script":"76a91439bdc95926a6c41f1a9d729b956aeef922b5425988ac" + }, + { + "spent":true, + "tx_index":178173457, + "type":0, + "addr":"1GpQRQAAMGuQ3AMcivFHDCPnV69qtQ65RZ", + "value":67818, + "n":1, + "script":"76a914ad80fcd0914b6c1b277e4b4acb8874a6ed26974d88ac" + } + ], + "lock_time":0, + "size":257, + "double_spend":false, + "time":1475088020, + "tx_index":178173457, + "vin_sz":1, + "hash":"275d655cd862298c1f86a0c6a01fe5fa5eb277851abf40d6057c8863f7a919c3", + "vout_sz":2 } } diff --git a/tests/exchange_delegate_spec.js b/tests/exchange_delegate_spec.js new file mode 100644 index 000000000..b78c49ddf --- /dev/null +++ b/tests/exchange_delegate_spec.js @@ -0,0 +1,353 @@ +let proxyquire = require('proxyquireify')(require); + +let delegate; + +let trade; + +let acc0 = { + labels: {}, + receiveIndex: 0, + receivingAddressesLabels: [], + receiveAddressAtIndex (i) { return `0-${i}`; }, + getLabelForReceivingAddress (i) { + return this.labels[i]; + }, + setLabelForReceivingAddress (i, label) { + this.labels[i] = label; + }, + removeLabelForReceivingAddress (i) { + this.labels[i] = undefined; + } +}; + +let MyWallet = { + wallet: { + hdwallet: { + accounts: [ + acc0 + ], + defaultAccount: acc0 + }, + accountInfo: { + email: 'info@blockchain.com', + mobile: '+1 55512341234', + isEmailVerified: true, + isMobileVerified: true + }, + external: { + save () {} + } + } +}; + +let emailVerified = true; +let mobileVerified = true; + +const API = { + getBalances () {}, + getHistory (addresses) { + if (addresses.indexOf('address-with-tx') > -1) { + return Promise.resolve({txs: [{hash: 'hash', block_height: 11}]}); + } else { + return Promise.resolve({txs: []}); + } + }, + + request (action, method, {fields}, headers) { + return new Promise((resolve, reject) => { + if ((action === 'GET') && (method === 'wallet/signed-token')) { + if (fields === 'email') { + if (emailVerified) { + resolve({success: true, token: 'json-web-token-email'}); + } else { + resolve({success: false}); + } + } + if (fields === 'email|mobile') { + if (emailVerified && mobileVerified) { + resolve({success: true, token: 'json-web-token-email-mobile'}); + } else { + resolve({success: false}); + } + } + if (emailVerified && (fields === 'email|wallet_age')) { + resolve({success: true, token: 'json-web-token-email-wallet-age'}); + } + if (emailVerified && (fields === 'mobile')) { + resolve({success: true, token: 'json-web-token-mobile'}); + } else { + resolve({success: false}); + } + } else { + reject('bad call'); + } + }); + } +}; + +let eventListener = { + callback: null +}; + +let WalletStore = { + addEventListener (callback) { + eventListener.callback = callback; + } +}; + +const TX = ({hash, block_height}) => ({ + hash, + confirmations: block_height - 10 // eslint-disable-line camelcase +}); + +let stubs = { + './wallet': MyWallet, + './api': API, + './wallet-store': WalletStore, + './wallet-transaction': TX +}; + +let ExchangeDelegate = proxyquire('../src/exchange-delegate', stubs); + +describe('ExchangeDelegate', () => { + describe('class', () => + describe('new ExchangeDelegate()', () => + it('...', () => pending()) + ) + ); + + describe('instance', () => { + beforeEach(() => { + trade = { + _account_index: 0 + }; + delegate = new ExchangeDelegate(MyWallet.wallet); + delegate.trades = [trade]; + }); + + describe('debug', () => + it('should set debug', () => { + delegate.debug = true; + expect(delegate.debug).toEqual(true); + }) + ); + + describe('getters', () => + it('trades should be an array', () => expect(delegate.trades.length).toBeDefined()) + ); + + describe('save()', () => + it('should call save on external', () => { + spyOn(MyWallet.wallet.external, 'save'); + delegate.save(); + expect(MyWallet.wallet.external.save).toHaveBeenCalled(); + }) + ); + + describe('email()', () => { + it('should get the users email', () => expect(delegate.email()).toEqual('info@blockchain.com')); + + it("should return null if the user doesn't have an email", () => { + MyWallet.wallet.accountInfo.email = null; + expect(delegate.email()).toEqual(null); + }); + }); + + describe('mobile()', () => { + it('should get the users mobile number', () => expect(delegate.mobile()).toEqual('+1 55512341234')); + + it("should return null if the user doesn't have a mobile number", () => { + MyWallet.wallet.accountInfo.mobile = null; + expect(delegate.mobile()).toEqual(null); + }); + }); + + describe('isEmailVerified()', () => + it('should be true is users email is verified', () => expect(delegate.isEmailVerified()).toEqual(true)) + ); + + describe('isMobileVerified()', () => + it('should be true is users mobile is verified', () => expect(delegate.isMobileVerified()).toEqual(true)) + ); + + describe('getToken()', () => { + afterEach(() => { + emailVerified = true; + mobileVerified = true; + }); + + it('should get the token', done => { + let promise = delegate.getToken('partner', {mobile: true}); + expect(promise).toBeResolvedWith('json-web-token-email-mobile', done); + }); + + it('should reject if email is not verified', done => { + emailVerified = false; + let promise = delegate.getToken('partner', {mobile: true}); + expect(promise).toBeRejected(done); + }); + + it('should reject if mobile is not verified', done => { + mobileVerified = false; + let promise = delegate.getToken('partner', {mobile: true}); + expect(promise).toBeRejected(done); + }); + }); + + describe('getReceiveAddress()', () => + it('should get the trades receive address', () => { + trade._account_index = 0; + trade._receive_index = 0; + expect(delegate.getReceiveAddress(trade)).toEqual('0-0'); + }) + ); + + describe('reserveReceiveAddress()', () => { + it('should return the first available address', () => expect(delegate.reserveReceiveAddress().receiveAddress).toEqual('0-0')); + + it('should fail if gap limit', () => { + MyWallet.wallet.hdwallet.accounts[0].receiveIndex = 19; + MyWallet.wallet.hdwallet.accounts[0].lastUsedReceiveIndex = 0; + delegate.trades = []; + expect(() => delegate.reserveReceiveAddress()).toThrow(new Error('gap_limit')); + }); + + describe('.commit()', () => { + let account; + + beforeEach(() => { + account = MyWallet.wallet.hdwallet.accounts[0]; + account.receiveIndex = 0; + account.lastUsedReceiveIndex = 0; + trade = { id: 1, _account_index: 0, _receive_index: 0 }; + }); + + it('should label the address', () => { + delegate.trades = []; + let reservation = delegate.reserveReceiveAddress(); + reservation.commit(trade); + expect(account.getLabelForReceivingAddress(0)).toEqual('Exchange order #1'); + }); + + it('should allow custom label prefix for each exchange', () => { + delegate.trades = []; + delegate.labelBase = 'Coinify order'; + let reservation = delegate.reserveReceiveAddress(); + reservation.commit(trade); + expect(account.getLabelForReceivingAddress(0)).toEqual('Coinify order #1'); + }); + + it('should append to existing label if at gap limit', () => { + delegate.trades = [{ id: 0, _receive_index: 16, receiveAddress: '0-16', state: 'completed' }]; + account.receiveIndex = 19; + let reservation = delegate.reserveReceiveAddress(); + reservation.commit(trade); + expect(account.getLabelForReceivingAddress(16)).toEqual('Exchange order #0, #1'); + }); + }); + }); + + describe('releaseReceiveAddress()', () => { + let account; + + beforeEach(() => { + account = MyWallet.wallet.hdwallet.accounts[0]; + account.receiveIndex = 1; + trade = { id: 1, receiveAddress: '0-16', _account_index: 0, _receive_index: 0 }; + }); + + it('should remove the label', () => { + account.labels[0] = 'Coinify order #1'; + delegate.releaseReceiveAddress(trade); + expect(account.labels[0]).not.toBeDefined(); + }); + + it('should remove one of multible ids in a label', () => { + account.labels[0] = 'Coinify order #0, 1'; + delegate.trades = [{ id: 0, _receive_index: 16, receiveAddress: '0-16', state: 'completed' }]; + delegate.releaseReceiveAddress(trade); + expect(account.labels[0]).toEqual('Coinify order #0'); + }); + }); + + describe('checkAddress()', () => { + it('should resolve with nothing if no transaction is found ', done => { + let promise = delegate.checkAddress('address'); + expect(promise).toBeResolvedWith(undefined, done); + }); + + it('should resolve with transaction', done => { + let promise = delegate.checkAddress('address-with-tx'); + expect(promise).toBeResolvedWith(jasmine.objectContaining({hash: 'hash', confirmations: 1}), done); + }); + }); + + describe('monitorAddress()', () => { + let e = + {callback () {}}; + + beforeEach(() => spyOn(e, 'callback')); + + it('should add an eventListener', () => { + spyOn(WalletStore, 'addEventListener'); + delegate.monitorAddress(); + expect(WalletStore.addEventListener).toHaveBeenCalled(); + }); + + it('should callback if transaction happens on address', () => { + delegate.monitorAddress('1abc', e.callback); + + eventListener.callback('on_tx_received', { + out: [{ + addr: '1abc' + }] + }); + + expect(e.callback).toHaveBeenCalled(); + }); + + it('should callback if transaction happens on a different address', () => { + e = {callback () {}}; + + spyOn(e, 'callback'); + + delegate.monitorAddress('1abc', e.callback); + + eventListener.callback('on_tx_received', { + out: [{ + addr: '1abcdef' + }] + }); + + expect(e.callback).not.toHaveBeenCalled(); + }); + }); + + describe('serializeExtraFields()', () => + it('should add receive account and index', () => { + trade = { + _account_index: 0, + _receive_index: 0 + }; + let obj = {}; + delegate.serializeExtraFields(obj, trade); + expect(obj.account_index).toEqual(0); + expect(obj.receive_index).toEqual(0); + }) + ); + + describe('deserializeExtraFields()', () => + it('should set the trades receive account and index', () => { + trade = { + }; + let obj = { + account_index: 0, + receive_index: 0 + }; + delegate.deserializeExtraFields(obj, trade); + expect(trade._account_index).toEqual(0); + expect(trade._receive_index).toEqual(0); + }) + ); + }); +}); diff --git a/tests/exchange_delegate_spec.js.coffee b/tests/exchange_delegate_spec.js.coffee deleted file mode 100644 index 80335ad88..000000000 --- a/tests/exchange_delegate_spec.js.coffee +++ /dev/null @@ -1,269 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -delegate = undefined - -trade = undefined - -acc0 = { - labels: {} - receiveIndex: 0 - receivingAddressesLabels: [] - receiveAddressAtIndex: (i) -> "0-" + i - getLabelForReceivingAddress: (i) -> - this.labels[i] - setLabelForReceivingAddress: (i, label) -> - this.labels[i] = label - removeLabelForReceivingAddress: (i) -> - this.labels[i] = undefined -} - -MyWallet = { - wallet: { - hdwallet: { - accounts: [ - acc0 - ] - defaultAccount: acc0 - } - accountInfo: - email: 'info@blockchain.com' - isEmailVerified: true - external: - save: () -> - } -} - -emailVerified = true - -API = - getBalances: () -> - getHistory: (addresses) -> - if addresses.indexOf('address-with-tx') > -1 - Promise.resolve({txs: [{hash: 'hash', block_height: 11}]}) - else - Promise.resolve({txs: []}) - - request: (action, method, data, headers) -> - return new Promise (resolve, reject) -> - if action == 'GET' && method == "wallet/signed-email-token" - if emailVerified - resolve({success: true, token: 'json-web-token'}) - else - resolve({success: false}) - else - reject('bad call') - -eventListener = { - callback: null -} - -WalletStore = { - addEventListener: (callback) -> - eventListener.callback = callback - -} - -TX = (tx) -> - { - hash: tx.hash - confirmations: tx.block_height - 10 - } - -stubs = { - './wallet': MyWallet, - './api': API, - './wallet-store': WalletStore, - './wallet-transaction': TX -} - -ExchangeDelegate = proxyquire('../src/exchange-delegate', stubs) - -describe "ExchangeDelegate", -> - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "class", -> - describe "new ExchangeDelegate()", -> - it "...", -> - pending() - - describe "instance", -> - beforeEach -> - trade = { - _account_index: 0 - } - delegate = new ExchangeDelegate(MyWallet.wallet) - delegate.trades = [trade] - - describe "debug", -> - it "should set debug", -> - delegate.debug = true - expect(delegate.debug).toEqual(true) - - describe "getters", -> - it "trades should be an array", -> - expect(delegate.trades.length).toBeDefined() - - describe "save()", -> - it "should call save on external", -> - spyOn(MyWallet.wallet.external, "save") - delegate.save() - expect(MyWallet.wallet.external.save).toHaveBeenCalled() - - describe "email()", -> - it "should get the users email", -> - expect(delegate.email()).toEqual('info@blockchain.com') - - it "should return null if the user doesn't have an email", -> - MyWallet.wallet.accountInfo.email = null - expect(delegate.email()).toEqual(null) - - describe "isEmailVerified()", -> - it "should be true is users email is verified", -> - expect(delegate.isEmailVerified()).toEqual(true) - - describe "getEmailToken()", -> - afterEach -> - emailVerified = true - - it 'should get the token', (done) -> - promise = delegate.getEmailToken() - expect(promise).toBeResolvedWith('json-web-token', done); - - it 'should reject if email is not verified', (done) -> - emailVerified = false - promise = delegate.getEmailToken() - expect(promise).toBeRejected(done); - - describe "getReceiveAddress()", -> - it "should get the trades receive address", -> - trade._account_index = 0 - trade._receive_index = 0 - expect(delegate.getReceiveAddress(trade)).toEqual('0-0') - - describe "reserveReceiveAddress()", -> - it "should return the first available address", -> - expect(delegate.reserveReceiveAddress().receiveAddress).toEqual('0-0') - - it "should fail if gap limit", () -> - MyWallet.wallet.hdwallet.accounts[0].receiveIndex = 19 - MyWallet.wallet.hdwallet.accounts[0].lastUsedReceiveIndex = 0 - delegate.trades = [] - expect(() -> delegate.reserveReceiveAddress()).toThrow(new Error('gap_limit')) - - describe ".commit()", -> - account = undefined - - beforeEach -> - account = MyWallet.wallet.hdwallet.accounts[0] - account.receiveIndex = 0 - account.lastUsedReceiveIndex = 0 - trade = { id: 1, _account_index: 0, _receive_index: 0 } - - it "should label the address", -> - delegate.trades = [] - reservation = delegate.reserveReceiveAddress() - reservation.commit(trade) - expect(account.getLabelForReceivingAddress(0)).toEqual("Coinify order #1") - - it "should append to existing label if at gap limit", -> - delegate.trades = [{ id: 0, _receive_index: 16, receiveAddress: '0-16', state: 'completed' }] - account.receiveIndex = 19 - reservation = delegate.reserveReceiveAddress() - reservation.commit(trade) - expect(account.getLabelForReceivingAddress(16)).toEqual("Coinify order #0, #1") - - describe "releaseReceiveAddress()", -> - account = undefined - - beforeEach -> - account = MyWallet.wallet.hdwallet.accounts[0] - account.receiveIndex = 1 - trade = { id: 1, receiveAddress: '0-16', _account_index: 0, _receive_index: 0} - - it "should remove the label", -> - account.labels[0] = "Coinify order #1" - delegate.releaseReceiveAddress(trade) - expect(account.labels[0]).not.toBeDefined() - - it "should remove one of multible ids in a label", -> - account.labels[0] = "Coinify order #0, 1" - delegate.trades = [{ id: 0, _receive_index: 16, receiveAddress: '0-16', state: 'completed' }] - delegate.releaseReceiveAddress(trade) - expect(account.labels[0]).toEqual("Coinify order #0") - - describe "checkAddress()", -> - it "should resolve with nothing if no transaction is found ", (done) -> - promise = delegate.checkAddress('address') - expect(promise).toBeResolvedWith(undefined, done) - - it "should resolve with transaction", (done) -> - promise = delegate.checkAddress('address-with-tx') - expect(promise).toBeResolvedWith(jasmine.objectContaining({hash: 'hash', confirmations: 1}), done) - - describe "monitorAddress()", -> - e = - callback: () -> - - beforeEach -> - spyOn(e, 'callback') - - it "should add an eventListener", -> - spyOn(WalletStore, "addEventListener") - delegate.monitorAddress() - expect(WalletStore.addEventListener).toHaveBeenCalled() - - it "should callback if transaction happens on address", -> - delegate.monitorAddress("1abc", e.callback) - - eventListener.callback('on_tx_received', { - out: [{ - addr: "1abc" - }] - }) - - expect(e.callback).toHaveBeenCalled() - - it "should callback if transaction happens on a different address", -> - e = - callback: () -> - - spyOn(e, 'callback') - - delegate.monitorAddress("1abc", e.callback) - - eventListener.callback('on_tx_received', { - out: [{ - addr: "1abcdef" - }] - }) - - expect(e.callback).not.toHaveBeenCalled() - - - describe "serializeExtraFields()", -> - it "should add receive account and index", -> - trade = { - _account_index: 0 - _receive_index: 0 - } - obj = {} - delegate.serializeExtraFields(obj, trade) - expect(obj.account_index).toEqual(0) - expect(obj.receive_index).toEqual(0) - - describe "deserializeExtraFields()", -> - it "should set the trades receive account and index", -> - trade = { - } - obj = { - account_index: 0 - receive_index: 0 - } - delegate.deserializeExtraFields(obj, trade) - expect(trade._account_index).toEqual(0) - expect(trade._receive_index).toEqual(0) diff --git a/tests/external_spec.js b/tests/external_spec.js new file mode 100644 index 000000000..fc3aecd87 --- /dev/null +++ b/tests/external_spec.js @@ -0,0 +1,128 @@ +let proxyquire = require('proxyquireify')(require); + +describe('External', () => { + let mockPayload = { + coinify: {}, + sfox: {} + }; + + let Metadata = { + fromMasterHDNode (n, masterhdnode) { + return { + create () {}, + fetch () { + return Promise.resolve(mockPayload); + } + }; + } + }; + + let Coinify = obj => { + if (!obj.trades) { + obj.trades = []; + } + return obj; + }; + + let SFOX = obj => { + if (!obj.trades) { + obj.trades = []; + } + return obj; + }; + + let ExchangeDelegate = () => ({}); + + Coinify.new = () => + ({ + trades: [] + }) + ; + + SFOX.new = () => + ({ + trades: [] + }) + ; + + let stubs = { + 'bitcoin-coinify-client': Coinify, + 'bitcoin-sfox-client': SFOX, + './metadata': Metadata, + './exchange-delegate': ExchangeDelegate + }; + + let External = proxyquire('../src/external', stubs); + + let wallet = { + hdwallet: { + getMasterHDNode () {} + } + }; + + let e; + + describe('class', () => + describe('new External()', () => + it('should transform an Object to an External', () => { + e = new External(wallet); + return expect(e.constructor.name).toEqual('External'); + }) + ) + ); + + return describe('instance', () => { + beforeEach(() => { + e = new External(wallet); + }); + + describe('fetch', () => { + it('should include partners if present', done => { + let promise = e.fetch().then(res => { + expect(e._coinify).toBeDefined(); + return expect(e._sfox).toBeDefined(); + }); + return expect(promise).toBeResolved(done); + }); + + it('should not cointain any partner by default', done => { + mockPayload = {}; + let promise = e.fetch().then(res => { + expect(e._coinify).toBeUndefined(); + return expect(e._sfox).toBeUndefined(); + }); + return expect(promise).toBeResolved(done); + }); + + return it('should not deserialize non-expected fields', done => { + mockPayload = {coinify: {}, rarefield: 'I am an intruder'}; + let promise = e.fetch().then(res => { + expect(e._coinify).toBeDefined(); + return expect(e._rarefield).toBeUndefined(); + }); + return expect(promise).toBeResolved(done); + }); + }); + + return describe('JSON serializer', () => { + beforeEach(() => { + e._coinify = {}; + e._sfox = {}; + }); + + it('should store partners', () => { + let json = JSON.stringify(e, null, 2); + return expect(json).toEqual(JSON.stringify({coinify: {}, sfox: {}}, null, 2)); + }); + + return it('should not serialize non-expected fields', () => { + e.rarefield = 'I am an intruder'; + let json = JSON.stringify(e, null, 2); + let obj = JSON.parse(json); + + expect(obj.coinify).toBeDefined(); + return expect(obj.rarefield).not.toBeDefined(); + }); + }); + }); +}); diff --git a/tests/external_spec.js.coffee b/tests/external_spec.js.coffee deleted file mode 100644 index 5b039f5cb..000000000 --- a/tests/external_spec.js.coffee +++ /dev/null @@ -1,110 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -MyWallet = { - wallet: { - syncWallet: () -> - } -} - -mockPayload = {coinify: {}} - -Metadata = (n) -> - { - create: () -> - fetch: () -> - Promise.resolve(mockPayload) - } - -Coinify = (obj) -> - if !obj.trades - obj.trades = [] - return obj - -ExchangeDelegate = () -> - {} - -Coinify.new = () -> - { - trades: [] - } - -stubs = { - './wallet': MyWallet, - './coinify/coinify' : Coinify, - './metadata' : Metadata, - './exchange-delegate' : ExchangeDelegate -} - -External = proxyquire('../src/external', stubs) - -describe "External", -> - - e = undefined - - beforeEach -> - spyOn(MyWallet, "syncWallet") - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "class", -> - describe "new External()", -> - it "should transform an Object to an External", -> - e = new External({coinify: {}}) - expect(e.constructor.name).toEqual("External") - - it "should include partners if present", (done) -> - e = new External() - promise = e.fetch().then((res) -> - expect(e._coinify).toBeDefined() - ) - expect(promise).toBeResolved(done) - - it "should not cointain any partner by default", (done) -> - mockPayload = {} - e = new External() - promise = e.fetch().then((res) -> - expect(e._coinify).toBeUndefined() - ) - expect(promise).toBeResolved(done) - - it 'should not deserialize non-expected fields', (done) -> - mockPayload = {coinify: {}, rarefield: "I am an intruder"} - e = new External() - promise = e.fetch().then((res) -> - expect(e._coinify).toBeDefined() - expect(e._rarefield).toBeUndefined() - ) - expect(promise).toBeResolved(done) - - describe "instance", -> - beforeEach -> - e = new External({}) - - describe "addCoinify", -> - - it "should initialize a Coinify object", -> - e.addCoinify() - expect(e.coinify).toBeDefined(); - - it "should check if already present", -> - e.addCoinify() - expect(() -> e.addCoinify()).toThrow() - - describe "JSON serializer", -> - beforeEach -> - e = new External() - e._coinify = {} - - it 'should store partners', -> - json = JSON.stringify(e, null, 2) - expect(json).toEqual(JSON.stringify({coinify: {}}, null, 2)) - - it 'should not serialize non-expected fields', -> - e.rarefield = "I am an intruder" - json = JSON.stringify(e, null, 2) - b = JSON.parse(json) - - expect(b.coinify).toBeDefined() - expect(b.rarefield).not.toBeDefined() diff --git a/tests/hdaccount_spec.js b/tests/hdaccount_spec.js new file mode 100644 index 000000000..eeca7cfa7 --- /dev/null +++ b/tests/hdaccount_spec.js @@ -0,0 +1,476 @@ +let proxyquire = require('proxyquireify')(require); +let MyWallet; +let HDAccount; + +describe('HDAccount', () => { + // account = HDAccount.fromExtPublicKey("xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq", undefined, "Example account"); + + let account; + let object = { + 'label': 'My great wallet', + 'archived': false, + 'xpriv': 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E', + 'xpub': 'xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq', + 'address_labels': [{'index': 0, 'label': 'root'}], + 'cache': { + 'receiveAccount': 'xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP', + 'changeAccount': 'xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m' + } + }; + + beforeEach(() => { + MyWallet = { + syncWallet () {}, + wallet: { + getHistory () {} + } + }; + + spyOn(MyWallet, 'syncWallet'); + spyOn(MyWallet.wallet, 'getHistory'); + }); + // account = new HDAccount(object) + + describe('Constructor', () => { + describe('without arguments', () => { + beforeEach(() => { + let KeyRing = () => ({init () {}}); + let KeyChain = {}; + let stubs = { './wallet': MyWallet, './keyring': KeyRing, './keychain': KeyChain }; + HDAccount = proxyquire('../src/hd-account', stubs); + }); + + it('should create an empty HDAccount with default options', () => { + account = new HDAccount(); + expect(account.balance).toEqual(null); + expect(account.archived).not.toBeTruthy(); + expect(account.active).toBeTruthy(); + expect(account.receiveIndex).toEqual(0); + expect(account.changeIndex).toEqual(0); + expect(account.maxLabeledReceiveIndex).toEqual(-1); + }); + + it('should create an HDAccount from AccountMasterKey', () => { + let accountZero = { + toBase58 () { return 'accountZeroBase58'; }, + neutered () { + return { + toBase58 () { return 'accountZeroNeuteredBase58'; } + }; + } + }; + + let a = HDAccount.fromAccountMasterKey(accountZero, 0, 'label'); + + expect(a.label).toEqual('label'); + expect(a._xpriv).toEqual('accountZeroBase58'); + expect(a._xpub).toEqual('accountZeroNeuteredBase58'); + }); + + it('should create an HDAccount from Wallet master key', () => { + let masterkey = { + deriveHardened (i) { + return { + deriveHardened (j) { + return { + deriveHardened (k) { + return { + toBase58 () { + return `m/${i}/${j}/${k}`; + }, + neutered () { + return {toBase58 () {}}; + } + }; + } + }; + } + }; + } + }; + + let a = HDAccount.fromWalletMasterKey(masterkey, 0, 'label'); + + expect(a._xpriv).toEqual('m/44/0/0'); + expect(a.label).toEqual('label'); + }); + }); + + it('should transform an Object to an HDAccount', () => { + let stubs = { './wallet': MyWallet }; + HDAccount = proxyquire('../src/hd-account', stubs); + account = new HDAccount(object); + expect(account.extendedPublicKey).toEqual(object.xpub); + expect(account.extendedPrivateKey).toEqual(object.xpriv); + expect(account.label).toEqual(object.label); + expect(account.archived).toEqual(object.archived); + expect(account.receiveIndex).toEqual(0); + expect(account.changeIndex).toEqual(0); + expect(account.n_tx).toEqual(0); + expect(account.balance).toEqual(null); + expect(account.keyRing).toBeDefined(); + expect(account.receiveAddress).toBeDefined(); + expect(account.changeAddress).toBeDefined(); + expect(account.receivingAddressesLabels.length).toEqual(1); + }); + }); + + describe('JSON serializer', () => + it('should hold: fromJSON . toJSON = id', () => { + let json1 = JSON.stringify(account, null, 2); + let racc = JSON.parse(json1, HDAccount.reviver); + let json2 = JSON.stringify(racc, null, 2); + expect(json1).toEqual(json2); + }) + ); + + describe('instance', () => { + beforeEach(() => { + let stubs = { './wallet': MyWallet }; + HDAccount = proxyquire('../src/hd-account', stubs); + account = new HDAccount(object); + }); + + describe('.incrementReceiveIndex', () => + it('should increment the received index', () => { + let initial = account.receiveIndex; + account.incrementReceiveIndex(); + let final = account.receiveIndex; + expect(final).toEqual(initial + 1); + }) + ); + + describe('.incrementReceiveIndexIfLast', () => { + it('should not increment the received index', () => { + account._receiveIndex = 10; + let initial = account.receiveIndex; + account.incrementReceiveIndexIfLast(5); + let final = account.receiveIndex; + expect(final).toEqual(initial); + }); + + it('should increment the received index', () => { + account._receiveIndex = 10; + let initial = account.receiveIndex; + account.incrementReceiveIndexIfLast(10); + let final = account.receiveIndex; + expect(final).toEqual(initial + 1); + }); + }); + + describe('.get/setLabelForReceivingAddress', () => { + it('should set the label sync and get the label', () => { + let fail = reason => console.log(reason); + + let success = () => {}; + + account.setLabelForReceivingAddress(10, 'my label').then(success).catch(fail); + expect(account._address_labels[10]).toEqual('my label'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + expect(account.getLabelForReceivingAddress(10)).toEqual('my label'); + }); + + it('should not set a non-valid label', () => { + let fail = reason => except(reason).toEqual('NOT_ALPHANUMERIC'); + + let success = () => {}; + + account.setLabelForReceivingAddress(10, 0).then(success).catch(fail); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should not set a label with a gap too wide', () => { + let fail = reason => except(reason).toEqual('GAP'); + + let success = () => {}; + + account.setLabelForReceivingAddress(100, 'my label').then(success).catch(fail); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('Setter', () => { + it('active shoud toggle archived', () => { + account.active = false; + expect(account.archived).toBeTruthy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + account.active = true; + expect(account.archived).toBeFalsy(); + }); + + it('archived should archive the account and sync wallet', () => { + account.archived = true; + expect(account.archived).toBeTruthy(); + expect(account.active).not.toBeTruthy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('archived should throw exception if is non-boolean set', () => { + let wrongSet = () => { account.archived = 'failure'; }; + expect(wrongSet).toThrow(); + }); + + it('archived should call MyWallet.sync.getHistory when set to false', () => { + account.archived = false; + expect(MyWallet.wallet.getHistory).toHaveBeenCalled(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('balance should be set and not sync wallet', () => { + account.balance = 100; + expect(account.balance).toEqual(100); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('balance should throw exception if is non-Number set', () => { + let wrongSet = () => { account.balance = 'failure'; }; + expect(wrongSet).toThrow(); + }); + + it('n_tx should be set and not sync wallet', () => { + account.n_tx = 100; + expect(account.n_tx).toEqual(100); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('n_tx should throw exception if is non-Number set', () => { + let wrongSet = () => { account.n_tx = 'failure'; }; + expect(wrongSet).toThrow(); + }); + + it('label should be set and sync wallet', () => { + account.label = 'my label'; + expect(account.label).toEqual('my label'); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('label should be valid', () => { + let test = () => { account.label = 0; }; + expect(test).toThrow(); + }); + + it('xpriv is read only', () => { + let wrongSet = () => { account.extendedPrivateKey = 'not allowed'; }; + expect(wrongSet).toThrow(); + }); + + it('xpub is read only', () => { + let wrongSet = () => { account.extendedPublicKey = 'not allowed'; }; + expect(wrongSet).toThrow(); + }); + + it('receiveAddress is read only', () => { + let wrongSet = () => { account.receiveAddress = 'not allowed'; }; + expect(wrongSet).toThrow(); + }); + + it('changeAddress is read only', () => { + let wrongSet = () => { account.changeAddress = 'not allowed'; }; + expect(wrongSet).toThrow(); + }); + + it('index is read only', () => { + let wrongSet = () => { account.index = 'not allowed'; }; + expect(wrongSet).toThrow(); + }); + + it('KeyRing is read only', () => { + let wrongSet = () => { account.keyRing = 'not allowed'; }; + expect(wrongSet).toThrow(); + }); + + it('lastUsedReceiveIndex must be a number', () => { + let invalid = () => { account.lastUsedReceiveIndex = '1'; }; + let valid = () => { account.lastUsedReceiveIndex = 1; }; + expect(invalid).toThrow(); + expect(account.lastUsedReceiveIndex).toEqual(0); + expect(valid).not.toThrow(); + expect(account.lastUsedReceiveIndex).toEqual(1); + }); + + it('lastUsedReceiveIndex must be a positive number', () => { + let invalid = () => { account.lastUsedReceiveIndex = -534.23; }; + expect(invalid).toThrow(); + expect(account.lastUsedReceiveIndex).toEqual(0); + }); + + it('receiveIndex must be a number', () => { + let invalid = () => { account.receiveIndex = '1'; }; + let valid = () => { account.receiveIndex = 1; }; + expect(invalid).toThrow(); + expect(account.receiveIndex).toEqual(0); + expect(valid).not.toThrow(); + expect(account.receiveIndex).toEqual(1); + }); + + it('receiveIndex must be a positive number', () => { + let invalid = () => { account.receiveIndex = -534.34; }; + expect(invalid).toThrow(); + expect(account.receiveIndex).toEqual(0); + }); + + it('changeIndex must be a number', () => { + let invalid = () => { account.changeIndex = '1'; }; + let valid = () => { account.changeIndex = 1; }; + expect(invalid).toThrow(); + expect(account.changeIndex).toEqual(0); + expect(valid).not.toThrow(); + expect(account.changeIndex).toEqual(1); + }); + + it('changeIndex must be a positive number', () => { + let invalid = () => { account.changeIndex = -534.234; }; + expect(invalid).toThrow(); + expect(account.changeIndex).toEqual(0); + }); + }); + + describe('Getter', () => { + it('maxLabeledReceiveIndex should return the highest labeled index', () => { + expect(account.maxLabeledReceiveIndex).toEqual(0); + + account.setLabelForReceivingAddress(1, 'label1'); + account.setLabelForReceivingAddress(10, 'label100'); + + expect(account.maxLabeledReceiveIndex).toEqual(10); + }); + + it('labeledReceivingAddresses should return all the labeled receiving addresses', () => { + expect(account.labeledReceivingAddresses.length).toEqual(1); + + account.setLabelForReceivingAddress(1, 'label1'); + account.setLabelForReceivingAddress(10, 'label100'); + + expect(account.labeledReceivingAddresses.length).toEqual(3); + }); + }); + + describe('.encrypt', () => { + beforeEach(() => { account = new HDAccount(object); }); + + it('should fail and don\'t sync when encryption fails', () => { + let wrongEnc = () => account.encrypt(() => null); + expect(wrongEnc).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should write in a temporary field and let the original key intact', () => { + let originalKey = account.extendedPrivateKey; + account.encrypt(() => 'encrypted key'); + expect(account._temporal_xpriv).toEqual('encrypted key'); + expect(account.extendedPrivateKey).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if watch only account', () => { + account._xpriv = null; + account.encrypt(() => 'encrypted key'); + expect(account.extendedPrivateKey).toEqual(null); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if no cipher provided', () => { + let originalKey = account.extendedPrivateKey; + account.encrypt(undefined); + expect(account.extendedPrivateKey).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('.decrypt', () => { + beforeEach(() => { account = new HDAccount(object); }); + + it('should fail and don\'t sync when decryption fails', () => { + let wrongEnc = () => account.decrypt(() => null); + expect(wrongEnc).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should write in a temporary field and let the original key intact', () => { + let originalKey = account.extendedPrivateKey; + account.decrypt(() => 'decrypted key'); + expect(account._temporal_xpriv).toEqual('decrypted key'); + expect(account.extendedPrivateKey).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if watch only account', () => { + account._xpriv = null; + account.decrypt(() => 'decrypted key'); + expect(account.extendedPrivateKey).toEqual(null); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should do nothing if no cipher provided', () => { + let originalKey = account.extendedPrivateKey; + account.decrypt(undefined); + expect(account.extendedPrivateKey).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('.persist', () => { + beforeEach(() => { account = new HDAccount(object); }); + + it('should do nothing if temporary is empty', () => { + let originalKey = account.extendedPrivateKey; + account.persist(); + expect(account.extendedPrivateKey).toEqual(originalKey); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should swap and delete if we have a temporary value', () => { + account._temporal_xpriv = 'encrypted key'; + let temp = account._temporal_xpriv; + account.persist(); + expect(account.extendedPrivateKey).toEqual(temp); + expect(account._temporal_xpriv).not.toBeDefined(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('.removeLabelForReceivingAddress', () => + it('should remove the label and sync the wallet', () => { + let fail = reason => console.log(reason); + + let resolve = () => {}; + + account.setLabelForReceivingAddress(0, 'Savings').then(resolve).catch(fail); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + account.removeLabelForReceivingAddress(0); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + expect(account.getLabelForReceivingAddress(0)).not.toEqual('Savings'); + }) + ); + + describe('.fromExtPublicKey', () => { + it('should import a correct key', () => { + account = HDAccount.fromExtPublicKey('xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8', 0, 'New account'); + expect(account._xpriv).toEqual(null); + expect(account.label).toEqual('New account'); + }); + + it('should not import a truncated key', () => expect(() => HDAccount.fromExtPublicKey('xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGh', 0, 'New account')).toThrowError('Invalid checksum')); + }); + + describe('.fromExtPrivateKey', () => { + it('should import a correct key', () => { + account = HDAccount.fromExtPrivateKey('xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', undefined, 'Another new account'); + expect(account.label).toEqual('Another new account'); + expect(account._xpub).toEqual('xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8'); + expect(account.isEncrypted).toBeFalsy(); + expect(account.isUnEncrypted).toBeTruthy(); + expect(account.index).toEqual(null); + }); + + it('should not import a truncated key', () => expect(() => HDAccount.fromExtPrivateKey('xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6', undefined, 'Another new account')).toThrowError('Invalid checksum')); + }); + + describe('.factory', () => + it('should not touch already instanciated objects', () => { + let fromFactory = HDAccount.factory(account); + expect(account).toEqual(fromFactory); + }) + ); + }); +}); diff --git a/tests/hdaccount_spec.js.coffee b/tests/hdaccount_spec.js.coffee deleted file mode 100644 index 951d16eec..000000000 --- a/tests/hdaccount_spec.js.coffee +++ /dev/null @@ -1,430 +0,0 @@ -proxyquire = require('proxyquireify')(require) -MyWallet = undefined -HDAccount = undefined - -describe "HDAccount", -> - - - # account = HDAccount.fromExtPublicKey("xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq", undefined, "Example account"); - - account = undefined - object = - 'label': 'My great wallet' - 'archived': false - 'xpriv': 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E' - 'xpub': 'xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq' - 'address_labels': [{"index": 0, "label": "root"}] - 'cache': - 'receiveAccount': 'xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP' - 'changeAccount': 'xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m' - - - beforeEach -> - MyWallet = - syncWallet: () -> - wallet: - getHistory: () -> - - spyOn(MyWallet, "syncWallet") - spyOn(MyWallet.wallet, "getHistory") - # account = new HDAccount(object) - - describe "Constructor", -> - describe "without arguments", -> - beforeEach -> - KeyRing = () -> {init: () ->} - KeyChain = {} - stubs = { './wallet': MyWallet, './keyring' : KeyRing, './keychain' : KeyChain} - HDAccount = proxyquire('../src/hd-account', stubs) - - - it "should create an empty HDAccount with default options", -> - account = new HDAccount() - expect(account.balance).toEqual(null) - expect(account.archived).not.toBeTruthy() - expect(account.active).toBeTruthy() - expect(account.receiveIndex).toEqual(0) - expect(account.changeIndex).toEqual(0) - expect(account.maxLabeledReceiveIndex).toEqual(-1) - - it "should create an HDAccount from AccountMasterKey", -> - accountZero = - toBase58: () -> "accountZeroBase58" - neutered: () -> - { - toBase58: () -> "accountZeroNeuteredBase58" - } - - a = HDAccount.fromAccountMasterKey(accountZero, 0, "label") - - expect(a.label).toEqual("label") - expect(a._xpriv).toEqual("accountZeroBase58") - expect(a._xpub).toEqual("accountZeroNeuteredBase58") - - - - it "should create an HDAccount from Wallet master key", -> - - masterkey = - deriveHardened: (i) -> - deriveHardened: (j) -> - deriveHardened: (k) -> - toBase58: () -> - "m/" + i + "/" + j + "/" + k - neutered: () -> - toBase58: () -> - - a = HDAccount.fromWalletMasterKey(masterkey, 0, "label") - - expect(a._xpriv).toEqual("m/44/0/0") - expect(a.label).toEqual("label") - - it "should transform an Object to an HDAccount", -> - stubs = { './wallet': MyWallet} - HDAccount = proxyquire('../src/hd-account', stubs) - account = new HDAccount(object) - expect(account.extendedPublicKey).toEqual(object.xpub) - expect(account.extendedPrivateKey).toEqual(object.xpriv) - expect(account.label).toEqual(object.label) - expect(account.archived).toEqual(object.archived) - expect(account.receiveIndex).toEqual(0) - expect(account.changeIndex).toEqual(0) - expect(account.n_tx).toEqual(0) - expect(account.balance).toEqual(null) - expect(account.keyRing).toBeDefined() - expect(account.receiveAddress).toBeDefined() - expect(account.changeAddress).toBeDefined() - expect(account.receivingAddressesLabels.length).toEqual(1) - - - describe "JSON serializer", -> - it 'should hold: fromJSON . toJSON = id', -> - json1 = JSON.stringify(account, null, 2) - racc = JSON.parse(json1, HDAccount.reviver) - json2 = JSON.stringify(racc, null, 2) - expect(json1).toEqual(json2) - - describe "instance", -> - beforeEach -> - stubs = { './wallet': MyWallet} - HDAccount = proxyquire('../src/hd-account', stubs) - - account = new HDAccount(object) - - describe ".incrementReceiveIndex", -> - it 'should increment the received index', -> - initial = account.receiveIndex - account.incrementReceiveIndex() - final = account.receiveIndex - expect(final).toEqual(initial + 1) - - describe ".incrementReceiveIndexIfLast", -> - - it 'should not increment the received index', -> - account._receiveIndex = 10 - initial = account.receiveIndex - account.incrementReceiveIndexIfLast(5) - final = account.receiveIndex - expect(final).toEqual(initial) - - it 'should increment the received index', -> - account._receiveIndex = 10 - initial = account.receiveIndex - account.incrementReceiveIndexIfLast(10) - final = account.receiveIndex - expect(final).toEqual(initial + 1) - - describe ".get/setLabelForReceivingAddress", -> - - it 'should set the label sync and get the label', -> - fail = (reason) -> - console.log(reason) - - success = () -> - - account.setLabelForReceivingAddress(10, "my label").then(success).catch(fail) - expect(account._address_labels[10]).toEqual("my label") - expect(MyWallet.syncWallet).toHaveBeenCalled() - expect(account.getLabelForReceivingAddress(10)).toEqual("my label") - - it "should not set a non-valid label", -> - fail = (reason) -> - except(reason).toEqual('NOT_ALPHANUMERIC') - - success = () -> - - account.setLabelForReceivingAddress(10, 0).then(success).catch(fail) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "should not set a label with a gap too wide", -> - fail = (reason) -> - except(reason).toEqual('GAP') - - success = () -> - - account.setLabelForReceivingAddress(100, "my label").then(success).catch(fail) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe "Setter", -> - - it "active shoud toggle archived", -> - account.active = false - expect(account.archived).toBeTruthy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - account.active = true - expect(account.archived).toBeFalsy() - - it "archived should archive the account and sync wallet", -> - account.archived = true - expect(account.archived).toBeTruthy() - expect(account.active).not.toBeTruthy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "archived should throw exception if is non-boolean set", -> - wrongSet = () -> account.archived = "failure" - expect(wrongSet).toThrow() - - it "archived should call MyWallet.sync.getHistory when set to false", -> - account.archived = false - expect(MyWallet.wallet.getHistory).toHaveBeenCalled() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "balance should be set and not sync wallet", -> - account.balance = 100 - expect(account.balance).toEqual(100) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "balance should throw exception if is non-Number set", -> - wrongSet = () -> account.balance = "failure" - expect(wrongSet).toThrow() - - it "n_tx should be set and not sync wallet", -> - account.n_tx = 100 - expect(account.n_tx).toEqual(100) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "n_tx should throw exception if is non-Number set", -> - wrongSet = () -> account.n_tx = "failure" - expect(wrongSet).toThrow() - - it "label should be set and sync wallet", -> - account.label = "my label" - expect(account.label).toEqual("my label") - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it "label should be valid", -> - test = () -> - account.label = 0 - - expect(test).toThrow() - - it "xpriv is read only", -> - account.extendedPrivateKey = "not allowed" - expect(account.extendedPrivateKey).not.toEqual("not allowed") - - it "xpub is read only", -> - account.extendedPublicKey = "not allowed" - expect(account.extendedPublicKey).not.toEqual("not allowed") - - it "receiveAddress is read only", -> - account.receiveAddress = "not allowed" - expect(account.receiveAddress).not.toEqual("not allowed") - - it "changeAddress is read only", -> - account.changeAddress = "not allowed" - expect(account.changeAddress).not.toEqual("not allowed") - - it "index is read only", -> - account.index = "not allowed" - expect(account.index).not.toEqual("not allowed") - - it "KeyRing is read only", -> - account.keyRing = "not allowed" - expect(account.keyRing).not.toEqual("not allowed") - - it "lastUsedReceiveIndex must be a number", -> - invalid = () -> - account.lastUsedReceiveIndex = "1" - - valid = () -> - account.lastUsedReceiveIndex = 1 - - expect(invalid).toThrow() - expect(account.lastUsedReceiveIndex).toEqual(0) - expect(valid).not.toThrow() - expect(account.lastUsedReceiveIndex).toEqual(1) - - it "lastUsedReceiveIndex must be a positive number", -> - invalid = () -> - account.lastUsedReceiveIndex = -534.23 - - expect(invalid).toThrow() - expect(account.lastUsedReceiveIndex).toEqual(0) - - it "receiveIndex must be a number", -> - invalid = () -> - account.receiveIndex = "1" - - valid = () -> - account.receiveIndex = 1 - - expect(invalid).toThrow() - expect(account.receiveIndex).toEqual(0) - expect(valid).not.toThrow() - expect(account.receiveIndex).toEqual(1) - - it "receiveIndex must be a positive number", -> - invalid = () -> - account.receiveIndex = -534.34 - - expect(invalid).toThrow() - expect(account.receiveIndex).toEqual(0) - - it "changeIndex must be a number", -> - invalid = () -> - account.changeIndex = "1" - - valid = () -> - account.changeIndex = 1 - - expect(invalid).toThrow() - expect(account.changeIndex).toEqual(0) - expect(valid).not.toThrow() - expect(account.changeIndex).toEqual(1) - - it "changeIndex must be a positive number", -> - invalid = () -> - account.changeIndex = -534.234 - - expect(invalid).toThrow() - expect(account.changeIndex).toEqual(0) - - describe "Getter", -> - it "maxLabeledReceiveIndex should return the highest labeled index", -> - expect(account.maxLabeledReceiveIndex).toEqual(0) - - account.setLabelForReceivingAddress(1, "label1") - account.setLabelForReceivingAddress(10, "label100") - - expect(account.maxLabeledReceiveIndex).toEqual(10) - - it "labeledReceivingAddresses should return all the labeled receiving addresses", -> - expect(account.labeledReceivingAddresses.length).toEqual(1) - - account.setLabelForReceivingAddress(1, "label1") - account.setLabelForReceivingAddress(10, "label100") - - expect(account.labeledReceivingAddresses.length).toEqual(3) - - describe ".encrypt", -> - beforeEach -> - account = new HDAccount(object) - - it 'should fail and don\'t sync when encryption fails', -> - wrongEnc = () -> account.encrypt(() -> null) - expect(wrongEnc).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should write in a temporary field and let the original key intact', -> - originalKey = account.extendedPrivateKey - account.encrypt(() -> "encrypted key") - expect(account._temporal_xpriv).toEqual("encrypted key") - expect(account.extendedPrivateKey).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if watch only account', -> - account._xpriv = null - account.encrypt(() -> "encrypted key") - expect(account.extendedPrivateKey).toEqual(null) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if no cipher provided', -> - originalKey = account.extendedPrivateKey - account.encrypt(undefined) - expect(account.extendedPrivateKey).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe ".decrypt", -> - beforeEach -> - account = new HDAccount(object) - - it 'should fail and don\'t sync when decryption fails', -> - wrongEnc = () -> account.decrypt(() -> null) - expect(wrongEnc).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should write in a temporary field and let the original key intact', -> - originalKey = account.extendedPrivateKey - account.decrypt(() -> "decrypted key") - expect(account._temporal_xpriv).toEqual("decrypted key") - expect(account.extendedPrivateKey).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if watch only account', -> - account._xpriv = null - account.decrypt(() -> "decrypted key") - expect(account.extendedPrivateKey).toEqual(null) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should do nothing if no cipher provided', -> - originalKey = account.extendedPrivateKey - account.decrypt(undefined) - expect(account.extendedPrivateKey).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe ".persist", -> - beforeEach -> - account = new HDAccount(object) - - it 'should do nothing if temporary is empty', -> - originalKey = account.extendedPrivateKey - account.persist() - expect(account.extendedPrivateKey).toEqual(originalKey) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should swap and delete if we have a temporary value', -> - account._temporal_xpriv = "encrypted key" - temp = account._temporal_xpriv - account.persist() - expect(account.extendedPrivateKey).toEqual(temp) - expect(account._temporal_xpriv).not.toBeDefined() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe ".removeLabelForReceivingAddress", -> - it "should remove the label and sync the wallet", -> - fail = (reason) -> - console.log(reason) - - resolve = () -> - - account.setLabelForReceivingAddress(0, "Savings").then(resolve).catch(fail) - expect(MyWallet.syncWallet).toHaveBeenCalled() - account.removeLabelForReceivingAddress(0) - expect(MyWallet.syncWallet).toHaveBeenCalled() - expect(account.getLabelForReceivingAddress(0)).not.toEqual("Savings") - - describe ".fromExtPublicKey", -> - it "should import a correct key", -> - account = HDAccount.fromExtPublicKey("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", 0, "New account") - expect(account._xpriv).toEqual(null) - expect(account.label).toEqual("New account") - - it "should not import a truncated key", -> - expect(() -> HDAccount.fromExtPublicKey("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGh", 0, "New account")).toThrowError('Invalid checksum') - - describe ".fromExtPrivateKey", -> - it "should import a correct key", -> - account = HDAccount.fromExtPrivateKey("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", undefined, "Another new account") - expect(account.label).toEqual("Another new account") - expect(account._xpub).toEqual("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8") - expect(account.isEncrypted).toBeFalsy() - expect(account.isUnEncrypted).toBeTruthy() - expect(account.index).toEqual(null) - - it "should not import a truncated key", -> - expect(() -> HDAccount.fromExtPrivateKey("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6", undefined, "Another new account")).toThrowError('Invalid checksum') - - describe ".factory", -> - it "should not touch already instanciated objects", -> - fromFactory = HDAccount.factory(account) - expect(account).toEqual(fromFactory) \ No newline at end of file diff --git a/tests/hdwallet_spec.js b/tests/hdwallet_spec.js new file mode 100644 index 000000000..99a7212a1 --- /dev/null +++ b/tests/hdwallet_spec.js @@ -0,0 +1,533 @@ +let proxyquire = require('proxyquireify')(require); +let MyWallet; +let HDWallet; +let BIP39; +let Bitcoin; + +describe('HDWallet', () => { + let wallet; + let walletSecondPw; + let object = { + 'seed_hex': '7e061ca8e579e5e70e9989ca40d342fe', + 'passphrase': '', + 'mnemonic_verified': false, + 'default_account_idx': 0, + 'accounts': [ { + 'label': 'My Bitcoin Wallet', + 'archived': false, + 'xpriv': 'xprv9yko4kDvhYSdUcqK5e8naLwtGE1Ca57mwJ6JMB8WxeYq8t1w3PpiZfGGvLN6N6GEwLF8XuHnp8HeNLrWWviAjXxb2BFEiLaW2UgukMZ3Zva', + 'xpub': 'xpub6Ck9UFkpXuzvh6unBffnwUtcpFqgyXqdJX1u9ZY8Wz5p1gM5aw8y7TakmcEWLA9rJkc59BJzn61p3qqKSaqFkSPMbbhGA9YDNmphj9SKBVJ', + 'address_labels': [], + 'cache': { + 'receiveAccount': 'xpub6FD59hfbH1UWQA9B8NP1C8bh3jc6i2tpM6b8f4Wi9gHWQttZbBBtEsDDZAiPsw7e3427SzvQsFu2sdubjbZHDQdqYXN6x3hTDCrG5bZFEhB', + 'changeAccount': 'xpub6FD59hfbH1UWRrY38bVLPPLPLxcA1XBqsQgB95AgsSWngxbwqPBMd5Z3of8PNicLwE9peQ9g4SeWWtBTzUKLwfjSioAg73RRh7dJ5rWYxM7' + } + } ] + }; + + let NodeInstance = node => + ({ + toBase58 () { return node; }, + derive (n) { + return node + `/${n}`; + } + }) + ; + + Bitcoin = { + HDNode: { + fromSeedBuffer (masterHex, network) { + if (masterHex === 'bad') { + throw new Error('bad'); + } + return new NodeInstance(masterHex.replace('-buffer', '')); + }, + fromBase58 (base58) { + return new NodeInstance(base58.replace('-base58', '')); + } + } + }; + + let hdAccountFactory = ({xpub, xpriv}, label) => { + let state = { + isEncrypted: false, + isUnEncrypted: true + }; + let persist = function () { + this.isEncrypted = state.isEncrypted; + this.isUnEncrypted = state.isUnEncrypted; + }; + return { + encrypt (cipher) { + if (cipher && (cipher('test') === null)) { + throw new Error('bad cipher'); + } + state.isEncrypted = true; + state.isUnEncrypted = false; + this._temporal_xpriv = 'something'; + return { + persist + }; + }, + decrypt (cipher) { + if (cipher && (cipher('test') === null)) { + throw new Error('bad cipher'); + } + state.isEncrypted = false; + state.isUnEncrypted = true; + this._temporal_xpriv = 'something'; + }, + persist, + + isEncrypted: state.isEncrypted, + isUnEncrypted: state.isUnEncrypted, + label, + archived: false, + extendedPublicKey: xpub, + extendedPrivateKey: xpriv, + toJSON () { return {}; } + }; + }; + + let HDAccount = { + factory: hdAccountFactory, + + fromWalletMasterKey (masterkey, accIndex, label) { + let node = masterkey.deriveHardened(43).deriveHardened(0).deriveHardened(accIndex); + let account = hdAccountFactory(node, label); + return account; + } + }; + + beforeEach(() => { + MyWallet = { + get_history () {}, + syncWallet () {} + }; + spyOn(MyWallet, 'syncWallet'); + spyOn(MyWallet, 'get_history'); + }); + + describe('Constructor', () => { + beforeEach(() => { + let KeyRing = () => ({init () {}}); + let KeyChain = {}; + let stubs = { './wallet': MyWallet, './keyring': KeyRing, './keychain': KeyChain }; + HDWallet = proxyquire('../src/hd-wallet', stubs); + }); + + it('should create an empty HDWallet with default options', () => { + wallet = new HDWallet(); + expect(wallet._accounts.length).toEqual(0); + }); + + it('should transform an Object to an HDAccount', () => { + let stubs = { './wallet': MyWallet, './hd-account': HDAccount }; + HDWallet = proxyquire('../src/hd-wallet', stubs); + spyOn(HDAccount, 'factory'); + wallet = new HDWallet(object); + + expect(wallet._seedHex).toEqual(object.seed_hex); + expect(wallet._bip39Password).toEqual(object.passphrase); + expect(wallet._mnemonic_verified).toEqual(object.mnemonic_verified); + expect(wallet._default_account_idx).toEqual(object.default_account_idx); + expect(HDAccount.factory.calls.count()).toEqual(object.accounts.length); + }); + }); + + describe('instance', () => { + BIP39 = { + mnemonicToEntropy (mnemonic) { + return '0123456789abcdef0123456789abcdef'; + } + }; + + beforeEach(() => { + let stubs = { + './wallet': MyWallet, + 'bip39': BIP39, + 'bitcoinjs-lib': Bitcoin, + './hd-account': HDAccount + }; + HDWallet = proxyquire('../src/hd-wallet', stubs); + wallet = new HDWallet(object); + walletSecondPw = HDWallet.new('mnemonic', 'password'); + }); + + describe('Setter', () => { + let testSet = (prop) => () => { wallet[prop] = 'not allowed'; }; + + it('seedHex is read only', () => { + expect(testSet('seedHex')).toThrow(); + }); + + it('bip39Password is read only', () => { + expect(testSet('bip39Password')).toThrow(); + }); + + it('isMnemonicVerified is read only', () => { + expect(testSet('isMnemonicVerified')).toThrow(); + }); + + it('defaultAccount is read only', () => { + expect(testSet('defaultAccount')).toThrow(); + }); + + it('accounts is read only', () => { + expect(testSet('accounts')).toThrow(); + }); + + it('activeAccounts is read only', () => { + expect(testSet('activeAccounts')).toThrow(); + }); + + it('xpubs is read only', () => { + expect(testSet('xpubs')).toThrow(); + }); + + it('activeXpubs is read only', () => { + expect(testSet('activeXpubs')).toThrow(); + }); + + it('balanceActiveAccounts is read only', () => { + expect(testSet('balanceActiveAccounts')).toThrow(); + }); + + it('lastAccount is read only', () => { + expect(testSet('lastAccount')).toThrow(); + }); + + it('defaultAccountIndex should throw exception if is non-number set', () => { + let wrongSet = () => { wallet.defaultAccountIndex = 'failure'; }; + expect(wrongSet).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('defaultAccountIndex should be set and sync wallet', () => { + wallet.defaultAccountIndex = 0; + expect(wallet.defaultAccountIndex).toEqual(0); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + }); + + describe('HDWallet.new()', () => { + beforeEach(() => { + let stubs = { + './wallet': MyWallet + }; + + HDWallet = proxyquire('../src/hd-wallet', stubs); + }); + + it('should return an hdwallet with the correct non-encrypted seedHex', () => { + let hdw = HDWallet.new('bicycle balcony prefer kid flower pole goose crouch century lady worry flavor', undefined, null); + expect(hdw._seedHex).toEqual('15e23aa73d25994f1921a1256f93f72c'); + }); + + it('should return an hdwallet with a random encrypted seedHex', () => { + let encoder = msg => `encrypted-${msg}`; + let hdw = HDWallet.new('bicycle balcony prefer kid flower pole goose crouch century lady worry flavor', undefined, encoder); + expect(hdw._seedHex).toEqual('encrypted-15e23aa73d25994f1921a1256f93f72c'); + }); + }); + + describe('Getter', () => { + it('seedHex', () => expect(wallet.seedHex).toEqual(object.seed_hex)); + + it('bip39Password', () => expect(wallet.bip39Password).toEqual(object.passphrase)); + + it('isMnemonicVerified', () => expect(wallet.isMnemonicVerified).toEqual(object.mnemonic_verified)); + + it('defaultAccount', () => expect(wallet.defaultAccount).toBeDefined()); + + it('accounts', () => expect(wallet.accounts.length).toEqual(object.accounts.length)); + + it('activeAccounts', () => expect(wallet.activeAccounts.length).toEqual(1)); + + it('xpubs', () => expect(wallet.xpubs[0]).toEqual(object.accounts[0].xpub)); + + it('activeXpubs', () => expect(wallet.activeXpubs[0]).toEqual(object.accounts[0].xpub)); + + it('balanceActiveAccounts null balance', () => expect(wallet.balanceActiveAccounts).toEqual(null)); + + it('balanceActiveAccounts not null balance', () => { + wallet.accounts[0].balance = 1000; + expect(wallet.balanceActiveAccounts).toEqual(1000); + }); + + it('lastAccount', () => expect(wallet.lastAccount.extendedPublicKey).toEqual(object.accounts[0].xpub)); + + it('defaultAccountIndex', () => expect(wallet.defaultAccountIndex).toEqual(0)); + + describe('getMasterHDNode', () => { + it("should be 'm'", () => { + spyOn(HDWallet, 'getMasterHex').and.callFake(() => 'm-buffer'); + expect(wallet.getMasterHDNode().toBase58()).toEqual('m'); + }); + + it('should throw when the seed hex is bad', () => { + spyOn(HDWallet, 'getMasterHex').and.callFake(() => 'bad'); + expect(() => wallet.getMasterHDNode()).toThrow(); + }); + }); + }); + + describe('Method', () => { + it('.isValidAccountIndex should be (0 =< index < #accounts - 1)', () => { + expect(wallet.isValidAccountIndex(-1)).toBeFalsy(); + expect(wallet.isValidAccountIndex(-1.242)).toBeFalsy(); + expect(wallet.isValidAccountIndex(0)).toBeTruthy(); + expect(wallet.isValidAccountIndex(+1)).toBeFalsy(); + expect(wallet.isValidAccountIndex(+1.325453)).toBeFalsy(); + expect(wallet.isValidAccountIndex({'a': 1})).toBeFalsy(); + }); + + it('.verifyMnemonic should set to true and sync', () => { + wallet.verifyMnemonic(); + expect(wallet.isMnemonicVerified).toBeTruthy(); + expect(MyWallet.syncWallet).toHaveBeenCalled(); + }); + + it('.account should return an account given the xpub', () => { + let { xpub } = object.accounts[0]; + expect(wallet.account(xpub).extendedPublicKey).toEqual(xpub); + }); + + it('.account should return null if no xpub', () => { + let xpub = 'this is not good'; + expect(wallet.account(xpub)).toEqual(null); + }); + + it('.activeAccount should not return an archived account', () => { + let { xpub } = object.accounts[0]; + wallet.accounts[0].archived = true; + expect(wallet.activeAccount(xpub)).toEqual(null); + }); + + it('.activeAccount should return an active account', () => { + let { xpub } = object.accounts[0]; + expect(wallet.activeAccount(xpub).extendedPublicKey).toEqual(xpub); + }); + }); + + describe('JSON serialization', () => + + it('should hold: fromJSON . toJSON = id', () => { + let json1 = JSON.stringify(wallet, null, 2); + let rwall = JSON.parse(json1, HDWallet.reviver); + let json2 = JSON.stringify(rwall, null, 2); + expect(json1).toEqual(json2); + }) + ); + + describe('.encrypt', () => { + it('should fail and not sync when encryption fails', () => { + let wrongEnc = () => wallet.encrypt(() => null); + expect(wrongEnc).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should write in a temporary field and let the original elements intact', () => { + wallet._bip39Password = 'something'; + let originalSeed = wallet.seedHex; + let originalpass = wallet.bip39Password; + + wallet.encrypt(() => 'encrypted'); + + let areAccountsEncrypted = !wallet.accounts.map((o) => o._temporal_xpriv).some(e => e === undefined); + expect(wallet._temporal_seedHex).toEqual('encrypted'); + expect(wallet._temporal_bip39Password).toEqual('encrypted'); + expect(wallet.seedHex).toEqual(originalSeed); + expect(wallet.bip39Password).toEqual(originalpass); + expect(areAccountsEncrypted).toBeTruthy(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + // check the undefined cipher case on the implementation + // it 'should do nothing if no cipher provided', -> + // wallet._bip39Password = "something" + // originalSeed = wallet.seedHex + // originalpass = wallet.bip39Password + + // wallet.encrypt(undefined) + + // areAccountsEncrypted = not wallet.accounts + // .map((a) -> a._temporal_xpriv) + // .some((e) -> e is undefined) + // expect(wallet._temporal_seedHex).toEqual(undefined) + // expect(wallet._temporal_bip39Password).toEqual(undefined) + // expect(wallet.seedHex).toEqual(originalSeed) + // expect(wallet.bip39Password).toEqual(originalpass) + // expect(areAccountsEncrypted).toBeFalsy() + + describe('.decrypt', () => { + it('should fail and don\'t sync when decryption fails', () => { + let wrongEnc = () => wallet.decrypt(() => null); + expect(wrongEnc).toThrow(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should write in a temporary field and let the original elements intact', () => { + wallet._bip39Password = 'something'; + let originalSeed = wallet.seedHex; + let originalpass = wallet.bip39Password; + + wallet.decrypt(() => 'decrypted'); + + let areAccountsDecrypted = !wallet.accounts.map((o) => o._temporal_xpriv).some(e => e === undefined); + expect(wallet._temporal_seedHex).toEqual('decrypted'); + expect(wallet._temporal_bip39Password).toEqual('decrypted'); + expect(wallet.seedHex).toEqual(originalSeed); + expect(wallet.bip39Password).toEqual(originalpass); + expect(areAccountsDecrypted).toBeTruthy(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + // check the undefined cipher case on the implementation + // it 'should do nothing if no cipher provided', -> + // wallet._bip39Password = "something" + // originalSeed = wallet.seedHex + // originalpass = wallet.bip39Password + + // wallet.encrypt(undefined) + + // areAccountsEncrypted = not wallet.accounts + // .map((a) -> a._temporal_xpriv) + // .some((e) -> e is undefined) + // expect(wallet._temporal_seedHex).toEqual(undefined) + // expect(wallet._temporal_bip39Password).toEqual(undefined) + // expect(wallet.seedHex).toEqual(originalSeed) + // expect(wallet.bip39Password).toEqual(originalpass) + // expect(areAccountsEncrypted).toBeFalsy() + + describe('.persist', () => { + it('should do nothing if temporary is empty', () => { + let originalSeed = wallet.seedHex; + let originalpass = wallet.bip39Password; + wallet.persist(); + expect(wallet.seedHex).toEqual(originalSeed); + expect(wallet.bip39Password).toEqual(originalpass); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + + it('should swap and delete if we have temporary values', () => { + wallet._temporal_seedHex = 'encrypted seed'; + wallet._temporal_bip39Password = 'encrypted bip39pass'; + let tempSeed = wallet._temporal_seedHex; + let tempPass = wallet._temporal_bip39Password; + wallet.persist(); + expect(wallet.seedHex).toEqual(tempSeed); + expect(wallet.bip39Password).toEqual(tempPass); + expect(wallet._temporal_seedHex).not.toBeDefined(); + expect(wallet._temporal_bip39Password).not.toBeDefined(); + expect(MyWallet.syncWallet).not.toHaveBeenCalled(); + }); + }); + + describe('.factory', () => + it('should not touch an existing object', () => { + let fromFactory = HDWallet.factory(wallet); + expect(fromFactory).toEqual(wallet); + }) + ); + + describe('.newAccount', () => { + beforeEach(() => { + let mockHDNode = { + toBase58 () { return 'm'; }, + deriveHardened (purpose) { + return { + deriveHardened (coinType) { + return { + deriveHardened (account) { + let acc = `m/${purpose}'/${coinType}'/${account}'`; + return { + toBase58 () { return `${acc}-base58`; }, + neutered () { + return {toBase58 () { return `${acc}-neutered-base58`; }}; + } + }; + } + }; + } + }; + } + }; + spyOn(wallet, 'getMasterHDNode').and.returnValue(mockHDNode); + spyOn(walletSecondPw, 'getMasterHDNode').and.returnValue(mockHDNode); + }); + + let observer = { + cipher (mode) { + if (mode === 'enc') { + return () => 'aSBhbSBlbmNyeXB0ZWQ='; + } else if (mode === 'dec') { + return () => '0123456789abecdf0123456789abecdf'; + } else { + expect(true).toEqual(false); + } + } + }; + + it('should create a new account without a cipher and with an empty passphrase', () => { + wallet = wallet.newAccount('Savings'); + + expect(wallet.accounts.length).toEqual(2); + expect(wallet.accounts[wallet.accounts.length - 1].label).toEqual('Savings'); + }); + + it('should create a new account without a cipher and with a password', () => { + walletSecondPw = walletSecondPw.newAccount('Savings'); + + expect(walletSecondPw.accounts.length).toEqual(1); + expect(walletSecondPw.accounts[wallet.accounts.length - 1].label).toEqual('Savings'); + }); + + it('should create a new account with a cipher and with an empty passphrase', () => { + wallet = wallet.newAccount('Savings', observer.cipher); + expect(wallet.accounts.length).toEqual(2); + expect(wallet.accounts[wallet.accounts.length - 1].label).toEqual('Savings'); + }); + + it('should create a new account with a cipher and with a password', () => { + walletSecondPw = walletSecondPw.newAccount('Savings', observer.cipher); + + expect(walletSecondPw.accounts.length).toEqual(1); + expect(walletSecondPw.accounts[wallet.accounts.length - 1].label).toEqual('Savings'); + }); + }); + + describe('isUnEncrypted and isEncrypted', () => { + let observer = { + cipher (mode) { + if (mode === 'enc') { + return () => 'aSBhbSBlbmNyeXB0ZWQ='; + } else if (mode === 'dec') { + return () => '0123456789abecdf0123456789abecdf'; + } else { + expect(true).toEqual(false); + } + } + }; + + it('should be correct for the non encrypted test HDWallet', () => { + expect(wallet.isUnEncrypted).toBeTruthy(); + expect(wallet.isEncrypted).toBeFalsy(); + }); + + it('should considered an encrypted but non persisted wallet as unencrypted', () => { + wallet = wallet.encrypt(observer.cipher('enc')); + expect(wallet.isUnEncrypted).toBeTruthy(); + expect(wallet.isEncrypted).toBeFalsy(); + }); + + it('should considered an encrypted and persisted wallet as encrypted', () => { + wallet = wallet.encrypt(observer.cipher('enc')).persist(); + expect(wallet.isUnEncrypted).toBeFalsy(); + expect(wallet.isEncrypted).toBeTruthy(); + }); + }); + }); +}); diff --git a/tests/hdwallet_spec.js.coffee b/tests/hdwallet_spec.js.coffee deleted file mode 100644 index c7c8a3d5e..000000000 --- a/tests/hdwallet_spec.js.coffee +++ /dev/null @@ -1,477 +0,0 @@ -proxyquire = require('proxyquireify')(require) -MyWallet = undefined -HDWallet = undefined -BIP39 = undefined -Bitcoin = undefined - -describe "HDWallet", -> - wallet = undefined - walletSecondPw = undefined - object = - 'seed_hex': '7e061ca8e579e5e70e9989ca40d342fe' - 'passphrase': '' - 'mnemonic_verified': false - 'default_account_idx': 0 - 'accounts': [ { - 'label': 'My Bitcoin Wallet' - 'archived': false - 'xpriv': 'xprv9yko4kDvhYSdUcqK5e8naLwtGE1Ca57mwJ6JMB8WxeYq8t1w3PpiZfGGvLN6N6GEwLF8XuHnp8HeNLrWWviAjXxb2BFEiLaW2UgukMZ3Zva' - 'xpub': 'xpub6Ck9UFkpXuzvh6unBffnwUtcpFqgyXqdJX1u9ZY8Wz5p1gM5aw8y7TakmcEWLA9rJkc59BJzn61p3qqKSaqFkSPMbbhGA9YDNmphj9SKBVJ' - 'address_labels': [] - 'cache': - 'receiveAccount': 'xpub6FD59hfbH1UWQA9B8NP1C8bh3jc6i2tpM6b8f4Wi9gHWQttZbBBtEsDDZAiPsw7e3427SzvQsFu2sdubjbZHDQdqYXN6x3hTDCrG5bZFEhB' - 'changeAccount': 'xpub6FD59hfbH1UWRrY38bVLPPLPLxcA1XBqsQgB95AgsSWngxbwqPBMd5Z3of8PNicLwE9peQ9g4SeWWtBTzUKLwfjSioAg73RRh7dJ5rWYxM7' - } ] - - nodeInstance = (node) -> - { - toBase58: () -> node - derive: (n) -> - node + "/#{ n }" - } - - Bitcoin = { - HDNode: { - fromSeedBuffer: (masterHex, network) -> - if masterHex == 'bad' - throw new Error('bad') - new nodeInstance(masterHex.replace('-buffer','')) - fromBase58: (base58) -> - new nodeInstance(base58.replace('-base58', '')) - } - } - - hdAccountFactory = (node, label) -> - state = - isEncrypted: false - isUnEncrypted: true - persist = () -> - this.isEncrypted = state.isEncrypted - this.isUnEncrypted = state.isUnEncrypted - { - encrypt: (cipher) -> - if cipher && cipher('test') == null - throw new Error('bad cipher') - state.isEncrypted = true - state.isUnEncrypted = false - this._temporal_xpriv = "something" - { - persist: persist - } - decrypt: (cipher) -> - if cipher && cipher('test') == null - throw new Error('bad cipher') - state.isEncrypted = false - state.isUnEncrypted = true - this._temporal_xpriv = "something" - persist: persist - - isEncrypted: state.isEncrypted - isUnEncrypted: state.isUnEncrypted - label: label - archived: false - extendedPublicKey: node.xpub - extendedPrivateKey: node.xpriv - toJSON: () -> {} - } - - HDAccount = { - factory: hdAccountFactory - - fromWalletMasterKey: (masterkey, accIndex, label) -> - node = masterkey.deriveHardened(43).deriveHardened(0).deriveHardened(accIndex) - account = hdAccountFactory(node, label) - account - } - - beforeEach -> - MyWallet = - get_history: () -> - syncWallet: () -> - spyOn(MyWallet, "syncWallet") - spyOn(MyWallet, "get_history") - - describe "Constructor", -> - - beforeEach -> - KeyRing = () -> {init: () ->} - KeyChain = {} - stubs = { './wallet': MyWallet, './keyring' : KeyRing, './keychain' : KeyChain} - HDWallet = proxyquire('../src/hd-wallet', stubs) - - it "should create an empty HDWallet with default options", -> - wallet = new HDWallet() - expect(wallet._accounts.length).toEqual(0) - - it "should transform an Object to an HDAccount", -> - stubs = { './wallet': MyWallet, './hd-account': HDAccount} - HDWallet = proxyquire('../src/hd-wallet', stubs) - spyOn(HDAccount, "factory") - wallet = new HDWallet(object) - - expect(wallet._seedHex).toEqual(object.seed_hex) - expect(wallet._bip39Password).toEqual(object.passphrase) - expect(wallet._mnemonic_verified).toEqual(object.mnemonic_verified) - expect(wallet._default_account_idx).toEqual(object.default_account_idx) - expect(HDAccount.factory.calls.count()).toEqual(object.accounts.length) - - describe "instance", -> - - BIP39 = - mnemonicToEntropy: (mnemonic) -> - "0123456789abcdef0123456789abcdef" - - beforeEach -> - stubs = { - './wallet': MyWallet, - 'bip39' : BIP39, - 'bitcoinjs-lib' : Bitcoin, - './hd-account' : HDAccount - } - HDWallet = proxyquire('../src/hd-wallet', stubs) - wallet = new HDWallet(object) - walletSecondPw = HDWallet.new("mnemonic", "password") - - - describe "Setter", -> - - it "seedHex is read only", -> - wallet.seedHex = "not allowed" - expect(wallet.seedHex).not.toEqual("not allowed") - - it "bip39Password is read only", -> - wallet.bip39Password = "not allowed" - expect(wallet.bip39Password).not.toEqual("not allowed") - - it "isMnemonicVerified is read only", -> - wallet.isMnemonicVerified = "not allowed" - expect(wallet.isMnemonicVerified).not.toEqual("not allowed") - - it "defaultAccount is read only", -> - wallet.defaultAccount = "not allowed" - expect(wallet.defaultAccount).not.toEqual("not allowed") - - it "accounts is read only", -> - wallet.accounts = "not allowed" - expect(wallet.accounts).not.toEqual("not allowed") - - it "activeAccounts is read only", -> - wallet.activeAccounts = "not allowed" - expect(wallet.activeAccounts).not.toEqual("not allowed") - - it "xpubs is read only", -> - wallet.xpubs = "not allowed" - expect(wallet.xpubs).not.toEqual("not allowed") - - it "activeXpubs is read only", -> - wallet.activeXpubs = "not allowed" - expect(wallet.activeXpubs).not.toEqual("not allowed") - - it "balanceActiveAccounts is read only", -> - wallet.balanceActiveAccounts = "not allowed" - expect(wallet.balanceActiveAccounts).not.toEqual("not allowed") - - it "lastAccount is read only", -> - wallet.lastAccount = "not allowed" - expect(wallet.lastAccount).not.toEqual("not allowed") - - it "defaultAccountIndex should throw exception if is non-number set", -> - wrongSet = () -> wallet.defaultAccountIndex = "failure" - expect(wrongSet).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it "defaultAccountIndex should be set and sync wallet", -> - wallet.defaultAccountIndex = 0 - expect(wallet.defaultAccountIndex).toEqual(0) - expect(MyWallet.syncWallet).toHaveBeenCalled() - - describe "HDWallet.new()", -> - - beforeEach -> - - stubs = { - './wallet': MyWallet, - } - - HDWallet = proxyquire('../src/hd-wallet', stubs) - - it "should return an hdwallet with the correct non-encrypted seedHex", -> - hdw = HDWallet.new("bicycle balcony prefer kid flower pole goose crouch century lady worry flavor" , undefined, null) - expect(hdw._seedHex).toEqual('15e23aa73d25994f1921a1256f93f72c'); - - it "should return an hdwallet with a random encrypted seedHex", -> - encoder = (msg) -> "encrypted-" + msg - hdw = HDWallet.new("bicycle balcony prefer kid flower pole goose crouch century lady worry flavor", undefined, encoder) - expect(hdw._seedHex).toEqual('encrypted-15e23aa73d25994f1921a1256f93f72c'); - - describe "Getter", -> - - it "seedHex", -> - expect(wallet.seedHex).toEqual(object.seed_hex) - - it "bip39Password", -> - expect(wallet.bip39Password).toEqual(object.passphrase) - - it "isMnemonicVerified", -> - expect(wallet.isMnemonicVerified).toEqual(object.mnemonic_verified) - - it "defaultAccount", -> - expect(wallet.defaultAccount).toBeDefined() - - it "accounts", -> - expect(wallet.accounts.length).toEqual(object.accounts.length) - - it "activeAccounts", -> - expect(wallet.activeAccounts.length).toEqual(1) - - it "xpubs", -> - expect(wallet.xpubs[0]).toEqual(object.accounts[0].xpub) - - it "activeXpubs", -> - expect(wallet.activeXpubs[0]).toEqual(object.accounts[0].xpub) - - it "balanceActiveAccounts null balance", -> - expect(wallet.balanceActiveAccounts).toEqual(null) - - it "balanceActiveAccounts not null balance", -> - wallet.accounts[0].balance = 1000 - expect(wallet.balanceActiveAccounts).toEqual(1000) - - it "lastAccount", -> - expect(wallet.lastAccount.extendedPublicKey).toEqual(object.accounts[0].xpub) - - it "defaultAccountIndex", -> - expect(wallet.defaultAccountIndex).toEqual(0) - - describe "getMasterHDNode", -> - it "should be 'm'", -> - spyOn(HDWallet,"getMasterHex").and.callFake(() -> - "m-buffer" - ) - expect(wallet.getMasterHDNode().toBase58()).toEqual('m') - - it "should throw when the seed hex is bad", -> - spyOn(HDWallet,"getMasterHex").and.callFake(() -> - "bad" - ) - expect(() -> wallet.getMasterHDNode()).toThrow() - - - describe "Method", -> - - it ".isValidAccountIndex should be (0 =< index < #accounts - 1)", -> - expect(wallet.isValidAccountIndex(-1)).toBeFalsy() - expect(wallet.isValidAccountIndex(-1.242)).toBeFalsy() - expect(wallet.isValidAccountIndex(0)).toBeTruthy() - expect(wallet.isValidAccountIndex(+1)).toBeFalsy() - expect(wallet.isValidAccountIndex(+1.325453)).toBeFalsy() - expect(wallet.isValidAccountIndex({'a': 1})).toBeFalsy() - - - it ".verifyMnemonic should set to true and sync", -> - wallet.verifyMnemonic() - expect(wallet.isMnemonicVerified).toBeTruthy() - expect(MyWallet.syncWallet).toHaveBeenCalled() - - it ".account should return an account given the xpub", -> - xpub = object.accounts[0].xpub - expect(wallet.account(xpub).extendedPublicKey).toEqual(xpub) - - it ".account should return null if no xpub", -> - xpub = "this is not good" - expect(wallet.account(xpub)).toEqual(null) - - it ".activeAccount should not return an archived account", -> - xpub = object.accounts[0].xpub - wallet.accounts[0].archived = true - expect(wallet.activeAccount(xpub)).toEqual(null) - - it ".activeAccount should return an active account", -> - xpub = object.accounts[0].xpub - expect(wallet.activeAccount(xpub).extendedPublicKey).toEqual(xpub) - - describe "JSON serialization", -> - - it 'should hold: fromJSON . toJSON = id', -> - json1 = JSON.stringify(wallet, null, 2) - rwall = JSON.parse(json1, HDWallet.reviver) - json2 = JSON.stringify(rwall, null, 2) - expect(json1).toEqual(json2) - - describe ".encrypt", -> - - it 'should fail and not sync when encryption fails', -> - wrongEnc = () -> wallet.encrypt(() -> null) - expect(wrongEnc).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should write in a temporary field and let the original elements intact', -> - wallet._bip39Password = "something" - originalSeed = wallet.seedHex - originalpass = wallet.bip39Password - - wallet.encrypt(() -> "encrypted") - - areAccountsEncrypted = not wallet.accounts.map((a) -> a._temporal_xpriv).some((e) -> e is undefined) - expect(wallet._temporal_seedHex).toEqual("encrypted") - expect(wallet._temporal_bip39Password).toEqual("encrypted") - expect(wallet.seedHex).toEqual(originalSeed) - expect(wallet.bip39Password).toEqual(originalpass) - expect(areAccountsEncrypted).toBeTruthy() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - # check the undefined cipher case on the implementation - # it 'should do nothing if no cipher provided', -> - # wallet._bip39Password = "something" - # originalSeed = wallet.seedHex - # originalpass = wallet.bip39Password - - # wallet.encrypt(undefined) - - # areAccountsEncrypted = not wallet.accounts - # .map((a) -> a._temporal_xpriv) - # .some((e) -> e is undefined) - # expect(wallet._temporal_seedHex).toEqual(undefined) - # expect(wallet._temporal_bip39Password).toEqual(undefined) - # expect(wallet.seedHex).toEqual(originalSeed) - # expect(wallet.bip39Password).toEqual(originalpass) - # expect(areAccountsEncrypted).toBeFalsy() - - describe ".decrypt", -> - - it 'should fail and don\'t sync when decryption fails', -> - wrongEnc = () -> wallet.decrypt(() -> null) - expect(wrongEnc).toThrow() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should write in a temporary field and let the original elements intact', -> - wallet._bip39Password = "something" - originalSeed = wallet.seedHex - originalpass = wallet.bip39Password - - wallet.decrypt(() -> "decrypted") - - areAccountsDecrypted = not wallet.accounts.map((a) -> a._temporal_xpriv).some((e) -> e is undefined) - expect(wallet._temporal_seedHex).toEqual("decrypted") - expect(wallet._temporal_bip39Password).toEqual("decrypted") - expect(wallet.seedHex).toEqual(originalSeed) - expect(wallet.bip39Password).toEqual(originalpass) - expect(areAccountsDecrypted).toBeTruthy() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - # check the undefined cipher case on the implementation - # it 'should do nothing if no cipher provided', -> - # wallet._bip39Password = "something" - # originalSeed = wallet.seedHex - # originalpass = wallet.bip39Password - - # wallet.encrypt(undefined) - - # areAccountsEncrypted = not wallet.accounts - # .map((a) -> a._temporal_xpriv) - # .some((e) -> e is undefined) - # expect(wallet._temporal_seedHex).toEqual(undefined) - # expect(wallet._temporal_bip39Password).toEqual(undefined) - # expect(wallet.seedHex).toEqual(originalSeed) - # expect(wallet.bip39Password).toEqual(originalpass) - # expect(areAccountsEncrypted).toBeFalsy() - - describe ".persist", -> - - it 'should do nothing if temporary is empty', -> - originalSeed = wallet.seedHex - originalpass = wallet.bip39Password - wallet.persist() - expect(wallet.seedHex).toEqual(originalSeed) - expect(wallet.bip39Password).toEqual(originalpass) - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - it 'should swap and delete if we have temporary values', -> - wallet._temporal_seedHex = "encrypted seed" - wallet._temporal_bip39Password = "encrypted bip39pass" - temp_seed = wallet._temporal_seedHex - temp_pass = wallet._temporal_bip39Password - wallet.persist() - expect(wallet.seedHex).toEqual(temp_seed) - expect(wallet.bip39Password).toEqual(temp_pass) - expect(wallet._temporal_seedHex).not.toBeDefined() - expect(wallet._temporal_bip39Password).not.toBeDefined() - expect(MyWallet.syncWallet).not.toHaveBeenCalled() - - describe ".factory", -> - it "should not touch an existing object", -> - fromFactory = HDWallet.factory(wallet) - expect(fromFactory).toEqual(wallet) - - describe ".newAccount", -> - beforeEach -> - mockHDNode = { - toBase58: () -> 'm' - deriveHardened: (purpose) -> - deriveHardened: (coinType) -> - deriveHardened: (account) -> - acc = "m/#{ purpose }'/#{ coinType }'/#{ account }'" - { - toBase58: () -> acc + '-base58' - neutered: () -> - toBase58: () -> acc + '-neutered-base58' - } - } - spyOn(wallet, 'getMasterHDNode').and.returnValue mockHDNode - spyOn(walletSecondPw, 'getMasterHDNode').and.returnValue mockHDNode - - observer = - cipher: (mode) -> - if mode == "enc" - return () -> "aSBhbSBlbmNyeXB0ZWQ=" - else if mode == "dec" - return () -> "0123456789abecdf0123456789abecdf" - else - expect(true).toEqual(false) - - it "should create a new account without a cipher and with an empty passphrase", -> - wallet = wallet.newAccount("Savings") - - expect(wallet.accounts.length).toEqual(2) - expect(wallet.accounts[wallet.accounts.length - 1].label).toEqual('Savings') - - it "should create a new account without a cipher and with a password", -> - walletSecondPw = walletSecondPw.newAccount("Savings") - - expect(walletSecondPw.accounts.length).toEqual(1) - expect(walletSecondPw.accounts[wallet.accounts.length - 1].label).toEqual('Savings') - - it "should create a new account with a cipher and with an empty passphrase", -> - wallet = wallet.newAccount("Savings", observer.cipher) - expect(wallet.accounts.length).toEqual(2) - expect(wallet.accounts[wallet.accounts.length - 1].label).toEqual('Savings') - - it "should create a new account with a cipher and with a password", -> - walletSecondPw = walletSecondPw.newAccount("Savings", observer.cipher) - - expect(walletSecondPw.accounts.length).toEqual(1) - expect(walletSecondPw.accounts[wallet.accounts.length - 1].label).toEqual('Savings') - - describe "isUnEncrypted and isEncrypted", -> - observer = - cipher: (mode) -> - if mode == "enc" - return () -> "aSBhbSBlbmNyeXB0ZWQ=" - else if mode == "dec" - return () -> "0123456789abecdf0123456789abecdf" - else - expect(true).toEqual(false) - - it "should be correct for the non encrypted test HDWallet", -> - expect(wallet.isUnEncrypted).toBeTruthy() - expect(wallet.isEncrypted).toBeFalsy() - - it "should considered an encrypted but non persisted wallet as unencrypted", -> - wallet = wallet.encrypt(observer.cipher('enc')) - expect(wallet.isUnEncrypted).toBeTruthy() - expect(wallet.isEncrypted).toBeFalsy() - - it "should considered an encrypted and persisted wallet as encrypted", -> - wallet = wallet.encrypt(observer.cipher('enc')).persist() - expect(wallet.isUnEncrypted).toBeFalsy() - expect(wallet.isEncrypted).toBeTruthy() diff --git a/tests/helpers_spec.js b/tests/helpers_spec.js new file mode 100644 index 000000000..916b0478b --- /dev/null +++ b/tests/helpers_spec.js @@ -0,0 +1,406 @@ +let proxyquire = require('proxyquireify')(require); +let Bitcoin = require('bitcoinjs-lib'); +let BigInteger = require('bigi'); + +const ImportExport = { + shouldResolve: false, + shouldReject: false, + shouldFail: true, + + parseBIP38toECPair (b58, pass, succ, wrong, error) { + if (ImportExport.shouldResolve) { + succ(new Bitcoin.ECPair(BigInteger.fromByteArrayUnsigned(BigInteger.fromBuffer(new Buffer('E9873D79C6D87DC0FB6A5778633389F4453213303DA61F20BD67FC233AA33262', 'hex')).toByteArray()), null, {compressed: true})); + } else if (ImportExport.shouldReject) { + wrong(); + } else if (ImportExport.shouldFail) { + error(); + } + } +}; + +describe('Helpers', () => { + let Helpers = proxyquire('../src/helpers', { + './import-export': ImportExport + }); + + describe('getHostName', () => + it('should be localhost in Jasmine', () => expect(Helpers.getHostName()).toEqual('localhost')) + ); + + describe('TOR', () => { + it('should be detected based on window.location', () => { + // hostname is "localhost" in test: + expect(Helpers.tor()).toBeFalsy(); + + spyOn(Helpers, 'getHostName').and.returnValue('blockchainbdgpzk.onion'); + expect(Helpers.tor()).toBeTruthy(); + }); + + it('should not detect false positives', () => { + spyOn(Helpers, 'getHostName').and.returnValue('the.onion.org'); + expect(Helpers.tor()).toBeFalsy(); + }); + }); + + describe('isBitcoinAddress', () => { + it('should recognize valid addresses', () => { + expect(Helpers.isBitcoinAddress('1KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEb')).toBeTruthy(); + expect(Helpers.isBitcoinAddress('3A1KUd5H4hBEHk4bZB4C3hGgvuXuVX7p7t')).toBeTruthy(); + }); + + it('should not recognize bad addresses', () => { + expect(Helpers.isBitcoinAddress('1KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEa')).toBeFalsy(); + expect(Helpers.isBitcoinAddress('5KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEb')).toBeFalsy(); + expect(Helpers.isBitcoinAddress('1KM7w12SkjzJ1FYV2g1UCMzHjv')).toBeFalsy(); + }); + }); + + describe('isAlphaNum', () => { + it('should recognize alphanumerical strings', () => expect(Helpers.isAlphaNum('a,sdfw-g4+ 234e1.1_')).toBeTruthy()); + + it('should not recognize non alphanumerical strings', () => { + expect(Helpers.isAlphaNum('')).toBeFalsy(); + expect(Helpers.isAlphaNum(122342)).toBeFalsy(); + expect(Helpers.isAlphaNum({'a': 1})).toBeFalsy(); + }); + }); + + describe('isSeedHex', () => { + it('should recognize seed hex', () => expect(Helpers.isAlphaNum('0123456789abcdef0123456789abcdef')).toBeTruthy()); + + it('should not recognize non valid seed hex', () => { + expect(Helpers.isSeedHex('')).toBeFalsy(); + expect(Helpers.isSeedHex(122342)).toBeFalsy(); + expect(Helpers.isSeedHex({'a': 1})).toBeFalsy(); + expect(Helpers.isSeedHex('4JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn')).toBeFalsy(); + }); + }); + + describe('and', () => + it('should work', () => { + expect(Helpers.and(0, 1)).toBeFalsy(); + expect(Helpers.and(1, 0)).toBeFalsy(); + expect(Helpers.and(1, 1)).toBeTruthy(); + expect(Helpers.and(0, 0)).toBeFalsy(); + }) + ); + + describe('isBitcoinPrivateKey', () => { + it('should recognize valid private keys', () => expect(Helpers.isBitcoinPrivateKey('5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn')).toBeTruthy()); + + it('should not recognize private keys with a bad header', () => expect(Helpers.isBitcoinPrivateKey('4JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn')).toBeFalsy()); + + it('should not recognize private keys with a bad checksum', () => expect(Helpers.isBitcoinPrivateKey('4JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gw')).toBeFalsy()); + + it('should not recognize private keys with a bad length', () => expect(Helpers.isBitcoinPrivateKey('5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUC')).toBeFalsy()); + }); + + describe('isPositiveInteger', () => { + it('should include 1', () => expect(Helpers.isPositiveInteger(1)).toBe(true)); + + it('should include 0', () => expect(Helpers.isPositiveInteger(0)).toBe(true)); + + it('should exclude -1', () => expect(Helpers.isPositiveInteger(-1)).toBe(false)); + + it('should exclude 1.1', () => expect(Helpers.isPositiveInteger(1.1)).toBe(false)); + }); + + describe('isPositiveNumber', () => { + it('should include 1', () => expect(Helpers.isPositiveNumber(1)).toBe(true)); + + it('should include 0', () => expect(Helpers.isPositiveNumber(0)).toBe(true)); + + it('should exclude -1', () => expect(Helpers.isPositiveNumber(-1)).toBe(false)); + + it('should include 1.1', () => expect(Helpers.isPositiveNumber(1.1)).toBe(true)); + }); + + describe('scorePassword', () => { + it('should give a good score to strong passwords', () => { + expect(Helpers.scorePassword('u*3Fq1D&qvq3Qy6045^NcDJhD0TODs') > 60).toBeTruthy(); + expect(Helpers.scorePassword('&T3m#ABtzlJCH0Nv!QQ4') > 60).toBeTruthy(); + expect(Helpers.scorePassword('I!&JrqDszO') > 60).toBeTruthy(); + }); + + it('should give a low score to weak passwords', () => { + expect(Helpers.scorePassword('correctbattery') < 20).toBeTruthy(); + expect(Helpers.scorePassword('123456123456123456') < 20).toBeTruthy(); + expect(Helpers.scorePassword('') === 0).toBeTruthy(); + }); + + it('should set a score of 0 to non strings', () => expect(Helpers.scorePassword(0) === 0).toBeTruthy()); + }); + + describe('asyncOnce', () => { + it('should only execute once', done => { + let observer = { + func () {}, + before () {} + }; + + spyOn(observer, 'func'); + spyOn(observer, 'before'); + + let async = Helpers.asyncOnce(observer.func, 20, observer.before); + + async(); + async(); + async(); + async(); + + let result = () => { + expect(observer.func).toHaveBeenCalledTimes(1); + expect(observer.before).toHaveBeenCalledTimes(4); + + done(); + }; + + return setTimeout(result, 1000); + }); + + it('should work with arguments', done => { + let observer = { + func () {}, + before () {} + }; + + spyOn(observer, 'func'); + spyOn(observer, 'before'); + + let async = Helpers.asyncOnce(observer.func, 20, observer.before); + + async(1); + async(1); + async(1); + async(1); + + let result = () => { + expect(observer.func).toHaveBeenCalledTimes(1); + expect(observer.func).toHaveBeenCalledWith(1); + expect(observer.before).toHaveBeenCalledTimes(4); + + done(); + }; + + return setTimeout(result, 1000); + }); + }); + + describe('guessFee', () => + // TODO make these tests pass + // it "should not compute fee for null input", -> + // expect(Helpers.guessFee(1, 1, null)).toBe(NaN) + // expect(Helpers.guessFee(null, 1, 10000)).toBe(NaN) + // expect(Helpers.guessFee(1, null, 10000)).toBe(NaN) + + // it 'should not return a fee when using negative values', -> + // expect(Helpers.guessFee(-1, 1, 10000)).toEqual(NaN) + // expect(Helpers.guessFee(1, -1, 10000)).toEqual(NaN) + // expect(Helpers.guessFee(1, 1, -10000)).toEqual(NaN) + + // it 'should not return a fee when using non integer values', -> + // expect(Helpers.guessFee(1.5, 1, 10000)).toEqual(NaN) + // expect(Helpers.guessFee(1, 1.2, 10000)).toEqual(NaN) + + // (148 * input + 34 * outputs + 10) * fee per kb (10 = overhead) + describe('standard formula', () => { + it('should work for 1 input, 1 output and 10000 fee per kB', () => expect(Helpers.guessFee(1, 1, 10000)).toEqual(1920)); + + it('should work for 1 input, 2 output and 10000 fee per kB', () => expect(Helpers.guessFee(1, 2, 10000)).toEqual(2260)); + + it('should round up for 2 input, 1 output and 10000 fee per kB', () => expect(Helpers.guessFee(2, 1, 10000)).toEqual(3401)); + + it('should work for 1 input, 1 output and 15000 fee per kB', () => expect(Helpers.guessFee(1, 1, 15000)).toEqual(2880)); + }) + ); + + describe('guessSize', () => + // TODO make these tests pass + // it "should not compute size for null input", -> + // expect(Helpers.guessSize(1, 1)).toBe(NaN) + // expect(Helpers.guessSize(null, 1)).toBe(NaN) + // expect(Helpers.guessSize(1, null)).toBe(NaN) + + // it 'should not return a fee when using negative values', -> + // expect(Helpers.guessSize(-1, 1)).toEqual(NaN) + // expect(Helpers.guessSize(1, -1)).toEqual(NaN) + // expect(Helpers.guessSize(1, 1)).toEqual(NaN) + + // it 'should not return a fee when using non integer values', -> + // expect(Helpers.guessSize(1.5, 1)).toEqual(NaN) + // expect(Helpers.guessSize(1, 1.2)).toEqual(NaN) + + // (148 * input + 34 * outputs + 10) (10 = overhead) + describe('standard formula', () => { + it('should work for 1 input, 1 output', () => expect(Helpers.guessSize(1, 1)).toEqual(192)); + + it('should work for 1 input, 2 output', () => expect(Helpers.guessSize(1, 2, 10000)).toEqual(226)); + + it('should work for 2 input, 1 output', () => expect(Helpers.guessSize(2, 1, 10000)).toEqual(340)); + }) + ); + + describe('isValidBIP39Mnemonic', () => { + it('should recognize BIP-39 test vectors', () => { + expect(Helpers.isValidBIP39Mnemonic('letter advice cage absurd amount doctor acoustic avoid letter advice cage above')).toBeTruthy(); + expect(Helpers.isValidBIP39Mnemonic('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about')).toBeTruthy(); + expect(Helpers.isValidBIP39Mnemonic('vessel ladder alter error federal sibling chat ability sun glass valve picture')).toBeTruthy(); + expect(Helpers.isValidBIP39Mnemonic('cat swing flag economy stadium alone churn speed unique patch report train')).toBeTruthy(); + }); + + it('should not recognize invalid mnemonics', () => { + expect(Helpers.isValidBIP39Mnemonic('letter advice cage absurd amount doctor acoustic avoid lettre advice cage above')).toBeFalsy(); + expect(Helpers.isValidBIP39Mnemonic('abandon abandn abandon abandon abandon abandon abandon abandon abandon abandon abandon about')).toBeFalsy(); + expect(Helpers.isValidBIP39Mnemonic('vessel ladder alter error federal sibling chat ability sun glass valves picture')).toBeFalsy(); + expect(Helpers.isValidBIP39Mnemonic('cat swing flag economy stadum alone churn speed unique patch report train')).toBeFalsy(); + }); + + it("should not recognize things that aren't mnemonics", () => { + expect(Helpers.isValidBIP39Mnemonic('')).toBeFalsy(); + expect(Helpers.isValidBIP39Mnemonic('a')).toBeFalsy(); + expect(Helpers.isValidBIP39Mnemonic(0)).toBeFalsy(); + expect(Helpers.isValidBIP39Mnemonic({ 'mnemonic': 'cat swing flag economy stadium alone churn speed unique patch report train' })).toBeFalsy(); + }); + }); + + describe('detectPrivateKeyFormat', () => { + it('should reject invalid formats', () => { + let res = Helpers.detectPrivateKeyFormat('46c56bnXQiBjk9mqSYE7ykVQ7NzrRy'); + expect(res).toBeNull(); + }); + + it('should recognise sipa', () => { + let res = Helpers.detectPrivateKeyFormat('5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn'); + expect(res).toEqual('sipa'); + }); + }); + + describe('privateKeyStringToKey', () => + it('should convert sipa format', () => { + let res = Helpers.privateKeyStringToKey('5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn', 'sipa'); + expect(Helpers.isKey(res)).toBeTruthy(); + }) + ); + + describe('privateKeyCorrespondsToAddress', () => { + afterEach(() => { + ImportExport.shouldResolve = false; + ImportExport.shouldReject = false; + ImportExport.shouldFail = false; + }); + + it('should not recognize invalid formats', done => { + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyN', '46c56bnXQiBjk9mqSYE7ykVQ7NzrRy'); + expect(promise).toBeRejected(done); + }); + + it('should not match base58 private keys to wrong addresses', done => { + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyN', '5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn'); + return promise.then(data => { + expect(data).toEqual(null); + done(); + }).catch(e => { + console.log(e); + assert(false); + done(); + }); + }); + + it('should not match mini private keys to wrong addresses', done => { + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyN', 'S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy'); + return promise.then(data => { + expect(data).toEqual(null); + done(); + }).catch(e => { + console.log(e); + assert(false); + done(); + }); + }); + + it('should not recognize BIP-38 addresses without password', done => { + ImportExport.shouldResolve = true; + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg'); + expect(promise).toBeRejected(done); + }); + + it('should not recognize BIP-38 addresses with an empty password', done => { + ImportExport.shouldResolve = true; + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg', ''); + expect(promise).toBeRejected(done); + }); + + it('should not recognize BIP-38 addresses with a bad password', done => { + ImportExport.shouldReject = true; + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg', 'pass'); + expect(promise).toBeRejected(done); + }); + + it('should not recognize BIP-38 addresses when decryption fails', done => { + ImportExport.shouldFail = true; + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg', 'pass'); + expect(promise).toBeRejected(done); + }); + + it('should recognize BIP-38 addresses when decryption succeeds', done => { + ImportExport.shouldResolve = true; + let promise = Helpers.privateKeyCorrespondsToAddress('19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR', '6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg', 'pass'); + return promise.then(data => { + expect(data).not.toEqual(null); + done(); + }); + }); + + it('should match base58 private keys to their right addresses', done => { + let promise = Helpers.privateKeyCorrespondsToAddress('1BDSbDEechSue77wS44Jn2uDiFaQWom2dG', '5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn'); + return promise.then(data => { + expect(data).not.toEqual(null); + done(); + }); + }); + + it('should match mini private keys to their right addresses', done => { + let promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', 'S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy'); + return promise.then(data => { + expect(data).not.toEqual(null); + done(); + }); + }); + }); + + describe('isValidPrivateKey', () => { + it('should not recognize invalid hex keys', () => { + expect(Helpers.isValidPrivateKey('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141')).toBeFalsy(); + expect(Helpers.isValidPrivateKey('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')).toBeFalsy(); + expect(Helpers.isValidPrivateKey('0000000000000000000000000000000000000000000000000000000000000000')).toBeFalsy(); + }); + + it('should recognize valide hex keys', () => { + expect(Helpers.isValidPrivateKey('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364139')).toBeTruthy(); + expect(Helpers.isValidPrivateKey('0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF')).toBeTruthy(); + expect(Helpers.isValidPrivateKey('0000000000000000000000000000000000000000000000000000000000000001')).toBeTruthy(); + }); + + it('should not recognize invalid base 64 keys', () => { + expect(Helpers.isValidPrivateKey('ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8=098')).toBeFalsy(); + expect(Helpers.isValidPrivateKey('////////////////////////////////////////////')).toBeFalsy(); + }); + + it('should recognize valid base 64 keys', () => { + expect(Helpers.isValidPrivateKey('ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8=')).toBeTruthy(); + expect(Helpers.isValidPrivateKey('/////////////////////rqu3OavSKA7v9JejNA2QTk=')).toBeTruthy(); + expect(Helpers.isValidPrivateKey('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=')).toBeTruthy(); + }); + + it('should recognize BIP-38 keys', () => { + expect(Helpers.isValidPrivateKey('6PRMUxAWM4XyK8b3wyJRpTwvDdmCKakuP6aGxr3D8MuUaCWVLXM2wnGUCT')).toBeTruthy(); + expect(Helpers.isValidPrivateKey('6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg')).toBeTruthy(); + }); + }); + + describe('verifyMessage', () => { + it('should verify valid messages', () => expect(Helpers.verifyMessage('1LGAzcG9dafqtW8eHkFUPjkDKemjv5dxKd', 'HxvX3mVUI4cQgpKB98bjl/NOYi2BiaSZEsdfCulyJ7GAWrfP/9WkDazCe45lyhWPZQwZKnYZILz5h3SHn4xFPzg=', 'Wright, it is not the same as if I sign Craig Wright, Satoshi.')).toBeTruthy()); + + it('should not verify invalid messages', () => expect(Helpers.verifyMessage('12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S', 'IH+xpXCKouEcd0E8Hv3NkrYWbhq0P7pAQpI1GcQ2hF2AAsqL2o4agDE8V81i071/bTMz00YKw2YRMoyFMzThZwM=', 'Wright, it is not the same as if I sign Craig Wright, Satoshi.')).toBeFalsy()); + }); +}); diff --git a/tests/helpers_spec.js.coffee b/tests/helpers_spec.js.coffee deleted file mode 100644 index a1ef0d172..000000000 --- a/tests/helpers_spec.js.coffee +++ /dev/null @@ -1,389 +0,0 @@ -proxyquire = require('proxyquireify')(require) -Bitcoin = require('bitcoinjs-lib') -BigInteger = require('bigi'); - -ImportExport = - shouldResolve: false - shouldReject: false - shouldFail: true - - parseBIP38toECPair: (b58, pass, succ, wrong, error) -> - if ImportExport.shouldResolve - succ(new Bitcoin.ECPair(new BigInteger.fromByteArrayUnsigned(BigInteger.fromBuffer(new Buffer('E9873D79C6D87DC0FB6A5778633389F4453213303DA61F20BD67FC233AA33262', 'hex')).toByteArray()), null, {compressed: true})) - else if ImportExport.shouldReject - wrong() - else if ImportExport.shouldFail - error() - -describe "Helpers", -> - - Helpers = proxyquire('../src/helpers', { - './import-export': ImportExport - }) - - describe "getHostName", -> - it "should be localhost in Jasmine", -> - expect(Helpers.getHostName()).toEqual("localhost") - - describe "TOR", -> - it "should be detected based on window.location", -> - # hostname is "localhost" in test: - expect(Helpers.tor()).toBeFalsy() - - spyOn(Helpers, "getHostName").and.returnValue "blockchainbdgpzk.onion" - expect(Helpers.tor()).toBeTruthy() - - it "should not detect false positives", -> - spyOn(Helpers, "getHostName").and.returnValue "the.onion.org" - expect(Helpers.tor()).toBeFalsy() - - - describe "isBitcoinAddress", -> - it "should recognize valid addresses", -> - expect(Helpers.isBitcoinAddress("1KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEb")).toBeTruthy() - expect(Helpers.isBitcoinAddress("3A1KUd5H4hBEHk4bZB4C3hGgvuXuVX7p7t")).toBeTruthy() - - it "should not recognize bad addresses", -> - expect(Helpers.isBitcoinAddress("1KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEa")).toBeFalsy() - expect(Helpers.isBitcoinAddress("5KM7w12SkjzJ1FYV2g1UCMzHjv3pkMgkEb")).toBeFalsy() - expect(Helpers.isBitcoinAddress("1KM7w12SkjzJ1FYV2g1UCMzHjv")).toBeFalsy() - - describe "isAlphaNum", -> - it "should recognize alphanumerical strings", -> - expect(Helpers.isAlphaNum("a,sdfw-g4+ 234e1.1_")).toBeTruthy() - - it "should not recognize non alphanumerical strings", -> - expect(Helpers.isAlphaNum("")).toBeFalsy() - expect(Helpers.isAlphaNum(122342)).toBeFalsy() - expect(Helpers.isAlphaNum({'a': 1})).toBeFalsy() - - describe "isSeedHex", -> - it "should recognize seed hex", -> - expect(Helpers.isAlphaNum("0123456789abcdef0123456789abcdef")).toBeTruthy() - - it "should not recognize non valid seed hex", -> - expect(Helpers.isSeedHex("")).toBeFalsy() - expect(Helpers.isSeedHex(122342)).toBeFalsy() - expect(Helpers.isSeedHex({'a': 1})).toBeFalsy() - expect(Helpers.isSeedHex("4JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn")).toBeFalsy() - - describe "and", -> - it "should work", -> - expect(Helpers.and(0, 1)).toBeFalsy() - expect(Helpers.and(1, 0)).toBeFalsy() - expect(Helpers.and(1, 1)).toBeTruthy() - expect(Helpers.and(0, 0)).toBeFalsy() - - describe "isBitcoinPrivateKey", -> - it "should recognize valid private keys", -> - expect(Helpers.isBitcoinPrivateKey("5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn")).toBeTruthy() - - it "should not recognize private keys with a bad header", -> - expect(Helpers.isBitcoinPrivateKey("4JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn")).toBeFalsy() - - it "should not recognize private keys with a bad checksum", -> - expect(Helpers.isBitcoinPrivateKey("4JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gw")).toBeFalsy() - - it "should not recognize private keys with a bad length", -> - expect(Helpers.isBitcoinPrivateKey("5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUC")).toBeFalsy() - - describe "isPositiveInteger", -> - it "should include 1", -> - expect(Helpers.isPositiveInteger(1)).toBe(true) - - it "should include 0", -> - expect(Helpers.isPositiveInteger(0)).toBe(true) - - it "should exclude -1", -> - expect(Helpers.isPositiveInteger(-1)).toBe(false) - - it "should exclude 1.1", -> - expect(Helpers.isPositiveInteger(1.1)).toBe(false) - - describe "isPositiveNumber", -> - it "should include 1", -> - expect(Helpers.isPositiveNumber(1)).toBe(true) - - it "should include 0", -> - expect(Helpers.isPositiveNumber(0)).toBe(true) - - it "should exclude -1", -> - expect(Helpers.isPositiveNumber(-1)).toBe(false) - - it "should include 1.1", -> - expect(Helpers.isPositiveNumber(1.1)).toBe(true) - - describe "scorePassword", -> - it "should give a good score to strong passwords", -> - expect(Helpers.scorePassword('u*3Fq1D&qvq3Qy6045^NcDJhD0TODs') > 60).toBeTruthy() - expect(Helpers.scorePassword('&T3m#ABtzlJCH0Nv!QQ4') > 60).toBeTruthy() - expect(Helpers.scorePassword('I!&JrqDszO') > 60).toBeTruthy() - - it "should give a low score to weak passwords", -> - expect(Helpers.scorePassword('correctbattery') < 20).toBeTruthy() - expect(Helpers.scorePassword('123456123456123456') < 20).toBeTruthy() - expect(Helpers.scorePassword('') == 0).toBeTruthy() - - it "should set a score of 0 to non strings", -> - expect(Helpers.scorePassword(0) == 0).toBeTruthy() - - describe "asyncOnce", -> - - it "should only execute once", (done) -> - observer = - func: () -> - before: () -> - - spyOn(observer, "func") - spyOn(observer, "before") - - async = Helpers.asyncOnce(observer.func, 20, observer.before) - - async() - async() - async() - async() - - result = () -> - expect(observer.func).toHaveBeenCalledTimes(1) - expect(observer.before).toHaveBeenCalledTimes(4) - - done() - - setTimeout(result, 1000) - - it "should work with arguments", (done) -> - observer = - func: () -> - before: () -> - - spyOn(observer, "func") - spyOn(observer, "before") - - async = Helpers.asyncOnce(observer.func, 20, observer.before) - - async(1) - async(1) - async(1) - async(1) - - result = () -> - expect(observer.func).toHaveBeenCalledTimes(1) - expect(observer.func).toHaveBeenCalledWith(1) - expect(observer.before).toHaveBeenCalledTimes(4) - - done() - - setTimeout(result, 1000) - - - describe "guessFee", -> - # TODO make these tests pass - # it "should not compute fee for null input", -> - # expect(Helpers.guessFee(1, 1, null)).toBe(NaN) - # expect(Helpers.guessFee(null, 1, 10000)).toBe(NaN) - # expect(Helpers.guessFee(1, null, 10000)).toBe(NaN) - - #it 'should not return a fee when using negative values', -> - # expect(Helpers.guessFee(-1, 1, 10000)).toEqual(NaN) - # expect(Helpers.guessFee(1, -1, 10000)).toEqual(NaN) - # expect(Helpers.guessFee(1, 1, -10000)).toEqual(NaN) - - #it 'should not return a fee when using non integer values', -> - # expect(Helpers.guessFee(1.5, 1, 10000)).toEqual(NaN) - # expect(Helpers.guessFee(1, 1.2, 10000)).toEqual(NaN) - - # (148 * input + 34 * outputs + 10) * fee per kb (10 = overhead) - describe "standard formula", -> - it 'should work for 1 input, 1 output and 10000 fee per kB', -> - expect(Helpers.guessFee(1,1,10000)).toEqual(1920) - - it 'should work for 1 input, 2 output and 10000 fee per kB', -> - expect(Helpers.guessFee(1,2,10000)).toEqual(2260) - - it 'should round up for 2 input, 1 output and 10000 fee per kB', -> - expect(Helpers.guessFee(2,1,10000)).toEqual(3401) - - it 'should work for 1 input, 1 output and 15000 fee per kB', -> - expect(Helpers.guessFee(1,1,15000)).toEqual(2880) - - - describe "guessSize", -> - # TODO make these tests pass - # it "should not compute size for null input", -> - # expect(Helpers.guessSize(1, 1)).toBe(NaN) - # expect(Helpers.guessSize(null, 1)).toBe(NaN) - # expect(Helpers.guessSize(1, null)).toBe(NaN) - - #it 'should not return a fee when using negative values', -> - # expect(Helpers.guessSize(-1, 1)).toEqual(NaN) - # expect(Helpers.guessSize(1, -1)).toEqual(NaN) - # expect(Helpers.guessSize(1, 1)).toEqual(NaN) - - #it 'should not return a fee when using non integer values', -> - # expect(Helpers.guessSize(1.5, 1)).toEqual(NaN) - # expect(Helpers.guessSize(1, 1.2)).toEqual(NaN) - - # (148 * input + 34 * outputs + 10) (10 = overhead) - describe "standard formula", -> - it 'should work for 1 input, 1 output', -> - expect(Helpers.guessSize(1,1)).toEqual(192) - - it 'should work for 1 input, 2 output', -> - expect(Helpers.guessSize(1,2,10000)).toEqual(226) - - it 'should work for 2 input, 1 output', -> - expect(Helpers.guessSize(2,1,10000)).toEqual(340) - - describe "isValidBIP39Mnemonic", -> - - it "should recognize BIP-39 test vectors", -> - expect(Helpers.isValidBIP39Mnemonic("letter advice cage absurd amount doctor acoustic avoid letter advice cage above")).toBeTruthy() - expect(Helpers.isValidBIP39Mnemonic("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")).toBeTruthy() - expect(Helpers.isValidBIP39Mnemonic("vessel ladder alter error federal sibling chat ability sun glass valve picture")).toBeTruthy() - expect(Helpers.isValidBIP39Mnemonic("cat swing flag economy stadium alone churn speed unique patch report train")).toBeTruthy() - - it "should not recognize invalid mnemonics", -> - expect(Helpers.isValidBIP39Mnemonic("letter advice cage absurd amount doctor acoustic avoid lettre advice cage above")).toBeFalsy() - expect(Helpers.isValidBIP39Mnemonic("abandon abandn abandon abandon abandon abandon abandon abandon abandon abandon abandon about")).toBeFalsy() - expect(Helpers.isValidBIP39Mnemonic("vessel ladder alter error federal sibling chat ability sun glass valves picture")).toBeFalsy() - expect(Helpers.isValidBIP39Mnemonic("cat swing flag economy stadum alone churn speed unique patch report train")).toBeFalsy() - - it "should not recognize things that aren't mnemonics", -> - expect(Helpers.isValidBIP39Mnemonic("")).toBeFalsy() - expect(Helpers.isValidBIP39Mnemonic("a")).toBeFalsy() - expect(Helpers.isValidBIP39Mnemonic(0)).toBeFalsy() - expect(Helpers.isValidBIP39Mnemonic({ 'mnemonic': "cat swing flag economy stadium alone churn speed unique patch report train" })).toBeFalsy() - - describe "detectPrivateKeyFormat", -> - it "should reject invalid formats", -> - res = Helpers.detectPrivateKeyFormat("46c56bnXQiBjk9mqSYE7ykVQ7NzrRy") - expect(res).toBeNull() - - it "should recognise sipa", -> - res = Helpers.detectPrivateKeyFormat("5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn") - expect(res).toEqual("sipa") - - describe "privateKeyStringToKey", -> - it "should convert sipa format", -> - res = Helpers.privateKeyStringToKey("5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn", "sipa") - expect(Helpers.isKey(res)).toBeTruthy() - - - describe "privateKeyCorrespondsToAddress", -> - - afterEach -> - ImportExport.shouldResolve = false - ImportExport.shouldReject = false - ImportExport.shouldFail = false - - it "should not recognize invalid formats", (done) -> - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyN', "46c56bnXQiBjk9mqSYE7ykVQ7NzrRy") - expect(promise).toBeRejected(done) - - it "should not match base58 private keys to wrong addresses", (done) -> - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyN', "5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn") - promise.then((data) -> - expect(data).toEqual(null) - done() - ).catch((e) -> - console.log(e) - assert(false) - done() - ) - - it "should not match mini private keys to wrong addresses", (done) -> - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyN', "S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy") - promise.then((data) -> - expect(data).toEqual(null) - done() - ).catch((e) -> - console.log(e) - assert(false) - done() - ) - - it "should not recognize BIP-38 addresses without password", (done) -> - ImportExport.shouldResolve = true - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg") - expect(promise).toBeRejected(done) - - it "should not recognize BIP-38 addresses with an empty password", (done) -> - ImportExport.shouldResolve = true - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "") - expect(promise).toBeRejected(done) - - it "should not recognize BIP-38 addresses with a bad password", (done) -> - ImportExport.shouldReject = true - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "pass") - expect(promise).toBeRejected(done) - - it "should not recognize BIP-38 addresses when decryption fails", (done) -> - ImportExport.shouldFail = true - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "pass") - expect(promise).toBeRejected(done) - - it "should recognize BIP-38 addresses when decryption succeeds", (done) -> - ImportExport.shouldResolve = true - promise = Helpers.privateKeyCorrespondsToAddress('19GuvDvMMUZ8vq84wT79fvnvhMd5MnfTkR', "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "pass") - promise.then((data) -> - expect(data).not.toEqual(null) - done() - ) - - it "should match base58 private keys to their right addresses", (done) -> - promise = Helpers.privateKeyCorrespondsToAddress('1BDSbDEechSue77wS44Jn2uDiFaQWom2dG', "5JFXNQvtFZSobCCRPxnTZiW1PDVnXvGBg5XeuUDoUCi8LRsV3gn") - promise.then((data) -> - expect(data).not.toEqual(null) - done() - ) - - it "should match mini private keys to their right addresses", (done) -> - promise = Helpers.privateKeyCorrespondsToAddress('1PZuicD1ACRfBuKEgp2XaJhVvnwpeETDyn', "S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy") - promise.then((data) -> - expect(data).not.toEqual(null) - done() - ) - - describe "isValidPrivateKey", -> - - it "should not recognize invalid hex keys", -> - expect(Helpers.isValidPrivateKey("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141")).toBeFalsy() - expect(Helpers.isValidPrivateKey("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")).toBeFalsy() - expect(Helpers.isValidPrivateKey("0000000000000000000000000000000000000000000000000000000000000000")).toBeFalsy() - - - it "should recognize valide hex keys", -> - expect(Helpers.isValidPrivateKey("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364139")).toBeTruthy() - expect(Helpers.isValidPrivateKey("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF")).toBeTruthy() - expect(Helpers.isValidPrivateKey("0000000000000000000000000000000000000000000000000000000000000001")).toBeTruthy() - - it "should not recognize invalid base 64 keys", -> - expect(Helpers.isValidPrivateKey("ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8=098")).toBeFalsy() - expect(Helpers.isValidPrivateKey("////////////////////////////////////////////")).toBeFalsy() - - it "should recognize valid base 64 keys", -> - expect(Helpers.isValidPrivateKey("ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8=")).toBeTruthy() - expect(Helpers.isValidPrivateKey("/////////////////////rqu3OavSKA7v9JejNA2QTk=")).toBeTruthy() - expect(Helpers.isValidPrivateKey("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE=")).toBeTruthy() - - it "should recognize BIP-38 keys", -> - expect(Helpers.isValidPrivateKey("6PRMUxAWM4XyK8b3wyJRpTwvDdmCKakuP6aGxr3D8MuUaCWVLXM2wnGUCT")).toBeTruthy() - expect(Helpers.isValidPrivateKey("6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg")).toBeTruthy() - - describe "verifyMessage", -> - - it "should verify valid messages", -> - expect(Helpers.verifyMessage("1LGAzcG9dafqtW8eHkFUPjkDKemjv5dxKd", "HxvX3mVUI4cQgpKB98bjl/NOYi2BiaSZEsdfCulyJ7GAWrfP/9WkDazCe45lyhWPZQwZKnYZILz5h3SHn4xFPzg=", "Wright, it is not the same as if I sign Craig Wright, Satoshi.")).toBeTruthy() - - it "should not verify invalid messages", -> - expect(Helpers.verifyMessage("12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", "IH+xpXCKouEcd0E8Hv3NkrYWbhq0P7pAQpI1GcQ2hF2AAsqL2o4agDE8V81i071/bTMz00YKw2YRMoyFMzThZwM=", "Wright, it is not the same as if I sign Craig Wright, Satoshi.")).toBeFalsy() - - - describe "precisionToSatoshiBN", -> - - it "should parse valid strings with fractional values", -> - expect(Helpers.precisionToSatoshiBN("21.0349756").intValue()).toEqual(new BigInteger("2103497560").intValue()) - - it "should parse valid strings with fractional values", -> - expect(Helpers.precisionToSatoshiBN("1").intValue()).toEqual(new BigInteger("100000000").intValue()) \ No newline at end of file diff --git a/tests/keychain_spec.js b/tests/keychain_spec.js new file mode 100644 index 000000000..8bec0ec8f --- /dev/null +++ b/tests/keychain_spec.js @@ -0,0 +1,47 @@ +let proxyquire = require('proxyquireify')(require); +let KeyChain = proxyquire('../src/keychain', {}); +let Base58 = require('bs58'); + +describe('KeyChain constructor', () => { + it('should construct from cache', () => { + let receiveAccount = 'xpub6EFgBWeVDxjHRXVj1GviKqBcLZT6pK8fnpQC9DwXHmLtUjWMdg3MHLCksWcUSn7AUqkYWE4vHUG73NKANWJuCH3sJfvjfk4HZUjwfrRA7p1'; + let kc = new KeyChain(null, null, receiveAccount); + let address = kc.getAddress(100); + expect(address).toEqual('1AFu9ceBtznn9AFDrEiXNaUV6aNXWv5dGk'); + }); + + it('should construct from extended public key and index', () => { + let xpub = 'xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq'; + let kc = new KeyChain(xpub, 0, null); + let address = kc.getAddress(100); + expect(address).toEqual('16XJvK8jvEfh9R4bnfyovUrWqCp57fg4j1'); + }); + + it('should construct from extended private key and get key for index', () => { + let xpriv = 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E'; + let kc = new KeyChain(xpriv, 0, null); + let pkey = Base58.encode(kc.getPrivateKey(100).keyPair.d.toBuffer(32)); + expect(pkey).toEqual('ETsc7CKyRYFNzHPVfR4GDPj3NyJBMLiACRrXg814tJ5w'); + }); + + it('should not print xpriv when you ask for xpub', () => { + let xpriv = 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E'; + let kc = new KeyChain(xpriv, 0, null); + expect(kc.xpub).toEqual('xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP'); + }); + + it('should not create a chain given an invalid index', () => { + let xpriv = 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E'; + let kc = new KeyChain(xpriv, -1, null); + expect(kc.xpub).toEqual(null); + }); + + describe('.init', () => + it('should not overwrite an existing chain', () => { + let xpriv = 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E'; + let kc = new KeyChain(xpriv, 0, null); + let fromInit = kc.init('xprv9yko4kDvhYSdUcqK5e8naLwtGE1Ca57mwJ6JMB8WxeYq8t1w3PpiZfGGvLN6N6GEwLF8XuHnp8HeNLrWWviAjXxb2BFEiLaW2UgukMZ3Zva', 0, null); + expect(fromInit.xpub).toEqual(kc.xpub); + }) + ); +}); diff --git a/tests/keychain_spec.js.coffee b/tests/keychain_spec.js.coffee deleted file mode 100644 index 63ee7165d..000000000 --- a/tests/keychain_spec.js.coffee +++ /dev/null @@ -1,40 +0,0 @@ -proxyquire = require('proxyquireify')(require) -KeyChain = proxyquire('../src/keychain', {}) -Base58 = require('bs58'); - -describe "KeyChain constructor", -> - - it "should construct from cache", -> - receiveAccount = "xpub6EFgBWeVDxjHRXVj1GviKqBcLZT6pK8fnpQC9DwXHmLtUjWMdg3MHLCksWcUSn7AUqkYWE4vHUG73NKANWJuCH3sJfvjfk4HZUjwfrRA7p1" - kc = new KeyChain(null, null, receiveAccount) - address = kc.getAddress(100); - expect(address).toEqual("1AFu9ceBtznn9AFDrEiXNaUV6aNXWv5dGk") - - it "should construct from extended public key and index", -> - xpub = "xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq" - kc = new KeyChain(xpub, 0, null) - address = kc.getAddress(100); - expect(address).toEqual("16XJvK8jvEfh9R4bnfyovUrWqCp57fg4j1") - - it "should construct from extended private key and get key for index", -> - xpriv = "xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E" - kc = new KeyChain(xpriv, 0, null) - pkey = Base58.encode(kc.getPrivateKey(100).keyPair.d.toBuffer(32)); - expect(pkey).toEqual("ETsc7CKyRYFNzHPVfR4GDPj3NyJBMLiACRrXg814tJ5w") - - it "should not print xpriv when you ask for xpub", -> - xpriv = "xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E" - kc = new KeyChain(xpriv, 0, null) - expect(kc.xpub).toEqual("xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP") - - it "should not create a chain given an invalid index", -> - xpriv = "xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E" - kc = new KeyChain(xpriv, -1, null) - expect(kc.xpub).toEqual(null) - - describe ".init", -> - it "should not overwrite an existing chain", -> - xpriv = "xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E" - kc = new KeyChain(xpriv, 0, null) - fromInit = kc.init("xprv9yko4kDvhYSdUcqK5e8naLwtGE1Ca57mwJ6JMB8WxeYq8t1w3PpiZfGGvLN6N6GEwLF8XuHnp8HeNLrWWviAjXxb2BFEiLaW2UgukMZ3Zva", 0, null) - expect(fromInit.xpub).toEqual(kc.xpub) diff --git a/tests/keyring_spec.js b/tests/keyring_spec.js new file mode 100644 index 000000000..a3f354da9 --- /dev/null +++ b/tests/keyring_spec.js @@ -0,0 +1,73 @@ +let proxyquire = require('proxyquireify')(require); +let KeyRing = proxyquire('../src/keyring', {}); +let Base58 = require('bs58'); + +describe('KeyRing', () => { + let cache = { + 'receiveAccount': 'xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP', + 'changeAccount': 'xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m' + }; + let xpriv = 'xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E'; + let xpub = 'xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq'; + + let cacheKR = new KeyRing(null, cache); + let publicKR = new KeyRing(xpub, null); + let privateKR = new KeyRing(xpriv, null); + + it('should be constructed from cache', () => { + expect(cacheKR._receiveChain.xpub).toEqual('xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP'); + expect(cacheKR._changeChain.xpub).toEqual('xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m'); + }); + + it('should be constructed from xpub', () => { + expect(publicKR._receiveChain.xpub).toEqual('xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP'); + expect(publicKR._changeChain.xpub).toEqual('xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m'); + }); + + it('should be constructed from xpriv', () => { + expect(privateKR._receiveChain.xpub).toEqual('xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP'); + expect(privateKR._changeChain.xpub).toEqual('xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m'); + }); + + it('should generate key from path when private keyring', () => { + let pkey = Base58.encode(privateKR.privateKeyFromPath('M/1/101').keyPair.d.toBuffer(32)); + expect(pkey).toEqual('FsY7NFHZNQJL6LzNt7zGqthrMBpfNuDkGwQUCBhQCpTv'); + }); + + it('should not generate private key from path when public keyring', () => { + let pkey = publicKR.privateKeyFromPath('M/1/101'); + expect(pkey).toBe(null); + }); + + it('should not generate private key from path when cached keyring', () => { + let pkey = cacheKR.privateKeyFromPath('M/1/101'); + expect(pkey).toBe(null); + + pkey = cacheKR.privateKeyFromPath('M/0/101'); + expect(pkey).toBe(null); + }); + + it('should not serialize non-expected fields or xprivs', () => { + privateKR.rarefield = 'I am an intruder'; + let json = JSON.stringify(privateKR, null, 2); + let object = JSON.parse(json); + expect(object.receiveAccount).toBeDefined(); + expect(object.changeAccount).toBeDefined(); + expect(object.rarefield).not.toBeDefined(); + expect(object.receiveAccount).toBe(cache.receiveAccount); + expect(object.changeAccount).toBe(cache.changeAccount); + }); + + describe('.init', () => { + it('should not touch an already created object', () => { + let fromInit = privateKR.init(xpriv, null); + expect(fromInit).toEqual(privateKR); + }); + + it('should do nothing with undefined arguments on an empty object', () => { + let kr = new KeyRing(); + let fromInit = kr.init(); + expect(fromInit).toEqual(kr); + }); + }); +}); diff --git a/tests/keyring_spec.js.coffee b/tests/keyring_spec.js.coffee deleted file mode 100644 index 856a68d90..000000000 --- a/tests/keyring_spec.js.coffee +++ /dev/null @@ -1,63 +0,0 @@ -proxyquire = require('proxyquireify')(require) -KeyRing = proxyquire('../src/keyring', {}) -Base58 = require('bs58'); - - -describe "KeyRing", -> - - cache = - "receiveAccount": "xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP" - "changeAccount": "xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m" - xpriv = "xprv9zJ1cTHnqzgBXr9Uq9jXrdbk2LwApa3Vu6dquzhmckQyj1hvK9xugPNsycfveTGcTy2571Rq71daBpe1QESUsjX7d2ZHVVXEwJEwDiiMD7E" - xpub = "xpub6DHN1xpggNEUkLDwwBGYDmYUaNmfE2mMGKZSiP7PB5wxbp34rhHAEBhMpsjHEwZWsHY2kPmPPD1w6gxGSBe3bXQzCn2WV8FRd7ZKpsiGHMq" - - cacheKR = new KeyRing(null , cache) - publicKR = new KeyRing(xpub , null) - privateKR = new KeyRing(xpriv, null) - - it "should be constructed from cache", -> - expect(cacheKR._receiveChain.xpub).toEqual("xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP") - expect(cacheKR._changeChain.xpub).toEqual("xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m") - - it "should be constructed from xpub", -> - expect(publicKR._receiveChain.xpub).toEqual("xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP") - expect(publicKR._changeChain.xpub).toEqual("xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m") - - it "should be constructed from xpriv", -> - expect(privateKR._receiveChain.xpub).toEqual("xpub6FMWuMox3fJxEv2TSLN6jYQg6tHZBS7tKRSu7w4Q7F9K2UsSu4RxtwxfeHVhUv3csTSCRkKREpiVdr8EquBPXfBDZSMe84wmN9LzR3rwNZP") - expect(privateKR._changeChain.xpub).toEqual("xpub6FMWuMox3fJxGARtaDVY6e9st4Hk5j8Ui6r7XLnBPFXPXkajXNiAfiEqBakuDKYYeRf4ERtPm1TawBqKaBWj2dsHNJT4rSsugssTnaDsz2m") - - it "should generate key from path when private keyring", -> - pkey = Base58.encode(privateKR.privateKeyFromPath("M/1/101").keyPair.d.toBuffer(32)) - expect(pkey).toEqual("FsY7NFHZNQJL6LzNt7zGqthrMBpfNuDkGwQUCBhQCpTv") - - it "should not generate private key from path when public keyring", -> - pkey = publicKR.privateKeyFromPath("M/1/101") - expect(pkey).toBe(null) - - it "should not generate private key from path when cached keyring", -> - pkey = cacheKR.privateKeyFromPath("M/1/101") - expect(pkey).toBe(null) - - pkey = cacheKR.privateKeyFromPath("M/0/101") - expect(pkey).toBe(null) - - it 'should not serialize non-expected fields or xprivs', -> - privateKR.rarefield = "I am an intruder" - json = JSON.stringify(privateKR, null, 2) - object = JSON.parse(json) - expect(object.receiveAccount).toBeDefined() - expect(object.changeAccount).toBeDefined() - expect(object.rarefield).not.toBeDefined() - expect(object.receiveAccount).toBe(cache.receiveAccount) - expect(object.changeAccount).toBe(cache.changeAccount) - - describe ".init", -> - it "should not touch an already created object", -> - fromInit = privateKR.init(xpriv, null) - expect(fromInit).toEqual(privateKR) - - it "should do nothing with undefined arguments on an empty object", -> - kr = new KeyRing() - fromInit = kr.init() - expect(fromInit).toEqual(kr) diff --git a/tests/metadata_spec.js b/tests/metadata_spec.js new file mode 100644 index 000000000..b585fbcee --- /dev/null +++ b/tests/metadata_spec.js @@ -0,0 +1,287 @@ +let proxyquire = require('proxyquireify')(require); + +let OriginalWalletCrypto = require('../src/wallet-crypto'); +let OriginalBitcoin = require('bitcoinjs-lib'); + +// mock derivation to generate hdnode from string deterministically +let masterhdnode = { + deriveHardened (purpose) { + return { + deriveHardened (payloadType) { + return { + deriveHardened (i) { + return BitcoinJS.HDNode.fromSeedBuffer( + OriginalWalletCrypto.sha256( + `m/${purpose}'/${payloadType}'/${i}'`)); + } + }; + } + }; + } +}; + +let metahdnode = { + deriveHardened (payloadType) { + return { + deriveHardened (i) { + return BitcoinJS.HDNode.fromSeedBuffer( + OriginalWalletCrypto.sha256( + `m/proposit'/${payloadType}'/${i}'`)); + } + }; + } +}; + +var BitcoinJS = {}; +let WalletCrypto = {}; +let stubs = { + './wallet-crypto': WalletCrypto, + 'bitcoinjs-lib': BitcoinJS +}; +let Metadata = proxyquire('../src/metadata', stubs); + +describe('Metadata', () => { + let c; + let response = { + payload: 'Q1ayZRanBA5pzRsYQOzni43yMVl53T65DGUjp1cEgzpl/HOcc6PcGtWkrvOREtnc', + version: 1, + type_id: -1, + signature: 'IBZQntOxNxJlg5nKMmzi7mH4l3+BZrZnVDz+eJas3QKaCApbcQuTy9XCTSuSRpWuJ4mmsW/PuWhAFOv63DZ6+fs=', + prev_magic_hash: '6006136dcb283dff85ce8b5c25f8a339437943b136bfefe2b10d7a902b25f957', + created_at: 1480072957000, + updated_at: 1480072976166, + address: '19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA' + }; + + let nonEncResponse = { + payload: 'eyJoZWxsbyI6IndvcmxkIn0=', + version: 1, + type_id: -1, + signature: 'IAC4PIBfjHPSp9w7Lg/UaVfAo1WZnB07aViLXasNQZQnMDFWS4q8mDgs7o8DhDb0yP+QwbZ/rrFlEARDy6+d0S8=', + prev_magic_hash: '33f698a2819f6e779cb0cc579a08975d2fc92632c31d294f363bde70560e722a', + created_at: 1480072957000, + updated_at: 1480074223395, + address: '19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA' + }; + + beforeEach(() => JasminePromiseMatchers.install()); + + afterEach(() => JasminePromiseMatchers.uninstall()); + + describe('Metadata.message', () => { + it('should compute message with prevMagicHash', () => { + let payload = Buffer.from('payload'); + let prevMagic = Buffer.from('prevMagic'); + let message = Metadata.message(payload, prevMagic); + return expect(message).toBe('cHJldk1hZ2ljI59Z7VXnN8dxR89VrQwbAwttfudIp0JpUvm4UtWpNeU='); + }); + + return it('should compute message without prevMagicHash', () => { + let payload = Buffer.from('payload'); + let prevMagic; + let message = Metadata.message(payload, prevMagic); + return expect(message).toBe('cGF5bG9hZA=='); + }); + }); + + describe('Metadata.magic', () => { + it('should compute magicHash with prevMagicHash', () => { + let payload = Buffer.from('payload'); + let prevMagic = Buffer.from('prevMagic'); + let magic = Metadata.magic(payload, prevMagic); + return expect(magic.toString('base64')).toBe('CDaNC0fPlsRlIyjKeELKrrttBEP7g27PCv/pIY4cWCQ='); + }); + + return it('should compute magicHash without prevMagicHash', () => { + let payload = Buffer.from('payload'); + let prevMagic; + let magic = Metadata.magic(payload, prevMagic); + return expect(magic.toString('base64')).toBe('ADDQotVAKs732nTFsqr7RtJsY9n3Ng6OKIcEd/BNjCI='); + }); + }); + + describe('Metadata.computeSignature', () => { + it('should compute signature with prevMagicHash', () => { + let k = OriginalBitcoin.ECPair.fromWIF('L1tXV2tuvFWvLw2JTZ1yYz8gxSXPawvoDemrwruTtwp4hhn5cbD3'); + let payload = Buffer.from('payload'); + let prevMagic = Buffer.from('prevMagic'); + let signature = Metadata.computeSignature(k, payload, prevMagic); + return expect(signature.toString('base64')).toBe('H0Ggd/NL6cfGVMCUnUEtbHcFmwbt2i3CXP4dzAtMd6lFCKdbPuCezCVnfRoSvAWeajvP0CkgWxNLnWzjqv1gKfw='); + }); + + return it('should compute signature without prevMagicHash', () => { + let k = OriginalBitcoin.ECPair.fromWIF('L1tXV2tuvFWvLw2JTZ1yYz8gxSXPawvoDemrwruTtwp4hhn5cbD3'); + let payload = Buffer.from('payload'); + let prevMagic; + let signature = Metadata.computeSignature(k, payload, prevMagic); + return expect(signature.toString('base64')).toBe('INBtCI3+o9zQuTwijKDN1L/caBjmXI38hJAJ6sse9+L6O8dwvPptLUl/aP4l9Rz+zfJ9bUJj1UJwp/YeQJBFBBM='); + }); + }); + + describe('Metadata.verifyResponse', () => { + it('should propagate null', () => { + let verified = Metadata.verifyResponse('1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX', null); + return expect(verified).toBe(null); + }); + + it('should verify and compute the new magic hash', () => { + let verified = Metadata.verifyResponse(response.address, response); + let expectedMagicHash = Buffer.from('sS4b2JTeq53jyrAVYX8WQeIU/wDezNiFX34jNYSmfKQ=', 'base64'); + return expect(verified).toEqual(jasmine.objectContaining({compute_new_magic_hash: expectedMagicHash})); + }); + + return it('should fail and launch an exception', () => { + let shouldFail = () => Metadata.verifyResponse('1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX', response); + return expect(shouldFail).toThrow(new Error('METADATA_SIGNATURE_VERIFICATION_ERROR')); + }); + }); + + describe('Metadata.extractResponse', () => { + it('should propagate null', () => { + let extracted = Metadata.extractResponse('encrypteionKey', null); + return expect(extracted).toBe(null); + }); + + it('should extract encrypted data', () => { + let wif = 'Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'; + let k = OriginalBitcoin.ECPair.fromWIF(wif); + let pkbuff = k.d.toBuffer(); + let enck = OriginalWalletCrypto.sha256(pkbuff); + let extracted = JSON.stringify(Metadata.extractResponse(enck, response)); + let hello = JSON.stringify({hello: 'world'}); + return expect(extracted).toBe(hello); + }); + + return it('should extract non-encrypted data', () => { + let extracted = JSON.stringify(Metadata.extractResponse(undefined, nonEncResponse)); + let hello = JSON.stringify({hello: 'world'}); + return expect(extracted).toBe(hello); + }); + }); + + describe('class', () => + describe('new Metadata()', () => { + it('should instantiate', () => { + let k = OriginalBitcoin.ECPair.fromWIF('Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'); + let m = new Metadata(k); + return expect(m.constructor.name).toEqual('Metadata'); + }); + + it('should set the address', () => { + let k = OriginalBitcoin.ECPair.fromWIF('Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'); + let m = new Metadata(k); + return expect(m._address).toEqual('19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA'); + }); + + it('should set the signature KeyPair', () => { + let k = OriginalBitcoin.ECPair.fromWIF('Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'); + let m = new Metadata(k); + return expect(m._signKey.toWIF()).toEqual('Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'); + }); + + return it('should set the encryption key', () => { + let k = OriginalBitcoin.ECPair.fromWIF('Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'); + let m = new Metadata(k, 'enc'); + return expect(m._encKeyBuffer).toEqual('enc'); + }); + }) + ); + + describe('read', () => { + it('should resolve with null for 404 entry', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => Promise.resolve(null)); + let promise = Metadata.read('19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA'); + promise.then(res => expect(res).toBe(null)); + return expect(promise).toBeResolved(done); + }); + + return it('should read non-encrypted data', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => new Promise(resolve => resolve(nonEncResponse))); + let promise = Metadata.read('19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA'); + return expect(promise).toBeResolvedWith(jasmine.objectContaining({hello: 'world'}), done); + }); + }); + + describe('API', () => { + beforeEach(() => { + let k = OriginalBitcoin.ECPair.fromWIF('Kz5XipXFW4v4CVEd1N77q5rRdFFsgVovC2AuivvZ5MfDZhQBzuFA'); + let pkbuff = k.d.toBuffer(); + let enck = OriginalWalletCrypto.sha256(pkbuff); + c = new Metadata(k, enck); + }); + + describe('fetch', () => { + it('should resolve with null for 404 entry', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => Promise.resolve(null)); + let promise = c.fetch(); + promise.then(res => expect(res).toBe(null)); + return expect(promise).toBeResolved(done); + }); + + return it('should resolve decrypted data', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => Promise.resolve(response)); + let promise = c.fetch(); + return expect(promise).toBeResolvedWith(jasmine.objectContaining({hello: 'world'}), done); + }); + }); + + describe('create', () => + + it('should call request with encrypted data', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => Promise.resolve(response)); + spyOn(WalletCrypto, 'encryptDataWithKey').and.callFake((data, key) => Buffer.from(data).toString('base64')); + let promise = c.create({hello: 'world'}); + promise.then(() => { + return expect(Metadata.request).toHaveBeenCalledWith( + 'PUT', + '19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA', + Object({ version: 1, payload: 'eyJoZWxsbyI6IndvcmxkIn0=', signature: 'IEPGABLAeYLlFvRcxJjPwHtPCZIg16sUqLUInw5MhxUzMGbUSnHB+R1a0KRkqTQ0JsGSFzXol+wweZEqMgrHtuQ=', prev_magic_hash: null, type_id: -1 }) + ); + } + ); + return expect(promise).toBeResolved(done); + }) + ); + + return describe('update', () => { + it('should call request with encrypted data', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => Promise.resolve(response)); + spyOn(WalletCrypto, 'encryptDataWithKey').and.callFake((data, key) => Buffer.from(data).toString('base64')); + let promise = c.update({hello: 'world'}); + promise.then(() => { + return expect(Metadata.request).toHaveBeenCalledWith( + 'PUT', + '19ryWY7sn9G6yX74AJKSs83vnhdydcvDjA', + Object({ version: 1, payload: 'eyJoZWxsbyI6IndvcmxkIn0=', signature: 'IEPGABLAeYLlFvRcxJjPwHtPCZIg16sUqLUInw5MhxUzMGbUSnHB+R1a0KRkqTQ0JsGSFzXol+wweZEqMgrHtuQ=', prev_magic_hash: null, type_id: -1 }) + ); + } + ); + return expect(promise).toBeResolved(done); + }); + + return it('should not update if no data changes', done => { + spyOn(Metadata, 'request').and.callFake((method, endpoint, data) => Promise.resolve(response)); + spyOn(WalletCrypto, 'encryptDataWithKey').and.callFake((data, key) => Buffer.from(data).toString('base64')); + c._value = 'no changes'; + let promise = c.update('no changes'); + promise.then(() => { + return expect(Metadata.request).not.toHaveBeenCalled(); + } + ); + return expect(promise).toBeResolved(done); + }); + }); + }); + + return describe('Factory', () => { + it('should create metadata instance from metadata hdnode with the right derivation', () => { + let m = Metadata.fromMetadataHDNode(metahdnode, 1714); + return expect(m._address).toBe('1Auq6HbwMkxM3gVjdN8RbQdZbF5sfLuskv'); + }); + + return it('should create metadata instance from masterdata hdnode with the right derivation', () => { + let m = Metadata.fromMasterHDNode(masterhdnode, 1714); + return expect(m._address).toBe('12auwetBz7DetiL4i58L813y8bdN8UtPzc'); + }); + }); +}); diff --git a/tests/metadata_spec.js.coffee b/tests/metadata_spec.js.coffee deleted file mode 100644 index a00a4ff10..000000000 --- a/tests/metadata_spec.js.coffee +++ /dev/null @@ -1,480 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -OriginalWalletCrypto = require('../src/wallet-crypto'); - -MyWallet = - wallet: - syncWallet: () -> - hdwallet: - getMasterHDNode: () -> - deriveHardened: (purpose) -> - deriveHardened: (payloadType) -> - deriveHardened: (i) -> - path = "m/#{ purpose }'/#{ payloadType }'/#{ i }'" - { - getAddress: () -> path + "-address" - getPublicKeyBuffer: () -> - slice: (start, offset) -> - "#{ path }-pubkey-buffer-slice-#{ start }-#{ offset }" - keyPair: - toString: () -> "#{ path }-keyPair" - d: - toBuffer: () -> - "#{ path }-private-key-buffer" - } - -BitcoinJS = { - message: - magicHash: (payload) -> - "#{ payload }|magicHash" - sign: (keyPair, payload) -> - "#{ payload }|#{ keyPair }-signature" - verify: (address, signatureBuffer, message) -> - signature = signatureBuffer.toString('utf8') - key = address.replace("0'-address", "0'-keyPair") - if signature.indexOf(key) == -1 - false - else - true -} - -WalletCrypto = { - sha256: (x) -> - if x == "info.blockchain.metadata" - OriginalWalletCrypto.sha256(x) # Too tedious to mock - else - "#{ x }|sha256" - - encryptDataWithKey: (data, key) -> - "random|#{ data }|encrypted-with-random+#{ key }|base64" - - - decryptDataWithKey: (data, key) -> - payload = data.split('|')[1] -} - -stubs = { - './wallet': MyWallet, - './wallet-crypto': WalletCrypto, - 'bitcoinjs-lib': BitcoinJS -} - -Metadata = proxyquire('../src/metadata', stubs) - -describe "Metadata", -> - - c = undefined - helloWorld = {hello: "world"} - unencryptedData = JSON.stringify(helloWorld) - encryptedData = "random|#{ unencryptedData }|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64" - serverPayload = { - version:1, - payload_type_id:2 - payload: encryptedData, - signature:"#{ encryptedData }|m/510742'/2'/0'-keyPair-signature", - created_at:1468316898000, - updated_at:1468316941000, - } - expectedPayloadPOST = { - version:1, - payload_type_id:2 - payload: encryptedData, - signature:"#{ encryptedData }|m/510742'/2'/0'-keyPair-signature" - } - unencryptedDataPUT = JSON.stringify({hello: 'world again'}) - encryptedDataPUT = "random|#{ unencryptedDataPUT }|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64" - - expectedPayloadPUT = { - version:1, - payload_type_id:2 - prev_magic_hash: "#{ unencryptedDataPUT }|magicHash" - payload: encryptedDataPUT, - signature:"#{ encryptedDataPUT }|m/510742'/2'/0'-keyPair-signature" - } - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "class", -> - describe "new Metadata()", -> - - it "should instantiate", -> - c = new Metadata(2) - expect(c.constructor.name).toEqual("Metadata") - - it "should set the address", -> - expect(c._address).toEqual("m/510742'/2'/0'-address") - - it "should set the signature KeyPair", -> - expect(c._signatureKeyPair.toString()).toEqual("m/510742'/2'/0'-keyPair") - - it "should set the encryption key", -> - expect(c._encryptionKey.toString()).toEqual("m/510742'/2'/1'-private-key-buffer|sha256") - - - describe "API", -> - beforeEach -> - c = new Metadata(2) - spyOn(c, "request").and.callFake((method, endpoint, data) -> - if method == "GET" && endpoint == "" - new Promise((resolve) -> resolve(serverPayload)) - else # 404 is resolved as null - new Promise((resolve) -> resolve(null)) - ) - - describe "API", -> - describe "GET", -> - it "should call request with GET", -> - c.GET("m/510742'/2'/0'-keyPair") - expect(c.request).toHaveBeenCalledWith( - 'GET', - "m/510742'/2'/0'-keyPair", - undefined - ) - - it "should resolve with an encrypted payload", -> - promise = c.GET("m/510742'/2'/0'-keyPair") - expect(promise).toBeResolvedWith(serverPayload) - - it "should resolve 404 with null", -> - promise = c.GET("m/510742'/3'/0'-keyPair") - expect(promise).toBeResolvedWith(null) - - describe "POST", -> - it "should call request with POST", -> - c.POST("m/510742'/3'/0'-keyPair", "new_payload") - expect(c.request).toHaveBeenCalledWith( - 'POST', - "m/510742'/3'/0'-keyPair", - "new_payload" - ) - - describe "PUT", -> - it "should call request with PUT", -> - c.PUT("m/510742'/3'/0'-keyPair", "new_payload") - expect(c.request).toHaveBeenCalledWith( - 'PUT', - "m/510742'/3'/0'-keyPair", - "new_payload" - ) - - describe "instance", -> - promise = undefined - - beforeEach -> - c = new Metadata(2) - - spyOn(c, "GET").and.callFake((endpoint, data) -> - new Promise (resolve, reject) -> - if endpoint == "m/510742'/2'/0'-address" # 200 - payloadWithBase64sig = JSON.parse(JSON.stringify(serverPayload)); - payloadWithBase64sig.signature = Buffer(serverPayload.signature, 'utf8').toString('base64') - resolve(payloadWithBase64sig) - else if endpoint == "m/510742'/3'/0'-address" # 404 - resolve(null) - else - reject("Unknown endpoint") - ) - - spyOn(c, "POST").and.callFake((endpoint, data) -> - new Promise (resolve, reject) -> - if endpoint == "m/510742'/2'/0'-address" - if data.payload && data.payload.split('|')[1] == '"fail"' - reject() - else - resolve({}) - else - reject("Unknown endpoint") - ) - - spyOn(c, "PUT").and.callFake((endpoint, data) -> - new Promise (resolve, reject) -> - if endpoint == "m/510742'/2'/0'-address" - resolve({}) - else - reject("Unknown endpoint") - ) - - describe "setMagicHash", -> - it "should calculate and store based on contents", -> - c.setMagicHash(encryptedData) - expect(c._magicHash).toEqual("#{ encryptedData }|magicHash") - - describe "create", -> - it "should encrypt data", (done) -> - spyOn(WalletCrypto, "encryptDataWithKey") - c.create({hello: 'world'}).then -> - expect(WalletCrypto.encryptDataWithKey).toHaveBeenCalledWith( - JSON.stringify({hello: 'world'}), - c._encryptionKey - ) - done() - - it "magicHash should be null initially", -> - expect(c._magicHash).toEqual(null) - - it "value should be null initially", -> - expect(c._value).toEqual(null) - - describe "POST", -> - postData = undefined - - beforeEach (done) -> - c.create({hello: 'world'}).then -> - postData = c.POST.calls.argsFor(0)[1] - done() - - it "should be called", -> - expect(c.POST).toHaveBeenCalled() - - it "should use the right address", -> - expect(c.POST.calls.argsFor(0)[0]).toEqual("m/510742'/2'/0'-address") - - it "should use version 1", -> - expect(postData.version).toEqual(1) - - it "should use the right payload type", -> - expect(postData.payload_type_id).toEqual(c._payloadTypeId) - - it "should send encrypted payload", -> - expect(postData.payload).toEqual(expectedPayloadPOST.payload) - - it "should send signature", -> - expect(postData.signature).toEqual(expectedPayloadPOST.signature) - - it "should not send additional arguments", -> - expect(Object.keys(postData).length).toEqual(4) - - describe "if successful", -> - beforeEach -> - promise = c.create({hello: 'world'}) - - it "should resolve", (done) -> - expect(promise).toBeResolved(done) - - it "should remember the new value", (done) -> - promise.then(() -> - expect(c._value).toEqual({hello: "world"}) - done() - ) - - it "should remember the magic hash", (done) -> - promise.then(() -> - expect(c._magicHash).toEqual("random|{\"hello\":\"world\"}|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64|magicHash") - done() - ) - - describe "if failed", -> - beforeEach -> - promise = c.create('fail') - - it "should reject", (done) -> - expect(promise).toBeRejected(done) - - it "should not have a value or magic hash", (done) -> - promise.catch(() -> - expect(c._magicHash).toEqual(null) - done() - ) - - describe "fetch", -> - it "magicHash should be null initially", -> - expect(c._magicHash).toEqual(null) - - it "value should be null initially", -> - expect(c._value).toEqual(null) - - it "should GET", (done) -> - c.fetch().then -> - expect(c.GET).toHaveBeenCalled() - done() - - it "should use the right address", (done) -> - c.fetch().then -> - expect(c.GET.calls.argsFor(0)[0]).toEqual("m/510742'/2'/0'-address") - done() - - it "should decrypt data and verify signature", (done) -> - spyOn(WalletCrypto, "decryptDataWithKey").and.callThrough() - spyOn(BitcoinJS.message, "verify").and.callThrough() - - c.fetch().then(() -> - expect(WalletCrypto.decryptDataWithKey).toHaveBeenCalledWith( - encryptedData, - c._encryptionKey - ) - - expect(BitcoinJS.message.verify).toHaveBeenCalled() - - args = BitcoinJS.message.verify.calls.argsFor(0) - - expect(args[0]).toEqual("m/510742'/2'/0'-address") - expect(args[1].toString('utf8')).toEqual(serverPayload.signature) - expect(args[2]).toEqual(encryptedData) - - done() - ) - - - - describe "if successful", -> - beforeEach -> - promise = c.fetch() - - it "should resolve with payload", (done) -> - expect(promise).toBeResolvedWith(jasmine.objectContaining({hello: "world"}), done) - - it "should remember the new value", (done) -> - promise.then(() -> - expect(c._value).toEqual({hello: "world"}) - done() - ) - - it "should remember the magic hash", (done) -> - promise.then(() -> - expect(c._magicHash).toEqual("random|{\"hello\":\"world\"}|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64|magicHash") - done() - ) - - describe "if resolved with null", -> - beforeEach -> - c._payloadTypeId = 3 - c._address = "m/510742'/3'/0'-address" - promise = c.fetch() - - it "should return null", (done) -> - expect(promise).toBeResolvedWith(null) - done() - - it "should not have a value or magic hash", (done) -> - promise.then((val) -> - expect(c._magicHash).toEqual(null) - done() - ) - - describe "if failed", -> - beforeEach -> - c._payloadTypeId = -1 - c._address = "fail" - promise = c.fetch() - - it "should reject", (done) -> - expect(promise).toBeRejected() - done() - - it "should not have a value or magic hash", (done) -> - promise.catch(() -> - expect(c._magicHash).toEqual(null) - done() - ) - - describe "update", -> - beforeEach -> - c._magicHash = "random|{\"hello\":\"world\"}|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64|magicHash" - c._value = helloWorld - c._previousPayload = '{"hello":"world"}' - spyOn(WalletCrypto, "encryptDataWithKey").and.callThrough() - - it "should immedidately resolve for identical object", (done) -> - promise = c.update(helloWorld) - expect(promise).toBeResolved() - promise.then(() -> - expect(WalletCrypto.encryptDataWithKey).not.toHaveBeenCalled() - done() - ) - - it "should immedidately resolve for the same string", (done) -> - promise = c.update({hello: 'world'}) - expect(promise).toBeResolved() - promise.then(() -> - expect(WalletCrypto.encryptDataWithKey).not.toHaveBeenCalled() - done() - ) - - it "should update on the server", (done) -> - promise = c.update({hello: 'world again'}) - expect(promise).toBeResolved() - - promise.then(() -> - expect(WalletCrypto.encryptDataWithKey).toHaveBeenCalledWith( - JSON.stringify({hello: 'world again'}), - c._encryptionKey - ) - - done() - ) - - describe "PUT", -> - prevHash = undefined - putData = undefined - - beforeEach (done) -> - prevHash = c._magicHash - c.update({hello: 'world again'}).then -> - putData = c.PUT.calls.argsFor(0)[1] - done() - - it "should be called", -> - expect(c.PUT).toHaveBeenCalled() - - it "should use the right address", -> - expect(c.PUT.calls.argsFor(0)[0]).toEqual("m/510742'/2'/0'-address") - - it "should use version 1", -> - expect(putData.version).toEqual(1) - - it "should use the right payload type", -> - expect(putData.payload_type_id).toEqual(c._payloadTypeId) - - it "should send the previous magic hash", -> - expect(putData.prev_magic_hash).toEqual(prevHash) - - it "should send encrypted payload", -> - expect(putData.payload).toEqual(expectedPayloadPUT.payload) - - it "should send signature", -> - expect(putData.signature).toEqual(expectedPayloadPUT.signature) - - it "should not send additional arguments", -> - expect(Object.keys(putData).length).toEqual(5) - - describe "if successful", -> - beforeEach -> - promise = c.update({hello: 'world again'}) - - it "should remember the new value", (done) -> - promise.then(() -> - expect(c._value).toEqual({hello: "world again"}) - done() - ) - - it "should remember the magic hash", (done) -> - promise.then(() -> - expect(c._magicHash).toEqual("random|{\"hello\":\"world again\"}|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64|magicHash") - done() - ) - - describe "if failed", -> - beforeEach -> - c._payloadTypeId = -1 - c._address = "fail" - promise = c.update({hello: 'world again'}) - - it "should reject", (done) -> - expect(promise).toBeRejected() - done() - - it "should keep the previous value", (done) -> - promise.catch(() -> - expect(c._value).toEqual(helloWorld) - done() - ) - - it "should keep the previous magic hash", (done) -> - promise.catch(() -> - expect(c._magicHash).toEqual("random|{\"hello\":\"world\"}|encrypted-with-random+m/510742'/2'/1'-private-key-buffer|sha256|base64|magicHash") - done() - ) diff --git a/tests/mocks/spender_mock.js b/tests/mocks/spender_mock.js new file mode 100644 index 000000000..51d070988 --- /dev/null +++ b/tests/mocks/spender_mock.js @@ -0,0 +1,98 @@ +var Bitcoin = require('bitcoinjs-lib'); + +var spenderM = {}; + +// ############################################################################# +// Addres to Address payment mock + +spenderM.addToAdd = {}; +spenderM.addToAdd.coins = { + 'unspent_outputs': [ + { + 'tx_hash': 'c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4', + 'tx_hash_big_endian': 'f42af175402dfe93178c8f7e3571155653faa97853c36169bc6d33188f3271c9', + 'tx_index': 85110550, + 'tx_output_n': 1, + 'script': '76a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac', + 'value': 40000, + 'value_hex': '009c40', + 'confirmations': 267, + 'hash': 'f42af175402dfe93178c8f7e3571155653faa97853c36169bc6d33188f3271c9', + 'index': 1 + }, + { + 'tx_hash': '6876d0264ed25aa69b30cda92020f7d7e78ae611be91368c843dfb27139fb45a', + 'tx_hash_big_endian': '5ab49f1327fb3d848c3691be11e68ae7d7f72020a9cd309ba65ad24e26d07668', + 'tx_index': 85114355, + 'tx_output_n': 0, + 'script': '76a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac', + 'value': 30000, + 'value_hex': '7530', + 'confirmations': 262, + 'hash': '5ab49f1327fb3d848c3691be11e68ae7d7f72020a9cd309ba65ad24e26d07668', + 'index': 0 + } + ] +}; +// encrypted with password 'hola' +spenderM.addToAdd.encPrivateKey = '6lzfKSFi/9xkli8eRaPDoyHDkaBzRWKdWEamoMvsmJptnhwPZzA/w5EzIljjjrNpbQn8CzvxJci756AXO/Cq7A=='; +spenderM.addToAdd.privateKey = '8CvkFF5WZ7w5YDeoPfJ9BNPKEQUgqZGrHUSdboHaRyoi'; +spenderM.addToAdd.fromAddress = '1CCMvFa5Ric3CcnRWJzSaZYXmCtZzzDLiX'; +spenderM.addToAdd.toAddress = '1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF'; +spenderM.addToAdd.amount = 20000; +spenderM.addToAdd.toAddresses = ['1PiHKNK4NTxcuZEWqxKn9tF82kUoen2QTD', '1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF']; +spenderM.addToAdd.amounts = [20000, 10000]; +spenderM.addToAdd.txHash1M = '0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008a47304402202fdf972a3481bd81442e6c212cbfd07cd8e1db440b5a4981b059f96e4c9342290220353447e526dc141308428b8affce3028cc21bded6d158b5b8471cde51ecf901c0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff02204e0000000000001976a914f9217131115f61d6835b97bf388089c9f999e33688ac10270000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac00000000'; +spenderM.addToAdd.txHash2M = '0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008b483045022100eb594841c5c78d9fc56543e33c4fabdb17117e0acb2de727bb8d24f01036de9f0220727009bee45330678dfc4bde9b04754012e932e5f8e0554b01b75b4bb74956150141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff0210270000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac204e0000000000001976a914f9217131115f61d6835b97bf388089c9f999e33688ac00000000'; +spenderM.addToAdd.fee = 10000; +spenderM.addToAdd.note = undefined; +spenderM.addToAdd.txHash1 = '0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008b483045022100c3ea4e526b5aa88a3f4122c093f27afc3b1a7f6a3d6938139668a1ed8d0a77c202207b3b53f96827938f22677a64068e50907a5662bc108d35e0180269872bf9c8be0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff02204e0000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac10270000000000001976a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac00000000'; +spenderM.addToAdd.txHash2 = '0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008a473044022044e111fbce09ce4a4013a94be2136c8f7e2ca6439ea24e37fcda934f1d6fcc34022077670efeee20559a28500d7840df061e24b505f8a309a1eabe87edaad633c0cb0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff0210270000000000001976a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac204e0000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac00000000'; +spenderM.addToAdd.sweepHex = '0100000002c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008a47304402203cd3133b9ac7d8768e6cf2cc54d0328a4d6cfa0a3907a7f103c868bc40d945d402202f59a390e61582a3d9da403542573fb8d3b7072bd4c4475b14a98101cca7a2090141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff6876d0264ed25aa69b30cda92020f7d7e78ae611be91368c843dfb27139fb45a000000008a473044022077e571a6781123e1791a29022438b12a6607db7c04ceaa47f7636c171c56313602203e69e7fd67c8a344325f8e2fe5d511ddd6978a9ada246234534f561b01a2e4bc0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff0160ea0000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac00000000'; +spenderM.addToAdd.toHdAccount = [{ getReceiveAddress () { return '1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF'; } }]; +spenderM.addToAdd.fromAddPKey = Bitcoin.ECPair.fromWIF('5JdSM2jdvnZm8sLBz8Ac9Sfq1utDcxopzPhswf3c645s18AjX92'); +spenderM.addToAdd.privKey = '8CvkFF5WZ7w5YDeoPfJ9BNPKEQUgqZGrHUSdboHaRyoi'; +// ############################################################################# +spenderM.AccountToAdd = {}; +spenderM.AccountToAdd.coins = { 'unspent_outputs': [ + { + 'tx_hash': 'd93b4753e25b1669cb6522784af7ae391cb7181048e9330e09b62cd5374fe100', + 'tx_hash_big_endian': '00e14f37d52cb6090e33e9481018b71c39aef74a782265cb69165be253473bd9', + 'tx_index': 85757889, + 'tx_output_n': 0, + 'script': '76a914a93b2f63bb449b5c7c9bc05b37c541577266691b88ac', + 'xpub': { + 'm': 'xpub6DHN1xpggNEUbWgGJyMPRFGvYm6pizUnv4TQMAtgYBikkh75dyp9Gf9QcKETpWZkLjtB4zYr2eVaHQ4g3rhj46Aeu4FykMWSayrqmRmEMEZ', + 'path': 'M/0/30' + }, + 'value': 200000, + 'value_hex': '030d40', + 'confirmations': 0, + 'hash': '00e14f37d52cb6090e33e9481018b71c39aef74a782265cb69165be253473bd9', + 'index': 0 + } +] +}; +spenderM.AccountToAdd.toAddress = '1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF'; +spenderM.AccountToAdd.amount = 10000; +spenderM.AccountToAdd.fee = 10000; +spenderM.AccountToAdd.note = undefined; +spenderM.AccountToAdd.fromAccount = 0; +spenderM.AccountToAdd.txHash1 = 'a75fb7ac06ea57291ba23dfdb0b1328d5ecd532df45075089ed82fa4bb6e4b71'; +spenderM.AccountToAdd.txHash2 = 'e636e504df7dd6100f20bf0bdaaf9bda4d2b9402d39d76ebbc283485d4d466ab'; +spenderM.AccountToAdd.fromHdAccount = [ + { + getReceiveAddress () { return '1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF'; }, + getChangeAddress () { return '1PiHKNK4NTxcuZEWqxKn9tF82kUoen2QTD'; }, + containsAddressInCache () { return false; }, + extendedPrivateKey: 'xprv9zJ1cTHnqzgBP2boCwpP47LBzjGLKXkwYqXoYnV4yrBmstmw6SVtirpvm4GESg9YLn9R386qpmnsrcC5rvrpEJAXSrfqQR3qGtjGv5ddV9g' + } +]; +spenderM.AccountToAdd.fromHdAccountERROR = [ + { + getReceiveAddress () { return '1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF'; }, + getChangeAddress () { return '1PiHKNK4NTxcuZEWqxKn9tF82kUoen2QTD'; }, + containsAddressInCache () { return true; }, + extendedPrivateKey: 'xprv9zJ1cTHnqzgBP2boCwpP47LBzjGLKXkwYqXoYnV4yrBmstmw6SVtirpvm4GESg9YLn9R386qpmnsrcC5rvrpEJAXSrfqQR3qGtjGv5ddV9g' + } +]; diff --git a/tests/mocks/spender_mock.js.coffee b/tests/mocks/spender_mock.js.coffee deleted file mode 100644 index 5b22c2be0..000000000 --- a/tests/mocks/spender_mock.js.coffee +++ /dev/null @@ -1,94 +0,0 @@ -Bitcoin = require('bitcoinjs-lib'); - -global.spenderM = {} - -################################################################################ -# Addres to Address payment mock - -spenderM.addToAdd = {} -spenderM.addToAdd.coins = 'unspent_outputs': [ - { - 'tx_hash': 'c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4' - 'tx_hash_big_endian': 'f42af175402dfe93178c8f7e3571155653faa97853c36169bc6d33188f3271c9' - 'tx_index': 85110550 - 'tx_output_n': 1 - 'script': '76a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac' - 'value': 40000 - 'value_hex': '009c40' - 'confirmations': 267 - 'hash': 'f42af175402dfe93178c8f7e3571155653faa97853c36169bc6d33188f3271c9' - 'index': 1 - } - { - 'tx_hash': '6876d0264ed25aa69b30cda92020f7d7e78ae611be91368c843dfb27139fb45a' - 'tx_hash_big_endian': '5ab49f1327fb3d848c3691be11e68ae7d7f72020a9cd309ba65ad24e26d07668' - 'tx_index': 85114355 - 'tx_output_n': 0 - 'script': '76a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac' - 'value': 30000 - 'value_hex': '7530' - 'confirmations': 262 - 'hash': '5ab49f1327fb3d848c3691be11e68ae7d7f72020a9cd309ba65ad24e26d07668' - 'index': 0 - } - ] -# encrypted with password "hola" -spenderM.addToAdd.encPrivateKey = "6lzfKSFi/9xkli8eRaPDoyHDkaBzRWKdWEamoMvsmJptnhwPZzA/w5EzIljjjrNpbQn8CzvxJci756AXO/Cq7A==" -spenderM.addToAdd.privateKey = "8CvkFF5WZ7w5YDeoPfJ9BNPKEQUgqZGrHUSdboHaRyoi" -spenderM.addToAdd.fromAddress = "1CCMvFa5Ric3CcnRWJzSaZYXmCtZzzDLiX" -spenderM.addToAdd.toAddress = "1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF" -spenderM.addToAdd.amount = 20000 -spenderM.addToAdd.toAddresses = ["1PiHKNK4NTxcuZEWqxKn9tF82kUoen2QTD","1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF"] -spenderM.addToAdd.amounts = [20000,10000] -spenderM.addToAdd.txHash1M = "0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008a47304402202fdf972a3481bd81442e6c212cbfd07cd8e1db440b5a4981b059f96e4c9342290220353447e526dc141308428b8affce3028cc21bded6d158b5b8471cde51ecf901c0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff02204e0000000000001976a914f9217131115f61d6835b97bf388089c9f999e33688ac10270000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac00000000" -spenderM.addToAdd.txHash2M = "0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008b483045022100eb594841c5c78d9fc56543e33c4fabdb17117e0acb2de727bb8d24f01036de9f0220727009bee45330678dfc4bde9b04754012e932e5f8e0554b01b75b4bb74956150141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff0210270000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac204e0000000000001976a914f9217131115f61d6835b97bf388089c9f999e33688ac00000000" -spenderM.addToAdd.fee = 10000 -spenderM.addToAdd.note = undefined -spenderM.addToAdd.txHash1 = "0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008b483045022100c3ea4e526b5aa88a3f4122c093f27afc3b1a7f6a3d6938139668a1ed8d0a77c202207b3b53f96827938f22677a64068e50907a5662bc108d35e0180269872bf9c8be0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff02204e0000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac10270000000000001976a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac00000000" -spenderM.addToAdd.txHash2 = "0100000001c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008a473044022044e111fbce09ce4a4013a94be2136c8f7e2ca6439ea24e37fcda934f1d6fcc34022077670efeee20559a28500d7840df061e24b505f8a309a1eabe87edaad633c0cb0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff0210270000000000001976a9147acf6bb7b804392ed3f3537a4f999220cf1c4e8288ac204e0000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac00000000" -spenderM.addToAdd.sweepHex = "0100000002c971328f18336dbc6961c35378a9fa53561571357e8f8c1793fe2d4075f12af4010000008a47304402203cd3133b9ac7d8768e6cf2cc54d0328a4d6cfa0a3907a7f103c868bc40d945d402202f59a390e61582a3d9da403542573fb8d3b7072bd4c4475b14a98101cca7a2090141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff6876d0264ed25aa69b30cda92020f7d7e78ae611be91368c843dfb27139fb45a000000008a473044022077e571a6781123e1791a29022438b12a6607db7c04ceaa47f7636c171c56313602203e69e7fd67c8a344325f8e2fe5d511ddd6978a9ada246234534f561b01a2e4bc0141044ca8bee9fa5d4e372a00e65116db5eeb8920bb796c12beaeb994e2026b411d3837494022cced88832dac7a494a47de55fc90dcd5e20bbc96f116c44773167a87ffffffff0160ea0000000000001976a914fd342e1afdf81720024ec3bdeaeb6e2753973d0d88ac00000000" -spenderM.addToAdd.toHdAccount = [{ getReceiveAddress: () -> "1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF" }] -spenderM.addToAdd.fromAddPKey = Bitcoin.ECPair.fromWIF("5JdSM2jdvnZm8sLBz8Ac9Sfq1utDcxopzPhswf3c645s18AjX92") -spenderM.addToAdd.privKey = "8CvkFF5WZ7w5YDeoPfJ9BNPKEQUgqZGrHUSdboHaRyoi" -################################################################################ -spenderM.AccountToAdd = {} -spenderM.AccountToAdd.coins = 'unspent_outputs': [ - { - 'tx_hash': 'd93b4753e25b1669cb6522784af7ae391cb7181048e9330e09b62cd5374fe100' - 'tx_hash_big_endian': '00e14f37d52cb6090e33e9481018b71c39aef74a782265cb69165be253473bd9' - 'tx_index': 85757889 - 'tx_output_n': 0 - 'script': '76a914a93b2f63bb449b5c7c9bc05b37c541577266691b88ac' - 'xpub': - 'm': 'xpub6DHN1xpggNEUbWgGJyMPRFGvYm6pizUnv4TQMAtgYBikkh75dyp9Gf9QcKETpWZkLjtB4zYr2eVaHQ4g3rhj46Aeu4FykMWSayrqmRmEMEZ' - 'path': 'M/0/30' - 'value': 200000 - 'value_hex': '030d40' - 'confirmations': 0 - 'hash': '00e14f37d52cb6090e33e9481018b71c39aef74a782265cb69165be253473bd9' - 'index': 0 - } -] -spenderM.AccountToAdd.toAddress = "1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF" -spenderM.AccountToAdd.amount = 10000 -spenderM.AccountToAdd.fee = 10000 -spenderM.AccountToAdd.note = undefined -spenderM.AccountToAdd.fromAccount = 0 -spenderM.AccountToAdd.txHash1 = "a75fb7ac06ea57291ba23dfdb0b1328d5ecd532df45075089ed82fa4bb6e4b71" -spenderM.AccountToAdd.txHash2 = "e636e504df7dd6100f20bf0bdaaf9bda4d2b9402d39d76ebbc283485d4d466ab" -spenderM.AccountToAdd.fromHdAccount = [ - { - getReceiveAddress: () -> "1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF" - getChangeAddress: () -> "1PiHKNK4NTxcuZEWqxKn9tF82kUoen2QTD" - containsAddressInCache: () -> false - extendedPrivateKey: "xprv9zJ1cTHnqzgBP2boCwpP47LBzjGLKXkwYqXoYnV4yrBmstmw6SVtirpvm4GESg9YLn9R386qpmnsrcC5rvrpEJAXSrfqQR3qGtjGv5ddV9g" - } -] -spenderM.AccountToAdd.fromHdAccountERROR = [ - { - getReceiveAddress: () -> "1Q5pU54M3ombtrGEGpAheWQtcX2DZ3CdqF" - getChangeAddress: () -> "1PiHKNK4NTxcuZEWqxKn9tF82kUoen2QTD" - containsAddressInCache: () -> true - extendedPrivateKey: "xprv9zJ1cTHnqzgBP2boCwpP47LBzjGLKXkwYqXoYnV4yrBmstmw6SVtirpvm4GESg9YLn9R386qpmnsrcC5rvrpEJAXSrfqQR3qGtjGv5ddV9g" - } -] diff --git a/tests/payment_spec.js b/tests/payment_spec.js new file mode 100644 index 000000000..aa61fc5ce --- /dev/null +++ b/tests/payment_spec.js @@ -0,0 +1,183 @@ + +let proxyquire = require('proxyquireify')(require); +let unspent = require('./data/unspent-outputs'); +let fees = require('./data/fee-data'); + +let MyWallet = { + wallet: { + fee_per_kb: 10000, + isUpgradedToHD: true, + key () { return { priv: null, address: '16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee' }; }, + spendableActiveAddresses: [ + '16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee', + '1FBHaa3JNjTbhvzMBdv2ymaahmgSSJ4Mis', + '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9' + ], + hdwallet: { + accounts: [ + { + receiveAddress: '1CAAZHV1YJcWojefgTEJMG1TjqyEzDuvA6', + extendedPublicKey: 'xpub6DX2ZjB6qgNH5GFusizVD2yHsm7T9vD6eQNHzth4Zy6MPQim96UPdHurhXDSaz8aUtPo3XktydjkMt1ZJCL9pjPm9YXJYW3K9cYDcJAuT2v' + }, + { + receiveAddress: '1K8ChnK2TCpADx6auTDjB613zrf4wBsawx', + extendedPublicKey: 'xpub6DX2ZjB6qgNH8YVEAX4tKdTGrEyLF5h2FVarCmWvRUpVREYL6c93xvt7ZFGK9x6vNjwiRxAd1pEo2WU5YNKPhnAZ8sh4CUefbGQJ8aUJaEv' + } + ] + } + } +}; + +const API = { + getUnspent (addresses, conf) { return Promise.resolve(unspent); }, + getFees () { return Promise.resolve(fees); } +}; + +let Helpers = + {guessFee (nInputs, nOutputs, feePerKb) { return nInputs * 100; }}; + +let Payment = proxyquire('../src/payment', { + './wallet': MyWallet, + './api': API, + './helpers': Helpers +}); + +describe('Payment', () => { + let payment; + let { hdwallet } = MyWallet.wallet; + + let data = { + address: '16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee', + addressesFromPk: ['1Q57STy6daELZqToY4Rs2BKWxau2kzwjdy', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9'], // Compressed and uncompressed + addresses: ['16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee', '1FBHaa3JNjTbhvzMBdv2ymaahmgSSJ4Mis', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9'] + }; + + beforeEach(() => { payment = new Payment(); }); + + describe('new', () => + + it('should create a new payment', done => { + spyOn(Payment, 'return').and.callThrough(); + payment = new Payment(); + expect(Payment.return).toHaveBeenCalled(); + expect(payment.payment).toBeResolved(done); + }) + ); + + describe('to', () => { + it('should set an address', done => { + payment.to(data.address); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: [data.address] }), done); + }); + + it('should set multiple addresses', done => { + payment.to(data.addresses); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: data.addresses }), done); + }); + + it('should set to an account index', done => { + payment.to(1); + let { receiveAddress } = hdwallet.accounts[1]; + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: [receiveAddress] }), done); + }); + + it('should not set to an invalid account index', done => { + payment.to(-1); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: null }), done); + }); + }); + + describe('from', () => { + it('should set to an address ', done => { + payment.from(data.address); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: [data.address] }), done); + }); + + it('should set multiple addresses', done => { + payment.from(data.addresses); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: data.addresses }), done); + }); + + it('should set an account index', done => { + payment.from(0); + let xpub = hdwallet.accounts[0].extendedPublicKey; + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: [xpub] }), done); + }); + + it('should all addresses if no argument is specified', done => { + payment.from(); + let legacyAddresses = MyWallet.wallet.spendableActiveAddresses; + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: legacyAddresses }), done); + }); + + it('should set the correct sweep amount and sweep fee', done => { + payment.from(data.address); + payment.payment.then(({sweepAmount, sweepFee}) => { + expect(sweepAmount).toEqual(12520); + expect(sweepFee).toEqual(7480); + + done(); + }); + }); + + it('should set an address from a private key', done => { + payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu'); // PK for 12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9 + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: data.addressesFromPk }), done); + }); + + it('should not set an address from an invalid string', done => { + payment.from('1badaddresss'); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: null, change: null }), done); + }); + }); + + describe('amount', () => { + it('should not set negative amounts', done => { + payment.amount(-1); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: null }), done); + }); + + it('should not set amounts that aren\'t positive integers', done => { + payment.amount('100000000'); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: null }), done); + }); + + it('should not set amounts if an element of the array is invalid', done => { + payment.amount([10000, 20000, 30000, '324345']); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: null }), done); + }); + + it('should set amounts from a valid number', done => { + payment.amount(3000); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: [3000] }), done); + }); + + it('should set amounts from a valid number array', done => { + payment.amount([3000, 20000]); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: [3000, 20000] }), done); + }); + }); + + describe('fee', () => { + it('should not set a non positive integer fee (use 2 blocks fee-per-kb)', done => { + payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu'); + payment.amount(5000); + payment.fee(-3000); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ finalFee: 4520 }), done); + }); + + it('should not set a string fee (use 2 blocks fee-per-kb)', done => { + payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu'); + payment.amount(5000); + payment.fee('3000'); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ finalFee: 4520 }), done); + }); + + it('should set a valid fee', done => { + payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu'); + payment.amount(5000); + payment.fee(1000); + expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ finalFee: 1000 }), done); + }); + }); +}); diff --git a/tests/payment_spec.js.coffee b/tests/payment_spec.js.coffee deleted file mode 100644 index 54a370801..000000000 --- a/tests/payment_spec.js.coffee +++ /dev/null @@ -1,163 +0,0 @@ - -proxyquire = require('proxyquireify')(require) -unspent = require('./data/unspent-outputs') -fees = require('./data/fee-data') - -MyWallet = - wallet: - fee_per_kb: 10000 - isUpgradedToHD: true - key: () -> { priv: null, address: '16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee' } - spendableActiveAddresses: [ - '16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee', - '1FBHaa3JNjTbhvzMBdv2ymaahmgSSJ4Mis', - '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9' - ] - hdwallet: - accounts: [ - { - receiveAddress: '1CAAZHV1YJcWojefgTEJMG1TjqyEzDuvA6', - extendedPublicKey: 'xpub6DX2ZjB6qgNH5GFusizVD2yHsm7T9vD6eQNHzth4Zy6MPQim96UPdHurhXDSaz8aUtPo3XktydjkMt1ZJCL9pjPm9YXJYW3K9cYDcJAuT2v' - }, - { - receiveAddress: '1K8ChnK2TCpADx6auTDjB613zrf4wBsawx', - extendedPublicKey: 'xpub6DX2ZjB6qgNH8YVEAX4tKdTGrEyLF5h2FVarCmWvRUpVREYL6c93xvt7ZFGK9x6vNjwiRxAd1pEo2WU5YNKPhnAZ8sh4CUefbGQJ8aUJaEv' - } - ] - -API = - getUnspent: (addresses, conf) -> Promise.resolve(unspent) - getFees: () -> Promise.resolve(fees) - -Helpers = - guessFee: (nInputs, nOutputs, feePerKb) -> nInputs * 100 - -Payment = proxyquire('../src/payment', { - './wallet': MyWallet - './api': API, - './helpers': Helpers -}) - -describe 'Payment', -> - payment = undefined - hdwallet = MyWallet.wallet.hdwallet - - data = - address: '16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee' - addressesFromPk: ['1Q57STy6daELZqToY4Rs2BKWxau2kzwjdy', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9'] # Compressed and uncompressed - addresses: ['16SPAGz8vLpP3jNTcP7T2io1YccMbjhkee', '1FBHaa3JNjTbhvzMBdv2ymaahmgSSJ4Mis', '12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9'] - - beforeEach -> - JasminePromiseMatchers.install() - payment = new Payment() - - afterEach -> - JasminePromiseMatchers.uninstall() - - - describe 'new', -> - - it 'should create a new payment', (done) -> - spyOn(Payment, 'return').and.callThrough() - payment = new Payment() - expect(Payment.return).toHaveBeenCalled() - expect(payment.payment).toBeResolved(done) - - describe 'to', -> - - it 'should set an address', (done) -> - payment.to(data.address) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: [data.address] }), done) - - it 'should set multiple addresses', (done) -> - payment.to(data.addresses) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: data.addresses }), done) - - it 'should set to an account index', (done) -> - payment.to(1) - receiveAddress = hdwallet.accounts[1].receiveAddress - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: [receiveAddress] }), done) - - it 'should not set to an invalid account index', (done) -> - payment.to(-1) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ to: null }), done) - - describe 'from', -> - - it 'should set to an address ', (done) -> - payment.from(data.address) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: [data.address] }), done) - - it 'should set multiple addresses', (done) -> - payment.from(data.addresses) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: data.addresses }), done) - - it 'should set an account index', (done) -> - payment.from(0) - xpub = hdwallet.accounts[0].extendedPublicKey - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: [xpub] }), done) - - it 'should all addresses if no argument is specified', (done) -> - payment.from() - legacyAddresses = MyWallet.wallet.spendableActiveAddresses - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: legacyAddresses }), done) - - it 'should set the correct sweep amount and sweep fee', (done) -> - payment.from(data.address) - payment.payment.then((res) -> - expect(res.sweepAmount).toEqual(12520) - expect(res.sweepFee).toEqual(7480) - - done() - ) - - it 'should set an address from a private key', (done) -> - payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu') # PK for 12C5rBJ7Ev3YGBCbJPY6C8nkGhkUTNqfW9 - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: data.addressesFromPk }), done) - - it 'should not set an address from an invalid string', (done) -> - payment.from('1badaddresss') - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ from: null, change: null }), done) - - describe 'amount', -> - - it 'should not set negative amounts', (done) -> - payment.amount(-1) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: null }), done) - - it 'should not set amounts that aren\'t positive integers', (done) -> - payment.amount('100000000') - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: null }), done) - - it 'should not set amounts if an element of the array is invalid', (done) -> - payment.amount([10000, 20000, 30000, "324345"]) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: null }), done) - - it 'should set amounts from a valid number', (done) -> - payment.amount(3000) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: [3000] }), done) - - it 'should set amounts from a valid number array', (done) -> - payment.amount([3000, 20000]) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ amounts: [3000, 20000] }), done) - - describe 'fee', -> - - it 'should not set a non positive integer fee (use 2 blocks fee-per-kb)', (done) -> - - payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu') - payment.amount(5000) - payment.fee(-3000) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ finalFee: 4520 }), done) - - it 'should not set a string fee (use 2 blocks fee-per-kb)', (done) -> - payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu') - payment.amount(5000) - payment.fee('3000') - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ finalFee: 4520 }), done) - - it 'should set a valid fee', (done) -> - payment.from('5JrXwqEhjpVF7oXnHPsuddTc6CceccLRTfNpqU2AZH8RkPMvZZu') - payment.amount(5000) - payment.fee(1000) - expect(payment.payment).toBeResolvedWith(jasmine.objectContaining({ finalFee: 1000 }), done) diff --git a/tests/rng_spec.js b/tests/rng_spec.js new file mode 100644 index 000000000..c51383912 --- /dev/null +++ b/tests/rng_spec.js @@ -0,0 +1,172 @@ +let proxyquire = require('proxyquireify')(require); + +const RNG = proxyquire('../src/rng', {}); + +describe('RNG', () => { + let serverBuffer = new Buffer( + '0000000000000000000000000000000000000000000000000000000000000010', + 'hex' + ); + + let longServerBuffer = new Buffer( + '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010', + 'hex' + ); + + let zerosServerBuffer = new Buffer( + '0000000000000000000000000000000000000000000000000000000000000000', + 'hex' + ); + + let shortServerBuffer = new Buffer( + '0000000000000000000000000000000001', + 'hex' + ); + + let xorBuffer = new Buffer( + '0000000000000000000000000000000000000000000000000000000000000011', + 'hex' + ); + + let xorFailingServerBuffer = new Buffer( + '0000000000000000000000000000000000000000000000000000000000000001', + 'hex' + ); + + describe('.xor()', () => { + it('should be an xor operation', () => { + let A = new Buffer('a123456c', 'hex'); + let B = new Buffer('ff0123cd', 'hex'); + let R = '5e2266a1'; + expect(RNG.xor(A, B).toString('hex')).toEqual(R); + }); + + it('should return the shortest common length', () => { + let A = new Buffer('a123', 'hex'); + let B = new Buffer('ff0123cd', 'hex'); + let R = '5e22'; + expect(RNG.xor(A, B).toString('hex')).toEqual(R); + }); + }); + + describe('.run()', () => { + let browser = { + lastBit: 1 + }; + + beforeEach(() => { + window.crypto.getRandomValues = array => { + array[31] = browser.lastBit; + }; + + spyOn(console, 'log').and.callFake(() => { }); + }); + + it('should ask for 32 bytes from the server', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => { + expect(bytes).toEqual(32); + return serverBuffer; + }); + RNG.run(); + }); + + it('should ask an arbitrary amount of bytes from the server', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => { + expect(bytes).toEqual(64); + return longServerBuffer; + }); + + RNG.run(64); + }); + + it('returns the mixed entropy', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => serverBuffer); + + expect(RNG.run().toString('hex')).toEqual(xorBuffer.toString('hex')); + }); + + it('fails if server data is all zeros', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => zerosServerBuffer); + expect(() => RNG.run()).toThrow(); + }); + + it('fails if browser data is all zeros', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => serverBuffer); + browser.lastBit = 0; + expect(() => RNG.run()).toThrow(); + browser.lastBit = 1; + }); + + it('fails if server data has the wrong length', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => shortServerBuffer); + expect(() => RNG.run()).toThrow(); + }); + + it('fails if combined entropy is all zeros', () => { + spyOn(RNG, 'getServerEntropy').and.callFake( + bytes => xorFailingServerBuffer); + expect(() => RNG.run()).toThrow(); + }); + }); + + describe('.getServerEntropy()', () => { + let mock = { + responseText: '0000000000000000000000000000000000000000000000000000000000000010', + statusCode: 200 + }; + + let request = { + open () {}, + setRequestHeader () {}, + send () { + this.status = mock.statusCode; + this.responseText = mock.responseText; + } + }; + + beforeEach(() => { + spyOn(window, 'XMLHttpRequest').and.returnValue(request); + spyOn(request, 'open').and.callThrough(); + }); + + it('makes a GET request to the backend', () => { + RNG.getServerEntropy(32); + expect(request.open).toHaveBeenCalled(); + expect(request.open.calls.argsFor(0)[0]).toEqual('GET'); + expect(request.open.calls.argsFor(0)[1]).toContain('v2/randombytes'); + }); + + it('returns a buffer is successful', () => { + let res = RNG.getServerEntropy(32); + expect(Buffer.isBuffer(res)).toBeTruthy(); + }); + + it('returns a 32 bytes buffer if nBytes not indicated and if is successful', () => { + let res = RNG.getServerEntropy(); + expect(Buffer.isBuffer(res)).toBeTruthy(); + expect(res.length).toEqual(32); + }); + + it('throws an exception if result is not hex', () => { + mock.responseText = 'This page was not found'; + expect(() => RNG.getServerEntropy(32)).toThrow(); + }); + + it('throws an exception if result is the wrong length', () => { + mock.responseText = '000001'; + expect(() => RNG.getServerEntropy(3)).not.toThrow(); + expect(() => RNG.getServerEntropy(32)).toThrow(); + }); + + it('throws an exception if the server does not return a 200', () => { + mock.statusCode = 500; + expect(() => RNG.getServerEntropy(32)).toThrow(); + }); + }); +}); diff --git a/tests/rng_spec.js.coffee b/tests/rng_spec.js.coffee deleted file mode 100644 index d84b92920..000000000 --- a/tests/rng_spec.js.coffee +++ /dev/null @@ -1,160 +0,0 @@ -proxyquire = require('proxyquireify')(require); - -RNG = proxyquire('../src/rng', {}) - -describe "RNG", -> - - serverBuffer = new Buffer( - "0000000000000000000000000000000000000000000000000000000000000010", - "hex" - ) - - longServerBuffer = new Buffer( - "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010", - "hex" - ) - - zerosServerBuffer = new Buffer( - "0000000000000000000000000000000000000000000000000000000000000000", - "hex" - ) - - shortServerBuffer = new Buffer( - "0000000000000000000000000000000001", - "hex" - ) - - xorBuffer = new Buffer( - "0000000000000000000000000000000000000000000000000000000000000011", - "hex" - ) - - xorFailingServerBuffer = new Buffer( - "0000000000000000000000000000000000000000000000000000000000000001", - "hex" - ) - - describe ".xor()", -> - - it "should be an xor operation", -> - A = new Buffer('a123456c', 'hex') - B = new Buffer('ff0123cd', 'hex') - R = '5e2266a1' - expect(RNG.xor(A,B).toString('hex')).toEqual(R) - - it "should return the shortest common length", -> - A = new Buffer('a123' , 'hex') - B = new Buffer('ff0123cd', 'hex') - R = '5e22' - expect(RNG.xor(A,B).toString('hex')).toEqual(R) - - describe ".run()", -> - browser = { - lastBit: 1 - } - - beforeEach -> - window.crypto.getRandomValues = (array) -> - array[31] = browser.lastBit - return - - spyOn(console, "log").and.callFake(() -> ) - - it "should ask for 32 bytes from the server", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - expect(bytes).toEqual(32) - serverBuffer - ) - RNG.run() - - it "should ask an arbitrary amount of bytes from the server", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - expect(bytes).toEqual(64) - longServerBuffer - ) - - RNG.run(64) - - it "returns the mixed entropy", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - serverBuffer - ) - - expect(RNG.run().toString("hex")).toEqual(xorBuffer.toString("hex")) - - it "fails if server data is all zeros", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - zerosServerBuffer - ) - expect(() -> RNG.run()).toThrow() - - it "fails if browser data is all zeros", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - serverBuffer - ) - browser.lastBit = 0 - expect(() -> RNG.run()).toThrow() - browser.lastBit = 1 - - it "fails if server data has the wrong length", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - shortServerBuffer - ) - expect(() -> RNG.run()).toThrow() - - it "fails if combined entropy is all zeros", -> - spyOn(RNG, "getServerEntropy").and.callFake( - (bytes) -> - xorFailingServerBuffer - ) - expect(() -> RNG.run()).toThrow() - - describe ".getServerEntropy()", -> - mock = - responseText: "0000000000000000000000000000000000000000000000000000000000000010" - statusCode: 200 - - request = - open: () -> - setRequestHeader: () -> - send: () -> - this.status = mock.statusCode - this.responseText = mock.responseText - - beforeEach -> - spyOn(window, "XMLHttpRequest").and.returnValue request - spyOn(request, "open").and.callThrough() - - it "makes a GET request to the backend", -> - RNG.getServerEntropy(32) - expect(request.open).toHaveBeenCalled() - expect(request.open.calls.argsFor(0)[0]).toEqual("GET") - expect(request.open.calls.argsFor(0)[1]).toContain("v2/randombytes") - - it "returns a buffer is successful", -> - res = RNG.getServerEntropy(32) - expect(Buffer.isBuffer(res)).toBeTruthy() - - it "returns a 32 bytes buffer if nBytes not indicated and if is successful", -> - res = RNG.getServerEntropy() - expect(Buffer.isBuffer(res)).toBeTruthy() - expect(res.length).toEqual(32) - - it "throws an exception if result is not hex", -> - mock.responseText = "This page was not found" - expect(() -> RNG.getServerEntropy(32)).toThrow() - - it "throws an exception if result is the wrong length", -> - mock.responseText = "000001" - expect(() -> RNG.getServerEntropy(3)).not.toThrow() - expect(() -> RNG.getServerEntropy(32)).toThrow() - - it "throws an exception if the server does not return a 200", -> - mock.statusCode = 500 - expect(() -> RNG.getServerEntropy(32)).toThrow() diff --git a/tests/transaction_list_spec.js b/tests/transaction_list_spec.js new file mode 100644 index 000000000..96ebe8a20 --- /dev/null +++ b/tests/transaction_list_spec.js @@ -0,0 +1,107 @@ + +let proxyquire = require('proxyquireify')(require); + +let tx = require('./data/transactions')['default']; + +let TransactionList = proxyquire('../src/transaction-list', { + './wallet-transaction': { + factory (tx) { return tx; } + } +}); + +describe('TransactionList', () => { + let txList; + + beforeEach(() => { txList = new TransactionList(10); }); + + it('should lookup a transaction by its hash', () => { + txList.pushTxs({ hash: 'abcdef', txType: 'sent' }); + tx = txList.transaction('abcdef'); + expect(tx.txType).toEqual('sent'); + }); + + it('should add txs to the tx list', () => { + txList.pushTxs({ txType: 'sent' }); + expect(txList.transactions.length).toEqual(1); + }); + + it('should not add duplicate txs to the tx list', () => { + txList.pushTxs({ txType: 'sent', hash: '1234' }); + txList.pushTxs({ txType: 'sent', hash: '1234' }); + expect(txList.transactions().length).toEqual(1); + expect(txList.fetched).toEqual(1); + }); + + it('should have defined its load number', () => expect(txList.loadNumber).toEqual(10)); + + it('should correctly handle transactions identities', () => { + let tx1 = { + hash: '234234', + txType: 'sent', + belongsTo (identity) { return identity === 'imported'; } + }; + + let tx2 = { + hash: '6786', + txType: 'sent', + belongsTo (identity) { return identity === '0/1/23'; } + }; + + txList.pushTxs(tx1); + txList.pushTxs(tx2); + expect(txList.transactions().length).toEqual(2); + expect(txList.transactions('').length).toEqual(2); + expect(txList.transactions('imported').length).toEqual(1); + }); + + describe('events', () => { + let spy; + let unsub; + + beforeEach(() => { + spy = jasmine.createSpy(); + unsub = txList.subscribe(spy); + }); + + it('should send event listeners an update when pushTxs is called', () => { + txList.pushTxs({ txType: 'sent' }); + expect(spy).toHaveBeenCalled(); + }); + + it('should have the ability to unsubscribe', () => { + unsub(); + txList.pushTxs({ txType: 'sent' }); + expect(spy).not.toHaveBeenCalled(); + }); + + it('should not add things that aren\'t functions as listeners', () => { + let res = txList.subscribe({garbadge: true}); + expect(res).toEqual(undefined); + }); + }); + + describe('.wipe', () => + + it('should empty the list', () => { + txList.wipe(); + expect(txList.transactions().length).toEqual(0); + }) + ); + + describe('transactionsForIOS', () => + + it('should return all transactions in the correct format', () => { + txList.pushTxs({ txType: 'sent', hash: '1234' }); + txList.pushTxs({ txType: 'sent', hash: '1234' }); + txList.pushTxs({ txType: 'sent', hash: '3234' }); + + let ios = txList.transactionsForIOS(); + + expect(ios.length).toEqual(2); + expect(ios[0].myHash).toEqual('1234'); + expect(ios[1].myHash).toEqual('3234'); + expect(ios[0].txType).toEqual('sent'); + expect(ios[1].txType).toEqual('sent'); + }) + ); +}); diff --git a/tests/transaction_list_spec.js.coffee b/tests/transaction_list_spec.js.coffee deleted file mode 100644 index 7ca5f6a7c..000000000 --- a/tests/transaction_list_spec.js.coffee +++ /dev/null @@ -1,93 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -tx = require('./data/transactions')["default"] - -TransactionList = proxyquire('../src/transaction-list', { - './wallet-transaction': { - factory: (tx) -> tx - } -}) - -describe 'TransactionList', -> - txList = undefined - - beforeEach -> - txList = new TransactionList(10) - - it 'should lookup a transaction by its hash', -> - txList.pushTxs({ hash: 'abcdef', txType: 'sent' }) - tx = txList.transaction('abcdef') - expect(tx.txType).toEqual('sent') - - it 'should add txs to the tx list', -> - txList.pushTxs({ txType: 'sent' }) - expect(txList.transactions.length).toEqual(1) - - it 'should not add duplicate txs to the tx list', -> - txList.pushTxs({ txType: 'sent', hash: "1234"}) - txList.pushTxs({ txType: 'sent', hash: "1234"}) - expect(txList.transactions().length).toEqual(1) - expect(txList.fetched).toEqual(1) - - it "should have defined its load number", -> - expect(txList.loadNumber).toEqual(10) - - it 'should correctly handle transactions identities', -> - tx1 = - hash: '234234', - txType: 'sent', - belongsTo: (identity) -> identity == 'imported' - - tx2 = - hash: '6786', - txType: 'sent', - belongsTo: (identity) -> identity == '0/1/23' - - txList.pushTxs(tx1) - txList.pushTxs(tx2) - expect(txList.transactions().length).toEqual(2) - expect(txList.transactions('').length).toEqual(2) - expect(txList.transactions('imported').length).toEqual(1) - - describe 'events', -> - spy = undefined - unsub = undefined - - beforeEach -> - spy = jasmine.createSpy() - unsub = txList.subscribe(spy) - - it 'should send event listeners an update when pushTxs is called', -> - txList.pushTxs({ txType: 'sent' }) - expect(spy).toHaveBeenCalled() - - it 'should have the ability to unsubscribe', -> - unsub() - txList.pushTxs({ txType: 'sent' }) - expect(spy).not.toHaveBeenCalled() - - it 'should not add things that aren\'t functions as listeners', -> - res = txList.subscribe({garbadge: true}) - expect(res).toEqual(undefined) - - describe ".wipe", -> - - it "should empty the list", -> - txList.wipe() - expect(txList.transactions().length).toEqual(0) - - describe 'transactionsForIOS', -> - - it "should return all transactions in the correct format", -> - txList.pushTxs({ txType: 'sent', hash: "1234"}) - txList.pushTxs({ txType: 'sent', hash: "1234"}) - txList.pushTxs({ txType: 'sent', hash: "3234"}) - - ios = txList.transactionsForIOS() - - expect(ios.length).toEqual(2) - expect(ios[0].myHash).toEqual('1234') - expect(ios[1].myHash).toEqual('3234') - expect(ios[0].txType).toEqual('sent') - expect(ios[1].txType).toEqual('sent') diff --git a/tests/transaction_spend_spec.js b/tests/transaction_spend_spec.js new file mode 100644 index 000000000..972b3d760 --- /dev/null +++ b/tests/transaction_spend_spec.js @@ -0,0 +1,463 @@ +let proxyquire = require('proxyquireify')(require); + +let Bitcoin = require('bitcoinjs-lib'); +let { EventEmitter } = require('events'); +let Helpers = require('../src/helpers'); + +let MyWallet; +let stubs = + {'./wallet': MyWallet}; + +describe('Transaction', () => { + let Transaction = proxyquire('../src/transaction', stubs); + + let observer; + let data; + let payment; + let ee = new EventEmitter(); + + beforeEach(() => { + data = { + from: '1DiJVG3oD3yeqW26qcVaghwTjvMaVoeghX', + privateKey: 'AWrnMsqe2AJYmrzKsN8qRosHRiCSKag3fcmvUA9wdJDj', + to: '1gvtg5mEEpTNVYDtEx6n4J7oyVpZGU13h', + amount: 50000, + toMultiple: ['1gvtg5mEEpTNVYDtEx6n4J7oyVpZGU13h', '1FfmbHfnpaZjKFvyi1okTjJJusN455paPH'], + multipleAmounts: [20000, 10000], + fee: 10000, + feePerKb: 10000, + note: 'That is an expensive toy', + unspentMock: [ + { + 'tx_hash': '594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861', + 'hash': '6108a5ff4ca949905271a375ee290676cdac04c30f7616d8b768509d72664c59', + 'tx_hash_big_endian': '6108a5ff4ca949905271a375ee290676cdac04c30f7616d8b768509d72664c59', + 'tx_index': 82222265, + 'index': 0, + 'tx_output_n': 0, + 'script': '76a91449f842901a0c81fb9c0c0f8c61027d2b085a2a9088ac', + 'value': 61746, + 'value_hex': '00f132', + 'confirmations': 0 + } + ], + unspentMockXPub: [ + { + 'tx_hash': '6108a5ff4ca949905271a375ee290676cdac04c30f7616d8b768509d72664c59', + 'hash': '594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861', + 'tx_hash_big_endian': '594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861', + 'tx_index': 82222264, + 'index': 0, + 'tx_output_n': 0, + 'script': '76a91449f842901a0c81fb9c0c0f8c61027d2b083a2a9088ac', + 'value': 234235, + 'value_hex': '00f132', + 'confirmations': 1, + 'xpub': { 'path': 'm/0/0' } + } + ] + }; + + payment = { + selectedCoins: data.unspentMock, + to: data.to, + amounts: data.amount, + finalFee: data.fee, + change: data.from + }; + + observer = { + success () { }, + error () { } + }; + + spyOn(observer, 'success'); + spyOn(observer, 'error'); + + let BlockchainAPI = { + get_unspent () {}, + push_tx () {} + }; + + spyOn(BlockchainAPI, 'push_tx') + .and.callFake((tx, note, success, error) => success()); + + spyOn(BlockchainAPI, 'get_unspent') + .and.callFake((xpubList, success, error, conf, nocache) => success(data.unspentMock)); + }); + + // spyOn(WalletStore, "getPrivateKey").and.callFake((address) -> 'AWrnMsqe2AJYmrzKsN8qRosHRiCSKag3fcmvUA9wdJDj') + + describe('create new Transaction', () => { + it('should fail without unspent outputs', () => { + payment.selectedCoins = null; + try { + return new Transaction(payment); + } catch (e) { + expect(e.name).toBe('AssertionError'); + expect(e.message.error).toBe('NO_UNSPENT_OUTPUTS'); + } + }); + + it('should fail without amount lower than dust threshold', () => { + payment.amounts = 100; + try { + return new Transaction(payment); + } catch (e) { + expect(e.name).toBe('AssertionError'); + expect(e.message.error).toBe('BELOW_DUST_THRESHOLD'); + } + }); + + it('should give dust change to miners', () => { + payment.amounts = [61700]; + + let tx = new Transaction(payment); + + expect(tx.transaction.tx.outs.length).toEqual(1); + }); + + it('should create multiple outputs', () => { + payment.to = data.toMultiple; + payment.amounts = data.multipleAmounts; + let tx = new Transaction(payment, ee); + + let privateKeyBase58 = data.privateKey; + let format = Helpers.detectPrivateKeyFormat(privateKeyBase58); + let key = Helpers.privateKeyStringToKey(privateKeyBase58, format); + + // Jasmine seems to break the strict equality test here: + // https://github.com/bitcoinjs/bitcoinjs-lib/blob/v2.1.4/src/transaction_builder.js#L332 + + // Check the network property is set: + expect(key.network).toEqual(Bitcoin.networks.bitcoin); + expect(tx.transaction.network).toEqual(Bitcoin.networks.bitcoin); + + // Override it so the assert statement passes: + key.network = Bitcoin.networks.bitcoin; + tx.transaction.network = Bitcoin.networks.bitcoin; + // End of Jasmine workaround + + key.compressed = false; + + let privateKeys = [key]; + + tx.addPrivateKeys(privateKeys); + + tx = tx.sign().build(); + + let expectedHex = '0100000001594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861000000008a4730440220354fd8f420d1f3ffc802af13d451f853d26f343b10225e92a17d3e831edb81960220074d8dac3c497a0481e2041df4f3cd7a82e32415c11b2054b246187f3ff733a8014104a7392f5628776b530aa5fbb41ac10c327ccd2cf64622a81671038ecda25084af786fd54d43689241694d1d65e6bde98756fa01dfd2f5a90d5318ab3fb7bad8c1ffffffff03204e0000000000001976a914078d35591e340799ee96968936e8b2ea8ce504a688ac10270000000000001976a914a0e6ca5444e4d8b7c80f70237f332320387f18c788acf2540000000000001976a9148b71295471e921703a938aa9e01433deb07c1aa588ac00000000'; + expect(tx.toHex()).toEqual(expectedHex); + }); + }); + + describe('provide Transaction with private keys', () => { + it('should want addresses when supplied with unspent outputs', () => { + let transaction = new Transaction(payment, ee); + + expect(transaction.addressesOfNeededPrivateKeys.length).toBe(1); + expect(transaction.pathsOfNeededPrivateKeys.length).toBe(0); + }); + + it('should accept the right private key', () => { + let transaction = new Transaction(payment, ee); + + let privateKeyBase58 = data.privateKey; + let format = Helpers.detectPrivateKeyFormat(privateKeyBase58); + let key = Helpers.privateKeyStringToKey(privateKeyBase58, format); + key.compressed = false; + let privateKeys = [key]; + + transaction.addPrivateKeys(privateKeys); + expect(transaction.privateKeys).toEqual(privateKeys); + }); + + it('should not accept the wrong private key', () => { + let transaction = new Transaction(payment, ee); + + let privateKeyWIF = '5JfdACpmDbLk7jmjU6kuCdLNFgedL19RnbjZYENAEG8Ntto9zRc'; + let format = Helpers.detectPrivateKeyFormat(privateKeyWIF); + let key = Helpers.privateKeyStringToKey(privateKeyWIF, format); + let privateKeys = [key]; + + expect(() => transaction.addPrivateKeys(privateKeys)).toThrow; + }); + + it('should sign and produce the correct signed script', () => { + let transaction = new Transaction(payment, ee); + let privateKeyBase58 = data.privateKey; + let format = Helpers.detectPrivateKeyFormat(privateKeyBase58); + let key = Helpers.privateKeyStringToKey(privateKeyBase58, format); + key.compressed = false; + let privateKeys = [key]; + + transaction.addPrivateKeys(privateKeys); + + // Jasmine seems to break the strict equality test here: + // https://github.com/bitcoinjs/bitcoinjs-lib/blob/v2.1.4/src/transaction_builder.js#L332 + + // Check the network property is set: + expect(key.network).toEqual(Bitcoin.networks.bitcoin); + expect(transaction.transaction.network).toEqual(Bitcoin.networks.bitcoin); + + // Override it so the assert statement passes: + key.network = Bitcoin.networks.bitcoin; + transaction.transaction.network = Bitcoin.networks.bitcoin; + // End of Jasmine workaround + + let tx = transaction.sign().build(); + + let expectedHex = '0100000001594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861000000008a4730440220187d6b567d29fe10bea29aa36158edb3fcd9bed5e835b93b9f30d630aea1c7740220612be05b0d87b0a170f7ead7f9688d7172c704f63deb74705779cf8ac26ec3b9014104a7392f5628776b530aa5fbb41ac10c327ccd2cf64622a81671038ecda25084af786fd54d43689241694d1d65e6bde98756fa01dfd2f5a90d5318ab3fb7bad8c1ffffffff0250c30000000000001976a914078d35591e340799ee96968936e8b2ea8ce504a688acd2060000000000001976a9148b71295471e921703a938aa9e01433deb07c1aa588ac00000000'; + expect(tx.toHex()).toEqual(expectedHex); + }); + + describe('BIP69 transaction inputs and outputs', () => { + let getIn; + let getOut; + + let testVectors = require('./data/bip69-test-vectors'); + + beforeEach(() => { + getIn = ({hash}) => [].reverse.call(hash).toString('hex'); + getOut = ({script}) => script.toString('hex'); + }); + + it('should sort testvector 1', () => { + let i; + let sortedInputs = [ + '0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57', + '26aa6e6d8b9e49bb0630aac301db6757c02e3619feb4ee0eea81eb1672947024', + '28e0fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2', + '381de9b9ae1a94d9c17f6a08ef9d341a5ce29e2e60c36a52d333ff6203e58d5d', + '3b8b2f8efceb60ba78ca8bba206a137f14cb5ea4035e761ee204302d46b98de2', + '402b2c02411720bf409eff60d05adad684f135838962823f3614cc657dd7bc0a', + '54ffff182965ed0957dba1239c27164ace5a73c9b62a660c74b7b7f15ff61e7a', + '643e5f4e66373a57251fb173151e838ccd27d279aca882997e005016bb53d5aa', + '6c1d56f31b2de4bfc6aaea28396b333102b1f600da9c6d6149e96ca43f1102b1', + '7a1de137cbafb5c70405455c49c5104ca3057a1f1243e6563bb9245c9c88c191', + '7d037ceb2ee0dc03e82f17be7935d238b35d1deabf953a892a4507bfbeeb3ba4', + 'a5e899dddb28776ea9ddac0a502316d53a4a3fca607c72f66c470e0412e34086', + 'b4112b8f900a7ca0c8b0e7c4dfad35c6be5f6be46b3458974988e1cdb2fa61b8', + 'bafd65e3c7f3f9fdfdc1ddb026131b278c3be1af90a4a6ffa78c4658f9ec0c85', + 'de0411a1e97484a2804ff1dbde260ac19de841bebad1880c782941aca883b4e9', + 'f0a130a84912d03c1d284974f563c5949ac13f8342b8112edff52971599e6a45', + 'f320832a9d2e2452af63154bc687493484a0e7745ebd3aaf9ca19eb80834ad60']; + let sortedOutputs = [ + '76a9144a5fba237213a062f6f57978f796390bdcf8d01588ac', + '76a9145be32612930b8323add2212a4ec03c1562084f8488ac']; + + let txHex = testVectors[0]; + let tx = new Transaction(payment, ee); + tx.transaction.tx = Bitcoin.Transaction.fromHex(txHex); + tx.privateKeys = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]; + tx.addressesOfInputs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]; + tx.sortBIP69(); + + let allSortedIns = ((() => { + let result = []; + for (i = 0; i <= 16; i++) { + result.push(getIn(tx.transaction.tx.ins[i]) === sortedInputs[i]); + } + return result; + })()).every(x => x); + let allSortedOuts = ((() => { + let result1 = []; + for (i = 0; i <= 1; i++) { + result1.push(getOut(tx.transaction.tx.outs[i]) === sortedOutputs[i]); + } + return result1; + })()).every(x => x); + + expect(allSortedIns).toBeTruthy(); + expect(allSortedOuts).toBeTruthy(); + }); + + it('should sort testvector 2', () => { + let i; + let sortedInputs = [ + '35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055', + '35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055']; + let sortedOutputs = [ + '41046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac', + '41044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac']; + + let txHex = testVectors[1]; + let tx = new Transaction(payment, ee); + tx.transaction.tx = Bitcoin.Transaction.fromHex(txHex); + tx.privateKeys = [1, 2]; + tx.addressesOfInputs = [1, 2]; + tx.transaction.tx.ins.reverse(); // change default because it is already correct + tx.transaction.tx.outs.reverse(); // change default because it is already correct + tx.sortBIP69(); + let allSortedIns = ((() => { + let result = []; + for (i = 0; i <= 1; i++) { + result.push(getIn(tx.transaction.tx.ins[i]) === sortedInputs[i]); + } + return result; + })()).every(x => x); + let allSortedOuts = ((() => { + let result1 = []; + for (i = 0; i <= 1; i++) { + result1.push(getOut(tx.transaction.tx.outs[i]) === sortedOutputs[i]); + } + return result1; + })()).every(x => x); + expect(allSortedIns).toBeTruthy(); + expect(allSortedOuts).toBeTruthy(); + }); + }); + + describe('Outputs with xpub information', () => + + it('should add the path to the private key to pathsOfNeededPrivateKeys', () => { + payment.selectedCoins = data.unspentMockXPub; + let tx = new Transaction(payment); + + expect(tx.pathsOfNeededPrivateKeys.length).toEqual(1); + }) + ); + }); + + describe('Transaction helpers', () => { + it('Transaction.inputCost should be 0.148 per kb', () => { + let ic = Transaction.inputCost(10000); + expect(ic).toBe(1480); + }); + + it('Transaction.guessSize should be zero', () => { + let s = Transaction.guessSize(0, 10); + expect(s).toBe(0); + }); + + it('Transaction.guessSize should be zero', () => { + let s = Transaction.guessSize(10, 0); + expect(s).toBe(0); + }); + + it('Transaction.guessSize should be right', () => { + let s = Transaction.guessSize(10, 10); + expect(s).toBe(1830); + }); + + it('Transaction.guessFee should be right', () => { + let s = Transaction.guessFee(11, 7, 25000); + expect(s).toBe(46900); + }); + + it('Transaction.filterUsableCoins should return an empty array if given a bad argument', () => { + let s = Transaction.filterUsableCoins(1, 1000000); + expect(s).toEqual([]); + }); + + it('Transaction.filterUsableCoins should filter all coins', () => { + let s = Transaction.filterUsableCoins(data.unspentMock, 1000000); + expect(s).toEqual([]); + }); + + it('Transaction.filterUsableCoins should not filter any coins', () => { + let s = Transaction.filterUsableCoins(data.unspentMock, 1000); + expect(s).toEqual(data.unspentMock); + }); + + it('Transaction.filterUsableCoins should work for empty list', () => { + let s = Transaction.filterUsableCoins([], 1000); + expect(s).toEqual([]); + }); + + it('Transaction.maxAvailableAmount should be computed right', () => { + let coins = [{value: 40000}, {value: 30000}, {value: 20000}, {value: 10000}]; + let m = Transaction.maxAvailableAmount(coins, 1000); + expect(m).toEqual({ amount: 99330, fee: 670 }); + }); + + it('Transaction.maxAvailableAmount should be computed right for empty lists', () => { + let coins = []; + let m = Transaction.maxAvailableAmount(coins, 1000); + expect(m).toEqual({ amount: 0, fee: 0 }); + }); + + it('Transaction.sumOfCoins should be computed right', () => { + let coins = [{value: 40000}, {value: 30000}, {value: 20000}, {value: 10000}]; + let m = Transaction.sumOfCoins(coins); + expect(m).toBe(100000); + }); + + it('Transaction.sumOfCoins should be computed right for empty lists', () => { + let coins = []; + let m = Transaction.sumOfCoins(coins); + expect(m).toBe(0); + }); + + it('Transaction.selectCoins empty list with fee-per-kb', () => { + let coins = []; + let amounts = [10000]; + let fee = 10000; + let isAbsFee = false; + let s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); + expect(s).toEqual({'coins': [], 'fee': 0}); + }); + + it('Transaction.selectCoins empty list with fee-per-kb', () => { + let coins = []; + let amounts = [10000]; + let fee = 10000; + let isAbsFee = true; + let s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); + expect(s).toEqual({'coins': [], 'fee': 0}); + }); + + it('Transaction.selectCoins with fee-Per-kb', () => { + let coins = [{value: 40000}, {value: 30000}, {value: 20000}, {value: 10000}]; + let amounts = [10000, 30000]; + let fee = 10000; + let isAbsFee = false; + let s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); + expect(s).toEqual({'coins': [{value: 40000}, {value: 30000}], 'fee': 4080}); + }); + + it('Transaction.selectCoins with absolute fee', () => { + let coins = [{value: 40000}, {value: 30000}, {value: 20000}, {value: 10000}]; + let amounts = [10000, 30000]; + let fee = 10000; + let isAbsFee = true; + let s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); + expect(s).toEqual({'coins': [{value: 40000}, {value: 30000}], 'fee': 10000}); + }); + + it('Transaction.confirmationEstimation with absolute fee', () => { + let feeRanges = [60000, 50000, 40000, 30000, 20000, 10000]; + let fee = 12000; + let s = Transaction.confirmationEstimation(feeRanges, fee); + expect(s).toEqual(6); + }); + + it('Transaction.confirmationEstimation with absolute fee', () => { + let feeRanges = [60000, 50000, 40000, 30000, 20000, 10000]; + let fee = 12000; + let s = Transaction.confirmationEstimation(feeRanges, fee); + expect(s).toEqual(6); + }); + + it('Transaction.confirmationEstimation with absolute fee', () => { + let feeRanges = [60000, 50000, 40000, 30000, 20000, 10000]; + let fee = 30000; + let s = Transaction.confirmationEstimation(feeRanges, fee); + expect(s).toEqual(4); + }); + + it('Transaction.confirmationEstimation with absolute fee', () => { + let feeRanges = [60000, 50000, 40000, 30000, 20000, 10000]; + let fee = 2000; + let s = Transaction.confirmationEstimation(feeRanges, fee); + expect(s).toEqual(Infinity); + }); + + it('Transaction.confirmationEstimation with absolute fee', () => { + let feeRanges = [0, 0, 0, 0, 0, 0]; + let fee = 70000; + let s = Transaction.confirmationEstimation(feeRanges, fee); + expect(s).toBeNull(); + }); + }); +}); diff --git a/tests/transaction_spend_spec.js.coffee b/tests/transaction_spend_spec.js.coffee deleted file mode 100644 index ce3bae0df..000000000 --- a/tests/transaction_spend_spec.js.coffee +++ /dev/null @@ -1,403 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -Bitcoin = require('bitcoinjs-lib') -EventEmitter = require('events').EventEmitter -Helpers = require('../src/helpers') - -MyWallet = undefined -stubs = - './wallet': MyWallet - -describe "Transaction", -> - - Transaction = proxyquire('../src/transaction', stubs) - - observer = undefined - data = undefined - payment = undefined - ee = new EventEmitter(); - - beforeEach -> - data = - from: "1DiJVG3oD3yeqW26qcVaghwTjvMaVoeghX" - privateKey: 'AWrnMsqe2AJYmrzKsN8qRosHRiCSKag3fcmvUA9wdJDj' - to: "1gvtg5mEEpTNVYDtEx6n4J7oyVpZGU13h" - amount: 50000 - toMultiple: ["1gvtg5mEEpTNVYDtEx6n4J7oyVpZGU13h","1FfmbHfnpaZjKFvyi1okTjJJusN455paPH"] - multipleAmounts: [20000,10000] - fee: 10000 - feePerKb: 10000 - note: "That is an expensive toy" - unspentMock: [ - { - "tx_hash": "594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861" - "hash": "6108a5ff4ca949905271a375ee290676cdac04c30f7616d8b768509d72664c59" - "tx_hash_big_endian": "6108a5ff4ca949905271a375ee290676cdac04c30f7616d8b768509d72664c59" - "tx_index": 82222265 - "index": 0 - "tx_output_n": 0 - "script": "76a91449f842901a0c81fb9c0c0f8c61027d2b085a2a9088ac" - "value": 61746 - "value_hex": "00f132" - "confirmations": 0 - } - ] - unspentMockXPub: [ - { - "tx_hash": "6108a5ff4ca949905271a375ee290676cdac04c30f7616d8b768509d72664c59" - "hash": "594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861" - "tx_hash_big_endian": "594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861" - "tx_index": 82222264 - "index": 0 - "tx_output_n": 0 - "script": "76a91449f842901a0c81fb9c0c0f8c61027d2b083a2a9088ac" - "value": 234235 - "value_hex": "00f132" - "confirmations": 1, - "xpub": { "path": "m/0/0" } - } - ] - - payment = - selectedCoins: data.unspentMock - to: data.to - amounts: data.amount - finalFee: data.fee - change: data.from - - observer = - success: () -> return - error: () -> return - - spyOn(observer, 'success') - spyOn(observer, 'error') - - window.BlockchainAPI = - get_unspent: () -> - push_tx: () -> - - spyOn(BlockchainAPI, "push_tx") - .and.callFake((tx, note, success, error) -> - success()) - - spyOn(BlockchainAPI, "get_unspent") - .and.callFake((xpubList,success,error,conf,nocache) -> - success(getUnspentMock)) - - # spyOn(WalletStore, "getPrivateKey").and.callFake((address) -> 'AWrnMsqe2AJYmrzKsN8qRosHRiCSKag3fcmvUA9wdJDj') - - describe "create new Transaction", -> - it "should fail without unspent outputs", -> - - payment.selectedCoins = null - try - new Transaction(payment) - catch e - expect(e.name).toBe('AssertionError') - expect(e.message.error).toBe('NO_UNSPENT_OUTPUTS') - - it "should fail without amount lower than dust threshold", -> - - payment.amounts = 100 - try - new Transaction(payment) - catch e - expect(e.name).toBe('AssertionError') - expect(e.message.error).toBe('BELOW_DUST_THRESHOLD') - - it "should give dust change to miners", -> - payment.amounts = [61700] - - tx = new Transaction(payment) - - expect(tx.transaction.tx.outs.length).toEqual(1) - - - it "should create multiple outputs", -> - payment.to = data.toMultiple - payment.amounts = data.multipleAmounts - tx = new Transaction(payment, ee) - - privateKeyBase58 = data.privateKey - format = Helpers.detectPrivateKeyFormat(privateKeyBase58) - key = Helpers.privateKeyStringToKey(privateKeyBase58, format) - - # Jasmine seems to break the strict equality test here: - # https://github.com/bitcoinjs/bitcoinjs-lib/blob/v2.1.4/src/transaction_builder.js#L332 - - # Check the network property is set: - expect(key.network).toEqual(Bitcoin.networks.bitcoin) - expect(tx.transaction.network).toEqual(Bitcoin.networks.bitcoin) - - # Override it so the assert statement passes: - key.network = Bitcoin.networks.bitcoin - tx.transaction.network = Bitcoin.networks.bitcoin - # End of Jasmine workaround - - key.compressed = false; - - privateKeys = [key] - - tx.addPrivateKeys(privateKeys) - - tx = tx.sign().build() - - expectedHex = '0100000001594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861000000008a4730440220354fd8f420d1f3ffc802af13d451f853d26f343b10225e92a17d3e831edb81960220074d8dac3c497a0481e2041df4f3cd7a82e32415c11b2054b246187f3ff733a8014104a7392f5628776b530aa5fbb41ac10c327ccd2cf64622a81671038ecda25084af786fd54d43689241694d1d65e6bde98756fa01dfd2f5a90d5318ab3fb7bad8c1ffffffff03204e0000000000001976a914078d35591e340799ee96968936e8b2ea8ce504a688ac10270000000000001976a914a0e6ca5444e4d8b7c80f70237f332320387f18c788acf2540000000000001976a9148b71295471e921703a938aa9e01433deb07c1aa588ac00000000' - expect(tx.toHex()).toEqual(expectedHex) - - describe "provide Transaction with private keys", -> - - it "should want addresses when supplied with unspent outputs", -> - - transaction = new Transaction(payment, ee) - - expect(transaction.addressesOfNeededPrivateKeys.length).toBe(1) - expect(transaction.pathsOfNeededPrivateKeys.length).toBe(0) - - it "should accept the right private key", -> - - transaction = new Transaction(payment, ee) - - privateKeyBase58 = data.privateKey - format = Helpers.detectPrivateKeyFormat(privateKeyBase58) - key = Helpers.privateKeyStringToKey(privateKeyBase58, format) - key.compressed = false; - privateKeys = [key] - - transaction.addPrivateKeys(privateKeys) - expect(transaction.privateKeys).toEqual(privateKeys) - - it "should not accept the wrong private key", -> - - transaction = new Transaction(payment, ee) - - privateKeyWIF = '5JfdACpmDbLk7jmjU6kuCdLNFgedL19RnbjZYENAEG8Ntto9zRc' - format = Helpers.detectPrivateKeyFormat(privateKeyWIF) - key = Helpers.privateKeyStringToKey(privateKeyWIF, format) - privateKeys = [key] - - expect( () -> transaction.addPrivateKeys(privateKeys) ).toThrow - - it "should sign and produce the correct signed script", -> - - transaction = new Transaction(payment, ee) - privateKeyBase58 = data.privateKey - format = Helpers.detectPrivateKeyFormat(privateKeyBase58) - key = Helpers.privateKeyStringToKey(privateKeyBase58, format) - key.compressed = false; - privateKeys = [key] - - transaction.addPrivateKeys(privateKeys) - - # Jasmine seems to break the strict equality test here: - # https://github.com/bitcoinjs/bitcoinjs-lib/blob/v2.1.4/src/transaction_builder.js#L332 - - # Check the network property is set: - expect(key.network).toEqual(Bitcoin.networks.bitcoin) - expect(transaction.transaction.network).toEqual(Bitcoin.networks.bitcoin) - - # Override it so the assert statement passes: - key.network = Bitcoin.networks.bitcoin - transaction.transaction.network = Bitcoin.networks.bitcoin - # End of Jasmine workaround - - tx = transaction.sign().build() - - expectedHex = '0100000001594c66729d5068b7d816760fc304accd760629ee75a371529049a94cffa50861000000008a4730440220187d6b567d29fe10bea29aa36158edb3fcd9bed5e835b93b9f30d630aea1c7740220612be05b0d87b0a170f7ead7f9688d7172c704f63deb74705779cf8ac26ec3b9014104a7392f5628776b530aa5fbb41ac10c327ccd2cf64622a81671038ecda25084af786fd54d43689241694d1d65e6bde98756fa01dfd2f5a90d5318ab3fb7bad8c1ffffffff0250c30000000000001976a914078d35591e340799ee96968936e8b2ea8ce504a688acd2060000000000001976a9148b71295471e921703a938aa9e01433deb07c1aa588ac00000000' - expect(tx.toHex()).toEqual(expectedHex) - - describe "BIP69 transaction inputs and outputs", -> - getIn = undefined - getOut = undefined - - testVectors = require('./data/bip69-test-vectors') - - beforeEach -> - getIn = (input) -> [].reverse.call(input.hash).toString("hex") - getOut = (output) -> output.script.toString("hex") - - - it "should sort testvector 1", -> - sortedInputs = [ - "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57", - "26aa6e6d8b9e49bb0630aac301db6757c02e3619feb4ee0eea81eb1672947024", - "28e0fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2", - "381de9b9ae1a94d9c17f6a08ef9d341a5ce29e2e60c36a52d333ff6203e58d5d", - "3b8b2f8efceb60ba78ca8bba206a137f14cb5ea4035e761ee204302d46b98de2", - "402b2c02411720bf409eff60d05adad684f135838962823f3614cc657dd7bc0a", - "54ffff182965ed0957dba1239c27164ace5a73c9b62a660c74b7b7f15ff61e7a", - "643e5f4e66373a57251fb173151e838ccd27d279aca882997e005016bb53d5aa", - "6c1d56f31b2de4bfc6aaea28396b333102b1f600da9c6d6149e96ca43f1102b1", - "7a1de137cbafb5c70405455c49c5104ca3057a1f1243e6563bb9245c9c88c191", - "7d037ceb2ee0dc03e82f17be7935d238b35d1deabf953a892a4507bfbeeb3ba4", - "a5e899dddb28776ea9ddac0a502316d53a4a3fca607c72f66c470e0412e34086", - "b4112b8f900a7ca0c8b0e7c4dfad35c6be5f6be46b3458974988e1cdb2fa61b8", - "bafd65e3c7f3f9fdfdc1ddb026131b278c3be1af90a4a6ffa78c4658f9ec0c85", - "de0411a1e97484a2804ff1dbde260ac19de841bebad1880c782941aca883b4e9", - "f0a130a84912d03c1d284974f563c5949ac13f8342b8112edff52971599e6a45", - "f320832a9d2e2452af63154bc687493484a0e7745ebd3aaf9ca19eb80834ad60"] - sortedOutputs = [ - "76a9144a5fba237213a062f6f57978f796390bdcf8d01588ac", - "76a9145be32612930b8323add2212a4ec03c1562084f8488ac"] - - txHex = testVectors[0] - tx = new Transaction(payment, ee) - tx.transaction.tx = new Bitcoin.Transaction.fromHex(txHex) - tx.privateKeys = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17] - tx.addressesOfInputs = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17] - tx.sortBIP69() - - allSortedIns = (getIn( tx.transaction.tx.ins[i] ) is sortedInputs[i] for i in [0..16]).every((x)->x) - allSortedOuts = (getOut(tx.transaction.tx.outs[i]) is sortedOutputs[i] for i in [0..1 ]).every((x)->x) - - expect(allSortedIns).toBeTruthy() - expect(allSortedOuts).toBeTruthy() - - it "should sort testvector 2", -> - sortedInputs = [ - "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", - "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055"] - sortedOutputs = [ - "41046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac", - "41044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac"] - - txHex = testVectors[1] - tx = new Transaction(payment, ee) - tx.transaction.tx = new Bitcoin.Transaction.fromHex(txHex) - tx.privateKeys = [1,2] - tx.addressesOfInputs = [1,2] - tx.transaction.tx.ins.reverse(); # change default because it is already correct - tx.transaction.tx.outs.reverse(); # change default because it is already correct - tx.sortBIP69() - allSortedIns = (getIn( tx.transaction.tx.ins[i] ) is sortedInputs[i] for i in [0..1]).every((x)->x) - allSortedOuts = (getOut(tx.transaction.tx.outs[i]) is sortedOutputs[i] for i in [0..1]).every((x)->x) - expect(allSortedIns).toBeTruthy() - expect(allSortedOuts).toBeTruthy() - - describe "Outputs with xpub information", -> - - it "should add the path to the private key to pathsOfNeededPrivateKeys", -> - payment.selectedCoins = data.unspentMockXPub - tx = new Transaction(payment) - - expect(tx.pathsOfNeededPrivateKeys.length).toEqual(1) - - describe "Transaction helpers", -> - - it "Transaction.inputCost should be 0.148 per kb", -> - ic = Transaction.inputCost(10000); - expect(ic).toBe(1480) - - it "Transaction.guessSize should be zero", -> - s = Transaction.guessSize(0,10); - expect(s).toBe(0) - - it "Transaction.guessSize should be zero", -> - s = Transaction.guessSize(10,0); - expect(s).toBe(0) - - it "Transaction.guessSize should be right", -> - s = Transaction.guessSize(10,10); - expect(s).toBe(1830) - - it "Transaction.guessFee should be right", -> - s = Transaction.guessFee(11,7, 25000); - expect(s).toBe(46900) - - it "Transaction.filterUsableCoins should return an empty array if given a bad argument", -> - s = Transaction.filterUsableCoins(1, 1000000); - expect(s).toEqual([]) - - it "Transaction.filterUsableCoins should filter all coins", -> - s = Transaction.filterUsableCoins(data.unspentMock, 1000000); - expect(s).toEqual([]) - - it "Transaction.filterUsableCoins should not filter any coins", -> - s = Transaction.filterUsableCoins(data.unspentMock, 1000); - expect(s).toEqual(data.unspentMock) - - it "Transaction.filterUsableCoins should work for empty list", -> - s = Transaction.filterUsableCoins([], 1000); - expect(s).toEqual([]) - - it "Transaction.maxAvailableAmount should be computed right", -> - coins = [{value: 40000},{value: 30000},{value: 20000},{value: 10000}] - m = Transaction.maxAvailableAmount(coins, 1000); - expect(m).toEqual({ amount: 99330, fee: 670 }) - - it "Transaction.maxAvailableAmount should be computed right for empty lists", -> - coins = [] - m = Transaction.maxAvailableAmount(coins, 1000); - expect(m).toEqual({ amount: 0, fee: 0 }) - - it "Transaction.sumOfCoins should be computed right", -> - coins = [{value: 40000},{value: 30000},{value: 20000},{value: 10000}] - m = Transaction.sumOfCoins(coins); - expect(m).toBe(100000) - - it "Transaction.sumOfCoins should be computed right for empty lists", -> - coins = [] - m = Transaction.sumOfCoins(coins); - expect(m).toBe(0) - - it "Transaction.selectCoins empty list with fee-per-kb", -> - coins = [] - amounts = [10000] - fee = 10000 - isAbsFee = false - s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); - expect(s).toEqual({"coins": [], "fee": 0}) - - it "Transaction.selectCoins empty list with fee-per-kb", -> - coins = [] - amounts = [10000] - fee = 10000 - isAbsFee = true - s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); - expect(s).toEqual({"coins": [], "fee": 0}) - - it "Transaction.selectCoins with fee-Per-kb", -> - coins = [{value: 40000},{value: 30000},{value: 20000},{value: 10000}] - amounts = [10000,30000] - fee = 10000 - isAbsFee = false - s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); - expect(s).toEqual({"coins": [{value: 40000},{value: 30000}], "fee": 4080}) - - it "Transaction.selectCoins with absolute fee", -> - coins = [{value: 40000},{value: 30000},{value: 20000},{value: 10000}] - amounts = [10000,30000] - fee = 10000 - isAbsFee = true - s = Transaction.selectCoins(coins, amounts, fee, isAbsFee); - expect(s).toEqual({"coins": [{value: 40000},{value: 30000}], "fee": 10000}) - - it "Transaction.confirmationEstimation with absolute fee", -> - feeRanges = [60000,50000,40000,30000,20000,10000] - fee = 12000 - s = Transaction.confirmationEstimation(feeRanges, fee); - expect(s).toEqual(6) - - it "Transaction.confirmationEstimation with absolute fee", -> - feeRanges = [60000,50000,40000,30000,20000,10000] - fee = 12000 - s = Transaction.confirmationEstimation(feeRanges, fee); - expect(s).toEqual(6) - - it "Transaction.confirmationEstimation with absolute fee", -> - feeRanges = [60000,50000,40000,30000,20000,10000] - fee = 30000 - s = Transaction.confirmationEstimation(feeRanges, fee); - expect(s).toEqual(4) - - it "Transaction.confirmationEstimation with absolute fee", -> - feeRanges = [60000,50000,40000,30000,20000,10000] - fee = 2000 - s = Transaction.confirmationEstimation(feeRanges, fee); - expect(s).toEqual(Infinity) - - it "Transaction.confirmationEstimation with absolute fee", -> - feeRanges = [0,0,0,0,0,0] - fee = 70000 - s = Transaction.confirmationEstimation(feeRanges, fee); - expect(s).toBeNull() diff --git a/tests/wallet_crypto_spec.js b/tests/wallet_crypto_spec.js new file mode 100644 index 000000000..3513ebebd --- /dev/null +++ b/tests/wallet_crypto_spec.js @@ -0,0 +1,553 @@ +let proxyquire = require('proxyquireify')(require); + +let Crypto = { +}; + +let stubs = { + 'crypto': Crypto +}; + +let WalletCrypto = proxyquire('../src/wallet-crypto', stubs); + +describe('WalletCrypto', () => { + let walletData = require('./data/wallet-data'); + + describe('stretchPassword()', () => + it('should stretch a password', () => { + let password = '1234567890'; + let salt = 'a633e05b567f64482d7620170bd45201'; + let pbkdf2Iterations = 10; + + expect(WalletCrypto.stretchPassword(password, salt, pbkdf2Iterations).toString('hex')).toBe('4be158806522094dd184bc9c093ea185c6a4ec003bdc6323108e3f5eeb7e388d'); + }) + ); + + describe('decryptPasswordWithProcessedPin()', () => + it('should return the password', () => { + let data = 'pKPZ1y8lM9aixrcaW3VGHdZkQL37ESyFQvgxVSIjhEo='; + let password = '60ae40d723196edea0ae35ace25db8961905dd8582ae813801c7494e71173925'; + let pbkdf2Iterations = 1; + + let decryptedPassword = WalletCrypto.decryptPasswordWithProcessedPin(data, password, pbkdf2Iterations); + + expect(decryptedPassword).toBe('testtest12'); + }) + ); + + describe('encrypt', () => { + beforeEach(() => { + spyOn(WalletCrypto.Buffer, 'concat').and.callFake(array => { + let res = array.join('|'); + return { + toString (type) { return `${res}|${type}`; } + }; + }); + spyOn(Crypto, 'randomBytes').and.callFake(() => 'random-bytes'); + }); + + let data = JSON.stringify({hello: 'world'}); + + beforeEach(() => + spyOn(WalletCrypto.AES, 'encrypt').and.callFake((dataBytes, key, salt, options) => `${dataBytes}|encrypted-with-${salt}+${key}`) + ); + + describe('encryptDataWithKey()', () => { + it('should take JSON string and return Base64 concatenated IV + payload', () => { + let res = WalletCrypto.encryptDataWithKey(data, 'aes-256-key', 'random'); + + expect(res).toEqual(`random|${data}|encrypted-with-random+aes-256-key|base64`); + }); + + it('should generate an IV if not provided', () => { + let res = WalletCrypto.encryptDataWithKey(data, 'aes-256-key'); + + expect(res).toEqual(`random-bytes|${data}|encrypted-with-random-bytes+aes-256-key|base64`); + }); + }); + + describe('encryptDataWithPassword()', () => pending()); + }); + + describe('decrypt', () => { + let data = JSON.stringify({hello: 'world'}); + + beforeEach(() => + spyOn(WalletCrypto.AES, 'decrypt').and.callFake((payload, key, iv, options) => { + let payloadComponents = payload.split('|'); + let cryptoComponents = payloadComponents[1].replace('encrypted-with-', '').split('+'); + if (cryptoComponents[0] !== key) { + throw new Error('Wrong AES key'); + } + if (cryptoComponents[1] !== iv) { + throw new Error('Wrong AES initialization vector'); + } + let res = payloadComponents[0]; + return { + toString () { return res; } + }; + }) + ); + + describe('decryptBufferWithKey()', () => + it('should decrypt when given an IV, payload and key', () => { + let res = WalletCrypto.decryptBufferWithKey( + `${data}|encrypted-with-aes-256-key+random`, + 'random', + 'aes-256-key' + ); + expect(res).toEqual(data); + }) + ); + + describe('decryptDataWithKey()', () => + it('should decode Base64, extract IV and call decryptBufferWithKey()', () => { + spyOn(WalletCrypto, 'decryptBufferWithKey'); + + let key = 'c9a429594db2ae5b3df33aa651825073114dc59df230fbf74aeccab2cd99bd59'; + + WalletCrypto.decryptDataWithKey( + 'bvOk5Ppz9rPDLwlR1wObQoyPVybs4h+fg0eynTe4u/CePkZOPFvobnTpzGGX10CY', + Buffer(key, 'hex') + ); + + expect(WalletCrypto.decryptBufferWithKey).toHaveBeenCalled(); + expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[0].toString('hex')).toEqual( + '8c8f5726ece21f9f8347b29d37b8bbf09e3e464e3c5be86e74e9cc6197d74098' // Payload + ); + expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[1].toString('hex')).toEqual( + '6ef3a4e4fa73f6b3c32f0951d7039b42' // IV + ); + expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[2].toString('hex')).toEqual( + key // key + ); + }) + ); + }); + + describe('decryptDataWithPassword()', () => // decrypt() + it('should decode Base64, extract IV, stretch pwd and call decryptBufferWithKey()', () => { + let data = 'zKHay4ml1+lRidNioY/Da7R5E6ERiwU7bDYsSS2UUH0='; + spyOn(WalletCrypto, 'stretchPassword').and.returnValue(Buffer('b484f16b5980e877439f5c65aba87572176548280feda10b730e978911dcc825', 'hex')); + spyOn(WalletCrypto, 'decryptBufferWithKey').and.returnValue('test'); + + let res = WalletCrypto.decrypt(data, '1234', 10); + + expect(WalletCrypto.stretchPassword).toHaveBeenCalledWith( + '1234', + jasmine.anything(), + 10, + 256 + ); + expect(WalletCrypto.stretchPassword.calls.argsFor(0)[1].toString('hex')).toEqual( + 'cca1dacb89a5d7e95189d362a18fc36b' // salt + ); + expect(WalletCrypto.decryptBufferWithKey).toHaveBeenCalled(); + expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[0].toString('hex')).toEqual( + 'b47913a1118b053b6c362c492d94507d' // Encrypted payload + ); + + expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[1].toString('hex')).toEqual( + 'cca1dacb89a5d7e95189d362a18fc36b' + ); // IV is same as salt used for password stretching + + expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[2].toString('hex')).toEqual( + 'b484f16b5980e877439f5c65aba87572176548280feda10b730e978911dcc825' + ); // The key returned by strechPassword() + + expect(res).toEqual('test'); + }) + ); + + describe('pairing code', () => + it('should decrypt', () => { + let payload = 'qf7x+dZBbUQnzgbL6SJfI8w/nracmNe4YiGIww+J40V42u4T8SgyjfJdIqPsKkQC7m0UMpQ4OpvkBl2H4NIolJizO/N0zauRnkaSeqK32rY='; + let encryptionPhrase = 'efd93e61900251ab6cd19a4f14e5a2229866e150b04f40fe7912f392c24b5a7d'; + let guid = '6f209355-9e4b-4708-9d64-272d76bb8062'; + + let decrypted = WalletCrypto.decrypt(payload, encryptionPhrase, 10); + let components = decrypted.split('|'); + expect(components[0]).toBe(guid); + }) + ); + + describe('decryptWallet', () => { + it('should decrypt a legacy v1 wallet with CBC, ISO10126, 10 iterations', () => { + let data = 'OPWBr1rsrvGsbNpIidlztqc0YsPwS0gg51rz6gWlrsJzY+VidziSekuiy7AcxVF42sMcJp9XD41xPsmq0m9yEWrFw6QufwLjSWNE4IK8mD6jIYH35a7fWKbK0LXGq4UIHCfM2W8WVoz/l0QO+JrGrqC3gg8qGyHP3NsVZKVAqG6cGmBi9WEs688U5B0NNGPXPLKE1ZXzHbSd6Pdub1xWv/BEo4RsAu1NySQJpcq3hqo9nLMsza9aiwKH5rG1aMUDu50LNtGs3vCx8ZAkcZpYVp1ZLeoD3pnZVc7siq3kiqJ7zDQoE3FORgD6PuAc6YB2PXW6I3ubw4hkvFMnkIK4/Cc/AEB8RYar6rjmgPVYXSm+ok39sPi9ppIE23k4LkFzz3dUbTM2ub1kKPCUoJLp2E4tUg4hqRidaC7rNxkPyI3lyBWrS8JD457pFYlTWYsUtU1P2sHhxKZuKdeDPQ/Jvo0y+xO5rK9OgmKCg0qxuwaXf4NYu6laqaGEQywRmRyhT1f3E0pQZ371dObo4FOdiVEODhvadPf0FCHjOtuaxWwEkFwyFHVtc0lVNhcy0rg65j2efHpDUXqQnqFBgc2PG23BVI1gY0JIDT5zp33wdFX3r6MjYxSUV7KbRBDwCD0Q3a9NepX9bqv3wpi1qYJ6kcht0iMAE+5WmHHeHWza2HFMXcUApSAU6cu2fzOfK+4gRMJPjNAdk/nVQT1UnWy9k+s6jvwaXBPI10ewaTz5ayRTXyku36N1xLsM6DnBPoJCunuDEXMI5dILgr3BVSCqvzboWsRW04bAfFbpYANioQgdDD5zerygHa61V7ICyD/x5G4li6VLIefsCGGBo+7fU149zYkHv7ruH8F/J26b11UH+gpThimLgenJectT3MnksMFaz8LiSRn6jnz7CMeXssxBoYRT0gvq4MN6JxFGD01HfcqfVuBkWXk8Mo1OE75v3HWovrJOrTXhYbr+JaPppA=='; + let password = 'testpassword'; + + let obj = { + success (result, obj) {}, + error (e) { + console.log('decryptWallet error:'); + console.log(e); + } + }; + + spyOn(obj, 'success'); + + WalletCrypto.decryptWallet(data, password, obj.success, obj.error); + expect(obj.success).toHaveBeenCalled(); + expect(obj.success.calls.argsFor(0)[0].guid).toBe('6253e902-ce79-4027-bdc4-af51ed970eb5'); + }); + + it('should decrypt a legacy v1 wallet with OFB, nopad, 1 iteration', () => { + let data = '1LbdcPCYTFMr3Yv27kaJ/kqKflbhLMsQJXAL4EyWOzTKiDHmVrO6JTInh0GzXwTcDSJJQw4G614LT7YyfaZLd3+shhFDQycvSLKrnC4wM40CcIE1Ow335YlolV6+WAJAAQ951ipS2cUERXSpxbbDy41AbUG+wIX/R2wCbiosT6OLHjnMSuQWx5CyEB1ZqRUXfc3TYwbh1iCJdBDaPFnmTPN/LpRegC99DFEJ0E94dPim42Q/8KV4mDNvCOUGjLfqsi/3I+M6kGJtzC5CUo0dVZ0bzzTTXEXm7Ga0eo3arCdMQUDPVGcbJbGK8qTcWcHXHw90EJEGBUi6i4p1XXw+26T5k+Qs6Fl7GD3PHl5Urusk4twB5DM/Hwp5vFWnsCsbM78CRhJl3rbheBMx6t1RJKvxpOUrh/OyvOwhWtU6hRXmS/zrwwDqbWiVG9/LzZP19hnAvlTJ0o1jF/Dmz+EoTaotkVHFZDUuYDKfWPY/+K7sQGkagfdv5W1X9rfary22prDUo8kFiRvNHvGMie3TiM1ucSdpBj1DpgfwCxnWzLIeoRl8ZJFUN/V8CkGjpPoyBYZFfjCeGq9+ET0wXL4s9qfNXaHfx2oEuiwcLcDDf71us655hIW6t/OPaPZ3WPhnW4KqrTtL4xduYrisYbhoZkmqJe5Jl110jhiKdNYdvmOcFo764RDPcu2Oa1bszYhGTbRPc45kLauJUAW3e/UUbA87RpAqFnOLH/0vcNMJvcKbvID9A1RSZNjmlerSOptab+yYI97D6EitvFDx9tFMmUrfWFRGs5bOrjiAHobcCqpJ60AivYHPkkkaz8gROz30aqa9Dpz15wplbOVIpKVqf8hly8Bghj5QK4+m2Nf2qALDdBfX+1ZIUh23kiy0s3duWoJQttIyK0AeEU8oHa0MRP5T/ikMatVvw7GVmcDzO8pFBdBxssxReEhnXCpd4nmjcuWbroa+5oaCMWtzXH4I'; + let password = 'testpassword'; + + let obj = { + success (result, obj) {}, + error (e) { + console.log('decryptWallet error:'); + console.log(e); + } + }; + + spyOn(obj, 'success'); + + WalletCrypto.decryptWallet(data, password, obj.success, obj.error); + expect(obj.success).toHaveBeenCalled(); + expect(obj.success.calls.argsFor(0)[0].guid).toBe('6253e902-ce79-4027-bdc4-af51ed970eb5'); + }); + + it('should decrypt a legacy v1 wallet with OFB, ISO7816, 1 iteration', () => { + let data = 'ag6Tn5xvl/9OwQ9uvRl1wcOBaYPsCHFEyv+RjSHwU+evHlsBvsWErhxRFWpaK8oqvy0ECH2F8IH8NWMwoXqGbmV7FNgaXiJMhNCtiJvRIv0oY9OoMvQajVvmmXO+WdepwPGVJ130xUz8aZ9raTUGJySKo5/Aq8zBybXqG9UdxSG6QKxRaQNyvh4nb/T5h9b4opwoMpX+Mqh4HRWcURTgQ+RgGC9wKqdrpCL5nka5RsbRj3eQEG6ZyOzq4ROCjE217RqZhS+n6qjw1lBPPwFREbbWZ2NVVvXRJP1tv+VfRCK69lqoHS6GoTrf4aZy35t/GCISEgzyLVCpcpERgC9uUKzSRoarltHd+LPV7RB6+qA1UxBvzbdcYplBO+fdcWHlrx3YES9lpSKB+B/+PP/aFOg+3z/cQbtkLJ5KkXwrrceKqylNznq9pq5u/a8vjTWkkddgWJzBeQD0FF9kj5zZCJXRb0BwoST7oT952FHz751egpnC5WbCLvM0UgnLH9jEcxkuc2vKpnAEEjyXvry/8WWrzGPsnJgEH6wGvxoocqYWcGIFBMj6iHstDMKa4BsaWvqMOUEyvYnGqvo29Xykzlvou3LUAfoz5xE32UKwZRLlsVeDZafQHk5V10yckRETJaA1P6ZGvpuHGIRsXGlpTZJMCz9XoJB7uXYeI66Rw5JYHgezVtrOTTFNUk/ZuPMty4rRAGIaNiFRQsJE5hY7SHbwvLyoY2xmnAl4J57J2mn9CNXYWMqK9WiF/xPTYU9YphHSlYWL2/pMZux0SqW6pSIoF6YoXKl7bv71bAf+tVYeysLLezVdGYi44ezdOQ12sH1ipTerk1LWJL10DVjh58uigHed+gceF2w1gjcUlU7Xsh4Z+m2UlIekSETmFW/uY2FMQz6oz3fikxwRdXY7gNY5VES9DNA34MV92wfMgRNJD1I9dWRMrPxhRoClZJA1uSBMg8n5jjnhhznTzCJ33g=='; + let password = 'testpassword'; + + let obj = { + success (result, obj) {}, + error (e) { + console.log('decryptWallet error:'); + console.log(e); + } + }; + + spyOn(obj, 'success'); + + WalletCrypto.decryptWallet(data, password, obj.success, obj.error); + expect(obj.success).toHaveBeenCalled(); + expect(obj.success.calls.argsFor(0)[0].guid).toBe('6253e902-ce79-4027-bdc4-af51ed970eb5'); + }); + + it('should decrypt a legacy v1 wallet with CBC, ISO10126, 1 iteration', () => { + let data = 'BuAqphHuJDoKyTIHRXlzlm1ZMVo1NKM9l6fDFldPGOPTobqEG+9RjO9UHLcUO04HAbqogDZdoOxU2cyNM7bi7JYEZDTlfiIYldFkJUP+zGA4Wg+twCS+oOJrv4D8VoKaFl8oAnIns2n8o8MEQscxxkBzjNWoZ3yE6jG1AjtoYt8owrz97S8XFmnSaK3/Q3doTMKFPaRZ7NAR7z0gWI4KaHtfmHMYEsiGy33M4riq3DCyiGGezg+f2IlUUsRj/+VmxZd5KlCpA4ZIG3fPpp19BPj2S3yFdDqZtMjfggo570iHi7eMogFrZAo+yiLJlYSaoWT8fs8vTmnm8pxxRYb1601AmrBnNlPPpvGRNSBiV/rfKdaVqPYygonTBca5fLBA+QQQ8k5HJdiKRjgfJkZWpjexxdXTuLi/bRKYfKOp5GT+fDrgNAv/E56U4rpwpolAQYzJI8XGDF6CZCVJHWAIPqq8XO7sBBofRuGpUoJMZChDoSbVJV12ZYKSo52j/oo+G/e/Rylrydb2u2qte4RdvYQE+Fr9FIX8nR3JUJwXpXx3/rUImPJa+9P9+ySOjRo4zvmh04Zj+C6eGfsLO4GvpKe748d6WrQzTZwzcVmzCeFA0spcDmJaeQ6iB9HKh8yYo5YDWiEiuHw1CIxDb83wnlLepxmk0isEMCIXETNJ+8ns1263kBbYKbt9FyfB8gNrSHEM7tyVc5v03Z++c11+9dSONnLyQArQSLCvnktPQlKm2cb/rRfmbk8ASlCfZkkhn6TR7Ugcvgoj+Sh7fm9LWYISdPx3vmPriplQjvnYnrvBcXoNmJuFmNG6tBgCK8wdbtOCtFw4KS0e8nAOjZ4o0xfNuLv+TmMyyA9xAXRGDEPV+5Z//MAQJc69usAazTaojxicUh0RghUlQOpiqHW5Mbg0tUlkAkCJJYJKfu6elNDslw8l6NwjIvaR7rgdF6JGXbRrX9zl81dAYfyWxJg9Nw=='; + let password = 'testpassword'; + + let obj = { + success (result, obj) {}, + error (e) { + console.log('decryptWallet error:'); + console.log(e); + } + }; + + spyOn(obj, 'success'); + + WalletCrypto.decryptWallet(data, password, obj.success, obj.error); + expect(obj.success).toHaveBeenCalled(); + expect(obj.success.calls.argsFor(0)[0].guid).toBe('6253e902-ce79-4027-bdc4-af51ed970eb5'); + }); + + it('should decrypt a test v2 wallet', () => { + let data = '{"pbkdf2_iterations":5000,"version":2,"payload":"b+OfaGwd/zcOsSy3BUXD6w6u18rVOiFFeSHLNCZYhgfoPGj3csxlYL4gAGYYBqQDT6BzWdfT3/7rq0Oe44mMdFfKP8TWyt41aPOXKZo5MgfBOtamvNMD7PtW5Qxap9VUemKCdWMe5IS7R80/md2E3WPxUlWIqW2mooOFFpfoicVMGnJormlUaiUIsG9hv43MtP3g18eYv3P3OE+2LGyjavmSTbmgLznUA8TcAJNNLwD8br9kBp/TTd1wTFNMwyjILlD20HgoX4S/922qhReR4dxTjJUjplE5LJVBkemwNDNqCXZVo94r3RizD7SBwXneawVGIQZYOZ0aBvXdEe2ooIoZ2fIGMsdJCMvzXBXZ85jdfRg0LFtB9+YaOwZsM3TlY6wr7hk3sSpTVtHf22+egIWQEa1rylt3ywK/ArdAq4ydpGOtjfjfZfiKazc0CYiqVxJ5HBD4NBfh1moLZnOcHnzM5EnW6jO9dVek579C4sh7s9kAztwCVX5r4GfSTovPvuwzcEVxUL4uEHd+zaczQHU8zWIGrXupWKHfkjeoWxDM0iW6edBWtv13/TJnjTkeJzraQS1iU4iQGkPplITqctHIgschR8Cj2HurUcW9Y/2C5nEdhfMjnTfCxzspLWObOCGWnLEMWOJhC4MPsUJy3p+R1JD7p1QlyRQ0I8vtn+FEuEUL4ui9Fe9zfRvd1xfiJ+0CBCAtRwzEaBqCnI9GciErdTysUw0lvcBKbPU05ajsSfbYIzow2O6SG/PCqIcUbePChuqVJBRPmK1erzC1XNt2HFRYKTqIWy/jtx4cKgXBtK750LX+67Wcvw/OjX3rthvfE4ZZYYl/smJUB32b2tfSF72+aSzmC9nz/fJTztNahQIPbU/vGE2R/R5CQZCjvQxvgsfXKVk9N0Av9gpH1w+i0BgAIwi5mBA+zZlgUteNcuEWkPgE4NpN+uue9mlq/t1rpySjOgt0D50gUmJMfQGgYUoN3OtyjGUt437Auk6Y37aHYXZekjDPCdgr7yYDvkmUVeB1f31swzh/nwz4lbw9ezSTHIYBMN4XW4xuG3vtefu6fIK0BDncXEE7d3BM9OnWzTBtJQVvWZW2+lslb/PjmEFTY/UoELKjpG4wTvm+LfI+Q2jlLT7VblZeUVdEeONhElkadl/SQQq2sRVbEHzyZhlEc3cbznT5BPeGEoIG70NOckCxl57F84VO17d9xvtMs5joaYB0nr2yRuBFU9/T1FOLiQKjOv/GCOLLL5IdOUlVHNlj17EQ7KtaghNTA7ndPoVp7pgc+Gu2zKn0yBVKzLTD8t7L429pCs9N0QsO00w+tVLZwH9l37rNkqFaW1BhOD+T4VnGdy1fYIeqKfyWpf9UExPaZniki7+syIPoQ7bc0ZDWzr7FcIuRsPzOu3koW7fOPxYYaIWHLGSvtnfsRVb029CoKsr/RQNA8zKN6SuYjguEdA63/GEUIIDX9QpAlyGAGFN2Dfm4DgKipDHgqiIykf/PldocnF2iS96l+JFHFwtnXBjNOVjhbmQDJOCK1IhZfjimqfRBmFATmPerpMqDllACinmu7WcEufEn/NdKRVagki0GEWhWbcg+LApuwTSG8K/6JR7dHjh1McuZ60PObVV6qSjvpvIW+THuQ52C5DUQz7ReLuc/BMe1zd/xFB982AikeTIDFQl03QJ7w5inVBrDUcJEbNt8Adg+byPozUDnw06+i705h5gZXuWioklrHbt7OlSHcWfWnZa7HqZHJ128mi80OzpK2qePp6/mYv/zbcjKPh47H9m2LoV2zeAzlwMsgQtp0wu8bXbietEB0KtvVTarU14iFOftwZnhgVv3oIUVZQ7zkua4J+eiMy6gDQNNUB+0t11F8w5c8Ivnp+rg1nD4RJaOHnIZ2IU9ie2PpaiuIiyyaX1drAzNOM5MU0dOrDBTIkreM8bZH8LllYnkwa62nlY4EjWtVivhDk9rB7bOUZMhQmHB9Y539EmHJxvqIborRDGDC/3AvuFSRZN1jug6KZhYjr8aG98HhN3cFw6M6DwDS8llhkfMdM1l/fFS3LjjuVz1yAOdI4GWVDKdjKKjW2rbwjnpgoK+plxN8KqM8JEt6z3gPJwvIhImysXjXNKxbjHWVKGAYD28iK6f4csk1NrgCHM12r8Lkwf0gXW1AVvxufYKzpa/9MrA6V7B+Y3fJ9ooDAlikVCFzVEFD1894hgWhOXPeZS5qOqnW4bvViGsi0dhQvWnztBu+PUN531oL/Ecx4q/U0gz6oxVJ2YtoHG3G/H8ZFdq4bMwXM1A3b7rd2sIdO6EY87g2PgpoPF7idnfQdxxu3lC6HjaMwbn8mxwwJSSoGO3QYmZWRhRf8wJteGm3ha5rDXS952WGBc1o7K2zx7kbjURR92pkdyPqSyUfwYovZXpzCawGlAqbIbRtcxxQdod4OBXCdqmCBklxjUb529/arFJYZXJJuxIY6bM630mriMvyYAuHkgIbsiTwyTcwJ15OsQxy95aNYiHU1HotJKJhNRdpzBvAKxSoGhD40vkM72EdpB6ymeAUeABRb3wvD1KY+a2GcPVSE7ZVqaNJ+hGOdDLq41+Kwh9/mR0zlmmIKKxwBX3IL9EX7eesKVOKH7JFMggLNi2fxuc46OCQVIws6p54jUj1ksce45EYE6syEZkB8Ekhk3V4s5DwBDXBuaHJa70AKRZPlG9QGXLeVmF/JS9PhcEA+3QpOb79+OZF9BBnz6hPPrttAPXGMqoNQvDySEV4l4tijrZJHe0YMOw9zoVQYA5hJW1+kS4lUq7SIC7gSN+vWAMBh6LFAity5JikUlXoYievM8zqySbr4xP9HI/zph3tHmONkfTBKCo/dkD1QaQfD37Qgqo40q4WDgbS78MMq0AeHU9WTVoa1IBkn5aSAelq3rTMVaKTzBOxKdc7EI+Pl1MNf8+0/ggNPC6InCpvOcNdbPuQxM3NLqMrZYNb5x+xoxOXrAvDH4wnF59JkvGhr6OOU4RNfv5YJZ/U3r9NyhvQtJExHKWKE6fxjDa3bruZ4UqQZXxwW5XgMS6+5QN4j1dOJhiL9CnWsNEQvSTa0rfZv3AcZs41hPrY8NXAueYZfjccec30lH7iGDgP2NOvd8tRbSO+X/I9OFxc6m2CoEoT4sniy6rBUCh19hJOt2P08x8U371wZkEG1yQzOF1mrmvlAT+aZP0WUS55JM1sqGdEHHTjJuv4h9NSCP8nURwj6y5hfqnUQPnofUTGvwV24E6+HvQTXoaLxks5k1Rw8moKfhsVEmdj2Arh9O21vwhEYc1+uWFNqT8s5f0OzQ9n0glGdg4SWFsZ+ttEyHaE5HTkuNm/dXtQi6GOcjZzR41+8gMh8O3KFhkZqxLfpdWSNeZ8GpY/LBvt1ZIXIIXjjybYDOz2Sx5ANssXt8nyk4nyjl2Ggv5BNzhbcQfstC6Pg0JVFhXtYizCqyZsjWhwaKynnP3UCqf5xJteuFwOyZyXpeEw/tdtQqtpw81T2iba3+69q7tkpv7FJtMraGfbD/BBWRAQ/W23AwREkru3Pm2sbfRSqnc84b3ZARUKpV6k8MEALWOcNFrP95uBQPHaBGZSQR4KZdlHvEjRN8WqZNgfH78lnUvWbKkdXGSrMeMrZVZwXI4fUlfexnqa/YeVmNewRDajuRTWnYMlyQgQae54FF+iz1NSGSNt6eruL4vgVFT3R09hxCDsqf5tNCDR88XC/DtDZvyPVs+e9Va/DA2x5lir6p/U9BKo5mNBG9UKvrOvep7Yu7Lfn2dN7cutDAY2hZRrKeVJxwXg1IlVJnsCDHockVfe4JSsAiUPZQQpPx7id5GbUOcXT9dtHgFxt5BpqzTldrbXYqvsit0kusUhkdJc5Q/+g9gIYqRNLLouNum0heZZ82dKfmhYwoED+hupYW1WidTIEkVcv3cDeYPPBdxcU7Zli1M2n7M+AbulhmZXxe+RDkbFvK7m8rzAJdOjI7Vxn3+8/8X3sQl1tpTWd9XLz+O/l+y58IZI6jpCSZGRLfJpjNJ4wlJs3Vr3aTmP0m4ayyz28lIItH46URrSVc/TUQehaVw09ouMlMN/HOXc7nvPL+2Ll161h6reVLuo6FOfrIWHGYNtklB3gKbEYeLEUFaIgW5am/A+mhJKiaC8LsdeOhX6f980YDN4cXkn/NvOXKfJ1D4X6MptfQy4dBj/jE4rMIcUmPXZi9tZKXJvk5IchO1RLnS6bh9ys+oWz0w0OaofE5NbvoFj26uLNhD3TPesmrt+0JW1okiqQe9Jr0HLlhlQQp0zwoLH3aHWbY6h+Y4zVn0sEYZ3qKaoLT6MzMZtdYRX6YbLyQF5i2dAQRto9LE18Y9GX/kKHER61Is5xAkCRIbgl4v7Dq2rxPQoKD97/qqXN6OVLKasg3mUyqPjL5+clTb0Iqlfjx7+/ftXpjaHTM0YQWMa1TZlB2D4dXVWb4/R+HlqTiX7owY58+Kih14Rt9dtiIndjvcil3N2CdUz9bW6ur8MhtW5qWrTz1tXXpsWgcZrVCzECuFJZfltXRvbgz1Z3j9CUXJwtzYWKekxEvGP9tHnccvGyI3hC6llqHs5mkaNJRGkdeuwNgNdbJS+8ZU8YyRj/s5SCh9A9ncOoI6kZh27GcqckLBq8au+zFwr6zPusdTWUH18G9cEPeL0tsfeDJKz3jI9pl8l6emTEQ3ROveqSZC/D28pmK1z3IcGDP60bwbOTGHLnjd+S6PjuoVi1l7v4bTpNwnvslK7mQ9cuG8xzPppfMlyOtdKFvMBEFYUh0vjBLNrQT3M9DaCvLDvQNIA13whWYzyNbirSahYlSrw5Er4edXzIkjw2M8vmiCS0HG/s10I3KKj0Ck6RiT9dxg7Rez3wl+nvlobMZiihBfO4FZVEP2wbgHmI9vkDKBSiTXVkCA3cXPxa5qJNrKRcOCtZXhQ7SsQViClH490T+lqMjICwSn4EOV7sO9VaKv4neywsedm+aX6uwQM/Xhwji8Z6SFesW+fWCWsZoQ1IshxXYtra9diD2CUk1Z1Sb7oExROd93sSPdFP99LArUPatN9zSUlXIEEekV3NycBa/2kyEm7nlYkZb51b0YQClYivGKD1wPa5rvYBv2IUAqsiV0Sh8RDqQKqucZfZvdl7IMrxmcs9T4p07tEsk8RbqWJBEtc0aSMFrIIkNLzazzAPMfaOi+OhDVj4nPrIeIpDoPjqppTfVZw2ZZu+kBov2hgamYakhx7z3ZbOfAqA696RqvIyx0MYZJ65Rt2ObaM1EwZfyyTsBztvwf23KqUSuOr9qsVhlfmTjSDoT0mUGT7uR3mCAVhsVMAfp3LmzNTmzWAOgBYKvBv45czqGCBkwFA4Gs8Ed/IxvUbErm3tGiGjHzrO7JrDDoZ5dGbzsfg1WPl3EyUyipn9C5md1wGDXH4eFG8SbXajk+v/G6uqK5EVgghWKBSfuYMvRy8ujghet0BInXddyZwhCOT1GNSSR5HiUPmkYR2trSs06fDBtut5j70YnEY7qEmvaV84WTz2rybAVWSWeJd8SDZcrvJyZyFCU49PRxGMnGZDpLvXel/W2EfbqIn0/f9lwCrEtpiUUzPSMlu1Sp956CdvHRLtv2pLqryp6Tw1aBGFHFll6eEgnKnu93IPLojXUTfhWHU4stAWtjM+lfSAMapZWjEyS3cI5rekBxJ6qH9zjSTMcHzhDzfTomIjKZJwRCzU2KjSxZbVOMtNoNbBAD6HW8/KXwtvVETlQZwSLUpUeCWdGLNyT4s/HN+vaG7j/BQjTIj3YM1gfHhpzHvvrPzTq/2/myJS1mq+Iu+lF8bHQQ/g81gDv17xoMZUIOjNSmYLs8PkAFDNWpmmc1Tz3In7MxQMj+xZWgW4l4+7HTfTDtBBW+KmG5haiwqwGfCPBX13kYWzqu5CLne141BYZEqcsVF0Qwmp14+QfjLcKuHjXz0G8NKUqzC2xgP80Pljc79MxjBCthVplUKoAVU84krApYRR/LGI7AbrS8V6La8hBMt6XCPX4xF2EkuoQ2D5O6U5afAhcSIyPtZDPHz91e+yH87Al7rmUZPaslux/yhuCjmdzARr8g+e4JHlBgD2udSdIaKuodT7mWST6f3WLE7jKxSOn0IssYSi2mkV9nNHQY1QIdf87Y8R9dJfodAT/5lxJdi7X+yDpp9WGwXrUqvfohAG2H37fZis2H4y/2Rf5vRJgw0Lvs3haeFkzgkvuKxy6sjRX96p6sIpoEfpiuP4bV6Z94VpMsbcrNCA2Pkh4Mew5xNwf3UVPen/6LqjCUMdWoPn8Fz0sGRGISrBCrvVo3yJrG+tifxrny5LEjCH3tkVa+6K6s1qud/x43oAkusBI4Myb17OvE+WVeslWenvkpQ/yotXU0wY9LIJjp8/oIXh1T/p+ZKRuB1q6Y7JmLf5zbl3JNVnAG/PgNsr5JXp7lhrIR19gfUIajml99M9gB+Ly0luHd18jTCHWkbEqXxSekXf8eeOoKouSaTUDcJtxlupmNG7H5uU1KM+TQLaz+I+hgAyO5PLjW4VWkjJXImiWYtzya20oFe2gsg5ZyFJPuNyCtFopkHmRti0gL726fF4FfinhKyuPKa/Jf7O+g/DM87ycRPts4C11B8OqcSa0OxomaiD2yms0viTme66fDi0/Y6O/glCaOj1msrwB2OXC3Q6nyIrHhdfcxL8CwAyJUWr6VdHYEZ8GisK+hYh3hCNk/AJpcsS7KboCP1emlVxnEKT/iBWpUpXDp/i3Rlu7np/y1FeoER57zzvbniYaRRuHMHByGzQ/VH5Pex6zswB+xXLut5zu4/cQN4I4XNBaH2ftBGFo6ZLL12NCSMWDApVvVaaMy+EETlFJW1eZ7PkjS93iyh6rDP7GrUtPbOjVPlCpZYgWHMWpXurUcT6ztmTE+kW3MgtTn6fixGSoo7yukQIs8fscKsexKnSXXHPxQT58r8mHMgvvYIL3iOfQccVlMEhOvLiRE9gjEA7gm7A7d0kqMLtHbrQoevygbb5uMaLWvSYstyKvnvbW3vh1FHzK6oBWfihyTxDJ5v2R12on9RIbGAzcDsFI8BrvFw9vck9UMcPxFkZDMD0vucvWYNwTb6qRpbc8UBh4U6sNgcf4MgcDit3e0zlL49gqZwximmwkfb0MqxM2cLBTfPMuVaPGV91tYh+jzNke/etB6pGRtrqS8qXM68beOUy2qe7cMV8BcUwk2fF7LMlKN9QPXIjXpmnfHv8ILZobBN4yH07oatUopPsdrtx3b3iQ4Ii98Ku8Qay9KSQznXZfRmwztmqXedBq94abr8yVlUbOoQUuq63OS10g4c9G+ktIKbN+SSahU49HIg0vPUsAaEu3hVH6bfxo2yp/wfVPBCmXd16NMs0ii30/8WvU2NVApSUpahIUzIKESuTS3gnquuxsl3d7EUTZOBwYAUjVmPjIm94d43tuTwr3WTkgM6Rmtj9D97LHewkjlmmqjDeG0l0OCObqecyMnOSzjCzUtILkgUcubMNUzpxRv8VHODMJ7buTtEAwuqFPSZapWUkGNZSP0jORzJaa2yOhXDJ6vn2k7g0hi3JnDJfMqMHo5suI9UErsevrVv87GDDtnKMelqvAVoZs+oTj3xum3JDPE7pmYjA7VfxsGMm9IyTOIjw6xrTt44Z45zi9a+TLxykiPVMMuUcCuPre8qi/9jyEtM4MBttYVI/5kAWYFwM/a5nKv+I4zSP4gc7TMk3cwfqG+GNMVgcDEXuTRBjHjT36w+7S6L+fJiIF592kVx5xYAhk6hAozv1tevbvsHV0BpVNE6rwjzyqAZE+GzG/WYk0fpygJhTGZjn0sUZvQtu5dy41+Ylkih+AZFoAnrIZwXygJb5EVRvIbU1q0EXdyXK3RowqSjuZchFpVhfI0qqFbUCYWJCaB+4ydOpJN99fJvxeVfYj4OgvmYrNHt0ofSGA2m64vcw6wufBZhlBa9geP5A1BeXyhu9BWJaaG5xnTgJ06KbJrp4ienwKWGHbPuci4V/ldK8PN9tpT53Ea+NiiPQrHr+hC0/Ss3YU0jnKZGB7KNZtT19B56KvBhpWSCWvt9kxXsbu+Jz2sl6LoqeMzuX5g1zg1iXwRbFLP2LwQTOaadQuMPegiQ4xLYhCyJkgZwFps9ebmNA0KPRojMJJaYdNr9i1l6gLCaYVhhryi3qOvf+SJuQhm6Pz9p31zbbE5k8S7kOXDg6hTWToSW8Zv3/l0WkH1xaKRFYnfn9EP4oRw7jlo1SHLiGMV7LuYrliekZCfwD4nNqhg7yF4s6kHTcChxaifDxD3tFumMxBaI6N0Kx0m+rJV3pEMrHUVNestWpVsbHxWjUgMJnJy2J4leN7+1X7Wn6a3lNYWFtfpOcs96lwyJn6zj05OCuLF/03PFz4aPfbnJXx1wW0Z349QGd0FOV150lgrq82/za5Y6OaW93+bGdnx4W9sZOdiJyKmmbIT3vnh3lBaQ/8dr7sQkRERb1uiEmgMGd7fmWAFEcDsFpD1S57W7a/LiQbPYACcp2OunWPkU0w1QseFpVgZsmeKFxaZPeW0N+3rVzj4azgDzwtvq/0pwMsULZ3QLANqda8kjPHaCDdAjkzgV0kXOZ02djIfxfvNp6ayND2bjVWgTPSmHQ+go4JkuDv297rymxyVeSoE1VmXuxIUMy9QS9k9Hj0YGC9WSnRmUu5ujbd+FPMDA9XBcSRfef1LNrr2kdCh1FYfrBrtVE+WZD0IgST3nNFLsDD4coaBU4A4Y03kggIqWzp5F4ao7F8aOFBYTcIbiYEyqvNsV79yjQaCPMtY3khDTWPQbeLf7Ji9FVtA9KReyIXRXfBWhZ9P/SpI74m8zfV8LRKc7dH1poyfOHDVlzLWzQbU09ecMDJgIyW6ZaLIVmlsPDrHFoiQ0eSTU4ZNh5lwfbsmw0o4CL1GZlqN/N+2YQCOHq/rjFgnhZ6vKsg2+E1r4SyOrWSyvA1kKOMimi/RH5JIGJeUHOmG6K6Q1mYQGnqJ3ZNDni0yItaqeANRpoHOZ6r+JiDuOJCcySjWaHaEb/j8yOCcN9jxr/jj1I6K8OY8hpubD1EGlwB4XYolVRZ804rnBRqsmurz4CcmZWma+0aL+cTcjuSynFo5cJbgKdlNl93QxtP7lOHWUA9YUcmExYrfZFatF+x15WoZoUuCbdzPVL4PAidQgeuSkQGJ/5oB7+xXuxNPPuEp321jY7JeuNpJoptVp2zfO8F3GeMkFZRagPS7iZovFvV5lCL87rwfB+7wlBRYV6T6lMg8dWQgz4cLK0UTPTFDl1vGzLiS9SFiwRLQajWYdo0VUaSTJ1b1Mfm1AH+91FLKxECMtf5yvPWGp3NeYf3NPv5rfSFtm0ncZhOpTY4AftfYCtXyhbq/kZ+05gLk3DcR82Xgy34sgO5WLtONX9HHSuITJQKCCdKmNmaz3/ow53UoErKpLIgetL0bRMs4TDpYMc5ERYO4nPvo9G9o9QHxK+H2JTEQb/EXioiSAkj7cUcxhBAvoZAJnzFLLL9L5C6beSR8sHIzMDxM/DYGdv0FPqxwbCfM0f2RXemwfmvpnCAid5Rqsa3tZ/h2iQtlvhST7gdrXR9T/TzM9DhcFywHLoday8K/FA4jd99TWp0PY5w9aD5HlfIjyJuXP6AD3dv0yVkoBDEUYU7ur3Y1OZAdrGEu9GVk2ORq72fap34EiIZGJ1yInONdKCdV/2FtyJHQJNNV+buEqQuvdHyOM16MFXSTXo8Mrd0hTDgJ9/ZEpULmUZ0MVGv/K3N3Lwcy22exFLWbNkE9icZ2ELnm0mD+VVwpACq/zgNXehul8WPuYqhA8t+zOhjNbvHIXWx7z9BZweJONdsmG1MVlk8JsRGHtapfQItxYsVDtcVSYnVfup3b4BoXNB4vcUK6h/sm3EEeF4JnTGrRZbQFWwLYOD8OC1WjQ6iu3QKeNcQ5yCcaeLtqbzlmm5xA6QyQjOGuu9rEMqo+QCnp6D5KAtaHc9ARLj+5L3wN8OziqaATPI6aY9mLssLPw025FkX6IXDXbG9YB9zg0ciOnVibBODVpkuuVFQBmsd0l1wjRFOUpvmD7qY+lN9VOjCVclH988COsn12sflQhAnst+7UXMXwtFIHxSboGp7dgoagW6jg0hbcrfmCr3FshtqKRGFVl38/6HGN+PrQ/HtTk/DOtJEM6SrMgbVgU8swY7iox31m0ODKRBHuZP8tArTA6zx3MkPwu6+WcQeOFU4oOtaD/Yrmda/h9Ysn/g6CasLvjvmCEBU00cbS3TXMOzWAvo45H8Q84+J+EFt/y9kRjhnxnUqPGy4XQlUliteJmnXi4wLhCx17GMO605UWr64EmmOPrjQaoPOLr9H6Isefv0gPL7oF6wJPKoZfFitcw64Id26X0GfFv5rXQveQcIsM6/DKtepaysQidqUPHspWCrAMCeZQpTOTZ3NRFCE6eAs0yadySI8YJuifmggyYSwPX9NsPYVo3nmc4jLxm4WvRLa3OiXzowAJC8ftU6rOCEReD7YMKteOmXw/hZymLTMxwPTDDBMFKZZdPozbNq2I3rTNOLzOf1hCk7Y6v8zTpJdczUyNtAaWexD+I/nTW4JMZ9FNiGkVJN4zhtsA6ftRC5HJYOQUp2788B9dgmMJ0FIwipxoXd90CtCjgtIfUMK992eRMPWNNBqqLX5cITyGDEJqySLPIviWlIKSLIlqtO7iAd8EyBe7OH0f3O4gmu9lyBxgf6/1U7BhH5myfocmdlSrGe6cLDX7iKXlzN3vVk1PljoqMNOYv/1y0NXawTY5qhu9INM8RR1YPHZm/OAou+z8fI81LmjpD4QDdY8Cjav5TXc2+IbplK8jHpiHwPtltpnwwtYXcvIt8RXc0usBaFIBdR2PCrw/m3D8llFyT77J5MhjeU2wgZRcCEO0ZtW1O1v1b1NpUPwl3JXoxoiDtZhGtSzkMJbtZup9iSoOr8855/Ag+Gbp+FumyQdxhICXLBdvtxq9hZMrvcoBoUo17+22FSkEfZRO3SyUt33wO9STAYms64SRXH5sIDy45Msgxskxv3d2EhFBPkjelBB/J98UrlfPbdk9drRdG6+McNmwDX9ho/1qgvaBNn5tHJyLJXOoIeR/eIaVwrbSfyErvOyPEPGvdHpJ6zDu/ziPORChCHd/KQVCDo3Q4iFpKnZULFhLFm8H4sMZPIvTGNn1akMWsYzhg1NRPRpyB8OU4Ulq5uQKgrE/vvMFUXgsg8KV0W+53ZmCdTwopRFEvHH0vOHoGXr1RsD/WgXTZcAIpAzS4gNwm7e807vWEw+4BX3gtHSs6fFYd6tkiMM591Ak/hCMpjcSNjWoNyIssToZMrawHxemZ0i6MRUshU65gwz6rwLsc1oUNZCI50FrRBt6qea4FqzrVT7RAXqorwrkoKgKN8EiQlkdddLggl9j5ixOjqwRtNNRsZ/J4TuR5nmCQ/+047LbmqSeCoSl4EqVM3lBm9OzMOLIkDCfv61mQiaVhRv/8D5tNJjFmQzDC2O0EgMbpmqJ8hfLhDCCw7U+OhtOa8PfSiRClVQHRLilXKxZEkYq6rF18/lxcvbu7jVa3Z/TArr9wTn0uO/SLozueUlP707UiHB/7BlYjookSXzivwFTiXg0GEFYLPpLf2sWHHPFnrC7i5MWoiAZRxymtOTkm5VxqeylKGk1YkttypCpCZGRDH3ZylI1HkDE5GqMeG5hyBJdOIxd0nZXm/PUbjVLZf7TUnEwo2UcHgLjBUVZEy8gDpLVvIHwhXTcJV6BmM5iubsfkKqUt7TB/X14/i20bulM9nnpFyHnoRNvgywiekBMDqPNSoaGKQJP1cEvcumP5fJCxkKeI4DWUgCabNbJfW4CaFri4YL9d8/t+0DwAZ7XTLzKb8lCi2Ydg+dnlNkqzYONjUUzZYlBGsW2b02vsCxgbVZBXuqmIoo5VieBI1H5B6GR3RG1zAYPzhrtATKQ2qi+GA6YkSM0CMHiQKrwr6w0cKWDOy+0U6U2PpQQeZw1Q4eMn8/WS4WLhMlYzo7XZ0uyUJHHM9qC1lshFxF6KEBKxR7hlwzc/5sU6svgOuZaxHtVwijxi9Bc5bTHN0VCPokWYsCRU/JJV+PhQbD+sbap5msAr/33tzjrWZ4z4z2NKxazEMlGFF9oZQNfT92dxchrjkP5uQxFCvc2RiCVLOZPUAbRygO7hsHEkAKuOsZJ2Zf/2CNXlGgQq58axqT0i3HbunKR3BYouAXk4jbKbuxctr46TFE9WqhUtJV2b8WhysjQRtndCWBUOW+aFqaLNgOUDPbURatni+hfk42tzMbFh7MJAK2QRzooGUqeFJLDnPsLoXc1X7bejsmjjzwlN+GCsVjdF86JBkGvIpaBLTiTO+ipT+s/FDNoHd+PO1rzAgY6JnUEeBJTz/1Mi+J0ie8WtkWJ5PVkuGwqnIiac4yr1HedVgCZ03fuc2F026CNiJtCroyQTzq9UaqSvR/1qShTOGCBi7Jqu0YKhU6X2oaa1kWqvKKSITwRs3s7OJlXv5eF+R359f4CK3H3h3A7Ncx61XoImKro/aH9sbIOsLFSoWWn883vC2Zpn/vTVstqSSc/qSrDmn6E6x5rnyNOKnFcjTRMvQZpFOmOZY1Wwp6vPjtM+keoKp/+EmDbzlmK5VX7x61M5+wMWrIAKr/+M0uY6FENLqZ2MWFamrYw1uYq6q1cb173jadQOiJ676MXuAlHb1SlF9+RQ7HCyOj+z2jqlaxp3IHrjYEOPqqH8DunUDdkPTTEMD1jBeJiaE+2lq0EosNWuVizZ6U/fpS6nf6rn339J4SaFg=="}'; + let password = 'testtest12'; + + let obj = { + success (result, obj) {}, + error (e) { + console.log('decryptWallet error:'); + console.log(e); + } + }; + + spyOn(obj, 'success'); + + WalletCrypto.decryptWallet(data, password, obj.success, obj.error); + + expect(obj.success).toHaveBeenCalled(); + expect(obj.success.calls.argsFor(0)[0].guid).toBe('cc90a34d-9eeb-49e7-95ef-9741b77de443'); + }); + }); + + describe('decryptWalletSync()', () => { + describe('wallet v3', () => + walletData.v3.forEach(({guid, enc, password}) => it(`should decrypt ${guid}`, () => { + let dec = WalletCrypto.decryptWalletSync(enc, password); + expect(dec.guid).toEqual(guid); + }) + ) + ); + + describe('legacy wallet v2', () => + walletData.v2.forEach(({guid, enc, password}) => it(`should decrypt ${guid}`, () => { + let dec = WalletCrypto.decryptWalletSync(enc, password); + expect(dec.guid).toEqual(guid); + }) + ) + ); + + describe('legacy wallet v1', () => + walletData.v1.forEach(wallet => + it(`should decrypt ${wallet.mode}, ${wallet.padding}, ${wallet.iterations} iterations`, () => { + let dec = WalletCrypto.decryptWalletSync(wallet.enc, wallet.password); + expect(dec.guid).toEqual(wallet.guid); + }) + ) + ); + + describe('non-existing wallet v4', () => + walletData.v4.forEach(wallet => + it(`should not decrypt ${wallet.mode}, ${wallet.padding}, ${wallet.iterations} iterations`, () => { + let observers = { + success () {}, + error () {} + }; + + spyOn(observers, 'success').and.callThrough(); + spyOn(observers, 'error').and.callThrough(); + + expect(() => WalletCrypto.decryptWalletSync(wallet.enc, wallet.password)).toThrow(Error('Wallet version 4 not supported.')); + WalletCrypto.decryptWallet(wallet.enc, wallet.password, observers.success, observers.error); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }) + ) + ); + }); + + describe('encryptWallet()', () => { + let v3 = walletData.v3[0]; + + it('should encrypt a v3 wallet', () => { + spyOn(Crypto, 'randomBytes').and.callFake(bytes => { + let salt = new Buffer(v3.iv, 'hex'); + let padding = new Buffer(v3.pad, 'hex'); + return bytes === 16 ? salt : padding; + }); + let enc = WalletCrypto.encryptWallet(JSON.stringify(v3.data), v3.password, v3.iterations, 3); + expect(enc).toEqual(v3.enc); + }); + }); + + describe('aes-256', () => { + let vectors = require('./data/aes-256-vectors'); + + ['cbc', 'ofb', 'ecb'].forEach(mode => + + describe(`${mode}`, () => { + let key = new Buffer(vectors[mode].key, 'hex'); + + let opts = { + mode: WalletCrypto.AES[mode.toUpperCase()], + padding: WalletCrypto.pad.NoPadding + }; + + return vectors[mode].tests.forEach(caseData => { + let enc; + + let iv = caseData.iv ? new Buffer(caseData.iv, 'hex') : null; + let testvector = new Buffer(caseData.testvector, 'hex'); + let ciphertext = new Buffer(caseData.ciphertext, 'hex'); + + it(`should encrypt ${caseData.testvector}`, () => { + enc = WalletCrypto.AES.encrypt(testvector, key, iv, opts); + expect(enc.compare(ciphertext)).toEqual(0); + }); + + it(`should decrypt ${caseData.testvector}`, () => { + let dec = WalletCrypto.AES.decrypt(enc, key, iv, opts); + expect(dec.compare(testvector)).toEqual(0); + }); + }); + }) + ); + + it('should use CBC if no mode is given', () => { + let key = new Buffer(vectors['cbc'].key, 'hex'); + + let opts = + {padding: WalletCrypto.pad.NoPadding}; + + vectors['cbc'].tests.forEach(caseData => { + let iv = caseData.iv ? new Buffer(caseData.iv, 'hex') : null; + let testvector = new Buffer(caseData.testvector, 'hex'); + let ciphertext = new Buffer(caseData.ciphertext, 'hex'); + + let enc = WalletCrypto.AES.encrypt(testvector, key, iv, opts); + expect(enc.compare(ciphertext)).toEqual(0); + + let dec = WalletCrypto.AES.decrypt(enc, key, iv, opts); + expect(dec.compare(testvector)).toEqual(0); + }); + }); + }); + + describe('padding', () => { + let BLOCK_SIZE_BYTES = 16; + let { pad } = WalletCrypto; + let input = new Buffer(10).fill(0xff); + + describe('NoPadding', () => { + it('should not add bytes when padding', () => { + let output = pad.NoPadding.pad(input, BLOCK_SIZE_BYTES); + expect(output.compare(input)).toEqual(0); + }); + + it('should not remove bytes when unpadding', () => { + let output = pad.NoPadding.unpad(input); + expect(output.compare(input)).toEqual(0); + }); + }); + + describe('ZeroPadding', () => { + it('should fill the remaining block space with 0x00 bytes', () => { + let output = pad.ZeroPadding.pad(input, BLOCK_SIZE_BYTES); + expect(output.length).toEqual(BLOCK_SIZE_BYTES); + expect(output.toString('hex').match(/(00)+$/)[0].length / 2).toEqual(6); + }); + + it('should remove all trailing 0x00 bytes when unpadding', () => { + let padded = Buffer.concat([ input, new Buffer(6).fill(0x00) ]); + let output = pad.ZeroPadding.unpad(padded); + expect(output.length).toEqual(10); + }); + + it('should unpad a ZeroPadding padded buffer', () => { + let output = pad.ZeroPadding.unpad(pad.ZeroPadding.pad(input, BLOCK_SIZE_BYTES)); + expect(output.compare(input)).toEqual(0); + }); + }); + + describe('Iso10126', () => { + it('should set the last byte to the padding length', () => { + let output = pad.Iso10126.pad(input, BLOCK_SIZE_BYTES); + expect(output[output.length - 1]).toEqual(0x06); + }); + + it('should pad using random bytes', () => { + spyOn(Crypto, 'randomBytes').and.callThrough(); + pad.Iso10126.pad(input, BLOCK_SIZE_BYTES); + expect(Crypto.randomBytes).toHaveBeenCalledWith(5); + }); + + it('should unpad based on the last byte', () => { + let padded = new Buffer(BLOCK_SIZE_BYTES); + padded[padded.length - 1] = 0x07; + let output = pad.Iso10126.unpad(padded); + expect(output.length).toEqual(9); + }); + + it('should unpad an Iso10126 padded buffer', () => { + let output = pad.Iso97971.unpad(pad.Iso97971.pad(input, BLOCK_SIZE_BYTES)); + expect(output.compare(input)).toEqual(0); + }); + }); + + describe('Iso97971', () => { + it('should set the first padding byte to 0x80', () => { + let output = pad.Iso97971.pad(input, BLOCK_SIZE_BYTES); + expect(output[input.length]).toEqual(0x80); + }); + + it('should pad the rest with 0x00 bytes', () => { + let output = pad.Iso97971.pad(input, BLOCK_SIZE_BYTES); + expect(output.toString('hex').match(/(00)+$/)[0].length / 2).toEqual(5); + }); + + it('should unpad an Iso97971 padded buffer', () => { + let output = pad.Iso97971.unpad(pad.Iso97971.pad(input, BLOCK_SIZE_BYTES)); + expect(output.compare(input)).toEqual(0); + }); + }); + }); + + describe('cipherFunction', () => { + it('should not modify the message is all parameters are falsy', () => expect(WalletCrypto.cipherFunction()('toto')).toEqual('toto')); + + it('should not modify the operation is unknown', () => expect(WalletCrypto.cipherFunction('password', 'key', 1000, 'nop')('toto')).toEqual('toto')); + }); + + describe('scrypt', () => { + let observer = + {callback (hash) {}}; + + beforeEach(() => { + // overrride as a temporary solution + window.setTimeout = myFunction => myFunction(); + }); + + // Crypto_scrypt test vectors can be found at the end of this document: + // # http://www.tarsnap.com/scrypt/scrypt.pdf + + it('Official test vector 1 should work', () => { + spyOn(observer, 'callback'); + let expected = '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906'; + WalletCrypto.scrypt('', '', 16, 1, 1, 64, observer.callback); + expect(observer.callback).toHaveBeenCalled(); + let computed = observer.callback.calls.argsFor(0)[0].toString('hex'); + expect(expected).toEqual(computed); + }); + + // Not using official test vectors 2-4, because they are too slow. Using + // Haskell generated test vectors below instead. + + // Disabled because it is too slow + // it "Official test vector 2 should work", -> + // spyOn(observer, "callback") + // expected = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731\ + // 622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640" + // ImportExport.Crypto_scrypt "password", "NaCl" , 1024, 8, 16, 64, observer.callback + // expect(observer.callback).toHaveBeenCalled() + // computed = observer.callback.calls.argsFor(0)[0].toString("hex") + // expect(expected).toEqual(computed) + + // Disabled because it is too slow + // it "Official test vector 3 should work", -> + // spyOn(observer, "callback") + // expected = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2\ + // d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887" + // ImportExport.Crypto_scrypt "pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, observer.callback + // expect(observer.callback).toHaveBeenCalled() + // computed = observer.callback.calls.argsFor(0)[0].toString("hex") + // expect(expected).toEqual(computed) + + // Disabled because it is too slow and PhantomJS runs out of memory + // it "Official test vector 4 should work", -> + // spyOn(observer, "callback") + // expected = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa47\ + // 8e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4" + // ImportExport.Crypto_scrypt "pleaseletmein", "SodiumChloride" , 1048576, 8, 1, 64, observer.callback + // expect(observer.callback).toHaveBeenCalled() + // computed = observer.callback.calls.argsFor(0)[0].toString("hex") + // expect(expected).toEqual(computed) + + // The next test vectors for crypto scrypt have been generated using this lib: + // # https://hackage.haskell.org/package/scrypt-0.3.2/docs/Crypto-Scrypt.html + + it('haskell generated test vector 1 should work', () => { + spyOn(observer, 'callback'); + let expected = '53019da47bc9fbdc4f719183e08d149bc1cd6b5bf3ab24df8a7c69daed193c692d0d56d4c2af3ce3f98a317671bdb40afb15aaf4f08146cffbc4ccdd66817402'; + WalletCrypto.scrypt('suchCrypto', 'soSalty', 16, 8, 1, 64, observer.callback); + expect(observer.callback).toHaveBeenCalled(); + let computed = observer.callback.calls.argsFor(0)[0].toString('hex'); + expect(expected).toEqual(computed); + }); + + it('haskell generated test vector 2 should work', () => { + spyOn(observer, 'callback'); + let expected = '56f5f2c4809f3ab95ecc334e64450392bf6f1f7187653b1ba920f39b4c44b2d6b47a243c70b2c3444bc31cfec9c57893dd39fa0688bd8a5d1cdcbe08b17b432b'; + WalletCrypto.scrypt('ΜΟΛΩΝ', 'ΛΑΒΕ', 32, 4, 4, 64, observer.callback); + expect(observer.callback).toHaveBeenCalled(); + let computed = observer.callback.calls.argsFor(0)[0].toString('hex'); + expect(expected).toEqual(computed); + }); + + it('haskell generated test vector 3 should work', () => { + spyOn(observer, 'callback'); + let expected = 'f890a6beae1dc3f627f9d9bcca8a96950b11758beb1edf1b072c8b8522d155629db68aba34619e1ae45b4b6b2917bcb8fd1698b536124df69d5c36d7f28fbe0e'; + WalletCrypto.scrypt('ϓ␀𐐀💩', 'ϓ␀𐐀💩', 64, 2, 2, 64, observer.callback); + expect(observer.callback).toHaveBeenCalled(); + let computed = observer.callback.calls.argsFor(0)[0].toString('hex'); + expect(expected).toEqual(computed); + }); + }); +}); diff --git a/tests/wallet_crypto_spec.js.coffee b/tests/wallet_crypto_spec.js.coffee deleted file mode 100644 index f5b36af89..000000000 --- a/tests/wallet_crypto_spec.js.coffee +++ /dev/null @@ -1,493 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -Crypto = { -} - -stubs = { - 'crypto': Crypto, -} - -WalletCrypto = proxyquire('../src/wallet-crypto', stubs) - -describe 'WalletCrypto', -> - walletData = require('./data/wallet-data') - - describe "stretchPassword()", -> - it "should stretch a password", -> - password = "1234567890" - salt = 'a633e05b567f64482d7620170bd45201' - pbkdf2_iterations = 10 - - expect(WalletCrypto.stretchPassword(password, salt, pbkdf2_iterations).toString('hex')).toBe("4be158806522094dd184bc9c093ea185c6a4ec003bdc6323108e3f5eeb7e388d") - - describe "decryptPasswordWithProcessedPin()", -> - it "should return the password", -> - data = 'pKPZ1y8lM9aixrcaW3VGHdZkQL37ESyFQvgxVSIjhEo=' - password = '60ae40d723196edea0ae35ace25db8961905dd8582ae813801c7494e71173925' - pbkdf2_iterations = 1 - - decrypted_password = WalletCrypto.decryptPasswordWithProcessedPin(data, password, pbkdf2_iterations) - - expect(decrypted_password).toBe('testtest12') - - describe "encrypt", -> - beforeEach -> - spyOn(WalletCrypto.Buffer, "concat").and.callFake((array) -> - res = array.join('|') - { - toString: (type) -> "#{ res }|#{ type }" - } - ) - spyOn(Crypto, "randomBytes").and.callFake(() -> - "random-bytes" - ) - - data = JSON.stringify({hello: "world"}) - - beforeEach -> - spyOn(WalletCrypto.AES, "encrypt").and.callFake((dataBytes, key, salt, options) -> - "#{ dataBytes }|encrypted-with-#{ salt }+#{ key }" - ) - - describe "encryptDataWithKey()", -> - it "should take JSON string and return Base64 concatenated IV + payload", -> - res = WalletCrypto.encryptDataWithKey(data, "aes-256-key", "random") - - expect(res).toEqual("random|#{ data }|encrypted-with-random+aes-256-key|base64") - - it "should generate an IV if not provided", -> - res = WalletCrypto.encryptDataWithKey(data, "aes-256-key") - - expect(res).toEqual("random-bytes|#{ data }|encrypted-with-random-bytes+aes-256-key|base64") - - describe "encryptDataWithPassword()", -> - pending() - - describe "decrypt", -> - data = JSON.stringify({hello: "world"}) - - beforeEach -> - spyOn(WalletCrypto.AES, "decrypt").and.callFake((payload, key, iv, options) -> - payloadComponents = payload.split("|") - cryptoComponents = payloadComponents[1].replace('encrypted-with-','').split("+") - if cryptoComponents[0] != key - throw new Error('Wrong AES key') - if cryptoComponents[1] != iv - throw new Error('Wrong AES initialization vector') - res = payloadComponents[0] - { - toString: () -> res - } - ) - - describe "decryptBufferWithKey()", -> - it "should decrypt when given an IV, payload and key", -> - res = WalletCrypto.decryptBufferWithKey( - "#{ data }|encrypted-with-aes-256-key+random", - 'random', - 'aes-256-key' - ) - expect(res).toEqual(data) - - describe "decryptDataWithKey()", -> - it "should decode Base64, extract IV and call decryptBufferWithKey()", -> - spyOn(WalletCrypto, "decryptBufferWithKey") - - key = 'c9a429594db2ae5b3df33aa651825073114dc59df230fbf74aeccab2cd99bd59' - - res = WalletCrypto.decryptDataWithKey( - 'bvOk5Ppz9rPDLwlR1wObQoyPVybs4h+fg0eynTe4u/CePkZOPFvobnTpzGGX10CY', - Buffer(key, 'hex') - ) - - expect(WalletCrypto.decryptBufferWithKey).toHaveBeenCalled() - expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[0].toString('hex')).toEqual( - '8c8f5726ece21f9f8347b29d37b8bbf09e3e464e3c5be86e74e9cc6197d74098' # Payload - ) - expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[1].toString('hex')).toEqual( - '6ef3a4e4fa73f6b3c32f0951d7039b42' # IV - ) - expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[2].toString('hex')).toEqual( - key # key - ) - - - describe "decryptDataWithPassword()", -> # decrypt() - it "should decode Base64, extract IV, stretch pwd and call decryptBufferWithKey()", -> - data = "zKHay4ml1+lRidNioY/Da7R5E6ERiwU7bDYsSS2UUH0=" - spyOn(WalletCrypto, "stretchPassword").and.returnValue Buffer('b484f16b5980e877439f5c65aba87572176548280feda10b730e978911dcc825', 'hex') - spyOn(WalletCrypto, "decryptBufferWithKey").and.returnValue 'test' - - res = WalletCrypto.decrypt(data, "1234", 10) - - expect(WalletCrypto.stretchPassword).toHaveBeenCalledWith( - '1234', - jasmine.anything() - 10, - 256 - ) - expect(WalletCrypto.stretchPassword.calls.argsFor(0)[1].toString('hex')).toEqual( - 'cca1dacb89a5d7e95189d362a18fc36b' # salt - ) - expect(WalletCrypto.decryptBufferWithKey).toHaveBeenCalled() - expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[0].toString('hex')).toEqual( - 'b47913a1118b053b6c362c492d94507d' # Encrypted payload - ) - - expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[1].toString('hex')).toEqual( - 'cca1dacb89a5d7e95189d362a18fc36b' - ) # IV is same as salt used for password stretching - - expect(WalletCrypto.decryptBufferWithKey.calls.argsFor(0)[2].toString('hex')).toEqual( - 'b484f16b5980e877439f5c65aba87572176548280feda10b730e978911dcc825' - ) # The key returned by strechPassword() - - expect(res).toEqual("test") - - describe "pairing code", -> - it "should decrypt", -> - - payload = "qf7x+dZBbUQnzgbL6SJfI8w/nracmNe4YiGIww+J40V42u4T8SgyjfJdIqPsKkQC7m0UMpQ4OpvkBl2H4NIolJizO/N0zauRnkaSeqK32rY=" - encryption_phrase = "efd93e61900251ab6cd19a4f14e5a2229866e150b04f40fe7912f392c24b5a7d" - guid = "6f209355-9e4b-4708-9d64-272d76bb8062" - - decrypted = WalletCrypto.decrypt(payload, encryption_phrase, 10) - components = decrypted.split('|') - expect(components[0]).toBe(guid) - - describe "decryptWallet", -> - - it "should decrypt a legacy v1 wallet with CBC, ISO10126, 10 iterations", -> - data = 'OPWBr1rsrvGsbNpIidlztqc0YsPwS0gg51rz6gWlrsJzY+VidziSekuiy7AcxVF42sMcJp9XD41xPsmq0m9yEWrFw6QufwLjSWNE4IK8mD6jIYH35a7fWKbK0LXGq4UIHCfM2W8WVoz/l0QO+JrGrqC3gg8qGyHP3NsVZKVAqG6cGmBi9WEs688U5B0NNGPXPLKE1ZXzHbSd6Pdub1xWv/BEo4RsAu1NySQJpcq3hqo9nLMsza9aiwKH5rG1aMUDu50LNtGs3vCx8ZAkcZpYVp1ZLeoD3pnZVc7siq3kiqJ7zDQoE3FORgD6PuAc6YB2PXW6I3ubw4hkvFMnkIK4/Cc/AEB8RYar6rjmgPVYXSm+ok39sPi9ppIE23k4LkFzz3dUbTM2ub1kKPCUoJLp2E4tUg4hqRidaC7rNxkPyI3lyBWrS8JD457pFYlTWYsUtU1P2sHhxKZuKdeDPQ/Jvo0y+xO5rK9OgmKCg0qxuwaXf4NYu6laqaGEQywRmRyhT1f3E0pQZ371dObo4FOdiVEODhvadPf0FCHjOtuaxWwEkFwyFHVtc0lVNhcy0rg65j2efHpDUXqQnqFBgc2PG23BVI1gY0JIDT5zp33wdFX3r6MjYxSUV7KbRBDwCD0Q3a9NepX9bqv3wpi1qYJ6kcht0iMAE+5WmHHeHWza2HFMXcUApSAU6cu2fzOfK+4gRMJPjNAdk/nVQT1UnWy9k+s6jvwaXBPI10ewaTz5ayRTXyku36N1xLsM6DnBPoJCunuDEXMI5dILgr3BVSCqvzboWsRW04bAfFbpYANioQgdDD5zerygHa61V7ICyD/x5G4li6VLIefsCGGBo+7fU149zYkHv7ruH8F/J26b11UH+gpThimLgenJectT3MnksMFaz8LiSRn6jnz7CMeXssxBoYRT0gvq4MN6JxFGD01HfcqfVuBkWXk8Mo1OE75v3HWovrJOrTXhYbr+JaPppA==' - password = 'testpassword' - - obj = - success: (result, obj) -> - error: (e) -> - console.log("decryptWallet error:") - console.log(e) - - spyOn(obj, "success") - - WalletCrypto.decryptWallet(data, password, obj.success, obj.error) - expect(obj.success).toHaveBeenCalled() - expect(obj.success.calls.argsFor(0)[0].guid).toBe("6253e902-ce79-4027-bdc4-af51ed970eb5") - - - it "should decrypt a legacy v1 wallet with OFB, nopad, 1 iteration", -> - data = '1LbdcPCYTFMr3Yv27kaJ/kqKflbhLMsQJXAL4EyWOzTKiDHmVrO6JTInh0GzXwTcDSJJQw4G614LT7YyfaZLd3+shhFDQycvSLKrnC4wM40CcIE1Ow335YlolV6+WAJAAQ951ipS2cUERXSpxbbDy41AbUG+wIX/R2wCbiosT6OLHjnMSuQWx5CyEB1ZqRUXfc3TYwbh1iCJdBDaPFnmTPN/LpRegC99DFEJ0E94dPim42Q/8KV4mDNvCOUGjLfqsi/3I+M6kGJtzC5CUo0dVZ0bzzTTXEXm7Ga0eo3arCdMQUDPVGcbJbGK8qTcWcHXHw90EJEGBUi6i4p1XXw+26T5k+Qs6Fl7GD3PHl5Urusk4twB5DM/Hwp5vFWnsCsbM78CRhJl3rbheBMx6t1RJKvxpOUrh/OyvOwhWtU6hRXmS/zrwwDqbWiVG9/LzZP19hnAvlTJ0o1jF/Dmz+EoTaotkVHFZDUuYDKfWPY/+K7sQGkagfdv5W1X9rfary22prDUo8kFiRvNHvGMie3TiM1ucSdpBj1DpgfwCxnWzLIeoRl8ZJFUN/V8CkGjpPoyBYZFfjCeGq9+ET0wXL4s9qfNXaHfx2oEuiwcLcDDf71us655hIW6t/OPaPZ3WPhnW4KqrTtL4xduYrisYbhoZkmqJe5Jl110jhiKdNYdvmOcFo764RDPcu2Oa1bszYhGTbRPc45kLauJUAW3e/UUbA87RpAqFnOLH/0vcNMJvcKbvID9A1RSZNjmlerSOptab+yYI97D6EitvFDx9tFMmUrfWFRGs5bOrjiAHobcCqpJ60AivYHPkkkaz8gROz30aqa9Dpz15wplbOVIpKVqf8hly8Bghj5QK4+m2Nf2qALDdBfX+1ZIUh23kiy0s3duWoJQttIyK0AeEU8oHa0MRP5T/ikMatVvw7GVmcDzO8pFBdBxssxReEhnXCpd4nmjcuWbroa+5oaCMWtzXH4I' - password = 'testpassword' - - obj = - success: (result, obj) -> - error: (e) -> - console.log("decryptWallet error:") - console.log(e) - - spyOn(obj, "success") - - WalletCrypto.decryptWallet(data, password, obj.success, obj.error) - expect(obj.success).toHaveBeenCalled() - expect(obj.success.calls.argsFor(0)[0].guid).toBe("6253e902-ce79-4027-bdc4-af51ed970eb5") - - - it "should decrypt a legacy v1 wallet with OFB, ISO7816, 1 iteration", -> - data = 'ag6Tn5xvl/9OwQ9uvRl1wcOBaYPsCHFEyv+RjSHwU+evHlsBvsWErhxRFWpaK8oqvy0ECH2F8IH8NWMwoXqGbmV7FNgaXiJMhNCtiJvRIv0oY9OoMvQajVvmmXO+WdepwPGVJ130xUz8aZ9raTUGJySKo5/Aq8zBybXqG9UdxSG6QKxRaQNyvh4nb/T5h9b4opwoMpX+Mqh4HRWcURTgQ+RgGC9wKqdrpCL5nka5RsbRj3eQEG6ZyOzq4ROCjE217RqZhS+n6qjw1lBPPwFREbbWZ2NVVvXRJP1tv+VfRCK69lqoHS6GoTrf4aZy35t/GCISEgzyLVCpcpERgC9uUKzSRoarltHd+LPV7RB6+qA1UxBvzbdcYplBO+fdcWHlrx3YES9lpSKB+B/+PP/aFOg+3z/cQbtkLJ5KkXwrrceKqylNznq9pq5u/a8vjTWkkddgWJzBeQD0FF9kj5zZCJXRb0BwoST7oT952FHz751egpnC5WbCLvM0UgnLH9jEcxkuc2vKpnAEEjyXvry/8WWrzGPsnJgEH6wGvxoocqYWcGIFBMj6iHstDMKa4BsaWvqMOUEyvYnGqvo29Xykzlvou3LUAfoz5xE32UKwZRLlsVeDZafQHk5V10yckRETJaA1P6ZGvpuHGIRsXGlpTZJMCz9XoJB7uXYeI66Rw5JYHgezVtrOTTFNUk/ZuPMty4rRAGIaNiFRQsJE5hY7SHbwvLyoY2xmnAl4J57J2mn9CNXYWMqK9WiF/xPTYU9YphHSlYWL2/pMZux0SqW6pSIoF6YoXKl7bv71bAf+tVYeysLLezVdGYi44ezdOQ12sH1ipTerk1LWJL10DVjh58uigHed+gceF2w1gjcUlU7Xsh4Z+m2UlIekSETmFW/uY2FMQz6oz3fikxwRdXY7gNY5VES9DNA34MV92wfMgRNJD1I9dWRMrPxhRoClZJA1uSBMg8n5jjnhhznTzCJ33g==' - password = 'testpassword' - - obj = - success: (result, obj) -> - error: (e) -> - console.log("decryptWallet error:") - console.log(e) - - spyOn(obj, "success") - - WalletCrypto.decryptWallet(data, password, obj.success, obj.error) - expect(obj.success).toHaveBeenCalled() - expect(obj.success.calls.argsFor(0)[0].guid).toBe("6253e902-ce79-4027-bdc4-af51ed970eb5") - - - it "should decrypt a legacy v1 wallet with CBC, ISO10126, 1 iteration", -> - data = 'BuAqphHuJDoKyTIHRXlzlm1ZMVo1NKM9l6fDFldPGOPTobqEG+9RjO9UHLcUO04HAbqogDZdoOxU2cyNM7bi7JYEZDTlfiIYldFkJUP+zGA4Wg+twCS+oOJrv4D8VoKaFl8oAnIns2n8o8MEQscxxkBzjNWoZ3yE6jG1AjtoYt8owrz97S8XFmnSaK3/Q3doTMKFPaRZ7NAR7z0gWI4KaHtfmHMYEsiGy33M4riq3DCyiGGezg+f2IlUUsRj/+VmxZd5KlCpA4ZIG3fPpp19BPj2S3yFdDqZtMjfggo570iHi7eMogFrZAo+yiLJlYSaoWT8fs8vTmnm8pxxRYb1601AmrBnNlPPpvGRNSBiV/rfKdaVqPYygonTBca5fLBA+QQQ8k5HJdiKRjgfJkZWpjexxdXTuLi/bRKYfKOp5GT+fDrgNAv/E56U4rpwpolAQYzJI8XGDF6CZCVJHWAIPqq8XO7sBBofRuGpUoJMZChDoSbVJV12ZYKSo52j/oo+G/e/Rylrydb2u2qte4RdvYQE+Fr9FIX8nR3JUJwXpXx3/rUImPJa+9P9+ySOjRo4zvmh04Zj+C6eGfsLO4GvpKe748d6WrQzTZwzcVmzCeFA0spcDmJaeQ6iB9HKh8yYo5YDWiEiuHw1CIxDb83wnlLepxmk0isEMCIXETNJ+8ns1263kBbYKbt9FyfB8gNrSHEM7tyVc5v03Z++c11+9dSONnLyQArQSLCvnktPQlKm2cb/rRfmbk8ASlCfZkkhn6TR7Ugcvgoj+Sh7fm9LWYISdPx3vmPriplQjvnYnrvBcXoNmJuFmNG6tBgCK8wdbtOCtFw4KS0e8nAOjZ4o0xfNuLv+TmMyyA9xAXRGDEPV+5Z//MAQJc69usAazTaojxicUh0RghUlQOpiqHW5Mbg0tUlkAkCJJYJKfu6elNDslw8l6NwjIvaR7rgdF6JGXbRrX9zl81dAYfyWxJg9Nw==' - password = 'testpassword' - - obj = - success: (result, obj) -> - error: (e) -> - console.log("decryptWallet error:") - console.log(e) - - spyOn(obj, "success") - - WalletCrypto.decryptWallet(data, password, obj.success, obj.error) - expect(obj.success).toHaveBeenCalled() - expect(obj.success.calls.argsFor(0)[0].guid).toBe("6253e902-ce79-4027-bdc4-af51ed970eb5") - - - it "should decrypt a test v2 wallet", -> - data = '{"pbkdf2_iterations":5000,"version":2,"payload":"b+OfaGwd/zcOsSy3BUXD6w6u18rVOiFFeSHLNCZYhgfoPGj3csxlYL4gAGYYBqQDT6BzWdfT3/7rq0Oe44mMdFfKP8TWyt41aPOXKZo5MgfBOtamvNMD7PtW5Qxap9VUemKCdWMe5IS7R80/md2E3WPxUlWIqW2mooOFFpfoicVMGnJormlUaiUIsG9hv43MtP3g18eYv3P3OE+2LGyjavmSTbmgLznUA8TcAJNNLwD8br9kBp/TTd1wTFNMwyjILlD20HgoX4S/922qhReR4dxTjJUjplE5LJVBkemwNDNqCXZVo94r3RizD7SBwXneawVGIQZYOZ0aBvXdEe2ooIoZ2fIGMsdJCMvzXBXZ85jdfRg0LFtB9+YaOwZsM3TlY6wr7hk3sSpTVtHf22+egIWQEa1rylt3ywK/ArdAq4ydpGOtjfjfZfiKazc0CYiqVxJ5HBD4NBfh1moLZnOcHnzM5EnW6jO9dVek579C4sh7s9kAztwCVX5r4GfSTovPvuwzcEVxUL4uEHd+zaczQHU8zWIGrXupWKHfkjeoWxDM0iW6edBWtv13/TJnjTkeJzraQS1iU4iQGkPplITqctHIgschR8Cj2HurUcW9Y/2C5nEdhfMjnTfCxzspLWObOCGWnLEMWOJhC4MPsUJy3p+R1JD7p1QlyRQ0I8vtn+FEuEUL4ui9Fe9zfRvd1xfiJ+0CBCAtRwzEaBqCnI9GciErdTysUw0lvcBKbPU05ajsSfbYIzow2O6SG/PCqIcUbePChuqVJBRPmK1erzC1XNt2HFRYKTqIWy/jtx4cKgXBtK750LX+67Wcvw/OjX3rthvfE4ZZYYl/smJUB32b2tfSF72+aSzmC9nz/fJTztNahQIPbU/vGE2R/R5CQZCjvQxvgsfXKVk9N0Av9gpH1w+i0BgAIwi5mBA+zZlgUteNcuEWkPgE4NpN+uue9mlq/t1rpySjOgt0D50gUmJMfQGgYUoN3OtyjGUt437Auk6Y37aHYXZekjDPCdgr7yYDvkmUVeB1f31swzh/nwz4lbw9ezSTHIYBMN4XW4xuG3vtefu6fIK0BDncXEE7d3BM9OnWzTBtJQVvWZW2+lslb/PjmEFTY/UoELKjpG4wTvm+LfI+Q2jlLT7VblZeUVdEeONhElkadl/SQQq2sRVbEHzyZhlEc3cbznT5BPeGEoIG70NOckCxl57F84VO17d9xvtMs5joaYB0nr2yRuBFU9/T1FOLiQKjOv/GCOLLL5IdOUlVHNlj17EQ7KtaghNTA7ndPoVp7pgc+Gu2zKn0yBVKzLTD8t7L429pCs9N0QsO00w+tVLZwH9l37rNkqFaW1BhOD+T4VnGdy1fYIeqKfyWpf9UExPaZniki7+syIPoQ7bc0ZDWzr7FcIuRsPzOu3koW7fOPxYYaIWHLGSvtnfsRVb029CoKsr/RQNA8zKN6SuYjguEdA63/GEUIIDX9QpAlyGAGFN2Dfm4DgKipDHgqiIykf/PldocnF2iS96l+JFHFwtnXBjNOVjhbmQDJOCK1IhZfjimqfRBmFATmPerpMqDllACinmu7WcEufEn/NdKRVagki0GEWhWbcg+LApuwTSG8K/6JR7dHjh1McuZ60PObVV6qSjvpvIW+THuQ52C5DUQz7ReLuc/BMe1zd/xFB982AikeTIDFQl03QJ7w5inVBrDUcJEbNt8Adg+byPozUDnw06+i705h5gZXuWioklrHbt7OlSHcWfWnZa7HqZHJ128mi80OzpK2qePp6/mYv/zbcjKPh47H9m2LoV2zeAzlwMsgQtp0wu8bXbietEB0KtvVTarU14iFOftwZnhgVv3oIUVZQ7zkua4J+eiMy6gDQNNUB+0t11F8w5c8Ivnp+rg1nD4RJaOHnIZ2IU9ie2PpaiuIiyyaX1drAzNOM5MU0dOrDBTIkreM8bZH8LllYnkwa62nlY4EjWtVivhDk9rB7bOUZMhQmHB9Y539EmHJxvqIborRDGDC/3AvuFSRZN1jug6KZhYjr8aG98HhN3cFw6M6DwDS8llhkfMdM1l/fFS3LjjuVz1yAOdI4GWVDKdjKKjW2rbwjnpgoK+plxN8KqM8JEt6z3gPJwvIhImysXjXNKxbjHWVKGAYD28iK6f4csk1NrgCHM12r8Lkwf0gXW1AVvxufYKzpa/9MrA6V7B+Y3fJ9ooDAlikVCFzVEFD1894hgWhOXPeZS5qOqnW4bvViGsi0dhQvWnztBu+PUN531oL/Ecx4q/U0gz6oxVJ2YtoHG3G/H8ZFdq4bMwXM1A3b7rd2sIdO6EY87g2PgpoPF7idnfQdxxu3lC6HjaMwbn8mxwwJSSoGO3QYmZWRhRf8wJteGm3ha5rDXS952WGBc1o7K2zx7kbjURR92pkdyPqSyUfwYovZXpzCawGlAqbIbRtcxxQdod4OBXCdqmCBklxjUb529/arFJYZXJJuxIY6bM630mriMvyYAuHkgIbsiTwyTcwJ15OsQxy95aNYiHU1HotJKJhNRdpzBvAKxSoGhD40vkM72EdpB6ymeAUeABRb3wvD1KY+a2GcPVSE7ZVqaNJ+hGOdDLq41+Kwh9/mR0zlmmIKKxwBX3IL9EX7eesKVOKH7JFMggLNi2fxuc46OCQVIws6p54jUj1ksce45EYE6syEZkB8Ekhk3V4s5DwBDXBuaHJa70AKRZPlG9QGXLeVmF/JS9PhcEA+3QpOb79+OZF9BBnz6hPPrttAPXGMqoNQvDySEV4l4tijrZJHe0YMOw9zoVQYA5hJW1+kS4lUq7SIC7gSN+vWAMBh6LFAity5JikUlXoYievM8zqySbr4xP9HI/zph3tHmONkfTBKCo/dkD1QaQfD37Qgqo40q4WDgbS78MMq0AeHU9WTVoa1IBkn5aSAelq3rTMVaKTzBOxKdc7EI+Pl1MNf8+0/ggNPC6InCpvOcNdbPuQxM3NLqMrZYNb5x+xoxOXrAvDH4wnF59JkvGhr6OOU4RNfv5YJZ/U3r9NyhvQtJExHKWKE6fxjDa3bruZ4UqQZXxwW5XgMS6+5QN4j1dOJhiL9CnWsNEQvSTa0rfZv3AcZs41hPrY8NXAueYZfjccec30lH7iGDgP2NOvd8tRbSO+X/I9OFxc6m2CoEoT4sniy6rBUCh19hJOt2P08x8U371wZkEG1yQzOF1mrmvlAT+aZP0WUS55JM1sqGdEHHTjJuv4h9NSCP8nURwj6y5hfqnUQPnofUTGvwV24E6+HvQTXoaLxks5k1Rw8moKfhsVEmdj2Arh9O21vwhEYc1+uWFNqT8s5f0OzQ9n0glGdg4SWFsZ+ttEyHaE5HTkuNm/dXtQi6GOcjZzR41+8gMh8O3KFhkZqxLfpdWSNeZ8GpY/LBvt1ZIXIIXjjybYDOz2Sx5ANssXt8nyk4nyjl2Ggv5BNzhbcQfstC6Pg0JVFhXtYizCqyZsjWhwaKynnP3UCqf5xJteuFwOyZyXpeEw/tdtQqtpw81T2iba3+69q7tkpv7FJtMraGfbD/BBWRAQ/W23AwREkru3Pm2sbfRSqnc84b3ZARUKpV6k8MEALWOcNFrP95uBQPHaBGZSQR4KZdlHvEjRN8WqZNgfH78lnUvWbKkdXGSrMeMrZVZwXI4fUlfexnqa/YeVmNewRDajuRTWnYMlyQgQae54FF+iz1NSGSNt6eruL4vgVFT3R09hxCDsqf5tNCDR88XC/DtDZvyPVs+e9Va/DA2x5lir6p/U9BKo5mNBG9UKvrOvep7Yu7Lfn2dN7cutDAY2hZRrKeVJxwXg1IlVJnsCDHockVfe4JSsAiUPZQQpPx7id5GbUOcXT9dtHgFxt5BpqzTldrbXYqvsit0kusUhkdJc5Q/+g9gIYqRNLLouNum0heZZ82dKfmhYwoED+hupYW1WidTIEkVcv3cDeYPPBdxcU7Zli1M2n7M+AbulhmZXxe+RDkbFvK7m8rzAJdOjI7Vxn3+8/8X3sQl1tpTWd9XLz+O/l+y58IZI6jpCSZGRLfJpjNJ4wlJs3Vr3aTmP0m4ayyz28lIItH46URrSVc/TUQehaVw09ouMlMN/HOXc7nvPL+2Ll161h6reVLuo6FOfrIWHGYNtklB3gKbEYeLEUFaIgW5am/A+mhJKiaC8LsdeOhX6f980YDN4cXkn/NvOXKfJ1D4X6MptfQy4dBj/jE4rMIcUmPXZi9tZKXJvk5IchO1RLnS6bh9ys+oWz0w0OaofE5NbvoFj26uLNhD3TPesmrt+0JW1okiqQe9Jr0HLlhlQQp0zwoLH3aHWbY6h+Y4zVn0sEYZ3qKaoLT6MzMZtdYRX6YbLyQF5i2dAQRto9LE18Y9GX/kKHER61Is5xAkCRIbgl4v7Dq2rxPQoKD97/qqXN6OVLKasg3mUyqPjL5+clTb0Iqlfjx7+/ftXpjaHTM0YQWMa1TZlB2D4dXVWb4/R+HlqTiX7owY58+Kih14Rt9dtiIndjvcil3N2CdUz9bW6ur8MhtW5qWrTz1tXXpsWgcZrVCzECuFJZfltXRvbgz1Z3j9CUXJwtzYWKekxEvGP9tHnccvGyI3hC6llqHs5mkaNJRGkdeuwNgNdbJS+8ZU8YyRj/s5SCh9A9ncOoI6kZh27GcqckLBq8au+zFwr6zPusdTWUH18G9cEPeL0tsfeDJKz3jI9pl8l6emTEQ3ROveqSZC/D28pmK1z3IcGDP60bwbOTGHLnjd+S6PjuoVi1l7v4bTpNwnvslK7mQ9cuG8xzPppfMlyOtdKFvMBEFYUh0vjBLNrQT3M9DaCvLDvQNIA13whWYzyNbirSahYlSrw5Er4edXzIkjw2M8vmiCS0HG/s10I3KKj0Ck6RiT9dxg7Rez3wl+nvlobMZiihBfO4FZVEP2wbgHmI9vkDKBSiTXVkCA3cXPxa5qJNrKRcOCtZXhQ7SsQViClH490T+lqMjICwSn4EOV7sO9VaKv4neywsedm+aX6uwQM/Xhwji8Z6SFesW+fWCWsZoQ1IshxXYtra9diD2CUk1Z1Sb7oExROd93sSPdFP99LArUPatN9zSUlXIEEekV3NycBa/2kyEm7nlYkZb51b0YQClYivGKD1wPa5rvYBv2IUAqsiV0Sh8RDqQKqucZfZvdl7IMrxmcs9T4p07tEsk8RbqWJBEtc0aSMFrIIkNLzazzAPMfaOi+OhDVj4nPrIeIpDoPjqppTfVZw2ZZu+kBov2hgamYakhx7z3ZbOfAqA696RqvIyx0MYZJ65Rt2ObaM1EwZfyyTsBztvwf23KqUSuOr9qsVhlfmTjSDoT0mUGT7uR3mCAVhsVMAfp3LmzNTmzWAOgBYKvBv45czqGCBkwFA4Gs8Ed/IxvUbErm3tGiGjHzrO7JrDDoZ5dGbzsfg1WPl3EyUyipn9C5md1wGDXH4eFG8SbXajk+v/G6uqK5EVgghWKBSfuYMvRy8ujghet0BInXddyZwhCOT1GNSSR5HiUPmkYR2trSs06fDBtut5j70YnEY7qEmvaV84WTz2rybAVWSWeJd8SDZcrvJyZyFCU49PRxGMnGZDpLvXel/W2EfbqIn0/f9lwCrEtpiUUzPSMlu1Sp956CdvHRLtv2pLqryp6Tw1aBGFHFll6eEgnKnu93IPLojXUTfhWHU4stAWtjM+lfSAMapZWjEyS3cI5rekBxJ6qH9zjSTMcHzhDzfTomIjKZJwRCzU2KjSxZbVOMtNoNbBAD6HW8/KXwtvVETlQZwSLUpUeCWdGLNyT4s/HN+vaG7j/BQjTIj3YM1gfHhpzHvvrPzTq/2/myJS1mq+Iu+lF8bHQQ/g81gDv17xoMZUIOjNSmYLs8PkAFDNWpmmc1Tz3In7MxQMj+xZWgW4l4+7HTfTDtBBW+KmG5haiwqwGfCPBX13kYWzqu5CLne141BYZEqcsVF0Qwmp14+QfjLcKuHjXz0G8NKUqzC2xgP80Pljc79MxjBCthVplUKoAVU84krApYRR/LGI7AbrS8V6La8hBMt6XCPX4xF2EkuoQ2D5O6U5afAhcSIyPtZDPHz91e+yH87Al7rmUZPaslux/yhuCjmdzARr8g+e4JHlBgD2udSdIaKuodT7mWST6f3WLE7jKxSOn0IssYSi2mkV9nNHQY1QIdf87Y8R9dJfodAT/5lxJdi7X+yDpp9WGwXrUqvfohAG2H37fZis2H4y/2Rf5vRJgw0Lvs3haeFkzgkvuKxy6sjRX96p6sIpoEfpiuP4bV6Z94VpMsbcrNCA2Pkh4Mew5xNwf3UVPen/6LqjCUMdWoPn8Fz0sGRGISrBCrvVo3yJrG+tifxrny5LEjCH3tkVa+6K6s1qud/x43oAkusBI4Myb17OvE+WVeslWenvkpQ/yotXU0wY9LIJjp8/oIXh1T/p+ZKRuB1q6Y7JmLf5zbl3JNVnAG/PgNsr5JXp7lhrIR19gfUIajml99M9gB+Ly0luHd18jTCHWkbEqXxSekXf8eeOoKouSaTUDcJtxlupmNG7H5uU1KM+TQLaz+I+hgAyO5PLjW4VWkjJXImiWYtzya20oFe2gsg5ZyFJPuNyCtFopkHmRti0gL726fF4FfinhKyuPKa/Jf7O+g/DM87ycRPts4C11B8OqcSa0OxomaiD2yms0viTme66fDi0/Y6O/glCaOj1msrwB2OXC3Q6nyIrHhdfcxL8CwAyJUWr6VdHYEZ8GisK+hYh3hCNk/AJpcsS7KboCP1emlVxnEKT/iBWpUpXDp/i3Rlu7np/y1FeoER57zzvbniYaRRuHMHByGzQ/VH5Pex6zswB+xXLut5zu4/cQN4I4XNBaH2ftBGFo6ZLL12NCSMWDApVvVaaMy+EETlFJW1eZ7PkjS93iyh6rDP7GrUtPbOjVPlCpZYgWHMWpXurUcT6ztmTE+kW3MgtTn6fixGSoo7yukQIs8fscKsexKnSXXHPxQT58r8mHMgvvYIL3iOfQccVlMEhOvLiRE9gjEA7gm7A7d0kqMLtHbrQoevygbb5uMaLWvSYstyKvnvbW3vh1FHzK6oBWfihyTxDJ5v2R12on9RIbGAzcDsFI8BrvFw9vck9UMcPxFkZDMD0vucvWYNwTb6qRpbc8UBh4U6sNgcf4MgcDit3e0zlL49gqZwximmwkfb0MqxM2cLBTfPMuVaPGV91tYh+jzNke/etB6pGRtrqS8qXM68beOUy2qe7cMV8BcUwk2fF7LMlKN9QPXIjXpmnfHv8ILZobBN4yH07oatUopPsdrtx3b3iQ4Ii98Ku8Qay9KSQznXZfRmwztmqXedBq94abr8yVlUbOoQUuq63OS10g4c9G+ktIKbN+SSahU49HIg0vPUsAaEu3hVH6bfxo2yp/wfVPBCmXd16NMs0ii30/8WvU2NVApSUpahIUzIKESuTS3gnquuxsl3d7EUTZOBwYAUjVmPjIm94d43tuTwr3WTkgM6Rmtj9D97LHewkjlmmqjDeG0l0OCObqecyMnOSzjCzUtILkgUcubMNUzpxRv8VHODMJ7buTtEAwuqFPSZapWUkGNZSP0jORzJaa2yOhXDJ6vn2k7g0hi3JnDJfMqMHo5suI9UErsevrVv87GDDtnKMelqvAVoZs+oTj3xum3JDPE7pmYjA7VfxsGMm9IyTOIjw6xrTt44Z45zi9a+TLxykiPVMMuUcCuPre8qi/9jyEtM4MBttYVI/5kAWYFwM/a5nKv+I4zSP4gc7TMk3cwfqG+GNMVgcDEXuTRBjHjT36w+7S6L+fJiIF592kVx5xYAhk6hAozv1tevbvsHV0BpVNE6rwjzyqAZE+GzG/WYk0fpygJhTGZjn0sUZvQtu5dy41+Ylkih+AZFoAnrIZwXygJb5EVRvIbU1q0EXdyXK3RowqSjuZchFpVhfI0qqFbUCYWJCaB+4ydOpJN99fJvxeVfYj4OgvmYrNHt0ofSGA2m64vcw6wufBZhlBa9geP5A1BeXyhu9BWJaaG5xnTgJ06KbJrp4ienwKWGHbPuci4V/ldK8PN9tpT53Ea+NiiPQrHr+hC0/Ss3YU0jnKZGB7KNZtT19B56KvBhpWSCWvt9kxXsbu+Jz2sl6LoqeMzuX5g1zg1iXwRbFLP2LwQTOaadQuMPegiQ4xLYhCyJkgZwFps9ebmNA0KPRojMJJaYdNr9i1l6gLCaYVhhryi3qOvf+SJuQhm6Pz9p31zbbE5k8S7kOXDg6hTWToSW8Zv3/l0WkH1xaKRFYnfn9EP4oRw7jlo1SHLiGMV7LuYrliekZCfwD4nNqhg7yF4s6kHTcChxaifDxD3tFumMxBaI6N0Kx0m+rJV3pEMrHUVNestWpVsbHxWjUgMJnJy2J4leN7+1X7Wn6a3lNYWFtfpOcs96lwyJn6zj05OCuLF/03PFz4aPfbnJXx1wW0Z349QGd0FOV150lgrq82/za5Y6OaW93+bGdnx4W9sZOdiJyKmmbIT3vnh3lBaQ/8dr7sQkRERb1uiEmgMGd7fmWAFEcDsFpD1S57W7a/LiQbPYACcp2OunWPkU0w1QseFpVgZsmeKFxaZPeW0N+3rVzj4azgDzwtvq/0pwMsULZ3QLANqda8kjPHaCDdAjkzgV0kXOZ02djIfxfvNp6ayND2bjVWgTPSmHQ+go4JkuDv297rymxyVeSoE1VmXuxIUMy9QS9k9Hj0YGC9WSnRmUu5ujbd+FPMDA9XBcSRfef1LNrr2kdCh1FYfrBrtVE+WZD0IgST3nNFLsDD4coaBU4A4Y03kggIqWzp5F4ao7F8aOFBYTcIbiYEyqvNsV79yjQaCPMtY3khDTWPQbeLf7Ji9FVtA9KReyIXRXfBWhZ9P/SpI74m8zfV8LRKc7dH1poyfOHDVlzLWzQbU09ecMDJgIyW6ZaLIVmlsPDrHFoiQ0eSTU4ZNh5lwfbsmw0o4CL1GZlqN/N+2YQCOHq/rjFgnhZ6vKsg2+E1r4SyOrWSyvA1kKOMimi/RH5JIGJeUHOmG6K6Q1mYQGnqJ3ZNDni0yItaqeANRpoHOZ6r+JiDuOJCcySjWaHaEb/j8yOCcN9jxr/jj1I6K8OY8hpubD1EGlwB4XYolVRZ804rnBRqsmurz4CcmZWma+0aL+cTcjuSynFo5cJbgKdlNl93QxtP7lOHWUA9YUcmExYrfZFatF+x15WoZoUuCbdzPVL4PAidQgeuSkQGJ/5oB7+xXuxNPPuEp321jY7JeuNpJoptVp2zfO8F3GeMkFZRagPS7iZovFvV5lCL87rwfB+7wlBRYV6T6lMg8dWQgz4cLK0UTPTFDl1vGzLiS9SFiwRLQajWYdo0VUaSTJ1b1Mfm1AH+91FLKxECMtf5yvPWGp3NeYf3NPv5rfSFtm0ncZhOpTY4AftfYCtXyhbq/kZ+05gLk3DcR82Xgy34sgO5WLtONX9HHSuITJQKCCdKmNmaz3/ow53UoErKpLIgetL0bRMs4TDpYMc5ERYO4nPvo9G9o9QHxK+H2JTEQb/EXioiSAkj7cUcxhBAvoZAJnzFLLL9L5C6beSR8sHIzMDxM/DYGdv0FPqxwbCfM0f2RXemwfmvpnCAid5Rqsa3tZ/h2iQtlvhST7gdrXR9T/TzM9DhcFywHLoday8K/FA4jd99TWp0PY5w9aD5HlfIjyJuXP6AD3dv0yVkoBDEUYU7ur3Y1OZAdrGEu9GVk2ORq72fap34EiIZGJ1yInONdKCdV/2FtyJHQJNNV+buEqQuvdHyOM16MFXSTXo8Mrd0hTDgJ9/ZEpULmUZ0MVGv/K3N3Lwcy22exFLWbNkE9icZ2ELnm0mD+VVwpACq/zgNXehul8WPuYqhA8t+zOhjNbvHIXWx7z9BZweJONdsmG1MVlk8JsRGHtapfQItxYsVDtcVSYnVfup3b4BoXNB4vcUK6h/sm3EEeF4JnTGrRZbQFWwLYOD8OC1WjQ6iu3QKeNcQ5yCcaeLtqbzlmm5xA6QyQjOGuu9rEMqo+QCnp6D5KAtaHc9ARLj+5L3wN8OziqaATPI6aY9mLssLPw025FkX6IXDXbG9YB9zg0ciOnVibBODVpkuuVFQBmsd0l1wjRFOUpvmD7qY+lN9VOjCVclH988COsn12sflQhAnst+7UXMXwtFIHxSboGp7dgoagW6jg0hbcrfmCr3FshtqKRGFVl38/6HGN+PrQ/HtTk/DOtJEM6SrMgbVgU8swY7iox31m0ODKRBHuZP8tArTA6zx3MkPwu6+WcQeOFU4oOtaD/Yrmda/h9Ysn/g6CasLvjvmCEBU00cbS3TXMOzWAvo45H8Q84+J+EFt/y9kRjhnxnUqPGy4XQlUliteJmnXi4wLhCx17GMO605UWr64EmmOPrjQaoPOLr9H6Isefv0gPL7oF6wJPKoZfFitcw64Id26X0GfFv5rXQveQcIsM6/DKtepaysQidqUPHspWCrAMCeZQpTOTZ3NRFCE6eAs0yadySI8YJuifmggyYSwPX9NsPYVo3nmc4jLxm4WvRLa3OiXzowAJC8ftU6rOCEReD7YMKteOmXw/hZymLTMxwPTDDBMFKZZdPozbNq2I3rTNOLzOf1hCk7Y6v8zTpJdczUyNtAaWexD+I/nTW4JMZ9FNiGkVJN4zhtsA6ftRC5HJYOQUp2788B9dgmMJ0FIwipxoXd90CtCjgtIfUMK992eRMPWNNBqqLX5cITyGDEJqySLPIviWlIKSLIlqtO7iAd8EyBe7OH0f3O4gmu9lyBxgf6/1U7BhH5myfocmdlSrGe6cLDX7iKXlzN3vVk1PljoqMNOYv/1y0NXawTY5qhu9INM8RR1YPHZm/OAou+z8fI81LmjpD4QDdY8Cjav5TXc2+IbplK8jHpiHwPtltpnwwtYXcvIt8RXc0usBaFIBdR2PCrw/m3D8llFyT77J5MhjeU2wgZRcCEO0ZtW1O1v1b1NpUPwl3JXoxoiDtZhGtSzkMJbtZup9iSoOr8855/Ag+Gbp+FumyQdxhICXLBdvtxq9hZMrvcoBoUo17+22FSkEfZRO3SyUt33wO9STAYms64SRXH5sIDy45Msgxskxv3d2EhFBPkjelBB/J98UrlfPbdk9drRdG6+McNmwDX9ho/1qgvaBNn5tHJyLJXOoIeR/eIaVwrbSfyErvOyPEPGvdHpJ6zDu/ziPORChCHd/KQVCDo3Q4iFpKnZULFhLFm8H4sMZPIvTGNn1akMWsYzhg1NRPRpyB8OU4Ulq5uQKgrE/vvMFUXgsg8KV0W+53ZmCdTwopRFEvHH0vOHoGXr1RsD/WgXTZcAIpAzS4gNwm7e807vWEw+4BX3gtHSs6fFYd6tkiMM591Ak/hCMpjcSNjWoNyIssToZMrawHxemZ0i6MRUshU65gwz6rwLsc1oUNZCI50FrRBt6qea4FqzrVT7RAXqorwrkoKgKN8EiQlkdddLggl9j5ixOjqwRtNNRsZ/J4TuR5nmCQ/+047LbmqSeCoSl4EqVM3lBm9OzMOLIkDCfv61mQiaVhRv/8D5tNJjFmQzDC2O0EgMbpmqJ8hfLhDCCw7U+OhtOa8PfSiRClVQHRLilXKxZEkYq6rF18/lxcvbu7jVa3Z/TArr9wTn0uO/SLozueUlP707UiHB/7BlYjookSXzivwFTiXg0GEFYLPpLf2sWHHPFnrC7i5MWoiAZRxymtOTkm5VxqeylKGk1YkttypCpCZGRDH3ZylI1HkDE5GqMeG5hyBJdOIxd0nZXm/PUbjVLZf7TUnEwo2UcHgLjBUVZEy8gDpLVvIHwhXTcJV6BmM5iubsfkKqUt7TB/X14/i20bulM9nnpFyHnoRNvgywiekBMDqPNSoaGKQJP1cEvcumP5fJCxkKeI4DWUgCabNbJfW4CaFri4YL9d8/t+0DwAZ7XTLzKb8lCi2Ydg+dnlNkqzYONjUUzZYlBGsW2b02vsCxgbVZBXuqmIoo5VieBI1H5B6GR3RG1zAYPzhrtATKQ2qi+GA6YkSM0CMHiQKrwr6w0cKWDOy+0U6U2PpQQeZw1Q4eMn8/WS4WLhMlYzo7XZ0uyUJHHM9qC1lshFxF6KEBKxR7hlwzc/5sU6svgOuZaxHtVwijxi9Bc5bTHN0VCPokWYsCRU/JJV+PhQbD+sbap5msAr/33tzjrWZ4z4z2NKxazEMlGFF9oZQNfT92dxchrjkP5uQxFCvc2RiCVLOZPUAbRygO7hsHEkAKuOsZJ2Zf/2CNXlGgQq58axqT0i3HbunKR3BYouAXk4jbKbuxctr46TFE9WqhUtJV2b8WhysjQRtndCWBUOW+aFqaLNgOUDPbURatni+hfk42tzMbFh7MJAK2QRzooGUqeFJLDnPsLoXc1X7bejsmjjzwlN+GCsVjdF86JBkGvIpaBLTiTO+ipT+s/FDNoHd+PO1rzAgY6JnUEeBJTz/1Mi+J0ie8WtkWJ5PVkuGwqnIiac4yr1HedVgCZ03fuc2F026CNiJtCroyQTzq9UaqSvR/1qShTOGCBi7Jqu0YKhU6X2oaa1kWqvKKSITwRs3s7OJlXv5eF+R359f4CK3H3h3A7Ncx61XoImKro/aH9sbIOsLFSoWWn883vC2Zpn/vTVstqSSc/qSrDmn6E6x5rnyNOKnFcjTRMvQZpFOmOZY1Wwp6vPjtM+keoKp/+EmDbzlmK5VX7x61M5+wMWrIAKr/+M0uY6FENLqZ2MWFamrYw1uYq6q1cb173jadQOiJ676MXuAlHb1SlF9+RQ7HCyOj+z2jqlaxp3IHrjYEOPqqH8DunUDdkPTTEMD1jBeJiaE+2lq0EosNWuVizZ6U/fpS6nf6rn339J4SaFg=="}' - password = 'testtest12' - - obj = - success: (result, obj) -> - error: (e) -> - console.log("decryptWallet error:") - console.log(e) - - spyOn(obj, "success") - - WalletCrypto.decryptWallet(data, password, obj.success, obj.error) - - expect(obj.success).toHaveBeenCalled() - expect(obj.success.calls.argsFor(0)[0].guid).toBe("cc90a34d-9eeb-49e7-95ef-9741b77de443") - - return - - describe 'decryptWalletSync()', -> - - describe 'wallet v3', -> - walletData.v3.forEach (wallet) -> - it "should decrypt #{wallet.guid}", -> - dec = WalletCrypto.decryptWalletSync(wallet.enc, wallet.password) - expect(dec.guid).toEqual(wallet.guid) - - describe 'legacy wallet v2', -> - walletData.v2.forEach (wallet) -> - it "should decrypt #{wallet.guid}", -> - dec = WalletCrypto.decryptWalletSync(wallet.enc, wallet.password) - expect(dec.guid).toEqual(wallet.guid) - - describe 'legacy wallet v1', -> - walletData.v1.forEach (wallet) -> - it "should decrypt #{wallet.mode}, #{wallet.padding}, #{wallet.iterations} iterations", -> - dec = WalletCrypto.decryptWalletSync(wallet.enc, wallet.password) - expect(dec.guid).toEqual(wallet.guid) - - describe 'non-existing wallet v4', -> - walletData.v4.forEach (wallet) -> - it "should not decrypt #{wallet.mode}, #{wallet.padding}, #{wallet.iterations} iterations", -> - observers = - success: () -> - error: () -> - - spyOn(observers, "success").and.callThrough() - spyOn(observers, "error").and.callThrough() - - expect(() -> WalletCrypto.decryptWalletSync(wallet.enc, wallet.password)).toThrow(Error("Wallet version 4 not supported.")) - WalletCrypto.decryptWallet(wallet.enc, wallet.password, observers.success, observers.error) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - - describe 'encryptWallet()', -> - v3 = walletData.v3[0] - - it 'should encrypt a v3 wallet', -> - spyOn(Crypto, 'randomBytes').and.callFake((bytes) -> - salt = new Buffer(v3.iv, 'hex') - padding = new Buffer(v3.pad, 'hex') - return if bytes == 16 then salt else padding - ) - enc = WalletCrypto.encryptWallet(JSON.stringify(v3.data), v3.password, v3.iterations, 3) - expect(enc).toEqual(v3.enc) - - describe 'aes-256', -> - vectors = require('./data/aes-256-vectors') - - ['cbc', 'ofb', 'ecb'].forEach (mode) -> - - describe "#{mode}", -> - key = new Buffer(vectors[mode].key, 'hex') - - opts = - mode: WalletCrypto.AES[mode.toUpperCase()] - padding: WalletCrypto.pad.NoPadding - - vectors[mode].tests.forEach (caseData) -> - enc = undefined - - iv = if caseData.iv then new Buffer(caseData.iv, 'hex') else null - testvector = new Buffer(caseData.testvector, 'hex') - ciphertext = new Buffer(caseData.ciphertext, 'hex') - - it "should encrypt #{caseData.testvector}", -> - enc = WalletCrypto.AES.encrypt(testvector, key, iv, opts) - expect(enc.compare(ciphertext)).toEqual(0) - - it "should decrypt #{caseData.testvector}", -> - dec = WalletCrypto.AES.decrypt(enc, key, iv, opts) - expect(dec.compare(testvector)).toEqual(0) - - it "should use CBC if no mode is given", -> - key = new Buffer(vectors['cbc'].key, 'hex') - - opts = - padding: WalletCrypto.pad.NoPadding - - vectors['cbc'].tests.forEach (caseData) -> - - iv = if caseData.iv then new Buffer(caseData.iv, 'hex') else null - testvector = new Buffer(caseData.testvector, 'hex') - ciphertext = new Buffer(caseData.ciphertext, 'hex') - - enc = WalletCrypto.AES.encrypt(testvector, key, iv, opts) - expect(enc.compare(ciphertext)).toEqual(0) - - dec = WalletCrypto.AES.decrypt(enc, key, iv, opts) - expect(dec.compare(testvector)).toEqual(0) - - - describe 'padding', -> - - BLOCK_SIZE_BYTES = 16 - pad = WalletCrypto.pad - input = new Buffer(10).fill(0xff) - - describe 'NoPadding', -> - it 'should not add bytes when padding', -> - output = pad.NoPadding.pad(input, BLOCK_SIZE_BYTES) - expect(output.compare(input)).toEqual(0) - - it 'should not remove bytes when unpadding', -> - output = pad.NoPadding.unpad(input) - expect(output.compare(input)).toEqual(0) - - describe 'ZeroPadding', -> - it 'should fill the remaining block space with 0x00 bytes', -> - output = pad.ZeroPadding.pad(input, BLOCK_SIZE_BYTES) - expect(output.length).toEqual(BLOCK_SIZE_BYTES) - expect(output.toString('hex').match(/(00)+$/)[0].length/2).toEqual(6) - - it 'should remove all trailing 0x00 bytes when unpadding', -> - padded = Buffer.concat([ input, new Buffer(6).fill(0x00) ]) - output = pad.ZeroPadding.unpad(padded) - expect(output.length).toEqual(10) - - it 'should unpad a ZeroPadding padded buffer', -> - output = pad.ZeroPadding.unpad(pad.ZeroPadding.pad(input, BLOCK_SIZE_BYTES)) - expect(output.compare(input)).toEqual(0) - - describe 'Iso10126', -> - it 'should set the last byte to the padding length', -> - output = pad.Iso10126.pad(input, BLOCK_SIZE_BYTES) - expect(output[output.length - 1]).toEqual(0x06) - - it 'should pad using random bytes', -> - spyOn(Crypto, 'randomBytes').and.callThrough() - pad.Iso10126.pad(input, BLOCK_SIZE_BYTES) - expect(Crypto.randomBytes).toHaveBeenCalledWith(5) - - it 'should unpad based on the last byte', -> - padded = new Buffer(BLOCK_SIZE_BYTES) - padded[padded.length - 1] = 0x07 - output = pad.Iso10126.unpad(padded) - expect(output.length).toEqual(9) - - it 'should unpad an Iso10126 padded buffer', -> - output = pad.Iso97971.unpad(pad.Iso97971.pad(input, BLOCK_SIZE_BYTES)) - expect(output.compare(input)).toEqual(0) - - describe 'Iso97971', -> - it 'should set the first padding byte to 0x80', -> - output = pad.Iso97971.pad(input, BLOCK_SIZE_BYTES) - expect(output[input.length]).toEqual(0x80) - - it 'should pad the rest with 0x00 bytes', -> - output = pad.Iso97971.pad(input, BLOCK_SIZE_BYTES) - expect(output.toString('hex').match(/(00)+$/)[0].length/2).toEqual(5) - - it 'should unpad an Iso97971 padded buffer', -> - output = pad.Iso97971.unpad(pad.Iso97971.pad(input, BLOCK_SIZE_BYTES)) - expect(output.compare(input)).toEqual(0) - - describe 'cipherFunction', -> - it 'should not modify the message is all parameters are falsy', -> - expect(WalletCrypto.cipherFunction()('toto')).toEqual('toto') - - it 'should not modify the operation is unknown', -> - expect(WalletCrypto.cipherFunction('password', 'key', 1000, 'nop')('toto')).toEqual('toto') - - describe "scrypt", -> - - observer = - callback: (hash) -> - - beforeEach -> - # overrride as a temporary solution - window.setTimeout = (myFunction) -> myFunction() - - # Crypto_scrypt test vectors can be found at the end of this document: - ## http://www.tarsnap.com/scrypt/scrypt.pdf - - it "Official test vector 1 should work", -> - spyOn(observer, "callback") - expected = "77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442\ - fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906" - WalletCrypto.scrypt "", "" , 16, 1, 1, 64, observer.callback - expect(observer.callback).toHaveBeenCalled() - computed = observer.callback.calls.argsFor(0)[0].toString("hex") - expect(expected).toEqual(computed) - - # Not using official test vectors 2-4, because they are too slow. Using - # Haskell generated test vectors below instead. - - # Disabled because it is too slow - # it "Official test vector 2 should work", -> - # spyOn(observer, "callback") - # expected = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731\ - # 622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640" - # ImportExport.Crypto_scrypt "password", "NaCl" , 1024, 8, 16, 64, observer.callback - # expect(observer.callback).toHaveBeenCalled() - # computed = observer.callback.calls.argsFor(0)[0].toString("hex") - # expect(expected).toEqual(computed) - - # Disabled because it is too slow - # it "Official test vector 3 should work", -> - # spyOn(observer, "callback") - # expected = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2\ - # d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887" - # ImportExport.Crypto_scrypt "pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, observer.callback - # expect(observer.callback).toHaveBeenCalled() - # computed = observer.callback.calls.argsFor(0)[0].toString("hex") - # expect(expected).toEqual(computed) - - # Disabled because it is too slow and PhantomJS runs out of memory - # it "Official test vector 4 should work", -> - # spyOn(observer, "callback") - # expected = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa47\ - # 8e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4" - # ImportExport.Crypto_scrypt "pleaseletmein", "SodiumChloride" , 1048576, 8, 1, 64, observer.callback - # expect(observer.callback).toHaveBeenCalled() - # computed = observer.callback.calls.argsFor(0)[0].toString("hex") - # expect(expected).toEqual(computed) - - # The next test vectors for crypto scrypt have been generated using this lib: - ## https://hackage.haskell.org/package/scrypt-0.3.2/docs/Crypto-Scrypt.html - - it "haskell generated test vector 1 should work", -> - spyOn(observer, "callback") - expected = "53019da47bc9fbdc4f719183e08d149bc1cd6b5bf3ab24df8a7c69daed193c69\ - 2d0d56d4c2af3ce3f98a317671bdb40afb15aaf4f08146cffbc4ccdd66817402" - WalletCrypto.scrypt "suchCrypto", "soSalty" , 16, 8, 1, 64, observer.callback - expect(observer.callback).toHaveBeenCalled() - computed = observer.callback.calls.argsFor(0)[0].toString("hex") - expect(expected).toEqual(computed) - - it "haskell generated test vector 2 should work", -> - spyOn(observer, "callback") - expected = "56f5f2c4809f3ab95ecc334e64450392bf6f1f7187653b1ba920f39b4c44b2d6\ - b47a243c70b2c3444bc31cfec9c57893dd39fa0688bd8a5d1cdcbe08b17b432b" - WalletCrypto.scrypt "ΜΟΛΩΝ", "ΛΑΒΕ" , 32, 4, 4, 64, observer.callback - expect(observer.callback).toHaveBeenCalled() - computed = observer.callback.calls.argsFor(0)[0].toString("hex") - expect(expected).toEqual(computed) - - it "haskell generated test vector 3 should work", -> - spyOn(observer, "callback") - expected = "f890a6beae1dc3f627f9d9bcca8a96950b11758beb1edf1b072c8b8522d15562\ - 9db68aba34619e1ae45b4b6b2917bcb8fd1698b536124df69d5c36d7f28fbe0e" - WalletCrypto.scrypt "ϓ␀𐐀💩", "ϓ␀𐐀💩" , 64, 2, 2, 64, observer.callback - expect(observer.callback).toHaveBeenCalled() - computed = observer.callback.calls.argsFor(0)[0].toString("hex") - expect(expected).toEqual(computed) diff --git a/tests/wallet_network_spec.js b/tests/wallet_network_spec.js new file mode 100644 index 000000000..930cfef02 --- /dev/null +++ b/tests/wallet_network_spec.js @@ -0,0 +1,434 @@ +let proxyquire = require('proxyquireify')(require); + +describe('WalletNetwork', () => { + const API = { + retry (f) { + return f(); + }, + request (action, method, data, headers) { + console.log(action, method, data, headers); + return new Promise((resolve, reject) => { + if (method === 'uuid-generator') { + if (API.callFail) { + resolve({}); + } else { + resolve({uuids: ['1234', '5678']}); + } + } else if (method === 'wallet') { + if (data.method === 'recover-wallet') { + if (data.captcha === 'eH1hs') { + resolve({success: true, message: 'Sent email'}); + } else { + resolve({success: false, message: 'Invalid Captcha'}); + } + } else if (data.method === 'reset-two-factor-form') { + if (data.kaptcha === 'eH1hs') { + resolve({success: true, message: 'Request Submitted'}); + } else { + resolve({success: false, message: 'Invalid Captcha'}); + } + } + } else if (method === 'wallet/1234') { + if (API.callFail) { + throw ''; // eslint-disable-line no-throw-literal + } else { + resolve('done'); + } + } else if (method === 'wallet/poll-for-session-guid') { + resolve({guid: '1234'}); + } else if ((action === 'POST') && (method === 'sessions')) { + if (API.callFail) { + throw new Error(); + } else { + resolve({token: 'token'}); + } + } else if (method === 'kaptcha.jpg') { + resolve('image'); + } else { + reject('bad call'); + } + }); + }, + + securePost () { + if (API.callFail) { + return Promise.reject('api call fail'); + } else { + return Promise.resolve('api call success'); + } + }, + + securePostCallbacks (url, {method}, success, error) { + if (API.callFail) { + error('api call fail'); + } else { + if (method === 'wallet.aes.json') { + if (API.badChecksum) { + success({payload: 'hex data'}); + } else { + success({payload: 'Not modified'}); + } + } + } + } + }; + + const WalletCrypto = { + encryptWallet () { + if (WalletCrypto.encryptError) { + return []; + } else { + return ['encrypted']; + } + }, + decryptWalletSync () { + if (WalletCrypto.decryptError) { + throw Error(''); + } else { + return 'decrypted'; + } + }, + sha256 (msg) { return msg; } + }; + + let MyWallet = { + wallet: { + defaultPbkdf2Iterations: 10000, + isUpgradedToHD: true + } + }; + + let isPolling = false; + let WalletStore = { + isPolling () { return isPolling; }, + setIsPolling (val) { isPolling = val; } + }; + + let WalletNetwork = proxyquire('../src/wallet-network', { + './api': API, + './wallet-crypto': WalletCrypto, + './wallet': MyWallet, + './wallet-store': WalletStore + }); + + describe('generateUUIDs', () => { + it('should return two UUIDs', done => { + let promise = WalletNetwork.generateUUIDs(2); + expect(promise).toBeResolvedWith(jasmine.objectContaining(['1234', '5678']), done); + }); + + it('should not be resolved if the API call fails', done => { + API.callFail = true; + let promise = WalletNetwork.generateUUIDs(2); + expect(promise).toBeRejectedWith('Could not generate uuids', done); + API.callFail = false; + }); + }); + + describe('recoverGuid', () => { + beforeEach(() => spyOn(API, 'request').and.callThrough()); + + it('should POST the users email and captcha', done => { + let promise = WalletNetwork.recoverGuid('token', 'a@b.com', 'eH1hs'); + expect(promise).toBeResolved(done); + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].email).toEqual('a@b.com'); + expect(API.request.calls.argsFor(0)[2].captcha).toEqual('eH1hs'); + }); + + it('should resolve if succesful', done => { + let promise = WalletNetwork.recoverGuid('token', 'a@b.com', 'eH1hs'); + expect(promise).toBeResolvedWith('Sent email', done); + }); + }); + + describe('resendTwoFactorSms', () => { + beforeEach(() => spyOn(API, 'request').and.callThrough()); + + it('should GET with resend_code to true', done => { + WalletNetwork.resendTwoFactorSms('1234', 'token').then(() => done()); + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].resend_code).toEqual(true); + }); + + it('should pass on the session token', (done) => { + WalletNetwork.resendTwoFactorSms('1234', 'token').then(() => done()); + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual('token'); + }); + + it('should not be resolved if the call fails', done => { + API.callFail = true; + let promise = WalletNetwork.resendTwoFactorSms('1234', 'token'); + expect(promise).toBeRejectedWith('Could not resend two factor sms', done); + API.callFail = false; + }); + }); + + describe('requestTwoFactorReset', () => { + beforeEach(() => spyOn(API, 'request').and.callThrough()); + + it('should POST a captcha and other fields', done => { + let promise = WalletNetwork.requestTwoFactorReset('token', '1234', 'a@b.com', '', '', 'Hi support', 'eH1hs'); + expect(promise).toBeResolved(done); + + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].kaptcha).toEqual('eH1hs'); + }); + + it('should resolve if succesful', done => { + let promise = WalletNetwork.requestTwoFactorReset('token', '1234', 'a@b.com', '', '', 'Hi support', 'eH1hs'); + expect(promise).toBeResolvedWith('Request Submitted', done); + }); + + it('should reject if captcha is invalid', done => { + let promise = WalletNetwork.requestTwoFactorReset('token', '1234', 'a@b.com', '', '', 'Hi support', "can't read"); + expect(promise).toBeRejectedWith('Invalid Captcha', done); + }); + }); + + describe('insertWallet', () => { + let observers = + {callback () {}}; + + beforeEach(() => spyOn(observers, 'callback')); + + it('should not go through without a GUID', () => { + try { + WalletNetwork.insertWallet(); + } catch (e) { + expect(e.toString()).toEqual('AssertionError: GUID missing'); + } + }); + + it('should not go through without a shared key', () => { + try { + WalletNetwork.insertWallet('1234'); + } catch (e) { + expect(e.toString()).toEqual('AssertionError: Shared Key missing'); + } + }); + + it('should not go through without a password', () => { + try { + WalletNetwork.insertWallet('1234', 'key'); + } catch (e) { + expect(e.toString()).toEqual('AssertionError: Password missing'); + } + }); + + it('should not go through without a callback', () => { + try { + WalletNetwork.insertWallet('1234', 'key', 'password', {}); + } catch (e) { + expect(e.toString()).toEqual('AssertionError: decryptWalletProgress must be a function'); + } + + try { + WalletNetwork.insertWallet('1234', 'key', 'password', {}, 'sdfsdfs'); + } catch (e) { + expect(e.toString()).toEqual('AssertionError: decryptWalletProgress must be a function'); + } + }); + + it('should not be resolved if the encryption fails', done => { + WalletCrypto.encryptError = true; + let promise = WalletNetwork.insertWallet('1234', 'key', 'password', 'badcb', observers.callback); + expect(promise).toBeRejectedWith('Error encrypting the JSON output', done); + expect(observers.callback).not.toHaveBeenCalled(); + }); + + it('should not be resolved if the decryption fails', done => { + WalletCrypto.decryptError = true; + let promise = WalletNetwork.insertWallet('1234', 'key', 'password', 'badcb', observers.callback); + expect(promise).toBeRejectedWith('', done); + expect(observers.callback).toHaveBeenCalled(); + }); + + it('should not be resolved if the api call fails', done => { + API.callFail = true; + let promise = WalletNetwork.insertWallet('1234', 'key', 'password', 'badcb', observers.callback); + expect(promise).toBeRejected(done); + expect(observers.callback).toHaveBeenCalled(); + }); + + it('should be resolved if the api call does not fails', done => { + let promise = WalletNetwork.insertWallet('1234', 'key', 'password', 'badcb', observers.callback); + expect(promise).toBeResolved(done); + expect(observers.callback).toHaveBeenCalled(); + }); + + return afterEach(() => { + WalletCrypto.encryptError = false; + WalletCrypto.decryptError = false; + API.callFail = false; + }); + }); + + describe('checkWalletChecksum', () => { + let observers = { + success () {}, + error () {} + }; + + beforeEach(() => { + spyOn(observers, 'success'); + spyOn(observers, 'error'); + }); + + it('should not go through without a checksum', () => { + try { + WalletNetwork.checkWalletChecksum(); + } catch (e) { + expect(e.toString()).toEqual('AssertionError: Payload checksum missing'); + } + }); + + it('should go through without callbacks', () => WalletNetwork.checkWalletChecksum('payload_checksum')); + + it('should be able to fail without callbacks', () => { + API.callFail = true; + WalletNetwork.checkWalletChecksum('payload_checksum'); + }); + + it('should go through with callbacks', () => WalletNetwork.checkWalletChecksum('payload_checksum', observers.success, observers.error)); + + it('should not be succeed if the api call fails', () => { + API.callFail = true; + WalletNetwork.checkWalletChecksum('payload_checksum', observers.success, observers.error); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + + it('should not be succeed if the checksum is bad', () => { + API.badChecksum = true; + WalletNetwork.checkWalletChecksum('payload_checksum', observers.success, observers.error); + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + + return afterEach(() => { + API.callFail = false; + API.badChecksum = false; + }); + }); + + describe('obtainSessionToken()', () => { + it('should POST /sessions', done => { + let promise = WalletNetwork.obtainSessionToken(); + expect(promise).toBeResolvedWith('token', done); + }); + + it('should fail if network problem', done => { + API.callFail = true; + let promise = WalletNetwork.obtainSessionToken(); + expect(promise).toBeRejected(done); + }); + + afterEach(() => { API.callFail = false; }); + }); + + describe('fetchWallet', () => { + beforeEach(() => { + spyOn(API, 'request').and.callThrough(); + + WalletNetwork.fetchWallet( + '1234', + 'token' + ); + }); + + it('should pass the guid along', () => expect(API.request.calls.argsFor(0)[1]).toEqual('wallet/1234')); + + it('should use an X-Session-ID header', () => expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual('token')); + }); + + describe('fetchWalletWithSharedKey', () => { + beforeEach(() => { + spyOn(API, 'request').and.callThrough(); + + WalletNetwork.fetchWalletWithSharedKey( + '1234', + 'shared-key' + ); + }); + + it('should pass the guid along', () => expect(API.request.calls.argsFor(0)[1]).toEqual('wallet/1234')); + + it('should pass the shared key along', () => expect(API.request.calls.argsFor(0)[2]['sharedKey']).toEqual('shared-key')); + + it('should not use an X-Session-ID header', () => expect(API.request.calls.argsFor(0)[3]).toEqual({})); + }); + + describe('fetchWalletWithTwoFactor', () => { + beforeEach(() => spyOn(API, 'request').and.callThrough()); + + it('should pass code (as object) along', () => { + WalletNetwork.fetchWalletWithTwoFactor( + '1234', + 'token', + {type: 5, code: 'BF399'} + ); + + expect(API.request.calls.argsFor(0)[2].payload).toEqual('BF399'); + }); + + it('should convert SMS code to upper case', () => { + WalletNetwork.fetchWalletWithTwoFactor( + '1234', + 'token', + {type: 5, code: 'bf399'} + ); + + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].payload).toEqual('BF399'); + }); + + it('should not convert Yubikey code to upper case', () => { + WalletNetwork.fetchWalletWithTwoFactor( + '1234', + 'password', + {type: 1, code: 'abcdef123'} + ); + + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].payload).toEqual('abcdef123'); + }); + }); + + describe('pollForSessionGUID', () => { + beforeEach(() => { + WalletStore.setIsPolling(false); + spyOn(API, 'request').and.callThrough(); + WalletNetwork.pollForSessionGUID('token'); + }); + + it('should call wallet/poll-for-session-guid', () => expect(API.request.calls.argsFor(0)[1]).toEqual('wallet/poll-for-session-guid')); + + it('should pass the session token', () => expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual('token')); + }); + + describe('getCaptchaImage', () => { + beforeEach(() => { + spyOn(WalletNetwork, 'obtainSessionToken').and.callFake(() => Promise.resolve('token')); + spyOn(API, 'request').and.callThrough(); + }); + + it('should create a new session', () => { + WalletNetwork.getCaptchaImage(); + expect(WalletNetwork.obtainSessionToken).toHaveBeenCalled(); + }); + + it('should call kaptcha.jpg', done => { + let promise = WalletNetwork.getCaptchaImage().then(() => expect(API.request.calls.argsFor(0)[1]).toEqual('kaptcha.jpg')); + + expect(promise).toBeResolved(done); + }); + + it('should pass session token along', done => { + let promise = WalletNetwork.getCaptchaImage().then(() => expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual('token')); + + expect(promise).toBeResolved(done); + }); + }); +}); diff --git a/tests/wallet_network_spec.js.coffee b/tests/wallet_network_spec.js.coffee deleted file mode 100644 index 75af74b44..000000000 --- a/tests/wallet_network_spec.js.coffee +++ /dev/null @@ -1,384 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -describe "WalletNetwork", -> - - API = - retry: (f) -> - f() - request: (action, method, data, headers) -> - console.log(action, method, data, headers) - new Promise (resolve, reject) -> - if method == "uuid-generator" - if API.callFail - resolve({}) - else - resolve({uuids: ['1234', '5678']}) - else if method == "wallet" - if data.method == "recover-wallet" - if data.captcha == "eH1hs" - resolve({success: true, message: "Sent email"}) - else - resolve({success: false, message: "Invalid Captcha"}) - else if data.method == "reset-two-factor-form" - if data.kaptcha == "eH1hs" - resolve({success: true, message: "Request Submitted"}) - else - resolve({success: false, message: "Invalid Captcha"}) - else if method == "wallet/1234" - if API.callFail - throw ''; - else - resolve('done') - else if method == "wallet/poll-for-session-guid" - resolve({guid: "1234"}) - else if action == "POST" && method == "sessions" - if API.callFail - throw ''; - else - resolve({token: 'token'}) - else if method == "kaptcha.jpg" - resolve('image') - else - reject('bad call') - - securePost: () -> - if API.callFail - return Promise.reject('api call fail') - else - return Promise.resolve('api call success') - - securePostCallbacks: (url, data, success, error) -> - if API.callFail - return error('api call fail') - else - if data.method == "wallet.aes.json" - if API.badChecksum - success({payload: 'hex data'}) - else - success({payload: 'Not modified'}) - - WalletCrypto = - encryptWallet: () -> - if WalletCrypto.encryptError - [] - else - ['encrypted'] - decryptWalletSync: () -> - if WalletCrypto.decryptError - throw Error(''); - else - 'decrypted' - sha256: (msg) -> msg - - MyWallet = - wallet: - defaultPbkdf2Iterations: 10000 - isUpgradedToHD: true - - isPolling = false - WalletStore = - isPolling: () -> isPolling - setIsPolling: (val) -> isPolling = val - - WalletNetwork = proxyquire('../src/wallet-network', { - './api': API, - './wallet-crypto': WalletCrypto, - './wallet': MyWallet, - './wallet-store' : WalletStore - }) - - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "generateUUIDs", -> - it "should return two UUIDs", (done) -> - promise = WalletNetwork.generateUUIDs(2) - expect(promise).toBeResolvedWith(jasmine.objectContaining(['1234', '5678']), done) - - it "should not be resolved if the API call fails", (done) -> - API.callFail = true - promise = WalletNetwork.generateUUIDs(2) - expect(promise).toBeRejectedWith('Could not generate uuids', done) - API.callFail = false - - describe "recoverGuid", -> - beforeEach -> - spyOn(API, "request").and.callThrough() - - it "should POST the users email and captcha", (done) -> - promise = WalletNetwork.recoverGuid("token", "a@b.com", "eH1hs") - expect(promise).toBeResolved(done) - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].email).toEqual('a@b.com') - expect(API.request.calls.argsFor(0)[2].captcha).toEqual('eH1hs') - - it "should resolve if succesful", (done) -> - promise = WalletNetwork.recoverGuid("token", "a@b.com", "eH1hs") - expect(promise).toBeResolvedWith('Sent email', done) - - describe "resendTwoFactorSms", -> - beforeEach -> - spyOn(API, "request").and.callThrough() - - it "should GET with resend_code to true", (done) -> - WalletNetwork.resendTwoFactorSms("1234", "token").then(() -> done()) - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].resend_code).toEqual(true) - - it "should pass on the session token", -> - WalletNetwork.resendTwoFactorSms("1234", "token").then(() -> done()) - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual("token") - - it "should not be resolved if the call fails", (done) -> - API.callFail = true - promise = WalletNetwork.resendTwoFactorSms("1234", "token") - expect(promise).toBeRejectedWith('Could not resend two factor sms', done) - API.callFail = false - - describe "requestTwoFactorReset", -> - beforeEach -> - spyOn(API, "request").and.callThrough() - - it "should POST a captcha and other fields", (done) -> - promise = WalletNetwork.requestTwoFactorReset("token", "1234", "a@b.com", "", "", "Hi support", "eH1hs") - expect(promise).toBeResolved(done) - - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].kaptcha).toEqual("eH1hs") - - it "should resolve if succesful", (done) -> - promise = WalletNetwork.requestTwoFactorReset("token", "1234", "a@b.com", "", "", "Hi support", "eH1hs") - expect(promise).toBeResolvedWith('Request Submitted', done) - - it "should reject if captcha is invalid", (done) -> - promise = WalletNetwork.requestTwoFactorReset("token", "1234", "a@b.com", "", "", "Hi support", "can't read") - expect(promise).toBeRejectedWith('Invalid Captcha', done) - - describe "insertWallet", -> - - observers = - callback: () -> - - beforeEach -> - spyOn(observers, "callback") - - it "should not go through without a GUID", -> - try - WalletNetwork.insertWallet() - catch e - expect(e.toString()).toEqual('AssertionError: GUID missing') - - it "should not go through without a shared key", -> - try - WalletNetwork.insertWallet("1234") - catch e - expect(e.toString()).toEqual('AssertionError: Shared Key missing') - - it "should not go through without a password", -> - try - WalletNetwork.insertWallet("1234", "key") - catch e - expect(e.toString()).toEqual('AssertionError: Password missing') - - it "should not go through without a callback", -> - try - WalletNetwork.insertWallet("1234", "key", "password", {}) - catch e - expect(e.toString()).toEqual('AssertionError: decryptWalletProgress must be a function') - - try - WalletNetwork.insertWallet("1234", "key", "password", {}, "sdfsdfs") - catch e - expect(e.toString()).toEqual('AssertionError: decryptWalletProgress must be a function') - - it "should not be resolved if the encryption fails", (done) -> - WalletCrypto.encryptError = true - promise = WalletNetwork.insertWallet("1234", "key", "password", "badcb", observers.callback) - expect(promise).toBeRejectedWith('Error encrypting the JSON output', done) - expect(observers.callback).not.toHaveBeenCalled() - - it "should not be resolved if the decryption fails", (done) -> - WalletCrypto.decryptError = true - promise = WalletNetwork.insertWallet("1234", "key", "password", "badcb", observers.callback) - expect(promise).toBeRejectedWith('', done) - expect(observers.callback).toHaveBeenCalled() - - it "should not be resolved if the api call fails", (done) -> - API.callFail = true - promise = WalletNetwork.insertWallet("1234", "key", "password", "badcb", observers.callback) - expect(promise).toBeRejected(done) - expect(observers.callback).toHaveBeenCalled() - - - it "should be resolved if the api call does not fails", (done) -> - promise = WalletNetwork.insertWallet("1234", "key", "password", "badcb", observers.callback) - expect(promise).toBeResolved(done) - expect(observers.callback).toHaveBeenCalled() - - afterEach -> - WalletCrypto.encryptError = false - WalletCrypto.decryptError = false - API.callFail = false - - describe "checkWalletChecksum", -> - - observers = - success: () -> - error: () -> - - beforeEach -> - spyOn(observers, "success") - spyOn(observers, "error") - - it "should not go through without a checksum", -> - try - WalletNetwork.checkWalletChecksum() - catch e - expect(e.toString()).toEqual('AssertionError: Payload checksum missing') - - it "should go through without callbacks", -> - WalletNetwork.checkWalletChecksum('payload_checksum') - - it "should be able to fail without callbacks", -> - API.callFail = true - WalletNetwork.checkWalletChecksum('payload_checksum') - - it "should go through with callbacks", -> - WalletNetwork.checkWalletChecksum('payload_checksum', observers.success, observers.error) - - it "should not be succeed if the api call fails", -> - API.callFail = true - WalletNetwork.checkWalletChecksum('payload_checksum', observers.success, observers.error) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - it "should not be succeed if the checksum is bad", -> - API.badChecksum = true - WalletNetwork.checkWalletChecksum('payload_checksum', observers.success, observers.error) - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - afterEach -> - API.callFail = false - API.badChecksum = false - - describe "obtainSessionToken()", -> - it "should POST /sessions", (done) -> - promise = WalletNetwork.obtainSessionToken() - expect(promise).toBeResolvedWith("token", done) - - it "should fail if network problem", (done) -> - API.callFail = true - promise = WalletNetwork.obtainSessionToken() - expect(promise).toBeRejected(done) - - afterEach -> - API.callFail = false - - describe "fetchWallet", -> - beforeEach -> - spyOn(API, "request").and.callThrough() - - WalletNetwork.fetchWallet( - "1234", - "token" - ) - - it "should pass the guid along", -> - expect(API.request.calls.argsFor(0)[1]).toEqual("wallet/1234") - - it "should use an X-Session-ID header", -> - expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual("token") - - describe "fetchWalletWithSharedKey", -> - beforeEach -> - spyOn(API, "request").and.callThrough() - - WalletNetwork.fetchWalletWithSharedKey( - "1234", - "shared-key" - ) - - it "should pass the guid along", -> - expect(API.request.calls.argsFor(0)[1]).toEqual("wallet/1234") - - it "should pass the shared key along", -> - expect(API.request.calls.argsFor(0)[2]['sharedKey']).toEqual("shared-key") - - it "should not use an X-Session-ID header", -> - expect(API.request.calls.argsFor(0)[3]).toEqual({}) - - describe "fetchWalletWithTwoFactor", -> - beforeEach -> - spyOn(API, "request").and.callThrough() - - it "should pass code (as object) along", -> - WalletNetwork.fetchWalletWithTwoFactor( - "1234", - "token" - {type: 5, code: "BF399"} - ) - - expect(API.request.calls.argsFor(0)[2].payload).toEqual("BF399") - - it "should convert SMS code to upper case", -> - WalletNetwork.fetchWalletWithTwoFactor( - "1234", - "token", - {type: 5, code: "bf399"} - ) - - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].payload).toEqual("BF399") - - it "should not convert Yubikey code to upper case", -> - WalletNetwork.fetchWalletWithTwoFactor( - "1234", - "password", - {type: 1, code: "abcdef123"} - ) - - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].payload).toEqual("abcdef123") - - describe "pollForSessionGUID", -> - beforeEach -> - WalletStore.setIsPolling(false) - spyOn(API, "request").and.callThrough() - WalletNetwork.pollForSessionGUID("token") - - it "should call wallet/poll-for-session-guid", -> - expect(API.request.calls.argsFor(0)[1]).toEqual("wallet/poll-for-session-guid") - - it "should pass the session token", -> - expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual("token") - - describe "getCaptchaImage", -> - - beforeEach -> - spyOn(WalletNetwork, "obtainSessionToken").and.callFake(() -> - Promise.resolve("token") - ) - spyOn(API, "request").and.callThrough() - - it "should create a new session", -> - WalletNetwork.getCaptchaImage() - expect(WalletNetwork.obtainSessionToken).toHaveBeenCalled() - - it "should call kaptcha.jpg", (done)-> - promise = WalletNetwork.getCaptchaImage().then(() -> - expect(API.request.calls.argsFor(0)[1]).toEqual("kaptcha.jpg") - ) - - expect(promise).toBeResolved(done) - - it "should pass session token along", (done)-> - promise = WalletNetwork.getCaptchaImage().then(() -> - expect(API.request.calls.argsFor(0)[3].sessionToken).toEqual("token") - ) - - expect(promise).toBeResolved(done) diff --git a/tests/wallet_signup_spec.js b/tests/wallet_signup_spec.js new file mode 100644 index 000000000..094a0d6fe --- /dev/null +++ b/tests/wallet_signup_spec.js @@ -0,0 +1,82 @@ +let proxyquire = require('proxyquireify')(require); + +const WalletNetwork = { + generateUUIDs () { + if (WalletNetwork.generateBadUUIDs) { + return new Promise(resolve => resolve(['a8098c1a-f8', 'b77e160355e'])); + } else { + return new Promise(resolve => resolve(['a8098c1a-f86e-11da-bd1a-00112444be1e', '6fa459ea-ee8a-3ca4-894e-db77e160355e'])); + } + } +}; + +let Wallet = + {new (guid, sharedKey, mnemonic, bip39Password, firstAccountLabel, success, error) { success(); }}; + +let stubs = { + './blockchain-wallet': Wallet, + './wallet-network': WalletNetwork +}; + +let WalletSignup = proxyquire('../src/wallet-signup', stubs); + +describe('WalletSignup', () => + + describe('generateNewWallet', () => { + let observers = null; + + it('should not generate a wallet with a password longer than 256 chars', () => { + let password = (new Array(1024)).join('x'); + expect(() => WalletSignup.generateNewWallet(password, 'a@a.co', undefined, undefined, 'My Wallet')).toThrow(); + }); + + describe('it should not generate a wallet with bad UUIDs', () => { + beforeEach(done => { + observers = { + success () { done(); }, + error () { done(); }, + progress () {} + }; + + spyOn(observers, 'success').and.callThrough(); + spyOn(observers, 'error').and.callThrough(); + spyOn(observers, 'progress'); + + WalletNetwork.generateBadUUIDs = true; + WalletSignup.generateNewWallet('pass', 'a@a.co', undefined, undefined, 'My Wallet', observers.success, observers.error, observers.progress); + }); + + it('', () => { + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + expect(observers.progress).toHaveBeenCalled(); + + WalletNetwork.generateBadUUIDs = false; + }); + }); + + describe('should generate a wallet when all conditions are met', () => { + observers = null; + + beforeEach(done => { + observers = { + success () { done(); }, + error () { done(); }, + progress () {} + }; + + spyOn(observers, 'success').and.callThrough(); + spyOn(observers, 'error').and.callThrough(); + spyOn(observers, 'progress'); + + WalletSignup.generateNewWallet('totot', 'a@a.co', undefined, undefined, 'My Wallet', observers.success, observers.error, observers.progress); + }); + + it('', () => { + expect(observers.success).toHaveBeenCalled(); + expect(observers.error).not.toHaveBeenCalled(); + expect(observers.progress).toHaveBeenCalled(); + }); + }); + }) +); diff --git a/tests/wallet_signup_spec.js.coffee b/tests/wallet_signup_spec.js.coffee deleted file mode 100644 index a50e17ff6..000000000 --- a/tests/wallet_signup_spec.js.coffee +++ /dev/null @@ -1,72 +0,0 @@ -proxyquire = require('proxyquireify')(require) - - -WalletNetwork = - generateUUIDs: () -> - if WalletNetwork.generateBadUUIDs - new Promise((resolve) -> resolve(['a8098c1a-f8', 'b77e160355e'])) - else - new Promise((resolve) -> resolve(['a8098c1a-f86e-11da-bd1a-00112444be1e', '6fa459ea-ee8a-3ca4-894e-db77e160355e'])) - -Wallet = - new: (guid, sharedKey, mnemonic, bip39Password, firstAccountLabel, success, error) -> success() - -stubs = - './blockchain-wallet': Wallet - './wallet-network': WalletNetwork - -WalletSignup = proxyquire('../src/wallet-signup', stubs) - - -describe "WalletSignup", -> - - describe "generateNewWallet", -> - - observers = null - - it "should not generate a wallet with a password longer than 256 chars", -> - password = (new Array(1024)).join("x") - expect(() -> WalletSignup.generateNewWallet(password, 'a@a.co', undefined, undefined, 'My Wallet')).toThrow() - - describe "it should not generate a wallet with bad UUIDs", -> - - beforeEach (done) -> - observers = - success: () -> done() - error: () -> done() - progress: () -> - - spyOn(observers, "success").and.callThrough() - spyOn(observers, "error").and.callThrough() - spyOn(observers, "progress") - - WalletNetwork.generateBadUUIDs = true - WalletSignup.generateNewWallet('pass', 'a@a.co', undefined, undefined, 'My Wallet', observers.success, observers.error, observers.progress) - - it "", -> - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - expect(observers.progress).toHaveBeenCalled() - - WalletNetwork.generateBadUUIDs = false - - describe "should generate a wallet when all conditions are met", -> - - observers = null - - beforeEach (done) -> - observers = - success: () -> done() - error: () -> done() - progress: () -> - - spyOn(observers, "success").and.callThrough() - spyOn(observers, "error").and.callThrough() - spyOn(observers, "progress") - - WalletSignup.generateNewWallet('totot', 'a@a.co', undefined, undefined, 'My Wallet', observers.success, observers.error, observers.progress) - - it "", -> - expect(observers.success).toHaveBeenCalled() - expect(observers.error).not.toHaveBeenCalled() - expect(observers.progress).toHaveBeenCalled() diff --git a/tests/wallet_spec.js b/tests/wallet_spec.js new file mode 100644 index 000000000..6cc635c8b --- /dev/null +++ b/tests/wallet_spec.js @@ -0,0 +1,690 @@ +let proxyquire = require('proxyquireify')(require); + +let walletStoreGuid; +let walletStoreEncryptedWalletData; +let WalletStore = { + setGuid (guid) { + walletStoreGuid = guid; + }, + getGuid () { + return walletStoreGuid; + }, + setRealAuthType () {}, + setSyncPubKeys () {}, + setLanguage () {}, + setEncryptedWalletData (data) { + walletStoreEncryptedWalletData = data; + }, + getEncryptedWalletData () { return walletStoreEncryptedWalletData || 'encrypted'; } +}; + +let BlockchainSettingsAPI = { + changeLanguage () {}, + changeLocalCurrency () {} +}; + +let WalletCrypto = { + decryptWallet () {} +}; + +let hdwallet = { + guid: '1234', + sharedKey: 'shared', + scanBip44 () { + return { + then (cb) { + cb(); + return { + catch () {} + }; + } + }; + } +}; + +let WalletSignup = { + generateNewWallet (inputedPassword, inputedEmail, mnemonic, bip39Password, firstAccountName, successCallback, errorCallback) { + successCallback(hdwallet); + } +}; + +const API = { + securePostCallbacks () {}, + request (action, method, data, withCred) {} +}; + +const WalletNetwork = { + insertWallet () { + console.log(WalletNetwork.failInsertion); + if (WalletNetwork.failInsertion) { + return new Promise((resolve, reject) => reject()); + } else { + return new Promise(resolve => resolve()); + } + }, + + establishSession (token) { + return { + then (cb) { + if (token !== 'token') { + token = 'new_token'; + } + cb(token); + return { + catch (cb) {} + }; + } + }; + }, + + fetchWallet (guid, sessionToken, needsTwoFactorCode, authorizationRequired) { + return { + then (cb) { + if (guid === 'wallet-2fa') { + needsTwoFactorCode(1); + } else if (guid === 'wallet-email-auth') { + authorizationRequired().then(() => + // WalletNetwork proceeds with login and then calls success: + cb({guid, payload: 'encrypted'}) + ); + } else if (guid === 'wallet-email-auth-2fa') { + authorizationRequired().then(() => + // WalletNetwork proceeds with login and now asks for 2FA: + needsTwoFactorCode(1) + ); + } else { + WalletStore.setGuid(guid); + WalletStore.setEncryptedWalletData('encrypted'); + cb({guid, payload: 'encrypted'}); + } + return { + catch (cb) {} + }; + } + }; + }, + + fetchWalletWithSharedKey (guid) { + return { + then (cb) { + WalletStore.setGuid(guid); + WalletStore.setEncryptedWalletData('encrypted'); + cb({guid, payload: 'encrypted'}); + return { + catch (cb) {} + }; + } + }; + }, + + fetchWalletWithTwoFactor (guid, sessionToken, twoFactorCode) { + return { + then (cb) { + WalletStore.setGuid(guid); + WalletStore.setEncryptedWalletData('encrypted'); + cb({guid, payload: 'encrypted'}); + return { + catch (cb) {} + }; + } + }; + }, + + pollForSessionGUID (token) { + return { + then (cb) { + cb(); + return { + catch (cb) {} + }; + } + }; + } +}; + +let BIP39 = { + generateMnemonic (str, rng, wlist) { + let mnemonic = 'bicycle balcony prefer kid flower pole goose crouch century lady worry flavor'; + let seed = rng(32); + return seed === 'random' ? mnemonic : 'failure'; + } +}; + +const RNG = { + run (input) { + if (RNG.shouldThrow) { + throw new Error('Connection failed'); + } + return 'random'; + } +}; + +let stubs = { + './wallet-store': WalletStore, + './wallet-crypto': WalletCrypto, + './wallet-signup': WalletSignup, + './api': API, + './wallet-network': WalletNetwork, + 'bip39': BIP39, + './rng': RNG, + './blockchain-settings-api': BlockchainSettingsAPI +}; + +let MyWallet = proxyquire('../src/wallet', stubs); + +describe('Wallet', () => { + let callbacks; + + beforeEach(() => { + WalletStore.setGuid(undefined); + return WalletStore.setEncryptedWalletData(undefined); + }); + + describe('makePairingCode()', () => { + let success; + let error; + + beforeEach(() => { + MyWallet.wallet = { + guid: 'wallet-guid', + sharedKey: 'shared-key' + }; + spyOn(API, 'securePostCallbacks').and.callFake((_a, _b, cb) => cb('enc-phrase')); + spyOn(WalletStore, 'getPassword').and.returnValue('pw'); + spyOn(WalletCrypto, 'encrypt').and.callFake(d => `(enc:${d})`); + success = jasmine.createSpy('pairing code success'); + error = jasmine.createSpy('pairing code error'); + }); + + it('should make a pairing code', () => { + MyWallet.makePairingCode(success, error); + expect(success).toHaveBeenCalledWith('1|wallet-guid|(enc:shared-key|7077)'); + expect(error).not.toHaveBeenCalled(); + }); + + it('should call WalletCrypto.encrypt with the encryption phrase', () => { + MyWallet.makePairingCode(success, error); + expect(WalletCrypto.encrypt).toHaveBeenCalledWith('shared-key|7077', 'enc-phrase', 10); + expect(error).not.toHaveBeenCalled(); + }); + }); + + describe('login', () => { + callbacks = { + success () {}, + needsTwoFactorCode () {}, + wrongTwoFactorCode () {}, + authorizationRequired () {}, + otherError () {}, + newSessionToken () {} + }; + + beforeEach(() => { + spyOn(MyWallet, 'didFetchWallet').and.callFake(obj => + ({ + then (cb) { + obj.encrypted = undefined; + cb(obj); + return { + catch (cb) {} + }; + } + }) + ); + + spyOn(MyWallet, 'initializeWallet').and.callFake((inputedPassword, didDecrypt, didBuildHD) => + ({ + then (cb) { + if (inputedPassword === 'password') { + cb(); + } + return { + catch (cb) { + if (inputedPassword !== 'password') { + return cb('WRONG_PASSWORD'); + } + } + }; + } + }) + ); + + spyOn(callbacks, 'success'); + spyOn(WalletNetwork, 'establishSession').and.callThrough(); + spyOn(callbacks, 'wrongTwoFactorCode'); + spyOn(API, 'request').and.callThrough(); + spyOn(callbacks, 'authorizationRequired').and.callFake(cb => cb()); + }); + + describe('with a shared key', () => { + it('should not not use a session token', done => { + let promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: null, + sharedKey: 'shared-key' + }, + callbacks + ); + + expect(promise).toBeResolved(done); + expect(WalletNetwork.establishSession).not.toHaveBeenCalled(); + }); + + it('should return the guid', done => { + let promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: null, + sharedKey: 'shared-key' + }, + callbacks + ); + + expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: '1234'}), done); + }); + }); + + describe('without shared key', () => { + it('should use a session token', () => { + MyWallet.login( + '1234', + 'password', + { + twoFactor: null + }, + callbacks + ); + + expect(WalletNetwork.establishSession).toHaveBeenCalled(); + }); + + it('should return guid', done => { + let promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: null + }, + callbacks + ); + + expect(promise).toBeResolvedWith(jasmine.objectContaining( + {guid: '1234'} + ), done); + }); + + it('should reuse an existing session token if provided', () => { + MyWallet.login( + '1234', + 'password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + + expect(WalletNetwork.establishSession).toHaveBeenCalledWith('token'); + }); + + it('should not reuse a null token', () => { + MyWallet.login( + '1234', + 'password', + { + twoFactor: null, + sessionToken: undefined + }, + callbacks + ); + + expect(WalletNetwork.establishSession).not.toHaveBeenCalledWith(null); + }); + + it('should announce a new session token', () => { + spyOn(callbacks, 'newSessionToken'); + + MyWallet.login( + 'wallet-2fa', + 'password', + { + twoFactor: null, + sessionToken: null + }, + callbacks + ); + expect(callbacks.newSessionToken).toHaveBeenCalledWith('new_token'); + }); + + it('should ask for 2FA if applicable and include method', () => { + spyOn(callbacks, 'needsTwoFactorCode'); + + MyWallet.login( + 'wallet-2fa', + 'password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + expect(callbacks.needsTwoFactorCode).toHaveBeenCalledWith(1); + }); + }); + + describe('email authoritzation', () => { + let promise; + + beforeEach(() => { + spyOn(WalletNetwork, 'pollForSessionGUID').and.callThrough(); + + promise = MyWallet.login( + 'wallet-email-auth', + 'password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + }); + + it('should notify user if applicable', () => expect(callbacks.authorizationRequired).toHaveBeenCalled()); + + it('should start polling to check for authoritzation, using token', () => expect(WalletNetwork.pollForSessionGUID).toHaveBeenCalledWith('token')); + + it('should continue login after request is approved', done => + expect(promise).toBeResolvedWith(jasmine.objectContaining( + {guid: 'wallet-email-auth'} + ), done) + ); + }); + + describe('email authoritzation and 2FA', () => { + beforeEach(() => { + spyOn(WalletNetwork, 'pollForSessionGUID').and.callThrough(); + + MyWallet.login( + 'wallet-email-auth-2fa', + 'password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + }); + + it('should start polling to check for authoritzation, using token', () => expect(WalletNetwork.pollForSessionGUID).toHaveBeenCalledWith('token')); + + it('should ask for 2FA after email auth', done => + spyOn(callbacks, 'needsTwoFactorCode').and.callFake(method => { + expect(method).toEqual(1); + done(); + }) + ); + }); + + describe('with 2FA', () => { + beforeEach(() => spyOn(WalletNetwork, 'fetchWalletWithTwoFactor').and.callThrough()); + + it('should return guid', done => { + let promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: {type: 5, code: 'BF399'}, + sessionToken: 'token' + }, + callbacks + ); + + expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: '1234'}), done); + }); + + it('should call WalletNetwork.fetchWalletWithTwoFactor with the code and session token', done => { + let promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: {type: 5, code: 'BF399'}, + sessionToken: 'token' + }, + callbacks + ); + + expect(promise).toBeResolved(done); + expect(WalletNetwork.fetchWalletWithTwoFactor).toHaveBeenCalled(); + expect(WalletNetwork.fetchWalletWithTwoFactor.calls.argsFor(0)[2]).toEqual( + {type: 5, code: 'BF399'} + ); + expect(WalletNetwork.fetchWalletWithTwoFactor.calls.argsFor(0)[1]).toEqual('token'); + }); + + it('should not call fetchWalletWithTwoFactor() when null', done => { + let promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + + expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: '1234'}), done); + expect(WalletNetwork.fetchWalletWithTwoFactor).not.toHaveBeenCalled(); + }); + }); + + describe('wrong password', () => { + let promise; + + beforeEach(() => { + spyOn(WalletNetwork, 'fetchWallet').and.callThrough(); + + promise = MyWallet.login( + '1234', + 'wrong_password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + }); + + it('should fetch the wallet and throw an error', done => { + expect(promise).toBeRejectedWith('WRONG_PASSWORD', done); + expect(WalletNetwork.fetchWallet).toHaveBeenCalled(); + }); + + it('should not fetch wallet again at the next attempt', done => { + // Second attempt: + promise = MyWallet.login( + '1234', + 'password', + { + twoFactor: null, + sessionToken: 'token' + }, + callbacks + ); + + expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: '1234'}), done); + expect(WalletNetwork.fetchWallet.calls.count()).toEqual(1); + }); + }); + }); // First attempt only + + describe('didFetchWallet', () => { + beforeEach(() => spyOn(WalletStore, 'setEncryptedWalletData').and.callThrough()); + + it('should resolve', done => { + let promise = MyWallet.didFetchWallet({payload: ''}); + expect(promise).toBeResolved(done); + }); + + it('should update the wallet store', () => { + MyWallet.didFetchWallet({payload: 'encrypted'}); + expect(WalletStore.setEncryptedWalletData).toHaveBeenCalled(); + }); + + it("should not update the wallet store if there's no payload", () => { + MyWallet.didFetchWallet({}); + MyWallet.didFetchWallet({payload: ''}); + + expect(WalletStore.setEncryptedWalletData).not.toHaveBeenCalled(); + }); + + it("should not update the wallet store if payload is 'Not modified'", () => { + MyWallet.didFetchWallet({payload: 'Not modified'}); + + expect(WalletStore.setEncryptedWalletData).not.toHaveBeenCalled(); + }); + }); + + describe('initializeWallet', () => { + beforeEach(() => + spyOn(MyWallet, 'decryptAndInitializeWallet').and.callFake(() => { + MyWallet.wallet = { + loadExternal () { + return Promise.resolve(); + }, + incStats () { + return Promise.reject(); + }, + saveGUIDtoMetadata () { + return Promise.reject(); + } + }; + return Promise.resolve(); + }) + ); + + it('should call decryptAndInitializeWallet()', done => { + let check = () => expect(MyWallet.decryptAndInitializeWallet).toHaveBeenCalled(); + + let promise = MyWallet.initializeWallet().then(check); + expect(promise).toBeResolved(done); + }); + + it('should initialize wallet with stats failure', done => { + let promise = MyWallet.initializeWallet(); + expect(promise).toBeResolved(done); + }); + + it('should initialize wallet with saveGUID failure', done => { + let promise = MyWallet.initializeWallet(); + expect(promise).toBeResolved(done); + }); + }); + + describe('decryptAndInitializeWallet', () => { + beforeEach(() => spyOn(WalletCrypto, 'decryptWallet')); + + it('should call WalletCrypto.decryptWallet', () => { + MyWallet.decryptAndInitializeWallet(() => {}, () => {}); + expect(WalletCrypto.decryptWallet).toHaveBeenCalled(); + }); + }); + + describe('recoverFromMnemonic', () => { + beforeEach(() => { + spyOn(WalletSignup, 'generateNewWallet').and.callThrough(); + spyOn(WalletStore, 'unsafeSetPassword'); + }); + + it('should generate a new wallet', () => { + MyWallet.recoverFromMnemonic('a@b.com', 'secret', 'nuclear bunker sphaghetti monster dim sum sauce', undefined, () => {}); + expect(WalletSignup.generateNewWallet).toHaveBeenCalled(); + expect(WalletSignup.generateNewWallet.calls.argsFor(0)[0]).toEqual('secret'); + expect(WalletSignup.generateNewWallet.calls.argsFor(0)[1]).toEqual('a@b.com'); + expect(WalletSignup.generateNewWallet.calls.argsFor(0)[2]).toEqual('nuclear bunker sphaghetti monster dim sum sauce'); + }); + + it('should call unsafeSetPassword', () => { + MyWallet.recoverFromMnemonic('a@b.com', 'secret', 'nuclear bunker sphaghetti monster dim sum sauce', undefined, () => {}); + expect(WalletStore.unsafeSetPassword).toHaveBeenCalledWith('secret'); + }); + + it('should pass guid, shared key, password and session token upon success', done => { + let obs = { + success () {} + }; + spyOn(obs, 'success').and.callThrough(); + + MyWallet.recoverFromMnemonic('a@b.com', 'secret', 'nuclear bunker sphaghetti monster dim sum sauce', undefined, obs.success); + + let result = () => { + expect(obs.success).toHaveBeenCalledWith({ guid: '1234', sharedKey: 'shared', password: 'secret', sessionToken: 'new_token' }); + done(); + }; + + setTimeout(result, 1); + }); + + it('should scan address space', () => { + spyOn(hdwallet, 'scanBip44').and.callThrough(); + MyWallet.recoverFromMnemonic('a@b.com', 'secret', 'nuclear bunker sphaghetti monster dim sum sauce', undefined, () => {}); + expect(hdwallet.scanBip44).toHaveBeenCalled(); + }); + }); + + describe('createNewWallet', () => { + beforeEach(() => { + spyOn(BIP39, 'generateMnemonic').and.callThrough(); + spyOn(RNG, 'run').and.callThrough(); + }); + + it('should call BIP39.generateMnemonic with our RNG', () => { + MyWallet.createNewWallet(); + expect(BIP39.generateMnemonic).toHaveBeenCalled(); + expect(RNG.run).toHaveBeenCalled(); + }); + + it('should call errorCallback if RNG throws', done => { + // E.g. because there was a network failure. + // This assumes BIP39.generateMnemonic does not rescue a throw + // inside the RNG + + let observers = + {error () { done(); }}; + + spyOn(observers, 'error').and.callThrough(); + + RNG.shouldThrow = true; + MyWallet.createNewWallet('a@b.com', '1234', 'My Wallet', 'en', 'usd', observers.success, observers.error); + expect(observers.error).toHaveBeenCalledWith('Connection failed'); + + RNG.shouldThrow = false; + }); + + describe('when the wallet insertion fails', () => { + let observers = null; + + beforeEach(done => { + observers = { + success () { done(); }, + error () { done(); } + }; + + spyOn(observers, 'success').and.callThrough(); + spyOn(observers, 'error').and.callThrough(); + + WalletNetwork.failInsertion = true; + return MyWallet.createNewWallet('a@b.com', '1234', 'My Wallet', 'en', 'usd', observers.success, observers.error); + }); + + it('should fail', () => { + expect(observers.success).not.toHaveBeenCalled(); + expect(observers.error).toHaveBeenCalled(); + }); + + afterEach(() => { WalletNetwork.failInsertion = false; }); + }); + }); +}); diff --git a/tests/wallet_spec.js.coffee b/tests/wallet_spec.js.coffee deleted file mode 100644 index 96f870c09..000000000 --- a/tests/wallet_spec.js.coffee +++ /dev/null @@ -1,598 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -walletStoreGuid = undefined -walletStoreEncryptedWalletData = undefined -WalletStore = { - setGuid: (guid) -> - walletStoreGuid = guid - getGuid: () -> - walletStoreGuid - setRealAuthType: () -> - setSyncPubKeys: () -> - setLanguage: () -> - setEncryptedWalletData: (data) -> - walletStoreEncryptedWalletData = data - getEncryptedWalletData: () -> walletStoreEncryptedWalletData || "encrypted" -} - -BlockchainSettingsAPI = { - changeLanguage: () -> - changeLocalCurrency: () -> -} - -WalletCrypto = { - decryptWallet: () -> -} - -hdwallet = { - guid: "1234", - sharedKey: "shared" - scanBip44: () -> { - then: (cb) -> - cb() - { - catch: () -> - } - } -} - -WalletSignup = { - generateNewWallet: (inputedPassword, inputedEmail, mnemonic, bip39Password, firstAccountName, successCallback, errorCallback) -> - successCallback(hdwallet) -} - -API = - securePostCallbacks: () -> - request: (action, method, data, withCred) -> - -WalletNetwork = - insertWallet: () -> - console.log(WalletNetwork.failInsertion) - if WalletNetwork.failInsertion - new Promise((resolve, reject) -> reject()) - else - new Promise((resolve) -> resolve()) - - establishSession: (token) -> - then: (cb) -> - if token != "token" - token = "new_token" - cb(token) - { - catch: (cb) -> - } - - fetchWallet: (guid, sessionToken, needsTwoFactorCode, authorizationRequired) -> - then: (cb) -> - if guid == "wallet-2fa" - needsTwoFactorCode(1) - else if guid == "wallet-email-auth" - authorizationRequired().then(() -> - # WalletNetwork proceeds with login and then calls success: - cb({guid: guid, payload: "encrypted"}) - ) - else if guid == "wallet-email-auth-2fa" - authorizationRequired().then(() -> - # WalletNetwork proceeds with login and now asks for 2FA: - needsTwoFactorCode(1) - ) - else - WalletStore.setGuid(guid) - WalletStore.setEncryptedWalletData("encrypted") - cb({guid: guid, payload: "encrypted"}) - { - catch: (cb) -> - } - - fetchWalletWithSharedKey: (guid) -> - then: (cb) -> - WalletStore.setGuid(guid) - WalletStore.setEncryptedWalletData("encrypted") - cb({guid: guid, payload: "encrypted"}) - { - catch: (cb) -> - } - - fetchWalletWithTwoFactor: (guid, sessionToken, twoFactorCode) -> - then: (cb) -> - WalletStore.setGuid(guid) - WalletStore.setEncryptedWalletData("encrypted") - cb({guid: guid, payload: "encrypted"}) - { - catch: (cb) -> - } - - pollForSessionGUID: (token) -> - then: (cb) -> - cb() - { - catch: (cb) -> - } - -BIP39 = { - generateMnemonic: (str, rng, wlist) -> - mnemonic = "bicycle balcony prefer kid flower pole goose crouch century lady worry flavor" - seed = rng(32) - if seed = "random" then mnemonic else "failure" -} - -RNG = { - run: (input) -> - if RNG.shouldThrow - throw new Error('Connection failed'); - "random" -} - -stubs = { - './wallet-store': WalletStore, - './wallet-crypto': WalletCrypto, - './wallet-signup': WalletSignup, - './api': API, - './wallet-network' : WalletNetwork, - 'bip39': BIP39, - './rng' : RNG, - './blockchain-settings-api' : BlockchainSettingsAPI -} - -MyWallet = proxyquire('../src/wallet', stubs) - -describe "Wallet", -> - - callbacks = undefined - - beforeEach -> - JasminePromiseMatchers.install() - WalletStore.setGuid(undefined) - WalletStore.setEncryptedWalletData(undefined) - - afterEach -> - JasminePromiseMatchers.uninstall() - - describe "makePairingCode()", -> - success = undefined - error = undefined - - beforeEach -> - MyWallet.wallet = - guid: 'wallet-guid' - sharedKey: 'shared-key' - spyOn(API, 'securePostCallbacks').and.callFake((_a, _b, cb) -> cb('enc-phrase')) - spyOn(WalletStore, 'getPassword').and.returnValue('pw') - spyOn(WalletCrypto, 'encrypt').and.callFake((d) -> "(enc:#{d})") - success = jasmine.createSpy('pairing code success') - error = jasmine.createSpy('pairing code error') - - it "should make a pairing code", -> - MyWallet.makePairingCode(success, error) - expect(success).toHaveBeenCalledWith('1|wallet-guid|(enc:shared-key|7077)') - expect(error).not.toHaveBeenCalled() - - it "should call WalletCrypto.encrypt with the encryption phrase", -> - MyWallet.makePairingCode(success, error) - expect(WalletCrypto.encrypt).toHaveBeenCalledWith('shared-key|7077', 'enc-phrase', 10) - expect(error).not.toHaveBeenCalled() - - describe "login", -> - - callbacks = { - success: () -> - needsTwoFactorCode: () -> - wrongTwoFactorCode: () -> - authorizationRequired: () -> - otherError: () -> - newSessionToken: () -> - } - - beforeEach -> - - spyOn(MyWallet, "didFetchWallet").and.callFake((obj) -> - { - then: (cb) -> - obj.encrypted = undefined - cb(obj) - { - catch: (cb) -> - } - } - ) - - spyOn(MyWallet,"initializeWallet").and.callFake((inputedPassword, didDecrypt, didBuildHD) -> - { - then: (cb) -> - if(inputedPassword == "password") - cb() - { - catch: (cb) -> - if(inputedPassword != "password") - cb("WRONG_PASSWORD") - } - } - ) - - spyOn(callbacks, "success") - spyOn(WalletNetwork, "establishSession").and.callThrough() - spyOn(callbacks, "wrongTwoFactorCode") - spyOn(API, "request").and.callThrough() - spyOn(callbacks, "authorizationRequired").and.callFake((cb) -> - cb() - ) - - describe "with a shared key", -> - it "should not not use a session token", (done) -> - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: null, - sharedKey: "shared-key" - }, - callbacks - ) - - expect(promise).toBeResolved(done) - expect(WalletNetwork.establishSession).not.toHaveBeenCalled() - - it "should return the guid", (done) -> - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: null, - sharedKey: "shared-key" - }, - callbacks - ) - - expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: "1234"}), done) - - describe "without shared key", -> - - it "should use a session token", -> - MyWallet.login( - "1234", - "password", - { - twoFactor: null - }, - callbacks - ) - - expect(WalletNetwork.establishSession).toHaveBeenCalled() - - it "should return guid", (done) -> - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: null - }, - callbacks - ) - - expect(promise).toBeResolvedWith(jasmine.objectContaining( - {guid: "1234"} - ), done) - - - it "should reuse an existing session token if provided", -> - - MyWallet.login( - "1234", - "password", - { - twoFactor: null, - sessionToken: "token" - }, - callbacks - ) - - expect(WalletNetwork.establishSession).toHaveBeenCalledWith("token") - - it "should not reuse a null token", -> - MyWallet.login( - "1234", - "password", - { - twoFactor: null, - sessionToken: undefined - }, - callbacks - ) - - expect(WalletNetwork.establishSession).not.toHaveBeenCalledWith(null) - - it "should announce a new session token", -> - spyOn(callbacks, "newSessionToken") - - MyWallet.login( - "wallet-2fa", - "password", - { - twoFactor: null, - sessionToken: null - }, - callbacks - ) - expect(callbacks.newSessionToken).toHaveBeenCalledWith("new_token") - - it "should ask for 2FA if applicable and include method", -> - spyOn(callbacks, "needsTwoFactorCode") - - MyWallet.login( - "wallet-2fa", - "password", - { - twoFactor: null, - sessionToken: "token" - }, - callbacks - ) - expect(callbacks.needsTwoFactorCode).toHaveBeenCalledWith(1) - - describe "email authoritzation", -> - promise = undefined - - beforeEach -> - spyOn(WalletNetwork, "pollForSessionGUID").and.callThrough() - - promise = MyWallet.login( - "wallet-email-auth", - "password", - { - twoFactor: null, - sessionToken: "token" - }, - callbacks - ) - - it "should notify user if applicable", -> - expect(callbacks.authorizationRequired).toHaveBeenCalled() - - it "should start polling to check for authoritzation, using token", -> - expect(WalletNetwork.pollForSessionGUID).toHaveBeenCalledWith("token") - - it "should continue login after request is approved", (done) -> - expect(promise).toBeResolvedWith(jasmine.objectContaining( - {guid: "wallet-email-auth"} - ), done) - - describe "email authoritzation and 2FA", -> - promise = undefined - beforeEach -> - spyOn(WalletNetwork, "pollForSessionGUID").and.callThrough() - - promise = MyWallet.login( - "wallet-email-auth-2fa", - "password", - { - twoFactor: null, - sessionToken: "token" - }, - callbacks - ) - - it "should start polling to check for authoritzation, using token", -> - expect(WalletNetwork.pollForSessionGUID).toHaveBeenCalledWith("token") - - - it "should ask for 2FA after email auth", (done) -> - spyOn(callbacks, "needsTwoFactorCode").and.callFake((method) -> - expect(method).toEqual(1) - done() - ) - - describe "with 2FA", -> - beforeEach -> - spyOn(WalletNetwork, "fetchWalletWithTwoFactor").and.callThrough() - - it "should return guid", (done) -> - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: {type: 5, code: "BF399"} - sessionToken: "token" - }, - callbacks - ) - - expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: "1234"}), done) - - it "should call WalletNetwork.fetchWalletWithTwoFactor with the code and session token", (done) -> - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: {type: 5, code: "BF399"} - sessionToken: "token" - }, - callbacks - ) - - expect(promise).toBeResolved(done) - expect(WalletNetwork.fetchWalletWithTwoFactor).toHaveBeenCalled() - expect(WalletNetwork.fetchWalletWithTwoFactor.calls.argsFor(0)[2]).toEqual( - {type: 5, code: "BF399"} - ) - expect(WalletNetwork.fetchWalletWithTwoFactor.calls.argsFor(0)[1]).toEqual("token") - - - it "should not call fetchWalletWithTwoFactor() when null", (done) -> - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: null - sessionToken: "token" - }, - callbacks - ) - - expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: "1234"}), done) - expect(WalletNetwork.fetchWalletWithTwoFactor).not.toHaveBeenCalled() - - describe "wrong password", -> - promise = undefined - - beforeEach -> - spyOn(WalletNetwork, "fetchWallet").and.callThrough() - - promise = MyWallet.login( - "1234", - "wrong_password", - { - twoFactor: null - sessionToken: "token" - }, - callbacks - ) - - it "should fetch the wallet and throw an error", (done) -> - expect(promise).toBeRejectedWith("WRONG_PASSWORD", done) - expect(WalletNetwork.fetchWallet).toHaveBeenCalled() - - it "should not fetch wallet again at the next attempt", (done) -> - # Second attempt: - promise = MyWallet.login( - "1234", - "password", - { - twoFactor: null - sessionToken: "token" - }, - callbacks - ) - - expect(promise).toBeResolvedWith(jasmine.objectContaining({guid: "1234"}), done) - expect(WalletNetwork.fetchWallet.calls.count()).toEqual(1) # First attempt only - - describe "didFetchWallet", -> - beforeEach -> - spyOn(WalletStore, "setEncryptedWalletData").and.callThrough() - - it "should resolve", (done) -> - promise = MyWallet.didFetchWallet({payload: ""}) - expect(promise).toBeResolved(done) - - it "should update the wallet store", -> - MyWallet.didFetchWallet({payload: "encrypted"}) - expect(WalletStore.setEncryptedWalletData).toHaveBeenCalled() - - it "should not update the wallet store if there's no payload", -> - MyWallet.didFetchWallet({}) - MyWallet.didFetchWallet({payload: ""}) - - expect(WalletStore.setEncryptedWalletData).not.toHaveBeenCalled() - - it "should not update the wallet store if payload is 'Not modified'", -> - MyWallet.didFetchWallet({payload: "Not modified"}) - - expect(WalletStore.setEncryptedWalletData).not.toHaveBeenCalled() - - describe "initializeWallet", -> - beforeEach -> - spyOn(MyWallet, "decryptAndInitializeWallet").and.callFake(() -> - MyWallet.wallet = { - loadExternal: () -> - Promise.resolve() - } - Promise.resolve() - ) - - it "should call decryptAndInitializeWallet()", (done) -> - check = () -> - expect(MyWallet.decryptAndInitializeWallet).toHaveBeenCalled() - - promise = MyWallet.initializeWallet().then(check) - expect(promise).toBeResolved(done) - - describe "decryptAndInitializeWallet", -> - beforeEach -> - spyOn(WalletCrypto, "decryptWallet") - - it "should call WalletCrypto.decryptWallet", -> - MyWallet.decryptAndInitializeWallet((() ->), (() ->)) - expect(WalletCrypto.decryptWallet).toHaveBeenCalled() - - describe "recoverFromMnemonic", -> - beforeEach -> - spyOn(WalletSignup, "generateNewWallet").and.callThrough() - spyOn(WalletStore, "unsafeSetPassword") - - it "should generate a new wallet", -> - MyWallet.recoverFromMnemonic("a@b.com", "secret", "nuclear bunker sphaghetti monster dim sum sauce", undefined, (() ->)) - expect(WalletSignup.generateNewWallet).toHaveBeenCalled() - expect(WalletSignup.generateNewWallet.calls.argsFor(0)[0]).toEqual("secret") - expect(WalletSignup.generateNewWallet.calls.argsFor(0)[1]).toEqual("a@b.com") - expect(WalletSignup.generateNewWallet.calls.argsFor(0)[2]).toEqual("nuclear bunker sphaghetti monster dim sum sauce") - - it "should call unsafeSetPassword", -> - MyWallet.recoverFromMnemonic("a@b.com", "secret", "nuclear bunker sphaghetti monster dim sum sauce", undefined, (() ->)) - expect(WalletStore.unsafeSetPassword).toHaveBeenCalledWith("secret") - - it "should pass guid, shared key, password and session token upon success", (done) -> - obs = { - success: () -> - } - spyOn(obs, "success").and.callThrough() - - MyWallet.recoverFromMnemonic("a@b.com", "secret", "nuclear bunker sphaghetti monster dim sum sauce", undefined, obs.success) - - result = () -> - expect(obs.success).toHaveBeenCalledWith({ guid: '1234', sharedKey: 'shared', password: 'secret', sessionToken: 'new_token' }) - done() - - setTimeout(result, 1) - - it "should scan address space", -> - spyOn(hdwallet, "scanBip44").and.callThrough() - MyWallet.recoverFromMnemonic("a@b.com", "secret", "nuclear bunker sphaghetti monster dim sum sauce", undefined, (() ->)) - expect(hdwallet.scanBip44).toHaveBeenCalled() - - describe "createNewWallet", -> - beforeEach -> - spyOn(BIP39, "generateMnemonic").and.callThrough() - spyOn(RNG, "run").and.callThrough() - - it "should call BIP39.generateMnemonic with our RNG", -> - w = MyWallet.createNewWallet() - expect(BIP39.generateMnemonic).toHaveBeenCalled() - expect(RNG.run).toHaveBeenCalled() - - it "should call errorCallback if RNG throws", (done) -> - # E.g. because there was a network failure. - # This assumes BIP39.generateMnemonic does not rescue a throw - # inside the RNG - - observers = - error: () -> done() - - spyOn(observers, "error").and.callThrough() - - RNG.shouldThrow = true - MyWallet.createNewWallet('a@b.com', "1234", 'My Wallet', 'en', 'usd', observers.success, observers.error) - expect(observers.error).toHaveBeenCalledWith('Connection failed') - - RNG.shouldThrow = false - - describe "when the wallet insertion fails", -> - - observers = null - - beforeEach (done) -> - observers = - success: () -> done() - error: () -> done() - - spyOn(observers, "success").and.callThrough() - spyOn(observers, "error").and.callThrough() - - WalletNetwork.failInsertion = true - MyWallet.createNewWallet('a@b.com', "1234", 'My Wallet', 'en', 'usd', observers.success, observers.error) - - it "should fail", -> - expect(observers.success).not.toHaveBeenCalled() - expect(observers.error).toHaveBeenCalled() - - afterEach -> - WalletNetwork.failInsertion = false diff --git a/tests/wallet_token_endpoints.js b/tests/wallet_token_endpoints.js new file mode 100644 index 000000000..b2e29bc25 --- /dev/null +++ b/tests/wallet_token_endpoints.js @@ -0,0 +1,146 @@ +let proxyquire = require('proxyquireify')(require); + +describe('token endpoints', () => { + let API = { + request (action, method, data, withCred) { + return new Promise((resolve, reject) => { + if ((data.token === 'token') || (data.token === 'authorize-approve-token')) { + resolve({success: true, guid: '1234'}); + } else if (data.token === 'authorize-approve-token-different-browser') { + if (data.confirm_approval != null) { + resolve({success: true}); + } else { + resolve({success: null}); + } + } else if (data.token === 'token-fail-200') { + resolve({success: false, error: 'Invalid Token'}); + } else if (data.token === 'token-fail-500') { + reject('Server error'); + } else { + reject('Unknown'); + } + }); + } + }; + + let Helpers = {}; + + let WTE = proxyquire('../src/wallet-token-endpoints', { + './api': API, + './helpers': Helpers + }); + + describe('postTokenEndpoint', () => { + beforeEach(() => spyOn(API, 'request').and.callThrough()); + + it('should require a token and extra params', () => { + expect(() => WTE.postTokenEndpoint('method')).toThrow(); + expect(() => WTE.postTokenEndpoint('method', 'token')).toThrow(); + expect(() => WTE.postTokenEndpoint('method', 'token', {})).not.toThrow(); + }); + + it('should submit a token', done => { + let promise = WTE.postTokenEndpoint('method', 'token', {}); + expect(promise).toBeResolved(done); + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].token).toEqual('token'); + }); + + it('should call success with the guid', done => { + let promise = WTE.postTokenEndpoint('method', 'token', {}); + expect(promise).toBeResolvedWith(jasmine.objectContaining({ success: true, guid: '1234' }), done); + }); + + it('should errorCallback if server returns 500', done => { + let promise = WTE.postTokenEndpoint('method', 'token-fail-500', {}); + expect(promise).toBeRejectedWith('Server error', done); + }); + + it('should errorCallback message if server returns 200 and {success: false}', done => { + let promise = WTE.postTokenEndpoint('method', 'token-fail-200', {}); + expect(promise).toBeRejectedWith(jasmine.objectContaining({ success: false, error: 'Invalid Token' }), done); + }); + }); + + describe('verifyEmail', () => { + it('should submit a token', done => { + spyOn(API, 'request').and.callThrough(); + let promise = WTE.verifyEmail('token'); + expect(promise).toBeResolved(done); + + expect(API.request).toHaveBeenCalled(); + expect(API.request.calls.argsFor(0)[2].token).toEqual('token'); + }); + + it('shoud call postTokenEndpoint with token', done => { + spyOn(WTE, 'postTokenEndpoint').and.callThrough(); + let promise = WTE.verifyEmail('token'); + expect(promise).toBeResolved(done); + + expect(WTE.postTokenEndpoint).toHaveBeenCalled(); + expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual('token'); + }); + }); + + describe('unsubscribe', () => + + it('shoud call postTokenEndpoint with token', done => { + spyOn(WTE, 'postTokenEndpoint').and.callThrough(); + let promise = WTE.unsubscribe('token'); + expect(promise).toBeResolved(done); + + expect(WTE.postTokenEndpoint).toHaveBeenCalled(); + expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual('token'); + }) + ); + + describe('resetTwoFactor', () => + + it('shoud call postTokenEndpoint with token', done => { + spyOn(WTE, 'postTokenEndpoint').and.callThrough(); + let promise = WTE.resetTwoFactor('token'); + expect(promise).toBeResolved(done); + + expect(WTE.postTokenEndpoint).toHaveBeenCalled(); + expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual('token'); + }) + ); + + describe('authorizeApprove', () => { + let callbacks = + {differentBrowser () {}}; + + beforeEach(() => spyOn(callbacks, 'differentBrowser')); + + it('shoud call postTokenEndpoint with token', done => { + spyOn(WTE, 'postTokenEndpoint').and.callThrough(); + let promise = WTE.authorizeApprove('token'); + expect(promise).toBeResolved(done); + + expect(WTE.postTokenEndpoint).toHaveBeenCalled(); + expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual('token'); + }); + + it('should resolve if same browser', done => { + let promise = WTE.authorizeApprove('authorize-approve-token'); + expect(promise).toBeResolvedWith(jasmine.objectContaining({ success: true, guid: '1234' }), done); + }); + + it('should differentBrowserCallback', done => { + let promise = WTE.authorizeApprove('authorize-approve-token-different-browser', callbacks.differentBrowser) + .then(() => expect(callbacks.differentBrowser).toHaveBeenCalled()); + expect(promise).toBeResolved(done); + }); + + it('should pass differentBrowserApproved along', done => { + let promise = WTE.authorizeApprove('authorize-approve-token-different-browser', callbacks.differentBrowser, true) + .then(() => expect(callbacks.differentBrowser).not.toHaveBeenCalled()); + expect(promise).toBeResolved(done); + }); + + it('should also consider a rejected browser success', done => { + let promise = WTE.authorizeApprove('authorize-approve-token-different-browser', callbacks.differentBrowser, false); + expect(promise).toBeResolvedWith(jasmine.objectContaining({ success: true }), done); + }); + }); +}); diff --git a/tests/wallet_token_endpoints.js.coffee b/tests/wallet_token_endpoints.js.coffee deleted file mode 100644 index 3d4fe3bca..000000000 --- a/tests/wallet_token_endpoints.js.coffee +++ /dev/null @@ -1,138 +0,0 @@ -proxyquire = require('proxyquireify')(require) - -describe "token endpoints", -> - beforeEach -> - JasminePromiseMatchers.install() - - afterEach -> - JasminePromiseMatchers.uninstall() - - API = { - request: (action, method, data, withCred) -> - new Promise (resolve, reject) -> - if data.token == "token" || data.token == "authorize-approve-token" - resolve({success: true, guid: '1234'}) - else if data.token == "authorize-approve-token-different-browser" - if data.confirm_approval? - resolve({success: true}) - else - resolve({success: null}) - else if data.token == "token-fail-200" - resolve({success: false, error: "Invalid Token"}) - else if data.token == "token-fail-500" - reject("Server error") - else - reject("Unknown") - } - - Helpers = {} - - WTE = proxyquire('../src/wallet-token-endpoints', { - './api': API, - './helpers': Helpers - }) - - describe "postTokenEndpoint", -> - - beforeEach -> - spyOn(API, "request").and.callThrough() - - it "should require a token and extra params", -> - expect(() -> WTE.postTokenEndpoint("method")).toThrow() - expect(() -> WTE.postTokenEndpoint("method", "token")).toThrow() - expect(() -> WTE.postTokenEndpoint("method", "token", {})).not.toThrow() - - it "should submit a token", (done) -> - promise = WTE.postTokenEndpoint("method", "token", {}) - expect(promise).toBeResolved(done) - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].token).toEqual('token') - - it "should call success with the guid", (done) -> - promise = WTE.postTokenEndpoint("method", "token", {}) - expect(promise).toBeResolvedWith(jasmine.objectContaining({ success: true, guid: "1234"}), done) - - it "should errorCallback if server returns 500", (done) -> - promise = WTE.postTokenEndpoint("method", "token-fail-500", {}) - expect(promise).toBeRejectedWith('Server error', done) - - it "should errorCallback message if server returns 200 and {success: false}", (done) -> - promise = WTE.postTokenEndpoint("method", "token-fail-200", {}) - expect(promise).toBeRejectedWith(jasmine.objectContaining({ success: false, error: 'Invalid Token' }), done) - - describe "verifyEmail", -> - - it "should submit a token", (done) -> - spyOn(API, "request").and.callThrough() - promise = WTE.verifyEmail("token") - expect(promise).toBeResolved(done) - - expect(API.request).toHaveBeenCalled() - expect(API.request.calls.argsFor(0)[2].token).toEqual('token') - - it "shoud call postTokenEndpoint with token", (done) -> - spyOn(WTE, "postTokenEndpoint").and.callThrough() - promise = WTE.verifyEmail("token") - expect(promise).toBeResolved(done) - - expect(WTE.postTokenEndpoint).toHaveBeenCalled() - expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual("token") - - describe "unsubscribe", -> - - it "shoud call postTokenEndpoint with token", (done) -> - spyOn(WTE, "postTokenEndpoint").and.callThrough() - promise = WTE.unsubscribe("token") - expect(promise).toBeResolved(done) - - expect(WTE.postTokenEndpoint).toHaveBeenCalled() - expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual("token") - - describe "resetTwoFactor", -> - - it "shoud call postTokenEndpoint with token", (done) -> - spyOn(WTE, "postTokenEndpoint").and.callThrough() - promise = WTE.resetTwoFactor("token") - expect(promise).toBeResolved(done) - - expect(WTE.postTokenEndpoint).toHaveBeenCalled() - expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual("token") - - describe "authorizeApprove", -> - callbacks = - differentBrowser: () -> - - beforeEach -> - spyOn(callbacks, "differentBrowser") - - it "shoud call postTokenEndpoint with token", (done) -> - spyOn(WTE, "postTokenEndpoint").and.callThrough() - promise = WTE.authorizeApprove("token") - expect(promise).toBeResolved(done) - - expect(WTE.postTokenEndpoint).toHaveBeenCalled() - expect(WTE.postTokenEndpoint.calls.argsFor(0)[1]).toEqual("token") - - it "should resolve if same browser", (done) -> - promise = WTE.authorizeApprove("authorize-approve-token") - expect(promise).toBeResolvedWith(jasmine.objectContaining({ success: true, guid: "1234"}), done) - - it "should differentBrowserCallback", (done) -> - promise = WTE.authorizeApprove("authorize-approve-token-different-browser", callbacks.differentBrowser) - .then(() -> - expect(callbacks.differentBrowser).toHaveBeenCalled() - ) - expect(promise).toBeResolved(done) - - it "should pass differentBrowserApproved along", (done) -> - - promise = WTE.authorizeApprove("authorize-approve-token-different-browser", callbacks.differentBrowser, true) - .then(() -> - expect(callbacks.differentBrowser).not.toHaveBeenCalled() - ) - expect(promise).toBeResolved(done) - - it "should also consider a rejected browser success", (done) -> - - promise = WTE.authorizeApprove("authorize-approve-token-different-browser", callbacks.differentBrowser, false) - expect(promise).toBeResolvedWith(jasmine.objectContaining({ success: true}),done) diff --git a/tests/wallet_transaction_spec.js b/tests/wallet_transaction_spec.js new file mode 100644 index 000000000..789d9fc1f --- /dev/null +++ b/tests/wallet_transaction_spec.js @@ -0,0 +1,160 @@ + +let proxyquire = require('proxyquireify')(require); + +let MyWallet = { + wallet: { + getNote (hash) { return null; }, + containsLegacyAddress (addr) { return (addr === '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa') || (addr === '1GpQRQAAMGuQ3AMcivFHDCPnV69qtQ65RZ') || (addr === '16GJsXtwq5JsYDAFaTXGMgj4W178nT3Pqj'); }, + hdwallet: { + account () { return { index: 0, label: 'Savings' }; } + }, + + key (addr) { + if (addr === '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa') { + return { label: 'Genesis', isWatchOnly: true, archived: false }; + } else if (addr === '1GpQRQAAMGuQ3AMcivFHDCPnV69qtQ65RZ') { + return { label: 'Legacy Addr 1', isWatchOnly: false, archived: false }; + } else if (addr === '16GJsXtwq5JsYDAFaTXGMgj4W178nT3Pqj') { + return { label: 'Legacy Addr 2', isWatchOnly: false, archived: false }; + } + }, + + latestBlock: { height: 399680 }, + getAddressBookLabel (address) { return 'addressBookLabel'; } + } +}; + +let transactions = require('./data/transactions'); + +let Tx = proxyquire('../src/wallet-transaction', { + './wallet': MyWallet +}); + +describe('Transaction', () => { + describe('new', () => + it('should create an empty transaction given no argument', () => { + let tx = new Tx(); + expect(tx.inputs.length).toEqual(0); + expect(tx.out.length).toEqual(0); + expect(tx.confirmations).toEqual(0); + }) + ); + + describe('default', () => { + it('should be recognized', () => { + let tx = Tx.factory(transactions['default']); + expect(tx.processedInputs.length).toBe(2); + expect(tx.processedOutputs.length).toBe(1); + expect(tx.processedInputs[0].address).toBe('1AaFJUs2XY1sGGg7p7ucJSZEJF3zB6r4Eh'); + expect(tx.fromWatchOnly).toBeTruthy(); + expect(tx.toWatchOnly).toBeFalsy(); + expect(tx.txType).toEqual('sent'); + expect(tx.amount).toEqual(-30000); + expect(tx.from.address).toEqual('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'); + expect(tx.to[0].address).toEqual('1M5hoG1pCTDsPqZwG6WH25ziwYYaXNMLrU'); + }); + + it('should have a fee equal to the difference between inputs and outputs value', () => { + let tx = Tx.factory(transactions['default']); + + expect(tx.fee).toEqual(10000); + }); + + it('should have a correct number of confirmations', () => { + let tx = Tx.factory(transactions['default']); + + expect(tx.confirmations).toEqual(3); + }); + + it('should have correctly tagged outputs', () => { + let tx = Tx.factory(transactions['default']); + expect(tx.processedOutputs[0].coinType).toEqual('external'); + }); + + it('should have correctly tagged inputs', () => { + let tx = Tx.factory(transactions['default']); + expect(tx.processedInputs[0].coinType).toEqual('0/1/15'); + expect(tx.processedInputs[1].coinType).toEqual('legacy'); + expect(tx.belongsTo('imported')).toEqual(true); + }); + }); + + describe('coinbase', () => { + it('should be recognized', () => { + let tx = Tx.factory(transactions['coinbase']); + expect(tx.processedInputs.length).toBe(1); + expect(tx.processedOutputs.length).toBe(1); + expect(tx.processedInputs[0].address).toBe('Coinbase'); + expect(tx.processedInputs[0].amount).toBe(100000); + expect(tx.txType).toEqual('received'); + expect(tx.amount).toEqual(100000); + }); + + it('should have a fee equal to 0', () => { + let tx = Tx.factory(transactions['coinbase']); + + expect(tx.fee).toEqual(0); + }); + }); + + describe('internal', () => { + it('should be recognized', () => { + let tx = Tx.factory(transactions['internal']); + expect(tx.processedInputs.length).toBe(1); + expect(tx.processedOutputs.length).toBe(2); + expect(tx.fromWatchOnly).toBeFalsy(); + expect(tx.toWatchOnly).toBeFalsy(); + }); + + it('should have a fee equal to 10000', () => { + let tx = Tx.factory(transactions['internal']); + expect(tx.fee).toEqual(10000); + }); + + it('should be categorized as a transfer', () => { + let tx = Tx.factory(transactions['internal']); + expect(tx.txType).toEqual('transfer'); + expect(tx.changeAmount).toEqual(10000); + expect(tx.amount).toEqual(80000); + }); + }); + + describe('from imported address to imported address', () => + it('should have the right amount', () => { + let tx = Tx.factory(transactions['from_imported']); + expect(tx.processedInputs.length).toBe(1); + expect(tx.processedOutputs.length).toBe(2); + expect(tx.fromWatchOnly).toBeFalsy(); + expect(tx.toWatchOnly).toBeFalsy(); + expect(tx.txType).toEqual('transfer'); + expect(tx.fee).toEqual(10000); + expect(tx.amount).toEqual(4968); + }) + ); + + describe('factory', () => + it('should not touch already existing objects', () => { + let tx = Tx.factory(transactions['coinbase']); + let fromFactory = Tx.factory(tx); + expect(tx).toEqual(fromFactory); + }) + ); + + describe('IOSfactory', () => + it('should create valid iOS objects', () => { + let tx = Tx.factory(transactions['default']); + let ios = Tx.IOSfactory(tx); + expect(ios.time).toEqual(tx.time); + expect(ios.result).toEqual(tx.result); + expect(ios.amount).toEqual(tx.amount); + expect(ios.confirmations).toEqual(tx.confirmations); + expect(ios.myHash).toEqual(tx.hash); + expect(ios.txType).toEqual(tx.txType); + expect(ios.block_height).toEqual(tx.block_height); + expect(ios.fromWatchOnly).toEqual(tx.fromWatchOnly); + expect(ios.toWatchOnly).toEqual(tx.toWatchOnly); + expect(ios.to).toEqual(tx.to); + expect(ios.from).toEqual(tx.from); + }) + ); +}); diff --git a/tests/wallet_transaction_spec.js.coffee b/tests/wallet_transaction_spec.js.coffee deleted file mode 100644 index cf417830e..000000000 --- a/tests/wallet_transaction_spec.js.coffee +++ /dev/null @@ -1,118 +0,0 @@ - -proxyquire = require('proxyquireify')(require) - -MyWallet = - wallet: - getNote: (hash) -> null - containsLegacyAddress: (addr) -> addr == "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" - hdwallet: - account: () -> { index: 0, label: "Savings" } - - key: () -> { label: "Genesis", isWatchOnly: true } - latestBlock: { height: 399680 } - getAddressBookLabel: (address) -> "addressBookLabel" - -transactions = require('./data/transactions') - -Tx = proxyquire('../src/wallet-transaction', { - './wallet': MyWallet -}) - -describe 'Transaction', -> - - describe "new", -> - it "should create an empty transaction given no argument", -> - tx = new Tx() - expect(tx.inputs.length).toEqual(0) - expect(tx.out.length).toEqual(0) - expect(tx.confirmations).toEqual(0) - - describe "default", -> - it "should be recognized", -> - tx = Tx.factory(transactions["default"]) - expect(tx.processedInputs.length).toBe(2) - expect(tx.processedOutputs.length).toBe(1) - expect(tx.processedInputs[0].address).toBe("1AaFJUs2XY1sGGg7p7ucJSZEJF3zB6r4Eh") - expect(tx.fromWatchOnly).toBeTruthy() - expect(tx.toWatchOnly).toBeFalsy() - expect(tx.txType).toEqual("sent") - expect(tx.amount).toEqual(-30000) - expect(tx.from.address).toEqual("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa") - expect(tx.to[0].address).toEqual("1M5hoG1pCTDsPqZwG6WH25ziwYYaXNMLrU") - - it "should have a fee equal to the difference between inputs and outputs value", -> - tx = Tx.factory(transactions["default"]) - - expect(tx.fee).toEqual(10000) - - it "should have a correct number of confirmations", -> - tx = Tx.factory(transactions["default"]) - - expect(tx.confirmations).toEqual(3) - - it "should have correctly tagged outputs", -> - tx = Tx.factory(transactions["default"]) - expect(tx.processedOutputs[0].coinType).toEqual("external") - - it "should have correctly tagged inputs", -> - tx = Tx.factory(transactions["default"]) - expect(tx.processedInputs[0].coinType).toEqual("0/1/15") - expect(tx.processedInputs[1].coinType).toEqual("legacy") - expect(tx.belongsTo("imported")).toEqual(true) - - describe "coinbase", -> - it "should be recognized", -> - tx = Tx.factory(transactions["coinbase"]) - expect(tx.processedInputs.length).toBe(1) - expect(tx.processedOutputs.length).toBe(1) - expect(tx.processedInputs[0].address).toBe("Coinbase") - expect(tx.processedInputs[0].amount).toBe(100000) - expect(tx.txType).toEqual("received") - expect(tx.amount).toEqual(100000) - - it "should have a fee equal to 0", -> - tx = Tx.factory(transactions["coinbase"]) - - expect(tx.fee).toEqual(0) - - - describe "internal", -> - it "should be recognized", -> - tx = Tx.factory(transactions["internal"]) - expect(tx.processedInputs.length).toBe(1) - expect(tx.processedOutputs.length).toBe(2) - expect(tx.fromWatchOnly).toBeFalsy() - expect(tx.toWatchOnly).toBeFalsy() - - it "should have a fee equal to 10000", -> - tx = Tx.factory(transactions["internal"]) - expect(tx.fee).toEqual(10000) - - it "should be categorized as a transfer", -> - tx = Tx.factory(transactions["internal"]) - expect(tx.txType).toEqual("transfer") - expect(tx.changeAmount).toEqual(10000) - expect(tx.amount).toEqual(80000) - - - describe "factory", -> - it "should not touch already existing objects", -> - tx = Tx.factory(transactions["coinbase"]) - fromFactory = Tx.factory(tx) - expect(tx).toEqual(fromFactory) - - describe "IOSfactory", -> - it "should create valid iOS objects", -> - tx = Tx.factory(transactions["default"]) - ios = Tx.IOSfactory(tx) - expect(ios.time).toEqual(tx.time) - expect(ios.result).toEqual(tx.result) - expect(ios.amount).toEqual(tx.amount) - expect(ios.confirmations).toEqual(tx.confirmations) - expect(ios.myHash).toEqual(tx.hash) - expect(ios.txType).toEqual(tx.txType) - expect(ios.block_height).toEqual(tx.block_height) - expect(ios.fromWatchOnly).toEqual(tx.fromWatchOnly) - expect(ios.toWatchOnly).toEqual(tx.toWatchOnly) - expect(ios.to).toEqual(tx.to) - expect(ios.from).toEqual(tx.from)