Merge pull request #1878 from matiu/ref/sign-message

Ref/sign message
This commit is contained in:
Gustavo Maximiliano Cortez 2014-11-27 17:52:43 -03:00
commit 3784c7931a
13 changed files with 953 additions and 892 deletions

View file

@ -1,26 +0,0 @@
'use strict';
var bitcore = require('bitcore');
var Transaction = bitcore.Transaction;
function BuilderMockV0 (data) {
this.vanilla = data;
this.tx = new Transaction();
this.tx.parse(new Buffer(data.tx, 'hex'));
};
BuilderMockV0.prototype.build = function() {
return this.tx;
};
BuilderMockV0.prototype.getSelectedUnspent = function() {
return [];
};
BuilderMockV0.prototype.toObj = function() {
return this.vanilla;
};
module.exports = BuilderMockV0;

View file

@ -1,15 +1,16 @@
'use strict';
var bitcore = require('bitcore');
var _ = require('lodash');
var preconditions = require('preconditions').singleton();
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TransactionBuilder = bitcore.TransactionBuilder;
var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance();
var log = require('../log');
var TX_MAX_SIZE_KB = 50;
var VERSION = 1;
@ -26,22 +27,24 @@ function TxProposal(opts) {
this.version = opts.version;
this.builder = opts.builder;
this.createdTs = opts.createdTs;
this._inputSigners = [];
// CopayerIds
this.creator = opts.creator;
// Copayer Actions ( copayerId: timeStamp )
this.signedBy = opts.signedBy || {};
this.seenBy = opts.seenBy || {};
this.rejectedBy = opts.rejectedBy || {};
this.sentTs = opts.sentTs || null;
this.sentTxid = opts.sentTxid || null;
this.comment = opts.comment || null;
this.readonly = opts.readonly || null;
this.merchant = opts.merchant || null;
this.paymentAckMemo = null;
this.paymentAckMemo = opts.paymentAckMemo || null;
this.paymentProtocolURL = opts.paymentProtocolURL || null;
if (opts.creator) {
this.resetCache();
// New Tx Proposal
if (_.isEmpty(this.seenBy) && opts.creator) {
var now = Date.now();
var me = {};
me[opts.creator] = now;
@ -55,8 +58,6 @@ function TxProposal(opts) {
throw new Error('Could not sign generated tx');
}
}
this._sync();
}
TxProposal.prototype._checkPayPro = function() {
@ -94,13 +95,32 @@ TxProposal.prototype.isFullySigned = function() {
return this.builder && this.builder.isFullySigned();
};
TxProposal.prototype.getMySignatures = function() {
preconditions.checkState(this._mySignatures, 'Still no signatures from us');
return _.clone(this._mySignatures);
};
TxProposal.prototype._setMySignatures = function(signaturesBefore) {
var mySigs = [];
_.each(this.getSignatures(), function(signatures, index) {
var diff = _.difference(signatures, signaturesBefore[index]);
preconditions.checkState(diff.length == 1, 'more that one signature added!');
mySigs.push(diff[0].toString('hex'));
})
this._mySignatures = mySigs;
return;
};
TxProposal.prototype.sign = function(keys, signerId) {
var before = this.countSignatures();
var signaturesBefore = this.getSignatures();
this.builder.sign(keys);
var signaturesAdded = this.countSignatures() > before;
if (signaturesAdded){
if (signaturesAdded) {
this.signedBy[signerId] = Date.now();
this._setMySignatures(signaturesBefore);
}
return signaturesAdded;
};
@ -111,6 +131,7 @@ TxProposal.prototype._check = function() {
throw new Error('Invalid tx proposal');
}
// Should be able to build
var tx = this.builder.build();
var txSize = tx.getSize();
@ -153,52 +174,207 @@ TxProposal.prototype.addMerchantData = function(merchantData) {
this._checkPayPro();
};
TxProposal.prototype.getSignatures = function() {
var ins = this.builder.build().ins;
var sigs = _.map(ins, function(value) {
var script = new bitcore.Script(value.s);
var nchunks = script.chunks.length;
return _.map(script.chunks.slice(1, nchunks - 1), function(buffer) {
return buffer.toString('hex');
});
});
return sigs;
};
TxProposal.prototype.rejectCount = function() {
return _.size(this.rejectedBy);
};
TxProposal.prototype.isPending = function(maxRejectCount) {
preconditions.checkArgument(typeof maxRejectCount != 'undefined');
if (this.rejectCount() > maxRejectCount || this.sentTxid)
TxProposal.prototype.isFinallyRejected = function(maxRejectCount) {
return this.rejectCount() > maxRejectCount;
};
TxProposal.prototype.isPending = function(maxRejectCount) {
preconditions.checkArgument(_.isNumber(maxRejectCount));
if (this.isFinallyRejected(maxRejectCount) || this.sentTxid)
return false;
return true;
};
TxProposal.prototype._setSigned = function(copayerId) {
TxProposal.prototype._updateSignedBy = function() {
this._inputSigners = [];
// Sign powns rejected
if (this.rejectedBy[copayerId]) {
log.info("WARN: a previously rejected transaction was signed by:", copayerId);
delete this.rejectedBy[copayerId];
}
var tx = this.builder.build();
for (var i in tx.ins) {
var scriptSig = new Script(tx.ins[i].s);
var signatureCount = scriptSig.countSignatures();
this.signedBy[copayerId] = Date.now();
var info = TxProposal._infoFromRedeemScript(scriptSig);
var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL);
var signersPubKey = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash);
if (signersPubKey.length !== signatureCount)
throw new Error('Invalid signature');
return this;
};
this._inputSigners[i] = signersPubKey;
/**
*
* @desc verify signatures of ONE copayer, using an array of signatures for each input
*
* @param {string[]} signatures, of the same copayer, one for each input
* @return {string[]} array for signing pubkeys for each input
*/
TxProposal.prototype._addSignatureAndVerify = function(signatures) {
var self = this;
var ret = [];
var tx = self.builder.build();
var newScriptSigs = [];
_.each(tx.ins, function(input, index) {
var scriptSig = new Script(input.s);
var info = TxProposal.infoFromRedeemScript(scriptSig);
var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL);
var keys = TxProposal.formatKeys(info.keys);
var sig = new Buffer(signatures[index], 'hex');
var hashType = sig[sig.length - 1];
if (hashType !== Transaction.SIGHASH_ALL)
throw new Error('BADSIG: Invalid signature: Bad hash type');
var sigRaw = new Buffer(sig.slice(0, sig.length - 1));
var signingPubKeyHex = self._verifyOneSignature(keys, sigRaw, txSigHash);
if (!signingPubKeyHex)
throw new Error('BADSIG: Invalid signatures: invalid for input:' + index);
// now insert it
var keysHex = _.pluck(keys, 'keyHex');
var prio = _.indexOf(keysHex, signingPubKeyHex);
preconditions.checkState(prio >= 0);
var currentKeys = self.getSignersPubKeys()[index];
if (_.indexOf(currentKeys, signingPubKeyHex) >= 0)
throw new Error('BADSIG: Already have this signature');
var currentPrios = _.map(currentKeys, function(key) {
var prio = _.indexOf(keysHex, key);
preconditions.checkState(prio >= 0);
return prio;
});
var insertAt = 0;
while ( !_.isUndefined(currentPrios[insertAt]) && prio > currentPrios[insertAt] )
insertAt++;
// Insert it! (1 is OP_0!)
scriptSig.chunks.splice(1 + insertAt, 0, sig);
scriptSig.updateBuffer();
newScriptSigs.push(scriptSig.buffer);
});
preconditions.checkState(newScriptSigs.length === tx.ins.length);
// If we reach here, all signatures are OK, let's update the TX.
_.each(tx.ins, function(input, index) {
input.s = newScriptSigs[index];
// TODO just to keep TransactionBuilder
self.builder.inputsSigned++;
});
this.resetCache();
};
TxProposal.prototype.resetCache = function() {
this.cache = {
pubkeysForScript: {},
};
};
TxProposal.prototype._sync = function() {
this._check();
this._updateSignedBy();
return this;
}
/**
* addSignature
*
* @param {string[]} signatures from *ONE* copayer, one signature for each TX input.
* @return {boolean} true = signatures added
*/
TxProposal.prototype.addSignature = function(copayerId, signatures) {
preconditions.checkArgument(_.isArray(signatures));
if (this.isFullySigned())
return false;
var tx = this.builder.build();
preconditions.checkArgument(signatures.length === tx.ins.length, 'Wrong number of signatures given');
this._addSignatureAndVerify(signatures);
this._setSigned(copayerId);
return false;
};
/**
*
* getSignersPubKey
* @desc get Pubkeys of signers, for each input. this is CPU intensive
*
* @return {string[][]} array of hashes for signing pubkeys for each input
*/
TxProposal.prototype.getSignersPubKeys = function(forceUpdate) {
var self = this;
var signersPubKey = [];
if (!self.cache.signersPubKey || forceUpdate) {
log.debug('PERFORMANCE WARN: Verifying *all* TX signatures:', self.getId());
var tx = self.builder.build();
_.each(tx.ins, function(input, index) {
if (!self.cache.pubkeysForScript[input.s]) {
var scriptSig = new Script(input.s);
var signatureCount = scriptSig.countSignatures();
var info = TxProposal.infoFromRedeemScript(scriptSig);
var txSigHash = tx.hashForSignature(info.script, parseInt(index), Transaction.SIGHASH_ALL);
var inputSignersPubKey = self.verifySignatures(info.keys, scriptSig, txSigHash);
// Does scriptSig has strings that are not signatures?
if (inputSignersPubKey.length !== signatureCount)
throw new Error('Invalid signature');
self.cache.pubkeysForScript[input.s] = inputSignersPubKey;
}
signersPubKey[index] = self.cache.pubkeysForScript[input.s];
});
self.cache.signersPubKey = signersPubKey;
} else {
log.debug('Using signatures verification cache')
}
return self.cache.signersPubKey;
};
TxProposal.prototype.getId = function() {
preconditions.checkState(this.builder);
return this.builder.build().getNormalizedHash().toString('hex');
if (!this.ntxid) {
this.ntxid = this.builder.build().getNormalizedHash().toString('hex');
}
return this.ntxid;
};
TxProposal.prototype.toObj = function() {
var o = JSON.parse(JSON.stringify(this));
delete o['builder'];
delete o['cache'];
o.builderObj = this.builder.toObj();
return o;
};
@ -216,46 +392,43 @@ TxProposal.fromObj = function(o, forceOpts) {
preconditions.checkArgument(o.builderObj);
delete o['builder'];
forceOpts = forceOpts || {};
if (forceOpts) {
o.builderObj.opts = o.builderObj.opts || {};
}
o.builderObj.opts = o.builderObj.opts || {};
// force opts is requested.
for (var k in forceOpts) {
o.builderObj.opts[k] = forceOpts[k];
}
// Handle undef options
_.each(forceOpts, function(value, key) {
o.builderObj.opts[key] = value;
});
// Handle undef fee options
if (_.isUndefined(forceOpts.fee) && _.isUndefined(forceOpts.feeSat)) {
if (o.builderObj.opts) {
o.builderObj.opts.fee = undefined;
o.builderObj.opts.feeSat = undefined;
}
o.builderObj.opts.fee = undefined;
o.builderObj.opts.feeSat = undefined;
}
try {
o.builder = TransactionBuilder.fromObj(o.builderObj);
} catch (e) {
// backwards (V0) compatatibility fix.
if (!o.version) {
o.builder = new BuilderMockV0(o.builderObj);
o.readonly = 1;
};
throw new Error(e);
return null;
}
return new TxProposal(o);
};
TxProposal.fromUntrustedObj = function(o, forceOpts) {
return TxProposal.fromObj(TxProposal._trim(o), forceOpts);
var trimmed = TxProposal._trim(o);
var txp = TxProposal.fromObj(trimmed, forceOpts);
if (!txp)
throw new Error('Invalid Transaction');
txp._check();
return txp;
};
TxProposal.prototype.toObjTrim = function() {
return TxProposal._trim(this.toObj());
};
TxProposal._formatKeys = function(keys) {
TxProposal.formatKeys = function(keys) {
var ret = [];
for (var i in keys) {
if (!Buffer.isBuffer(keys[i]))
@ -271,31 +444,66 @@ TxProposal._formatKeys = function(keys) {
return ret;
};
TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) {
/**
* @desc Verify a single signature, for a given hash, tested against a given list of public keys.
* @param keys
* @param sigRaw
* @param txSigHash
* @return {string?} on valid signature, return the signing public key hex representation
*/
TxProposal.prototype._verifyOneSignature = function(keys, sigRaw, txSigHash) {
preconditions.checkArgument(Buffer.isBuffer(txSigHash));
preconditions.checkArgument(Buffer.isBuffer(sigRaw));
preconditions.checkArgument(_.isArray(keys));
preconditions.checkArgument(keys[0].keyObj);
var signingKey = _.find(keys, function(key) {
var ret = false;
try {
ret = key.keyObj.verifySignatureSync(txSigHash, sigRaw);
} catch (e) {};
return ret;
});
return signingKey ? signingKey.keyHex : null;
};
/**
* @desc verify transaction signatures
*
* @param inKeys
* @param scriptSig
* @param txSigHash
* @return {string[]} signing pubkeys, in order of apperance
*/
TxProposal.prototype.verifySignatures = function(inKeys, scriptSig, txSigHash) {
preconditions.checkArgument(Buffer.isBuffer(txSigHash));
preconditions.checkArgument(inKeys);
preconditions.checkState(Buffer.isBuffer(inKeys[0]));
var self = this;
if (scriptSig.chunks[0] !== 0)
throw new Error('Invalid scriptSig');
var keys = TxProposal._formatKeys(inKeys);
var keys = TxProposal.formatKeys(inKeys);
var ret = [];
for (var i = 1; i <= scriptSig.countSignatures(); i++) {
var chunk = scriptSig.chunks[i];
log.debug('\t Verifying CHUNK:', i);
var sigRaw = new Buffer(chunk.slice(0, chunk.length - 1));
for (var j in keys) {
var k = keys[j];
if (k.keyObj.verifySignatureSync(txSigHash, sigRaw)) {
ret.push(k.keyHex);
break;
}
}
var signingPubKeyHex = self._verifyOneSignature(keys, sigRaw, txSigHash);
if (!signingPubKeyHex)
throw new Error('Found a signature that is invalid');
ret.push(signingPubKeyHex);
}
return ret;
};
TxProposal._infoFromRedeemScript = function(s) {
TxProposal.infoFromRedeemScript = function(s) {
var redeemScript = new Script(s.chunks[s.chunks.length - 1]);
if (!redeemScript)
throw new Error('Bad scriptSig (no redeemscript)');
@ -340,17 +548,7 @@ TxProposal.prototype.getSent = function() {
return this.sentTs;
}
TxProposal.prototype._allSignatures = function() {
var ret = {};
for (var i in this._inputSigners)
for (var j in this._inputSigners[i])
ret[this._inputSigners[i][j]] = true;
return ret;
};
TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
TxProposal.prototype.setCopayers = function(pubkeyToCopayerMap) {
var newCopayer = {},
oldCopayers = {},
newSignedBy = {},
@ -375,9 +573,9 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
}
var iSig = this._inputSigners[0];
var iSig = this.getSignersPubKeys();
for (var i in iSig) {
var copayerId = keyMap[iSig[i]];
var copayerId = pubkeyToCopayerMap[iSig[i]];
if (!copayerId)
throw new Error('Found unknown signature')
@ -390,24 +588,19 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
}
}
// Seems unncessary to check this:
// if (!newCopayer[senderId] && !readOnlyPeers[senderId])
// throw new Error('TX must have a (new) senders signature')
if (Object.keys(newCopayer).length > 1)
throw new Error('New TX must have only 1 new signature');
// Handler creator / createdTs.
// from senderId, and must be signed by senderId
// from senderId, and must be signed by senderId * DISABLED*
//
if (isNew) {
this.creator = Object.keys(newCopayer)[0];
this.seenBy[this.creator] = this.createdTs = Date.now();
}
//Ended. Update this.
for (var i in newCopayer) {
this.signedBy[i] = newCopayer[i];
}
//Ended. Update this
_.extend(this.signedBy, newCopayer);
// signedBy has preference over rejectedBy
for (var i in this.signedBy) {
@ -417,21 +610,6 @@ TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
return Object.keys(newCopayer);
};
// merge will not merge any metadata.
TxProposal.prototype.merge = function(incoming) {
preconditions.checkArgument(_.isFunction(incoming._sync));
incoming._sync();
// Note that all inputs must have the same number of signatures, so checking
// one (0) is OK.
var before = this._inputSigners[0].length;
this.builder.merge(incoming.builder);
this._sync();
var after = this._inputSigners[0].length;
return after !== before;
};
//This should be on bitcore / Transaction
TxProposal.prototype.countSignatures = function() {
var tx = this.builder.build();

View file

@ -1,16 +1,16 @@
'use strict';
var BuilderMockV0 = require('./BuilderMockV0');;
var preconditions = require('preconditions').singleton();
var bitcore = require('bitcore');
var util = bitcore.util;
var Transaction = bitcore.Transaction;
var BuilderMockV0 = require('./BuilderMockV0');;
var TxProposal = require('./TxProposal');;
var Script = bitcore.Script;
var Key = bitcore.Key;
var buffertools = bitcore.buffertools;
var preconditions = require('preconditions').instance();
var log = require('../log');
var TxProposal = require('./TxProposal');;
function TxProposals(opts) {
opts = opts || {};
@ -51,9 +51,9 @@ TxProposals.prototype.getNtxidsSince = function(sinceTs) {
preconditions.checkArgument(sinceTs);
var ret = [];
for(var ii in this.txps){
for (var ii in this.txps) {
var txp = this.txps[ii];
if (txp.createdTs >= sinceTs)
if (txp.createdTs >= sinceTs)
ret.push(ii);
}
return ret;
@ -96,101 +96,41 @@ TxProposals.prototype.toObj = function() {
};
TxProposals.prototype.merge = function(inObj, builderOpts) {
var incomingTx = TxProposal.fromUntrustedObj(inObj, builderOpts);
var myTxps = this.txps;
var ntxid = incomingTx.getId();
var ret = {
ntxid: ntxid
};
if (myTxps[ntxid]) {
// Merge an existing txProposal
ret.hasChanged = myTxps[ntxid].merge(incomingTx);
} else {
// Create a new one
ret.new = ret.hasChanged = 1;
this.txps[ntxid] = incomingTx;
}
ret.txp = this.txps[ntxid];
return ret;
};
// Add a LOCALLY CREATED (trusted) tx proposal
TxProposals.prototype.add = function(txp) {
txp._sync();
var ntxid = txp.getId();
this.txps[ntxid] = txp;
return ntxid;
};
TxProposals.prototype.exist = function(ntxid) {
return this.txps[ntxid] ? true : false;
};
TxProposals.prototype.get = function(ntxid) {
var ret = this.txps[ntxid];
if (!ret)
throw new Error('Unknown TXP: '+ntxid);
throw new Error('Unknown TXP: ' + ntxid);
return ret;
};
TxProposals.prototype.getTxProposal = function(ntxid, copayers) {
var txp = this.get(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
TxProposals.prototype.getUsedUnspent = function(maxRejectCount) {
var ret = {};
for (var i in this.txps) {
if (!this.txps[i].isPending(maxRejectCount))
continue;
var self = this;
var u = this.txps[i].builder.getSelectedUnspent();
var p = this.getTxProposal(i);
_.each(this.txps, function(txp) {
if (!txp.isPending(maxRejectCount))
return
for (var j in u) {
ret[u[j].txid + ',' + u[j].vout] = 1;
}
}
_.each(txp.builder.getSelectedUnspent(), function(u) {
ret[u.txid + ',' + u.vout] = 1;
});
});
return ret;
};

View file

@ -122,6 +122,7 @@ inherits(Wallet, events.EventEmitter);
Wallet.TX_BROADCASTED = 'txBroadcasted';
Wallet.TX_PROPOSAL_SENT = 'txProposalSent';
Wallet.TX_SIGNED = 'txSigned';
Wallet.TX_SIGNED_AND_BROADCASTED = 'txSignedAndBroadcasted';
Wallet.prototype.emitAndKeepAlive = function(args) {
var args = Array.prototype.slice.call(arguments);
@ -131,7 +132,7 @@ Wallet.prototype.emitAndKeepAlive = function(args) {
};
/**
* @desc Fixed & Forced TransactionBuilder options, for genereration transactions.
* @desc Fixed & Forced TransactionBuilder options, for genererating transactions.
*
* @static
* @property lockTime null
@ -338,76 +339,37 @@ Wallet.prototype._onPublicKeyRing = function(senderId, data) {
/**
* @desc
* Demultiplexes calls to update TxProposal updates
*
* @param {string} senderId - the copayer that sent this event
* @param {Object} m - the data received
* @emits txProposalEvent
*/
Wallet.prototype._processProposalEvents = function(senderId, m) {
var ev;
if (m) {
if (m.new) {
ev = {
type: 'new',
cId: senderId
}
} else if (m.newCopayer && m.newCopayer.length) {
ev = {
type: 'signed',
cId: m.newCopayer[0]
};
} else {
log.error('unknown tx proposal event:', m)
}
}
if (ev)
this.emitAndKeepAlive('txProposalEvent', ev);
};
/* OTDO
events.push({
type: 'signed',
cId: k,
txId: ntxid
});
*/
/**
* @desc
* Retrieves a keymap from from a transaction proposal set extracts a maps from
* Retrieves a keymap from a transaction proposal set extracts a maps from
* public key to cosignerId for each signed input of the transaction proposal.
*
* @param {TxProposals} txp - the transaction proposals
* @return {Object}
* @return {Object} [pubkey] -> copayerId
*/
Wallet.prototype._getKeyMap = function(txp) {
Wallet.prototype._getPubkeyToCopayerMap = function(txp) {
preconditions.checkArgument(txp);
var inSig0, keyMapAll = {};
var inSig0, keyMapAll = {},
self = this;
for (var i in txp._inputSigners) {
var keyMap = this.publicKeyRing.copayersForPubkeys(txp._inputSigners[i], txp.inputChainPaths);
var signersPubKeys = txp.getSignersPubKeys();
_.each(signersPubKeys, function(inputSignersPubKey, i) {
var keyMap = self.publicKeyRing.copayersForPubkeys(inputSignersPubKey, txp.inputChainPaths);
if (_.size(keyMap) !== _.size(txp._inputSigners[i]))
if (_.size(keyMap) !== _.size(inputSignersPubKey))
throw new Error('Signature does not match known copayers');
for (var j in keyMap) {
keyMapAll[j] = keyMap[j];
}
_.extend(keyMapAll, keyMap);
// From here -> only to check that all inputs have the same sigs
var inSigArr = [];
_.each(keyMap, function(value, key) {
inSigArr.push(value);
});
var inSigArr = _.values(keyMap);
var inSig = JSON.stringify(inSigArr.sort());
if (i === '0') {
if (!inSig0) {
inSig0 = inSig;
continue;
} else {
if (inSig !== inSig0)
throw new Error('found inputs with different signatures');
}
if (inSig !== inSig0)
throw new Error('found inputs with different signatures');
}
});
return keyMapAll;
};
@ -442,11 +404,10 @@ Wallet.prototype._checkIfTxIsSent = function(ntxid, cb) {
* and send `seen` messages to peers if aplicable.
* @param ntxid
*/
Wallet.prototype._setTxProposalSeen = function(ntxid) {
var txp = this.txProposals.get(ntxid);
Wallet.prototype._setTxProposalSeen = function(txp) {
if (!txp.getSeen(this.getMyCopayerId())) {
txp.setSeen(this.getMyCopayerId());
this.sendSeen(ntxid);
this.sendSeen(txp.getId());
}
};
@ -458,17 +419,13 @@ Wallet.prototype._setTxProposalSeen = function(ntxid) {
* @param ntxid
* @param {transactionCallback} cb
*/
Wallet.prototype._updateTxProposalSent = function(ntxid, cb) {
Wallet.prototype._updateTxProposalSent = function(txp, cb) {
var self = this;
var txp = this.txProposals.get(ntxid);
this._checkIfTxIsSent(ntxid, function(err, txid) {
this._checkIfTxIsSent(txp.getId(), function(err, txid) {
if (err) return cb(err);
if (txid) {
if (!txp.getSent()) {
txp.setSent(txid);
}
txp.setSent(txid);
self.emitAndKeepAlive('txProposalsUpdated');
}
if (cb)
@ -486,13 +443,10 @@ Wallet.prototype._updateTxProposalSent = function(ntxid, cb) {
* @param mergeInfo Proposals merge information, as returned by TxProposals.merge
* @return {fetchPaymentRequestCallback}
*/
Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) {
Wallet.prototype._processTxProposalPayPro = function(txp, cb) {
var self = this;
var txp = mergeInfo.txp;
var isNew = mergeInfo.new;
var ntxid = mergeInfo.ntxid;
if (!isNew || !txp.paymentProtocolURL)
if (!txp.paymentProtocolURL)
return cb();
log.info('Received a Payment Protocol TX Proposal');
@ -513,35 +467,36 @@ Wallet.prototype._processTxProposalPayPro = function(mergeInfo, cb) {
};
/**
* _processIncomingTxProposal
*
* @desc Process an incoming transaction proposal. Runs safety and sanity checks on it.
* @desc Process an NEW incoming transaction proposal. Runs safety and sanity checks on it.
*
* @param mergeInfo Proposals merge information, as returned by TxProposals.merge
* @return {errCallback}
*/
Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) {
if (!mergeInfo) return cb();
Wallet.prototype._processIncomingNewTxProposal = function(txp, cb) {
var self = this;
self._processTxProposalPayPro(mergeInfo, function(err) {
var ntxid = txp.getId();
self._processTxProposalPayPro(txp, function(err) {
if (err) return cb(err);
self._setTxProposalSeen(mergeInfo.ntxid);
self._setTxProposalSeen(txp);
var tx = mergeInfo.txp.builder.build();
if (tx.isComplete())
self._updateTxProposalSent(mergeInfo.ntxid);
else {
self.emitAndKeepAlive('txProposalsUpdated');
}
var tx = txp.builder.build();
if (tx.isComplete() && !txp.getSent())
self._updateTxProposalSent(txp);
return cb();
});
};
/* only for stubbing */
Wallet.prototype._txProposalFromUntrustedObj = function(data, opts) {
return TxProposal.fromUntrustedObj(data, opts);
};
/**
* @desc
* Handles a 'TXPROPOSAL' network message
* Handles a NEW 'TXPROPOSAL' network message
*
* @param {string} senderId - the id of the sender
* @param {Object} data - the data received
@ -549,37 +504,56 @@ Wallet.prototype._processIncomingTxProposal = function(mergeInfo, cb) {
* @emits txProposalsUpdated
*/
Wallet.prototype._onTxProposal = function(senderId, data) {
preconditions.checkArgument(data.txProposal);
var self = this;
var m;
try {
m = self.txProposals.merge(data.txProposal, Wallet.builderOpts);
var keyMap = self._getKeyMap(m.txp);
m.newCopayer = m.txp.setCopayers(senderId, keyMap);
var incomingTx = self._txProposalFromUntrustedObj(data.txProposal, Wallet.builderOpts);
var incomingNtxid = incomingTx.getId();
} catch (e) {
log.error('Corrupt TX proposal received from:', senderId, e.toString());
if (m && m.ntxid)
self.txProposals.deleteOne(m.ntxid);
m = null;
log.warn(e);
return;
}
if (m) {
if (this.txProposals.exist(incomingNtxid)) {
log.warn('Ignoring existing tx Proposal:' + incomingNtxid);
return;
}
self._processIncomingTxProposal(m, function(err) {
self._processIncomingNewTxProposal(incomingTx, function(err) {
if (err) {
log.warn('Corrupt TX proposal received from:', senderId, err.toString());
return;
}
if (err) {
log.error('Corrupt TX proposal received from:', senderId, err.toString());
if (m && m.ntxid)
self.txProposals.deleteOne(m.ntxid);
m = null;
} else {
if (m && m.hasChanged)
self.sendTxProposal(m.ntxid);
}
self._processProposalEvents(senderId, m);
var pubkeyToCopayerMap = self._getPubkeyToCopayerMap(incomingTx);
incomingTx.setCopayers(pubkeyToCopayerMap);
self.txProposals.add(incomingTx);
self.emitAndKeepAlive('txProposalEvent', {
type: 'new',
cId: senderId,
});
}
});
};
Wallet.prototype._onSignature = function(senderId, data) {
var self = this;
try {
var localTx = this.txProposals.get(data.ntxid);
} catch (e) {
log.info('Ignoring signature for unknown tx Proposal:' + data.ntxid);
return;
};
localTx.addSignature(senderId, data.signatures);
self.issueTxIfComplete(data.ntxid, function(err, txid) {
self.emitAndKeepAlive('txProposalEvent', {
type: txid ? 'signedAndBroadcasted' : 'signed',
cId: senderId,
});
});
};
/**
@ -753,6 +727,9 @@ Wallet.prototype._onData = function(senderId, data, ts) {
case 'txProposal':
this._onTxProposal(senderId, data);
break;
case 'signature':
this._onSignature(senderId, data);
break;
case 'indexes':
this._onIndexes(senderId, data);
break;
@ -1278,6 +1255,27 @@ Wallet.prototype.sendReject = function(ntxid) {
});
};
/**
* @desc Send a signature for a TX Proposal
* @param {string} ntxid
*/
Wallet.prototype.sendSignature = function(ntxid) {
preconditions.checkArgument(ntxid);
var txp = this.txProposals.get(ntxid);
var signatures = txp.getMySignatures();
preconditions.checkState(signatures && signatures.length);
this._sendToPeers(null, {
type: 'signature',
ntxid: ntxid,
signatures: signatures,
walletId: this.id,
});
};
/**
* @desc Notify other peers that a wallet has been backed up and it's ready to be used
* @param {string[]} [recipients] - the pubkeys of the recipients
@ -1396,45 +1394,28 @@ Wallet.prototype.generateAddress = function(isChange, cb) {
};
/**
* @desc Retrieve all the Transaction proposals (see {@link TxProposals})
* @return {Object[]} each object returned represents a transaction proposal, with two additional
* booleans: <tt>signedByUs</tt> and <tt>rejectedByUs</tt>. An optional third boolean signals
* whether the transaction was finally rejected (<tt>finallyRejected</tt> set to true).
*/
Wallet.prototype.getTxProposals = function() {
var ret = [];
var self = this;
var copayers = self.getRegisteredCopayerIds();
var myId = self.getMyCopayerId();
_.each(self.txProposals.txps, function(txp, ntxid){
txp.signedByUs = txp.signedBy[myId] ? true : false;
txp.rejectedByUs = txp.rejectedBy[self.getMyCopayerId()] ? true : false;
txp.finallyRejected = self.totalCopayers - txp.rejectCount < self.requiredCopayers;
txp.isPending = !txp.finallyRejected && !txp.sentTxid;
if (!txp.readonly || txp.finallyRejected || txp.sentTs) {
ret.push(txp);
}
});
return ret;
};
/**
* TODO: get this out of here
* @desc get list of actions (see {@link getPendingTxProposals})
*/
Wallet.prototype._getActionList = function(actions) {
if (!actions) return;
var peers = Object.keys(actions).map(function(i) {
return {
cId: i,
actions: actions[i]
}
});
Wallet.prototype._getActionList = function(txp) {
preconditions.checkArgument(txp);
return peers.sort(function(a, b) {
return !!b.actions.create - !!a.actions.create;
var self = this;
var peers = [];
_.each(self.getRegisteredCopayerIds(), function(copayerId) {
var actions = {
rejected: txp.rejectedBy[copayerId],
sign: txp.signedBy[copayerId],
seen: txp.seenBy[copayerId],
create: (txp.creator === copayerId) ? txp.createdTs : null,
};
peers.push({
cId: copayerId,
actions: actions,
});
});
return peers;
};
/**
@ -1446,15 +1427,22 @@ Wallet.prototype.getPendingTxProposals = function() {
var ret = [];
ret.txs = [];
var pendingForUs = 0;
var txps = this.getTxProposals();
var txps = this.txProposals.txps;
var maxRejectCount = this.maxRejectCount();
var satToUnit = 1 / this.settings.unitToSatoshi;
_.each(_.where(txps, 'isPending'), function(txp) {
_.each(txps, function(inTxp, ntxid) {
if (!inTxp.isPending(maxRejectCount))
return;
var txp = _.clone(inTxp);
txp.ntxid = ntxid;
pendingForUs++;
var addresses = {};
var outs = JSON.parse(txp.builder.vanilla.outs);
outs.forEach(function(o) {
if (!self.publicKeyRing.addressToPath[o.Straddress]) {
if (!self.addressIsOwn(o.address)) {
if (!addresses[o.address]) addresses[o.address] = 0;
addresses[o.address] += (o.amountSatStr || Math.round(o.amount * bitcore.util.COIN));
};
@ -1469,7 +1457,7 @@ Wallet.prototype.getPendingTxProposals = function() {
// extra fields
txp.fee = txp.builder.feeSat * satToUnit;
txp.missingSignatures = txp.builder.build().countInputMissingSignatures(0);
txp.actionList = self._getActionList(txp.peerActions);
txp.actionList = self._getActionList(txp);
ret.txs.push(txp);
});
@ -1537,6 +1525,16 @@ Wallet.prototype.sign = function(ntxid) {
return true;
};
Wallet.prototype.issueTxIfComplete = function(ntxid, cb) {
var txp = this.txProposals.get(ntxid);
var tx = txp.builder.build();
if (tx.isComplete()) {
this.issueTx(ntxid, cb);
} else {
return cb();
}
};
/**
*
@ -1551,13 +1549,13 @@ Wallet.prototype.sign = function(ntxid) {
*/
Wallet.prototype.signAndSend = function(ntxid, cb) {
if (this.sign(ntxid)) {
var txp = this.txProposals.get(ntxid);
if (txp.isFullySigned()) {
return this.broadcastTx(ntxid, cb);
} else {
this.sendTxProposal(ntxid);
return cb(null, ntxid, Wallet.TX_SIGNED);
}
this.sendSignature(ntxid);
this.issueTxIfComplete(ntxid, function(err, txid, status) {
if (!txid)
return cb(null, ntxid, Wallet.TX_SIGNED);
else
return cb(null, ntxid, Wallet.TX_SIGNED_AND_BROADCASTED);
});
} else {
return cb(new Error('Could not sign the proposal'));
}
@ -1572,13 +1570,12 @@ Wallet.prototype.signAndSend = function(ntxid, cb) {
* @param cb
* @return {undefined}
*/
Wallet.prototype._doBroadcastTx = function(ntxid, cb) {
Wallet.prototype.broadcastToBitcoinNetwork = function(ntxid, cb) {
var self = this;
var txp = this.txProposals.get(ntxid);
var tx = txp.builder.build();
if (!tx.isComplete())
throw new Error('Tx is not complete. Can not broadcast');
var tx = txp.builder.build();
preconditions.checkState(tx.isComplete(), 'tx is not complete');
var txHex = tx.serialize().toString('hex');
@ -1595,7 +1592,7 @@ Wallet.prototype._doBroadcastTx = function(ntxid, cb) {
return cb(err, txid);
});
} else {
log.info('Wallet:' + self.getName() + ' broadcasted a TX. BITCOIND txid:', txid);
log.info('Wallet:' + self.getName() + ' broadcasted a TX! TXID:', txid);
return cb(null, txid);
}
});
@ -1610,10 +1607,10 @@ Wallet.prototype._doBroadcastTx = function(ntxid, cb) {
* @param {string} txid - the transaction id on the blockchain
* @param {signCallback} cb
*/
Wallet.prototype.broadcastTx = function(ntxid, cb) {
Wallet.prototype.issueTx = function(ntxid, cb) {
var self = this;
self._doBroadcastTx(ntxid, function(err, txid) {
self.broadcastToBitcoinNetwork(ntxid, function(err, txid) {
if (err) return cb(err);
preconditions.checkState(txid);
@ -1630,8 +1627,6 @@ Wallet.prototype.broadcastTx = function(ntxid, cb) {
self.onPayProPaymentAck(ntxid, data);
});
}
self.sendTxProposal(ntxid);
self.emitAndKeepAlive('txProposalsUpdated');
return cb(null, txid, Wallet.TX_BROADCASTED);
});
@ -2133,58 +2128,6 @@ Wallet.prototype.getUnspent = function(cb) {
});
};
// TODO. not used.
Wallet.prototype.removeTxWithSpentInputs = function(cb) {
var self = this;
cb = cb || function() {};
if (!_.some(self.getTxProposals(), {
isPending: true
}))
return cb();
var proposalsChanged = false;
this.blockchain.getUnspent(this.getAddressesStr(), function(err, unspentList) {
if (err) return cb(err);
var txps = _.where(self.getTxProposals(), {
isPending: true
});
if (txps.length === 0) return cb();
var inputs = _.flatten(_.map(txps, function(txp) {
return _.map(txp.builder.utxos, function(utxo) {
return {
ntxid: txp.ntxid,
txid: utxo.txid,
vout: utxo.vout,
};
});
}));
_.each(unspentList, function(unspent) {
_.each(inputs, function(input) {
input.unspent = input.unspent || (input.txid === unspent.txid && input.vout === unspent.vout);
});
});
_.each(inputs, function(input) {
if (!input.unspent) {
proposalsChanged = true;
self.txProposals.deleteOne(input.ntxid);
}
});
if (proposalsChanged) {
self.emitAndKeepAlive('txProposalsUpdated');
}
return cb();
});
};
/**
* spend
*
@ -2252,11 +2195,10 @@ Wallet.prototype.spend = function(opts, cb) {
log.debug('TXP Added: ', ntxid);
console.log('[Wallet.js.2233]'); //TODO
self.sendIndexes();
// Needs only one signature? Broadcast it!
if (!self.requiresMultipleSignatures()) {
self.broadcastTx(ntxid, cb);
self.issueTx(ntxid, cb);
} else {
self.sendTxProposal(ntxid);
self.emitAndKeepAlive('txProposalsUpdated');
@ -2361,6 +2303,7 @@ Wallet.prototype._createTxProposal = function(toAddress, amountSat, comment, utx
signWith: keys,
});
console.log('[Wallet.js.2303]'); //TODO
return txp;
};
@ -2590,7 +2533,7 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) {
opts = opts || {};
var addresses = self.getAddressesInfo();
var proposals = self.getTxProposals();
var proposals = self.txProposals.txps;
var satToUnit = 1 / self.settings.unitToSatoshi;
var indexedProposals = _.indexBy(proposals, 'sentTxid');
@ -2689,18 +2632,11 @@ Wallet.prototype.getTransactionHistory = function(opts, cb) {
if (proposal) {
// TODO refactor
tx.comment = proposal.comment;
tx.sentTs = proposal.sentTs;
tx.merchant = proposal.merchant;
tx.peerActions = proposal.peerActions;
tx.finallyRejected = proposal.finallyRejected;
tx.merchant = proposal.merchant;
tx.paymentAckMemo = proposal.paymentAckMemo;
tx.peerActions = proposal.peerActions;
tx.finallyRejected = proposal.finallyRejected;
if (tx.peerActions) {
tx.actionList = self._getActionList(tx.peerActions);
}
tx.actionList = self._getActionList(proposal);
}
};