UNPKG

@kaiachain/web3js-ext

Version:
221 lines (220 loc) 12 kB
"use strict"; 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;