3 Commits

Author SHA1 Message Date
dependabot[bot]
510347a80b Bump json5 from 1.0.1 to 1.0.2
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-09 05:34:08 +00:00
Pasha8914
4861f36c56 🚑 ability to configure min maxPriorityFeePerGas 2022-10-14 18:07:31 +10:00
Pasha8914
06b380b488 feat: cached gas prices 2022-08-18 23:17:00 +10:00
16 changed files with 203 additions and 28 deletions

View File

@@ -77,6 +77,8 @@ type GasOracleOptions = {
defaultRpc?: string
blocksCount?: number
percentile?: number
blockTime?: number // seconds
shouldCache?: boolean
fallbackGasPrices?: FallbackGasPrices
}
@@ -85,7 +87,10 @@ const options: GasOracleOptions = {
percentile: 5, // Which percentile of effective priority fees to include
blocksCount: 10, // How many blocks to consider for priority fee estimation
defaultRpc: 'https://api.mycryptoapi.com/eth',
blockTime: 10, // seconds
shouldCache: false,
timeout: 10000, // specifies the number of milliseconds before the request times out.
minPriority: 0, // specifies the min maxPriorityFeePerGas.
fallbackGasPrices: {
gasPrices: {
instant: 28,
@@ -101,6 +106,14 @@ const options: GasOracleOptions = {
}
```
### The Oracle can cache rpc calls
For caching needs to provide to GasOracleOptions
`shouldCache: true`
`blockTime: <Chain block time duration>`
### EIP-1559 (estimated) gasPrice only
```typescript

View File

@@ -1,6 +1,6 @@
{
"name": "gas-price-oracle",
"version": "0.5.0",
"version": "0.5.2",
"description": "Gas Price Oracle library for Ethereum dApps.",
"homepage": "https://github.com/peppersec/gas-price-oracle",
"main": "./lib/index.js",
@@ -57,7 +57,8 @@
},
"dependencies": {
"axios": "^0.21.2",
"bignumber.js": "^9.0.0"
"bignumber.js": "^9.0.0",
"node-cache": "^5.1.2"
},
"files": [
"lib/**/*"

View File

@@ -11,4 +11,17 @@ const INT_PRECISION = 0
const SUCCESS_STATUS = 200
const BG_ZERO = new BigNumber(0)
const PERCENT_MULTIPLIER = 100
export { GWEI, DEFAULT_TIMEOUT, ROUND_UP, ROUND_DOWN, GWEI_PRECISION, INT_PRECISION, SUCCESS_STATUS, BG_ZERO, PERCENT_MULTIPLIER }
const DEFAULT_BLOCK_DURATION = 10
export {
GWEI,
DEFAULT_TIMEOUT,
ROUND_UP,
ROUND_DOWN,
GWEI_PRECISION,
INT_PRECISION,
SUCCESS_STATUS,
BG_ZERO,
PERCENT_MULTIPLIER,
DEFAULT_BLOCK_DURATION,
}

View File

@@ -0,0 +1,20 @@
import NodeCache, { Options } from 'node-cache'
export class NodeJSCache<T> {
private nodeCache: NodeCache
constructor(params: Options) {
this.nodeCache = new NodeCache(params)
}
async get(key: string): Promise<T | undefined> {
return await this.nodeCache.get<T>(key)
}
async set(key: string, value: T): Promise<boolean> {
return await this.nodeCache.set(key, value)
}
async has(key: string): Promise<boolean> {
return await this.nodeCache.has(key)
}
}

View File

@@ -0,0 +1 @@
export * from './cacheNode'

View File

@@ -3,23 +3,29 @@ import BigNumber from 'bignumber.js'
import { FeeHistory, Block } from '@/types'
import { Config, EstimateOracle, EstimatedGasPrice, CalculateFeesParams, GasEstimationOptionsPayload } from './types'
import { RpcFetcher } from '@/services'
import { ChainId, NETWORKS } from '@/config'
import { BG_ZERO, PERCENT_MULTIPLIER } from '@/constants'
import { RpcFetcher, NodeJSCache } from '@/services'
import { findMax, fromNumberToHex, fromWeiToGwei, getMedian } from '@/utils'
import { BG_ZERO, DEFAULT_BLOCK_DURATION, PERCENT_MULTIPLIER } from '@/constants'
import { DEFAULT_PRIORITY_FEE, PRIORITY_FEE_INCREASE_BOUNDARY, FEE_HISTORY_BLOCKS, FEE_HISTORY_PERCENTILE } from './constants'
// !!! MAKE SENSE ALL CALCULATIONS IN GWEI !!!
export class Eip1559GasPriceOracle implements EstimateOracle {
public configuration: Config = {
shouldCache: false,
chainId: ChainId.MAINNET,
fallbackGasPrices: undefined,
minPriority: DEFAULT_PRIORITY_FEE,
blockTime: DEFAULT_BLOCK_DURATION,
blocksCount: NETWORKS[ChainId.MAINNET].blocksCount,
percentile: NETWORKS[ChainId.MAINNET].percentile,
fallbackGasPrices: undefined,
}
private fetcher: RpcFetcher
private cache: NodeJSCache<EstimatedGasPrice>
private FEES_KEY = (chainId: ChainId) => `estimate-fee-${chainId}`
constructor({ fetcher, ...options }: GasEstimationOptionsPayload) {
this.fetcher = fetcher
const chainId = options?.chainId || this.configuration.chainId
@@ -29,10 +35,19 @@ export class Eip1559GasPriceOracle implements EstimateOracle {
if (options) {
this.configuration = { ...this.configuration, ...options }
}
this.cache = new NodeJSCache({ stdTTL: this.configuration.blockTime, useClones: false })
}
public async estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice> {
try {
const cacheKey = this.FEES_KEY(this.configuration.chainId)
const cachedFees = await this.cache.get(cacheKey)
if (cachedFees) {
return cachedFees
}
const { data: latestBlock } = await this.fetcher.makeRpcCall<{ result: Block }>({
method: 'eth_getBlockByNumber',
params: ['latest', false],
@@ -52,7 +67,12 @@ export class Eip1559GasPriceOracle implements EstimateOracle {
params: [blockCount, 'latest', rewardPercentiles],
})
return this.calculateFees({ baseFee, feeHistory: data.result })
const fees = await this.calculateFees({ baseFee, feeHistory: data.result })
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, fees)
}
return fees
} catch (err) {
if (fallbackGasPrices) {
return fallbackGasPrices
@@ -116,8 +136,10 @@ export class Eip1559GasPriceOracle implements EstimateOracle {
private async calculateFees({ baseFee, feeHistory }: CalculateFeesParams): Promise<EstimatedGasPrice> {
const estimatedPriorityFee = await this.getPriorityFromChain(feeHistory)
const { highest: maxPriorityFeePerGas } = findMax([estimatedPriorityFee ?? BG_ZERO, new BigNumber(DEFAULT_PRIORITY_FEE)])
const { highest: maxPriorityFeePerGas } = findMax([
estimatedPriorityFee ?? BG_ZERO,
new BigNumber(this.configuration.minPriority),
])
const maxFeePerGas = baseFee.plus(maxPriorityFeePerGas)
if (this.checkIsGreaterThanMax(maxFeePerGas) || this.checkIsGreaterThanMax(maxPriorityFeePerGas)) {

View File

@@ -23,6 +23,8 @@ export type Options = {
chainId?: number
blocksCount?: number
percentile?: number
blockTime?: number
shouldCache?: boolean
fallbackGasPrices: EstimatedGasPrice | undefined
}
@@ -30,7 +32,7 @@ export type GasEstimationOptionsPayload = Options & {
fetcher: RpcFetcher
}
export type Config = Required<Options> & { fallbackGasPrices?: EstimatedGasPrice }
export type Config = Required<Options> & { fallbackGasPrices?: EstimatedGasPrice; minPriority: number }
export abstract class EstimateOracle {
public configuration: Config
public abstract estimateFees(fallbackGasPrices?: EstimatedGasPrice): Promise<EstimatedGasPrice>

View File

@@ -36,6 +36,9 @@ export type GasOracleOptions = {
defaultRpc?: string
blocksCount?: number
percentile?: number
blockTime?: number
shouldCache?: boolean
minPriority?: number
fallbackGasPrices?: FallbackGasPrices
}

View File

@@ -3,4 +3,5 @@ export * from './gas-price-oracle'
export * from './gas-estimation'
export * from './legacy-gas-price'
export * from './cacher'
export * from './rpcFetcher'

View File

@@ -2,9 +2,9 @@ const DEFAULT_GAS_PRICE = { instant: 0, fast: 0, standard: 0, low: 0 }
const MULTIPLIERS = {
instant: 1.3,
standard: 0.85,
low: 0.5,
fast: 1,
fast: 1.2,
standard: 1.1,
low: 1,
}
export { MULTIPLIERS, DEFAULT_GAS_PRICE }

View File

@@ -14,9 +14,9 @@ import {
GetGasPriceFromRespInput,
} from './types'
import { RpcFetcher } from '@/services'
import { ChainId, NETWORKS } from '@/config'
import { GWEI, DEFAULT_TIMEOUT, GWEI_PRECISION } from '@/constants'
import { RpcFetcher, NodeJSCache } from '@/services'
import { GWEI, DEFAULT_TIMEOUT, GWEI_PRECISION, DEFAULT_BLOCK_DURATION } from '@/constants'
import { MULTIPLIERS, DEFAULT_GAS_PRICE } from './constants'
@@ -95,22 +95,27 @@ export class LegacyGasPriceOracle implements LegacyOracle {
public onChainOracles: OnChainOracles = {}
public offChainOracles: OffChainOracles = {}
public configuration: Required<LegacyOptions> = {
shouldCache: false,
chainId: ChainId.MAINNET,
timeout: DEFAULT_TIMEOUT,
blockTime: DEFAULT_BLOCK_DURATION,
defaultRpc: NETWORKS[ChainId.MAINNET].rpcUrl,
fallbackGasPrices: LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice),
}
private readonly fetcher: RpcFetcher
private cache: NodeJSCache<GasPrice>
private LEGACY_KEY = (chainId: ChainId) => `legacy-fee-${chainId}`
constructor({ fetcher, ...options }: LegacyOptionsPayload) {
this.fetcher = fetcher
if (options) {
this.configuration = { ...this.configuration, ...options }
}
const fallbackGasPrices =
this.configuration.fallbackGasPrices || LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice)
const { defaultGasPrice } = NETWORKS[ChainId.MAINNET]
const fallbackGasPrices = this.configuration.fallbackGasPrices || LegacyGasPriceOracle.getMultipliedPrices(defaultGasPrice)
this.configuration.fallbackGasPrices = LegacyGasPriceOracle.normalize(fallbackGasPrices)
const network = NETWORKS[this.configuration.chainId]?.oracles
@@ -118,6 +123,8 @@ export class LegacyGasPriceOracle implements LegacyOracle {
this.offChainOracles = { ...network.offChainOracles }
this.onChainOracles = { ...network.onChainOracles }
}
this.cache = new NodeJSCache({ stdTTL: this.configuration.blockTime, useClones: false })
}
public addOffChainOracle(oracle: OffChainOracle): void {
@@ -228,9 +235,19 @@ export class LegacyGasPriceOracle implements LegacyOracle {
this.lastGasPrice = fallbackGasPrices || this.configuration.fallbackGasPrices
}
const cacheKey = this.LEGACY_KEY(this.configuration.chainId)
const cachedFees = await this.cache.get(cacheKey)
if (cachedFees) {
return cachedFees
}
if (Object.keys(this.offChainOracles).length > 0) {
try {
this.lastGasPrice = await this.fetchGasPricesOffChain(shouldGetMedian)
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, this.lastGasPrice)
}
return this.lastGasPrice
} catch (e) {
console.error('Failed to fetch gas prices from offchain oracles...')
@@ -240,7 +257,11 @@ export class LegacyGasPriceOracle implements LegacyOracle {
if (Object.keys(this.onChainOracles).length > 0) {
try {
const fastGas = await this.fetchGasPricesOnChain()
this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, this.lastGasPrice)
}
return this.lastGasPrice
} catch (e) {
console.error('Failed to fetch gas prices from onchain oracles...')
@@ -249,7 +270,11 @@ export class LegacyGasPriceOracle implements LegacyOracle {
try {
const fastGas = await this.fetchGasPriceFromRpc()
this.lastGasPrice = LegacyGasPriceOracle.getCategorize(fastGas)
if (this.configuration.shouldCache) {
await this.cache.set(cacheKey, this.lastGasPrice)
}
return this.lastGasPrice
} catch (e) {
console.error('Failed to fetch gas prices from default RPC. Last known gas will be returned')

View File

@@ -36,7 +36,9 @@ export type GasPrice = Record<GasPriceKey, number>
export type LegacyOptions = {
chainId?: number
timeout?: number
blockTime?: number
defaultRpc?: string
shouldCache?: boolean
fallbackGasPrices?: GasPrice
}

View File

@@ -10,6 +10,7 @@ import chaiAsPromised from 'chai-as-promised'
import mockery from 'mockery'
import { before, describe } from 'mocha'
import { sleep } from '@/utils'
import { ChainId, NETWORKS } from '@/config'
import { GWEI_PRECISION } from '@/constants'
@@ -32,7 +33,7 @@ beforeEach('beforeEach', function () {
oracle = new GasPriceOracle()
})
const INJECTED_RPC_URL = 'https://ethereum-rpc.trustwalletapp.com'
const INJECTED_RPC_URL = 'https://cloudflare-eth.com'
describe('eip-1559 gasOracle', function () {
describe('eip constructor', function () {
it('should set default values', function () {
@@ -61,14 +62,14 @@ describe('eip-1559 gasOracle', function () {
describe(`estimateGas ${chainId}`, function () {
it('should return error if not eip-1559 not supported', async function () {
if (chainId === ChainId.OPTIMISM || chainId === ChainId.ARBITRUM || chainId === ChainId.BSC) {
if (chainId === ChainId.OPTIMISM || chainId === ChainId.BSC) {
await eipOracle.eip1559
.estimateFees()
.should.be.rejectedWith('An error occurred while fetching current base fee, falling back')
}
})
if (chainId === ChainId.OPTIMISM || chainId === ChainId.ARBITRUM || chainId === ChainId.BSC) {
if (chainId === ChainId.OPTIMISM || chainId === ChainId.BSC) {
return
}
@@ -122,6 +123,34 @@ describe('eip-1559 gasOracle', function () {
estimateGas.maxFeePerGas.should.be.at.equal(estimatedMaxFee)
}
})
it('should cache', async function () {
eipOracle = new GasPriceOracle({ shouldCache: true, chainId })
const estimateGasFirst: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
await sleep(2000)
const estimateGasSecond: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
if (estimateGasFirst?.maxFeePerGas) {
estimateGasFirst.maxFeePerGas.should.be.at.equal(estimateGasSecond?.maxFeePerGas)
}
await sleep(4000)
const estimateGasThird: EstimatedGasPrice = await eipOracle.eip1559.estimateFees()
if (estimateGasSecond?.maxFeePerGas) {
estimateGasSecond.maxFeePerGas.should.be.at.equal(estimateGasThird?.maxFeePerGas)
}
})
})
})
describe('estimate ARBITRUM', function () {
it('should be priority 0', async function () {
const eipOracle = new GasPriceOracle({ minPriority: 0, chainId: ChainId.ARBITRUM })
const estimateGas: EstimatedGasPrice = await eipOracle.eip1559.estimateFees(FALLBACK_ESTIMATE)
console.log('estimateGas.maxPriorityFeePerGas', estimateGas.maxPriorityFeePerGas)
estimateGas.maxPriorityFeePerGas.should.be.at.equal(0)
})
})
})

View File

@@ -8,6 +8,7 @@ import mockery from 'mockery'
import BigNumber from 'bignumber.js'
import { before, describe } from 'mocha'
import { sleep } from '@/utils'
import { ChainId, NETWORKS } from '@/config'
import { DEFAULT_TIMEOUT } from '@/constants'
import { GasPriceOracle } from '@/services/gas-price-oracle'
@@ -41,7 +42,7 @@ beforeEach('beforeEach', function () {
;({ onChainOracles, offChainOracles } = oracle.legacy)
})
const INJECTED_RPC_URL = 'https://ethereum-rpc.trustwalletapp.com'
const INJECTED_RPC_URL = 'https://cloudflare-eth.com'
describe('legacy gasOracle', function () {
describe('legacy constructor', function () {
@@ -77,6 +78,7 @@ describe('legacy gasOracle', function () {
mockery.enable({ useCleanCache: true, warnOnUnregistered: false })
const { GasPriceOracle } = require('../index')
oracle = new GasPriceOracle()
// @ts-ignore
await oracle.legacy.fetchGasPricesOffChain(true).should.be.rejectedWith('All oracles are down. Probably a network error.')
mockery.disable()
})
@@ -105,6 +107,7 @@ describe('legacy gasOracle', function () {
it('should remove oracle', async function () {
await oracle.legacy.fetchGasPricesOnChain()
oracle.legacy.removeOnChainOracle('chainlink')
// @ts-ignore
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
})
@@ -113,6 +116,7 @@ describe('legacy gasOracle', function () {
await oracle.legacy.fetchGasPricesOnChain()
oracle.legacy.removeOnChainOracle('chainlink')
// @ts-ignore
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
oracle.legacy.addOnChainOracle(toAdd)
@@ -127,6 +131,7 @@ describe('legacy gasOracle', function () {
const { GasPriceOracle } = require('../index')
oracle = new GasPriceOracle()
// @ts-ignore
await oracle.legacy.fetchGasPricesOnChain().should.be.rejectedWith('All oracles are down. Probably a network error.')
mockery.disable()
})
@@ -157,6 +162,7 @@ describe('legacy gasOracle', function () {
const { GasPriceOracle } = require('../index')
oracle = new GasPriceOracle()
// @ts-ignore
await oracle.legacy.fetchGasPriceFromRpc().should.be.rejectedWith('Default RPC is down. Probably a network error.')
mockery.disable()
})
@@ -185,7 +191,7 @@ describe('legacy gasOracle', function () {
const gas = (await oracle.gasPrices({ isLegacy: true })) as unknown as GasPrice
const shouldBe = LegacyGasPriceOracle.getMultipliedPrices(NETWORKS[ChainId.MAINNET].defaultGasPrice)
const shouldBe = LegacyGasPriceOracle.getCategorize(NETWORKS[ChainId.MAINNET].defaultGasPrice)
gas.instant.should.be.equal(shouldBe.instant)
gas.fast.should.be.equal(shouldBe.fast)
@@ -217,6 +223,25 @@ describe('legacy gasOracle', function () {
mockery.disable()
})
it('should cache', async function () {
const oracle = new GasPriceOracle({ shouldCache: true })
const gasPricesFirst = await oracle.legacy.gasPrices()
await sleep(2000)
const gasPricesSecond = await oracle.legacy.gasPrices()
if (gasPricesFirst.fast) {
gasPricesFirst.fast.should.be.at.equal(gasPricesSecond?.fast)
}
await sleep(4000)
const gasPricesThird = await oracle.legacy.gasPrices()
if (gasPricesSecond.fast) {
gasPricesSecond.fast.should.be.at.equal(gasPricesThird?.fast)
}
})
})
describe('median', function () {

View File

@@ -1,2 +1,8 @@
export * from './math'
export * from './crypto'
const sleep = (time: number): Promise<boolean> => {
return new Promise((res) => setTimeout(() => res(true), time))
}
export { sleep }

View File

@@ -467,6 +467,11 @@ cliui@^5.0.0:
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"
clone@2.x:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -1338,9 +1343,9 @@ json-stable-stringify-without-jsonify@^1.0.1:
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
@@ -1437,9 +1442,9 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
mkdirp@0.5.5:
version "0.5.5"
@@ -1520,6 +1525,13 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
node-cache@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
dependencies:
clone "2.x"
node-environment-flags@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"