@lidofinance/next-pages 0.45.1 → 0.46.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/dist/api/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import {Readable as $FgOCV$Readable} from "node:stream";
1
+ import {Transform as $FgOCV$Transform, Readable as $FgOCV$Readable} from "node:stream";
2
2
  import {Counter as $FgOCV$Counter} from "prom-client";
3
3
  import {iterateUrls as $FgOCV$iterateUrls} from "@lidofinance/rpc";
4
4
 
@@ -20,17 +20,45 @@ const $29c7c52f2735c037$export$fc20b1372f40c2c5 = ({ registry: registry })=>asyn
20
20
 
21
21
  const $7d34ac0315ec4c05$export$c982278a0ddc4dbe = "Something went wrong. Sorry, try again later :(";
22
22
  const $7d34ac0315ec4c05$export$56a189db5508cdcb = "Healthy RPC services are over!";
23
- class $7d34ac0315ec4c05$export$fc97cac2ba066ec7 extends Error {
23
+ class $7d34ac0315ec4c05$export$bb8c263ad32caa8 extends Error {
24
+ }
25
+ class $7d34ac0315ec4c05$export$fc97cac2ba066ec7 extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
24
26
  constructor(message){
25
27
  super(message || "Unsupported chainId");
26
28
  }
27
29
  }
28
- class $7d34ac0315ec4c05$export$2dfa2d8949b43fdc extends Error {
30
+ class $7d34ac0315ec4c05$export$2dfa2d8949b43fdc extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
29
31
  constructor(message){
30
32
  super(message || "Unsupported HTTP method");
31
33
  }
32
34
  }
