From b213eecb3e9b6f480461f10bfd6f574860677541 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sat, 8 Feb 2025 23:24:03 +0100 Subject: [PATCH 1/6] feat: add CoW API quote payload construction Add ABI loading helper to reduce duplication Add quote payload construction for CoW API Add initial block handler for GNO/COW quotes Use Python booleans for JSON conversion --- cow-trader/bot.py | 67 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index 2393d8a..cca758f 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -1,13 +1,16 @@ import json import os from pathlib import Path -from typing import Dict +from typing import Annotated, Dict +import click import numpy as np import pandas as pd from ape import Contract, accounts, chain +from ape.api import BlockAPI from ape.types import LogFilter from silverback import SilverbackBot, StateSnapshot +from taskiq import Context, TaskiqDepends # Initialize bot bot = SilverbackBot() @@ -16,18 +19,27 @@ 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") +TOKEN_ALLOWLIST_FILEPATH = os.environ.get("TOKEN_ALLOWLIST_FILEPATH", "./abi/TokenAllowlist.json") -# Load GPv2Settlement ABI -abi_path = Path(GPV2_ABI_FILEPATH) -with open(abi_path) as f: - gpv2_settlement_abi = json.load(f) - -# Gnosis Chain Addresses +# Addresses +SAFE_ADDRESS = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe" # PLACEHOLDER +TOKEN_ALLOWLIST_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41" GNO_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb" +COW_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c" + + +# Load ABI helper function +def _load_abi(abi_name: str) -> Dict: + """Load ABI from json file""" + abi_path = Path(os.environ.get(f"{abi_name}_ABI_FILEPATH", f"./abi/{abi_name}.json")) + with open(abi_path) as f: + return json.load(f) + # Contracts -GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=gpv2_settlement_abi) +GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=_load_abi("GPv2Settlement")) +TOKEN_ALLOWLIST_CONTRACt = Contract(TOKEN_ALLOWLIST_ADDRESS, abi=_load_abi("TokenAllowlist")) # Variables START_BLOCK = int(os.environ.get("START_BLOCK", chain.blocks.head.number)) @@ -128,6 +140,33 @@ def _process_historical_gno_trades( return trades_db +# CoW Swap trading helper functions +def _construct_quote_payload( + sell_token: str, + buy_token: str, + sell_amount: str, +) -> Dict: + """ + Construct payload for CoW Protocol quote request using PreSign method. + Returns dict with required quote parameters. + """ + return { + "sellToken": sell_token, + "buyToken": buy_token, + "sellAmountBeforeFee": sell_amount, + "from": SAFE_ADDRESS, + "receiver": SAFE_ADDRESS, + "appData": "{}", + "appDataHash": "0xb48d38f93eaa084033fc5970bf96e559c33c4cdc07d889ab00b4d63f9590739d", + "sellTokenBalance": "erc20", + "buyTokenBalance": "erc20", + "priceQuality": "verified", + "signingScheme": "presign", + "onchainOrder": False, + "kind": "sell", + } + + # Silverback bot @bot.on_startup() def app_startup(startup_state: StateSnapshot): @@ -144,3 +183,15 @@ def app_startup(startup_state: StateSnapshot): _save_block_db({"last_processed_block": chain.blocks.head.number}) return {"message": "Starting...", "block_number": startup_state.last_block_seen} + + +@bot.on_(chain.blocks) +def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]): + """Execute block handler""" + quote_payload = _construct_quote_payload( + sell_token=GNO_ADDRESS, + buy_token=COW_ADDRESS, + sell_amount="1000000000000000000", + ) + + click.echo(f"Quote Payload: {quote_payload}") From 6ab1cbe01071b9287cfe87d43e25883928524a6b Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sat, 8 Feb 2025 23:38:04 +0100 Subject: [PATCH 2/6] feat: add CoW API quote request functionality Add API constants for CoW Swap endpoints Add quote payload construction with presign scheme Add quote request function with basic error handling Test successful quote request for GNO/COW pair --- cow-trader/bot.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index cca758f..df41cca 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -6,6 +6,7 @@ import click import numpy as np import pandas as pd +import requests from ape import Contract, accounts, chain from ape.api import BlockAPI from ape.types import LogFilter @@ -41,6 +42,10 @@ def _load_abi(abi_name: str) -> Dict: GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=_load_abi("GPv2Settlement")) TOKEN_ALLOWLIST_CONTRACt = Contract(TOKEN_ALLOWLIST_ADDRESS, abi=_load_abi("TokenAllowlist")) +# API +API_BASE_URL = "https://api.cow.fi/xdai/api/v1" +API_HEADERS = {"accept": "application/json", "Content-Type": "application/json"} + # Variables START_BLOCK = int(os.environ.get("START_BLOCK", chain.blocks.head.number)) @@ -167,6 +172,16 @@ def _construct_quote_payload( } +def _get_quote(payload: Dict) -> Dict: + """ + Get quote from CoW API + Returns quote response or raises exception + """ + response = requests.post(url=f"{API_BASE_URL}/quote", headers=API_HEADERS, json=payload) + response.raise_for_status() + return response.json() + + # Silverback bot @bot.on_startup() def app_startup(startup_state: StateSnapshot): @@ -189,9 +204,11 @@ def app_startup(startup_state: StateSnapshot): def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]): """Execute block handler""" quote_payload = _construct_quote_payload( - sell_token=GNO_ADDRESS, - buy_token=COW_ADDRESS, - sell_amount="1000000000000000000", + sell_token=GNO_ADDRESS, buy_token=COW_ADDRESS, sell_amount="1000000000000000000" ) - click.echo(f"Quote Payload: {quote_payload}") + try: + quote = _get_quote(quote_payload) + click.echo(f"Quote received: {quote}") + except requests.RequestException as e: + click.echo(f"Quote request failed: {e}") From e3a73767dd72c436ed75f326bacc4d30f5944840 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sat, 8 Feb 2025 23:43:49 +0100 Subject: [PATCH 3/6] feat: add CoW API order submission Add order payload construction from quote response Add order submission function to /orders endpoint --- cow-trader/bot.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index df41cca..083aee8 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -182,6 +182,43 @@ def _get_quote(payload: Dict) -> Dict: return response.json() +def _construct_order_payload(quote_response: Dict) -> Dict: + """ + Transform quote response into order request payload + """ + quote = quote_response["quote"] + + return { + "sellToken": quote["sellToken"], + "buyToken": quote["buyToken"], + "receiver": quote["receiver"], + "sellAmount": quote["sellAmount"], + "buyAmount": quote["buyAmount"], + "validTo": quote["validTo"], + "feeAmount": quote["feeAmount"], + "kind": quote["kind"], + "partiallyFillable": quote["partiallyFillable"], + "sellTokenBalance": quote["sellTokenBalance"], + "buyTokenBalance": quote["buyTokenBalance"], + "signingScheme": "presign", + "signature": "0x", + "from": quote_response["from"], + "quoteId": quote_response["id"], + "appData": quote["appData"], + "appDataHash": quote["appDataHash"], + } + + +def _submit_order(order_payload: Dict) -> Dict: + """ + Submit order to CoW API + Returns order response or raises exception + """ + response = requests.post(url=f"{API_BASE_URL}/orders", headers=API_HEADERS, json=order_payload) + response.raise_for_status() + return response.json() + + # Silverback bot @bot.on_startup() def app_startup(startup_state: StateSnapshot): From d677f04771e7270419c6934cd6c6e93c273a566f Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 9 Feb 2025 00:00:49 +0100 Subject: [PATCH 4/6] feat: add unified order creation and submission flow Add create_and_submit_order function that combines quote and order flow Clean up error handling in API functions Strip quotes from order UID response --- cow-trader/bot.py | 62 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index 083aee8..ea42849 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -195,7 +195,7 @@ def _construct_order_payload(quote_response: Dict) -> Dict: "sellAmount": quote["sellAmount"], "buyAmount": quote["buyAmount"], "validTo": quote["validTo"], - "feeAmount": quote["feeAmount"], + "feeAmount": "0", "kind": quote["kind"], "partiallyFillable": quote["partiallyFillable"], "sellTokenBalance": quote["sellTokenBalance"], @@ -209,14 +209,51 @@ def _construct_order_payload(quote_response: Dict) -> Dict: } -def _submit_order(order_payload: Dict) -> Dict: +def _submit_order(order_payload: Dict) -> str: """ Submit order to CoW API - Returns order response or raises exception + Returns order UID string or raises exception """ - response = requests.post(url=f"{API_BASE_URL}/orders", headers=API_HEADERS, json=order_payload) - response.raise_for_status() - return response.json() + try: + response = requests.post( + url=f"{API_BASE_URL}/orders", headers=API_HEADERS, json=order_payload + ) + response.raise_for_status() + return response.text.strip('"') + except requests.RequestException as e: + if e.response is not None: + error_data = e.response.json() + error_type = error_data.get("errorType", "Unknown") + error_description = error_data.get("description", str(e)) + raise Exception(f"{error_type} - {error_description}") + raise Exception(f"Order request failed: {e}") + + +def create_and_submit_order( + sell_token: str, + buy_token: str, + sell_amount: str, +) -> tuple[str | None, str | None]: + """ + Create and submit order to CoW API + Returns (order_uid, error_message) + """ + try: + quote_payload = _construct_quote_payload( + sell_token=sell_token, buy_token=buy_token, sell_amount=sell_amount + ) + quote = _get_quote(quote_payload) + click.echo(f"Quote received: {quote}") + + order_payload = _construct_order_payload(quote) + click.echo(f"Submitting order payload: {order_payload}") + + order_uid = _submit_order(order_payload) + click.echo(f"Order response: {order_uid}") + return order_uid, None + + except Exception as e: + return None, str(e) # Silverback bot @@ -240,12 +277,11 @@ def app_startup(startup_state: StateSnapshot): @bot.on_(chain.blocks) def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]): """Execute block handler""" - quote_payload = _construct_quote_payload( - sell_token=GNO_ADDRESS, buy_token=COW_ADDRESS, sell_amount="1000000000000000000" + order_uid, error = create_and_submit_order( + sell_token=GNO_ADDRESS, buy_token=COW_ADDRESS, sell_amount="20000000000000000000" ) - try: - quote = _get_quote(quote_payload) - click.echo(f"Quote received: {quote}") - except requests.RequestException as e: - click.echo(f"Quote request failed: {e}") + if error: + click.echo(f"Order failed: {error}") + else: + click.echo(f"Order submitted successfully. UID: {order_uid}") From 9ef577278bca52374c34eeb70252000bd64003b4 Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 9 Feb 2025 00:25:43 +0100 Subject: [PATCH 5/6] feat: add order storage with encoded data Add order storage in CSV with encoded GPv2Order.Data struct Move GPv2Order ABI to separate file Add order saving on successful submission --- cow-trader/bot.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index ea42849..40d485b 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -10,6 +10,7 @@ from ape import Contract, accounts, chain from ape.api import BlockAPI from ape.types import LogFilter +from eth_abi import encode from silverback import SilverbackBot, StateSnapshot from taskiq import Context, TaskiqDepends @@ -21,6 +22,7 @@ BLOCK_FILEPATH = os.environ.get("BLOCK_FILEPATH", ".db/block.csv") GPV2_ABI_FILEPATH = os.environ.get("GPV2_ABI_FILEPATH", "./abi/GPv2Settlement.json") TOKEN_ALLOWLIST_FILEPATH = os.environ.get("TOKEN_ALLOWLIST_FILEPATH", "./abi/TokenAllowlist.json") +ORDERS_FILEPATH = os.environ.get("ORDERS_FILEPATH", ".db/orders.csv") # Addresses SAFE_ADDRESS = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe" # PLACEHOLDER @@ -30,7 +32,7 @@ COW_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c" -# Load ABI helper function +# ABI def _load_abi(abi_name: str) -> Dict: """Load ABI from json file""" abi_path = Path(os.environ.get(f"{abi_name}_ABI_FILEPATH", f"./abi/{abi_name}.json")) @@ -42,6 +44,9 @@ def _load_abi(abi_name: str) -> Dict: GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=_load_abi("GPv2Settlement")) TOKEN_ALLOWLIST_CONTRACt = Contract(TOKEN_ALLOWLIST_ADDRESS, abi=_load_abi("TokenAllowlist")) +# ABI +GPV2_ORDER_ABI = _load_abi("GPv2Order") + # API API_BASE_URL = "https://api.cow.fi/xdai/api/v1" API_HEADERS = {"accept": "application/json", "Content-Type": "application/json"} @@ -98,6 +103,26 @@ def _save_block_db(data: Dict): df.to_csv(BLOCK_FILEPATH, index=False) +def _load_orders_db() -> pd.DataFrame: + """ + Load orders database from CSV file or create new if doesn't exist + """ + dtype = {"orderUid": str, "encodedOrder": str, "signed": bool} + + df = ( + pd.read_csv(ORDERS_FILEPATH, dtype=dtype) + if os.path.exists(ORDERS_FILEPATH) + else pd.DataFrame(columns=dtype.keys()).astype(dtype) + ) + return df + + +def _save_orders_db(df: pd.DataFrame) -> None: + """Save orders to CSV file""" + os.makedirs(os.path.dirname(ORDERS_FILEPATH), exist_ok=True) + df.to_csv(ORDERS_FILEPATH, index=False) + + # Historical log helper functions def _process_trade_log(log) -> Dict: """Process trade log and return formatted dictionary entry""" @@ -229,6 +254,56 @@ def _submit_order(order_payload: Dict) -> str: raise Exception(f"Order request failed: {e}") +def _save_order(order_uid: str, order_payload: Dict, signed: bool) -> None: + """ + Save order to database + Encodes order parameters using eth_abi + """ + + types = [ + "address", + "address", + "address", + "uint256", + "uint256", + "uint32", + "bytes32", + "uint256", + "string", + "bool", + "string", + "string", + ] + + values = [ + order_payload["sellToken"], + order_payload["buyToken"], + order_payload["receiver"], + int(order_payload["sellAmount"]), + int(order_payload["buyAmount"]), + order_payload["validTo"], + bytes.fromhex(order_payload["appDataHash"][2:]), + 0, + order_payload["kind"], + order_payload["partiallyFillable"], + order_payload["sellTokenBalance"], + order_payload["buyTokenBalance"], + ] + + encoded_order = encode(types, values) + + df = _load_orders_db() + + new_order = { + "orderUid": order_uid, + "encodedOrder": "0x" + encoded_order.hex(), + "signed": signed, + } + df = pd.concat([df, pd.DataFrame([new_order])], ignore_index=True) + + _save_orders_db(df) + + def create_and_submit_order( sell_token: str, buy_token: str, @@ -250,6 +325,9 @@ def create_and_submit_order( order_uid = _submit_order(order_payload) click.echo(f"Order response: {order_uid}") + + _save_order(order_uid, order_payload, False) + return order_uid, None except Exception as e: From e82d9d0dcaa5df106441ae17a88ec964cd584ceb Mon Sep 17 00:00:00 2001 From: lumoswiz Date: Sun, 9 Feb 2025 22:58:28 +0100 Subject: [PATCH 6/6] refactor: simplify order storage to raw values Store order parameters directly in CSV instead of encoding them. This makes it easier to: - Read and debug orders - Construct GPv2Order.Data when needed - Maintain order data --- cow-trader/bot.py | 59 +++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/cow-trader/bot.py b/cow-trader/bot.py index 40d485b..661caae 100644 --- a/cow-trader/bot.py +++ b/cow-trader/bot.py @@ -10,7 +10,6 @@ from ape import Contract, accounts, chain from ape.api import BlockAPI from ape.types import LogFilter -from eth_abi import encode from silverback import SilverbackBot, StateSnapshot from taskiq import Context, TaskiqDepends @@ -107,7 +106,16 @@ def _load_orders_db() -> pd.DataFrame: """ Load orders database from CSV file or create new if doesn't exist """ - dtype = {"orderUid": str, "encodedOrder": str, "signed": bool} + dtype = { + "orderUid": str, + "signed": bool, + "sellToken": str, + "buyToken": str, + "receiver": str, + "sellAmount": str, + "buyAmount": str, + "validTo": int, + } df = ( pd.read_csv(ORDERS_FILEPATH, dtype=dtype) @@ -255,52 +263,21 @@ def _submit_order(order_payload: Dict) -> str: def _save_order(order_uid: str, order_payload: Dict, signed: bool) -> None: - """ - Save order to database - Encodes order parameters using eth_abi - """ - - types = [ - "address", - "address", - "address", - "uint256", - "uint256", - "uint32", - "bytes32", - "uint256", - "string", - "bool", - "string", - "string", - ] - - values = [ - order_payload["sellToken"], - order_payload["buyToken"], - order_payload["receiver"], - int(order_payload["sellAmount"]), - int(order_payload["buyAmount"]), - order_payload["validTo"], - bytes.fromhex(order_payload["appDataHash"][2:]), - 0, - order_payload["kind"], - order_payload["partiallyFillable"], - order_payload["sellTokenBalance"], - order_payload["buyTokenBalance"], - ] - - encoded_order = encode(types, values) - + """Save order to database with individual fields""" df = _load_orders_db() new_order = { "orderUid": order_uid, - "encodedOrder": "0x" + encoded_order.hex(), "signed": signed, + "sellToken": order_payload["sellToken"], + "buyToken": order_payload["buyToken"], + "receiver": order_payload["receiver"], + "sellAmount": order_payload["sellAmount"], + "buyAmount": order_payload["buyAmount"], + "validTo": order_payload["validTo"], } - df = pd.concat([df, pd.DataFrame([new_order])], ignore_index=True) + df = pd.concat([df, pd.DataFrame([new_order])], ignore_index=True) _save_orders_db(df)