dao-treasury 0.0.10__cp310-cp310-win32.whl → 0.0.70__cp310-cp310-win32.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.
Files changed (58) hide show
  1. dao_treasury/.grafana/provisioning/dashboards/breakdowns/Expenses.json +551 -0
  2. dao_treasury/.grafana/provisioning/dashboards/breakdowns/Revenue.json +551 -0
  3. dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +7 -7
  4. dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +220 -0
  5. dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +153 -29
  6. dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +181 -29
  7. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +808 -0
  8. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +602 -0
  9. dao_treasury/.grafana/provisioning/dashboards/treasury/Current Treasury Assets.json +981 -0
  10. dao_treasury/.grafana/provisioning/dashboards/treasury/Historical Treasury Balances.json +2989 -0
  11. dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +478 -0
  12. dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
  13. dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
  14. dao_treasury/__init__.py +36 -10
  15. dao_treasury/_docker.cp310-win32.pyd +0 -0
  16. dao_treasury/_docker.py +169 -37
  17. dao_treasury/_nicknames.cp310-win32.pyd +0 -0
  18. dao_treasury/_nicknames.py +32 -0
  19. dao_treasury/_wallet.cp310-win32.pyd +0 -0
  20. dao_treasury/_wallet.py +164 -12
  21. dao_treasury/constants.cp310-win32.pyd +0 -0
  22. dao_treasury/constants.py +39 -0
  23. dao_treasury/db.py +925 -150
  24. dao_treasury/docker-compose.yaml +6 -5
  25. dao_treasury/main.py +238 -28
  26. dao_treasury/sorting/__init__.cp310-win32.pyd +0 -0
  27. dao_treasury/sorting/__init__.py +219 -115
  28. dao_treasury/sorting/_matchers.cp310-win32.pyd +0 -0
  29. dao_treasury/sorting/_matchers.py +261 -17
  30. dao_treasury/sorting/_rules.cp310-win32.pyd +0 -0
  31. dao_treasury/sorting/_rules.py +166 -21
  32. dao_treasury/sorting/factory.cp310-win32.pyd +0 -0
  33. dao_treasury/sorting/factory.py +245 -37
  34. dao_treasury/sorting/rule.cp310-win32.pyd +0 -0
  35. dao_treasury/sorting/rule.py +228 -46
  36. dao_treasury/sorting/rules/__init__.cp310-win32.pyd +0 -0
  37. dao_treasury/sorting/rules/__init__.py +1 -0
  38. dao_treasury/sorting/rules/ignore/__init__.cp310-win32.pyd +0 -0
  39. dao_treasury/sorting/rules/ignore/__init__.py +1 -0
  40. dao_treasury/sorting/rules/ignore/llamapay.cp310-win32.pyd +0 -0
  41. dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
  42. dao_treasury/streams/__init__.cp310-win32.pyd +0 -0
  43. dao_treasury/streams/__init__.py +0 -0
  44. dao_treasury/streams/llamapay.cp310-win32.pyd +0 -0
  45. dao_treasury/streams/llamapay.py +388 -0
  46. dao_treasury/treasury.py +118 -25
  47. dao_treasury/types.cp310-win32.pyd +0 -0
  48. dao_treasury/types.py +104 -7
  49. dao_treasury-0.0.70.dist-info/METADATA +134 -0
  50. dao_treasury-0.0.70.dist-info/RECORD +54 -0
  51. dao_treasury-0.0.70.dist-info/top_level.txt +2 -0
  52. dao_treasury__mypyc.cp310-win32.pyd +0 -0
  53. a743a720bbc4482d330e__mypyc.cp310-win32.pyd +0 -0
  54. dao_treasury/.grafana/provisioning/datasources/sqlite.yaml +0 -10
  55. dao_treasury-0.0.10.dist-info/METADATA +0 -36
  56. dao_treasury-0.0.10.dist-info/RECORD +0 -28
  57. dao_treasury-0.0.10.dist-info/top_level.txt +0 -2
  58. {dao_treasury-0.0.10.dist-info → dao_treasury-0.0.70.dist-info}/WHEEL +0 -0
