@lidofinance/next-pages 0.46.0 → 0.48.0

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/README.md CHANGED
@@ -4,7 +4,7 @@ Common API and UI next pages.
4
4
 
5
5
  ## Installation
6
6
 
7
- * React 17 || 18
7
+ - React 17 || 18
8
8
 
9
9
  ```bash
10
10
  yarn add @lidofinance/next-pages
@@ -20,47 +20,45 @@ yarn add next@^12.3.0 prom-client@^14.0.0 @lidofinance/api-logger@^0.36.0 @lidof
20
20
  #### Health
21
21
 
22
22
  ```ts
23
- import { health } from '@lidofinance/next-pages';
23
+ import { health } from '@lidofinance/next-pages'
24
24
  // or import { health } from '@lidofinance/next-pages/api';
25
25
 
26
- export default health;
26
+ export default health
27
27
  ```
28
28
 
29
29
  #### Metrics
30
30
 
31
31
  ```ts
32
- import { registry } from 'utilsApi/metrics';
33
- import { metricsFactory } from '@lidofinance/next-pages';
32
+ import { registry } from 'utilsApi/metrics'
33
+ import { metricsFactory } from '@lidofinance/next-pages'
34
34
  // or import { metricsFactory } from '@lidofinance/next-pages/api';
35
35
 
36
36
  const metrics = metricsFactory({
37
37
  registry,
38
- });
38
+ })
39
39
 
40
- export default metrics;
40
+ export default metrics
41
41
  ```
42
42
 
43
43
  #### RPC
44
44
 
45
45
  ```ts
46
- import getConfig from 'next/config';
47
- import { rpcFactory } from '@lidofinance/api-pages';
46
+ import getConfig from 'next/config'
47
+ import { rpcFactory } from '@lidofinance/api-pages'
48
48
  // or import { rpcFactory } from '@lidofinance/next-pages/api';
49
- import { fetchRPC, serverLogger } from 'utilsApi';
50
- import { registry } from 'utilsApi/metrics';
51
- import { METRICS_PREFIX } from '../../config';
49
+ import { fetchRPC, serverLogger } from 'utilsApi'
50
+ import { registry } from 'utilsApi/metrics'
51
+ import { METRICS_PREFIX } from '../../config'
52
52
 
53
- const { publicRuntimeConfig, serverRuntimeConfig } = getConfig();
54
- const { defaultChain } = publicRuntimeConfig;
55
- const { infuraApiKey, alchemyApiKey } = serverRuntimeConfig;
53
+ const { publicRuntimeConfig, serverRuntimeConfig } = getConfig()
54
+ const { defaultChain } = publicRuntimeConfig
55
+ const { infuraApiKey, alchemyApiKey } = serverRuntimeConfig
56
56
 
57
- // Should be a separate file
58
57
  export const enum CHAINS {
59
58
  Mainnet = 1,
60
59
  Goerli = 5,
61
60
  }
62
61
 
63
- // Should be a separate file
64
62
  export const providers: Record<CHAINS, [string, ...string[]]> = {
65
63
  [CHAINS.Mainnet]: [
66
64
  `https://mainnet.infura.io/v3/${infuraApiKey}`,
@@ -70,24 +68,87 @@ export const providers: Record<CHAINS, [string, ...string[]]> = {
70
68
  `https://goerli.infura.io/v3/${infuraApiKey}`,
71
69
  `https://eth-goerli.alchemyapi.io/v2/${alchemyApiKey}`,
72
70
  ],
73
- };
71
+ }
72
+
73
+ const allowedCallAddresses: Record<string, string[]> = {
74
+ /* Mapping of chainIds to array of addresses permitted for eth_getLogs */
75
+ }
76
+
77
+ const allowedLogsAddresses: Record<string, string[]> = {
78
+ /* Mapping of chainIds to array of addresses permitted for eth_call */
79
+ }
80
+
81
+ // example
82
+ const allowedRPCMethods = [
83
+ 'test',
84
+ 'eth_call',
85
+ 'eth_gasPrice',
86
+ 'eth_getCode',
87
+ 'eth_estimateGas',
88
+ 'eth_getBlockByNumber',
89
+ 'eth_feeHistory',
90
+ 'eth_maxPriorityFeePerGas',
91
+ 'eth_getBalance',
92
+ 'eth_blockNumber',
93
+ 'eth_getTransactionByHash',
94
+ 'eth_getTransactionReceipt',
95
+ 'eth_getTransactionCount',
96
+ 'eth_sendRawTransaction',
97
+ 'eth_getLogs',
98
+ 'eth_chainId',
99
+ 'net_version',
100
+ ]
74
101
 
75
102
  const rpc = rpcFactory({
76
- fetchRPC,
103
+ fetchRPC: trackedFetchRpcFactory({
104
+ registry: Metrics.registry,
105
+ prefix: METRICS_PREFIX,
106
+ }),
77
107
  metrics: {
78
108
  prefix: METRICS_PREFIX,
79
- registry,
109
+ registry: Metrics.registry,
80
110
  },
81
- allowedRPCMethods: [
82
- 'eth_call',
83
- 'eth_gasPrice',
84
- // ...
85
- ],
86
111
  defaultChain,
87
- providers,
88
- });
112
+ providers: {
113
+ [CHAINS.Mainnet]: secretConfig.rpcUrls_1,
114
+ [CHAINS.Holesky]: secretConfig.rpcUrls_17000,
115
+ },
116
+ // optional validation options to protect from misuse of public endpoints
117
+ // Strongly recommended to use all of them
118
+ validation: {
119
+ // !!! IMPORTANT, MUST HAVE !!!
120
+ // allowed JSON RPC methods
121
+ allowedRPCMethods,
122
+
123
+ // !!! IMPORTANT, MUST HAVE !!!
124
+ // mapping chainId - addresses fro eth_call, put only contracts you dapp interacts with
125
+ // no entry for chainId or empty array will prevent method for this chainId
126
+ allowedCallAddresses,
127
+
128
+ // !!! IMPORTANT, MUST HAVE !!!
129
+ // mapping chainId - addresses fro eth_getLogs put only contracts here if you need to fetch events from them
130
+ // no entry for chainId or empty array will prevent method for this chainId
131
+ allowedLogsAddresses,
132
+
133
+ // max JSON-RPC batch size
134
+ maxBatchCount: 20, // DEFAULT
135
+
136
+ // prevents empty `address` param for eth_getLogs, that can results in massive queries
137
+ blockEmptyAddressGetLogs: true, // DEFAULT
138
+
139
+ // only 20k blocks size historical queries for eth_getLogs
140
+ // fetches and caches current block to compare with ambiguous blockTags(e.g. "latest")
141
+ maxGetLogsRange: 20_000, // DEFAULT
142
+
143
+ /// cache TTL for maxGetLogsRange
144
+ currentBlockTTLms: 60_000; // DEFAULT
145
+
146
+ // 1mb max response after which data stream is interrupted and 413 is returned
147
+ maxResponseSize: 1_000_000, // DEFAULT
148
+ },
149
+ })
89
150
 
90
- export default rpc;
151
+ export default rpc
91
152
  ```
92
153
 
93
154
  ### UI pages
@@ -98,17 +159,19 @@ export default rpc;
98
159
  - **Page500**
99
160
 
100
161
  Example for `pages/404.tsx`
162
+
101
163
  ```tsx
102
- import { Page404 } from '@lidofinance/next-ui-pages/ui';
164
+ import { Page404 } from '@lidofinance/next-ui-pages/ui'
103
165
 
104
- export default Page404;
166
+ export default Page404
105
167
  ```
106
168
 
107
169
  Example for `pages/500.tsx`
170
+
108
171
  ```tsx
109
- import { Page500 } from '@lidofinance/next-ui-pages/ui';
172
+ import { Page500 } from '@lidofinance/next-ui-pages/ui'
110
173
 
111
- export default Page500;
174
+ export default Page500
112
175
  ```
113
176
 
114
177
  #### Error page as generic component
@@ -116,10 +179,10 @@ export default Page500;
116
179
  Example for `pages/404.tsx`
117
180
 
118
181
  ```tsx
119
- import { FC } from 'react';
120
- import { PageError } from '@lidofinance/next-ui-pages/ui';
182
+ import { FC } from 'react'
183
+ import { PageError } from '@lidofinance/next-ui-pages/ui'
121
184
 
122
- const Page404: FC = () => <PageError title="404" content="Page Not Found" />;
185
+ const Page404: FC = () => <PageError title="404" content="Page Not Found" />
123
186
 
