diff --git a/package.json b/package.json index 928e2ae..e533de5 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dependencies": { "@commander-js/extra-typings": "^11.0.0", "@cowprotocol/contracts": "^1.4.0", - "@cowprotocol/cow-sdk": "^4.0.0", + "@cowprotocol/cow-sdk": "^4.0.3", "chalk": "^4.1.2", "ethers": "^5.7.2", "express": "^4.18.2", diff --git a/src/domain/chainContext.ts b/src/domain/chainContext.ts index 73220e3..dfbe831 100644 --- a/src/domain/chainContext.ts +++ b/src/domain/chainContext.ts @@ -9,7 +9,11 @@ import { RegistryBlock, blockToRegistryBlock, } from "../types"; -import { SupportedChainId, OrderBookApi } from "@cowprotocol/cow-sdk"; +import { + SupportedChainId, + OrderBookApi, + ApiBaseUrls, +} from "@cowprotocol/cow-sdk"; import { addContract } from "./addContract"; import { checkForAndPlaceOrder } from "./checkForAndPlaceOrder"; import { ethers } from "ethers"; @@ -32,6 +36,8 @@ const WATCHDOG_FREQUENCY = 5 * 1000; // 5 seconds const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11"; +export const SDK_BACKOFF_NUM_OF_ATTEMPTS = 5; + enum ChainSync { /** The chain is currently in the warm-up phase, synchronising from contract genesis or lastBlockProcessed */ SYNCING = "SYNCING", @@ -77,12 +83,14 @@ export class ChainContext { orderBook: OrderBookApi; contract: ComposableCoW; multicall: Multicall3; + orderBookApiBaseUrls?: ApiBaseUrls; protected constructor( options: RunSingleOptions, provider: ethers.providers.Provider, chainId: SupportedChainId, - registry: Registry + registry: Registry, + orderBookApi?: string ) { const { deploymentBlock, pageSize, dryRun } = options; this.deploymentBlock = deploymentBlock; @@ -92,7 +100,19 @@ export class ChainContext { this.provider = provider; this.chainId = chainId; this.registry = registry; - this.orderBook = new OrderBookApi({ chainId: this.chainId }); + this.orderBookApiBaseUrls = orderBookApi + ? ({ + [this.chainId]: orderBookApi, + } as ApiBaseUrls) // FIXME: do not do this casting once this is fixed https://github.com/cowprotocol/cow-sdk/issues/176 + : undefined; + + this.orderBook = new OrderBookApi({ + chainId: this.chainId, + baseUrls: this.orderBookApiBaseUrls, + backoffOpts: { + numOfAttempts: SDK_BACKOFF_NUM_OF_ATTEMPTS, + }, + }); this.contract = composableCowContract(this.provider, this.chainId); this.multicall = Multicall3__factory.connect(MULTICALL3, this.provider); @@ -108,7 +128,7 @@ export class ChainContext { options: RunSingleOptions, storage: DBService ): Promise { - const { rpc, deploymentBlock } = options; + const { rpc, orderBookApi, deploymentBlock } = options; const provider = new ethers.providers.JsonRpcProvider(rpc); const chainId = (await provider.getNetwork()).chainId; @@ -120,7 +140,13 @@ export class ChainContext { ); // Save the context to the static map to be used by the API - const context = new ChainContext(options, provider, chainId, registry); + const context = new ChainContext( + options, + provider, + chainId, + registry, + orderBookApi + ); ChainContext.chains[chainId] = context; return context; diff --git a/src/domain/checkForAndPlaceOrder.ts b/src/domain/checkForAndPlaceOrder.ts index 080dc1b..5caf357 100644 --- a/src/domain/checkForAndPlaceOrder.ts +++ b/src/domain/checkForAndPlaceOrder.ts @@ -28,7 +28,7 @@ import { SupportedChainId, formatEpoch, } from "@cowprotocol/cow-sdk"; -import { ChainContext } from "./chainContext"; +import { ChainContext, SDK_BACKOFF_NUM_OF_ATTEMPTS } from "./chainContext"; import { pollingOnChainDurationSeconds, activeOrdersTotal, @@ -236,7 +236,8 @@ async function _processConditionalOrder( context: ChainContext, orderRef: string ): Promise { - const { provider, orderBook, dryRun, chainId } = context; + const { provider, orderBook, dryRun, chainId, orderBookApiBaseUrls } = + context; const { handler } = conditionalOrder.params; const log = getLogger( "checkForAndPlaceOrder:_processConditionalOrder", @@ -262,6 +263,12 @@ async function _processConditionalOrder( blockNumber, }, provider, + orderbookApiConfig: { + baseUrls: orderBookApiBaseUrls, + backoffOpts: { + numOfAttempts: SDK_BACKOFF_NUM_OF_ATTEMPTS, + }, + }, }; let pollResult = await pollConditionalOrder( pollParams, diff --git a/src/index.ts b/src/index.ts index 72246e0..b078d57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,16 @@ const deploymentBlockOption = new Option( .env("DEPLOYMENT_BLOCK") .argParser(parseIntOption); +const multiOrderBookApiOption = new Option( + "--orderBookApi ", + "Orderbook API base URLs (i.e. https://api.cow.fi/mainnet, https://api.cow.fi/xdai, etc.)" +).default([]); + +const orderBookApiOption = new Option( + "--orderBookApi ", + "Orderbook API base URL (i.e. https://api.cow.fi/mainnet)" +).default(undefined); + async function main() { program.name("watch-tower").description(description).version(version); @@ -108,6 +118,7 @@ async function main() { .description("Run the watch-tower, monitoring only a single chain") .addOption(rpcOption) .addOption(deploymentBlockOption) + .addOption(orderBookApiOption) .addOption(databasePathOption) .addOption(logLevelOption) .addOption(watchdogTimeoutOption) @@ -119,7 +130,8 @@ async function main() { .addOption(disableNotificationsOption) .addOption(slackWebhookOption) .action((options) => { - const { logLevel } = options; + const { logLevel, orderBookApi } = options; + const [pageSize, apiPort, watchdogTimeout, deploymentBlock] = [ options.pageSize, options.apiPort, @@ -130,7 +142,14 @@ async function main() { initLogging({ logLevel }); // Run the watch-tower - run({ ...options, deploymentBlock, pageSize, apiPort, watchdogTimeout }); + run({ + ...options, + deploymentBlock, + orderBookApi, + pageSize, + apiPort, + watchdogTimeout, + }); }); program @@ -142,6 +161,7 @@ async function main() { ) .addOption(multiRpcOption) .addOption(multiDeploymentBlockOption) + .addOption(multiOrderBookApiOption) .addOption(databasePathOption) .addOption(logLevelOption) .addOption(watchdogTimeoutOption) @@ -161,7 +181,11 @@ async function main() { ].map((value) => Number(value)); initLogging({ logLevel }); - const { rpc: rpcs, deploymentBlock: deploymentBlocksEnv } = options; + const { + rpc: rpcs, + orderBookApi: orderBookApis, + deploymentBlock: deploymentBlocksEnv, + } = options; // Ensure that the deployment blocks are all numbers const deploymentBlocks = deploymentBlocksEnv.map((block) => @@ -176,11 +200,17 @@ async function main() { throw new Error("RPC and deployment blocks must be the same length"); } + // Ensure that the orderBookApis and RPCs are the same length + if (orderBookApis.length > 0 && rpcs.length !== orderBookApis.length) { + throw new Error("orderBookApi and RPC urls must be the same length"); + } + // Run the watch-tower runMulti({ ...options, rpcs, deploymentBlocks, + orderBookApis, pageSize, apiPort, watchdogTimeout, diff --git a/src/types/index.ts b/src/types/index.ts index 010d184..be918ad 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,12 +23,14 @@ export interface RunOptions extends WatchtowerOptions { export interface RunSingleOptions extends RunOptions { rpc: string; + orderBookApi?: string; deploymentBlock: number; } export interface RunMultiOptions extends RunOptions { rpcs: string[]; deploymentBlocks: number[]; + orderBookApis: string[]; } export interface ReplayBlockOptions extends WatchtowerReplayOptions { diff --git a/yarn.lock b/yarn.lock index c9607bb..922f8e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -466,10 +466,10 @@ resolved "https://registry.yarnpkg.com/@cowprotocol/contracts/-/contracts-1.4.0.tgz#e93e5f25aac76feeaa348fa57231903274676247" integrity sha512-XLs3SlPmXD4lbiWIO7mxxuCn1eE5isuO6EUlE1cj17HqN/wukDAN0xXYPx6umOH/XdjGS33miMiPHELEyY9siw== -"@cowprotocol/cow-sdk@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-4.0.0.tgz#5b81bea462206719d9d02180a341dd905cb77573" - integrity sha512-bJvR//XkEJngxbDedOCahDnawwCG/yKn1jwe+3eiQwSqoJx3gvimP/ahNW9BstMvJZ96Pen0xKrBg0BiJA55lA== +"@cowprotocol/cow-sdk@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-4.0.3.tgz#bd4da3e1821c33e6f5a29e04786aaa16c346bb02" + integrity sha512-bEGpHwfFpUv4he5kH99gmmJU1kZaqRH4JUCeFqIzZAxey86i+qLzVS00r3GDw5o/tKYY/0677hgusH2srr8MZw== dependencies: "@cowprotocol/contracts" "^1.4.0" "@ethersproject/abstract-signer" "^5.7.0"