@lifi/rpc-wrapper
Version:
LI.FI rpc-wrapper
386 lines (385 loc) • 17.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { constants, Contract } from 'ethers';
import * as providers from '@ethersproject/providers';
import { ERC20Abi } from './abi';
import { parseError, RpcError, ServerError, StallTimeout } from './errors';
import { perfProfileHrtime } from './utils';
import { RPCProviderCache } from './RPCProviderCache';
export class RPCProvider extends providers.FallbackProvider {
/**
*
* @param rpcConfigs - prioritized provider configs
* @param logger - optional logger
* @param cacheTTL - provider cache time to live (ttl) in seconds
* @param pollingInterval - interval in seconds between each call to getBlockNumber
*/
constructor(rpcConfigs, logger, cacheTTL, pollingInterval, rpcTimeoutForExecute = 10000) {
// need to initialize a static json rpc provider to satisfy the fallbackprovider
// this ensures that methods implemented by Fallbackprovider but not RPCprovider
// are exposed to the user
const provider = new providers.StaticJsonRpcProvider(rpcConfigs[0].url);
super([{ provider }]);
this.rpcConfigs = rpcConfigs;
this.logger = logger;
this.retryNumber = 2;
this.ctx = { method: '', args: {} };
this.setMethodCtx = (method, args = {}) => {
this.ctx = { method, args };
};
this.rpcTimeoutForExecute = rpcTimeoutForExecute;
if (pollingInterval)
this._pollingInterval = pollingInterval;
this._highestBlockNumber = 0;
provider._pollingInterval = this.pollingInterval;
// initialize providerCache
this.providerCache = new RPCProviderCache(cacheTTL, pollingInterval);
this._nextId = 42;
}
get _cache() {
if (this._eventLoopCache == null) {
this._eventLoopCache = {};
}
return this._eventLoopCache;
}
/**
* Gets the current transaction count.
*
* @param addressOrName - The hexadecimal string address or name
* @param blockTag (default: "latest") - The block tag to get the transaction count for. Use "latest" mined-only transactions.
* Use "pending" for transactions that have not been mined yet, but will (supposedly) be mined in the pending
* block (essentially, transactions included in the mempool, but this behavior is not consistent).
*
* @returns Number of transactions sent AKA the current nonce.
*/
getTransactionCount(addressOrName, blockTag) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getTransactionCount', {
addressOrName: yield addressOrName,
blockTag: JSON.stringify(yield blockTag),
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
const transactionCount = yield provider.getTransactionCount(addressOrName, blockTag);
// TODO cache transaction count
return transactionCount;
})));
});
}
/**
* The RPC method execute wrapper is used for wrapping and parsing errors, as well as ensuring that
* providers are ready before any call is made. Also used for executing multiple retries for RPC
* requests to providers. This is to circumvent any issues related to unreliable internet/network
* issues, whether locally, or externally (for the provider's network).
*
* @param method - The method callback to execute and wrap in retries.
* @returns The object of the specified generic type.
* @throws CustomError if the method fails to execute.
*/
execute(method) {
var _a, _b, _c, _d, _e;
return __awaiter(this, void 0, void 0, function* () {
const errors = [];
for (let i = 0; i < this.retryNumber; i++) {
const rpcConfig = this.rpcConfigs[i % this.rpcConfigs.length];
const provider = this.providerCache.getProvider(rpcConfig.url);
try {
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug(`[RpcProvider] ${this.ctx.method}.debug. Trying provider: ${rpcConfig.url} on chain ${rpcConfig.chainId}...`);
const tick = process.hrtime();
const res = yield Promise.race([
method(provider),
new Promise((_, reject) => setTimeout(() => {
reject(new StallTimeout());
}, this.rpcTimeoutForExecute)),
]);
const tock = perfProfileHrtime(tick);
(_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`[RpcProvider] ${this.ctx.method}.debug. Successfully response from: ${rpcConfig.url} on chain ${rpcConfig.chainId}! Took ${tock}`);
return res;
}
catch (e) {
// It will return instantly if the error is already a CustomError.
const error = parseError(e, Object.assign({ RPCUrl: rpcConfig.url }, this.ctx));
(_c = this.logger) === null || _c === void 0 ? void 0 : _c.debug(`[RpcProvider] ${this.ctx.method}.debug. Failed to get data from ${rpcConfig.url} on chain ${rpcConfig.chainId}. Cause: ${error} `);
// Extra logging for timeouts
if (error.type === StallTimeout.type) {
(_d = this.logger) === null || _d === void 0 ? void 0 : _d.warn(`[RpcProvider] ${this.ctx.method}.warn. Timeout for ${rpcConfig.url} on chain ${rpcConfig.chainId}!`, this.ctx.args);
}
if (error.type === ServerError.type || error.type === RpcError.type) {
// If the method threw a RpcError, or ServerError, that indicates a problem with the provider and not
// the call - so we'll retry the call with a different provider (if available).
errors.push(error);
}
else {
// e.g. a TransactionReverted, TransactionReplaced, etc.
throw error;
}
}
}
(_e = this.logger) === null || _e === void 0 ? void 0 : _e.error(`[RpcProvider] ${this.ctx.method}.error. Cause: ${RpcError.reasons.FailedToSend}. Errors: ${errors}`);
throw new RpcError(RpcError.reasons.FailedToSend, { errors });
});
}
stopwatch(fn) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const tick = process.hrtime();
const res = yield fn();
const tock = perfProfileHrtime(tick);
const logType = `${this.ctx.method}.debug. Time: `;
(_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug(`$[RpcProvider] ${logType.padEnd(25)} ${tock}`);
return res;
});
}
/**
* Get the current balance for the specified address.
*
* @param addressOrName - The hexadecimal string address whose balance we are getting.
* @param blockTag - string | number of the block.
* @param assetId - The ID (address) of the asset whose balance we are getting.
* @param abi (default = ERC20) - The ABI of the token contract to use, if non-native token.
*
* @returns A BigNumber representing the current value held by the wallet at the
* specified address.
*/
getBalance(addressOrName, blockTag, assetId = constants.AddressZero, abi) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getBalance', {
addressOrName: yield addressOrName,
blockTag: JSON.stringify(yield blockTag),
assetId,
abi: (_a = abi === null || abi === void 0 ? void 0 : abi.join(',')) !== null && _a !== void 0 ? _a : '',
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
if (assetId === constants.AddressZero) {
return yield provider.getBalance(addressOrName, blockTag);
}
else {
const contract = new Contract(assetId, abi !== null && abi !== void 0 ? abi : ERC20Abi, provider);
return yield contract.balanceOf(addressOrName);
}
})));
});
}
/**
* Returns the Array of Log matching the filter.
*
* @param filter - Filter | FilterByBlockHash object
*
* @returns Array of logs
*/
getLogs(filter) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getLogs', {
filter: JSON.stringify(filter),
});
this.setMethodCtx('getLogs');
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getLogs(filter);
})));
});
}
/**
*
* Returns the block number (or height) of the most recently mined block.
*
* @returns number
*/
getBlockNumber() {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getBlockNumber');
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getBlockNumber();
})));
});
}
/**
*
* Get the block from the network, where the result.transactions is a list of transaction hashes.
*
* @param blockHashOrBlockTag - block hash or block tag
*
* @returns Block
*/
getBlock(blockHashOrBlockTag) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getBlock', {
blockHashOrBlockTag: JSON.stringify(yield blockHashOrBlockTag),
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getBlock(blockHashOrBlockTag);
})));
});
}
/**
*
* Get gas price.
*
* @returns BigNumber
*/
getGasPrice() {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getGasPrice');
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getGasPrice();
})));
});
}
/**
*
* Returns an estimate of the amount of gas that would be required
* to submit transaction to the network.
*
* @param transaction - ethers TransactionRequest type
*
* @returns BigNumber
*/
estimateGas(transaction) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('estimateGas', {
transaction: JSON.stringify(transaction),
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.estimateGas(transaction);
})));
});
}
/**
*
* Get the block from the network, where the result.transactions is an Array
* of TransactionResponse objects.
*
* @param blockHashOrBlockTag - block hash or block tag
*
* @returns BlockWithTransactions
*/
getBlockWithTransactions(blockHashOrBlockTag) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getBlockWithTransactions', {
blockHashOrBlockTag: JSON.stringify(yield blockHashOrBlockTag),
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getBlockWithTransactions(blockHashOrBlockTag);
})));
});
}
waitForTransaction(transactionHash, confirmations, timeout) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('waitForTransaction', {
transactionHash,
confirmations: (_a = confirmations === null || confirmations === void 0 ? void 0 : confirmations.toString()) !== null && _a !== void 0 ? _a : '1',
timeout: (_b = timeout === null || timeout === void 0 ? void 0 : timeout.toString()) !== null && _b !== void 0 ? _b : '0',
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.waitForTransaction(transactionHash, confirmations, timeout);
})));
});
}
/**
*
* Returns the transaction receipt for hash or null if the transaction has not been mined.
*
* @param transactionHash - transaction hash
*
* @returns TransactionReceipt
*/
getTransactionReceipt(transactionHash) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getTransactionReceipt', {
transactionHash: yield transactionHash,
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getTransactionReceipt(transactionHash);
})));
});
}
/**
*
* Returns the transaction response for hash or null if the transaction has not been mined.
*
* @param transactionHash - transaction hash
*
* @returns TransactionReceipt
*/
getTransaction(transactionHash) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getTransaction', {
transactionHash: yield transactionHash,
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getTransaction(transactionHash);
})));
});
}
detectNetwork() {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('detectNetwork');
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.detectNetwork();
})));
});
}
/**
*
* Returns the contract code of address as of the blockTag block height.
* If there is no contract currently deployed, the result is 0x.
*
* @param addressOrName - The hexadecimal string address who Code we are getting or name
* @param blockTag - string or number of the block
*
* @returns string
*/
getCode(addressOrName, blockTag) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('getCode', {
addressOrName: yield addressOrName,
blockTag: JSON.stringify(yield blockTag),
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.getCode(addressOrName, blockTag);
})));
});
}
/**
* The raw send method allows you to call any method on the RPC provider
*
* @param method - The name of the method
* @param params - The parameters that the method receives
* @returns
*/
send(method, params) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('send', {
params: JSON.stringify(params),
method,
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.send(method, params);
})));
});
}
/**
* sendTransaction allows you to send a signed transaction to the network
* defined in the constructor.
*
* @param signedTransaction - The signed transaction
* @returns
*/
sendTransaction(signedTransaction) {
return __awaiter(this, void 0, void 0, function* () {
this.setMethodCtx('sendTransaction', {
signedTransaction: yield signedTransaction,
});
return this.stopwatch(() => this.execute((provider) => __awaiter(this, void 0, void 0, function* () {
return yield provider.sendTransaction(signedTransaction);
})));
});
}
}