377 lines
14 KiB
JavaScript
377 lines
14 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BaseTransaction = void 0;
|
|
const common_1 = require("@ethereumjs/common");
|
|
const util_1 = require("@ethereumjs/util");
|
|
const types_js_1 = require("./types.js");
|
|
const util_js_1 = require("./util.js");
|
|
/**
|
|
* This base class will likely be subject to further
|
|
* refactoring along the introduction of additional tx types
|
|
* on the Ethereum network.
|
|
*
|
|
* It is therefore not recommended to use directly.
|
|
*/
|
|
class BaseTransaction {
|
|
constructor(txData, opts) {
|
|
this.cache = {
|
|
hash: undefined,
|
|
dataFee: undefined,
|
|
senderPubKey: undefined,
|
|
};
|
|
/**
|
|
* List of tx type defining EIPs,
|
|
* e.g. 1559 (fee market) and 2930 (access lists)
|
|
* for FeeMarketEIP1559Transaction objects
|
|
*/
|
|
this.activeCapabilities = [];
|
|
/**
|
|
* The default chain the tx falls back to if no Common
|
|
* is provided and if the chain can't be derived from
|
|
* a passed in chainId (only EIP-2718 typed txs) or
|
|
* EIP-155 signature (legacy txs).
|
|
*
|
|
* @hidden
|
|
*/
|
|
this.DEFAULT_CHAIN = common_1.Chain.Mainnet;
|
|
const { nonce, gasLimit, to, value, data, v, r, s, type } = txData;
|
|
this._type = Number((0, util_1.bytesToBigInt)((0, util_1.toBytes)(type)));
|
|
this.txOptions = opts;
|
|
const toB = (0, util_1.toBytes)(to === '' ? '0x' : to);
|
|
const vB = (0, util_1.toBytes)(v);
|
|
const rB = (0, util_1.toBytes)(r);
|
|
const sB = (0, util_1.toBytes)(s);
|
|
this.nonce = (0, util_1.bytesToBigInt)((0, util_1.toBytes)(nonce));
|
|
this.gasLimit = (0, util_1.bytesToBigInt)((0, util_1.toBytes)(gasLimit));
|
|
this.to = toB.length > 0 ? new util_1.Address(toB) : undefined;
|
|
this.value = (0, util_1.bytesToBigInt)((0, util_1.toBytes)(value));
|
|
this.data = (0, util_1.toBytes)(data === '' ? '0x' : data);
|
|
this.v = vB.length > 0 ? (0, util_1.bytesToBigInt)(vB) : undefined;
|
|
this.r = rB.length > 0 ? (0, util_1.bytesToBigInt)(rB) : undefined;
|
|
this.s = sB.length > 0 ? (0, util_1.bytesToBigInt)(sB) : undefined;
|
|
this._validateCannotExceedMaxInteger({ value: this.value, r: this.r, s: this.s });
|
|
// geth limits gasLimit to 2^64-1
|
|
this._validateCannotExceedMaxInteger({ gasLimit: this.gasLimit }, 64);
|
|
// EIP-2681 limits nonce to 2^64-1 (cannot equal 2^64-1)
|
|
this._validateCannotExceedMaxInteger({ nonce: this.nonce }, 64, true);
|
|
const createContract = this.to === undefined || this.to === null;
|
|
const allowUnlimitedInitCodeSize = opts.allowUnlimitedInitCodeSize ?? false;
|
|
const common = opts.common ?? this._getCommon();
|
|
if (createContract && common.isActivatedEIP(3860) && allowUnlimitedInitCodeSize === false) {
|
|
(0, util_js_1.checkMaxInitCodeSize)(common, this.data.length);
|
|
}
|
|
}
|
|
/**
|
|
* Returns the transaction type.
|
|
*
|
|
* Note: legacy txs will return tx type `0`.
|
|
*/
|
|
get type() {
|
|
return this._type;
|
|
}
|
|
/**
|
|
* Checks if a tx type defining capability is active
|
|
* on a tx, for example the EIP-1559 fee market mechanism
|
|
* or the EIP-2930 access list feature.
|
|
*
|
|
* Note that this is different from the tx type itself,
|
|
* so EIP-2930 access lists can very well be active
|
|
* on an EIP-1559 tx for example.
|
|
*
|
|
* This method can be useful for feature checks if the
|
|
* tx type is unknown (e.g. when instantiated with
|
|
* the tx factory).
|
|
*
|
|
* See `Capabilities` in the `types` module for a reference
|
|
* on all supported capabilities.
|
|
*/
|
|
supports(capability) {
|
|
return this.activeCapabilities.includes(capability);
|
|
}
|
|
/**
|
|
* Validates the transaction signature and minimum gas requirements.
|
|
* @returns {string[]} an array of error strings
|
|
*/
|
|
getValidationErrors() {
|
|
const errors = [];
|
|
if (this.isSigned() && !this.verifySignature()) {
|
|
errors.push('Invalid Signature');
|
|
}
|
|
if (this.getBaseFee() > this.gasLimit) {
|
|
errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`);
|
|
}
|
|
return errors;
|
|
}
|
|
/**
|
|
* Validates the transaction signature and minimum gas requirements.
|
|
* @returns {boolean} true if the transaction is valid, false otherwise
|
|
*/
|
|
isValid() {
|
|
const errors = this.getValidationErrors();
|
|
return errors.length === 0;
|
|
}
|
|
/**
|
|
* The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee)
|
|
*/
|
|
getBaseFee() {
|
|
const txFee = this.common.param('gasPrices', 'tx');
|
|
let fee = this.getDataFee();
|
|
if (txFee)
|
|
fee += txFee;
|
|
if (this.common.gteHardfork('homestead') && this.toCreationAddress()) {
|
|
const txCreationFee = this.common.param('gasPrices', 'txCreation');
|
|
if (txCreationFee)
|
|
fee += txCreationFee;
|
|
}
|
|
return fee;
|
|
}
|
|
/**
|
|
* The amount of gas paid for the data in this tx
|
|
*/
|
|
getDataFee() {
|
|
const txDataZero = this.common.param('gasPrices', 'txDataZero');
|
|
const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero');
|
|
let cost = util_1.BIGINT_0;
|
|
for (let i = 0; i < this.data.length; i++) {
|
|
this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero);
|
|
}
|
|
if ((this.to === undefined || this.to === null) && this.common.isActivatedEIP(3860)) {
|
|
const dataLength = BigInt(Math.ceil(this.data.length / 32));
|
|
const initCodeCost = this.common.param('gasPrices', 'initCodeWordCost') * dataLength;
|
|
cost += initCodeCost;
|
|
}
|
|
return cost;
|
|
}
|
|
/**
|
|
* If the tx's `to` is to the creation address
|
|
*/
|
|
toCreationAddress() {
|
|
return this.to === undefined || this.to.bytes.length === 0;
|
|
}
|
|
isSigned() {
|
|
const { v, r, s } = this;
|
|
if (v === undefined || r === undefined || s === undefined) {
|
|
return false;
|
|
}
|
|
else {
|
|
return true;
|
|
}
|
|
}
|
|
/**
|
|
* Determines if the signature is valid
|
|
*/
|
|
verifySignature() {
|
|
try {
|
|
// Main signature verification is done in `getSenderPublicKey()`
|
|
const publicKey = this.getSenderPublicKey();
|
|
return (0, util_1.unpadBytes)(publicKey).length !== 0;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Returns the sender's address
|
|
*/
|
|
getSenderAddress() {
|
|
return new util_1.Address((0, util_1.publicToAddress)(this.getSenderPublicKey()));
|
|
}
|
|
/**
|
|
* Signs a transaction.
|
|
*
|
|
* Note that the signed tx is returned as a new object,
|
|
* use as follows:
|
|
* ```javascript
|
|
* const signedTx = tx.sign(privateKey)
|
|
* ```
|
|
*/
|
|
sign(privateKey) {
|
|
if (privateKey.length !== 32) {
|
|
const msg = this._errorMsg('Private key must be 32 bytes in length.');
|
|
throw new Error(msg);
|
|
}
|
|
// Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature
|
|
// and want to recreate a signature (where EIP155 should be applied)
|
|
// Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail
|
|
// 2021-06-23
|
|
let hackApplied = false;
|
|
if (this.type === types_js_1.TransactionType.Legacy &&
|
|
this.common.gteHardfork('spuriousDragon') &&
|
|
!this.supports(types_js_1.Capability.EIP155ReplayProtection)) {
|
|
this.activeCapabilities.push(types_js_1.Capability.EIP155ReplayProtection);
|
|
hackApplied = true;
|
|
}
|
|
const msgHash = this.getHashedMessageToSign();
|
|
const ecSignFunction = this.common.customCrypto?.ecsign ?? util_1.ecsign;
|
|
const { v, r, s } = ecSignFunction(msgHash, privateKey);
|
|
const tx = this.addSignature(v, r, s, true);
|
|
// Hack part 2
|
|
if (hackApplied) {
|
|
const index = this.activeCapabilities.indexOf(types_js_1.Capability.EIP155ReplayProtection);
|
|
if (index > -1) {
|
|
this.activeCapabilities.splice(index, 1);
|
|
}
|
|
}
|
|
return tx;
|
|
}
|
|
/**
|
|
* Returns an object with the JSON representation of the transaction
|
|
*/
|
|
toJSON() {
|
|
return {
|
|
type: (0, util_1.bigIntToHex)(BigInt(this.type)),
|
|
nonce: (0, util_1.bigIntToHex)(this.nonce),
|
|
gasLimit: (0, util_1.bigIntToHex)(this.gasLimit),
|
|
to: this.to !== undefined ? this.to.toString() : undefined,
|
|
value: (0, util_1.bigIntToHex)(this.value),
|
|
data: (0, util_1.bytesToHex)(this.data),
|
|
v: this.v !== undefined ? (0, util_1.bigIntToHex)(this.v) : undefined,
|
|
r: this.r !== undefined ? (0, util_1.bigIntToHex)(this.r) : undefined,
|
|
s: this.s !== undefined ? (0, util_1.bigIntToHex)(this.s) : undefined,
|
|
};
|
|
}
|
|
/**
|
|
* Does chain ID checks on common and returns a common
|
|
* to be used on instantiation
|
|
* @hidden
|
|
*
|
|
* @param common - {@link Common} instance from tx options
|
|
* @param chainId - Chain ID from tx options (typed txs) or signature (legacy tx)
|
|
*/
|
|
_getCommon(common, chainId) {
|
|
// Chain ID provided
|
|
if (chainId !== undefined) {
|
|
const chainIdBigInt = (0, util_1.bytesToBigInt)((0, util_1.toBytes)(chainId));
|
|
if (common) {
|
|
if (common.chainId() !== chainIdBigInt) {
|
|
const msg = this._errorMsg(`The chain ID does not match the chain ID of Common. Got: ${chainIdBigInt}, expected: ${common.chainId()}`);
|
|
throw new Error(msg);
|
|
}
|
|
// Common provided, chain ID does match
|
|
// -> Return provided Common
|
|
return common.copy();
|
|
}
|
|
else {
|
|
if (common_1.Common.isSupportedChainId(chainIdBigInt)) {
|
|
// No Common, chain ID supported by Common
|
|
// -> Instantiate Common with chain ID
|
|
return new common_1.Common({ chain: chainIdBigInt });
|
|
}
|
|
else {
|
|
// No Common, chain ID not supported by Common
|
|
// -> Instantiate custom Common derived from DEFAULT_CHAIN
|
|
return common_1.Common.custom({
|
|
name: 'custom-chain',
|
|
networkId: chainIdBigInt,
|
|
chainId: chainIdBigInt,
|
|
}, { baseChain: this.DEFAULT_CHAIN });
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// No chain ID provided
|
|
// -> return Common provided or create new default Common
|
|
return common?.copy() ?? new common_1.Common({ chain: this.DEFAULT_CHAIN });
|
|
}
|
|
}
|
|
/**
|
|
* Validates that an object with BigInt values cannot exceed the specified bit limit.
|
|
* @param values Object containing string keys and BigInt values
|
|
* @param bits Number of bits to check (64 or 256)
|
|
* @param cannotEqual Pass true if the number also cannot equal one less the maximum value
|
|
*/
|
|
_validateCannotExceedMaxInteger(values, bits = 256, cannotEqual = false) {
|
|
for (const [key, value] of Object.entries(values)) {
|
|
switch (bits) {
|
|
case 64:
|
|
if (cannotEqual) {
|
|
if (value !== undefined && value >= util_1.MAX_UINT64) {
|
|
const msg = this._errorMsg(`${key} cannot equal or exceed MAX_UINT64 (2^64-1), given ${value}`);
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
else {
|
|
if (value !== undefined && value > util_1.MAX_UINT64) {
|
|
const msg = this._errorMsg(`${key} cannot exceed MAX_UINT64 (2^64-1), given ${value}`);
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
break;
|
|
case 256:
|
|
if (cannotEqual) {
|
|
if (value !== undefined && value >= util_1.MAX_INTEGER) {
|
|
const msg = this._errorMsg(`${key} cannot equal or exceed MAX_INTEGER (2^256-1), given ${value}`);
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
else {
|
|
if (value !== undefined && value > util_1.MAX_INTEGER) {
|
|
const msg = this._errorMsg(`${key} cannot exceed MAX_INTEGER (2^256-1), given ${value}`);
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
break;
|
|
default: {
|
|
const msg = this._errorMsg('unimplemented bits value');
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static _validateNotArray(values) {
|
|
const txDataKeys = [
|
|
'nonce',
|
|
'gasPrice',
|
|
'gasLimit',
|
|
'to',
|
|
'value',
|
|
'data',
|
|
'v',
|
|
'r',
|
|
's',
|
|
'type',
|
|
'baseFee',
|
|
'maxFeePerGas',
|
|
'chainId',
|
|
];
|
|
for (const [key, value] of Object.entries(values)) {
|
|
if (txDataKeys.includes(key)) {
|
|
if (Array.isArray(value)) {
|
|
throw new Error(`${key} cannot be an array`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Returns the shared error postfix part for _error() method
|
|
* tx type implementations.
|
|
*/
|
|
_getSharedErrorPostfix() {
|
|
let hash = '';
|
|
try {
|
|
hash = this.isSigned() ? (0, util_1.bytesToHex)(this.hash()) : 'not available (unsigned)';
|
|
}
|
|
catch (e) {
|
|
hash = 'error';
|
|
}
|
|
let isSigned = '';
|
|
try {
|
|
isSigned = this.isSigned().toString();
|
|
}
|
|
catch (e) {
|
|
hash = 'error';
|
|
}
|
|
let hf = '';
|
|
try {
|
|
hf = this.common.hardfork();
|
|
}
|
|
catch (e) {
|
|
hf = 'error';
|
|
}
|
|
let postfix = `tx type=${this.type} hash=${hash} nonce=${this.nonce} value=${this.value} `;
|
|
postfix += `signed=${isSigned} hf=${hf}`;
|
|
return postfix;
|
|
}
|
|
}
|
|
exports.BaseTransaction = BaseTransaction;
|
|
//# sourceMappingURL=baseTransaction.js.map
|