import { ethers, Interface, type InterfaceAbi, JsonRpcProvider } from 'ethers'; import type { Abi } from 'abitype'; import type { TypedContract } from '../../types/typed-contract'; import type { IAddress, IContractRunner, IInfuraOptions } from '../../types'; /** * Web3 Helper Utilities * * Core utilities for blockchain interaction: * - Provider Management: createProvider, getInfuraProvider, getChainId * - Contract Helpers: createContract, getDecimals, getSymbol, getTokenMetadata * - Transaction Utilities: simulateTransaction, checkAddress * - Historical Queries: getHistoricalContractData, getHistoricalContractDataByDate * - Oracle Integration: getLoanOracleFeeRate * - Block Utilities: determineBlockCutoff, determineBlockSkipInternal */ /** * Get maximum block range for historical queries per chain. * Prevents RPC timeouts by limiting lookback window. * @param chain Chain ID * @returns Maximum number of blocks to query */ export declare const determineBlockCutoff: (chain: number) => 120000 | 3456000 | 150000; /** * Approximate block time in seconds per chain. * Used to estimate time ranges from block counts. * @param chain - Chain ID * @returns Seconds per block */ export declare const determineSecondsPerBlock: (chain: number) => number; /** * Get block interval for paginated historical queries per chain. * Respects RPC eth_getLogs range limits. * @param chain Chain ID * @returns Block interval for pagination */ export declare const determineBlockSkipInternal: (chain: number) => 1000 | 8000 | 10000; /** * Maximum JSON-RPC calls ethers may coalesce into a single batched HTTP * request for a given endpoint. * * Providers advertise wildly different batch limits and reject the whole batch * — not the excess — when exceeded, so one oversized batch fails every call * inside it. dRPC's free tier caps batches at 3, which made every Mezo read * fail with `server response 500 … "Batch of more than 3 requests are not * allowed"` (the single highest-volume SDK error in production). * * Detection is host-based rather than chain-based because the limit is a * property of the endpoint, not the chain: the same chain served by a * self-hosted node has no such cap. * * @param rpcUrl - The RPC endpoint URL. Unparseable values fall back to the * conservative default rather than throwing. * @returns Batch size cap to pass to ethers as `batchMaxCount`. */ export declare const determineRpcBatchMaxCount: (rpcUrl: string) => number; /** * Retrieve chain ID from web3 provider. * Handles both initialized and uninitialized provider states. * @param provider Web3 provider instance * @returns Numeric chain ID */ export declare const getChainId: (provider: IContractRunner) => Promise; /** * Create ethers contract instance with address validation. * @param provider Web3 provider for contract calls * @param address Contract address (must be checksummed) * @param abi Contract ABI (must be declared with `as const`) * @returns Strongly-typed contract instance or undefined if address invalid */ export declare function createContract({ provider, address, abi, }: { address: string; provider: IContractRunner; abi: TAbi; }): TypedContract | undefined; /** * Create or reuse a cached JSON-RPC provider. * Passing `chainId` enables ethers' `staticNetwork` to skip the `eth_chainId` * round-trip on first use. * * @param rpcUrl RPC endpoint URL — must be a non-empty string. * @param chainId Optional chain ID — enables staticNetwork * @throws Error with a clear, remediable message when `rpcUrl` is missing or * empty. Without this guard, ethers v6 silently defaults to * `http://localhost:8545` and surfaces a cryptic * `network is not available yet (NETWORK_ERROR)` on the first RPC call. */ export declare const createProvider: (rpcUrl: string, chainId?: number) => JsonRpcProvider; /** * Create or reuse a cached Infura provider for the specified chain. * @param infura Configuration with chain ID and API key */ export declare const getInfuraProvider: (infura: IInfuraOptions) => JsonRpcProvider; export { explorerLink } from './explorer-link'; /** * Fetch token decimals from contract or Solana mint. * Results are cached to minimize RPC calls. * * **Never throws** — a failed read resolves `undefined`. Callers that must not * silently proceed on an unknown scale (any path that encodes an amount) should * use {@link getDecimalsOrThrow} instead: feeding `undefined` into * `toNormalizedBn` silently defaults to 18 decimals. * * Transient transport faults (provider rate limits, socket resets, an empty * response to `decimals()`) are logged at `warn` — a breadcrumb, not a * standalone Sentry issue — since the caller sees the same `undefined` either * way. Everything else is logged at `error`. The read itself is **not** * retried here; that is opt-in via {@link getDecimalsOrThrow}. * * @param provider Web3 provider * @param address Token contract address or Solana mint * @param isVault Resolve `address` as a vault (evm-2 vaults redirect to the * receipt token) rather than reading it as a plain ERC-20. Defaults to `true`. * @returns Number of decimals for the token, or `undefined` when the read failed. */ export declare const getDecimals: (provider: IContractRunner, address: IAddress, isVault?: boolean) => Promise; /** * Read a token's `decimals()` with the **same cache and stampede protection as * {@link getDecimals}**, but surfacing failures instead of swallowing them, and * retrying the transient ones. * * Two reasons this exists rather than a flag on `getDecimals`: * * 1. **`getDecimals` must keep returning `undefined` on failure** — a dozen * read paths depend on that. Amount-encoding paths need the opposite: an * `undefined` reaching `toNormalizedBn` silently means 18 decimals, which * misencodes the transaction. Here the original error propagates, so the * caller's `AugustSDKError` keeps its cause and its Sentry grouping. * 2. **Retry.** Providers intermittently return an empty response to * `decimals()`, which ethers reports as * `missing revert data (action="call", data="0x313ce567", …)` — a shape a * deployed ERC-20 cannot legitimately produce, since `decimals()` takes no * arguments. That is retried here via {@link isEmptyViewResponse} scoped to * this exact selector, alongside ordinary transport faults * ({@link isRetryableRpcError}). The empty-view widening applies **only** * here; everywhere else the strict transport definition stands. A genuine * revert (`CALL_EXCEPTION` carrying revert data) is never retried. * * Cache is shared with `getDecimals`, so a read-then-write flow against the * same token costs one `decimals()` RPC in total, and N concurrent callers for * an uncached token collapse to one. * * **Exception — an unresolvable chain scope bypasses the cache entirely.** When * {@link resolveProviderScope} cannot determine the chain (a runner with no * resolved network and no connection URL, e.g. a browser provider before its * first request), the only available key is the shared `unknown` bucket. Read * paths happily use that bucket, and this function deliberately does not: the * two paths have different blast radii. A wrong cached `decimals` on a read * renders a wrong number on screen; on a write it misencodes an amount the user * then *signs*. The collision needed — the same address being a different token * with different decimals on two chains, within one process, across a * mid-session chain switch — is narrow, but "no worse than the read path" is * not the bar for a value that ends up in a transaction. In that case the read * is neither served from nor written to the cache, and it also skips the * in-flight dedup map, since joining another caller under an untrusted key * would reintroduce exactly the collision being avoided. Retries still apply; * the only thing forgone is the RPC saving. * * RPC cost: 1 on a cache miss, 0 on a hit, up to 3 on a miss that keeps * faulting (~750ms of added latency in the worst case before it gives up). * Always ≥1 when the chain scope is unresolvable. * * @param runner - Provider or signer to read through. A signer is unwrapped to * its provider for cache scoping, so it shares entries with reads on the * same chain. * @param address - Token address. Read directly as an ERC-20 — pass the * receipt-token address yourself for an `evm-2` vault's share scale. * @param tag - Low-cardinality label for retry breadcrumbs (e.g. * `'vaultDeposit:poolDecimals'`). * @returns The token's decimals. * @throws The underlying read error once retries are exhausted, or immediately * when the failure is neither a transport fault nor an empty view response. * * @example * ```ts * // Amount encoding must not proceed on a guessed scale. * const decimals = await getDecimalsOrThrow(signer, token, 'deposit:decimals'); * const amount = toNormalizedBn(userInput, decimals); * ``` */ export declare const getDecimalsOrThrow: (runner: IContractRunner, address: IAddress, tag: string) => Promise; /** * Fetch the whitelisted deposit-asset addresses from a vault's whitelist * contract. Cached with a 5-minute TTL; a fresh fetch fans out into N * decimals/symbol reads downstream. * @internal */ export declare const getWhitelistedAssets: (provider: IContractRunner, whitelistAddress: IAddress) => Promise; /** * Read an `evm-2` vault's receipt (LP) token address via `lpTokenAddress()`, * retrying only the transient transport faults and surfacing everything else * unchanged. * * Why this exists: `lpTokenAddress()` sits immediately before the receipt-token * `decimals()` read on every `evm-2` write path (approve / deposit / request * redeem / swap-router deposit). Those `decimals()` reads were hardened against * provider blips ({@link getDecimalsOrThrow}); the `lpTokenAddress()` read one * line above them was not, so a single truncated `eth_call` response still * failed the whole write with * `missing revert data (action="call", data="0xf5ae497a", …)` — observed in * production against a mainnet vault whose `lpTokenAddress()` demonstrably * returns a real address when the provider is healthy. * * **This must never hide a misrouted vault, and does not.** `lpTokenAddress()` * exists *only* on `evm-2` vaults. A vault wrongly classified as `evm-2` returns * empty returndata for it, deterministically and forever, and that empty * response is byte-identical to the transient one — it is the *only* signal the * SDK gets that its version routing was wrong. So the retry here is deliberately * shaped to preserve that signal: * * - it is **bounded** (3 attempts, ~750ms of backoff in total — see * `retryOnTransientRpc`), never open-ended; * - on exhaustion it **rethrows the original error object**, with its identity, * message, `code` and `transaction.data` intact, so the caller's * `AugustSDKError` cause and Sentry grouping are exactly what they are today; * - it has **no fallback**: it never substitutes another address, never resolves * the vault address itself, and never resolves `null`/`undefined`. It either * returns a real receipt-token address or throws. * * Net effect: a blip costs a retry, a misroute costs three `eth_call`s and then * fails exactly as loudly as before. * * **Deliberately not cached.** Unlike `decimals()`, the vault→receipt-token * mapping is not memoized here. Caching it would make a misroute's first * (failed) probe and every subsequent one diverge, and would put a * vault-identity mapping in the cache on the money path. The lenient, cached * reader is {@link getReceiptTokenAddress} — use that on read paths that can * tolerate `undefined`. * * RPC cost: exactly 1 `eth_call` on success; at most 3 when the provider keeps * faulting. * * @param runner - Provider or signer to read through. * @param vault - Address of the `evm-2` tokenized vault. * @param tag - Low-cardinality label for retry breadcrumbs (e.g. * `'vaultRequestRedeem:receiptToken'`). * @returns The vault's receipt (LP) token address. Never `null`/`undefined`. * @throws The underlying read error, unmodified, once the bounded retries are * exhausted — or immediately when the failure is neither a transport fault nor * an empty response to this exact selector (e.g. a genuine revert carrying * revert data). * * @example * ```ts * const receiptToken = await getReceiptTokenAddressOrThrow( * signer, * vault, * 'vaultRequestRedeem:receiptToken', * ); * const decimals = await getDecimalsOrThrow(signer, receiptToken, 'tag'); * ``` */ export declare const getReceiptTokenAddressOrThrow: (runner: IContractRunner, vault: IAddress, tag: string) => Promise; /** * Fetch receipt token address from tokenized vault contract. * Results are cached to minimize RPC calls. * * **Never throws** — a failed read logs at error level and resolves * `undefined`, which several read paths depend on. The underlying * `lpTokenAddress()` call is made through * {@link getReceiptTokenAddressOrThrow}, so a transient provider blip is * absorbed by a bounded retry before that happens; a deterministic failure (a * vault that has no `lpTokenAddress()` because it is not `evm-2`) still resolves * `undefined` after the attempts are spent, exactly as before. Callers on a * money path must use {@link getReceiptTokenAddressOrThrow} directly instead — * an `undefined` receipt-token address there would silently mis-address a * `decimals()` read. * * @param provider Web3 provider * @param address Tokenized vault contract address * @returns Receipt token address, or `undefined` when the read failed. */ export declare const getReceiptTokenAddress: (provider: IContractRunner, address: IAddress) => Promise<`0x${string}`>; /** * Fetch token symbol from contract. * Results are cached to minimize RPC calls. * @param provider Web3 provider * @param address Token contract address * @returns Token symbol string */ export declare const getSymbol: (provider: IContractRunner, address: IAddress | undefined, isVault?: boolean) => Promise; /** * Batch fetch multiple token metadata fields in a single call. * Efficiently retrieves name, symbol, decimals, and totalSupply. * @param provider Web3 provider * @param asset Token contract address * @param meta Array of metadata fields to fetch * @returns Array of metadata values in same order as requested */ export declare function getTokenMetadata(provider: IContractRunner, asset: IAddress, meta: ('symbol' | 'decimals' | 'name' | 'totalSupply')[]): Promise; /** * Fetch management fee percentage from vault contract. * Performs static call first to check if method exists. * @todo fix so that when lending pool does not have "managementFeePercent", to not break everything * @param provider Web3 provider * @param address Vault contract address * @returns Management fee as percentage or undefined if not supported */ export declare const getManagementFeePercent: (provider: IContractRunner, address: IAddress) => Promise; /** * Simulate contract transaction without executing on-chain. * Useful for testing transaction viability before sending. * @param provider Web3 provider * @param abi Contract ABI * @param functionName Function to simulate * @param options Transaction parameters including args, from, to * @returns Decoded function result or false if simulation fails */ export declare function simulateTransaction(provider: IContractRunner, abi: string[] | any, functionName: string, options?: { args?: (string | number | bigint)[][]; from?: string; to?: string; }): Promise; /** * Validate Ethereum address format and checksum. * @param address Address string to validate * @param logger Optional logger for error messages * @param type Context type for logging * @returns True if address is valid and checksummed */ export declare function checkAddress(address: string, logger?: any, type?: 'wallet' | 'contract'): boolean; type ILoanOracleFeeCategories = 'LOAN.REPAY.INTERESTS'; /** * Fetch dynamic fee rate from on-chain oracle contract. * Used for calculating loan interest repayment fees. * @param provider Web3 provider * @param category_id Fee category identifier * @param address Context address for fee calculation * @param chainId Optional chain ID (auto-detected if not provided) * @returns Fee rate as decimal number */ export declare function getLoanOracleFeeRate(provider: IContractRunner, category_id: ILoanOracleFeeCategories, address: IAddress, chainId?: number): Promise; /** * Query historical contract state across a block range. * Useful for reconstructing historical data like TVL over time. * @param provider Web3 provider * @param contractAddress Target contract address * @param abi Contract ABI * @param methodName View function to call * @param args Function arguments * @param startBlock Starting block number * @param endBlock Ending block number (current block if not specified) * @param blockInterval Blocks between each query * @returns Array of results with block numbers and timestamps */ export declare function getHistoricalContractData(provider: IContractRunner, contractAddress: IAddress, abi: Interface | InterfaceAbi, methodName: string, args: (string | number | bigint | boolean)[], startBlock: number, endBlock: number, blockInterval?: number): Promise; /** * Query historical contract state at regular time intervals. * Uses binary search to find blocks closest to target timestamps. * @param provider Web3 provider * @param contractAddress Target contract address * @param abi Contract ABI * @param methodName View function to call * @param args Function arguments * @param startDate Start date (string or timestamp) * @param endDate End date (string or timestamp) * @param intervalHours Hours between each query * @returns Array of results with dates, blocks, and values */ export declare function getHistoricalContractDataByDate(provider: IContractRunner, contractAddress: IAddress, abi: Interface | InterfaceAbi, methodName: string, args: (string | number | bigint | boolean)[], startDate: string | number, endDate: string | number, intervalHours?: number): Promise;