@across-protocol/sdk 3.1.18 → 3.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/cachedProvider.d.ts +17 -0
- package/dist/cjs/providers/cachedProvider.js +149 -0
- package/dist/cjs/providers/cachedProvider.js.map +1 -0
- package/dist/cjs/providers/constants.d.ts +4 -0
- package/dist/cjs/providers/constants.js +8 -0
- package/dist/cjs/providers/constants.js.map +1 -0
- package/dist/cjs/providers/index.d.ts +5 -0
- package/dist/cjs/providers/index.js +9 -0
- package/dist/cjs/providers/index.js.map +1 -0
- package/dist/cjs/providers/rateLimitedProvider.d.ts +10 -0
- package/dist/cjs/providers/rateLimitedProvider.js +88 -0
- package/dist/cjs/providers/rateLimitedProvider.js.map +1 -0
- package/dist/cjs/providers/retryProvider.d.ts +16 -0
- package/dist/cjs/providers/retryProvider.js +224 -0
- package/dist/cjs/providers/retryProvider.js.map +1 -0
- package/dist/cjs/providers/utils.d.ts +21 -0
- package/dist/cjs/providers/utils.js +63 -0
- package/dist/cjs/providers/utils.js.map +1 -0
- package/dist/cjs/utils/NetworkUtils.d.ts +1 -0
- package/dist/cjs/utils/NetworkUtils.js +10 -1
- package/dist/cjs/utils/NetworkUtils.js.map +1 -1
- package/dist/cjs/utils/ObjectUtils.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/providers/cachedProvider.d.ts +17 -0
- package/dist/esm/providers/cachedProvider.js +167 -0
- package/dist/esm/providers/cachedProvider.js.map +1 -0
- package/dist/esm/providers/constants.d.ts +7 -0
- package/dist/esm/providers/constants.js +10 -0
- package/dist/esm/providers/constants.js.map +1 -0
- package/dist/esm/providers/index.d.ts +5 -0
- package/dist/esm/providers/index.js +6 -0
- package/dist/esm/providers/index.js.map +1 -0
- package/dist/esm/providers/rateLimitedProvider.d.ts +10 -0
- package/dist/esm/providers/rateLimitedProvider.js +101 -0
- package/dist/esm/providers/rateLimitedProvider.js.map +1 -0
- package/dist/esm/providers/retryProvider.d.ts +16 -0
- package/dist/esm/providers/retryProvider.js +249 -0
- package/dist/esm/providers/retryProvider.js.map +1 -0
- package/dist/esm/providers/utils.d.ts +39 -0
- package/dist/esm/providers/utils.js +89 -0
- package/dist/esm/providers/utils.js.map +1 -0
- package/dist/esm/utils/NetworkUtils.d.ts +6 -0
- package/dist/esm/utils/NetworkUtils.js +13 -0
- package/dist/esm/utils/NetworkUtils.js.map +1 -1
- package/dist/esm/utils/ObjectUtils.js +1 -1
- package/dist/esm/utils/ObjectUtils.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/providers/cachedProvider.d.ts +18 -0
- package/dist/types/providers/cachedProvider.d.ts.map +1 -0
- package/dist/types/providers/constants.d.ts +8 -0
- package/dist/types/providers/constants.d.ts.map +1 -0
- package/dist/types/providers/index.d.ts +6 -0
- package/dist/types/providers/index.d.ts.map +1 -0
- package/dist/types/providers/rateLimitedProvider.d.ts +11 -0
- package/dist/types/providers/rateLimitedProvider.d.ts.map +1 -0
- package/dist/types/providers/retryProvider.d.ts +17 -0
- package/dist/types/providers/retryProvider.d.ts.map +1 -0
- package/dist/types/providers/utils.d.ts +40 -0
- package/dist/types/providers/utils.d.ts.map +1 -0
- package/dist/types/utils/NetworkUtils.d.ts +6 -0
- package/dist/types/utils/NetworkUtils.d.ts.map +1 -1
- package/dist/types/utils/ObjectUtils.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/index.ts +1 -0
- package/src/providers/cachedProvider.ts +155 -0
- package/src/providers/constants.ts +11 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/rateLimitedProvider.ts +94 -0
- package/src/providers/retryProvider.ts +262 -0
- package/src/providers/utils.ts +119 -0
- package/src/utils/NetworkUtils.ts +13 -0
- package/src/utils/ObjectUtils.ts +1 -0
|
@@ -64,4 +64,10 @@ export declare function chainIsCCTPEnabled(chainId: number): boolean;
|
|
|
64
64
|
* @returns True if chainId requires manual L1 -> L2 finalization, otherwise false.
|
|
65
65
|
*/
|
|
66
66
|
export declare function chainRequiresL1ToL2Finalization(chainId: number): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Returns the origin of a URL.
|
|
69
|
+
* @param url A URL.
|
|
70
|
+
* @returns The origin of the URL, or "UNKNOWN" if the URL is invalid.
|
|
71
|
+
*/
|
|
72
|
+
export declare function getOriginFromURL(url: string): string;
|
|
67
73
|
//# sourceMappingURL=NetworkUtils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/NetworkUtils.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAGjE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAErE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAa3D;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExE"}
|
|
1
|
+
{"version":3,"file":"NetworkUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/NetworkUtils.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAGjE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAErE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAa3D;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAExE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMpD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/ObjectUtils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ObjectUtils.d.ts","sourceRoot":"","sources":["../../../src/utils/ObjectUtils.ts"],"names":[],"mappings":"AAMA,wBAAgB,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI,CAyBjE;AAGD,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,GAAG,EAAE,EACd,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,GAAG,CASL;AACD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,GAAG,EAAE,EACd,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GACrC,GAAG,CAOL;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,GAAG,GAAG,CAM1F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,CAEvG"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@across-protocol/sdk",
|
|
3
3
|
"author": "UMA Team",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.19",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"homepage": "https://docs.across.to/reference/sdk",
|
|
7
7
|
"files": [
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"@size-limit/preset-small-lib": "^7.0.8",
|
|
59
59
|
"@typechain/ethers-v5": "^11.1.1",
|
|
60
60
|
"@typechain/hardhat": "^6.1.6",
|
|
61
|
+
"@types/async": "^3.2.24",
|
|
61
62
|
"@types/chai": "^4.3.6",
|
|
62
63
|
"@types/dotenv": "^8.2.0",
|
|
63
64
|
"@types/lodash": "^4.14.199",
|
|
@@ -104,12 +105,15 @@
|
|
|
104
105
|
"@eth-optimism/sdk": "^3.3.1",
|
|
105
106
|
"@pinata/sdk": "^2.1.0",
|
|
106
107
|
"@types/mocha": "^10.0.1",
|
|
108
|
+
"@uma/logger": "^1.3.0",
|
|
107
109
|
"@uma/sdk": "^0.34.1",
|
|
108
110
|
"arweave": "^1.14.4",
|
|
111
|
+
"async": "^3.2.5",
|
|
109
112
|
"axios": "^0.27.2",
|
|
110
113
|
"big-number": "^2.0.0",
|
|
111
114
|
"decimal.js": "^10.3.1",
|
|
112
115
|
"ethers": "^5.7.2",
|
|
116
|
+
"lodash": "^4.17.21",
|
|
113
117
|
"lodash.get": "^4.4.2",
|
|
114
118
|
"superstruct": "^0.15.4",
|
|
115
119
|
"tslib": "^2.6.2"
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { random } from "lodash";
|
|
2
|
+
import { CachingMechanismInterface } from "../interfaces";
|
|
3
|
+
import { BLOCK_NUMBER_TTL, PROVIDER_CACHE_TTL, PROVIDER_CACHE_TTL_MODIFIER as ttl_modifier } from "./constants";
|
|
4
|
+
import { RateLimitedProvider } from "./rateLimitedProvider";
|
|
5
|
+
import { CacheType } from "./utils";
|
|
6
|
+
|
|
7
|
+
export class CacheProvider extends RateLimitedProvider {
|
|
8
|
+
public readonly getBlockByNumberPrefix: string;
|
|
9
|
+
public readonly getLogsCachePrefix: string;
|
|
10
|
+
public readonly callCachePrefix: string;
|
|
11
|
+
public readonly baseTTL: number;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
providerCacheNamespace: string,
|
|
15
|
+
readonly redisClient?: CachingMechanismInterface,
|
|
16
|
+
// Note: if not provided, this is set to POSITIVE_INFINITY, meaning no cache entries are set with the standard TTL.
|
|
17
|
+
readonly standardTtlBlockDistance = Number.POSITIVE_INFINITY,
|
|
18
|
+
// Note: if not provided, this is set to POSITIVE_INFINITY, meaning no cache entries are set with no TTL.
|
|
19
|
+
readonly noTtlBlockDistance = Number.POSITIVE_INFINITY,
|
|
20
|
+
readonly providerCacheTtl = PROVIDER_CACHE_TTL,
|
|
21
|
+
...jsonRpcConstructorParams: ConstructorParameters<typeof RateLimitedProvider>
|
|
22
|
+
) {
|
|
23
|
+
super(...jsonRpcConstructorParams);
|
|
24
|
+
|
|
25
|
+
const { chainId } = this.network;
|
|
26
|
+
|
|
27
|
+
// Pre-compute as much of the redis key as possible.
|
|
28
|
+
const cachePrefix = `${providerCacheNamespace},${new URL(this.connection.url).hostname},${chainId}`;
|
|
29
|
+
this.getBlockByNumberPrefix = `${cachePrefix}:getBlockByNumber,`;
|
|
30
|
+
this.getLogsCachePrefix = `${cachePrefix}:eth_getLogs,`;
|
|
31
|
+
this.callCachePrefix = `${cachePrefix}:eth_call,`;
|
|
32
|
+
|
|
33
|
+
const _ttlVar = providerCacheTtl;
|
|
34
|
+
const _ttl = Number(_ttlVar);
|
|
35
|
+
if (isNaN(_ttl) || _ttl <= 0) {
|
|
36
|
+
throw new Error(`PROVIDER_CACHE_TTL (${_ttlVar}) must be numeric and > 0`);
|
|
37
|
+
}
|
|
38
|
+
this.baseTTL = _ttl;
|
|
39
|
+
}
|
|
40
|
+
override async send(method: string, params: Array<unknown>): Promise<unknown> {
|
|
41
|
+
const cacheType = this.redisClient ? await this.cacheType(method, params) : CacheType.NONE;
|
|
42
|
+
|
|
43
|
+
if (cacheType !== CacheType.NONE) {
|
|
44
|
+
const redisKey = this.buildRedisKey(method, params);
|
|
45
|
+
|
|
46
|
+
// Attempt to pull the result from the cache.
|
|
47
|
+
const redisResult = await this.redisClient?.get<string>(redisKey);
|
|
48
|
+
|
|
49
|
+
// If cache has the result, parse the json and return it.
|
|
50
|
+
if (redisResult) {
|
|
51
|
+
return JSON.parse(redisResult);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cache does not have the result. Query it directly and cache.
|
|
55
|
+
const result = await super.send(method, params);
|
|
56
|
+
|
|
57
|
+
// Note: use swtich to ensure all enum cases are handled.
|
|
58
|
+
switch (cacheType) {
|
|
59
|
+
case CacheType.WITH_TTL:
|
|
60
|
+
{
|
|
61
|
+
// Apply a random margin to spread expiry over a larger time window.
|
|
62
|
+
const ttl = this.baseTTL + Math.ceil(random(-ttl_modifier, ttl_modifier, true) * this.baseTTL);
|
|
63
|
+
await this.redisClient?.set(redisKey, JSON.stringify(result), ttl);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case CacheType.NO_TTL:
|
|
67
|
+
await this.redisClient?.set(redisKey, JSON.stringify(result), Number.POSITIVE_INFINITY);
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unexpected Cache type: ${cacheType}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Return the cached result.
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return await super.send(method, params);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private buildRedisKey(method: string, params: Array<unknown>) {
|
|
81
|
+
// Only handles eth_getLogs and eth_call right now.
|
|
82
|
+
switch (method) {
|
|
83
|
+
case "eth_getBlockByNumber":
|
|
84
|
+
return this.getBlockByNumberPrefix + JSON.stringify(params);
|
|
85
|
+
case "eth_getLogs":
|
|
86
|
+
return this.getLogsCachePrefix + JSON.stringify(params);
|
|
87
|
+
case "eth_call":
|
|
88
|
+
return this.callCachePrefix + JSON.stringify(params);
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`CacheProvider::buildRedisKey: invalid JSON-RPC method ${method}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private cacheType(method: string, params: Array<unknown>): Promise<CacheType> {
|
|
95
|
+
// Today, we only cache eth_getLogs and eth_call.
|
|
96
|
+
if (method === "eth_getLogs") {
|
|
97
|
+
const [{ fromBlock, toBlock }] = params as { toBlock: number; fromBlock: number }[];
|
|
98
|
+
|
|
99
|
+
// Handle odd cases where the ordering is flipped, etc.
|
|
100
|
+
// toBlock/fromBlock is in hex, so it must be parsed before being compared to the first unsafe block.
|
|
101
|
+
const fromBlockNumber = parseInt(String(fromBlock), 16);
|
|
102
|
+
const toBlockNumber = parseInt(String(toBlock), 16);
|
|
103
|
+
|
|
104
|
+
// Handle cases where the input block numbers are not hex values ("latest", "pending", etc).
|
|
105
|
+
// This would result in the result of the above being NaN.
|
|
106
|
+
if (Number.isNaN(fromBlockNumber) || Number.isNaN(toBlockNumber)) {
|
|
107
|
+
return Promise.resolve(CacheType.NONE);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (toBlockNumber < fromBlockNumber) {
|
|
111
|
+
throw new Error("CacheProvider::shouldCache toBlock cannot be smaller than fromBlock.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this.cacheTypeForBlock(toBlock);
|
|
115
|
+
} else if ("eth_call" === method || "eth_getBlockByNumber" === method) {
|
|
116
|
+
// Pull out the block tag from params. Its position in params is dependent on the method.
|
|
117
|
+
// We are only interested in numeric block tags, which would be hex-encoded strings.
|
|
118
|
+
const idx = method === "eth_getBlockByNumber" ? 0 : 1;
|
|
119
|
+
const blockNumber = parseInt(String(params[idx]), 16);
|
|
120
|
+
|
|
121
|
+
// If the block number isn't present or is a text string, this will be NaN and we return false.
|
|
122
|
+
if (Number.isNaN(blockNumber)) {
|
|
123
|
+
return Promise.resolve(CacheType.NONE);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If the block is old enough to cache, cache the call.
|
|
127
|
+
return this.cacheTypeForBlock(blockNumber);
|
|
128
|
+
} else {
|
|
129
|
+
return Promise.resolve(CacheType.NONE);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async cacheTypeForBlock(blockNumber: number): Promise<CacheType> {
|
|
134
|
+
// Note: this method is an internal method provided by the BaseProvider. It allows the caller to specify a maxAge of
|
|
135
|
+
// the block that is allowed. This means if a block has been retrieved within the last n seconds, no provider
|
|
136
|
+
// query will be made.
|
|
137
|
+
const currentBlockNumber = await super._getInternalBlockNumber(BLOCK_NUMBER_TTL * 1000);
|
|
138
|
+
|
|
139
|
+
// Determine the distance that the block is from HEAD.
|
|
140
|
+
const headDistance = currentBlockNumber - blockNumber;
|
|
141
|
+
|
|
142
|
+
// If the distance from head is large enough, set with no TTL.
|
|
143
|
+
if (headDistance > this.noTtlBlockDistance) {
|
|
144
|
+
return CacheType.NO_TTL;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If the distance is <= noTtlBlockDistance, but > standardTtlBlockDistance, use standard TTL.
|
|
148
|
+
if (headDistance > this.standardTtlBlockDistance) {
|
|
149
|
+
return CacheType.WITH_TTL;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Too close to HEAD, no cache.
|
|
153
|
+
return CacheType.NONE;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This is how many seconds stale the block number can be for us to use it for evaluating the reorg distance in the cache provider.
|
|
2
|
+
export const BLOCK_NUMBER_TTL = 60;
|
|
3
|
+
|
|
4
|
+
// This is the TTL for the provider cache.
|
|
5
|
+
export const PROVIDER_CACHE_TTL = 3600;
|
|
6
|
+
export const PROVIDER_CACHE_TTL_MODIFIER = 0.15;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A default timeout for requests of 60 seconds.
|
|
10
|
+
*/
|
|
11
|
+
export const defaultTimeout = 60 * 1000;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// StaticJsonRpcProvider is used in place of JsonRpcProvider to avoid redundant eth_chainId queries prior to each
|
|
2
|
+
// request. This is safe to use when the back-end provider is guaranteed not to change.
|
|
3
|
+
// See https://docs.ethers.io/v5/api/providers/jsonrpc-provider/#StaticJsonRpcProvider
|
|
4
|
+
|
|
5
|
+
import { QueueObject, queue } from "async";
|
|
6
|
+
import { ethers } from "ethers";
|
|
7
|
+
import { RateLimitTask } from "./utils";
|
|
8
|
+
import { getOriginFromURL } from "../utils/NetworkUtils";
|
|
9
|
+
import { Logger } from "winston";
|
|
10
|
+
import { Logger as umaLogger } from "@uma/logger";
|
|
11
|
+
|
|
12
|
+
// This provider is a very small addition to the StaticJsonRpcProvider that ensures that no more than `maxConcurrency`
|
|
13
|
+
// requests are ever in flight. It uses the async/queue library to manage this.
|
|
14
|
+
export class RateLimitedProvider extends ethers.providers.StaticJsonRpcProvider {
|
|
15
|
+
// The queue object that manages the tasks.
|
|
16
|
+
private queue: QueueObject<RateLimitTask>;
|
|
17
|
+
|
|
18
|
+
// Takes the same arguments as the JsonRpcProvider, but it has an additional maxConcurrency value at the beginning
|
|
19
|
+
// of the list.
|
|
20
|
+
constructor(
|
|
21
|
+
maxConcurrency: number,
|
|
22
|
+
readonly pctRpcCallsLogged: number,
|
|
23
|
+
readonly logger: Logger = umaLogger,
|
|
24
|
+
...cacheConstructorParams: ConstructorParameters<typeof ethers.providers.StaticJsonRpcProvider>
|
|
25
|
+
) {
|
|
26
|
+
super(...cacheConstructorParams);
|
|
27
|
+
|
|
28
|
+
// This sets up the queue. Each task is executed by calling the superclass's send method, which fires off the
|
|
29
|
+
// request. This queue sends out requests concurrently, but stops once the concurrency limit is reached. The
|
|
30
|
+
// maxConcurrency is configured here.
|
|
31
|
+
this.queue = queue(async ({ sendArgs, resolve, reject }: RateLimitTask, callback: () => void) => {
|
|
32
|
+
await this.wrapSendWithLog(...sendArgs)
|
|
33
|
+
.then(resolve)
|
|
34
|
+
.catch(reject);
|
|
35
|
+
// we need this for the queue to know that the task is done
|
|
36
|
+
// @see: https://caolan.github.io/async/v3/global.html
|
|
37
|
+
callback();
|
|
38
|
+
}, maxConcurrency);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async wrapSendWithLog(method: string, params: Array<unknown>) {
|
|
42
|
+
if (this.pctRpcCallsLogged <= 0 || Math.random() > this.pctRpcCallsLogged / 100) {
|
|
43
|
+
// Non sample path: no logging or timing, just issue the request.
|
|
44
|
+
return super.send(method, params);
|
|
45
|
+
} else {
|
|
46
|
+
const loggerArgs = {
|
|
47
|
+
at: "ProviderUtils",
|
|
48
|
+
message: "Provider response sample",
|
|
49
|
+
provider: getOriginFromURL(this.connection.url),
|
|
50
|
+
method,
|
|
51
|
+
params,
|
|
52
|
+
chainId: this.network.chainId,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// In this path we log an rpc response sample.
|
|
56
|
+
// Note: use performance.now() to ensure a purely monotonic clock.
|
|
57
|
+
const startTime = performance.now();
|
|
58
|
+
try {
|
|
59
|
+
const result = await super.send(method, params);
|
|
60
|
+
const elapsedTimeS = (performance.now() - startTime) / 1000;
|
|
61
|
+
this.logger.debug({
|
|
62
|
+
...loggerArgs,
|
|
63
|
+
success: true,
|
|
64
|
+
timeElapsed: elapsedTimeS,
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// Log errors as well.
|
|
69
|
+
// For now, to keep logs light, don't log the error itself, just propogate and let it be handled higher up.
|
|
70
|
+
const elapsedTimeS = (performance.now() - startTime) / 1000;
|
|
71
|
+
this.logger.debug({
|
|
72
|
+
...loggerArgs,
|
|
73
|
+
success: false,
|
|
74
|
+
timeElapsed: elapsedTimeS,
|
|
75
|
+
});
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override send(method: string, params: Array<unknown>): Promise<unknown> {
|
|
82
|
+
// This simply creates a promise and adds the arguments and resolve and reject handlers to the task.
|
|
83
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
84
|
+
const task: RateLimitTask = {
|
|
85
|
+
sendArgs: [method, params],
|
|
86
|
+
resolve,
|
|
87
|
+
reject,
|
|
88
|
+
};
|
|
89
|
+
// We didn't previously wait for this push so we can emulate
|
|
90
|
+
// the same behavior with the `void` keyword.
|
|
91
|
+
void this.queue.push(task);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { ethers, logger } from "ethers";
|
|
2
|
+
import { CachingMechanismInterface } from "../interfaces";
|
|
3
|
+
import { delay, isDefined, isPromiseFulfilled, isPromiseRejected } from "../utils";
|
|
4
|
+
import { getOriginFromURL } from "../utils/NetworkUtils";
|
|
5
|
+
import { CacheProvider } from "./cachedProvider";
|
|
6
|
+
import { compareRpcResults, createSendErrorWithMessage, formatProviderError } from "./utils";
|
|
7
|
+
import { AugmentedLogger } from "@uma/logger";
|
|
8
|
+
import { PROVIDER_CACHE_TTL } from "./constants";
|
|
9
|
+
|
|
10
|
+
export class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
|
|
11
|
+
readonly providers: ethers.providers.StaticJsonRpcProvider[];
|
|
12
|
+
constructor(
|
|
13
|
+
params: ConstructorParameters<typeof ethers.providers.StaticJsonRpcProvider>[],
|
|
14
|
+
chainId: number,
|
|
15
|
+
readonly nodeQuorumThreshold: number,
|
|
16
|
+
readonly retries: number,
|
|
17
|
+
readonly delay: number,
|
|
18
|
+
readonly maxConcurrency: number,
|
|
19
|
+
providerCacheNamespace: string,
|
|
20
|
+
pctRpcCallsLogged: number,
|
|
21
|
+
redisClient?: CachingMechanismInterface,
|
|
22
|
+
standardTtlBlockDistance?: number,
|
|
23
|
+
noTtlBlockDistance?: number,
|
|
24
|
+
providerCacheTtl = PROVIDER_CACHE_TTL,
|
|
25
|
+
logger?: AugmentedLogger
|
|
26
|
+
) {
|
|
27
|
+
// Initialize the super just with the chainId, which stops it from trying to immediately send out a .send before
|
|
28
|
+
// this derived class is initialized.
|
|
29
|
+
super(undefined, chainId);
|
|
30
|
+
this.providers = params.map(
|
|
31
|
+
(inputs) =>
|
|
32
|
+
new CacheProvider(
|
|
33
|
+
providerCacheNamespace,
|
|
34
|
+
redisClient,
|
|
35
|
+
standardTtlBlockDistance,
|
|
36
|
+
noTtlBlockDistance,
|
|
37
|
+
providerCacheTtl,
|
|
38
|
+
maxConcurrency,
|
|
39
|
+
pctRpcCallsLogged,
|
|
40
|
+
logger,
|
|
41
|
+
...inputs
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
if (this.nodeQuorumThreshold < 1 || !Number.isInteger(this.nodeQuorumThreshold)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`nodeQuorum,Threshold cannot be < 1 and must be an integer. Currently set to ${this.nodeQuorumThreshold}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (this.retries < 0 || !Number.isInteger(this.retries)) {
|
|
50
|
+
throw new Error(`retries cannot be < 0 and must be an integer. Currently set to ${this.retries}`);
|
|
51
|
+
}
|
|
52
|
+
if (this.delay < 0) {
|
|
53
|
+
throw new Error(`delay cannot be < 0. Currently set to ${this.delay}`);
|
|
54
|
+
}
|
|
55
|
+
if (this.nodeQuorumThreshold > this.providers.length) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`nodeQuorumThreshold (${this.nodeQuorumThreshold}) must be <= the number of providers (${this.providers.length})`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override async send(method: string, params: Array<unknown>): Promise<unknown> {
|
|
63
|
+
const quorumThreshold = this._getQuorum(method, params);
|
|
64
|
+
const requiredProviders = this.providers.slice(0, quorumThreshold);
|
|
65
|
+
const fallbackProviders = this.providers.slice(quorumThreshold);
|
|
66
|
+
const errors: [ethers.providers.StaticJsonRpcProvider, string][] = [];
|
|
67
|
+
|
|
68
|
+
// This function is used to try to send with a provider and if it fails pop an element off the fallback list to try
|
|
69
|
+
// with that one. Once the fallback provider list is empty, the method throws. Because the fallback providers are
|
|
70
|
+
// removed, we ensure that no provider is used more than once because we care about quorum, making sure all
|
|
71
|
+
// considered responses come from unique providers.
|
|
72
|
+
const tryWithFallback = (
|
|
73
|
+
provider: ethers.providers.StaticJsonRpcProvider
|
|
74
|
+
): Promise<[ethers.providers.StaticJsonRpcProvider, unknown]> => {
|
|
75
|
+
return this._trySend(provider, method, params)
|
|
76
|
+
.then((result): [ethers.providers.StaticJsonRpcProvider, unknown] => [provider, result])
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
// Append the provider and error to the error array.
|
|
79
|
+
errors.push([provider, err?.stack || err?.toString()]);
|
|
80
|
+
|
|
81
|
+
// If there are no new fallback providers to use, terminate the recursion by throwing an error.
|
|
82
|
+
// Otherwise, we can try to call another provider.
|
|
83
|
+
if (fallbackProviders.length === 0) {
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// This line does two things:
|
|
88
|
+
// 1. Removes a fallback provider from the array so it cannot be used as a fallback for another required
|
|
89
|
+
// provider.
|
|
90
|
+
// 2. Recursively calls this method with that provider so it goes through the same try logic as the previous one.
|
|
91
|
+
return tryWithFallback(fallbackProviders.shift()!);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const results = await Promise.allSettled(requiredProviders.map(tryWithFallback));
|
|
96
|
+
|
|
97
|
+
if (!results.every(isPromiseFulfilled)) {
|
|
98
|
+
// Format the error so that it's very clear which providers failed and succeeded.
|
|
99
|
+
const errorTexts = errors.map(([provider, errorText]) => formatProviderError(provider, errorText));
|
|
100
|
+
const successfulProviderUrls = results.filter(isPromiseFulfilled).map((result) => result.value[0].connection.url);
|
|
101
|
+
throw createSendErrorWithMessage(
|
|
102
|
+
`Not enough providers succeeded. Errors:\n${errorTexts.join("\n")}\n` +
|
|
103
|
+
`Successful Providers:\n${successfulProviderUrls.join("\n")}`,
|
|
104
|
+
results.find(isPromiseRejected)?.reason
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const values = results.map((result) => result.value);
|
|
109
|
+
|
|
110
|
+
// Start at element 1 and begin comparing.
|
|
111
|
+
// If _all_ values are equal, we have hit quorum, so return.
|
|
112
|
+
if (values.slice(1).every(([, output]) => compareRpcResults(method, values[0][1], output))) {
|
|
113
|
+
return values[0][1];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const throwQuorumError = () => {
|
|
117
|
+
const errorTexts = errors.map(([provider, errorText]) => formatProviderError(provider, errorText));
|
|
118
|
+
const successfulProviderUrls = values.map(([provider]) => provider.connection.url);
|
|
119
|
+
throw new Error(
|
|
120
|
+
"Not enough providers agreed to meet quorum.\n" +
|
|
121
|
+
"Providers that errored:\n" +
|
|
122
|
+
`${errorTexts.join("\n")}\n` +
|
|
123
|
+
"Providers that succeeded, but some failed to match:\n" +
|
|
124
|
+
successfulProviderUrls.join("\n")
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Exit early if there are no fallback providers left.
|
|
129
|
+
if (fallbackProviders.length === 0) {
|
|
130
|
+
throwQuorumError();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try each fallback provider in parallel.
|
|
134
|
+
const fallbackResults = await Promise.allSettled(
|
|
135
|
+
fallbackProviders.map((provider) =>
|
|
136
|
+
this._trySend(provider, method, params)
|
|
137
|
+
.then((result): [ethers.providers.StaticJsonRpcProvider, unknown] => [provider, result])
|
|
138
|
+
.catch((err) => {
|
|
139
|
+
errors.push([provider, err?.stack || err?.toString()]);
|
|
140
|
+
throw new Error("No fallbacks during quorum search");
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// This filters only the fallbacks that succeeded.
|
|
146
|
+
const fallbackValues = fallbackResults.filter(isPromiseFulfilled).map((promise) => promise.value);
|
|
147
|
+
|
|
148
|
+
// Group the results by the count of that result.
|
|
149
|
+
const counts = [...values, ...fallbackValues].reduce(
|
|
150
|
+
(acc, curr) => {
|
|
151
|
+
const [, result] = curr;
|
|
152
|
+
|
|
153
|
+
// Find the first result that matches the return value.
|
|
154
|
+
const existingMatch = acc.find(([existingResult]) => compareRpcResults(method, existingResult, result));
|
|
155
|
+
|
|
156
|
+
// Increment the count if a match is found, else add a new element to the match array with a count of 1.
|
|
157
|
+
if (existingMatch) {
|
|
158
|
+
existingMatch[1]++;
|
|
159
|
+
} else {
|
|
160
|
+
acc.push([result, 1]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Return the same acc object because it was modified in place.
|
|
164
|
+
return acc;
|
|
165
|
+
},
|
|
166
|
+
[[undefined, 0]] as [unknown, number][] // Initialize with [undefined, 0] as the first element so something is always returned.
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Sort so the result with the highest count is first.
|
|
170
|
+
counts.sort(([, a], [, b]) => b - a);
|
|
171
|
+
|
|
172
|
+
// Extract the result by grabbing the first element.
|
|
173
|
+
const [quorumResult, count] = counts[0];
|
|
174
|
+
|
|
175
|
+
// If this count is less than we need for quorum, throw the quorum error.
|
|
176
|
+
if (count < quorumThreshold) {
|
|
177
|
+
throwQuorumError();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If we've achieved quorum, then we should still log the providers that mismatched with the quorum result.
|
|
181
|
+
const mismatchedProviders = Object.fromEntries(
|
|
182
|
+
[...values, ...fallbackValues]
|
|
183
|
+
.filter(([, result]) => !compareRpcResults(method, result, quorumResult))
|
|
184
|
+
.map(([provider, result]) => [provider.connection.url, result])
|
|
185
|
+
);
|
|
186
|
+
const quorumProviders = [...values, ...fallbackValues]
|
|
187
|
+
.filter(([, result]) => compareRpcResults(method, result, quorumResult))
|
|
188
|
+
.map(([provider]) => provider.connection.url);
|
|
189
|
+
if (Object.keys(mismatchedProviders).length > 0 || errors.length > 0) {
|
|
190
|
+
logger.warn({
|
|
191
|
+
at: "ProviderUtils",
|
|
192
|
+
message: "Some providers mismatched with the quorum result or failed 🚸",
|
|
193
|
+
notificationPath: "across-warn",
|
|
194
|
+
method,
|
|
195
|
+
params,
|
|
196
|
+
quorumProviders,
|
|
197
|
+
mismatchedProviders,
|
|
198
|
+
erroringProviders: errors.map(([provider, errorText]) => formatProviderError(provider, errorText)),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return quorumResult;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_validateResponse(method: string, _: Array<unknown>, response: unknown): boolean {
|
|
206
|
+
// Basic validation logic to start.
|
|
207
|
+
// Note: eth_getTransactionReceipt is ignored here because null responses are expected in the case that ethers is
|
|
208
|
+
// polling for the transaction receipt and receiving null until it does.
|
|
209
|
+
return isDefined(response) || method === "eth_getTransactionReceipt";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async _sendAndValidate(
|
|
213
|
+
provider: ethers.providers.StaticJsonRpcProvider,
|
|
214
|
+
method: string,
|
|
215
|
+
params: Array<unknown>
|
|
216
|
+
): Promise<unknown> {
|
|
217
|
+
const response = await provider.send(method, params);
|
|
218
|
+
if (!this._validateResponse(method, params, response)) {
|
|
219
|
+
// Not a warning to avoid spam since this could trigger a lot.
|
|
220
|
+
logger.debug({
|
|
221
|
+
at: "ProviderUtils",
|
|
222
|
+
message: "Provider returned invalid response",
|
|
223
|
+
provider: getOriginFromURL(provider.connection.url),
|
|
224
|
+
method,
|
|
225
|
+
params,
|
|
226
|
+
response,
|
|
227
|
+
});
|
|
228
|
+
throw new Error("Response failed validation");
|
|
229
|
+
}
|
|
230
|
+
return response;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_trySend(provider: ethers.providers.StaticJsonRpcProvider, method: string, params: Array<unknown>): Promise<unknown> {
|
|
234
|
+
let promise = this._sendAndValidate(provider, method, params);
|
|
235
|
+
for (let i = 0; i < this.retries; i++) {
|
|
236
|
+
promise = promise.catch(() => delay(this.delay).then(() => this._sendAndValidate(provider, method, params)));
|
|
237
|
+
}
|
|
238
|
+
return promise;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_getQuorum(method: string, params: Array<unknown>): number {
|
|
242
|
+
// Only use quorum if this is a historical query that doesn't depend on the current block number.
|
|
243
|
+
|
|
244
|
+
// All logs queries should use quorum.
|
|
245
|
+
if (method === "eth_getLogs") {
|
|
246
|
+
return this.nodeQuorumThreshold;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// getBlockByNumber should only use the quorum if it's not asking for the latest block.
|
|
250
|
+
if (method === "eth_getBlockByNumber" && params[0] !== "latest") {
|
|
251
|
+
return this.nodeQuorumThreshold;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// eth_call should only use quorum for queries at a specific past block.
|
|
255
|
+
if (method === "eth_call" && params[1] !== "latest") {
|
|
256
|
+
return this.nodeQuorumThreshold;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// All other calls should use quorum 1 to avoid errors due to sync differences.
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
}
|