diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..28d2d0a14 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,51 @@ +module.exports = function(grunt) { + + //Load NPM tasks + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-mocha-test'); + grunt.loadNpmTasks('grunt-markdown'); + grunt.loadNpmTasks('grunt-shell'); + + // Project Configuration + grunt.initConfig({ + shell: { + browserify: { + options: { + stdout: true, + stderr: true + }, + command: grunt.option('target') === 'dev' ? + 'node ./util/build.js -d ' : 'node ./util/build.js ' + } + }, + watch: { + readme: { + files: ['README.md'], + tasks: ['markdown'] + }, + scripts: { + files: ['**/*.js', '**/*.html', '!**/node_modules/**', '!browser/bundle.js', '!browser/testdata.js', '!lib/**js', '!browser/vendor-bundle.js'], + tasks: ['shell'], + }, + }, + mochaTest: { + options: { + reporter: 'spec', + }, + src: ['test/*.js'], + }, + markdown: { + all: { + files: [{ + expand: true, + src: 'README.md', + dest: '.', + ext: '.html' + }] + } + } + }); + + grunt.registerTask('default', ['shell','watch']); + +}; diff --git a/concat.sh b/concat.sh new file mode 100755 index 000000000..f747c6684 --- /dev/null +++ b/concat.sh @@ -0,0 +1,7 @@ +#! /bin/bash + +cd vendor/ +cat browser-adapter.js crypto-2.0.js crypto-3.1.js jsbn.js jsbn2.js prng4.js util.js rng.js ec.js sec.js ecdsa.js eckey.js > vendor-bundle.js +mv vendor-bundle.js ../ +cd ../ + diff --git a/copay.js b/copay.js new file mode 100644 index 000000000..5b2a2951c --- /dev/null +++ b/copay.js @@ -0,0 +1,5 @@ + +module.exports.Storage = require('./js/models/Storage'); +module.exports.PublicKeyRing = require('./js/models/PublicKeyRing'); +module.exports.CopayPeer = require('./js/models/CopayPeer'); +module.exports.FakeStorage = require('./test/FakeStorage'); diff --git a/index.html b/index.html index 534d427a2..b5942f087 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,8 @@ -
+
+
@@ -38,8 +39,17 @@ +
+
+ {{$root.flashMessage.type}}: + {{$root.flashMessage.message}} + Dismiss +
+ +
+
@@ -61,7 +71,19 @@
-

Join wallet

+

Open a Existing Wallet

+ +
+
+ +
+
+
+
+
+

Join a Network Wallet

@@ -71,7 +93,7 @@
-

Create a new wallet

+

Create a New Wallet

@@ -85,6 +107,14 @@

I am

{{$root.peerId}}

+ +
    +
  • [DEBUG] Pubkeys that you have: {{$root.publicKeyRing.registeredCopayers()}} +
  • + ${{pub.extendedPublicKeyString()}} +
  • +
+

Copayers ({{$root.connectedPeers.length}}/5)

Copayers ({{$root.connectedPeers.length}}/5)

@@ -250,12 +280,15 @@ + + + + - diff --git a/js/config.js b/js/config.js index 3793d88bd..abead97d4 100644 --- a/js/config.js +++ b/js/config.js @@ -1,45 +1,8 @@ 'use strict'; -//Setting up route -angular - .module('copay') - .config(function($routeProvider) { - - $routeProvider - .when('/', { - templateUrl: 'signin.html' - }) - .when('/signin', { - templateUrl: 'signin.html' - }) - .when('/home', { - templateUrl: 'home.html' - }) - .when('/join/:id', { - templateUrl: 'join.html' - }) - .when('/peer', { - templateUrl: 'peer.html' - }) - .when('/transactions', { - templateUrl: 'transactions.html' - }) - .when('/send', { - templateUrl: 'send.html' - }) - .when('/backup', { - templateUrl: 'backup.html' - }) - .otherwise({ - templateUrl: '404.html' - }); - }); - -//Setting HTML5 Location Mode -angular - .module('copay') - .config(function($locationProvider) { - $locationProvider - .html5Mode(false); - //.hashPrefix('!'); - }); +var config = { + networkName: 'testnet', + p2pApiKey: 'lwjd5qra8257b9', + p2pDebug: 3, + maxPeers: 5, +}; diff --git a/js/controllers/header.js b/js/controllers/header.js index 5e510854a..6a2787e54 100644 --- a/js/controllers/header.js +++ b/js/controllers/header.js @@ -32,9 +32,12 @@ angular.module('copay.header').controller('HeaderController', }; $scope.signout = function() { - $rootScope.isLogged = false; Network.disconnect(function() { $location.path('signin'); }); }; + + $scope.clearFlashMessage = function() { + $rootScope.flashMessage = {}; + }; }); diff --git a/js/controllers/signin.js b/js/controllers/signin.js index 9acfe3b70..fee00a14a 100644 --- a/js/controllers/signin.js +++ b/js/controllers/signin.js @@ -2,38 +2,60 @@ angular.module('copay.signin').controller('SigninController', function($scope, $rootScope, $location, Network, Storage) { - var peerData = Storage.get('peerData'); + + var peerData = Storage.get($rootScope.walletId, 'peerData'); $scope.loading = false; $rootScope.peerId = peerData ? peerData.peerId : null; + $scope.listWalletIds = function() { + return Storage.getWalletIds(); + }; + $scope.create = function() { $scope.loading = true; + Network.createWallet(); Network.init(function() { $location.path('peer'); + $rootScope.$digest(); }); }; + $scope.open = function(walletId) { + $scope.loading = true; + + if (Network.openWallet(walletId)) { + Network.init(function() { + $location.path('peer'); + $rootScope.$digest(); + }); + } + }; + $scope.join = function(cid) { $scope.loading = true; if (cid) { - $rootScope.connectedTo.push(cid); - Network.init(function() { - Network.connect(cid, function() { - $location.path('peer'); + Network.connect(cid, + function() { + $location.path('peer'); + $rootScope.$digest(); + }, function() { + +console.log('[signin.js.46] SETTING MESSAGE'); //TODO + $rootScope.flashMessage = { message: 'Connection refussed', type: 'error'}; + $location.path('home'); + $rootScope.$digest(); }); }); } }; - if (peerData && peerData.peerId && peerData.connectedTo.length > 0) { + if (peerData && peerData.peerId && peerData.connectedPeers.length > 0) { $rootScope.peerId = peerData.peerId; - $rootScope.connectedPeers = peerData.connectedPeers; - - $scope.join(peerData.connectedTo[0]); + $scope.join(peerData.connectedPeers); } }); diff --git a/js/init.js b/js/init.js index 4c8f29b30..d9e3481d8 100644 --- a/js/init.js +++ b/js/init.js @@ -1,5 +1,5 @@ 'use strict'; - +var copay = require('copay'); angular.element(document).ready(function() { // Init the app angular.bootstrap(document, ['copay']); diff --git a/js/models/CopayPeer.js b/js/models/CopayPeer.js new file mode 100644 index 000000000..5a25206de --- /dev/null +++ b/js/models/CopayPeer.js @@ -0,0 +1,284 @@ + +var imports = require('soop').imports(); +var EventEmitter= imports.EventEmitter || require('events').EventEmitter; + +/* + * Emits + * 'networkChange' + * when network layout has change (new/lost peers, etc) + * + * 'data' + * when an unknown data type arrives + * + * Provides + * send(toPeerIds, {data}, cb?) + * + */ + +function CopayPeer(opts) { + opts = opts || {}; + this.peerId = opts.peerId; + this.apiKey = opts.apiKey || 'lwjd5qra8257b9'; + this.debug = opts.debug || 3; + this.maxPeers = opts.maxPeers || 5; + this.connectedPeers = []; +} + +CopayPeer.parent=EventEmitter; + +// Array helpers +CopayPeer._arrayDiff = function(a, b) { + var seen = []; + var diff = []; + + for (var i = 0; i < b.length; i++) + seen[b[i]] = true; + + for (var j = 0; j < a.length; j++) + if (!seen[a[j]]) + diff.push(a[j]); + + return diff; +}; + +CopayPeer._inArray = function(el, array) { + return array.indexOf(el) > -1; +}; + +CopayPeer._arrayPushOnce = function(el, array) { + var ret = false; + if (!CopayPeer._inArray(el, array)) { + array.push(el); + ret = true; + } + return ret; +}; + +CopayPeer._arrayRemove = function(el, array) { + var pos = array.indexOf(el); + if (pos >= 0) array.splice(pos, 1); + + return array; +}; + +// DEBUG +CopayPeer.prototype._showConnectedPeers = function() { + console.log("### CONNECTED PEERS", this.connectedPeers); +}; + +CopayPeer.prototype._onClose = function(peerId) { + this.connectedPeers = CopayPeer._arrayRemove(peerId, this.connectedPeers); + this._notify(); +}; + +CopayPeer.prototype._connectToPeers = function(peerIds) { + var self = this; + var ret = false; + var arrayDiff1= CopayPeer._arrayDiff(peerIds, this.connectedPeers); + var arrayDiff = CopayPeer._arrayDiff(arrayDiff1, [this.peerId]); + arrayDiff.forEach(function(peerId) { + console.log('### CONNECTING TO:', peerId); + self.connectTo(peerId); + ret = true; + }); + return ret; +}; + +CopayPeer.prototype._onData = function(data, isInbound) { + var obj; + try { + obj = JSON.parse(data); + } catch (e) { + console.log('### ERROR ON DATA: "%s" ', data, isInbound, e); + return; + }; + console.log('### RECEIVED TYPE: %s FROM %s', obj.data.type, obj.sender); + + switch(obj.data.type) { + case 'peerList': + this._connectToPeers(obj.data.peers); + this._notify(); + break; + case 'disconnect': + this._onClose(obj.sender); + break; + default: + this.emit('data', obj.sender, obj.data, isInbound); + } +}; + +CopayPeer.prototype._sendPeers = function(peerIds) { + console.log('#### SENDING PEER LIST: ', this.connectedPeers, ' TO ', peerIds?peerIds: 'ALL'); + this.send(peerIds, { + type: 'peerList', + peers: this.connectedPeers, + }); +}; + +CopayPeer.prototype._addPeer = function(peerId, isInbound) { + + var hasChanged = CopayPeer._arrayPushOnce(peerId, this.connectedPeers); + + + if (isInbound && hasChanged) { + this._sendPeers(); //broadcast peer list + } + else { + if (isInbound) { + this._sendPeers(peerId); + } + } +}; + +CopayPeer.prototype._setupConnectionHandlers = function( + dataConn, isInbound, openCallback, closeCallback) { + + var self=this; + + dataConn.on('open', function() { + if (!CopayPeer._inArray(dataConn.peer, self.connectedPeers)) { + + console.log('### DATA CONNECTION READY TO: ADDING PEER: %s (inbound: %s)', + dataConn.peer, isInbound); + + self._addPeer(dataConn.peer, isInbound); + self._notify( isInbound ? dataConn.peer : null); + if (typeof openCallback === 'function') openCallback(); + } + }); + + dataConn.on('data', function(data) { + self._onData(data, isInbound); + }); + + dataConn.on('error', function(e) { + console.log('### DATA ERROR',e ); //TODO + }); + + dataConn.on('close', function() { + console.log('### CLOSE RECV FROM:', dataConn.peer); //TODO + self._onClose(dataConn.peer); + if (typeof closeCallback === 'function') closeCallback(); + }); +}; + +CopayPeer.prototype._notify = function(newPeer) { + this._showConnectedPeers(); + this.emit('networkChange', newPeer); +}; + +CopayPeer.prototype._setupPeerHandlers = function(openCallback) { + var self=this; + var p = this.peer; + + + p.on('open', function(peerId) { + console.log('### PEER OPEN. I AM:' + peerId); + self.peerId = peerId; + self.connectedPeers = [peerId]; + self._notify(); + return openCallback(peerId); + }); + + p.on('error', function(err) { + console.log('### PEER ERROR:', err); + }); + + p.on('connection', function(dataConn) { + + console.log('### NEW INBOUND CONNECTION %d/%d', self.connectedPeers.length, self.maxPeers); + if (self.connectedPeers.length >= self.maxPeers) { + console.log('### PEER REJECTED. PEER MAX LIMIT REACHED'); + dataConn.on('open', function() { + console.log('### CLOSING CONN FROM:' + dataConn.peer); + dataConn.close(); + }); + } + else { + self._setupConnectionHandlers(dataConn, true); + } + }); +}; + +CopayPeer.prototype.start = function(openCallback) { + // Start PeerJS Peer + this.peer = new Peer(this.peerId, { + key: this.apiKey, // TODO: we need our own PeerServer KEY (http://peerjs.com/peerserver) + debug: this.debug, + }); + + this._setupPeerHandlers(openCallback); +}; + +CopayPeer.prototype._sendToOne = function(peerId, data, cb) { + if (peerId !== this.peerId) { + var conns = this.peer.connections[peerId]; + + if (conns) { + var str = JSON.stringify({ + sender: this.peerId, + data: data + }); + + for (var i = 0; i < conns.length; i++) { + var conn = conns[i]; + conn.send(str); + } + } + } + if (typeof cb === 'function') cb(); +}; + +CopayPeer.prototype.send = function(peerIds, data, cb) { + var self=this; + + if (!peerIds) { + peerIds = this.connectedPeers; + data.isBroadcast = 1; + } +console.log('[CopayPeer.js.216:SENDD:]',data); //TODO + + if (Array.isArray(peerIds)) { + var l = peerIds.length; + var i = 0; + peerIds.forEach(function(peerId) { + self._sendToOne(peerId, data, function () { + if (++i === l && typeof cb === 'function') cb(); + }); + }); + } + else if (typeof peerIds === 'string') + self._sendToOne(peerIds, data, cb); +}; + +CopayPeer.prototype.connectTo = function(peerId, openCallback, closeCallback ) { + var self = this; + + console.log('### STARTING TO CONNECT TO:' + peerId ); + + var dataConn = this.peer.connect(peerId, { +// label: 'wallet', + serialization: 'none', + reliable: true, + metadata: { message: 'hi copayer!' } + }); + + self._setupConnectionHandlers(dataConn, false, openCallback, closeCallback); +}; + +CopayPeer.prototype.disconnect = function(peerId, cb) { + var self = this; + + this.send(null, { type: 'disconnect' }, function() { + self.connectedPeers = []; + self.peerId = null; + if (self.peer) { + self.peer.disconnect(); + self.peer.destroy(); + self.peer = null; + } + if (typeof cb === 'function') cb(); + }); +}; + +module.exports = require('soop')(CopayPeer); diff --git a/js/models/Peer.js b/js/models/Peer.js new file mode 100644 index 000000000..2d76a7d15 --- /dev/null +++ b/js/models/Peer.js @@ -0,0 +1,8 @@ + + +function Peer(id) { + this.id = id; +}; + + +module.exports = require('soop')(Peer); diff --git a/js/models/PublicKeyRing.js b/js/models/PublicKeyRing.js index e047ecb9e..ec159403b 100644 --- a/js/models/PublicKeyRing.js +++ b/js/models/PublicKeyRing.js @@ -12,8 +12,6 @@ var Transaction = bitcore.Transaction; var buffertools = bitcore.buffertools; var Storage = imports.Storage || require('./Storage'); -var log = imports.log || console.log; - var storage = Storage.default(); /* @@ -31,7 +29,7 @@ var CHANGE_BRANCH = 'm/1/'; function PublicKeyRing(opts) { opts = opts || {}; - this.network = opts.network === 'livenet' ? + this.network = opts.networkName === 'livenet' ? bitcore.networks.livenet : bitcore.networks.testnet; this.requiredCopayers = opts.requiredCopayers || 3; @@ -52,33 +50,17 @@ PublicKeyRing.getRandomId = function () { }; PublicKeyRing.decrypt = function (passphrase, encPayload) { - log('[wallet.js.35] TODO READ: passphrase IGNORED'); + console.log('[wallet.js.35] TODO READ: passphrase IGNORED'); return encPayload; }; PublicKeyRing.encrypt = function (passphrase, payload) { - log('[wallet.js.92] TODO: passphrase IGNORED'); + console.log('[wallet.js.92] TODO: passphrase IGNORED'); return payload; }; -PublicKeyRing.read = function (id, passphrase) { - var encPayload = storage.read(id); - if (!encPayload) - throw new Error('Could not find wallet data'); - var data; - try { - data = JSON.parse( PublicKeyRing.decrypt( passphrase, encPayload )); - } catch (e) { - throw new Error('error in storage: '+ e.toString()); - return; - }; - - if (data.id !== id) - throw new Error('Wrong id in data'); - - var config = { network: data.networkName === 'livenet' ? - bitcore.networks.livenet : bitcore.networks.testnet - }; +PublicKeyRing.fromObj = function (data) { + var config = { networkName: data.networkName || 'livenet' }; var w = new PublicKeyRing(config); @@ -97,6 +79,25 @@ PublicKeyRing.read = function (id, passphrase) { return w; }; +PublicKeyRing.read = function (id, passphrase) { + var encPayload = storage.get(id); + if (!encPayload) + throw new Error('Could not find wallet data'); + var data; + try { + data = JSON.parse( PublicKeyRing.decrypt( passphrase, encPayload )); + } catch (e) { + throw new Error('error in storage: '+ e.toString()); + return; + }; + + if (data.id !== id) + throw new Error('Wrong id in data'); + + + return PublicKeyRing.fromObj(data); +}; + PublicKeyRing.prototype.toObj = function() { return { id: this.id, @@ -123,7 +124,7 @@ PublicKeyRing.prototype.store = function (passphrase) { if (!this.id) throw new Error('wallet has no id'); - storage.save(this.id, PublicKeyRing.encrypt(passphrase,this.serialize())); + storage.set(this.id, PublicKeyRing.encrypt(passphrase,this.serialize())); this.dirty = 0; return true; @@ -136,7 +137,7 @@ PublicKeyRing.prototype.registeredCopayers = function () { PublicKeyRing.prototype.haveAllRequiredPubKeys = function () { - return this.registeredCopayers() === this.totalCopayers; + return this.registeredCopayers() >= this.totalCopayers; }; PublicKeyRing.prototype._checkKeys = function() { @@ -188,7 +189,7 @@ PublicKeyRing.prototype.getCopayersPubKeys = function (index, isChange) { PublicKeyRing.prototype._checkIndexRange = function (index, isChange) { if ( (isChange && index > this.changeAddressIndex) || (!isChange && index > this.addressIndex)) { - log('Out of bounds at getAddress: Index %d isChange: %d', index, isChange); + console.log('Out of bounds at getAddress: Index %d isChange: %d', index, isChange); throw new Error('index out of bound'); } }; @@ -238,9 +239,16 @@ PublicKeyRing.prototype.getAddresses = function() { return ret; }; -PublicKeyRing.prototype._checkInPRK = function(inPKR) { - if (this.id !== inPKR.id) +PublicKeyRing.prototype._checkInPRK = function(inPKR, ignoreId) { + + + if (!inPKR.ts) { + throw new Error('inPRK bad format: Did you use .toObj()?'); + } + + if (!ignoreId && this.id !== inPKR.id) { throw new Error('inPRK id mismatch'); + } if (this.network.name !== inPKR.networkName) throw new Error('inPRK network mismatch'); @@ -298,10 +306,10 @@ PublicKeyRing.prototype._mergePubkeys = function(inPKR) { return hasChanged; }; -PublicKeyRing.prototype.merge = function(inPKR) { +PublicKeyRing.prototype.merge = function(inPKR, ignoreId) { var hasChanged = false; - this._checkInPRK(inPKR); + this._checkInPRK(inPKR, ignoreId); if (this._mergeIndexes(inPKR)) hasChanged = true; diff --git a/js/models/Storage.js b/js/models/Storage.js new file mode 100644 index 000000000..6f9ce6e59 --- /dev/null +++ b/js/models/Storage.js @@ -0,0 +1,26 @@ +'use strict'; + +var imports = require('soop').imports(); + +function Storage() { + this.data = {}; +} + +Storage.prototype.get = function(k) { + return JSON.parse(localStorage.getItem(k)); +}; + +Storage.prototype.set = function(k,v) { + localStorage.setItem(k, JSON.stringify(v)); +}; + +Storage.prototype.remove = function(k) { + localStorage.removeItem(k); +}; + + +Storage.prototype.clearAll = function() { + localStorage.clear(); +}; + +module.exports = require('soop')(Storage); diff --git a/js/routes.js b/js/routes.js new file mode 100644 index 000000000..3793d88bd --- /dev/null +++ b/js/routes.js @@ -0,0 +1,45 @@ +'use strict'; + +//Setting up route +angular + .module('copay') + .config(function($routeProvider) { + + $routeProvider + .when('/', { + templateUrl: 'signin.html' + }) + .when('/signin', { + templateUrl: 'signin.html' + }) + .when('/home', { + templateUrl: 'home.html' + }) + .when('/join/:id', { + templateUrl: 'join.html' + }) + .when('/peer', { + templateUrl: 'peer.html' + }) + .when('/transactions', { + templateUrl: 'transactions.html' + }) + .when('/send', { + templateUrl: 'send.html' + }) + .when('/backup', { + templateUrl: 'backup.html' + }) + .otherwise({ + templateUrl: '404.html' + }); + }); + +//Setting HTML5 Location Mode +angular + .module('copay') + .config(function($locationProvider) { + $locationProvider + .html5Mode(false); + //.hashPrefix('!'); + }); diff --git a/js/services/network.js b/js/services/network.js index 1551345d5..d1cf09fcc 100644 --- a/js/services/network.js +++ b/js/services/network.js @@ -3,217 +3,176 @@ angular.module('copay.network') .factory('Network', function($rootScope, Storage) { var peer; - $rootScope.connectedPeers = []; - $rootScope.connectedTo = []; - $rootScope.peerId = null; - // Array helpers - var _arrayDiff = function(a, b) { - var seen = []; - var diff = []; - for (var i = 0; i < b.length; i++) - seen[b[i]] = true; - - for (var i = 0; i < a.length; i++) - if (!seen[a[i]]) - diff.push(a[i]); - - return diff; + var _refreshUx = function() { + var cp = $rootScope.cp; + console.log('*** UPDATING UX'); //TODO + $rootScope.peerId = cp.peerId; + $rootScope.connectedPeers = cp.connectedPeers; + $rootScope.$digest(); }; - var _inArray = function(el, array) { - return array.indexOf(el) > -1; - }; - - var _arrayPushOnce = function(el, array) { - if (!_inArray(el, array)) array.push(el); - }; - - var _arrayRemove = function(el, array) { - var pos = array.indexOf(el); - if (pos >= 0) array.splice(pos, 1); - - return array; - }; - - // General helpers - var _saveDataStorage = function() { - Storage.save('peerData', { + var _store = function() { + Storage.set($rootScope.walletId, 'peerData', { peerId: $rootScope.peerId, - connectedTo: $rootScope.connectedTo, connectedPeers: $rootScope.connectedPeers }); }; - var _sender = function(pid, data, cb) { - if (pid !== $rootScope.peerId) { - var conns = peer.connections[pid]; + // set new inbound connections + var _setNewPeer = function(newPeer) { + var cp = $rootScope.cp; + console.log('#### SENDING PKR 1111 '); + cp.send(newPeer, { + type: 'publicKeyRing', + publicKeyRing: $rootScope.publicKeyRing.toObj(), + }); + }; - if (conns) { - var str = JSON.stringify({ - sender: $rootScope.peerId, - data: data - }); + var _handleNetworkChange = function(newPeer) { + var cp = $rootScope.cp; - for (var i = 0; i < conns.length; i++) { - var conn = conns[i]; - conn.send(str); + if (newPeer) + _setNewPeer(newPeer); + + _store(); + _refreshUx(); + }; + + // TODO -> probably not in network.js + var createWallet = function(walletId) { + console.log('### CREATING WALLET. ID:' + walletId); + + //TODO create a wallet and WalletId, not only pkr + var pkr = new copay.PublicKeyRing({ + network: config.networkName, + id: walletId, + }); + pkr.addCopayer(); + console.log('\t### PublicKeyRing Initialized:'); + Storage.addWalletId(pkr.id); + Storage.set(pkr.id, 'publicKeyRing', pkr.toObj()); + + $rootScope.walletId = pkr.id; + $rootScope.publicKeyRing = pkr; + }; + + var openWallet = function (walletId) { + var ret = false; + var pkr = Storage.get(walletId, 'publicKeyRing'); + + if (pkr) { + console.log('### WALLET OPENED:', walletId, pkr); + $rootScope.walletId = walletId; + $rootScope.publicKeyRing = new copay.PublicKeyRing.fromObj(pkr); + ret = true; + } + return ret; + }; + + var closeWallet = function() { + console.log('### CLOSING WALLET'); + $rootScope.walletId = null; + $rootScope.publicKeyRing = null; + //TODO + }; + + var _checkWallet = function(walletId) { + console.log('[network.js.79:_checkWallet:]',walletId); //TODO + + if ($rootScope.walletId && $rootScope.walletId !== walletId) + closeWallet(); + + if ($rootScope.walletId) + return; + + if (!openWallet(walletId)) { + createWallet(walletId); + } + }; + + var _handleData = function(senderId, data, isInbound) { + var cp = $rootScope.cp; + + switch(data.type) { + case 'publicKeyRing': + _checkWallet(data.publicKeyRing.id); + var shouldSend = false; + + var recipients, pkr = $rootScope.publicKeyRing; + if (pkr.merge(data.publicKeyRing, true) && !data.isBroadcast) { + console.log('### BROADCASTING PKR'); + recipients = null; + shouldSend = true; + } + else if (isInbound && !data.isBroadcast) { + // always replying to connecting peer + console.log('### REPLYING PKR TO:', senderId); + recipients = senderId; + shouldSend = true; } - if (typeof cb === 'function') cb(); - } - } - }; + if (shouldSend) { + console.log('### SENDING PKR TO:', recipients); + cp.send( recipients, { + type: 'publicKeyRing', + publicKeyRing: $rootScope.publicKeyRing.toObj(), + }); + } - var _onData = function(data) { - var obj = JSON.parse(data); - - switch(obj.data.type) { - case 'connectedPeers': - _connectToPeers(obj.data.peers); - break; - case 'getPeers': - _send(obj.sender, { - type: 'connectToPeers', - peers: $rootScope.connectedPeers - }); - break; - case 'disconnect': - _onClose(obj.sender); + _refreshUx(); break; } }; - - var _onClose = function(pid) { - $rootScope.connectedPeers = _arrayRemove(pid, $rootScope.connectedPeers); - $rootScope.connectedTo = _arrayRemove(pid, $rootScope.connectedTo); - - _saveDataStorage(); - - $rootScope.$digest(); - }; - - var _connectToPeers = function(peers) { - var arrayDiff = _arrayDiff(peers, $rootScope.connectedTo); - - arrayDiff.forEach(function(pid) { - _connect(pid); - }); + var _setupHandlers = function () { + var cp = $rootScope.cp; + cp.on('networkChange', _handleNetworkChange); + cp.on('data', _handleData); }; // public methods var init = function(cb) { - peer = new Peer($rootScope.peerId, { - key: 'lwjd5qra8257b9', // TODO: we need our own PeerServer KEY (http://peerjs.com/peerserver) - debug: 3 - }); - peer.on('open', function(pid) { - $rootScope.peerId = pid; - _arrayPushOnce(pid, $rootScope.connectedPeers); - _saveDataStorage(); + var cp = $rootScope.cp = new copay.CopayPeer({ + apiKey: config.p2pApiKey, + debug: config.p2pDebug, + maxPeers: config.maxPeers, // TODO: This should be on wallet configuration + }); + _setupHandlers(); - cb(); - - $rootScope.$digest(); - }); - - peer.on('connection', function(conn) { - if (conn.label === 'wallet') { - conn.on('open', function() { - if (!_inArray(conn.peer, $rootScope.connectedTo)) { - var c = peer.connect(conn.peer, { - label: 'wallet', - serialization: 'none', - reliable: false, - metadata: { message: 'hi copayer!' } - }); - - c.on('open', function() { - $rootScope.connectedTo.push(conn.peer); - _arrayPushOnce(conn.peer, $rootScope.connectedPeers); - _saveDataStorage(); - - $rootScope.$digest(); - }); - - c.on('data', _onData); - - c.on('close', function() { - _onClose(c.peer); - }); - } - }); - } + // inicia session + cp.start(function(peerId) { + return cb(); }); }; - var connect = function(pid, cb) { - if (pid !== $rootScope.peerId) { - var c = peer.connect(pid, { - label: 'wallet', - serialization: 'none', - reliable: false, - metadata: { message: 'hi copayer!' } - }); + var disconnect = function() { + if ($rootScope.cp) { + $rootScope.cp.disconnect(); + } + Storage.remove('peerData'); + $rootScope.isLogged = false; + _refreshUx(); + }; - c.on('open', function() { - _arrayPushOnce(pid, $rootScope.connectedTo); - _arrayPushOnce(pid, $rootScope.connectedPeers); - - _send(pid, { type: 'getPeers' }); - _saveDataStorage(); - - if (typeof cb === 'function') cb(); - - $rootScope.$digest(); - }); - - c.on('data', _onData); - - c.on('close', function() { - _onClose(c.peer); + var connect = function(peerId, openCallback, failCallBack) { + if ($rootScope.cp) { + $rootScope.cp.connectTo(peerId, openCallback, function () { + disconnect(); + failCallBack(); }); } + else + return failCallBack(); }; - var _send = function(pids, data, cb) { - if (Array.isArray(pids)) - pids.forEach(function(pid) { - _sender(pid, data, cb); - }); - else if (typeof pids === 'string') - _sender(pids, data, cb); - }; - - var disconnect = function(cb) { - var conns = $rootScope.connectedPeers.length; - var i = 1; - - _send($rootScope.connectedPeers, { type: 'disconnect' }, function() { - i += 1; - - if (i === conns) { - peer.disconnect(); - peer.destroy(); - - if (typeof cb === 'function') cb(); - } - }); - - Storage.remove('peerData'); - - $rootScope.connectedPeers = []; - $rootScope.connectedTo = []; - $rootScope.peerId = null; - } - return { init: init, connect: connect, - send: _send, - disconnect: disconnect + disconnect: disconnect, + createWallet: createWallet, + openWallet: openWallet, } }); diff --git a/js/services/storage.js b/js/services/storage.js index b036f5aa4..b35cf499e 100644 --- a/js/services/storage.js +++ b/js/services/storage.js @@ -2,21 +2,63 @@ angular.module('copay.storage') .factory('Storage', function($rootScope) { + + + var _key = function(walletId, key) { + return walletId + '::' + key; + }; + + var _pushKey = function(walletId, key) { + var keys = localStorage.getItem(walletId); + localStorage.setItem(walletId, (keys?keys+',':'') +key); + }; + return { - get: function(key) { + getGlobal: function( key) { return JSON.parse(localStorage.getItem(key)); }, - - save: function(key, data) { + setGlobal: function( key, data) { localStorage.setItem(key, JSON.stringify(data)); }, - - remove: function(key) { - localStorage.removeItem(key); + get: function(walletId, key) { + if (!walletId) return; + return JSON.parse(localStorage.getItem(_key(walletId,key))); + }, + set: function(walletId, key, data) { + if (!walletId) return; + var k = _key(walletId,key); + localStorage.setItem(k, JSON.stringify(data)); + _pushKey(walletId, k); + }, + remove: function(walletId, key) { + localStorage.removeItem(_key(walletId,key)); + }, + clearAll: function(walletId){ + var keys = localStorage.getItem(walletId); + keys.split(',').forEach(function(k){ + localStorage.removeItem(key); + }); + }, + addWalletId: function(walletId) { + var ids = localStorage.getItem('walletIds'); + localStorage.setItem('walletIds', (ids?ids+',':'') + walletId); + }, + delWalletId: function(walletId) { + var ids = localStorage.getItem('walletIds'); + if (ids) { + var is = ids.split(','); + var newIds = []; + is.forEach(function(i) { + if (i != walletId) { + newIds.push(i); + } + }); + localStorage.setItem('walletIds', newIds.join(',') ); + } + }, + getWalletIds: function() { + var ids = localStorage.getItem('walletIds'); + return ids ? ids.split(',') : []; }, - - clearAll: function() { - localStorage.clear(); - } }; }); diff --git a/package.json b/package.json index 39ae9613b..8e593f154 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,19 @@ }, "homepage": "https://github.com/bitpay/copay", "devDependencies": { - "grunt-cli": "~0.1.13", + "grunt-contrib-watch": "~0.5.3", + "grunt-mocha-test": "~0.8.2", + "grunt-shell": "~0.6.4", + "grunt-browserify": "~2.0.0", + "grunt-markdown": "~0.5.0", "karma": "~0.12.1", "karma-chrome-launcher": "~0.1.2", "mocha": "~1.18.2", - "karma-mocha": "~0.1.3" + "karma-mocha": "~0.1.3", + "buffertools": "~2.0.1", + "browserify": "~3.32.1", + "browser-pack": "~2.0.1", + "commander": "~2.1.0", + "uglifyify": "~1.2.3" } } diff --git a/test/FakeStorage.js b/test/FakeStorage.js index 164b526ba..f12fc6f95 100644 --- a/test/FakeStorage.js +++ b/test/FakeStorage.js @@ -3,11 +3,11 @@ var FakeStorage = function(){ this.storage = {}; }; -FakeStorage.prototype.read = function (id) { +FakeStorage.prototype.set = function (id) { return this.storage[id]; }; -FakeStorage.prototype.save = function(id, payload) { +FakeStorage.prototype.get = function(id, payload) { this.storage[id] = payload; } diff --git a/test/index.html b/test/index.html new file mode 100644 index 000000000..d84916368 --- /dev/null +++ b/test/index.html @@ -0,0 +1,28 @@ + + + + Mocha + + + + + +
+ + + + + + + + + + + + diff --git a/test/test.publickeyring.js b/test/test.publickeyring.js index 994911447..b78896aeb 100644 --- a/test/test.publickeyring.js +++ b/test/test.publickeyring.js @@ -2,24 +2,24 @@ var chai = chai || require('chai'); var should = chai.should(); -var bitcore = bitcore || require('../node_modules/bitcore'); +var bitcore = bitcore || require('bitcore'); var Address = bitcore.Address; var buffertools = bitcore.buffertools; -var copay = copay || {}; -var fakeStorage = require('./FakeStorage'); +var copay = copay || require('../copay'); +var fakeStorage = copay.FakeStorage; var PublicKeyRing = copay.PublicKeyRing || require('soop').load('../js/models/PublicKeyRing', {Storage: fakeStorage}); var aMasterPubKey = 'tprv8ZgxMBicQKsPdSVTiWXEqCCzqRaRr9EAQdn5UVMpT9UHX67Dh1FmzEMbavPumpAicsUm2XvC6NTdcWB89yN5DUWx5HQ7z3KByUg7Ht74VRZ'; var config = { - network:'livenet', + networkName:'livenet', }; -var createW = function (network) { +var createW = function (networkName) { var config = { - network: network || 'livenet', + networkName: networkName || 'livenet', }; var w = new PublicKeyRing(config); @@ -39,7 +39,7 @@ describe('PublicKeyRing model', function() { it('should create an instance (livenet)', function () { var w = new PublicKeyRing({ - network: config.network + networkName: config.networkName }); should.exist(w); w.network.name.should.equal('livenet'); @@ -158,7 +158,7 @@ describe('PublicKeyRing model', function() { w.generateAddress(false); var w2 = new PublicKeyRing({ - network: 'livenet', + networkName: 'livenet', id: w.id, }); w2.merge(w.toObj()).should.equal(true); @@ -182,33 +182,48 @@ describe('PublicKeyRing model', function() { w.generateAddress(false); + + var w2 = new PublicKeyRing({ + networkName: 'livenet', + }); + (function() { w2.merge(w.toObj());}).should.throw(); + (function() { w2.merge(w,true);}).should.throw(); + +console.log('[test.publickeyring.js.190]'); //TODO + w2.merge(w.toObj(),true).should.equal(true); + +console.log('[test.publickeyring.js.193]'); //TODO + + var w3 = new PublicKeyRing({ - network: 'livenet', + networkName: 'livenet', id: w.id, requiredCopayers: 2, }); (function() { w3.merge(w.toObj());}).should.throw(); var w4 = new PublicKeyRing({ - network: 'testnet', + networkName: 'testnet', id: w.id, }); (function() { w4.merge(w.toObj());}).should.throw(); var w5 = new PublicKeyRing({ - network: 'livenet', + networkName: 'livenet', id: w.id, totalCopayers: 4, }); (function() { w5.merge(w.toObj());}).should.throw(); var w6 = new PublicKeyRing({ - network: 'livenet', + networkName: 'livenet', id: w.id, }); (function() { w6.merge(w);}).should.throw(); w.networkName= 'livenet'; (function() { w6.merge(w);}).should.throw(); + + }); @@ -222,7 +237,7 @@ describe('PublicKeyRing model', function() { } var w2 = new PublicKeyRing({ - network: 'livenet', + networkName: 'livenet', id: w.id, }); should.exist(w); @@ -248,17 +263,14 @@ describe('PublicKeyRing model', function() { for(var i=0; i<5; i++) { w.haveAllRequiredPubKeys().should.equal(false); var w2 = new PublicKeyRing({ - network: 'livenet', + networkName: 'livenet', id: w.id, }); w2.addCopayer(); - w.merge(w2.toObj()); + w.merge(w2.toObj()).should.equal(true); } w.haveAllRequiredPubKeys().should.equal(true); }); - - - }); diff --git a/test/test.storage.js b/test/test.storage.js new file mode 100644 index 000000000..d108b6e5b --- /dev/null +++ b/test/test.storage.js @@ -0,0 +1,15 @@ +'use strict'; + +var chai = chai || require('chai'); +var should = chai.should(); +var copay = copay || require('../copay'); +var Storage = copay.Storage || require('../js/models/Storage'); + +describe('Storage model', function() { + + it('should create an instance', function () { + var s = new Storage(); + should.exist(s); + }); +}); + diff --git a/util/build.js b/util/build.js new file mode 100755 index 000000000..d1d16a476 --- /dev/null +++ b/util/build.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +'use strict'; + +var fs = require('fs'); +var browserify = require('browserify'); +var browserPack = require('browser-pack'); +var exec = require('child_process').exec; +var sys = require('sys'); +var puts = function(error, stdout, stderr) { + if (error) console.log(error); + //sys.puts(stdout); + //sys.puts(stderr); +}; + +var pack = function (params) { + var file = require.resolve('soop'); + var dir = file.substr(0, file.length - String('soop.js').length); + var preludePath = dir + 'example/custom_prelude.js'; + params.raw = true; + params.sourceMapPrefix = '//#'; + params.prelude = fs.readFileSync(preludePath, 'utf8'); + params.preludePath = preludePath; + return browserPack(params); +}; + +var createBundle = function(opts) { + + + opts.dir = opts.dir || 'js/'; + + // concat browser vendor files + exec('cd ' + opts.dir + 'browser; sh concat.sh', puts); + + var bopts = { + pack: pack, + debug: true, + standalone: 'copay', + insertGlobals: true + }; + var b = browserify(bopts); + b.require('./copay', { + expose: 'copay' + }); + + if (!opts.dontminify) { + b.transform({ + global: true + }, 'uglifyify'); + } + var bundle = b.bundle(); + return bundle; +}; + +if (require.main === module) { + var list = function(val) { + return val.split(','); + }; + var program = require('commander'); + program + .version('0.0.1') + .option('-d, --dontminify', 'Don\'t minify the code.') + .option('-o, --stdout', 'Specify output as stdout') + .parse(process.argv); + var copayBundle = createBundle(program); + copayBundle.pipe(program.stdout ? process.stdout : fs.createWriteStream('lib/copayBundle.js')); +} + +module.exports.createBundle = createBundle;