eth-protocols-py 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eth_protocols/__init__.py +13 -0
- eth_protocols/camelot_v3/__init__.py +5 -0
- eth_protocols/camelot_v3/pool.py +163 -0
- eth_protocols/helpers/__init__.py +9 -0
- eth_protocols/helpers/dex_pairs.py +254 -0
- eth_protocols/helpers/multicall.py +35 -0
- eth_protocols/helpers/price_tracker.py +170 -0
- eth_protocols/logger.py +3 -0
- eth_protocols/tokens/__init__.py +5 -0
- eth_protocols/tokens/erc20/__init__.py +198 -0
- eth_protocols/tokens/erc20/events.py +37 -0
- eth_protocols/types/__init__.py +5 -0
- eth_protocols/types/dex_pair.py +123 -0
- eth_protocols/uniswap_v2/__init__.py +8 -0
- eth_protocols/uniswap_v2/factory.py +46 -0
- eth_protocols/uniswap_v2/pair.py +146 -0
- eth_protocols/uniswap_v2/price.py +104 -0
- eth_protocols/uniswap_v3/__init__.py +5 -0
- eth_protocols/uniswap_v3/liquidity.py +3 -0
- eth_protocols/uniswap_v3/pool.py +168 -0
- eth_protocols/utils/lookup_dict.py +29 -0
- eth_protocols_py-0.1.4.dist-info/METADATA +23 -0
- eth_protocols_py-0.1.4.dist-info/RECORD +25 -0
- eth_protocols_py-0.1.4.dist-info/WHEEL +5 -0
- eth_protocols_py-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from typing import Annotated, ClassVar, Generic, cast
|
|
3
|
+
|
|
4
|
+
from eth_rpc import get_current_network
|
|
5
|
+
from eth_rpc.types import BLOCK_STRINGS, MaybeAwaitable, Network, primitives
|
|
6
|
+
from eth_typeshed import ERC20 as ERC20Contract
|
|
7
|
+
from eth_typeshed.erc20 import OwnerRequest, OwnerSpenderRequest
|
|
8
|
+
from eth_typing import ChecksumAddress, HexAddress
|
|
9
|
+
from eth_utils import to_checksum_address
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, WrapValidator
|
|
11
|
+
from typing_extensions import TypeVar
|
|
12
|
+
|
|
13
|
+
from .events import TransferEvents
|
|
14
|
+
|
|
15
|
+
NetworkType = TypeVar("NetworkType", bound=Network | None, default=None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ERC20(BaseModel, Generic[NetworkType]):
|
|
19
|
+
model_config = ConfigDict(frozen=True)
|
|
20
|
+
_network: ClassVar[Network | None] = None
|
|
21
|
+
|
|
22
|
+
tokens: ClassVar[dict[tuple[Network, ChecksumAddress], "ERC20"]] = {}
|
|
23
|
+
events: TransferEvents = Field(default_factory=TransferEvents)
|
|
24
|
+
|
|
25
|
+
address: Annotated[
|
|
26
|
+
ChecksumAddress, WrapValidator(lambda addr, y, z: to_checksum_address(addr))
|
|
27
|
+
]
|
|
28
|
+
network: Network = Field(default_factory=get_current_network)
|
|
29
|
+
|
|
30
|
+
_contract: ERC20Contract = PrivateAttr()
|
|
31
|
+
_decimals: primitives.uint256 | None = PrivateAttr(None)
|
|
32
|
+
_name: primitives.string | None = PrivateAttr(None)
|
|
33
|
+
_symbol: primitives.string | None = PrivateAttr(None)
|
|
34
|
+
|
|
35
|
+
def __class_getitem__(cls, network: Network):
|
|
36
|
+
cls._network = network
|
|
37
|
+
return cls
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def sync(self) -> "ERC20Sync":
|
|
41
|
+
obj = deepcopy(self)
|
|
42
|
+
obj.__class__ = ERC20Sync
|
|
43
|
+
obj = cast(ERC20Sync, obj)
|
|
44
|
+
return obj
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def raw(self) -> ERC20Contract:
|
|
48
|
+
return self._contract
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def get_network(cls, network: Network | None) -> Network:
|
|
52
|
+
return network or cls._network or get_current_network()
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def load(cls, address: HexAddress, network: Network | None = None):
|
|
56
|
+
checksum_address = to_checksum_address(address)
|
|
57
|
+
|
|
58
|
+
network = cls.get_network(network)
|
|
59
|
+
key = (network, checksum_address)
|
|
60
|
+
if key not in cls.tokens:
|
|
61
|
+
cls.tokens[key] = cls(address=checksum_address, network=network)
|
|
62
|
+
|
|
63
|
+
# unset the network on the class instance
|
|
64
|
+
cls._network = None
|
|
65
|
+
return cls.tokens[key]
|
|
66
|
+
|
|
67
|
+
def model_post_init(self, __context):
|
|
68
|
+
self._contract = ERC20Contract(address=self.address)
|
|
69
|
+
self.events.set_address(self.address)
|
|
70
|
+
|
|
71
|
+
def set_decimals(self, decimals: int):
|
|
72
|
+
if not decimals:
|
|
73
|
+
return
|
|
74
|
+
self._decimals = decimals
|
|
75
|
+
|
|
76
|
+
def set_symbol(self, symbol: int):
|
|
77
|
+
if not symbol:
|
|
78
|
+
return
|
|
79
|
+
self._symbol = symbol
|
|
80
|
+
|
|
81
|
+
async def decimals(self) -> primitives.uint256:
|
|
82
|
+
if not self._decimals:
|
|
83
|
+
self._decimals = await self._contract.decimals().get()
|
|
84
|
+
assert self._decimals
|
|
85
|
+
return self._decimals
|
|
86
|
+
|
|
87
|
+
async def name(self) -> primitives.string:
|
|
88
|
+
if not self._name:
|
|
89
|
+
self._name = await self._contract.name().get()
|
|
90
|
+
assert self._name
|
|
91
|
+
return self._name
|
|
92
|
+
|
|
93
|
+
async def symbol(self) -> primitives.string:
|
|
94
|
+
if not self._symbol:
|
|
95
|
+
self._symbol = await self._contract.symbol().get()
|
|
96
|
+
assert self._symbol
|
|
97
|
+
return self._symbol
|
|
98
|
+
|
|
99
|
+
def total_supply(
|
|
100
|
+
self,
|
|
101
|
+
block_number: int | BLOCK_STRINGS = "latest",
|
|
102
|
+
sync: bool = False,
|
|
103
|
+
) -> MaybeAwaitable[primitives.uint256]:
|
|
104
|
+
return self._contract.total_supply().get(
|
|
105
|
+
block_number=block_number,
|
|
106
|
+
sync=sync,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def balance_of(
|
|
110
|
+
self,
|
|
111
|
+
owner: HexAddress,
|
|
112
|
+
block_number: int | BLOCK_STRINGS = "latest",
|
|
113
|
+
sync: bool = False,
|
|
114
|
+
) -> MaybeAwaitable[int]:
|
|
115
|
+
return self._contract.balance_of(OwnerRequest(owner=owner)).get(
|
|
116
|
+
block_number=block_number,
|
|
117
|
+
sync=sync,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def allowance(
|
|
121
|
+
self,
|
|
122
|
+
owner: HexAddress,
|
|
123
|
+
spender: HexAddress,
|
|
124
|
+
block_number: int | BLOCK_STRINGS = "latest",
|
|
125
|
+
sync: bool = False,
|
|
126
|
+
) -> MaybeAwaitable[int]:
|
|
127
|
+
return self._contract.allowance(
|
|
128
|
+
OwnerSpenderRequest(owner=owner, spender=spender)
|
|
129
|
+
).get(block_number=block_number, sync=sync)
|
|
130
|
+
|
|
131
|
+
def __repr__(self):
|
|
132
|
+
return f"<ERC20 address={self.address}>"
|
|
133
|
+
|
|
134
|
+
def __lt__(self, other: "ERC20" | HexAddress) -> bool:
|
|
135
|
+
if isinstance(other, ERC20):
|
|
136
|
+
return self.address < other.address
|
|
137
|
+
return self.address < other
|
|
138
|
+
|
|
139
|
+
def __gt__(self, other: "ERC20" | HexAddress) -> bool:
|
|
140
|
+
if isinstance(other, ERC20):
|
|
141
|
+
return self.address > other.address
|
|
142
|
+
return self.address > other
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ERC20Sync(ERC20):
|
|
146
|
+
def decimals(self) -> int: # type: ignore
|
|
147
|
+
if not self._decimals:
|
|
148
|
+
self._decimals = self._contract.decimals().get(sync=True)
|
|
149
|
+
assert self._decimals
|
|
150
|
+
return self._decimals
|
|
151
|
+
|
|
152
|
+
def name(self) -> str: # type: ignore
|
|
153
|
+
if not self._name:
|
|
154
|
+
self._name = self._contract.name().get(sync=True)
|
|
155
|
+
assert self._name
|
|
156
|
+
return self._name
|
|
157
|
+
|
|
158
|
+
def symbol(self) -> str: # type: ignore
|
|
159
|
+
if not self._symbol:
|
|
160
|
+
self._symbol = self._contract.symbol().get(sync=True)
|
|
161
|
+
assert self._symbol
|
|
162
|
+
return self._symbol
|
|
163
|
+
|
|
164
|
+
def total_supply(self, block_number: int | BLOCK_STRINGS = "latest") -> int: # type: ignore
|
|
165
|
+
return self._contract.total_supply().get(block_number=block_number, sync=True)
|
|
166
|
+
|
|
167
|
+
def balance_of( # type: ignore
|
|
168
|
+
self,
|
|
169
|
+
owner: HexAddress,
|
|
170
|
+
block_number: int | BLOCK_STRINGS = "latest",
|
|
171
|
+
) -> int:
|
|
172
|
+
return self._contract.balance_of(OwnerRequest(owner=owner)).get(
|
|
173
|
+
block_number=block_number,
|
|
174
|
+
sync=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def allowance( # type: ignore
|
|
178
|
+
self,
|
|
179
|
+
owner: HexAddress,
|
|
180
|
+
spender: HexAddress,
|
|
181
|
+
block_number: int | BLOCK_STRINGS = "latest",
|
|
182
|
+
) -> int:
|
|
183
|
+
return self._contract.allowance(
|
|
184
|
+
OwnerSpenderRequest(owner=owner, spender=spender)
|
|
185
|
+
).get(
|
|
186
|
+
block_number=block_number,
|
|
187
|
+
sync=True,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def __lt__(self, other: "ERC20" | HexAddress) -> bool:
|
|
191
|
+
if isinstance(other, ERC20):
|
|
192
|
+
return self.address < other.address
|
|
193
|
+
return self.address < other
|
|
194
|
+
|
|
195
|
+
def __gt__(self, other: "ERC20" | HexAddress) -> bool:
|
|
196
|
+
if isinstance(other, ERC20):
|
|
197
|
+
return self.address > other.address
|
|
198
|
+
return self.address > other
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
|
|
3
|
+
from eth_rpc import EventData
|
|
4
|
+
from eth_rpc.constants import ADDRESS_ZERO
|
|
5
|
+
from eth_rpc.utils import address_to_topic
|
|
6
|
+
from eth_typeshed.erc20 import TransferEvent, TransferEventType
|
|
7
|
+
from eth_typing import HexAddress
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TransferEvents(BaseModel):
|
|
12
|
+
address: HexAddress | None = None
|
|
13
|
+
|
|
14
|
+
def set_address(self, address: HexAddress):
|
|
15
|
+
self.address = address
|
|
16
|
+
|
|
17
|
+
def mints(self) -> AsyncIterator[EventData[TransferEventType]]:
|
|
18
|
+
return self.transfers(
|
|
19
|
+
sender=ADDRESS_ZERO,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def burns(self) -> AsyncIterator[EventData[TransferEventType]]:
|
|
23
|
+
return self.transfers(
|
|
24
|
+
recipient=ADDRESS_ZERO,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def transfers(
|
|
28
|
+
self,
|
|
29
|
+
sender: HexAddress | None = None,
|
|
30
|
+
recipient: HexAddress | None = None,
|
|
31
|
+
step_size: int | None = None,
|
|
32
|
+
) -> AsyncIterator[EventData[TransferEventType]]:
|
|
33
|
+
return TransferEvent.set_filter(
|
|
34
|
+
addresses=[self.address],
|
|
35
|
+
topic1=address_to_topic(sender) if sender else None,
|
|
36
|
+
topic2=address_to_topic(recipient) if recipient else None,
|
|
37
|
+
).backfill(step_size=step_size)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import Annotated, Any, ClassVar, Generic, Optional, cast
|
|
5
|
+
|
|
6
|
+
from eth_protocols.tokens import ERC20
|
|
7
|
+
from eth_rpc import get_current_network
|
|
8
|
+
from eth_rpc.types import BLOCK_STRINGS, MaybeAwaitable, Network
|
|
9
|
+
from eth_typeshed._base import ProtocolBase
|
|
10
|
+
from eth_typing import ChecksumAddress, HexAddress
|
|
11
|
+
from eth_utils import to_checksum_address
|
|
12
|
+
from pydantic import (
|
|
13
|
+
BaseModel,
|
|
14
|
+
ConfigDict,
|
|
15
|
+
Field,
|
|
16
|
+
PrivateAttr,
|
|
17
|
+
ValidationInfo,
|
|
18
|
+
ValidatorFunctionWrapHandler,
|
|
19
|
+
computed_field,
|
|
20
|
+
)
|
|
21
|
+
from pydantic.functional_validators import WrapValidator
|
|
22
|
+
from typing_extensions import Self, TypeVar
|
|
23
|
+
|
|
24
|
+
NetworkType = TypeVar("NetworkType", bound=Network | None, default=None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_token(
|
|
28
|
+
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
29
|
+
) -> ERC20:
|
|
30
|
+
if isinstance(v, ERC20):
|
|
31
|
+
return v
|
|
32
|
+
return ERC20.load(address=v)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DexPair(ABC, BaseModel, Generic[NetworkType]):
|
|
36
|
+
model_config = ConfigDict(frozen=True)
|
|
37
|
+
_network: ClassVar[Network | None] = None
|
|
38
|
+
pairs: ClassVar[dict[tuple[Network, ChecksumAddress], "DexPair"]] = {}
|
|
39
|
+
|
|
40
|
+
pair_address: Annotated[
|
|
41
|
+
ChecksumAddress, WrapValidator(lambda addr, y, z: to_checksum_address(addr))
|
|
42
|
+
]
|
|
43
|
+
token0: Annotated[ERC20, WrapValidator(load_token)]
|
|
44
|
+
token1: Annotated[ERC20, WrapValidator(load_token)]
|
|
45
|
+
network: Network = Field(default_factory=get_current_network)
|
|
46
|
+
|
|
47
|
+
_contract: ProtocolBase
|
|
48
|
+
_reserve0: Optional[int] = PrivateAttr(None)
|
|
49
|
+
_reserve1: Optional[int] = PrivateAttr(None)
|
|
50
|
+
|
|
51
|
+
def __class_getitem__(cls, network: Network):
|
|
52
|
+
cls._network = network
|
|
53
|
+
return cls
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def load(cls, pair: HexAddress | Self, **kwargs):
|
|
57
|
+
if isinstance(pair, DexPair):
|
|
58
|
+
return pair
|
|
59
|
+
pair = cast(HexAddress, pair)
|
|
60
|
+
checksum_address = to_checksum_address(pair)
|
|
61
|
+
network = cls._network or get_current_network()
|
|
62
|
+
key = (network, checksum_address)
|
|
63
|
+
if key not in cls.pairs:
|
|
64
|
+
cls.pairs[key] = cls[network].load_pair(pair_address=checksum_address) # type: ignore
|
|
65
|
+
# unset the network on the class instance
|
|
66
|
+
cls._network = None
|
|
67
|
+
return cls.pairs[key]
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def load_pair(
|
|
71
|
+
cls,
|
|
72
|
+
pair_address: HexAddress,
|
|
73
|
+
): ...
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def address(self) -> ChecksumAddress:
|
|
77
|
+
return self.pair_address
|
|
78
|
+
|
|
79
|
+
async def reserves(self):
|
|
80
|
+
return Decimal(self._reserve0 / 10**self.decimals0), Decimal(
|
|
81
|
+
self._reserve1 / 10**self.decimals1
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def get_reserve(self, token: HexAddress) -> Decimal:
|
|
85
|
+
token = to_checksum_address(token)
|
|
86
|
+
if self._reserve0 is None and self._reserve1 is None:
|
|
87
|
+
raise ValueError("Must set reserves first")
|
|
88
|
+
if token == self.token0.address:
|
|
89
|
+
return Decimal(self._reserve0 / 10**self.decimals0)
|
|
90
|
+
if token == self.token1.address:
|
|
91
|
+
return Decimal(self._reserve1 / 10**self.decimals1)
|
|
92
|
+
return Decimal(0)
|
|
93
|
+
|
|
94
|
+
def set_reserves(self, reserve0: int, reserve1: int):
|
|
95
|
+
self._reserve0 = reserve0
|
|
96
|
+
self._reserve1 = reserve1
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def get_price(
|
|
100
|
+
self, token: HexAddress, block_number: Optional[int | BLOCK_STRINGS] = None
|
|
101
|
+
) -> Decimal: ...
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
def get_reserves(
|
|
105
|
+
self, block_number: int | BLOCK_STRINGS = "latest", sync: bool = False
|
|
106
|
+
) -> MaybeAwaitable[tuple[int, int]]: ...
|
|
107
|
+
|
|
108
|
+
@computed_field # type: ignore[misc]
|
|
109
|
+
@property
|
|
110
|
+
def flipped(self) -> bool:
|
|
111
|
+
return self.token0.address < self.token1.address
|
|
112
|
+
|
|
113
|
+
@cached_property
|
|
114
|
+
def decimals0(self):
|
|
115
|
+
return self.token0.sync.decimals()
|
|
116
|
+
|
|
117
|
+
@cached_property
|
|
118
|
+
def decimals1(self):
|
|
119
|
+
return self.token1.sync.decimals()
|
|
120
|
+
|
|
121
|
+
async def load_decimals(self):
|
|
122
|
+
await self.token0.decimals()
|
|
123
|
+
await self.token1.decimals()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
from eth_rpc import get_current_network
|
|
4
|
+
from eth_rpc.types import Network
|
|
5
|
+
from eth_typeshed.uniswap_v2 import GetPairRequest, UniswapV2Factory
|
|
6
|
+
from eth_typing import ChecksumAddress, HexAddress
|
|
7
|
+
from eth_utils import to_checksum_address
|
|
8
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
9
|
+
|
|
10
|
+
from .pair import V2Pair
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class V2Factory(BaseModel):
|
|
14
|
+
pairs: ClassVar[
|
|
15
|
+
dict[tuple[Network, ChecksumAddress, ChecksumAddress], "ChecksumAddress"]
|
|
16
|
+
] = Field({})
|
|
17
|
+
_network: ClassVar[Network | None] = None
|
|
18
|
+
|
|
19
|
+
_contract: UniswapV2Factory = PrivateAttr()
|
|
20
|
+
address: HexAddress
|
|
21
|
+
|
|
22
|
+
def __class_getitem__(cls, network: Network):
|
|
23
|
+
cls._network = network
|
|
24
|
+
|
|
25
|
+
def model_post_init(self, __context):
|
|
26
|
+
self._contract = UniswapV2Factory(address=self.address)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
async def load_pair(cls, token0: HexAddress, token1: HexAddress) -> "V2Pair":
|
|
30
|
+
network: Network = cls._network or get_current_network()
|
|
31
|
+
token0 = to_checksum_address(token0)
|
|
32
|
+
token1 = to_checksum_address(token1)
|
|
33
|
+
key = (network, token0, token1)
|
|
34
|
+
|
|
35
|
+
if key not in cls.pairs:
|
|
36
|
+
pair_address: HexAddress = await cls._contract.get_pair(
|
|
37
|
+
GetPairRequest(
|
|
38
|
+
token_a=token0,
|
|
39
|
+
token_b=token1,
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
cls.pairs[key] = to_checksum_address(pair_address)
|
|
43
|
+
|
|
44
|
+
return V2Pair.load(
|
|
45
|
+
pair_address=cls.pairs[key],
|
|
46
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import ClassVar, Optional
|
|
4
|
+
|
|
5
|
+
from eth_hash.auto import keccak as keccak_256
|
|
6
|
+
from eth_protocols.tokens import ERC20
|
|
7
|
+
from eth_protocols.types import DexPair
|
|
8
|
+
from eth_rpc import Contract, get_current_network
|
|
9
|
+
from eth_rpc.types import BLOCK_STRINGS, MaybeAwaitable, Network
|
|
10
|
+
from eth_typeshed.multicall import multicall
|
|
11
|
+
from eth_typeshed.uniswap_v2 import UniswapV2Pair
|
|
12
|
+
from eth_typing import HexAddress, HexStr
|
|
13
|
+
from eth_utils import to_checksum_address
|
|
14
|
+
from pydantic import PrivateAttr
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class V2Pair(DexPair):
|
|
18
|
+
_network: ClassVar[Network | None] = None
|
|
19
|
+
_contract: UniswapV2Pair = PrivateAttr()
|
|
20
|
+
|
|
21
|
+
def __class_getitem__(cls, network: Network):
|
|
22
|
+
cls._network = network
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def load_static(
|
|
26
|
+
cls,
|
|
27
|
+
pair_address: HexAddress,
|
|
28
|
+
tokena: HexAddress,
|
|
29
|
+
tokenb: HexAddress,
|
|
30
|
+
):
|
|
31
|
+
|
|
32
|
+
token0, token1 = (
|
|
33
|
+
(tokena, tokenb) if tokena.lower() < tokenb.lower() else (tokenb, tokena)
|
|
34
|
+
)
|
|
35
|
+
network = cls._network or get_current_network()
|
|
36
|
+
return V2Pair(
|
|
37
|
+
pair_address=pair_address, # type: ignore
|
|
38
|
+
token0=ERC20.load(token0, network=network),
|
|
39
|
+
token1=ERC20.load(token1, network=network),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
async def load_pair(
|
|
44
|
+
cls,
|
|
45
|
+
pair_address: HexAddress,
|
|
46
|
+
):
|
|
47
|
+
_contract = UniswapV2Pair(address=pair_address)
|
|
48
|
+
token0, token1 = await multicall.execute(
|
|
49
|
+
_contract.token0(),
|
|
50
|
+
_contract.token1(),
|
|
51
|
+
)
|
|
52
|
+
network = cls._network or get_current_network()
|
|
53
|
+
return V2Pair(
|
|
54
|
+
pair_address=pair_address, # type: ignore
|
|
55
|
+
token0=ERC20.load(token0, network=network),
|
|
56
|
+
token1=ERC20.load(token1, network=network),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def model_post_init(self, __context):
|
|
60
|
+
self._contract = UniswapV2Pair(address=self.pair_address)
|
|
61
|
+
|
|
62
|
+
def get_reserves(
|
|
63
|
+
self, block_number: int | BLOCK_STRINGS = "latest", sync: bool = False
|
|
64
|
+
) -> MaybeAwaitable[tuple[int, int]]:
|
|
65
|
+
if sync:
|
|
66
|
+
reserve0, reserve1, _ = self._contract.get_reserves().get(
|
|
67
|
+
block_number=block_number, sync=sync
|
|
68
|
+
)
|
|
69
|
+
self.set_reserves(reserve0, reserve1)
|
|
70
|
+
return reserve0, reserve1
|
|
71
|
+
|
|
72
|
+
# create an async function that loads the reserves
|
|
73
|
+
async def get_reserves() -> tuple[int, int]:
|
|
74
|
+
reserve0, reserve1, _ = await self._contract.get_reserves().get(
|
|
75
|
+
block_number=block_number
|
|
76
|
+
)
|
|
77
|
+
self.set_reserves(reserve0, reserve1)
|
|
78
|
+
return reserve0, reserve1
|
|
79
|
+
|
|
80
|
+
return get_reserves
|
|
81
|
+
|
|
82
|
+
def get_price(
|
|
83
|
+
self, token: HexAddress, block_number: Optional[int | BLOCK_STRINGS] = None
|
|
84
|
+
) -> Decimal:
|
|
85
|
+
token = to_checksum_address(token)
|
|
86
|
+
assert (
|
|
87
|
+
token.lower() == self.token0.address.lower()
|
|
88
|
+
or token.lower() == self.token1.address.lower()
|
|
89
|
+
), "invalid token"
|
|
90
|
+
token_a, token_b = (
|
|
91
|
+
(self.token0, self.token1)
|
|
92
|
+
if token.lower() == self.token0.address.lower()
|
|
93
|
+
else (self.token1, self.token0)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if block_number:
|
|
97
|
+
reserve0, reserve1, _ = self._contract.get_reserves().get(
|
|
98
|
+
block_number=block_number
|
|
99
|
+
)
|
|
100
|
+
if token == self.token0.address:
|
|
101
|
+
reserve_a = Decimal(reserve0 / 10**self.decimals0)
|
|
102
|
+
reserve_b = Decimal(reserve1 / 10**self.decimals1)
|
|
103
|
+
return Decimal(reserve_b / reserve_a)
|
|
104
|
+
if token == self.token1.address:
|
|
105
|
+
reserve_a = Decimal(reserve1 / 10**self.decimals1)
|
|
106
|
+
reserve_b = Decimal(reserve0 / 10**self.decimals0)
|
|
107
|
+
return Decimal(reserve_b / reserve_a)
|
|
108
|
+
return Decimal("0")
|
|
109
|
+
|
|
110
|
+
reserve_a = self.get_reserve(token_a.address)
|
|
111
|
+
reserve_b = self.get_reserve(token_b.address)
|
|
112
|
+
|
|
113
|
+
return Decimal(reserve_b / reserve_a)
|
|
114
|
+
|
|
115
|
+
def get_other_token(self, token: HexAddress) -> HexAddress:
|
|
116
|
+
token = to_checksum_address(token)
|
|
117
|
+
if token == self.token0.address:
|
|
118
|
+
return self.token1.address
|
|
119
|
+
if token == self.token1.address:
|
|
120
|
+
return self.token0.address
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"{token=} cannot be found {self.token0.address=} {self.token1.address=}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def load_decimals(self):
|
|
126
|
+
await asyncio.gather(
|
|
127
|
+
self.token0.decimals(),
|
|
128
|
+
self.token1.decimals(),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_uniswap_v2_pair_addr(token_a: HexAddress, token_b: HexAddress) -> HexAddress:
|
|
133
|
+
# get factory address
|
|
134
|
+
factory_address = HexAddress(HexStr("0x5C69BEE701EF814A2B6A3EDD4B1652CB9CC5AA6F"))
|
|
135
|
+
factory_contract = Contract(address=factory_address)
|
|
136
|
+
|
|
137
|
+
# sort tokens and create salt
|
|
138
|
+
token_a, token_b = sorted([token_a, token_b])
|
|
139
|
+
salt = keccak_256(bytes.fromhex(token_a[2:]) + bytes.fromhex(token_b[2:]))
|
|
140
|
+
|
|
141
|
+
# hard coded init code because it's deployed via a contract. this would require a trace to do it generically
|
|
142
|
+
keccak_init_code = bytes.fromhex(
|
|
143
|
+
"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return to_checksum_address(factory_contract.create2(salt, keccak_init_code))
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from eth_rpc import EventData, EventSubscriber
|
|
4
|
+
from eth_typeshed.constants import Factories
|
|
5
|
+
from eth_typeshed.uniswap_v2 import V2SyncEvent, V2SyncEventType
|
|
6
|
+
from eth_typing import HexAddress
|
|
7
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
8
|
+
|
|
9
|
+
from .factory import V2Factory
|
|
10
|
+
from .pair import V2Pair
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TerminateError(Exception):
|
|
14
|
+
"""This is a custom error used to interrupt the subscription"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UniswapV2PriceSubscriber(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
from eth_typeshed.constants import Tokens, Routers
|
|
20
|
+
from eth_rpc import PrivateKeyWallet
|
|
21
|
+
|
|
22
|
+
class MyLimitOrder(UniswapV2PriceSubscriber):
|
|
23
|
+
limit_price: float
|
|
24
|
+
router: UniswapV2Router
|
|
25
|
+
wallet: PrivateKeyWallet
|
|
26
|
+
|
|
27
|
+
async def handle_price_update(self, price: Decimal, event: EventData[V2SyncEventType]):
|
|
28
|
+
if price < limit_price:
|
|
29
|
+
await self.router.swap(
|
|
30
|
+
mint_amount_out=min_amount_out,
|
|
31
|
+
path=[Tokens.Ethereum.WETH, Tokens.Ethereum.USDC],
|
|
32
|
+
recipient=self.wallet.address,
|
|
33
|
+
).execute(self.wallet)
|
|
34
|
+
self.terminate()
|
|
35
|
+
|
|
36
|
+
async def amain():
|
|
37
|
+
limit_order = MyLimitOrder(
|
|
38
|
+
wallet=PrivateKeyWallet(),
|
|
39
|
+
)
|
|
40
|
+
pair = V2Factory.load_pair(token0=Tokens.Ethereum.USDC, token1=Tokens.Ethereum.WETH)
|
|
41
|
+
limit_order.add_pair(pair)
|
|
42
|
+
await limit_order.subscribe()
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
pairs: dict[HexAddress, V2Pair] = Field(default_factory=dict)
|
|
46
|
+
factory_address: HexAddress = Field(Factories.Ethereum.UniswapV2)
|
|
47
|
+
|
|
48
|
+
_factory: V2Factory = PrivateAttr()
|
|
49
|
+
_prepared: bool = PrivateAttr(False)
|
|
50
|
+
_subscriber: EventSubscriber = PrivateAttr(
|
|
51
|
+
default_factory=lambda: EventSubscriber()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def model_post_init(self, __context):
|
|
55
|
+
self._factory = V2Factory(self.factory_address)
|
|
56
|
+
self._subscriber.add_receiver(self, [V2SyncEvent])
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def factory(self):
|
|
60
|
+
return self._factory
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def subscriber(self):
|
|
64
|
+
return self._subscriber
|
|
65
|
+
|
|
66
|
+
async def add_pair(
|
|
67
|
+
self,
|
|
68
|
+
pair: HexAddress | V2Pair,
|
|
69
|
+
):
|
|
70
|
+
pair = V2Pair.load(pair)
|
|
71
|
+
self.pairs[pair.address.lower()] = pair # type: ignore
|
|
72
|
+
|
|
73
|
+
async def subscribe(self):
|
|
74
|
+
try:
|
|
75
|
+
await self.subscriber.listen(
|
|
76
|
+
addresses=[pair_address for pair_address in self.pairs.keys()]
|
|
77
|
+
)
|
|
78
|
+
except TerminateError:
|
|
79
|
+
# This is a simple way to exit out of the subscribe loop
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
async def put(self, event_data: EventData[V2SyncEventType]):
|
|
83
|
+
await self.handle_price_update(
|
|
84
|
+
self.get_price(event_data.event, event_data.log.address),
|
|
85
|
+
event_data,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_price(self, event: V2SyncEventType, address: HexAddress):
|
|
89
|
+
pair = self.pairs[address.lower()] # type: ignore
|
|
90
|
+
token0_reserves = event.reserve0 / 10 ** pair.token0.sync.decimals()
|
|
91
|
+
token1_reserves = event.reserve1 / 10 ** pair.token1.sync.decimals()
|
|
92
|
+
|
|
93
|
+
if pair.flipped:
|
|
94
|
+
return token1_reserves / token0_reserves
|
|
95
|
+
return token0_reserves / token1_reserves
|
|
96
|
+
|
|
97
|
+
async def handle_price_update(
|
|
98
|
+
self, price: Decimal, event: EventData[V2SyncEventType]
|
|
99
|
+
):
|
|
100
|
+
"""Define an action based on a price"""
|
|
101
|
+
|
|
102
|
+
def terminate(self):
|
|
103
|
+
"""Ends the subscription by catching a custom error"""
|
|
104
|
+
raise TerminateError()
|