@@ -0,0 +1,388 @@
1
+ import asyncio
2
+ import datetime as dt
3
+ import decimal
4
+ from logging import getLogger
5
+ from typing import (
6
+ Awaitable,
7
+ Callable,
8
+ Dict,
9
+ Final,
10
+ Iterator,
11
+ List,
12
+ Optional,
13
+ Set,
14
+ final,
15
+ )
16
+
17
+ import dank_mids
18
+ import pony.orm
19
+ from a_sync import AsyncThreadPoolExecutor, igather
20
+ from brownie.network.event import _EventItem
21
+ from eth_typing import BlockNumber, ChecksumAddress, HexAddress, HexStr
22
+ from tqdm.asyncio import tqdm_asyncio
23
+
24
+ import y
25
+ from y.time import NoBlockFound, UnixTimestamp
26
+ from y.utils.events import decode_logs, get_logs_asap
27
+
28
+ from dao_treasury import constants
29
+ from dao_treasury.db import (
30
+ Stream,
31
+ StreamedFunds,
32
+ Address,
33
+ Token,
34
+ must_sort_outbound_txgroup_dbid,
35
+ )
36
+ from dao_treasury._wallet import TreasuryWallet
37
+
38
+
39
+ logger: Final = getLogger(__name__)
40
+
41
+ _UTC: Final = dt.timezone.utc
42
+
43
+ _ONE_DAY: Final = 60 * 60 * 24
44
+
45
+ _STREAMS_THREAD: Final = AsyncThreadPoolExecutor(1)
46
+
47
+ create_task: Final = asyncio.create_task
48
+ sleep: Final = asyncio.sleep
49
+
50
+ datetime: Final = dt.datetime
51
+ timedelta: Final = dt.timedelta
52
+ fromtimestamp: Final = datetime.fromtimestamp
53
+ now: Final = datetime.now
54
+
55
+ Decimal: Final = decimal.Decimal
56
+
57
+ ObjectNotFound: Final = pony.orm.ObjectNotFound
58
+ commit: Final = pony.orm.commit
59
+ db_session: Final = pony.orm.db_session
60
+
61
+ Contract: Final = y.Contract
62
+ Network: Final = y.Network
63
+ get_block_at_timestamp: Final = y.get_block_at_timestamp
64
+ get_price: Final = y.get_price
65
+
66
+
67
+ networks: Final = [Network.Mainnet]
68
+
69
+ factories: List[HexAddress] = []
70
+
71
+ if dai_stream_factory := {
72
+ Network.Mainnet: "0x60c7B0c5B3a4Dc8C690b074727a17fF7aA287Ff2",
73
+ }.get(constants.CHAINID):
74
+ factories.append(dai_stream_factory)
75
+
76
+ if yfi_stream_factory := {
77
+ Network.Mainnet: "0xf3764eC89B1ad20A31ed633b1466363FAc1741c4",
78
+ }.get(constants.CHAINID):
79
+ factories.append(yfi_stream_factory)
80
+
81
+
82
+ def _generate_dates(
83
+ start: dt.datetime, end: dt.datetime, stop_at_today: bool = True
84
+ ) -> Iterator[dt.datetime]:
85
+ current = start
86
+ while current < end:
87
+ yield current
88
+ current += timedelta(days=1)
89
+ if stop_at_today and current.date() > now(_UTC).date():
90
+ break
91
+
92
+
93
+ _StreamToStart = Callable[[HexStr, Optional[BlockNumber]], Awaitable[int]]
94
+
95
+ _streamToStart_cache: Final[Dict[HexStr, _StreamToStart]] = {}
96
+
97
+
98
+ def _get_streamToStart(stream_id: HexStr) -> _StreamToStart:
99
+ if streamToStart := _streamToStart_cache.get(stream_id):
100
+ return streamToStart
101
+ with db_session:
102
+ contract: y.Contract = Stream[stream_id].contract.contract # type: ignore [misc]
103
+ streamToStart = contract.streamToStart.coroutine
104
+ _streamToStart_cache[stream_id] = streamToStart
105
+ return streamToStart
106
+
107
+
108
+ async def _get_start_timestamp(
109
+ stream_id: HexStr, block: Optional[BlockNumber] = None
110
+ ) -> int:
111
+ streamToStart = _streamToStart_cache.get(stream_id)
112
+ if streamToStart is None:
113
+ streamToStart = await _STREAMS_THREAD.run(_get_streamToStart, stream_id)
114
+ # try:
115
+ return int(await streamToStart(f"0x{stream_id}", block_identifier=block)) # type: ignore [call-arg]
116
+ # except Exception:
117
+ # return 0
118
+
119
+
120
+ def _pause_stream(stream_id: HexStr) -> None:
121
+ with db_session:
122
+ Stream[stream_id].pause() # type: ignore [misc]
123
+
124
+
125
+ def _stop_stream(stream_id: str, block: BlockNumber) -> None:
126
+ with db_session:
127
+ Stream[stream_id].stop_stream(block) # type: ignore [misc]
128
+
129
+
130
+ _block_timestamps: Final[Dict[BlockNumber, UnixTimestamp]] = {}
131
+
132
+
133
+ async def _get_block_timestamp(block: BlockNumber) -> UnixTimestamp:
134
+ if timestamp := _block_timestamps.get(block):
135
+ return timestamp
136
+ timestamp = await dank_mids.eth.get_block_timestamp(block)
137
+ _block_timestamps[block] = timestamp
138
+ return timestamp
139
+
140
+
141
+ """
142
+ class _StreamProcessor(ABC):
143
+ @abstractmethod
144
+ async def _load_streams(self) -> None:
145
+ ...
146
+ """
147
+
148
+
149
+ @final
150
+ class LlamaPayProcessor:
151
+ """
152
+ Generalized async processor for DAO stream contracts.
153
+ Args are passed in at construction time.
154
+ Supports time-bounded admin periods for filtering.
155
+ """
156
+
157
+ handled_events: Final = (
158
+ "StreamCreated",
159
+ "StreamCreatedWithReason",
160
+ "StreamModified",
161
+ "StreamPaused",
162
+ "StreamCancelled",
163
+ )
164
+ skipped_events: Final = (
165
+ "PayerDeposit",
166
+ "PayerWithdraw",
167
+ "Withdraw",
168
+ )
169
+
170
+ def __init__(self) -> None:
171
+ self.stream_contracts: Final = {Contract(addr) for addr in factories}
172
+
173
+ async def _get_streams(self) -> None:
174
+ await igather(
175
+ self._load_contract_events(stream_contract)
176
+ for stream_contract in self.stream_contracts
177
+ )
178
+
179
+ async def _load_contract_events(self, stream_contract: y.Contract) -> None:
180
+ events = decode_logs(
181
+ await get_logs_asap(stream_contract.address, None, sync=False)
182
+ )
183
+ keys: Set[str] = set(events.keys())
184
+ for k in keys:
185
+ if k not in self.handled_events and k not in self.skipped_events:
186
+ raise NotImplementedError(f"Need to handle event: {k}")
187
+
188
+ if "StreamCreated" in keys:
189
+ for event in events["StreamCreated"]:
190
+ from_address, *_ = event.values()
191
+ from_address = Address.get_or_insert(from_address).address
192
+ if not TreasuryWallet.check_membership(
193
+ from_address, event.block_number
194
+ ):
195
+ continue
196
+ await _STREAMS_THREAD.run(self._get_stream, event)
197
+
198
+ if "StreamCreatedWithReason" in keys:
199
+ for event in events["StreamCreatedWithReason"]:
200
+ from_address, *_ = event.values()
201
+ from_address = Address.get_or_insert(from_address).address
202
+ if not TreasuryWallet.check_membership(
203
+ from_address, event.block_number
204
+ ):
205
+ continue
206
+ await _STREAMS_THREAD.run(self._get_stream, event)
207
+
208
+ if "StreamModified" in keys:
209
+ for event in events["StreamModified"]:
210
+ from_address, _, _, old_stream_id, *_ = event.values()
211
+ if not TreasuryWallet.check_membership(
212
+ from_address, event.block_number
213
+ ):
214
+ continue
215
+ await _STREAMS_THREAD.run(
216
+ _stop_stream, old_stream_id.hex(), event.block_number
217
+ )
218
+ await _STREAMS_THREAD.run(self._get_stream, event)
219
+
220
+ if "StreamPaused" in keys:
221
+ for event in events["StreamPaused"]:
222
+ from_address, *_, stream_id = event.values()
223
+ if not TreasuryWallet.check_membership(
224
+ from_address, event.block_number
225
+ ):
226
+ continue
227
+ await _STREAMS_THREAD.run(_pause_stream, stream_id.hex())
228
+
229
+ if "StreamCancelled" in keys:
230
+ for event in events["StreamCancelled"]:
231
+ from_address, *_, stream_id = event.values()
232
+ if not TreasuryWallet.check_membership(
233
+ from_address, event.block_number
234
+ ):
235
+ continue
236
+ await _STREAMS_THREAD.run(
237
+ _stop_stream, stream_id.hex(), event.block_number
238
+ )
239
+
240
+ def _get_stream(self, log: _EventItem) -> Stream:
241
+ with db_session:
242
+ if log.name == "StreamCreated":
243
+ from_address, to_address, amount_per_second, stream_id = log.values()
244
+ reason = None
245
+ elif log.name == "StreamCreatedWithReason":
246
+ from_address, to_address, amount_per_second, stream_id, reason = (
247
+ log.values()
248
+ )
249
+ elif log.name == "StreamModified":
250
+ (
251
+ from_address,
252
+ _,
253
+ _,
254
+ old_stream_id,
255
+ to_address,
256
+ amount_per_second,
257
+ stream_id,
258
+ ) = log.values()
259
+ reason = Stream[old_stream_id.hex()].reason # type: ignore [misc]
260
+ else:
261
+ raise NotImplementedError("This is not an appropriate event log.")
262
+
263
+ stream_id_hex = stream_id.hex()
264
+ try:
265
+ return Stream[stream_id_hex] # type: ignore [misc]
266
+ except ObjectNotFound:
267
+ entity = Stream(
268
+ stream_id=stream_id_hex,
269
+ contract=Address.get_dbid(log.address),
270
+ start_block=log.block_number,
271
+ token=Token.get_dbid(Contract(log.address).token()),
272
+ from_address=Address.get_dbid(from_address),
273
+ to_address=Address.get_dbid(to_address),
274
+ amount_per_second=amount_per_second,
275
+ txgroup=must_sort_outbound_txgroup_dbid,
276
+ )
277
+ if reason is not None:
278
+ entity.reason = reason
279
+ commit()
280
+ return entity
281
+
282
+ def streams_for_recipient(
283
+ self, recipient: ChecksumAddress, at_block: Optional[BlockNumber] = None
284
+ ) -> List[Stream]:
285
+ with db_session:
286
+ streams = Stream.select(lambda s: s.to_address.address == recipient)
287
+ if at_block is None:
288
+ return list(streams)
289
+ return [
290
+ s for s in streams if (s.end_block is None or at_block <= s.end_block)
291
+ ]
292
+
293
+ def streams_for_token(
294
+ self, token: ChecksumAddress, include_inactive: bool = False
295
+ ) -> List[Stream]:
296
+ with db_session:
297
+ streams = Stream.select(lambda s: s.token.address.address == token)
298
+ return (
299
+ list(streams)
300
+ if include_inactive
301
+ else [s for s in streams if s.is_alive]
302
+ )
303
+
304
+ async def process_streams(self, run_forever: bool = False) -> None:
305
+ logger.info("Processing stream events and streamed funds...")
306
+ # Always sync events before processing
307
+ await self._get_streams()
308
+ with db_session:
309
+ streams = [s.stream_id for s in Stream.select()]
310
+ await tqdm_asyncio.gather(
311
+ *(
312
+ self.process_stream(stream_id, run_forever=run_forever)
313
+ for stream_id in streams
314
+ ),
315
+ desc="LlamaPay Streams",
316
+ )
317
+
318
+ async def process_stream(
319
+ self, stream_id: HexStr, run_forever: bool = False
320
+ ) -> None:
321
+ start, end = await _STREAMS_THREAD.run(Stream._get_start_and_end, stream_id)
322
+ for date_obj in _generate_dates(start, end, stop_at_today=not run_forever):
323
+ if await self.process_stream_for_date(stream_id, date_obj) is None:
324
+ return
325
+
326
+ async def process_stream_for_date(
327
+ self, stream_id: HexStr, date_obj: dt.datetime
328
+ ) -> Optional[StreamedFunds]:
329
+ entity = await _STREAMS_THREAD.run(
330
+ StreamedFunds.get_entity, stream_id, date_obj
331
+ )
332
+ if entity:
333
+ return entity
334
+
335
+ stream_token, start_date = await _STREAMS_THREAD.run(
336
+ Stream._get_token_and_start_date, stream_id
337
+ )
338
+ check_at = date_obj + timedelta(days=1) - timedelta(seconds=1)
339
+ if check_at > now(tz=_UTC):
340
+ await sleep((check_at - now(tz=_UTC)).total_seconds())
341
+
342
+ while True:
343
+ try:
344
+ block = await get_block_at_timestamp(check_at, sync=False)
345
+ except NoBlockFound:
346
+ sleep_time = (check_at - now(tz=_UTC)).total_seconds()
347
+ logger.debug(
348
+ "no block found for %s, sleeping %ss", check_at, sleep_time
349
+ )
350
+ await sleep(sleep_time)
351
+ else:
352
+ break
353
+
354
+ price_fut = create_task(get_price(stream_token, block, sync=False))
355
+ start_timestamp = await _get_start_timestamp(stream_id, block)
356
+ if start_timestamp == 0:
357
+ if await _STREAMS_THREAD.run(Stream.check_closed, stream_id):
358
+ price_fut.cancel()
359
+ return None
360
+
361
+ while start_timestamp == 0:
362
+ block -= 1
363
+ start_timestamp = await _get_start_timestamp(stream_id, block)
364
+
365
+ block_datetime = fromtimestamp(await _get_block_timestamp(block), tz=_UTC)
366
+ assert block_datetime.date() == date_obj.date()
367
+ seconds_active = (check_at - block_datetime).seconds
368
+ is_last_day = True
369
+ else:
370
+ seconds_active = int(check_at.timestamp()) - start_timestamp
371
+ is_last_day = False
372
+
373
+ seconds_active_today = min(seconds_active, _ONE_DAY)
374
+ if seconds_active_today < _ONE_DAY and not is_last_day:
375
+ if date_obj.date() != start_date:
376
+ seconds_active_today = _ONE_DAY
377
+
378
+ with db_session:
379
+ price = Decimal(await price_fut)
380
+ entity = await _STREAMS_THREAD.run(
381
+ StreamedFunds.create_entity,
382
+ stream_id,
383
+ date_obj,
384
+ price,
385
+ seconds_active_today,
386
+ is_last_day,
387
+ )
388
+ return entity
dao_treasury/treasury.py CHANGED
@@ -1,19 +1,39 @@
1
- from asyncio import create_task
1
+ """Treasury orchestration and analytics interface.
2
+
3
+ This module defines the Treasury class, which aggregates DAO wallets, sets up
4
+ sorting rules, and manages transaction ingestion and streaming analytics.
5
+ It coordinates the end-to-end flow from wallet configuration to database
6
+ population and dashboard analytics.
7
+
8
+ Key Responsibilities:
9
+ - Aggregate and manage DAO-controlled wallets.
10
+ - Ingest and process on-chain transactions.
11
+ - Apply sorting/categorization rules.
12
+ - Integrate with streaming protocols (e.g., LlamaPay).
13
+ - Populate the database for analytics and dashboards.
14
+
15
+ This is the main entry point for orchestrating DAO treasury analytics.
16
+ """
17
+
18
+ from asyncio import create_task, gather
2
19
  from logging import getLogger
