|
| 1 | +import { mkdir, writeFile } from "node:fs/promises"; |
| 2 | +import path from "node:path"; |
| 3 | +import { setTimeout } from "node:timers/promises"; |
| 4 | +import { |
| 5 | + type Abi, |
| 6 | + type Address, |
| 7 | + BaseError, |
| 8 | + type ContractEventName, |
| 9 | + createPublicClient, |
| 10 | + erc20Abi, |
| 11 | + http, |
| 12 | + parseAbi, |
| 13 | +} from "viem"; |
| 14 | +import { |
| 15 | + getRpcProviderUrl, |
| 16 | + type RpcProvider, |
| 17 | + SUPPORTED_RPC_PROVIDERS, |
| 18 | +} from "../src/dev/index.js"; |
| 19 | +import { |
| 20 | + type GearboxChain, |
| 21 | + getChain, |
| 22 | + type NetworkType, |
| 23 | + SUPPORTED_NETWORKS, |
| 24 | +} from "../src/sdk/index.js"; |
| 25 | + |
| 26 | +const ErrorsByProvider = new Map<RpcProvider, Set<string>>(); |
| 27 | + |
| 28 | +const BLOCK_RANGES = [ |
| 29 | + 0n, |
| 30 | + 10_000_000n, |
| 31 | + 5_000_000n, |
| 32 | + 1_000_000n, |
| 33 | + 500_000n, |
| 34 | + 300_000n, |
| 35 | + 200_000n, |
| 36 | + 100_000n, |
| 37 | + 50_000n, |
| 38 | + 10_000n, |
| 39 | + 5_000n, |
| 40 | + 1_000n, |
| 41 | +]; |
| 42 | + |
| 43 | +interface Scenario<abi extends Abi | readonly unknown[] = Abi> { |
| 44 | + id: string; |
| 45 | + name: string; |
| 46 | + abi: abi; |
| 47 | + eventName: ContractEventName<abi>; |
| 48 | + address: Address | ((chain: GearboxChain) => Address); |
| 49 | +} |
| 50 | + |
| 51 | +function addError(provider: RpcProvider, error: BaseError): void { |
| 52 | + const errors = ErrorsByProvider.get(provider) ?? new Set<string>(); |
| 53 | + errors.add(error.name); |
| 54 | + ErrorsByProvider.set(provider, errors); |
| 55 | +} |
| 56 | + |
| 57 | +async function testProviderNetworkRange( |
| 58 | + network: NetworkType, |
| 59 | + provider: RpcProvider, |
| 60 | + blockRange: bigint, |
| 61 | + scenario: Scenario, |
| 62 | +): Promise<boolean> { |
| 63 | + const chain = getChain(network); |
| 64 | + const address = |
| 65 | + typeof scenario.address === "function" |
| 66 | + ? scenario.address(chain) |
| 67 | + : scenario.address; |
| 68 | + const rpcUrl = getRpcProviderUrl( |
| 69 | + provider, |
| 70 | + network, |
| 71 | + process.env[`${provider}_KEY`.toUpperCase()] ?? "", |
| 72 | + ); |
| 73 | + if (!rpcUrl) { |
| 74 | + return false; |
| 75 | + } |
| 76 | + |
| 77 | + try { |
| 78 | + const client = createPublicClient({ |
| 79 | + transport: http(rpcUrl, { timeout: 120_000 }), |
| 80 | + }); |
| 81 | + |
| 82 | + // Get latest block number |
| 83 | + const { number: finalizedBlock } = await client.getBlock({ |
| 84 | + blockTag: "finalized", |
| 85 | + }); |
| 86 | + |
| 87 | + // Get logs for Transfer event from block 0 to latest block |
| 88 | + await client.getContractEvents({ |
| 89 | + address, |
| 90 | + abi: scenario.abi, |
| 91 | + eventName: scenario.eventName, |
| 92 | + fromBlock: blockRange === 0n ? 0n : finalizedBlock - blockRange, |
| 93 | + toBlock: finalizedBlock, |
| 94 | + }); |
| 95 | + |
| 96 | + await writeFile( |
| 97 | + `tmp/${scenario.id}/${provider}-${network}-${blockRange}-ok.txt`.toLowerCase(), |
| 98 | + "OK", |
| 99 | + "utf-8", |
| 100 | + ); |
| 101 | + return true; |
| 102 | + } catch (error) { |
| 103 | + let code = ""; |
| 104 | + if (error instanceof BaseError) { |
| 105 | + code = `${error.name}\n\n`; |
| 106 | + addError(provider, error); |
| 107 | + } |
| 108 | + await writeFile( |
| 109 | + `tmp/${scenario.id}/${provider}-${network}-${blockRange}-error.txt`.toLowerCase(), |
| 110 | + `${code}${error}`, |
| 111 | + "utf-8", |
| 112 | + ); |
| 113 | + return false; |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +async function testProviderNetwork( |
| 118 | + network: NetworkType, |
| 119 | + provider: RpcProvider, |
| 120 | + scenario: Scenario, |
| 121 | +): Promise<bigint | undefined> { |
| 122 | + for (const blockRange of BLOCK_RANGES) { |
| 123 | + const success = await testProviderNetworkRange( |
| 124 | + network, |
| 125 | + provider, |
| 126 | + blockRange, |
| 127 | + scenario, |
| 128 | + ); |
| 129 | + if (success) { |
| 130 | + return blockRange; |
| 131 | + } |
| 132 | + await setTimeout(1000); |
| 133 | + } |
| 134 | + return undefined; |
| 135 | +} |
| 136 | + |
| 137 | +async function testNetwork( |
| 138 | + network: NetworkType, |
| 139 | + scenario: Scenario, |
| 140 | +): Promise<Record<RpcProvider, bigint | undefined>> { |
| 141 | + console.log(`\n=== Testing ${network}`); |
| 142 | + const results = await Promise.all( |
| 143 | + SUPPORTED_RPC_PROVIDERS.map(provider => |
| 144 | + testProviderNetwork(network, provider, scenario), |
| 145 | + ), |
| 146 | + ); |
| 147 | + return results.reduce( |
| 148 | + (acc, result, index) => { |
| 149 | + acc[SUPPORTED_RPC_PROVIDERS[index]] = result; |
| 150 | + return acc; |
| 151 | + }, |
| 152 | + {} as Record<RpcProvider, bigint | undefined>, |
| 153 | + ); |
| 154 | +} |
| 155 | + |
| 156 | +function formatBlockRange(range: bigint | undefined): string { |
| 157 | + if (range === undefined) { |
| 158 | + return "✗"; |
| 159 | + } |
| 160 | + if (range === 0n) { |
| 161 | + return "∞"; |
| 162 | + } |
| 163 | + if (range >= 1_000_000n) { |
| 164 | + return `${range / 1_000_000n}M`; |
| 165 | + } |
| 166 | + if (range >= 1_000n) { |
| 167 | + return `${range / 1_000n}K`; |
| 168 | + } |
| 169 | + return range.toString(); |
| 170 | +} |
| 171 | + |
| 172 | +function printResultsTable( |
| 173 | + results: Map<NetworkType, Record<RpcProvider, bigint | undefined>>, |
| 174 | +): void { |
| 175 | + const networks = Array.from(results.keys()); |
| 176 | + const providers = SUPPORTED_RPC_PROVIDERS; |
| 177 | + |
| 178 | + // Calculate column widths |
| 179 | + const networkColWidth = Math.max( |
| 180 | + "Network".length, |
| 181 | + ...networks.map(n => n.length), |
| 182 | + ); |
| 183 | + const providerColWidths = providers.map(provider => |
| 184 | + Math.max( |
| 185 | + provider.length, |
| 186 | + ...networks.map(network => { |
| 187 | + const value = results.get(network)?.[provider]; |
| 188 | + return formatBlockRange(value).length; |
| 189 | + }), |
| 190 | + ), |
| 191 | + ); |
| 192 | + |
| 193 | + // Print header |
| 194 | + const headerRow = |
| 195 | + "│ " + |
| 196 | + "Network".padEnd(networkColWidth) + |
| 197 | + " │ " + |
| 198 | + providers |
| 199 | + .map((provider, i) => provider.padEnd(providerColWidths[i])) |
| 200 | + .join(" │ ") + |
| 201 | + " │"; |
| 202 | + const separatorRow = |
| 203 | + "├" + |
| 204 | + "─".repeat(networkColWidth + 2) + |
| 205 | + "┼" + |
| 206 | + providerColWidths.map(w => "─".repeat(w + 2)).join("┼") + |
| 207 | + "┤"; |
| 208 | + const topBorder = |
| 209 | + "┌" + |
| 210 | + "─".repeat(networkColWidth + 2) + |
| 211 | + "┬" + |
| 212 | + providerColWidths.map(w => "─".repeat(w + 2)).join("┬") + |
| 213 | + "┐"; |
| 214 | + const bottomBorder = |
| 215 | + "└" + |
| 216 | + "─".repeat(networkColWidth + 2) + |
| 217 | + "┴" + |
| 218 | + providerColWidths.map(w => "─".repeat(w + 2)).join("┴") + |
| 219 | + "┘"; |
| 220 | + |
| 221 | + console.log("\n" + topBorder); |
| 222 | + console.log(headerRow); |
| 223 | + console.log(separatorRow); |
| 224 | + |
| 225 | + // Print data rows |
| 226 | + for (const network of networks) { |
| 227 | + const networkResults = results.get(network); |
| 228 | + if (!networkResults) continue; |
| 229 | + const row = |
| 230 | + "│ " + |
| 231 | + network.padEnd(networkColWidth) + |
| 232 | + " │ " + |
| 233 | + providers |
| 234 | + .map((provider, i) => { |
| 235 | + const value = networkResults[provider]; |
| 236 | + return formatBlockRange(value).padEnd(providerColWidths[i]); |
| 237 | + }) |
| 238 | + .join(" │ ") + |
| 239 | + " │"; |
| 240 | + console.log(row); |
| 241 | + } |
| 242 | + |
| 243 | + console.log(bottomBorder); |
| 244 | +} |
| 245 | + |
| 246 | +async function testScenario(scenario: Scenario): Promise<void> { |
| 247 | + console.log(`\nStarting scenario ${scenario.name}...\n`); |
| 248 | + await mkdir(path.join("tmp", scenario.id), { recursive: true }); |
| 249 | + |
| 250 | + const results = new Map< |
| 251 | + NetworkType, |
| 252 | + Record<RpcProvider, bigint | undefined> |
| 253 | + >(); |
| 254 | + |
| 255 | + for (const network of SUPPORTED_NETWORKS) { |
| 256 | + results.set(network, await testNetwork(network, scenario)); |
| 257 | + } |
| 258 | + console.log(`\n=== Results for ${scenario.name}`); |
| 259 | + printResultsTable(results); |
| 260 | + console.log(`\n`); |
| 261 | +} |
| 262 | + |
| 263 | +function printErrorsTable(): void { |
| 264 | + console.info(`\n\n=== Errors by provider ===`); |
| 265 | + for (const p of SUPPORTED_RPC_PROVIDERS) { |
| 266 | + const errors = ErrorsByProvider.get(p) ?? new Set<string>(); |
| 267 | + console.log(`${p}: ${Array.from(errors).join(", ")}`); |
| 268 | + } |
| 269 | +} |
| 270 | + |
| 271 | +async function main(): Promise<void> { |
| 272 | + const scenarios: Scenario[] = [ |
| 273 | + { |
| 274 | + id: "rare", |
| 275 | + name: "Rare event", |
| 276 | + address: "0x77777777144339Bdc3aCceE992D8d4D31734CB2e", |
| 277 | + abi: parseAbi([ |
| 278 | + "event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)", |
| 279 | + ]), |
| 280 | + eventName: "OwnershipTransferred", |
| 281 | + }, |
| 282 | + { |
| 283 | + id: "impossible", |
| 284 | + name: "Impossible event", |
| 285 | + address: "0x77777777144339Bdc3aCceE992D8d4D31734CB2e", |
| 286 | + abi: parseAbi(["event SomeEvent()"]), |
| 287 | + eventName: "SomeEvent", |
| 288 | + }, |
| 289 | + { |
| 290 | + id: "frequent", |
| 291 | + name: "Frequent event", |
| 292 | + address: chain => chain.wellKnownToken.address, |
| 293 | + abi: erc20Abi, |
| 294 | + eventName: "Transfer", |
| 295 | + }, |
| 296 | + ]; |
| 297 | + for (const scenario of scenarios) { |
| 298 | + await testScenario(scenario); |
| 299 | + } |
| 300 | + printErrorsTable(); |
| 301 | + console.log("\n✓ All tests completed!"); |
| 302 | +} |
| 303 | + |
| 304 | +main().catch(error => { |
| 305 | + console.error("Fatal error:", error); |
| 306 | + process.exit(1); |
| 307 | +}); |
0 commit comments