Skip to content
This repository was archived by the owner on Nov 17, 2025. It is now read-only.

Commit 8ac9256

Browse files
authored
Merge pull request #670 from bancorprotocol/release-candidate
Release candidate - pair finder, eth_getLogs and more
2 parents d9e31ea + 1e3cb1a commit 8ac9256

File tree

87 files changed

+4941
-2118
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+4941
-2118
lines changed

.github/workflows/release-and-pypi-publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ jobs:
8383
id: bump_version_and_set_output
8484
run: |
8585
poetry version patch
86+
echo new_version=$(poetry version | cut -d' ' -f2) >> $GITHUB_OUTPUT
8687
git checkout main
8788
git config --local user.email "action@github.com"
8889
git config --local user.name "GitHub Action"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,5 @@ logs/*
7373
missing_tokens_df.csv
7474
tokens_and_fee_df.csv
7575
fastlane_bot/tests/nbtest/*
76+
77+
.python-version

fastlane_bot/bot.py

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -144,19 +144,19 @@ def get_curves(self) -> CPCContainer:
144144
ADDRDEC = {t.address: (t.address, int(t.decimals)) for t in tokens}
145145

146146
for p in pools_and_tokens:
147+
p.ADDRDEC = ADDRDEC
147148
try:
148-
p.ADDRDEC = ADDRDEC
149149
curves += [
150150
curve for curve in p.to_cpc()
151151
if all(curve.params[tkn] not in self.ConfigObj.TAX_TOKENS for tkn in ['tknx_addr', 'tkny_addr'])
152152
]
153153
except SolidlyV2StablePoolsNotSupported as e:
154154
self.ConfigObj.logger.debug(
155-
f"[bot.get_curves] SolidlyV2StablePoolsNotSupported: {e}\n"
155+
f"[bot.get_curves] Solidly V2 stable pools not supported: {e}\n"
156156
)
157157
except NotImplementedError as e:
158158
self.ConfigObj.logger.error(
159-
f"[bot.get_curves] Pool type not yet supported, error: {e}\n"
159+
f"[bot.get_curves] Not supported: {e}\n"
160160
)
161161
except ZeroDivisionError as e:
162162
self.ConfigObj.logger.error(
@@ -241,36 +241,17 @@ def _convert_trade_instructions(
241241
List[Dict[str, Any]]
242242
The trade instructions.
243243
"""
244-
errorless_trade_instructions_dicts = [
245-
{k: v for k, v in trade_instructions_dic[i].items() if k != "error"}
246-
for i in range(len(trade_instructions_dic))
247-
]
248-
result = (
249-
{
250-
**ti,
244+
return [
245+
TradeInstruction(**{
246+
**{k: v for k, v in ti.items() if k != "error"},
251247
"raw_txs": "[]",
252248
"pair_sorting": "",
253249
"ConfigObj": self.ConfigObj,
254250
"db": self.db,
255-
}
256-
for ti in errorless_trade_instructions_dicts
257-
if ti is not None
258-
)
259-
result = self._add_strategy_id_to_trade_instructions_dic(result)
260-
result = [TradeInstruction(**ti) for ti in result]
261-
return result
262-
263-
def _add_strategy_id_to_trade_instructions_dic(
264-
self, trade_instructions_dic: Generator
265-
) -> List[Dict[str, Any]]:
266-
lst = []
267-
for ti in trade_instructions_dic:
268-
cid = ti["cid"].split('-')[0]
269-
ti["strategy_id"] = self.db.get_pool(
270-
cid=cid
271-
).strategy_id
272-
lst.append(ti)
273-
return lst
251+
"strategy_id": self.db.get_pool(cid=ti["cid"].split('-')[0]).strategy_id
252+
})
253+
for ti in trade_instructions_dic if ti["error"] is None
254+
]
274255

275256
def _get_deadline(self, block_number) -> int:
276257
"""

fastlane_bot/config/__init__.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@
1010
- ``ConfigProvider`` (``provider``; provider for network access)
1111
- ``Config`` (``config``; main configuration class, integrates the above)
1212
13-
Submodules provide the following
14-
15-
- Constants (``constants`` and ``selectors``; various constants)
16-
- ``MultiCaller`` and related (``multicaller``; TODO: what is this?)
17-
- ``NetworkBase`` and ``EthereumNetwork`` (``connect``; network/chain connection code TODO: details)
18-
- ``Cloaker`` (``cloaker``; deprecated)
19-
20-
21-
2213
---
2314
(c) Copyright Bprotocol foundation 2023-24.
2415
All rights reserved.

fastlane_bot/config/constants.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
}
2020

2121
ETHEREUM = "ethereum"
22+
UNISWAP_V2_NAME = "uniswap_v2"
23+
UNISWAP_V3_NAME = "uniswap_v3"
2224
PANCAKESWAP_V2_NAME = "pancakeswap_v2"
2325
PANCAKESWAP_V3_NAME = "pancakeswap_v3"
2426
BUTTER_V3_NAME = "butter_v3"
@@ -31,3 +33,16 @@
3133
ECHODEX_V3_NAME = "echodex_v3"
3234
SECTA_V3_NAME = "secta_v3"
3335
METAVAULT_V3_NAME = "metavault_v3"
36+
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
37+
38+
BLOCK_CHUNK_SIZE_MAP = {
39+
"ethereum": 0,
40+
"polygon": 0,
41+
"polygon_zkevm": 0,
42+
"arbitrum_one": 0,
43+
"optimism": 0,
44+
"coinbase_base": 0,
45+
"fantom": 5000,
46+
"mantle": 0,
47+
"linea": 0,
48+
}

fastlane_bot/config/multicaller.py

Lines changed: 26 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,19 @@
11
"""
2-
This is the multicaller module. TODO: BETTER NAME
3-
4-
TODO-MIKE: What exactly does this do and is it a bona fide config module?
2+
MultiCaller class
53
64
---
75
(c) Copyright Bprotocol foundation 2023-24.
86
All rights reserved.
97
Licensed under MIT.
108
"""
11-
from functools import partial
12-
from typing import List, Callable, ContextManager, Any, Dict
9+
from typing import Any, List, Dict
1310

14-
import web3
1511
from eth_abi import decode
16-
from web3 import Web3
12+
from web3.contract.contract import ContractFunction
1713

1814
from fastlane_bot.data.abi import MULTICALL_ABI
1915

2016

21-
def cast(typ, val):
22-
"""Cast a value to a type.
23-
24-
This returns the value unchanged. To the type checker this
25-
signals that the return value has the designated type, but at
26-
runtime we intentionally don't check anything (we want this
27-
to be as fast as possible).
28-
"""
29-
return val
30-
31-
3217
def collapse_if_tuple(abi: Dict[str, Any]) -> str:
3318
"""
3419
Converts a tuple from a dict to a parenthesized list of its types.
@@ -46,141 +31,38 @@ def collapse_if_tuple(abi: Dict[str, Any]) -> str:
4631
... )
4732
'(address,uint256,bytes)'
4833
"""
49-
typ = abi["type"]
50-
if not isinstance(typ, str):
51-
raise TypeError(
52-
"The 'type' must be a string, but got %r of type %s" % (typ, type(typ))
53-
)
54-
elif not typ.startswith("tuple"):
55-
return typ
56-
57-
delimited = ",".join(collapse_if_tuple(c) for c in abi["components"])
58-
# Whatever comes after "tuple" is the array dims. The ABI spec states that
59-
# this will have the form "", "[]", or "[k]".
60-
array_dim = typ[5:]
61-
collapsed = "({}){}".format(delimited, array_dim)
62-
63-
return collapsed
64-
65-
66-
def get_output_types_from_abi(abi: List[Dict[str, Any]], function_name: str) -> List[str]:
67-
"""
68-
Get the output types from an ABI.
69-
70-
Parameters
71-
----------
72-
abi : List[Dict[str, Any]]
73-
The ABI
74-
function_name : str
75-
The function name
76-
77-
Returns
78-
-------
79-
List[str]
80-
The output types
81-
82-
"""
83-
for item in abi:
84-
if item['type'] == 'function' and item['name'] == function_name:
85-
return [collapse_if_tuple(cast(Dict[str, Any], item)) for item in item['outputs']]
86-
raise ValueError(f"No function named {function_name} found in ABI.")
34+
if abi["type"].startswith("tuple"):
35+
delimited = ",".join(collapse_if_tuple(c) for c in abi["components"])
36+
return "({}){}".format(delimited, abi["type"][len("tuple"):])
37+
return abi["type"]
8738

8839

89-
class ContractMethodWrapper:
90-
"""
91-
Wraps a contract method to be used with multicall.
92-
"""
93-
__DATE__ = "2022-09-26"
94-
__VERSION__ = "0.0.2"
95-
96-
def __init__(self, original_method, multicaller):
97-
self.original_method = original_method
98-
self.multicaller = multicaller
99-
100-
def __call__(self, *args, **kwargs):
101-
contract_call = self.original_method(*args, **kwargs)
102-
self.multicaller.add_call(contract_call)
103-
return contract_call
104-
105-
106-
class MultiCaller(ContextManager):
40+
class MultiCaller:
10741
"""
10842
Context manager for multicalls.
10943
"""
11044
__DATE__ = "2022-09-26"
11145
__VERSION__ = "0.0.2"
11246

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

114-
def __init__(self, contract: web3.contract.Contract,
115-
web3: Web3,
116-
block_identifier: Any = 'latest', multicall_address = "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696"):
117-
self._contract_calls: List[Callable] = []
118-
self.contract = contract
119-
self.block_identifier = block_identifier
120-
self.web3 = web3
121-
self.MULTICALL_CONTRACT_ADDRESS = self.web3.to_checksum_address(multicall_address)
122-
123-
def __enter__(self) -> 'MultiCaller':
124-
return self
125-
126-
def __exit__(self, exc_type, exc_val, exc_tb):
127-
pass
128-
129-
def add_call(self, fn: Callable, *args, **kwargs) -> None:
130-
self._contract_calls.append(partial(fn, *args, **kwargs))
131-
132-
def multicall(self) -> List[Any]:
133-
calls_for_aggregate = []
134-
output_types_list = []
135-
_calls_for_aggregate = {}
136-
_output_types_list = {}
137-
for fn in self._contract_calls:
138-
fn_name = str(fn).split('functools.partial(<Function ')[1].split('>')[0]
139-
output_types = get_output_types_from_abi(self.contract.abi, fn_name)
140-
if fn_name in _calls_for_aggregate:
141-
_calls_for_aggregate[fn_name].append({
142-
'target': self.contract.address,
143-
'callData': fn()._encode_transaction_data()
144-
})
145-
_output_types_list[fn_name].append(output_types)
146-
else:
147-
_calls_for_aggregate[fn_name] = [{
148-
'target': self.contract.address,
149-
'callData': fn()._encode_transaction_data()
150-
}]
151-
_output_types_list[fn_name] = [output_types]
152-
153-
for fn_list in _calls_for_aggregate.keys():
154-
calls_for_aggregate += (_calls_for_aggregate[fn_list])
155-
output_types_list += (_output_types_list[fn_list])
156-
157-
encoded_data = self.web3.eth.contract(
158-
abi=MULTICALL_ABI,
159-
address=self.MULTICALL_CONTRACT_ADDRESS
160-
).functions.aggregate(calls_for_aggregate).call(block_identifier=self.block_identifier)
161-
162-
if not isinstance(encoded_data, list):
163-
raise TypeError(f"Expected encoded_data to be a list, got {type(encoded_data)} instead.")
164-
165-
encoded_data = encoded_data[1]
166-
decoded_data_list = []
167-
for output_types, encoded_output in zip(output_types_list, encoded_data):
168-
decoded_data = decode(output_types, encoded_output)
169-
decoded_data_list.append(decoded_data)
170-
171-
return_data = [i[0] for i in decoded_data_list if len(i) == 1]
172-
return_data += [i[1] for i in decoded_data_list if len(i) > 1]
52+
def add_call(self, call: ContractFunction):
53+
self.contract_calls.append({'target': call.address, 'callData': call._encode_transaction_data()})
54+
self.output_types_list.append([collapse_if_tuple(item) for item in call.abi['outputs']])
17355

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

182-
for idx in range(total_pools):
183-
new_return.append((return_data[idx][0], return_data[idx][1], return_data[idx + total_pools]))
184-
return_data = new_return
62+
result_list = [
63+
decode(output_types, encoded_output[1]) if encoded_output[0] else (None,)
64+
for output_types, encoded_output in zip(self.output_types_list, encoded_data)
65+
]
18566

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

0 commit comments

Comments
 (0)