Merge pull request #95 from maraoz/feature/auto-peer-discovery

auto peer discovery
This commit is contained in:
Matias Alejo Garcia 2014-04-18 19:16:35 -03:00
commit 0a188532b9
14 changed files with 148 additions and 106 deletions

View file

@ -61,7 +61,7 @@
<span ng-show="$root.wallet.publicKeyRing.totalCopayers - $root.wallet.publicKeyRing.registeredCopayers()==1"> <span ng-show="$root.wallet.publicKeyRing.totalCopayers - $root.wallet.publicKeyRing.registeredCopayers()==1">
One key is One key is
</span> </span>
missing. Ask your copayers to join your session: <b>{{$root.wallet.network.peerId}}</b> missing. Share this secret with your other copayers for them to join your wallet: <b>{{$root.wallet.network.peerId}}</b>
</div> </div>
</div> </div>
@ -81,8 +81,9 @@
<div ng-show="!loading"> <div ng-show="!loading">
<div class="row"> <div class="row">
<div class="large-6 columns"> <div class="large-6 columns">
<h3>Join a Network Wallet</h3> <h3>Join Wallet Creation</h3>
<input type="text" class="form-control" placeholder="Peer ID" ng-model="connectionId" autofocus> <input type="text" class="form-control" placeholder="Paste secret here"
ng-model="connectionId" autofocus>
</div> </div>
<div class="large-3 columns"> <div class="large-3 columns">
<button class="button primary expand round" type="button" ng-click="join(connectionId)">Join</button> <button class="button primary expand round" type="button" ng-click="join(connectionId)">Join</button>
@ -109,7 +110,7 @@
<div ng-show="walletIds.length>0"> <div ng-show="walletIds.length>0">
<div class="row"> <div class="row">
<div class="large-6 columns"> <div class="large-6 columns">
<h3>Open a Existing Wallet</h3> <h3>Open Existing Wallet</h3>
<select class="form-control" ng-model="selectedWalletId" ng-options="walletId for walletId in walletIds"> <select class="form-control" ng-model="selectedWalletId" ng-options="walletId for walletId in walletIds">
</select> </select>
</div> </div>

View file

@ -3,13 +3,13 @@
var config = { var config = {
networkName: 'testnet', networkName: 'testnet',
network: { network: {
key: 'lwjd5qra8257b9', // key: 'lwjd5qra8257b9',
// This is for running local peerJs with params: ./peerjs -p 10009 -k 'sdfjhwefh' // This is for running local peerJs with params: ./peerjs -p 10009 -k 'sdfjhwefh'
// key: 'sdfjhwefh', key: 'sdfjhwefh',
// host: 'localhost', host: '192.168.1.100',
// port: 10009, port: 10009,
// path: '/', path: '/',
maxPeers: 3, maxPeers: 10,
debug: 3, debug: 3,
}, },
limits: { limits: {
@ -23,12 +23,12 @@ var config = {
verbose: 1, verbose: 1,
}, },
blockchain: { blockchain: {
host: 'localhost', host: 'test.insight.is',
port: 3001 port: 80
}, },
socket: { socket: {
host: 'localhost', host: 'test.insight.is',
port: 3001 port: 80
}, },
verbose: 1, verbose: 1,
}; };

View file

@ -33,7 +33,6 @@ angular.module('copay.setup').controller('SetupController',
}; };
var w = walletFactory.create(opts); var w = walletFactory.create(opts);
controllerUtils.setupUxHandlers(w); controllerUtils.setupUxHandlers(w);
w.netStart();
}; };
}); });

View file

