-
Notifications
You must be signed in to change notification settings - Fork 2
Txm no longer receives Viem objects #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
93e306e
53b315d
71e4631
1b25f8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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}`)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -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 | ||
| /** | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
|
@@ -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, | ||
| }) | ||
| } | ||
|
|
||
| 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, | ||
| }), | ||
| ) | ||
|
|
||
|
|
@@ -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.", | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why seperate const rpcChainId = await this.viemClient.safeGetChainId()
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.