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.
@@ -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,5 @@
1
+ from .dex_pair import DexPair
2
+
3
+ __all__ = [
4
+ "DexPair",
5
+ ]
@@ -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,8 @@
1
+ from .factory import V2Factory
2
+ from .pair import V2Pair, get_uniswap_v2_pair_addr
3
+
4
+ __all__ = [
5
+ "V2Factory",
6
+ "V2Pair",
7
+ "get_uniswap_v2_pair_addr",
8
+ ]
@@ -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()
@@ -0,0 +1,5 @@
1
+ from .pool import V3Pool
2
+
3
+ __all__ = [
4
+ "V3Pool",
5
+ ]
@@ -0,0 +1,3 @@
1
+ class UniswapV3Liquidity:
2
+ def __init__(self):
3
+ pass