@ -17,12 +17,11 @@ angular.module('copay.signin').controller('SigninController',
$location.path('setup'); $location.path('setup');
}; };
$scope.open = function(walletId) { $scope.open = function(walletId, opts) {
$scope.loading = true; $scope.loading = true;
var w = walletFactory.open(walletId); var w = walletFactory.open(walletId, opts);
controllerUtils.setupUxHandlers(w); controllerUtils.setupUxHandlers(w);
w.netStart();
}; };
$scope.join = function(cid) { $scope.join = function(cid) {
@ -31,9 +30,8 @@ angular.module('copay.signin').controller('SigninController',
controllerUtils.onError($scope); controllerUtils.onError($scope);
$rootScope.$digest(); $rootScope.$digest();
}); });
walletFactory.connectTo(cid, function(w) { walletFactory.connectTo(cid, function(data) {
controllerUtils.setupUxHandlers(w); $scope.open(data.walletId, data.opts);
w.netStart();
}); });
}; };

View file

@ -16,13 +16,17 @@ function PrivateKey(opts) {
var init = opts.extendedPrivateKeyString || this.network.name; var init = opts.extendedPrivateKeyString || this.network.name;
this.bip = opts.BIP32 || new BIP32(init); this.bip = opts.BIP32 || new BIP32(init);
this.privateKeyCache = opts.privateKeyCache || {}; this.privateKeyCache = opts.privateKeyCache || {};
this._calcId();
}; };
PrivateKey.prototype._calcId = function() { PrivateKey.prototype.getId = function(prefix) {
this.id = util.ripe160(this.bip.extendedPublicKey).toString('hex'); var buf = this.bip.extendedPublicKey;
if (prefix) {
buf = Buffer.concat([prefix, buf]);
}
return util.ripe160(buf).toString('hex');
}; };
PrivateKey.fromObj = function(obj) { PrivateKey.fromObj = function(obj) {
return new PrivateKey(obj); return new PrivateKey(obj);
}; };

View file

