diff --git a/package.json b/package.json index ca6dbeef..95f36ba9 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@godaddy/terminus": "4.9.0", "@snapshot-labs/snapshot.js": "^0.3.56", "@types/pg": "^8.6.1", - "axios": "0.24.0", + "axios": "^0.27.2", "axios-cache-adapter": "^2.7.3", "axios-rate-limit": "^1.3.0", "axios-retry": "^3.2.4", @@ -67,6 +67,7 @@ "dotenv": "10.0.0", "ethers": "^5.6.2", "express": "4.17.1", + "http-status-codes": "^2.2.0", "jsonwebtoken": "^8.5.1", "lodash": "4.17.21", "log4js": "6.3.0", diff --git a/scripts/gas-refund-program/staking/spsp-stakes-tracker.ts b/scripts/gas-refund-program/staking/spsp-stakes-tracker.ts index 2d2d9281..e1e629be 100644 --- a/scripts/gas-refund-program/staking/spsp-stakes-tracker.ts +++ b/scripts/gas-refund-program/staking/spsp-stakes-tracker.ts @@ -3,7 +3,6 @@ import { BigNumber as EthersBN, Contract, Event, EventFilter } from 'ethers'; import { CHAIN_ID_MAINNET, NULL_ADDRESS, - PSP_ADDRESS, } from '../../../src/lib/constants'; import { Provider } from '../../../src/lib/provider'; import * as ERC20ABI from '../../../src/lib/abi/erc20.abi.json'; @@ -21,9 +20,12 @@ import { SPSPAddresses, SPSPHelper, } from '../../../src/lib/staking/spsp-helper'; +import { configLoader } from '../../../src/config'; const logger = global.LOGGER('SPSPStakesTracker'); +const config = configLoader.getConfig(CHAIN_ID_MAINNET); + const SPSPAddressesSet = new Set(SPSPAddresses); const SPSPPrototypeContract = new Contract( @@ -33,7 +35,7 @@ const SPSPPrototypeContract = new Contract( ); const PSPContract = new Contract( - PSP_ADDRESS[CHAIN_ID_MAINNET], + config.pspAddress, ERC20ABI, Provider.getJsonRpcProvider(CHAIN_ID_MAINNET), ); diff --git a/scripts/gas-refund-program/transactions-indexing/txs-covalent.ts b/scripts/gas-refund-program/transactions-indexing/txs-covalent.ts index 3566da9e..b3dc3b71 100644 --- a/scripts/gas-refund-program/transactions-indexing/txs-covalent.ts +++ b/scripts/gas-refund-program/transactions-indexing/txs-covalent.ts @@ -1,3 +1,4 @@ +import { configLoader } from '../../../src/config'; import { covalentClient } from '../../../src/lib/utils/data-providers-clients'; import { CovalentAPI, @@ -5,8 +6,10 @@ import { GasRefundTransaction, } from '../types'; +const globalConfig = configLoader.getGlobalConfig(); + interface GetContractTXsByNetworkInput { - chainId: number; + chainId: number contract: string; startTimestamp: number; endTimestamp: number; @@ -31,7 +34,6 @@ export const covalentGetTXsForContract = async ({ contract, }); - const { COVALENT_API_KEY } = process.env; const path = (page: number) => { /* Covalent API only has time relative pagination for tx scanning (give me tx within last X seconds). * We take a safety margin to counter possible edge case of relative - not absolute - range bounds @@ -58,7 +60,7 @@ export const covalentGetTXsForContract = async ({ throw new Error('only query historic data'); } - return `/${chainId}/address/${contract}/transactions_v2/?key=${COVALENT_API_KEY}&no-logs=true&page-number=${page}&page-size=1000&block-signed-at-limit=${startSecondsAgo}&block-signed-at-span=${duration}&match={"to_address": "${contract}"}`; + return `/${chainId}/address/${contract}/transactions_v2/?key=${globalConfig.covalentV1ApiKey}&no-logs=true&page-number=${page}&page-size=1000&block-signed-at-limit=${startSecondsAgo}&block-signed-at-span=${duration}&match={"to_address": "${contract}"}`; }; // todo: better would be to first call the end point with page-size=0 just to get the total number of items, and then construct many request promises and run concurrently - currently this isn't possible (as `total_count` is null) in the covalent api but scheduled diff --git a/src/app.ts b/src/app.ts index dac91ee8..8e2c9c02 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,4 @@ import { - VOLUME_TRACKER_INIT_TIME, VOLUME_TRACKER_SUPPORTED_NETWORKS, } from './lib/constants'; import { shutdown as shutdownLog4js } from './lib/log4js'; @@ -7,6 +6,7 @@ import Database from './database'; import VolumeTracker from './lib/volume-tracker'; import WebServer from './web-server'; import { PoolInfo } from './lib/pool-info'; +import { configLoader } from './config'; const logger = global.LOGGER(); @@ -23,13 +23,14 @@ export async function startApp() { await Promise.all( VOLUME_TRACKER_SUPPORTED_NETWORKS.map(network => { - if (!VOLUME_TRACKER_INIT_TIME[network]) { + const config = configLoader.getConfig(network); + if (!config.volumeTrackerInitTime) { throw new Error( 'VolumeTracker INIT_TIME env var is missing for network ' + network, ); } return VolumeTracker.createInstance( - VOLUME_TRACKER_INIT_TIME[network], + config.volumeTrackerInitTime, network, ).startIndexing(); }), diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 00000000..2313ff82 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,113 @@ +/* eslint-disable */ +import axios from 'axios'; +import { SERVICE_CONFIGURATION_SERVICE_HTTP } from '../env'; +import { sleep } from './utils'; +import { ApplicationError } from '../errors'; +import { CHAIN_ID_MAINNET } from '../lib/constants'; + +const serviceName = 'volumetracker'; + +export const CORS_ALLOWED_HEADERS = 'Accept, Content-Type, Origin'; +export const REQUEST_BODY_SIZE_LIMIT_BYTES = 256 * 1024; // 256KB +export const CACHE_CONTROL_PREFLIGHT_REQUESTS_MAX_AGE_SECS = 24 * 60 * 60; // 24 hours + +const CONFIG_SERVICE_TIMEOUT = 5000; +const CONFIG_SERVICE_RETRY_INTERVAL = 3000; + +export type NetworkMap = { [network: number]: T }; + +export type Config = { + network: number; + augustusAddress: string; + augustusV4Address: string; + pspAddress: string; + isStaking: boolean; + rewardDistributionAddress: string; + safetyModuleAddress: string; + privateHttpArchiveProvider: string; + coinGeckoPlatform: string; + multicallV2Address: string; + volumeTrackerInitTime: number; +}; + +type GlobalConfig = { + apiKeyCoingecko: string; + covalentV1ApiKey: string; + covalentV1HttpUrl: string; + apiPrefineryHttp: string; + apiKeyPrefinery: string; + apiAplcapiHttp: string; + apiKeyAplcapi: string; + databaseUrl: string; + apiKeyCaptcha: string; + apiKeySubmitAccount: string; +}; + +type ConfigResponse = { + networks: NetworkMap; + global: GlobalConfig; +}; + +class ConfigLoader { + public byNetwork: NetworkMap = {}; + + public enabledNetworks: number[] = []; + + public global?: GlobalConfig; + + public isLoaded: Promise; + + public hasStartedNotifier?: (value: void | PromiseLike) => void; + + constructor() { + this.isLoaded = new Promise(resolve => { + this.hasStartedNotifier = resolve; + }); + } + + async load() { + console.log(`Try to get config from ${SERVICE_CONFIGURATION_SERVICE_HTTP}`); + while (true) { + try { + const configs = ( + await axios.get( + `${SERVICE_CONFIGURATION_SERVICE_HTTP}/configuration?service=${serviceName}`, + { timeout: CONFIG_SERVICE_TIMEOUT }, + ) + ).data; + this.global = configs.global; + for (const network in configs.networks) { + const config = configs.networks[network]; + this.byNetwork[network] = config; + } + this.enabledNetworks.push(CHAIN_ID_MAINNET); + break; + } catch (e) { + console.error('Error downloading configuration:', e); + } + await sleep(CONFIG_SERVICE_RETRY_INTERVAL); + } + console.log(`received config`); + } + + getConfig(network: number): Config { + const config = this.byNetwork[network]; + if (!config) { + throw new ApplicationError(`Missing config for network ${network}`); + } + return config; + } + + getGlobalConfig(): GlobalConfig { + if (!this.global) { + throw new ApplicationError(`Missing global config`); + } + return this.global; + } +} +export const configLoader = new ConfigLoader(); + +export const init = async () => { + await configLoader.load(); + configLoader.hasStartedNotifier!(); +}; diff --git a/src/config/utils.ts b/src/config/utils.ts new file mode 100644 index 00000000..60b24933 --- /dev/null +++ b/src/config/utils.ts @@ -0,0 +1,3 @@ +/* eslint-disable */ +export const sleep = (ms: number) => + new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/database.ts b/src/database.ts index dfc50d1c..23959ca9 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,16 +1,14 @@ import { Client } from 'pg'; import { Sequelize } from 'sequelize-typescript'; import * as cls from 'cls-hooked'; +import { IS_DEV } from './env'; +import { configLoader } from './config'; -const logger = global.LOGGER(); - -const IS_DEV = process.env.NODE_ENV === 'development'; +const globalConfig = configLoader.getGlobalConfig(); -const DATABASE_URL = - process.env.DATABASE_URL || - 'postgres://paraswap:paraswap@127.0.0.1:32780/volume_tracker'; +const logger = global.LOGGER(); -const DATABASE_NAME = process.env.DATABASE_NAME || 'volume_tracker'; +const DATABASE_NAME = 'volume_tracker'; export class Database { sequelize: Sequelize; @@ -22,14 +20,15 @@ export class Database { } // create a volume-tracker DB if it doesn't exist already - const connectionStringParts = DATABASE_URL.split('/'); + const connectionStringParts = globalConfig.databaseUrl.split('/'); const connectionStringDBName = connectionStringParts[connectionStringParts.length - 1]; if (connectionStringDBName !== DATABASE_NAME) { logger.info( 'Database name in connection string is different than expected', ); - const client = new Client({ connectionString: DATABASE_URL }); + + const client = new Client({ connectionString: globalConfig.databaseUrl }); await client.connect(); try { await client.query(`CREATE DATABASE ${DATABASE_NAME};`); @@ -62,7 +61,7 @@ export class Database { }); try { - logger.info('Connecting to database...'); + logger.info('Connecting to database...', connectionStringDBName); await this.sequelize.authenticate(); logger.info('Connected to database'); } catch (e) { diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000..a86792bf --- /dev/null +++ b/src/env.ts @@ -0,0 +1,18 @@ +import { EnvironmentError } from './errors/environment-error'; + +function getEnvOrThrow( + name: string, + defaultValue: string | undefined = undefined, +) { + const value = process.env[name] || defaultValue; + if (value === undefined) { + throw new EnvironmentError(name); + } + return value; +} +export const SERVICE_CONFIGURATION_SERVICE_HTTP = getEnvOrThrow( + 'SERVICE_CONFIGURATION_SERVICE_HTTP', +); +export const NODE_ENV = getEnvOrThrow('NODE_ENV', 'development'); +export const IS_DEV = NODE_ENV === 'development'; +export const PORT = parseInt(getEnvOrThrow('PORT', '3236'), 10); diff --git a/src/errors/application-error.ts b/src/errors/application-error.ts new file mode 100644 index 00000000..189e8667 --- /dev/null +++ b/src/errors/application-error.ts @@ -0,0 +1,6 @@ +export class ApplicationError extends Error { + // eslint-disable-next-line no-useless-constructor + constructor(message: string, public isLogged: boolean = false) { + super(message); + } +} diff --git a/src/errors/config-error.ts b/src/errors/config-error.ts new file mode 100644 index 00000000..d22fdd0f --- /dev/null +++ b/src/errors/config-error.ts @@ -0,0 +1,8 @@ +import { ApplicationError } from './application-error'; + +export class ConfigError extends ApplicationError { + // eslint-disable-next-line no-useless-constructor + constructor(message: string) { + super(message); + } +} diff --git a/src/errors/database-error.ts b/src/errors/database-error.ts new file mode 100644 index 00000000..447a7989 --- /dev/null +++ b/src/errors/database-error.ts @@ -0,0 +1,8 @@ +import { ApplicationError } from './application-error'; + +export class DatabaseError extends ApplicationError { + // eslint-disable-next-line no-useless-constructor + constructor(message: string) { + super(message); + } +} diff --git a/src/errors/database-rollback-error.ts b/src/errors/database-rollback-error.ts new file mode 100644 index 00000000..e319533b --- /dev/null +++ b/src/errors/database-rollback-error.ts @@ -0,0 +1,10 @@ +import { DatabaseError } from './database-error'; + +// This error indicates only that we want to rollback, but not send the user InternalServerError +// So this error should be caught after transaction and later returned the value you want +export class DatabaseRollbackError extends DatabaseError { + // eslint-disable-next-line no-useless-constructor + constructor(message?: string) { + super(message || 'Database Rollback'); + } +} diff --git a/src/errors/environment-error.ts b/src/errors/environment-error.ts new file mode 100644 index 00000000..e35d6431 --- /dev/null +++ b/src/errors/environment-error.ts @@ -0,0 +1,7 @@ +import { ApplicationError } from './application-error'; + +export class EnvironmentError extends ApplicationError { + constructor(key: string) { + super(`Required environment variable '${key}' expected!`); + } +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 00000000..d5328d41 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,13 @@ +export { ApplicationError } from './application-error'; + +export { EnvironmentError } from './environment-error'; +export { DatabaseError } from './database-error'; +export { ConfigError } from './config-error'; +export { LimitOrderError } from './limit-order-error'; + +export { RestError } from './rest-errors/rest-error'; +export { BadRequestError } from './rest-errors/bad-request-error'; +export { InternalServerError } from './rest-errors/internal-server-error'; +export { NotFoundError } from './rest-errors/not-found-error'; + +export { ValidationError } from './rest-errors/validation-error'; diff --git a/src/errors/limit-order-error.ts b/src/errors/limit-order-error.ts new file mode 100644 index 00000000..6731bb55 --- /dev/null +++ b/src/errors/limit-order-error.ts @@ -0,0 +1,8 @@ +import { ApplicationError } from './application-error'; + +export class LimitOrderError extends ApplicationError { + // eslint-disable-next-line no-useless-constructor + constructor(message: string) { + super(message); + } +} diff --git a/src/errors/rest-errors/bad-request-error.ts b/src/errors/rest-errors/bad-request-error.ts new file mode 100644 index 00000000..b96056e6 --- /dev/null +++ b/src/errors/rest-errors/bad-request-error.ts @@ -0,0 +1,8 @@ +import { StatusCodes } from 'http-status-codes'; +import { ClientSideError } from './client-side-error'; + +export class BadRequestError extends ClientSideError { + constructor(message: string) { + super(message, StatusCodes.BAD_REQUEST); + } +} diff --git a/src/errors/rest-errors/black-list-error.ts b/src/errors/rest-errors/black-list-error.ts new file mode 100644 index 00000000..abd84f7c --- /dev/null +++ b/src/errors/rest-errors/black-list-error.ts @@ -0,0 +1,7 @@ +import { ApplicationError } from '../application-error'; + +export class BlackListError extends ApplicationError { + constructor(message: string, readonly statusCode: number) { + super(message); + } +} diff --git a/src/errors/rest-errors/client-side-error.ts b/src/errors/rest-errors/client-side-error.ts new file mode 100644 index 00000000..89092f61 --- /dev/null +++ b/src/errors/rest-errors/client-side-error.ts @@ -0,0 +1,8 @@ +import { RestError } from './rest-error'; + +export class ClientSideError extends RestError { + // eslint-disable-next-line no-useless-constructor + constructor(message: string, statusCode: number) { + super(message, statusCode); + } +} diff --git a/src/errors/rest-errors/internal-server-error.ts b/src/errors/rest-errors/internal-server-error.ts new file mode 100644 index 00000000..97d514a6 --- /dev/null +++ b/src/errors/rest-errors/internal-server-error.ts @@ -0,0 +1,8 @@ +import { StatusCodes } from 'http-status-codes'; +import { RestError } from './rest-error'; + +export class InternalServerError extends RestError { + constructor(message: string) { + super(message, StatusCodes.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/errors/rest-errors/not-found-error.ts b/src/errors/rest-errors/not-found-error.ts new file mode 100644 index 00000000..0839816f --- /dev/null +++ b/src/errors/rest-errors/not-found-error.ts @@ -0,0 +1,8 @@ +import { StatusCodes } from 'http-status-codes'; +import { ClientSideError } from './client-side-error'; + +export class NotFoundError extends ClientSideError { + constructor(message: string) { + super(message, StatusCodes.NOT_FOUND); + } +} diff --git a/src/errors/rest-errors/rest-error.ts b/src/errors/rest-errors/rest-error.ts new file mode 100644 index 00000000..227644cd --- /dev/null +++ b/src/errors/rest-errors/rest-error.ts @@ -0,0 +1,7 @@ +import { ApplicationError } from '../application-error'; + +export class RestError extends ApplicationError { + constructor(message: string, readonly statusCode: number) { + super(message); + } +} diff --git a/src/errors/rest-errors/validation-error.ts b/src/errors/rest-errors/validation-error.ts new file mode 100644 index 00000000..91166501 --- /dev/null +++ b/src/errors/rest-errors/validation-error.ts @@ -0,0 +1,13 @@ +import { BadRequestError } from './bad-request-error'; + +export class ValidationError extends BadRequestError { + readonly rawMessage: string; + + readonly key?: string; + + constructor(message: string, key?: string) { + super(key !== undefined ? `'${key}': ${message}` : message); + this.rawMessage = message; + this.key = key; + } +} diff --git a/src/index.ts b/src/index.ts index d805b09a..baf514ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,18 @@ -require('dotenv').config(); - +/* eslint-disable */ if (process.env.NEW_RELIC_LICENSE_KEY) { require('newrelic'); } +require('dotenv').config(); + +import { init } from './config'; + +const main = async () => { + await init(); -import { handleErrors, startApp } from './app'; + const { handleErrors, startApp } = require('./app'); -handleErrors(); + handleErrors(); + startApp(); +}; -startApp(); +main(); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 23f39529..bcd538c7 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -5,13 +5,6 @@ export const CHAIN_ID_POLYGON = 137; export const CHAIN_ID_AVALANCHE = 43114; export const CHAIN_ID_FANTOM = 250; -export const PSP_ADDRESS: { [chainId: number]: string } = { - [CHAIN_ID_MAINNET]: '0xcafe001067cdef266afb7eb5a286dcfd277f3de5', - [CHAIN_ID_BINANCE]: '0xcafe001067cdef266afb7eb5a286dcfd277f3de5', - [CHAIN_ID_FANTOM]: '0xcafe001067cdef266afb7eb5a286dcfd277f3de5', - [CHAIN_ID_POLYGON]: '0x42d61d766b85431666b39b89c43011f24451bff6', -}; - export const STAKING_CHAIN_IDS = [CHAIN_ID_MAINNET, CHAIN_ID_ROPSTEN]; export const STAKING_CHAIN_IDS_SET = new Set([ CHAIN_ID_MAINNET, @@ -19,35 +12,7 @@ export const STAKING_CHAIN_IDS_SET = new Set([ ]); export const VOLUME_TRACKER_SUPPORTED_NETWORKS = [CHAIN_ID_MAINNET]; -export const VOLUME_TRACKER_INIT_TIME: { [network: number]: number } = { - [CHAIN_ID_MAINNET]: parseInt(process.env.INIT_TIME || '0'), //TODO: use the block info to the init time from the init block -}; - -export const Web3Provider: { [network: number]: string } = { - [CHAIN_ID_MAINNET]: process.env.HTTP_PROVIDER || '', - [CHAIN_ID_ROPSTEN]: process.env.HTTP_PROVIDER_3 || '', - [CHAIN_ID_BINANCE]: process.env.HTTP_PROVIDER_56 || '', - [CHAIN_ID_POLYGON]: process.env.HTTP_PROVIDER_137 || '', - [CHAIN_ID_FANTOM]: process.env.HTTP_PROVIDER_250 || '', - [CHAIN_ID_AVALANCHE]: process.env.HTTP_PROVIDER_43114 || '', -}; - -// TODO: in future we can fetch it from the api directly -export const AugustusV5Address: { [network: number]: string } = { - [CHAIN_ID_MAINNET]: '0xdef171fe48cf0115b1d80b88dc8eab59176fee57', - [CHAIN_ID_BINANCE]: '0xdef171fe48cf0115b1d80b88dc8eab59176fee57', - [CHAIN_ID_POLYGON]: '0xdef171fe48cf0115b1d80b88dc8eab59176fee57', -}; - -export const AugustusV4Address: { [network: number]: string } = { - [CHAIN_ID_MAINNET]: '0x1bd435f3c054b6e901b7b108a0ab7617c808677b', - [CHAIN_ID_BINANCE]: '0x55A0E3b6579972055fAA983482acEb4B251dcF15', - [CHAIN_ID_POLYGON]: '0x90249ed4d69D70E709fFCd8beE2c5A566f65dADE', -}; -export const RewardDistributionAddress: { [network: string]: string } = { - [CHAIN_ID_MAINNET]: '0x8145cDeeD63e2E3c103F885CbB2cD02a00F54873', -}; // TODO: in future we can fetch it from the api directly export const ZeroXV2Address: { [network: number]: string } = { @@ -59,16 +24,6 @@ export const ZeroXV4Address: { [network: number]: string } = { [CHAIN_ID_MAINNET]: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', }; -// TODO: set using env variable -export const ParaswapApiURL = 'https://apiv5.paraswap.io'; - -export const MULTICALL_ADDRESS: any = { - [CHAIN_ID_MAINNET]: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441', - [CHAIN_ID_ROPSTEN]: '0x293405FE3aDefDB94A8A1Ed50873a15C6Cc83BC5', - [CHAIN_ID_BINANCE]: '0xdc6e2b14260f972ad4e5a31c68294fba7e720701', - [CHAIN_ID_POLYGON]: '0xdC6E2b14260F972ad4e5a31c68294Fba7E720701', - [CHAIN_ID_FANTOM]: '0xdC6E2b14260F972ad4e5a31c68294Fba7E720701', -}; export type MulticallEncodedData = { returnData: string[] }; @@ -90,3 +45,5 @@ export const Balancer_80PSP_20WETH_poolId = '0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d00020000000000000000011a'; export const Balancer_80PSP_20WETH_address = Balancer_80PSP_20WETH_poolId.substring(0, 42); // or 0xcb0e14e96f2cefa8550ad8e4aea344f211e5061d + +export const CONFIG_VOLUME_TRACKER_INIT_TIME_1 =16198272 diff --git a/src/lib/epoch-info.ts b/src/lib/epoch-info.ts index f0e7bb7d..5fccb225 100644 --- a/src/lib/epoch-info.ts +++ b/src/lib/epoch-info.ts @@ -4,10 +4,10 @@ import * as RewardDistributionAbi from './abi/reward-distribution.abi.json'; import { CHAIN_ID_ROPSTEN, CHAIN_ID_MAINNET, - RewardDistributionAddress, } from './constants'; import { BlockInfo } from './block-info'; import { Provider } from './provider'; +import { configLoader } from '../config'; const logger = global.LOGGER(); @@ -59,9 +59,14 @@ export class EpochInfo { constructor(protected network: number, lazy: boolean = false) { this.blockInfo = BlockInfo.getInstance(this.network); + const config = configLoader.getConfig(network); const provider = Provider.getJsonRpcProvider(this.network); + + if (!config.rewardDistributionAddress) { + throw new Error(`missing rewardDistributionAddress for network ${network}`); + } this.rewardDistribution = new Contract( - RewardDistributionAddress[this.network], + config.rewardDistributionAddress, RewardDistributionAbi, provider, ); diff --git a/src/lib/onboarding/beta-tester.ts b/src/lib/onboarding/beta-tester.ts index c11bcc4c..7fb615c6 100644 --- a/src/lib/onboarding/beta-tester.ts +++ b/src/lib/onboarding/beta-tester.ts @@ -1,19 +1,20 @@ import axios from 'axios'; import * as jwt from 'jsonwebtoken'; import { assert } from 'ts-essentials'; +import { configLoader } from '../../config'; import { AuthToken } from './types'; +const config = configLoader.getGlobalConfig(); + const logger = global.LOGGER('BetaTest'); -const _pk = process.env.APLCAPI_KEY; -const baseUrl = process.env.APLCAPI_BASE_URL; const keyid = 'SQ994H8QHA'; const issuerId = '2e01237d-0848-46cc-a3ac-b0f6245eb42b'; export function generateAuthToken(): AuthToken { - assert(_pk, 'set APLCAPI_KEY env var'); + assert(config.apiKeyAplcapi, 'set APLCAPI_KEY env var'); - const pk = _pk.replace(/\\n/g, '\n'); + const pk = config.apiKeyAplcapi.replace(/\\n/g, '\n'); const iat = Math.floor(Date.now() / 1000); const exp = iat + 15 * 60; @@ -46,11 +47,11 @@ export async function createTester({ email: string; authToken: string; }) { - assert(baseUrl, 'set APLCAPI_BASE_URL env var'); + assert(config.apiAplcapiHttp, 'set APLCAPI_BASE_URL env var'); try { await axios.post( - baseUrl + '/v1/betaTesters', + config.apiAplcapiHttp + '/v1/betaTesters', { data: { attributes: { diff --git a/src/lib/onboarding/mail-service-client.ts b/src/lib/onboarding/mail-service-client.ts index 12196801..5aab6244 100644 --- a/src/lib/onboarding/mail-service-client.ts +++ b/src/lib/onboarding/mail-service-client.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; import { assert } from 'ts-essentials'; +import { configLoader } from '../../config'; import { constructHttpClient } from '../utils/http-client'; import { AccountCreationError, @@ -16,7 +17,7 @@ import { const logger = global.LOGGER('MailService'); -const { MAIL_SERVICE_BASE_URL, MAIL_SERVICE_API_KEY } = process.env; +const globalConfig = configLoader.getGlobalConfig(); const mailServiceClient = constructHttpClient({ cacheOptions: { @@ -54,10 +55,10 @@ export async function createNewAccount( account: AccountToCreate, isVerified: boolean, ): Promise { - assert(MAIL_SERVICE_BASE_URL, 'set MAIL_SERVICE_BASE_URL env var'); - assert(MAIL_SERVICE_API_KEY, 'set MAIL_SERVICE_API_KEY env var'); + assert(globalConfig.apiPrefineryHttp, 'set MAIL_SERVICE_BASE_URL env var'); + assert(globalConfig.apiKeyPrefinery, 'set MAIL_SERVICE_API_KEY env var'); - const apiUrl = `${MAIL_SERVICE_BASE_URL}/betas/17942/testers?api_key=${MAIL_SERVICE_API_KEY}`; + const apiUrl = `${globalConfig.apiPrefineryHttp}/betas/17942/testers?api_key=${globalConfig.apiKeyPrefinery}`; const createdAccount = { ...account, @@ -91,10 +92,10 @@ export async function createNewAccount( export async function removeUserFromWaitlist({ uuid, }: Pick): Promise { - assert(MAIL_SERVICE_BASE_URL, 'set MAIL_SERVICE_BASE_URL env var'); - assert(MAIL_SERVICE_API_KEY, 'set MAIL_SERVICE_API_KEY env var'); + assert(globalConfig.apiPrefineryHttp, 'set MAIL_SERVICE_BASE_URL env var'); + assert(globalConfig.apiKeyPrefinery, 'set MAIL_SERVICE_API_KEY env var'); - const apiUrl = `${MAIL_SERVICE_BASE_URL}/betas/17942/testers/${uuid}?api_key=${MAIL_SERVICE_API_KEY}`; + const apiUrl = `${globalConfig.apiPrefineryHttp}/betas/17942/testers/${uuid}?api_key=${globalConfig.apiKeyPrefinery}`; try { await mailServiceClient.delete(apiUrl); @@ -106,10 +107,10 @@ export async function removeUserFromWaitlist({ export async function fetchAccountByUUID({ uuid, }: Pick): Promise { - assert(MAIL_SERVICE_BASE_URL, 'set MAIL_SERVICE_BASE_URL env var'); - assert(MAIL_SERVICE_API_KEY, 'set MAIL_SERVICE_API_KEY env var'); + assert(globalConfig.apiPrefineryHttp, 'set MAIL_SERVICE_BASE_URL env var'); + assert(globalConfig.apiKeyPrefinery, 'set MAIL_SERVICE_API_KEY env var'); - const apiUrl = `${MAIL_SERVICE_BASE_URL}/betas/17942/testers/${uuid}?api_key=${MAIL_SERVICE_API_KEY}`; + const apiUrl = `${globalConfig.apiPrefineryHttp}/betas/17942/testers/${uuid}?api_key=${globalConfig.apiKeyPrefinery}`; try { const { data: registeredAccount } = diff --git a/src/lib/onboarding/router.ts b/src/lib/onboarding/router.ts index e78a6910..dbebe7ef 100644 --- a/src/lib/onboarding/router.ts +++ b/src/lib/onboarding/router.ts @@ -16,6 +16,9 @@ import { } from './errors'; import { Utils } from '../utils'; import { isAddress } from '@ethersproject/address'; +import { configLoader } from '../../config'; + +const globalConfig = configLoader.getGlobalConfig(); const logger = global.LOGGER('OnboardingRouter'); @@ -77,11 +80,11 @@ router.get('/check-eligibility/:address/:blockNumber', async (req, res) => { router.post('/submit-verified', async (req, res) => { try { assert( - process.env.SUBMIT_ACCOUNT_API_KEY, + globalConfig.apiKeySubmitAccount, 'set SUBMIT_ACCOUNT_API_KEY env var', ); - if (req.headers['x-auth-token'] !== process.env.SUBMIT_ACCOUNT_API_KEY) + if (req.headers['x-auth-token'] !== globalConfig.apiKeySubmitAccount) throw new AuthorizationError(); const account = req.body; diff --git a/src/lib/onboarding/verification-service.ts b/src/lib/onboarding/verification-service.ts index cb0d9dfe..6decdb83 100644 --- a/src/lib/onboarding/verification-service.ts +++ b/src/lib/onboarding/verification-service.ts @@ -1,8 +1,11 @@ import { assert } from 'ts-essentials'; import { URLSearchParams } from 'url'; +import { configLoader } from '../../config'; import { constructHttpClient } from '../utils/http-client'; import { VerificationError } from './errors'; +const config = configLoader.getGlobalConfig(); + const logger = global.LOGGER('verificationService'); const responseVerificationClient = constructHttpClient({ @@ -15,10 +18,10 @@ const responseVerificationClient = constructHttpClient({ }); export const verifyKey = async (key: string) => { - assert(process.env.CAPTCHA_SECRET_KEY, 'CAPTCHA_SECRET_KEY should be set'); + assert(config.apiKeyCaptcha, 'CAPTCHA_SECRET_KEY should be set'); const params = new URLSearchParams(); - params.append('secret', process.env.CAPTCHA_SECRET_KEY); + params.append('secret', config.apiKeyCaptcha); params.append('response', key); try { diff --git a/src/lib/pool-info.ts b/src/lib/pool-info.ts index 33aafbce..c6840200 100644 --- a/src/lib/pool-info.ts +++ b/src/lib/pool-info.ts @@ -2,7 +2,6 @@ import type { JsonRpcProvider } from '@ethersproject/providers'; import { Contract } from '@ethersproject/contracts'; import { Interface } from '@ethersproject/abi'; import { - MULTICALL_ADDRESS, DEFAULT_CHAIN_ID, CHAIN_ID_ROPSTEN, CHAIN_ID_MAINNET, @@ -17,6 +16,7 @@ import BigNumber from 'bignumber.js'; import VolumeTracker from './volume-tracker'; import { BlockInfo } from './block-info'; import { EpochInfo } from './epoch-info'; +import { configLoader } from '../config'; export enum PoolType { AMMPool = 'AMMPool', @@ -339,9 +339,11 @@ export class PoolInfo { private network: number, private poolConfigs: PoolConfig[], ) { + const config = configLoader.getConfig(network); + this.provider = Provider.getJsonRpcProvider(this.network); this.multicallContract = new Contract( - MULTICALL_ADDRESS[this.network], + config.multicallV2Address, MultiCallerABI, this.provider, ); diff --git a/src/lib/provider.ts b/src/lib/provider.ts index 9817f819..a823c50b 100644 --- a/src/lib/provider.ts +++ b/src/lib/provider.ts @@ -1,15 +1,16 @@ import { JsonRpcProvider } from '@ethersproject/providers'; -import { Web3Provider } from './constants'; +import { configLoader, NetworkMap } from '../config'; export class Provider { - static jsonRpcProviders: { [network: number]: JsonRpcProvider } = {}; + static jsonRpcProviders: NetworkMap = {}; + static getJsonRpcProvider(network: number): JsonRpcProvider { + const config = configLoader.getConfig(network); + if (!this.jsonRpcProviders[network]) { - if (!Web3Provider[network]) + if (!config.privateHttpArchiveProvider) throw new Error(`Provider not defined for network ${network}`); - this.jsonRpcProviders[network] = new JsonRpcProvider( - Web3Provider[network], - ); + this.jsonRpcProviders[network] = new JsonRpcProvider(config.privateHttpArchiveProvider); } return this.jsonRpcProviders[network]; } diff --git a/src/lib/staking/safety-module-helper.ts b/src/lib/staking/safety-module-helper.ts index f9ace86d..c5599fb9 100644 --- a/src/lib/staking/safety-module-helper.ts +++ b/src/lib/staking/safety-module-helper.ts @@ -7,8 +7,6 @@ import { Balancer_80PSP_20WETH_poolId, CHAIN_ID_MAINNET, MulticallEncodedData, - MULTICALL_ADDRESS, - PSP_ADDRESS, SAFETY_MODULE_ADDRESS, } from '../constants'; import { Provider } from '../provider'; @@ -16,6 +14,7 @@ import { Contract, BigNumber as EthersBN } from 'ethers'; import { Interface } from '@ethersproject/abi'; import { getTokenHolders } from '../utils/covalent'; import { DataByAccount, PSPStakesForStaker, StkPSPBPtState } from './types'; +import { Config, configLoader } from '../../config'; export class SafetyModuleHelper { private static instance: SafetyModuleHelper; @@ -33,11 +32,14 @@ export class SafetyModuleHelper { bVaultIface: Interface; erc20Iface: Interface; + private config: Config; + constructor() { + this.config = configLoader.getConfig(this.chainId); const provider = Provider.getJsonRpcProvider(this.chainId); this.multicallContract = new Contract( - MULTICALL_ADDRESS[this.chainId], + this.config.multicallV2Address, MultiCallerABI, provider, ); @@ -130,7 +132,7 @@ export class SafetyModuleHelper { target: BalancerVaultAddress, callData: this.bVaultIface.encodeFunctionData('getPoolTokenInfo', [ Balancer_80PSP_20WETH_poolId, - PSP_ADDRESS[this.chainId], + this.config.pspAddress, ]), }, ]; diff --git a/src/lib/staking/spsp-helper.ts b/src/lib/staking/spsp-helper.ts index dfb091ee..d4280867 100644 --- a/src/lib/staking/spsp-helper.ts +++ b/src/lib/staking/spsp-helper.ts @@ -3,9 +3,7 @@ import { Contract } from 'ethers'; import { CHAIN_ID_MAINNET, MulticallEncodedData, - MULTICALL_ADDRESS, NULL_ADDRESS, - PSP_ADDRESS, } from '../constants'; import { Provider } from '../provider'; import * as ERC20ABI from '../abi/erc20.abi.json'; @@ -20,12 +18,15 @@ import { PSPStakesForStaker, SPSPStakesByAccount, } from './types'; +import { configLoader } from '../../config'; const logger = global.LOGGER('SPSPHelper'); const chainId = CHAIN_ID_MAINNET; const provider = Provider.getJsonRpcProvider(chainId); +const config = configLoader.getConfig(CHAIN_ID_MAINNET); + export const SPSPAddresses = PoolConfigsMap[CHAIN_ID_MAINNET].filter( p => p.isActive, ).map(p => p.address.toLowerCase()); @@ -47,7 +48,7 @@ export class SPSPHelper { constructor() { this.multicallContract = new Contract( - MULTICALL_ADDRESS[chainId], + config.multicallV2Address, MultiCallerABI, provider, ); @@ -59,7 +60,7 @@ export class SPSPHelper { ); this.PSPContract = new Contract( - PSP_ADDRESS[this.chainId], + config.pspAddress, ERC20ABI, Provider.getJsonRpcProvider(this.chainId), ); @@ -135,7 +136,7 @@ export class SPSPHelper { this.SPSPPrototypeContract.interface.encodeFunctionData('pspsLocked'), }, { - target: PSP_ADDRESS[chainId], + target: config.pspAddress, callData: this.PSPContract.interface.encodeFunctionData('balanceOf', [ pool, ]), diff --git a/src/lib/staking/staking.ts b/src/lib/staking/staking.ts index 2b4aa038..616add87 100644 --- a/src/lib/staking/staking.ts +++ b/src/lib/staking/staking.ts @@ -90,8 +90,8 @@ export class StakingService { ); Object.entries(stkPSPBPtStakers).forEach( - ([account, pspStakedInStkPSPBPt]) => { - if (!pspStakedInStkPSPBPt) return; + ([account, _pspStakedInStkPSPBPt]) => { + if (!_pspStakedInStkPSPBPt) return; if (!pspStakersWithStakes[account]?.pspStaked) { pspStakersWithStakes[account] = { pspStaked: BigInt(0), @@ -99,6 +99,7 @@ export class StakingService { }; } + const pspStakedInStkPSPBPt = _pspStakedInStkPSPBPt as bigint; pspStakersWithStakes[account].pspStaked += pspStakedInStkPSPBPt; totalPSPStaked += pspStakedInStkPSPBPt; totalPSPStakedStkPSPBpt += pspStakedInStkPSPBPt; diff --git a/src/lib/swaps-tracker.ts b/src/lib/swaps-tracker.ts index a9e63cb9..5c4ea2be 100644 --- a/src/lib/swaps-tracker.ts +++ b/src/lib/swaps-tracker.ts @@ -5,12 +5,12 @@ import { Contract } from '@ethersproject/contracts'; import { ZeroXV4Address, ZeroXV2Address, - Web3Provider, CHAIN_ID_MAINNET, CHAIN_ID_BINANCE, CHAIN_ID_POLYGON, CHAIN_ID_AVALANCHE, CHAIN_ID_FANTOM, + CHAIN_ID_ROPSTEN, } from './constants'; import { PriceApi } from './price-api'; import { BlockInfo } from './block-info'; @@ -20,6 +20,16 @@ import { Utils } from './utils'; const logger = global.LOGGER(); +export const Web3Provider: { [network: number]: string } = { + [CHAIN_ID_MAINNET]: process.env.HTTP_PROVIDER || '', + [CHAIN_ID_ROPSTEN]: process.env.HTTP_PROVIDER_3 || '', + [CHAIN_ID_BINANCE]: process.env.HTTP_PROVIDER_56 || '', + [CHAIN_ID_POLYGON]: process.env.HTTP_PROVIDER_137 || '', + [CHAIN_ID_FANTOM]: process.env.HTTP_PROVIDER_250 || '', + [CHAIN_ID_AVALANCHE]: process.env.HTTP_PROVIDER_43114 || '', +}; + + export type Swap = { id: string; uuid: string | null; @@ -48,7 +58,6 @@ export type Swap = { timestamp: number; }; -const INIT_TIME = parseInt(process.env.INIT_TIME || '0'); //TODO: use the block info to the init time from the init block const defaultBlockDelay = 20; const defaultIndexRefreshDelay = 5 * 60 * 1000; diff --git a/src/lib/utils/covalent.ts b/src/lib/utils/covalent.ts index 9a0037bc..00ae0639 100644 --- a/src/lib/utils/covalent.ts +++ b/src/lib/utils/covalent.ts @@ -2,8 +2,11 @@ import { assert } from 'ts-essentials'; import { URLSearchParams } from 'url'; import { queryPaginatedData, QueryPaginatedDataParams } from './helpers'; import { covalentClient } from './data-providers-clients'; +import { configLoader } from '../../config'; -const COVALENT_API_KEY = process.env.COVALENT_API_KEY || 'ckey_docs'; // public, is rate-limited and unreliable +const config = configLoader.getGlobalConfig(); + +const COVALENT_API_KEY = config.covalentV1ApiKey || 'ckey_docs'; // public, is rate-limited and unreliable interface TokenHoldersOptions { token: string; diff --git a/src/lib/utils/data-providers-clients.ts b/src/lib/utils/data-providers-clients.ts index e661bbd8..4fc3ce7e 100644 --- a/src/lib/utils/data-providers-clients.ts +++ b/src/lib/utils/data-providers-clients.ts @@ -1,5 +1,8 @@ +import { configLoader } from '../../config'; import { constructHttpClient } from './http-client'; +const global = configLoader.getGlobalConfig(); + export const coingeckoClient = constructHttpClient({ axiosConfig: { baseURL: 'https://api.coingecko.com/api/v3', @@ -14,7 +17,7 @@ export const coingeckoClient = constructHttpClient({ export const covalentClient = constructHttpClient({ axiosConfig: { - baseURL: 'https://api.covalenthq.com/v1', + baseURL: global.covalentV1HttpUrl, timeout: 30_000, }, rateLimitOptions: { diff --git a/src/lib/volume-cache.ts b/src/lib/volume-cache.ts index 4536b569..df01aebd 100644 --- a/src/lib/volume-cache.ts +++ b/src/lib/volume-cache.ts @@ -13,7 +13,7 @@ const DAY = 1000 * 60 * 60 * 24; export const MAX_PERIOD = 120 * DAY // 60 days const CACHE_MAX_SIZE = 60 // days -const GC_INTERVAL = process.env.VOLUME_AGGREGATION_GC_INTERVAL || DAY / 2 // 12 hours +const GC_INTERVAL = DAY / 2 // 12 hours export class VolumesCache { private volumes: VolumeCache = {} diff --git a/src/lib/volume-tracker.ts b/src/lib/volume-tracker.ts index 859aa3ef..bbe51069 100644 --- a/src/lib/volume-tracker.ts +++ b/src/lib/volume-tracker.ts @@ -5,8 +5,6 @@ import * as moment from 'moment'; import type { JsonRpcProvider } from '@ethersproject/providers'; import { Contract } from '@ethersproject/contracts'; import { - AugustusV5Address, - AugustusV4Address, ZeroXV4Address, ZeroXV2Address, CHAIN_ID_MAINNET, @@ -18,19 +16,16 @@ import * as ZeroXV2Abi from './abi/zerox.v2.abi.json'; import * as ZeroXV4Abi from './abi/zerox.v4.abi.json'; import { generatePeriods, MAX_PERIOD, VolumesCache } from './volume-cache'; import { Op } from 'sequelize'; +import { Config } from '../config/index'; import { Sequelize } from 'sequelize-typescript'; import { DataType_USD_VALUE_DECIMALS } from './sql-data-types'; import Database from '../database'; import { Volume, VolumeAttributes } from '../models/Volume'; import { VolumeSyncStatus } from '../models/VolumeSyncStatus'; +import { configLoader } from '../config'; const logger = global.LOGGER(); -const INIT_TIME = parseInt(process.env.INIT_TIME || '0'); //TODO: use the block info to the init time from the init block -if (!INIT_TIME) { - throw new Error('VolumeTracker INIT_TIME env var is missing'); -} - const defaultBlockDelay = 20; const defaultIndexRefreshDelay = 5 * 60 * 1000; @@ -543,6 +538,8 @@ export class VolumeTracker { initBlock: number | null = null; marketMakerAddressMap: { [address: string]: string }; + protected config: Config; + static createInstance( initTime: number, network: number, @@ -586,9 +583,11 @@ export class VolumeTracker { ZeroXV4Abi, this.provider, ); + + this.config = configLoader.getConfig(network); this.takerAddresses = [ - AugustusV5Address[this.network], - AugustusV4Address[this.network], + this.config.augustusV4Address, + this.config.augustusAddress, ].filter(a => !!a); this.priceApi = new PriceApi(initTime, this.network); this.blockInfo = BlockInfo.getInstance(this.network); @@ -861,7 +860,7 @@ export class VolumeTracker { } while (toBlock < maxToBlockNumber); logger.info('Indexing completed'); } catch (e) { - if (reportFailure) logger.error('Transaction failed', e); + if (reportFailure) logger.error('Transaction failed', e, (e as Error).stack); } finally { this.isIndexing = false; } diff --git a/yarn.lock b/yarn.lock index 46fc64f6..17276d9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1801,13 +1801,21 @@ axios-retry@^3.2.4: "@babel/runtime" "^7.15.4" is-retry-allowed "^2.2.0" -axios@*, axios@0.24.0: +axios@*: version "0.24.0" resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== dependencies: follow-redirects "^1.14.4" +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + babel-jest@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" @@ -2927,6 +2935,11 @@ follow-redirects@^1.14.4: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== +follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -2936,6 +2949,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3172,6 +3194,11 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-status-codes@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be" + integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng== + https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"