txManager
This commit is contained in:
parent
383693bd44
commit
b152c67548
@ -62,9 +62,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||||||
|
|
||||||
## New relayer architecture
|
## New relayer architecture
|
||||||
|
|
||||||
1. TreeWatcher module keeps track of Account Tree changes and automatically caches the actual state in Redis
|
1. TreeWatcher module keeps track of Account Tree changes and automatically caches the actual state in Redis and emits `treeUpdate` event to redis pub/sub channel
|
||||||
2. Server module is Express.js instance to accepts http requests
|
2. Server module is Express.js instance that accepts http requests
|
||||||
3. Controller contains handlers for the Server endpoints. It validates input data and put a Job to Queue
|
3. Controller contains handlers for the Server endpoints. It validates input data and adds a Job to Queue.
|
||||||
4. Queue module is used by Controller to put and get Job from queue (bull wrapper)
|
4. Queue module is used by Controller to put and get Job from queue (bull wrapper)
|
||||||
5. Status module contains handler to get a Job status. It's used by UI for pull updates
|
5. Status module contains handler to get a Job status. It's used by UI for pull updates
|
||||||
6. Validate contains validation logic for all endpoints
|
6. Validate contains validation logic for all endpoints
|
||||||
|
@ -152,5 +152,5 @@ module.exports = {
|
|||||||
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
||||||
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
|
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
|
||||||
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20,
|
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20,
|
||||||
rewardAccount: '0x0000000000000000000000000000000000000000',
|
rewardAccount: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.5",
|
"ajv": "^6.12.5",
|
||||||
|
"async-mutex": "^0.2.4",
|
||||||
"bull": "^3.12.1",
|
"bull": "^3.12.1",
|
||||||
"circomlib": "git+https://github.com/tornadocash/circomlib.git#5beb6aee94923052faeecea40135d45b6ce6172c",
|
"circomlib": "git+https://github.com/tornadocash/circomlib.git#5beb6aee94923052faeecea40135d45b6ce6172c",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
169
src/TxManager.js
Normal file
169
src/TxManager.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
const Web3 = require('web3')
|
||||||
|
const { Mutex } = require('async-mutex')
|
||||||
|
const { GasPriceOracle } = require('gas-price-oracle')
|
||||||
|
const { toWei, toHex, toBN, BN } = require('web3-utils')
|
||||||
|
const { sleep, when } = require('./utils')
|
||||||
|
|
||||||
|
const nonceErrors = [
|
||||||
|
'Returned error: Transaction nonce is too low. Try incrementing the nonce.',
|
||||||
|
'Returned error: nonce too low',
|
||||||
|
]
|
||||||
|
|
||||||
|
const gasPriceErrors = [
|
||||||
|
'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.',
|
||||||
|
'Returned error: replacement transaction underpriced',
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
MAX_RETRIES: 10,
|
||||||
|
GAS_BUMP_PERCENTAGE: 5,
|
||||||
|
GAS_BUMP_INTERVAL: 1000 * 60 * 5,
|
||||||
|
MAX_GAS_PRICE: 1000,
|
||||||
|
POLL_INTERVAL: 3000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TxManager {
|
||||||
|
constructor({ privateKey, rpcUrl, broadcastNodes = [], config = {} }) {
|
||||||
|
this.config = Object.assign({ ...defaultConfig }, config)
|
||||||
|
this._privateKey = privateKey
|
||||||
|
this._web3 = new Web3(rpcUrl)
|
||||||
|
this._broadcastNodes = broadcastNodes
|
||||||
|
this.address = this._web3.eth.accounts.privateKeyToAccount('0x' + privateKey).address
|
||||||
|
this._web3.eth.accounts.wallet.add('0x' + privateKey)
|
||||||
|
this._web3.eth.defaultAccount = this.address
|
||||||
|
this._gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
|
||||||
|
this._mutex = new Mutex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo get rid of it
|
||||||
|
async init() {
|
||||||
|
this.nonce = await this.web3.eth.getTransactionCount(this.address, 'latest')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits transaction to Ethereum network. Resolves when tx gets enough confirmations.
|
||||||
|
* todo: return PromiEvent that emits progress events
|
||||||
|
*
|
||||||
|
* @param tx Transaction to send
|
||||||
|
*/
|
||||||
|
async submit(tx) {
|
||||||
|
const release = await this._mutex.acquire()
|
||||||
|
try {
|
||||||
|
await new Transaction(tx, this).submit()
|
||||||
|
} finally {
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Transaction {
|
||||||
|
constructor(tx, manager) {
|
||||||
|
Object.assign(this, manager)
|
||||||
|
this.manager = manager
|
||||||
|
this.tx = tx
|
||||||
|
this.retries = 0
|
||||||
|
this.hash = null
|
||||||
|
// store all submitted hashes to catch cases when an old tx is mined
|
||||||
|
// todo: what to do if old tx with the same nonce was submitted
|
||||||
|
// by other client and we don't have its hash?
|
||||||
|
this.hashes = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
await this._prepare()
|
||||||
|
await this._send()
|
||||||
|
// we could have bumped nonce during execution, so get the latest one + 1
|
||||||
|
this.manager.nonce = this.tx.nonce + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepare() {
|
||||||
|
this.tx.gas = await this._web3.eth.estimateGas(this.tx)
|
||||||
|
this.tx.gasPrice = await this._getGasPrice('fast')
|
||||||
|
this.tx.nonce = this.nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
async _send() {
|
||||||
|
const signedTx = await this._web3.eth.accounts.signTransaction(this.tx, this.privateKey)
|
||||||
|
this.tx.date = Date.now()
|
||||||
|
this.tx.hash = signedTx.transactionHash
|
||||||
|
this.hashes.push(this.tx.hash)
|
||||||
|
try {
|
||||||
|
await this._broadcast(signedTx.rawTransaction)
|
||||||
|
// The most reliable way to see if one of our tx was mined is to track current nonce
|
||||||
|
while(this.tx.nonce <= await this._getLastNonce()) {
|
||||||
|
if (Date.now() - this.tx.date >= this.config.GAS_BUMP_INTERVAL) {
|
||||||
|
if (this._increaseGasPrice()) {
|
||||||
|
return this._send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(this.config.POLL_INTERVAL)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await this._handleSendError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts tx to multiple nodes, waits for tx hash only on main node
|
||||||
|
*/
|
||||||
|
_broadcast(rawTx) {
|
||||||
|
const main = this._web3.eth.sendSignedTransaction(rawTx)
|
||||||
|
for (const node of this._broadcastNodes) {
|
||||||
|
try {
|
||||||
|
new Web3(node).eth.sendSignedTransaction(rawTx)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed to send transaction to node ${node}: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return when(main, 'transactionHash')
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSendError(e) {
|
||||||
|
console.log('Got error', e)
|
||||||
|
|
||||||
|
// nonce is too low, trying to increase and resubmit
|
||||||
|
if (nonceErrors.includes(e.message)) {
|
||||||
|
console.log(`Nonce ${this.tx.nonce} is too low, increasing and retrying`)
|
||||||
|
if (this.retries <= this.config.MAX_RETRIES) {
|
||||||
|
this.tx.nonce++
|
||||||
|
this.retries++
|
||||||
|
return this._send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there is already a pending tx with higher gas price, trying to bump and resubmit
|
||||||
|
if (gasPriceErrors.includes(e.message)) {
|
||||||
|
console.log(`Gas price ${this.tx.gasPrice} gwei is too low, increasing and retrying`)
|
||||||
|
this._increaseGasPrice()
|
||||||
|
return this._send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_increaseGasPrice() {
|
||||||
|
const newGasPrice = toBN(this.tx.gasPrice).mul(toBN(this.config.GAS_BUMP_PERCENTAGE)).div(toBN(100))
|
||||||
|
const maxGasPrice = toBN(toWei(this.config.MAX_GAS_PRICE.toString(), 'gwei'))
|
||||||
|
if (toBN(this.tx.gasPrice).eq(maxGasPrice)) {
|
||||||
|
console.log('Already at max gas price, not bumping')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
|
||||||
|
console.log(`Increasing gas price to ${this.tx.gasPrice}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getGasPrice(type) {
|
||||||
|
const gasPrices = await this._gasPriceOracle.gasPrices()
|
||||||
|
const result = gasPrices[type].toString()
|
||||||
|
console.log(`${type} gas price is now ${result} gwei`)
|
||||||
|
return toHex(toWei(gasPrices[type].toString(), 'gwei'))
|
||||||
|
}
|
||||||
|
|
||||||
|
_getLastNonce() {
|
||||||
|
return this.web3.eth.getTransactionCount(this.address, 'latest')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TxManager
|
||||||
|
|
15
src/utils.js
15
src/utils.js
@ -2,6 +2,8 @@ const { instances, netId } = require('../config')
|
|||||||
const { poseidon } = require('circomlib')
|
const { poseidon } = require('circomlib')
|
||||||
const { toBN } = require('web3-utils')
|
const { toBN } = require('web3-utils')
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(res => setTimeout(res, ms))
|
||||||
|
|
||||||
function getInstance(address) {
|
function getInstance(address) {
|
||||||
const inst = instances[`netId${netId}`]
|
const inst = instances[`netId${netId}`]
|
||||||
for (const currency of Object.keys(inst)) {
|
for (const currency of Object.keys(inst)) {
|
||||||
@ -33,8 +35,21 @@ function setSafeInterval(func, interval) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A promise that resolves when the source emits specified event
|
||||||
|
*/
|
||||||
|
function when(source, event) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
source.once(event, payload => {
|
||||||
|
resolve(payload)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getInstance,
|
getInstance,
|
||||||
setSafeInterval,
|
setSafeInterval,
|
||||||
poseidonHash2,
|
poseidonHash2,
|
||||||
|
sleep,
|
||||||
|
when,
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ ajv.addKeyword('isKnownContract', {
|
|||||||
errors: true
|
errors: true
|
||||||
})
|
})
|
||||||
|
|
||||||
ajv.addKeyword('isForFee', {
|
ajv.addKeyword('isFeeRecipient', {
|
||||||
validate: (schema, data) => {
|
validate: (schema, data) => {
|
||||||
try {
|
try {
|
||||||
return rewardAccount === data
|
return rewardAccount === data
|
||||||
@ -42,10 +42,8 @@ const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress:
|
|||||||
const proofType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' }
|
const proofType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' }
|
||||||
const encryptedAccountType = { type: 'string', pattern: '^0x[a-fA-F0-9]{392}$' }
|
const encryptedAccountType = { type: 'string', pattern: '^0x[a-fA-F0-9]{392}$' }
|
||||||
const bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' }
|
const bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' }
|
||||||
const instanceType = JSON.parse(JSON.stringify(addressType))
|
const instanceType = { ...addressType, isKnownContract: true }
|
||||||
instanceType.isKnownContract = true
|
const relayerType = { ...addressType, isFeeRecipient: true }
|
||||||
const relayerType = JSON.parse(JSON.stringify(addressType))
|
|
||||||
relayerType.isForFee = true
|
|
||||||
|
|
||||||
const tornadoWithdrawSchema = {
|
const tornadoWithdrawSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -56,7 +54,6 @@ const tornadoWithdrawSchema = {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
maxItems: 6,
|
maxItems: 6,
|
||||||
minItems: 6,
|
minItems: 6,
|
||||||
uniqueItems: true,
|
|
||||||
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type]
|
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,6 +16,7 @@ redisSubscribe.subscribe('treeUpdate', fetchTree)
|
|||||||
let web3
|
let web3
|
||||||
let nonce
|
let nonce
|
||||||
let currentTx
|
let currentTx
|
||||||
|
let currentJob
|
||||||
let tree
|
let tree
|
||||||
|
|
||||||
async function fetchTree() {
|
async function fetchTree() {
|
||||||
@ -30,13 +31,17 @@ async function fetchTree() {
|
|||||||
|
|
||||||
async function watcher() {
|
async function watcher() {
|
||||||
if (currentTx && Date.now() - currentTx.date > gasBumpInterval) {
|
if (currentTx && Date.now() - currentTx.date > gasBumpInterval) {
|
||||||
|
bumpGasPrice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bumpGasPrice() {
|
||||||
const newGasPrice = toBN(currentTx.gasPrice).mul(toBN(gasBumpPercentage)).div(toBN(100))
|
const newGasPrice = toBN(currentTx.gasPrice).mul(toBN(gasBumpPercentage)).div(toBN(100))
|
||||||
const maxGasPrice = toBN(toWei(maxGasPrice.toString(), 'Gwei'))
|
const maxGasPrice = toBN(toWei(maxGasPrice.toString(), 'Gwei'))
|
||||||
currentTx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
|
currentTx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
|
||||||
currentTx.date = Date.now()
|
currentTx.date = Date.now()
|
||||||
console.log(`Resubmitting with gas price ${fromWei(currentTx.gasPrice.toString(), 'gwei')} gwei`)
|
console.log(`Resubmitting with gas price ${fromWei(currentTx.gasPrice.toString(), 'gwei')} gwei`)
|
||||||
//await this.sendTx(tx, null, 9999)
|
await sendTx(currentTx, updateTxHash)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@ -49,7 +54,6 @@ async function init() {
|
|||||||
setSafeInterval(watcher, 1000)
|
setSafeInterval(watcher, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function checkTornadoFee(contract, fee, refund) {
|
async function checkTornadoFee(contract, fee, refund) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -58,6 +62,7 @@ async function process(job) {
|
|||||||
if (job.type !== 'tornadoWithdraw') {
|
if (job.type !== 'tornadoWithdraw') {
|
||||||
throw new Error('not implemented')
|
throw new Error('not implemented')
|
||||||
}
|
}
|
||||||
|
currentJob = job
|
||||||
console.log(Date.now(), ' withdraw started', job.id)
|
console.log(Date.now(), ' withdraw started', job.id)
|
||||||
const { proof, args, contract } = job.data
|
const { proof, args, contract } = job.data
|
||||||
const fee = toBN(args[4])
|
const fee = toBN(args[4])
|
||||||
@ -68,7 +73,7 @@ async function process(job) {
|
|||||||
const instance = new web3.eth.Contract(tornadoABI, contract)
|
const instance = new web3.eth.Contract(tornadoABI, contract)
|
||||||
const data = instance.methods.withdraw(proof, ...args).encodeABI()
|
const data = instance.methods.withdraw(proof, ...args).encodeABI()
|
||||||
const gasPrices = await gasPriceOracle.gasPrices()
|
const gasPrices = await gasPriceOracle.gasPrices()
|
||||||
const tx = {
|
currentTx = {
|
||||||
from: web3.eth.defaultAccount,
|
from: web3.eth.defaultAccount,
|
||||||
value: numberToHex(refund),
|
value: numberToHex(refund),
|
||||||
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||||
@ -77,22 +82,66 @@ async function process(job) {
|
|||||||
data,
|
data,
|
||||||
nonce,
|
nonce,
|
||||||
}
|
}
|
||||||
// nonce++ later
|
|
||||||
|
|
||||||
const gas = await web3.eth.estimateGas(tx)
|
try {
|
||||||
tx.gas = gas
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
currentTx.gas = await web3.eth.estimateGas(currentTx)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('Revert', e)
|
||||||
|
throw new Error(`Revert by smart contract ${e.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce++
|
||||||
|
await sendTx(currentTx, updateTxHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForTx(hash) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTxHash(txHash) {
|
||||||
|
console.log(`A new successfully sent tx ${txHash}`)
|
||||||
|
currentJob.data.txHash = txHash
|
||||||
|
await currentJob.update(currentJob.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTx(tx, onTxHash, retryAttempt) {
|
||||||
let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey)
|
let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey)
|
||||||
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
|
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
|
||||||
|
|
||||||
result.once('transactionHash', async (txHash) => {
|
if (onTxHash) {
|
||||||
console.log(`A new successfully sent tx ${txHash}`)
|
result.once('transactionHash', onTxHash)
|
||||||
job.data.txHash = txHash
|
}
|
||||||
await job.update(job.data)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
try { // await returns once tx is mined
|
||||||
await result
|
await result
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Error for tx with nonce ${tx.nonce}\n${e.message}`)
|
||||||
|
if (nonceErrors.includes(e.message)) {
|
||||||
|
console.log('nonce too low, retrying')
|
||||||
|
if (retryAttempt <= 10) {
|
||||||
|
tx.nonce++
|
||||||
|
return sendTx(tx, onTxHash, retryAttempt + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gasPriceErrors.includes(e.message)) {
|
||||||
|
return bumpGasPrice()
|
||||||
|
}
|
||||||
|
throw new Error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nonceErrors = [
|
||||||
|
'Returned error: Transaction nonce is too low. Try incrementing the nonce.',
|
||||||
|
'Returned error: nonce too low',
|
||||||
|
]
|
||||||
|
|
||||||
|
const gasPriceErrors = [
|
||||||
|
'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.',
|
||||||
|
'Returned error: replacement transaction underpriced',
|
||||||
|
]
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await init()
|
await init()
|
||||||
|
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -322,6 +322,13 @@ async-limiter@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
||||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||||
|
|
||||||
|
async-mutex@^0.2.4:
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.2.4.tgz#f6ea5f9cc73147f395f86fa573a2af039fe63082"
|
||||||
|
integrity sha512-fcQKOXUKMQc57JlmjBCHtkKNrfGpHyR7vu18RfuLfeTAf4hK9PgOadPR5cDrBQ682zasrLUhJFe7EKAHJOduDg==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.0.0"
|
||||||
|
|
||||||
asynckit@^0.4.0:
|
asynckit@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||||
@ -3540,6 +3547,11 @@ tslib@^1.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
||||||
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
|
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
|
||||||
|
|
||||||
|
tslib@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
|
||||||
|
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
|
||||||
|
|
||||||
tunnel-agent@^0.6.0:
|
tunnel-agent@^0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||||
|
Loading…
Reference in New Issue
Block a user