diff --git a/build-and-push.sh b/build-and-push.sh new file mode 100755 index 000000000..e343b272d --- /dev/null +++ b/build-and-push.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -e +# Check for environment argument +if [[ "$1" != "dev" && "$1" != "prod" ]]; then + echo "Error: Please specify environment (dev or prod) as first argument" + echo "Usage: $0 " + exit 1 +fi + +if [[ -z "$2" ]]; then + if [[ "$1" == "prod" ]]; then + echo "Error: Second argument is not provided" + echo "Usage: $0 prod " + exit 1 + else + TAG="latest" + fi +else + TAG="$2" +fi + +# Set account based on environment +if [[ "$1" == "dev" ]]; then + ACCOUNT="371717752603" # sandbox account +else + ACCOUNT="001138754299" # prod account +fi + +# Set platform arguments if on ARM +platform='' +platformarg='' +if [[ "$(uname -p)" == "arm" ]]; then + platform='--platform' + platformarg='linux/amd64' +fi + +# Login to ECR +set -x +aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.us-west-2.amazonaws.com + +# Build and push each image +image_name="eco-solver" +dockerfile="./Dockerfile" +build_context="." + +echo "Building ${image_name} using ${dockerfile}..." + +# Build the image with correct context +docker build "${build_context}" -f "${dockerfile}" \ + -t "${ACCOUNT}.dkr.ecr.us-west-2.amazonaws.com/${image_name}:latest" \ + ${platform:+"$platform=$platformarg"} + +# Tag with git tag if available +if [[ "${TAG}" != "latest" ]]; then + docker tag "${ACCOUNT}.dkr.ecr.us-west-2.amazonaws.com/${image_name}:latest" \ + "${ACCOUNT}.dkr.ecr.us-west-2.amazonaws.com/${image_name}:${TAG}" + docker push "${ACCOUNT}.dkr.ecr.us-west-2.amazonaws.com/${image_name}:${TAG}" +fi + + +# Push latest tag +docker push "${ACCOUNT}.dkr.ecr.us-west-2.amazonaws.com/${image_name}:latest" diff --git a/config/default.ts b/config/default.ts index af61d08a0..2e4f8782a 100644 --- a/config/default.ts +++ b/config/default.ts @@ -1,16 +1,16 @@ export default { aws: [ { - region: 'us-east-2', - secretID: 'eco-solver-secrets-dev', + region: 'us-west-2', + secretID: 'eco-solver-secrets-prod-test', }, { - region: 'us-east-2', - secretID: 'eco-solver-configs-dev', + region: 'us-west-2', + secretID: 'eco-solver-configs-prod-test', }, { - region: 'us-east-2', - secretID: 'eco-solver-whitelist-dev', + region: 'us-west-2', + secretID: 'eco-solver-whitelist-prod-test', }, ], cache: { @@ -127,9 +127,6 @@ export default { defaultBlockTime: 2, }, - indexer: { - url: 'https://indexer.eco.com', - }, withdraws: { chunkSize: 20, intervalDuration: 360_000, @@ -186,9 +183,16 @@ export default { useHyperlaneDefaultHook: false, }, + warpRoutes: {}, + safe: {}, + crowdLiquidity: {}, + fulfillment: { + run: 'single', + }, + externalAPIs: {}, logger: { - usePino: true, + usePino: false, pinoConfig: { pinoHttp: { level: 'debug', diff --git a/config/development.ts b/config/development.ts index c1e4f4c37..04579b3a8 100644 --- a/config/development.ts +++ b/config/development.ts @@ -2,6 +2,24 @@ export default { server: { url: 'http://localhost:3000', }, + aws: [ + { + region: 'us-west-2', + secretID: 'eco-solver-secrets-prod', + }, + { + region: 'us-west-2', + secretID: 'eco-solver-configs-prod', + }, + { + region: 'us-west-2', + secretID: 'eco-solver-configs-chains-prod', + }, + { + region: 'us-west-2', + secretID: 'eco-solver-whitelist-prod', + }, + ], logger: { usePino: false, @@ -32,70 +50,6 @@ export default { }, }, }, - intentSources: [ - { - network: 'opt-sepolia', - chainID: 11155420, - tokens: [ - '0x5fd84259d66Cd46123540766Be93DFE6D43130D7', //usdc - '0x8327Db9040811545C13331A453aBe9C7AA1aCDf8', - '0x368d7C52B0F62228907C133204605a5B11A1dB6d', - '0x00D2d1162c689179e8bA7a3b936f80A010A0b5CF', - '0x3328C29843F7c7dfF7381aF54A03C7423431Eaa4', - '0xd3F4Bef596a04e2be4fbeB17Dd70f02F717c5a6c', - '0x93551e3F61F8E3EE73DDc096BddbC1ADc52f5A3a', - ], - }, - { - network: 'base-sepolia', - chainID: 84532, - tokens: [ - '0xAb1D243b07e99C91dE9E4B80DFc2B07a8332A2f7', //usdc - '0x8bDa9F5C33FBCB04Ea176ea5Bc1f5102e934257f', - '0x93551e3F61F8E3EE73DDc096BddbC1ADc52f5A3a', - ], - }, - ], - solvers: { - //base sepolia - 84532: { - targets: { - //base sepolia USDC - '0xAb1D243b07e99C91dE9E4B80DFc2B07a8332A2f7': { - contractType: 'erc20', - selectors: ['transfer(address,uint256)'], - minBalance: 1000, - }, - '0x8bDa9F5C33FBCB04Ea176ea5Bc1f5102e934257f': { - contractType: 'erc20', - selectors: ['transfer(address,uint256)'], - minBalance: 1000, - }, - '0x93551e3F61F8E3EE73DDc096BddbC1ADc52f5A3a': { - contractType: 'erc20', - selectors: ['transfer(address,uint256)'], - minBalance: 1000, - }, - }, - network: 'base-sepolia', - chainID: 84532, - averageBlockTime: 2, - }, - //op sepolia - 11155420: { - targets: { - //op sepolia USDC - '0x5fd84259d66Cd46123540766Be93DFE6D43130D7': { - contractType: 'erc20', - selectors: ['transfer(address,uint256)'], - minBalance: 1000, - }, - }, - network: 'opt-sepolia', - chainID: 11155420, - averageBlockTime: 2, - }, - }, solverRegistrationConfig: { apiOptions: { diff --git a/config/preproduction.ts b/config/preproduction.ts index 8faaba481..94816d2d7 100644 --- a/config/preproduction.ts +++ b/config/preproduction.ts @@ -5,19 +5,19 @@ export default { aws: [ { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-secrets-pre-prod', }, { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-configs-pre-prod', }, { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-configs-chains-preprod', }, { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-whitelist-pre-prod', }, ], diff --git a/config/production.ts b/config/production.ts index 80293ee4a..bb1516fce 100644 --- a/config/production.ts +++ b/config/production.ts @@ -1,31 +1,21 @@ export default { - server: { - url: 'https://solver.prod.bend.eco', - }, - aws: [ { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-secrets-prod', }, { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-configs-prod', }, { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-configs-chains-prod', }, { - region: 'us-east-2', + region: 'us-west-2', secretID: 'eco-solver-whitelist-prod', }, ], //don't add anything else here - - solverRegistrationConfig: { - apiOptions: { - baseUrl: 'https://quotes.eco.com', - }, - }, } diff --git a/config/staging.ts b/config/staging.ts index cfab97d7b..4fcae01b2 100644 --- a/config/staging.ts +++ b/config/staging.ts @@ -1,27 +1,17 @@ export default { - server: { - url: 'https://solver.staging.bend.eco', - }, - aws: [ { - region: 'us-east-2', - secretID: 'eco-solver-secrets-staging', + region: 'us-west-2', + secretID: 'eco-solver-secrets-prod', }, { - region: 'us-east-2', - secretID: 'eco-solver-configs-staging', + region: 'us-west-2', + secretID: 'eco-solver-configs-prod', }, { - region: 'us-east-2', - secretID: 'eco-solver-whitelist-staging', + region: 'us-west-2', + secretID: 'eco-solver-whitelist-prod', }, ], //don't add anything else here - - solverRegistrationConfig: { - apiOptions: { - baseUrl: 'https://quotes-preprod.eco.com', - }, - }, } diff --git a/config/test.ts b/config/test.ts index 486844b0b..2717f7dde 100644 --- a/config/test.ts +++ b/config/test.ts @@ -1,6 +1,6 @@ export default { aws: { - region: 'test-us-east-2', + region: 'test-us-west-2', secretID: 'test-eco-solver-secrets', }, test: 'hi', diff --git a/docker-compose.yml b/docker-compose.yml index 03608c09f..205debd42 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,78 +1,35 @@ -services: - app: - build: - context: . - dockerfile: Dockerfile - container_name: eco-solver-app - ports: - - '3000:3000' - depends_on: - mongodb: - condition: service_healthy - redis: - condition: service_healthy - environment: - AWS_CONFIG_FILE: /root/.aws/config - AWS_PROFILE: ${AWS_PROFILE} - - NODE_ENV: ${NODE_ENV:-development} - NODE_CONFIG: | - { - "database": { - "auth": { - "enabled": false - }, - "uriPrefix": "mongodb://", - "uri": "mongodb:27017", - "dbName": "eco-solver-local" - }, - "redis": { - "connection": { - "host": "redis", - "port": 6379 - } - } - } - volumes: - - ./src:/usr/src/app/src - - ./config:/usr/src/app/config - - ~/.aws:/root/.aws:ro - restart: unless-stopped - command: ['yarn', 'start:dev'] - profiles: ['app', 'all'] +version: '3.8' +services: mongodb: - image: mongo:8.0 + image: mongo:latest container_name: eco-solver-mongodb ports: - - '27017:27017' + - "27017:27017" volumes: - mongodb_data:/data/db environment: - MONGO_INITDB_DATABASE=eco-solver-local healthcheck: - test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"] + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/eco-solver-local --quiet interval: 10s timeout: 5s retries: 5 - start_period: 30s - restart: unless-stopped - profiles: ['db', 'all'] + start_period: 10s redis: - image: redis:7.4-alpine + image: redis:latest container_name: eco-solver-redis ports: - - '6379:6379' + - "6379:6379" volumes: - redis_data:/data healthcheck: - test: ['CMD', 'redis-cli', 'ping'] + test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 - restart: unless-stopped - profiles: ['db', 'all'] + start_period: 10s volumes: mongodb_data: diff --git a/package.json b/package.json index 145bd7c16..40b439593 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "dependencies": { "@aws-sdk/client-kms": "^3.744.0", "@aws-sdk/client-secrets-manager": "^3.592.0", - "@eco-foundation/chains": "^1.0.32", + "@eco-foundation/chains": "^1.0.36", "@eco-foundation/routes-ts": "^2.1.19", "@launchdarkly/node-server-sdk": "^9.7.1", "@liaoliaots/nestjs-redis-health": "^9.0.4", @@ -51,6 +51,7 @@ "@lit-protocol/auth-helpers": "^7.0.4", "@lit-protocol/constants": "^7.0.4", "@lit-protocol/lit-node-client": "^7.0.4", + "@metalayer/viem-chains": "^0.2.2", "@nestjs/bullmq": "^10.1.1", "@nestjs/cache-manager": "^3.0.0", "@nestjs/common": "^10.4.16", diff --git a/src/balance/balance.service.ts b/src/balance/balance.service.ts index 67a1748ad..d30807c92 100644 --- a/src/balance/balance.service.ts +++ b/src/balance/balance.service.ts @@ -145,9 +145,9 @@ export class BalanceService implements OnApplicationBootstrap { const [balance = 0n, decimals = 0] = [results[index * 2], results[index * 2 + 1]] //throw if we suddenly start supporting tokens with not 6 decimals //audit conversion of validity to see its support - if ((decimals as number) != 6) { - throw EcoError.BalanceServiceInvalidDecimals(tokenAddress) - } + // if ((decimals as number) != 6) { + // throw EcoError.BalanceServiceInvalidDecimals(tokenAddress) + // } tokenBalances[tokenAddress] = { address: tokenAddress, balance: balance as bigint, diff --git a/src/common/chains/supported.ts b/src/common/chains/supported.ts index 8ea83d637..792ea36ef 100644 --- a/src/common/chains/supported.ts +++ b/src/common/chains/supported.ts @@ -1,8 +1,27 @@ import { EcoRoutesChains } from '@eco-foundation/chains' import { Chain } from 'viem' +import { TESTNET_ROLLUPS } from '@metalayer/viem-chains' +import { bscTestnet, arbitrumSepolia } from 'viem/chains' /** * List of supported chains for the solver that have modified RPC URLs or are defined in the project */ -// export const ChainsSupported: Chain[] = [anvil, ...(EcoRoutesChains as Chain[])] -export const ChainsSupported: Chain[] = [...(EcoRoutesChains as Chain[])] +// Configure custom RPC URLs for specific chains +const configureChainRpc = (chain: Chain): Chain => { + // Create a deep copy of the chain to avoid modifying the original + const modifiedChain = JSON.parse(JSON.stringify(chain)) as Chain + + // Set custom RPC URL for BSC testnet + if (modifiedChain.id === 97) { + modifiedChain.rpcUrls.default.http = [ + 'https://bnb-testnet.g.alchemy.com/v2/Pck8yx8R7BLJ6MqbVD34L', + ] + } + + return modifiedChain +} + +// Apply the configuration to all Caldera chains +const calderaChains = [bscTestnet, arbitrumSepolia, ...TESTNET_ROLLUPS].map(configureChainRpc) + +export const ChainsSupported: Chain[] = [...(EcoRoutesChains as Chain[]), ...calderaChains] diff --git a/src/common/chains/transport.ts b/src/common/chains/transport.ts index 5f64acee7..d9a34dfff 100644 --- a/src/common/chains/transport.ts +++ b/src/common/chains/transport.ts @@ -11,6 +11,8 @@ export type TransportOptions = | { isWebsocket: true; options?: WebSocketTransportConfig } | { isWebsocket?: false; options?: HttpTransportConfig } + + /** * Returns transport for the chain with the given api key * diff --git a/src/eco-configs/eco-config.service.ts b/src/eco-configs/eco-config.service.ts index f7ce81505..fe0485f2b 100644 --- a/src/eco-configs/eco-config.service.ts +++ b/src/eco-configs/eco-config.service.ts @@ -12,11 +12,11 @@ import { SafeType, Solver, } from './eco-config.types' -import { Chain, getAddress, zeroAddress } from 'viem' +import { Chain, getAddress, zeroAddress, extractChain } from 'viem' import { addressKeys } from '@/common/viem/utils' import { ChainsSupported } from '@/common/chains/supported' import { getChainConfig } from './utils' -import { EcoChains } from '@eco-foundation/chains' +import { EcoChain, EcoChains } from '@eco-foundation/chains' import { EcoError } from '../common/errors/eco-error' /** * Service class for managing application configuration from multiple sources. @@ -297,7 +297,13 @@ export class EcoConfigService { * @returns The RPC URL string for the specified chain */ getRpcUrl(chain: Chain, websocketEnabled: boolean = false) { - const rpcChain = this.ecoChains.getChain(chain.id) + let rpcChain: EcoChain | Chain + try { + rpcChain = this.ecoChains.getChain(chain.id) + } catch (e) { + rpcChain = extractChain({ chains: ChainsSupported, id: chain.id }) + } + const custom = rpcChain.rpcUrls.caldera || rpcChain.rpcUrls.alchemy || rpcChain.rpcUrls.quicknode const def = rpcChain.rpcUrls.default @@ -309,7 +315,11 @@ export class EcoConfigService { rpc = custom?.http?.[0] || def?.http?.[0] } if (!rpc) { - throw EcoError.ChainExistsButRPCNotFound(chain.id) + rpc = custom?.http?.[0] || def?.http?.[0] + websocketEnabled = false + if (!rpc) { + throw EcoError.ChainExistsButRPCNotFound(chain.id) + } } return { rpcUrl: rpc, diff --git a/src/eco-configs/utils.ts b/src/eco-configs/utils.ts index 114f41dfc..20cbded40 100644 --- a/src/eco-configs/utils.ts +++ b/src/eco-configs/utils.ts @@ -30,11 +30,12 @@ export function getNodeEnv(): NodeEnv { * @returns true if the node env is preproduction or development */ export function isPreEnv(): boolean { - return ( - getNodeEnv() === NodeEnv.preproduction || - getNodeEnv() === NodeEnv.development || - getNodeEnv() === NodeEnv.staging - ) + return false + // return ( + // getNodeEnv() === NodeEnv.preproduction || + // getNodeEnv() === NodeEnv.development || + // getNodeEnv() === NodeEnv.staging + // ) } /** @@ -45,9 +46,30 @@ export function isPreEnv(): boolean { */ export function getChainConfig(chainID: number | string): EcoChainConfig { const id = isPreEnv() ? `${chainID}-${ChainPrefix}` : chainID.toString() - const config = EcoProtocolAddresses[id] - if (config === undefined) { - throw EcoError.ChainConfigNotFound(id) + return getCalderaChainConfig() + // const config = EcoProtocolAddresses[id] + // if (config === undefined) { + // throw EcoError.ChainConfigNotFound(id) + // } else { + // return CALDERA_CHAIN_CONFIG + // } +} + +function getCalderaChainConfig(): EcoChainConfig { + const env = getNodeEnv() + if (env === NodeEnv.production) { + return { + IntentSource: '0x192b12FAB612AB8c54f4B416500Ea71CF61a9473', + Inbox: '0xCB96D5Db5071b3335F8DB5a97BC90E274AAe24bF', + HyperProver: '0x0000000000000000000000000000000000000000', + MetaProver: '0x83C09c0C0579C23A6acEFD6a2b6285Bcec904207', + } + } else { + return { + IntentSource: '0x50673016E0720d6B7FA5Af3290709Fc8bAF65A70', + Inbox: '0xCc71EA5C67795EF12be2328C14F7E96A39D71067', + HyperProver: '0x0000000000000000000000000000000000000000', + MetaProver: '0xcF415cFD2f287Ea5e394BAA1f12035fC57d6EED8', + } } - return config } diff --git a/src/intent-processor/services/intent-processor.service.ts b/src/intent-processor/services/intent-processor.service.ts index 6fb408d55..a485c39ee 100644 --- a/src/intent-processor/services/intent-processor.service.ts +++ b/src/intent-processor/services/intent-processor.service.ts @@ -105,7 +105,7 @@ export class IntentProcessorService implements OnApplicationBootstrap { const intentSourceAddr = this.getIntentSource() const proves = await this.indexerService.getNextSendBatch(intentSourceAddr) - const batchProvesPerChain = _.groupBy(proves, (prove) => prove.destinationChainId) + let batchProvesPerChain = _.groupBy(proves, (prove) => prove.destinationChainId) this.logger.debug( EcoLogMessage.fromDefault({ @@ -116,6 +116,7 @@ export class IntentProcessorService implements OnApplicationBootstrap { }, }), ) + batchProvesPerChain = {} // TODO: Remove when Eco supports Metaprover for send batches const jobsData: ExecuteSendBatchJobData[] = [] diff --git a/src/liquidity-manager/services/liquidity-manager.service.ts b/src/liquidity-manager/services/liquidity-manager.service.ts index 5eb2fa344..be44d1233 100644 --- a/src/liquidity-manager/services/liquidity-manager.service.ts +++ b/src/liquidity-manager/services/liquidity-manager.service.ts @@ -36,6 +36,7 @@ import { KernelAccountClientService } from '@/transaction/smart-wallets/kernel/k import { TokenConfig } from '@/balance/types' import { removeJobSchedulers } from '@/bullmq/utils/queue' import { EcoLogMessage } from '@/common/logging/eco-log-message' +import { getNodeEnv, NodeEnv } from '@/eco-configs/utils' @Injectable() export class LiquidityManagerService implements OnApplicationBootstrap { @@ -70,8 +71,14 @@ export class LiquidityManagerService implements OnApplicationBootstrap { this.config = this.ecoConfigService.getLiquidityManager() // Use OP as the default chain assuming the Kernel wallet is the same across all chains - const opChainId = 10 - const client = await this.kernelAccountClientService.getClient(opChainId) + const env = getNodeEnv() + let chainId + if (env === NodeEnv.production) { + chainId = 33139 // apechain + } else { + chainId = 33111 // curtis + } + const client = await this.kernelAccountClientService.getClient(chainId) const kernelAddress = client.kernelAccount.address // Track rebalances for Solver diff --git a/src/transaction/smart-wallets/kernel/create.kernel.account.ts b/src/transaction/smart-wallets/kernel/create.kernel.account.ts index f6c9003d7..67d50d106 100644 --- a/src/transaction/smart-wallets/kernel/create.kernel.account.ts +++ b/src/transaction/smart-wallets/kernel/create.kernel.account.ts @@ -53,6 +53,7 @@ export async function createKernelAccountClient< >( parameters: KernelAccountClientConfig, owner>, ): Promise<{ client: KernelAccountClient; args: DeployFactoryArgs }> { + const logger = getLogger() const { key = 'kernelAccountClient', name = 'Kernel Account Client', transport } = parameters const { account } = parameters @@ -91,13 +92,15 @@ export async function createKernelAccountClient< kernelVersion, }) } catch (e) { - EcoLogMessage.fromDefault({ - message: `createKernelAccountClient createKernelAccount: `, - properties: { - chainID: walletClient.chain?.id, - e, - }, - }) + logger.log( + EcoLogMessage.fromDefault({ + message: `createKernelAccountClient createKernelAccount: `, + properties: { + chainID: walletClient.chain?.id, + e, + }, + }), + ) throw e } diff --git a/yarn.lock b/yarn.lock index 43a45a28f..6dd0377c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1472,10 +1472,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@eco-foundation/chains@^1.0.32": - version "1.0.32" - resolved "https://registry.yarnpkg.com/@eco-foundation/chains/-/chains-1.0.32.tgz#281b05cff36893f0fec907ced85ce96cb6111f8f" - integrity sha512-CT5xJkg/a36AqnsTN88cO+clnt54EI/h+SR+G+dZ8MZBlB0Q2iqYypn1ii/4OHKobVOmg+HjNOjOW4EQJgxLkg== +"@eco-foundation/chains@^1.0.36": + version "1.0.36" + resolved "https://registry.yarnpkg.com/@eco-foundation/chains/-/chains-1.0.36.tgz#3ecee59a6b6dffa2b67144fc91ce568bbd8bf974" + integrity sha512-rtnyAvPEyYgDilbfNyjel0xdV7DDsNojorkwdsTXLAEg7VWeIgnltZjGknaWn3YqycCCdphUI+sVvCTKvZgx5A== dependencies: typia "^8.0.4" viem "^2.24.1" @@ -2912,6 +2912,11 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== +"@metalayer/viem-chains@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@metalayer/viem-chains/-/viem-chains-0.2.2.tgz#359fdb7c215cebffbe871ef6ae5876f992aff5e8" + integrity sha512-74m4iyBYAnoah2H2iMNqV+Ao2dr15dkLG8ITygNFeRo62SnsJeRdA4rTKBsMRnfavqmnKwrLHE44OtLa055pYg== + "@microsoft/tsdoc@0.15.0": version "0.15.0" resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz#f29a55df17cb6e87cfbabce33ff6a14a9f85076d"