@ -9,6 +9,7 @@ var Address = bitcore.Address;
var Script = bitcore.Script; var Script = bitcore.Script;
var coinUtil = bitcore.util; var coinUtil = bitcore.util;
var Transaction = bitcore.Transaction; var Transaction = bitcore.Transaction;
var util = bitcore.util;
var Storage = imports.Storage || require('../storage/Base.js'); var Storage = imports.Storage || require('../storage/Base.js');
var storage = Storage.default(); var storage = Storage.default();
@ -76,6 +77,14 @@ PublicKeyRing.prototype.serialize = function () {
return JSON.stringify(this.toObj()); return JSON.stringify(this.toObj());
}; };
PublicKeyRing.prototype.getCopayerId = function(i, prefix) {
var buf = this.copayersBIP32[i].extendedPublicKey;
if (prefix) {
buf = Buffer.concat([prefix, buf]);
}
return util.ripe160(buf).toString('hex');
}
PublicKeyRing.prototype.registeredCopayers = function () { PublicKeyRing.prototype.registeredCopayers = function () {
return this.copayersBIP32.length; return this.copayersBIP32.length;
@ -225,12 +234,12 @@ PublicKeyRing.prototype._checkInPRK = function(inPKR, ignoreId) {
if ( if (
this.requiredCopayers && inPKR.requiredCopayers && this.requiredCopayers && inPKR.requiredCopayers &&
(this.requiredCopayers !== inPKR.requiredCopayers)) (this.requiredCopayers !== inPKR.requiredCopayers))
throw new Error('inPRK requiredCopayers mismatch'); throw new Error('inPRK requiredCopayers mismatch '+this.requiredCopayers+'!='+inPKR.requiredCopayers);
if ( if (
this.totalCopayers && inPKR.totalCopayers && this.totalCopayers && inPKR.totalCopayers &&
(this.totalCopayers !== inPKR.totalCopayers)) (this.totalCopayers !== inPKR.totalCopayers))
throw new Error('inPRK requiredCopayers mismatch'); throw new Error('inPRK totalCopayers mismatch'+this.totalCopayers+'!='+inPKR.requiredCopayers);
}; };

View file

@ -48,7 +48,9 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) {
var shouldSend = false; var shouldSend = false;
var recipients, pkr = this.publicKeyRing; var recipients, pkr = this.publicKeyRing;
var inPKR = copay.PublicKeyRing.fromObj(data.publicKeyRing); var inPKR = copay.PublicKeyRing.fromObj(data.publicKeyRing);
if (pkr.merge(inPKR, true) && !data.isBroadcast) {
var hasChanged = pkr.merge(inPKR, true);
if (hasChanged && !data.isBroadcast) {
this.log('### BROADCASTING PKR'); this.log('### BROADCASTING PKR');
recipients = null; recipients = null;
shouldSend = true; shouldSend = true;
@ -133,6 +135,19 @@ Wallet.prototype._optsToObj = function () {
return obj; return obj;
}; };
Wallet.prototype.generatePeerId = function(index) {
var idBuf = new Buffer(this.id);
if (typeof index === 'undefined') {
// return my own peerId
var gen = this.privateKey.getId(idBuf);
return gen;
}
// return peer number 'index' peerId
return this.publicKeyRing.getCopayerId(index, idBuf);
};
Wallet.prototype.netStart = function() { Wallet.prototype.netStart = function() {
var self = this; var self = this;
var net = this.network; var net = this.network;
@ -141,15 +156,25 @@ Wallet.prototype.netStart = function() {
net.on('data', self._handleData.bind(self) ); net.on('data', self._handleData.bind(self) );
net.on('open', function() {}); // TODO net.on('open', function() {}); // TODO
net.on('openError', function() { net.on('openError', function() {
this.log('[Wallet.js.132:openError:] GOT openError'); //TODO self.log('[Wallet.js.132:openError:] GOT openError'); //TODO
self.emit('openError'); self.emit('openError');
}); });
net.on('close', function() { net.on('close', function() {
self.emit('close'); self.emit('close');
}); });
var startOpts = {
peerId: self.generatePeerId()
}
net.start(function(peerId) { net.start(function(peerId) {
self.emit('created'); self.emit('created');
}); var myId = self.generatePeerId();
for (var i=0; i<self.publicKeyRing.registeredCopayers(); i++) {
var otherPeerId = self.generatePeerId(i);
if (otherPeerId !== myId) {
net.connectTo(otherPeerId);
}
}
}, startOpts);
}; };
Wallet.prototype.store = function(isSync) { Wallet.prototype.store = function(isSync) {
@ -207,6 +232,7 @@ Wallet.prototype.sendWalletId = function(recipients) {
this.network.send(recipients, { this.network.send(recipients, {
type: 'walletId', type: 'walletId',
walletId: this.id, walletId: this.id,
opts: this._optsToObj()
}); });
}; };
@ -236,7 +262,7 @@ Wallet.prototype.getTxProposals = function() {
self.txProposals.txps.forEach(function(txp) { self.txProposals.txps.forEach(function(txp) {
var i = {txp:txp}; var i = {txp:txp};
i.ntxid = txp.builder.build().getNormalizedHash(); i.ntxid = txp.builder.build().getNormalizedHash();
i.signedByUs = txp.signedBy[self.privateKey.id]?true:false; i.signedByUs = txp.signedBy[self.privateKey.getId()]?true:false;
ret.push(i); ret.push(i);
}); });
return ret; return ret;
@ -267,7 +293,7 @@ Wallet.prototype.sign = function(ntxid) {
var ret = txp.builder.sign(keys); var ret = txp.builder.sign(keys);
if (ret.signaturesAdded) { if (ret.signaturesAdded) {
txp.signedBy[this.privateKey.id] = Date.now(); txp.signedBy[this.privateKey.getId()] = Date.now();
this.log('[Wallet.js.230:ret:]',ret); //TODO this.log('[Wallet.js.230:ret:]',ret); //TODO
if (ret.isFullySigned) { if (ret.isFullySigned) {
this.log('[Wallet.js.231] BROADCASTING TX!!!'); //TODO this.log('[Wallet.js.231] BROADCASTING TX!!!'); //TODO
@ -294,8 +320,8 @@ Wallet.prototype.addSeenToTxProposals = function() {
var self=this; var self=this;
this.txProposals.txps.forEach(function(txp) { this.txProposals.txps.forEach(function(txp) {
if (!txp.seenBy[self.privateKey.id]) { if (!txp.seenBy[self.privateKey.getId()]) {
txp.seenBy[self.privateKey.id] = Date.now(); txp.seenBy[self.privateKey.getId()] = Date.now();
ret = true; ret = true;
} }
}); });

View file

@ -115,48 +115,12 @@ WalletFactory.prototype.create = function(opts) {
return w; return w;
}; };
WalletFactory.prototype.open = function(walletId) { WalletFactory.prototype.open = function(walletId, opts) {
var w = this.read(walletId) || this.create({ this.log('Opening walletId:' + walletId);
id: walletId, opts = opts || {};
verbose: this.verbose, opts.id = walletId;
}); opts.verbose = this.verbose;
return w; var w = this.read(walletId) || this.create(opts);
};
WalletFactory.prototype.openRemote = function(peedId) {
var s = WalletFactory.storage;
opts = opts || {};
this.log('### CREATING NEW WALLET.' + (opts.id ? ' USING ID: ' + opts.id : ' NEW ID'));
opts.privateKey = opts.privateKey || new PrivateKey({ networkName: this.networkName });
this.log('\t### PrivateKey Initialized');
var requiredCopayers = opts.requiredCopayers || this.walletDefaults.requiredCopayers;
var totalCopayers = opts.totalCopayers || this.walletDefaults.totalCopayers;
opts.publicKeyRing = opts.publicKeyRing || new PublicKeyRing({
networkName: this.networkName,
requiredCopayers: requiredCopayers,
totalCopayers: totalCopayers,
});
opts.publicKeyRing.addCopayer(opts.privateKey.getExtendedPublicKeyString());
this.log('\t### PublicKeyRing Initialized');
opts.txProposals = opts.txProposals || new TxProposals({
networkName: this.networkName,
});
this.log('\t### TxProposals Initialized');
opts.storage = this.storage;
opts.network = this.network;
opts.blockchain = this.blockchain;
opts.spendUnconfirmed = typeof opts.spendUnconfirmed === undefined
?this.walletDefaults.spendUnconfirmed : opts.spendUnconfirmed;
opts.requiredCopayers = requiredCopayers;
opts.totalCopayers = totalCopayers;
var w = new Wallet(opts);
w.store(); w.store();
return w; return w;
}; };
@ -174,9 +138,8 @@ WalletFactory.prototype.connectTo = function(peerId, cb) {
var self=this; var self=this;
self.network.start(function() { self.network.start(function() {
self.network.connectTo(peerId) self.network.connectTo(peerId)
self.network.on('walletId', function(walletId) { self.network.on('walletId', function(data) {
self.log('Opening walletId:' + walletId); return cb(data);
return cb(self.open(walletId));
}); });
}); });
}; };

View file

@ -1,8 +0,0 @@
function Peer(id) {
this.id = id;
};
module.exports = require('soop')(Peer);

View file

@ -21,7 +21,7 @@ function Network(opts) {
this.peerId = opts.peerId; this.peerId = opts.peerId;
this.apiKey = opts.apiKey || 'lwjd5qra8257b9'; this.apiKey = opts.apiKey || 'lwjd5qra8257b9';
this.debug = opts.debug || 3; this.debug = opts.debug || 3;
this.maxPeers = opts.maxPeers || 5; this.maxPeers = opts.maxPeers || 10;
this.opts = { key: opts.key }; this.opts = { key: opts.key };
// For using your own peerJs server // For using your own peerJs server
@ -29,6 +29,7 @@ function Network(opts) {
if (opts[k]) self.opts[k]=opts[k]; if (opts[k]) self.opts[k]=opts[k];
}); });
this.connectedPeers = []; this.connectedPeers = [];
this.started = false;
} }
Network.parent=EventEmitter; Network.parent=EventEmitter;
@ -64,7 +65,6 @@ Network._arrayPushOnce = function(el, array) {
Network._arrayRemove = function(el, array) { Network._arrayRemove = function(el, array) {
var pos = array.indexOf(el); var pos = array.indexOf(el);
if (pos >= 0) array.splice(pos, 1); if (pos >= 0) array.splice(pos, 1);
return array; return array;
}; };
@ -105,7 +105,7 @@ Network.prototype._onData = function(data, isInbound) {
this._onClose(obj.sender); this._onClose(obj.sender);
break; break;
case 'walletId': case 'walletId':
this.emit('walletId', obj.data.walletId); this.emit('walletId', obj.data);
break; break;
default: default:
this.emit('data', obj.sender, obj.data, isInbound); this.emit('data', obj.sender, obj.data, isInbound);
@ -123,8 +123,6 @@ Network.prototype._sendPeers = function(peerIds) {
Network.prototype._addPeer = function(peerId, isInbound) { Network.prototype._addPeer = function(peerId, isInbound) {
var hasChanged = Network._arrayPushOnce(peerId, this.connectedPeers); var hasChanged = Network._arrayPushOnce(peerId, this.connectedPeers);
if (isInbound && hasChanged) { if (isInbound && hasChanged) {
this._sendPeers(); //broadcast peer list this._sendPeers(); //broadcast peer list
} }
@ -187,15 +185,12 @@ Network.prototype._setupPeerHandlers = function(openCallback) {
p.on('open', function(peerId) { p.on('open', function(peerId) {
self.peerId = peerId; self.peerId = peerId;
self.connectedPeers = [peerId]; self.connectedPeers = [peerId];
self._notifyNetworkChange();
return openCallback(peerId); return openCallback(peerId);
}); });
p.on('error', function(err) { p.on('error', function(err) {
console.log('### PEER ERROR:', err); console.log('### PEER ERROR:', err);
self.peer.disconnect(); //self.disconnect(null, true); // force disconnect
self.peer.destroy();
self.peer = null;
self._checkAnyPeer(); self._checkAnyPeer();
}); });
@ -215,12 +210,33 @@ Network.prototype._setupPeerHandlers = function(openCallback) {
}); });
}; };
Network.prototype.start = function(openCallback) { Network.prototype.start = function(openCallback, opts) {
opts = opts || {};
// Start PeerJS Peer // Start PeerJS Peer
if (this.peer) return openCallback(); // This is for connectTo-> peer is started before var self = this;
if (this.started) {
// network already started, restarting network layer
opts.connectedPeers = this.connectedPeers;
Network._arrayRemove(this.peerId, opts.connectedPeers);
this.disconnect(function() {
self.start(openCallback, opts);
}, true); // fast disconnect
return;
}
opts = opts || {};
opts.connectedPeers = opts.connectedPeers || [];
this.peerId = this.peerId || opts.peerId;
this.peer = new Peer(this.peerId, this.opts); this.peer = new Peer(this.peerId, this.opts);
this._setupPeerHandlers(openCallback); this._setupPeerHandlers(openCallback);
for (var i = 0; i<opts.connectedPeers.length; i++) {
var otherPeerId = opts.connectedPeers[i];
this.connectTo(otherPeerId);
}
this.started = true;
}; };
Network.prototype._sendToOne = function(peerId, data, cb) { Network.prototype._sendToOne = function(peerId, data, cb) {
@ -279,11 +295,12 @@ Network.prototype.connectTo = function(peerId) {
}; };
Network.prototype.disconnect = function(cb) { Network.prototype.disconnect = function(cb, forced) {
var self = this; var self = this;
self.closing = 1; self.closing = 1;
this.send(null, { type: 'disconnect' }, function() { var cleanUp = function() {
self.connectedPeers = []; self.connectedPeers = [];
self.started = false;
self.peerId = null; self.peerId = null;
if (self.peer) { if (self.peer) {
self.peer.disconnect(); self.peer.disconnect();
@ -292,7 +309,12 @@ Network.prototype.disconnect = function(cb) {
} }
self.closing = 0; self.closing = 0;
if (typeof cb === 'function') cb(); if (typeof cb === 'function') cb();
}); };
if (!forced) {
this.send(null, { type: 'disconnect' }, cleanUp);
} else {
cleanUp();
}
}; };
module.exports = require('soop')(Network); module.exports = require('soop')(Network);

View file

@ -43,6 +43,8 @@ angular.module('copay.controllerUtils').factory('controllerUtils', function ($ro
}); });
w.on('openError', root.onErrorDigest); w.on('openError', root.onErrorDigest);
w.on('close', root.onErrorDigest); w.on('close', root.onErrorDigest);
w.netStart();
}; };
root.handleTransactionByAddress = function(scope, cb) { root.handleTransactionByAddress = function(scope, cb) {

27
test/mocks/FakeNetwork.js Normal file
View file

@ -0,0 +1,27 @@
var imports = require('soop').imports();
var EventEmitter= imports.EventEmitter || require('events').EventEmitter;
function Network(opts) {
}
Network.parent=EventEmitter;
Network.prototype.start = function(openCallback, opts) {
// start! :D
};
Network.prototype.send = function(peerIds, data, cb) {
// send! c:
};
Network.prototype.connectTo = function(peerId) {
// connect C:
};
Network.prototype.disconnect = function(cb) {
// disconect :c
};
module.exports = require('soop')(Network);

View file

@ -69,16 +69,15 @@ describe('PrivateKey model', function() {
it('should calculate .id', function () { it('should calculate .id', function () {
var w1 = new PrivateKey(config); var w1 = new PrivateKey(config);
should.exist(w1.id); should.exist(w1.getId());
w1.id.length.should.equal(40); w1.getId().length.should.equal(40);
}); });
it('fromObj toObj roundtrip', function () { it('fromObj toObj roundtrip', function () {
var w1 = new PrivateKey(config); var w1 = new PrivateKey(config);
var w2 = PrivateKey.fromObj(w1.toObj()); var w2 = PrivateKey.fromObj(w1.toObj());
w2.toObj().extendedPrivateKeyString.should.equal(w1.toObj().extendedPrivateKeyString); w2.toObj().extendedPrivateKeyString.should.equal(w1.toObj().extendedPrivateKeyString);
w2.id.should.equal(w1.id); w2.getId().should.equal(w1.getId());
JSON.stringify(w2.get(1,1).storeObj()).should JSON.stringify(w2.get(1,1).storeObj()).should
.equal(JSON.stringify(w1.get(1,1).storeObj())); .equal(JSON.stringify(w1.get(1,1).storeObj()));

View file

@ -3,12 +3,12 @@
var chai = chai || require('chai'); var chai = chai || require('chai');
var should = chai.should(); var should = chai.should();
var WebRTC = require('../js/models/network/WebRTC'); var FakeNetwork = require('./mocks/FakeNetwork');
var Insight = require('../js/models/blockchain/Insight'); var Insight = require('../js/models/blockchain/Insight');
var FakeStorage = require('./mocks/FakeStorage'); var FakeStorage = require('./mocks/FakeStorage');
var WalletFactory = typeof copay === 'undefined' ? require('soop').load('../js/models/core/WalletFactory',{ var WalletFactory = typeof copay === 'undefined' ? require('soop').load('../js/models/core/WalletFactory',{
Network: WebRTC, Network: FakeNetwork,
Blockchain: Insight, Blockchain: Insight,
Storage: FakeStorage, Storage: FakeStorage,
}) : copay.WalletFactory; }) : copay.WalletFactory;