1
1
import json
2
2
import os
3
3
from pathlib import Path
4
- from typing import Dict
4
+ from typing import Annotated , Dict
5
5
6
+ import click
6
7
import numpy as np
7
8
import pandas as pd
9
+ import requests
8
10
from ape import Contract , accounts , chain
11
+ from ape .api import BlockAPI
9
12
from ape .types import LogFilter
10
13
from silverback import SilverbackBot , StateSnapshot
14
+ from taskiq import Context , TaskiqDepends
11
15
12
16
# Initialize bot
13
17
bot = SilverbackBot ()
16
20
TRADE_FILEPATH = os .environ .get ("TRADE_FILEPATH" , ".db/trades.csv" )
17
21
BLOCK_FILEPATH = os .environ .get ("BLOCK_FILEPATH" , ".db/block.csv" )
18
22
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" )
19
25
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"
26
29
GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
27
30
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
+
28
41
29
42
# 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" }
31
52
32
53
# Variables
33
54
START_BLOCK = int (os .environ .get ("START_BLOCK" , chain .blocks .head .number ))
@@ -81,6 +102,35 @@ def _save_block_db(data: Dict):
81
102
df .to_csv (BLOCK_FILEPATH , index = False )
82
103
83
104
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
+
84
134
# Historical log helper functions
85
135
def _process_trade_log (log ) -> Dict :
86
136
"""Process trade log and return formatted dictionary entry"""
@@ -128,6 +178,139 @@ def _process_historical_gno_trades(
128
178
return trades_db
129
179
130
180
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
+
131
314
# Silverback bot
132
315
@bot .on_startup ()
133
316
def app_startup (startup_state : StateSnapshot ):
@@ -144,3 +327,16 @@ def app_startup(startup_state: StateSnapshot):
144
327
_save_block_db ({"last_processed_block" : chain .blocks .head .number })
145
328
146
329
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