124
- export default Page404;
187
+ export default Page404
125
188
  ```
@@ -1,4 +1,4 @@
1
1
  export { health } from './health';
2
2
  export { type MetricsFactoryParameters, metricsFactory } from './metricsFactory';
3
- export { type RpcProviders, DEFAULT_API_ERROR_MESSAGE, HEALTHY_RPC_SERVICES_ARE_OVER, UnsupportedChainIdError, UnsupportedHTTPMethodError, type RPCFactoryParams, rpcFactory, } from './rpcFactory';
3
+ export { type RPCFactoryParams, type RPCFactoryValidationParams, type RpcProviders, rpcFactory } from './rpcFactory';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,KAAK,wBAAwB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAChF,OAAO,EACL,KAAK,YAAY,EACjB,yBAAyB,EACzB,6BAA6B,EAC7B,uBAAuB,EACvB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,UAAU,GACX,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,KAAK,wBAAwB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAChF,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,0BAA0B,EAAE,KAAK,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA"}
package/dist/api/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import {Transform as $FgOCV$Transform, Readable as $FgOCV$Readable} from "node:stream";
1
+ import {Readable as $FgOCV$Readable, Transform as $FgOCV$Transform} from "node:stream";
2
+ import {pipeline as $FgOCV$pipeline} from "node:stream/promises";
2
3
  import {Counter as $FgOCV$Counter} from "prom-client";
3
4
  import {iterateUrls as $FgOCV$iterateUrls} from "@lidofinance/rpc";
5
+ import {Cache as $FgOCV$Cache} from "memory-cache";
4
6
 
5
7
  const $6ad1ea13f08e3d24$export$f0784a54fb0af903 = (_req, res)=>{
6
8
  res.status(200).send({
@@ -18,140 +20,230 @@ const $29c7c52f2735c037$export$fc20b1372f40c2c5 = ({ registry: registry })=>asyn
18
20
 
19
21
 
20
22
 
21
- const $7d34ac0315ec4c05$export$c982278a0ddc4dbe = "Something went wrong. Sorry, try again later :(";
22
- const $7d34ac0315ec4c05$export$56a189db5508cdcb = "Healthy RPC services are over!";
23
- class $7d34ac0315ec4c05$export$bb8c263ad32caa8 extends Error {
23
+
24
+ const $09f9daeda4842558$export$c982278a0ddc4dbe = "Something went wrong. Sorry, try again later :(";
25
+ const $09f9daeda4842558$export$56a189db5508cdcb = "Healthy RPC services are over!";
26
+ class $09f9daeda4842558$export$bb8c263ad32caa8 extends Error {
24
27
  }
25
- class $7d34ac0315ec4c05$export$fc97cac2ba066ec7 extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
28
+ class $09f9daeda4842558$export$fc97cac2ba066ec7 extends $09f9daeda4842558$export$bb8c263ad32caa8 {
26
29
  constructor(message){
27
30
  super(message || "Unsupported chainId");
28
31
  }
29
32
  }
30
- class $7d34ac0315ec4c05$export$2dfa2d8949b43fdc extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
33
+ class $09f9daeda4842558$export$2dfa2d8949b43fdc extends $09f9daeda4842558$export$bb8c263ad32caa8 {
31
34
  constructor(message){
32
35
  super(message || "Unsupported HTTP method");
33
36
  }
34
37
  }
35
- class $7d34ac0315ec4c05$export$2adbfa76b71ba748 extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
38
+ class $09f9daeda4842558$export$2adbfa76b71ba748 extends $09f9daeda4842558$export$bb8c263ad32caa8 {
36
39
  constructor(message){
37
40
  super(message || "Invalid Request");
38
41
  }
39
42
  }
40
- class $7d34ac0315ec4c05$export$e224c1080d950012 extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
43
+ class $09f9daeda4842558$export$e224c1080d950012 extends $09f9daeda4842558$export$bb8c263ad32caa8 {
41
44
  constructor(message){
42
- super(message || "Invalid Request");
45
+ super(message || "Response size too large");
43
46
  }
44
47
  }
45
- const $7d34ac0315ec4c05$var$createSizeLogger = (MAX_SIZE)=>{
46
- let bytesWritten = 0;
47
- const logSizeStream = new (0, $FgOCV$Transform)({
48
- transform (chunk, _encoding, callback) {
49
- bytesWritten += chunk.length;
50
- if (bytesWritten > MAX_SIZE) // Emit an error if size exceeds MAX_SIZE
51
- return callback(new $7d34ac0315ec4c05$export$e224c1080d950012(`Stream size exceeds the maximum limit of ${MAX_SIZE} bytes`));
52
- return callback(null, chunk) // Pass the chunk through
53
- ;
54
- },
55
- flush (callback) {
56
- callback();
48
+
49
+
50
+
51
+
52
+
53
+ // validation factories
54
+ const $5a404da009e373d8$var$zeroBN = BigInt(0);
55
+ const $5a404da009e373d8$export$564dd6c0fb12e680 = (defaultChain, providers, maxBatchSize)=>{
56
+ return (req)=>{
57
+ if (req.method !== "POST") // We don't care about tracking blocked requests here
58
+ throw new (0, $09f9daeda4842558$export$2dfa2d8949b43fdc)();
59
+ const chainId = Number(req.query.chainId || defaultChain);
60
+ // Allow only chainId of specified chains
61
+ if (providers[chainId] == null) // We don't care about tracking blocked requests here
62
+ throw new (0, $09f9daeda4842558$export$fc97cac2ba066ec7)();
63
+ const requests = Array.isArray(req.body) ? req.body : [
64
+ req.body
65
+ ];
66
+ if (typeof maxBatchSize === "number" && requests.length > maxBatchSize) throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`Too many batched requests`);
67
+ return {
68
+ chainId: chainId,
69
+ requests: requests
70
+ };
71
+ };
72
+ };
73
+ const $5a404da009e373d8$export$bd57dbd3bc9a1cc6 = (allowedMethods)=>{
74
+ const methodsMap = new Set(allowedMethods ?? []);
75
+ return (request, context)=>{
76
+ const method = request.method;
77
+ if (typeof method !== "string") throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method isn't string`);
78
+ if (allowedMethods && !methodsMap.has(request.method)) {
79
+ context.rpcRequestBlocked.inc({
80
+ reason: "method not allowed"
81
+ });
82
+ throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method ${method} not allowed`);
57
83
  }
58
- });
59
- return logSizeStream;
84
+ };
85
+ };
86
+ const $5a404da009e373d8$export$b724143801441d66 = (allowedAddress)=>{
87
+ const allowedCallAddressMap = $5a404da009e373d8$var$createAllowedAddressMap(allowedAddress);
88
+ return (request, { chainId: chainId, rpcRequestBlocked: rpcRequestBlocked })=>{
89
+ const { params: params } = request;
90
+ if (Array.isArray(params) && params[0] && typeof params[0] === "object" && "to" in params[0] && typeof params[0].to === "string") {
91
+ if (!allowedCallAddressMap[chainId]?.has(params[0].to.toLowerCase())) {
92
+ rpcRequestBlocked.inc({
93
+ reason: "address not allowed for eth_call"
94
+ });
95
+ throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`Address not allowed for eth_call`);
96
+ }
97
+ } else throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method eth_call is invalid`);
98
+ };
99
+ };
100
+ const $5a404da009e373d8$export$5d8ee2232a9cb21a = (allowedAddress, blockEmptyAddressGetLogs, maxBlockRange, currentBlockTTL = 60000)=>{
101
+ const allowedAddressMap = $5a404da009e373d8$var$createAllowedAddressMap(allowedAddress ?? {});
102
+ const maxBlockRangeBN = maxBlockRange ? BigInt(maxBlockRange) : $5a404da009e373d8$var$zeroBN;
103
+ const currentBlock = new (0, $FgOCV$Cache)();
104
+ return async (request, { chainId: chainId, rpcRequestBlocked: rpcRequestBlocked, requestRPC: requestRPC })=>{
105
+ try {
106
+ const params = request.params[0];
107
+ const fromBlock = params.fromBlock;
108
+ const toBlock = params.toBlock ?? "latest";
109
+ if (blockEmptyAddressGetLogs || allowedAddress) {
110
+ // address validation
111
+ const address = params.address;
112
+ if (blockEmptyAddressGetLogs && !address) throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method eth_getLogs address is invalid`);
113
+ const addresses = Array.isArray(address) ? address : [
114
+ address
115
+ ];
116
+ if (blockEmptyAddressGetLogs && addresses.length === 0) throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method eth_getLogs address is invalid`);
117
+ if (allowedAddress && addresses.some((eventAddress)=>// needs this check before toLowerCase
118
+ typeof eventAddress !== "string" || !allowedAddressMap[chainId]?.has(eventAddress.toLowerCase()))) {
119
+ rpcRequestBlocked.inc({
120
+ reason: "address not allowed for eth_getLogs"
121
+ });
122
+ throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`Address not allowed for eth_getLogs`);
123
+ }
124
+ }
125
+ // block range validation
126
+ if (maxBlockRange) {
127
+ if (fromBlock === "earliest") throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method eth_getLogs fromBlock is invalid`);
128
+ const shouldValidateBlockDistance = fromBlock.startsWith("0x");
129
+ if (shouldValidateBlockDistance) {
130
+ const normalizedFromBlock = BigInt(fromBlock);
131
+ let normalizedToBlock;
132
+ if (toBlock.startsWith("0x")) normalizedToBlock = BigInt(toBlock);
133
+ else {
134
+ const cached = currentBlock.get(chainId);
135
+ if (cached) normalizedToBlock = cached;
136
+ else {
137
+ normalizedToBlock = await requestRPC(chainId, {
138
+ id: 1,
139
+ jsonrpc: "2.0",
140
+ method: "eth_blockNumber"
141
+ }).then((res)=>res.json()).then((res)=>BigInt(res.result));
142
+ currentBlock.put(chainId, normalizedToBlock, currentBlockTTL);
143
+ }
144
+ }
145
+ const range = normalizedToBlock - normalizedFromBlock;
146
+ if (range < $5a404da009e373d8$var$zeroBN || range > maxBlockRangeBN) {
147
+ rpcRequestBlocked.inc({
148
+ reason: "eth_getLogs range is invalid"
149
+ });
150
+ throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method eth_getLogs range is invalid`);
151
+ }
152
+ }
153
+ }
154
+ } catch (error) {
155
+ if (error instanceof (0, $09f9daeda4842558$export$bb8c263ad32caa8)) throw error;
156
+ throw new (0, $09f9daeda4842558$export$2adbfa76b71ba748)(`RPC method eth_getLogs is invalid`);
157
+ }
158
+ };
60
159
  };
