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
19 changes: 1 addition & 18 deletions packages/txm/lib/GasEstimator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SpanStatusCode, context, trace } from "@opentelemetry/api"
import { type Result, err, ok } from "neverthrow"
import { encodeFunctionData } from "viem"
import type { Transaction } from "./Transaction.js"
import type { TransactionManager } from "./TransactionManager.js"
import { TraceMethod } from "./telemetry/traces"
Expand Down Expand Up @@ -70,26 +69,10 @@ export class DefaultGasLimitEstimator implements GasEstimator {
): Promise<Result<bigint, EstimateGasError>> {
const span = trace.getSpan(context.active())!

const abi = transactionManager.abiManager.get(transaction.contractName)

if (!abi) {
const description = `ABI not found for contract ${transaction.contractName}`
span.recordException(new Error(description))
span.setStatus({ code: SpanStatusCode.ERROR })
return err({
cause: EstimateGasErrorCause.EstimateGasABINotFound,
description,
})
}

const functionName = transaction.functionName
const args = transaction.args
const data = encodeFunctionData({ abi, functionName, args })

const gasResult = await transactionManager.viemClient.safeEstimateGas({
account: transactionManager.viemWallet.account,
to: transaction.address,
data,
data: transaction.calldata,
value: 0n,
})

Expand Down
12 changes: 3 additions & 9 deletions packages/txm/lib/RetryPolicyManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Result, err, ok } from "neverthrow"
import { type TransactionReceipt, encodeErrorResult } from "viem"
import { type Result, ok } from "neverthrow"
import { type Abi, type TransactionReceipt, encodeErrorResult } from "viem"
import type { Attempt, Transaction } from "./Transaction"
import type { TransactionManager } from "./TransactionManager"
import { TraceMethod } from "./telemetry/traces"
Expand Down Expand Up @@ -89,8 +89,8 @@ export class DefaultRetryPolicyManager implements RetryPolicyManager {
@TraceMethod("txm.retry-policy-manager.is-custom-error")
protected async isCustomError(
transactionManager: TransactionManager,
transaction: Transaction,
attempt: Attempt,
abi: Abi,
customError: string,
): Promise<Result<boolean, Error>> {
const { output } = await this.getRevertMessageAndOutput(transactionManager, attempt)
Expand All @@ -99,12 +99,6 @@ export class DefaultRetryPolicyManager implements RetryPolicyManager {
return ok(false)
}

const abi = transactionManager.abiManager.get(transaction.contractName)

if (!abi) {
return err(new Error("Contract not found"))
}

return ok(
encodeErrorResult({
abi: abi,
Expand Down
167 changes: 90 additions & 77 deletions packages/txm/lib/Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type UUID, bigIntReplacer, bigIntReviver, createUUID } from "@happy.tech/common"
import { type Hex, type UUID, bigIntReplacer, bigIntReviver, createUUID } from "@happy.tech/common"
import { context, trace } from "@opentelemetry/api"
import type { Insertable, Selectable } from "kysely"
import type { Address, ContractFunctionArgs, Hash } from "viem"
import { type Address, type ContractFunctionArgs, type Hash, encodeFunctionData } from "viem"
import type { ABIManager } from "./AbiManager"
import type { LatestBlock } from "./BlockMonitor"
import { Topics, eventBus } from "./EventBus.js"
import type { TransactionTable } from "./db/types.js"
Expand Down Expand Up @@ -54,26 +55,18 @@ export interface Attempt {
gas: bigint
}

export enum TransactionCallDataFormat {
Raw = "Raw",
Function = "Function",
}

export const NotFinalizedStatuses = [TransactionStatus.Pending, TransactionStatus.Cancelling]

export interface TransactionConstructorConfig {
interface TransactionConstructorBaseConfig {
/**
* The address of the contract that will be called
*/
address: Address
/**
* The function name of the contract that will be called
*/
functionName: string
/**
* This doesn't need to match the Solidity contract name but must match the contract alias of one of the contracts
* that you have provided when initializing the transaction manager with the ABI Manager
*/
contractName: string
/**
* The arguments of the function that will be called
*/
args: ContractFunctionArgs
/**
* The deadline of the transaction in seconds (optional)
* This is used to try to cancel the transaction if it is not included in a block after the deadline to save gas
Expand All @@ -85,6 +78,21 @@ export interface TransactionConstructorConfig {
metadata?: Record<string, unknown>
}

type TransactionConstructorCalldataConfig = TransactionConstructorBaseConfig & {
calldata: Hex
functionName?: string
contractName?: string
args?: ContractFunctionArgs
}

type TransactionConstructorFunctionConfig = TransactionConstructorBaseConfig & {
functionName: string
contractName: string
args: ContractFunctionArgs
}

export type TransactionConstructorConfig = TransactionConstructorCalldataConfig | TransactionConstructorFunctionConfig

export class Transaction {
readonly intentId: UUID

Expand All @@ -94,12 +102,14 @@ export class Transaction {

readonly address: Address

readonly functionName: string
readonly functionName?: string

readonly args: ContractFunctionArgs
readonly args?: ContractFunctionArgs

// This doesn't need to match the Solidity contract name but must match the contract alias of one of the contracts that you have provided when initializing the transaction manager with the ABI Manager
readonly contractName: string
readonly contractName?: string

readonly calldata: Hex

readonly deadline: number | undefined

Expand Down Expand Up @@ -129,51 +139,50 @@ export class Transaction {
*/
readonly metadata: Record<string, unknown>

constructor({
address,
functionName,
contractName,
args,
deadline,
metadata,
intentId,
from,
chainId,
status,
attempts,
collectionBlock,
createdAt,
updatedAt,
pendingFlush,
notPersisted,
}: TransactionConstructorConfig & {
intentId?: UUID
from: Address
chainId: number
status?: TransactionStatus
attempts?: Attempt[]
collectionBlock?: bigint
createdAt?: Date
updatedAt?: Date
pendingFlush?: boolean
notPersisted?: boolean
}) {
this.intentId = intentId ?? createUUID()
this.from = from
this.chainId = chainId
this.address = address
this.functionName = functionName
this.contractName = contractName
this.args = args
this.deadline = deadline
this.status = status ?? TransactionStatus.Pending
this.attempts = attempts ?? []
this.collectionBlock = collectionBlock
this.createdAt = createdAt ?? new Date()
this.updatedAt = updatedAt ?? new Date()
this.metadata = metadata ?? {}
this.pendingFlush = pendingFlush ?? true
this.notPersisted = notPersisted ?? true
constructor(
config: TransactionConstructorConfig & {
intentId?: UUID
from: Address
chainId: number
status?: TransactionStatus
attempts?: Attempt[]
collectionBlock?: bigint
createdAt?: Date
updatedAt?: Date
pendingFlush?: boolean
notPersisted?: boolean
},
abiManager: ABIManager,
) {
this.intentId = config.intentId ?? createUUID()
this.from = config.from
this.chainId = config.chainId
this.address = config.address
this.deadline = config.deadline
this.status = config.status ?? TransactionStatus.Pending
this.attempts = config.attempts ?? []
this.collectionBlock = config.collectionBlock
this.createdAt = config.createdAt ?? new Date()
this.updatedAt = config.updatedAt ?? new Date()
this.metadata = config.metadata ?? {}
this.pendingFlush = config.pendingFlush ?? true
this.notPersisted = config.notPersisted ?? true

if ("calldata" in config) {
this.calldata = config.calldata
this.functionName = config.functionName
this.contractName = config.contractName
this.args = config.args
} else {
const abi = abiManager.get(config.contractName)
if (!abi) {
throw new Error(`ABI not found for contract ${config.contractName}`)
}
this.calldata = encodeFunctionData({ abi, functionName: config.functionName, args: config.args })
this.functionName = config.functionName
this.contractName = config.contractName
this.args = config.args
}
}

addAttempt(attempt: Attempt): void {
Expand Down Expand Up @@ -261,7 +270,8 @@ export class Transaction {
address: this.address,
functionName: this.functionName,
contractName: this.contractName,
args: JSON.stringify(this.args, bigIntReplacer),
calldata: this.calldata,
args: this.args ? JSON.stringify(this.args, bigIntReplacer) : undefined,
deadline: this.deadline,
collectionBlock: this.collectionBlock ? Number(this.collectionBlock) : undefined,
status: this.status,
Expand All @@ -272,18 +282,21 @@ export class Transaction {
}
}

static fromDbRow(row: Selectable<TransactionTable>): Transaction {
return new Transaction({
...row,
args: JSON.parse(row.args, bigIntReviver),
attempts: JSON.parse(row.attempts, bigIntReviver),
collectionBlock: row.collectionBlock ? BigInt(row.collectionBlock) : undefined,
metadata: row.metadata ? JSON.parse(row.metadata, bigIntReviver) : undefined,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
notPersisted: false,
pendingFlush: false,
})
static fromDbRow(row: Selectable<TransactionTable>, abiManager: ABIManager): Transaction {
return new Transaction(
{
...row,
args: row.args ? JSON.parse(row.args, bigIntReviver) : undefined,
attempts: JSON.parse(row.attempts, bigIntReviver),
collectionBlock: row.collectionBlock ? BigInt(row.collectionBlock) : undefined,
metadata: row.metadata ? JSON.parse(row.metadata, bigIntReviver) : undefined,
createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt),
notPersisted: false,
pendingFlush: false,
},
abiManager,
)
}

toJson(): string {
Expand Down
13 changes: 8 additions & 5 deletions packages/txm/lib/TransactionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,14 @@ export class TransactionManager {
* @returns A new transaction.
*/
public createTransaction(params: TransactionConstructorConfig): Transaction {
return new Transaction({
...params,
from: this.viemWallet.account.address,
chainId: this.viemWallet.chain.id,
})
return new Transaction(
{
...params,
from: this.viemWallet.account.address,
chainId: this.viemWallet.chain.id,
},
this.abiManager,
)
}

/**
Expand Down
8 changes: 6 additions & 2 deletions packages/txm/lib/TransactionRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export class TransactionRepository {
.selectAll()
.execute()

this.notFinalizedTransactions = transactionRows.map((row) => Transaction.fromDbRow(row))
this.notFinalizedTransactions = transactionRows.map((row) =>
Transaction.fromDbRow(row, this.transactionManager.abiManager),
)

if (this.transactionManager.finalizedTransactionPurgeTime > 0) {
eventBus.on(Topics.NewBlock, this.purgeFinalizedTransactions.bind(this))
Expand Down Expand Up @@ -85,7 +87,9 @@ export class TransactionRepository {

const persistedTransaction = persistedTransactionResult.value

return persistedTransaction ? ok(Transaction.fromDbRow(persistedTransaction)) : ok(undefined)
return persistedTransaction
? ok(Transaction.fromDbRow(persistedTransaction, this.transactionManager.abiManager))
: ok(undefined)
}

@TraceMethod("txm.transaction-repository.save-transactions")
Expand Down
24 changes: 2 additions & 22 deletions packages/txm/lib/TransactionSubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { bigIntReplacer } from "@happy.tech/common"
import { SpanStatusCode, context, trace } from "@opentelemetry/api"
import { type Result, err, ok } from "neverthrow"
import type { TransactionRequestEIP1559 } from "viem"
import { TransactionRejectedRpcError, encodeFunctionData, keccak256 } from "viem"
import { TransactionRejectedRpcError, keccak256 } from "viem"
import type { EstimateGasErrorCause } from "./GasEstimator.js"
import { type Attempt, AttemptType, type Transaction } from "./Transaction.js"
import type { TransactionManager } from "./TransactionManager.js"
Expand Down Expand Up @@ -101,26 +101,6 @@ export class TransactionSubmitter {
gas: attempt.gas ?? 21000n,
}
} else {
const abi = this.txmgr.abiManager.get(transaction.contractName)

if (!abi) {
span.addEvent("txm.transaction-submitter.send-attempt.abi-not-found", {
transactionIntentId: transaction.intentId,
contractName: transaction.contractName,
})
span.setStatus({ code: SpanStatusCode.ERROR })
logger.error(`ABI not found for contract ${transaction.contractName}`)
return err({
cause: AttemptSubmissionErrorCause.ABINotFound,
description: `ABI not found for contract ${transaction.contractName}`,
flushed: false,
})
}

const functionName = transaction.functionName
const args = transaction.args
const data = encodeFunctionData({ abi, functionName, args })

let gas: bigint
if (attempt.gas === undefined) {
const gasResult = await this.txmgr.gasEstimator.estimateGas(this.txmgr, transaction)
Expand All @@ -147,7 +127,7 @@ export class TransactionSubmitter {
type: "eip1559",
from: this.txmgr.viemWallet.account.address,
to: transaction.address,
data,
data: transaction.calldata,
value: 0n,
nonce: attempt.nonce,
maxFeePerGas: attempt.maxFeePerGas,
Expand Down
14 changes: 7 additions & 7 deletions packages/txm/lib/db/migrations/Migration20241111163800.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ async function up(db: Kysely<Database>) {
await db.schema
.createTable("transaction")
.addColumn("intentId", "text", (col) => col.notNull())
.addColumn("chainId", "integer", (col) => col.notNull())
.addColumn("address", "text", (col) => col.notNull())
.addColumn("functionName", "text", (col) => col.notNull())
.addColumn("args", "json", (col) => col.notNull())
.addColumn("contractName", "text", (col) => col.notNull())
.addColumn("chainId", "integer")
.addColumn("address", "text")
.addColumn("functionName", "text")
.addColumn("args", "json")
.addColumn("contractName", "text")
.addColumn("deadline", "integer")
.addColumn("status", "text", (col) => col.notNull())
.addColumn("attempts", "json", (col) => col.notNull())
.addColumn("status", "text")
.addColumn("attempts", "json")
.addColumn("metadata", "json")
.execute()
}
Expand Down
Loading
Loading