@kaiachain/web3js-ext
Version:
web3.js extension for kaiachain blockchain
221 lines (220 loc) • 12 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports._parseTxType = exports.bufferedGasLimit = exports.prepareTransaction = exports.signTransactionAsFeePayer = exports.context_signTransactionAsFeePayer = exports.context_signTransaction = void 0;
const js_ext_core_1 = require("@kaiachain/js-ext-core");
const lodash_1 = __importDefault(require("lodash"));
const web3_errors_1 = require("web3-errors");
const web3_eth_1 = require("web3-eth");
const web3_eth_accounts_1 = require("web3-eth-accounts");
const web3_utils_1 = require("web3-utils");
const web3_validator_1 = require("web3-validator");
const transaction_builder_js_1 = require("../eth/utils/transaction_builder.js");
const klaytn_tx_js_1 = require("./klaytn_tx.js");
// Analogous to web3/src/accounts.ts:signTransactionWithContext
function context_signTransaction(context) {
return async (transaction, privateKey) => {
const tx = resolveTransaction(transaction);
const priv = (0, web3_utils_1.bytesToHex)(privateKey);
const preparedTx = await prepareTransaction(tx, context, privateKey);
// Relies on the original web3-eth-accounts:signTransaction()
// Klaytn-specific logic are realized in `preparedTx` of class KlaytnTypedTransaction.
return (0, web3_eth_accounts_1.signTransaction)(preparedTx, priv);
};
}
exports.context_signTransaction = context_signTransaction;
// Analogous to web3/src/accounts.ts:signTransactionWithContext
// but instead calls signTransactionAsFeePayer.
function context_signTransactionAsFeePayer(context) {
return async (transaction, privateKey) => {
const tx = resolveTransaction(transaction);
const priv = (0, web3_utils_1.bytesToHex)(privateKey);
if (!(0, js_ext_core_1.isFeePayerSigTxType)(_parseTxType(tx.type))) {
throw new Error(`signTransactionAsFeePayer not supported for tx type ${tx.type}`);
}
populateFeePayerAndSignatures(tx, (0, web3_eth_accounts_1.privateKeyToAddress)(privateKey));
const preparedTx = await prepareTransaction(tx, context, privateKey);
// Not using the original web3-eth-accounts:signTransaction()
// Use the separate signTransactionAsFeePayer() instead.
return signTransactionAsFeePayer(preparedTx, priv);
};
}
exports.context_signTransactionAsFeePayer = context_signTransactionAsFeePayer;
// Convert 'Transaction | string' type to 'Transaction' by decoding the RLP string.
function resolveTransaction(transaction) {
if (lodash_1.default.isString(transaction)) {
return js_ext_core_1.KlaytnTxFactory.fromRLP(transaction).toObject();
}
else {
return transaction;
}
}
// Fill feePayer field if empty. Filters out dummy feePayer and feePayerSignatures from caver.
async function populateFeePayerAndSignatures(transaction, expectedFeePayer) {
// A SenderTxHashRLP returned from caver may have dummy feePayer even if SenderTxHashRLP shouldn't have feePayer.
// So ignore AddressZero in the feePayer field.
if (!transaction.feePayer || transaction.feePayer == "0x0000000000000000000000000000000000000000") {
transaction.feePayer = expectedFeePayer;
}
// A SenderTxHashRLP returned from caver may have dummy feePayerSignatures if SenderTxHashRLP shouldn't have feePayerSignatures.
// So ignore [ '0x01', '0x', '0x' ] in the feePayerSignatures field.
if (lodash_1.default.isArray(transaction.feePayerSignatures)) {
transaction.feePayerSignatures = transaction.feePayerSignatures.filter((sig) => {
return !(lodash_1.default.isArray(sig) && sig.length == 3 && sig[0] == "0x01" && sig[1] == "0x" && sig[2] == "0x");
});
}
}
// Analogous to web3-eth-accounts:signTransaction,
// but instead calls KlaytnTx.*AsFeePayer methods.
async function signTransactionAsFeePayer(transaction, privateKey) {
if (!(transaction instanceof klaytn_tx_js_1.KlaytnTypedTransaction)) {
throw new Error("attempted signTransactionAsFeePayer with non-klaytn tx");
}
const signedTx = transaction.signAsFeePayer((0, web3_utils_1.hexToBytes)(privateKey));
if ((0, web3_validator_1.isNullish)(signedTx.feePayer_v) || (0, web3_validator_1.isNullish)(signedTx.feePayer_r) || (0, web3_validator_1.isNullish)(signedTx.feePayer_s)) {
throw new web3_errors_1.TransactionSigningError("Signer Error");
}
const validationErrors = signedTx.validate(true);
if (validationErrors.length > 0) {
let errorString = "Signer Error ";
for (const validationError of validationErrors) {
errorString += `${errorString} ${validationError}.`;
}
throw new web3_errors_1.TransactionSigningError(errorString);
}
const rawTx = (0, web3_utils_1.bytesToHex)(signedTx.serializeAsFeePayer());
const txHash = (0, web3_utils_1.sha3Raw)(rawTx); // using keccak in web3-utils.sha3Raw instead of SHA3 (NIST Standard) as both are different
return {
messageHash: (0, web3_utils_1.bytesToHex)(signedTx.getMessageToSignAsFeePayer(true)),
v: `0x${signedTx.feePayer_v.toString(16)}`,
r: `0x${signedTx.feePayer_r.toString(16).padStart(64, "0")}`,
s: `0x${signedTx.feePayer_s.toString(16).padStart(64, "0")}`,
rawTransaction: rawTx,
transactionHash: (0, web3_utils_1.bytesToHex)(txHash),
};
}
exports.signTransactionAsFeePayer = signTransactionAsFeePayer;
// Fill required fields from the context. Analogous to prepareTransactionForSigning from web3-eth,
// but also support Klaytn TxTypes.
// See web3-eth/src/utils/prepare_transaction_for_signing.ts:prepareTransactionForSigning
async function prepareTransaction(transaction, context, privateKey) {
const fillGasPrice = true;
const fillGasLimit = true;
// fall back to original web3-eth prepareTransactionForSigning
if (!(0, js_ext_core_1.isKlaytnTxType)(_parseTxType(transaction.type))) {
return await (0, web3_eth_1.prepareTransactionForSigning)(transaction, context, privateKey, fillGasPrice, fillGasLimit);
}
// If gasLimit is not specified, prepareTransactionForSigning will call eth_estimateGas.
// But in that case, we have to add some buffer thereafter.
const gasLimitWasEmpty = (0, web3_validator_1.isNullish)(transaction.gasLimit) && (0, web3_validator_1.isNullish)(transaction.gas);
// To fill tx.chainId field if provider is not available.
const chainIdFromTx = getChainIdFromTx(transaction);
// Save klaytn-specific fields and the 'type' field.
transaction = lodash_1.default.clone(transaction);
const savedFields = saveCustomFields(transaction);
// getTransactionFromOrToAttr is the function used in prepareTransactionForSigning.
// But because prepareTransactionForSigning does not return 'from' field,
// we calculate separately here.
const txFrom = (0, transaction_builder_js_1.getTransactionFromOrToAttr)("from", context, transaction, privateKey);
transaction.from ?? (transaction.from = txFrom);
// prepareTransactionForSigning fills or converts:
// - nonce: call eth_getTransactionCount on tx.from
// - value: 0
// - data: ensure tx.data == tx.input
// - common: web3 internal data
// - chain: web3 internal data
// - hardfork: web3 internal data
// - chainId: call eth_chainId
// - networkId: call net_version
// - gasPrice: call eth_gasPrice because transaction.type is set to 0 by saveCustomFields()
// - gasLimit: copy tx.gasLimit = tx.gas if defined. call eth_estimateGas if not defined.
// prepareTransactionForSigning will return a web3-eth-accounts:Transaction (LegacyTransaction)
// because transaction.type == 0.
const preparedTx = await (0, web3_eth_1.prepareTransactionForSigning)(transaction, context, privateKey, fillGasPrice, fillGasLimit);
// Assemble all fields. Upper entry is overwritten by lower entry.
const txData = {
// All fields from LegacyTransaction, filled by prepareTransactionForSigning
...preparedTx,
// If gasLimitWasEmpty (i.e. was filled in prepareTransactionForSigning),
// then add some buffer. Otherwise, use the original value (i.e. supplied by user).
gasLimit: (gasLimitWasEmpty ? bufferedGasLimit(preparedTx.gasLimit) : preparedTx.gasLimit),
// Note that LegacyTransaction has no 'from' attribute.
// Therefore we need to find 'from' using getTransactionFromOrToAttr.
from: txFrom,
// chainId from input transaction or from Web3Context.
chainId: (chainIdFromTx ?? preparedTx.common.chainId()),
// Add Klaytn-specific fields such as 'feePayer' and overwrite the 'type' field.
...savedFields,
};
// Extract the private field BaseTransaction.txOptions.
const txOptions = preparedTx.txOptions;
// Wrap in the KlaytnTypedTransaction class.
return new klaytn_tx_js_1.KlaytnTypedTransaction(txData, txOptions);
}
exports.prepareTransaction = prepareTransaction;
// Attempt to extract chainId from transaction. Returns bigint to be compatible with web3.js.
function getChainIdFromTx(transaction) {
if (transaction.chainId) {
return BigInt(transaction.chainId);
}
const chainIdFromSig = (0, js_ext_core_1.getChainIdFromSignatureTuples)(transaction.txSignatures) ??
(0, js_ext_core_1.getChainIdFromSignatureTuples)(transaction.feePayerSignatures);
if (chainIdFromSig) {
return BigInt(chainIdFromSig);
}
return undefined;
}
function bufferedGasLimit(gasLimit) {
// Sometimes Klaytn node's eth_estimateGas may return insufficient amount.
// To avoid this, add buffer to the estimated gas.
// References:
// - web3.js uses estimateGas result as-is.
// https://github.com/web3/web3.js/blob/v4.3.0/packages/web3-eth/src/utils/transaction_builder.ts#L238
// - Metamask multiplies by 1 or 1.5 depending on chainId
// https://github.com/MetaMask/metamask-extension/blob/v11.3.0/ui/ducks/send/helpers.js#L126
// TODO: To minimize buffer, add constant intrinsic gas overhead instead of multiplier.
// Chose 2.5 because of high intrinsic gas overhead of multisig and feepayer txs.
const gasLimitMultiplier = 2.5;
return Math.ceil(Number(gasLimit) * gasLimitMultiplier);
}
exports.bufferedGasLimit = bufferedGasLimit;
// See web3-types/src/eth_types.ts:TransactionBase and its child interfaces
const web3jsAllowedTransactionKeys = [
"value", "accessList", "common", "gas", "gasPrice", "type", "maxFeePerGas",
"maxPriorityFeePerGas", "data", "input", "nonce", "chain", "hardfork",
"chainId", "networkId", "gasLimit", "yParity", "v", "r", "s",
"from", "to",
];
// web3.js may strip or reject some Klaytn-specific transaction fields.
// To preserve transaction fields around web3js function calls, use saveCustomFields.
function saveCustomFields(tx) {
// Save fields that are not allowed in web3.js
const savedFields = {};
for (const key in tx) {
if (web3jsAllowedTransactionKeys.indexOf(key) === -1) {
savedFields[key] = lodash_1.default.get(tx, key);
lodash_1.default.unset(tx, key);
}
}
// Save txtype that is not supported in web3.js.
// and disguise as legacy (type 0) transaction
// because web3js-ext's KlaytnTx is based on web3js's LegacyTransaction.
if (js_ext_core_1.KlaytnTxFactory.has(tx.type)) {
savedFields["type"] = tx.type;
tx.type = 0;
}
return savedFields;
}
// TODO: Remove it after js-ext-core parseTxType() accepts bigint and null
function _parseTxType(type) {
if (typeof type === "bigint") {
type = Number(type);
}
if (type === null) {
return 0;
}
return (0, js_ext_core_1.parseTxType)(type);
}
exports._parseTxType = _parseTxType;