Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve(ProviderUtils): Avoid redundant eth_chainId rpc calls #308

Merged
merged 1 commit into from
Sep 21, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions src/utils/ProviderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ interface RateLimitTask {
reject: (err: any) => void;
}

// This provider is a very small addition to the JsonRpcProvider that ensures that no more than `maxConcurrency`
// StaticJsonRpcProvider is used in place of JsonRpcProvider to avoid redundant eth_chainId queries prior to each
// request. This is safe to use when the back-end provider is guaranteed not to change.
// See https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#StaticJsonRpcProvider

// This provider is a very small addition to the StaticJsonRpcProvider that ensures that no more than `maxConcurrency`
// requests are ever in flight. It uses the async/queue library to manage this.
class RateLimitedProvider extends ethers.providers.JsonRpcProvider {
class RateLimitedProvider extends ethers.providers.StaticJsonRpcProvider {
// The queue object that manages the tasks.
private queue: QueueObject;

// Takes the same arguments as the JsonRpcProvider, but it has an additional maxConcurrency value at the beginning
// of the list.
constructor(
maxConcurrency: number,
...jsonRpcConstructorParams: ConstructorParameters<typeof ethers.providers.JsonRpcProvider>
...jsonRpcConstructorParams: ConstructorParameters<typeof ethers.providers.StaticJsonRpcProvider>
) {
super(...jsonRpcConstructorParams);

Expand Down Expand Up @@ -59,7 +63,7 @@ function delay(s: number): Promise<void> {
return new Promise<void>((resolve) => setTimeout(resolve, Math.round(s * 1000)));
}

function formatProviderError(provider: ethers.providers.JsonRpcProvider, rawErrorText: string) {
function formatProviderError(provider: ethers.providers.StaticJsonRpcProvider, rawErrorText: string) {
return `Provider ${provider.connection.url} failed with error: ${rawErrorText}`;
}

Expand All @@ -68,10 +72,10 @@ function createSendErrorWithMessage(message: string, sendError: any) {
return { ...sendError, ...error };
}

class RetryProvider extends ethers.providers.JsonRpcProvider {
readonly providers: ethers.providers.JsonRpcProvider[];
class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
readonly providers: ethers.providers.StaticJsonRpcProvider[];
constructor(
params: ConstructorParameters<typeof ethers.providers.JsonRpcProvider>[],
params: ConstructorParameters<typeof ethers.providers.StaticJsonRpcProvider>[],
chainId: number,
readonly nodeQuorumThreshold: number,
readonly retries: number,
Expand Down Expand Up @@ -99,17 +103,17 @@ class RetryProvider extends ethers.providers.JsonRpcProvider {
const quorumThreshold = this._getQuorum(method, params);
const requiredProviders = this.providers.slice(0, quorumThreshold);
const fallbackProviders = this.providers.slice(quorumThreshold);
const errors: [ethers.providers.JsonRpcProvider, string][] = [];
const errors: [ethers.providers.StaticJsonRpcProvider, string][] = [];

// This function is used to try to send with a provider and if it fails pop an element off the fallback list to try
// with that one. Once the fallback provider list is empty, the method throws. Because the fallback providers are
// removed, we ensure that no provider is used more than once because we care about quorum, making sure all
// considered responses come from unique providers.
const tryWithFallback = (
provider: ethers.providers.JsonRpcProvider
): Promise<[ethers.providers.JsonRpcProvider, any]> => {
provider: ethers.providers.StaticJsonRpcProvider
): Promise<[ethers.providers.StaticJsonRpcProvider, any]> => {
return this._trySend(provider, method, params)
.then((result): [ethers.providers.JsonRpcProvider, any] => [provider, result])
.then((result): [ethers.providers.StaticJsonRpcProvider, any] => [provider, result])
.catch((err) => {
// Append the provider and error to the error array.
errors.push([provider, err?.stack || err?.toString()]);
Expand Down Expand Up @@ -166,7 +170,7 @@ class RetryProvider extends ethers.providers.JsonRpcProvider {
const fallbackResults = await Promise.allSettled(
fallbackProviders.map((provider) =>
this._trySend(provider, method, params)
.then((result): [ethers.providers.JsonRpcProvider, any] => [provider, result])
.then((result): [ethers.providers.StaticJsonRpcProvider, any] => [provider, result])
.catch((err) => {
errors.push([provider, err?.stack || err?.toString()]);
throw new Error("No fallbacks during quorum search");
Expand Down Expand Up @@ -207,7 +211,7 @@ class RetryProvider extends ethers.providers.JsonRpcProvider {
return quorumResult;
}

_trySend(provider: ethers.providers.JsonRpcProvider, method: string, params: Array<any>): Promise<any> {
_trySend(provider: ethers.providers.StaticJsonRpcProvider, method: string, params: Array<any>): Promise<any> {
let promise = provider.send(method, params);
for (let i = 0; i < this.retries; i++) {
promise = promise.catch(() => delay(this.delay).then(() => provider.send(method, params)));
Expand Down