From ec7d91e37087cf37ab19d71f7d807fe17e27df1b Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 10:09:35 +0100 Subject: [PATCH 01/11] chore: add abi directory to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9bfe6db..06f52c3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ wheels/ # Ape .pytest_cache/ -.build/ \ No newline at end of file +.build/ + +# Abi +abi/ \ No newline at end of file From 53ec108a6fc0ff9e0c30e0e07313b27010251cf6 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 10:17:11 +0100 Subject: [PATCH 02/11] feat: initial CoW Protocol bot setup Initialize SilverbackBot for monitoring CoW Protocol trades Define Gnosis Chain contract addresses Add GPv2Settlement contract interface and ABI loading --- bot.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 bot.py diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..ef2f885 --- /dev/null +++ b/bot.py @@ -0,0 +1,20 @@ +import json + +import Path +from ape import Contract +from silverback import SilverbackBot + +# Initialize bot +bot = SilverbackBot() + +# Load GPv2Settlement ABI +abi_path = Path("./abi/GPv2Settlement.json") +with open(abi_path) as f: + gpv2_settlement_abi = json.load(f) + +# Gnosis Chain Addresses +GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" +GNO_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" + +# Contracts +GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=gpv2_settlement_abi) From 51de1f51dfecac114beec11cd750ff7205cd5190 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 10:21:28 +0100 Subject: [PATCH 03/11] chore: add .env to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 06f52c3..265f993 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,7 @@ wheels/ .build/ # Abi -abi/ \ No newline at end of file +abi/ + +# Environment vars +.env \ No newline at end of file From 35d2f3aa0c7d316ef4eb2e89ed71465f82bd80cf Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 10:30:12 +0100 Subject: [PATCH 04/11] chore: add .db local storage to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 265f993..b584979 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ wheels/ abi/ # Environment vars -.env \ No newline at end of file +.env + +# Local storage +.db \ No newline at end of file From 8a4ad089a18a85783fa805c900c1c2a37c212ef9 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 11:02:18 +0100 Subject: [PATCH 05/11] feat: add trade database persistence Add functions to load and save trade history to CSV Set up trade database schema with block number index Store core trade data: owner, tokens, amounts and timestamp --- bot.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index ef2f885..190207c 100644 --- a/bot.py +++ b/bot.py @@ -1,14 +1,22 @@ import json +import os +from pathlib import Path +from typing import Dict -import Path -from ape import Contract +import numpy as np +import pandas as pd +from ape import Contract, chain from silverback import SilverbackBot # Initialize bot bot = SilverbackBot() +# File path configuration +TRADE_FILEPATH = os.environ.get("TRADE_FILEPATH", ".db/trades.csv") +GPV2_ABI_FILEPATH = os.environ.get("GPV2_ABI_FILEPATH", "./abi/GPv2Settlement.json") + # Load GPv2Settlement ABI -abi_path = Path("./abi/GPv2Settlement.json") +abi_path = Path(GPV2_ABI_FILEPATH) with open(abi_path) as f: gpv2_settlement_abi = json.load(f) @@ -18,3 +26,39 @@ # Contracts GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=gpv2_settlement_abi) + +# Variables +START_BLOCK = int(os.environ.get("START_BLOCK", chain.blocks.head.number)) + + +# Local storage helper functions +def _load_trades_db() -> Dict: + """ + Load trades database from CSV file or create new if doesn't exist. + Returns dict with trade data indexed by transaction hash. + """ + dtype = { + "block_number": np.int64, + "owner": str, + "sellToken": str, + "buyToken": str, + "sellAmount": object, + "buyAmount": object, + "timestamp": np.int64, + } + + df = ( + pd.read_csv(TRADE_FILEPATH, dtype=dtype) + if os.path.exists(TRADE_FILEPATH) + else pd.DataFrame(columns=dtype.keys()).astype(dtype) + ) + return df.set_index("block_number").to_dict("index") + + +def _save_trades_db(trades_dict: Dict) -> None: + """ + Save trades dictionary back to CSV file. + """ + df = pd.DataFrame.from_dict(trades_dict, orient="index") + df.index.name = "transaction_hash" + df.to_csv(TRADE_FILEPATH) From 5a2fb6cb523516b44585051df65adbee7dbad26c Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 11:03:40 +0100 Subject: [PATCH 06/11] feat: add historical GNO trade log processing Add helper functions to fetch and process historical GNO trades Create LogFilter for GPv2Settlement Trade events Filter for trades involving GNO token Format trade logs for database storage --- bot.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 190207c..b6c1421 100644 --- a/bot.py +++ b/bot.py @@ -5,7 +5,8 @@ import numpy as np import pandas as pd -from ape import Contract, chain +from ape import Contract, accounts, chain +from ape.types import LogFilter from silverback import SilverbackBot # Initialize bot @@ -62,3 +63,51 @@ def _save_trades_db(trades_dict: Dict) -> None: df = pd.DataFrame.from_dict(trades_dict, orient="index") df.index.name = "transaction_hash" df.to_csv(TRADE_FILEPATH) + + +# Historical log helper functions +def _process_trade_log(log) -> Dict: + """Process trade log and return formatted dictionary entry""" + return { + "block_number": log.block_number, + "owner": log.owner, + "sellToken": log.sellToken, + "buyToken": log.buyToken, + "sellAmount": str(log.sellAmount), + "buyAmount": str(log.buyAmount), + "timestamp": log.timestamp, + } + + +def _get_historical_gno_trades( + settlement_contract, + gno_address: str, + start_block: int, + stop_block: int = chain.blocks.head.number, +): + """Get historical GNO trades from start_block to stop_block""" + log_filter = LogFilter( + addresses=[settlement_contract.address], + events=[settlement_contract.Trade.abi], + start_block=start_block, + stop_block=stop_block, + ) + + for log in accounts.provider.get_contract_logs(log_filter): + if log.sellToken == gno_address or log.buyToken == gno_address: + yield log + + +def _process_historical_gno_trades( + settlement_contract, gno_address: str, start_block: int, stop_block: int +) -> Dict: + """Process historical GNO trades and store in database""" + trades_db = _load_trades_db() + + for log in _get_historical_gno_trades( + settlement_contract, gno_address, start_block, stop_block + ): + trades_db[log.transaction_hash] = _process_trade_log(log) + + _save_trades_db(trades_db) + return trades_db From 04962f40412dbaf3cccb71300b084c01b72b95bc Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 11:04:22 +0100 Subject: [PATCH 07/11] feat: initialize historical trade processing on startup Add startup handler to process GNO trades Process trades from START_BLOCK to current block Store processed trades in database --- bot.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index b6c1421..7996d39 100644 --- a/bot.py +++ b/bot.py @@ -7,7 +7,7 @@ import pandas as pd from ape import Contract, accounts, chain from ape.types import LogFilter -from silverback import SilverbackBot +from silverback import SilverbackBot, StateSnapshot # Initialize bot bot = SilverbackBot() @@ -111,3 +111,16 @@ def _process_historical_gno_trades( _save_trades_db(trades_db) return trades_db + + +# Silverback bot +@bot.on_startup() +def app_startup(startup_state: StateSnapshot): + _process_historical_gno_trades( + GPV2_SETTLEMENT_CONTRACT, + GNO_ADDRESS, + start_block=START_BLOCK, + stop_block=chain.blocks.head.number, + ) + + return {"message": "Starting...", "block_number": startup_state.last_block_seen} From 597243d2ad0ed3f8ff66c7a88818536019ed0951 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 11:04:47 +0100 Subject: [PATCH 08/11] docs: add bot run command to readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3b74b9b..eae2c78 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # Cow Swap Agent Automated CoW Swap trading agent built with Silverback SDK + +## Run + +```bash +silverback run --network gnosis:mainnet:alchemy +``` From 3433022eaa01c28150abbfd5599d3c3660c9ffba Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 11:05:51 +0100 Subject: [PATCH 09/11] chore: add .silverback to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b584979..51216f0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ abi/ .env # Local storage -.db \ No newline at end of file +.db + +# Silverback +.silverback* \ No newline at end of file From 0c863d5086699c1aeecc3184a9ffba2e6e24a815 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 11:08:44 +0100 Subject: [PATCH 10/11] feat: add block persistence tracking Add functions to load and save last processed block to CSV Track bot processing progress across restarts Use last processed block as starting point for historical trade processing --- bot.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 7996d39..b98da74 100644 --- a/bot.py +++ b/bot.py @@ -14,6 +14,7 @@ # File path configuration TRADE_FILEPATH = os.environ.get("TRADE_FILEPATH", ".db/trades.csv") +BLOCK_FILEPATH = os.environ.get("BLOCK_FILEPATH", ".db/block.csv") GPV2_ABI_FILEPATH = os.environ.get("GPV2_ABI_FILEPATH", "./abi/GPv2Settlement.json") # Load GPv2Settlement ABI @@ -65,6 +66,23 @@ def _save_trades_db(trades_dict: Dict) -> None: df.to_csv(TRADE_FILEPATH) +def _load_block_db() -> Dict: + """Load the last processed block from CSV file or create new if doesn't exist""" + df = ( + pd.read_csv(BLOCK_FILEPATH) + if os.path.exists(BLOCK_FILEPATH) + else pd.DataFrame({"last_processed_block": [START_BLOCK]}) + ) + return {"last_processed_block": df["last_processed_block"].iloc[0]} + + +def _save_block_db(data: Dict): + """Save the last processed block to CSV file""" + os.makedirs(os.path.dirname(BLOCK_FILEPATH), exist_ok=True) + df = pd.DataFrame([data]) + df.to_csv(BLOCK_FILEPATH, index=False) + + # Historical log helper functions def _process_trade_log(log) -> Dict: """Process trade log and return formatted dictionary entry""" @@ -116,11 +134,16 @@ def _process_historical_gno_trades( # Silverback bot @bot.on_startup() def app_startup(startup_state: StateSnapshot): + block_db = _load_block_db() + last_processed_block = block_db["last_processed_block"] + _process_historical_gno_trades( GPV2_SETTLEMENT_CONTRACT, GNO_ADDRESS, - start_block=START_BLOCK, + start_block=last_processed_block, stop_block=chain.blocks.head.number, ) + _save_block_db({"last_processed_block": chain.blocks.head.number}) + return {"message": "Starting...", "block_number": startup_state.last_block_seen} From a554a230818b3394cfce6bb5ab081edb3f7fbb9f Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Tue, 4 Feb 2025 14:03:48 +0100 Subject: [PATCH 11/11] fix: WETH Gnosis Chain address in test --- tests/test_allowlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_allowlist.py b/tests/test_allowlist.py index b57b218..1a5d367 100644 --- a/tests/test_allowlist.py +++ b/tests/test_allowlist.py @@ -6,7 +6,7 @@ class TokenAddresses: GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c" - WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + WETH = "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1" SAFE = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe"