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
6 changes: 5 additions & 1 deletion apps/docs/src/pages/txm/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,8 @@ const hookHandler: TxmHookHandler = (tx: Transaction) => {
}

txm.addHook(TxmHookType.OnStatusChange, hookHandler)
```
<<<<<<< HEAD
```
=======
```
>>>>>>> e24f8b15 (fix: docs)
168 changes: 62 additions & 106 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@
"solady": "0.0.237"
},
"devDependencies": {
"@happy.tech/happybuild": "workspace:0.1.1",
"@happy.tech/configs": "workspace:0.1.0",
"@happy.tech/happybuild": "workspace:0.1.1",
"@openzeppelin/upgrades-core": "^1.36.0",
"@types/node": "^22.0.2",
"node-jq": "^6.0.1",
"permissionless": "^0.2.0",
"solhint": "^5.0.5",
"viem": "^2.21.53"
Expand Down
2 changes: 1 addition & 1 deletion packages/txm/lib/GasEstimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class DefaultGasLimitEstimator implements GasEstimator {
account: transactionManager.viemWallet.account,
to: transaction.address,
data: transaction.calldata,
value: 0n,
value: transaction.value,
})

if (gasResult.isErr()) {
Expand Down
12 changes: 11 additions & 1 deletion packages/txm/lib/Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Hex, type UUID, bigIntReplacer, bigIntReviver, createUUID } from "@happy.tech/common"
import { type Hex, type UUID, bigIntReplacer, bigIntReviver, bigIntToZeroPadded, createUUID } from "@happy.tech/common"
import { context, trace } from "@opentelemetry/api"
import type { Insertable, Selectable } from "kysely"
import { type Address, type ContractFunctionArgs, type Hash, encodeFunctionData } from "viem"
Expand Down Expand Up @@ -67,6 +67,11 @@ interface TransactionConstructorBaseConfig {
* The address of the contract that will be called
*/
address: Address
/**
* The value of the transaction in wei
* Defaults to 0n
*/
value?: bigint
/**
* 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 Down Expand Up @@ -111,6 +116,8 @@ export class Transaction {

readonly calldata: Hex

readonly value: bigint

readonly deadline: number | undefined

status: TransactionStatus
Expand Down Expand Up @@ -158,6 +165,7 @@ export class Transaction {
this.from = config.from
this.chainId = config.chainId
this.address = config.address
this.value = config.value ?? 0n
this.deadline = config.deadline
this.status = config.status ?? TransactionStatus.Pending
this.attempts = config.attempts ?? []
Expand Down Expand Up @@ -270,6 +278,7 @@ export class Transaction {
address: this.address,
functionName: this.functionName,
contractName: this.contractName,
value: bigIntToZeroPadded(this.value), // We convert the bigint value to a zero-padded string because 'value' can exceed the numeric limits of Number

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we need the zero padding here actually? Couldn't we just this.value.toString()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Padding is needed to ensure that when we compare two strings representing bigints, the comparisons work correctly. We're not comparing numbers but strings, so without padding, the comparison would be alphabetical. For example, "9" would be considered greater than "10" because alphabetically, "9" comes after "1"

calldata: this.calldata,
args: this.args ? JSON.stringify(this.args, bigIntReplacer) : undefined,
deadline: this.deadline,
Expand All @@ -286,6 +295,7 @@ export class Transaction {
return new Transaction(
{
...row,
value: BigInt(row.value),
args: row.args ? JSON.parse(row.args, bigIntReviver) : undefined,
attempts: JSON.parse(row.attempts, bigIntReviver),
collectionBlock: row.collectionBlock ? BigInt(row.collectionBlock) : undefined,
Expand Down
20 changes: 10 additions & 10 deletions packages/txm/lib/TransactionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export type TransactionManagerConfig = {
/** The private key of the account used for signing transactions. */
privateKey: Hex

gas: {
gas?: {
/** Optional EIP-1559 parameters. If not provided, defaults to the OP stack's stock parameters. */
eip1559?: EIP1559Parameters
/**
Expand Down Expand Up @@ -179,7 +179,7 @@ export type TransactionManagerConfig = {
* This is a record of aliases to ABIs. The aliases are used to reference the ABIs in the
* transactions.
*/
abis: Record<string, Abi>
abis?: Record<string, Abi>

/**
* The expected interval (in seconds) for the creation of a new block on the blockchain.
Expand Down Expand Up @@ -402,14 +402,14 @@ export class TransactionManager {
this.rpcLivenessMonitor = new RpcLivenessMonitor(this)

this.chainId = _config.chainId
this.eip1559 = _config.gas.eip1559 ?? opStackDefaultEIP1559Parameters
this.abiManager = new ABIManager(_config.abis)

this.baseFeeMargin = _config.gas.baseFeePercentageMargin ?? 20n
this.maxPriorityFeePerGas = _config.gas.maxPriorityFeePerGas
this.minPriorityFeePerGas = _config.gas.minPriorityFeePerGas
this.priorityFeeTargetPercentile = _config.gas.priorityFeeTargetPercentile ?? 50
this.priorityFeeAnalysisBlocks = _config.gas.priorityFeeAnalysisBlocks ?? 2
this.eip1559 = _config.gas?.eip1559 ?? opStackDefaultEIP1559Parameters
this.abiManager = new ABIManager(_config.abis ?? {})

this.baseFeeMargin = _config.gas?.baseFeePercentageMargin ?? 20n
this.maxPriorityFeePerGas = _config.gas?.maxPriorityFeePerGas
this.minPriorityFeePerGas = _config.gas?.minPriorityFeePerGas
this.priorityFeeTargetPercentile = _config.gas?.priorityFeeTargetPercentile ?? 50
this.priorityFeeAnalysisBlocks = _config.gas?.priorityFeeAnalysisBlocks ?? 2

this.rpcAllowDebug = _config.rpc.allowDebug ?? false
this.blockTime = _config.blockTime ?? 2n
Expand Down
2 changes: 1 addition & 1 deletion packages/txm/lib/TransactionSubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class TransactionSubmitter {
from: this.txmgr.viemWallet.account.address,
to: transaction.address,
data: transaction.calldata,
value: 0n,
value: transaction.value,
nonce: attempt.nonce,
maxFeePerGas: attempt.maxFeePerGas,
maxPriorityFeePerGas: attempt.maxPriorityFeePerGas,
Expand Down
8 changes: 8 additions & 0 deletions packages/txm/lib/db/migrations/Migration20250421120000.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Kysely } from "kysely"
import type { Database } from "../types"

export async function up(db: Kysely<Database>) {
await db.schema.alterTable("transaction").addColumn("value", "text").execute()
}

export const migration20250421120000 = { up }
2 changes: 2 additions & 0 deletions packages/txm/lib/db/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { migration20241111223000 } from "./Migration20241111223000"
import { migration20241205104400 } from "./Migration20241205104400"
import { migration20250121110600 } from "./Migration20250121110600"
import { migration20250410123000 } from "./Migration20250410123000"
import { migration20250421120000 } from "./Migration20250421120000"

export const migrations = {
"20241111163800": migration20241111163800,
"20241111223000": migration20241111223000,
"20241205104400": migration20241205104400,
"20250121110600": migration20250121110600,
"20250410123000": migration20250410123000,
"20250421120000": migration20250421120000,
}
1 change: 1 addition & 0 deletions packages/txm/lib/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface TransactionTable {
from: Address
chainId: number
address: Address
value: string
functionName: string | undefined
args: string | undefined
contractName: string | undefined
Expand Down
39 changes: 38 additions & 1 deletion packages/txm/test/txm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { deployMockContracts } from "./utils/contracts"
import { assertIsDefined, assertIsOk, assertReceiptReverted, assertReceiptSuccess } from "./utils/customAsserts"
import { cleanDB, getPersistedTransaction } from "./utils/db"
import { bigIntToZeroPadded } from "@happy.tech/common"

const retryManager = new TestRetryManager()

Expand Down Expand Up @@ -612,6 +613,42 @@ test("Transaction cancelled due to deadline passing", async () => {
})
})

test("Execute a transaction with value", async () => {
const value = BigInt(1000)
const transactionToSend = await txm.createTransaction({
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
calldata: "0x",
value,
})

transactionQueue.push(transactionToSend)

await mineBlock(2)

const transactionExecuted = await txm.getTransaction(transactionToSend.intentId)

if (!assertIsOk(transactionExecuted)) return

const transactionExecutedValue = transactionExecuted.value

if (!assertIsDefined(transactionExecutedValue)) return

const blockchainTransaction = await directBlockchainClient.getTransaction({
hash: transactionExecutedValue.attempts[0].hash,
})

const blockchainTransactionReceipt = await directBlockchainClient.getTransactionReceipt({
hash: blockchainTransaction.hash,
})

const persistedTransaction = await getPersistedTransaction(transactionToSend.intentId)

expect(blockchainTransactionReceipt?.status).toBe("success")
expect(blockchainTransaction.value).toBe(value)
expect(persistedTransaction).toBeDefined()
expect(persistedTransaction?.value).toBe(bigIntToZeroPadded(value))
})

test("Correctly calculates baseFeePerGas after a block with high gas usage", async () => {
const transactionBurner = await txm.createTransaction({
address: deployment.MockGasBurner,
Expand Down Expand Up @@ -885,4 +922,4 @@ test("RPC liveness monitor works correctly", async () => {
value: previousLivenessWindow,
configurable: true,
})
})
})
14 changes: 12 additions & 2 deletions support/common/lib/utils/bigint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,19 @@ export const bigIntReviver = (_key: string, value: unknown): unknown => {
}

/**
* Converts a bigint to a zero-padded string
* The maximum number of digits in the max uint256 value
*/
export function bigIntToZeroPadded(value: bigint, totalDigits: number): string {
export const DIGITS_MAX_UINT256 = 78

/**
* Converts a bigint to a zero-padded string representation
* @param value - The bigint value to convert
* @param totalDigits - The total number of digits in the resulting string (defaults to {@link DIGITS_MAX_UINT256})
* @returns A string representation of the bigint with leading zeros to match the specified length
* @example
* bigIntToZeroPadded(123n, 5) // returns "00123"
*/
export function bigIntToZeroPadded(value: bigint, totalDigits: number = DIGITS_MAX_UINT256): string {
const str = value.toString()
return str.padStart(totalDigits, "0")
}
Expand Down