Skip to content

Commit

Permalink
Merge pull request #670 from bancorprotocol/release-candidate
Browse files Browse the repository at this point in the history
Release candidate - pair finder, eth_getLogs and more
  • Loading branch information
zavelevsky authored May 27, 2024
2 parents d9e31ea + 1e3cb1a commit 8ac9256
Show file tree
Hide file tree
Showing 87 changed files with 4,941 additions and 2,118 deletions.
1 change: 1 addition & 0 deletions .github/workflows/release-and-pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
id: bump_version_and_set_output
run: |
poetry version patch
echo new_version=$(poetry version | cut -d' ' -f2) >> $GITHUB_OUTPUT
git checkout main
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,5 @@ logs/*
missing_tokens_df.csv
tokens_and_fee_df.csv
fastlane_bot/tests/nbtest/*

.python-version
39 changes: 10 additions & 29 deletions fastlane_bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,19 @@ def get_curves(self) -> CPCContainer:
ADDRDEC = {t.address: (t.address, int(t.decimals)) for t in tokens}

for p in pools_and_tokens:
p.ADDRDEC = ADDRDEC
try:
p.ADDRDEC = ADDRDEC
curves += [
curve for curve in p.to_cpc()
if all(curve.params[tkn] not in self.ConfigObj.TAX_TOKENS for tkn in ['tknx_addr', 'tkny_addr'])
]
except SolidlyV2StablePoolsNotSupported as e:
self.ConfigObj.logger.debug(
f"[bot.get_curves] SolidlyV2StablePoolsNotSupported: {e}\n"
f"[bot.get_curves] Solidly V2 stable pools not supported: {e}\n"
)
except NotImplementedError as e:
self.ConfigObj.logger.error(
f"[bot.get_curves] Pool type not yet supported, error: {e}\n"
f"[bot.get_curves] Not supported: {e}\n"
)
except ZeroDivisionError as e:
self.ConfigObj.logger.error(
Expand Down Expand Up @@ -241,36 +241,17 @@ def _convert_trade_instructions(
List[Dict[str, Any]]
The trade instructions.
"""
errorless_trade_instructions_dicts = [
{k: v for k, v in trade_instructions_dic[i].items() if k != "error"}
for i in range(len(trade_instructions_dic))
]
result = (
{
**ti,
return [
TradeInstruction(**{
**{k: v for k, v in ti.items() if k != "error"},
"raw_txs": "[]",
"pair_sorting": "",
"ConfigObj": self.ConfigObj,
"db": self.db,
}
for ti in errorless_trade_instructions_dicts
if ti is not None
)
result = self._add_strategy_id_to_trade_instructions_dic(result)
result = [TradeInstruction(**ti) for ti in result]
return result

def _add_strategy_id_to_trade_instructions_dic(
self, trade_instructions_dic: Generator
) -> List[Dict[str, Any]]:
lst = []
for ti in trade_instructions_dic:
cid = ti["cid"].split('-')[0]
ti["strategy_id"] = self.db.get_pool(
cid=cid
).strategy_id
lst.append(ti)
return lst
"strategy_id": self.db.get_pool(cid=ti["cid"].split('-')[0]).strategy_id
})
for ti in trade_instructions_dic if ti["error"] is None
]

def _get_deadline(self, block_number) -> int:
"""
Expand Down
9 changes: 0 additions & 9 deletions fastlane_bot/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@
- ``ConfigProvider`` (``provider``; provider for network access)
- ``Config`` (``config``; main configuration class, integrates the above)
Submodules provide the following
- Constants (``constants`` and ``selectors``; various constants)
- ``MultiCaller`` and related (``multicaller``; TODO: what is this?)
- ``NetworkBase`` and ``EthereumNetwork`` (``connect``; network/chain connection code TODO: details)
- ``Cloaker`` (``cloaker``; deprecated)
---
(c) Copyright Bprotocol foundation 2023-24.
All rights reserved.
Expand Down
15 changes: 15 additions & 0 deletions fastlane_bot/config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
}

ETHEREUM = "ethereum"
UNISWAP_V2_NAME = "uniswap_v2"
UNISWAP_V3_NAME = "uniswap_v3"
PANCAKESWAP_V2_NAME = "pancakeswap_v2"
PANCAKESWAP_V3_NAME = "pancakeswap_v3"
BUTTER_V3_NAME = "butter_v3"
Expand All @@ -31,3 +33,16 @@
ECHODEX_V3_NAME = "echodex_v3"
SECTA_V3_NAME = "secta_v3"
METAVAULT_V3_NAME = "metavault_v3"
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"

BLOCK_CHUNK_SIZE_MAP = {
"ethereum": 0,
"polygon": 0,
"polygon_zkevm": 0,
"arbitrum_one": 0,
"optimism": 0,
"coinbase_base": 0,
"fantom": 5000,
"mantle": 0,
"linea": 0,
}
170 changes: 26 additions & 144 deletions fastlane_bot/config/multicaller.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
"""
This is the multicaller module. TODO: BETTER NAME
TODO-MIKE: What exactly does this do and is it a bona fide config module?
MultiCaller class
---
(c) Copyright Bprotocol foundation 2023-24.
All rights reserved.
Licensed under MIT.
"""
from functools import partial
from typing import List, Callable, ContextManager, Any, Dict
from typing import Any, List, Dict

import web3
from eth_abi import decode
from web3 import Web3
from web3.contract.contract import ContractFunction

from fastlane_bot.data.abi import MULTICALL_ABI


def cast(typ, val):
"""Cast a value to a type.
This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return val


def collapse_if_tuple(abi: Dict[str, Any]) -> str:
"""
Converts a tuple from a dict to a parenthesized list of its types.
Expand All @@ -46,141 +31,38 @@ def collapse_if_tuple(abi: Dict[str, Any]) -> str:
... )
'(address,uint256,bytes)'
"""
typ = abi["type"]
if not isinstance(typ, str):
raise TypeError(
"The 'type' must be a string, but got %r of type %s" % (typ, type(typ))
)
elif not typ.startswith("tuple"):
return typ

delimited = ",".join(collapse_if_tuple(c) for c in abi["components"])
# Whatever comes after "tuple" is the array dims. The ABI spec states that
# this will have the form "", "[]", or "[k]".
array_dim = typ[5:]
collapsed = "({}){}".format(delimited, array_dim)

return collapsed


def get_output_types_from_abi(abi: List[Dict[str, Any]], function_name: str) -> List[str]:
"""
Get the output types from an ABI.
Parameters
----------
abi : List[Dict[str, Any]]
The ABI
function_name : str
The function name
Returns
-------
List[str]
The output types
"""
for item in abi:
if item['type'] == 'function' and item['name'] == function_name:
return [collapse_if_tuple(cast(Dict[str, Any], item)) for item in item['outputs']]
raise ValueError(f"No function named {function_name} found in ABI.")
if abi["type"].startswith("tuple"):
delimited = ",".join(collapse_if_tuple(c) for c in abi["components"])
return "({}){}".format(delimited, abi["type"][len("tuple"):])
return abi["type"]


class ContractMethodWrapper:
"""
Wraps a contract method to be used with multicall.
"""
__DATE__ = "2022-09-26"
__VERSION__ = "0.0.2"

def __init__(self, original_method, multicaller):
self.original_method = original_method
self.multicaller = multicaller

def __call__(self, *args, **kwargs):
contract_call = self.original_method(*args, **kwargs)
self.multicaller.add_call(contract_call)
return contract_call


class MultiCaller(ContextManager):
class MultiCaller:
"""
Context manager for multicalls.
"""
__DATE__ = "2022-09-26"
__VERSION__ = "0.0.2"

def __init__(self, web3: Any, multicall_contract_address: str):
self.multicall_contract = web3.eth.contract(abi=MULTICALL_ABI, address=multicall_contract_address)
self.contract_calls: List[ContractFunction] = []
self.output_types_list: List[List[str]] = []

def __init__(self, contract: web3.contract.Contract,
web3: Web3,
block_identifier: Any = 'latest', multicall_address = "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696"):
self._contract_calls: List[Callable] = []
self.contract = contract
self.block_identifier = block_identifier
self.web3 = web3
self.MULTICALL_CONTRACT_ADDRESS = self.web3.to_checksum_address(multicall_address)

def __enter__(self) -> 'MultiCaller':
return self

def __exit__(self, exc_type, exc_val, exc_tb):
pass

def add_call(self, fn: Callable, *args, **kwargs) -> None:
self._contract_calls.append(partial(fn, *args, **kwargs))

def multicall(self) -> List[Any]:
calls_for_aggregate = []
output_types_list = []
_calls_for_aggregate = {}
_output_types_list = {}
for fn in self._contract_calls:
fn_name = str(fn).split('functools.partial(<Function ')[1].split('>')[0]
output_types = get_output_types_from_abi(self.contract.abi, fn_name)
if fn_name in _calls_for_aggregate:
_calls_for_aggregate[fn_name].append({
'target': self.contract.address,
'callData': fn()._encode_transaction_data()
})
_output_types_list[fn_name].append(output_types)
else:
_calls_for_aggregate[fn_name] = [{
'target': self.contract.address,
'callData': fn()._encode_transaction_data()
}]
_output_types_list[fn_name] = [output_types]

for fn_list in _calls_for_aggregate.keys():
calls_for_aggregate += (_calls_for_aggregate[fn_list])
output_types_list += (_output_types_list[fn_list])

encoded_data = self.web3.eth.contract(
abi=MULTICALL_ABI,
address=self.MULTICALL_CONTRACT_ADDRESS
).functions.aggregate(calls_for_aggregate).call(block_identifier=self.block_identifier)

if not isinstance(encoded_data, list):
raise TypeError(f"Expected encoded_data to be a list, got {type(encoded_data)} instead.")

encoded_data = encoded_data[1]
decoded_data_list = []
for output_types, encoded_output in zip(output_types_list, encoded_data):
decoded_data = decode(output_types, encoded_output)
decoded_data_list.append(decoded_data)

return_data = [i[0] for i in decoded_data_list if len(i) == 1]
return_data += [i[1] for i in decoded_data_list if len(i) > 1]
def add_call(self, call: ContractFunction):
self.contract_calls.append({'target': call.address, 'callData': call._encode_transaction_data()})
self.output_types_list.append([collapse_if_tuple(item) for item in call.abi['outputs']])

# Handling for Bancor POL - combine results into a Tuple
if "tokenPrice" in _calls_for_aggregate and "amountAvailableForTrading" in _calls_for_aggregate:
new_return = []
returned_items = int(len(return_data))
total_pools = int(returned_items / 2)
assert returned_items % 2 == 0, f"[multicaller.py multicall] non-even number of returned calls for Bancor POL {returned_items}"
total_pools = int(total_pools)
def run_calls(self, block_identifier: Any = 'latest') -> List[Any]:
encoded_data = self.multicall_contract.functions.tryAggregate(
False,
self.contract_calls
).call(block_identifier=block_identifier)

for idx in range(total_pools):
new_return.append((return_data[idx][0], return_data[idx][1], return_data[idx + total_pools]))
return_data = new_return
result_list = [
decode(output_types, encoded_output[1]) if encoded_output[0] else (None,)
for output_types, encoded_output in zip(self.output_types_list, encoded_data)
]

return return_data
# Convert every single-value tuple into a single value
return [result if len(result) > 1 else result[0] for result in result_list]
Loading

0 comments on commit 8ac9256

Please sign in to comment.