UNPKG

pchainjs-tx

Version:

An simple module for creating, manipulating and signing pchain transactions

318 lines (282 loc) 8.55 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