feat(sdk): Add unified error parsing to handle server and timeout errors (#4201)

### Description
Adds logic to parse the Ethers and SmartProvider error code to direct
users to change their RPC.
`RPC request failed with ${errorMsg}. Check RPC validity. To override
RPC URLs, see:
https://docs.hyperlane.xyz/docs/deploy-hyperlane-troubleshooting#override-rpc-urls`

### Related issues
https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4200

### Backward compatibility
Yes

### Testing
Manual/Unit Tests

---------

Co-authored-by: Noah Bayindirli 🥂 <15343884+nbayindirli@users.noreply.github.com>
pull/4208/head
Lee 4 months ago committed by GitHub
parent 6140e6f397
commit 4907b510cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/clever-scissors-itch.md
  2. 65
      typescript/sdk/src/providers/SmartProvider/SmartProvider.foundry-test.ts
  3. 34
      typescript/sdk/src/providers/SmartProvider/SmartProvider.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Add logic to parse SmartProvider errors to handle ethers and smart provider errors

@ -81,4 +81,69 @@ describe('SmartProvider', async () => {
expect(network.chainId).to.equal(NETWORK);
expect(Array.isArray(logs)).to.be.true;
});
// it('throws with invalid RPC', async () => {
// const INVALID_URL = 'http://1337.1337.1337.1:8545';
// const NETWORK = 11337;
// const smartProvider = HyperlaneSmartProvider.fromRpcUrl(
// NETWORK,
// INVALID_URL,
// {
// maxRetries: 3,
// },
// );
// const signer = new Wallet(PK, smartProvider);
// try {
// const factory = new ERC20__factory(signer);
// await factory.deploy('fake', 'FAKE');
// } catch (e: any) {
// expect(e.message).to.equal(
// getSmartProviderErrorMessage(EthersError.SERVER_ERROR),
// );
// }
// });
// it('throws with multiple invalid RPCs', async () => {
// const INVALID_URL_1 = 'http://1337.1337.1337.1:8545';
// const INVALID_URL_2 = 'http://1338.1338.1338.1:8545';
// const NETWORK = 11337;
// const smartProvider = new HyperlaneSmartProvider(
// NETWORK,
// [{ http: INVALID_URL_1 }, { http: INVALID_URL_2 }],
// [],
// {
// maxRetries: 3,
// },
// );
// const signer = new Wallet(PK, smartProvider);
// try {
// const factory = new ERC20__factory(signer);
// await factory.deploy('fake', 'FAKE');
// } catch (e: any) {
// expect(e.message).to.equal(
// getSmartProviderErrorMessage(EthersError.SERVER_ERROR),
// );
// }
// });
// it('handles invalid and valid RPCs', async () => {
// const INVALID_URL = 'http://1337.1337.1337.1:8545';
// const NETWORK = 11337;
// const smartProvider = new HyperlaneSmartProvider(
// NETWORK,
// [{ http: INVALID_URL }, { http: URL }],
// [],
// {
// maxRetries: 3,
// },
// );
// const signer = new Wallet(PK, smartProvider);
// const factory = new ERC20__factory(signer);
// const erc20 = await factory.deploy('fake', 'FAKE');
// expect(erc20.address).to.not.be.empty;
// });
});

@ -1,4 +1,4 @@
import { BigNumber, providers, utils } from 'ethers';
import { BigNumber, errors as EthersError, providers, utils } from 'ethers';
import pino, { Logger } from 'pino';
import {
@ -27,6 +27,16 @@ import {
SmartProviderOptions,
} from './types.js';
export const getSmartProviderErrorMessage = (errorMsg: string) =>
`${errorMsg}: RPC request failed. Check RPC validity. To override RPC URLs, see: https://docs.hyperlane.xyz/docs/deploy-hyperlane-troubleshooting#override-rpc-urls`;
// This is a partial list. If needed, check the full list for more: https://github.com/ethers-io/ethers.js/blob/fc66b8ad405df9e703d42a4b23bc452ec3be118f/src.ts/utils/errors.ts#L77-L85
const RPC_SERVER_ERRORS = [
EthersError.NOT_IMPLEMENTED,
EthersError.SERVER_ERROR,
EthersError.UNKNOWN_ERROR,
EthersError.UNSUPPORTED_OPERATION,
];
const DEFAULT_MAX_RETRIES = 1;
const DEFAULT_BASE_RETRY_DELAY_MS = 250; // 0.25 seconds
const DEFAULT_STAGGER_DELAY_MS = 1000; // 1 seconds
@ -315,7 +325,7 @@ export class HyperlaneSmartProvider
return result.value;
} else if (result.status === ProviderStatus.Timeout) {
this.throwCombinedProviderErrors(
providerResultErrors,
[result, ...providerResultErrors],
`All providers timed out for method ${method}`,
);
} else if (result.status === ProviderStatus.Error) {
@ -405,13 +415,25 @@ export class HyperlaneSmartProvider
}
protected throwCombinedProviderErrors(
errors: unknown[],
errors: any[],
fallbackMsg: string,
): void {
this.logger.error(fallbackMsg);
// TODO inspect the errors in some clever way to choose which to throw
if (errors.length > 0) throw errors[0];
else throw new Error(fallbackMsg);
if (errors.length === 0) throw new Error(fallbackMsg);
const rpcServerError = errors.find((e) =>
RPC_SERVER_ERRORS.includes(e.code),
);
const timedOutError = errors.find(
(e) => e.status === ProviderStatus.Timeout,
);
if (rpcServerError) {
throw Error(getSmartProviderErrorMessage(rpcServerError.code));
} else if (timedOutError) {
throw Error(getSmartProviderErrorMessage(ProviderStatus.Timeout));
} else {
throw Error(getSmartProviderErrorMessage(ProviderStatus.Error)); // Assumes that all errors are of ProviderStatus.Error
}
}
}

Loading…
Cancel
Save