diff --git a/copay.js b/copay.js index 8e8b03089..3a816a42c 100644 --- a/copay.js +++ b/copay.js @@ -1,6 +1,7 @@ // core module.exports.PublicKeyRing = require('./js/models/core/PublicKeyRing'); -module.exports.TxProposals = require('./js/models/core/TxProposals'); +module.exports.TxProposal = require('./js/models/core/TxProposal'); +module.exports.TxProposalsSet = require('./js/models/core/TxProposalsSet'); module.exports.PrivateKey = require('./js/models/core/PrivateKey'); module.exports.Passphrase = require('./js/models/core/Passphrase'); module.exports.HDPath = require('./js/models/core/HDPath'); diff --git a/js/models/core/HDPath.js b/js/models/core/HDPath.js index 2e32ade7e..126b84a0a 100644 --- a/js/models/core/HDPath.js +++ b/js/models/core/HDPath.js @@ -33,12 +33,12 @@ HDPath.FullBranch = function(addressIndex, isChange, copayerIndex) { return BIP45_PUBLIC_PREFIX + '/' + sub; }; -HDPath.indicesForPath = function(path) { +HDPath.indexesForPath = function(path) { preconditions.shouldBeString(path); var s = path.split('/'); return { isChange: s[3] === '1', - index: parseInt(s[4]), + addressIndex: parseInt(s[4]), copayerIndex: parseInt(s[2]) }; }; diff --git a/js/models/core/TxProposalsSet.js b/js/models/core/TxProposalsSet.js new file mode 100644 index 000000000..22d444303 --- /dev/null +++ b/js/models/core/TxProposalsSet.js @@ -0,0 +1,160 @@ +'use strict'; + +var BuilderMockV0 = require('./BuilderMockV0');; +var bitcore = require('bitcore'); +var util = bitcore.util; +var Transaction = bitcore.Transaction; +var BuilderMockV0 = require('./BuilderMockV0');; +var Script = bitcore.Script; +var Key = bitcore.Key; +var buffertools = bitcore.buffertools; +var preconditions = require('preconditions').instance(); + + +function TxProposalsSet(opts) { + opts = opts || {}; + this.walletId = opts.walletId; + this.network = opts.networkName === 'livenet' ? + bitcore.networks.livenet : bitcore.networks.testnet; + this.txps = {}; +} + +TxProposalsSet.fromObj = function(o, forceOpts) { + var ret = new TxProposalsSet({ + networkName: o.networkName, + walletId: o.walletId, + }); + + o.txps.forEach(function(o2) { + var t = TxProposal.fromObj(o2, forceOpts); + if (t.builder) { + var id = t.getID(); + ret.txps[id] = t; + } + }); + return ret; +}; + +TxProposalsSet.prototype.getNtxids = function() { + return Object.keys(this.txps); +}; + +TxProposalsSet.prototype.toObj = function(onlyThisNtxid) { + if (onlyThisNtxid) throw new Error(); + var ret = []; + for (var id in this.txps) { + + if (onlyThisNtxid && id != onlyThisNtxid) + continue; + + var t = this.txps[id]; + if (!t.sent) + ret.push(t.toObj()); + } + return { + txps: ret, + walletId: this.walletId, + networkName: this.network.name, + }; +}; + + +TxProposalsSet.prototype.mergeFromObj = function(txProposalObj, allowedPubKeys, opts) { + var inTxp = TxProposal.fromObj(txProposalObj, opts); + var mergeInfo = this.txProposals.merge(inTxp, allowedPubKeys); + mergeInfo.inTxp = inTxp; + return mergeInfo; +}; + + +TxProposalsSet.prototype.merge = function(inTxp, allowedPubKeys) { + var myTxps = this.txps; + + var ntxid = inTxp.getID(); + var ret = {}; + ret.events = []; + ret.events.hasChanged = false; + + if (myTxps[ntxid]) { + var v0 = myTxps[ntxid]; + var v1 = inTxp; + ret = v0.merge(v1, allowedPubKeys); + } else { + this.txps[ntxid] = inTxp; + ret.hasChanged = true; + ret.events.push({ + type: 'new', + cid: inTxp.creator, + tx: ntxid + }); + } + return ret; +}; + +// Add a LOCALLY CREATED (trusted) tx proposal +TxProposalsSet.prototype.add = function(data) { + var txp = new TxProposal(data); + var ntxid = txp.getID(); + this.txps[ntxid] = txp; + return ntxid; +}; + +TxProposalsSet.prototype.setSent = function(ntxid, txid) { + //sent TxProposalsSet are local an not broadcasted. + this.txps[ntxid].setSent(txid); +}; + + +TxProposalsSet.prototype.getTxProposal = function(ntxid, copayers) { + var txp = this.txps[ntxid]; + var i = JSON.parse(JSON.stringify(txp)); + i.builder = txp.builder; + i.ntxid = ntxid; + i.peerActions = {}; + + if (copayers) { + for (var j = 0; j < copayers.length; j++) { + var p = copayers[j]; + i.peerActions[p] = {}; + } + } + + for (var p in txp.seenBy) { + i.peerActions[p] = { + seen: txp.seenBy[p] + }; + } + for (var p in txp.signedBy) { + i.peerActions[p] = i.peerActions[p] || {}; + i.peerActions[p].sign = txp.signedBy[p]; + } + var r = 0; + for (var p in txp.rejectedBy) { + i.peerActions[p] = i.peerActions[p] || {}; + i.peerActions[p].rejected = txp.rejectedBy[p]; + r++; + } + i.rejectCount = r; + + var c = txp.creator; + i.peerActions[c] = i.peerActions[c] || {}; + i.peerActions[c].create = txp.createdTs; + return i; +}; + +//returns the unspent txid-vout used in PENDING Txs +TxProposalsSet.prototype.getUsedUnspent = function(maxRejectCount) { + var ret = {}; + for (var i in this.txps) { + var u = this.txps[i].builder.getSelectedUnspent(); + var p = this.getTxProposal(i); + if (p.rejectCount > maxRejectCount || p.sentTxid) + continue; + + for (var j in u) { + ret[u[j].txid + ',' + u[j].vout] = 1; + } + } + return ret; +}; + diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index c7ae6403d..856e5cb43 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -17,7 +17,7 @@ var Address = bitcore.Address; var HDParams = require('./HDParams'); var PublicKeyRing = require('./PublicKeyRing'); -var TxProposals = require('./TxProposals'); +var TxProposalsSet = require('./TxProposalsSet'); var PrivateKey = require('./PrivateKey'); var copayConfig = require('../../../config'); @@ -36,7 +36,7 @@ function Wallet(opts) { }); if (copayConfig.forceNetwork && this.getNetworkName() !== copayConfig.networkName) throw new Error('Network forced to ' + copayConfig.networkName + - ' and tried to create a Wallet with network ' + this.getNetworkName()); + ' and tried to create a Wallet with network ' + this.getNetworkName()); this.log('creating ' + opts.requiredCopayers + ' of ' + opts.totalCopayers + ' wallet'); @@ -59,10 +59,10 @@ function Wallet(opts) { Wallet.builderOpts = { - lockTime: null, - signhash: bitcore.Transaction.SIGNHASH_ALL, - fee: null, - feeSat: null, + lockTime: null, + signhash: bitcore.Transaction.SIGNHASH_ALL, + fee: null, + feeSat: null, }; Wallet.parent = EventEmitter; @@ -129,28 +129,25 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) { }; - Wallet.prototype._handleTxProposal = function(senderId, data) { this.log('RECV TXPROPOSAL: ', data); - var inTxp = TxProposals.TxProposal.fromObj(data.txProposal, Wallet.builderOpts); + var mergeInfo; - - - var valid = inTxp.isValid(); - if (!valid) { + try { + mergeInfo = this.txProposals.mergeFromObj(data.txProposal, senderId, Wallet.builderOpts); + } catch (e) { var corruptEvent = { type: 'corrupt', - cId: inTxp.creator + cId: mergeInfo.inTxp.creator }; this.emit('txProposalEvent', corruptEvent); return; } - var mergeInfo = this.txProposals.merge(inTxp, senderId); - var added = this.addSeenToTxProposals(); + var added = this.addSeenToTxProposals(); if (added) { this.log('### BROADCASTING txProposals with my seenBy updated.'); - this.sendTxProposal(inTxp.getID()); + this.sendTxProposal(mergeInfo.inTxp.getID()); } this.emit('txProposalsUpdated'); @@ -193,24 +190,24 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) { // This handler is repeaded on WalletFactory (#join). TODO case 'walletId': this.sendWalletReady(senderId); - break; + break; case 'walletReady': this.sendPublicKeyRing(senderId); - this.sendAddressBook(senderId); - this.sendAllTxProposals(senderId); // send old txps - break; + this.sendAddressBook(senderId); + this.sendAllTxProposals(senderId); // send old txps + break; case 'publicKeyRing': this._handlePublicKeyRing(senderId, data, isInbound); - break; + break; case 'txProposal': this._handleTxProposal(senderId, data, isInbound); - break; + break; case 'indexes': this._handleIndexes(senderId, data, isInbound); - break; + break; case 'addressbook': this._handleAddressBook(senderId, data, isInbound); - break; + break; } }; @@ -389,7 +386,7 @@ Wallet.fromObj = function(o, storage, network, blockchain) { opts.addressBook = o.addressBook; opts.publicKeyRing = PublicKeyRing.fromObj(o.publicKeyRing); - opts.txProposals = TxProposals.fromObj(o.txProposals, Wallet.builderOpts); + opts.txProposals = TxProposalsSet.fromObj(o.txProposals, Wallet.builderOpts); opts.privateKey = PrivateKey.fromObj(o.privateKey); opts.storage = storage; @@ -497,7 +494,7 @@ Wallet.prototype.generateAddress = function(isChange, cb) { }; -Wallet.prototype.getTxProposals = function() { +Wallet.prototype.getTxProposalsSet = function() { var ret = []; var copayers = this.getRegisteredCopayerIds(); for (var ntxid in this.txProposals.txps) { @@ -734,11 +731,11 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos } var b = new Builder(opts) - .setUnspent(utxos) - .setOutputs([{ - address: toAddress, - amountSatStr: amountSatStr, - }]); + .setUnspent(utxos) + .setOutputs([{ + address: toAddress, + amountSatStr: amountSatStr, + }]); var selectedUtxos = b.getSelectedUnspent(); var inputChainPaths = selectedUtxos.map(function(utxo) { @@ -831,29 +828,29 @@ Wallet.prototype.indexDiscovery = function(start, change, cosigner, gap, cb) { var self = this; async.doWhilst( function _do(next) { - // Optimize window to minimize the derivations. - var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; - var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner); - self.blockchain.checkActivity(addresses, function(err, actives) { - if (err) throw err; + // Optimize window to minimize the derivations. + var scanWindow = (lastActive == -1) ? gap : gap - (scanIndex - lastActive) + 1; + var addresses = self.deriveAddresses(scanIndex, scanWindow, change, cosigner); + self.blockchain.checkActivity(addresses, function(err, actives) { + if (err) throw err; - // Check for new activities in the newlly scanned addresses - var recentActive = actives.reduce(function(r, e, i) { - return e ? scanIndex + i : r; - }, lastActive); - hasActivity = lastActive != recentActive; - lastActive = recentActive; - scanIndex += scanWindow; - next(); - }); - }, - function _while() { - return hasActivity; - }, - function _finnaly(err) { - if (err) return cb(err); - cb(null, lastActive); - } + // Check for new activities in the newlly scanned addresses + var recentActive = actives.reduce(function(r, e, i) { + return e ? scanIndex + i : r; + }, lastActive); + hasActivity = lastActive != recentActive; + lastActive = recentActive; + scanIndex += scanWindow; + next(); + }); + }, + function _while() { + return hasActivity; + }, + function _finnaly(err) { + if (err) return cb(err); + cb(null, lastActive); + } ); } diff --git a/js/models/core/WalletFactory.js b/js/models/core/WalletFactory.js index 6f335ed0f..cd89d30da 100644 --- a/js/models/core/WalletFactory.js +++ b/js/models/core/WalletFactory.js @@ -2,7 +2,7 @@ var imports = require('soop').imports(); -var TxProposals = require('./TxProposals'); +var TxProposalsSet = require('./TxProposalsSet'); var PublicKeyRing = require('./PublicKeyRing'); var PrivateKey = require('./PrivateKey'); var Wallet = require('./Wallet'); @@ -124,10 +124,10 @@ WalletFactory.prototype.create = function(opts) { opts.nickname); this.log('\t### PublicKeyRing Initialized'); - opts.txProposals = opts.txProposals || new TxProposals({ + opts.txProposals = opts.txProposals || new TxProposalsSet({ networkName: this.networkName, }); - this.log('\t### TxProposals Initialized'); + this.log('\t### TxProposalsSet Initialized'); this.storage._setPassphrase(opts.passphrase); diff --git a/test/test.HDPath.js b/test/test.HDPath.js index 500ba348d..1bf5bf0b0 100644 --- a/test/test.HDPath.js +++ b/test/test.HDPath.js @@ -70,9 +70,9 @@ describe('HDPath model', function() { ].forEach(function(datum) { var path = datum[0]; var result = datum[1]; - it('should get the correct indices for path ' + path, function() { - var i = HDPath.indicesForPath(path); - i.index.should.equal(result.index); + it('should get the correct indexes for path ' + path, function() { + var i = HDPath.indexesForPath(path); + i.addressIndex.should.equal(result.index); i.isChange.should.equal(result.isChange); }); });