33
- const $7d34ac0315ec4c05$export$9bfcc39f106ad871 = ({ metrics: { prefix: prefix, registry: registry }, providers: providers, fetchRPC: fetchRPC, serverLogger: serverLogger = console, defaultChain: defaultChain, allowedRPCMethods: allowedRPCMethods })=>{
35
+ class $7d34ac0315ec4c05$export$2adbfa76b71ba748 extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
36
+ constructor(message){
37
+ super(message || "Invalid Request");
38
+ }
39
+ }
40
+ class $7d34ac0315ec4c05$export$e224c1080d950012 extends $7d34ac0315ec4c05$export$bb8c263ad32caa8 {
41
+ constructor(message){
42
+ super(message || "Invalid Request");
43
+ }
44
+ }
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();
57
+ }
58
+ });
59
+ return logSizeStream;
60
+ };
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 })=>{
34
62
  const rpcRequestBlocked = new (0, $FgOCV$Counter)({
35
63
  name: prefix + "rpc_service_request_blocked",
36
64
  help: "RPC service request blocked",
@@ -38,6 +66,14 @@ const $7d34ac0315ec4c05$export$9bfcc39f106ad871 = ({ metrics: { prefix: prefix,
38
66
  registers: []
39
67
  });
40
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
+ }, {});
41
77
  return async (req, res)=>{
42
78
  try {
43
79
  // Accept only POST requests
@@ -47,15 +83,43 @@ const $7d34ac0315ec4c05$export$9bfcc39f106ad871 = ({ metrics: { prefix: prefix,
47
83
  // Allow only chainId of specified chains
48
84
  if (providers[chainId] == null) // We don't care about tracking blocked requests here
49
85
  throw new $7d34ac0315ec4c05$export$fc97cac2ba066ec7();
50
- // TODO: consider returning array of validators instead of throwing error right away
51
- // Check if provided methods are allowed
52
- for (const { method: method } of Array.isArray(req.body) ? req.body : [
86
+ const requests = Array.isArray(req.body) ? req.body : [
53
87
  req.body
54
- ]){
55
- if (typeof method !== "string") throw new Error(`RPC method isn't string`);
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
91
+ // 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`);
56
95
  if (!allowedRPCMethods.includes(method)) {
57
96
  rpcRequestBlocked.inc();
58
- throw new Error(`RPC method ${method} isn't allowed`);
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`);
59
123
  }
60
124
  }
61
125
  const requested = await (0, $FgOCV$iterateUrls)(providers[chainId], // TODO: consider adding verification that body is actually matches FetchRpcInitBody
@@ -63,14 +127,29 @@ const $7d34ac0315ec4c05$export$9bfcc39f106ad871 = ({ metrics: { prefix: prefix,
63
127
  body: req.body
64
128
  }, {
65
129
  chainId: chainId
66
- }), serverLogger.error);
130
+ }), // eslint-disable-next-line @typescript-eslint/unbound-method
131
+ console.error);
67
132
  res.setHeader("Content-Type", requested.headers.get("Content-Type") ?? "application/json");
68
- if (requested.body) (0, $FgOCV$Readable).fromWeb(requested.body).pipe(res);
69
- else res.status(requested.status).json("There are a problems with RPC provider");
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");
70
148
  } catch (error) {
71
- if (error instanceof Error) {
149
+ if (error instanceof $7d34ac0315ec4c05$export$bb8c263ad32caa8) res.status(400).json(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
150
+ else if (error instanceof Error) {
72
151
  // TODO: check if there are errors duplication with iterateUrls
73
- serverLogger.error(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
152
+ console.error(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
74
153
  res.status(500).json(error.message ?? $7d34ac0315ec4c05$export$c982278a0ddc4dbe);
75
154
  } else res.status(500).json($7d34ac0315ec4c05$export$56a189db5508cdcb);
76
155
  }
@@ -1 +1 @@
1
- {"mappings":";;;;ACEO,MAAM,4CAAS,CAAC,MAAsB;IAC3C,IAAI,OAAO,KAAK,KAAK;QAAE,QAAQ;IAAK;AACtC;;;ACGO,MAAM,4CACX,CAAC,YAAE,QAAQ,EAA4B,GACvC,OAAO,MAAsB;QAC3B,MAAM,mBAAmB,MAAM,SAAS;QACxC,IAAI,KAAK;IACX;;;;;;ACDK,MAAM,4CAA4B;AAElC,MAAM,4CAAgC;AAEtC,MAAM,kDAAgC;IAC3C,YAAY,OAAgB,CAAE;QAC5B,KAAK,CAAC,WAAW;IACnB;AACF;AAEO,MAAM,kDAAmC;IAC9C,YAAY,OAAgB,CAAE;QAC5B,KAAK,CAAC,WAAW;IACnB;AACF;AAqBO,MAAM,4CAAa,CAAC,EACzB,SAAS,UAAE,MAAM,YAAE,QAAQ,EAAE,aAC7B,SAAS,YACT,QAAQ,gBACR,eAAe,uBACf,YAAY,qBACZ,iBAAiB,EACA;IACjB,MAAM,oBAAoB,IAAI,CAAA,GAAA,cAAM,EAAE;QACpC,MAAM,SAAS;QACf,MAAM;QACN,YAAY,EAAE;QACd,WAAW,EAAE;IACf;IACA,SAAS,eAAe;IAExB,OAAO,OAAO,KAAqB;QACjC,IAAI;YACF,4BAA4B;YAC5B,IAAI,IAAI,WAAW,QACjB,qDAAqD;YACrD,MAAM,IAAI;YAGZ,MAAM,UAAU,OAAO,IAAI,MAAM,WAAW;YAE5C,yCAAyC;YACzC,IAAI,SAAS,CAAC,QAAQ,IAAI,MACxB,qDAAqD;YACrD,MAAM,IAAI;YAGZ,oFAAoF;YACpF,wCAAwC;YACxC,KAAK,MAAM,UAAE,MAAM,EAAE,IAAI,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAO;gBAAC,IAAI;aAAK,CAAE;gBACxE,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,MAAM,CAAC,uBAAuB,CAAC;gBAE3C,IAAI,CAAC,kBAAkB,SAAS,SAAS;oBACvC,kBAAkB;oBAClB,MAAM,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,cAAc,CAAC;gBACtD;YACF;YAEA,MAAM,YAAY,MAAM,CAAA,GAAA,kBAAU,EAChC,SAAS,CAAC,QAAQ,EAClB,oFAAoF;YACpF,CAAC,MAAQ,SAAS,KAAK;oBAAE,MAAM,IAAI;gBAAyB,GAAG;6BAAE;gBAAQ,IACzE,aAAa;YAGf,IAAI,UAAU,gBAAgB,UAAU,QAAQ,IAAI,mBAAmB;YACvE,IAAI,UAAU,MACZ,CAAA,GAAA,eAAO,EAAE,QAAQ,UAAU,MAAwB,KAAK;iBAExD,IAAI,OAAO,UAAU,QAAQ,KAAK;QAEtC,EAAE,OAAO,OAAO;YACd,IAAI,iBAAiB,OAAO;gBAC1B,+DAA+D;gBAC/D,aAAa,MAAM,MAAM,WAAW;gBACpC,IAAI,OAAO,KAAK,KAAK,MAAM,WAAW;YACxC,OACE,IAAI,OAAO,KAAK,KAAK;QAEzB;IACF;AACF;","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 } 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 { ServerLogger } from '@lidofinance/api-logger'\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 UnsupportedChainIdError extends Error {\n constructor(message?: string) {\n super(message || 'Unsupported chainId')\n }\n}\n\nexport class UnsupportedHTTPMethodError extends Error {\n constructor(message?: string) {\n super(message || 'Unsupported HTTP method')\n }\n}\n\nexport type RPCFactoryParams = {\n metrics: {\n prefix: string\n registry: Registry\n }\n providers: RpcProviders\n fetchRPC: TrackedFetchRPC\n /**\n * @deprecated you should mask logs via pino & satanizer, see policies\n * for additional details or reach out ui/secops\n */\n serverLogger?: ServerLogger\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}\n\nexport const rpcFactory = ({\n metrics: { prefix, registry },\n providers,\n fetchRPC,\n serverLogger = console,\n defaultChain,\n allowedRPCMethods,\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 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 // TODO: consider returning array of validators instead of throwing error right away\n // Check if provided methods are allowed\n for (const { method } of Array.isArray(req.body) ? req.body : [req.body]) {\n if (typeof method !== 'string') {\n throw new Error(`RPC method isn't string`)\n }\n if (!allowedRPCMethods.includes(method)) {\n rpcRequestBlocked.inc()\n throw new Error(`RPC method ${method} isn't allowed`)\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 serverLogger.error,\n )\n\n res.setHeader('Content-Type', requested.headers.get('Content-Type') ?? 'application/json')\n if (requested.body) {\n Readable.fromWeb(requested.body as ReadableStream).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 Error) {\n // TODO: check if there are errors duplication with iterateUrls\n serverLogger.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.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,14 +1,21 @@
1
1
  import type { NextApiRequest, NextApiResponse } from 'next';
2
2
  import { Registry } from 'prom-client';
3
3
  import type { TrackedFetchRPC } from '@lidofinance/api-rpc';
4
- import type { ServerLogger } from '@lidofinance/api-logger';
5
4
  export type RpcProviders = Record<string | number, [string, ...string[]]>;
6
5
  export declare const DEFAULT_API_ERROR_MESSAGE = "Something went wrong. Sorry, try again later :(";
7
6
  export declare const HEALTHY_RPC_SERVICES_ARE_OVER = "Healthy RPC services are over!";
8
- export declare class UnsupportedChainIdError extends Error {
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 {
9
16
  constructor(message?: string);
10
17
  }
11
- export declare class UnsupportedHTTPMethodError extends Error {
18
+ export declare class SizeTooLargeError extends ClientError {
12
19
  constructor(message?: string);
13
20
  }
14
21
  export type RPCFactoryParams = {
@@ -18,13 +25,13 @@ export type RPCFactoryParams = {
18
25
  };
19
26
  providers: RpcProviders;
20
27
  fetchRPC: TrackedFetchRPC;
21
- /**
22
- * @deprecated you should mask logs via pino & satanizer, see policies
23
- * for additional details or reach out ui/secops
24
- */
25
- serverLogger?: ServerLogger;
26
28
  defaultChain: string | number;
27
29
  allowedRPCMethods: string[];
30
+ allowedCallAddresses?: Record<number, string[]>;
31
+ allowedLogsAddresses?: Record<number, string[]>;
32
+ disallowEmptyAddressGetLogs?: boolean;
33
+ maxBatchCount?: number;
34
+ maxResponseSize?: number;
28
35
  };
29
- export declare const rpcFactory: ({ metrics: { prefix, registry }, providers, fetchRPC, serverLogger, defaultChain, allowedRPCMethods, }: RPCFactoryParams) => (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
36
+ export declare const rpcFactory: ({ metrics: { prefix, registry }, providers, fetchRPC, defaultChain, allowedRPCMethods, allowedCallAddresses, allowedLogsAddresses, maxBatchCount, maxResponseSize, disallowEmptyAddressGetLogs, }: RPCFactoryParams) => (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
30
37
  //# sourceMappingURL=rpcFactory.d.ts.map
@@ -1 +1 @@
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;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,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,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,qBAAa,0BAA2B,SAAQ,KAAK;gBACvC,OAAO,CAAC,EAAE,MAAM;CAG7B;AAED,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;;;OAGG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;IAI7B,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAED,eAAO,MAAM,UAAU,2GAOpB,gBAAgB,WASE,cAAc,OAAO,eAAe,KAAG,QAAQ,IAAI,CAmDvE,CAAA"}
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"}
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.45.1",
10
+ "version": "0.46.0",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist"
@@ -58,20 +58,20 @@
58
58
  }
59
59
  },
60
60
  "peerDependencies": {
61
- "@lidofinance/api-logger": "~0.45.1",
62
- "@lidofinance/api-rpc": "~0.45.1",
63
- "@lidofinance/rpc": "~0.45.1",
64
- "@lidofinance/ui-pages": "~0.45.1",
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",
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.45.1",
71
- "@lidofinance/api-rpc": "~0.45.1",
72
- "@lidofinance/config-prettier": "~0.45.1",
73
- "@lidofinance/rpc": "~0.45.1",
74
- "@lidofinance/ui-pages": "~0.45.1",
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",
75
75
  "@types/react": "^18.0.25",
76
76
  "next": "^12.3.1",
77
77
  "prom-client": "^14.1.0"