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
2 changes: 2 additions & 0 deletions packages/common/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ export { convertToSafeViemWalletClient, convertToSafeViemPublicClient } from "./
export { hexSchema } from "./utils/zod.js"

export { HappyMethodNames } from "./utils/constants"

export { getUrlProtocol } from "./utils/url.js"
3 changes: 3 additions & 0 deletions packages/common/lib/utils/safeViemClients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Account,
Chain,
EstimateGasErrorType,
GetChainIdErrorType,
GetTransactionReceiptErrorType,
Hash,
PublicClient,
Expand Down Expand Up @@ -83,6 +84,7 @@ export interface SafeViemPublicClient extends ViemPublicClient {
safeDebugTransaction: (
...args: DebugTransactionSchema["Parameters"]
) => ResultAsync<DebugTransactionSchema["ReturnType"], RpcErrorType>
safeGetChainId: () => ResultAsync<Awaited<ReturnType<ViemPublicClient["getChainId"]>>, GetChainIdErrorType>
}

export function convertToSafeViemPublicClient(client: ViemPublicClient): SafeViemPublicClient {
Expand All @@ -99,6 +101,7 @@ export function convertToSafeViemPublicClient(client: ViemPublicClient): SafeVie
}),
unknownToError,
),
safeGetChainId: async () => ResultAsync.fromPromise(client.getChainId(), unknownToError),
})

return client as SafeViemPublicClient
Expand Down
17 changes: 17 additions & 0 deletions packages/common/lib/utils/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type Result, err, ok } from "neverthrow"

export function getUrlProtocol(url: string): Result<"http" | "websocket", Error> {
const parsedUrl = new URL(url)

const protocol = parsedUrl.protocol.replace(":", "")

if (protocol === "http" || protocol === "https") {
return ok("http")
}

if (protocol === "ws" || protocol === "wss") {
return ok("websocket")
}

return err(new Error(`Protocol not supported: ${protocol}`))
}
2 changes: 2 additions & 0 deletions packages/randomness-service/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const envSchema = z.object({
.string()
.trim()
.transform((s) => BigInt(s)),
RPC_URL: z.string().trim(),
CHAIN_ID: z.number().int().positive(),
})

