eth-portfolio 0.5.4__cp310-cp310-macosx_11_0_arm64.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.
Potentially problematic release.
This version of eth-portfolio might be problematic. Click here for more details.
- eth_portfolio/__init__.py +25 -0
- eth_portfolio/_argspec.cpython-310-darwin.so +0 -0
- eth_portfolio/_argspec.py +42 -0
- eth_portfolio/_cache.py +121 -0
- eth_portfolio/_config.cpython-310-darwin.so +0 -0
- eth_portfolio/_config.py +4 -0
- eth_portfolio/_db/__init__.py +0 -0
- eth_portfolio/_db/decorators.py +148 -0
- eth_portfolio/_db/entities.py +311 -0
- eth_portfolio/_db/utils.py +610 -0
- eth_portfolio/_decimal.py +156 -0
- eth_portfolio/_decorators.py +84 -0
- eth_portfolio/_exceptions.py +67 -0
- eth_portfolio/_ledgers/__init__.py +0 -0
- eth_portfolio/_ledgers/address.py +925 -0
- eth_portfolio/_ledgers/portfolio.py +328 -0
- eth_portfolio/_loaders/__init__.py +33 -0
- eth_portfolio/_loaders/_nonce.cpython-310-darwin.so +0 -0
- eth_portfolio/_loaders/_nonce.py +196 -0
- eth_portfolio/_loaders/balances.cpython-310-darwin.so +0 -0
- eth_portfolio/_loaders/balances.py +94 -0
- eth_portfolio/_loaders/token_transfer.py +217 -0
- eth_portfolio/_loaders/transaction.py +241 -0
- eth_portfolio/_loaders/utils.cpython-310-darwin.so +0 -0
- eth_portfolio/_loaders/utils.py +68 -0
- eth_portfolio/_shitcoins.cpython-310-darwin.so +0 -0
- eth_portfolio/_shitcoins.py +342 -0
- eth_portfolio/_stableish.cpython-310-darwin.so +0 -0
- eth_portfolio/_stableish.py +42 -0
- eth_portfolio/_submodules.py +73 -0
- eth_portfolio/_utils.py +225 -0
- eth_portfolio/_ydb/__init__.py +0 -0
- eth_portfolio/_ydb/token_transfers.py +146 -0
- eth_portfolio/address.py +397 -0
- eth_portfolio/buckets.py +212 -0
- eth_portfolio/constants.cpython-310-darwin.so +0 -0
- eth_portfolio/constants.py +87 -0
- eth_portfolio/portfolio.py +661 -0
- eth_portfolio/protocols/__init__.py +65 -0
- eth_portfolio/protocols/_base.py +107 -0
- eth_portfolio/protocols/convex.py +17 -0
- eth_portfolio/protocols/dsr.py +51 -0
- eth_portfolio/protocols/lending/README.md +6 -0
- eth_portfolio/protocols/lending/__init__.py +50 -0
- eth_portfolio/protocols/lending/_base.py +57 -0
- eth_portfolio/protocols/lending/compound.py +187 -0
- eth_portfolio/protocols/lending/liquity.py +110 -0
- eth_portfolio/protocols/lending/maker.py +111 -0
- eth_portfolio/protocols/lending/unit.py +46 -0
- eth_portfolio/protocols/liquity.py +16 -0
- eth_portfolio/py.typed +0 -0
- eth_portfolio/structs/__init__.py +43 -0
- eth_portfolio/structs/modified.py +69 -0
- eth_portfolio/structs/structs.py +626 -0
- eth_portfolio/typing/__init__.py +1419 -0
- eth_portfolio/typing/balance/single.py +176 -0
- eth_portfolio-0.5.4.dist-info/METADATA +26 -0
- eth_portfolio-0.5.4.dist-info/RECORD +83 -0
- eth_portfolio-0.5.4.dist-info/WHEEL +6 -0
- eth_portfolio-0.5.4.dist-info/entry_points.txt +2 -0
- eth_portfolio-0.5.4.dist-info/top_level.txt +3 -0
- eth_portfolio__mypyc.cpython-310-darwin.so +0 -0
- eth_portfolio_scripts/__init__.py +20 -0
- eth_portfolio_scripts/_args.py +26 -0
- eth_portfolio_scripts/_logging.py +15 -0
- eth_portfolio_scripts/_portfolio.py +209 -0
- eth_portfolio_scripts/_utils.py +106 -0
- eth_portfolio_scripts/balances.cpython-310-darwin.so +0 -0
- eth_portfolio_scripts/balances.py +57 -0
- eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json +1962 -0
- eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +10 -0
- eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml +11 -0
- eth_portfolio_scripts/docker/__init__.cpython-310-darwin.so +0 -0
- eth_portfolio_scripts/docker/__init__.py +16 -0
- eth_portfolio_scripts/docker/check.cpython-310-darwin.so +0 -0
- eth_portfolio_scripts/docker/check.py +67 -0
- eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
- eth_portfolio_scripts/docker/docker_compose.cpython-310-darwin.so +0 -0
- eth_portfolio_scripts/docker/docker_compose.py +100 -0
- eth_portfolio_scripts/main.py +119 -0
- eth_portfolio_scripts/py.typed +1 -0
- eth_portfolio_scripts/victoria/__init__.py +73 -0
- eth_portfolio_scripts/victoria/types.py +38 -0
eth_portfolio/address.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the :class:`~PortfolioAddress` class, which represents an address managed by the `eth-portfolio` system.
|
|
3
|
+
The :class:`~PortfolioAddress` class is designed to manage different aspects of an Ethereum address within the portfolio,
|
|
4
|
+
such as transactions, transfers, balances, and interactions with both external and lending protocols.
|
|
5
|
+
|
|
6
|
+
Key components and functionalities provided by the :class:`~eth_portfolio.address.PortfolioAddress` class include:
|
|
7
|
+
- Handling Ethereum and token balances
|
|
8
|
+
- Managing debt and collateral from lending protocols
|
|
9
|
+
- Tracking transactions and transfers (both internal and token transfers)
|
|
10
|
+
- Providing comprehensive balance descriptions at specific block heights
|
|
11
|
+
|
|
12
|
+
The class leverages asynchronous operations using the `a_sync` library to efficiently gather and process data.
|
|
13
|
+
It also integrates with various submodules from `eth-portfolio` to load balances, manage ledgers, and interact
|
|
14
|
+
with external protocols.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from asyncio import Task, create_task, gather
|
|
19
|
+
from typing import Dict, Final, Optional, final
|
|
20
|
+
|
|
21
|
+
import a_sync
|
|
22
|
+
import dank_mids
|
|
23
|
+
import eth_retry
|
|
24
|
+
import y
|
|
25
|
+
from a_sync.exceptions import MappingIsEmptyError
|
|
26
|
+
from eth_typing import BlockNumber
|
|
27
|
+
from y import convert
|
|
28
|
+
from y._decorators import stuck_coro_debugger
|
|
29
|
+
from y.datatypes import Address, Block
|
|
30
|
+
|
|
31
|
+
from eth_portfolio import protocols
|
|
32
|
+
from eth_portfolio._ledgers.address import (
|
|
33
|
+
AddressInternalTransfersLedger,
|
|
34
|
+
AddressLedgerBase,
|
|
35
|
+
AddressTokenTransfersLedger,
|
|
36
|
+
AddressTransactionsLedger,
|
|
37
|
+
PandableLedgerEntryList,
|
|
38
|
+
)
|
|
39
|
+
from eth_portfolio._loaders import balances
|
|
40
|
+
from eth_portfolio._utils import _LedgeredBase, _get_price
|
|
41
|
+
from eth_portfolio.typing import Balance, RemoteTokenBalances, TokenBalances, WalletBalances
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
logger: Final = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
checksum: Final = convert.to_address
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@final
|
|
50
|
+
class PortfolioAddress(_LedgeredBase[AddressLedgerBase]):
|
|
51
|
+
"""
|
|
52
|
+
Represents a portfolio address within the eth-portfolio system.
|
|
53
|
+
|
|
54
|
+
This class is designed to manage different aspects of an Ethereum address within the portfolio,
|
|
55
|
+
such as transactions, transfers, balances, and interactions with both external and lending protocols.
|
|
56
|
+
|
|
57
|
+
Key components and functionalities provided by the :class:`~eth_portfolio.address.PortfolioAddress` class include:
|
|
58
|
+
- Handling Ethereum and token balances
|
|
59
|
+
- Managing debt and collateral from lending protocols
|
|
60
|
+
- Tracking transactions and transfers (both internal and token transfers)
|
|
61
|
+
- Providing comprehensive balance descriptions at specific block heights
|
|
62
|
+
|
|
63
|
+
The class leverages asynchronous operations using the `a_sync` library to efficiently gather and process data.
|
|
64
|
+
It also integrates with various submodules from `eth-portfolio` to load balances, manage ledgers, and interact
|
|
65
|
+
with external protocols.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
address: Address,
|
|
71
|
+
start_block: Block,
|
|
72
|
+
load_prices: bool,
|
|
73
|
+
num_workers_transactions: int = 1000,
|
|
74
|
+
asynchronous: bool = False,
|
|
75
|
+
) -> None: # type: ignore
|
|
76
|
+
"""
|
|
77
|
+
Initializes the :class:`~PortfolioAddress` instance.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
address: The Ethereum address to manage.
|
|
81
|
+
start_block: The block number from which to start tracking.
|
|
82
|
+
load_prices: Flag indicating if price loading is enabled.
|
|
83
|
+
num_workers_transactions (optional): Number of workers for transaction processing. Defaults to 1000.
|
|
84
|
+
asynchronous (optional): Flag for asynchronous operation. Defaults to False.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
TypeError: If `asynchronous` is not a boolean.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
>>> address = PortfolioAddress('0x1234...', 0, True)
|
|
91
|
+
>>> print(address)
|
|
92
|
+
|
|
93
|
+
>>> address = PortfolioAddress('0x1234...', 0, False, num_workers_transactions=500, asynchronous=True)
|
|
94
|
+
>>> print(address)
|
|
95
|
+
|
|
96
|
+
See Also:
|
|
97
|
+
- :class:`~eth_portfolio.portfolio.Portfolio`
|
|
98
|
+
- :class:`~eth_portfolio._ledgers.address.AddressTransactionsLedger`
|
|
99
|
+
- :class:`~eth_portfolio._ledgers.address.AddressInternalTransfersLedger`
|
|
100
|
+
- :class:`~eth_portfolio._ledgers.address.AddressTokenTransfersLedger`
|
|
101
|
+
"""
|
|
102
|
+
self.address: Final = convert.to_address(address)
|
|
103
|
+
"""
|
|
104
|
+
The address being managed.
|
|
105
|
+
"""
|
|
106
|
+
if not isinstance(asynchronous, bool):
|
|
107
|
+
raise TypeError(f"`asynchronous` must be a boolean, you passed {type(asynchronous)}")
|
|
108
|
+
|
|
109
|
+
self.asynchronous: Final = asynchronous
|
|
110
|
+
"""
|
|
111
|
+
Flag indicating if the operations are asynchronous.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
self.load_prices: Final = load_prices
|
|
115
|
+
"""
|
|
116
|
+
Indicates if price loading is enabled.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
super().__init__(start_block)
|
|
120
|
+
|
|
121
|
+
self.transactions: Final = AddressTransactionsLedger(self, num_workers_transactions) # type: ignore [misc]
|
|
122
|
+
"""
|
|
123
|
+
Ledger for tracking transactions.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
self.internal_transfers: Final = AddressInternalTransfersLedger(self) # type: ignore [misc]
|
|
127
|
+
"""
|
|
128
|
+
Ledger for tracking internal transfers.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
self.token_transfers: Final = AddressTokenTransfersLedger(self) # type: ignore [misc]
|
|
132
|
+
"""
|
|
133
|
+
Ledger for tracking token transfers.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __str__(self) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Returns the string representation of the address.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
The address as a string.
|
|
142
|
+
"""
|
|
143
|
+
return self.address
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Returns the string representation of the PortfolioAddress instance.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The string representation.
|
|
151
|
+
"""
|
|
152
|
+
return f"<{self.__class__.__name__} address={self.address} at {hex(id(self))}>"
|
|
153
|
+
|
|
154
|
+
def __eq__(self, other: object) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Checks equality with another object.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
other: The object to compare with.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
True if equal, False otherwise.
|
|
163
|
+
"""
|
|
164
|
+
if isinstance(other, PortfolioAddress):
|
|
165
|
+
return self.address == other.address
|
|
166
|
+
elif isinstance(other, str):
|
|
167
|
+
return self.address == checksum(other)
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
def __hash__(self) -> int:
|
|
171
|
+
"""
|
|
172
|
+
Returns the hash of the address.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The hash value.
|
|
176
|
+
"""
|
|
177
|
+
return hash(self.address)
|
|
178
|
+
|
|
179
|
+
# Primary functions
|
|
180
|
+
|
|
181
|
+
@stuck_coro_debugger
|
|
182
|
+
async def describe(self, block: int) -> WalletBalances:
|
|
183
|
+
"""
|
|
184
|
+
Describes all of the wallet's balances at a given block.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
block: The block number.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
:class:`~eth_portfolio.typing.WalletBalances`: The wallet balances.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
TypeError: If block is not an integer.
|
|
194
|
+
|
|
195
|
+
Examples:
|
|
196
|
+
>>> wallet_balances = await address.describe(12345678)
|
|
197
|
+
"""
|
|
198
|
+
if not isinstance(block, int):
|
|
199
|
+
raise TypeError(f"Block must be an integer. You passed {type(block)} {block}")
|
|
200
|
+
coros = {
|
|
201
|
+
"assets": self.assets(block, sync=False),
|
|
202
|
+
"debt": self.debt(block, sync=False),
|
|
203
|
+
"external": self.external_balances(block, sync=False),
|
|
204
|
+
}
|
|
205
|
+
return WalletBalances(await a_sync.gather(coros), block=block) # type: ignore [arg-type]
|
|
206
|
+
|
|
207
|
+
@stuck_coro_debugger
|
|
208
|
+
async def assets(self, block: Optional[Block] = None) -> TokenBalances:
|
|
209
|
+
"""
|
|
210
|
+
Retrieves the balances for every asset in the wallet at a given block.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
block (optional): The block number to query. Defaults to None, which uses the latest block.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
:class:`~eth_portfolio.typing.TokenBalances`: The asset balances at `block`.
|
|
217
|
+
|
|
218
|
+
Examples:
|
|
219
|
+
>>> assets = await address.assets(12345678)
|
|
220
|
+
"""
|
|
221
|
+
return await self.balances(block=block, sync=False) # type: ignore [return-value]
|
|
222
|
+
|
|
223
|
+
@stuck_coro_debugger
|
|
224
|
+
async def debt(self, block: Optional[Block] = None) -> RemoteTokenBalances:
|
|
225
|
+
"""
|
|
226
|
+
Retrieves all debt balances for the wallet at a given block.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
block (optional): The block number. Defaults to None, which uses the latest block.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
:class:`~eth_portfolio.typing.RemoteTokenBalances`: The debt balances at `block`.
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
>>> debt = await address.debt(12345678)
|
|
236
|
+
"""
|
|
237
|
+
return await protocols.lending.debt(self.address, block=block)
|
|
238
|
+
|
|
239
|
+
@stuck_coro_debugger
|
|
240
|
+
async def external_balances(self, block: Optional[Block] = None) -> RemoteTokenBalances:
|
|
241
|
+
"""
|
|
242
|
+
Retrieves the balances owned by the wallet, but not held *in* the wallet, at a given block.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
block (optional): The block number. Defaults to None, which uses the latest block.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
:class:`~eth_portfolio.typing.RemoteTokenBalances`: The external balances.
|
|
249
|
+
|
|
250
|
+
Examples:
|
|
251
|
+
>>> external_balances = await address.external_balances(12345678)
|
|
252
|
+
"""
|
|
253
|
+
staking: "Task[RemoteTokenBalances]"
|
|
254
|
+
collateral: RemoteTokenBalances
|
|
255
|
+
|
|
256
|
+
staking = create_task(self.staking(block, sync=False)) # type: ignore [arg-type]
|
|
257
|
+
try:
|
|
258
|
+
collateral = await self.collateral(block, sync=False) # type: ignore [assignment]
|
|
259
|
+
except:
|
|
260
|
+
staking.cancel()
|
|
261
|
+
raise
|
|
262
|
+
else:
|
|
263
|
+
return collateral + await staking
|
|
264
|
+
|
|
265
|
+
# Assets
|
|
266
|
+
|
|
267
|
+
@stuck_coro_debugger
|
|
268
|
+
async def balances(self, block: Optional[Block]) -> TokenBalances:
|
|
269
|
+
"""
|
|
270
|
+
Retrieves balances for all assets in the wallet at a given block.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
block: The block number.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
:class:`~eth_portfolio.typing.TokenBalances`: The balances.
|
|
277
|
+
|
|
278
|
+
Examples:
|
|
279
|
+
>>> balances = await address.balances(12345678)
|
|
280
|
+
"""
|
|
281
|
+
eth_balance, token_balances = await gather(
|
|
282
|
+
self.eth_balance(block, sync=False),
|
|
283
|
+
self.token_balances(block, sync=False),
|
|
284
|
+
)
|
|
285
|
+
token_balances[y.EEE_ADDRESS] = eth_balance # type: ignore [call-overload]
|
|
286
|
+
return token_balances # type: ignore [return-value]
|
|
287
|
+
|
|
288
|
+
@eth_retry.auto_retry
|
|
289
|
+
@stuck_coro_debugger
|
|
290
|
+
async def eth_balance(self, block: Optional[Block]) -> Balance:
|
|
291
|
+
"""
|
|
292
|
+
Retrieves the ETH balance for the wallet at a given block.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
block: The block number.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
:class:`~eth_portfolio.typing.Balance`: The ETH balance at `block`.
|
|
299
|
+
|
|
300
|
+
Examples:
|
|
301
|
+
>>> eth_balance = await address.eth_balance(12345678)
|
|
302
|
+
"""
|
|
303
|
+
if balance := await dank_mids.eth.get_balance(
|
|
304
|
+
self.address, block_identifier=block # type: ignore [arg-type]
|
|
305
|
+
): # TODO: move hex into dank
|
|
306
|
+
price = await _get_price(y.WRAPPED_GAS_COIN, block)
|
|
307
|
+
return Balance(
|
|
308
|
+
balance.scaled,
|
|
309
|
+
round(balance.scaled * price, 18),
|
|
310
|
+
token=y.EEE_ADDRESS,
|
|
311
|
+
block=block,
|
|
312
|
+
)
|
|
313
|
+
return Balance(token=y.EEE_ADDRESS, block=block)
|
|
314
|
+
|
|
315
|
+
@stuck_coro_debugger
|
|
316
|
+
async def token_balances(self, block: BlockNumber) -> TokenBalances:
|
|
317
|
+
"""
|
|
318
|
+
Retrieves the balances for all tokens in the wallet at a given block.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
block: The block number.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
:class:`~eth_portfolio.typing.TokenBalances`: The token balances at `block`.
|
|
325
|
+
|
|
326
|
+
Examples:
|
|
327
|
+
>>> token_balances = await address.token_balances(12345678)
|
|
328
|
+
"""
|
|
329
|
+
try:
|
|
330
|
+
data = a_sync.map(
|
|
331
|
+
balances.load_token_balance,
|
|
332
|
+
self.token_transfers._yield_tokens_at_block(block=block),
|
|
333
|
+
address=self.address,
|
|
334
|
+
block=block,
|
|
335
|
+
)
|
|
336
|
+
return TokenBalances(await data, block=block)
|
|
337
|
+
except MappingIsEmptyError:
|
|
338
|
+
return TokenBalances(block=block)
|
|
339
|
+
|
|
340
|
+
@stuck_coro_debugger
|
|
341
|
+
async def collateral(self, block: Optional[Block] = None) -> RemoteTokenBalances:
|
|
342
|
+
"""
|
|
343
|
+
Retrieves all balances held by lending protocols on behalf of the wallet at a given block.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
block (optional): The block number. Defaults to None, which uses the latest block.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
:class:`~eth_portfolio.typing.RemoteTokenBalances`: The collateral balances.
|
|
350
|
+
|
|
351
|
+
Examples:
|
|
352
|
+
>>> collateral = await address.collateral(12345678)
|
|
353
|
+
"""
|
|
354
|
+
return await protocols.lending.collateral(self.address, block=block)
|
|
355
|
+
|
|
356
|
+
@stuck_coro_debugger
|
|
357
|
+
async def staking(self, block: Optional[Block] = None) -> RemoteTokenBalances:
|
|
358
|
+
"""
|
|
359
|
+
Retrieves all balances staked in protocols supported by eth_portfolio on behalf of the wallet at a given block.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
block (optional): The block number. Defaults to None, which uses the latest block.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
:class:`~eth_portfolio.typing.RemoteTokenBalances`: The staked balances.
|
|
366
|
+
|
|
367
|
+
Examples:
|
|
368
|
+
>>> staking_balances = await address.staking(12345678)
|
|
369
|
+
"""
|
|
370
|
+
return await protocols.balances(self.address, block=block)
|
|
371
|
+
|
|
372
|
+
# Ledger Entries
|
|
373
|
+
|
|
374
|
+
@stuck_coro_debugger
|
|
375
|
+
async def all(self, start_block: Block, end_block: Block) -> Dict[str, PandableLedgerEntryList]:
|
|
376
|
+
"""
|
|
377
|
+
Retrieves all ledger entries between two blocks.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
start_block: The starting block number.
|
|
381
|
+
end_block: The ending block number.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Dict[str, :class:`~eth_portfolio._ledgers.address.PandableLedgerEntryList`]: The ledger entries.
|
|
385
|
+
|
|
386
|
+
Examples:
|
|
387
|
+
>>> all_entries = await address.all(12000000, 12345678)
|
|
388
|
+
"""
|
|
389
|
+
return await a_sync.gather(
|
|
390
|
+
{
|
|
391
|
+
"transactions": self.transactions.get(start_block, end_block, sync=False),
|
|
392
|
+
"internal_transactions": self.internal_transfers.get(
|
|
393
|
+
start_block, end_block, sync=False
|
|
394
|
+
),
|
|
395
|
+
"token_transfers": self.token_transfers.get(start_block, end_block, sync=False),
|
|
396
|
+
}
|
|
397
|
+
)
|
eth_portfolio/buckets.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Final, Optional, Set, Dict
|
|
3
|
+
|
|
4
|
+
from a_sync import igather
|
|
5
|
+
from eth_typing import ChecksumAddress
|
|
6
|
+
from faster_async_lru import alru_cache
|
|
7
|
+
from y.constants import CHAINID, STABLECOINS, WRAPPED_GAS_COIN
|
|
8
|
+
from y.convert import to_address
|
|
9
|
+
from y.datatypes import Address, AnyAddressType
|
|
10
|
+
from y.exceptions import ContractNotVerified
|
|
11
|
+
from y.prices.lending.aave import aave
|
|
12
|
+
from y.prices.lending.compound import CToken, compound
|
|
13
|
+
from y.prices.stable_swap.curve import curve
|
|
14
|
+
from y.prices.yearn import YearnInspiredVault, is_yearn_vault
|
|
15
|
+
|
|
16
|
+
from eth_portfolio.constants import BTC_LIKE, ETH_LIKE, INTL_STABLECOINS
|
|
17
|
+
from eth_portfolio._stableish import STABLEISH_COINS
|
|
18
|
+
|
|
19
|
+
logger: Final = logging.getLogger(__name__)
|
|
20
|
+
log_debug: Final = logger.debug
|
|
21
|
+
|
|
22
|
+
SORT_AS_STABLES: Final = STABLECOINS.keys() | STABLEISH_COINS[CHAINID]
|
|
23
|
+
OTHER_LONG_TERM_ASSETS: Final[Set[ChecksumAddress]] = {}.get(CHAINID, set()) # type: ignore [call-overload]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_token_bucket(
|
|
27
|
+
token: AnyAddressType, custom_buckets: Optional[Dict[str, str]] = None
|
|
28
|
+
) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Categorize a token into a specific bucket based on its type.
|
|
31
|
+
|
|
32
|
+
This function attempts to categorize a given token into predefined buckets
|
|
33
|
+
such as "Cash & cash equivalents", "ETH", "BTC", "Other long term assets",
|
|
34
|
+
or "Other short term assets". The categorization is based on the token's
|
|
35
|
+
characteristics and its presence in specific sets like :data:`ETH_LIKE`, :data:`BTC_LIKE`,
|
|
36
|
+
and :data:`OTHER_LONG_TERM_ASSETS`.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
token: The address of the token to categorize.
|
|
40
|
+
custom_buckets: Optional mapping of token_address (lowercase) to bucket name.
|
|
41
|
+
If provided, after unwrapping the token, the function will check if the
|
|
42
|
+
unwrapped token address (lowercased) is present in this mapping and, if so,
|
|
43
|
+
return the mapped bucket name instead of using the default categorization logic.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A string representing the bucket category of the token.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If the token's source has not been verified and the error message
|
|
50
|
+
does not match the expected pattern.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
Categorize a stablecoin:
|
|
54
|
+
|
|
55
|
+
>>> await get_token_bucket("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
|
|
56
|
+
'Cash & cash equivalents'
|
|
57
|
+
|
|
58
|
+
Categorize an ETH-like token:
|
|
59
|
+
|
|
60
|
+
>>> await get_token_bucket("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE")
|
|
61
|
+
'ETH'
|
|
62
|
+
|
|
63
|
+
Use a custom mapping:
|
|
64
|
+
|
|
65
|
+
>>> custom_buckets = {"0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "My Stablecoin Bucket"}
|
|
66
|
+
>>> await get_token_bucket("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", custom_buckets=custom_buckets)
|
|
67
|
+
'My Stablecoin Bucket'
|
|
68
|
+
|
|
69
|
+
See Also:
|
|
70
|
+
- :func:`_unwrap_token`
|
|
71
|
+
- :func:`_is_stable`
|
|
72
|
+
"""
|
|
73
|
+
token_address = to_address(token)
|
|
74
|
+
try:
|
|
75
|
+
token_address = await _unwrap_token(token_address)
|
|
76
|
+
except ContractNotVerified as e:
|
|
77
|
+
return "Other short term assets"
|
|
78
|
+
|
|
79
|
+
# Check custom mapping AFTER unwrapping
|
|
80
|
+
if custom_buckets:
|
|
81
|
+
custom_bucket = custom_buckets.get(str(token_address).lower())
|
|
82
|
+
if custom_bucket is not None:
|
|
83
|
+
return custom_bucket
|
|
84
|
+
|
|
85
|
+
if _is_stable(token_address):
|
|
86
|
+
return "Cash & cash equivalents"
|
|
87
|
+
if token_address in ETH_LIKE:
|
|
88
|
+
return "ETH"
|
|
89
|
+
if token_address in BTC_LIKE:
|
|
90
|
+
return "BTC"
|
|
91
|
+
if token_address in OTHER_LONG_TERM_ASSETS:
|
|
92
|
+
return "Other long term assets"
|
|
93
|
+
return "Other short term assets"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@alru_cache(maxsize=None)
|
|
97
|
+
async def _unwrap_token(token: Any) -> ChecksumAddress:
|
|
98
|
+
"""
|
|
99
|
+
Recursively unwrap a token to its underlying asset.
|
|
100
|
+
|
|
101
|
+
This function attempts to unwrap a given token to its underlying asset by
|
|
102
|
+
checking if the token is a Yearn vault, a Curve pool, an Aave aToken, or a
|
|
103
|
+
Compound market. It recursively retrieves the underlying asset until it
|
|
104
|
+
reaches the base token.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
token: The address of the token to unwrap.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The address of the underlying asset.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
Unwrap a Yearn vault token:
|
|
114
|
+
|
|
115
|
+
>>> await _unwrap_token("0x5f18C75AbDAe578b483E5F43f12a39cF75b973a9")
|
|
116
|
+
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
|
|
117
|
+
|
|
118
|
+
See Also:
|
|
119
|
+
- :func:`y.prices.yearn.is_yearn_vault`
|
|
120
|
+
- :class:`y.prices.yearn.YearnInspiredVault`
|
|
121
|
+
- :class:`y.prices.stable_swap.curve`
|
|
122
|
+
- :class:`y.prices.lending.aave`
|
|
123
|
+
- :class:`y.prices.lending.compound.CToken`
|
|
124
|
+
"""
|
|
125
|
+
log_debug("unwrapping %s", token)
|
|
126
|
+
if str(token) in {"ETH", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"}:
|
|
127
|
+
log_debug("returning eee address")
|
|
128
|
+
return token
|
|
129
|
+
|
|
130
|
+
if await is_yearn_vault(token, sync=False):
|
|
131
|
+
underlying = await YearnInspiredVault(token, asynchronous=True).underlying
|
|
132
|
+
log_debug("underlying: %s", underlying)
|
|
133
|
+
return await _unwrap_token(underlying)
|
|
134
|
+
if curve and (pool := await curve.get_pool(token)):
|
|
135
|
+
pool_tokens = set(await igather(map(_unwrap_token, await pool.coins)))
|
|
136
|
+
log_debug("pool_tokens: %s", pool_tokens)
|
|
137
|
+
if pool_bucket := _pool_bucket(pool_tokens):
|
|
138
|
+
log_debug("returning pool bucket: %s", pool_bucket)
|
|
139
|
+
return pool_bucket # type: ignore
|
|
140
|
+
if aave and await aave.is_atoken(token):
|
|
141
|
+
log_debug("atoken")
|
|
142
|
+
return str(await aave.underlying(token))
|
|
143
|
+
if compound and await compound.is_compound_market(token):
|
|
144
|
+
log_debug("unwrapping ctoken %s", token)
|
|
145
|
+
try:
|
|
146
|
+
return str(await CToken(token, asynchronous=True).underlying)
|
|
147
|
+
except AttributeError:
|
|
148
|
+
return WRAPPED_GAS_COIN
|
|
149
|
+
log_debug("returning: %s", token)
|
|
150
|
+
return token
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _pool_bucket(pool_tokens: set) -> Optional[str]:
|
|
154
|
+
"""
|
|
155
|
+
Determine the bucket for a set of pool tokens.
|
|
156
|
+
|
|
157
|
+
This function checks if a set of pool tokens belongs to specific categories
|
|
158
|
+
such as BTC-like, ETH-like, or stablecoins, and returns the corresponding
|
|
159
|
+
bucket.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
pool_tokens: A set of token addresses representing the pool tokens.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
A string representing the bucket category of the pool tokens, or None if
|
|
166
|
+
no specific category is found.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
Determine the bucket for a BTC-like pool:
|
|
170
|
+
|
|
171
|
+
>>> _pool_bucket({"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"})
|
|
172
|
+
'0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599'
|
|
173
|
+
|
|
174
|
+
See Also:
|
|
175
|
+
- :data:`BTC_LIKE`
|
|
176
|
+
- :data:`ETH_LIKE`
|
|
177
|
+
- :data:`STABLECOINS`
|
|
178
|
+
- :data:`INTL_STABLECOINS`
|
|
179
|
+
"""
|
|
180
|
+
log_debug("Pool tokens: %s", pool_tokens)
|
|
181
|
+
if pool_tokens < BTC_LIKE:
|
|
182
|
+
return list(BTC_LIKE)[0]
|
|
183
|
+
if pool_tokens < ETH_LIKE:
|
|
184
|
+
return list(ETH_LIKE)[0]
|
|
185
|
+
if pool_tokens < SORT_AS_STABLES:
|
|
186
|
+
return list(SORT_AS_STABLES)[0]
|
|
187
|
+
return list(INTL_STABLECOINS)[0] if pool_tokens < INTL_STABLECOINS else None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _is_stable(token: ChecksumAddress) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Check if a token is a stablecoin or stable-ish coin.
|
|
193
|
+
|
|
194
|
+
This function checks if a given token is present in the :data:`STABLECOINS`,
|
|
195
|
+
:data:`INTL_STABLECOINS`, or :data:`STABLEISH_COINS` sets, indicating that it is
|
|
196
|
+
a stablecoin or considered stable by the wider market.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
token: The address of the token to check.
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
Check if a token is a stablecoin or stable-ish coin:
|
|
203
|
+
|
|
204
|
+
>>> _is_stable("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")
|
|
205
|
+
True
|
|
206
|
+
|
|
207
|
+
See Also:
|
|
208
|
+
- :data:`STABLECOINS`
|
|
209
|
+
- :data:`INTL_STABLECOINS`
|
|
210
|
+
- :data:`STABLEISH_COINS`
|
|
211
|
+
"""
|
|
212
|
+
return token in SORT_AS_STABLES or token in INTL_STABLECOINS
|
|
Binary file
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Final
|
|
3
|
+
|
|
4
|
+
from brownie import chain
|
|
5
|
+
from y import Network, convert, weth
|
|
6
|
+
|
|
7
|
+
ERC20_TRANSFER_EVENT_HASH: Final = (
|
|
8
|
+
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
|
9
|
+
)
|
|
10
|
+
ERC677_TRANSFER_EVENT_HASH: Final = (
|
|
11
|
+
"0xe19260aff97b920c7df27010903aeb9c8d2be5d310a2c67824cf3f15396e4c16"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
NUM_WALLETS: Final = int(os.environ.get("NUM_WALLETS", "10"))
|
|
15
|
+
ADDRESSES: Final = [
|
|
16
|
+
convert.to_address(address)
|
|
17
|
+
for address in {os.environ.get(f"PORTFOLIO_ADDRESS_{i}", None) for i in range(NUM_WALLETS)}
|
|
18
|
+
if address is not None
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
TRANSFER_SIGS: Final = [ERC20_TRANSFER_EVENT_HASH, ERC677_TRANSFER_EVENT_HASH]
|
|
22
|
+
|
|
23
|
+
ETH_LIKE: Final = {
|
|
24
|
+
Network.Mainnet: {
|
|
25
|
+
"ETH",
|
|
26
|
+
weth.address,
|
|
27
|
+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
|
|
28
|
+
"0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb", # seth
|
|
29
|
+
"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", # eth
|
|
30
|
+
"0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", # steth
|
|
31
|
+
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", # wstETH
|
|
32
|
+
"0x9559Aaa82d9649C7A7b220E7c461d2E74c9a3593", # reth
|
|
33
|
+
"0xE95A203B1a91a908F9B9CE46459d101078c2c3cb", # ankreth
|
|
34
|
+
"0x04C154b66CB340F3Ae24111CC767e0184Ed00Cc6", # pxETH
|
|
35
|
+
"0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3", # oETH
|
|
36
|
+
"0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6", # alETH
|
|
37
|
+
"0x1BED97CBC3c24A4fb5C069C6E311a967386131f7", # yETH
|
|
38
|
+
"0x24Ae2dA0f361AA4BE46b48EB19C91e02c5e4f27E", # mevETH
|
|
39
|
+
"0x5E8422345238F34275888049021821E8E08CAa1f", # frxETH
|
|
40
|
+
"0x821A278dFff762c76410264303F25bF42e195C0C", # pETH
|
|
41
|
+
"0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", # cbETH
|
|
42
|
+
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee", # weETH
|
|
43
|
+
"0x7C07F7aBe10CE8e33DC6C5aD68FE033085256A84", # icETH
|
|
44
|
+
"0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD", # ETH2x-FLI
|
|
45
|
+
"0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8", # ETH+
|
|
46
|
+
"0x3d1E5Cf16077F349e999d6b21A4f646e83Cd90c5", # dETH
|
|
47
|
+
"0x005F893EcD7bF9667195642f7649DA8163e23658", # dgnETH
|
|
48
|
+
},
|
|
49
|
+
}.get(chain.id, set())
|
|
50
|
+
|
|
51
|
+
BTC_LIKE: Final = {
|
|
52
|
+
Network.Mainnet: {
|
|
53
|
+
"0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D", # renbtc
|
|
54
|
+
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", # wbtc
|
|
55
|
+
"0xfE18be6b3Bd88A2D2A7f928d00292E7a9963CfC6", # sbtc
|
|
56
|
+
"0x8064d9Ae6cDf087b1bcd5BDf3531bD5d8C537a68", # obtc
|
|
57
|
+
"0x9BE89D2a4cd102D8Fecc6BF9dA793be995C22541", # bbtc
|
|
58
|
+
"0x0316EB71485b0Ab14103307bf65a021042c6d380", # hbtc
|
|
59
|
+
"0x5228a22e72ccC52d415EcFd199F99D0665E7733b", # pbtc
|
|
60
|
+
"0x8dAEBADE922dF735c38C80C7eBD708Af50815fAa", # tbtc
|
|
61
|
+
"0x66eFF5221ca926636224650Fd3B9c497FF828F7D", # multiBTC
|
|
62
|
+
"0x657e8C867D8B37dCC18fA4Caead9C45EB088C642", # eBTC
|
|
63
|
+
},
|
|
64
|
+
}.get(chain.id, set())
|
|
65
|
+
|
|
66
|
+
INTL_STABLECOINS: Final = {
|
|
67
|
+
Network.Mainnet: {
|
|
68
|
+
"0xD71eCFF9342A5Ced620049e616c5035F1dB98620", # sEUR
|
|
69
|
+
"0xC581b735A1688071A1746c968e0798D642EDE491", # EURT
|
|
70
|
+
"0xdB25f211AB05b1c97D595516F45794528a807ad8", # EURS
|
|
71
|
+
"0x96E61422b6A9bA0e068B6c5ADd4fFaBC6a4aae27", # ibEUR
|
|
72
|
+
"0x9fcf418B971134625CdF38448B949C8640971671", # EURN
|
|
73
|
+
"0x39b8B6385416f4cA36a20319F70D28621895279D", # EURe
|
|
74
|
+
"0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", # EURC
|
|
75
|
+
"0x3F1B0278A9ee595635B61817630cC19DE792f506", # sAUD
|
|
76
|
+
"0xFAFdF0C4c1CB09d430Bf88c75D88BB46DAe09967", # ibAUD
|
|
77
|
+
"0x97fe22E7341a0Cd8Db6F6C021A24Dc8f4DAD855F", # sGBP
|
|
78
|
+
"0x69681f8fde45345C3870BCD5eaf4A05a60E7D227", # ibGBP
|
|
79
|
+
"0xF6b1C627e95BFc3c1b4c9B825a032Ff0fBf3e07d", # sJPY
|
|
80
|
+
"0x5555f75e3d5278082200Fb451D1b6bA946D8e13b", # ibJPY
|
|
81
|
+
"0x0F83287FF768D1c1e17a42F44d644D7F22e8ee1d", # sCHF
|
|
82
|
+
"0x1CC481cE2BD2EC7Bf67d1Be64d4878b16078F309", # ibCHF
|
|
83
|
+
"0x269895a3dF4D73b077Fc823dD6dA1B95f72Aaf9B", # sKRW
|
|
84
|
+
"0x95dFDC8161832e4fF7816aC4B6367CE201538253", # ibKRW
|
|
85
|
+
"0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8", # EURA
|
|
86
|
+
},
|
|
87
|
+
}.get(chain.id, set())
|