copay/js/models/TxProposal.js

440 lines
11 KiB
JavaScript
Raw Normal View History

'use strict';
var bitcore = require('bitcore');
2014-10-25 15:57:12 -07:00
var _ = require('lodash');
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 VERSION = 1;
var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version', 'comment', 'paymentProtocolURL'];
function TxProposal(opts) {
preconditions.checkArgument(opts);
2014-08-03 18:34:47 -07:00
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
preconditions.checkArgument(opts.builder, 'no builder');
preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths');
this.inputChainPaths = opts.inputChainPaths;
this.version = opts.version;
this.builder = opts.builder;
this.createdTs = opts.createdTs;
this._inputSigners = [];
// CopayerIds
this.creator = opts.creator;
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.paymentProtocolURL = opts.paymentProtocolURL || null;
2014-11-22 15:00:18 -08:00
if (opts.creator) {
var now = Date.now();
var me = {};
me[opts.creator] = now;
2014-11-23 11:31:08 -08:00
this.signedBy = me;
this.signedBy = _.clone(me);
2014-11-22 15:00:18 -08:00
this.creator = opts.creator;
this.createdTs = now;
}
2014-08-03 18:34:47 -07:00
this._sync();
}
2014-11-21 06:06:47 -08:00
TxProposal.prototype._checkPayPro = function() {
if (!this.merchant) return;
if (this.paymentProtocolURL !== this.merchant.request_url)
throw new Error('PayPro: Mismatch on Payment URLs');
if (!this.merchant.outs || this.merchant.outs.length !== 1)
throw new Error('PayPro: Unsopported number of outputs');
2014-11-22 15:00:18 -08:00
if (this.merchant.expires < (this.getSent() || Date.now() / 1000.))
2014-11-21 06:06:47 -08:00
throw new Error('PayPro: Request expired');
if (!this.merchant.total || !this.merchant.outs[0].amountSatStr || !this.merchant.outs[0].address)
throw new Error('PayPro: Missing amount');
var outs = JSON.parse(this.builder.vanilla.outs);
if (_.size(outs) != 1)
2014-11-21 06:06:47 -08:00
throw new Error('PayPro: Wrong outs in Tx');
var ppOut = this.merchant.outs[0];
var txOut = outs[0];
2014-11-21 06:06:47 -08:00
if (ppOut.address !== txOut.address)
throw new Error('PayPro: Wrong out address in Tx');
2014-11-22 19:29:50 -08:00
if (ppOut.amountSatStr !== txOut.amountSatStr + '')
2014-11-21 06:06:47 -08:00
throw new Error('PayPro: Wrong amount in Tx');
};
2014-08-03 18:34:47 -07:00
2014-11-23 10:52:39 -08:00
TxProposal.prototype.isFullySigned = function() {
2014-11-23 11:31:08 -08:00
return this.builder && this.builder.isFullySigned();
2014-11-23 10:52:39 -08:00
};
TxProposal.prototype.sign = function(keys, signerId) {
2014-11-23 11:31:08 -08:00
var before = this.countSignatures();
this.builder.sign(keys);
2014-11-23 10:52:39 -08:00
2014-11-23 11:31:08 -08:00
var signaturesAdded = this.countSignatures() > before;
2014-11-23 10:52:39 -08:00
if (signaturesAdded){
2014-11-23 11:31:08 -08:00
this.signedBy[signerId] = Date.now();
2014-11-23 10:52:39 -08:00
}
2014-11-23 11:31:08 -08:00
return signaturesAdded;
2014-11-23 10:52:39 -08:00
};
2014-08-03 18:34:47 -07:00
TxProposal.prototype._check = function() {
if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) {
throw new Error('Invalid tx proposal');
}
var tx = this.builder.build();
if (!tx.ins.length)
throw new Error('Invalid tx proposal: no ins');
2014-09-02 06:38:30 -07:00
_.each(tx.ins, function(value, index) {
var scriptSig = value.s;
2014-08-03 18:34:47 -07:00
if (!scriptSig || !scriptSig.length) {
throw new Error('Invalid tx proposal: no signatures');
}
2014-09-02 06:38:30 -07:00
var hashType = tx.getHashType(index);
2014-08-03 18:34:47 -07:00
if (hashType && hashType !== Transaction.SIGHASH_ALL)
throw new Error('Invalid tx proposal: bad signatures');
2014-09-02 06:38:30 -07:00
});
2014-11-21 06:06:47 -08:00
this._checkPayPro();
};
TxProposal.prototype.trimForStorage = function() {
// TODO (remove builder / builderObj. utxos, etc)
//
return this;
};
TxProposal.prototype.addMerchantData = function(merchantData) {
2014-11-22 19:29:50 -08:00
preconditions.checkArgument(merchantData.pr);
preconditions.checkArgument(merchantData.request_url);
2014-11-21 06:06:47 -08:00
var m = _.clone(merchantData);
2014-11-22 19:29:50 -08:00
if (!this.paymentProtocolURL)
this.paymentProtocolURL = m.request_url;
2014-11-21 06:06:47 -08:00
// remove unneeded data
m.raw = m.pr.pki_data = m.pr.signature = undefined;
this.merchant = m;
this._checkPayPro();
2014-08-03 18:34:47 -07:00
};
2014-08-21 10:12:55 -07:00
TxProposal.prototype.rejectCount = function() {
2014-09-02 06:38:30 -07:00
return _.size(this.rejectedBy);
2014-08-21 10:12:55 -07:00
};
TxProposal.prototype.isPending = function(maxRejectCount) {
2014-08-21 10:54:38 -07:00
preconditions.checkArgument(typeof maxRejectCount != 'undefined');
if (this.rejectCount() > maxRejectCount || this.sentTxid)
2014-08-21 10:12:55 -07:00
return false;
return true;
};
2014-08-03 18:34:47 -07:00
TxProposal.prototype._updateSignedBy = function() {
this._inputSigners = [];
2014-08-03 18:34:47 -07:00
var tx = this.builder.build();
for (var i in tx.ins) {
var scriptSig = new Script(tx.ins[i].s);
var signatureCount = scriptSig.countSignatures();
2014-08-03 18:34:47 -07:00
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)
2014-08-03 18:34:47 -07:00
throw new Error('Invalid signature');
this._inputSigners[i] = signersPubKey;
2014-08-03 18:34:47 -07:00
};
};
2014-08-03 18:34:47 -07:00
TxProposal.prototype._sync = function() {
this._check();
this._updateSignedBy();
return this;
}
TxProposal.prototype.getId = function() {
2014-08-03 18:34:47 -07:00
preconditions.checkState(this.builder);
return this.builder.build().getNormalizedHash().toString('hex');
};
TxProposal.prototype.toObj = function() {
var o = JSON.parse(JSON.stringify(this));
delete o['builder'];
o.builderObj = this.builder.toObj();
return o;
};
2014-08-03 19:57:23 -07:00
TxProposal._trim = function(o) {
2014-08-03 18:34:47 -07:00
var ret = {};
CORE_FIELDS.forEach(function(k) {
ret[k] = o[k];
});
2014-08-03 18:34:47 -07:00
return ret;
};
TxProposal.fromObj = function(o, forceOpts) {
preconditions.checkArgument(o.builderObj);
delete o['builder'];
2014-09-09 06:43:13 -07:00
forceOpts = forceOpts || {};
2014-09-09 06:43:13 -07:00
2014-09-29 13:36:34 -07:00
if (forceOpts) {
2014-09-09 06:43:29 -07:00
o.builderObj.opts = o.builderObj.opts || {};
}
2014-09-09 06:43:13 -07:00
// force opts is requested.
for (var k in forceOpts) {
o.builderObj.opts[k] = forceOpts[k];
}
// Handle undef options
if (_.isUndefined(forceOpts.fee) && _.isUndefined(forceOpts.feeSat)) {
if (o.builderObj.opts) {
o.builderObj.opts.fee = undefined;
o.builderObj.opts.feeSat = undefined;
}
2014-09-09 06:43:13 -07:00
}
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;
};
}
return new TxProposal(o);
};
2014-07-31 21:09:46 -07:00
2014-08-03 19:57:23 -07:00
TxProposal.fromUntrustedObj = function(o, forceOpts) {
2014-08-04 13:12:53 -07:00
return TxProposal.fromObj(TxProposal._trim(o), forceOpts);
2014-08-03 19:57:23 -07:00
};
2014-08-04 13:12:53 -07:00
TxProposal.prototype.toObjTrim = function() {
return TxProposal._trim(this.toObj());
};
2014-07-31 12:04:41 -07:00
TxProposal._formatKeys = function(keys) {
var ret = [];
for (var i in keys) {
if (!Buffer.isBuffer(keys[i]))
throw new Error('keys must be buffers');
var k = new Key();
2014-07-31 12:04:41 -07:00
k.public = keys[i];
ret.push({
keyObj: k,
keyHex: keys[i].toString('hex'),
});
}
2014-07-31 12:04:41 -07:00
return ret;
};
2014-07-31 06:50:00 -07:00
TxProposal._verifySignatures = function(inKeys, scriptSig, txSigHash) {
preconditions.checkArgument(Buffer.isBuffer(txSigHash));
2014-07-31 06:42:09 -07:00
preconditions.checkArgument(inKeys);
preconditions.checkState(Buffer.isBuffer(inKeys[0]));
if (scriptSig.chunks[0] !== 0)
throw new Error('Invalid scriptSig');
2014-07-31 06:45:50 -07:00
var keys = TxProposal._formatKeys(inKeys);
var ret = [];
2014-07-31 06:42:09 -07:00
for (var i = 1; i <= scriptSig.countSignatures(); i++) {
var chunk = scriptSig.chunks[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);
2014-07-31 06:42:09 -07:00
break;
2014-07-31 12:04:41 -07:00
}
}
}
return ret;
};
2014-07-31 12:04:41 -07:00
TxProposal._infoFromRedeemScript = function(s) {
2014-07-31 06:42:09 -07:00
var redeemScript = new Script(s.chunks[s.chunks.length - 1]);
if (!redeemScript)
2014-07-31 21:09:46 -07:00
throw new Error('Bad scriptSig (no redeemscript)');
var pubkeys = redeemScript.capture();
if (!pubkeys || !pubkeys.length)
2014-07-31 21:09:46 -07:00
throw new Error('Bad scriptSig (no pubkeys)');
2014-07-31 12:04:41 -07:00
return {
keys: pubkeys,
script: redeemScript,
2014-07-31 12:04:41 -07:00
};
};
2014-07-31 12:04:41 -07:00
TxProposal.prototype.mergeBuilder = function(incoming) {
var b0 = this.builder;
var b1 = incoming.builder;
2014-07-31 12:04:41 -07:00
var before = JSON.stringify(b0.toObj());
b0.merge(b1);
var after = JSON.stringify(b0.toObj());
return after !== before;
};
2014-09-29 13:36:34 -07:00
TxProposal.prototype.getSeen = function(copayerId) {
return this.seenBy[copayerId];
};
TxProposal.prototype.setSeen = function(copayerId) {
2014-08-03 18:34:47 -07:00
if (!this.seenBy[copayerId])
this.seenBy[copayerId] = Date.now();
};
TxProposal.prototype.setRejected = function(copayerId) {
2014-08-03 19:57:23 -07:00
if (this.signedBy[copayerId])
throw new Error('Can not reject a signed TX');
if (!this.rejectedBy[copayerId])
this.rejectedBy[copayerId] = Date.now();
2014-11-21 06:06:47 -08:00
return this;
};
TxProposal.prototype.setSent = function(sentTxid) {
this.sentTxid = sentTxid;
this.sentTs = Date.now();
2014-11-21 06:06:47 -08:00
return this;
};
2014-10-07 08:17:27 -07:00
TxProposal.prototype.getSent = function() {
return this.sentTs;
}
2014-07-31 12:04:41 -07:00
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;
2014-07-31 12:04:41 -07:00
return ret;
};
2014-07-31 12:04:41 -07:00
2014-08-03 18:34:47 -07:00
TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) {
2014-08-03 19:57:23 -07:00
var newCopayer = {},
oldCopayers = {},
newSignedBy = {},
readOnlyPeers = {},
isNew = 1;
2014-07-31 12:04:41 -07:00
2014-08-04 13:12:53 -07:00
for (var k in this.signedBy) {
2014-08-03 18:34:47 -07:00
oldCopayers[k] = 1;
isNew = 0;
};
2014-08-04 13:12:53 -07:00
if (isNew == 0) {
if (!this.creator || !this.createdTs)
throw new Error('Existing TX has no creator');
if (!this.signedBy[this.creator])
throw new Error('Existing TX is not signed by creator');
if (Object.keys(this.signedBy).length === 0)
throw new Error('Existing TX has no signatures');
}
2014-07-31 12:04:41 -07:00
var iSig = this._inputSigners[0];
2014-08-04 13:12:53 -07:00
for (var i in iSig) {
2014-08-03 18:34:47 -07:00
var copayerId = keyMap[iSig[i]];
2014-08-21 18:02:55 -07:00
2014-08-03 18:34:47 -07:00
if (!copayerId)
throw new Error('Found unknown signature')
if (oldCopayers[copayerId]) {
//Already have it. Do nothing
} else {
2014-08-04 13:12:53 -07:00
newCopayer[copayerId] = Date.now();
2014-08-03 18:34:47 -07:00
delete oldCopayers[i];
}
}
2014-08-04 13:12:53 -07:00
// Seems unncessary to check this:
// if (!newCopayer[senderId] && !readOnlyPeers[senderId])
// throw new Error('TX must have a (new) senders signature')
2014-08-03 18:34:47 -07:00
2014-08-04 13:12:53 -07:00
if (Object.keys(newCopayer).length > 1)
2014-08-03 19:57:23 -07:00
throw new Error('New TX must have only 1 new signature');
2014-08-03 18:34:47 -07:00
// Handler creator / createdTs.
// from senderId, and must be signed by senderId
if (isNew) {
2014-08-03 19:57:23 -07:00
this.creator = Object.keys(newCopayer)[0];
2014-08-04 03:16:09 -07:00
this.seenBy[this.creator] = this.createdTs = Date.now();
2014-08-04 13:12:53 -07:00
}
2014-08-03 18:34:47 -07:00
//Ended. Update this.
2014-08-04 13:12:53 -07:00
for (var i in newCopayer) {
2014-08-03 19:57:23 -07:00
this.signedBy[i] = newCopayer[i];
2014-08-03 18:34:47 -07:00
}
// signedBy has preference over rejectedBy
2014-08-04 13:12:53 -07:00
for (var i in this.signedBy) {
delete this.rejectedBy[i];
2014-08-03 18:34:47 -07:00
}
2014-08-03 19:57:23 -07:00
return Object.keys(newCopayer);
2014-08-03 18:34:47 -07:00
};
// merge will not merge any metadata.
TxProposal.prototype.merge = function(incoming) {
var hasChanged = this.mergeBuilder(incoming);
this._sync();
return hasChanged;
2014-07-31 12:04:41 -07:00
};
//This should be on bitcore / Transaction
TxProposal.prototype.countSignatures = function() {
var tx = this.builder.build();
var ret = 0;
for (var i in tx.ins) {
ret += tx.countInputSignatures(i);
}
return ret;
};
2014-07-31 12:04:41 -07:00
module.exports = TxProposal;