Skip to content

Commit 7640419

Browse files
authored
feat: cow api integration (#8)
This PR adds CoW API integration for quote and order submission, with order storage for later use with TradingModule. ### Changes: - Add CoW API quote and order submission - Add error handling for API responses - Add order storage with encoded GPv2Order.Data - Move GPv2Order ABI to separate file
1 parent 72f74e3 commit 7640419

File tree

1 file changed

+204
-8
lines changed

1 file changed

+204
-8
lines changed

cow-trader/bot.py

Lines changed: 204 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import json
22
import os
33
from pathlib import Path
4-
from typing import Dict
4+
from typing import Annotated, Dict
55

6+
import click
67
import numpy as np
78
import pandas as pd
9+
import requests
810
from ape import Contract, accounts, chain
11+
from ape.api import BlockAPI
912
from ape.types import LogFilter
1013
from silverback import SilverbackBot, StateSnapshot
14+
from taskiq import Context, TaskiqDepends
1115

1216
# Initialize bot
1317
bot = SilverbackBot()
@@ -16,18 +20,35 @@
1620
TRADE_FILEPATH = os.environ.get("TRADE_FILEPATH", ".db/trades.csv")
1721
BLOCK_FILEPATH = os.environ.get("BLOCK_FILEPATH", ".db/block.csv")
1822
GPV2_ABI_FILEPATH = os.environ.get("GPV2_ABI_FILEPATH", "./abi/GPv2Settlement.json")
23+
TOKEN_ALLOWLIST_FILEPATH = os.environ.get("TOKEN_ALLOWLIST_FILEPATH", "./abi/TokenAllowlist.json")
24+
ORDERS_FILEPATH = os.environ.get("ORDERS_FILEPATH", ".db/orders.csv")
1925

20-
# Load GPv2Settlement ABI
21-
abi_path = Path(GPV2_ABI_FILEPATH)
22-
with open(abi_path) as f:
23-
gpv2_settlement_abi = json.load(f)
24-
25-
# Gnosis Chain Addresses
26+
# Addresses
27+
SAFE_ADDRESS = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe" # PLACEHOLDER
28+
TOKEN_ALLOWLIST_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
2629
GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
2730
GNO_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
31+
COW_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
32+
33+
34+
# ABI
35+
def _load_abi(abi_name: str) -> Dict:
36+
"""Load ABI from json file"""
37+
abi_path = Path(os.environ.get(f"{abi_name}_ABI_FILEPATH", f"./abi/{abi_name}.json"))
38+
with open(abi_path) as f:
39+
return json.load(f)
40+
2841

2942
# Contracts
30-
GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=gpv2_settlement_abi)
43+
GPV2_SETTLEMENT_CONTRACT = Contract(GPV2_SETTLEMENT_ADDRESS, abi=_load_abi("GPv2Settlement"))
44+
TOKEN_ALLOWLIST_CONTRACt = Contract(TOKEN_ALLOWLIST_ADDRESS, abi=_load_abi("TokenAllowlist"))
45+
46+
# ABI
47+
GPV2_ORDER_ABI = _load_abi("GPv2Order")
48+
49+
# API
50+
API_BASE_URL = "https://api.cow.fi/xdai/api/v1"
51+
API_HEADERS = {"accept": "application/json", "Content-Type": "application/json"}
3152

3253
# Variables
3354
START_BLOCK = int(os.environ.get("START_BLOCK", chain.blocks.head.number))
@@ -81,6 +102,35 @@ def _save_block_db(data: Dict):
81102
df.to_csv(BLOCK_FILEPATH, index=False)
82103

83104

