icjs-tx
Version:
An simple module for creating, manipulating and signing irchain transactions
340 lines (297 loc) • 9.71 kB
JavaScript
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var ircUtil = require('icjs-util');
var fees = require('icjs-common/params.json');
var BN = ircUtil.BN;
// secp256k1n/2
var N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16);
/**
* Creates a new transaction object.
*
* @example
* var rawTx = {
* nonce: '00',
* gasPrice: '09184e72a000',
* gasLimit: '2710',
* to: '0000000000000000000000000000000000000000',
* value: '00',
* data: '7f7465737432000000000000000000000000000000000000000000000000000000600057',
* v: '1c',
* r: '5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
* s: '5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13'
* };
* var tx = new Transaction(rawTx);
*
* @class
* @param {Buffer | Array | Object} data a transaction can be initiailized with either a buffer containing the RLP serialized transaction or an array of buffers relating to each of the tx Properties, listed in order below in the exmple.
*
* Or lastly an Object containing the Properties of the transaction like in the Usage example.
*
* For Object and Arrays each of the elements can either be a Buffer, a hex-prefixed (0x) String , Number, or an object with a toBuffer method such as Bignum
*
* @property {Buffer} raw The raw rlp encoded transaction
* @param {Buffer} data.nonce nonce number
* @param {Buffer} data.gasLimit transaction gas limit
* @param {Buffer} data.gasPrice transaction gas price
* @param {Buffer} data.to to the to address
* @param {Buffer} data.value the amount of irc sent
* @param {Buffer} data.data this will contain the data of the message or the init of a contract
* @param {Buffer} data.v EC recovery ID
* @param {Buffer} data.r EC signature parameter
* @param {Buffer} data.s EC signature parameter
* @param {Number} data.chainId EIP 155 chainId - mainnet: 1, ropsten: 3
*/
var Transaction = function () {
function Transaction(data) {
_classCallCheck(this, Transaction);
data = data || {};
// Define Properties
var fields = [{
name: 'nonce',
length: 32,
allowLess: true,
default: Buffer.from([])
}, {
name: 'gasPrice',
length: 32,
allowLess: true,
default: Buffer.from([])
}, {
name: 'gasLimit',
alias: 'gas',
length: 32,
allowLess: true,
default: Buffer.from([])
}, {
name: 'to',
allowZero: true,
length: 20,
default: Buffer.from([])
}, {
name: 'value',
length: 32,
allowLess: true,
default: Buffer.from([])
}, {
name: 'data',
alias: 'input',
allowZero: true,
default: Buffer.from([])
}, {
name: 'v',
allowZero: true,
default: Buffer.from([0x1c])
}, {
name: 'r',
length: 32,
allowZero: true,
allowLess: true,
default: Buffer.from([])
}, {
name: 's',
length: 32,
allowZero: true,
allowLess: true,
default: Buffer.from([])
}];
/**
* Returns the rlp encoding of the transaction
* @method serialize
* @return {Buffer}
* @memberof Transaction
* @name serialize
*/
// attached serialize
ircUtil.defineProperties(this, fields, data);
/**
* @property {Buffer} from (read only) sender address of this transaction, mathematically derived from other parameters.
* @name from
* @memberof Transaction
*/
Object.defineProperty(this, 'from', {
enumerable: true,
configurable: true,
get: this.getSenderAddress.bind(this)
});
// calculate chainId from signature
var sigV = ircUtil.bufferToInt(this.v);
var chainId = Math.floor((sigV - 35) / 2);
if (chainId < 0) chainId = 0;
// set chainId
this._chainId = chainId || data.chainId || 0;
this._homestead = true;
}
/**
* If the tx's `to` is to the creation address
* @return {Boolean}
*/
_createClass(Transaction, [{
key: 'toCreationAddress',
value: function toCreationAddress() {
return this.to.toString('hex') === '';
}
/**
* Computes a sha3-256 hash of the serialized tx
* @param {Boolean} [includeSignature=true] whether or not to inculde the signature
* @return {Buffer}
*/
}, {
key: 'hash',
value: function hash(includeSignature) {
if (includeSignature === undefined) includeSignature = true;
// EIP155 spec:
// when computing the hash of a transaction for purposes of signing or recovering,
// instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data),
// hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0
var items = void 0;
if (includeSignature) {
items = this.raw;
} else {
if (this._chainId > 0) {
var raw = this.raw.slice();
this.v = this._chainId;
this.r = 0;
this.s = 0;
items = this.raw;
this.raw = raw;
} else {
items = this.raw.slice(0, 6);
}
}
// create hash
return ircUtil.rlphash(items);
}
/**
* returns the public key of the sender
* @return {Buffer}
*/
}, {
key: 'getChainId',
value: function getChainId() {
return this._chainId;
}
/**
* returns the sender's address
* @return {Buffer}
*/
}, {
key: 'getSenderAddress',
value: function getSenderAddress() {
if (this._from) {
return this._from;
}
var pubkey = this.getSenderPublicKey();
this._from = ircUtil.publicToAddress(pubkey);
return this._from;
}
/**
* returns the public key of the sender
* @return {Buffer}
*/
}, {
key: 'getSenderPublicKey',
value: function getSenderPublicKey() {
if (!this._senderPubKey || !this._senderPubKey.length) {
if (!this.verifySignature()) throw new Error('Invalid Signature');
}
return this._senderPubKey;
}
/**
* Determines if the signature is valid
* @return {Boolean}
*/
}, {
key: 'verifySignature',
value: function verifySignature() {
var msgHash = this.hash(false);
// All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid.
if (this._homestead && new BN(this.s).cmp(N_DIV_2) === 1) {
return false;
}
try {
var v = ircUtil.bufferToInt(this.v);
if (this._chainId > 0) {
v -= this._chainId * 2 + 8;
}
this._senderPubKey = ircUtil.ecrecover(msgHash, v, this.r, this.s);
} catch (e) {
return false;
}
return !!this._senderPubKey;
}
/**
* sign a transaction with a given a private key
* @param {Buffer} privateKey
*/
}, {
key: 'sign',
value: function sign(privateKey) {
var msgHash = this.hash(false);
var sig = ircUtil.ecsign(msgHash, privateKey);
if (this._chainId > 0) {
sig.v += this._chainId * 2 + 8;
}
Object.assign(this, sig);
}
/**
* The amount of gas paid for the data in this tx
* @return {BN}
*/
}, {
key: 'getDataFee',
value: function getDataFee() {
var data = this.raw[5];
var cost = new BN(0);
for (var i = 0; i < data.length; i++) {
data[i] === 0 ? cost.iaddn(fees.txDataZeroGas.v) : cost.iaddn(fees.txDataNonZeroGas.v);
}
return cost;
}
/**
* the minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee)
* @return {BN}
*/
}, {
key: 'getBaseFee',
value: function getBaseFee() {
var fee = this.getDataFee().iaddn(fees.txGas.v);
if (this._homestead && this.toCreationAddress()) {
fee.iaddn(fees.txCreation.v);
}
return fee;
}
/**
* the up front amount that an account must have for this transaction to be valid
* @return {BN}
*/
}, {
key: 'getUpfrontCost',
value: function getUpfrontCost() {
return new BN(this.gasLimit).imul(new BN(this.gasPrice)).iadd(new BN(this.value));
}
/**
* validates the signature and checks to see if it has enough gas
* @param {Boolean} [stringError=false] whether to return a string with a dscription of why the validation failed or return a Bloolean
* @return {Boolean|String}
*/
}, {
key: 'validate',
value: function validate(stringError) {
var errors = [];
if (!this.verifySignature()) {
errors.push('Invalid Signature');
}
if (this.getBaseFee().cmp(new BN(this.gasLimit)) > 0) {
errors.push(['gas limit is too low. Need at least ' + this.getBaseFee()]);
}
if (stringError === undefined || stringError === false) {
return errors.length === 0;
} else {
return errors.join(' ');
}
}
}]);
return Transaction;
}();
module.exports = Transaction;