UNPKG

six-caver-js

Version:

caver-js is a JavaScript API library that allows developers to interact with a Klaytn node

349 lines (290 loc) 12.7 kB
/* Copyright 2020 The caver-js Authors This file is part of the caver-js library. The caver-js library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The caver-js library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the caver-js. If not, see <http://www.gnu.org/licenses/>. */ const _ = require('lodash') const Bytes = require('eth-lib/lib/bytes') const RLP = require('eth-lib/lib/rlp') const Hash = require('eth-lib/lib/hash') const TransactionHasher = require('../transactionHasher/transactionHasher') const utils = require('../../../caver-utils') const Keyring = require('../../../caver-wallet/src/keyring/keyringFactory') const SingleKeyring = require('../../../caver-wallet/src/keyring/singleKeyring') const MultipleKeyring = require('../../../caver-wallet/src/keyring/multipleKeyring') const RoleBasedKeyring = require('../../../caver-wallet/src/keyring/roleBasedKeyring') const { TX_TYPE_STRING, refineSignatures, typeDetectionFromRLPEncoding } = require('../transactionHelper/transactionHelper') const { KEY_ROLE } = require('../../../caver-wallet/src/keyring/keyringHelper') const { validateParams } = require('../../../caver-core-helpers/src/validateFunction') const SignatureData = require('../../../caver-wallet/src/keyring/signatureData') /** * Abstract class that implements common logic for each transaction type. * @class */ class AbstractTransaction { /** * Abstract class that implements common logic for each transaction type. * In this constructor, type, tag, nonce, gasPrice, chainId, gas and signatures are set as transaction member variables. * * @constructor * @param {string} typeString - The type string of transaction. * @param {object} createTxObj - The parameters to create a transaction instance. */ constructor(typeString, createTxObj) { this._type = typeString createTxObj.type = typeString const err = validateParams(createTxObj) if (err) throw err this.from = createTxObj.from this.gas = createTxObj.gas // The variables below are values that the user does not need to pass to the parameter. if (createTxObj.nonce !== undefined) this.nonce = createTxObj.nonce if (createTxObj.gasPrice !== undefined) this.gasPrice = createTxObj.gasPrice if (createTxObj.chainId !== undefined) this.chainId = createTxObj.chainId this.signatures = createTxObj.signatures || [] } /** * @type {string} */ get type() { return this._type } /** * @type {string} */ get from() { return this._from } set from(address) { if ( this.type === TX_TYPE_STRING.TxTypeLegacyTransaction && (address === '0x' || address === '0x0000000000000000000000000000000000000000') ) { this._from = address.toLowerCase() } else { if (!utils.isAddress(address)) throw new Error(`Invalid address ${address}`) this._from = address.toLowerCase() } } /** * @type {string} */ get nonce() { return this._nonce } set nonce(n) { this._nonce = utils.numberToHex(n) } /** * @type {string} */ get gas() { return this._gas } set gas(g) { this._gas = utils.numberToHex(g) } /** * @type {string} */ get gasPrice() { return this._gasPrice } set gasPrice(g) { this._gasPrice = utils.numberToHex(g) } /** * @type {string} */ get chainId() { return this._chainId } set chainId(ch) { this._chainId = utils.toHex(ch) } /** * @type {Array<string>|Array.<Array<string>>} */ get signatures() { return this._signatures } set signatures(sigs) { this._signatures = refineSignatures(sigs, this.type === TX_TYPE_STRING.TxTypeLegacyTransaction) } /** * Returns the RLP-encoded string of this transaction (i.e., rawTransaction). * This method has to be overrided in classes which extends AbstractTransaction. * * @return {string} */ getRLPEncoding() { throw new Error(`Not implemented.`) } /** * Returns the RLP-encoded string to make the signature of this transaction. * This method has to be overrided in classes which extends AbstractTransaction. * getCommonRLPEncodingForSignature is used in getRLPEncodingForSignature. * * @return {string} */ getCommonRLPEncodingForSignature() { throw new Error(`Not implemented.`) } /** * Signs to the transaction with private key(s) in the `key`. * @async * @param {Keyring|string} key - The instance of Keyring, private key string or KlaytnWalletKey string. * @param {number} [index] - The index of private key to use. If index is undefined, all private keys in keyring will be used. * @param {function} [hasher] - The function to get hash of transaction. In order to use a custom hasher, the index must be defined. * @return {Transaction} */ async sign(key, index, hasher = TransactionHasher.getHashForSignature) { // User parameter input cases // (key) / (key index) / (key hasher) / (key index hasher) if (_.isFunction(index)) { hasher = index index = undefined } let keyring = key if (_.isString(key)) { keyring = Keyring.createFromPrivateKey(key) } if (!(keyring instanceof SingleKeyring) && !(keyring instanceof MultipleKeyring) && !(keyring instanceof RoleBasedKeyring)) throw new Error( `Unsupported key type. The key must be a single private key string, KlaytnWalletKey string, or Keyring instance.` ) // When user attempt to sign with a updated keyring into a TxTypeLegacyTransaction error should be thrown. if (this.type === TX_TYPE_STRING.TxTypeLegacyTransaction && keyring.isDecoupled()) throw new Error(`A legacy transaction cannot be signed with a decoupled keyring.`) if (!this.from || this.from === '0x' || this.from === '0x0000000000000000000000000000000000000000') this.from = keyring.address if (this.from.toLowerCase() !== keyring.address.toLowerCase()) throw new Error(`The from address of the transaction is different with the address of the keyring to use.`) await this.fillTransaction() const hash = hasher(this) const role = this.type.includes('AccountUpdate') ? KEY_ROLE.roleAccountUpdateKey : KEY_ROLE.roleTransactionKey const sig = keyring.sign(hash, this.chainId, role, index) this.appendSignatures(sig) return this } /** * Appends signatures to the transaction. * * @param {SignatureData|Array.<SignatureData>|Array.<string>|Array.<Array.<string>>} signatures - An array of signatures to append to the transaction. * One signature can be defined in the form of a one-dimensional array or two-dimensional array, * and more than one signatures should be defined in the form of a two-dimensional array. */ appendSignatures(signatures) { let sig = signatures if (_.isString(sig)) sig = utils.resolveSignature(sig) if (sig instanceof SignatureData) sig = [sig] if (!_.isArray(sig)) throw new Error(`Failed to append signatures: invalid signatures format ${sig}`) if (_.isString(sig[0])) sig = [sig] this.signatures = this.signatures.concat(sig) } /** * Combines RLP-encoded transactions (rawTransaction) to the transaction from RLP-encoded transaction strings and returns a single transaction with all signatures combined. * When combining the signatures into a transaction instance, * an error is thrown if the decoded transaction contains different value except signatures. * * @param {Array.<string>} rlpEncodedTxs - An array of RLP-encoded transaction strings. * @return {string} */ combineSignedRawTransactions(rlpEncodedTxs) { if (!_.isArray(rlpEncodedTxs)) throw new Error(`The parameter must be an array of RLP-encoded transaction strings.`) // If the signatures are empty, there may be an undefined member variable. // In this case, the empty information is filled with the decoded result. let fillVariables = false if (utils.isEmptySig(this.signatures)) fillVariables = true for (const encoded of rlpEncodedTxs) { const type = typeDetectionFromRLPEncoding(encoded) if (this.type !== type) throw new Error(`Transaction type mismatch: Signatures from different transactions cannot be combined.`) const decoded = this.constructor.decode(encoded) // Signatures can only be combined for the same transaction. // Therefore, compare whether the decoded transaction is the same as this. for (const k in decoded) { if (k === '_signatures' || k === '_feePayerSignatures') continue if (this[k] === undefined && fillVariables) this[k] = decoded[k] const differentTxError = `Transactions containing different information cannot be combined.` // Compare with the RLP-encoded accountKey string, because 'account' is an object. if (k === '_account') { if (this[k].getRLPEncodingAccountKey() !== decoded[k].getRLPEncodingAccountKey()) throw new Error(differentTxError) continue } if (this[k] !== decoded[k]) throw new Error(differentTxError) } this.appendSignatures(decoded.signatures) } return this.getRLPEncoding() } /** * Returns RawTransaction(RLP-encoded transaction string) * * @return {string} */ getRawTransaction() { return this.getRLPEncoding() } /** * Returns a hash string of transaction * * @return {string} */ getTransactionHash() { return Hash.keccak256(this.getRLPEncoding()) } /** * Returns a senderTxHash of transaction * * @return {string} */ getSenderTxHash() { return this.getTransactionHash() } /** * Returns an RLP-encoded transaction string for making signature * * @return {string} */ getRLPEncodingForSignature() { this.validateOptionalValues() if (this.chainId === undefined) throw new Error(`chainId is undefined. Define chainId in transaction or use 'transaction.fillTransaction' to fill values.`) return RLP.encode([this.getCommonRLPEncodingForSignature(), Bytes.fromNat(this.chainId), '0x', '0x']) } /** * Fills empty optional transaction properties(gasPrice, nonce, chainId). */ async fillTransaction() { const [chainId, gasPrice, nonce] = await Promise.all([ isNot(this.chainId) ? AbstractTransaction._klaytnCall.getChainId() : this.chainId, isNot(this.gasPrice) ? AbstractTransaction._klaytnCall.getGasPrice() : this.gasPrice, isNot(this.nonce) ? AbstractTransaction._klaytnCall.getTransactionCount(this.from, 'pending') : this.nonce, ]) this.chainId = chainId this.gasPrice = gasPrice this.nonce = nonce } /** * Checks that member variables that can be defined by the user are defined. * If there is an undefined variable, an error occurs. */ validateOptionalValues() { if (this.gasPrice === undefined) throw new Error(`gasPrice is undefined. Define gasPrice in transaction or use 'transaction.fillTransaction' to fill values.`) if (this.nonce === undefined) throw new Error(`nonce is undefined. Define nonce in transaction or use 'transaction.fillTransaction' to fill values.`) } } const isNot = function(value) { return _.isUndefined(value) || _.isNull(value) } module.exports = AbstractTransaction