105+
def _load_orders_db() -> pd.DataFrame:
106+
"""
107+
Load orders database from CSV file or create new if doesn't exist
108+
"""
109+
dtype = {
110+
"orderUid": str,
111+
"signed": bool,
112+
"sellToken": str,
113+
"buyToken": str,
114+
"receiver": str,
115+
"sellAmount": str,
116+
"buyAmount": str,
117+
"validTo": int,
118+
}
119+
120+
df = (
121+
pd.read_csv(ORDERS_FILEPATH, dtype=dtype)
122+
if os.path.exists(ORDERS_FILEPATH)
123+
else pd.DataFrame(columns=dtype.keys()).astype(dtype)
124+
)
125+
return df
126+
127+
128+
def _save_orders_db(df: pd.DataFrame) -> None:
129+
"""Save orders to CSV file"""
130+
os.makedirs(os.path.dirname(ORDERS_FILEPATH), exist_ok=True)
131+
df.to_csv(ORDERS_FILEPATH, index=False)
132+
133+
84134
# Historical log helper functions
85135
def _process_trade_log(log) -> Dict:
86136
"""Process trade log and return formatted dictionary entry"""
@@ -128,6 +178,139 @@ def _process_historical_gno_trades(
128178
return trades_db
129179

130180

181+
# CoW Swap trading helper functions
182+
def _construct_quote_payload(
183+
sell_token: str,
184+
buy_token: str,
185+
sell_amount: str,
186+
) -> Dict:
187+
"""
188+
Construct payload for CoW Protocol quote request using PreSign method.
189+
Returns dict with required quote parameters.
190+
"""
191+
return {
192+
"sellToken": sell_token,
193+
"buyToken": buy_token,
194+
"sellAmountBeforeFee": sell_amount,
195+
"from": SAFE_ADDRESS,
196+
"receiver": SAFE_ADDRESS,
197+
"appData": "{}",
198+
"appDataHash": "0xb48d38f93eaa084033fc5970bf96e559c33c4cdc07d889ab00b4d63f9590739d",
199+
"sellTokenBalance": "erc20",
200+
"buyTokenBalance": "erc20",
201+
"priceQuality": "verified",
202+
"signingScheme": "presign",
203+
"onchainOrder": False,
204+
"kind": "sell",
205+
}
206+
207+
208+
def _get_quote(payload: Dict) -> Dict:
209+
"""
210+
Get quote from CoW API
211+
Returns quote response or raises exception
212+
"""
213+
response = requests.post(url=f"{API_BASE_URL}/quote", headers=API_HEADERS, json=payload)
214+
response.raise_for_status()
215+
return response.json()
216+
217+
218+
def _construct_order_payload(quote_response: Dict) -> Dict:
219+
"""
220+
Transform quote response into order request payload
221+
"""
222+
quote = quote_response["quote"]
223+
224+
return {
225+
"sellToken": quote["sellToken"],
226+
"buyToken": quote["buyToken"],
227+
"receiver": quote["receiver"],
228+
"sellAmount": quote["sellAmount"],
229+
"buyAmount": quote["buyAmount"],
230+
"validTo": quote["validTo"],
231+
"feeAmount": "0",
232+
"kind": quote["kind"],
233+
"partiallyFillable": quote["partiallyFillable"],
234+
"sellTokenBalance": quote["sellTokenBalance"],
235+
"buyTokenBalance": quote["buyTokenBalance"],
236+
"signingScheme": "presign",
237+
"signature": "0x",
238+
"from": quote_response["from"],
239+
"quoteId": quote_response["id"],
240+
"appData": quote["appData"],
241+
"appDataHash": quote["appDataHash"],
242+
}
243+
244+
245+
def _submit_order(order_payload: Dict) -> str:
246+
"""
247+
Submit order to CoW API
248+
Returns order UID string or raises exception
249+
"""
250+
try:
251+
response = requests.post(
252+
url=f"{API_BASE_URL}/orders", headers=API_HEADERS, json=order_payload
253+
)
254+
response.raise_for_status()
255+
return response.text.strip('"')
256+
except requests.RequestException as e:
257+
if e.response is not None:
258+
error_data = e.response.json()
259+
error_type = error_data.get("errorType", "Unknown")
260+
error_description = error_data.get("description", str(e))
261+
raise Exception(f"{error_type} - {error_description}")
262+
raise Exception(f"Order request failed: {e}")
263+
264+
265+
def _save_order(order_uid: str, order_payload: Dict, signed: bool) -> None:
266+
"""Save order to database with individual fields"""
267+
df = _load_orders_db()
268+
269+
new_order = {
270+
"orderUid": order_uid,
271+
"signed": signed,
272+
"sellToken": order_payload["sellToken"],
273+
"buyToken": order_payload["buyToken"],
274+
"receiver": order_payload["receiver"],
275+
"sellAmount": order_payload["sellAmount"],
276+
"buyAmount": order_payload["buyAmount"],
277+
"validTo": order_payload["validTo"],
278+
}
279+
280+
df = pd.concat([df, pd.DataFrame([new_order])], ignore_index=True)
281+
_save_orders_db(df)
282+
283+
284+
def create_and_submit_order(
285+
sell_token: str,
286+
buy_token: str,
287+
sell_amount: str,
288+
) -> tuple[str | None, str | None]:
289+
"""
290+
Create and submit order to CoW API
291+
Returns (order_uid, error_message)
292+
"""
293+
try:
294+
quote_payload = _construct_quote_payload(
295+
sell_token=sell_token, buy_token=buy_token, sell_amount=sell_amount
296+
)
297+
quote = _get_quote(quote_payload)
298+
click.echo(f"Quote received: {quote}")
299+
300+
order_payload = _construct_order_payload(quote)
301+
click.echo(f"Submitting order payload: {order_payload}")
302+
303+
order_uid = _submit_order(order_payload)
304+
click.echo(f"Order response: {order_uid}")
305+
306+
_save_order(order_uid, order_payload, False)
307+
308+
return order_uid, None
309+
310+
except Exception as e:
311+
return None, str(e)
312+
313+
131314
# Silverback bot
132315
@bot.on_startup()
133316
def app_startup(startup_state: StateSnapshot):
@@ -144,3 +327,16 @@ def app_startup(startup_state: StateSnapshot):
144327
_save_block_db({"last_processed_block": chain.blocks.head.number})
145328

146329
return {"message": "Starting...", "block_number": startup_state.last_block_seen}
330+
331+
332+
@bot.on_(chain.blocks)
333+
def exec_block(block: BlockAPI, context: Annotated[Context, TaskiqDepends()]):
334+
"""Execute block handler"""
335+
order_uid, error = create_and_submit_order(
336+
sell_token=GNO_ADDRESS, buy_token=COW_ADDRESS, sell_amount="20000000000000000000"
337+
)
338+
339+
if error:
340+
click.echo(f"Order failed: {error}")
341+
else:
342+
click.echo(f"Order submitted successfully. UID: {order_uid}")

0 commit comments

Comments
 (0)