bitcore-wallet-service/lib/model/txproposal.js

333 lines
8.2 KiB
JavaScript
Raw Normal View History

2015-01-27 11:40:27 -08:00
'use strict';
2015-01-28 12:06:29 -08:00
var _ = require('lodash');
2015-02-07 07:48:57 -08:00
var Uuid = require('uuid');
2015-03-12 07:49:01 -07:00
var WalletUtils = require('bitcore-wallet-utils');
var Bitcore = WalletUtils.Bitcore;
2015-02-06 10:15:54 -08:00
var Address = Bitcore.Address;
2015-01-28 12:06:29 -08:00
2015-01-28 11:40:07 -08:00
var TxProposalAction = require('./txproposalaction');
2015-02-17 16:20:08 -08:00
function TxProposal() {
2015-07-29 13:45:25 -07:00
this.version = '2.0.0';
2015-02-17 16:20:08 -08:00
};
2015-02-02 10:29:14 -08:00
TxProposal.Types = {
SIMPLE: 'simple',
MULTIPLEOUTPUTS: 'multiple_outputs',
};
TxProposal.isTypeSupported = function(type) {
2015-06-25 07:03:11 -07:00
return _.contains(_.values(TxProposal.Types), type);
};
2015-06-18 07:57:07 -07:00
TxProposal._create = {};
TxProposal._create.simple = function(txp, opts) {
txp.toAddress = opts.toAddress;
txp.amount = opts.amount;
txp.outputOrder = _.shuffle(_.range(2));
2015-06-25 07:03:11 -07:00
try {
txp.network = Bitcore.Address(txp.toAddress).toObject().network;
} catch (ex) {}
2015-06-18 07:57:07 -07:00
};
TxProposal._create.undefined = TxProposal._create.simple;
2015-06-18 07:57:07 -07:00
TxProposal._create.multiple_outputs = function(txp, opts) {
2015-06-25 07:43:47 -07:00
txp.outputs = _.map(opts.outputs, function(output) {
return _.pick(output, ['amount', 'toAddress', 'message']);
});
2015-06-18 07:57:07 -07:00
txp.outputOrder = _.shuffle(_.range(txp.outputs.length + 1));
2015-06-25 07:03:11 -07:00
try {
txp.network = Bitcore.Address(txp.outputs[0].toAddress).toObject().network;
} catch (ex) {}
};
2015-02-17 16:20:08 -08:00
TxProposal.create = function(opts) {
2015-02-02 12:07:18 -08:00
opts = opts || {};
2015-01-27 11:40:27 -08:00
2015-02-17 16:20:08 -08:00
var x = new TxProposal();
2015-02-12 05:26:13 -08:00
2015-06-25 07:03:11 -07:00
x.type = opts.type || TxProposal.Types.SIMPLE;
2015-02-12 05:26:13 -08:00
var now = Date.now();
2015-02-17 16:20:08 -08:00
x.createdOn = Math.floor(now / 1000);
2015-02-21 20:35:25 -08:00
x.id = _.padLeft(now, 14, '0') + Uuid.v4();
2015-03-30 07:29:19 -07:00
x.walletId = opts.walletId;
2015-02-17 16:20:08 -08:00
x.creatorId = opts.creatorId;
x.message = opts.message;
2015-03-26 13:52:59 -07:00
x.payProUrl = opts.payProUrl;
2015-02-17 16:20:08 -08:00
x.proposalSignature = opts.proposalSignature;
x.changeAddress = opts.changeAddress;
x.inputs = [];
x.inputPaths = [];
x.requiredSignatures = opts.requiredSignatures;
x.requiredRejections = opts.requiredRejections;
2015-07-27 05:00:37 -07:00
x.walletN = opts.walletN;
2015-02-17 16:20:08 -08:00
x.status = 'pending';
2015-02-20 07:33:46 -08:00
x.actions = [];
2015-03-12 11:21:24 -07:00
x.fee = null;
2015-06-11 11:26:34 -07:00
x.feePerKb = opts.feePerKb;
x.excludeUnconfirmedUtxos = opts.excludeUnconfirmedUtxos;
2015-02-17 16:20:08 -08:00
2015-06-25 07:03:11 -07:00
if (_.isFunction(TxProposal._create[x.type])) {
TxProposal._create[x.type](x, opts);
}
2015-02-17 16:20:08 -08:00
return x;
2015-01-27 11:40:27 -08:00
};
2015-02-05 10:50:18 -08:00
TxProposal.fromObj = function(obj) {
2015-02-02 12:07:18 -08:00
var x = new TxProposal();
2015-01-27 11:40:27 -08:00
2015-06-25 07:03:11 -07:00
if (obj.version == '1.0.0') {
x.type = TxProposal.Types.SIMPLE;
} else {
x.type = obj.type;
}
2015-02-02 10:29:14 -08:00
x.version = obj.version;
2015-02-02 12:07:18 -08:00
x.createdOn = obj.createdOn;
x.id = obj.id;
2015-03-30 07:29:19 -07:00
x.walletId = obj.walletId;
2015-02-02 12:07:18 -08:00
x.creatorId = obj.creatorId;
x.outputs = obj.outputs;
2015-02-02 12:07:18 -08:00
x.toAddress = obj.toAddress;
x.amount = obj.amount;
x.message = obj.message;
2015-03-26 13:52:59 -07:00
x.payProUrl = obj.payProUrl;
2015-02-10 05:22:23 -08:00
x.proposalSignature = obj.proposalSignature;
2015-02-02 12:07:18 -08:00
x.changeAddress = obj.changeAddress;
x.inputs = obj.inputs;
x.requiredSignatures = obj.requiredSignatures;
2015-02-09 07:27:50 -08:00
x.requiredRejections = obj.requiredRejections;
2015-07-27 05:00:37 -07:00
x.walletN = obj.walletN;
2015-02-02 12:07:18 -08:00
x.status = obj.status;
x.txid = obj.txid;
2015-02-24 07:27:44 -08:00
x.broadcastedOn = obj.broadcastedOn;
2015-02-04 06:43:12 -08:00
x.inputPaths = obj.inputPaths;
2015-02-20 07:33:46 -08:00
x.actions = _.map(obj.actions, function(action) {
return TxProposalAction.fromObj(action);
2015-02-02 12:07:18 -08:00
});
2015-03-12 08:01:44 -07:00
x.outputOrder = obj.outputOrder;
2015-03-12 11:21:24 -07:00
x.fee = obj.fee;
2015-04-23 08:33:01 -07:00
x.network = obj.network;
2015-06-11 11:26:34 -07:00
x.feePerKb = obj.feePerKb;
x.excludeUnconfirmedUtxos = obj.excludeUnconfirmedUtxos;
2015-02-02 12:07:18 -08:00
return x;
2015-01-27 11:40:27 -08:00
};
TxProposal.prototype.setInputs = function(inputs) {
this.inputs = inputs;
this.inputPaths = _.pluck(inputs, 'path');
};
2015-02-04 16:38:23 -08:00
2015-02-05 10:50:18 -08:00
TxProposal.prototype._updateStatus = function() {
2015-02-02 12:07:18 -08:00
if (this.status != 'pending') return;
2015-01-28 11:40:07 -08:00
2015-02-02 12:07:18 -08:00
if (this.isRejected()) {
this.status = 'rejected';
} else if (this.isAccepted()) {
this.status = 'accepted';
}
2015-01-28 11:40:07 -08:00
};
2015-02-04 16:38:23 -08:00
2015-02-13 16:00:12 -08:00
TxProposal.prototype._getCurrentSignatures = function() {
2015-02-20 07:33:46 -08:00
var acceptedActions = _.filter(this.actions, {
type: 'accept'
2015-02-13 16:16:18 -08:00
});
return _.map(acceptedActions, function(x) {
2015-02-13 16:00:12 -08:00
return {
signatures: x.signatures,
2015-02-13 16:16:18 -08:00
xpub: x.xpub,
2015-02-13 16:00:12 -08:00
};
});
};
TxProposal.prototype.getBitcoreTx = function() {
2015-02-05 10:50:18 -08:00
var self = this;
2015-03-12 07:49:01 -07:00
var t = WalletUtils.buildTx(this);
2015-02-13 16:00:12 -08:00
var sigs = this._getCurrentSignatures();
_.each(sigs, function(x) {
2015-02-13 16:16:18 -08:00
self._addSignaturesToBitcoreTx(t, x.signatures, x.xpub);
2015-02-13 16:00:12 -08:00
});
2015-02-05 10:50:18 -08:00
return t;
2015-02-04 16:38:23 -08:00
};
2015-02-06 10:15:54 -08:00
TxProposal.prototype.getNetworkName = function() {
var someAddress = this.toAddress || this.outputs[0].toAddress;
return Bitcore.Address(someAddress).toObject().network;
2015-02-06 10:15:54 -08:00
};
2015-02-04 16:38:23 -08:00
2015-02-05 12:22:38 -08:00
TxProposal.prototype.getRawTx = function() {
var t = this.getBitcoreTx();
2015-02-05 12:22:38 -08:00
2015-03-04 08:37:00 -08:00
return t.uncheckedSerialize();
2015-02-05 12:22:38 -08:00
};
2015-07-27 08:19:27 -07:00
TxProposal.prototype.getEstimatedSize = function() {
2015-07-27 04:49:54 -07:00
// Note: found empirically based on all multisig P2SH inputs and within m & n allowed limits.
var safetyMargin = 0.05;
var walletM = this.requiredSignatures;
var overhead = 4 + 4 + 9 + 9;
2015-07-27 05:00:37 -07:00
var inputSize = walletM * 72 + this.walletN * 36 + 44;
2015-07-27 04:49:54 -07:00
var outputSize = 34;
var nbInputs = this.inputs.length;
var nbOutputs = (_.isArray(this.outputs) ? this.outputs.length : 1) + 1;
var size = overhead + inputSize * nbInputs + outputSize * nbOutputs;
2015-07-27 08:19:27 -07:00
return parseInt((size * (1 + safetyMargin)).toFixed(0));
};
TxProposal.prototype.estimateFee = function() {
var size = this.getEstimatedSize();
var fee = this.feePerKb * size / 1000;
2015-07-27 04:49:54 -07:00
// Round up to nearest bit
this.fee = parseInt((Math.ceil(fee / 100) * 100).toFixed(0));
};
/**
* getTotalAmount
*
* @return {Number} total amount of all outputs excluding change output
*/
TxProposal.prototype.getTotalAmount = function() {
if (this.type == TxProposal.Types.MULTIPLEOUTPUTS) {
2015-06-25 07:03:11 -07:00
return _.pluck(this.outputs, 'amount')
.reduce(function(total, n) {
return total + n;
});
} else {
return this.amount;
}
};
2015-02-05 12:22:38 -08:00
2015-02-10 11:55:00 -08:00
/**
* getActors
*
* @return {String[]} copayerIds that performed actions in this proposal (accept / reject)
*/
2015-02-10 11:30:58 -08:00
TxProposal.prototype.getActors = function() {
2015-02-20 07:33:46 -08:00
return _.pluck(this.actions, 'copayerId');
2015-02-10 11:30:58 -08:00
};
2015-02-10 11:55:00 -08:00
/**
* getApprovers
*
* @return {String[]} copayerIds that approved the tx proposal (accept)
*/
TxProposal.prototype.getApprovers = function() {
return _.pluck(
_.filter(this.actions, {
type: 'accept'
}), 'copayerId');
};
2015-02-10 11:55:00 -08:00
/**
* getActionBy
*
* @param {String} copayerId
* @return {Object} type / createdOn
*/
TxProposal.prototype.getActionBy = function(copayerId) {
2015-02-20 07:33:46 -08:00
return _.find(this.actions, {
copayerId: copayerId
});
2015-02-10 11:55:00 -08:00
};
2015-02-15 10:46:29 -08:00
TxProposal.prototype.addAction = function(copayerId, type, comment, signatures, xpub) {
2015-02-17 16:20:08 -08:00
var action = TxProposalAction.create({
2015-02-02 12:07:18 -08:00
copayerId: copayerId,
type: type,
2015-02-05 10:50:18 -08:00
signatures: signatures,
2015-02-13 16:00:12 -08:00
xpub: xpub,
2015-02-15 10:46:29 -08:00
comment: comment,
2015-02-02 12:07:18 -08:00
});
2015-02-20 07:33:46 -08:00
this.actions.push(action);
2015-02-02 12:07:18 -08:00
this._updateStatus();
2015-01-28 11:40:07 -08:00
};
2015-02-13 16:00:12 -08:00
TxProposal.prototype._addSignaturesToBitcoreTx = function(t, signatures, xpub) {
2015-02-05 10:50:18 -08:00
var self = this;
2015-02-04 16:38:23 -08:00
2015-02-05 10:50:18 -08:00
if (signatures.length != this.inputs.length)
2015-02-16 09:27:01 -08:00
throw new Error('Number of signatures does not match number of inputs');
2015-02-05 10:50:18 -08:00
2015-02-23 10:54:57 -08:00
var i = 0,
2015-02-05 10:50:18 -08:00
x = new Bitcore.HDPublicKey(xpub);
_.each(signatures, function(signatureHex) {
var input = self.inputs[i];
try {
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
var pub = x.derive(self.inputPaths[i]).publicKey;
var s = {
inputIndex: i,
signature: signature,
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
publicKey: pub,
};
2015-02-24 07:27:44 -08:00
t.inputs[i].addSignature(t, s);
2015-02-05 10:50:18 -08:00
i++;
2015-02-13 16:00:12 -08:00
} catch (e) {};
2015-02-05 10:50:18 -08:00
});
2015-02-13 16:00:12 -08:00
2015-02-23 10:54:57 -08:00
if (i != t.inputs.length)
2015-02-16 09:27:01 -08:00
throw new Error('Wrong signatures');
2015-02-04 16:38:23 -08:00
};
2015-02-05 10:50:18 -08:00
2015-02-13 16:00:12 -08:00
TxProposal.prototype.sign = function(copayerId, signatures, xpub) {
try {
2015-02-16 09:27:01 -08:00
// Tests signatures are OK
var t = this.getBitcoreTx();
2015-02-13 16:00:12 -08:00
this._addSignaturesToBitcoreTx(t, signatures, xpub);
2015-02-16 09:27:01 -08:00
2015-02-15 10:46:29 -08:00
this.addAction(copayerId, 'accept', null, signatures, xpub);
2015-02-13 16:00:12 -08:00
return true;
} catch (e) {
return false;
}
2015-01-28 11:40:07 -08:00
};
2015-02-15 10:46:29 -08:00
TxProposal.prototype.reject = function(copayerId, reason) {
this.addAction(copayerId, 'reject', reason);
2015-01-28 11:40:07 -08:00
};
2015-02-06 12:04:10 -08:00
TxProposal.prototype.isPending = function() {
2015-02-15 11:15:45 -08:00
return !_.contains(['broadcasted', 'rejected'], this.status);
2015-02-06 12:04:10 -08:00
};
2015-02-05 10:50:18 -08:00
TxProposal.prototype.isAccepted = function() {
2015-02-20 07:33:46 -08:00
var votes = _.countBy(this.actions, 'type');
2015-02-02 12:07:18 -08:00
return votes['accept'] >= this.requiredSignatures;
2015-01-28 11:40:07 -08:00
};
2015-02-05 10:50:18 -08:00
TxProposal.prototype.isRejected = function() {
2015-02-20 07:33:46 -08:00
var votes = _.countBy(this.actions, 'type');
2015-02-09 07:27:50 -08:00
return votes['reject'] >= this.requiredRejections;
2015-01-28 11:40:07 -08:00
};
2015-02-15 11:15:45 -08:00
TxProposal.prototype.isBroadcasted = function() {
return this.status == 'broadcasted';
};
2015-02-05 10:50:18 -08:00
TxProposal.prototype.setBroadcasted = function(txid) {
2015-02-02 12:07:18 -08:00
this.txid = txid;
this.status = 'broadcasted';
2015-02-24 07:27:44 -08:00
this.broadcastedOn = Math.floor(Date.now() / 1000);
2015-01-28 11:40:07 -08:00
};
2015-01-27 11:40:27 -08:00
module.exports = TxProposal;