import { createWalletClient, fallback, http, isHex, keccak256, type Account, type Hex, type PublicClient, type Transport, type WalletClient, } from 'viem'; import { bsc } from 'viem/chains'; import { BSC_MAINNET } from '@agripinaa/shared'; const DEFAULT_GAS_LIMIT = 2_000_000n; const DEFAULT_MAX_TX_FEE_WEI = 10_000_000_000_000_000n; // 0.01 BNB /** * A fallback transport can submit a raw transaction successfully to one RPC, * lose that response, then receive `already known` from the next RPC. The * transaction hash is deterministic over the signed bytes, so recovering it * locally is both exact and safer than resubmitting with another nonce. * * Never recover from a generic `nonce too low`: that can describe an unrelated * transaction. The backend must explicitly say these same raw bytes are known. */ export function knownRawTransactionHash( method: string, params: readonly unknown[] | undefined, error: unknown, ): Hex | undefined { if (method !== 'eth_sendRawTransaction') return undefined; const message = error instanceof Error ? error.message : String(error); if (!/\b(already known|known transaction)\b/i.test(message)) return undefined; const serialized = params?.[0]; if (typeof serialized !== 'string' || serialized === '0x' || !isHex(serialized)) return undefined; return keccak256(serialized); } function recoverKnownRawTransactions(base: Transport): Transport { return (options) => { const transport = base(options); return { ...transport, async request({ method, params }) { try { return await transport.request({ method, params }); } catch (error) { const hash = knownRawTransactionHash(method, params as readonly unknown[] | undefined, error); if (hash) return hash; throw error; } }, } as ReturnType; }; } function positiveEnv(name: string, fallbackValue: bigint): bigint { const raw = process.env[name]; if (raw === undefined) return fallbackValue; if (!/^\d+$/.test(raw) || BigInt(raw) <= 0n) { throw new Error(`${name} must be a positive base-unit integer`); } return BigInt(raw); } export function boundedLegacyFees(input: { requestedGas?: bigint; gasPrice: bigint; gasLimit?: bigint; maxTxFeeWei?: bigint; }): { gas: bigint; gasPrice: bigint } { const ceiling = input.gasLimit ?? DEFAULT_GAS_LIMIT; const gas = input.requestedGas ?? ceiling; const maxTxFeeWei = input.maxTxFeeWei ?? DEFAULT_MAX_TX_FEE_WEI; if (gas <= 0n || gas > ceiling) { throw new Error(`transaction gas ${gas} exceeds agent limit ${ceiling}`); } if (input.gasPrice <= 0n || input.gasPrice * gas > maxTxFeeWei) { throw new Error( `transaction fee ceiling exceeded: gas=${gas}, gasPrice=${input.gasPrice}, maxWei=${maxTxFeeWei}`, ); } return { gas, gasPrice: input.gasPrice }; } /** * Local-account wallet whose writes never trust a provider-supplied gas * estimate. The limit is explicit and the legacy gas price comes from the * quorum public client; a hostile backend can delay a write, but cannot make * the signer authorize an unbounded fee. */ export function createGuardedWalletClient( account: Account, publicClient: PublicClient, ): WalletClient { const transport = recoverKnownRawTransactions( fallback(BSC_MAINNET.rpcUrls.map((url) => http(url))), ); const client = createWalletClient({ account, chain: bsc, transport }); const gasLimit = positiveEnv('AGENTS_MAX_GAS_LIMIT', DEFAULT_GAS_LIMIT); const maxTxFeeWei = positiveEnv('AGENTS_MAX_TX_FEE_WEI', DEFAULT_MAX_TX_FEE_WEI); const feeFields = async (requestedGas?: bigint) => boundedLegacyFees({ requestedGas, gasPrice: await publicClient.getGasPrice(), gasLimit, maxTxFeeWei, }); const writeContract = async (...args: Parameters) => { const request = args[0]; return client.writeContract({ ...request, ...await feeFields(request.gas), maxFeePerGas: undefined, maxPriorityFeePerGas: undefined, } as never); }; const sendTransaction = async (...args: Parameters) => { const request = args[0]; return client.sendTransaction({ ...request, ...await feeFields(request.gas), maxFeePerGas: undefined, maxPriorityFeePerGas: undefined, } as never); }; const overrides = new Map([ ['writeContract', writeContract], ['sendTransaction', sendTransaction], ]); return new Proxy(client, { get(target, property) { if (overrides.has(property)) return overrides.get(property); const value = Reflect.get(target, property, target) as unknown; return typeof value === 'function' ? value.bind(target) : value; }, }) as WalletClient; }