2014-04-10 13:57:41 -07:00
|
|
|
'use strict';
|
|
|
|
|
2014-04-15 14:23:35 -07:00
|
|
|
var imports = require('soop').imports();
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-04-15 14:23:35 -07:00
|
|
|
var bitcore = require('bitcore');
|
|
|
|
var coinUtil = bitcore.util;
|
2014-04-14 14:30:08 -07:00
|
|
|
var buffertools = bitcore.buffertools;
|
2014-04-15 14:23:35 -07:00
|
|
|
var Builder = bitcore.TransactionBuilder;
|
|
|
|
var http = require('http');
|
2014-04-16 13:50:10 -07:00
|
|
|
var EventEmitter= imports.EventEmitter || require('events').EventEmitter;
|
|
|
|
var copay = copay || require('../../../copay');
|
2014-04-10 13:57:41 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
function Wallet(opts) {
|
|
|
|
var self = this;
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
//required params
|
|
|
|
['storage', 'network', 'blockchain',
|
|
|
|
'requiredCopayers', 'totalCopayers', 'spendUnconfirmed',
|
|
|
|
'publicKeyRing', 'txProposals', 'privateKey'
|
|
|
|
].forEach( function(k){
|
|
|
|
if (typeof opts[k] === 'undefined') throw new Error('missing key:' + k);
|
|
|
|
self[k] = opts[k];
|
|
|
|
});
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-04-16 15:45:22 -07:00
|
|
|
console.log('creating '+opts.requiredCopayers+' of '+opts.totalCopayers+' wallet');
|
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
this.id = opts.id || Wallet.getRandomId();
|
|
|
|
this.publicKeyRing.walletId = this.id;
|
|
|
|
this.txProposals.walletId = this.id;
|
2014-04-14 13:42:10 -07:00
|
|
|
}
|
2014-04-10 13:57:41 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.parent=EventEmitter;
|
2014-04-15 10:28:49 -07:00
|
|
|
Wallet.prototype.log = function(){
|
|
|
|
if (!this.verbose) return;
|
2014-04-16 13:50:10 -07:00
|
|
|
console.log(arguments);
|
|
|
|
};
|
2014-04-15 10:28:49 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.getRandomId = function() {
|
|
|
|
var r = buffertools.toHex(coinUtil.generateNonce());
|
|
|
|
return r;
|
|
|
|
};
|
2014-04-15 10:28:49 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) {
|
|
|
|
this.log('RECV PUBLICKEYRING:',data);
|
|
|
|
|
|
|
|
var shouldSend = false;
|
|
|
|
var recipients, pkr = this.publicKeyRing;
|
|
|
|
var inPKR = copay.PublicKeyRing.fromObj(data.publicKeyRing);
|
|
|
|
if (pkr.merge(inPKR, true) && !data.isBroadcast) {
|
|
|
|
this.log('### BROADCASTING PKR');
|
|
|
|
recipients = null;
|
|
|
|
shouldSend = true;
|
|
|
|
}
|
|
|
|
else if (isInbound && !data.isBroadcast) {
|
|
|
|
// always replying to connecting peer
|
|
|
|
this.log('### REPLYING PKR TO:', senderId);
|
|
|
|
recipients = senderId;
|
|
|
|
shouldSend = true;
|
|
|
|
}
|
2014-04-15 08:52:28 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
if (shouldSend) {
|
|
|
|
this.sendPublicKeyRing(recipients);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.store();
|
2014-04-14 13:42:10 -07:00
|
|
|
};
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-04-14 14:30:08 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.prototype._handleTxProposals = function(senderId, data, isInbound) {
|
|
|
|
this.log('RECV TXPROPOSAL:',data); //TODO
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
var shouldSend = false;
|
|
|
|
var recipients;
|
|
|
|
var inTxp = copay.TxProposals.fromObj(data.txProposals);
|
|
|
|
var mergeInfo = this.txProposals.merge(inTxp, true);
|
2014-04-14 13:42:10 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
var addSeen = this.addSeenToTxProposals();
|
|
|
|
if ((mergeInfo.merged && !data.isBroadcast) || addSeen) {
|
|
|
|
this.log('### BROADCASTING txProposals. ' );
|
|
|
|
recipients = null;
|
|
|
|
shouldSend = true;
|
|
|
|
}
|
|
|
|
else if (isInbound && !data.isBroadcast) {
|
|
|
|
// always replying to connecting peer
|
|
|
|
this.log('### REPLYING txProposals TO:', senderId);
|
|
|
|
recipients = senderId;
|
|
|
|
shouldSend = true;
|
|
|
|
}
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
if (shouldSend)
|
|
|
|
this.sendTxProposals(recipients);
|
|
|
|
|
|
|
|
this.store();
|
2014-04-14 13:42:10 -07:00
|
|
|
};
|
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.prototype._handleData = function(senderId, data, isInbound) {
|
2014-04-16 16:58:57 -07:00
|
|
|
|
|
|
|
if (this.id !== data.walletId)
|
|
|
|
throw new Error('wrong message received: Bad wallet ID');
|
|
|
|
|
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
switch(data.type) {
|
|
|
|
case 'publicKeyRing':
|
|
|
|
this._handlePublicKeyRing(senderId, data, isInbound);
|
|
|
|
break;
|
|
|
|
case 'txProposals':
|
|
|
|
this._handleTxProposals(senderId, data, isInbound);
|
|
|
|
break;
|
|
|
|
case 'abort':
|
|
|
|
this.emit('abort');
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
2014-04-14 13:42:10 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.prototype._handleNetworkChange = function(newPeer) {
|
2014-04-16 16:58:57 -07:00
|
|
|
|
|
|
|
console.log('[Wallet.js.112:newPeer:]',newPeer); //TODO
|
2014-04-16 13:50:10 -07:00
|
|
|
if (!newPeer) return;
|
2014-04-15 08:17:28 -07:00
|
|
|
|
2014-04-16 16:58:57 -07:00
|
|
|
console.log('[Wallet.js.112:newPeer:]',newPeer); //TODO
|
2014-04-16 13:50:10 -07:00
|
|
|
this.log('#### Setting new PEER:', newPeer);
|
2014-04-16 16:58:57 -07:00
|
|
|
this.sendWalletId(newPeer);
|
2014-04-16 13:50:10 -07:00
|
|
|
this.sendPublicKeyRing(newPeer);
|
|
|
|
this.sendTxProposals(newPeer);
|
2014-04-14 13:42:10 -07:00
|
|
|
};
|
|
|
|
|
2014-04-16 15:18:19 -07:00
|
|
|
Wallet.prototype.netStart = function() {
|
2014-04-16 13:50:10 -07:00
|
|
|
var self = this;
|
|
|
|
var net = this.network;
|
2014-04-16 16:58:57 -07:00
|
|
|
net.on('networkChange', self._handleNetworkChange.bind(self) );
|
|
|
|
net.on('data', self._handleData.bind(self) );
|
2014-04-16 13:50:10 -07:00
|
|
|
net.on('open', function() {}); // TODO
|
|
|
|
net.on('close', function() {}); // TODO
|
|
|
|
net.start(function(peerId) {
|
|
|
|
self.emit('created');
|
|
|
|
});
|
|
|
|
};
|
2014-04-15 06:22:50 -07:00
|
|
|
|
|
|
|
Wallet.prototype.store = function() {
|
2014-04-16 13:50:10 -07:00
|
|
|
this.storage.set(this.id,'opts', {
|
|
|
|
id: this.id,
|
|
|
|
spendUnconfirmed: this.spendUnconfirmed,
|
|
|
|
requiredCopayers: this.requiredCopayers,
|
|
|
|
totalCopayers: this.totalCopayers,
|
|
|
|
});
|
2014-04-15 06:22:50 -07:00
|
|
|
this.storage.set(this.id,'publicKeyRing', this.publicKeyRing.toObj());
|
|
|
|
this.storage.set(this.id,'txProposals', this.txProposals.toObj());
|
|
|
|
this.storage.set(this.id,'privateKey', this.privateKey.toObj());
|
|
|
|
};
|
|
|
|
|
2014-04-15 08:17:28 -07:00
|
|
|
|
|
|
|
Wallet.prototype.sendTxProposals = function(recipients) {
|
2014-04-15 10:28:49 -07:00
|
|
|
this.log('### SENDING txProposals TO:', recipients||'All', this.txProposals);
|
2014-04-15 08:17:28 -07:00
|
|
|
|
|
|
|
this.network.send( recipients, {
|
|
|
|
type: 'txProposals',
|
|
|
|
txProposals: this.txProposals.toObj(),
|
|
|
|
walletId: this.id,
|
|
|
|
});
|
2014-04-16 13:50:10 -07:00
|
|
|
this.emit('txProposalsUpdated', this.txProposals);
|
2014-04-15 08:17:28 -07:00
|
|
|
};
|
|
|
|
|
2014-04-16 16:58:57 -07:00
|
|
|
|
|
|
|
Wallet.prototype.sendWalletId = function(recipients) {
|
|
|
|
this.log('### SENDING walletId TO:', recipients||'All', this.walletId);
|
|
|
|
|
|
|
|
this.network.send(recipients, {
|
|
|
|
type: 'walletId',
|
|
|
|
walletId: this.id,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-04-15 08:17:28 -07:00
|
|
|
Wallet.prototype.sendPublicKeyRing = function(recipients) {
|
2014-04-15 10:28:49 -07:00
|
|
|
this.log('### SENDING publicKeyRing TO:', recipients||'All', this.publicKeyRing.toObj());
|
2014-04-15 08:17:28 -07:00
|
|
|
|
|
|
|
this.network.send(recipients, {
|
|
|
|
type: 'publicKeyRing',
|
|
|
|
publicKeyRing: this.publicKeyRing.toObj(),
|
|
|
|
walletId: this.id,
|
|
|
|
});
|
2014-04-16 13:50:10 -07:00
|
|
|
this.emit('publicKeyRingUpdated', this.publicKeyRing);
|
2014-04-15 08:17:28 -07:00
|
|
|
};
|
|
|
|
|
2014-04-15 12:50:16 -07:00
|
|
|
Wallet.prototype.generateAddress = function() {
|
|
|
|
var addr = this.publicKeyRing.generateAddress();
|
|
|
|
this.store();
|
2014-04-16 13:50:10 -07:00
|
|
|
this.sendPublicKeyRing();
|
2014-04-15 12:50:16 -07:00
|
|
|
return addr;
|
|
|
|
};
|
2014-04-15 08:17:28 -07:00
|
|
|
|
2014-04-15 11:25:55 -07:00
|
|
|
Wallet.prototype.getTxProposals = function() {
|
|
|
|
var ret = [];
|
2014-04-16 13:50:10 -07:00
|
|
|
var self= this;
|
|
|
|
self.txProposals.txps.forEach(function(txp) {
|
2014-04-15 11:25:55 -07:00
|
|
|
var i = {txp:txp};
|
2014-04-16 13:50:10 -07:00
|
|
|
i.ntxid = txp.builder.build().getNormalizedHash();
|
|
|
|
i.signedByUs = txp.signedBy[self.privateKey.id]?true:false;
|
2014-04-15 11:25:55 -07:00
|
|
|
ret.push(i);
|
|
|
|
});
|
2014-04-16 13:50:10 -07:00
|
|
|
return ret;
|
|
|
|
};
|
2014-04-15 11:25:55 -07:00
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
// TODO: this can be precalculated.
|
|
|
|
Wallet.prototype._findTxByNtxid = function(ntxid) {
|
|
|
|
var ret;
|
|
|
|
var l = this.txProposals.txps.length;
|
|
|
|
var id = ntxid.toString('hex');
|
|
|
|
for(var i=0; i<l; i++) {
|
|
|
|
var txp = this.txProposals.txps[i];
|
|
|
|
var id2 = txp.builder.build().getNormalizedHash().toString('hex');
|
|
|
|
if (id === id2 ) {
|
|
|
|
ret = txp;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Wallet.prototype.sign = function(ntxid) {
|
|
|
|
var txp = this._findTxByNtxid(ntxid);
|
|
|
|
if (!txp) return;
|
|
|
|
|
|
|
|
var pkr = this.publicKeyRing;
|
|
|
|
var keys = this.privateKey.getAll(pkr.addressIndex, pkr.changeAddressIndex);
|
|
|
|
var ret = txp.builder.sign(keys);
|
|
|
|
|
|
|
|
if (ret.signaturesAdded) {
|
|
|
|
txp.signedBy[this.privateKey.id] = Date.now();
|
|
|
|
w.store();
|
|
|
|
this.sendTxProposals();
|
|
|
|
}
|
2014-04-15 11:25:55 -07:00
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
Wallet.prototype.addSeenToTxProposals = function() {
|
|
|
|
var ret=false;
|
2014-04-16 13:50:10 -07:00
|
|
|
var self=this;
|
|
|
|
|
2014-04-15 11:25:55 -07:00
|
|
|
this.txProposals.txps.forEach(function(txp) {
|
2014-04-16 13:50:10 -07:00
|
|
|
if (!txp.seenBy[self.privateKey.id]) {
|
|
|
|
txp.seenBy[self.privateKey.id] = Date.now();
|
2014-04-15 11:25:55 -07:00
|
|
|
ret = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
2014-04-15 11:50:22 -07:00
|
|
|
|
|
|
|
Wallet.prototype.getAddresses = function() {
|
|
|
|
return this.publicKeyRing.getAddresses();
|
|
|
|
};
|
|
|
|
|
2014-04-15 14:23:35 -07:00
|
|
|
Wallet.prototype.getAddressesStr = function() {
|
|
|
|
var ret = [];
|
|
|
|
this.publicKeyRing.getAddresses().forEach(function(a) {
|
|
|
|
ret.push(a.toString());
|
|
|
|
});
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-04-15 11:50:22 -07:00
|
|
|
Wallet.prototype.listUnspent = function(cb) {
|
2014-04-15 14:23:35 -07:00
|
|
|
this.blockchain.listUnspent(this.getAddressesStr(), cb);
|
|
|
|
};
|
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
|
|
|
|
Wallet.prototype.createTx = function(toAddress, amountSatStr, opts, cb) {
|
|
|
|
var self = this;
|
|
|
|
if (typeof opts === 'function') {
|
|
|
|
cb = opts;
|
|
|
|
opts = {};
|
|
|
|
}
|
|
|
|
opts = opts || {};
|
|
|
|
|
|
|
|
if (typeof opts.spendUnconfirmed === 'undefined') {
|
|
|
|
opts.spendUnconfirmed = this.spendUnconfirmed;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!opts.remainderOut) {
|
|
|
|
opts.remainderOut={ address: this.publicKeyRing.generateAddress(true).toString()};
|
|
|
|
}
|
|
|
|
|
|
|
|
self.listUnspent(function(utxos) {
|
|
|
|
// TODO check enough funds, etc.
|
|
|
|
self.createTxSync(toAddress, amountSatStr, utxos, opts);
|
|
|
|
self.store();
|
|
|
|
self.sendTxProposals();
|
|
|
|
return cb();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Wallet.prototype.createTxSync = function(toAddress, amountSatStr, utxos, opts) {
|
2014-04-15 14:23:35 -07:00
|
|
|
var pkr = this.publicKeyRing;
|
|
|
|
var priv = this.privateKey;
|
|
|
|
opts = opts || {};
|
|
|
|
|
|
|
|
var amountSat = bitcore.bignum(amountSatStr);
|
|
|
|
|
|
|
|
if (! pkr.isComplete() ) {
|
|
|
|
throw new Error('publicKeyRing is not complete');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!opts.remainderOut) {
|
|
|
|
opts.remainderOut ={ address: pkr.generateAddress(true).toString() };
|
|
|
|
};
|
|
|
|
|
|
|
|
var b = new Builder(opts)
|
|
|
|
.setUnspent(utxos)
|
|
|
|
.setHashToScriptMap(pkr.getRedeemScriptMap())
|
|
|
|
.setOutputs([{address: toAddress, amountSat: amountSat}])
|
|
|
|
;
|
|
|
|
|
|
|
|
var signRet;
|
|
|
|
if (priv) {
|
|
|
|
b.sign( priv.getAll(pkr.addressIndex, pkr.changeAddressIndex) );
|
|
|
|
}
|
|
|
|
var me = {};
|
|
|
|
if (priv) me[priv.id] = Date.now();
|
|
|
|
|
|
|
|
this.txProposals.add({
|
|
|
|
signedBy: priv && b.signaturesAdded ? me : {},
|
|
|
|
seenBy: priv ? me : {},
|
|
|
|
builder: b,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-04-16 13:50:10 -07:00
|
|
|
Wallet.prototype.connectTo = function(peerId) {
|
2014-04-16 16:58:57 -07:00
|
|
|
throw new Error('Wallet.connectTo.. not yet implemented!');
|
2014-04-15 11:50:22 -07:00
|
|
|
};
|
2014-04-16 16:58:57 -07:00
|
|
|
|
|
|
|
Wallet.prototype.disconnect = function() {
|
|
|
|
this.network.disconnect();
|
|
|
|
};
|
|
|
|
|
2014-04-15 08:17:28 -07:00
|
|
|
// // HERE? not sure
|
|
|
|
// Wallet.prototype.cleanPeers = function() {
|
|
|
|
// this.storage.remove('peerData');
|
|
|
|
// };
|
|
|
|
//
|
2014-04-16 13:50:10 -07:00
|
|
|
;
|
2014-04-14 14:30:08 -07:00
|
|
|
|
|
|
|
module.exports = require('soop')(Wallet);
|