chaingate
Version:
A complete TypeScript library for connecting to and making transactions on different blockchains
151 lines • 6.84 kB
JavaScript
import { ethers, SigningKey } from 'ethers';
import { Transaction } from '../../Transaction';
import { EvmFee } from './EvmFee';
import { EvmBroadcastedTransaction } from './EvmBroadcastedTransaction';
import { NotEnoughFundsError } from '../../errors';
export class EvmTransaction extends Transaction {
utils;
privateKeyProvider;
data;
constructor(utils, fromAddress, toAddress, amount, data, privateKeyProvider) {
super(fromAddress, toAddress, amount);
this.utils = utils;
this.data = data;
this.privateKeyProvider = privateKeyProvider;
}
async broadcast(fee) {
// Convert fee-level shortcut into concrete fee description when required
if (typeof fee === 'string') {
const suggested = this._suggestedFees ?? (await this.buildSuggestedFees());
fee = suggested[fee];
}
if (!fee.enoughFunds || !fee.feeAmount) {
throw new NotEnoughFundsError();
}
const signer = new ethers.Wallet(new SigningKey((await this.privateKeyProvider()).raw));
const nonce = await this.utils.addressTransactionCount(this.fromAddress);
const gasLimit = await this.estimateGasLimit(nonce);
const txBase = this.createBaseTransaction(nonce, gasLimit);
const tx = this.createTransactionWithFees(txBase, fee);
const txSigned = await signer.signTransaction(tx);
const broadcastedTx = await this.utils.broadcastTransaction(txSigned);
return new EvmBroadcastedTransaction(broadcastedTx.transactionId);
}
createBaseTransaction(nonce, gasLimit) {
return {
from: this.fromAddress,
to: this.toAddress,
value: ethers.toBigInt(this.amount.minimalUnitAmount.toString()),
data: this.data ?? '0x',
nonce,
gasLimit,
chainId: this.utils.currencyInfo.chainId,
};
}
async estimateGasLimit(nonce) {
try {
const limit = await this.utils.estimateGas(this.fromAddress, this.toAddress, this.amount, nonce, this.data ?? '0x');
return BigInt(limit);
}
catch (ex) {
if (this.isInsufficientAmountError(ex)) {
throw new NotEnoughFundsError();
}
throw ex;
}
}
isInsufficientAmountError(ex) {
return ex?.response?.data === 'Insufficient amount';
}
createTransactionWithFees(txBase, fee) {
if (fee.gasPrice) {
return {
...txBase,
gasPrice: ethers.toBigInt(fee.gasPrice.minimalUnitAmount.toString()),
};
}
if (fee.maxFeePerGas && fee.maxPriorityFeePerGas) {
return {
...txBase,
maxFeePerGas: ethers.toBigInt(fee.maxFeePerGas.minimalUnitAmount.toString()),
maxPriorityFeePerGas: ethers.toBigInt(fee.maxPriorityFeePerGas.minimalUnitAmount.toString()),
};
}
throw new Error('Invalid fee structure provided');
}
async buildSuggestedFees() {
const addressBalance = (await this.utils.addressBalance(this.fromAddress)).confirmed;
const feeRate = await this.utils.getFeeRate();
const estimatedGas = await this.getEstimatedGas(addressBalance);
const fees = {};
this.processFeeLevels(feeRate, estimatedGas, addressBalance, fees);
return fees;
}
async getEstimatedGas(addressBalance) {
if (!addressBalance.baseAmount.gte(this.amount.baseAmount)) {
return null;
}
try {
const nonce = await this.utils.addressTransactionCount(this.fromAddress);
return await this.utils.estimateGas(this.fromAddress, this.toAddress, this.amount, nonce, this.data ?? '0x');
}
catch {
return null;
}
}
processFeeLevels(feeRate, estimatedGas, addressBalance, fees) {
const levels = ['low', 'normal', 'high', 'maximum'];
for (const level of levels) {
const entry = feeRate[level];
const isLegacy = 'gasPrice' in entry;
const { feeAmount, enoughFunds } = estimatedGas !== null
? this.calculateFeeAmount(entry, estimatedGas, addressBalance, isLegacy)
: { feeAmount: null, enoughFunds: false };
fees[level] = this.createFeeInstance(entry, isLegacy, enoughFunds, feeAmount);
}
}
calculateFeeAmount(entry, estimatedGas, addressBalance, isLegacy) {
const gasPrice = isLegacy ? entry.gasPrice : entry.maxFeePerGas;
const feeBase = this.utils.amount(gasPrice, this.utils.currencyInfo.symbol).baseAmount;
const totalFeeBase = feeBase.mul(estimatedGas);
const feeAmount = this.utils.amount(totalFeeBase, this.utils.currencyInfo.symbol);
const enoughFunds = addressBalance.baseAmount.gte(this.amount.baseAmount.plus(feeAmount.baseAmount));
return { feeAmount, enoughFunds };
}
createFeeInstance(entry, isLegacy, enoughFunds, feeAmount) {
const feeConfig = isLegacy
? {
gasPrice: this.utils.amount(entry.gasPrice, this.utils.currencyInfo.symbol),
}
: {
maxFeePerGas: this.utils.amount(entry.maxFeePerGas, this.utils.currencyInfo.symbol),
maxPriorityFeePerGas: this.utils.amount(entry.maxPriorityFeePerGas, this.utils.currencyInfo.symbol),
};
return new EvmFee(feeConfig, true, enoughFunds, entry.confirmationTimeSecs, feeAmount);
}
async fee(fee) {
const addressBalance = (await this.utils.addressBalance(this.fromAddress)).confirmed;
const nonce = await this.utils.addressTransactionCount(this.fromAddress);
try {
const estimatedGas = await this.utils.estimateGas(this.fromAddress, this.toAddress, this.amount, nonce, this.data ?? '0x');
const { feeAmount, enoughFunds } = this.calculateFeeFromType(fee, estimatedGas, addressBalance);
return new EvmFee(fee, true, enoughFunds, null, feeAmount);
}
catch (ex) {
if (this.isInsufficientAmountError(ex)) {
return new EvmFee(fee, true, false, null, null);
}
throw ex;
}
}
calculateFeeFromType(fee, estimatedGas, addressBalance) {
const isLegacy = 'gasPrice' in fee;
const feeBase = isLegacy
? fee.gasPrice.baseAmount
: fee.maxFeePerGas.baseAmount;
const feeAmount = this.utils.amount(feeBase.mul(estimatedGas), this.utils.currencyInfo.symbol);
const enoughFunds = addressBalance.baseAmount.gte(this.amount.baseAmount.plus(feeAmount.baseAmount));
return { feeAmount, enoughFunds };
}
}
//# sourceMappingURL=EvmTransaction.js.map