3
20
  from pathlib import Path
4
- from typing import Final, Iterable, List, Optional, Union
21
+ from typing import Dict, Final, Iterable, List, Optional, Union
5
22
 
6
23
  import a_sync
7
24
  from a_sync.a_sync.abstract import ASyncABC
8
- from eth_typing import BlockNumber
25
+ from eth_typing import BlockNumber, HexAddress
9
26
  from eth_portfolio.structs import LedgerEntry
10
27
  from eth_portfolio.typing import PortfolioBalances
11
28
  from eth_portfolio_scripts._portfolio import ExportablePortfolio
29
+ from pony.orm import db_session
12
30
  from tqdm.asyncio import tqdm_asyncio
13
31
 
14
32
  from dao_treasury._wallet import TreasuryWallet
33
+ from dao_treasury.constants import CHAINID
15
34
  from dao_treasury.db import TreasuryTx
16
35
  from dao_treasury.sorting._rules import Rules
36
+ from dao_treasury.streams import llamapay
17
37
 
18
38
 
19
39
  Wallet = Union[TreasuryWallet, str]
@@ -32,19 +52,58 @@ class Treasury(a_sync.ASyncGenericBase): # type: ignore [misc]
32
52
  sort_rules: Optional[Path] = None,
33
53
  start_block: int = 0,