61
- const $7d34ac0315ec4c05$export$9bfcc39f106ad871 = ({ metrics: { prefix: prefix, registry: registry }, providers: providers, fetchRPC: fetchRPC, defaultChain: defaultChain, allowedRPCMethods: allowedRPCMethods, allowedCallAddresses: allowedCallAddresses = {}, allowedLogsAddresses: allowedLogsAddresses = {}, maxBatchCount: maxBatchCount, maxResponseSize: maxResponseSize = 1000000, disallowEmptyAddressGetLogs: disallowEmptyAddressGetLogs = false })=>{
160
+ const $5a404da009e373d8$export$7c5a2292697deb4b = (MAX_SIZE)=>(context)=>{
161
+ let bytesWritten = 0;
162
+ return new (0, $FgOCV$Transform)({
163
+ transform (chunk, _encoding, callback) {
164
+ bytesWritten += chunk.length;
165
+ if (bytesWritten > MAX_SIZE) {
166
+ // Emit an error if size exceeds MAX_SIZE
167
+ context.rpcRequestBlocked.inc({
168
+ reason: "response too large"
169
+ });
170
+ return callback(new (0, $09f9daeda4842558$export$e224c1080d950012)(`Stream size exceeds the maximum limit of ${MAX_SIZE} bytes`));
171
+ }
172
+ return callback(null, chunk) // Pass the chunk through
173
+ ;
174
+ },
175
+ flush (callback) {
176
+ callback();
177
+ }
178
+ });
179
+ };
180
+ const $5a404da009e373d8$export$e67f820dcb2fefda = (method, allowedRPCMethods)=>!allowedRPCMethods || allowedRPCMethods.includes(method);
181
+ const $5a404da009e373d8$var$createAllowedAddressMap = (allowedAddress)=>{
182
+ return Object.entries(allowedAddress).reduce((acc, [chainId, addresses])=>{
183
+ acc[chainId] = new Set(addresses.map((a)=>a.toLowerCase()));
184
+ return acc;
185
+ }, {});
186
+ };
187
+
188
+
189
+ const $3f76f1f6d2274759$export$9bfcc39f106ad871 = ({ metrics: metrics, providers: providers, fetchRPC: fetchRPC, defaultChain: defaultChain, validation: validation })=>{
190
+ const { allowedRPCMethods: allowedRPCMethods, allowedCallAddresses: allowedCallAddresses, allowedLogsAddresses: allowedLogsAddresses, maxResponseSize: maxResponseSize = 1000000, maxBatchCount: maxBatchCount = 20, currentBlockTTLms: currentBlockTTLms = 60000, maxGetLogsRange: maxGetLogsRange = 20000, blockEmptyAddressGetLogs: blockEmptyAddressGetLogs = true } = validation;
191
+ // optional metrics
192
+ const { registry: registry, prefix: prefix = "" } = metrics ?? {};
62
193
  const rpcRequestBlocked = new (0, $FgOCV$Counter)({
63
194
  name: prefix + "rpc_service_request_blocked",
64
195
  help: "RPC service request blocked",
65
- labelNames: [],
196
+ labelNames: [
197
+ "reason"
198
+ ],
66
199
  registers: []
67
200
  });
68
- registry.registerMetric(rpcRequestBlocked);
69
- const allowedCallAddressMap = Object.entries(allowedCallAddresses).reduce((acc, [chainId, addresses])=>{
70
- acc[chainId] = new Set(addresses.map((a)=>a.toLowerCase()));
71
- return acc;
72
- }, {});
73
- const allowedLogsAddressMap = Object.entries(allowedLogsAddresses).reduce((acc, [chainId, addresses])=>{
74
- acc[chainId] = new Set(addresses.map((a)=>a.toLowerCase()));
75
- return acc;
76
- }, {});
201
+ registry?.registerMetric(rpcRequestBlocked);
202
+ const validateBaseRequest = (0, $5a404da009e373d8$export$564dd6c0fb12e680)(defaultChain, providers, maxBatchCount);
203
+ const validateRpcMethod = (0, $5a404da009e373d8$export$bd57dbd3bc9a1cc6)(allowedRPCMethods);
204
+ const validateEthCall = allowedCallAddresses && (0, $5a404da009e373d8$export$e67f820dcb2fefda)("eth_call", allowedRPCMethods) ? (0, $5a404da009e373d8$export$b724143801441d66)(allowedCallAddresses) : undefined;
205
+ const validateEthGetLogs = (allowedLogsAddresses || blockEmptyAddressGetLogs) && (0, $5a404da009e373d8$export$e67f820dcb2fefda)("eth_getLogs", allowedRPCMethods) ? (0, $5a404da009e373d8$export$5d8ee2232a9cb21a)(allowedLogsAddresses, blockEmptyAddressGetLogs, maxGetLogsRange, currentBlockTTLms) : undefined;
206
+ const validateMaxSteamSize = (0, $5a404da009e373d8$export$7c5a2292697deb4b)(maxResponseSize);
207
+ const requestRPC = (chainId, body)=>(0, $FgOCV$iterateUrls)(providers[chainId], // TODO: consider adding verification that body is actually matches FetchRpcInitBody
208
+ (url)=>fetchRPC(url, {
209
+ body: body
210
+ }, {
211
+ chainId: chainId
212
+ }), // eslint-disable-next-line @typescript-eslint/unbound-method
213
+ console.error);
77
214
  return async (req, res)=>{
215
+ const abortController = new AbortController();
78
216
  try {
79
- // Accept only POST requests
80
- if (req.method !== "POST") // We don't care about tracking blocked requests here
81
- throw new $7d34ac0315ec4c05$export$2dfa2d8949b43fdc();
82
- const chainId = Number(req.query.chainId || defaultChain);
83
- // Allow only chainId of specified chains
84
- if (providers[chainId] == null) // We don't care about tracking blocked requests here
85
- throw new $7d34ac0315ec4c05$export$fc97cac2ba066ec7();
86
- const requests = Array.isArray(req.body) ? req.body : [
87
- req.body
88
- ];
89
- if (typeof maxBatchCount === "number" && requests.length > maxBatchCount) throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`Too many batched requests`);
90
- // Check if provided methods are allowed
217
+ const { chainId: chainId, requests: requests } = validateBaseRequest(req);
218
+ const validationContext = {
219
+ chainId: chainId,
220
+ rpcRequestBlocked: rpcRequestBlocked,
221
+ requestRPC: requestRPC
222
+ };
91
223
  // We throw HTTP error for ANY invalid RPC request out of batch
92
- // because we assume that frontend must not send invalid requests
93
- for (const { method: method, params: params } of requests){
94
- if (typeof method !== "string") throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`RPC method isn't string`);
95
- if (!allowedRPCMethods.includes(method)) {
96
- rpcRequestBlocked.inc();
97
- throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`RPC method ${method} isn't allowed`);
98
- }
99
- if (method === "eth_call" && allowedCallAddressMap[chainId]) {
100
- if (Array.isArray(params) && typeof params[0] === "object" && typeof params[0].to === "string") {
101
- if (!allowedCallAddressMap[chainId].has(params[0].to.toLowerCase())) {
102
- rpcRequestBlocked.inc();
103
- throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`Address not allowed for eth_call`);
104
- }
105
- } else throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`RPC method eth_call is invalid`);
106
- }
107
- if (method === "eth_getLogs" && (disallowEmptyAddressGetLogs || allowedLogsAddressMap[chainId])) {
108
- if (Array.isArray(params) && typeof params[0] === "object") {
109
- const address = params[0].address;
110
- if (disallowEmptyAddressGetLogs && (!address || Array.isArray(address) && address.length === 0)) {
111
- rpcRequestBlocked.inc();
112
- throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`No empty address on eth_getLogs`);
113
- }
114
- const addresses = Array.isArray(address) ? address : [
115
- address
116
- ];
117
- if (addresses.some((eventAddress)=>// needs this check before toLowerCase
118
- typeof eventAddress !== "string" || !allowedLogsAddressMap[chainId].has(eventAddress.toLowerCase()))) {
119
- rpcRequestBlocked.inc();
120
- throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`Address not allowed for eth_getLogs`);
121
- }
122
- } else throw new $7d34ac0315ec4c05$export$2adbfa76b71ba748(`Invalid eth_getLogs`);
123
- }
224
+ // because we assume that frontend must not send invalid requests at all
225
+ for (const request of requests){
226
+ validateRpcMethod(request, validationContext);
227
+ const method = request.method;
228
+ if (method === "eth_call" && validateEthCall) validateEthCall(request, validationContext);
229
+ if (method === "eth_getLogs" && validateEthGetLogs) await validateEthGetLogs(request, validationContext);
124
230
  }
