1
1
import json
2
2
import os
3
3
from pathlib import Path
4
- from typing import Annotated , Dict
4
+ from typing import Annotated , Dict , List
5
5
6
6
import click
7
- import numpy as np
8
7
import pandas as pd
9
8
import requests
10
9
from ape import Contract , accounts , chain
27
26
SAFE_ADDRESS = "0x5aFE3855358E112B5647B952709E6165e1c1eEEe" # PLACEHOLDER
28
27
TOKEN_ALLOWLIST_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
29
28
GPV2_SETTLEMENT_ADDRESS = "0x9008D19f58AAbD9eD0D60971565AA8510560ab41"
30
- GNO_ADDRESS = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
31
- COW_ADDRESS = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
29
+ GNO = "0x9C58BAcC331c9aa871AFD802DB6379a98e80CEdb"
30
+ COW = "0x177127622c4A00F3d409B75571e12cB3c8973d3c"
31
+ WETH = "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1"
32
+ SAFE = "0x4d18815D14fe5c3304e87B3FA18318baa5c23820"
33
+ WXDAI = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
34
+
35
+ MONITORED_TOKENS = [GNO , COW , WETH , SAFE , WXDAI ]
32
36
33
37
34
38
# ABI
@@ -52,29 +56,28 @@ def _load_abi(abi_name: str) -> Dict:
52
56
53
57
# Variables
54
58
START_BLOCK = int (os .environ .get ("START_BLOCK" , chain .blocks .head .number ))
59
+ HISTORICAL_BLOCK_STEP = int (os .environ .get ("HISTORICAL_BLOCK_STEP" , 720 ))
60
+ EXTENSION_INTERVAL = int (os .environ .get ("EXTENSION_INTERVAL" , 6 ))
55
61
56
62
57
63
# Local storage helper functions
58
- def _load_trades_db () -> Dict :
59
- """
60
- Load trades database from CSV file or create new if doesn't exist.
61
- Returns dict with trade data indexed by block number.
62
- """
64
+ def _load_trades_db () -> pd .DataFrame :
65
+ """Load trades database from CSV file or create new if doesn't exist"""
63
66
dtype = {
67
+ "block_number" : int ,
64
68
"owner" : str ,
65
69
"sellToken" : str ,
66
70
"buyToken" : str ,
67
- "sellAmount" : object ,
68
- "buyAmount" : object ,
69
- "block_number" : np .int64 ,
71
+ "sellAmount" : str ,
72
+ "buyAmount" : str ,
70
73
}
71
74
72
75
df = (
73
76
pd .read_csv (TRADE_FILEPATH , dtype = dtype )
74
77
if os .path .exists (TRADE_FILEPATH )
75
78
else pd .DataFrame (columns = dtype .keys ()).astype (dtype )
76
79
)
77
- return df . to_dict ( "records" )
80
+ return df
78
81
79
82
80
83
def _save_trades_db (trades_dict : Dict ) -> None :
@@ -132,25 +135,43 @@ def _save_orders_db(df: pd.DataFrame) -> None:
132
135
133
136
134
137
# Historical log helper functions
138
+ def get_canonical_pair (token_a : str , token_b : str ) -> tuple [str , str ]:
139
+ """Return tokens in canonical order (alphabetically by address)"""
140
+ return (token_a , token_b ) if token_a .lower () < token_b .lower () else (token_b , token_a )
141
+
142
+
143
+ def calculate_price (sell_amount : str , buy_amount : str ) -> float :
144
+ """Calculate price from amounts"""
145
+ return int (sell_amount ) / int (buy_amount )
146
+
147
+
135
148
def _process_trade_log (log ) -> Dict :
136
- """Process trade log and return formatted dictionary entry"""
149
+ """Process trade log with price calculation"""
150
+ token_a , token_b = get_canonical_pair (log .sellToken , log .buyToken )
151
+ price = calculate_price (log .sellAmount , log .buyAmount )
152
+
153
+ if token_a != log .sellToken :
154
+ price = 1 / price
155
+
137
156
return {
138
157
"block_number" : log .block_number ,
139
158
"owner" : log .owner ,
140
159
"sellToken" : log .sellToken ,
141
160
"buyToken" : log .buyToken ,
142
161
"sellAmount" : str (log .sellAmount ),
143
162
"buyAmount" : str (log .buyAmount ),
163
+ "token_a" : token_a ,
164
+ "token_b" : token_b ,
165
+ "price" : price ,
144
166
}
145
167
146
168
147
- def _get_historical_gno_trades (
169
+ def _get_historical_trades (
148
170
settlement_contract ,
149
- gno_address : str ,
150
171
start_block : int ,
151
172
stop_block : int = chain .blocks .head .number ,
152
173
):
153
- """Get historical GNO trades from start_block to stop_block """
174
+ """Get historical trades for monitored token pairs """
154
175
log_filter = LogFilter (
155
176
addresses = [settlement_contract .address ],
156
177
events = [settlement_contract .Trade .abi ],
@@ -159,23 +180,48 @@ def _get_historical_gno_trades(
159
180
)
160
181
161
182
for log in accounts .provider .get_contract_logs (log_filter ):
162
- if log .sellToken == gno_address or log .buyToken == gno_address :
183
+ if log .sellToken in MONITORED_TOKENS and log .buyToken in MONITORED_TOKENS :
163
184
yield log
164
185
165
186
166
- def _process_historical_gno_trades (
167
- settlement_contract , gno_address : str , start_block : int , stop_block : int
168
- ) -> Dict :
169
- """Process historical GNO trades and store in database"""
170
- trades_db = _load_trades_db ()
187
+ def _process_historical_trades (
188
+ settlement_contract , start_block : int , stop_block : int
189
+ ) -> List [Dict ]:
190
+ """Process historical trades and store in database"""
191
+ trades = []
192
+
193
+ for log in _get_historical_trades (settlement_contract , start_block , stop_block ):
194
+ trades .append (_process_trade_log (log ))
171
195
172
- for log in _get_historical_gno_trades (
173
- settlement_contract , gno_address , start_block , stop_block
174
- ):
175
- trades_db .append (_process_trade_log (log ))
196
+ if trades :
197
+ existing_trades = _load_trades_db ()
198
+ all_trades = pd .concat ([existing_trades , pd .DataFrame (trades )], ignore_index = True )
176
199
177
- _save_trades_db (trades_db )
178
- return trades_db
200
+ _save_trades_db (all_trades )
201
+
202
+ return trades
203
+
204
+
205
+ def extend_historical_trades () -> None :
206
+ """Extend trades.csv data further back in history"""
207
+ trades_df = _load_trades_db ()
208
+
209
+ if len (trades_df ) == 0 :
210
+ oldest_block = chain .blocks .head .number
211
+ else :
212
+ oldest_block = trades_df ["block_number" ].min ()
213
+
214
+ new_trades = _process_historical_trades (
215
+ GPV2_SETTLEMENT_CONTRACT ,
216
+ start_block = oldest_block - HISTORICAL_BLOCK_STEP ,
217
+ stop_block = oldest_block - 1 ,
218
+ )
219
+
220
+ new_trades_df = pd .DataFrame (new_trades )
221
+ all_trades = pd .concat ([new_trades_df , trades_df ])
222
+ all_trades = all_trades .sort_values ("block_number" , ascending = True )
223
+
224
+ _save_trades_db (all_trades )
179
225
180
226
181
227
# CoW Swap trading helper functions
@@ -313,30 +359,37 @@ def create_and_submit_order(
313
359
314
360
# Silverback bot
315
361
@bot .on_startup ()
316
- def app_startup (startup_state : StateSnapshot ):
362
+ def bot_startup (startup_state : StateSnapshot ):
363
+ """Initialize bot state and historical data"""
317
364
block_db = _load_block_db ()
318
365
last_processed_block = block_db ["last_processed_block" ]
319
366
320
- _process_historical_gno_trades (
367
+ _process_historical_trades (
321
368
GPV2_SETTLEMENT_CONTRACT ,
322
- GNO_ADDRESS ,
323
369
start_block = last_processed_block ,
324
370
stop_block = chain .blocks .head .number ,
325
371
)
326
372
327
373
_save_block_db ({"last_processed_block" : chain .blocks .head .number })
328
-
374
+ bot . state . last_extension_block = chain . blocks . head . number
329
375
return {"message" : "Starting..." , "block_number" : startup_state .last_block_seen }
330
376
331
377
332
378
@bot .on_ (chain .blocks )
333
379
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 } " )
380
+ _save_block_db ({"last_processed_block" : block .number })
381
+
382
+ if block .number - bot .state .last_extension_block >= EXTENSION_INTERVAL :
383
+ extend_historical_trades ()
384
+ bot .state .last_extension_block = block .number
385
+
386
+
387
+ # """Execute block handler"""
388
+ # order_uid, error = create_and_submit_order(
389
+ # sell_token=GNO, buy_token=COW, sell_amount="20000000000000000000"
390
+ # )
391
+ #
392
+ # if error:
393
+ # click.echo(f"Order failed: {error}")
394
+ # else:
395
+ # click.echo(f"Order submitted successfully. UID: {order_uid}")
0 commit comments