@kaiachain/ethers-ext
Version:
ethers.js extension for kaia blockchain
153 lines (152 loc) • 6.56 kB
JavaScript
import { resolveProperties, ZeroAddress, resolveAddress, Transaction, assert, } from "ethers";
import _ from "lodash";
import { getChainIdFromSignatureTuples, parseTransaction, parseTxType, } from "@kaiachain/js-ext-core";
// Normalize transaction request in Object or RLP format
export async function getTransactionRequest(transactionOrRLP) {
if (_.isString(transactionOrRLP)) {
return parseTransaction(transactionOrRLP);
}
else {
if (transactionOrRLP instanceof Transaction) {
return transactionOrRLP.toJSON();
}
const resolvedTx = await resolveProperties(transactionOrRLP);
// tx values transformation
if (typeof resolvedTx?.type === "string") {
resolvedTx.type = parseTxType(resolvedTx.type);
}
return resolvedTx;
}
}
export async function populateFrom(tx, expectedFrom) {
if (!tx.from || tx.from == "0x") {
tx.from = expectedFrom;
}
else {
assert(tx.from?.toString().toLowerCase() === expectedFrom?.toLowerCase(), `from address mismatch (wallet address=${expectedFrom}) (tx.from=${tx.from})`, "INVALID_ARGUMENT", { argument: "from", value: tx.from });
tx.from = expectedFrom;
}
}
export async function populateTo(tx, provider) {
if (!tx.to || tx.to == "0x") {
tx.to = ZeroAddress;
}
else {
tx.to = await resolveAddress(tx.to, provider);
}
}
export async function populateNonce(tx, provider, fromAddress) {
if (!tx.nonce) {
tx.nonce = await provider?.getTransactionCount(fromAddress);
}
}
export async function populateGasLimit(tx, provider) {
assert(!!provider, "provider is undefined", "MISSING_ARGUMENT");
if (!tx.gasLimit) {
// Sometimes Klaytn node's eth_estimateGas may return insufficient amount.
// To avoid this, add buffer to the estimated gas.
// References:
// - ethers.js uses estimateGas result as-is.
// - 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.
try {
const bufferMultiplier = 2.5;
const gasLimit = await provider?.estimateGas(tx);
tx.gasLimit = Math.ceil(Number(gasLimit) * bufferMultiplier); // overflow risk when gasLimit exceed Number.MAX_SAFE_INTEGER
}
catch (error) {
assert(false, "cannot estimate gas; transaction may fail or may require manual gas limit", "UNKNOWN_ERROR", {
data: tx,
});
}
}
}
export async function populateGasPrice(tx, provider) {
if (!tx.gasPrice) {
tx.gasPrice = (await provider?.getFeeData())?.gasPrice?.toString(); // https://github.com/ethers-io/ethers.js/discussions/4219
}
}
export function eip155sign(key, digest, chainId) {
const sig = key.sign(digest);
const recoveryParam = sig.v === 27 ? 0 : 1;
const v = recoveryParam + +chainId * 2 + 35;
return { r: sig.r, s: sig.s, v };
}
export async function populateChainId(tx, provider) {
if (!tx.chainId) {
tx.chainId =
getChainIdFromSignatureTuples(tx.txSignatures) ??
getChainIdFromSignatureTuples(tx.feePayerSignatures) ??
(await provider?.getNetwork())?.chainId.toString();
}
}
export async function populateFeePayerAndSignatures(tx, 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 (!tx.feePayer || tx.feePayer == ZeroAddress) {
tx.feePayer = expectedFeePayer;
}
else {
assert(tx.feePayer.toLowerCase() === expectedFeePayer.toLowerCase(), "feePayer address mismatch", "INVALID_ARGUMENT", {
argument: "feePayer",
value: tx.feePayer,
});
tx.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 (_.isArray(tx.feePayerSignatures)) {
tx.feePayerSignatures = tx.feePayerSignatures.filter((sig) => {
return !(_.isArray(sig) &&
sig.length == 3 &&
sig[0] == "0x01" &&
sig[1] == "0x" &&
sig[2] == "0x");
});
}
}
/**
* Delay the execution inside an async function in miliseconds.
*
* @param time (miliseconds) the amount of time to be delayed.
*/
export function sleep(time) {
return new Promise((res, _) => {
setTimeout(() => res(), time);
});
}
/**
* The poll function implements a retry mechanism for asynchronous operations.
* It repeatedly calls the callback function to fetch data and then uses the
* verify function to check if the retrieved data meets the desired criteria.
*
* @param callback A callback function that is responsible for fetching or retrieving data. It should be an asynchronous function that returns a Promise.
* @param verify A callback function that determines if the retrieved data meets the desired criteria. It should accept the data returned by callback and return a boolean value (true if the data is valid, false otherwise).
* @param retries (optional): An integer specifying the maximum number of times the function will attempt to poll before giving up. Defaults to 100.
* @returns A Promise that resolves to the data retrieved by callback when the verify function returns true, or rejects with an error if the maximum number of retries is reached.
*/
export async function poll(callback, verify, retries = 100) {
let result;
for (let i = 0; i < retries; i++) {
try {
const output = await callback();
if (output && verify(output)) {
result = output;
break;
}
}
catch (_) {
continue;
}
await sleep(250);
}
assert(result, "Transaction timeout!", "NETWORK_ERROR", {
event: "pollTransactionInPool",
});
return result;
}
// Poll for `eth_getTransaction` until the transaction is found in the transaction pool.
export async function pollTransactionInPool(txhash, provider) {
return poll(() => provider.getTransaction(txhash), (value) => typeof value?.hash === "string");
}