125
- const requested = await (0, $FgOCV$iterateUrls)(providers[chainId], // TODO: consider adding verification that body is actually matches FetchRpcInitBody
126
- (url)=>fetchRPC(url, {
127
- body: req.body
128
- }, {
129
- chainId: chainId
130
- }), // eslint-disable-next-line @typescript-eslint/unbound-method
131
- console.error);
132
- res.setHeader("Content-Type", requested.headers.get("Content-Type") ?? "application/json");
133
- if (requested.body) {
134
- const sizeLimit = $7d34ac0315ec4c05$var$createSizeLogger(maxResponseSize);
135
- const readableStream = (0, $FgOCV$Readable).fromWeb(requested.body);
136
- readableStream.pipe(sizeLimit).on("error", (error)=>{
137
- if (error instanceof $7d34ac0315ec4c05$export$e224c1080d950012) {
138
- console.warn(`[rpcFactory] RPC response too large: ${JSON.stringify(requests)}`);
139
- res.status(413).end() // Payload Too Large
140
- ;
141
- } else {
142
- res.statusCode = 500;
143
- res.end($7d34ac0315ec4c05$export$c982278a0ddc4dbe);
144
- }
145
- readableStream.destroy();
146
- }).pipe(res);
147
- } else res.status(requested.status).json("There are a problems with RPC provider");
231
+ const proxyedRPC = await requestRPC(chainId, req.body);
232
+ res.setHeader("Content-Type", proxyedRPC.headers.get("Content-Type") ?? "application/json");
233
+ if (!proxyedRPC.body) throw new Error("There are a problems with RPC provider");
234
+ // auto closes both Readable.fromWeb() and underlying proxyedRPC streams on error
235
+ await (0, $FgOCV$pipeline)((0, $FgOCV$Readable).fromWeb(proxyedRPC.body), validateMaxSteamSize(validationContext), res, {
236
+ signal: abortController.signal
237
+ });
148
238
  } catch (error) {
149
- if (error instanceof $7d34ac0315ec4c05$export$bb8c263ad32caa8) res.status(400).json(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
239
+ if (error instanceof (0, $09f9daeda4842558$export$bb8c263ad32caa8)) res.status(400).json(error.message ?? (0, $09f9daeda4842558$export$c982278a0ddc4dbe));
150
240
  else if (error instanceof Error) {
151
- // TODO: check if there are errors duplication with iterateUrls
152
- console.error(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
153
- res.status(500).json(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
154
- } else res.status(500).json($7d34ac0315ec4c05$export$56a189db5508cdcb);
241
+ console.error("[rpcFactory]" + (error.message ?? (0, $09f9daeda4842558$export$c982278a0ddc4dbe)));
242
+ if (!res.headersSent) res.status(500).json(error.message ?? (0, $09f9daeda4842558$export$c982278a0ddc4dbe));
243
+ } else res.status(500).json((0, $09f9daeda4842558$export$56a189db5508cdcb));
244
+ } finally{
245
+ // forces pipeline closure in case of external error/abort
246
+ abortController.abort();
155
247
  }
156
248
  };
157
249
  };