34
54
  label: str = "your org's treasury",
55
+ custom_buckets: Optional[Dict[HexAddress, str]] = None,
35
56
  asynchronous: bool = False,
36
57
  ) -> None:
37
- """
58
+ """Initialize the Treasury singleton for managing DAO funds.
59
+
60
+ This class aggregates multiple treasury wallets, sets up sorting rules,
61
+ and constructs an :class:`.ExportablePortfolio` for fetching balance and
62
+ transaction history.
63
+
38
64
  Args:
39
- wallets: Iterable[Union[TreasuryWallet, str]]
65
+ wallets: Iterable of wallet
66
+ addresses or :class:`.TreasuryWallet` instances representing
67
+ DAO-controlled wallets.
68
+ sort_rules: Directory path containing YAML rule files
69
+ for sorting transactions. See :class:`dao_treasury.sorting._rules.Rules`.
70
+ start_block: Block number from which to start loading portfolio
71
+ history.
72
+ label: Descriptive label for the portfolio, used in exported data.
73
+ asynchronous: Whether methods default to asynchronous mode.
74
+
75
+ Raises:
76
+ RuntimeError: If a second Treasury instance is initialized.
77
+ TypeError: If any item in `wallets` is not a str or TreasuryWallet.
78
+
79
+ Examples:
80
+ .. code-block:: python
81
+
82
+ # Create a synchronous Treasury
83
+ treasury = Treasury(
84
+ wallets=["0xAbc123...", TreasuryWallet("0xDef456...", start_block=1000)],
85
+ sort_rules=Path("/path/to/rules"),
86
+ start_block=500,
87
+ label="DAO Treasury",
88
+ asynchronous=False
89
+ )
40
90
 
