js-conflux-sdk
Version:
JavaScript Conflux Software Development Kit
304 lines (272 loc) • 10.2 kB
JavaScript
const { keccak256, ecdsaSign, ecdsaRecover, privateKeyToAddress, publicKeyToAddress } = require('./util/sign');
const rlp = require('./util/rlp');
const format = require('./util/format');
const cfxFormat = require('./rpc/types/formatter');
const { AccessList } = require('./primitives/AccessList');
const {
TXRLP_TYPE_PREFIX_2930,
TXRLP_TYPE_PREFIX_1559,
TRANSACTION_TYPE_LEGACY,
TRANSACTION_TYPE_EIP2930,
TRANSACTION_TYPE_EIP1559,
} = require('./CONST');
/**
* @typedef {import('./rpc/types/formatter').CallRequest} TransactionMeta
*/
class Transaction {
/**
* Decode rlp encoded raw transaction hex string
* @param {string} raw - rlp encoded transaction hex string
* @returns {Transaction} A Transaction instance
*/
static decodeRaw(raw) {
const buf = format.hexBuffer(raw);
const prefix = buf.slice(0, 4);
let tx = null;
if (prefix.equals(TXRLP_TYPE_PREFIX_2930)) {
tx = Transaction.decode2930(buf.slice(4));
} else if (prefix.equals(TXRLP_TYPE_PREFIX_1559)) {
tx = Transaction.decode1559(buf.slice(4));
} else {
tx = Transaction.decodeLegacy(raw);
}
const publicKey = tx.recover();
const hexAddress = publicKeyToAddress(format.hexBuffer(publicKey));
tx.from = format.address(hexAddress, tx.chainId);
return tx;
}
static formatTxMeta({ nonce, gas, to, value, storageLimit, epochHeight, chainId, data, r, s, v }) {
const chainIdNum = format.uInt(chainId);
return {
nonce: format.bigIntFromBuffer(nonce),
gas: format.bigIntFromBuffer(gas),
to: to.length === 0 ? null : format.address(to, chainIdNum),
value: format.bigIntFromBuffer(value),
storageLimit: format.bigIntFromBuffer(storageLimit),
epochHeight: format.bigIntFromBuffer(epochHeight),
chainId: chainIdNum,
data: format.hex(data),
v: v.length === 0 ? 0 : format.uInt(v),
r: format.hex(r),
s: format.hex(s),
};
}
static decodeLegacy(raw) {
const [
[nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data],
v,
r,
s,
] = rlp.decode(raw);
const formatedMeta = Transaction.formatTxMeta({ nonce, gas, to, value, storageLimit, epochHeight, chainId, data, r, s, v });
const tx = new Transaction({
type: TRANSACTION_TYPE_LEGACY,
gasPrice: format.bigIntFromBuffer(gasPrice),
...formatedMeta,
});
return tx;
}
static decode2930(raw) {
const [
[nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data, accessList],
v,
r,
s,
] = rlp.decode(raw);
const formatedMeta = Transaction.formatTxMeta({ nonce, gas, to, value, storageLimit, epochHeight, chainId, data, r, s, v });
const tx = new Transaction({
type: TRANSACTION_TYPE_EIP2930,
gasPrice: format.bigIntFromBuffer(gasPrice),
accessList,
...formatedMeta,
});
return tx;
}
static decode1559(raw) {
const [
[nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to, value, storageLimit, epochHeight, chainId, data, accessList],
v,
r,
s,
] = rlp.decode(raw);
const formatedMeta = Transaction.formatTxMeta({ nonce, gas, to, value, storageLimit, epochHeight, chainId, data, r, s, v });
const tx = new Transaction({
type: TRANSACTION_TYPE_EIP1559,
maxPriorityFeePerGas: format.bigIntFromBuffer(maxPriorityFeePerGas),
maxFeePerGas: format.bigIntFromBuffer(maxFeePerGas),
accessList,
...formatedMeta,
});
return tx;
}
/**
* Create a transaction.
*
* @param {object} options
* @param {number} [options.type] - Tx type: 0 for legacy, 1 for EIP-2930, 2 for EIP-1559
* @param {string} [options.from] - The sender address.
* @param {string|number} [options.nonce] - This allows to overwrite your own pending transactions that use the same nonce.
* @param {string|number} [options.gasPrice] - The price of gas for this transaction in drip.
* @param {string|number} [options.gas]- The amount of gas to use for the transaction (unused gas is refunded).
* @param {string} [options.to] - The destination address of the message, left undefined for a contract-creation transaction.
* @param {string|number} [options.value] - The value transferred for the transaction in drip, also the endowment if it’s a contract-creation transaction.
* @param {string|number} [options.storageLimit] - The storage limit specified by the sender.
* @param {string|number} [options.epochHeight] - The epoch proposed by the sender. Note that this is NOT the epoch of the block containing this transaction.
* @param {string|number} [options.chainId] - The chain ID specified by the sender.
* @param {string|Buffer} [options.data]- Either a ABI byte string containing the data of the function call on a contract, or in the case of a contract-creation transaction the initialisation code.
* @param {string|Buffer} [options.r] - ECDSA signature r
* @param {string|Buffer} [options.s] - ECDSA signature s
* @param {number} [options.v] - ECDSA signature v
* @param {array} [options.accessList] - EIP-2930 access list
* @param {string|number} [options.maxPriorityFeePerGas] - EIP-1559 maxPriorityFeePerGas
* @param {string|number} [options.maxFeePerGas] - EIP-1559 maxFeePerGas
* @return {Transaction}
*/
constructor({
type,
from,
nonce,
gasPrice,
gas,
to,
value,
storageLimit,
epochHeight,
chainId,
data,
v,
r,
s,
accessList,
maxPriorityFeePerGas,
maxFeePerGas,
}) {
this.type = type;
this.from = from;
this.nonce = nonce;
this.gasPrice = gasPrice;
this.gas = gas;
this.to = to;
this.value = value;
this.storageLimit = storageLimit;
this.epochHeight = epochHeight;
this.chainId = chainId;
this.data = data;
this.v = v;
this.r = r;
this.s = s;
this.accessList = accessList ? new AccessList(accessList) : undefined;
this.maxPriorityFeePerGas = maxPriorityFeePerGas;
this.maxFeePerGas = maxFeePerGas;
}
/**
* Getter of transaction hash include signature.
*
* > Note: calculate every time.
*
* @return {string|undefined} If transaction has r,s,v return hex string, else return undefined.
*/
get hash() {
try {
return format.hex(keccak256(this.encode(true)));
} catch (e) {
return undefined;
}
}
/**
* Sign transaction and set 'r','s','v'.
*
* @param {string} privateKey - Private key hex string.
* @param {number} networkId - fullnode's network id.
* @return {Transaction}
*/
sign(privateKey, networkId) {
const privateKeyBuffer = format.hexBuffer(privateKey);
const { r, s, v } = ecdsaSign(keccak256(this.encode(false)), privateKeyBuffer);
this.r = format.hex(r);
this.s = format.hex(s);
this.v = v;
const addressBuffer = privateKeyToAddress(privateKeyBuffer);
this.from = format.address(addressBuffer, networkId || this.chainId);
return this;
}
/**
* Recover public key from signed Transaction.
*
* @return {string}
*/
recover() {
const publicKey = ecdsaRecover(keccak256(this.encode(false)), {
r: format.hexBuffer(this.r),
s: format.hexBuffer(this.s),
v: format.uInt(this.v),
});
return format.publicKey(publicKey);
}
/**
* Infer the transaction type from the fields.
* @returns {number} Transaction type
*/
txType() {
if (this.type !== undefined) {
return this.type;
} else if (this.maxPriorityFeePerGas !== undefined && this.maxFeePerGas !== undefined) {
return TRANSACTION_TYPE_EIP1559;
} else if (this.accessList !== undefined) {
return TRANSACTION_TYPE_EIP2930;
} else {
return TRANSACTION_TYPE_LEGACY;
}
}
typePrefix() {
let prefix = Buffer.from([]);
if (this.txType() === TRANSACTION_TYPE_EIP2930) {
prefix = TXRLP_TYPE_PREFIX_2930;
} else if (this.txType() === TRANSACTION_TYPE_EIP1559) {
prefix = TXRLP_TYPE_PREFIX_1559;
}
return prefix;
}
encodeAccessList() {
return this.accessList ? this.accessList.encode() : [];
}
/**
* Encode rlp.
*
* @param {boolean} [includeSignature=false] - Whether or not to include the signature.
* @return {Buffer}
*/
encode(includeSignature) {
let raw;
if (this.txType() === TRANSACTION_TYPE_LEGACY) { // legacy transaction
const { nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data, v, r, s } = cfxFormat.signTx(this);
raw = includeSignature
? [[nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data], v, r, s]
: [nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data];
} else if (this.txType() === TRANSACTION_TYPE_EIP2930) { // 2930 transaction
const { nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data, v, r, s } = cfxFormat.signTx(this);
const accessList = this.encodeAccessList();
raw = includeSignature
? [[nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data, accessList], v, r, s]
: [nonce, gasPrice, gas, to, value, storageLimit, epochHeight, chainId, data, accessList];
} else if (this.txType() === TRANSACTION_TYPE_EIP1559) { // 1559 transaction
const { nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to, value, storageLimit, epochHeight, chainId, data, v, r, s } = cfxFormat.sign1559Tx(this);
const accessList = this.encodeAccessList();
raw = includeSignature
? [[nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to, value, storageLimit, epochHeight, chainId, data, accessList], v, r, s]
: [nonce, maxPriorityFeePerGas, maxFeePerGas, gas, to, value, storageLimit, epochHeight, chainId, data, accessList];
} else {
throw new Error('Unsupported transaction type');
}
return Buffer.concat([this.typePrefix(), rlp.encode(raw)]);
}
/**
* Get the raw transaction hex string.
*
* @return {string} Hex string
*/
serialize() {
return format.hex(this.encode(true));
}
}
module.exports = Transaction;