-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Full refactoring of the code. - There are multiple classes for fetching data. For now two such classes, `Web3API` and `OrderbookAPI` are implemented. - Tests are subclasses of `BaseTest`. They have - Tests and API use common data types. For now, `Trades`, `OrderData`, `OrderExecution` are implemented.
- Loading branch information
Showing
22 changed files
with
1,009 additions
and
1,174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
FROM python:3.11 | ||
COPY requirements.txt . | ||
RUN python -m pip install -r requirements.txt | ||
COPY . . | ||
RUN pip install -r requirements.txt | ||
CMD python3.11 -m src.daemon | ||
CMD python -m src.daemon |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
""" | ||
OrderbookAPI for fetching relevant data using the CoW Swap Orderbook API. | ||
""" | ||
# pylint: disable=logging-fstring-interpolation | ||
|
||
from typing import Any, Optional | ||
import json | ||
import requests | ||
from src.helper_functions import get_logger | ||
from src.models import Trade, OrderExecution | ||
from src.constants import ( | ||
header, | ||
REQUEST_TIMEOUT, | ||
SUCCESS_CODE, | ||
FAIL_CODE, | ||
) | ||
|
||
PROD_BASE_URL = "https://api.cow.fi/mainnet/api/v1/" | ||
BARN_BASE_URL = "https://barn.api.cow.fi/mainnet/api/v1/" | ||
|
||
|
||
class OrderbookAPI: | ||
""" | ||
Class for fetching data from a Web3 API. | ||
""" | ||
|
||
def __init__(self): | ||
self.logger = get_logger() | ||
|
||
def get_solver_competition_data(self, tx_hash: str) -> Optional[dict[str, Any]]: | ||
""" | ||
Get solver competition data from a transaction hash. | ||
The returned dict follows the schema outlined here: | ||
https://api.cow.fi/docs/#/default/get_api_v1_solver_competition_by_tx_hash__tx_hash_ | ||
""" | ||
prod_endpoint_url = f"{PROD_BASE_URL}solver_competition/by_tx_hash/{tx_hash}" | ||
barn_endpoint_url = f"{BARN_BASE_URL}solver_competition/by_tx_hash/{tx_hash}" | ||
try: | ||
json_competition_data = requests.get( | ||
prod_endpoint_url, | ||
headers=header, | ||
timeout=REQUEST_TIMEOUT, | ||
) | ||
if json_competition_data.status_code == SUCCESS_CODE: | ||
solver_competition_data = json.loads(json_competition_data.text) | ||
elif json_competition_data.status_code == FAIL_CODE: | ||
barn_competition_data = requests.get( | ||
barn_endpoint_url, headers=header, timeout=REQUEST_TIMEOUT | ||
) | ||
if barn_competition_data.status_code == SUCCESS_CODE: | ||
solver_competition_data = json.loads(barn_competition_data.text) | ||
else: | ||
return None | ||
except requests.RequestException as err: | ||
self.logger.warning( | ||
f"Connection error while fetching competition data. Hash: {tx_hash}, error: {err}" | ||
) | ||
return None | ||
return solver_competition_data | ||
|
||
def get_quote(self, trade: Trade) -> Optional[Trade]: | ||
""" | ||
Given a trade, compute buy_amount, sell_amount, and fee_amount of the trade | ||
as proposed by our quoting infrastructure. | ||
""" | ||
|
||
if trade.data.is_sell_order: | ||
kind = "sell" | ||
limit_amount_name = "sellAmountBeforeFee" | ||
executed_amount = trade.execution.sell_amount | ||
else: | ||
kind = "buy" | ||
limit_amount_name = "buyAmountAfterFee" | ||
executed_amount = trade.execution.buy_amount | ||
|
||
request_dict = { | ||
"sellToken": trade.data.sell_token, | ||
"buyToken": trade.data.buy_token, | ||
"receiver": "0x0000000000000000000000000000000000000000", | ||
"appData": "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
"partiallyFillable": False, | ||
"sellTokenBalance": "erc20", | ||
"buyTokenBalance": "erc20", | ||
"from": "0x0000000000000000000000000000000000000000", | ||
"priceQuality": "optimal", | ||
"signingScheme": "eip712", | ||
"onchainOrder": False, | ||
"kind": kind, | ||
limit_amount_name: str(executed_amount), | ||
} | ||
prod_endpoint_url = f"{PROD_BASE_URL}quote" | ||
|
||
try: | ||
quote_response = requests.post( | ||
prod_endpoint_url, | ||
headers=header, | ||
json=request_dict, | ||
timeout=REQUEST_TIMEOUT, | ||
) | ||
except requests.RequestException as err: | ||
self.logger.warning( | ||
f"Fee quote failed. Request: {request_dict}, error: {err}" | ||
) | ||
return None | ||
|
||
if quote_response.status_code != SUCCESS_CODE: | ||
error_response_json = json.loads(quote_response.content) | ||
self.logger.warning( | ||
f"Error {error_response_json['errorType']}," | ||
+ f"{error_response_json['description']} while getting quote for trade {trade}" | ||
) | ||
return None | ||
|
||
quote_json = json.loads(quote_response.text) | ||
self.logger.debug("Quote received: %s", quote_json) | ||
|
||
quote_buy_amount = int(quote_json["quote"]["buyAmount"]) | ||
quote_sell_amount = int(quote_json["quote"]["sellAmount"]) | ||
quote_fee_amount = int(quote_json["quote"]["feeAmount"]) | ||
|
||
quote_execution = OrderExecution( | ||
quote_buy_amount, quote_sell_amount, quote_fee_amount | ||
) | ||
|
||
return Trade(trade.data, quote_execution) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
""" | ||
Web3API for fetching relevant data using the web3 library. | ||
""" | ||
# pylint: disable=logging-fstring-interpolation | ||
|
||
from os import getenv | ||
from typing import Any, Optional | ||
from fractions import Fraction | ||
from dotenv import load_dotenv | ||
from web3 import Web3 | ||
from web3.types import TxData, TxReceipt | ||
from eth_typing import Address, HexStr | ||
from hexbytes import HexBytes | ||
from contracts.gpv2_settlement import gpv2_settlement | ||
from src.models import Trade, OrderData, OrderExecution | ||
from src.helper_functions import get_logger | ||
from src.constants import SETTLEMENT_CONTRACT_ADDRESS | ||
|
||
|
||
class Web3API: | ||
""" | ||
Class for fetching data from a Web3 API. | ||
""" | ||
|
||
def __init__(self): | ||
load_dotenv() | ||
infura_key = getenv("INFURA_KEY") | ||
self.url = f"https://mainnet.infura.io/v3/{infura_key}" | ||
self.web_3 = Web3(Web3.HTTPProvider(self.url)) | ||
self.contract = self.web_3.eth.contract( | ||
address=Address(HexBytes(SETTLEMENT_CONTRACT_ADDRESS)), abi=gpv2_settlement | ||
) | ||
self.logger = get_logger() | ||
|
||
def get_current_block_number(self) -> Optional[int]: | ||
""" | ||
Function that returns the current block number | ||
""" | ||
try: | ||
return int(self.web_3.eth.block_number) | ||
except ValueError as err: | ||
self.logger.warning(f"Error while fetching block number: {err}") | ||
return None | ||
|
||
def get_tx_hashes_by_block( | ||
self, start_block: int, end_block: int | ||
) -> Optional[list[str]]: | ||
""" | ||
Function filters hashes by contract address, and block ranges | ||
""" | ||
filter_criteria = { | ||
"fromBlock": int(start_block), | ||
"toBlock": int(end_block), | ||
"address": SETTLEMENT_CONTRACT_ADDRESS, | ||
"topics": [ | ||
"0xa07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17" | ||
# "0x40338ce1a7c49204f0099533b1e9a7ee0a3d261f84974ab7af36105b8c4e9db4" | ||
], | ||
} | ||
|
||
try: | ||
log_receipts = self.web_3.eth.filter(filter_criteria).get_all_entries() # type: ignore | ||
except ValueError as err: | ||
self.logger.warning(f"ValueError while fetching hashes: {err}") | ||
return None | ||
|
||
settlement_hashes_list = list( | ||
{log_receipt["transactionHash"].hex() for log_receipt in log_receipts} | ||
) | ||
|
||
return settlement_hashes_list | ||
|
||
def get_transaction(self, tx_hash: str) -> Optional[TxData]: | ||
""" | ||
Takes settlement hash as input, returns transaction data. | ||
""" | ||
try: | ||
transaction = self.web_3.eth.get_transaction(HexStr(tx_hash)) | ||
except ValueError as err: | ||
self.logger.warning(f"Error while fetching transaction: {err}") | ||
transaction = None | ||
|
||
return transaction | ||
|
||
def get_receipt(self, tx_hash: str) -> Optional[TxReceipt]: | ||
""" | ||
Get the receipt of a transaction from the transaction hash. | ||
This is used to obtain the gas used for the transaction. | ||
""" | ||
try: | ||
receipt = self.web_3.eth.wait_for_transaction_receipt(HexStr(tx_hash)) | ||
except ValueError as err: | ||
self.logger.warning(f"Error fetching log receipt: {err}") | ||
receipt = None | ||
return receipt | ||
|
||
def get_settlement(self, transaction: TxData) -> dict[str, Any]: | ||
""" | ||
Decode settlement from transaction using the settlement contract. | ||
""" | ||
return self.get_settlement_from_calldata(transaction["input"]) | ||
|
||
def get_settlement_from_calldata(self, calldata: str) -> dict[str, Any]: | ||
""" | ||
Decode settlement from transaction using the settlement contract. | ||
""" | ||
return self.contract.decode_function_input(calldata)[1] | ||
|
||
def get_trades(self, settlement: dict[str, Any]) -> list[Trade]: | ||
""" | ||
Get all trades from a settlement. | ||
""" | ||
trades = [] | ||
for i in range(len(settlement["trades"])): | ||
data = self.get_order_data_from_settlement(settlement, i) | ||
execution = self.get_order_execution_from_settlement(settlement, i) | ||
trades.append(Trade(data, execution)) | ||
|
||
return trades | ||
|
||
def get_order_data_from_settlement( | ||
self, settlement: dict[str, Any], i: int | ||
) -> OrderData: | ||
""" | ||
Given a settlement and the index of an trade, return order information. | ||
""" | ||
decoded_trade = settlement["trades"][i] | ||
tokens = settlement["tokens"] | ||
|
||
order_data = OrderData( | ||
decoded_trade["buyAmount"], | ||
decoded_trade["sellAmount"], | ||
decoded_trade["feeAmount"], | ||
tokens[decoded_trade["buyTokenIndex"]], | ||
tokens[decoded_trade["sellTokenIndex"]], | ||
self.is_sell_order(decoded_trade), | ||
self.is_partially_fillable(decoded_trade), | ||
) | ||
return order_data | ||
|
||
def get_order_execution_from_settlement( | ||
self, settlement: dict[str, Any], i: int | ||
) -> OrderExecution: | ||
# pylint: disable=too-many-locals | ||
""" | ||
Given a settlement and the index of a trade, compute the execution of the order. | ||
""" | ||
decoded_trade = settlement["trades"][i] | ||
tokens = settlement["tokens"] | ||
clearing_prices = settlement["clearingPrices"] | ||
|
||
buy_token = tokens[decoded_trade["buyTokenIndex"]] | ||
buy_token_price = clearing_prices[decoded_trade["buyTokenIndex"]] | ||
buy_token_index_ucp = tokens.index(buy_token) | ||
buy_token_price_ucp = clearing_prices[buy_token_index_ucp] | ||
|
||
sell_token = tokens[decoded_trade["sellTokenIndex"]] | ||
sell_token_price = clearing_prices[decoded_trade["sellTokenIndex"]] | ||
sell_token_index_ucp = tokens.index(sell_token) | ||
sell_token_price_ucp = clearing_prices[sell_token_index_ucp] | ||
|
||
executed_amount = decoded_trade["executedAmount"] | ||
precomputed_fee_amount = decoded_trade["feeAmount"] | ||
|
||
if self.is_sell_order(decoded_trade): # sell order | ||
buy_amount = int( | ||
executed_amount * Fraction(sell_token_price, buy_token_price) | ||
) | ||
sell_amount = int( | ||
buy_amount * Fraction(buy_token_price_ucp, sell_token_price_ucp) | ||
) | ||
fee_amount = precomputed_fee_amount + executed_amount - sell_amount | ||
else: # buy order | ||
buy_amount = executed_amount | ||
sell_amount = int( | ||
buy_amount * Fraction(buy_token_price_ucp, sell_token_price_ucp) | ||
) | ||
fee_amount = ( | ||
precomputed_fee_amount | ||
+ int(buy_amount * Fraction(buy_token_price, sell_token_price)) | ||
- sell_amount | ||
) | ||
|
||
return OrderExecution(buy_amount, sell_amount, fee_amount) | ||
|
||
def is_sell_order(self, decoded_trade): | ||
""" | ||
Check if the order corresponding to a trade is a sell order. | ||
""" | ||
return str(f"{decoded_trade['flags']:08b}")[-1] == "0" | ||
|
||
def is_partially_fillable(self, decoded_trade): | ||
""" | ||
Check if the order corresponding to a trade is partially-fillable. | ||
""" | ||
return str(f"{decoded_trade['flags']:08b}")[-2] == "1" | ||
|
||
def get_batch_gas_costs( | ||
self, transaction: TxData, receipt: TxReceipt | ||
) -> tuple[int, int]: | ||
""" | ||
Combine the transaction and receipt to return gas used and gas price. | ||
""" | ||
return int(receipt["gasUsed"]), int(transaction["gasPrice"]) | ||
|
||
def get_current_gas_price(self) -> Optional[int]: | ||
""" | ||
Get the current gas price. | ||
""" | ||
try: | ||
gas_price = int(self.web_3.eth.gas_price) | ||
except ValueError as err: | ||
self.logger.warning(f"Error fetching gas price: {err}") | ||
gas_price = None | ||
return gas_price |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.