@@ -159,5 +251,7 @@ const $7d34ac0315ec4c05$export$9bfcc39f106ad871 = ({ metrics: { prefix: prefix,
159
251
 
160
252
 
161
253
 
162
- export {$6ad1ea13f08e3d24$export$f0784a54fb0af903 as health, $29c7c52f2735c037$export$fc20b1372f40c2c5 as metricsFactory, $7d34ac0315ec4c05$export$c982278a0ddc4dbe as DEFAULT_API_ERROR_MESSAGE, $7d34ac0315ec4c05$export$56a189db5508cdcb as HEALTHY_RPC_SERVICES_ARE_OVER, $7d34ac0315ec4c05$export$fc97cac2ba066ec7 as UnsupportedChainIdError, $7d34ac0315ec4c05$export$2dfa2d8949b43fdc as UnsupportedHTTPMethodError, $7d34ac0315ec4c05$export$9bfcc39f106ad871 as rpcFactory};
254
+
255
+
256
+ export {$6ad1ea13f08e3d24$export$f0784a54fb0af903 as health, $29c7c52f2735c037$export$fc20b1372f40c2c5 as metricsFactory, $3f76f1f6d2274759$export$9bfcc39f106ad871 as rpcFactory};
163
257
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"mappings":"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","sources":["packages/next/pages/src/api/index.ts","packages/next/pages/src/api/health.ts","packages/next/pages/src/api/metricsFactory.ts","packages/next/pages/src/api/rpcFactory.ts"],"sourcesContent":["export { health } from './health'\nexport { type MetricsFactoryParameters, metricsFactory } from './metricsFactory'\nexport {\n type RpcProviders,\n DEFAULT_API_ERROR_MESSAGE,\n HEALTHY_RPC_SERVICES_ARE_OVER,\n UnsupportedChainIdError,\n UnsupportedHTTPMethodError,\n type RPCFactoryParams,\n rpcFactory,\n} from './rpcFactory'\n","import { NextApiRequest, NextApiResponse } from 'next'\n\nexport const health = (_req: NextApiRequest, res: NextApiResponse): void => {\n res.status(200).send({ status: 'ok' })\n}\n","import { NextApiRequest, NextApiResponse } from 'next'\nimport { Registry } from 'prom-client'\n\nexport type MetricsFactoryParameters = {\n registry: Registry\n}\n\nexport const metricsFactory =\n ({ registry }: MetricsFactoryParameters) =>\n async (_req: NextApiRequest, res: NextApiResponse) => {\n const collectedMetrics = await registry.metrics()\n res.send(collectedMetrics)\n }\n","import { Readable, Transform } from 'node:stream'\nimport { ReadableStream } from 'node:stream/web'\nimport type { NextApiRequest, NextApiResponse } from 'next'\nimport { Counter, Registry } from 'prom-client'\nimport type { TrackedFetchRPC } from '@lidofinance/api-rpc'\nimport type { FetchRpcInitBody } from '@lidofinance/rpc'\nimport { iterateUrls } from '@lidofinance/rpc'\n\nexport type RpcProviders = Record<string | number, [string, ...string[]]>\n\nexport const DEFAULT_API_ERROR_MESSAGE = 'Something went wrong. Sorry, try again later :('\n\nexport const HEALTHY_RPC_SERVICES_ARE_OVER = 'Healthy RPC services are over!'\n\nexport class ClientError extends Error {}\nexport class UnsupportedChainIdError extends ClientError {\n constructor(message?: string) {\n super(message || 'Unsupported chainId')\n }\n}\n\nexport class UnsupportedHTTPMethodError extends ClientError {\n constructor(message?: string) {\n super(message || 'Unsupported HTTP method')\n }\n}\n\nexport class InvalidRequestError extends ClientError {\n constructor(message?: string) {\n super(message || 'Invalid Request')\n }\n}\n\nexport class SizeTooLargeError extends ClientError {\n constructor(message?: string) {\n super(message || 'Invalid Request')\n }\n}\n\nconst createSizeLogger = (MAX_SIZE: number) => {\n let bytesWritten = 0\n const logSizeStream = new Transform({\n transform(chunk, _encoding, callback) {\n bytesWritten += chunk.length\n if (bytesWritten > MAX_SIZE) {\n // Emit an error if size exceeds MAX_SIZE\n return callback(new SizeTooLargeError(`Stream size exceeds the maximum limit of ${MAX_SIZE} bytes`))\n }\n return callback(null, chunk) // Pass the chunk through\n },\n flush(callback) {\n callback()\n },\n })\n return logSizeStream\n}\n\nexport type RPCFactoryParams = {\n metrics: {\n prefix: string\n registry: Registry\n }\n providers: RpcProviders\n fetchRPC: TrackedFetchRPC\n defaultChain: string | number\n // If we don't specify allowed RPC methods, then we can't use\n // fetchRPC with prometheus, otherwise it will blow up, if someone will send arbitrary\n // methods\n allowedRPCMethods: string[]\n // filtration by eth_call to addresses\n allowedCallAddresses?: Record<number, string[]>\n allowedLogsAddresses?: Record<number, string[]>\n disallowEmptyAddressGetLogs?: boolean\n maxBatchCount?: number\n maxResponseSize?: number\n}\n\nexport const rpcFactory = ({\n metrics: { prefix, registry },\n providers,\n fetchRPC,\n defaultChain,\n allowedRPCMethods,\n allowedCallAddresses = {},\n allowedLogsAddresses = {},\n maxBatchCount,\n maxResponseSize = 1_000_000, // ~1MB,\n disallowEmptyAddressGetLogs = false,\n}: RPCFactoryParams) => {\n const rpcRequestBlocked = new Counter({\n name: prefix + 'rpc_service_request_blocked',\n help: 'RPC service request blocked',\n labelNames: [],\n registers: [],\n })\n registry.registerMetric(rpcRequestBlocked)\n\n const allowedCallAddressMap = Object.entries(allowedCallAddresses).reduce((acc, [chainId, addresses]) => {\n acc[chainId] = new Set(addresses.map((a) => a.toLowerCase()))\n return acc\n }, {} as Record<string, Set<string>>)\n\n const allowedLogsAddressMap = Object.entries(allowedLogsAddresses).reduce((acc, [chainId, addresses]) => {\n acc[chainId] = new Set(addresses.map((a) => a.toLowerCase()))\n return acc\n }, {} as Record<string, Set<string>>)\n\n return async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {\n try {\n // Accept only POST requests\n if (req.method !== 'POST') {\n // We don't care about tracking blocked requests here\n throw new UnsupportedHTTPMethodError()\n }\n\n const chainId = Number(req.query.chainId || defaultChain)\n\n // Allow only chainId of specified chains\n if (providers[chainId] == null) {\n // We don't care about tracking blocked requests here\n throw new UnsupportedChainIdError()\n }\n\n const requests = Array.isArray(req.body) ? req.body : [req.body]\n\n if (typeof maxBatchCount === 'number' && requests.length > maxBatchCount) {\n throw new InvalidRequestError(`Too many batched requests`)\n }\n\n // Check if provided methods are allowed\n // We throw HTTP error for ANY invalid RPC request out of batch\n // because we assume that frontend must not send invalid requests\n for (const { method, params } of requests) {\n if (typeof method !== 'string') {\n throw new InvalidRequestError(`RPC method isn't string`)\n }\n if (!allowedRPCMethods.includes(method)) {\n rpcRequestBlocked.inc()\n throw new InvalidRequestError(`RPC method ${method} isn't allowed`)\n }\n if (method === 'eth_call' && allowedCallAddressMap[chainId]) {\n if (Array.isArray(params) && typeof params[0] === 'object' && typeof params[0].to === 'string') {\n if (!allowedCallAddressMap[chainId].has(params[0].to.toLowerCase())) {\n rpcRequestBlocked.inc()\n throw new InvalidRequestError(`Address not allowed for eth_call`)\n }\n } else throw new InvalidRequestError(`RPC method eth_call is invalid`)\n }\n if (method === 'eth_getLogs' && (disallowEmptyAddressGetLogs || allowedLogsAddressMap[chainId])) {\n if (Array.isArray(params) && typeof params[0] === 'object') {\n const address = params[0].address\n if (disallowEmptyAddressGetLogs && (!address || (Array.isArray(address) && address.length === 0))) {\n rpcRequestBlocked.inc()\n throw new InvalidRequestError(`No empty address on eth_getLogs`)\n }\n const addresses = Array.isArray(address) ? address : [address]\n if (\n addresses.some(\n (eventAddress) =>\n // needs this check before toLowerCase\n typeof eventAddress !== 'string' || !allowedLogsAddressMap[chainId].has(eventAddress.toLowerCase()),\n )\n ) {\n rpcRequestBlocked.inc()\n throw new InvalidRequestError(`Address not allowed for eth_getLogs`)\n }\n } else throw new InvalidRequestError(`Invalid eth_getLogs`)\n }\n }\n\n const requested = await iterateUrls(\n providers[chainId],\n // TODO: consider adding verification that body is actually matches FetchRpcInitBody\n (url) => fetchRPC(url, { body: req.body as FetchRpcInitBody }, { chainId }),\n // eslint-disable-next-line @typescript-eslint/unbound-method\n console.error,\n )\n\n res.setHeader('Content-Type', requested.headers.get('Content-Type') ?? 'application/json')\n if (requested.body) {\n const sizeLimit = createSizeLogger(maxResponseSize)\n const readableStream = Readable.fromWeb(requested.body as ReadableStream)\n readableStream\n .pipe(sizeLimit)\n .on('error', (error) => {\n if (error instanceof SizeTooLargeError) {\n console.warn(`[rpcFactory] RPC response too large: ${JSON.stringify(requests)}`)\n res.status(413).end() // Payload Too Large\n } else {\n res.statusCode = 500\n res.end(DEFAULT_API_ERROR_MESSAGE)\n }\n readableStream.destroy()\n })\n .pipe(res)\n } else {\n res.status(requested.status).json('There are a problems with RPC provider')\n }\n } catch (error) {\n if (error instanceof ClientError) {\n res.status(400).json(error.message ?? DEFAULT_API_ERROR_MESSAGE)\n } else if (error instanceof Error) {\n // TODO: check if there are errors duplication with iterateUrls\n console.error(error.message ?? DEFAULT_API_ERROR_MESSAGE)\n res.status(500).json(error.message ?? DEFAULT_API_ERROR_MESSAGE)\n } else {\n res.status(500).json(HEALTHY_RPC_SERVICES_ARE_OVER)\n }\n }\n }\n}\n"],"names":[],"version":3,"file":"index.js.map","sourceRoot":"../../../../../"}
1
+ {"mappings":"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","sources":["packages/next/pages/src/api/index.ts","packages/next/pages/src/api/health.ts","packages/next/pages/src/api/metricsFactory.ts","packages/next/pages/src/api/rpcFactory/index.ts","packages/next/pages/src/api/rpcFactory/rpc-factory.ts","packages/next/pages/src/api/rpcFactory/errors.ts","packages/next/pages/src/api/rpcFactory/validation.ts"],"sourcesContent":["export { health } from './health'\nexport { type MetricsFactoryParameters, metricsFactory } from './metricsFactory'\nexport { type RPCFactoryParams, type RPCFactoryValidationParams, type RpcProviders, rpcFactory } from './rpcFactory'\n","import { NextApiRequest, NextApiResponse } from 'next'\n\nexport const health = (_req: NextApiRequest, res: NextApiResponse): void => {\n res.status(200).send({ status: 'ok' })\n}\n","import { NextApiRequest, NextApiResponse } from 'next'\nimport { Registry } from 'prom-client'\n\nexport type MetricsFactoryParameters = {\n registry: Registry\n}\n\nexport const metricsFactory =\n ({ registry }: MetricsFactoryParameters) =>\n async (_req: NextApiRequest, res: NextApiResponse) => {\n const collectedMetrics = await registry.metrics()\n res.send(collectedMetrics)\n }\n","export { rpcFactory } from './rpc-factory'\nexport type { RPCFactoryParams, RPCFactoryValidationParams, RpcProviders } from './types'\n","import { Readable } from 'node:stream'\nimport { ReadableStream } from 'node:stream/web'\nimport { pipeline } from 'node:stream/promises'\n\nimport { Counter } from 'prom-client'\nimport { iterateUrls } from '@lidofinance/rpc'\n\nimport { DEFAULT_API_ERROR_MESSAGE, HEALTHY_RPC_SERVICES_ARE_OVER, ClientError } from './errors'\nimport {\n baseRequestValidationFactory,\n ethCallValidationFactory,\n ethGetLogsValidationFactory,\n rpcMethodsValidationFactory,\n shouldValidateRpcMethod,\n streamMaxSizeValidationFactory,\n} from './validation'\n\nimport type { RPCFactoryParams } from './types'\nimport type { NextApiRequest, NextApiResponse } from 'next'\nimport type { FetchRpcInitBody } from '@lidofinance/rpc'\n\nexport const rpcFactory = ({ metrics, providers, fetchRPC, defaultChain, validation }: RPCFactoryParams) => {\n const {\n allowedRPCMethods,\n allowedCallAddresses,\n allowedLogsAddresses,\n maxResponseSize = 1_000_000,\n maxBatchCount = 20,\n currentBlockTTLms = 60_000,\n maxGetLogsRange = 20_000,\n blockEmptyAddressGetLogs = true,\n } = validation\n\n // optional metrics\n const { registry, prefix = '' } = metrics ?? {}\n const rpcRequestBlocked = new Counter({\n name: prefix + 'rpc_service_request_blocked',\n help: 'RPC service request blocked',\n labelNames: ['reason'],\n registers: [],\n })\n registry?.registerMetric(rpcRequestBlocked)\n\n const validateBaseRequest = baseRequestValidationFactory(defaultChain, providers, maxBatchCount)\n\n const validateRpcMethod = rpcMethodsValidationFactory(allowedRPCMethods)\n\n const validateEthCall =\n allowedCallAddresses && shouldValidateRpcMethod('eth_call', allowedRPCMethods)\n ? ethCallValidationFactory(allowedCallAddresses)\n : undefined\n\n const validateEthGetLogs =\n (allowedLogsAddresses || blockEmptyAddressGetLogs) && shouldValidateRpcMethod('eth_getLogs', allowedRPCMethods)\n ? ethGetLogsValidationFactory(allowedLogsAddresses, blockEmptyAddressGetLogs, maxGetLogsRange, currentBlockTTLms)\n : undefined\n\n const validateMaxSteamSize = streamMaxSizeValidationFactory(maxResponseSize)\n\n const requestRPC = (chainId: number, body: FetchRpcInitBody) =>\n iterateUrls(\n providers[chainId],\n // TODO: consider adding verification that body is actually matches FetchRpcInitBody\n (url) => fetchRPC(url, { body }, { chainId }),\n // eslint-disable-next-line @typescript-eslint/unbound-method\n console.error,\n )\n\n return async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {\n const abortController = new AbortController()\n try {\n const { chainId, requests } = validateBaseRequest(req)\n\n const validationContext = {\n chainId,\n rpcRequestBlocked,\n requestRPC,\n }\n\n // We throw HTTP error for ANY invalid RPC request out of batch\n // because we assume that frontend must not send invalid requests at all\n for (const request of requests) {\n validateRpcMethod(request, validationContext)\n\n const method = request.method\n\n if (method === 'eth_call' && validateEthCall) {\n validateEthCall(request, validationContext)\n }\n if (method === 'eth_getLogs' && validateEthGetLogs) {\n await validateEthGetLogs(request, validationContext)\n }\n }\n\n const proxyedRPC = await requestRPC(chainId, req.body as FetchRpcInitBody)\n\n res.setHeader('Content-Type', proxyedRPC.headers.get('Content-Type') ?? 'application/json')\n if (!proxyedRPC.body) {\n throw new Error('There are a problems with RPC provider')\n }\n\n // auto closes both Readable.fromWeb() and underlying proxyedRPC streams on error\n await pipeline(\n Readable.fromWeb(proxyedRPC.body as ReadableStream),\n validateMaxSteamSize(validationContext),\n res,\n {\n signal: abortController.signal,\n },\n )\n } catch (error) {\n if (error instanceof ClientError) {\n res.status(400).json(error.message ?? DEFAULT_API_ERROR_MESSAGE)\n } else if (error instanceof Error) {\n console.error('[rpcFactory]' + (error.message ?? DEFAULT_API_ERROR_MESSAGE))\n if (!res.headersSent) {\n res.status(500).json(error.message ?? DEFAULT_API_ERROR_MESSAGE)\n }\n } else {\n res.status(500).json(HEALTHY_RPC_SERVICES_ARE_OVER)\n }\n } finally {\n // forces pipeline closure in case of external error/abort\n abortController.abort()\n }\n }\n}\n","export const DEFAULT_API_ERROR_MESSAGE = 'Something went wrong. Sorry, try again later :('\n\nexport const HEALTHY_RPC_SERVICES_ARE_OVER = 'Healthy RPC services are over!'\n\nexport class ClientError extends Error {}\n\nexport class UnsupportedChainIdError extends ClientError {\n constructor(message?: string) {\n super(message || 'Unsupported chainId')\n }\n}\n\nexport class UnsupportedHTTPMethodError extends ClientError {\n constructor(message?: string) {\n super(message || 'Unsupported HTTP method')\n }\n}\n\nexport class InvalidRequestError extends ClientError {\n constructor(message?: string) {\n super(message || 'Invalid Request')\n }\n}\n\nexport class SizeTooLargeError extends ClientError {\n constructor(message?: string) {\n super(message || 'Response size too large')\n }\n}\n","import { Transform } from 'node:stream'\n\nimport { Cache } from 'memory-cache'\n\nimport {\n ClientError,\n InvalidRequestError,\n SizeTooLargeError,\n UnsupportedChainIdError,\n UnsupportedHTTPMethodError,\n} from './errors'\n\nimport type { NextApiRequest } from 'next'\nimport type { RpcRequest, ValidationContext } from './types'\n\n// validation factories\n\nconst zeroBN = BigInt(0)\n\nexport const baseRequestValidationFactory = (\n defaultChain: number | string,\n providers: Record<string, unknown>,\n maxBatchSize?: number,\n) => {\n return (req: NextApiRequest) => {\n if (req.method !== 'POST') {\n // We don't care about tracking blocked requests here\n throw new UnsupportedHTTPMethodError()\n }\n\n const chainId = Number(req.query.chainId || defaultChain)\n\n // Allow only chainId of specified chains\n if (providers[chainId] == null) {\n // We don't care about tracking blocked requests here\n throw new UnsupportedChainIdError()\n }\n\n const requests = Array.isArray(req.body) ? req.body : [req.body]\n\n if (typeof maxBatchSize === 'number' && requests.length > maxBatchSize) {\n throw new InvalidRequestError(`Too many batched requests`)\n }\n return { chainId, requests }\n }\n}\n\nexport const rpcMethodsValidationFactory = (allowedMethods?: string[]) => {\n const methodsMap = new Set(allowedMethods ?? [])\n\n return (request: RpcRequest, context: ValidationContext) => {\n const method = request.method\n if (typeof method !== 'string') {\n throw new InvalidRequestError(`RPC method isn't string`)\n }\n if (allowedMethods && !methodsMap.has(request.method)) {\n context.rpcRequestBlocked.inc({ reason: 'method not allowed' })\n throw new InvalidRequestError(`RPC method ${method} not allowed`)\n }\n }\n}\n\nexport const ethCallValidationFactory = (allowedAddress: Record<number, string[]>) => {\n const allowedCallAddressMap = createAllowedAddressMap(allowedAddress)\n\n return (request: RpcRequest, { chainId, rpcRequestBlocked }: ValidationContext) => {\n const { params } = request\n if (\n Array.isArray(params) &&\n params[0] &&\n typeof params[0] === 'object' &&\n 'to' in params[0] &&\n typeof params[0].to === 'string'\n ) {\n if (!allowedCallAddressMap[chainId]?.has(params[0].to.toLowerCase())) {\n rpcRequestBlocked.inc({ reason: 'address not allowed for eth_call' })\n throw new InvalidRequestError(`Address not allowed for eth_call`)\n }\n } else throw new InvalidRequestError(`RPC method eth_call is invalid`)\n }\n}\n\nexport const ethGetLogsValidationFactory = (\n allowedAddress: Record<number, string[]> | undefined,\n blockEmptyAddressGetLogs: boolean | undefined,\n maxBlockRange: number | undefined,\n currentBlockTTL = 60_000,\n) => {\n const allowedAddressMap = createAllowedAddressMap(allowedAddress ?? {})\n\n const maxBlockRangeBN = maxBlockRange ? BigInt(maxBlockRange) : zeroBN\n\n const currentBlock = new Cache<number, bigint>()\n\n return async (request: RpcRequest, { chainId, rpcRequestBlocked, requestRPC }: ValidationContext) => {\n try {\n const params = request.params[0] as any\n const fromBlock: string = params.fromBlock\n const toBlock: string = params.toBlock ?? 'latest'\n\n if (blockEmptyAddressGetLogs || allowedAddress) {\n // address validation\n const address: string | string[] = params.address\n\n if (blockEmptyAddressGetLogs && !address)\n throw new InvalidRequestError(`RPC method eth_getLogs address is invalid`)\n\n const addresses = Array.isArray(address) ? address : [address]\n\n if (blockEmptyAddressGetLogs && addresses.length === 0) {\n throw new InvalidRequestError(`RPC method eth_getLogs address is invalid`)\n }\n if (\n allowedAddress &&\n addresses.some(\n (eventAddress) =>\n // needs this check before toLowerCase\n typeof eventAddress !== 'string' || !allowedAddressMap[chainId]?.has(eventAddress.toLowerCase()),\n )\n ) {\n rpcRequestBlocked.inc({\n reason: 'address not allowed for eth_getLogs',\n })\n throw new InvalidRequestError(`Address not allowed for eth_getLogs`)\n }\n }\n\n // block range validation\n if (maxBlockRange) {\n if (fromBlock === 'earliest') throw new InvalidRequestError(`RPC method eth_getLogs fromBlock is invalid`)\n\n const shouldValidateBlockDistance = fromBlock.startsWith('0x')\n\n if (shouldValidateBlockDistance) {\n const normalizedFromBlock = BigInt(fromBlock)\n let normalizedToBlock: bigint\n if (toBlock.startsWith('0x')) normalizedToBlock = BigInt(toBlock)\n else {\n const cached = currentBlock.get(chainId)\n if (cached) {\n normalizedToBlock = cached\n } else {\n normalizedToBlock = await requestRPC(chainId, {\n id: 1,\n jsonrpc: '2.0',\n method: 'eth_blockNumber',\n })\n .then((res) => res.json())\n .then((res) => BigInt(res.result))\n currentBlock.put(chainId, normalizedToBlock, currentBlockTTL)\n }\n }\n const range = normalizedToBlock - normalizedFromBlock\n\n if (range < zeroBN || range > maxBlockRangeBN) {\n rpcRequestBlocked.inc({\n reason: 'eth_getLogs range is invalid',\n })\n throw new InvalidRequestError(`RPC method eth_getLogs range is invalid`)\n }\n }\n }\n } catch (error) {\n if (error instanceof ClientError) throw error\n throw new InvalidRequestError(`RPC method eth_getLogs is invalid`)\n }\n }\n}\n\nexport const streamMaxSizeValidationFactory = (MAX_SIZE: number) => (context: ValidationContext) => {\n let bytesWritten = 0\n return new Transform({\n transform(chunk, _encoding, callback) {\n bytesWritten += chunk.length\n if (bytesWritten > MAX_SIZE) {\n // Emit an error if size exceeds MAX_SIZE\n context.rpcRequestBlocked.inc({ reason: 'response too large' })\n return callback(new SizeTooLargeError(`Stream size exceeds the maximum limit of ${MAX_SIZE} bytes`))\n }\n return callback(null, chunk) // Pass the chunk through\n },\n flush(callback) {\n callback()\n },\n })\n}\n\n// utils\n\nexport const shouldValidateRpcMethod = (method: string, allowedRPCMethods?: string[]) =>\n !allowedRPCMethods || allowedRPCMethods.includes(method)\n\nconst createAllowedAddressMap = (allowedAddress: Record<number, string[]>) => {\n return Object.entries(allowedAddress).reduce((acc, [chainId, addresses]) => {\n acc[chainId] = new Set(addresses.map((a) => a.toLowerCase()))\n return acc\n }, {} as Record<string, Set<string>>)\n}\n"],"names":[],"version":3,"file":"index.js.map","sourceRoot":"../../../../../"}
@@ -0,0 +1,17 @@
1
+ export declare const DEFAULT_API_ERROR_MESSAGE = "Something went wrong. Sorry, try again later :(";
2
+ export declare const HEALTHY_RPC_SERVICES_ARE_OVER = "Healthy RPC services are over!";
3
+ export declare class ClientError extends Error {
4
+ }
5
+ export declare class UnsupportedChainIdError extends ClientError {
6
+ constructor(message?: string);
7
+ }
8
+ export declare class UnsupportedHTTPMethodError extends ClientError {
9
+ constructor(message?: string);
10
+ }
11
+ export declare class InvalidRequestError extends ClientError {
12
+ constructor(message?: string);
13
+ }
14
+ export declare class SizeTooLargeError extends ClientError {
15
+ constructor(message?: string);
16
+ }
17
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/api/rpcFactory/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,yBAAyB,oDAAoD,CAAA;AAE1F,eAAO,MAAM,6BAA6B,mCAAmC,CAAA;AAE7E,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC,qBAAa,uBAAwB,SAAQ,WAAW;gBAC1C,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,0BAA2B,SAAQ,WAAW;gBAC7C,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,mBAAoB,SAAQ,WAAW;gBACtC,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,iBAAkB,SAAQ,WAAW;gBACpC,OAAO,CAAC,EAAE,MAAM;CAG7B"}
@@ -0,0 +1,3 @@
1
+ export { rpcFactory } from './rpc-factory';
2
+ export type { RPCFactoryParams, RPCFactoryValidationParams, RpcProviders } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/rpcFactory/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,YAAY,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { RPCFactoryParams } from './types';
2
+ import type { NextApiRequest, NextApiResponse } from 'next';
3
+ export declare const rpcFactory: ({ metrics, providers, fetchRPC, defaultChain, validation }: RPCFactoryParams) => (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
4
+ //# sourceMappingURL=rpc-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-factory.d.ts","sourceRoot":"","sources":["../../../src/api/rpcFactory/rpc-factory.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAG3D,eAAO,MAAM,UAAU,+DAAgE,gBAAgB,WA+ClF,cAAc,OAAO,eAAe,KAAG,QAAQ,IAAI,CA0DvE,CAAA"}
@@ -0,0 +1,34 @@
1
+ import type { TrackedFetchRPC } from '@lidofinance/api-rpc';
2
+ import type { FetchRpcInitBody } from '@lidofinance/rpc';
3
+ import type { Counter, Registry } from 'prom-client';
4
+ export type RpcProviders = Record<string | number, [string, ...string[]]>;
5
+ export type RPCFactoryValidationParams = {
6
+ maxBatchCount?: number;
7
+ maxResponseSize?: number;
8
+ allowedRPCMethods?: string[];
9
+ allowedCallAddresses?: Record<number, string[]>;
10
+ allowedLogsAddresses?: Record<number, string[]>;
11
+ blockEmptyAddressGetLogs?: boolean;
12
+ maxGetLogsRange?: number;
13
+ currentBlockTTLms?: number;
14
+ };
15
+ export type RPCFactoryParams = {
16
+ metrics?: {
17
+ prefix: string;
18
+ registry: Registry;
19
+ };
20
+ providers: RpcProviders;
21
+ fetchRPC: TrackedFetchRPC;
22
+ defaultChain: string | number;
23
+ validation: RPCFactoryValidationParams;
24
+ };
25
+ export type RpcRequest = {
26
+ method: string;
27
+ params: unknown[];
28
+ };
29
+ export type ValidationContext = {
30
+ chainId: number;
31
+ rpcRequestBlocked: Counter<'reason'>;
32
+ requestRPC: (chainId: number, body: FetchRpcInitBody) => Promise<Response>;
33
+ };
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/api/rpcFactory/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEpD,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC,CAAA;AAEzE,MAAM,MAAM,0BAA0B,GAAG;IACvC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IAC/C,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IAC/C,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE;QACR,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,QAAQ,CAAA;KACnB,CAAA;IACD,SAAS,EAAE,YAAY,CAAA;IACvB,QAAQ,EAAE,eAAe,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;IAC7B,UAAU,EAAE,0BAA0B,CAAA;CACvC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,OAAO,EAAE,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,iBAAiB,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpC,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3E,CAAA"}
@@ -0,0 +1,14 @@
1
+ /// <reference types="node" />
2
+ import { Transform } from 'node:stream';
3
+ import type { NextApiRequest } from 'next';
4
+ import type { RpcRequest, ValidationContext } from './types';
5
+ export declare const baseRequestValidationFactory: (defaultChain: number | string, providers: Record<string, unknown>, maxBatchSize?: number) => (req: NextApiRequest) => {
6
+ chainId: number;
7
+ requests: any[];
8
+ };
9
+ export declare const rpcMethodsValidationFactory: (allowedMethods?: string[]) => (request: RpcRequest, context: ValidationContext) => void;
10
+ export declare const ethCallValidationFactory: (allowedAddress: Record<number, string[]>) => (request: RpcRequest, { chainId, rpcRequestBlocked }: ValidationContext) => void;
11
+ export declare const ethGetLogsValidationFactory: (allowedAddress: Record<number, string[]> | undefined, blockEmptyAddressGetLogs: boolean | undefined, maxBlockRange: number | undefined, currentBlockTTL?: number) => (request: RpcRequest, { chainId, rpcRequestBlocked, requestRPC }: ValidationContext) => Promise<void>;
12
+ export declare const streamMaxSizeValidationFactory: (MAX_SIZE: number) => (context: ValidationContext) => Transform;
13
+ export declare const shouldValidateRpcMethod: (method: string, allowedRPCMethods?: string[]) => boolean;
14
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/api/rpcFactory/validation.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAYvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAM5D,eAAO,MAAM,4BAA4B,iBACzB,MAAM,GAAG,MAAM,aAClB,OAAO,MAAM,EAAE,OAAO,CAAC,iBACnB,MAAM,WAER,cAAc;;;CAqB5B,CAAA;AAED,eAAO,MAAM,2BAA2B,oBAAqB,MAAM,EAAE,eAGlD,UAAU,WAAW,iBAAiB,SAUxD,CAAA;AAED,eAAO,MAAM,wBAAwB,mBAAoB,OAAO,MAAM,EAAE,MAAM,EAAE,CAAC,eAG9D,UAAU,kCAAkC,iBAAiB,SAe/E,CAAA;AAED,eAAO,MAAM,2BAA2B,mBACtB,OAAO,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,4BAC1B,OAAO,GAAG,SAAS,iBAC9B,MAAM,GAAG,SAAS,yCASV,UAAU,8CAA8C,iBAAiB,kBAyEjG,CAAA;AAED,eAAO,MAAM,8BAA8B,aAAc,MAAM,eAAe,iBAAiB,cAgB9F,CAAA;AAID,eAAO,MAAM,uBAAuB,WAAY,MAAM,sBAAsB,MAAM,EAAE,YAC1B,CAAA"}
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/lidofinance/warehouse/issues"
8
8
  },
9
9
  "license": "MIT",
10
- "version": "0.46.0",
10
+ "version": "0.48.0",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist"
@@ -58,22 +58,26 @@
58
58
  }
59
59
  },
