rotationProvider.ts
1 import { BaseProvider, Network, StaticJsonRpcProvider } from '@ethersproject/providers'; 2 import { logger } from 'ethers'; 3 4 const DEFAULT_FALL_FORWARD_DELAY = 60000; 5 const MAX_RETRIES = 1; 6 7 interface RotationProviderConfig { 8 maxRetries?: number; 9 fallFowardDelay?: number; 10 } 11 12 function sleep(ms: number) { 13 return new Promise((resolve) => setTimeout(resolve, ms)); 14 } 15 16 /** 17 * Returns the network as long as all agree. Throws an error if any two networks do not match 18 * @param networks the list of networks to verify 19 * @returns Network 20 */ 21 export function checkNetworks(networks: Network[]): Network { 22 if (networks.length === 0) { 23 logger.throwArgumentError('no networks provided', 'networks', networks); 24 } 25 26 let result: Network | undefined; 27 28 for (let i = 0; i < networks.length; i++) { 29 const network = networks[i]; 30 31 if (!network) { 32 logger.throwArgumentError('network not defined', 'networks', networks); 33 } 34 35 if (!result) { 36 result = network; 37 continue; 38 } 39 40 // Make sure the network matches the previous networks 41 if ( 42 !( 43 result.name.toLowerCase() === network.name.toLowerCase() && 44 result.chainId === network.chainId && 45 (result.ensAddress?.toLowerCase() === network.ensAddress?.toLowerCase() || 46 (result.ensAddress == null && network.ensAddress == null)) 47 ) 48 ) { 49 logger.throwArgumentError('provider mismatch', 'networks', networks); 50 } 51 } 52 53 if (!result) { 54 logger.throwArgumentError('no networks defined', 'networks', networks); 55 } 56 57 return result; 58 } 59 60 /** 61 * The provider will rotate rpcs on error. 62 * If provider rotates away from the first RPC, rotate back after a set interval to prioritize using most reliable RPC. 63 * If provider rotates through all rpcs, delay to avoid spamming rpcs with requests. 64 */ 65 export class RotationProvider extends BaseProvider { 66 readonly providers: StaticJsonRpcProvider[]; 67 private currentProviderIndex = 0; 68 private firstRotationTimestamp = 0; 69 // number of full loops through provider array before throwing an error 70 private maxRetries = 0; 71 private retries = 0; 72 // if we rotate away from first rpc, return back after this delay 73 private fallForwardDelay: number; 74 75 private lastError = ''; 76 77 constructor(urls: string[], chainId: number, config?: RotationProviderConfig) { 78 super(chainId); 79 this.providers = urls.map((url) => new StaticJsonRpcProvider(url, chainId)); 80 81 this.maxRetries = config?.maxRetries || MAX_RETRIES; 82 this.fallForwardDelay = config?.fallFowardDelay || DEFAULT_FALL_FORWARD_DELAY; 83 } 84 85 /** 86 * If we rotate away from the first RPC, rotate back after a set interval to prioritize using most reliable RPC 87 */ 88 async fallForwardRotation() { 89 const now = new Date().getTime(); 90 const diff = now - this.firstRotationTimestamp; 91 if (diff < this.fallForwardDelay) { 92 await sleep(this.fallForwardDelay - diff); 93 this.currentProviderIndex = 0; 94 } 95 } 96 97 /** 98 * If rpc fails, rotate to next available and trigger rotation or fall forward delay where applicable 99 * @param prevIndex last updated index, checked to avoid having multiple active rotations 100 */ 101 private async rotateUrl(prevIndex: number) { 102 // don't rotate when another rotation was already triggered 103 if (prevIndex !== this.currentProviderIndex) return; 104 // if we rotate away from the first url, switch back after FALL_FORWARD_DELAY 105 if (this.currentProviderIndex === 0) { 106 this.currentProviderIndex += 1; 107 this.firstRotationTimestamp = new Date().getTime(); 108 this.fallForwardRotation(); 109 } else if (this.currentProviderIndex === this.providers.length - 1) { 110 this.retries += 1; 111 if (this.retries > this.maxRetries) { 112 this.retries = 0; 113 throw new Error( 114 `RotationProvider exceeded max number of retries. Last error: ${this.lastError}` 115 ); 116 } 117 this.currentProviderIndex = 0; 118 } else { 119 this.currentProviderIndex += 1; 120 } 121 } 122 123 async detectNetwork(): Promise<Network> { 124 const networks = await Promise.all(this.providers.map((c) => c.getNetwork())); 125 return checkNetworks(networks); 126 } 127 128 // eslint-disable-next-line @typescript-eslint/no-explicit-any 129 async send(method: string, params: Array<any>): Promise<any> { 130 const index = this.currentProviderIndex; 131 try { 132 return await this.providers[index].send(method, params); 133 } catch (e) { 134 console.error(e.message); 135 await this.rotateUrl(index); 136 return this.send(method, params); 137 } 138 } 139 140 // eslint-disable-next-line 141 async perform(method: string, params: any): Promise<any> { 142 const index = this.currentProviderIndex; 143 try { 144 return await this.providers[index].perform(method, params); 145 } catch (e) { 146 console.error(e.message); 147 this.lastError = e.message; 148 this.emit('debug', { 149 action: 'perform', 150 provider: this.providers[index], 151 }); 152 await this.rotateUrl(index); 153 return await this.perform(method, params); 154 } 155 } 156 }