diff --git a/Gruntfile.js b/Gruntfile.js index 86220b39f..e9b938c00 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -83,7 +83,6 @@ module.exports = function(grunt) { 'js/shell.js', // shell must be loaded before moment due to the way moment loads in a commonjs env 'lib/moment/min/moment.min.js', 'lib/qrcode-generator/js/qrcode.js', - 'lib/peer.js', 'lib/bitcore.js', 'lib/crypto-js/rollups/sha256.js', 'lib/crypto-js/rollups/pbkdf2.js', diff --git a/bower.json b/bower.json index 2d0777456..d58a6e511 100644 --- a/bower.json +++ b/bower.json @@ -10,7 +10,6 @@ "angular-foundation": "*", "angular-route": "~1.2.14", "angular-qrcode": "~3.1.0", - "peerjs": "=0.3.8", "angular-mocks": "~1.2.14", "mocha": "~1.18.2", "chai": "~1.9.1", diff --git a/config.js b/config.js index 5c658ea68..af79f4175 100644 --- a/config.js +++ b/config.js @@ -3,10 +3,13 @@ var defaultConfig = { // DEFAULT network (livenet or testnet) networkName: 'testnet', forceNetwork: false, + logLevel: 'info', // DEFAULT unit: Bit unitName: 'bits', unitToSatoshi: 100, + alternativeName: 'US Dollar', + alternativeIsoCode: 'USD', // wallet limits limits: { @@ -54,7 +57,11 @@ var defaultConfig = { storageSalt: 'mjuBtGybi/4=', }, - disableVideo: true, + rate: { + url: 'https://bitpay.com/api/rates', + updateFrequencySeconds: 60 * 60 + }, + verbose: 1, }; if (typeof module !== 'undefined') diff --git a/css/src/main.css b/css/src/main.css index 564e8c59d..352e30ad2 100644 --- a/css/src/main.css +++ b/css/src/main.css @@ -205,23 +205,6 @@ a:hover { color: #fff; } -.sidebar ul.copayer-list { - list-style-type: none; - padding:0; margin:0; -} - -.sidebar ul.copayer-list li { - margin-top: 15px; - font-weight: 100; - font-size: 12px; - color: #C9C9C9; -} - -.sidebar ul.copayer-list img { - width: 30px; - height: 30px; -} - .button.small.side-bar { padding: 0rem 0.4rem; } @@ -954,7 +937,15 @@ button, .button, p { cursor: pointer; } -.video-box { +.copay-box-small { + width: 40px; + text-align: center; + margin-right: 10px; + padding-bottom: 5px; + float: left; +} + +.copay-box { width: 70px; text-align: center; margin-right: 20px; @@ -962,11 +953,6 @@ button, .button, p { float: left; } -.video-small { - width: 50px; - height: 50px; -} - .icon-input { position: absolute; top: 11px; diff --git a/js/app.js b/js/app.js index 1eb8e8e6c..e90e55275 100644 --- a/js/app.js +++ b/js/app.js @@ -18,11 +18,6 @@ if (localConfig) { } } -var log = function() { - if (config.verbose) console.log(arguments); -} - - var copayApp = window.copayApp = angular.module('copayApp', [ 'ngRoute', 'angularMoment', diff --git a/js/controllers/copayers.js b/js/controllers/copayers.js index 8ca238111..c34aca096 100644 --- a/js/controllers/copayers.js +++ b/js/controllers/copayers.js @@ -26,4 +26,15 @@ angular.module('copayApp.controllers').controller('CopayersController', }); }; + // Cached list of copayers + $scope.copayers = $rootScope.wallet.getRegisteredPeerIds(); + + $scope.copayersList = function() { + return $rootScope.wallet.getRegisteredPeerIds(); + } + + $scope.isBackupReady = function(copayer) { + return $rootScope.wallet.publicKeyRing.isBackupReady(copayer.copayerId); + } + }); diff --git a/js/controllers/setup.js b/js/controllers/create.js similarity index 96% rename from js/controllers/setup.js rename to js/controllers/create.js index a87bc871c..81308f51d 100644 --- a/js/controllers/setup.js +++ b/js/controllers/create.js @@ -32,12 +32,11 @@ var valid_pairs = { '1,12': 489 }; -angular.module('copayApp.controllers').controller('SetupController', +angular.module('copayApp.controllers').controller('CreateController', function($scope, $rootScope, $location, $timeout, walletFactory, controllerUtils, Passphrase, backupService, notification) { controllerUtils.redirIfLogged(); $rootScope.fromSetup = true; - $rootScope.videoInfo = {}; $scope.loading = false; $scope.walletPassword = $rootScope.walletPassword; $scope.isMobile = !!window.cordova; diff --git a/js/controllers/send.js b/js/controllers/send.js index 87208e276..3dc673183 100644 --- a/js/controllers/send.js +++ b/js/controllers/send.js @@ -2,14 +2,67 @@ var bitcore = require('bitcore'); angular.module('copayApp.controllers').controller('SendController', - function($scope, $rootScope, $window, $timeout, $anchorScroll, $modal, isMobile, notification, controllerUtils) { + function($scope, $rootScope, $window, $timeout, $anchorScroll, $modal, isMobile, notification, controllerUtils, rateService) { $scope.title = 'Send'; $scope.loading = false; var satToUnit = 1 / config.unitToSatoshi; $scope.defaultFee = bitcore.TransactionBuilder.FEE_PER_1000B_SAT * satToUnit; $scope.unitToBtc = config.unitToSatoshi / bitcore.util.COIN; + $scope.unitToSatoshi = config.unitToSatoshi; $scope.minAmount = config.limits.minAmountSatoshi * satToUnit; + $scope.alternativeName = config.alternativeName; + $scope.alternativeIsoCode = config.alternativeIsoCode; + + $scope.isRateAvailable = false; + $scope.rateService = rateService; + + rateService.whenAvailable(function() { + $scope.isRateAvailable = true; + $scope.$digest(); + }); + + /** + * Setting the two related amounts as properties prevents an infinite + * recursion for watches while preserving the original angular updates + */ + Object.defineProperty($scope, + "alternative", { + get: function () { + return this._alternative; + }, + set: function (newValue) { + this._alternative = newValue; + if (typeof(newValue) === 'number' && $scope.isRateAvailable) { + this._amount = Number.parseFloat( + (rateService.fromFiat(newValue, config.alternativeIsoCode) * satToUnit + ).toFixed(config.unitDecimals), 10); + } else { + this._amount = 0; + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty($scope, + "amount", { + get: function () { + return this._amount; + }, + set: function (newValue) { + this._amount = newValue; + if (typeof(newValue) === 'number' && $scope.isRateAvailable) { + this._alternative = Number.parseFloat( + (rateService.toFiat(newValue * config.unitToSatoshi, config.alternativeIsoCode) + ).toFixed(2), 10); + } else { + this._alternative = 0; + } + }, + enumerable: true, + configurable: true + }); + $scope.loadTxs = function() { var opts = { pending: true, diff --git a/js/controllers/settings.js b/js/controllers/settings.js index 49f716519..0e72fceb2 100644 --- a/js/controllers/settings.js +++ b/js/controllers/settings.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $location, controllerUtils) { +angular.module('copayApp.controllers').controller('SettingsController', function($scope, $rootScope, $window, $location, controllerUtils, rateService) { controllerUtils.redirIfLogged(); $scope.title = 'Settings'; @@ -8,27 +8,46 @@ angular.module('copayApp.controllers').controller('SettingsController', function $scope.insightHost = config.blockchain.host; $scope.insightPort = config.blockchain.port; $scope.insightSecure = config.blockchain.schema === 'https'; - $scope.disableVideo = typeof config.disableVideo === undefined ? true : config.disableVideo; $scope.forceNetwork = config.forceNetwork; $scope.unitOpts = [{ name: 'Satoshis (100,000,000 satoshis = 1BTC)', shortName: 'SAT', - value: 1 + value: 1, + decimals: 0 }, { name: 'bits (1,000,000 bits = 1BTC)', shortName: 'bits', - value: 100 + value: 100, + decimals: 2 }, { name: 'mBTC (1,000 mBTC = 1BTC)', shortName: 'mBTC', - value: 100000 + value: 100000, + decimals: 5 }, { name: 'BTC', shortName: 'BTC', - value: 100000000 + value: 100000000, + decimals: 8 }]; + $scope.selectedAlternative = { + name: config.alternativeName, + isoCode: config.alternativeIsoCode + }; + $scope.alternativeOpts = rateService.isAvailable ? + rateService.listAlternatives() : [$scope.selectedAlternative]; + + rateService.whenAvailable(function() { + $scope.alternativeOpts = rateService.listAlternatives(); + for (var ii in $scope.alternativeOpts) { + if (config.alternativeIsoCode === $scope.alternativeOpts[ii].isoCode) { + $scope.selectedAlternative = $scope.alternativeOpts[ii]; + } + } + }); + for (var ii in $scope.unitOpts) { if (config.unitName === $scope.unitOpts[ii].shortName) { $scope.selectedUnit = $scope.unitOpts[ii]; @@ -65,10 +84,13 @@ angular.module('copayApp.controllers').controller('SettingsController', function schema: $scope.insightSecure ? 'https' : 'http', }, network: network, - disableVideo: $scope.disableVideo, unitName: $scope.selectedUnit.shortName, unitToSatoshi: $scope.selectedUnit.value, - version: copay.version, + unitDecimals: $scope.selectedUnit.decimals, + alternativeName: $scope.selectedAlternative.name, + alternativeIsoCode: $scope.selectedAlternative.isoCode, + + version: copay.version })); // Go home reloading the application diff --git a/js/controllers/sidebar.js b/js/controllers/sidebar.js index feebe40da..54fc3df8c 100644 --- a/js/controllers/sidebar.js +++ b/js/controllers/sidebar.js @@ -4,7 +4,7 @@ angular.module('copayApp.controllers').controller('SidebarController', function( $scope.menu = [{ 'title': 'Receive', - 'icon': 'fi-arrow-left', + 'icon': 'fi-download', 'link': 'receive' }, { 'title': 'Send', @@ -15,8 +15,8 @@ angular.module('copayApp.controllers').controller('SidebarController', function( 'icon': 'fi-clipboard-pencil', 'link': 'history' }, { - 'title': 'More', - 'icon': 'fi-download', + 'title': 'Settings', + 'icon': 'fi-widget', 'link': 'more' }]; diff --git a/js/controllers/video.js b/js/controllers/video.js deleted file mode 100644 index 520f84e30..000000000 --- a/js/controllers/video.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -angular.module('copayApp.controllers').controller('VideoController', - function($scope, $rootScope, $sce) { - - $rootScope.videoInfo = {}; - - // Cached list of copayers - $scope.copayers = $rootScope.wallet.getRegisteredPeerIds(); - - $scope.copayersList = function() { - return $rootScope.wallet.getRegisteredPeerIds(); - } - - $scope.hasVideo = function(copayer) { - return $rootScope.videoInfo[copayer.peerId]; - } - - $scope.isConnected = function(copayer) { - return $rootScope.wallet.getOnlinePeerIDs().indexOf(copayer.peerId) != -1; - } - - $scope.isBackupReady = function(copayer) { - return $rootScope.wallet.publicKeyRing.isBackupReady(copayer.copayerId); - } - - $scope.getVideoURL = function(copayer) { - if (config.disableVideo) return; - - var vi = $scope.videoInfo[copayer.peerId]; - if (!vi) return; - - if ($scope.isConnected(copayer)) { - // peer disconnected, remove his video - delete $rootScope.videoInfo[copayer.peerId]; - return; - } - - var encoded = vi.url; - var url = decodeURI(encoded); - var trusted = $sce.trustAsResourceUrl(url); - return trusted; - }; - -}); - diff --git a/js/directives.js b/js/directives.js index f3fec639a..268ab17e4 100644 --- a/js/directives.js +++ b/js/directives.js @@ -45,9 +45,10 @@ angular.module('copayApp.directives') link: function(scope, element, attrs, ctrl) { var val = function(value) { var availableBalanceNum = Number(($rootScope.availableBalance * config.unitToSatoshi).toFixed(0)); - var vNum = Number((value * config.unitToSatoshi).toFixed(0)) + feeSat; + var vNum = Number((value * config.unitToSatoshi).toFixed(0)); if (typeof vNum == "number" && vNum > 0) { + vNum = vNum + feeSat; if (availableBalanceNum < vNum || isNaN(availableBalanceNum)) { ctrl.$setValidity('enoughAmount', false); scope.notEnoughAmount = true; @@ -108,20 +109,6 @@ angular.module('copayApp.directives') } } }) - .directive('avatar', function($rootScope, controllerUtils) { - return { - link: function(scope, element, attrs) { - var peer = JSON.parse(attrs.peer) - var peerId = peer.peerId; - var nick = peer.nick; - element.addClass('video-small'); - var muted = controllerUtils.getVideoMutedStatus(peerId); - if (true || muted) { // mute everyone for now - element.attr("muted", true); - } - } - } - }) .directive('contact', function() { return { restrict: 'E', diff --git a/js/log.js b/js/log.js new file mode 100644 index 000000000..8a83b9a2e --- /dev/null +++ b/js/log.js @@ -0,0 +1,43 @@ +var config = require('../config'); + +var Logger = function(name) { + this.name = name || 'log'; + this.level = 2; +}; + +var levels = { + 'debug': 0, + 'info': 1, + 'log': 2, + 'warn': 3, + 'error': 4, + 'fatal': 5 +}; + +Object.keys(levels).forEach(function(level) { + Logger.prototype[level] = function() { + if (levels[level] >= levels[this.level]) { + var str = '[' + level + '] ' + this.name + ': ' + arguments[0], + extraArgs, + extraArgs = [].slice.call(arguments, 1); + if (console[level]) { + extraArgs.unshift(str); + console[level].apply(console, extraArgs); + } else { + if (extraArgs.length) { + str += JSON.stringify(extraArgs); + } + console.log(str); + } + } + }; +}); + +Logger.prototype.setLevel = function(level) { + this.level = level; +} + +var logger = new Logger('copay'); +logger.setLevel(config.logLevel); + +module.exports = logger; diff --git a/js/models/core/Passphrase.js b/js/models/core/Passphrase.js index 04485596c..86f57b1bd 100644 --- a/js/models/core/Passphrase.js +++ b/js/models/core/Passphrase.js @@ -15,14 +15,12 @@ Passphrase.prototype.get = function(password) { keySize: 512 / 32, iterations: this.iterations }); - return key512; }; Passphrase.prototype.getBase64 = function(password) { var key512 = this.get(password); var keyBase64 = key512.toString(CryptoJS.enc.Base64); - return keyBase64; }; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 43fd21509..0a9785516 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -15,6 +15,7 @@ var Base58Check = bitcore.Base58.base58Check; var Address = bitcore.Address; var PayPro = bitcore.PayPro; var Transaction = bitcore.Transaction; +var log = require('../../log'); var HDParams = require('./HDParams'); var PublicKeyRing = require('./PublicKeyRing'); @@ -72,12 +73,6 @@ Wallet.builderOpts = { feeSat: null, }; -Wallet.prototype.log = function() { - if (!this.verbose) return; - if (console) - console.log.apply(console, arguments); -}; - Wallet.getRandomId = function() { var r = bitcore.SecureRandom.getPseudoRandomBuffer(8).toString('hex'); return r; @@ -88,7 +83,7 @@ Wallet.prototype.seedCopayer = function(pubKey) { }; Wallet.prototype._onIndexes = function(senderId, data) { - this.log('RECV INDEXES:', data); + log.debug('RECV INDEXES:', data); var inIndexes = HDParams.fromList(data.indexes); var hasChanged = this.publicKeyRing.mergeIndexes(inIndexes); if (hasChanged) { @@ -98,7 +93,7 @@ Wallet.prototype._onIndexes = function(senderId, data) { }; Wallet.prototype._onPublicKeyRing = function(senderId, data) { - this.log('RECV PUBLICKEYRING:', data); + log.debug('RECV PUBLICKEYRING:', data); var inPKR = PublicKeyRing.fromObj(data.publicKeyRing); var wasIncomplete = !this.publicKeyRing.isComplete(); @@ -107,7 +102,7 @@ Wallet.prototype._onPublicKeyRing = function(senderId, data) { try { hasChanged = this.publicKeyRing.merge(inPKR, true); } catch (e) { - this.log('## WALLET ERROR', e); + log.debug('## WALLET ERROR', e); this.emit('connectionError', e.message); return; } @@ -205,7 +200,7 @@ Wallet.prototype._checkSentTx = function(ntxid, cb) { Wallet.prototype._onTxProposal = function(senderId, data) { var self = this; - this.log('RECV TXPROPOSAL: ', data); + log.debug('RECV TXPROPOSAL: ', data); var m; try { @@ -214,7 +209,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { ret.newCopayer = m.txp.setCopayers(senderId, keyMap); } catch (e) { - this.log('Corrupt TX proposal received from:', senderId, e); + log.debug('Corrupt TX proposal received from:', senderId, e); } if (m) { @@ -242,7 +237,7 @@ Wallet.prototype._onTxProposal = function(senderId, data) { Wallet.prototype._onReject = function(senderId, data) { preconditions.checkState(data.ntxid); - this.log('RECV REJECT:', data); + log.debug('RECV REJECT:', data); var txp = this.txProposals.get(data.ntxid); @@ -265,7 +260,7 @@ Wallet.prototype._onReject = function(senderId, data) { Wallet.prototype._onSeen = function(senderId, data) { preconditions.checkState(data.ntxid); - this.log('RECV SEEN:', data); + log.debug('RECV SEEN:', data); var txp = this.txProposals.get(data.ntxid); txp.setSeen(senderId); @@ -283,7 +278,7 @@ Wallet.prototype._onSeen = function(senderId, data) { Wallet.prototype._onAddressBook = function(senderId, data) { preconditions.checkState(data.addressBook); - this.log('RECV ADDRESSBOOK:', data); + log.debug('RECV ADDRESSBOOK:', data); var rcv = data.addressBook; var hasChange; for (var key in rcv) { @@ -311,7 +306,7 @@ Wallet.prototype.updateTimestamp = function(ts) { Wallet.prototype._onNoMessages = function() { - console.log('No messages at the server. Requesting sync'); //TODO + log.debug('No messages at the server. Requesting sync'); //TODO this.sendWalletReady(); }; @@ -322,7 +317,7 @@ Wallet.prototype._onData = function(senderId, data, ts) { preconditions.checkArgument(ts); preconditions.checkArgument(typeof ts === 'number'); - console.log('RECV', senderId, data); + log.debug('RECV', senderId, data); if (data.type !== 'walletId' && this.id !== data.walletId) { this.emit('corrupt', senderId); @@ -375,7 +370,7 @@ Wallet.prototype._onData = function(senderId, data, ts) { Wallet.prototype._onConnect = function(newCopayerId) { if (newCopayerId) { - this.log('#### Setting new COPAYER:', newCopayerId); + log.debug('#### Setting new COPAYER:', newCopayerId); this.sendWalletId(newCopayerId); } var peerID = this.network.peerFromCopayer(newCopayerId) @@ -460,28 +455,12 @@ Wallet.prototype.netStart = function(callback) { self.emit('ready', net.getPeer()); setTimeout(function() { self.emit('publicKeyRingUpdated', true); - //self.scheduleConnect(); // no connection logic for now self.emit('txProposalsUpdated'); }, 10); }); }; - -// not being used now -Wallet.prototype.scheduleConnect = function() { - var self = this; - if (self.network.isOnline()) { - self.connectToAll(); - self.currentDelay = self.currentDelay * 2 || self.reconnectDelay; - setTimeout(self.scheduleConnect.bind(self), self.currentDelay); - } -} - -Wallet.prototype.getOnlinePeerIDs = function() { - return this.network.getOnlinePeerIDs(); -}; - Wallet.prototype.getRegisteredCopayerIds = function() { var l = this.publicKeyRing.registeredCopayers(); var copayers = []; @@ -515,7 +494,7 @@ Wallet.prototype.keepAlive = function() { try { this.lock.keepAlive(); } catch (e) { - this.log(e); + log.debug(e); this.emit('locked', null, 'Wallet appears to be openned on other browser instance. Closing this one.'); } }; @@ -525,7 +504,7 @@ Wallet.prototype.store = function() { var wallet = this.toObj(); this.storage.setFromObj(this.id, wallet); - this.log('Wallet stored'); + log.debug('Wallet stored'); }; Wallet.prototype.toObj = function() { @@ -613,7 +592,7 @@ Wallet.prototype.sendAllTxProposals = function(recipients) { Wallet.prototype.sendTxProposal = function(ntxid, recipients) { preconditions.checkArgument(ntxid); - this.log('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals); + log.debug('### SENDING txProposal ' + ntxid + ' TO:', recipients || 'All', this.txProposals); this.send(recipients, { type: 'txProposal', txProposal: this.txProposals.get(ntxid).toObjTrim(), @@ -623,7 +602,7 @@ Wallet.prototype.sendTxProposal = function(ntxid, recipients) { Wallet.prototype.sendSeen = function(ntxid) { preconditions.checkArgument(ntxid); - this.log('### SENDING seen: ' + ntxid + ' TO: All'); + log.debug('### SENDING seen: ' + ntxid + ' TO: All'); this.send(null, { type: 'seen', ntxid: ntxid, @@ -633,7 +612,7 @@ Wallet.prototype.sendSeen = function(ntxid) { Wallet.prototype.sendReject = function(ntxid) { preconditions.checkArgument(ntxid); - this.log('### SENDING reject: ' + ntxid + ' TO: All'); + log.debug('### SENDING reject: ' + ntxid + ' TO: All'); this.send(null, { type: 'reject', ntxid: ntxid, @@ -643,7 +622,7 @@ Wallet.prototype.sendReject = function(ntxid) { Wallet.prototype.sendWalletReady = function(recipients) { - this.log('### SENDING WalletReady TO:', recipients || 'All'); + log.debug('### SENDING WalletReady TO:', recipients || 'All'); this.send(recipients, { type: 'walletReady', @@ -652,7 +631,7 @@ Wallet.prototype.sendWalletReady = function(recipients) { }; Wallet.prototype.sendWalletId = function(recipients) { - this.log('### SENDING walletId TO:', recipients || 'All', this.id); + log.debug('### SENDING walletId TO:', recipients || 'All', this.id); this.send(recipients, { type: 'walletId', @@ -664,7 +643,7 @@ Wallet.prototype.sendWalletId = function(recipients) { Wallet.prototype.sendPublicKeyRing = function(recipients) { - this.log('### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj()); + log.debug('### SENDING publicKeyRing TO:', recipients || 'All', this.publicKeyRing.toObj()); var publicKeyRing = this.publicKeyRing.toObj(); this.send(recipients, { @@ -675,7 +654,7 @@ Wallet.prototype.sendPublicKeyRing = function(recipients) { }; Wallet.prototype.sendIndexes = function(recipients) { var indexes = HDParams.serialize(this.publicKeyRing.indexes); - this.log('### INDEXES TO:', recipients || 'All', indexes); + log.debug('### INDEXES TO:', recipients || 'All', indexes); this.send(recipients, { type: 'indexes', @@ -685,7 +664,7 @@ Wallet.prototype.sendIndexes = function(recipients) { }; Wallet.prototype.sendAddressBook = function(recipients) { - this.log('### SENDING addressBook TO:', recipients || 'All', this.addressBook); + log.debug('### SENDING addressBook TO:', recipients || 'All', this.addressBook); this.send(recipients, { type: 'addressbook', addressBook: this.addressBook, @@ -796,23 +775,23 @@ Wallet.prototype.sendTx = function(ntxid, cb) { var tx = txp.builder.build(); if (!tx.isComplete()) throw new Error('Tx is not complete. Can not broadcast'); - this.log('Broadcasting Transaction'); + log.debug('Broadcasting Transaction'); var scriptSig = tx.ins[0].getScript(); var size = scriptSig.serialize().length; var txHex = tx.serialize().toString('hex'); - this.log('Raw transaction: ', txHex); + log.debug('Raw transaction: ', txHex); var self = this; this.blockchain.broadcast(txHex, function(err, txid) { - self.log('BITCOIND txid:', txid); + log.debug('BITCOIND txid:', txid); if (txid) { self.txProposals.get(ntxid).setSent(txid); self.sendTxProposal(ntxid); self.store(); return cb(txid); } else { - self.log('Sent failed. Checking if the TX was sent already'); + log.debug('Sent failed. Checking if the TX was sent already'); self._checkSentTx(ntxid, function(txid) { if (txid) self.store(); @@ -1003,10 +982,8 @@ Wallet.prototype.receivePaymentRequest = function(options, pr, cb) { self.emit('txProposalsUpdated'); } - self.log('You are currently on this BTC network:'); - self.log(network); - self.log('The server sent you a message:'); - self.log(memo); + log.debug('You are currently on this BTC network:', network); + log.debug('The server sent you a message:', memo); return cb(ntxid, merchantData); }); @@ -1025,7 +1002,7 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { var tx = txp.builder.build(); if (!tx.isComplete()) return; - this.log('Sending Transaction'); + log.debug('Sending Transaction'); var refund_outputs = []; @@ -1085,8 +1062,7 @@ Wallet.prototype.sendPaymentTx = function(ntxid, options, cb) { pay = pay.serialize(); - this.log('Sending Payment Message:'); - this.log(pay.toString('hex')); + log.debug('Sending Payment Message:', pay.toString('hex')); var buf = new ArrayBuffer(pay.length); var view = new Uint8Array(buf); @@ -1127,8 +1103,8 @@ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { var payment = ack.get('payment'); var memo = ack.get('memo'); - this.log('Our payment was acknowledged!'); - this.log('Message from Merchant: %s', memo); + log.debug('Our payment was acknowledged!'); + log.debug('Message from Merchant: %s', memo); payment = PayPro.Payment.decode(payment); var pay = new PayPro(); @@ -1141,7 +1117,7 @@ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { var tx = payment.message.transactions[0]; if (!tx) { - this.log('Sending to server was not met with a returned tx.'); + log.debug('Sending to server was not met with a returned tx.'); return this._checkSentTx(ntxid, function(txid) { self.log('[Wallet.js.1048:txid:%s]', txid); if (txid) self.store(); @@ -1159,8 +1135,8 @@ Wallet.prototype.receivePaymentRequestACK = function(ntxid, tx, txp, ack, cb) { var txid = tx.getHash().toString('hex'); var txHex = tx.serialize().toString('hex'); - this.log('Raw transaction: ', txHex); - this.log('BITCOIND txid:', txid); + log.debug('Raw transaction: ', txHex); + log.debug('BITCOIND txid:', txid); this.txProposals.get(ntxid).setSent(txid); this.sendTxProposal(ntxid); this.store(); @@ -1260,10 +1236,7 @@ Wallet.prototype.createPaymentTxSync = function(options, merchantData, unspent) if (options.fetch) return; - this.log(''); - this.log('Created transaction:'); - this.log(b.tx.getStandardizedObject()); - this.log(''); + log.debug('Created transaction: %s', b.tx.getStandardizedObject()); var myId = this.getMyCopayerId(); var now = Date.now(); @@ -1661,7 +1634,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos Wallet.prototype.updateIndexes = function(callback) { var self = this; - self.log('Updating indexes...'); + log.debug('Updating indexes...'); var tasks = this.publicKeyRing.indexes.map(function(index) { return function(callback) { @@ -1671,7 +1644,7 @@ Wallet.prototype.updateIndexes = function(callback) { async.parallel(tasks, function(err) { if (err) callback(err); - self.log('Indexes updated'); + log.debug('Indexes updated'); self.emit('publicKeyRingUpdated'); self.store(); callback(); @@ -1747,7 +1720,7 @@ Wallet.prototype.indexDiscovery = function(start, change, copayerIndex, gap, cb) Wallet.prototype.close = function() { - this.log('## CLOSING'); + log.debug('## CLOSING'); this.lock.release(); this.network.cleanUp(); this.blockchain.destroy(); diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index 61a384f75..2155bb2c1 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -4,6 +4,9 @@ var TxProposals = require('./TxProposals'); var PublicKeyRing = require('./PublicKeyRing'); var PrivateKey = require('./PrivateKey'); var Wallet = require('./Wallet'); +var preconditions = require('preconditions').instance(); + +var log = require('../../log'); var Async = module.exports.Async = require('../network/Async'); var Insight = module.exports.Insight = require('../blockchain/Insight'); @@ -11,7 +14,6 @@ var StorageLocalEncrypted = module.exports.StorageLocalEncrypted = require('../s /* * WalletFactory - * */ function WalletFactory(config, version) { @@ -27,19 +29,10 @@ function WalletFactory(config, version) { this.blockchain = new this.Blockchain(config.blockchain); this.networkName = config.networkName; - this.verbose = config.verbose; this.walletDefaults = config.wallet; this.version = version; } -WalletFactory.prototype.log = function() { - if (!this.verbose) return; - if (console) { - console.log.apply(console, arguments); - } -}; - - WalletFactory.prototype._checkRead = function(walletId) { var s = this.storage; var ret = @@ -112,7 +105,7 @@ WalletFactory.prototype.read = function(walletId, skipFields) { WalletFactory.prototype.create = function(opts) { opts = opts || {}; - this.log('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')); + log.debug('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID') + (opts.privateKey ? ' USING PrivateKey: ' + opts.privateKey.getId() : ' NEW PrivateKey')); var privOpts = { networkName: this.networkName, @@ -137,12 +130,12 @@ WalletFactory.prototype.create = function(opts) { opts.privateKey.deriveBIP45Branch().extendedPublicKeyString(), opts.nickname ); - this.log('\t### PublicKeyRing Initialized'); + log.debug('\t### PublicKeyRing Initialized'); opts.txProposals = opts.txProposals || new TxProposals({ networkName: this.networkName, }); - this.log('\t### TxProposals Initialized'); + log.debug('\t### TxProposals Initialized'); this.storage._setPassphrase(opts.passphrase); @@ -236,7 +229,7 @@ WalletFactory.prototype.joinCreateSession = function(secret, nickname, passphras //Create our PrivateK var privateKey = new PrivateKey(privOpts); - this.log('\t### PrivateKey Initialized'); + log.debug('\t### PrivateKey Initialized'); var opts = { copayerId: privateKey.getId(), privkey: privateKey.getIdPriv(), diff --git a/js/models/network/Async.js b/js/models/network/Async.js index 172133fae..4ba012614 100644 --- a/js/models/network/Async.js +++ b/js/models/network/Async.js @@ -2,6 +2,7 @@ var EventEmitter = require('events').EventEmitter; var bitcore = require('bitcore'); +var log = require('../../log'); var AuthMessage = bitcore.AuthMessage; var util = bitcore.util; var nodeUtil = require('util'); @@ -187,7 +188,7 @@ Network.prototype._onMessage = function(enc) { return; } - //console.log('receiving ' + JSON.stringify(payload)); + log.debug('receiving ' + JSON.stringify(payload)); var self = this; switch (payload.type) { @@ -234,8 +235,8 @@ Network.prototype._setupConnectionHandlers = function(cb) { }; Network.prototype._onError = function(err) { - console.log('RECV ERROR: ', err); - console.log(err.stack); + log.debug('RECV ERROR: ', err); + log.debug(err.stack); this.criticalError = err.message; }; @@ -348,7 +349,7 @@ Network.prototype.send = function(dest, payload, cb) { var to = dest[ii]; if (to == this.copayerId) continue; - //console.log('SEND to: ' + to, this.copayerId, payload); + log.debug('SEND to: ' + to, this.copayerId, payload); var message = this.encode(to, payload); this.socket.emit('message', message); } diff --git a/js/models/storage/LocalEncrypted.js b/js/models/storage/LocalEncrypted.js index c45dc1b85..b093eaaa4 100644 --- a/js/models/storage/LocalEncrypted.js +++ b/js/models/storage/LocalEncrypted.js @@ -2,6 +2,7 @@ var CryptoJS = require('node-cryptojs-aes').CryptoJS; var bitcore = require('bitcore'); +var preconditions = require('preconditions').instance(); var id = 0; function Storage(opts) { @@ -11,15 +12,12 @@ function Storage(opts) { if (opts.password) this._setPassphrase(opts.password); - try{ + try { this.localStorage = opts.localStorage || localStorage; this.sessionStorage = opts.sessionStorage || sessionStorage; - } catch (e) {}; - - if (!this.localStorage) - throw new Error('no localStorage'); - if (!this.sessionStorage) - throw new Error('no sessionStorage'); + } catch (e) {} + preconditions.checkState(this.localStorage, 'No localstorage found'); + preconditions.checkState(this.sessionStorage, 'No sessionStorage found'); } var pps = {}; @@ -40,11 +38,6 @@ Storage.prototype._encrypt = function(string) { return encryptedBase64; }; -Storage.prototype._encryptObj = function(obj) { - var string = JSON.stringify(obj); - return this._encrypt(string); -}; - Storage.prototype._decrypt = function(base64) { var decryptedStr = null; try { @@ -58,10 +51,6 @@ Storage.prototype._decrypt = function(base64) { return decryptedStr; }; -Storage.prototype._decryptObj = function(base64) { - var decryptedStr = this._decrypt(base64); - return JSON.parse(decryptedStr); -}; Storage.prototype._read = function(k) { var ret; @@ -98,7 +87,7 @@ Storage.prototype.removeGlobal = function(k) { }; Storage.prototype.getSessionId = function() { - var sessionId = this.sessionStorage.getItem('sessionId'); + var sessionId = this.sessionStorage.getItem('sessionId'); if (!sessionId) { sessionId = bitcore.SecureRandom.getRandomBuffer(8).toString('hex'); this.sessionStorage.setItem('sessionId', sessionId); @@ -131,7 +120,6 @@ Storage.prototype.setName = function(walletId, name) { Storage.prototype.getName = function(walletId) { var ret = this.getGlobal('nameFor::' + walletId); - return ret; }; @@ -145,7 +133,7 @@ Storage.prototype.getWalletIds = function() { if (split.length == 2) { var walletId = split[0]; - if (!walletId || walletId === 'nameFor' || walletId ==='lock') + if (!walletId || walletId === 'nameFor' || walletId === 'lock') continue; if (typeof uniq[walletId] === 'undefined') { @@ -207,14 +195,14 @@ Storage.prototype.clearAll = function() { this.localStorage.clear(); }; -Storage.prototype.export = function(obj) { - var encryptedObj = this._encryptObj(obj); - return encryptedObj; +Storage.prototype.import = function(base64) { + var decryptedStr = this._decrypt(base64); + return JSON.parse(decryptedStr); }; -Storage.prototype.import = function(base64) { - var decryptedObj = this._decryptObj(base64); - return decryptedObj; +Storage.prototype.export = function(obj) { + var string = JSON.stringify(obj); + return this._encrypt(string); }; module.exports = Storage; diff --git a/js/routes.js b/js/routes.js index 62b47ec37..3ff033a11 100644 --- a/js/routes.js +++ b/js/routes.js @@ -22,8 +22,8 @@ angular templateUrl: 'views/import.html', validate: false }) - .when('/setup', { - templateUrl: 'views/setup.html', + .when('/create', { + templateUrl: 'views/create.html', validate: false }) .when('/copayers', { diff --git a/js/services/controllerUtils.js b/js/services/controllerUtils.js index 143536504..ba0c9d2d6 100644 --- a/js/services/controllerUtils.js +++ b/js/services/controllerUtils.js @@ -2,17 +2,8 @@ var bitcore = require('bitcore'); angular.module('copayApp.services') - .factory('controllerUtils', function($rootScope, $sce, $location, notification, $timeout, video, uriHandler) { + .factory('controllerUtils', function($rootScope, $sce, $location, notification, $timeout, uriHandler, rateService) { var root = {}; - root.getVideoMutedStatus = function(copayer) { - if (!$rootScope.videoInfo) return; - - var vi = $rootScope.videoInfo[copayer] - if (!vi) { - return; - } - return vi.muted; - }; root.redirIfLogged = function() { if ($rootScope.wallet) { @@ -27,7 +18,6 @@ angular.module('copayApp.services') $rootScope.wallet = null; delete $rootScope['wallet']; - video.close(); // Clear rootScope for (var i in $rootScope) { if (i.charAt(0) != '$') { @@ -102,18 +92,6 @@ angular.module('copayApp.services') root.installStartupHandlers(w, $scope); root.updateGlobalAddresses(); - var handlePeerVideo = function(err, peerID, url) { - if (err) { - delete $rootScope.videoInfo[peerID]; - return; - } - $rootScope.videoInfo[peerID] = { - url: encodeURI(url), - muted: peerID === w.network.peerId - }; - $rootScope.$digest(); - }; - notification.enableHtml5Mode(); // for chrome: if support, enable it w.on('corrupt', function(peerId) { @@ -128,8 +106,6 @@ angular.module('copayApp.services') } else { $location.path('receive'); } - if (!config.disableVideo) - video.setOwnPeer(myPeerID, w, handlePeerVideo); }); w.on('publicKeyRingUpdated', function(dontDigest) { @@ -172,9 +148,6 @@ angular.module('copayApp.services') root.onErrorDigest(null, msg); }); w.on('connect', function(peerID) { - if (peerID && !config.disableVideo) { - video.callPeer(peerID, handlePeerVideo); - } $rootScope.$digest(); }); w.on('close', root.onErrorDigest); @@ -217,7 +190,15 @@ angular.module('copayApp.services') $rootScope.balanceByAddr = balanceByAddr; root.updateAddressList(); $rootScope.updatingBalance = false; - return cb ? cb() : null; + + rateService.whenAvailable(function() { + $rootScope.totalBalanceAlternative = rateService.toFiat(balanceSat, config.alternativeIsoCode); + $rootScope.alternativeIsoCode = config.alternativeIsoCode; + $rootScope.lockedBalanceAlternative = rateService.toFiat(balanceSat - safeBalanceSat, config.alternativeIsoCode); + + + return cb ? cb() : null; + }); }); }; diff --git a/js/services/rate.js b/js/services/rate.js new file mode 100644 index 000000000..0c28ae046 --- /dev/null +++ b/js/services/rate.js @@ -0,0 +1,83 @@ +'use strict'; + +var RateService = function(request) { + this.isAvailable = false; + this.UNAVAILABLE_ERROR = 'Service is not available - check for service.isAvailable or use service.whenAvailable'; + this.SAT_TO_BTC = 1 / 1e8; + var MINS_IN_HOUR = 60; + var MILLIS_IN_SECOND = 1000; + var rateServiceConfig = config.rate; + var updateFrequencySeconds = rateServiceConfig.updateFrequencySeconds || 60 * MINS_IN_HOUR; + var rateServiceUrl = rateServiceConfig.url || 'https://bitpay.com/api/rates'; + this.queued = []; + this.alternatives = []; + var that = this; + var backoffSeconds = 5; + var retrieve = function() { + request.get({ + url: rateServiceUrl, + json: true + }, function(err, response, listOfCurrencies) { + if (err) { + backoffSeconds *= 1.5; + setTimeout(retrieve, backoffSeconds * MILLIS_IN_SECOND); + return; + } + var rates = {}; + listOfCurrencies.forEach(function(element) { + rates[element.code] = element.rate; + that.alternatives.push({ + name: element.name, + isoCode: element.code, + rate: element.rate + }); + }); + that.isAvailable = true; + that.rates = rates; + that.queued.forEach(function(callback) { + setTimeout(callback, 1); + }); + setTimeout(retrieve, updateFrequencySeconds * MILLIS_IN_SECOND); + }); + }; + retrieve(); +}; + +RateService.prototype.whenAvailable = function(callback) { + if (this.isAvailable) { + setTimeout(callback, 1); + } else { + this.queued.push(callback); + } +}; + +RateService.prototype.toFiat = function(satoshis, code) { + if (!this.isAvailable) { + throw new Error(this.UNAVAILABLE_ERROR); + } + return satoshis * this.SAT_TO_BTC * this.rates[code]; +}; + +RateService.prototype.fromFiat = function(amount, code) { + if (!this.isAvailable) { + throw new Error(this.UNAVAILABLE_ERROR); + } + return amount / this.rates[code] / this.SAT_TO_BTC; +}; + +RateService.prototype.listAlternatives = function() { + if (!this.isAvailable) { + throw new Error(this.UNAVAILABLE_ERROR); + } + + var alts = []; + this.alternatives.forEach(function(element) { + alts.push({ + name: element.name, + isoCode: element.isoCode + }); + }); + return alts; +}; + +angular.module('copayApp.services').service('rateService', RateService); diff --git a/js/services/request.js b/js/services/request.js new file mode 100644 index 000000000..5a92093c9 --- /dev/null +++ b/js/services/request.js @@ -0,0 +1,6 @@ +'use strict'; + +angular.module('copayApp.services').factory('request', function() { + return require('request'); +}); + diff --git a/js/services/video.js b/js/services/video.js deleted file mode 100644 index cf7caeaa6..000000000 --- a/js/services/video.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; - -var Video = function() { - navigator.getUserMedia = navigator.getUserMedia || - navigator.webkitGetUserMedia || - navigator.mozGetUserMedia; - - this.mediaConnections = {}; - this.localStream = null; -}; - -Video.prototype.setOwnPeer = function(peer, wallet, cb) { - var self = this; - - var VWIDTH = 320; - var VHEIGHT = 320; - var constraints = { - audio: true, - video: { - mandatory: { - maxWidth: VWIDTH, - maxHeight: VHEIGHT, - } - } - }; - navigator.getUserMedia(constraints, function(stream) { - // This is called when user accepts using webcam - self.localStream = stream; - var online = wallet.getOnlinePeerIDs(); - for (var i = 0; i < online.length; i++) { - var o = online[i]; - if (o !== peer.id) { - self.callPeer(o, cb); - } - } - cb(null, peer.id, URL.createObjectURL(stream)); - }, function() { - cb(new Error('Failed to access the webcam and microphone.')); - }); - - // Receiving a call - peer.on('call', function(mediaConnection) { - if (self.localStream) { - mediaConnection.answer(self.localStream); - } else { - mediaConnection.answer(); - } - self._addCall(mediaConnection, cb); - }); - this.peer = peer; -}; - -Video.prototype.callPeer = function(peerID, cb) { - if (this.localStream) { - var mediaConnection = this.peer.call(peerID, this.localStream); - this._addCall(mediaConnection, cb); - } -}; - -Video.prototype._addCall = function(mediaConnection, cb) { - var self = this; - var peerID = mediaConnection.peer; - - // Wait for stream on the call, then set peer video display - mediaConnection.on('stream', function(stream) { - cb(null, peerID, URL.createObjectURL(stream)); - }); - - mediaConnection.on('close', function() { - cb(true, peerID, null); // ask to stop video streaming in UI - }); - mediaConnection.on('error', function(e) { - cb(e, peerID, null); - }); - this.mediaConnections[peerID] = mediaConnection; -} - -Video.prototype.close = function() { - if (this.localStream) { - this.localStream.stop(); - this.localStream.mozSrcObject = null; - this.localStream.src = ""; - this.localStream.src = null; - this.localStream = null; - } - for (var i = 0; this.mediaConnections.length; i++) { - this.mediaConnections[i].close(); - } - this.mediaConnections = {}; -}; - -angular.module('copayApp.services').value('video', new Video()); diff --git a/karma.conf.js b/karma.conf.js index 69fb2faf4..74d8f7377 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -28,7 +28,6 @@ module.exports = function(config) { 'lib/angular-route/angular-route.min.js', 'lib/angular-foundation/mm-foundation.min.js', 'lib/angular-foundation/mm-foundation-tpls.min.js', - 'lib/peerjs/peer.js', 'lib/bitcore.js', 'lib/crypto-js/rollups/sha256.js', 'lib/crypto-js/rollups/pbkdf2.js', @@ -42,6 +41,7 @@ module.exports = function(config) { //App-specific Code 'js/app.js', + 'js/log.js', 'js/routes.js', 'js/services/*.js', 'js/directives.js', diff --git a/lib/peer.js b/lib/peer.js deleted file mode 100644 index 0289ef5f2..000000000 --- a/lib/peer.js +++ /dev/null @@ -1,2657 +0,0 @@ -/*! peerjs.js build:0.3.8, development. Copyright(c) 2013 Michelle Bu */ -(function(exports){ -var binaryFeatures = {}; -binaryFeatures.useBlobBuilder = (function(){ - try { - new Blob([]); - return false; - } catch (e) { - return true; - } -})(); - -binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){ - try { - return (new Blob([new Uint8Array([])])).size === 0; - } catch (e) { - return true; - } -})(); - -exports.binaryFeatures = binaryFeatures; -exports.BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder || window.BlobBuilder; - -function BufferBuilder(){ - this._pieces = []; - this._parts = []; -} - -BufferBuilder.prototype.append = function(data) { - if(typeof data === 'number') { - this._pieces.push(data); - } else { - this.flush(); - this._parts.push(data); - } -}; - -BufferBuilder.prototype.flush = function() { - if (this._pieces.length > 0) { - var buf = new Uint8Array(this._pieces); - if(!binaryFeatures.useArrayBufferView) { - buf = buf.buffer; - } - this._parts.push(buf); - this._pieces = []; - } -}; - -BufferBuilder.prototype.getBuffer = function() { - this.flush(); - if(binaryFeatures.useBlobBuilder) { - var builder = new BlobBuilder(); - for(var i = 0, ii = this._parts.length; i < ii; i++) { - builder.append(this._parts[i]); - } - return builder.getBlob(); - } else { - return new Blob(this._parts); - } -}; -exports.BinaryPack = { - unpack: function(data){ - var unpacker = new Unpacker(data); - return unpacker.unpack(); - }, - pack: function(data){ - var packer = new Packer(); - packer.pack(data); - var buffer = packer.getBuffer(); - return buffer; - } -}; - -function Unpacker (data){ - // Data is ArrayBuffer - this.index = 0; - this.dataBuffer = data; - this.dataView = new Uint8Array(this.dataBuffer); - this.length = this.dataBuffer.byteLength; -} - - -Unpacker.prototype.unpack = function(){ - var type = this.unpack_uint8(); - if (type < 0x80){ - var positive_fixnum = type; - return positive_fixnum; - } else if ((type ^ 0xe0) < 0x20){ - var negative_fixnum = (type ^ 0xe0) - 0x20; - return negative_fixnum; - } - var size; - if ((size = type ^ 0xa0) <= 0x0f){ - return this.unpack_raw(size); - } else if ((size = type ^ 0xb0) <= 0x0f){ - return this.unpack_string(size); - } else if ((size = type ^ 0x90) <= 0x0f){ - return this.unpack_array(size); - } else if ((size = type ^ 0x80) <= 0x0f){ - return this.unpack_map(size); - } - switch(type){ - case 0xc0: - return null; - case 0xc1: - return undefined; - case 0xc2: - return false; - case 0xc3: - return true; - case 0xca: - return this.unpack_float(); - case 0xcb: - return this.unpack_double(); - case 0xcc: - return this.unpack_uint8(); - case 0xcd: - return this.unpack_uint16(); - case 0xce: - return this.unpack_uint32(); - case 0xcf: - return this.unpack_uint64(); - case 0xd0: - return this.unpack_int8(); - case 0xd1: - return this.unpack_int16(); - case 0xd2: - return this.unpack_int32(); - case 0xd3: - return this.unpack_int64(); - case 0xd4: - return undefined; - case 0xd5: - return undefined; - case 0xd6: - return undefined; - case 0xd7: - return undefined; - case 0xd8: - size = this.unpack_uint16(); - return this.unpack_string(size); - case 0xd9: - size = this.unpack_uint32(); - return this.unpack_string(size); - case 0xda: - size = this.unpack_uint16(); - return this.unpack_raw(size); - case 0xdb: - size = this.unpack_uint32(); - return this.unpack_raw(size); - case 0xdc: - size = this.unpack_uint16(); - return this.unpack_array(size); - case 0xdd: - size = this.unpack_uint32(); - return this.unpack_array(size); - case 0xde: - size = this.unpack_uint16(); - return this.unpack_map(size); - case 0xdf: - size = this.unpack_uint32(); - return this.unpack_map(size); - } -} - -Unpacker.prototype.unpack_uint8 = function(){ - var byte = this.dataView[this.index] & 0xff; - this.index++; - return byte; -}; - -Unpacker.prototype.unpack_uint16 = function(){ - var bytes = this.read(2); - var uint16 = - ((bytes[0] & 0xff) * 256) + (bytes[1] & 0xff); - this.index += 2; - return uint16; -} - -Unpacker.prototype.unpack_uint32 = function(){ - var bytes = this.read(4); - var uint32 = - ((bytes[0] * 256 + - bytes[1]) * 256 + - bytes[2]) * 256 + - bytes[3]; - this.index += 4; - return uint32; -} - -Unpacker.prototype.unpack_uint64 = function(){ - var bytes = this.read(8); - var uint64 = - ((((((bytes[0] * 256 + - bytes[1]) * 256 + - bytes[2]) * 256 + - bytes[3]) * 256 + - bytes[4]) * 256 + - bytes[5]) * 256 + - bytes[6]) * 256 + - bytes[7]; - this.index += 8; - return uint64; -} - - -Unpacker.prototype.unpack_int8 = function(){ - var uint8 = this.unpack_uint8(); - return (uint8 < 0x80 ) ? uint8 : uint8 - (1 << 8); -}; - -Unpacker.prototype.unpack_int16 = function(){ - var uint16 = this.unpack_uint16(); - return (uint16 < 0x8000 ) ? uint16 : uint16 - (1 << 16); -} - -Unpacker.prototype.unpack_int32 = function(){ - var uint32 = this.unpack_uint32(); - return (uint32 < Math.pow(2, 31) ) ? uint32 : - uint32 - Math.pow(2, 32); -} - -Unpacker.prototype.unpack_int64 = function(){ - var uint64 = this.unpack_uint64(); - return (uint64 < Math.pow(2, 63) ) ? uint64 : - uint64 - Math.pow(2, 64); -} - -Unpacker.prototype.unpack_raw = function(size){ - if ( this.length < this.index + size){ - throw new Error('BinaryPackFailure: index is out of range' - + ' ' + this.index + ' ' + size + ' ' + this.length); - } - var buf = this.dataBuffer.slice(this.index, this.index + size); - this.index += size; - - //buf = util.bufferToString(buf); - - return buf; -} - -Unpacker.prototype.unpack_string = function(size){ - var bytes = this.read(size); - var i = 0, str = '', c, code; - while(i < size){ - c = bytes[i]; - if ( c < 128){ - str += String.fromCharCode(c); - i++; - } else if ((c ^ 0xc0) < 32){ - code = ((c ^ 0xc0) << 6) | (bytes[i+1] & 63); - str += String.fromCharCode(code); - i += 2; - } else { - code = ((c & 15) << 12) | ((bytes[i+1] & 63) << 6) | - (bytes[i+2] & 63); - str += String.fromCharCode(code); - i += 3; - } - } - this.index += size; - return str; -} - -Unpacker.prototype.unpack_array = function(size){ - var objects = new Array(size); - for(var i = 0; i < size ; i++){ - objects[i] = this.unpack(); - } - return objects; -} - -Unpacker.prototype.unpack_map = function(size){ - var map = {}; - for(var i = 0; i < size ; i++){ - var key = this.unpack(); - var value = this.unpack(); - map[key] = value; - } - return map; -} - -Unpacker.prototype.unpack_float = function(){ - var uint32 = this.unpack_uint32(); - var sign = uint32 >> 31; - var exp = ((uint32 >> 23) & 0xff) - 127; - var fraction = ( uint32 & 0x7fffff ) | 0x800000; - return (sign == 0 ? 1 : -1) * - fraction * Math.pow(2, exp - 23); -} - -Unpacker.prototype.unpack_double = function(){ - var h32 = this.unpack_uint32(); - var l32 = this.unpack_uint32(); - var sign = h32 >> 31; - var exp = ((h32 >> 20) & 0x7ff) - 1023; - var hfrac = ( h32 & 0xfffff ) | 0x100000; - var frac = hfrac * Math.pow(2, exp - 20) + - l32 * Math.pow(2, exp - 52); - return (sign == 0 ? 1 : -1) * frac; -} - -Unpacker.prototype.read = function(length){ - var j = this.index; - if (j + length <= this.length) { - return this.dataView.subarray(j, j + length); - } else { - throw new Error('BinaryPackFailure: read index out of range'); - } -} - -function Packer(){ - this.bufferBuilder = new BufferBuilder(); -} - -Packer.prototype.getBuffer = function(){ - return this.bufferBuilder.getBuffer(); -} - -Packer.prototype.pack = function(value){ - var type = typeof(value); - if (type == 'string'){ - this.pack_string(value); - } else if (type == 'number'){ - if (Math.floor(value) === value){ - this.pack_integer(value); - } else{ - this.pack_double(value); - } - } else if (type == 'boolean'){ - if (value === true){ - this.bufferBuilder.append(0xc3); - } else if (value === false){ - this.bufferBuilder.append(0xc2); - } - } else if (type == 'undefined'){ - this.bufferBuilder.append(0xc0); - } else if (type == 'object'){ - if (value === null){ - this.bufferBuilder.append(0xc0); - } else { - var constructor = value.constructor; - if (constructor == Array){ - this.pack_array(value); - } else if (constructor == Blob || constructor == File) { - this.pack_bin(value); - } else if (constructor == ArrayBuffer) { - if(binaryFeatures.useArrayBufferView) { - this.pack_bin(new Uint8Array(value)); - } else { - this.pack_bin(value); - } - } else if ('BYTES_PER_ELEMENT' in value){ - if(binaryFeatures.useArrayBufferView) { - this.pack_bin(new Uint8Array(value.buffer)); - } else { - this.pack_bin(value.buffer); - } - } else if (constructor == Object){ - this.pack_object(value); - } else if (constructor == Date){ - this.pack_string(value.toString()); - } else if (typeof value.toBinaryPack == 'function'){ - this.bufferBuilder.append(value.toBinaryPack()); - } else { - throw new Error('Type "' + constructor.toString() + '" not yet supported'); - } - } - } else { - throw new Error('Type "' + type + '" not yet supported'); - } - this.bufferBuilder.flush(); -} - - -Packer.prototype.pack_bin = function(blob){ - var length = blob.length || blob.byteLength || blob.size; - if (length <= 0x0f){ - this.pack_uint8(0xa0 + length); - } else if (length <= 0xffff){ - this.bufferBuilder.append(0xda) ; - this.pack_uint16(length); - } else if (length <= 0xffffffff){ - this.bufferBuilder.append(0xdb); - this.pack_uint32(length); - } else{ - throw new Error('Invalid length'); - return; - } - this.bufferBuilder.append(blob); -} - -Packer.prototype.pack_string = function(str){ - var length = utf8Length(str); - - if (length <= 0x0f){ - this.pack_uint8(0xb0 + length); - } else if (length <= 0xffff){ - this.bufferBuilder.append(0xd8) ; - this.pack_uint16(length); - } else if (length <= 0xffffffff){ - this.bufferBuilder.append(0xd9); - this.pack_uint32(length); - } else{ - throw new Error('Invalid length'); - return; - } - this.bufferBuilder.append(str); -} - -Packer.prototype.pack_array = function(ary){ - var length = ary.length; - if (length <= 0x0f){ - this.pack_uint8(0x90 + length); - } else if (length <= 0xffff){ - this.bufferBuilder.append(0xdc) - this.pack_uint16(length); - } else if (length <= 0xffffffff){ - this.bufferBuilder.append(0xdd); - this.pack_uint32(length); - } else{ - throw new Error('Invalid length'); - } - for(var i = 0; i < length ; i++){ - this.pack(ary[i]); - } -} - -Packer.prototype.pack_integer = function(num){ - if ( -0x20 <= num && num <= 0x7f){ - this.bufferBuilder.append(num & 0xff); - } else if (0x00 <= num && num <= 0xff){ - this.bufferBuilder.append(0xcc); - this.pack_uint8(num); - } else if (-0x80 <= num && num <= 0x7f){ - this.bufferBuilder.append(0xd0); - this.pack_int8(num); - } else if ( 0x0000 <= num && num <= 0xffff){ - this.bufferBuilder.append(0xcd); - this.pack_uint16(num); - } else if (-0x8000 <= num && num <= 0x7fff){ - this.bufferBuilder.append(0xd1); - this.pack_int16(num); - } else if ( 0x00000000 <= num && num <= 0xffffffff){ - this.bufferBuilder.append(0xce); - this.pack_uint32(num); - } else if (-0x80000000 <= num && num <= 0x7fffffff){ - this.bufferBuilder.append(0xd2); - this.pack_int32(num); - } else if (-0x8000000000000000 <= num && num <= 0x7FFFFFFFFFFFFFFF){ - this.bufferBuilder.append(0xd3); - this.pack_int64(num); - } else if (0x0000000000000000 <= num && num <= 0xFFFFFFFFFFFFFFFF){ - this.bufferBuilder.append(0xcf); - this.pack_uint64(num); - } else{ - throw new Error('Invalid integer'); - } -} - -Packer.prototype.pack_double = function(num){ - var sign = 0; - if (num < 0){ - sign = 1; - num = -num; - } - var exp = Math.floor(Math.log(num) / Math.LN2); - var frac0 = num / Math.pow(2, exp) - 1; - var frac1 = Math.floor(frac0 * Math.pow(2, 52)); - var b32 = Math.pow(2, 32); - var h32 = (sign << 31) | ((exp+1023) << 20) | - (frac1 / b32) & 0x0fffff; - var l32 = frac1 % b32; - this.bufferBuilder.append(0xcb); - this.pack_int32(h32); - this.pack_int32(l32); -} - -Packer.prototype.pack_object = function(obj){ - var keys = Object.keys(obj); - var length = keys.length; - if (length <= 0x0f){ - this.pack_uint8(0x80 + length); - } else if (length <= 0xffff){ - this.bufferBuilder.append(0xde); - this.pack_uint16(length); - } else if (length <= 0xffffffff){ - this.bufferBuilder.append(0xdf); - this.pack_uint32(length); - } else{ - throw new Error('Invalid length'); - } - for(var prop in obj){ - if (obj.hasOwnProperty(prop)){ - this.pack(prop); - this.pack(obj[prop]); - } - } -} - -Packer.prototype.pack_uint8 = function(num){ - this.bufferBuilder.append(num); -} - -Packer.prototype.pack_uint16 = function(num){ - this.bufferBuilder.append(num >> 8); - this.bufferBuilder.append(num & 0xff); -} - -Packer.prototype.pack_uint32 = function(num){ - var n = num & 0xffffffff; - this.bufferBuilder.append((n & 0xff000000) >>> 24); - this.bufferBuilder.append((n & 0x00ff0000) >>> 16); - this.bufferBuilder.append((n & 0x0000ff00) >>> 8); - this.bufferBuilder.append((n & 0x000000ff)); -} - -Packer.prototype.pack_uint64 = function(num){ - var high = num / Math.pow(2, 32); - var low = num % Math.pow(2, 32); - this.bufferBuilder.append((high & 0xff000000) >>> 24); - this.bufferBuilder.append((high & 0x00ff0000) >>> 16); - this.bufferBuilder.append((high & 0x0000ff00) >>> 8); - this.bufferBuilder.append((high & 0x000000ff)); - this.bufferBuilder.append((low & 0xff000000) >>> 24); - this.bufferBuilder.append((low & 0x00ff0000) >>> 16); - this.bufferBuilder.append((low & 0x0000ff00) >>> 8); - this.bufferBuilder.append((low & 0x000000ff)); -} - -Packer.prototype.pack_int8 = function(num){ - this.bufferBuilder.append(num & 0xff); -} - -Packer.prototype.pack_int16 = function(num){ - this.bufferBuilder.append((num & 0xff00) >> 8); - this.bufferBuilder.append(num & 0xff); -} - -Packer.prototype.pack_int32 = function(num){ - this.bufferBuilder.append((num >>> 24) & 0xff); - this.bufferBuilder.append((num & 0x00ff0000) >>> 16); - this.bufferBuilder.append((num & 0x0000ff00) >>> 8); - this.bufferBuilder.append((num & 0x000000ff)); -} - -Packer.prototype.pack_int64 = function(num){ - var high = Math.floor(num / Math.pow(2, 32)); - var low = num % Math.pow(2, 32); - this.bufferBuilder.append((high & 0xff000000) >>> 24); - this.bufferBuilder.append((high & 0x00ff0000) >>> 16); - this.bufferBuilder.append((high & 0x0000ff00) >>> 8); - this.bufferBuilder.append((high & 0x000000ff)); - this.bufferBuilder.append((low & 0xff000000) >>> 24); - this.bufferBuilder.append((low & 0x00ff0000) >>> 16); - this.bufferBuilder.append((low & 0x0000ff00) >>> 8); - this.bufferBuilder.append((low & 0x000000ff)); -} - -function _utf8Replace(m){ - var code = m.charCodeAt(0); - - if(code <= 0x7ff) return '00'; - if(code <= 0xffff) return '000'; - if(code <= 0x1fffff) return '0000'; - if(code <= 0x3ffffff) return '00000'; - return '000000'; -} - -function utf8Length(str){ - if (str.length > 600) { - // Blob method faster for large strings - return (new Blob([str])).size; - } else { - return str.replace(/[^\u0000-\u007F]/g, _utf8Replace).length; - } -} -/** - * Light EventEmitter. Ported from Node.js/events.js - * Eric Zhang - */ - -/** - * EventEmitter class - * Creates an object with event registering and firing methods - */ -function EventEmitter() { - // Initialise required storage variables - this._events = {}; -} - -var isArray = Array.isArray; - - -EventEmitter.prototype.addListener = function(type, listener, scope, once) { - if ('function' !== typeof listener) { - throw new Error('addListener only takes instances of Function'); - } - - // To avoid recursion in the case that type == "newListeners"! Before - // adding it to the listeners, first emit "newListeners". - this.emit('newListener', type, typeof listener.listener === 'function' ? - listener.listener : listener); - - if (!this._events[type]) { - // Optimize the case of one listener. Don't need the extra array object. - this._events[type] = listener; - } else if (isArray(this._events[type])) { - - // If we've already got an array, just append. - this._events[type].push(listener); - - } else { - // Adding the second element, need to change to array. - this._events[type] = [this._events[type], listener]; - } - return this; -}; - -EventEmitter.prototype.on = EventEmitter.prototype.addListener; - -EventEmitter.prototype.once = function(type, listener, scope) { - if ('function' !== typeof listener) { - throw new Error('.once only takes instances of Function'); - } - - var self = this; - function g() { - self.removeListener(type, g); - listener.apply(this, arguments); - }; - - g.listener = listener; - self.on(type, g); - - return this; -}; - -EventEmitter.prototype.removeListener = function(type, listener, scope) { - if ('function' !== typeof listener) { - throw new Error('removeListener only takes instances of Function'); - } - - // does not use listeners(), so no side effect of creating _events[type] - if (!this._events[type]) return this; - - var list = this._events[type]; - - if (isArray(list)) { - var position = -1; - for (var i = 0, length = list.length; i < length; i++) { - if (list[i] === listener || - (list[i].listener && list[i].listener === listener)) - { - position = i; - break; - } - } - - if (position < 0) return this; - list.splice(position, 1); - if (list.length == 0) - delete this._events[type]; - } else if (list === listener || - (list.listener && list.listener === listener)) - { - delete this._events[type]; - } - - return this; -}; - - -EventEmitter.prototype.off = EventEmitter.prototype.removeListener; - - -EventEmitter.prototype.removeAllListeners = function(type) { - if (arguments.length === 0) { - this._events = {}; - return this; - } - - // does not use listeners(), so no side effect of creating _events[type] - if (type && this._events && this._events[type]) this._events[type] = null; - return this; -}; - -EventEmitter.prototype.listeners = function(type) { - if (!this._events[type]) this._events[type] = []; - if (!isArray(this._events[type])) { - this._events[type] = [this._events[type]]; - } - return this._events[type]; -}; - -EventEmitter.prototype.emit = function(type) { - var type = arguments[0]; - var handler = this._events[type]; - if (!handler) return false; - - if (typeof handler == 'function') { - switch (arguments.length) { - // fast cases - case 1: - handler.call(this); - break; - case 2: - handler.call(this, arguments[1]); - break; - case 3: - handler.call(this, arguments[1], arguments[2]); - break; - // slower - default: - var l = arguments.length; - var args = new Array(l - 1); - for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; - handler.apply(this, args); - } - return true; - - } else if (isArray(handler)) { - var l = arguments.length; - var args = new Array(l - 1); - for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; - - var listeners = handler.slice(); - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].apply(this, args); - } - return true; - } else { - return false; - } -}; - - - -/** - * Reliable transfer for Chrome Canary DataChannel impl. - * Author: @michellebu - */ -function Reliable(dc, debug) { - if (!(this instanceof Reliable)) return new Reliable(dc); - this._dc = dc; - - util.debug = debug; - - // Messages sent/received so far. - // id: { ack: n, chunks: [...] } - this._outgoing = {}; - // id: { ack: ['ack', id, n], chunks: [...] } - this._incoming = {}; - this._received = {}; - - // Window size. - this._window = 1000; - // MTU. - this._mtu = 500; - // Interval for setInterval. In ms. - this._interval = 0; - - // Messages sent. - this._count = 0; - - // Outgoing message queue. - this._queue = []; - - this._setupDC(); -}; - -// Send a message reliably. -Reliable.prototype.send = function(msg) { - // Determine if chunking is necessary. - var bl = util.pack(msg); - if (bl.size < this._mtu) { - this._handleSend(['no', bl]); - return; - } - - this._outgoing[this._count] = { - ack: 0, - chunks: this._chunk(bl) - }; - - if (util.debug) { - this._outgoing[this._count].timer = new Date(); - } - - // Send prelim window. - this._sendWindowedChunks(this._count); - this._count += 1; -}; - -// Set up interval for processing queue. -Reliable.prototype._setupInterval = function() { - // TODO: fail gracefully. - - var self = this; - this._timeout = setInterval(function() { - // FIXME: String stuff makes things terribly async. - var msg = self._queue.shift(); - if (msg._multiple) { - for (var i = 0, ii = msg.length; i < ii; i += 1) { - self._intervalSend(msg[i]); - } - } else { - self._intervalSend(msg); - } - }, this._interval); -}; - -Reliable.prototype._intervalSend = function(msg) { - var self = this; - msg = util.pack(msg); - util.blobToBinaryString(msg, function(str) { - self._dc.send(str); - }); - if (self._queue.length === 0) { - clearTimeout(self._timeout); - self._timeout = null; - //self._processAcks(); - } -}; - -// Go through ACKs to send missing pieces. -Reliable.prototype._processAcks = function() { - for (var id in this._outgoing) { - if (this._outgoing.hasOwnProperty(id)) { - this._sendWindowedChunks(id); - } - } -}; - -// Handle sending a message. -// FIXME: Don't wait for interval time for all messages... -Reliable.prototype._handleSend = function(msg) { - var push = true; - for (var i = 0, ii = this._queue.length; i < ii; i += 1) { - var item = this._queue[i]; - if (item === msg) { - push = false; - } else if (item._multiple && item.indexOf(msg) !== -1) { - push = false; - } - } - if (push) { - this._queue.push(msg); - if (!this._timeout) { - this._setupInterval(); - } - } -}; - -// Set up DataChannel handlers. -Reliable.prototype._setupDC = function() { - // Handle various message types. - var self = this; - this._dc.onmessage = function(e) { - var msg = e.data; - var datatype = msg.constructor; - // FIXME: msg is String until binary is supported. - // Once that happens, this will have to be smarter. - if (datatype === String) { - var ab = util.binaryStringToArrayBuffer(msg); - msg = util.unpack(ab); - self._handleMessage(msg); - } - }; -}; - -// Handles an incoming message. -Reliable.prototype._handleMessage = function(msg) { - var id = msg[1]; - var idata = this._incoming[id]; - var odata = this._outgoing[id]; - var data; - switch (msg[0]) { - // No chunking was done. - case 'no': - var message = id; - if (!!message) { - this.onmessage(util.unpack(message)); - } - break; - // Reached the end of the message. - case 'end': - data = idata; - - // In case end comes first. - this._received[id] = msg[2]; - - if (!data) { - break; - } - - this._ack(id); - break; - case 'ack': - data = odata; - if (!!data) { - var ack = msg[2]; - // Take the larger ACK, for out of order messages. - data.ack = Math.max(ack, data.ack); - - // Clean up when all chunks are ACKed. - if (data.ack >= data.chunks.length) { - util.log('Time: ', new Date() - data.timer); - delete this._outgoing[id]; - } else { - this._processAcks(); - } - } - // If !data, just ignore. - break; - // Received a chunk of data. - case 'chunk': - // Create a new entry if none exists. - data = idata; - if (!data) { - var end = this._received[id]; - if (end === true) { - break; - } - data = { - ack: ['ack', id, 0], - chunks: [] - }; - this._incoming[id] = data; - } - - var n = msg[2]; - var chunk = msg[3]; - data.chunks[n] = new Uint8Array(chunk); - - // If we get the chunk we're looking for, ACK for next missing. - // Otherwise, ACK the same N again. - if (n === data.ack[2]) { - this._calculateNextAck(id); - } - this._ack(id); - break; - default: - // Shouldn't happen, but would make sense for message to just go - // through as is. - this._handleSend(msg); - break; - } -}; - -// Chunks BL into smaller messages. -Reliable.prototype._chunk = function(bl) { - var chunks = []; - var size = bl.size; - var start = 0; - while (start < size) { - var end = Math.min(size, start + this._mtu); - var b = bl.slice(start, end); - var chunk = { - payload: b - } - chunks.push(chunk); - start = end; - } - util.log('Created', chunks.length, 'chunks.'); - return chunks; -}; - -// Sends ACK N, expecting Nth blob chunk for message ID. -Reliable.prototype._ack = function(id) { - var ack = this._incoming[id].ack; - - // if ack is the end value, then call _complete. - if (this._received[id] === ack[2]) { - this._complete(id); - this._received[id] = true; - } - - this._handleSend(ack); -}; - -// Calculates the next ACK number, given chunks. -Reliable.prototype._calculateNextAck = function(id) { - var data = this._incoming[id]; - var chunks = data.chunks; - for (var i = 0, ii = chunks.length; i < ii; i += 1) { - // This chunk is missing!!! Better ACK for it. - if (chunks[i] === undefined) { - data.ack[2] = i; - return; - } - } - data.ack[2] = chunks.length; -}; - -// Sends the next window of chunks. -Reliable.prototype._sendWindowedChunks = function(id) { - util.log('sendWindowedChunks for: ', id); - var data = this._outgoing[id]; - var ch = data.chunks; - var chunks = []; - var limit = Math.min(data.ack + this._window, ch.length); - for (var i = data.ack; i < limit; i += 1) { - if (!ch[i].sent || i === data.ack) { - ch[i].sent = true; - chunks.push(['chunk', id, i, ch[i].payload]); - } - } - if (data.ack + this._window >= ch.length) { - chunks.push(['end', id, ch.length]) - } - chunks._multiple = true; - this._handleSend(chunks); -}; - -// Puts together a message from chunks. -Reliable.prototype._complete = function(id) { - util.log('Completed called for', id); - var self = this; - var chunks = this._incoming[id].chunks; - var bl = new Blob(chunks); - util.blobToArrayBuffer(bl, function(ab) { - self.onmessage(util.unpack(ab)); - }); - delete this._incoming[id]; -}; - -// Ups bandwidth limit on SDP. Meant to be called during offer/answer. -Reliable.higherBandwidthSDP = function(sdp) { - // AS stands for Application-Specific Maximum. - // Bandwidth number is in kilobits / sec. - // See RFC for more info: http://www.ietf.org/rfc/rfc2327.txt - - // Chrome 31+ doesn't want us munging the SDP, so we'll let them have their - // way. - var version = navigator.appVersion.match(/Chrome\/(.*?) /); - if (version) { - version = parseInt(version[1].split('.').shift()); - if (version < 31) { - var parts = sdp.split('b=AS:30'); - var replace = 'b=AS:102400'; // 100 Mbps - if (parts.length > 1) { - return parts[0] + replace + parts[1]; - } - } - } - - return sdp; -}; - -// Overwritten, typically. -Reliable.prototype.onmessage = function(msg) {}; - -exports.Reliable = Reliable; -exports.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription; -exports.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; -exports.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate; -var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]}; -var dataCount = 1; - -var util = { - noop: function() {}, - - CLOUD_HOST: '0.peerjs.com', - CLOUD_PORT: 9000, - - // Browsers that need chunking: - chunkedBrowsers: {'Chrome': 1}, - chunkedMTU: 16300, // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually. - - // Logging logic - logLevel: 0, - setLogLevel: function(level) { - var debugLevel = parseInt(level, 10); - if (!isNaN(parseInt(level, 10))) { - util.logLevel = debugLevel; - } else { - // If they are using truthy/falsy values for debug - util.logLevel = level ? 3 : 0; - } - util.log = util.warn = util.error = util.noop; - if (util.logLevel > 0) { - util.error = util._printWith('ERROR'); - } - if (util.logLevel > 1) { - util.warn = util._printWith('WARNING'); - } - if (util.logLevel > 2) { - util.log = util._print; - } - }, - setLogFunction: function(fn) { - if (fn.constructor !== Function) { - util.warn('The log function you passed in is not a function. Defaulting to regular logs.'); - } else { - util._print = fn; - } - }, - - _printWith: function(prefix) { - return function() { - var copy = Array.prototype.slice.call(arguments); - copy.unshift(prefix); - util._print.apply(util, copy); - }; - }, - _print: function () { - var err = false; - var copy = Array.prototype.slice.call(arguments); - copy.unshift('PeerJS: '); - for (var i = 0, l = copy.length; i < l; i++){ - if (copy[i] instanceof Error) { - copy[i] = '(' + copy[i].name + ') ' + copy[i].message; - err = true; - } - } - err ? console.error.apply(console, copy) : console.log.apply(console, copy); - }, - // - - // Returns browser-agnostic default config - defaultConfig: defaultConfig, - // - - // Returns the current browser. - browser: (function() { - if (window.mozRTCPeerConnection) { - return 'Firefox'; - } else if (window.webkitRTCPeerConnection) { - return 'Chrome'; - } else if (window.RTCPeerConnection) { - return 'Supported'; - } else { - return 'Unsupported'; - } - })(), - // - - // Lists which features are supported - supports: (function() { - if (typeof RTCPeerConnection === 'undefined') { - return {}; - } - - var data = true; - var audioVideo = true; - - var binaryBlob = false; - var sctp = false; - var onnegotiationneeded = !!window.webkitRTCPeerConnection; - - var pc, dc; - try { - pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]}); - } catch (e) { - data = false; - audioVideo = false; - } - - if (data) { - try { - dc = pc.createDataChannel('_PEERJSTEST'); - } catch (e) { - data = false; - } - } - - if (data) { - // Binary test - try { - dc.binaryType = 'blob'; - binaryBlob = true; - } catch (e) { - } - - // Reliable test. - // Unfortunately Chrome is a bit unreliable about whether or not they - // support reliable. - var reliablePC = new RTCPeerConnection(defaultConfig, {}); - try { - var reliableDC = reliablePC.createDataChannel('_PEERJSRELIABLETEST', {}); - sctp = reliableDC.reliable; - } catch (e) { - } - reliablePC.close(); - } - - // FIXME: not really the best check... - if (audioVideo) { - audioVideo = !!pc.addStream; - } - - // FIXME: this is not great because in theory it doesn't work for - // av-only browsers (?). - if (!onnegotiationneeded && data) { - // sync default check. - var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]}); - negotiationPC.onnegotiationneeded = function() { - onnegotiationneeded = true; - // async check. - if (util && util.supports) { - util.supports.onnegotiationneeded = true; - } - }; - var negotiationDC = negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST'); - - setTimeout(function() { - negotiationPC.close(); - }, 1000); - } - - if (pc) { - pc.close(); - } - - return { - audioVideo: audioVideo, - data: data, - binaryBlob: binaryBlob, - binary: sctp, // deprecated; sctp implies binary support. - reliable: sctp, // deprecated; sctp implies reliable data. - sctp: sctp, - onnegotiationneeded: onnegotiationneeded - }; - }()), - // - - // Ensure alphanumeric ids - validateId: function(id) { - // Allow empty ids - return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id); - }, - - validateKey: function(key) { - // Allow empty keys - return !key || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(key); - }, - - - debug: false, - - inherits: function(ctor, superCtor) { - ctor.super_ = superCtor; - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); - }, - extend: function(dest, source) { - for(var key in source) { - if(source.hasOwnProperty(key)) { - dest[key] = source[key]; - } - } - return dest; - }, - pack: BinaryPack.pack, - unpack: BinaryPack.unpack, - - log: function () { - if (util.debug) { - var err = false; - var copy = Array.prototype.slice.call(arguments); - copy.unshift('PeerJS: '); - for (var i = 0, l = copy.length; i < l; i++){ - if (copy[i] instanceof Error) { - copy[i] = '(' + copy[i].name + ') ' + copy[i].message; - err = true; - } - } - err ? console.error.apply(console, copy) : console.log.apply(console, copy); - } - }, - - setZeroTimeout: (function(global) { - var timeouts = []; - var messageName = 'zero-timeout-message'; - - // Like setTimeout, but only takes a function argument. There's - // no time argument (always zero) and no arguments (you have to - // use a closure). - function setZeroTimeoutPostMessage(fn) { - timeouts.push(fn); - global.postMessage(messageName, '*'); - } - - function handleMessage(event) { - if (event.source == global && event.data == messageName) { - if (event.stopPropagation) { - event.stopPropagation(); - } - if (timeouts.length) { - timeouts.shift()(); - } - } - } - if (global.addEventListener) { - global.addEventListener('message', handleMessage, true); - } else if (global.attachEvent) { - global.attachEvent('onmessage', handleMessage); - } - return setZeroTimeoutPostMessage; - }(this)), - - // Binary stuff - - // chunks a blob. - chunk: function(bl) { - var chunks = []; - var size = bl.size; - var start = index = 0; - var total = Math.ceil(size / util.chunkedMTU); - while (start < size) { - var end = Math.min(size, start + util.chunkedMTU); - var b = bl.slice(start, end); - - var chunk = { - __peerData: dataCount, - n: index, - data: b, - total: total - }; - - chunks.push(chunk); - - start = end; - index += 1; - } - dataCount += 1; - return chunks; - }, - - blobToArrayBuffer: function(blob, cb){ - var fr = new FileReader(); - fr.onload = function(evt) { - cb(evt.target.result); - }; - fr.readAsArrayBuffer(blob); - }, - blobToBinaryString: function(blob, cb){ - var fr = new FileReader(); - fr.onload = function(evt) { - cb(evt.target.result); - }; - fr.readAsBinaryString(blob); - }, - binaryStringToArrayBuffer: function(binary) { - var byteArray = new Uint8Array(binary.length); - for (var i = 0; i < binary.length; i++) { - byteArray[i] = binary.charCodeAt(i) & 0xff; - } - return byteArray.buffer; - }, - randomToken: function () { - return Math.random().toString(36).substr(2); - }, - // - - isSecure: function() { - return location.protocol === 'https:'; - } -}; - -exports.util = util; -/** - * A peer who can initiate connections with other peers. - */ -function Peer(id, options) { - if (!(this instanceof Peer)) return new Peer(id, options); - EventEmitter.call(this); - - // Deal with overloading - if (id && id.constructor == Object) { - options = id; - id = undefined; - } else if (id) { - // Ensure id is a string - id = id.toString(); - } - // - - // Configurize options - options = util.extend({ - debug: 0, // 1: Errors, 2: Warnings, 3: All logs - host: util.CLOUD_HOST, - port: util.CLOUD_PORT, - key: 'peerjs', - path: '/', - token: util.randomToken(), - config: util.defaultConfig - }, options); - this.options = options; - // Detect relative URL host. - if (options.host === '/') { - options.host = window.location.hostname; - } - // Set path correctly. - if (options.path[0] !== '/') { - options.path = '/' + options.path; - } - if (options.path[options.path.length - 1] !== '/') { - options.path += '/'; - } - - // Set whether we use SSL to same as current host - if (options.secure === undefined && options.host !== util.CLOUD_HOST) { - options.secure = util.isSecure(); - } - // Set a custom log function if present - if (options.logFunction) { - util.setLogFunction(options.logFunction); - } - util.setLogLevel(options.debug); - // - - // Sanity checks - // Ensure WebRTC supported - if (!util.supports.audioVideo && !util.supports.data ) { - this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC'); - return; - } - // Ensure alphanumeric id - if (!util.validateId(id)) { - this._delayedAbort('invalid-id', 'ID "' + id + '" is invalid'); - return; - } - // Ensure valid key - if (!util.validateKey(options.key)) { - this._delayedAbort('invalid-key', 'API KEY "' + options.key + '" is invalid'); - return; - } - // Ensure not using unsecure cloud server on SSL page - if (options.secure && options.host === '0.peerjs.com') { - this._delayedAbort('ssl-unavailable', - 'The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS.'); - return; - } - // - - // States. - this.destroyed = false; // Connections have been killed - this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active - this.open = false; // Sockets and such are not yet open. - // - - // References - this.connections = {}; // DataConnections for this peer. - this._lostMessages = {}; // src => [list of messages] - // - - // Initialize the 'socket' (which is actually a mix of XHR streaming and - // websockets.) - var self = this; - this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key); - this.socket.on('message', function(data) { - self._handleMessage(data); - }); - this.socket.on('error', function(error) { - self._abort('socket-error', error); - }); - this.socket.on('close', function() { - if (!self.disconnected) { // If we haven't explicitly disconnected, emit error. - self._abort('socket-closed', 'Underlying socket is already closed.'); - } - }); - // - - // Start the connections - if (id) { - this._initialize(id); - } else { - this._retrieveId(); - } - // -}; - -util.inherits(Peer, EventEmitter); - -/** Get a unique ID from the server via XHR. */ -Peer.prototype._retrieveId = function(cb) { - var self = this; - var http = new XMLHttpRequest(); - var protocol = this.options.secure ? 'https://' : 'http://'; - var url = protocol + this.options.host + ':' + this.options.port - + this.options.path + this.options.key + '/id'; - var queryString = '?ts=' + new Date().getTime() + '' + Math.random(); - url += queryString; - - // If there's no ID we need to wait for one before trying to init socket. - http.open('get', url, true); - http.onerror = function(e) { - util.error('Error retrieving ID', e); - var pathError = ''; - if (self.options.path === '/' && self.options.host !== util.CLOUD_HOST) { - pathError = ' If you passed in a `path` to your self-hosted PeerServer, ' - + 'you\'ll also need to pass in that same path when creating a new' - + ' Peer.'; - } - self._abort('server-error', 'Could not get an ID from the server.' + pathError); - } - http.onreadystatechange = function() { - if (http.readyState !== 4) { - return; - } - if (http.status !== 200) { - http.onerror(); - return; - } - self._initialize(http.responseText); - }; - http.send(null); -}; - -/** Initialize a connection with the server. */ -Peer.prototype._initialize = function(id) { - var self = this; - this.id = id; - this.socket.start(this.id, this.options.token); -} - -/** Handles messages from the server. */ -Peer.prototype._handleMessage = function(message) { - var type = message.type; - var payload = message.payload; - var peer = message.src; - - switch (type) { - case 'OPEN': // The connection to the server is open. - this.emit('open', this.id); - this.open = true; - break; - case 'ERROR': // Server error. - this._abort('server-error', payload.msg); - break; - case 'ID-TAKEN': // The selected ID is taken. - this._abort('unavailable-id', 'ID `' + this.id + '` is taken'); - break; - case 'INVALID-KEY': // The given API key cannot be found. - this._abort('invalid-key', 'API KEY "' + this.options.key + '" is invalid'); - break; - - // - case 'LEAVE': // Another peer has closed its connection to this peer. - util.log('Received leave message from', peer); - this._cleanupPeer(peer); - break; - - case 'EXPIRE': // The offer sent to a peer has expired without response. - this.emit('error', new Error('Could not connect to peer ' + peer)); - break; - case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option. - var connectionId = payload.connectionId; - var connection = this.getConnection(peer, connectionId); - - if (connection) { - util.warn('Offer received for existing Connection ID:', connectionId); - //connection.handleMessage(message); - } else { - // Create a new connection. - if (payload.type === 'media') { - var connection = new MediaConnection(peer, this, { - connectionId: connectionId, - _payload: payload, - metadata: payload.metadata - }); - this._addConnection(peer, connection); - this.emit('call', connection); - } else if (payload.type === 'data') { - connection = new DataConnection(peer, this, { - connectionId: connectionId, - _payload: payload, - metadata: payload.metadata, - label: payload.label, - serialization: payload.serialization, - reliable: payload.reliable - }); - this._addConnection(peer, connection); - this.emit('connection', connection); - } else { - util.warn('Received malformed connection type:', payload.type); - return; - } - // Find messages. - var messages = this._getMessages(connectionId); - for (var i = 0, ii = messages.length; i < ii; i += 1) { - connection.handleMessage(messages[i]); - } - } - break; - default: - if (!payload) { - util.warn('You received a malformed message from ' + peer + ' of type ' + type); - return; - } - - var id = payload.connectionId; - var connection = this.getConnection(peer, id); - - if (connection && connection.pc) { - // Pass it on. - connection.handleMessage(message); - } else if (id) { - // Store for possible later use - this._storeMessage(id, message); - } else { - util.warn('You received an unrecognized message:', message); - } - break; - } -} - -/** Stores messages without a set up connection, to be claimed later. */ -Peer.prototype._storeMessage = function(connectionId, message) { - if (!this._lostMessages[connectionId]) { - this._lostMessages[connectionId] = []; - } - this._lostMessages[connectionId].push(message); -} - -/** Retrieve messages from lost message store */ -Peer.prototype._getMessages = function(connectionId) { - var messages = this._lostMessages[connectionId]; - if (messages) { - delete this._lostMessages[connectionId]; - return messages; - } else { - return []; - } -} - -/** - * Returns a DataConnection to the specified peer. See documentation for a - * complete list of options. - */ -Peer.prototype.connect = function(peer, options) { - if (this.disconnected) { - util.warn('You cannot connect to a new Peer because you called ' - + '.disconnect() on this Peer and ended your connection with the' - + ' server. You can create a new Peer to reconnect.'); - this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.')); - return; - } - var connection = new DataConnection(peer, this, options); - this._addConnection(peer, connection); - return connection; -} - -/** - * Returns a MediaConnection to the specified peer. See documentation for a - * complete list of options. - */ -Peer.prototype.call = function(peer, stream, options) { - if (this.disconnected) { - util.warn('You cannot connect to a new Peer because you called ' - + '.disconnect() on this Peer and ended your connection with the' - + ' server. You can create a new Peer to reconnect.'); - this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.')); - return; - } - if (!stream) { - util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.'); - return; - } - options = options || {}; - options._stream = stream; - var call = new MediaConnection(peer, this, options); - this._addConnection(peer, call); - return call; -} - -/** Add a data/media connection to this peer. */ -Peer.prototype._addConnection = function(peer, connection) { - if (!this.connections[peer]) { - this.connections[peer] = []; - } - this.connections[peer].push(connection); -} - -/** Retrieve a data/media connection for this peer. */ -Peer.prototype.getConnection = function(peer, id) { - var connections = this.connections[peer]; - if (!connections) { - return null; - } - for (var i = 0, ii = connections.length; i < ii; i++) { - if (connections[i].id === id) { - return connections[i]; - } - } - return null; -} - -Peer.prototype._delayedAbort = function(type, message) { - var self = this; - util.setZeroTimeout(function(){ - self._abort(type, message); - }); -} - -/** Destroys the Peer and emits an error message. */ -Peer.prototype._abort = function(type, message) { - util.error('Aborting. Error:', message); - var err = new Error(message); - err.type = type; - this.destroy(); - this.emit('error', err); -}; - -/** - * Destroys the Peer: closes all active connections as well as the connection - * to the server. - * Warning: The peer can no longer create or accept connections after being - * destroyed. - */ -Peer.prototype.destroy = function() { - if (!this.destroyed) { - this._cleanup(); - this.disconnect(); - this.destroyed = true; - } -} - - -/** Disconnects every connection on this peer. */ -Peer.prototype._cleanup = function() { - if (this.connections) { - var peers = Object.keys(this.connections); - for (var i = 0, ii = peers.length; i < ii; i++) { - this._cleanupPeer(peers[i]); - } - } - this.emit('close'); -} - -/** Closes all connections to this peer. */ -Peer.prototype._cleanupPeer = function(peer) { - var connections = this.connections[peer]; - for (var j = 0, jj = connections.length; j < jj; j += 1) { - connections[j].close(); - } -} - -/** - * Disconnects the Peer's connection to the PeerServer. Does not close any - * active connections. - * Warning: The peer can no longer create or accept connections after being - * disconnected. It also cannot reconnect to the server. - */ -Peer.prototype.disconnect = function() { - var self = this; - util.setZeroTimeout(function(){ - if (!self.disconnected) { - self.disconnected = true; - self.open = false; - if (self.socket) { - self.socket.close(); - } - self.id = null; - } - }); -} - -/** - * Get a list of available peer IDs. If you're running your own server, you'll - * want to set allow_discovery: true in the PeerServer options. If you're using - * the cloud server, email team@peerjs.com to get the functionality enabled for - * your key. - */ -Peer.prototype.listAllPeers = function(cb) { - cb = cb || function() {}; - var self = this; - var http = new XMLHttpRequest(); - var protocol = this.options.secure ? 'https://' : 'http://'; - var url = protocol + this.options.host + ':' + this.options.port - + this.options.path + this.options.key + '/peers'; - var queryString = '?ts=' + new Date().getTime() + '' + Math.random(); - url += queryString; - - // If there's no ID we need to wait for one before trying to init socket. - http.open('get', url, true); - http.onerror = function(e) { - self._abort('server-error', 'Could not get peers from the server.'); - cb([]); - } - http.onreadystatechange = function() { - if (http.readyState !== 4) { - return; - } - if (http.status === 401) { - var helpfulError = ''; - if (self.options.host !== util.CLOUD_HOST) { - helpfulError = 'It looks like you\'re using the cloud server. You can email ' - + 'team@peerjs.com to enable peer listing for your API key.'; - } else { - helpfulError = 'You need to enable `allow_discovery` on your self-hosted' - + ' PeerServer to use this feature.'; - } - throw new Error('It doesn\'t look like you have permission to list peers IDs. ' + helpfulError); - cb([]); - } else if (http.status !== 200) { - cb([]); - } else { - cb(JSON.parse(http.responseText)); - } - }; - http.send(null); -} - -exports.Peer = Peer; -/** - * Wraps a DataChannel between two Peers. - */ -function DataConnection(peer, provider, options) { - if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options); - EventEmitter.call(this); - - this.options = util.extend({ - serialization: 'binary', - reliable: false - }, options); - - // Connection is not open yet. - this.open = false; - this.type = 'data'; - this.peer = peer; - this.provider = provider; - - this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken(); - - this.label = this.options.label || this.id; - this.metadata = this.options.metadata; - this.serialization = this.options.serialization; - this.reliable = this.options.reliable; - - // Data channel buffering. - this._buffer = []; - this._buffering = false; - this.bufferSize = 0; - - // For storing large data. - this._chunkedData = {}; - - if (this.options._payload) { - this._peerBrowser = this.options._payload.browser; - } - - Negotiator.startConnection( - this, - this.options._payload || { - originator: true - } - ); -} - -util.inherits(DataConnection, EventEmitter); - -DataConnection._idPrefix = 'dc_'; - -/** Called by the Negotiator when the DataChannel is ready. */ -DataConnection.prototype.initialize = function(dc) { - this._dc = this.dataChannel = dc; - this._configureDataChannel(); -} - -DataConnection.prototype._configureDataChannel = function() { - var self = this; - if (util.supports.sctp) { - this._dc.binaryType = 'arraybuffer'; - } - this._dc.onopen = function() { - util.log('Data channel connection success'); - self.open = true; - self.emit('open'); - } - - // Use the Reliable shim for non Firefox browsers - if (!util.supports.sctp && this.reliable) { - this._reliable = new Reliable(this._dc, util.debug); - } - - if (this._reliable) { - this._reliable.onmessage = function(msg) { - self.emit('data', msg); - }; - } else { - this._dc.onmessage = function(e) { - self._handleDataMessage(e); - }; - } - this._dc.onclose = function(e) { - util.log('DataChannel closed for:', self.peer); - self.close(); - }; -} - -// Handles a DataChannel message. -DataConnection.prototype._handleDataMessage = function(e) { - var self = this; - var data = e.data; - var datatype = data.constructor; - if (this.serialization === 'binary' || this.serialization === 'binary-utf8') { - if (datatype === Blob) { - // Datatype should never be blob - util.blobToArrayBuffer(data, function(ab) { - data = util.unpack(ab); - self.emit('data', data); - }); - return; - } else if (datatype === ArrayBuffer) { - data = util.unpack(data); - } else if (datatype === String) { - // String fallback for binary data for browsers that don't support binary yet - var ab = util.binaryStringToArrayBuffer(data); - data = util.unpack(ab); - } - } else if (this.serialization === 'json') { - data = JSON.parse(data); - } - - // Check if we've chunked--if so, piece things back together. - // We're guaranteed that this isn't 0. - if (data.__peerData) { - var id = data.__peerData; - var chunkInfo = this._chunkedData[id] || {data: [], count: 0, total: data.total}; - - chunkInfo.data[data.n] = data.data; - chunkInfo.count += 1; - - if (chunkInfo.total === chunkInfo.count) { - // We've received all the chunks--time to construct the complete data. - data = new Blob(chunkInfo.data); - this._handleDataMessage({data: data}); - - // We can also just delete the chunks now. - delete this._chunkedData[id]; - } - - this._chunkedData[id] = chunkInfo; - return; - } - - this.emit('data', data); -} - -/** - * Exposed functionality for users. - */ - -/** Allows user to close connection. */ -DataConnection.prototype.close = function() { - if (!this.open) { - return; - } - this.open = false; - Negotiator.cleanup(this); - this.emit('close'); -} - -/** Allows user to send data. */ -DataConnection.prototype.send = function(data, chunked) { - if (!this.open) { - this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.')); - return; - } - if (this._reliable) { - // Note: reliable shim sending will make it so that you cannot customize - // serialization. - this._reliable.send(data); - return; - } - var self = this; - if (this.serialization === 'json') { - this._bufferedSend(JSON.stringify(data)); - } else if (this.serialization === 'binary' || this.serialization === 'binary-utf8') { - var blob = util.pack(data); - - // For Chrome-Firefox interoperability, we need to make Firefox "chunk" - // the data it sends out. - var needsChunking = util.chunkedBrowsers[this._peerBrowser] || util.chunkedBrowsers[util.browser]; - if (needsChunking && !chunked && blob.size > util.chunkedMTU) { - this._sendChunks(blob); - return; - } - - // DataChannel currently only supports strings. - if (!util.supports.sctp) { - util.blobToBinaryString(blob, function(str) { - self._bufferedSend(str); - }); - } else if (!util.supports.binaryBlob) { - // We only do this if we really need to (e.g. blobs are not supported), - // because this conversion is costly. - util.blobToArrayBuffer(blob, function(ab) { - self._bufferedSend(ab); - }); - } else { - this._bufferedSend(blob); - } - } else { - this._bufferedSend(data); - } -} - -DataConnection.prototype._bufferedSend = function(msg) { - if (this._buffering || !this._trySend(msg)) { - this._buffer.push(msg); - this.bufferSize = this._buffer.length; - } -} - -// Returns true if the send succeeds. -DataConnection.prototype._trySend = function(msg) { - try { - this._dc.send(msg); - } catch (e) { - this._buffering = true; - - var self = this; - setTimeout(function() { - // Try again. - self._buffering = false; - self._tryBuffer(); - }, 100); - return false; - } - return true; -} - -// Try to send the first message in the buffer. -DataConnection.prototype._tryBuffer = function() { - if (this._buffer.length === 0) { - return; - } - - var msg = this._buffer[0]; - - if (this._trySend(msg)) { - this._buffer.shift(); - this.bufferSize = this._buffer.length; - this._tryBuffer(); - } -} - -DataConnection.prototype._sendChunks = function(blob) { - var blobs = util.chunk(blob); - for (var i = 0, ii = blobs.length; i < ii; i += 1) { - var blob = blobs[i]; - this.send(blob, true); - } -} - -DataConnection.prototype.handleMessage = function(message) { - var payload = message.payload; - - switch (message.type) { - case 'ANSWER': - this._peerBrowser = payload.browser; - - // Forward to negotiator - Negotiator.handleSDP(message.type, this, payload.sdp); - break; - case 'CANDIDATE': - Negotiator.handleCandidate(this, payload.candidate); - break; - default: - util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer); - break; - } -} -/** - * Wraps the streaming interface between two Peers. - */ -function MediaConnection(peer, provider, options) { - if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options); - EventEmitter.call(this); - - this.options = util.extend({}, options); - - this.open = false; - this.type = 'media'; - this.peer = peer; - this.provider = provider; - this.metadata = this.options.metadata; - this.localStream = this.options._stream; - - this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken(); - if (this.localStream) { - Negotiator.startConnection( - this, - {_stream: this.localStream, originator: true} - ); - } -}; - -util.inherits(MediaConnection, EventEmitter); - -MediaConnection._idPrefix = 'mc_'; - -MediaConnection.prototype.addStream = function(remoteStream) { - util.log('Receiving stream', remoteStream); - - this.remoteStream = remoteStream; - this.emit('stream', remoteStream); // Should we call this `open`? - -}; - -MediaConnection.prototype.handleMessage = function(message) { - var payload = message.payload; - - switch (message.type) { - case 'ANSWER': - // Forward to negotiator - Negotiator.handleSDP(message.type, this, payload.sdp); - this.open = true; - break; - case 'CANDIDATE': - Negotiator.handleCandidate(this, payload.candidate); - break; - default: - util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer); - break; - } -} - -MediaConnection.prototype.answer = function(stream) { - if (this.localStream) { - util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?'); - return; - } - - this.options._payload._stream = stream; - - this.localStream = stream; - Negotiator.startConnection( - this, - this.options._payload - ) - // Retrieve lost messages stored because PeerConnection not set up. - var messages = this.provider._getMessages(this.id); - for (var i = 0, ii = messages.length; i < ii; i += 1) { - this.handleMessage(messages[i]); - } - this.open = true; -}; - -/** - * Exposed functionality for users. - */ - -/** Allows user to close connection. */ -MediaConnection.prototype.close = function() { - if (!this.open) { - return; - } - this.open = false; - Negotiator.cleanup(this); - this.emit('close') -}; -/** - * Manages all negotiations between Peers. - */ -var Negotiator = { - pcs: { - data: {}, - media: {} - }, // type => {peerId: {pc_id: pc}}. - //providers: {}, // provider's id => providers (there may be multiple providers/client. - queue: [] // connections that are delayed due to a PC being in use. -} - -Negotiator._idPrefix = 'pc_'; - -/** Returns a PeerConnection object set up correctly (for data, media). */ -Negotiator.startConnection = function(connection, options) { - var pc = Negotiator._getPeerConnection(connection, options); - - if (connection.type === 'media' && options._stream) { - // Add the stream. - pc.addStream(options._stream); - } - - // Set the connection's PC. - connection.pc = connection.peerConnection = pc; - // What do we need to do now? - if (options.originator) { - if (connection.type === 'data') { - // Create the datachannel. - var config = {}; - // Dropping reliable:false support, since it seems to be crashing - // Chrome. - /*if (util.supports.sctp && !options.reliable) { - // If we have canonical reliable support... - config = {maxRetransmits: 0}; - }*/ - // Fallback to ensure older browsers don't crash. - if (!util.supports.sctp) { - config = {reliable: options.reliable}; - } - var dc = pc.createDataChannel(connection.label, config); - connection.initialize(dc); - } - - if (!util.supports.onnegotiationneeded) { - Negotiator._makeOffer(connection); - } - } else { - Negotiator.handleSDP('OFFER', connection, options.sdp); - } -} - -Negotiator._getPeerConnection = function(connection, options) { - if (!Negotiator.pcs[connection.type]) { - util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.'); - } - - if (!Negotiator.pcs[connection.type][connection.peer]) { - Negotiator.pcs[connection.type][connection.peer] = {}; - } - var peerConnections = Negotiator.pcs[connection.type][connection.peer]; - - var pc; - // Not multiplexing while FF and Chrome have not-great support for it. - /*if (options.multiplex) { - ids = Object.keys(peerConnections); - for (var i = 0, ii = ids.length; i < ii; i += 1) { - pc = peerConnections[ids[i]]; - if (pc.signalingState === 'stable') { - break; // We can go ahead and use this PC. - } - } - } else */ - if (options.pc) { // Simplest case: PC id already provided for us. - pc = Negotiator.pcs[connection.type][connection.peer][options.pc]; - } - - if (!pc || pc.signalingState !== 'stable') { - pc = Negotiator._startPeerConnection(connection); - } - return pc; -} - -/* -Negotiator._addProvider = function(provider) { - if ((!provider.id && !provider.disconnected) || !provider.socket.open) { - // Wait for provider to obtain an ID. - provider.on('open', function(id) { - Negotiator._addProvider(provider); - }); - } else { - Negotiator.providers[provider.id] = provider; - } -}*/ - - -/** Start a PC. */ -Negotiator._startPeerConnection = function(connection) { - util.log('Creating RTCPeerConnection.'); - - var id = Negotiator._idPrefix + util.randomToken(); - var optional = {}; - - if (connection.type === 'data' && !util.supports.sctp) { - optional = {optional: [{RtpDataChannels: true}]}; - } else if (connection.type === 'media') { - // Interop req for chrome. - optional = {optional: [{DtlsSrtpKeyAgreement: true}]}; - } - - var pc = new RTCPeerConnection(connection.provider.options.config, optional); - Negotiator.pcs[connection.type][connection.peer][id] = pc; - - Negotiator._setupListeners(connection, pc, id); - - return pc; -} - -/** Set up various WebRTC listeners. */ -Negotiator._setupListeners = function(connection, pc, pc_id) { - var peerId = connection.peer; - var connectionId = connection.id; - var provider = connection.provider; - - // ICE CANDIDATES. - util.log('Listening for ICE candidates.'); - pc.onicecandidate = function(evt) { - if (evt.candidate) { - util.log('Received ICE candidates for:', connection.peer); - provider.socket.send({ - type: 'CANDIDATE', - payload: { - candidate: evt.candidate, - type: connection.type, - connectionId: connection.id - }, - dst: peerId - }); - } - }; - - pc.oniceconnectionstatechange = function() { - switch (pc.iceConnectionState) { - case 'disconnected': - case 'failed': - util.log('iceConnectionState is disconnected, closing connections to ' + peerId); - connection.close(); - break; - case 'completed': - pc.onicecandidate = util.noop; - break; - } - }; - - // Fallback for older Chrome impls. - pc.onicechange = pc.oniceconnectionstatechange; - - // ONNEGOTIATIONNEEDED (Chrome) - util.log('Listening for `negotiationneeded`'); - pc.onnegotiationneeded = function() { - util.log('`negotiationneeded` triggered'); - if (pc.signalingState == 'stable') { - Negotiator._makeOffer(connection); - } else { - util.log('onnegotiationneeded triggered when not stable. Is another connection being established?'); - } - }; - - // DATACONNECTION. - util.log('Listening for data channel'); - // Fired between offer and answer, so options should already be saved - // in the options hash. - pc.ondatachannel = function(evt) { - util.log('Received data channel'); - var dc = evt.channel; - var connection = provider.getConnection(peerId, connectionId); - connection.initialize(dc); - }; - - // MEDIACONNECTION. - util.log('Listening for remote stream'); - pc.onaddstream = function(evt) { - util.log('Received remote stream'); - var stream = evt.stream; - provider.getConnection(peerId, connectionId).addStream(stream); - }; -} - -Negotiator.cleanup = function(connection) { - util.log('Cleaning up PeerConnection to ' + connection.peer); - - var pc = connection.pc; - - if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) { - pc.close(); - connection.pc = null; - } -} - -Negotiator._makeOffer = function(connection) { - var pc = connection.pc; - pc.createOffer(function(offer) { - util.log('Created offer.'); - - if (!util.supports.sctp && connection.type === 'data' && connection.reliable) { - offer.sdp = Reliable.higherBandwidthSDP(offer.sdp); - } - - pc.setLocalDescription(offer, function() { - util.log('Set localDescription: offer', 'for:', connection.peer); - connection.provider.socket.send({ - type: 'OFFER', - payload: { - sdp: offer, - type: connection.type, - label: connection.label, - connectionId: connection.id, - reliable: connection.reliable, - serialization: connection.serialization, - metadata: connection.metadata, - browser: util.browser - }, - dst: connection.peer - }); - }, function(err) { - connection.provider.emit('error', err); - util.log('Failed to setLocalDescription, ', err); - }); - }, function(err) { - connection.provider.emit('error', err); - util.log('Failed to createOffer, ', err); - }, connection.options.constraints); -} - -Negotiator._makeAnswer = function(connection) { - var pc = connection.pc; - - pc.createAnswer(function(answer) { - util.log('Created answer.'); - - if (!util.supports.sctp && connection.type === 'data' && connection.reliable) { - answer.sdp = Reliable.higherBandwidthSDP(answer.sdp); - } - - pc.setLocalDescription(answer, function() { - util.log('Set localDescription: answer', 'for:', connection.peer); - connection.provider.socket.send({ - type: 'ANSWER', - payload: { - sdp: answer, - type: connection.type, - connectionId: connection.id, - browser: util.browser - }, - dst: connection.peer - }); - }, function(err) { - connection.provider.emit('error', err); - util.log('Failed to setLocalDescription, ', err); - }); - }, function(err) { - connection.provider.emit('error', err); - util.log('Failed to create answer, ', err); - }); -} - -/** Handle an SDP. */ -Negotiator.handleSDP = function(type, connection, sdp) { - sdp = new RTCSessionDescription(sdp); - var pc = connection.pc; - - util.log('Setting remote description', sdp); - pc.setRemoteDescription(sdp, function() { - util.log('Set remoteDescription:', type, 'for:', connection.peer); - - if (type === 'OFFER') { - Negotiator._makeAnswer(connection); - } - }, function(err) { - connection.provider.emit('error', err); - util.log('Failed to setRemoteDescription, ', err); - }); -} - -/** Handle a candidate. */ -Negotiator.handleCandidate = function(connection, ice) { - var candidate = ice.candidate; - var sdpMLineIndex = ice.sdpMLineIndex; - connection.pc.addIceCandidate(new RTCIceCandidate({ - sdpMLineIndex: sdpMLineIndex, - candidate: candidate - })); - util.log('Added ICE candidate for:', connection.peer); -} -/** - * An abstraction on top of WebSockets and XHR streaming to provide fastest - * possible connection for peers. - */ -function Socket(secure, host, port, path, key) { - if (!(this instanceof Socket)) return new Socket(secure, host, port, path, key); - - EventEmitter.call(this); - - // Disconnected manually. - this.disconnected = false; - this._queue = []; - - var httpProtocol = secure ? 'https://' : 'http://'; - var wsProtocol = secure ? 'wss://' : 'ws://'; - this._httpUrl = httpProtocol + host + ':' + port + path + key; - this._wsUrl = wsProtocol + host + ':' + port + path + 'peerjs?key=' + key; -} - -util.inherits(Socket, EventEmitter); - - -/** Check in with ID or get one from server. */ -Socket.prototype.start = function(id, token) { - this.id = id; - - this._httpUrl += '/' + id + '/' + token; - this._wsUrl += '&id='+id+'&token='+token; - - this._startXhrStream(); - this._startWebSocket(); -} - - -/** Start up websocket communications. */ -Socket.prototype._startWebSocket = function(id) { - var self = this; - - if (this._socket) { - return; - } - - this._socket = new WebSocket(this._wsUrl); - - this._socket.onmessage = function(event) { - var data; - try { - data = JSON.parse(event.data); - } catch(e) { - util.log('Invalid server message', event.data); - return; - } - self.emit('message', data); - }; - - // Take care of the queue of connections if necessary and make sure Peer knows - // socket is open. - this._socket.onopen = function() { - if (self._timeout) { - clearTimeout(self._timeout); - setTimeout(function(){ - self._http.abort(); - self._http = null; - }, 5000); - } - self._sendQueuedMessages(); - util.log('Socket open'); - }; -} - -/** Start XHR streaming. */ -Socket.prototype._startXhrStream = function(n) { - try { - var self = this; - this._http = new XMLHttpRequest(); - this._http._index = 1; - this._http._streamIndex = n || 0; - this._http.open('post', this._httpUrl + '/id?i=' + this._http._streamIndex, true); - this._http.onreadystatechange = function() { - if (this.readyState == 2 && this.old) { - this.old.abort(); - delete this.old; - } - if (this.readyState > 2 && this.status == 200 && this.responseText) { - self._handleStream(this); - } - }; - this._http.send(null); - this._setHTTPTimeout(); - } catch(e) { - util.log('XMLHttpRequest not available; defaulting to WebSockets'); - } -} - - -/** Handles onreadystatechange response as a stream. */ -Socket.prototype._handleStream = function(http) { - // 3 and 4 are loading/done state. All others are not relevant. - var messages = http.responseText.split('\n'); - - // Check to see if anything needs to be processed on buffer. - if (http._buffer) { - while (http._buffer.length > 0) { - var index = http._buffer.shift(); - var bufferedMessage = messages[index]; - try { - bufferedMessage = JSON.parse(bufferedMessage); - } catch(e) { - http._buffer.shift(index); - break; - } - this.emit('message', bufferedMessage); - } - } - - var message = messages[http._index]; - if (message) { - http._index += 1; - // Buffering--this message is incomplete and we'll get to it next time. - // This checks if the httpResponse ended in a `\n`, in which case the last - // element of messages should be the empty string. - if (http._index === messages.length) { - if (!http._buffer) { - http._buffer = []; - } - http._buffer.push(http._index - 1); - } else { - try { - message = JSON.parse(message); - } catch(e) { - util.log('Invalid server message', message); - return; - } - this.emit('message', message); - } - } -} - -Socket.prototype._setHTTPTimeout = function() { - var self = this; - this._timeout = setTimeout(function() { - var old = self._http; - if (!self._wsOpen()) { - self._startXhrStream(old._streamIndex + 1); - self._http.old = old; - } else { - old.abort(); - } - }, 25000); -} - -/** Is the websocket currently open? */ -Socket.prototype._wsOpen = function() { - return this._socket && this._socket.readyState == 1; -} - -/** Send queued messages. */ -Socket.prototype._sendQueuedMessages = function() { - for (var i = 0, ii = this._queue.length; i < ii; i += 1) { - this.send(this._queue[i]); - } -} - -/** Exposed send for DC & Peer. */ -Socket.prototype.send = function(data) { - if (this.disconnected) { - return; - } - - // If we didn't get an ID yet, we can't yet send anything so we should queue - // up these messages. - if (!this.id) { - this._queue.push(data); - return; - } - - if (!data.type) { - this.emit('error', 'Invalid message'); - return; - } - - var message = JSON.stringify(data); - if (this._wsOpen()) { - this._socket.send(message); - } else { - var http = new XMLHttpRequest(); - var url = this._httpUrl + '/' + data.type.toLowerCase(); - http.open('post', url, true); - http.setRequestHeader('Content-Type', 'application/json'); - http.send(message); - } -} - -Socket.prototype.close = function() { - if (!this.disconnected && this._wsOpen()) { - this._socket.close(); - this.disconnected = true; - } -} - -})(this); diff --git a/package.json b/package.json index 383f137d1..258a25603 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,9 @@ "travis-cov": "0.2.5", "uglifyify": "1.2.3", "crypto-js": "3.1.2", - "shelljs": "0.3.0" + "shelljs":"0.3.0", + "browser-request": "0.3.2", + "request": "2.40.0" }, "main": "app.js", "homepage": "https://github.com/bitpay/copay", diff --git a/test/run.sh b/test/run.sh old mode 100644 new mode 100755 diff --git a/test/test.Passphrase.js b/test/test.Passphrase.js index 54e61726b..75074108b 100644 --- a/test/test.Passphrase.js +++ b/test/test.Passphrase.js @@ -18,7 +18,7 @@ describe('Passphrase model', function() { should.exist(p); }); - it('should generate key from password', function () { + it('should generate key from password', function (done) { var p = new Passphrase({ salt: 'mjuBtGybi/4=', iterations: 10, @@ -33,6 +33,7 @@ describe('Passphrase model', function() { p.getBase64Async(pass, function (ret) { ret.toString().should.equal('IoP+EbmhibgvHAkgCAaSDL3Y73UvU96pEPkKtSb0Qazb1RKFVWR6fjkKGp/qBCImljzND3hRAws9bigszrqhfg=='); + done(); }); }); diff --git a/test/test.WalletFactory.js b/test/test.WalletFactory.js index 7e69bd2dc..b79bdcd5c 100644 --- a/test/test.WalletFactory.js +++ b/test/test.WalletFactory.js @@ -11,6 +11,9 @@ var FakeBlockchain = require('./mocks/FakeBlockchain'); var FakeStorage = require('./mocks/FakeStorage'); var WalletFactory = require('../js/models/core/WalletFactory'); var Passphrase = require('../js/models/core/Passphrase'); +var LocalEncrypted = copay.StorageLocalEncrypted; +var mockLocalStorage = require('./mocks/FakeLocalStorage'); +var mockSessionStorage = require('./mocks/FakeLocalStorage'); /** @@ -18,13 +21,13 @@ var Passphrase = require('../js/models/core/Passphrase'); **/ function getKeys(obj) { var keys; - if(obj.keys) { + if (obj.keys) { keys = obj.keys(); } else { keys = []; - for(var k in obj) { - if(Object.prototype.hasOwnProperty.call(obj, k)) { + for (var k in obj) { + if (Object.prototype.hasOwnProperty.call(obj, k)) { keys.push(k); } } @@ -51,15 +54,19 @@ function reconstructObject(obj, keys) { function assertObjectEqual(a, b, msg) { msg = msg || ''; - if( Object.prototype.toString.call( a ) === '[object Array]' && Object.prototype.toString.call( b ) === '[object Array]') { + if (Object.prototype.toString.call(a) === '[object Array]' && Object.prototype.toString.call(b) === '[object Array]') { // special case: array of objects - if (a.filter(function(e) { return Object.prototype.toString.call( e ) === '[object Object]' }).length > 0 || - b.filter(function(e) { return Object.prototype.toString.call( e ) === '[object Object]' }).length > 0 ){ + if (a.filter(function(e) { + return Object.prototype.toString.call(e) === '[object Object]' + }).length > 0 || + b.filter(function(e) { + return Object.prototype.toString.call(e) === '[object Object]' + }).length > 0) { if (a.length !== b.length) { JSON.stringify(a).should.equal(JSON.stringify(b), msg); } else { - for(var i = 0, l = a.length; i < l; i++) { + for (var i = 0, l = a.length; i < l; i++) { assertObjectEqual(a[i], b[i], msg + '[elements at index ' + i + ' should be equal]'); } } @@ -69,7 +76,7 @@ function assertObjectEqual(a, b, msg) { } } else { var orderedA = reconstructObject(a, getKeys(a).sort()), - orderedB = reconstructObject(b, getKeys(b).sort()); + orderedB = reconstructObject(b, getKeys(b).sort()); // compare as strings for diff tolls to show us the difference JSON.stringify(orderedA).should.equal(JSON.stringify(orderedB), msg) @@ -95,16 +102,12 @@ describe('WalletFactory model', function() { schema: 'https' }, networkName: 'testnet', - passphrase: 'test', - storageObj: new FakeStorage(), - networkObj: new FakeNetwork(), - blockchainObj: new FakeBlockchain(), + passphrase: { + iterations: 100, + storageSalt: 'mjuBtGybi/4=', + }, }; - beforeEach(function() { - config.storageObj.reset(); - }); - it('should create the factory', function() { var wf = new WalletFactory(config, '0.0.1'); should.exist(wf); @@ -113,21 +116,6 @@ describe('WalletFactory model', function() { wf.version.should.equal('0.0.1'); }); - it('should log', function() { - var c2 = JSON.parse(JSON.stringify(config)); - c2.verbose = 1; - c2.Storage = FakeStorage; - var wf = new WalletFactory(c2, '0.0.1'); - var save_console_log = console.log; - console.log = function() {}; - var spy = sinon.spy(console, 'log'); - wf.log('ok'); - sinon.assert.callCount(spy, 1); - spy.getCall(0).args[0].should.equal('ok'); - console.log = save_console_log; - }); - - it('#_checkRead should return false', function() { var wf = new WalletFactory(config); wf._checkRead('dummy').should.equal(false); @@ -144,11 +132,11 @@ describe('WalletFactory model', function() { var wf = new WalletFactory(config, '0.0.1'); var priv = 'tprv8ZgxMBicQKsPdEqHcA7RjJTayxA3gSSqeRTttS1JjVbgmNDZdSk9EHZK5pc52GY5xFmwcakmUeKWUDzGoMLGAhrfr5b3MovMUZUTPqisL2m'; var w = wf.create({ - privateKeyHex:priv, + privateKeyHex: priv, }); w.privateKey.toObj().extendedPrivateKeyString.should.equal(priv); }); - + it('should be able to create wallets with random pk', function() { var wf = new WalletFactory(config, '0.0.1'); var priv = 'tprv8ZgxMBicQKsPdEqHcA7RjJTayxA3gSSqeRTttS1JjVbgmNDZdSk9EHZK5pc52GY5xFmwcakmUeKWUDzGoMLGAhrfr5b3MovMUZUTPqisL2m'; @@ -158,7 +146,7 @@ describe('WalletFactory model', function() { w2.privateKey.toObj().extendedPrivateKeyString ); }); - + it('should be able to get wallets', function() { var wf = new WalletFactory(config, '0.0.1'); @@ -180,7 +168,7 @@ describe('WalletFactory model', function() { should.exist(w.txProposals.toObj()); should.exist(w.privateKey.toObj()); - assertObjectEqual(w.toObj(), JSON.parse(o)); + assertObjectEqual(w.toObj(), JSON.parse(o)); }); @@ -207,7 +195,9 @@ describe('WalletFactory model', function() { should.exist(w.publicKeyRing.getCopayerId); should.exist(w.txProposals.toObj()); should.exist(w.privateKey.toObj()); - (function() { assertObjectEqual(w.toObj(), JSON.parse(o))}).should.throw(); + (function() { + assertObjectEqual(w.toObj(), JSON.parse(o)) + }).should.throw(); }); it('support old index schema: #fromObj #toObj round trip', function() { @@ -224,7 +214,7 @@ describe('WalletFactory model', function() { should.exist(w.privateKey.toObj); // - var expected = JSON.parse(o2.replace(/cosigner/g,'copayerIndex')); + var expected = JSON.parse(o2.replace(/cosigner/g, 'copayerIndex')); assertObjectEqual(w.toObj(), expected); }); @@ -312,9 +302,6 @@ describe('WalletFactory model', function() { }, "verbose": 0, "themes": ["default"], - storageObj: new FakeStorage(), - networkObj: new FakeNetwork(), - blockchainObj: new FakeBlockchain(), }; var wf = new WalletFactory(sconfig, '0.0.1'); var opts = { @@ -457,31 +444,42 @@ describe('WalletFactory model', function() { should.exist(w.privateKey.toObj()); }); - it.skip('should be able to import encrypted legacy wallet', function(done) { + it('should be able to import simple 1-of-1 encrypted legacy testnet wallet', function(done) { var pp = new Passphrase(config.passphrase); - var wf = new WalletFactory(config, '0.0.1'); - // TODO: refactor this code from frontend to core and call a function - // see: js/controllers/import.js:~16 + var alternateConfig = JSON.parse(JSON.stringify(config)); + alternateConfig.Storage = LocalEncrypted; + alternateConfig.storage = { + localStorage: mockLocalStorage, + sessionStorage: mockSessionStorage + }; + var wf = new WalletFactory(alternateConfig, '0.4.7'); pp.getBase64Async(legacyPassword, function(passphrase) { var w, errMsg; - try { - w = wf.import(encryptedObj, passphrase); - } catch (e) { - errMsg = e.message; - } - - if (!w.isReady()) { - throw 'NOT READY'; - } - - w.updateIndexes(function(err) { - if (err) { - throw err; - } - should(w).exist(); - done(); - }); + w = wf.import(encryptedLegacyO, passphrase); + should.exist(w); + w.isReady().should.equal(true); + var wo = w.toObj(); + wo.opts.id.should.equal('48ba2f1ffdfe9708'); + wo.opts.spendUnconfirmed.should.equal(true); + wo.opts.requiredCopayers.should.equal(1); + wo.opts.totalCopayers.should.equal(1); + wo.opts.name.should.equal('pepe wallet'); + wo.opts.version.should.equal('0.4.7'); + wo.publicKeyRing.walletId.should.equal('48ba2f1ffdfe9708'); + wo.publicKeyRing.networkName.should.equal('testnet'); + wo.publicKeyRing.requiredCopayers.should.equal(1); + wo.publicKeyRing.totalCopayers.should.equal(1); + wo.publicKeyRing.indexes.length.should.equal(2); + JSON.stringify(wo.publicKeyRing.indexes[0]).should.equal('{"copayerIndex":2147483647,"changeIndex":0,"receiveIndex":1}'); + JSON.stringify(wo.publicKeyRing.indexes[1]).should.equal('{"copayerIndex":0,"changeIndex":0,"receiveIndex":1}'); + wo.publicKeyRing.copayersBackup.length.should.equal(1); + wo.publicKeyRing.copayersBackup[0].should.equal('0298f65b2694c55f9048bc05f10368242727c7f9d2065cbd788c3ecde1ec57f33f'); + wo.publicKeyRing.copayersExtPubKeys.length.should.equal(1); + wo.publicKeyRing.copayersExtPubKeys[0].should.equal('tpubD9SGoP7CXsqSKTiQxCZSCpicDcophqnE4yuqjfw5M9tAR3fSjT9GDGwPEUFCN7SSmRKGDLZgKQePYFaLWyK32akeSan45TNTd8sgef9Ymh6'); + wo.privateKey.extendedPrivateKeyString.should.equal('tprv8ZgxMBicQKsPfQCscb7CtJKzixxcVSyrCVcfr3WCFbtT8kYTzNubhjQ5R7AuYJgPCcSH4R8T34YVxeohKGhAB9wbB4eFBbQFjUpjGCqptHm'); + wo.privateKey.networkName.should.equal('testnet'); + done(); }); }); @@ -492,3 +490,5 @@ describe('WalletFactory model', function() { var o = '{"opts":{"id":"dbfe10c3fae71cea","spendUnconfirmed":1,"requiredCopayers":3,"totalCopayers":5,"version":"0.0.5"},"networkNonce":"0000000000000001","networkNonces":[],"publicKeyRing":{"walletId":"dbfe10c3fae71cea","networkName":"testnet","requiredCopayers":3,"totalCopayers":5,"indexes":[{"copayerIndex":2,"changeIndex":0,"receiveIndex":0}],"copayersBackup":[],"copayersExtPubKeys":["tpubD6NzVbkrYhZ4YGK8ZhZ8WVeBXNAAoTYjjpw9twCPiNGrGQYFktP3iVQkKmZNiFnUcAFMJRxJVJF6Nq9MDv2kiRceExJaHFbxUCGUiRhmy97","tpubD6NzVbkrYhZ4YKGDJkzWdQsQV3AcFemaQKiwNhV4RL8FHnBFvinidGdQtP8RKj3h34E65RkdtxjrggZYqsEwJ8RhhN2zz9VrjLnrnwbXYNc","tpubD6NzVbkrYhZ4YkDiewjb32Pp3Sz9WK2jpp37KnL7RCrHAyPpnLfgdfRnTdpn6DTWmPS7niywfgWiT42aJb1J6CjWVNmkgsMCxuw7j9DaGKB","tpubD6NzVbkrYhZ4XEtUAz4UUTWbprewbLTaMhR8NUvSJUEAh4Sidxr6rRPFdqqVRR73btKf13wUjds2i8vVCNo8sbKrAnyoTr3o5Y6QSbboQjk","tpubD6NzVbkrYhZ4Yj9AAt6xUVuGPVd8jXCrEE6V2wp7U3PFh8jYYvVad31b4VUXEYXzSnkco4fktu8r4icBsB2t3pCR3WnhVLedY2hxGcPFLKD"],"nicknameFor":{}},"txProposals":{"txps":[],"walletId":"dbfe10c3fae71cea","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPeoHLg3tY75z4xLeEe8MqAXLNcRA6J6UTRvHV8VZTXznt9eoTmSk1fwSrwZtMhY3XkNsceJ14h6sCXHSWinRqMSSbY8tfhHi","networkName":"testnet"},"addressBook":{}}'; var legacyO = '{"opts":{"id":"55d4bd062d32f90a","spendUnconfirmed":true,"requiredCopayers":2,"totalCopayers":2,"name":"xcvzxcv","version":"0.3.2"},"networkNonce":"53d25e8600000009","networkNonces":[],"publicKeyRing":{"walletId":"55d4bd062d32f90a","networkName":"testnet","requiredCopayers":2,"totalCopayers":2,"indexes":[{"copayerIndex":2147483647,"changeIndex":0,"receiveIndex":0},{"copayerIndex":0,"changeIndex":4,"receiveIndex":2},{"copayerIndex":1,"changeIndex":5,"receiveIndex":2}],"copayersBackup":["02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba","02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5"],"copayersExtPubKeys":["tpubD94LTzAUiW99mpA59nyf6fAHh4xKGmnwbgCV4gU2bRpeN9CRiMSurqme22px5NmJAo6FdcdH883Zu98VbqyhesCJ86kUEjH3Zpufy5FfcaC","tpubDA2U9H6LkRHDRbRxHBp4VTbxPc7JqsvtcLxrE5QJF8z1iT6hMJ1pXSVf57GWRcxXutYvpoXRurDVGsscJauMtnJBkYAWBVExYmm91XQE2zz"],"nicknameFor":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":"asdf","02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":"qwerqw"},"publicKeysCache":{"m/0/0/0":["028a4b63f26253f3a8731577b8e1ee480950ad5833ebbf106fe3463bfc07cc3b90","0332efa054c08cb77506a35ee0762cb7156f244566703ec08e433568ec0397bec8"],"m/1/0/0":["0220ad514cf593d0c3905d3bb49bc5767a9410823bf9b77ea5ef2cf1d1016d77a8","02fd42cf66f1dbdc7bbb9ae09aecea72df479ffe5a0c4641301067e331d12e416d"],"m/1/0/1":["0315f7868eaf1f9b7127e3f7e0222c5e473eea003e34700f4758b6873c525d6723","02a2e8ed5e90dd39e3842fc790e06178997dbca319987f365317589e2a71a93658"],"m/0/1/0":["0244a25a0b97b26707fd855c15b046b901be85a3b70a781d0678608e633440eeca","0358cdcbc528ddfb7173b0dab283f702be82546ff031e4a832a7270080cb875959"],"m/0/1/1":["025c9b49bdf17d97bd82ea1b87793082f857247f0f9b999937a166ec994bb1b41f","020389327ee8ae7d0ee3f8187842d23a4070bdd8a27c0bcddd05d80ef39009253d"],"m/1/1/0":["02fd0e7c62b7b58d1ea7bb4cb84d53b019df99d3703a42aed73a2cfa15f3af5d08","0355a15912e76072ef50e6643376b8a9da8422ed4f8ea07b1d84d4989be5a39b2e"],"m/1/1/1":["03bc3e1f4db32efd8eb1fd44a1665938d59628429c67e1e8b7054ab5717f4e6750","03c4c817b633ac31f44f16f390af831d35f7d98744a52a0f23e9598967342255f8"],"m/1/1/2":["02826fe7e9da408480ddeb1d4414c5100b350f862ca718e27122681e1a0ca35077","02bd25af907bb3edbf6b2cd1ea90eaa92cc93ec47bea7d339af44c1d2c05708e99"],"m/0/1/2":["0337a1a70364b94745d6e26d2d28919cf528304f52765f12ef43e3d6da0a6c8dc0","039d83db9aa43e6e00e0304e6971b6079d79dc12d8d55ce2e6fc24a52ba8d41329"],"m/0/0/1":["0359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b8138","037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d93"],"m/1/1/3":["02600e5c41670773a213a4cb58c8f2fa3e83840784bc7f0b56925e1075e06632c2","036d01867af5f61371151ef7d9026fa0400a623f6924e404ee0b856625268972f9"],"m/0/1/3":["03e5a9b039b187ca8e065627df402e4a5b196b94198542da7036879de08be63d2d","0304f3e0b70f696d80e5785dc7747d6dcb55ba24c31f2d80bf184b4e582e6b47fc"],"m/1/1/4":["03741afa5bd50d6ba5801064c810fae84f6a4557d6a88ddc8591d0d4eb68a8fc41","0214dd6ce6073b05999fb887098ca6f7e1d0b4fdc0760557786907df353df90d1c"],"m/2147483647/1/0":["033e072a53ea835763a03c66e35c35384736210a1bb7d7ee6d9a3e109e82426b30","02e37b5570c053da8a8ee587be86fc629775c4db890aba2745ccc4e4dcc8c31041"],"m/2147483647/1/1":["0228a6de42ef421c263d1efd9f28d9a7d15a261995028a24eff6b9f1c3fc46e6bf","0226cff885cb0d607cc9cf69a7608316eb3fb2ec344c0c9956246ba776116fc396"],"m/2147483647/1/2":["034fe2a8f0b98445eb5810fe36572ad2f64ed9bf64dc9de624f99c0142cb07c682","02f2c5c758e32293f5c193fd69afadbba83abafb397db01e6f2b447690e900475a"],"m/2147483647/1/3":["02b25ef9434446c51f10678f787e4913de582e34d164bd3b06af7732c5476df1a8","025d51a1efd59bcff22ee2e0af61b21a7ba5f639e20dfdf25690e926005177dd0c"],"m/2147483647/1/4":["03e5734e1d29b2f684d0446b7a2ffbd0ba8952570a502d0d14b1efd8f24b61be53","0258fc28a324848d8d0154e8614815e35c668d274a8f01957bb99aab8dc8f386c0"],"m/2147483647/1/5":["021f9e775246765e1cfba0ae453b4eae6cd4ae5a57a09c319edbe89d4dbbf23be3","02857f66571a1c3eb9e72d22ae88e734c03d448bced4dcfd345c2059468124c741"],"m/2147483647/1/6":["02c072f329391a25255dc6452e5f5220966869dbf736ba8a8c3ae9d273a84bc3fd","030920a8b8e88c4db2871a7df0878a86cf0695f6d96bb50c701c3454f3df25176a"],"m/2147483647/1/7":["036bf329fc19bce10cf1999fae5bfa80290ff7b44776b49c7b0dc9eec6cffcfa21","03955a549875b4f7b9be28b9ff4bcd51ad2bc224430b1634baef890585885d5e1b"],"m/2147483647/1/8":["024879c9c9a261b3141ecfa1c79c4efc25278c844ecd1dcfcb95d9c19581fbdd25","03fb4a5fdb91239df3ccf7f61a5b99e7e72483101e21c9d1ee0d85544e9354c6c7"],"m/2147483647/1/9":["035928a107ec01f78cd586914d5a49710fd42e352b1312e3ad0eeb2c9666fdf8e7","03a54c03093797854829c75357f092356352a109042bbb83bdac20cb4e5eca27ea"],"m/2147483647/1/10":["021e7a3a7efe888c5e820b5cf0f03317b2b4bf438d8563449aeb7a77cade97f136","03ec0960b3d1df52ca3cc2c82b7d97063400da4dd051bba2f9bab6cb44aee01efa"],"m/2147483647/1/11":["035d70c26b7f429861f555f7c0d99947411b23b7f95303fb8d5de5b82a95aa30fc","038b922f7024f5446d6b48e5253643543b35c006d90fd37688105c6cefcd8adb8a"],"m/2147483647/1/12":["02158d6503891c6c65a606221dbf5c68d0832288975914007968419939588ecb24","0248264cb1763a3f4de9b34787b4bc5443ec92ef915927494bb9f1c1c0b498c7ca"],"m/2147483647/1/13":["0349965eea38a25ae0c061faeac4c4e57e648bc4c0f059d07b3b8b7962cbc0dde5","0352243d9269565ce2a1ffdd0b8e43a442c6dd1c9edda86eaaf2cba5a4a95c40f1"],"m/2147483647/1/14":["030fa6e3d0c5cedc0581955395c77cbe134c912a47971023b9695332df3f7bb200","03f2cf09e33326fb59bf3f13e6298d2d5d29c9eae3b872e5a851e8d8d77259c883"],"m/2147483647/1/15":["02bf0d45e41339f552df6f8baf4392142921fd38b0f2a4388a905ff6cbacbc278a","03fabe46bb6706a1b8edfd28c046a8891b4530bbe5305080b72b0d08ebdf7b8c0a"],"m/2147483647/1/16":["03a4e3146ed34d6a8af4e4379e6edcff32cb0373ba232b3d746af3052f674133ac","030311b73c6f5c46ddffc0cfce6e5ed0b671d94267d8e52cd8837f2a479916eb91"],"m/2147483647/1/17":["03233df93c762d2f06c7f5f388e4e0a8dbdb13302acba0d2d6995c487d8aec9f2f","024badfdcb7e772ac7fc1c46d3943b07500edbbece105cdeff3eb9e9fcc9f54782"],"m/2147483647/1/18":["0364035475a098e00eb010c500cad3c90af3e81a4bd613144bc9433a150f14718b","028223dc8142154e7477ce000b3dc13e1d15a901553d9b18864c8645b582b38fe6"],"m/2147483647/1/19":["03971b74b4ac4bdaadf636baa4caa82fe5355471ed6ea05a9cbe5fc6c9e4b9db76","0202ebffacd01f83849e5bc5c0e2c317bc5fb2fbcb2d6d4482a5235f9f1308b61a"],"m/0/1/4":["03005ee9ff028c98fd132e531023f2f2b61ff0d26022f979dd98088d2ba167b031","0345ea82e8dfe38277f0c3aee18d2dd93edb63e8663ac83328a7934d2ca57006f6"],"m/0/1/5":["0391bc4990b71d8a3f156ae7107929ed6372b0b4ba8a868253f71ba7189d1efa02","0312a74cf2e7c0dd41897d04fabfd8cc3187b84a28305cfc79315b24e6fe23a6b4"],"m/0/1/6":["021a38c492607ff9684a4fec445e47b5b7100d3ef9e9dc0d0b37c0a646d28d4f77","03ae0b46ab36f97447ebaa53f2b5c8f090f15395378785f2fd285eeba17fbf3f65"],"m/0/1/7":["0308cdec88c1ffe16edc98853d9c08dbd4ba2541ba566668ca17bda19d7eb3481f","02dd622267c2e68287287b8b61724f76fbe84096a56aa5054af92f8fe25380e2d1"],"m/0/1/8":["039647da9ad725836bcb28a3e0497659a28d7749d1416c421a0a01c62d237ee962","022e22aa61eafda0dd8820427f1a06314d352a15ea8645e7ab9b80920017084d82"],"m/0/1/9":["03a4ade946076c6962b70c70ac7fad3a87efb59a1d0a4e32bda13a6d47fe9df961","029a07235aba04ab69526e117d836d5b3fae5cfc8c5e72b10c6d1afd261ccc19f3"],"m/0/1/10":["03c78e9b6493b22790db1acea20df9444e0f9c424fc5756e7a32c290ae01783953","0254c130ee467a96570c9f5ebea89de04f0b1db1686b164f2694339bef8f25dd88"],"m/0/1/11":["03a762c43318ef8d4840fab04c8db73797dc648825fac60f2730b4c76678df1cf3","0212c684a4de8e750ad2dfe2b136370ab9803eca178ed9a27b3990c29b067de35c"],"m/0/1/12":["02702d221f9b15c5cf75ac2f497a6c63e60213087c3d2d3be46768e3ebd238e26e","03ed58580744deb357258e44548212038670769d8d51e385d4fb8414311fd01b52"],"m/0/1/13":["0320e0597b54c62768352f433389cee4725d6094d7bcb5c72265edcc0933829aff","02c5706f11b9a85f3176c572842b7c9812c2195058d24d945bc026b00312740e76"],"m/0/1/14":["02fe43077676b844226d3aaa62e8a86d237710d92f882366944acbde0c8992fcaf","039a6a8662abb8910741cf331320549665e9feb28ca94d1ab6a43c84fa330b94ee"],"m/0/1/15":["0369f99f72847af93d50ab8ee75b6e7e912d26e27be96f6d6b7215cf7daeff7ba5","02521700cc07c953ba5aa586fb0e4795a34dffc68c5fb43e038be3866e40f4daed"],"m/0/1/16":["02f67d1d89bd8fe2f91c5b973cbdacfb4ba440e7656bce284cf73d549625607347","035da9cfac5a803dcb2b283b02a2515a4a1bcbf3d19e0d180aee8fc30193bc0555"],"m/0/1/17":["02c024ec199d240e8d6c66276b94b91071f7cdf2bef540c29d6d18d25de7b1cf7c","02190865f9dafae3f7f05c093463be5632946422ddda0a6fef6904390792516067"],"m/0/1/18":["035ed504d7704ad984a333b8eb0fceb8be043da9284de31ed84d9e68d90c75507d","033303c415b50421732402df00f4baa219f334647a7eb5014b9f8079864d6ab558"],"m/0/1/19":["02ce49fe86b0eee73663b1ee867b16b97c876af26f12764c528a2e6d0eb55ad3d7","03ab969bc81796b88e44c340d854df955fc60ea17ea92db5d3115595d6dec890d8"],"m/0/1/20":["03e2fa915378cbdffa0d919b0fb50c7256ca731b9d571b3365e486893a1d43079c","038d058b895cf084dccfcc9367e4796a5cf4ddceed6c35f6885d75c80119613350"],"m/0/1/21":["02fcb1bf644446b5b42205272af72f0aeab9e92ca29aafa91c5fb69142764017aa","035c5fe5c8811603279a5b72b6c30735d702817db1eab937c622269e28192ffa90"],"m/0/1/22":["03b39d61dc9a504b13ae480049c140dcffa23a6cc9c09d12d6d1f332fee5e18ca5","022929f515c5cf967474322468c3bd945bb6f281225b2c884b465680ef3052c07e"],"m/0/1/23":["03f40b82fe8cacff08879f13c45f443a3dc3ea98e1d75d5f32a19f5e5a8f7a905b","028415ee458e4dcfd440ce969726f3b58ae74fb6cf3995ced099579211e7419844"],"m/1/1/5":["032748a6282e21f571b8c8dd49e775deb83c90fcf88dc4ba81d878536973709c3f","020837cd68f14ce571b335eecd1b6fa0af43e1576dd9721aaca2a8ab639ac6b7cd"],"m/1/1/6":["0337032efb013dc92bb8dccfbdda9f5c28f0039a9c60953d41003d095e9f9778af","03ceed2da6b9603297061dc8eb930112ba726b2ccf5eec67f4866a05ca4049a22b"],"m/1/1/7":["0383c96ac2af7d203f69133b2fab6b68366b5075ad6957fa06759df3b20fbfec70","0311385f79834cedaf2230a48c0f9dc8e794da1869fc595db2518d62debb85579a"],"m/1/1/8":["03efc649680280f4e4df96da923bc88330275004125ebe5483c2f3e05ca52e19a4","02803c02d197d780388259afbd001ae41fa3eb3e2bac9627aff540521c184c3b23"],"m/1/1/9":["03af2fe6aa027a76b42c1c4050a040bfd026ad2daec1bb96a5fe2d026a7df919de","02ce14163047c640228796fb1f72bbe3afb05819ad141598a4f021058a6f79dd3b"],"m/1/1/10":["033770378bd762cf0408e44e4e604bef77e336170428c506949b1a4f1f2963e574","02c58ed43946f699dbd3e36d3e9aab2714cadeb19ecd3a56e4328c50336b4a76cb"],"m/1/1/11":["02898a1545fa19bdca92adc498698d27b86529cd4c08946d9d29604734b86f31af","02b402767a045ede072600924401c0d720000b2ed59fa444bfdbef4a5f1cead745"],"m/1/1/12":["039b8659430be49913e2cd869aa8c99ccf49a13df35837370b792033dadb891483","03264e63df292257cc76babb15d15bef620d1c2f8c3bbc78d6ea02d127e5ee7386"],"m/1/1/13":["02381a559791b8e86bf546e2c718ae63cf24eed0518a58e4d4a4b310adf2cd38fa","02d7f8283a4418d912508901b4a3db0d2103206dfdd74b3c75648671e20ecfd445"],"m/1/1/14":["020376e8c550b7d9faa0b2da947a2a36fab22c6e8190b6f99460b6022017bb97d7","03fbc5299190e6628de28c92aaa12e3a131b21eb7266462c46fbedeb86fa878055"],"m/1/1/15":["027209fd3b0cf7368180a5dbb16b928c997d33fccb78505d48440c7d23eadf5460","03450bfb22858726cd7e228e6733f69457546978a95188565c53e0d1c0d6070ea8"],"m/1/1/16":["03cb355ba04f64293793855121bab5831f84a3a3edf7cd31fccaa6d67c407a4912","028bc897a39c1224610b765a80f4cd8ab79cb37776f58fec9c10ac6f649d1f3c72"],"m/1/1/17":["03f4cb0564d7e2c6b85673503b7954db22779f29a8f3374904573984e318a96bf1","037c11b6ee906d84aa7eed359d758d986d912b6f8e5cbb1acf0982a77b3ef812c4"],"m/1/1/18":["02d2e5798f33f6889472857744316f2d253f25f88379610063f40cfe5798d9858f","0253cefdfe9ca987cbf1c950b6246d5b7a194d8dfad47c3a78dbbc5c1d01511d97"],"m/1/1/19":["0336c325f5aed366ffc10d553f2bfd4d69e66cbe1688d77af14efc8827aea2e318","0378b1b9a6074f9f2ab4fa9ad1e14649c621b0c8124a1b148914d3c10e6ab390c6"],"m/1/1/20":["03ea55740a734689ce778a8c00df8ebf4274c8f66de7d05646fe5c927773ff7f2e","02275b558d49aef955b6dee51a3c0a53f4b076b97bb3f26abcc82540168ec87cac"],"m/1/1/21":["03c77869c9984664eac9c238f4b6d806c9f48ca8a736c48450f398834db2aa915c","02d984f548c7f60c09dad3287cfc48807bc8157123989636c713be61be6a2e9ced"],"m/1/1/22":["03ed7c6a3c854c1f9459891691cc32671402f9e47126919878251e568dbdf353f8","02a113dab22cd9e46967b3fd76b9b9ec1d227d88817a9300e42d332cca2a0877fd"],"m/1/1/23":["02ee186432dcf69fda50a6fdbd94651817d8a271c273a5b70cab3ec4ae77a3753b","02291370aad9de0dac676355ced64e268b0c431a51f42f12d13f5144940fce4285"],"m/1/1/24":["02bf71435e84e66547c8c583d5ba226a5ac4d935e0a9f9603ecd8925c3e847e91a","03578d8657d285a89d9d597632db662cfef9baccfb55c76b1e87948a94fc9de30d"],"m/2147483647/0/0":["02a8425bbe23426219065969f695a6c3e242b24e57226bffdd542be8fd6be968c9","03057a42fdb6569fb1615b173ccb702453db2eac5be4291b82d4511461eafbed87"],"m/2147483647/0/1":["0250c3d3e86e332010c5233c2ec3bc728026002f0037cb3382d6318409b0e70796","02cfac1e7c4c88191201080f8316af52d9faa6ba624a6e160279e9fac4d1cf79a9"],"m/2147483647/0/2":["02a8c266a5b92eb50c8be91f95e4d1ad968b2f57d527377fd642d63fb84474f61a","028cc954ab31bd179ff80b8a05f95430ae534e61b3ff35f5284fa2fbe1832ceccb"],"m/2147483647/0/3":["02f719e1a7ab00ea98611453fb03d44c1da04655bed74af392534d70099039b4c2","03bfa548bfd4718c50bfce173f780eadcfb679d9c0206c91a2fa1879a9cf7558b2"],"m/2147483647/0/4":["0362c0695d397ca26bf47f0e641bb3cfb06ff29ccac2e1d56ded3afcf88b1e688d","02f9d87b05bdb3b9e82f506b43f813041c0e403274adc23d11e5e1651e34b606c2"],"m/2147483647/0/5":["033731323032d4ee08e858fc71f93970444333e183a1d5052e1d08cfb511e262c8","023e12556cef67ade35b7758916b5e1a3ebe074ccd35c5d8eff6b01321f63eb495"],"m/2147483647/0/6":["025d11b90081972bc1c258c9d6f476dfc2f95b69f0e9935322bf9c21deb580ff64","02b065f56a378907354f0738a0ed74f10660c6b5dd68c9f992093b75ce3d7d8b72"],"m/2147483647/0/7":["0210e721e8a35db9d8c855a0d346f60c09208f3be80b39e03af2c29db777332c71","0277f352969fadb1f1835f9a0fa99c6a3c7b6c281be5b2794c88a708eb177ea33d"],"m/2147483647/0/8":["02998d8d41e4215cd2a961a415a3ed0b1f984f1627719a7b102a75864943c4d87b","03d8ed7fc8f68a77f68d3afd007b7aa4c89944195143630ce183f0fa5438f2b559"],"m/2147483647/0/9":["0324fa91737588e4f85937303ce65c3b91b5f2ae506a72d92b83e3f5f9aeeb3c6f","02a011be72c4a400319212228106af278823a97acfe0a67e1ecd866d446b315114"],"m/2147483647/0/10":["025886ba287922a904881c7315e6fcc410a7976741771a5937d3a1a01b529f21fd","0243bb91ceed9d29d0c2ca66a8ab77e82110bbcc023beb4106f787964f44a0b972"],"m/2147483647/0/11":["0369d21684894cc2d4b2f5e581ede3cac9e8db4161a08e7737c1be129bb673d3d5","03c9ef27e3cd3dadc078fdfd9936a7ad9bf7954747085cf8f8a2a5bb3431f68a9f"],"m/2147483647/0/12":["03a73b8fd859bf6acebffdfffa2597199091daedd2c011ac67fc3494d8a1a8ceb6","025a213f7771c8be03f43f2e7f469ad4ef2cf6907ea284b227a786d1f55dfa7144"],"m/2147483647/0/13":["03a09f7ca257e1ab263cd5e6b0addc3ff868b93df132321d98775ca3505efb576f","03454c715739164bc55f347a651439cdf3ec146b35d2927beb60e8290b3916e082"],"m/2147483647/0/14":["03a64b1f7bd94a6b1a6e84ea444e0ba04e9deb86460934ccc37c0615a134a8257b","02794f09210b1811a455f3e1c7bcd35c76dff2523190fef9615eb27e2376acac1c"],"m/2147483647/0/15":["0392dca2fd9a3bc2b2a7d90a848719069fbc5f22bff7327bb8186c032514085263","032ee8a33ea76d70c7ae839448ca6c5b1af89146f2922e23ba1822df42dbc7e66a"],"m/2147483647/0/16":["031a22a1a3c1abad7c4d782ef6ba3cc00f2e8fe549eb33e0732200aff6d3174831","03bdce9781289e0c31cf727f4c93fe46f7930dd8fd68f818ce241f1ede268e8e0e"],"m/2147483647/0/17":["03b12d27e9aea2c2ad598e54e40860a705ac2ca2427aa511b501b38ec368ea5c7d","03e60d35d84d4536cad895215256b312bb4879a8d417251c279995e58f25da3d54"],"m/2147483647/0/18":["0380266cc9a9673676ad6a1b2e7148766df9c25b4dce299e5edc4f65b72aa58e64","0329e2a8a48c06c0c45dfdd2ab33e6455551557d8ebaf8c12fdf7470f8c45f1d28"],"m/2147483647/0/19":["036fe62af85560d7eea7c7af55e60b32a97dca80134d0aedffb19eb2705b9d6e01","02381c2c30b9f81e2a53c69028fbe11803acad0420b267719b7a80870be0baaeb7"],"m/0/0/2":["027bf94b8fc4e9b42683af25fda125ccab8760040717d100270dd4afd032692daf","026382c6c9357250d96dc21e43c053857a64efeac1887fdcbc107fbe3ecfc6115a"],"m/0/0/3":["03fd203acbd9af3cbbfb709458f8952078234a36094f12d00372e4b2b14cfdf419","03f2e5db59aea5dc89f53ac2a9f4ef66d41265c45afc5d763e0ca61ab70c7c61ec"],"m/0/0/4":["02a1d7cf4fcdbbf4de4002b844c3bff1639073f1cd6e5c4a4e02596b45d3f518c2","03b5fba813294e6ae096ea158833453caa5a945609b0a554696091b9b152bb0f7d"],"m/0/0/5":["0261d37e3b56ef4e106c59753037f516a4b1c45e056b2a3e00f8b77f15aaa7f8a5","0256a55e66e0de1603f0d600c0eb5f5486cf3512a776a36f3ab0d1941fc0dc9b09"],"m/0/0/6":["031db2826af215fe6cbe3f6e121b0497840fc49be133cff0a4d4eab679d6b99d70","021dd722c3f35dd04fcdb57f09b76c723d521fb36751de03ffd08096ddf1dc1f86"],"m/0/0/7":["0354ea75bdd9eb5beae7262e4a5eeb58bd10103ee0185e85b749ea39f6615d0f62","03f2c8f3b6478c0501a8578d5caf5ac2974f8213fc5e699d62dd2af58fbe8781d4"],"m/0/0/8":["0282e67df3bcd1e1662469b4c3151fb50ee1e46b75d787d91184c16b9803131f82","02921a7054af1e425f4137a5eb6b34d1f2b9d81c2625230194bc30657bb4277e11"],"m/0/0/9":["033e7e387933983ceab37c8388bd8ebc5119760f493ffe6f083bef0e5dfe22891d","02d660d60cc55d80912e0745cb142a8596a4604fbf72f9aadec0599aa2ed62461a"],"m/0/0/10":["022ce5b2750ae34512199856eab9e912dc25281cd8b88e7688a46c3b9a389701cb","02f14aa1608fce3b6088148709eb5fe72b61699c931fa8d95a45fab1106859d1b0"],"m/0/0/11":["0288dbef3302c1bc5556028adb33e2f9e03c119dbad4f706befb8ce86cea459f2b","03f13ced465e2e0a3aaa8895f3185d5711e0bebdaf507610b7a669ac8fc82da8fe"],"m/0/0/12":["031ab4677885340d2f927ccc9747f4346b79e4eb6c750695095a8a2524610fa94a","038c881910fbd8b50d193db4e0c84f5b7840820397f92cf0718a8e06d027125503"],"m/0/0/13":["031b568452cba22eb7a88c6085489e53e35abd16068882e71a140e47e12dee9c61","020d09885ee362101d12d34ce0918d41593634db1b9413e5415c6755753b9330e8"],"m/0/0/14":["024177bc9aa03cfc72eda2dfddffd7fe9d0c2f007fc3ba1a48280feae2b9fb117a","03394ad321668440c08da76eb35475ba3a8c0e8cbe0ed81468673a8c72d38fe457"],"m/0/0/15":["02037b1cc696ffbe9eba3684edd53653386ef6cd7728401c40120037593a4c2ae2","020ab8d6900ec9c11ca5d96dfc0ce7cf0ee71653a7c45118e89abb4b113147e53a"],"m/0/0/16":["023bcbb8d4726a546087cdb83740adf0ace879b7195a572c652fa8ce4dbe195a04","0392721b230d5163d28b27fc7e059b875711f12b3da448eabe7229bde57530e637"],"m/0/0/17":["02498ee74e849d3e9261dd1863038caf83d6a3bc2eeebecf17055d4bab44dee77f","03d4dc104b2e0981693e8097437de9b05334a85e2c8edb02783897859bdbc93e32"],"m/0/0/18":["0218a9f524fe54abf8c3afd21314296cfd93eaa9227acbd457e6c9a742dc233cf4","03760f3d0c5db969bda698ff9352e3b7c332216c34825f4c6e857e39c9aee7cd35"],"m/0/0/19":["033dd51f7737f0e9db79f5c38e4298bf3396346904ef3933d290a22e5b77048d9e","0221b2eedccb9a37515263071550069b3b349a166f0f131d0028e8600d9a2251b9"],"m/0/0/20":["02cb6c39161f3244d7769f7ab96346cae2cf21cb6f4538f5e7382d363dc2f836c7","034f7bda4d1e9ed6a3774608a4d6cd8582ab59fe3187f8a7a7cf914d89426ebe28"],"m/0/0/21":["035490549d65f1360f10340037250b171470ff4c86966318a2b1eead6d8b969aea","03f6a04f6fcd07a4f32c82d53710ed30e0f54d43d41c67c661d158b3d0830c3ea2"],"m/1/0/2":["02972eae7e4302e319c266578e14a07839c1e788296a92906e6d66d938211dad5f","039ed6b488f1571ad6527acd6b6c5b8453eacf6665dc5cb7852e33d1c8ea73f9fe"],"m/1/0/3":["02bec4728888c2c045108353994bae5731ec7a7b41459023b0023e10b8d616bd30","03ce1efe16214c9eac595382e46a68143dd11a335b3f7c971ddd719ac544a5fc4b"],"m/1/0/4":["030e2df1d341568225d8dfbe5d07e98dae9f90e0f43e19dcc68c998a6ed7bcc1f0","0380f4c07dc84faf42d51779f104aa6e3b5c3ce2d7684b3cb76d49faeefc2b69d6"],"m/1/0/5":["029a54ddaa25f433b493f4b72df8c1d41be2c4d2963b8b61ee63cc86d16c12d066","021567c95e0317442e7367aa4e3378dd46c5bcef5860f789272fea83b917de0669"],"m/1/0/6":["03590320d80b61cc0874b579f467c9b5ccc50d9ef875bcf6bdd12e2d0c211e8973","03ee4677b6ee89a9d355851f2230506c6897ff219062c0df4ad9a85c60f3535f93"],"m/1/0/7":["03caf98ab1c9b79d1dc8029453a6137c08787b04043b79af3cb42d41d2d3f1338f","023f39ae4e2f4f3887d5fc58e0d3a0d7ee267dc04aa257c75b6b2d67d2f5580f81"],"m/1/0/8":["0352a2a3ea8209c9a2b633d788796ac2d16c08022440e04a77ab2835c7f971d266","0291bc248b3da997f35e8fae98a75a91fdac2819d74c4e270899338d48f7389e87"],"m/1/0/9":["02468d32d9c3c62418d506d4cd0da6cd2022d5bcafdb5f847cf7bde7a48ec6848b","032713d90d12eb6a072f3c1db6c0d3b680d3f78883016135fc0f78e8193d41d4b4"],"m/1/0/10":["034863cc6bab9b059be53413ba75c5fc286647c20d7f9e5512ef4754ea301dd1ce","03a33ab9c32a2264ee2464ebbb5892f0e34acf0fdede4f87395a89e9dacdd4930e"],"m/1/0/11":["031e19296695bfe8a96ba3bf58afa805ee1bd5471fddb3929b1678d69d442d69c9","0270feb33956fd9e937019d629523e26437493c0856514011e6aec88baf7721295"],"m/1/0/12":["03cce695d3c3843bf73e851b2446a77d7e235e5b80b4f4474f9946292eb8218742","039ea96c8822f0ec7ed28308d277f3e730480d7573579cd11b89aef4364cd9ffeb"],"m/1/0/13":["02ab4ac38eb405e822d12c0f0f354f04f9ee1d991dde887a5c1171096fe503158f","036809e60cae1203da8884ea1f85d4669ce6e053f8ba605d775e271b70ab4f6787"],"m/1/0/14":["039d61da23a8610fa0ee58eb37d7cea7ea9396c79153da97280ccf5e46718e3bac","03015c27bcc778682781fd6ad30aa6041db0b7e24270818cdceece0043ccc34b26"],"m/1/0/15":["03c088ed669132835d2728b0ecf294271c8388988c6ae264d43ca24f50e4005f81","03e2c118c9445a2ddc4c8afeb0ba49e21be3f818a483d346418b8922b8a371a2b7"],"m/1/0/16":["02bba7df9847f463c6b23eca37a4bd6efa3801a52b8ddfad804d902e783b70c81c","03764b657f23996e31c64a701facc1cbeb0c9edfdd605e2c1ed36cf48197565d45"],"m/1/0/17":["020445179c522295b89bf4bfd582eb03422e3fa20dcd29263925e9f44282d476d8","036e47bdd32f3061aed1c1f8c2a32b038c7b72391cb1f80ebfc150e58f88372766"],"m/1/0/18":["024d88c4bfcbba713d49e1edcd035234aaa1ee76ad7bcf75bf074a16658a6b0b6d","02b861e7a20d89f6875d2e44c78dbadb99503e282e5e60e9f65657af6fea81d425"],"m/1/0/19":["023a8ca9d5300181f157e1930d3b0800eebe7683d8df72e6cbf28834dbf1be5d60","026053c4f84c10d15890c0b254522972931bc2d5b7cdf9c1f9f3137c22edf3ecd3"],"m/1/0/20":["03137c66e9f3d61aba659f408d77a293fa0f3fea4ccb911074a681d6f61a55d023","0291aa1bbfbef59b16b0e37e185a706c589d448cb02e860c5df9c9d7242ecc739f"],"m/1/0/21":["03c08673e0cae55318bc9dcc4b5f11eb3ff71d42de04015e255dde3fd8cba7e09e","02423d4eab06cd5b26e71d145283523c011d58032700c517f00b328d2c90cf109f"]}},"txProposals":{"txps":[{"creator":"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5","createdTs":1405543144016,"seenBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543144016,"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543144645},"signedBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543144016},"rejectedBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543170040},"sentTs":null,"sentTxid":null,"inputChainPaths":["m/45\'/0/0/0"],"comment":"blablabla","builderObj":{"valueInSat":"29000000","valueOutSat":"8900000","feeSat":"10000","remainderSat":"20090000","hashToScriptMap":{"2NBtv6DdXj8HBunyGqpW9H8bUtW5x3rfVTj":"5221028a4b63f26253f3a8731577b8e1ee480950ad5833ebbf106fe3463bfc07cc3b90210332efa054c08cb77506a35ee0762cb7156f244566703ec08e433568ec0397bec852ae"},"selectedUtxos":[{"address":"2NBtv6DdXj8HBunyGqpW9H8bUtW5x3rfVTj","txid":"a9f4dda3f092e37244bc4e77ea921fed01d5b8ea49613dfdc0dc8afdd70190b5","vout":1,"ts":1405543855,"scriptPubKey":"a914cc93216398b77b5f8c451ca3a357bef961678be987","amount":0.29,"confirmations":0,"confirmationsFromCache":false}],"inputsSigned":0,"signaturesAdded":1,"signhash":1,"spendUnconfirmed":true,"tx":"0100000001b59001d7fd8adcc0fd3d6149eab8d501ed1f92ea774ebc4472e392f0a3ddf4a9010000009300493046022100ccbb8f398f74a76236629b8499ffc6f9518a2091f5a61a9a352c0a10f615961e022100b8f0769c76cf33bec3d7f81d9da2b74cf6e8a5e0a24ee5f48172854d8bcdbfa101475221028a4b63f26253f3a8731577b8e1ee480950ad5833ebbf106fe3463bfc07cc3b90210332efa054c08cb77506a35ee0762cb7156f244566703ec08e433568ec0397bec852aeffffffff02a0cd8700000000001976a91485eb47fe98f349065d6f044e27a4ac541af79ee288ac908c32010000000017a914560c292066792531164149c5ed63ad2793a61b928700000000"}},{"creator":"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5","createdTs":1405543188745,"seenBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543188745,"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543189341},"signedBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543188745,"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543206819},"rejectedBy":{},"sentTs":1405543207304,"sentTxid":"169bc92693dd2e27724eeba81e54210e842035bd3af6c52e6a6a5e908f1a4f66","inputChainPaths":["m/45\'/0/0/0"],"comment":"que parece","builderObj":{"valueInSat":"29000000","valueOutSat":"9000000","feeSat":"10000","remainderSat":"19990000","hashToScriptMap":{"2NBtv6DdXj8HBunyGqpW9H8bUtW5x3rfVTj":"5221028a4b63f26253f3a8731577b8e1ee480950ad5833ebbf106fe3463bfc07cc3b90210332efa054c08cb77506a35ee0762cb7156f244566703ec08e433568ec0397bec852ae"},"selectedUtxos":[{"address":"2NBtv6DdXj8HBunyGqpW9H8bUtW5x3rfVTj","txid":"a9f4dda3f092e37244bc4e77ea921fed01d5b8ea49613dfdc0dc8afdd70190b5","vout":1,"ts":1405543855,"scriptPubKey":"a914cc93216398b77b5f8c451ca3a357bef961678be987","amount":0.29,"confirmations":1,"confirmationsFromCache":false}],"inputsSigned":1,"signaturesAdded":2,"signhash":1,"spendUnconfirmed":true,"tx":"0100000001b59001d7fd8adcc0fd3d6149eab8d501ed1f92ea774ebc4472e392f0a3ddf4a901000000da00483045022035423cc74824ba904907678dda3b62a20a787b96d1b3e9f3e9546f9c57f4e45902210080a1ff1c39f458ac1642b9e948bd62fd70563b5252e749cc8fc642cd763ee830014730440220524a13f36cfb03caa246d7d84de634ec9386f2c39c19bfa926037f48da86262b022050e58a6503d105ad2805f86806810a1aa7f20d6271e1340b42fa91ab6a30f3e801475221028a4b63f26253f3a8731577b8e1ee480950ad5833ebbf106fe3463bfc07cc3b90210332efa054c08cb77506a35ee0762cb7156f244566703ec08e433568ec0397bec852aeffffffff0240548900000000001976a91485eb47fe98f349065d6f044e27a4ac541af79ee288acf00531010000000017a9146130a9d51f996b7a1b9d3e10c80930834251909d8700000000"}},{"creator":"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba","createdTs":1405543505848,"seenBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543505848,"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543590221},"signedBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543505848,"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543590221},"rejectedBy":{},"sentTs":1405543610315,"sentTxid":"6fe851b54b777a75fe80fa204dc674395e2af69efb1f7c0017e909eb82c3d914","inputChainPaths":["m/45\'/0/1/1"],"comment":"mandaaaaaaa","builderObj":{"valueInSat":"19990000","valueOutSat":"19980000","feeSat":"10000","remainderSat":"0","hashToScriptMap":{"2N277q5r8Ab6XLJNCjXXFdh5itDJRQCv9ts":"5221020389327ee8ae7d0ee3f8187842d23a4070bdd8a27c0bcddd05d80ef39009253d21025c9b49bdf17d97bd82ea1b87793082f857247f0f9b999937a166ec994bb1b41f52ae"},"selectedUtxos":[{"address":"2N277q5r8Ab6XLJNCjXXFdh5itDJRQCv9ts","txid":"169bc92693dd2e27724eeba81e54210e842035bd3af6c52e6a6a5e908f1a4f66","vout":1,"ts":1405543157,"scriptPubKey":"a9146130a9d51f996b7a1b9d3e10c80930834251909d87","amount":0.1999,"confirmationsFromCache":false}],"inputsSigned":1,"signaturesAdded":2,"signhash":1,"spendUnconfirmed":true,"tx":"0100000001664f1a8f905e6a6a2ec5f63abd3520840e21541ea8eb4e72272edd9326c99b1601000000db0048304502206b18b3dba2646c552469d8ef52d7656f6a65f563032530f622abdfd8bd4c5cee022100e804b406eddebbc827646141e74dc64c76a770ed4e35183ffd35d265ad9f7d3b01483045022100f6c013638ff0a316b1baa93dfffba6a98cf3033c133e8bd899e933c9c3e47ce10220530f40e7ea52ae58bec695edbec6d566d2ee8e7b5f33f95e33093ad1e29a125401475221020389327ee8ae7d0ee3f8187842d23a4070bdd8a27c0bcddd05d80ef39009253d21025c9b49bdf17d97bd82ea1b87793082f857247f0f9b999937a166ec994bb1b41f52aeffffffff01e0de3001000000001976a91485eb47fe98f349065d6f044e27a4ac541af79ee288ac00000000"}},{"creator":"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba","createdTs":1405543781381,"seenBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543781381,"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543782017},"signedBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543781381},"rejectedBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543794590},"sentTs":null,"sentTxid":null,"inputChainPaths":["m/45\'/0/0/1"],"comment":"1","builderObj":{"valueInSat":"29000000","valueOutSat":"1000000","feeSat":"10000","remainderSat":"27990000","hashToScriptMap":{"2N4eyXKikdnnUT4S74MRNAYqXChhUYmZ1Sb":"52210359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b813821037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d9352ae"},"selectedUtxos":[{"address":"2N4eyXKikdnnUT4S74MRNAYqXChhUYmZ1Sb","txid":"6c9da5b0da4bab0d576033325e987b10ccf2b9bf479d306b6aae36efeaa56892","vout":0,"ts":1405543698,"scriptPubKey":"a9147d274ac50968d7823b6cbc1b38770deb7157995387","amount":0.29,"confirmationsFromCache":false}],"inputsSigned":0,"signaturesAdded":1,"signhash":1,"spendUnconfirmed":true,"tx":"01000000019268a5eaef36ae6a6b309d47bfb9f2cc107b985e323360570dab4bdab0a59d6c000000009200483045022064d877bc5171fbaef909c2a1a924e0023b3ccc0b530cb46653f06ecb230283e8022100bc6658d60ad4f7120d9226c8f6eada87f3b0388f73c458011988bab36e78ba15014752210359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b813821037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d9352aeffffffff0240420f00000000001976a91485eb47fe98f349065d6f044e27a4ac541af79ee288acf017ab010000000017a91421c4a435d9ac263ec55b35a1a5ca95e979639b9b8700000000"}},{"creator":"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5","createdTs":1405543835343,"seenBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543835343,"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543835968},"signedBy":{"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543835343},"rejectedBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543850998},"sentTs":null,"sentTxid":null,"inputChainPaths":["m/45\'/0/0/1"],"comment":"2","builderObj":{"valueInSat":"29000000","valueOutSat":"1000000","feeSat":"10000","remainderSat":"27990000","hashToScriptMap":{"2N4eyXKikdnnUT4S74MRNAYqXChhUYmZ1Sb":"52210359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b813821037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d9352ae"},"selectedUtxos":[{"address":"2N4eyXKikdnnUT4S74MRNAYqXChhUYmZ1Sb","txid":"6c9da5b0da4bab0d576033325e987b10ccf2b9bf479d306b6aae36efeaa56892","vout":0,"ts":1405543698,"scriptPubKey":"a9147d274ac50968d7823b6cbc1b38770deb7157995387","amount":0.29,"confirmationsFromCache":false}],"inputsSigned":0,"signaturesAdded":1,"signhash":1,"spendUnconfirmed":true,"tx":"01000000019268a5eaef36ae6a6b309d47bfb9f2cc107b985e323360570dab4bdab0a59d6c0000000092004830450220302baae7de2e0f102bf3af2d5f450f673e51bd143020141a769ccdcdf16af188022100e7abc087c76050ed649e7139a5a136969e74e24a8d8f6223d3219ad033a26451014752210359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b813821037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d9352aeffffffff0240420f00000000001976a91485eb47fe98f349065d6f044e27a4ac541af79ee288acf017ab010000000017a9148b102abba0729fb0690c61cf7187064d692d43d78700000000"}},{"creator":"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba","createdTs":1405543869803,"seenBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543869803,"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543870411},"signedBy":{"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba":1405543869803,"02b0c868a3889cd0cfc0e7fef9eaa6d85d7cf6f7573ae5c9d1d13645d22e2eb7e5":1405543890406},"rejectedBy":{},"sentTs":1405543890913,"sentTxid":"6a0f61574ad65e537e7e99298968db565f97b894b61f4c8f8fac8fcaedb83e2b","inputChainPaths":["m/45\'/0/0/1"],"comment":"3","builderObj":{"valueInSat":"29000000","valueOutSat":"1100000","feeSat":"10000","remainderSat":"27890000","hashToScriptMap":{"2N4eyXKikdnnUT4S74MRNAYqXChhUYmZ1Sb":"52210359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b813821037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d9352ae"},"selectedUtxos":[{"address":"2N4eyXKikdnnUT4S74MRNAYqXChhUYmZ1Sb","txid":"6c9da5b0da4bab0d576033325e987b10ccf2b9bf479d306b6aae36efeaa56892","vout":0,"ts":1405543698,"scriptPubKey":"a9147d274ac50968d7823b6cbc1b38770deb7157995387","amount":0.29,"confirmationsFromCache":false}],"inputsSigned":1,"signaturesAdded":2,"signhash":1,"spendUnconfirmed":true,"tx":"01000000019268a5eaef36ae6a6b309d47bfb9f2cc107b985e323360570dab4bdab0a59d6c00000000db00483045022100a8ce7907f9fd7dd41dd65c2dec425e008efea06ee7c80787c10c0e210fbf181302207712c0fdd1cb25836ac1fc2fd303c1e26b85e8980417719b9ed50e977a9693ec01483045022100d1780c4f028cd898920aca3eaceba352ed9306cd17f019ae2f634e8facad149a02203c84ab2093da8e22577e93f27a732f0728d4e6db0c749f3cd3d898d6a025152a014752210359c6d0d0d31f83301169901a6ffad9535f14014b5ab3b43561dbb2436a7b813821037d06f713f13a11967fd5edca265ff4c77528693a712c482256505693e4890d9352aeffffffff02e0c81000000000001976a91485eb47fe98f349065d6f044e27a4ac541af79ee288ac5091a9010000000017a914cc1cab78458b1a951b91c6dcd7eeeeb682f506388700000000"}}],"walletId":"55d4bd062d32f90a","networkName":"testnet"},"privateKey":{"extendedPrivateKeyString":"tprv8ZgxMBicQKsPdWUAmaaopPftevC72Jtiu19V8ee5XijL9JvogqfR95uVrL85f8yBdQMq3KyQtG3Q91yWQb3XDbWWpcdWFDAmJ7Xy2XWkGJu","networkName":"testnet","privateKeyCache":{"m/45\'/0/0/0":"b6fd8d1a079efd523da34f31ba81f544fc3d0a728a8a98299d8980682518e79c","m/45\'/0/1/1":"0f4d52d2a99e4c8c1c2edf09fef12407c3abd2304b961198c3f131a8c8443a13","m/45\'/0/0/1":"de5c191c343bd6017b98708c03344849624a14e2c167cfd6eb8dcb075d139293"}},"addressBook":{"msj42CCGruhRsFrGATiUuh25dtxYtnpbTx":{"hidden":false,"createdTs":1405543109222,"copayerId":"02c7b87033e4357d8afc6ab7fe31fff054772ea6251f0d9c8a835b1c1ac74f6fba","label":"faucet","signature":"3045022067576e5b37f2707a8dc66e57511ad9b10a3125bd95193fff6f8f6402969c3bf3022100adff9f417db07d88face13b3d13f422740d4421440cade1a205684dfdc5d733a"}}}'; +var encryptedLegacyO = 'U2FsdGVkX19yGM1uBAIzQa8Po/dvUicmxt1YyRk/S97PcZ6I6rHMp9dMagIrehg4Qd6JHn/ustmFHS7vmBYj0EBpf6rdXiQezaWnVAJS9/xYjAO36EFUbl+NmUanuwujAxgYdSP/sNssRLeInvExmZYW993EEclxkwL6YUyX66kKsxGQo2oWng0NreBJNhFmrbOEWeFje2PiWP57oUjKsurFzwpluAAarUTYSLud+nXeabC7opzOP5yqniWBMJz0Ou8gpNCWCMhG/P9F9ccVPY7juyd0Hf41FVse8nd2++axKB57+paozLdO+HRfV6zkMqC3h8gWY7LkS75j3bvqcTw9LhXmzE0Sz21n9yDnRpA4chiAvtwQvvBGgj1pFMKhNQU6Obac9ZwKYzUTgdDn3Uzg1UlDzgyOh9S89rbRTV84WB+hXwhuVluWzbNNYV3vXe5PFrocVktIrtS3xQh+k/7my4A6/gRRrzNYpKrUASJqDS/9u9WBkG35xD63J/qXjtG2M0YPwbI57BK1IK4K510b8V72lz5U2XQrIC4ldBwni1rpSavwCJV9xF6hUdOmNV8fZsVHP0NeN1PYlLkSb2QgfuoWnkcsJerwuFR7GZC/i6efrswtpO0wMEQr/J0CLbeXlHAru6xxjCBhWoJvZpMGw72zgnDLoyMNsEVglNhx/VlV9ZMYkkdaEYAxPOEIyZdQ5MS+2jEAlXf818n/xzJSVrniCn9be8EPePvkw35pivprvy09vbW4cKsWBKvgIyoT6A3OhUOCCS8E9cg0WAjjav2EymrbKmGWRHaiD+EoJqaDg6s20zhHn1YEa/YwvGGSB5+Hg8baLHD8ZASvxz4cFFAAVZrBUedRFgHzqwaMUlFXLgueivWUj7RXlIw6GuNhLoo1QkhZMacf23hrFxxQYvGBRw1hekBuDmcsGWljA28udBxBd5f9i+3gErttMLJ6IPaud590uvrxRIclu0Sz9R2EQX64YJxqDtLpMY0PjddSMu8vaDRpK9/ZSrnz/xrXsyabaafz4rE/ItFXjwFUFkvtmuauHTz6nmuKjVfxvNLNAiKb/gI7vQyUhnTbKIApe7XyJsjedNDtZqsPoJRIzdDmrZYxGStbAZ7HThqFJlSJ9NPNhH+E2jm3TwL5mwt0fFZ5h+p497lHMtIcKffESo7KNa2juSVNMDREk0NcyxGXGiVB2FWl4sLdvyhcsVq0I7tmW6OGZKRf8W49GCJXq6Ie69DJ9LB1DO67NV1jsYbsLx9uhE2yEmpWZ3jkoCV/Eas4grxt0CGN6EavzQ=='; +var legacyPassword = '1'; diff --git a/test/unit/controllers/controllersSpec.js b/test/unit/controllers/controllersSpec.js index c056246c8..b99da8e3c 100644 --- a/test/unit/controllers/controllersSpec.js +++ b/test/unit/controllers/controllersSpec.js @@ -29,7 +29,9 @@ describe("Unit: Controllers", function() { totalCopayers: 5, spendUnconfirmed: 1, reconnectDelay: 100, - networkName: 'testnet' + networkName: 'testnet', + alternativeName: 'lol currency', + alternativeIsoCode: 'LOL' }; it('Copay config should be binded', function() { @@ -65,11 +67,11 @@ describe("Unit: Controllers", function() { }); }); - describe('Setup Controller', function() { - var setupCtrl; + describe('Create Controller', function() { + var c; beforeEach(inject(function($controller, $rootScope) { scope = $rootScope.$new(); - setupCtrl = $controller('SetupController', { + c = $controller('CreateController', { $scope: scope, }); })); @@ -124,11 +126,21 @@ describe("Unit: Controllers", function() { }); describe('Send Controller', function() { - var scope, form, sendForm; + var scope, form, sendForm, sendCtrl; beforeEach(angular.mock.module('copayApp')); - beforeEach(angular.mock.inject(function($compile, $rootScope, $controller) { + beforeEach(module(function($provide) { + $provide.value('request', { + 'get': function(_, cb) { + cb(null, null, [{name: 'lol currency', code: 'LOL', rate: 2}]); + } + }); + })); + beforeEach(angular.mock.inject(function($compile, $rootScope, $controller, rateService) { scope = $rootScope.$new(); + scope.rateService = rateService; $rootScope.wallet = new FakeWallet(walletConfig); + config.alternativeName = 'lol currency'; + config.alternativeIsoCode = 'LOL'; var element = angular.element( '
' + '' + @@ -147,11 +159,12 @@ describe("Unit: Controllers", function() { '' + '' + '' + + '' + '' + '
' ); $compile(element2)(scope); - $controller('SendController', { + sendCtrl = $controller('SendController', { $scope: scope, $modal: {}, }); @@ -241,8 +254,22 @@ describe("Unit: Controllers", function() { config.unitToSatoshi = old; }); - - + it('should convert bits amount to fiat', function(done) { + scope.rateService.whenAvailable(function() { + sendForm.amount.$setViewValue(1e6); + scope.$digest(); + expect(scope.alternative).to.equal(2); + done(); + }); + }); + it('should convert fiat to bits amount', function(done) { + scope.rateService.whenAvailable(function() { + sendForm.alternative.$setViewValue(2); + scope.$digest(); + expect(scope.amount).to.equal(1e6); + done(); + }); + }); it('should create and send a transaction proposal', function() { sendForm.address.$setViewValue('mkfTyEk7tfgV611Z4ESwDDSZwhsZdbMpVy'); diff --git a/test/unit/services/servicesSpec.js b/test/unit/services/servicesSpec.js index df7eed5bf..cb0d7c839 100644 --- a/test/unit/services/servicesSpec.js +++ b/test/unit/services/servicesSpec.js @@ -79,8 +79,6 @@ describe("Unit: controllerUtils", function() { expect($rootScope.addrInfos[0].address).to.be.equal(Waddr);; })); }); - - }); describe("Unit: Notification Service", function() { @@ -118,12 +116,7 @@ describe("Unit: isMobile Service", function() { isMobile.any().should.equal(true); })); }); -describe("Unit: video service", function() { - beforeEach(angular.mock.module('copayApp.services')); - it('should contain a video service', inject(function(video) { - should.exist(video); - })); -}); + describe("Unit: uriHandler service", function() { beforeEach(angular.mock.module('copayApp.services')); it('should contain a uriHandler service', inject(function(uriHandler) { @@ -135,3 +128,36 @@ describe("Unit: uriHandler service", function() { }).should.not.throw(); })); }); + +describe('Unit: Rate Service', function() { + beforeEach(angular.mock.module('copayApp.services')); + it('should be injected correctly', inject(function(rateService) { + should.exist(rateService); + })); + it('should be possible to ask if it is available', + inject(function(rateService) { + should.exist(rateService.isAvailable); + }) + ); + beforeEach(module(function($provide) { + $provide.value('request', { + 'get': function(_, cb) { + cb(null, null, [{name: 'lol currency', code: 'LOL', rate: 2}]); + } + }); + })); + it('should be possible to ask for conversion from fiat', + inject(function(rateService) { + rateService.whenAvailable(function() { + (1).should.equal(rateService.fromFiat(2, 'LOL')); + }); + }) + ); + it('should be possible to ask for conversion to fiat', + inject(function(rateService) { + rateService.whenAvailable(function() { + (2).should.equal(rateService.toFiat(1e8, 'LOL')); + }); + }) + ); +}); diff --git a/views/copayers.html b/views/copayers.html index 47851a4be..ca50f0075 100644 --- a/views/copayers.html +++ b/views/copayers.html @@ -51,10 +51,10 @@
-
+
Waiting Copayer +
Creating wallet... diff --git a/views/home.html b/views/home.html index 9d3d48aa1..80f9d70bc 100644 --- a/views/home.html +++ b/views/home.html @@ -9,13 +9,13 @@ Open a wallet