mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-01-02 09:36:47 +00:00
Add ETH RPC API to watcher server (#535)
* Add ETH RPC API to watcher server * Add eth_call API handler * Add error handling to eth_call handler * Parse block tag in eth_call handler * Add a flag to enable ETH RPC server * Fix lint errors * Update block tag parsing
This commit is contained in:
parent
ea5ff93e21
commit
b46d8816b5
@ -32,7 +32,8 @@ import {
|
||||
readParty,
|
||||
UpstreamConfig,
|
||||
fillBlocks,
|
||||
createGQLLogger
|
||||
createGQLLogger,
|
||||
createEthRPCHandlers
|
||||
} from '@cerc-io/util';
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import type {
|
||||
@ -285,6 +286,7 @@ export class ServerCmd {
|
||||
const jobQueue = this._baseCmd.jobQueue;
|
||||
const indexer = this._baseCmd.indexer;
|
||||
const eventWatcher = this._baseCmd.eventWatcher;
|
||||
const ethProvider = this._baseCmd.ethProvider;
|
||||
|
||||
assert(config);
|
||||
assert(jobQueue);
|
||||
@ -317,9 +319,11 @@ export class ServerCmd {
|
||||
const gqlLogger = createGQLLogger(config.server.gql.logDir);
|
||||
const resolvers = await createResolvers(indexer, eventWatcher, gqlLogger);
|
||||
|
||||
const ethRPCHandlers = await createEthRPCHandlers(indexer, ethProvider);
|
||||
|
||||
// Create an Express app
|
||||
const app: Application = express();
|
||||
const server = await createAndStartServer(app, typeDefs, resolvers, config.server, paymentsManager);
|
||||
const server = await createAndStartServer(app, typeDefs, resolvers, ethRPCHandlers, config.server, paymentsManager);
|
||||
|
||||
await startGQLMetricsServer(config);
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import { DeepPartial, FindConditions, FindManyOptions } from 'typeorm';
|
||||
import { providers } from 'ethers';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import {
|
||||
IndexerInterface,
|
||||
@ -28,6 +28,8 @@ import { GetStorageAt, getStorageValue, MappingKey, StorageLayout } from '@cerc-
|
||||
export class Indexer implements IndexerInterface {
|
||||
_getStorageAt: GetStorageAt;
|
||||
_storageLayoutMap: Map<string, StorageLayout> = new Map();
|
||||
_contractMap: Map<string, ethers.utils.Interface> = new Map();
|
||||
|
||||
eventSignaturesMap: Map<string, string[]> = new Map();
|
||||
|
||||
constructor (ethClient: EthClient, storageLayoutMap?: Map<string, StorageLayout>) {
|
||||
@ -50,6 +52,10 @@ export class Indexer implements IndexerInterface {
|
||||
return this._storageLayoutMap;
|
||||
}
|
||||
|
||||
get contractMap (): Map<string, ethers.utils.Interface> {
|
||||
return this._contractMap;
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
"it-length-prefixed": "^8.0.4",
|
||||
"it-pipe": "^2.0.5",
|
||||
"it-pushable": "^3.1.2",
|
||||
"jayson": "^4.1.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -252,6 +252,9 @@ export interface ServerConfig {
|
||||
// Flag to specify whether RPC endpoint supports block hash as block tag parameter
|
||||
// https://ethereum.org/en/developers/docs/apis/json-rpc/#default-block
|
||||
rpcSupportsBlockHashParam: boolean;
|
||||
|
||||
// Enable ETH JSON RPC server at /rpc
|
||||
enableEthRPCServer: boolean;
|
||||
}
|
||||
|
||||
export interface FundingAmountsConfig {
|
||||
|
134
packages/util/src/eth-rpc-handlers.ts
Normal file
134
packages/util/src/eth-rpc-handlers.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { utils } from 'ethers';
|
||||
|
||||
import { JsonRpcProvider } from '@ethersproject/providers';
|
||||
|
||||
import { IndexerInterface } from './types';
|
||||
|
||||
const CODE_INVALID_PARAMS = -32602;
|
||||
const CODE_INTERNAL_ERROR = -32603;
|
||||
const CODE_SERVER_ERROR = -32000;
|
||||
|
||||
const ERROR_CONTRACT_MAP_NOT_SET = 'Contract map not set';
|
||||
const ERROR_CONTRACT_ABI_NOT_FOUND = 'Contract ABI not found';
|
||||
const ERROR_CONTRACT_INSUFFICIENT_PARAMS = 'Insufficient params';
|
||||
const ERROR_CONTRACT_NOT_RECOGNIZED = 'Contract not recognized';
|
||||
const ERROR_CONTRACT_METHOD_NOT_FOUND = 'Contract method not found';
|
||||
const ERROR_METHOD_NOT_IMPLEMENTED = 'Method not implemented';
|
||||
const ERROR_INVALID_BLOCK_TAG = 'Invalid block tag';
|
||||
const ERROR_BLOCK_NOT_FOUND = 'Block not found';
|
||||
|
||||
const DEFAULT_BLOCK_TAG = 'latest';
|
||||
|
||||
class ErrorWithCode extends Error {
|
||||
code: number;
|
||||
constructor (code: number, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export const createEthRPCHandlers = async (
|
||||
indexer: IndexerInterface,
|
||||
ethProvider: JsonRpcProvider
|
||||
): Promise<any> => {
|
||||
return {
|
||||
eth_blockNumber: async (args: any, callback: any) => {
|
||||
const syncStatus = await indexer.getSyncStatus();
|
||||
const result = syncStatus ? `0x${syncStatus.latestProcessedBlockNumber.toString(16)}` : '0x';
|
||||
|
||||
callback(null, result);
|
||||
},
|
||||
|
||||
eth_call: async (args: any, callback: any) => {
|
||||
try {
|
||||
if (!indexer.contractMap) {
|
||||
throw new ErrorWithCode(CODE_INTERNAL_ERROR, ERROR_CONTRACT_MAP_NOT_SET);
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_INSUFFICIENT_PARAMS);
|
||||
}
|
||||
|
||||
const { to, data } = args[0];
|
||||
const blockTag = args.length > 1 ? args[1] : DEFAULT_BLOCK_TAG;
|
||||
|
||||
const blockHash = await parseBlockTag(indexer, ethProvider, blockTag);
|
||||
|
||||
const watchedContract = indexer.getWatchedContracts().find(contract => contract.address === to);
|
||||
if (!watchedContract) {
|
||||
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_NOT_RECOGNIZED);
|
||||
}
|
||||
|
||||
const contractInterface = indexer.contractMap.get(watchedContract.kind);
|
||||
if (!contractInterface) {
|
||||
throw new ErrorWithCode(CODE_INTERNAL_ERROR, ERROR_CONTRACT_ABI_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Slice out method signature from data
|
||||
const functionSelector = data.slice(0, 10);
|
||||
|
||||
// Find the matching function from the ABI
|
||||
const functionFragment = contractInterface.getFunction(functionSelector);
|
||||
if (!functionFragment) {
|
||||
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_CONTRACT_METHOD_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Decode the data based on the matched function
|
||||
const decodedData = contractInterface.decodeFunctionData(functionFragment, data);
|
||||
|
||||
const functionName = functionFragment.name;
|
||||
const indexerMethod = (indexer as any)[functionName].bind(indexer);
|
||||
if (!indexerMethod) {
|
||||
throw new ErrorWithCode(CODE_SERVER_ERROR, ERROR_METHOD_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
const result = await indexerMethod(blockHash, to, ...decodedData);
|
||||
const encodedResult = contractInterface.encodeFunctionResult(functionFragment, Array.isArray(result.value) ? result.value : [result.value]);
|
||||
|
||||
callback(null, encodedResult);
|
||||
} catch (error: any) {
|
||||
let callBackError;
|
||||
if (error instanceof ErrorWithCode) {
|
||||
callBackError = { code: error.code, message: error.message };
|
||||
} else {
|
||||
callBackError = { code: CODE_SERVER_ERROR, message: error.message };
|
||||
}
|
||||
|
||||
callback(callBackError);
|
||||
}
|
||||
},
|
||||
|
||||
eth_getLogs: async (args: any, callback: any) => {
|
||||
// TODO: Implement
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const parseBlockTag = async (indexer: IndexerInterface, ethProvider: JsonRpcProvider, blockTag: string): Promise<string> => {
|
||||
if (utils.isHexString(blockTag)) {
|
||||
// Return value if hex string is of block hash length
|
||||
if (utils.hexDataLength(blockTag) === 32) {
|
||||
return blockTag;
|
||||
}
|
||||
|
||||
// Treat hex value as a block number
|
||||
const block = await ethProvider.getBlock(blockTag);
|
||||
if (block === null) {
|
||||
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_BLOCK_NOT_FOUND);
|
||||
}
|
||||
|
||||
return block.hash;
|
||||
}
|
||||
|
||||
if (blockTag === DEFAULT_BLOCK_TAG) {
|
||||
const syncStatus = await indexer.getSyncStatus();
|
||||
if (!syncStatus) {
|
||||
throw new ErrorWithCode(CODE_INTERNAL_ERROR, 'SyncStatus not found');
|
||||
}
|
||||
|
||||
return syncStatus.latestProcessedBlockHash;
|
||||
}
|
||||
|
||||
throw new ErrorWithCode(CODE_INVALID_PARAMS, ERROR_INVALID_BLOCK_TAG);
|
||||
};
|
@ -28,3 +28,4 @@ export * from './eth';
|
||||
export * from './consensus';
|
||||
export * from './validate-config';
|
||||
export * from './logger';
|
||||
export * from './eth-rpc-handlers';
|
||||
|
@ -11,6 +11,8 @@ import debug from 'debug';
|
||||
import responseCachePlugin from 'apollo-server-plugin-response-cache';
|
||||
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
|
||||
import queue from 'express-queue';
|
||||
import jayson from 'jayson';
|
||||
import { json as jsonParser } from 'body-parser';
|
||||
|
||||
import { TypeSource } from '@graphql-tools/utils';
|
||||
import { makeExecutableSchema } from '@graphql-tools/schema';
|
||||
@ -22,11 +24,13 @@ import { PaymentsManager, paymentsPlugin } from './payments';
|
||||
const log = debug('vulcanize:server');
|
||||
|
||||
const DEFAULT_GQL_PATH = '/graphql';
|
||||
const ETH_RPC_PATH = '/rpc';
|
||||
|
||||
export const createAndStartServer = async (
|
||||
app: Application,
|
||||
typeDefs: TypeSource,
|
||||
resolvers: any,
|
||||
ethRPCHandlers: any,
|
||||
serverConfig: ServerConfig,
|
||||
paymentsManager?: PaymentsManager
|
||||
): Promise<ApolloServer> => {
|
||||
@ -98,8 +102,25 @@ export const createAndStartServer = async (
|
||||
path: gqlPath
|
||||
});
|
||||
|
||||
if (serverConfig.enableEthRPCServer) {
|
||||
// Create a JSON-RPC server to handle ETH RPC calls
|
||||
const rpcServer = jayson.Server(ethRPCHandlers);
|
||||
|
||||
// Mount the JSON-RPC server to ETH_RPC_PATH
|
||||
app.use(
|
||||
ETH_RPC_PATH,
|
||||
jsonParser(),
|
||||
// TODO: Handle GET requests as well to match Geth's behaviour
|
||||
rpcServer.middleware()
|
||||
);
|
||||
}
|
||||
|
||||
httpServer.listen(port, host, () => {
|
||||
log(`Server is listening on ${host}:${port}${server.graphqlPath}`);
|
||||
log(`GQL server is listening on http://${host}:${port}${server.graphqlPath}`);
|
||||
|
||||
if (serverConfig.enableEthRPCServer) {
|
||||
log(`ETH JSON RPC server is listening on http://${host}:${port}${ETH_RPC_PATH}`);
|
||||
}
|
||||
});
|
||||
|
||||
return server;
|
||||
|
@ -3,7 +3,7 @@
|
||||
//
|
||||
|
||||
import { Connection, DeepPartial, EntityTarget, FindConditions, FindManyOptions, ObjectLiteral, QueryRunner } from 'typeorm';
|
||||
import { Transaction } from 'ethers';
|
||||
import { ethers, Transaction } from 'ethers';
|
||||
|
||||
import { MappingKey, StorageLayout } from '@cerc-io/solidity-mapper';
|
||||
|
||||
@ -161,6 +161,7 @@ export interface IndexerInterface {
|
||||
readonly serverConfig: ServerConfig
|
||||
readonly upstreamConfig: UpstreamConfig
|
||||
readonly storageLayoutMap: Map<string, StorageLayout>
|
||||
readonly contractMap: Map<string, ethers.utils.Interface>
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
readonly graphWatcher?: GraphWatcherInterface
|
||||
init (): Promise<void>
|
||||
|
66
yarn.lock
66
yarn.lock
@ -3635,6 +3635,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@^3.4.33":
|
||||
version "3.4.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
|
||||
integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/cors@2.8.12":
|
||||
version "2.8.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
|
||||
@ -3848,6 +3855,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
|
||||
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
|
||||
|
||||
"@types/node@^12.12.54":
|
||||
version "12.20.55"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
|
||||
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
|
||||
|
||||
"@types/node@^12.12.6":
|
||||
version "12.20.13"
|
||||
resolved "https://registry.npmjs.org/@types/node/-/node-12.20.13.tgz"
|
||||
@ -3954,6 +3966,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
||||
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
||||
|
||||
"@types/ws@^7.4.4":
|
||||
version "7.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
|
||||
integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ws@^8.5.3":
|
||||
version "8.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
|
||||
@ -4151,7 +4170,7 @@
|
||||
resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz"
|
||||
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
|
||||
|
||||
JSONStream@^1.0.4:
|
||||
JSONStream@^1.0.4, JSONStream@^1.3.5:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz"
|
||||
integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
|
||||
@ -7398,6 +7417,18 @@ es6-object-assign@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
|
||||
integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
es6-promisify@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
|
||||
integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz"
|
||||
@ -8293,6 +8324,11 @@ extsprintf@^1.2.0:
|
||||
resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz"
|
||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
||||
|
||||
eyes@^0.1.8:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
|
||||
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
|
||||
|
||||
fake-merkle-patricia-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz"
|
||||
@ -10422,6 +10458,11 @@ isobject@^3.0.0, isobject@^3.0.1:
|
||||
resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
isomorphic-ws@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
|
||||
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
|
||||
@ -10651,6 +10692,24 @@ jackspeak@^2.3.5:
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
jayson@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.2.tgz#443c26a8658703e0b2e881117b09395d88b6982e"
|
||||
integrity sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA==
|
||||
dependencies:
|
||||
"@types/connect" "^3.4.33"
|
||||
"@types/node" "^12.12.54"
|
||||
"@types/ws" "^7.4.4"
|
||||
JSONStream "^1.3.5"
|
||||
commander "^2.20.3"
|
||||
delay "^5.0.0"
|
||||
es6-promisify "^5.0.0"
|
||||
eyes "^0.1.8"
|
||||
isomorphic-ws "^4.0.1"
|
||||
json-stringify-safe "^5.0.1"
|
||||
uuid "^8.3.2"
|
||||
ws "^7.5.10"
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
|
||||
@ -17029,6 +17088,11 @@ ws@^7.4.6:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
|
||||
ws@^7.5.10:
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
ws@^8.11.0, ws@^8.12.1, ws@^8.4.0:
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
|
Loading…
Reference in New Issue
Block a user