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.

Files changed (83) hide show
  1. eth_portfolio/__init__.py +25 -0
  2. eth_portfolio/_argspec.cpython-310-darwin.so +0 -0
  3. eth_portfolio/_argspec.py +42 -0
  4. eth_portfolio/_cache.py +121 -0
  5. eth_portfolio/_config.cpython-310-darwin.so +0 -0
  6. eth_portfolio/_config.py +4 -0
  7. eth_portfolio/_db/__init__.py +0 -0
  8. eth_portfolio/_db/decorators.py +148 -0
  9. eth_portfolio/_db/entities.py +311 -0
  10. eth_portfolio/_db/utils.py +610 -0
  11. eth_portfolio/_decimal.py +156 -0
  12. eth_portfolio/_decorators.py +84 -0
  13. eth_portfolio/_exceptions.py +67 -0
  14. eth_portfolio/_ledgers/__init__.py +0 -0
  15. eth_portfolio/_ledgers/address.py +925 -0
  16. eth_portfolio/_ledgers/portfolio.py +328 -0
  17. eth_portfolio/_loaders/__init__.py +33 -0
  18. eth_portfolio/_loaders/_nonce.cpython-310-darwin.so +0 -0
  19. eth_portfolio/_loaders/_nonce.py +196 -0
  20. eth_portfolio/_loaders/balances.cpython-310-darwin.so +0 -0
  21. eth_portfolio/_loaders/balances.py +94 -0
  22. eth_portfolio/_loaders/token_transfer.py +217 -0
  23. eth_portfolio/_loaders/transaction.py +241 -0
  24. eth_portfolio/_loaders/utils.cpython-310-darwin.so +0 -0
  25. eth_portfolio/_loaders/utils.py +68 -0
  26. eth_portfolio/_shitcoins.cpython-310-darwin.so +0 -0
  27. eth_portfolio/_shitcoins.py +342 -0
  28. eth_portfolio/_stableish.cpython-310-darwin.so +0 -0
  29. eth_portfolio/_stableish.py +42 -0
  30. eth_portfolio/_submodules.py +73 -0
  31. eth_portfolio/_utils.py +225 -0
  32. eth_portfolio/_ydb/__init__.py +0 -0
  33. eth_portfolio/_ydb/token_transfers.py +146 -0
  34. eth_portfolio/address.py +397 -0
  35. eth_portfolio/buckets.py +212 -0
  36. eth_portfolio/constants.cpython-310-darwin.so +0 -0
  37. eth_portfolio/constants.py +87 -0
  38. eth_portfolio/portfolio.py +661 -0
  39. eth_portfolio/protocols/__init__.py +65 -0
  40. eth_portfolio/protocols/_base.py +107 -0
  41. eth_portfolio/protocols/convex.py +17 -0
  42. eth_portfolio/protocols/dsr.py +51 -0
  43. eth_portfolio/protocols/lending/README.md +6 -0
  44. eth_portfolio/protocols/lending/__init__.py +50 -0
  45. eth_portfolio/protocols/lending/_base.py +57 -0
  46. eth_portfolio/protocols/lending/compound.py +187 -0
  47. eth_portfolio/protocols/lending/liquity.py +110 -0
  48. eth_portfolio/protocols/lending/maker.py +111 -0
  49. eth_portfolio/protocols/lending/unit.py +46 -0
  50. eth_portfolio/protocols/liquity.py +16 -0
  51. eth_portfolio/py.typed +0 -0
  52. eth_portfolio/structs/__init__.py +43 -0
  53. eth_portfolio/structs/modified.py +69 -0
  54. eth_portfolio/structs/structs.py +626 -0
  55. eth_portfolio/typing/__init__.py +1419 -0
  56. eth_portfolio/typing/balance/single.py +176 -0
  57. eth_portfolio-0.5.4.dist-info/METADATA +26 -0
  58. eth_portfolio-0.5.4.dist-info/RECORD +83 -0
  59. eth_portfolio-0.5.4.dist-info/WHEEL +6 -0
  60. eth_portfolio-0.5.4.dist-info/entry_points.txt +2 -0
  61. eth_portfolio-0.5.4.dist-info/top_level.txt +3 -0
  62. eth_portfolio__mypyc.cpython-310-darwin.so +0 -0
  63. eth_portfolio_scripts/__init__.py +20 -0
  64. eth_portfolio_scripts/_args.py +26 -0
  65. eth_portfolio_scripts/_logging.py +15 -0
  66. eth_portfolio_scripts/_portfolio.py +209 -0
  67. eth_portfolio_scripts/_utils.py +106 -0
  68. eth_portfolio_scripts/balances.cpython-310-darwin.so +0 -0
  69. eth_portfolio_scripts/balances.py +57 -0
  70. eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json +1962 -0
  71. eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +10 -0
  72. eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml +11 -0
  73. eth_portfolio_scripts/docker/__init__.cpython-310-darwin.so +0 -0
  74. eth_portfolio_scripts/docker/__init__.py +16 -0
  75. eth_portfolio_scripts/docker/check.cpython-310-darwin.so +0 -0
  76. eth_portfolio_scripts/docker/check.py +67 -0
  77. eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
  78. eth_portfolio_scripts/docker/docker_compose.cpython-310-darwin.so +0 -0
  79. eth_portfolio_scripts/docker/docker_compose.py +100 -0
  80. eth_portfolio_scripts/main.py +119 -0
  81. eth_portfolio_scripts/py.typed +1 -0
  82. eth_portfolio_scripts/victoria/__init__.py +73 -0
  83. eth_portfolio_scripts/victoria/types.py +38 -0
@@ -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
+ )
@@ -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
@@ -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())