pchainjs-tx
Version:
An simple module for creating, manipulating and signing pchain transactions
315 lines (280 loc) • 8.59 kB
JavaScript
'use strict';
const ethUtil = require('ethereumjs-util');
const fees = require('ethereum-common/params.json');
const BN = ethUtil.BN;
const BigNumber = require('bignumber.js');
// secp256k1n/2
const N_DIV_2 = new BN('7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', 16);
/**
* Creates a new transaction object.
*
* @example
* var rawTx = {
* nonce: '0x00',
* gasPrice: '0x09184e72a000',
* gasLimit: '0x2710',
* to: '0x0000000000000000000000000000000000000000',
* value: '0x00',
* data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057',
* v: '0x1c',
* r: '0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab',
* s: '0x5bd428537f05f9830e93792f90ea6a3e2d1e 0e84952dd96edbae9f658f831ab13'
* };
* 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 ether 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 {String} data.chainId mainChain :"pchain",childChain 1 :"child_0"
* */
class Transaction {
constructor(data) {
data = data || {};
// Define Properties
const fields = [{
name: 'nonce',
length: 32,
allowLess: true,
default: new Buffer([])
}, {
name: 'gasPrice',
length: 32,
allowLess: true,
default: new Buffer([])
}, {
name: 'gasLimit',
alias: 'gas',
length: 32,
allowLess: true,
default: new Buffer([])
}, {
name: 'to',
allowZero: true,
length: 20,
default: new Buffer([])
}, {
name: 'value',
length: 32,
allowLess: true,
default: new Buffer([])
}, {
name: 'data',
alias: 'input',
allowZero: true,
default: new Buffer([])
}, {
name: 'v',
allowZero: true,
default: new Buffer([0x1c])
}, {
name: 'r',
length: 32,
allowZero: true,
allowLess: true,
default: new Buffer([])
}, {
name: 's',
length: 32,
allowZero: true,
allowLess: true,
default: new Buffer([])
}];
/**
* Returns the rlp encoding of the transaction
* @method serialize
* @return {Buffer}
* @memberof Transaction
* @name serialize
*/
// attached serialize
ethUtil.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
let sigV = ethUtil.bufferToInt(this.v);
let chainId = Math.floor((sigV - 35) / 2);
if (chainId < 0) chainId = 0;
this._chainName = data.chainId;
if (data.chainId) data.chainId = "0x" + ethUtil.keccak256(data.chainId).toString("hex");
// set chainId
this._chainId = chainId || data.chainId || 0;
this._homestead = true;
}
/**
* If the tx's `to` is to the creation address
* @return {Boolean}
*/
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}
*/
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
let items;
if (includeSignature) {
items = this.raw;
} else {
if (this._chainId > 0) {
const 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 ethUtil.rlphash(items);
}
/**
* returns chain ID
* @return {Buffer}
*/
getChainId() {
return this._chainName;
}
/**
* returns the sender's address
* @return {Buffer}
*/
getSenderAddress() {
if (this._from) {
return this._from;
}
const pubkey = this.getSenderPublicKey();
this._from = ethUtil.publicToAddress(pubkey);
return this._from;
}
/**
* returns the public key of the sender
* @return {Buffer}
*/
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}
*/
verifySignature() {
const 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 {
let v = new BigNumber("0x" + this.v.toString("hex"), 16);
if (this._chainId > 0) {
const MyChainId = new BigNumber(this._chainId);
var tarV = v.minus(8).minus(MyChainId.times(2));
v = tarV;
}
this._senderPubKey = ethUtil.ecrecover(msgHash, v, this.r, this.s);
} catch (e) {
console.log(e);
return false;
}
return !!this._senderPubKey;
}
/**
* sign a transaction with a given private key
* @param {Buffer} privateKey
*/
sign(privateKey) {
const msgHash = this.hash(false);
const sig = ethUtil.ecsign(msgHash, privateKey);
if (this._chainId > 0) {
// sig.v += this._chainId * 2 + 8
const orignV = new BigNumber(sig.v);
const MyChainId = new BigNumber(this._chainId);
var tarV = orignV.plus(8).plus(MyChainId.times(2));
sig.v = "0x" + tarV.toString(16);
}
Object.assign(this, sig);
}
/**
* The amount of gas paid for the data in this tx
* @return {BN}
*/
getDataFee() {
const data = this.raw[5];
const cost = new BN(0);
for (let 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}
*/
getBaseFee() {
const 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}
*/
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 description of why the validation failed or return a Boolean
* @return {Boolean|String}
*/
validate(stringError) {
const 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(' ');
}
}
}
module.exports = Transaction;