60
60
  "peerDependencies": {
61
- "@lidofinance/api-logger": "~0.46.0",
62
- "@lidofinance/api-rpc": "~0.46.0",
63
- "@lidofinance/rpc": "~0.46.0",
64
- "@lidofinance/ui-pages": "~0.46.0",
61
+ "@lidofinance/api-logger": "~0.48.0",
62
+ "@lidofinance/api-rpc": "~0.48.0",
63
+ "@lidofinance/rpc": "~0.48.0",
64
+ "@lidofinance/ui-pages": "~0.48.0",
65
65
  "next": "12 || 13",
66
66
  "prom-client": "^14.1.0",
67
67
  "react": "17 || 18"
68
68
  },
69
69
  "devDependencies": {
70
- "@lidofinance/api-logger": "~0.46.0",
71
- "@lidofinance/api-rpc": "~0.46.0",
72
- "@lidofinance/config-prettier": "~0.46.0",
73
- "@lidofinance/rpc": "~0.46.0",
74
- "@lidofinance/ui-pages": "~0.46.0",
70
+ "@lidofinance/api-logger": "~0.48.0",
71
+ "@lidofinance/api-rpc": "~0.48.0",
72
+ "@lidofinance/config-prettier": "~0.48.0",
73
+ "@lidofinance/rpc": "~0.48.0",
74
+ "@lidofinance/ui-pages": "~0.48.0",
75
+ "@types/memory-cache": "^0.2.6",
75
76
  "@types/react": "^18.0.25",
76
77
  "next": "^12.3.1",
77
78
  "prom-client": "^14.1.0"
79
+ },
80
+ "dependencies": {
81
+ "memory-cache": "^0.2.0"
78
82
  }
