Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/txm/lib/GasPriceOracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import type { TransactionManager } from "./TransactionManager.js"
*/
export class GasPriceOracle {
private txmgr: TransactionManager
private expectedNextBaseFeePerGas!: bigint
private targetPriorityFee!: bigint
public expectedNextBaseFeePerGas!: bigint
public targetPriorityFee!: bigint

constructor(_transactionManager: TransactionManager) {
this.txmgr = _transactionManager
Expand Down
2 changes: 1 addition & 1 deletion packages/txm/lib/TransactionCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class TransactionCollector {
transaction.changeStatus(TransactionStatus.Pending)
}

const submissionResult = await this.txmgr.transactionSubmitter.attemptSubmission(transaction, {
const submissionResult = await this.txmgr.transactionSubmitter.submitNewAttempt(transaction, {
type: AttemptType.Original,
nonce,
maxFeePerGas,
Expand Down
116 changes: 69 additions & 47 deletions packages/txm/lib/TransactionSubmitter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { LogTag, Logger } from "@happy.tech/common"
import { type Result, err, ok } from "neverthrow"
import type { Hash, Hex, TransactionRequestEIP1559 } from "viem"
import type { TransactionRequestEIP1559 } from "viem"
import { TransactionRejectedRpcError, encodeFunctionData, keccak256 } from "viem"
import type { EstimateGasErrorCause } from "./GasEstimator.js"
import { type Attempt, AttemptType, type Transaction } from "./Transaction.js"
import type { TransactionManager } from "./TransactionManager.js"

export interface SignReturn {
signedTransaction: Hex
hash: Hash
}

export type AttemptSubmissionParameters = Omit<Attempt, "hash" | "gas">

export enum AttemptSubmissionErrorCause {
Expand Down Expand Up @@ -50,30 +44,51 @@ export class TransactionSubmitter {
this.txmgr = txmgr
}

public async attemptSubmission(
public async submitNewAttempt(
transaction: Transaction,
payload: AttemptSubmissionParameters,
): Promise<AttemptSubmissionResult> {
const { nonce, maxFeePerGas, maxPriorityFeePerGas, type } = payload

return await this.sendAttempt(
transaction,
{
type,
nonce,
maxFeePerGas,
maxPriorityFeePerGas,
},
true,
)
}

async resubmitAttempt(transaction: Transaction, attempt: Attempt): Promise<AttemptSubmissionResult> {
return await this.sendAttempt(transaction, attempt, false)
}

private async sendAttempt(
transaction: Transaction,
attempt: Omit<Attempt, "hash" | "gas"> & Partial<Pick<Attempt, "hash" | "gas">>,
saveAttempt = false,
): Promise<AttemptSubmissionResult> {
let transactionRequest: TransactionRequestEIP1559 & { gas: bigint }
if (type === AttemptType.Cancellation) {

if (attempt.type === AttemptType.Cancellation) {
transactionRequest = {
type: "eip1559",
from: this.txmgr.viemWallet.account.address,
to: this.txmgr.viemWallet.account.address,
data: "0x",
value: 0n,
nonce,
maxFeePerGas,
maxPriorityFeePerGas,
gas: 21000n,
nonce: attempt.nonce,
maxFeePerGas: attempt.maxFeePerGas,
maxPriorityFeePerGas: attempt.maxPriorityFeePerGas,
gas: attempt.gas ?? 21000n,
}
} else {
const abi = this.txmgr.abiManager.get(transaction.contractName)

if (!abi) {
Logger.instance.error(LogTag.TXM, `ABI not found for contract ${transaction.contractName}`)
return err({
cause: AttemptSubmissionErrorCause.ABINotFound,
description: `ABI not found for contract ${transaction.contractName}`,
Expand All @@ -85,27 +100,32 @@ export class TransactionSubmitter {
const args = transaction.args
const data = encodeFunctionData({ abi, functionName, args })

const gasResult = await this.txmgr.gasEstimator.estimateGas(this.txmgr, transaction)

if (gasResult.isErr()) {
return err({
cause: gasResult.error.cause,
description: gasResult.error.description,
flushed: false,
})
let gas: bigint
if (attempt.gas === undefined) {
const gasResult = await this.txmgr.gasEstimator.estimateGas(this.txmgr, transaction)

if (gasResult.isErr()) {
return err({
cause: gasResult.error.cause,
description: gasResult.error.description,
flushed: false,
})
}

gas = gasResult.value
} else {
gas = attempt.gas
}

const gas = gasResult.value

transactionRequest = {
type: "eip1559",
from: this.txmgr.viemWallet.account.address,
to: transaction.address,
data,
value: 0n,
nonce,
maxFeePerGas,
maxPriorityFeePerGas,
nonce: attempt.nonce,
maxFeePerGas: attempt.maxFeePerGas,
maxPriorityFeePerGas: attempt.maxPriorityFeePerGas,
gas,
}
}
Expand All @@ -115,7 +135,7 @@ export class TransactionSubmitter {
if (signedTransactionResult.isErr()) {
return err({
cause: AttemptSubmissionErrorCause.FailedToSignTransaction,
description: `Failed to sign transaction ${transaction.intentId}. Details: ${signedTransactionResult.error}`,
description: `Failed to sign transaction ${transaction.intentId} for retry. Details: ${signedTransactionResult.error}`,
flushed: false,
})
}
Expand All @@ -124,24 +144,27 @@ export class TransactionSubmitter {

const hash = keccak256(signedTransaction)

transaction.addAttempt({
type,
hash: hash,
nonce: nonce,
maxFeePerGas: maxFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
gas: transactionRequest.gas,
})
if (saveAttempt) {
transaction.addAttempt({
type: attempt.type,
hash: hash,
nonce: attempt.nonce,
maxFeePerGas: attempt.maxFeePerGas,
maxPriorityFeePerGas: attempt.maxPriorityFeePerGas,
gas: transactionRequest.gas,
})

const updateResult = await this.txmgr.transactionRepository.saveTransactions([transaction])
const updateResult = await this.txmgr.transactionRepository.saveTransactions([transaction])

if (updateResult.isErr()) {
transaction.removeAttempt(hash)
return err({
cause: AttemptSubmissionErrorCause.FailedToUpdate,
description: `Failed to update transaction ${transaction.intentId}. Details: ${updateResult.error}`,
flushed: false,
})
if (updateResult.isErr()) {
transaction.removeAttempt(hash)

return err({
cause: AttemptSubmissionErrorCause.FailedToUpdate,
description: `Failed to update transaction ${transaction.intentId}. Details: ${updateResult.error}`,
flushed: false,
})
}
}

const sendRawTransactionResult = await this.txmgr.viemWallet.safeSendRawTransaction({
Expand All @@ -157,11 +180,10 @@ export class TransactionSubmitter {
}

this.txmgr.rpcLivenessMonitor.trackError()

return err({
cause: AttemptSubmissionErrorCause.FailedToSendRawTransaction,
description: `Failed to send raw transaction ${transaction.intentId}. Details: ${sendRawTransactionResult.error}`,
flushed: true,
description: `Failed to send raw transaction ${transaction.intentId} for retry. Details: ${sendRawTransactionResult.error}`,
flushed: saveAttempt,
})
}

Expand Down
31 changes: 27 additions & 4 deletions packages/txm/lib/TxMonitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ export class TxMonitor {
}
}

private shouldEmitNewAttempt(attempt: Attempt): boolean {
const { expectedNextBaseFeePerGas, targetPriorityFee } = this.transactionManager.gasPriceOracle

return (
attempt.maxPriorityFeePerGas < targetPriorityFee ||
attempt.maxFeePerGas - attempt.maxPriorityFeePerGas < expectedNextBaseFeePerGas
)
}

private async handleExpiredTransaction(transaction: Transaction): Promise<void> {
const attempt = transaction.lastAttempt

Expand All @@ -227,7 +236,7 @@ export class TxMonitor {

transaction.changeStatus(TransactionStatus.Cancelling)

await this.transactionManager.transactionSubmitter.attemptSubmission(transaction, {
await this.transactionManager.transactionSubmitter.submitNewAttempt(transaction, {
type: AttemptType.Cancellation,
nonce: attempt.nonce,
maxFeePerGas: replacementMaxFeePerGas,
Expand All @@ -246,12 +255,26 @@ export class TxMonitor {
return
}

if (!this.shouldEmitNewAttempt(attempt)) {
Logger.instance.info(
LogTag.TXM,
`Transaction ${transaction.intentId} is stuck, but the gas price is still sufficient for current network conditions. Sending same attempt again.`,
)
await this.transactionManager.transactionSubmitter.resubmitAttempt(transaction, attempt)
return
}

Logger.instance.info(
LogTag.TXM,
`Transaction ${transaction.intentId} is stuck and the gas price is below optimal network parameters. Sending new attempt.`,
)

const { replacementMaxFeePerGas, replacementMaxPriorityFeePerGas } = this.calcReplacementFee(
attempt.maxFeePerGas,
attempt.maxPriorityFeePerGas,
)

await this.transactionManager.transactionSubmitter.attemptSubmission(transaction, {
await this.transactionManager.transactionSubmitter.submitNewAttempt(transaction, {
type: AttemptType.Original,
nonce: attempt.nonce,
maxFeePerGas: replacementMaxFeePerGas,
Expand All @@ -269,7 +292,7 @@ export class TxMonitor {
const { maxFeePerGas: marketMaxFeePerGas, maxPriorityFeePerGas: marketMaxPriorityFeePerGas } =
this.transactionManager.gasPriceOracle.suggestGasForNextBlock()

const submissionResult = await this.transactionManager.transactionSubmitter.attemptSubmission(transaction, {
const submissionResult = await this.transactionManager.transactionSubmitter.submitNewAttempt(transaction, {
type: AttemptType.Original,
nonce,
maxFeePerGas: marketMaxFeePerGas,
Expand All @@ -286,7 +309,7 @@ export class TxMonitor {
const { maxFeePerGas: marketMaxFeePerGas, maxPriorityFeePerGas: marketMaxPriorityFeePerGas } =
this.transactionManager.gasPriceOracle.suggestGasForNextBlock()

const submissionResult = await this.transactionManager.transactionSubmitter.attemptSubmission(transaction, {
const submissionResult = await this.transactionManager.transactionSubmitter.submitNewAttempt(transaction, {
type: AttemptType.Original,
nonce,
maxFeePerGas: marketMaxFeePerGas,
Expand Down
Loading
Loading