diff --git a/packages/common/lib/index.ts b/packages/common/lib/index.ts index 6d7b9f0d7e..88a419e1cb 100644 --- a/packages/common/lib/index.ts +++ b/packages/common/lib/index.ts @@ -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" diff --git a/packages/common/lib/utils/safeViemClients.ts b/packages/common/lib/utils/safeViemClients.ts index 02f6becc59..ec24ec78e4 100644 --- a/packages/common/lib/utils/safeViemClients.ts +++ b/packages/common/lib/utils/safeViemClients.ts @@ -3,6 +3,7 @@ import type { Account, Chain, EstimateGasErrorType, + GetChainIdErrorType, GetTransactionReceiptErrorType, Hash, PublicClient, @@ -83,6 +84,7 @@ export interface SafeViemPublicClient extends ViemPublicClient { safeDebugTransaction: ( ...args: DebugTransactionSchema["Parameters"] ) => ResultAsync + safeGetChainId: () => ResultAsync>, GetChainIdErrorType> } export function convertToSafeViemPublicClient(client: ViemPublicClient): SafeViemPublicClient { @@ -99,6 +101,7 @@ export function convertToSafeViemPublicClient(client: ViemPublicClient): SafeVie }), unknownToError, ), + safeGetChainId: async () => ResultAsync.fromPromise(client.getChainId(), unknownToError), }) return client as SafeViemPublicClient diff --git a/packages/common/lib/utils/url.ts b/packages/common/lib/utils/url.ts new file mode 100644 index 0000000000..89f0ecb7f5 --- /dev/null +++ b/packages/common/lib/utils/url.ts @@ -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}`)) +} diff --git a/packages/randomness-service/src/env.ts b/packages/randomness-service/src/env.ts index 967a59bb1f..8de671c951 100644 --- a/packages/randomness-service/src/env.ts +++ b/packages/randomness-service/src/env.ts @@ -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) diff --git a/packages/randomness-service/src/index.ts b/packages/randomness-service/src/index.ts index 7b253beb93..8f1e286080 100644 --- a/packages/randomness-service/src/index.ts +++ b/packages/randomness-service/src/index.ts @@ -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" @@ -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, diff --git a/packages/transaction-manager/lib/TransactionManager.ts b/packages/transaction-manager/lib/TransactionManager.ts index c6faa22735..7914899dc1 100644 --- a/packages/transaction-manager/lib/TransactionManager.ts +++ b/packages/transaction-manager/lib/TransactionManager.ts @@ -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 - /** - * 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 + + 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 }