91
+ # Create an asynchronous Treasury
92
+ treasury_async = Treasury(
93
+ wallets=["0xAbc123..."],
94
+ asynchronous=True
95
+ )
41
96
  """
42
97
  global TREASURY
43
98
  if TREASURY is not None:
44
- raise RuntimeError(f"You can only initialize one {type(self).__name__} object")
99
+ raise RuntimeError(
100
+ f"You can only initialize one {type(self).__name__} object"
101
+ )
45
102
  ASyncABC.__init__(self)
103
+
46
104
  self.wallets: Final[List[TreasuryWallet]] = []
47
105
  """The collection of wallets owned or controlled by the on-chain org"""
106
+
48
107
  for wallet in wallets:
49
108
  if isinstance(wallet, str):
50
109
  self.wallets.append(TreasuryWallet(wallet)) # type: ignore [type-arg]
@@ -54,21 +113,27 @@ class Treasury(a_sync.ASyncGenericBase): # type: ignore [misc]
54
113
  raise TypeError(
55
114
  f"`wallets` can only contain: {wallet_types} You passed {wallet}"
56
115
  )
57
-
116
+
58
117
  self.sort_rules: Final = Rules(sort_rules) if sort_rules else None
59
118
 
60
119
  self.portfolio: Final = ExportablePortfolio(
61
120
  addresses=(
62
- wallet.address if isinstance(wallet, TreasuryWallet) else wallet
121
+ wallet.address
63
122
  for wallet in self.wallets
123
+ if wallet.networks is None or CHAINID in wallet.networks
64
124
  ),
65
125
  start_block=start_block,
66
126
  label=label,
67
127
  load_prices=True,
128
+ custom_buckets=custom_buckets,
68
129
  asynchronous=asynchronous,
69
130
  )
70
131
  """An eth_portfolio.Portfolio object used for exporting tx and balance history"""
71
132
 
133
+ self._llamapay: Final = (
134
+ llamapay.LlamaPayProcessor() if CHAINID in llamapay.networks else None
135
+ )
136
+
72
137
  self.asynchronous: Final = asynchronous
73
138
  """A boolean flag indicating whether the API for this `Treasury` object is sync or async by default"""
74
139
 
@@ -81,20 +146,48 @@ class Treasury(a_sync.ASyncGenericBase): # type: ignore [misc]
81
146
  def txs(self) -> a_sync.ASyncIterator[LedgerEntry]:
82
147
  return self.portfolio.ledger.all_entries
83
148
 
84
- async def populate_db(self, start_block: BlockNumber, end_block: BlockNumber) -> None:
85
- """returns: number of new txs"""
86
- # TODO: implement this
87
- # NOTE: ensure stream loader task has been started
88
- #global _streams_task
89
- #if _streams_task is None:
90
- # _streams_task = create_task(streams._get_coro())
91
- futs = []
92
- async for entry in self.portfolio.ledger[start_block:end_block]:
93
- if not entry.value:
94
- # TODO: add an arg in eth-port to skip 0 value
95
- logger.debug("zero value transfer, skipping %s", entry)
96
- continue
97
- futs.append(create_task(TreasuryTx.insert(entry)))
98
-
99
- if futs:
100
- await tqdm_asyncio.gather(*futs, desc="Insert Txs to Postgres")
149
+ async def _insert_txs(
150
+ self, start_block: BlockNumber, end_block: BlockNumber
151
+ ) -> None:
152
+ """Populate the database with treasury transactions in a block range.
153
+
154
+ Streams ledger entries from `start_block` up to (but not including)
155
+ `end_block`, skips zero-value transfers, and inserts each remaining entry
156
+ into the DB via :meth:`dao_treasury.db.TreasuryTx.insert`. Uses
157
+ :class:`tqdm.asyncio.tqdm_asyncio` to display progress.
158
+
159
+ Args:
160
+ start_block: First block number to include (inclusive).
161
+ end_block: Last block number to include (exclusive).
162
+
163
+ Examples:
164
+ >>> # Insert transactions from block 0 to 10000
165
+ >>> await treasury._insert_txs(0, 10000)
166
+ """
167
+ with db_session:
168
+ futs = []
169
+ async for entry in self.portfolio.ledger[start_block:end_block]:
170
+ if not entry.value:
171
+ # TODO: add an arg in eth-port to skip 0 value
172
+ logger.debug("zero value transfer, skipping %s", entry)
173
+ continue
174
+ futs.append(create_task(TreasuryTx.insert(entry)))
175
+ if futs:
176
+ await tqdm_asyncio.gather(*futs, desc="Insert Txs to Postgres")
177
+ logger.info(f"{len(futs)} transfers exported")
178
+
179
+ async def _process_streams(self) -> None:
180
+ if self._llamapay is not None:
181
+ await self._llamapay.process_streams(run_forever=True)
182
+
183
+ async def populate_db(
184
+ self, start_block: BlockNumber, end_block: BlockNumber
185
+ ) -> None:
186
+ """
187
+ Populate the database with treasury transactions and streams in parallel.
188
+ """
189
+ tasks = [self._insert_txs(start_block, end_block)]
190
+ if self._llamapay:
191
+ tasks.append(self._process_streams())
192
+ await gather(*tasks)
193
+ logger.info("db connection closed")
Binary file