From 2ad0d3621864dfeb9f52e667d1380bfa9b2ecb81 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 17:10:32 +0100 Subject: [PATCH 01/15] Setup account ID and project --- tenderly.yaml | 81 +++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/tenderly.yaml b/tenderly.yaml index 283fe06..0d1ad94 100644 --- a/tenderly.yaml +++ b/tenderly.yaml @@ -1,6 +1,9 @@ -account_id: 365d1c08-1187-453c-ace5-c0cc4e53a41f +account_id: 4edfc3a8-de04-463e-8408-34302bfa01e0 +project_slug: dev@cow.fi +provider: "" + actions: - mfw78/rndlabs: + devcow/project: runtime: v1 sources: actions specs: @@ -10,65 +13,67 @@ actions: trigger: transaction: filters: - - logEmitted: - startsWith: - # `ConditionalOrderCreated(address, (address,bytes32,bytes))` - - 0x2cceac5555b0ca45a3744ced542f54b56ad2eb45e521962372eef212a2cbf361 - network: - - 1 - - 5 - - 100 - status: success + - logEmitted: + startsWith: + # `ConditionalOrderCreated(address, (address,bytes32,bytes))` + - 0x2cceac5555b0ca45a3744ced542f54b56ad2eb45e521962372eef212a2cbf361 + network: + - 1 + - 5 + - 100 + status: success status: - - mined + - mined type: transaction + register_merkle_root: description: Listens to events that index a merkle root of conditional orders function: register:addContract trigger: transaction: filters: - - logEmitted: - startsWith: - # `MerkleRootSet(address, bytes32, (uint256, bytes))` - - 0x58662f46b4a87d0f96d929b24c37fe25c55d52c0025d0b2bec3936534cc31e57 - network: - - 1 - - 5 - - 100 - status: success + - logEmitted: + startsWith: + # `MerkleRootSet(address, bytes32, (uint256, bytes))` + - 0x58662f46b4a87d0f96d929b24c37fe25c55d52c0025d0b2bec3936534cc31e57 + network: + - 1 + - 5 + - 100 + status: success status: - - mined + - mined type: transaction + watch_settlements: description: Watch for settled trades and update the state function: watch:checkForSettlement trigger: transaction: filters: - - logEmitted: - startsWith: - # `Trade(address, address, address, uint256, uint256, uint256, bytes)` - - 0xa07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17 - network: - - 1 - - 5 - - 100 - status: success + - logEmitted: + startsWith: + # `Trade(address, address, address, uint256, uint256, uint256, bytes)` + - 0xa07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17 + network: + - 1 + - 5 + - 100 + status: success status: - - mined + - mined type: transaction + watch_orders: - description: Checks on every block if the registered smart order contract + description: + Checks on every block if the registered smart order contract wants to trade function: watch:checkForAndPlaceOrder trigger: block: blocks: 5 network: - - 1 - - 5 - - 100 + - 1 + - 5 + - 100 type: block -project_slug: rndlabs -provider: "" From a46bfa059d17e4c97058706994f7459d2593ecb5 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 17:11:17 +0100 Subject: [PATCH 02/15] Delete unused envs and add the local tenderly testing --- .env.example | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index fc012d2..4654847 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,12 @@ -ETHERSCAN_API_KEY= -PRIVATE_KEY= -SETTLEMENT=0x9008D19f58AAbD9eD0D60971565AA8510560ab41 +# CLI ETH_RPC_URL=http://erigon.dappnode:8545 -SAFE= -TWAP= -COMPOSABLE_COW= +PRIVATE_KEY= + +# Tenderly Local Tests +NETWORK=5 # Network to test. 1: Mainnet, 5: Goerli, 100: xDai +# NODE_URL_1= +# NODE_URL_5= +# NODE_URL_100= +# NODE_USER_100= +# NODE_PASSWORD_100= + From 9a3dc747f441942466a0f3f7c0c14c928af75152 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 17:11:46 +0100 Subject: [PATCH 03/15] Add convenient local run tenderly actions --- package.json | 77 ++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 26187c0..e991598 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,41 @@ { - "name": "composable-cow", - "version": "0.0.1", - "description": "Repository showcasing the power of composable cows 🐮", - "author": { - "name": "mfw78", - "email": "mfw78@rndlabs.xyz" - }, - "contributors": [ - { - "name": "Cow Protocol" - } - ], - "license": "MIT", - "devDependencies": { - "eslint": "8.39.0", - "prettier": "^2.8.8", - "prettier-plugin-solidity": "^1.1.3", - "solhint": "^3.4.1", - "solhint-plugin-prettier": "^0.0.5" - }, - "scripts": { - "fmt": "yarn fmt:contracts && yarn fmt:actions && yarn fmt:cli", - "lint": "yarn lint:contracts && yarn lint:actions && yarn lint:cli", - "build": "forge build && yarn build:actions && yarn build:cli", - "fmt:contracts": "forge fmt", - "lint:contracts": "solhint 'src/**/*.sol' 'test/**/*.sol'", - "fmt:actions": "prettier ./actions -w", - "build:actions": "cd actions && npm ci && yarn run build", - "lint:actions": "eslint && prettier --check ./actions", - "test:actions": "yarn build:actions && yarn ts-node actions/test/test_register.ts", - "fmt:cli": "prettier ./cli -w", - "lint:cli": "eslint && prettier --check ./cli", - "build:cli": "cd cli && npm ci && yarn run build", - "check-deployment": "yarn build:actions && yarn ts-node actions/test/run_local.ts" - }, - "dependencies": { - "ts-node": "^10.9.1" + "name": "composable-cow", + "version": "0.0.1", + "description": "Repository showcasing the power of composable cows 🐮", + "author": { + "name": "mfw78", + "email": "mfw78@rndlabs.xyz" + }, + "contributors": [ + { + "name": "Cow Protocol" } - } \ No newline at end of file + ], + "license": "MIT", + "devDependencies": { + "eslint": "8.39.0", + "prettier": "^2.8.8", + "prettier-plugin-solidity": "^1.1.3", + "solhint": "^3.4.1", + "solhint-plugin-prettier": "^0.0.5" + }, + "scripts": { + "fmt": "yarn fmt:contracts && yarn fmt:actions && yarn fmt:cli", + "lint": "yarn lint:contracts && yarn lint:actions && yarn lint:cli", + "build": "forge build && yarn build:actions && yarn build:cli", + "fmt:contracts": "forge fmt", + "lint:contracts": "solhint 'src/**/*.sol' 'test/**/*.sol'", + "fmt:actions": "prettier ./actions -w", + "build:actions": "cd actions && npm ci && yarn run build", + "lint:actions": "eslint && prettier --check ./actions", + "test:actions": "yarn build:actions && yarn ts-node actions/test/test_register.ts", + "start:actions": "yarn ts-node actions/test/run_local.ts", + "fmt:cli": "prettier ./cli -w", + "lint:cli": "eslint && prettier --check ./cli", + "build:cli": "cd cli && npm ci && yarn run build", + "check-deployment": "yarn build:actions && yarn start:actions" + }, + "dependencies": { + "ts-node": "^10.9.1" + } +} From 04b74267003177c1a1cbc80bb0a871d882a1e4f9 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 17:12:30 +0100 Subject: [PATCH 04/15] Add dotenv, so Tenderly Actions pick the .env file too --- actions/package-lock.json | 19 +++++++++++++++++++ actions/package.json | 7 ++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/actions/package-lock.json b/actions/package-lock.json index 1cbf134..8dcdbea 100644 --- a/actions/package-lock.json +++ b/actions/package-lock.json @@ -18,6 +18,7 @@ "@tenderly/actions-test": "^0.0.13", "@typechain/ethers-v5": "^10.2.0", "@types/node": "^18.16.3", + "dotenv": "^16.3.1", "ts-node": "^10.9.1", "typechain": "^8.1.1" } @@ -1106,6 +1107,18 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -2331,6 +2344,12 @@ "version": "4.0.2", "dev": true }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true + }, "elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", diff --git a/actions/package.json b/actions/package.json index 34a22e0..d9aa8b2 100644 --- a/actions/package.json +++ b/actions/package.json @@ -8,16 +8,17 @@ "build": "tsc" }, "devDependencies": { + "@tenderly/actions-test": "^0.0.13", "@typechain/ethers-v5": "^10.2.0", "@types/node": "^18.16.3", - "@tenderly/actions-test": "^0.0.13", "ts-node": "^10.9.1", - "typechain": "^8.1.1" + "typechain": "^8.1.1", + "dotenv": "^16.3.1" }, "dependencies": { "@cowprotocol/contracts": "^1.4.0", "@tenderly/actions": "^0.0.13", - "axios": "^1.4.0", + "axios": "^1.4.0", "ethers": "^5.7.2", "node-fetch": "2", "typescript": "^5.0.4" From c8463bdc94ac661e5abe1885969d1cd8b8e55b41 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 17:12:46 +0100 Subject: [PATCH 05/15] Add local testing for actions --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 70c1857..2eee54c 100644 --- a/README.md +++ b/README.md @@ -294,3 +294,24 @@ For local integration testing, including the use of [Tenderly Actions](#Tenderly source .env SAFE="address here" forge script script/submit_SingleOrder.s.sol:SubmitSingleOrder --rpc-url http://127.0.0.1:8545 --broadcast ``` + +#### Local test for Tenderly Web3 Actions + +Make sure you setup the environment (so you have your own `.env` file). + +Decide in which network you want to run the actions and provide the additional parameters for that network. For example: + +```ini +NETWORK=100 +NODE_URL_100=https://your-rpc-endpoint +NODE_USER_100=optionally-provide-user-if-auth-is-required +NODE_PASSWORD_100=optionally-provide-password-if-auth-is-required +``` + +```bash +# Build Actions +yarn build:actions + +# Run actions locally, so it starts to checking for new blocks, and executing the actions to create Composable Cow orders) +yarn start:actions +``` From 69e7f74d977e53a9677517a4d8140074a4316fcd Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 17:15:04 +0100 Subject: [PATCH 06/15] Add secrets from env, refactor loggic, simplify logs --- actions/register.ts | 14 ++-------- actions/test/run_local.ts | 29 +++++++++++++++---- actions/utils.ts | 59 +++++++++++++++++++++++++++++++++++++++ actions/watch.ts | 51 ++++++++++----------------------- 4 files changed, 98 insertions(+), 55 deletions(-) create mode 100644 actions/utils.ts diff --git a/actions/register.ts b/actions/register.ts index 897d701..257ecb7 100644 --- a/actions/register.ts +++ b/actions/register.ts @@ -23,11 +23,6 @@ export const addContract: ActionFn = async (context: Context, event: Event) => { // Load the registry const registry = await Registry.load(context, transactionEvent.network); - console.log( - `Current registry: ${JSON.stringify( - Array.from(registry.ownerOrders.entries()) - )}` - ); // Process the logs transactionEvent.logs.forEach((log) => { @@ -79,11 +74,6 @@ export const addContract: ActionFn = async (context: Context, event: Event) => { } } }); - console.log( - `Updated registry: ${JSON.stringify( - Array.from(registry.ownerOrders.entries()) - )}` - ); await registry.write(); }; @@ -105,7 +95,7 @@ export const add = async ( if (registry.ownerOrders.has(owner)) { const conditionalOrders = registry.ownerOrders.get(owner); console.log( - `adding conditional order ${params} to already existing contract ${owner}` + `Adding conditional order ${params} to already existing contract ${owner}` ); let exists: boolean = false; // Iterate over the conditionalOrders to make sure that the params are not already in the registry @@ -127,7 +117,7 @@ export const add = async ( }); } } else { - console.log(`adding conditional order ${params} to new contract ${owner}`); + console.log(`Adding conditional order ${params} to new contract ${owner}`); registry.ownerOrders.set( owner, new Set([{ params, proof, orders: new Map(), composableCow }]) diff --git a/actions/test/run_local.ts b/actions/test/run_local.ts index b64ff92..58dc03c 100644 --- a/actions/test/run_local.ts +++ b/actions/test/run_local.ts @@ -6,19 +6,36 @@ import { import { checkForAndPlaceOrder } from "../watch"; import { addContract } from "../register"; import { ethers } from "ethers"; +import assert = require("assert"); +import { getProvider } from "../utils"; + +require("dotenv").config(); const main = async () => { const testRuntime = new TestRuntime(); - const node_url = process.env["ETH_RPC_URL"]; - if (!node_url) { - throw "Please specify your node url via the ETH_RPC_URL env variable"; + // The web3 actions fetches the node url and computes the API based on the current chain id + const network = process.env.NETWORK; + assert(network, "network is required"); + + // Add secrets from local env (.env) for current network + const envNames = [ + `NODE_URL_${network}`, + `NODE_USER_${network}`, + `NODE_PASSWORD_${network}`, + ]; + for (const name of envNames) { + const envValue = process.env[name]; + if (envValue) { + await testRuntime.context.secrets.put(name, envValue); + } } - // The web3 actions fetches the node url and computes the API based on the current chain id - const provider = new ethers.providers.JsonRpcProvider(node_url); + // Get provider + const provider = await getProvider(testRuntime.context, network); + const nodeUrl = process.env[`NODE_URL_${network}`]; + new ethers.providers.JsonRpcProvider(nodeUrl); const { chainId } = await provider.getNetwork(); - await testRuntime.context.secrets.put(`NODE_URL_${chainId}`, node_url); provider.on("block", async (blockNumber) => { // Block watcher for creating new orders diff --git a/actions/utils.ts b/actions/utils.ts new file mode 100644 index 0000000..e98ac73 --- /dev/null +++ b/actions/utils.ts @@ -0,0 +1,59 @@ +import { Context } from "@tenderly/actions"; +import assert = require("assert"); + +import { ethers } from "ethers"; +import { ConnectionInfo, Logger } from "ethers/lib/utils"; + +const RPC_NETWORK_WITH_AUTH = ["100"]; + +async function getSecret(key: string, context: Context): Promise { + const value = await context.secrets.get(key); + assert(value, `${key} secret is required`); + + return value; +} + +export async function getProvider( + context: Context, + network: string +): Promise { + Logger.setLogLevel(Logger.levels.DEBUG); + const useAuth = RPC_NETWORK_WITH_AUTH.includes(network); + + const url = await getSecret(`NODE_URL_${network}`, context); + const providerConfig: ConnectionInfo = useAuth + ? { + url, + // TODO: This is a hack to make it work for HTTP endpoints (while we don't have a HTTPS one for Gnosis Chain), however I will delete once we have it + headers: { + Authorization: getAuthHeader({ + user: await getSecret(`NODE_USER_${network}`, context), + password: await getSecret(`NODE_PASSWORD_${network}`, context), + }), + }, + // user: await getSecret(`NODE_USER_${network}`, context), + // password: await getSecret(`NODE_PASSWORD_${network}`, context), + } + : { url }; + + return new ethers.providers.JsonRpcProvider(providerConfig); +} + +function getAuthHeader({ user, password }: { user: string; password: string }) { + return "Basic " + Buffer.from(`${user}:${password}`).toString("base64"); +} + +export function apiUrl(network: string): string { + switch (network) { + case "1": + return "https://api.cow.fi/mainnet"; + case "5": + return "https://api.cow.fi/goerli"; + case "100": + return "https://api.cow.fi/xdai"; + case "31337": + return "http://localhost:3000"; + default: + throw "Unsupported network"; + } +} diff --git a/actions/watch.ts b/actions/watch.ts index 0dcd5ce..7024513 100644 --- a/actions/watch.ts +++ b/actions/watch.ts @@ -13,10 +13,12 @@ import { } from "@cowprotocol/contracts"; import axios from "axios"; + import { BigNumber, ethers } from "ethers"; import { ComposableCoW__factory, GPv2Settlement__factory } from "./types"; import { Registry, OrderStatus } from "./register"; import { BytesLike, Logger } from "ethers/lib/utils"; +import { apiUrl, getProvider } from "./utils"; const GPV2SETTLEMENT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"; @@ -33,11 +35,6 @@ export const checkForSettlement: ActionFn = async ( const iface = GPv2Settlement__factory.createInterface(); const registry = await Registry.load(context, transactionEvent.network); - console.log( - `Current registry: ${JSON.stringify( - Array.from(registry.ownerOrders.entries()) - )}` - ); transactionEvent.logs.forEach((log) => { if (log.topics[0] === iface.getEventTopic("Trade")) { @@ -56,18 +53,13 @@ export const checkForSettlement: ActionFn = async ( // Check if the orderUid is in the conditionalOrder if (conditionalOrder.orders.has(orderUid)) { // Update the status of the orderUid to FILLED + console.log(`Update order ${orderUid} to status FILLED`); conditionalOrder.orders.set(orderUid, OrderStatus.FILLED); } }); } } }); - - console.log( - `Updated registry: ${JSON.stringify( - Array.from(registry.ownerOrders.entries()) - )}` - ); await registry.write(); }; @@ -84,12 +76,8 @@ export const checkForAndPlaceOrder: ActionFn = async ( const registry = await Registry.load(context, blockEvent.network); const chainContext = await ChainContext.create(context, blockEvent.network); - console.log(`Processing block ${blockEvent.blockNumber}...`); - // enumerate all the owners for (const [owner, conditionalOrders] of registry.ownerOrders.entries()) { - console.log(`Checking ${owner}...`); - // enumerate all the `ConditionalOrder`s for a given owner for (const conditionalOrder of conditionalOrders) { console.log(`Checking params ${conditionalOrder.params}...`); @@ -170,7 +158,7 @@ export const checkForAndPlaceOrder: ActionFn = async ( console.log("Removing conditional order from registry"); conditionalOrders.delete(conditionalOrder); } - console.log(`Unexpected error while processing order: ${e}`); + console.error(`Unexpected error while processing order: ${e}`); } } } @@ -232,20 +220,23 @@ async function placeOrder(order: any, api_url: string) { console.log(`API request: ${JSON.stringify(postData)}`); } } catch (error: any) { + const errorMessag = "Error placing order in API"; if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx - console.log(JSON.stringify(error.response)); + console.error( + `${errorMessag}. Result: ${JSON.stringify(error.response)}` + ); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js - console.log(error.request); + console.error(`${errorMessag}. Unresponsive API: ${error.request}`); } else if (error.message) { // Something happened in setting up the request that triggered an Error - console.log("Error", error.message); + console.error(`${errorMessag}. Internal Error: ${error.message}`); } else { - console.log(error); + console.error(`${errorMessag}. Unhandled Error: ${error.message}`); } throw error; } @@ -312,23 +303,9 @@ class ChainContext { context: Context, network: string ): Promise { - const node_url = await context.secrets.get(`NODE_URL_${network}`); - const provider = new ethers.providers.JsonRpcProvider(node_url); - return new ChainContext(provider, apiUrl(network)); - } -} + const provider = await getProvider(context, network); -function apiUrl(network: string): string { - switch (network) { - case "1": - return "https://api.cow.fi/mainnet"; - case "5": - return "https://api.cow.fi/goerli"; - case "100": - return "https://api.cow.fi/xdai"; - case "31337": - return "http://localhost:3000"; - default: - throw "Unsupported network"; + const providerNetwork = await provider.getNetwork(); + return new ChainContext(provider, apiUrl(network)); } } From 08eabb915908d97ec41406d72fcbe682239f6a71 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Mon, 7 Aug 2023 20:39:45 +0100 Subject: [PATCH 07/15] Improve logs --- actions/register.ts | 6 +- actions/utils.ts | 12 +++ actions/watch.ts | 232 +++++++++++++++++++++++++++----------------- 3 files changed, 157 insertions(+), 93 deletions(-) diff --git a/actions/register.ts b/actions/register.ts index 257ecb7..2be8eab 100644 --- a/actions/register.ts +++ b/actions/register.ts @@ -95,7 +95,7 @@ export const add = async ( if (registry.ownerOrders.has(owner)) { const conditionalOrders = registry.ownerOrders.get(owner); console.log( - `Adding conditional order ${params} to already existing contract ${owner}` + `[register:add] Adding conditional order ${params} to already existing contract ${owner}` ); let exists: boolean = false; // Iterate over the conditionalOrders to make sure that the params are not already in the registry @@ -117,7 +117,9 @@ export const add = async ( }); } } else { - console.log(`Adding conditional order ${params} to new contract ${owner}`); + console.log( + `[register:add] Adding conditional order ${params} to new contract ${owner}` + ); registry.ownerOrders.set( owner, new Set([{ params, proof, orders: new Map(), composableCow }]) diff --git a/actions/utils.ts b/actions/utils.ts index e98ac73..0d1885b 100644 --- a/actions/utils.ts +++ b/actions/utils.ts @@ -3,6 +3,7 @@ import assert = require("assert"); import { ethers } from "ethers"; import { ConnectionInfo, Logger } from "ethers/lib/utils"; +import { OrderStatus } from "./register"; const RPC_NETWORK_WITH_AUTH = ["100"]; @@ -57,3 +58,14 @@ export function apiUrl(network: string): string { throw "Unsupported network"; } } + +export function formatStatus(status: OrderStatus) { + switch (status) { + case OrderStatus.FILLED: + return "FILLED"; + case OrderStatus.SUBMITTED: + return "SUBMITTED"; + default: + return "UNKNOWN"; + } +} diff --git a/actions/watch.ts b/actions/watch.ts index 7024513..a98ddd2 100644 --- a/actions/watch.ts +++ b/actions/watch.ts @@ -15,10 +15,14 @@ import { import axios from "axios"; import { BigNumber, ethers } from "ethers"; -import { ComposableCoW__factory, GPv2Settlement__factory } from "./types"; -import { Registry, OrderStatus } from "./register"; +import { + ComposableCoW, + ComposableCoW__factory, + GPv2Settlement__factory, +} from "./types"; +import { Registry, OrderStatus, ConditionalOrder } from "./register"; import { BytesLike, Logger } from "ethers/lib/utils"; -import { apiUrl, getProvider } from "./utils"; +import { apiUrl, formatStatus, getProvider } from "./utils"; const GPV2SETTLEMENT = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"; @@ -63,6 +67,24 @@ export const checkForSettlement: ActionFn = async ( await registry.write(); }; +async function getTradeableOrderWithSignature( + owner: string, + conditionalOrder: ConditionalOrder, + contract: ComposableCoW +) { + return contract.callStatic + .getTradeableOrderWithSignature( + owner, + conditionalOrder.params, + "0x", + conditionalOrder.proof ? conditionalOrder.proof.path : [] + ) + .catch((e) => { + console.error('Error during "getTradeableOrderWithSignature" call', e); + throw e; + }); +} + /** * Watch for new blocks and check for orders to place * @param context tenderly context @@ -75,97 +97,123 @@ export const checkForAndPlaceOrder: ActionFn = async ( const blockEvent = event as BlockEvent; const registry = await Registry.load(context, blockEvent.network); const chainContext = await ChainContext.create(context, blockEvent.network); + const { network } = blockEvent; // enumerate all the owners for (const [owner, conditionalOrders] of registry.ownerOrders.entries()) { + const deletedOrders = []; // enumerate all the `ConditionalOrder`s for a given owner for (const conditionalOrder of conditionalOrders) { - console.log(`Checking params ${conditionalOrder.params}...`); + // console.log(`Checking params ${conditionalOrder.params}...`); const contract = ComposableCoW__factory.connect( conditionalOrder.composableCow, chainContext.provider ); - try { - const { order, signature } = - await contract.callStatic.getTradeableOrderWithSignature( - owner, - conditionalOrder.params, - "0x", - conditionalOrder.proof ? conditionalOrder.proof.path : [] - ); - const orderToSubmit: Order = { - ...order, - kind: kindToString(order.kind), - sellTokenBalance: balanceToString(order.sellTokenBalance), - buyTokenBalance: balanceToString(order.buyTokenBalance), - }; + const { deleteConditionalOrder } = await _checkForAndPlaceOrder( + owner, + network, + conditionalOrder, + contract, + chainContext + ); - // calculate the orderUid - const orderUid = computeOrderUid( - { - name: "Gnosis Protocol", - version: "v2", - chainId: blockEvent.network, - verifyingContract: GPV2SETTLEMENT, - }, - { - ...orderToSubmit, - receiver: - orderToSubmit.receiver === ethers.constants.AddressZero - ? undefined - : orderToSubmit.receiver, - }, - owner - ); + if (deleteConditionalOrder) { + deletedOrders.push(conditionalOrder); + } + } - // if the orderUid has not been submitted, or filled, then place the order - if (!conditionalOrder.orders.has(orderUid)) { - console.log( - `Placing orderuid ${orderUid} with Order: ${JSON.stringify(order)}` - ); + deletedOrders.forEach((conditionalOrder) => + conditionalOrders.delete(conditionalOrder) + ); + } - await placeOrder( - { ...orderToSubmit, from: owner, signature }, - chainContext.api_url - ); + // Update the registry + await registry.write(); +}; + +async function _checkForAndPlaceOrder( + owner: string, + network: string, + conditionalOrder: ConditionalOrder, + contract: ComposableCoW, + chainContext: ChainContext +): Promise<{ deleteConditionalOrder: boolean }> { + try { + const { order, signature } = await getTradeableOrderWithSignature( + owner, + conditionalOrder, + contract + ); + + const orderToSubmit: Order = { + ...order, + kind: kindToString(order.kind), + sellTokenBalance: balanceToString(order.sellTokenBalance), + buyTokenBalance: balanceToString(order.buyTokenBalance), + }; - conditionalOrder.orders.set(orderUid, OrderStatus.SUBMITTED); - } else { + // calculate the orderUid + const orderUid = getOrderUid(network, orderToSubmit, owner); + + // if the orderUid has not been submitted, or filled, then place the order + if (!conditionalOrder.orders.has(orderUid)) { + await placeOrder( + { ...orderToSubmit, from: owner, signature }, + chainContext.api_url + ); + + conditionalOrder.orders.set(orderUid, OrderStatus.SUBMITTED); + } else { + const orderStatus = conditionalOrder.orders.get(orderUid); + console.log( + `OrderUid ${orderUid} status: ${ + orderStatus ? formatStatus(orderStatus) : "Not found" + }` + ); + } + } catch (e: any) { + if (e.code === Logger.errors.CALL_EXCEPTION) { + switch (e.errorName) { + case "OrderNotValid": + // The conditional order has not expired, or been cancelled, but the order is not valid + // For example, with TWAPs, this may be after `span` seconds have passed in the epoch. + return { deleteConditionalOrder: false }; + case "SingleOrderNotAuthed": console.log( - `OrderUid ${orderUid} status: ${conditionalOrder.orders.get( - orderUid - )}` + `Single order on safe ${owner} not authed. Unfilled orders:` ); - } - } catch (e: any) { - if (e.code === Logger.errors.CALL_EXCEPTION) { - switch (e.errorName) { - case "OrderNotValid": - // The conditional order has not expired, or been cancelled, but the order is not valid - // For example, with TWAPs, this may be after `span` seconds have passed in the epoch. - continue; - case "SingleOrderNotAuthed": - console.log( - `Single order on safe ${owner} not authed. Unfilled orders:` - ); - case "ProofNotAuthed": - console.log( - `Proof on safe ${owner} not authed. Unfilled orders:` - ); - } - printUnfilledOrders(conditionalOrder.orders); - console.log("Removing conditional order from registry"); - conditionalOrders.delete(conditionalOrder); - } - console.error(`Unexpected error while processing order: ${e}`); + case "ProofNotAuthed": + console.log(`Proof on safe ${owner} not authed. Unfilled orders:`); } + printUnfilledOrders(conditionalOrder.orders); + console.log("Removing conditional order from registry"); + return { deleteConditionalOrder: true }; } + console.error(`Unexpected error while processing order: ${e?.message}`); } - // Update the registry - await registry.write(); -}; + return { deleteConditionalOrder: false }; +} + +function getOrderUid(network: string, orderToSubmit: Order, owner: string) { + return computeOrderUid( + { + name: "Gnosis Protocol", + version: "v2", + chainId: network, + verifyingContract: GPV2SETTLEMENT, + }, + { + ...orderToSubmit, + receiver: + orderToSubmit.receiver === ethers.constants.AddressZero + ? undefined + : orderToSubmit.receiver, + }, + owner + ); +} // --- Helpers --- @@ -174,7 +222,7 @@ export const checkForAndPlaceOrder: ActionFn = async ( * @param orders All the orders that are being tracked */ export const printUnfilledOrders = (orders: Map) => { - console.log("Unfilled orders:"); + console.log(`Unfilled orders (${orders.size}):`); for (const [orderUid, status] of orders.entries()) { if (status === OrderStatus.SUBMITTED) { console.log(orderUid); @@ -185,9 +233,9 @@ export const printUnfilledOrders = (orders: Map) => { /** * Place a new order * @param order to be placed on the cow protocol api - * @param api_url rest api url + * @param apiUrl rest api url */ -async function placeOrder(order: any, api_url: string) { +async function placeOrder(order: any, apiUrl: string) { try { const postData = { sellToken: order.sellToken, @@ -208,25 +256,27 @@ async function placeOrder(order: any, api_url: string) { }; // if the api_url doesn't contain localhost, post - if (!api_url.includes("localhost")) { - const { data } = await axios.post(`${api_url}/api/v1/orders`, postData, { - headers: { - "Content-Type": "application/json", - accept: "application/json", - }, - }); - console.log(`API response: ${data}`); - } else { - console.log(`API request: ${JSON.stringify(postData)}`); + console.log(`[placeOrder] API request with params:`, apiUrl, postData); + if (!apiUrl.includes("localhost")) { + const { status, data } = await axios.post( + `${apiUrl}/api/v1/orders`, + postData, + { + headers: { + "Content-Type": "application/json", + accept: "application/json", + }, + } + ); + console.log(`[placeOrder] API response`, { status, data }); } } catch (error: any) { - const errorMessag = "Error placing order in API"; + const errorMessag = "[placeOrder] Error placing order in API"; if (error.response) { + const { status, data } = error.response; // The request was made and the server responded with a status code // that falls out of the range of 2xx - console.error( - `${errorMessag}. Result: ${JSON.stringify(error.response)}` - ); + console.error(`${errorMessag}. Result: ${status}`, data); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of From 631337831e14c3594192b7ba692001e62599346b Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Aug 2023 08:57:41 +0100 Subject: [PATCH 08/15] Remove unused --- actions/test/run_local.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/actions/test/run_local.ts b/actions/test/run_local.ts index 58dc03c..e994a87 100644 --- a/actions/test/run_local.ts +++ b/actions/test/run_local.ts @@ -33,8 +33,6 @@ const main = async () => { // Get provider const provider = await getProvider(testRuntime.context, network); - const nodeUrl = process.env[`NODE_URL_${network}`]; - new ethers.providers.JsonRpcProvider(nodeUrl); const { chainId } = await provider.getNetwork(); provider.on("block", async (blockNumber) => { From 7703955f332548a4b5125af617ac0c042a6ed70e Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Aug 2023 09:29:45 +0100 Subject: [PATCH 09/15] Show the status number --- actions/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/utils.ts b/actions/utils.ts index 0d1885b..def05d0 100644 --- a/actions/utils.ts +++ b/actions/utils.ts @@ -66,6 +66,6 @@ export function formatStatus(status: OrderStatus) { case OrderStatus.SUBMITTED: return "SUBMITTED"; default: - return "UNKNOWN"; + return `UNKNOWN (${status})`; } } From 323924dc193f5e375c617a418dbfeb8cafe4309a Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Aug 2023 10:49:21 +0100 Subject: [PATCH 10/15] Try to catch error getting user/password to handle optionality --- actions/utils.ts | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/actions/utils.ts b/actions/utils.ts index def05d0..506732c 100644 --- a/actions/utils.ts +++ b/actions/utils.ts @@ -5,8 +5,6 @@ import { ethers } from "ethers"; import { ConnectionInfo, Logger } from "ethers/lib/utils"; import { OrderStatus } from "./register"; -const RPC_NETWORK_WITH_AUTH = ["100"]; - async function getSecret(key: string, context: Context): Promise { const value = await context.secrets.get(key); assert(value, `${key} secret is required`); @@ -19,23 +17,26 @@ export async function getProvider( network: string ): Promise { Logger.setLogLevel(Logger.levels.DEBUG); - const useAuth = RPC_NETWORK_WITH_AUTH.includes(network); const url = await getSecret(`NODE_URL_${network}`, context); - const providerConfig: ConnectionInfo = useAuth - ? { - url, - // TODO: This is a hack to make it work for HTTP endpoints (while we don't have a HTTPS one for Gnosis Chain), however I will delete once we have it - headers: { - Authorization: getAuthHeader({ - user: await getSecret(`NODE_USER_${network}`, context), - password: await getSecret(`NODE_PASSWORD_${network}`, context), - }), - }, - // user: await getSecret(`NODE_USER_${network}`, context), - // password: await getSecret(`NODE_PASSWORD_${network}`, context), - } - : { url }; + const user = await getSecret(`NODE_USER_${network}`, context).catch( + () => undefined + ); + const password = await getSecret(`NODE_PASSWORD_${network}`, context).catch( + () => undefined + ); + const providerConfig: ConnectionInfo = + user && password + ? { + url, + // TODO: This is a hack to make it work for HTTP endpoints (while we don't have a HTTPS one for Gnosis Chain), however I will delete once we have it + headers: { + Authorization: getAuthHeader({ user, password }), + }, + // user: await getSecret(`NODE_USER_${network}`, context), + // password: await getSecret(`NODE_PASSWORD_${network}`, context), + } + : { url }; return new ethers.providers.JsonRpcProvider(providerConfig); } From 70bd679495b9516bbb5dac9d57c820b083153b71 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Aug 2023 11:01:50 +0100 Subject: [PATCH 11/15] Add back log --- actions/watch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/watch.ts b/actions/watch.ts index a98ddd2..5a9851e 100644 --- a/actions/watch.ts +++ b/actions/watch.ts @@ -104,7 +104,7 @@ export const checkForAndPlaceOrder: ActionFn = async ( const deletedOrders = []; // enumerate all the `ConditionalOrder`s for a given owner for (const conditionalOrder of conditionalOrders) { - // console.log(`Checking params ${conditionalOrder.params}...`); + console.log(`Checking params ${conditionalOrder.params}...`); const contract = ComposableCoW__factory.connect( conditionalOrder.composableCow, chainContext.provider From 33c3ca8658b54b44d5572c3facbde701d33c827b Mon Sep 17 00:00:00 2001 From: mfw78 Date: Tue, 8 Aug 2023 12:13:58 +0000 Subject: [PATCH 12/15] chore: forge env vars --- .env.example | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.env.example b/.env.example index 4654847..df8ebc6 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,14 @@ ETH_RPC_URL=http://erigon.dappnode:8545 PRIVATE_KEY= +# Forge deployment scripts: +SETTLEMENT=0x9008D19f58AAbD9eD0D60971565AA8510560ab41 # GPv2Settlement contract to use for constructor initialisation. +# ETHERSCAN_API_KEY= +# Forge single order submission scripts: +# SAFE= +# TWAP= +# COMPOSABLE_COW= + # Tenderly Local Tests NETWORK=5 # Network to test. 1: Mainnet, 5: Goerli, 100: xDai # NODE_URL_1= From d651577eb5f7f01fdf4dcb0a19a750f6d7ec8e0b Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Tue, 8 Aug 2023 14:07:10 +0100 Subject: [PATCH 13/15] Add orderId in the logs --- actions/watch.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/actions/watch.ts b/actions/watch.ts index 5a9851e..3871a0f 100644 --- a/actions/watch.ts +++ b/actions/watch.ts @@ -159,6 +159,7 @@ async function _checkForAndPlaceOrder( // if the orderUid has not been submitted, or filled, then place the order if (!conditionalOrder.orders.has(orderUid)) { await placeOrder( + orderUid, { ...orderToSubmit, from: owner, signature }, chainContext.api_url ); @@ -235,7 +236,7 @@ export const printUnfilledOrders = (orders: Map) => { * @param order to be placed on the cow protocol api * @param apiUrl rest api url */ -async function placeOrder(order: any, apiUrl: string) { +async function placeOrder(orderUid: string, order: any, apiUrl: string) { try { const postData = { sellToken: order.sellToken, @@ -256,7 +257,11 @@ async function placeOrder(order: any, apiUrl: string) { }; // if the api_url doesn't contain localhost, post - console.log(`[placeOrder] API request with params:`, apiUrl, postData); + console.log( + `[placeOrder] Post order ${orderUid} with params:`, + apiUrl, + postData + ); if (!apiUrl.includes("localhost")) { const { status, data } = await axios.post( `${apiUrl}/api/v1/orders`, From 4964e3409406a245c47f13a01a3b6b47295a4da7 Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 10 Aug 2023 13:43:38 +0100 Subject: [PATCH 14/15] Rename var --- actions/watch.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/actions/watch.ts b/actions/watch.ts index 3871a0f..0138fd7 100644 --- a/actions/watch.ts +++ b/actions/watch.ts @@ -101,10 +101,10 @@ export const checkForAndPlaceOrder: ActionFn = async ( // enumerate all the owners for (const [owner, conditionalOrders] of registry.ownerOrders.entries()) { - const deletedOrders = []; + const ordersPendingDelete = []; // enumerate all the `ConditionalOrder`s for a given owner for (const conditionalOrder of conditionalOrders) { - console.log(`Checking params ${conditionalOrder.params}...`); + // console.log(`Checking params ${conditionalOrder.params}...`); const contract = ComposableCoW__factory.connect( conditionalOrder.composableCow, chainContext.provider @@ -119,11 +119,11 @@ export const checkForAndPlaceOrder: ActionFn = async ( ); if (deleteConditionalOrder) { - deletedOrders.push(conditionalOrder); + ordersPendingDelete.push(conditionalOrder); } } - deletedOrders.forEach((conditionalOrder) => + ordersPendingDelete.forEach((conditionalOrder) => conditionalOrders.delete(conditionalOrder) ); } From f8c4a3c257e93d428d2276a3a2b4850cac110e9c Mon Sep 17 00:00:00 2001 From: Anxo Rodriguez Date: Thu, 10 Aug 2023 14:16:39 +0100 Subject: [PATCH 15/15] Update readme with new addresses --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2eee54c..89452a4 100644 --- a/README.md +++ b/README.md @@ -224,9 +224,9 @@ tenderly actions deploy | Contact Name | Ethereum Mainnet | Goerli | Gnosis Chain | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `ComposableCoW` | [0xF487887DA5a4b4e3eC114FDAd97dc0F785d72738](https://etherscan.io/address/0xF487887DA5a4b4e3eC114FDAd97dc0F785d72738) | [0xF487887DA5a4b4e3eC114FDAd97dc0F785d72738](https://goerli.etherscan.io/0xF487887DA5a4b4e3eC114FDAd97dc0F785d72738) | [0xF487887DA5a4b4e3eC114FDAd97dc0F785d72738](https://gnosisscan.io/address/0xF487887DA5a4b4e3eC114FDAd97dc0F785d72738) | +| `ComposableCoW` | [0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74](https://etherscan.io/address/0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74) | [0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74](https://goerli.etherscan.io/0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74) | [0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74](https://gnosisscan.io/address/0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74) | | `TWAP` | [0x910d00a310f7Dc5B29FE73458F47f519be547D3d](https://etherscan.io/address/0x910d00a310f7Dc5B29FE73458F47f519be547D3d) | [0x910d00a310f7Dc5B29FE73458F47f519be547D3d](https://goerli.etherscan.io/0x910d00a310f7Dc5B29FE73458F47f519be547D3d) | [0x910d00a310f7Dc5B29FE73458F47f519be547D3d](https://gnosisscan.io/address/0x910d00a310f7Dc5B29FE73458F47f519be547D3d) | -| `CurrentBlockTimestampFactory` | [0x0899c7DC280363d263Cc954506134F249CceC4a5](https://etherscan.io/address/0x0899c7DC280363d263Cc954506134F249CceC4a5) | [0x0899c7DC280363d263Cc954506134F249CceC4a5](https://goerli.etherscan.io/0x0899c7DC280363d263Cc954506134F249CceC4a5) | [0x0899c7DC280363d263Cc954506134F249CceC4a5](https://gnosisscan.io/address/0x0899c7DC280363d263Cc954506134F249CceC4a5) | +| `CurrentBlockTimestampFactory` | [0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc](https://etherscan.io/address/0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc) | [0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc](https://goerli.etherscan.io/0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc) | [0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc](https://gnosisscan.io/address/0x52eD56Da04309Aca4c3FECC595298d80C2f16BAc) | | `GoodAfterTime` | TBD | TBD | TBD | | `PerpetualStableSwap` | TBD | TBD | TBD | | `TradeAboveThreshold` | TBD | TBD | TBD |