Skip to content

Commit

Permalink
Add timeout and request strategy (#9)
Browse files Browse the repository at this point in the history
* add timeout

* adds fastest strategy
  • Loading branch information
10xSebastian authored May 1, 2023
1 parent 85e4aed commit aa79923
Show file tree
Hide file tree
Showing 24 changed files with 1,198 additions and 419 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,41 @@ describe('resetCache', ()=>{
})
```

#### request strategy

Web3Client supports 2 different request strategies, `failover` (default) and `fastest`.

You can pick one or the other by providing the `stategy` option to `request`:

```javascript
import { request } from '@depay/web3-client'

let data = await request('ethereum://0x7a250d5630b4cf539739df2c5dacb4c659f2488d/getAmountsOut', { strategy: 'fastest' })
```

##### failover request strategy

(DEFAULT).

The `failover` request strategy picks the fastest RPC endpoint upon initailization (`setProviderEndpoints`) and sticks with the fastest RPC endpoint for the entire session (until window is reloaded).

If the chosen endpoint starts failing, it fails over to the next endpoint in the list and so on.

##### fastest request strategy

The `fastest` request startegy sends a request to all configured endpoints and returns a result as soon as one of the configured endpoints responds to the request.

#### request timeout

You can provide a request timeout in milli seconds to the request method:

```javascript
import { request } from '@depay/web3-client'

let data = await request('ethereum://0x7a250d5630b4cf539739df2c5dacb4c659f2488d/getAmountsOut', { timeout: 2000 })
// raises Web3ClientTimeout
```

### estimate

Estimates a transaction and returns gasLimit as BigNumber:
Expand Down Expand Up @@ -323,8 +358,6 @@ await setProviderEndpoints('ethereum', ['http://localhost:8545'])

Make sure you pass an array of endpoints to setProvider.

Currently we only connect to the first provider of that array, but for future reasons we might introduce RPC endpoint fallbacks eventually.

If you want to provide your own initalized providers, you can use `setProvider`:

```javascript
Expand Down
2 changes: 1 addition & 1 deletion dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="application/javascript"></script>
<script crossorigin src="https://unpkg.com/@depay/solana-web3.js@1"></script>
<script crossorigin src="https://unpkg.com/@depay/web3-blockchains@7"></script>
<script crossorigin src="https://unpkg.com/@depay/web3-blockchains@8"></script>
<script src="tmp/index.dev.js"></script>
</head>
<body>
Expand Down
140 changes: 94 additions & 46 deletions dist/esm/index.evm.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ const CHUNK_SIZE = 99;

class StaticJsonRpcBatchProvider extends ethers.providers.JsonRpcProvider {

constructor(url, network, endpoints) {
constructor(url, network, endpoints, failover) {
super(url);
this._network = network;
this._endpoint = url;
this._endpoints = endpoints;
this._failover = failover;
}

detectNetwork() {
Expand Down Expand Up @@ -69,6 +70,7 @@ class StaticJsonRpcBatchProvider extends ethers.providers.JsonRpcProvider {
}).catch((error) => {
if(error && error.code == 'SERVER_ERROR') {
const index = this._endpoints.indexOf(this._endpoint)+1;
this._failover();
this._endpoint = index >= this._endpoints.length ? this._endpoints[0] : this._endpoints[index];
this.requestChunk(chunk, this._endpoint);
} else {
Expand Down Expand Up @@ -139,37 +141,39 @@ let getWindow = () => {
return _window
};

// MAKE SURE PROVIDER SUPPORT BATCH SIZE OF 99 BATCH REQUESTS!
const ENDPOINTS = {
ethereum: ['https://rpc.ankr.com/eth', 'https://eth.llamarpc.com', 'https://ethereum.publicnode.com'],
bsc: ['https://bsc-dataseed.binance.org', 'https://bsc-dataseed1.ninicoin.io', 'https://bsc-dataseed3.defibit.io'],
polygon: ['https://polygon-rpc.com', 'https://poly-rpc.gateway.pokt.network', 'https://matic-mainnet.chainstacklabs.com'],
fantom: ['https://fantom.blockpi.network/v1/rpc/public', 'https://rpcapi.fantom.network', 'https://rpc.ftm.tools'],
velas: ['https://velas-mainnet.rpcfast.com/?api_key=xbhWBI1Wkguk8SNMu1bvvLurPGLXmgwYeC4S6g2H7WdwFigZSmPWVZRxrskEQwIf', 'https://evmexplorer.velas.com/rpc', 'https://explorer.velas.com/rpc'],
};

const getProviders = ()=> {
if(getWindow()._clientProviders == undefined) {
getWindow()._clientProviders = {};
const getAllProviders = ()=> {
if(getWindow()._Web3ClientProviders == undefined) {
getWindow()._Web3ClientProviders = {};
}
return getWindow()._clientProviders
return getWindow()._Web3ClientProviders
};

const setProvider$1 = (blockchain, provider)=> {
getProviders()[blockchain] = provider;
if(getAllProviders()[blockchain] === undefined) { getAllProviders()[blockchain] = []; }
const index = getAllProviders()[blockchain].indexOf(provider);
if(index > -1) {
getAllProviders()[blockchain].splice(index, 1);
}
getAllProviders()[blockchain].unshift(provider);
};

const setProviderEndpoints$1 = async (blockchain, endpoints)=> {

let endpoint;
getAllProviders()[blockchain] = endpoints.map((endpoint, index)=>
new StaticJsonRpcBatchProvider(endpoint, blockchain, endpoints, ()=>{
getAllProviders()[blockchain].splice(index, 1);
})
);

let provider;
let window = getWindow();

if(
window.fetch == undefined ||
(typeof process != 'undefined' && process['env'] && process['env']['NODE_ENV'] == 'test') ||
(typeof window.cy != 'undefined')
) {
endpoint = endpoints[0];
provider = getAllProviders()[blockchain][0];
} else {

let responseTimes = await Promise.all(endpoints.map((endpoint)=>{
Expand All @@ -193,34 +197,49 @@ const setProviderEndpoints$1 = async (blockchain, endpoints)=> {

const fastestResponse = Math.min(...responseTimes);
const fastestIndex = responseTimes.indexOf(fastestResponse);
endpoint = endpoints[fastestIndex];
provider = getAllProviders()[blockchain][fastestIndex];
}

setProvider$1(
blockchain,
new StaticJsonRpcBatchProvider(endpoint, blockchain, endpoints)
);
setProvider$1(blockchain, provider);
};

const getProvider$1 = async (blockchain)=> {

let providers = getProviders();
let providers = getAllProviders();
if(providers && providers[blockchain]){ return providers[blockchain][0] }

let window = getWindow();
if(window._Web3ClientGetProviderPromise && window._Web3ClientGetProviderPromise[blockchain]) { return await window._Web3ClientGetProviderPromise[blockchain] }

if(!window._Web3ClientGetProviderPromise){ window._Web3ClientGetProviderPromise = {}; }
window._Web3ClientGetProviderPromise[blockchain] = new Promise(async(resolve)=> {
await setProviderEndpoints$1(blockchain, Blockchains[blockchain].endpoints);
resolve(getWindow()._Web3ClientProviders[blockchain][0]);
});

return await window._Web3ClientGetProviderPromise[blockchain]
};

const getProviders = async(blockchain)=>{

let providers = getAllProviders();
if(providers && providers[blockchain]){ return providers[blockchain] }

let window = getWindow();
if(window._getProviderPromise && window._getProviderPromise[blockchain]) { return await window._getProviderPromise[blockchain] }
if(window._Web3ClientGetProvidersPromise && window._Web3ClientGetProvidersPromise[blockchain]) { return await window._Web3ClientGetProvidersPromise[blockchain] }

if(!window._getProviderPromise){ window._getProviderPromise = {}; }
window._getProviderPromise[blockchain] = new Promise(async(resolve)=> {
await setProviderEndpoints$1(blockchain, ENDPOINTS[blockchain]);
resolve(getWindow()._clientProviders[blockchain]);
if(!window._Web3ClientGetProvidersPromise){ window._Web3ClientGetProvidersPromise = {}; }
window._Web3ClientGetProvidersPromise[blockchain] = new Promise(async(resolve)=> {
await setProviderEndpoints$1(blockchain, Blockchains[blockchain].endpoints);
resolve(getWindow()._Web3ClientProviders[blockchain]);
});

return await window._getProviderPromise[blockchain]
return await window._Web3ClientGetProvidersPromise[blockchain]
};

var EVM = {
getProvider: getProvider$1,
getProviders,
setProviderEndpoints: setProviderEndpoints$1,
setProvider: setProvider$1,
};
Expand All @@ -231,23 +250,23 @@ supported.solana = [];

function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
let getCacheStore = () => {
if (getWindow()._cacheStore == undefined) {
if (getWindow()._Web3ClientCacheStore == undefined) {
resetCache();
}
return getWindow()._cacheStore
return getWindow()._Web3ClientCacheStore
};

let getPromiseStore = () => {
if (getWindow()._promiseStore == undefined) {
if (getWindow()._Web3ClientPromiseStore == undefined) {
resetCache();
}
return getWindow()._promiseStore
return getWindow()._Web3ClientPromiseStore
};

let resetCache = () => {
getWindow()._cacheStore = {};
getWindow()._promiseStore = {};
getWindow()._clientProviders = {};
getWindow()._Web3ClientCacheStore = {};
getWindow()._Web3ClientPromiseStore = {};
getWindow()._Web3ClientProviders = {};
};

let set = function ({ key, value, expires }) {
Expand Down Expand Up @@ -403,23 +422,21 @@ let paramsToContractArgs = ({ contract, method, params }) => {
})
};

let contractCall = ({ address, api, method, params, provider, block }) => {
const contractCall = ({ address, api, method, params, provider, block }) => {
let contract = new ethers.Contract(address, api, provider);
let args = paramsToContractArgs({ contract, method, params });
return contract[method](...args, { blockTag: block })
};

let balance = ({ address, provider }) => {
const balance = ({ address, provider }) => {
return provider.getBalance(address)
};

let transactionCount = ({ address, provider }) => {
const transactionCount = ({ address, provider }) => {
return provider.getTransactionCount(address)
};

var requestEVM = async ({ blockchain, address, api, method, params, block }) => {
const provider = await EVM.getProvider(blockchain);

const singleRequest = ({ blockchain, address, api, method, params, block, provider }) =>{
if (api) {
return contractCall({ address, api, method, params, provider, block })
} else if (method === 'latestBlockNumber') {
Expand All @@ -431,6 +448,36 @@ var requestEVM = async ({ blockchain, address, api, method, params, block }) =>
}
};

var requestEVM = async ({ blockchain, address, api, method, params, block, timeout, strategy = 'fallback' }) => {

if(strategy === 'fastest') {

return Promise.race((await EVM.getProviders(blockchain)).map((provider)=>{

const request = singleRequest({ blockchain, address, api, method, params, block, provider });

if(timeout) {
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>{ reject(new Error("Web3ClientTimeout")); }, timeout));
return Promise.race([request, timeoutPromise])
} else {
return request
}
}))

} else { // failover

const provider = await EVM.getProvider(blockchain);
const request = singleRequest({ blockchain, address, api, method, params, block, provider });

if(timeout) {
timeout = new Promise((_, reject)=>setTimeout(()=>{ reject(new Error("Web3ClientTimeout")); }, timeout));
return Promise.race([request, timeout])
} else {
return request
}
}
};

var parseUrl = (url) => {
if (typeof url == 'object') {
return url
Expand Down Expand Up @@ -458,9 +505,10 @@ var parseUrl = (url) => {
}
};

let request = async function (url, options) {
let { blockchain, address, method } = parseUrl(url);
let { api, params, cache: cache$1, block } = (typeof(url) == 'object' ? url : options) || {};
const request = async function (url, options) {

const { blockchain, address, method } = parseUrl(url);
const { api, params, cache: cache$1, block, timeout, strategy } = (typeof(url) == 'object' ? url : options) || {};

return await cache({
expires: cache$1 || 0,
Expand All @@ -469,7 +517,7 @@ let request = async function (url, options) {
if(supported.evm.includes(blockchain)) {


return await requestEVM({ blockchain, address, api, method, params, block })
return await requestEVM({ blockchain, address, api, method, params, block, strategy, timeout })


} else if(supported.solana.includes(blockchain)) ; else {
Expand Down
Loading

0 comments on commit aa79923

Please sign in to comment.