UNPKG

pchainjs-tx

Version:

An simple module for creating, manipulating and signing pchain transactions

315 lines (280 loc) 8.59 kB
'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;