const parsedEnv = envSchema.safeParse(process.env)
Expand Down
12 changes: 5 additions & 7 deletions packages/randomness-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { TransactionManager, TransactionStatus } from "@happychain/transaction-manager"
import type { LatestBlock, Transaction } from "@happychain/transaction-manager"
import { webSocket } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { anvil } from "viem/chains"
import { abis } from "./ABI/random.js"
import { CommitmentManager } from "./CommitmentManager.js"
import { CustomGasEstimator } from "./CustomGasEstimator.js"
Expand All @@ -18,12 +15,13 @@ class RandomnessService {
constructor() {
this.commitmentManager = new CommitmentManager()
this.txm = new TransactionManager({
account: privateKeyToAccount(env.PRIVATE_KEY),
transport: webSocket(),
chain: anvil,
privateKey: env.PRIVATE_KEY,
chainId: env.CHAIN_ID,
abis: abis,
gasEstimator: new CustomGasEstimator(),
rpcAllowDebug: true,
rpc: {
url: env.RPC_URL,
},
})
this.commitmentTransactionFactory = new CommitmentTransactionFactory(
this.txm,
Expand Down
155 changes: 134 additions & 21 deletions packages/transaction-manager/lib/TransactionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@ import {
type UUID,
convertToSafeViemPublicClient,
convertToSafeViemWalletClient,
getUrlProtocol,
} from "@happychain/common"
import { type Abi, type Account, type Chain, type Transport, createPublicClient, createWalletClient } from "viem"
import {
type Abi,
type Hex,
type Transport as ViemTransport,
createPublicClient,
createWalletClient,
defineChain,
http as viemHttpTransport,
webSocket as viemWebSocketTransport,
} from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { ABIManager } from "./AbiManager.js"
import { BlockMonitor, type LatestBlock } from "./BlockMonitor.js"
import { DefaultGasLimitEstimator, type GasEstimator } from "./GasEstimator.js"
Expand All @@ -20,12 +31,44 @@ import { TxMonitor } from "./TxMonitor.js"
import { type EIP1559Parameters, opStackDefaultEIP1559Parameters } from "./eip1559.js"

export type TransactionManagerConfig = {
/** The transport protocol used for the client. See {@link Transport} from viem for more details. */
transport: Transport
/** The account used for transactions. See {@link Account} from viem for more details. */
account: Account
/** The blockchain network configuration. See {@link Chain} from viem for more details. */
chain: Chain
/**
* The RPC node configuration
*/
rpc: {
/**
* The url of the RPC node.
* It can be a http or websocket url.
*/
url: string
/**
* The timeout for the RPC node.
* It is very important that the value of (timeout + retryDelay) * retries be less than the time block to avoid slowing down the transaction manager.
* Defaults to 500 milliseconds.
*/
timeout?: number
/**
* The number of retries for the RPC node.
* It is very important that the value of (timeout + retryDelay) * retries be less than the time block to avoid slowing down the transaction manager.
* Defaults to 2.
*/
retries?: number
/**
* The delay between retries.
* It is very important that the value of (timeout + retryDelay) * retries be less than the time block to avoid slowing down the transaction manager.
* Defaults to 50 milliseconds.
*/
retryDelay?: number
/**
* Enables debug methods on the RPC node.
* This is necessary for retrieving revert reasons for failed transactions
* and for increasing precision in managing transactions that fail due to out-of-gas errors
* Defaults to false.
*/
allowDebug?: boolean
}
/** The private key of the account used for signing transactions. */
privateKey: Hex

/** Optional EIP-1559 parameters. If not provided, defaults to the OP stack's stock parameters. */
eip1559?: EIP1559Parameters
/**
Expand All @@ -52,20 +95,17 @@ export type TransactionManagerConfig = {
*/
abis: Record<string, Abi>

/**
* Enables debug methods on the RPC node.
* This is necessary for retrieving revert reasons for failed transactions
* and for increasing precision in managing transactions that fail due to out-of-gas errors
* Defaults to false.
*/
rpcAllowDebug?: boolean

/**
* The expected interval (in seconds) for the creation of a new block on the blockchain.
* Defaults to 2 seconds.
*/
blockTime?: bigint

/**
* The chain ID of the blockchain.
*/
chainId: number

/**
* The time (in milliseconds) after which finalized transactions are purged from the database.
* If finalizedTransactionPurgeTime is 0, finalized transactions are not purged from the database.
Expand Down Expand Up @@ -105,6 +145,7 @@ export class TransactionManager {
public readonly transactionSubmitter: TransactionSubmitter
public readonly hookManager: HookManager

public readonly chainId: number
public readonly eip1559: EIP1559Parameters
public readonly baseFeeMargin: bigint
public readonly maxPriorityFeePerGas: bigint
Expand All @@ -114,18 +155,67 @@ export class TransactionManager {

constructor(_config: TransactionManagerConfig) {
this.collectors = []

const protocol = getUrlProtocol(_config.rpc.url)

if (protocol.isErr()) {
throw protocol.error
}

const retries = _config.rpc.retries || 2
const retryDelay = _config.rpc.retryDelay || 50
const timeout = _config.rpc.timeout || 500

let transport: ViemTransport
if (protocol.value === "http") {
transport = viemHttpTransport(_config.rpc.url, {
timeout,
retryCount: retries,
retryDelay,
})
} else {
transport = viemWebSocketTransport(_config.rpc.url, {
timeout,
retryCount: retries,
retryDelay,
})
}
Comment on lines +169 to +182

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

might be nice to extract this to some utility

const transport = getViemTransport(protocol, _config.rpc)

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.

I don't mind too much, it's only used here, and this is pretty simple function that is just a long list of things being initialized, so it's not like it pollutes reasoning about what's otherwise a complex flow.


const account = privateKeyToAccount(_config.privateKey)

/**
* Define the viem chain object.
* Certain properties required by viem are set to "Unknown" because they are not relevant to our library.
* This approach eliminates the need for users to provide unnecessary properties when configuring the library.
*/
const chain = defineChain({
id: _config.chainId,
name: "Unknown",
rpcUrls: {
default: {
http: protocol.value === "http" ? [_config.rpc.url] : [],
webSocket: protocol.value === "websocket" ? [_config.rpc.url] : [],
},
},
nativeCurrency: {
name: "Unknown",
symbol: "UNKNOWN",
decimals: 18,
},
})

this.viemWallet = convertToSafeViemWalletClient(
createWalletClient({
account: _config.account,
transport: _config.transport,
chain: _config.chain,
account,
transport,
chain,
}),
)

this.viemClient = convertToSafeViemPublicClient(
createPublicClient({
transport: _config.transport,
chain: _config.chain,
transport,
chain,
}),
)

Expand All @@ -139,15 +229,23 @@ export class TransactionManager {
this.transactionSubmitter = new TransactionSubmitter(this)
this.hookManager = new HookManager()

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

this.baseFeeMargin = _config.baseFeePercentageMargin || 20n
this.maxPriorityFeePerGas = _config.maxPriorityFeePerGas || 0n

this.rpcAllowDebug = _config.rpcAllowDebug || false
this.rpcAllowDebug = _config.rpc.allowDebug || false
this.blockTime = _config.blockTime || 2n
this.finalizedTransactionPurgeTime = _config.finalizedTransactionPurgeTime || 2 * 60 * 1000

const timePerRetry = timeout + retryDelay
if (timePerRetry * retries > this.blockTime * 1000n) {
console.warn(
"The value of (timeout + retryDelay) * retries is greater than the time block. This could slow down the transaction manager.",
)
}
}

/**
Expand Down Expand Up @@ -190,12 +288,27 @@ export class TransactionManager {
// Start the gas price oracle to prevent other parts of the application from calling `suggestGasForNextBlock` before the gas price oracle has initialized the gas price after processing the first block
const priceOraclePromise = this.gasPriceOracle.start()

// Get the chain ID of the RPC node
const rpcChainIdPromise = this.viemClient.safeGetChainId()

// Start the transaction repository
await this.transactionRepository.start()

// Start the nonce manager, which depends on the transaction repository
await this.nonceManager.start()

const rpcChainId = await rpcChainIdPromise
Comment on lines +291 to +300

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why seperate rpcChainIdPromise like this?

const rpcChainId = await this.viemClient.safeGetChainId()

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.

Latency optimization: kick off the call early, then do the initialization, then come back and wait until the id is there. Otherwise we would have to wait the entire network roundtrip for chainid without doing any useful work locally.

@GabrielMartinezRodriguez GabrielMartinezRodriguez Jan 6, 2025

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.

Yes, I do it this way for the reasons @norswap mentions


if (rpcChainId.isErr()) {
throw rpcChainId.error
}

if (rpcChainId.value !== this.chainId) {
const errorMessage = `The chain ID of the RPC node (${rpcChainId.value}) does not match the chain ID of the transaction manager (${this.chainId}).`
console.error(errorMessage)
throw new Error(errorMessage)
}

// Await the completion of the gas price oracle startup before marking the TransactionManager as started
await priceOraclePromise
}
Expand Down