@kaiachain/web3js-ext
Version:
web3.js extension for kaiachain blockchain
265 lines (261 loc) • 14.3 kB
JavaScript
"use strict";
/*
This file is part of web3.js.
web3.js 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.
web3.js 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 web3.js. If not, see <http://www.gnu.org/licenses/>.
*/
// Taken from https://github.com/web3/web3.js/blob/v4.3.0/packages/web3-eth/src/rpc_method_wrappers.ts
// Modified to support Klaytn TxTypes
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendTransaction = sendTransaction;
exports.sendSignedTransaction = sendSignedTransaction;
const js_ext_core_1 = require("@kaiachain/js-ext-core");
const web3_core_1 = require("web3-core");
const web3_eth_1 = require("web3-eth");
const web3_types_1 = require("web3-types");
const web3_utils_1 = require("web3-utils");
const web3_validator_1 = require("web3-validator");
const sign_js_1 = require("../accounts/sign.js");
const send_tx_helper_js_1 = require("./utils/send_tx_helper.js");
const transaction_builder_js_1 = require("./utils/transaction_builder.js");
const try_send_transaction_js_1 = require("./utils/try_send_transaction.js");
const wait_for_transaction_receipt_js_1 = require("./utils/wait_for_transaction_receipt.js");
// sendTransaction sends a transaction object.
//
// It eventually calls one of the following RPC methods:
// - eth_sendTransaction
// - eth_sendRawTransaction
// - klay_sendTransaction
// - klay_sendRawTransaction
//
// High-level logic is as follows:
// - Populate some fields
// - from, to: resolve wallet index to address
// - gasPrice, maxFeePerGas, maxPriorityFeePerGas: do not fill them
// - gasLimit:
// - Copy tx.gas -> tx.gasLimit.
// - Call estimateGas
// - If Klaytn TxType, add some buffer
// - Convert transaction to be suitable for eth_call RPC
// - Use getRpcTxObject()
// - Try to find the wallet for the 'from' address.
// - Call signAndSend()
// - If the wallet is found, sign the transaction to get rawTransaction RLP.
// - If Ethereum TxType, call eth_sendRawTransaction
// - If Klaytn TxType, call klay_sendRawTransaction
// - If the wallet is not found
// - If Ethereum TxType, call eth_sendTransaction
// - If Klaytn TxType
// - If provider is Kaikas, translate the 'type' field to upper snake case string.
// - Then call klay_sendTransaction
//
function sendTransaction(web3Context, transaction, returnFormat, options = { checkRevertBeforeSending: true }) {
// Below PromiEvent is a modified version of web3-eth sendTransaction
const promiEvent = new web3_core_1.Web3PromiEvent((resolve, reject) => {
setImmediate(() => {
(async () => {
const sendTxHelper = new send_tx_helper_js_1.SendTxHelper({
web3Context,
promiEvent,
options,
returnFormat,
});
// Klaytn: replaced formatTransaction() call with getRpcTxObject() to allow Klaytn TxTypes
// Resolve 'from' and 'to' field, like in the original web3-eth source
const tx = {
...transaction,
from: (0, transaction_builder_js_1.getTransactionFromOrToAttr)("from", web3Context, transaction),
to: (0, transaction_builder_js_1.getTransactionFromOrToAttr)("to", web3Context, transaction),
};
// Klaytn: fill 'gasLimit' field. The Original web3-eth did not fill 'gasLimit',
// but Kaikas (window.klaytn) requires 'gas' field nonempty.
// Fill 'tx.gasLimit' here, then rename to 'gas' in getRpcTxObject() below.
if ((0, web3_validator_1.isNullish)(tx.gasLimit) && !options.ignoreFillingGasLimit) {
if (!(0, web3_validator_1.isNullish)(tx.gas)) {
tx.gasLimit = tx.gas;
}
else {
const gasLimitHex = await (0, web3_eth_1.estimateGas)(web3Context, tx, "latest", web3_types_1.ETH_DATA_FORMAT);
const gasLimitNum = Number(gasLimitHex);
const bufferedNum = (0, sign_js_1.bufferedGasLimit)(gasLimitNum);
tx.gasLimit = (0, web3_utils_1.format)({ format: "uint" }, bufferedNum, web3_types_1.ETH_DATA_FORMAT);
}
}
// Note: getRpcTxObject() renames 'gasLimit' to 'gas'
// transactionFormattedForCall: contains only Ethereum fields. Used to find revert reasons using eth_call.
// transactionFormattedForSend: contains all fields including Klaytn-specific fields. This is the transaction to be sent.
const transactionFormattedForCall = {
...(0, js_ext_core_1.getRpcTxObject)(tx), // first get formatted fields (only Ethereum fields)
type: undefined, // then delete the 'type' field, so that it's interpreted as an Ethereum Tx.
};
const transactionFormattedForSend = {
...tx, // first copy all fields from tx (including Klaytn-specific fields)
...(0, js_ext_core_1.getRpcTxObject)(tx), // then overwrite with formatted fields (only Ethereum fields)
};
try {
// Klaytn: removed sendTxHelper.populateGasPrice() call at here.
// - Removed for simplicity, because it's not necessary.
// - If wallet is found, then tx is signed locally and be sent via sendRawTransaction.
// In this case gas price will be filled by prepareTransaction()
// - If wallet isn't found, then tx is sent via sendTransaction,
// where gas price will be determined by the RPC endpoint or Browser wallet.
await sendTxHelper.checkRevertBeforeSending(transactionFormattedForCall);
sendTxHelper.emitSending(transactionFormattedForSend);
let wallet;
if (web3Context.wallet && !(0, web3_validator_1.isNullish)(tx.from)) {
wallet = web3Context.wallet.get(tx.from);
}
// Klaytn: replaced sendTxHelper.signAndSend() call with signOrSend() to handle Klaytn TxTypes
const transactionHash = await signAndSend(web3Context, transactionFormattedForSend, wallet);
const transactionHashFormatted = (0, web3_utils_1.format)({ format: "bytes32" }, transactionHash, returnFormat);
sendTxHelper.emitSent(transactionFormattedForSend);
sendTxHelper.emitTransactionHash(transactionHashFormatted);
const transactionReceipt = await (0, wait_for_transaction_receipt_js_1.waitForTransactionReceipt)(web3Context, transactionHash, returnFormat);
const transactionReceiptFormatted = sendTxHelper.getReceiptWithEvents((0, web3_utils_1.format)(web3_eth_1.transactionReceiptSchema, transactionReceipt, returnFormat));
sendTxHelper.emitReceipt(transactionReceiptFormatted);
resolve(await sendTxHelper.handleResolve({
receipt: transactionReceiptFormatted,
tx: transactionFormattedForCall,
}));
sendTxHelper.emitConfirmation({
receipt: transactionReceiptFormatted,
transactionHash,
});
}
catch (error) {
reject(await sendTxHelper.handleError({
error,
tx: transactionFormattedForCall,
}));
}
})();
});
});
return promiEvent;
}
// sendSignedTransaction sends a signed raw transaction.
//
// It eventually calls one of the following RPC methods:
// - eth_sendRawTransaction
// - klay_sendRawTransaction
function sendSignedTransaction(web3Context, signedTransaction, returnFormat, options = { checkRevertBeforeSending: true }) {
// If not Klaytn TxType, fall back to web3-eth's original implementation
const txRLP = normalizeSignedTransaction(signedTransaction);
const txType = (0, web3_utils_1.hexToNumber)(txRLP.substring(0, 4));
if (!(0, js_ext_core_1.isKlaytnTxType)(txType)) {
return (0, web3_eth_1.sendSignedTransaction)(web3Context, signedTransaction, returnFormat, options);
}
// Below PromiEvent is a modified version of web3-eth sendSignedTransaction
const promiEvent = new web3_core_1.Web3PromiEvent((resolve, reject) => {
setImmediate(() => {
(async () => {
const sendTxHelper = new send_tx_helper_js_1.SendTxHelper({
web3Context,
promiEvent,
options,
returnFormat,
});
// Klaytn: using js-ext-core:KlaytnTxFactory (inside parseTransaction) instead of
// web3-eth-accounts:TransactionFactory
const signedTransactionFormattedHex = normalizeSignedTransaction(signedTransaction);
const txCallObject = (0, js_ext_core_1.getRpcTxObject)((0, js_ext_core_1.parseTransaction)(signedTransactionFormattedHex)); // argument for eth_call
try {
// Klaytn: removed v,r,s field removal
await sendTxHelper.checkRevertBeforeSending(txCallObject);
sendTxHelper.emitSending(signedTransactionFormattedHex);
const transactionHash = await (0, try_send_transaction_js_1.trySendTransaction)(web3Context,
// Klaytn: Using klay_sendRawTransaction instead of eth_sendRawTransaction
async () => web3Context.requestManager.send({
method: "klay_sendRawTransaction",
params: [signedTransactionFormattedHex],
}));
sendTxHelper.emitSent(signedTransactionFormattedHex);
const transactionHashFormatted = (0, web3_utils_1.format)({ format: "bytes32" }, transactionHash, returnFormat);
sendTxHelper.emitTransactionHash(transactionHashFormatted);
const transactionReceipt = await (0, wait_for_transaction_receipt_js_1.waitForTransactionReceipt)(web3Context, transactionHash, returnFormat);
const transactionReceiptFormatted = sendTxHelper.getReceiptWithEvents((0, web3_utils_1.format)(web3_eth_1.transactionReceiptSchema, transactionReceipt, returnFormat));
sendTxHelper.emitReceipt(transactionReceiptFormatted);
resolve(await sendTxHelper.handleResolve({
receipt: transactionReceiptFormatted,
tx: txCallObject,
}));
sendTxHelper.emitConfirmation({
receipt: transactionReceiptFormatted,
transactionHash,
});
}
catch (error) {
reject(await sendTxHelper.handleError({
error,
tx: txCallObject,
}));
}
})();
});
});
return promiEvent;
}
// Convert Bytes(string | Uint8Array) to hex string
function normalizeSignedTransaction(signedTransaction) {
if (signedTransaction instanceof Uint8Array) {
signedTransaction = (0, web3_utils_1.bytesToHex)(signedTransaction);
}
if (signedTransaction.length < 4 || !signedTransaction.startsWith("0x")) {
throw new Error(`Invalid signed transaction '${signedTransaction}'`);
}
return signedTransaction;
}
// Call one of the following RPC methods:
// - eth_sendTransaction
// - eth_sendRawTransaction
// - klay_sendTransaction
// - klay_sendRawTransaction
//
// Modified from web3-eth/src/send_tx/send_tx_helper.ts:signAndSend()
async function signAndSend(web3Context, tx, senderAccount) {
if (senderAccount) {
// senderAccount (i.e. private key) is given. Sign and sendRawTransaction.
const signResult = await senderAccount.signTransaction(tx);
let method;
if ((0, js_ext_core_1.isKlaytnTxType)((0, sign_js_1._parseTxType)(tx.type))) {
method = "klay_sendRawTransaction";
}
else {
method = "eth_sendRawTransaction";
}
return await (0, try_send_transaction_js_1.trySendTransaction)(web3Context, async () => web3Context.requestManager.send({
method: method,
params: [signResult.rawTransaction],
}), signResult.transactionHash);
}
else {
// senderAccount (i.e. private key) is not given. Call sendTransaction
// and let provider (remote RPC endpoint or Browser wallet) do the rest.
// Translate to string 'type' field that Kaikas understands.
if ((0, js_ext_core_1.isKlaytnTxType)((0, sign_js_1._parseTxType)(tx.type)) && isKaikas(web3Context.provider)) {
tx.type = (0, js_ext_core_1.getKaikasTxType)((0, sign_js_1._parseTxType)(tx.type));
}
let method;
if ((0, js_ext_core_1.isKlaytnTxType)((0, sign_js_1._parseTxType)(tx.type))) {
method = "klay_sendTransaction";
}
else {
method = "eth_sendTransaction";
}
return await (0, try_send_transaction_js_1.trySendTransaction)(web3Context, async () => web3Context.requestManager.send({
method: method,
params: [tx],
}));
}
}
function isKaikas(provider) {
return !(0, web3_validator_1.isNullish)(provider) && (provider.isKaikas == true);
}