Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration file #131

Merged
merged 9 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 9 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# Watch-Tower for Composable CoWs 🐮🎶
# Watch-Tower for Programmatic Orders 🐮🎶

mfw78 marked this conversation as resolved.
Show resolved Hide resolved
## Overview

The [`ComposableCoW`](https://github.com/cowprotocol/composable-cow) conditional order framework requires a watch-tower to monitor the blockchain for new orders, and to post them to the [CoW Protocol `OrderBook` API](https://api.cow.fi/docs/#/). The watch-tower is a standalone application that can be run locally as a script for development, or deployed as a docker container to a server, or dappnode.
The [programmatic order](https://docs.cow.fi/cow-protocol/concepts/order-types/programmatic-orders) framework requires a watch-tower to monitor the blockchain for new orders, and to post them to the [CoW Protocol `OrderBook` API](https://docs.cow.fi/cow-protocol/reference/apis/orderbook). The watch-tower is a standalone application that can be run locally as a script for development, or deployed as a docker container to a server, or dappnode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not from this PR, I would start saying with what it is and not what it requires.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something in these lines:

The programatic orders allow you to automatically place unlimited order, following your arbitrary trading strategy defined using the programmatic order frameworks.

Because smart contracts can't place orders in the Orderbook API, you will need a Watch-Tower that monitors these contracts and places an order on your behalf.

This project contains a general-purpose watch tower that will monitor all or some specific accounts, or orders depending on configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While nice, the idea is mostly that there is a hyperlink there that explains it, and reduces duplication of typing / explanation.

## Deployment

If running your own watch-tower instance, you will need the following:

- An RPC node connected to the Ethereum mainnet, Gnosis Chain, or Goerli.
- Internet access to the [CoW Protocol `OrderBook` API](https://api.cow.fi/docs/#/).
- Internet access to the [CoW Protocol `OrderBook` API](https://docs.cow.fi/cow-protocol/reference/apis/orderbook).

**CAUTION**: Conditional order types may consume considerable RPC calls.

**NOTE**: `deployment-block` refers to the block number at which the **`ComposableCoW`** contract was deployed to the respective chain. This is used to optimise the watch-tower by only fetching events from the blockchain after this block number. Refer to [Deployed Contracts](https://github.com/cowprotocol/composable-cow#deployed-contracts) for the respective chains.

**NOTE**: The `--page-size` option is used to specify the number of blocks to fetch from the blockchain when querying historical events (`eth_getLogs`). The default is `5000`, which is the maximum number of blocks that can be fetched in a single request from Infura. If you are running the watch-tower against your own RPC, you may want to set this to `0` to fetch all blocks in one request, as opposed to paging requests.
**NOTE**: The `pageSize` option is used to specify the number of blocks to fetch from the blockchain when querying historical events (`eth_getLogs`). The default is `5000`, which is the maximum number of blocks that can be fetched in a single request from Infura. If you are running the watch-tower against your own RPC, you may want to set this to `0` to fetch all blocks in one request, as opposed to paging requests.


### Docker
Expand All @@ -31,20 +31,13 @@ As an example, to run the latest version of the watch-tower via `docker`:

```bash
docker run --rm -it \
-v "$(pwd)/config.json.example:/config.json" \
ghcr.io/cowprotocol/watch-tower:latest \
run \
--chain-config <rpc>,<deployment-block> \
--page-size 5000
--config-path /config.json
```

**NOTE**: There are multiple optional arguments on the `--chain-config` parameter. For a full explanation of the optional arguments, use the `--help` flag:

```bash
docker run --rm -it \
ghcr.io/cowprotocol/watch-tower:latest \
run \
--help
```
**NOTE**: See the example `config.json.example` for an example configuration file.

### DAppNode

Expand Down Expand Up @@ -78,7 +71,7 @@ The watch-tower monitors the following events:
When a new event is discovered, the watch-tower will:

1. Fetch the conditional order(s) from the blockchain.
2. Post the **discrete** order(s) to the [CoW Protocol `OrderBook` API](https://api.cow.fi/docs/#/).
2. Post the **discrete** order(s) to the [CoW Protocol `OrderBook` API](https://docs.cow.fi/cow-protocol/reference/apis/orderbook).

### Storage (registry)

Expand Down Expand Up @@ -186,7 +179,7 @@ It is recommended to test against the Goerli testnet. To run the watch-tower:
# Install dependencies
yarn
# Run watch-tower
yarn cli run --chain-config <rpc>,<deployment-block> --page-size 5000
yarn cli run --config-path ./config.json
```
mfw78 marked this conversation as resolved.
Show resolved Hide resolved

### Testing
Expand Down
12 changes: 12 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"networks": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is nicer indeed. But now we have an hybrid of envs and config.

Shouldn't we add all the config in the config file if we just made it mandatory to add one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, I don't have a strong opinion either way on this, and only did this way so as to minimise work, and minimise type duplication between the watch-tower and the Pulumi configuration for infrastructure (not a strong argument though).

{
"name": "mainnet",
"rpc": "ws://172.20.0.5:8546",
"deploymentBlock": 17883049,
"filterPolicy": {
"defaultAction": "ACCEPT"
}
}
]
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
"lint:fix": "eslint --fix \"src/**/*.ts\" && prettier --write \"src/**/*.ts\"",
"test": "yarn build && jest ./dist",
"typechain": "typechain --target ethers-v5 --out-dir src/types/generated/ \"abi/*.json\"",
"prepare": "husky install && yarn typechain",
"configschema": "node ./src/types/build.js",
"prepare": "husky install && yarn typechain && yarn configschema",
"cli": "ts-node src/index.ts"
},
"devDependencies": {
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@typechain/ethers-v5": "^11.1.1",
"@types/ajv": "^1.0.0",
"@types/express": "^4.17.18",
"@types/jest": "^29.5.3",
"@types/node": "^18.16.3",
Expand All @@ -35,6 +37,7 @@
"eslint-plugin-unused-imports": "^3.0.0",
"husky": "^8.0.3",
"jest": "^28.1.3",
"json-schema-to-typescript": "^13.1.2",
"lint-staged": "^14.0.1",
"prettier": "^2.8.8",
"ts-node": "^10.9.1",
Expand All @@ -44,6 +47,8 @@
"@commander-js/extra-typings": "^11.0.0",
"@cowprotocol/contracts": "^1.4.0",
"@cowprotocol/cow-sdk": "^4.0.3",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"chalk": "^4.1.2",
"ethers": "^5.7.2",
"express": "^4.18.2",
Expand Down
3 changes: 0 additions & 3 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
export * from "./run";
export * from "./runMulti";
export * from "./replayBlock";
export * from "./replayTx";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this necesary in this PR? now is ok, but feels like could be done in a different PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to remove these features?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. runMulti has been superceded (actually, run was removed and runMulti was repurposed to run).
  2. replayBlock and replayTx weren't actually implemented (so removed dead stubs).

export * from "./dumpDb";
5 changes: 0 additions & 5 deletions src/commands/replayBlock.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/commands/replayTx.ts

This file was deleted.

30 changes: 23 additions & 7 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RunSingleOptions } from "../types";
import { RunOptions } from "../types";
import { getLogger, DBService } from "../utils";
import { ChainContext } from "../domain";
import { ApiService } from "../utils/api";
Expand All @@ -7,9 +7,9 @@ import { ApiService } from "../utils/api";
* Run the watch-tower 👀🐮
* @param options Specified by the CLI / environment for running the watch-tower
*/
export async function run(options: RunSingleOptions) {
export async function run(options: RunOptions) {
const log = getLogger("commands:run");
const { oneShot, disableApi, apiPort, databasePath } = options;
const { oneShot, disableApi, apiPort, databasePath, networks } = options;

// Open the database
const storage = DBService.getInstance(databasePath);
Expand All @@ -33,11 +33,27 @@ export async function run(options: RunSingleOptions) {

let exitCode = 0;
try {
const chainContext = await ChainContext.init(options, storage);
const runPromise = chainContext.warmUp(oneShot);
const chainContexts = await Promise.all(
networks.map((network) => {
const { name } = network;
log.info(`Starting chain ${name}...`);
return ChainContext.init(
{
...options,
...network,
},
storage
);
})
);

// Run the block watcher after warm up for the chain
await runPromise;
// Run the block watcher after warm up for each chain
const runPromises = chainContexts.map(async (context) => {
return context.warmUp(oneShot);
});

// Run all the chain contexts
await Promise.all(runPromises);
} catch (error) {
log.error("Unexpected error thrown when running watchtower", error);
exitCode = 1;
Expand Down
96 changes: 0 additions & 96 deletions src/commands/runMulti.ts

This file was deleted.

43 changes: 12 additions & 31 deletions src/domain/chainContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
RunSingleOptions,
Registry,
ReplayPlan,
ConditionalOrderCreatedEvent,
Expand All @@ -8,6 +7,7 @@ import {
Multicall3__factory,
RegistryBlock,
blockToRegistryBlock,
ContextOptions,
} from "../types";
import {
SupportedChainId,
Expand All @@ -34,10 +34,11 @@ import {
import { hexZeroPad } from "ethers/lib/utils";
import { FilterPolicy } from "../utils/filterPolicy";

const WATCHDOG_FREQUENCY = 5 * 1000; // 5 seconds
const WATCHDOG_FREQUENCY_SECS = 5; // 5 seconds
const WATCHDOG_TIMEOUT_DEFAULT_SECS = 30;

const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const FILTER_FREQUENCY_SECS = 60 * 60; // 1 hour
const PAGE_SIZE_DEFAULT = 5000;

export const SDK_BACKOFF_NUM_OF_ATTEMPTS = 5;

Expand Down Expand Up @@ -97,7 +98,7 @@ export class ChainContext {
multicall: Multicall3;

protected constructor(
options: RunSingleOptions,
options: ContextOptions,
provider: providers.Provider,
chainId: SupportedChainId,
registry: Registry
Expand All @@ -109,12 +110,12 @@ export class ChainContext {
watchdogTimeout,
owners,
orderBookApi,
filterPolicyConfig,
filterPolicy,
} = options;
this.deploymentBlock = deploymentBlock;
this.pageSize = pageSize;
this.pageSize = pageSize ?? PAGE_SIZE_DEFAULT;
this.dryRun = dryRun;
this.watchdogTimeout = watchdogTimeout;
this.watchdogTimeout = watchdogTimeout ?? WATCHDOG_TIMEOUT_DEFAULT_SECS;
this.addresses = owners;

this.provider = provider;
Expand All @@ -135,12 +136,7 @@ export class ChainContext {
},
});

this.filterPolicy = filterPolicyConfig
? new FilterPolicy({
configBaseUrl: filterPolicyConfig,
// configAuthToken: filterPolicyConfigAuthToken, // TODO: Implement authToken
})
: undefined;
this.filterPolicy = new FilterPolicy(filterPolicy);
this.contract = composableCowContract(this.provider, this.chainId);
this.multicall = Multicall3__factory.connect(MULTICALL3, this.provider);
}
Expand All @@ -153,7 +149,7 @@ export class ChainContext {
* @returns A chain context that is monitoring for orders on the chain.
*/
public static async init(
options: RunSingleOptions,
options: ContextOptions,
storage: DBService
): Promise<ChainContext> {
const { rpc, deploymentBlock } = options;
Expand Down Expand Up @@ -384,7 +380,7 @@ export class ChainContext {
// pod, we don't exit, but we do log an error and set the sync status to unknown.
while (true) {
// sleep for 5 seconds
await asyncSleep(WATCHDOG_FREQUENCY);
await asyncSleep(WATCHDOG_FREQUENCY_SECS * 1000);
const now = Math.floor(new Date().getTime() / 1000);
const timeElapsed = now - lastBlockReceived.timestamp;

Expand Down Expand Up @@ -460,7 +456,7 @@ async function processBlock(
blockNumberOverride?: number,
blockTimestampOverride?: number
) {
const { provider, chainId, filterPolicy } = context;
const { provider, chainId } = context;
const timer = processBlockDurationSeconds
.labels(context.chainId.toString())
.startTimer();
Expand All @@ -470,21 +466,6 @@ async function processBlock(
block.number.toString()
);

// Refresh the policy every hour
// NOTE: This is a temporary solution until we have a better way to update the filter policy
const blocksPerFilterFrequency =
FILTER_FREQUENCY_SECS /
(context.chainId === SupportedChainId.GNOSIS_CHAIN ? 5 : 12); // 5 seconds for gnosis, 12 seconds for mainnet
if (
filterPolicy &&
block.number % (FILTER_FREQUENCY_SECS / blocksPerFilterFrequency) == 0
) {
filterPolicy.reloadPolicies().catch((error) => {
console.log(`Error fetching the filter policy config for chain `, error);
return null;
});
}

// Transaction watcher for adding new contracts
let hasErrors = false;
for (const event of events) {
Expand Down
Loading
Loading