From 568ab5a239ce23854975cde03af747240be03499 Mon Sep 17 00:00:00 2001 From: Nour Alharithi Date: Tue, 24 Oct 2023 15:20:35 -0700 Subject: [PATCH] ws server works --- .gitignore | 1 + package.json | 5 +- src/dlob-subscriber/DLOBSubscriberIO.ts | 56 +++++++++++ src/index.ts | 84 +++++++++++----- src/utils.ts | 23 +++++ yarn.lock | 122 +++++++++++++++++++++++- 6 files changed, 260 insertions(+), 31 deletions(-) create mode 100644 src/dlob-subscriber/DLOBSubscriberIO.ts create mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore index e0e8c8c..61ce045 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,4 @@ src/**.js.map .env lib +src/playground.ts diff --git a/package.json b/package.json index d08ac1b..03ba8a7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "express-rate-limit": "^6.7.0", "morgan": "^1.10.0", "response-time": "^2.3.2", + "socket.io": "^4.7.2", + "socket.io-client": "^4.7.2", "typescript": "4.5.4", "winston": "^3.8.1" }, @@ -50,6 +52,7 @@ "prettify": "prettier --check './src/**/*.ts'", "prettify:fix": "prettier --write './src/**/*.ts'", "lint": "eslint . --ext ts --quiet", - "lint:fix": "eslint . --ext ts --fix" + "lint:fix": "eslint . --ext ts --fix", + "playground": "ts-node src/playground.ts" } } diff --git a/src/dlob-subscriber/DLOBSubscriberIO.ts b/src/dlob-subscriber/DLOBSubscriberIO.ts new file mode 100644 index 0000000..c87cbfa --- /dev/null +++ b/src/dlob-subscriber/DLOBSubscriberIO.ts @@ -0,0 +1,56 @@ +import { + BN, + DLOBSubscriber, + DLOBSubscriptionConfig, + L2OrderBookGenerator, + MarketType, + groupL2, +} from '@drift-labs/sdk'; +import { l2WithBNToStrings } from '../utils'; +import { Server } from 'socket.io'; +import { getIOServer } from '..'; + +type wsMarketL2Args = { + marketIndex: number; + marketType: MarketType; + marketName: string; + depth: number; + includeVamm: boolean; + grouping?: number; + fallbackL2Generators?: L2OrderBookGenerator[]; +}; + +export class DLOBSubscriberIO extends DLOBSubscriber { + public marketL2Args: wsMarketL2Args[] = []; + io: Server; + + constructor(config: DLOBSubscriptionConfig) { + super(config); + } + + override async updateDLOB(): Promise { + this.io = getIOServer(); + await super.updateDLOB(); + for (const l2Args of this.marketL2Args) { + this.getL2AndSendMsg(l2Args); + } + } + + getL2AndSendMsg(l2Args: wsMarketL2Args): void { + console.time('getL2AndSendMsg'); + const grouping = l2Args.grouping; + const l2 = this.getL2(l2Args); + let l2Formatted: any; + if (grouping) { + const groupingBN = new BN(grouping); + l2Formatted = l2WithBNToStrings(groupL2(l2, groupingBN, l2Args.depth)); + } else { + l2Formatted = l2WithBNToStrings(l2); + } + l2Formatted['marketName'] = l2Args.marketName; + l2Formatted['marketType'] = l2Args.marketType; + l2Formatted['marketIndex'] = l2Args.marketIndex; + this.io.emit('l2Update', l2Formatted); + console.timeEnd('getL2AndSendMsg'); + } +} diff --git a/src/index.ts b/src/index.ts index 88df067..ef2d26f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,6 @@ import { DLOBOrdersCoder, SpotMarkets, PerpMarkets, - DLOBSubscriber, MarketType, SpotMarketConfig, PhoenixSubscriber, @@ -31,7 +30,6 @@ import { isVariant, BN, groupL2, - L2OrderBook, Wallet, UserStatsMap, } from '@drift-labs/sdk'; @@ -48,6 +46,10 @@ import { View, } from '@opentelemetry/sdk-metrics-base'; import { ObservableResult } from '@opentelemetry/api'; +import * as http from 'http'; +import { Server } from 'socket.io'; +import { l2WithBNToStrings, sleep } from './utils'; +import { DLOBSubscriberIO } from './dlob-subscriber/DLOBSubscriberIO'; require('dotenv').config(); const driftEnv = (process.env.ENV || 'devnet') as DriftEnv; @@ -57,6 +59,12 @@ const sdkConfig = initialize({ env: process.env.ENV }); const stateCommitment: Commitment = 'processed'; const serverPort = process.env.PORT || 6969; +const maxPerpMarketIndex = Math.max( + ...sdkConfig.PERP_MARKETS.map((m) => m.marketIndex) +); +const maxSpotMarketIndex = Math.max( + ...sdkConfig.SPOT_MARKETS.map((m) => m.marketIndex) +); const bulkAccountLoaderPollingInterval = process.env .BULK_ACCOUNT_LOADER_POLLING_INTERVAL @@ -89,6 +97,13 @@ app.use(compression()); app.set('trust proxy', 1); app.use(logHttp); +const server = http.createServer(app); +const io = new Server(server); + +export function getIOServer(): Server { + return io; +} + const handleResponseTime = responseTime( (req: Request, res: Response, time: number) => { const endpoint = req.path; @@ -147,10 +162,6 @@ logger.info(`WS endpoint: ${wsEndpoint}`); logger.info(`DriftEnv: ${driftEnv}`); logger.info(`Commit: ${commitHash}`); -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - /** * Creates {count} buckets of size {increment} starting from {start}. Each bucket stores the count of values within its "size". * @param start @@ -398,7 +409,7 @@ const main = async () => { ); await userStatsMap.subscribe(); - const dlobSubscriber = new DLOBSubscriber({ + const dlobSubscriber = new DLOBSubscriberIO({ driftClient, dlobSource: userMap, slotSource: slotSubscriber, @@ -903,24 +914,6 @@ const main = async () => { } }); - const l2WithBNToStrings = (l2: L2OrderBook): any => { - for (const key of Object.keys(l2)) { - for (const idx in l2[key]) { - const level = l2[key][idx]; - const sources = level['sources']; - for (const sourceKey of Object.keys(sources)) { - sources[sourceKey] = sources[sourceKey].toString(); - } - l2[key][idx] = { - price: level.price.toString(), - size: level.size.toString(), - sources, - }; - } - } - return l2; - }; - const getOracleForMarket = ( marketType: MarketType, marketIndex: number @@ -1249,9 +1242,48 @@ const main = async () => { }); app.use(errorHandler); - app.listen(serverPort, () => { + server.listen(serverPort, () => { logger.info(`DLOB server listening on port http://localhost:${serverPort}`); }); + + /** + * Handle websocket logic here + */ + + io.on('connection', (socket) => { + console.log('a user connected'); + + socket.on('disconnect', () => { + console.log('user disconnected'); + }); + + socket.on('message', (msg: any) => { + const parsed = JSON.parse(msg); + if (parsed['type'] == 'subscribe') { + console.log('here'); + dlobSubscriber.marketL2Args.push({ + marketIndex: parseInt(parsed['marketIndex']), + marketType: isVariant(parsed['marketType'].toLowerCase(), 'spot') + ? MarketType.SPOT + : MarketType.PERP, + marketName: parsed['marketName'], + depth: parseInt(parsed['depth']), + includeVamm: parsed['includeVamm'] === 'true', + grouping: parsed['grouping'] + ? parseInt(parsed['grouping']) + : undefined, + fallbackL2Generators: isVariant(parsed['marketType'], 'spot') + ? [ + `${parsed['includePhoenix']}`.toLowerCase() === 'true' && + MARKET_SUBSCRIBERS[parseInt(parsed['marketIndex'])].phoenix, + `${parsed['includeSerum']}`.toLowerCase() === 'true' && + MARKET_SUBSCRIBERS[parseInt(parsed['marketIndex'])].serum, + ].filter((a) => !!a) + : [], + }); + } + }); + }); }; async function recursiveTryCatch(f: () => void) { diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..73d8fa1 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,23 @@ +import { L2OrderBook } from '@drift-labs/sdk'; + +export const l2WithBNToStrings = (l2: L2OrderBook): any => { + for (const key of Object.keys(l2)) { + for (const idx in l2[key]) { + const level = l2[key][idx]; + const sources = level['sources']; + for (const sourceKey of Object.keys(sources)) { + sources[sourceKey] = sources[sourceKey].toString(); + } + l2[key][idx] = { + price: level.price.toString(), + size: level.size.toString(), + sources, + }; + } + } + return l2; +}; + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/yarn.lock b/yarn.lock index 937059e..2195452 100644 --- a/yarn.lock +++ b/yarn.lock @@ -901,6 +901,11 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@solana/buffer-layout-utils@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" @@ -1090,6 +1095,11 @@ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cookies@*": version "0.7.7" resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" @@ -1100,6 +1110,13 @@ "@types/keygrip" "*" "@types/node" "*" +"@types/cors@^2.8.12": + version "2.8.15" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.15.tgz#eb143aa2f8807ddd78e83cbff141bbedd91b60ee" + integrity sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw== + dependencies: + "@types/node" "*" + "@types/express-serve-static-core@^4.17.18": version "4.17.31" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" @@ -1280,6 +1297,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== +"@types/node@>=10.0.0": + version "20.8.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" + integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== + dependencies: + undici-types "~5.25.1" + "@types/node@^12.12.54": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" @@ -1438,7 +1462,7 @@ abstract-logging@^2.0.0: resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== -accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -1620,6 +1644,11 @@ base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + basic-auth@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" @@ -1910,7 +1939,12 @@ cookie@0.5.0, cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cors@^2.8.5: +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@^2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -1951,7 +1985,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: +debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2065,6 +2099,38 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002" + integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + +engine.io@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.3.tgz#80b0692912cef3a417e1b7433301d6397bf0374b" + integrity sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + enquirer@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" @@ -3575,6 +3641,44 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-client@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" + integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" + integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + sonic-boom@^1.0.2: version "1.4.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.4.1.tgz#d35d6a74076624f12e6f917ade7b9d75e918f53e" @@ -3826,6 +3930,11 @@ typescript@4.5.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -3959,11 +4068,16 @@ ws@^7.4.5: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== -ws@^8.5.0: +ws@^8.5.0, ws@~8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"