79
83
  }
@@ -1,37 +0,0 @@
1
- import type { NextApiRequest, NextApiResponse } from 'next';
2
- import { Registry } from 'prom-client';
3
- import type { TrackedFetchRPC } from '@lidofinance/api-rpc';
4
- export type RpcProviders = Record<string | number, [string, ...string[]]>;
5
- export declare const DEFAULT_API_ERROR_MESSAGE = "Something went wrong. Sorry, try again later :(";
6
- export declare const HEALTHY_RPC_SERVICES_ARE_OVER = "Healthy RPC services are over!";
7
- export declare class ClientError extends Error {
8
- }
9
- export declare class UnsupportedChainIdError extends ClientError {
10
- constructor(message?: string);
11
- }
12
- export declare class UnsupportedHTTPMethodError extends ClientError {
13
- constructor(message?: string);
14
- }
15
- export declare class InvalidRequestError extends ClientError {
16
- constructor(message?: string);
17
- }
18
- export declare class SizeTooLargeError extends ClientError {
19
- constructor(message?: string);
20
- }
21
- export type RPCFactoryParams = {
22
- metrics: {
23
- prefix: string;
24
- registry: Registry;
25
- };
26
- providers: RpcProviders;
27
- fetchRPC: TrackedFetchRPC;
28
- defaultChain: string | number;
29
- allowedRPCMethods: string[];
30
- allowedCallAddresses?: Record<number, string[]>;
31
- allowedLogsAddresses?: Record<number, string[]>;
32
- disallowEmptyAddressGetLogs?: boolean;
33
- maxBatchCount?: number;
34
- maxResponseSize?: number;
35
- };
36
- export declare const rpcFactory: ({ metrics: { prefix, registry }, providers, fetchRPC, defaultChain, allowedRPCMethods, allowedCallAddresses, allowedLogsAddresses, maxBatchCount, maxResponseSize, disallowEmptyAddressGetLogs, }: RPCFactoryParams) => (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
37
- //# sourceMappingURL=rpcFactory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rpcFactory.d.ts","sourceRoot":"","sources":["../../src/api/rpcFactory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAC3D,OAAO,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAI3D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC,CAAA;AAEzE,eAAO,MAAM,yBAAyB,oDAAoD,CAAA;AAE1F,eAAO,MAAM,6BAA6B,mCAAmC,CAAA;AAE7E,qBAAa,WAAY,SAAQ,KAAK;CAAG;AACzC,qBAAa,uBAAwB,SAAQ,WAAW;gBAC1C,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,0BAA2B,SAAQ,WAAW;gBAC7C,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,mBAAoB,SAAQ,WAAW;gBACtC,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,iBAAkB,SAAQ,WAAW;gBACpC,OAAO,CAAC,EAAE,MAAM;CAG7B;AAoBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,QAAQ,CAAA;KACnB,CAAA;IACD,SAAS,EAAE,YAAY,CAAA;IACvB,QAAQ,EAAE,eAAe,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;IAI7B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAE3B,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IAC/C,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IAC/C,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,eAAO,MAAM,UAAU,sMAWpB,gBAAgB,WAmBE,cAAc,OAAO,eAAe,KAAG,QAAQ,IAAI,CAuGvE,CAAA"}