dao-treasury 0.0.17__cp312-cp312-win32.whl → 0.0.61__cp312-cp312-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.
Potentially problematic release.
This version of dao-treasury might be problematic. Click here for more details.
- dao_treasury/.grafana/provisioning/dashboards/breakdowns/Expenses.json +526 -0
- dao_treasury/.grafana/provisioning/dashboards/breakdowns/Revenue.json +526 -0
- dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +76 -2
- dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +225 -0
- dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +13 -17
- dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +167 -19
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +876 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +645 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Current Treasury Assets.json +593 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Historical Treasury Balances.json +2999 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +513 -0
- dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
- dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
- dao_treasury/__init__.py +24 -0
- dao_treasury/_docker.cp312-win32.pyd +0 -0
- dao_treasury/_docker.py +48 -23
- dao_treasury/_nicknames.cp312-win32.pyd +0 -0
- dao_treasury/_nicknames.py +32 -0
- dao_treasury/_wallet.cp312-win32.pyd +0 -0
- dao_treasury/_wallet.py +162 -10
- dao_treasury/constants.cp312-win32.pyd +0 -0
- dao_treasury/constants.py +39 -0
- dao_treasury/db.py +429 -57
- dao_treasury/docker-compose.yaml +6 -5
- dao_treasury/main.py +102 -13
- dao_treasury/sorting/__init__.cp312-win32.pyd +0 -0
- dao_treasury/sorting/__init__.py +181 -105
- dao_treasury/sorting/_matchers.cp312-win32.pyd +0 -0
- dao_treasury/sorting/_rules.cp312-win32.pyd +0 -0
- dao_treasury/sorting/_rules.py +1 -3
- dao_treasury/sorting/factory.cp312-win32.pyd +0 -0
- dao_treasury/sorting/factory.py +2 -6
- dao_treasury/sorting/rule.cp312-win32.pyd +0 -0
- dao_treasury/sorting/rule.py +16 -13
- dao_treasury/sorting/rules/__init__.cp312-win32.pyd +0 -0
- dao_treasury/sorting/rules/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/__init__.cp312-win32.pyd +0 -0
- dao_treasury/sorting/rules/ignore/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/llamapay.cp312-win32.pyd +0 -0
- dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
- dao_treasury/streams/__init__.cp312-win32.pyd +0 -0
- dao_treasury/streams/__init__.py +0 -0
- dao_treasury/streams/llamapay.cp312-win32.pyd +0 -0
- dao_treasury/streams/llamapay.py +388 -0
- dao_treasury/treasury.py +75 -28
- dao_treasury/types.cp312-win32.pyd +0 -0
- dao_treasury-0.0.61.dist-info/METADATA +120 -0
- dao_treasury-0.0.61.dist-info/RECORD +54 -0
- dao_treasury-0.0.61.dist-info/top_level.txt +2 -0
- dao_treasury__mypyc.cp312-win32.pyd +0 -0
- 52b51d40e96d4333695d__mypyc.cp312-win32.pyd +0 -0
- dao_treasury/.grafana/provisioning/datasources/sqlite.yaml +0 -10
- dao_treasury-0.0.17.dist-info/METADATA +0 -36
- dao_treasury-0.0.17.dist-info/RECORD +0 -30
- dao_treasury-0.0.17.dist-info/top_level.txt +0 -2
- {dao_treasury-0.0.17.dist-info → dao_treasury-0.0.61.dist-info}/WHEEL +0 -0
dao_treasury/db.py
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
Database models and utilities for DAO treasury reporting.
|
|
4
4
|
|
|
5
5
|
This module defines Pony ORM entities for:
|
|
6
|
+
|
|
6
7
|
- Blockchain networks (:class:`Chain`)
|
|
7
8
|
- On-chain addresses (:class:`Address`)
|
|
8
9
|
- ERC-20 tokens and native coin placeholder (:class:`Token`)
|
|
9
10
|
- Hierarchical transaction grouping (:class:`TxGroup`)
|
|
10
11
|
- Treasury transaction records (:class:`TreasuryTx`)
|
|
12
|
+
- Streams and StreamedFunds for streaming payments
|
|
11
13
|
|
|
12
14
|
It also provides helper functions for inserting ledger entries,
|
|
13
15
|
resolving integrity conflicts, caching transaction receipts,
|
|
@@ -16,27 +18,44 @@ and creating SQL views for reporting.
|
|
|
16
18
|
|
|
17
19
|
import typing
|
|
18
20
|
from asyncio import Semaphore
|
|
21
|
+
from collections import OrderedDict
|
|
19
22
|
from decimal import Decimal, InvalidOperation
|
|
20
23
|
from functools import lru_cache
|
|
21
24
|
from logging import getLogger
|
|
22
25
|
from os import path
|
|
23
26
|
from pathlib import Path
|
|
24
|
-
from typing import
|
|
27
|
+
from typing import (
|
|
28
|
+
TYPE_CHECKING,
|
|
29
|
+
Any,
|
|
30
|
+
Coroutine,
|
|
31
|
+
Dict,
|
|
32
|
+
Final,
|
|
33
|
+
Literal,
|
|
34
|
+
Tuple,
|
|
35
|
+
Union,
|
|
36
|
+
final,
|
|
37
|
+
overload,
|
|
38
|
+
)
|
|
39
|
+
from datetime import date, datetime, time, timezone
|
|
25
40
|
|
|
41
|
+
import eth_portfolio
|
|
26
42
|
from a_sync import AsyncThreadPoolExecutor
|
|
27
43
|
from brownie import chain
|
|
28
44
|
from brownie.convert.datatypes import HexString
|
|
45
|
+
from brownie.exceptions import EventLookupError
|
|
29
46
|
from brownie.network.event import EventDict, _EventItem
|
|
30
47
|
from brownie.network.transaction import TransactionReceipt
|
|
31
|
-
from eth_typing import ChecksumAddress, HexAddress
|
|
32
48
|
from eth_portfolio.structs import (
|
|
33
49
|
InternalTransfer,
|
|
34
50
|
LedgerEntry,
|
|
35
51
|
TokenTransfer,
|
|
36
52
|
Transaction,
|
|
37
53
|
)
|
|
54
|
+
from eth_retry import auto_retry
|
|
55
|
+
from eth_typing import ChecksumAddress, HexAddress, HexStr
|
|
38
56
|
from pony.orm import (
|
|
39
57
|
Database,
|
|
58
|
+
InterfaceError,
|
|
40
59
|
Optional,
|
|
41
60
|
PrimaryKey,
|
|
42
61
|
Required,
|
|
@@ -46,15 +65,20 @@ from pony.orm import (
|
|
|
46
65
|
composite_key,
|
|
47
66
|
composite_index,
|
|
48
67
|
db_session,
|
|
68
|
+
select,
|
|
49
69
|
)
|
|
50
70
|
from y import EEE_ADDRESS, Contract, Network, convert, get_block_timestamp_async
|
|
51
|
-
from y.
|
|
71
|
+
from y._db.decorators import retry_locked
|
|
52
72
|
from y.contracts import _get_code
|
|
53
73
|
from y.exceptions import ContractNotVerified
|
|
54
74
|
|
|
75
|
+
from dao_treasury.constants import CHAINID
|
|
55
76
|
from dao_treasury.types import TxGroupDbid, TxGroupName
|
|
56
77
|
|
|
57
78
|
|
|
79
|
+
EventItem = _EventItem[_EventItem[OrderedDict[str, Any]]]
|
|
80
|
+
|
|
81
|
+
|
|
58
82
|
SQLITE_DIR = Path(path.expanduser("~")) / ".dao-treasury"
|
|
59
83
|
"""Path to the directory in the user's home where the DAO treasury SQLite database is stored."""
|
|
60
84
|
|
|
@@ -62,8 +86,11 @@ SQLITE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
62
86
|
|
|
63
87
|
|
|
64
88
|
_INSERT_THREAD = AsyncThreadPoolExecutor(1)
|
|
89
|
+
_SORT_THREAD = AsyncThreadPoolExecutor(1)
|
|
90
|
+
_EVENTS_THREADS = AsyncThreadPoolExecutor(16)
|
|
65
91
|
_SORT_SEMAPHORE = Semaphore(50)
|
|
66
92
|
|
|
93
|
+
_UTC = timezone.utc
|
|
67
94
|
|
|
68
95
|
db = Database()
|
|
69
96
|
|
|
@@ -112,13 +139,13 @@ class Chain(DbEntity):
|
|
|
112
139
|
chainid = Required(int, unique=True)
|
|
113
140
|
"""Numeric chain ID matching the connected RPC via :data:`~y.constants.CHAINID`."""
|
|
114
141
|
|
|
115
|
-
addresses = Set("Address", reverse="chain")
|
|
142
|
+
addresses = Set("Address", reverse="chain", lazy=True)
|
|
116
143
|
"""Relationship to address records on this chain."""
|
|
117
144
|
|
|
118
|
-
tokens = Set("Token", reverse="chain")
|
|
145
|
+
tokens = Set("Token", reverse="chain", lazy=True)
|
|
119
146
|
"""Relationship to token records on this chain."""
|
|
120
147
|
|
|
121
|
-
treasury_txs = Set("TreasuryTx")
|
|
148
|
+
treasury_txs = Set("TreasuryTx", lazy=True)
|
|
122
149
|
"""Relationship to treasury transactions on this chain."""
|
|
123
150
|
|
|
124
151
|
@staticmethod
|
|
@@ -177,7 +204,7 @@ class Address(DbEntity):
|
|
|
177
204
|
address_id = PrimaryKey(int, auto=True)
|
|
178
205
|
"""Auto-incremented primary key for the addresses table."""
|
|
179
206
|
|
|
180
|
-
chain = Required(Chain, reverse="addresses")
|
|
207
|
+
chain = Required(Chain, reverse="addresses", lazy=True)
|
|
181
208
|
"""Reference to the chain on which this address resides."""
|
|
182
209
|
|
|
183
210
|
address = Required(str, index=True)
|
|
@@ -186,7 +213,7 @@ class Address(DbEntity):
|
|
|
186
213
|
nickname = Optional(str)
|
|
187
214
|
"""Optional human-readable label (e.g., contract name or token name)."""
|
|
188
215
|
|
|
189
|
-
is_contract = Required(bool, index=True)
|
|
216
|
+
is_contract = Required(bool, index=True, lazy=True)
|
|
190
217
|
"""Flag indicating whether the address is a smart contract."""
|
|
191
218
|
|
|
192
219
|
composite_key(address, chain)
|
|
@@ -197,29 +224,40 @@ class Address(DbEntity):
|
|
|
197
224
|
treasury_tx_from: Set["TreasuryTx"]
|
|
198
225
|
treasury_tx_to: Set["TreasuryTx"]
|
|
199
226
|
|
|
200
|
-
token = Optional("Token", index=True)
|
|
227
|
+
token = Optional("Token", index=True, lazy=True)
|
|
201
228
|
"""Optional back-reference to a Token if this address is one."""
|
|
202
|
-
# partners_tx = Set('PartnerHarvestEvent', reverse='wrapper')
|
|
229
|
+
# partners_tx = Set('PartnerHarvestEvent', reverse='wrapper', lazy=True)
|
|
203
230
|
|
|
204
|
-
treasury_tx_from = Set("TreasuryTx", reverse="from_address")
|
|
231
|
+
treasury_tx_from = Set("TreasuryTx", reverse="from_address", lazy=True)
|
|
205
232
|
"""Inverse relation for transactions sent from this address."""
|
|
206
233
|
|
|
207
|
-
treasury_tx_to = Set("TreasuryTx", reverse="to_address")
|
|
234
|
+
treasury_tx_to = Set("TreasuryTx", reverse="to_address", lazy=True)
|
|
208
235
|
"""Inverse relation for transactions sent to this address."""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
|
|
237
|
+
streams_from = Set("Stream", reverse="from_address", lazy=True)
|
|
238
|
+
streams_to = Set("Stream", reverse="to_address", lazy=True)
|
|
239
|
+
streams = Set("Stream", reverse="contract", lazy=True)
|
|
240
|
+
# vesting_escrows = Set("VestingEscrow", reverse="address", lazy=True)
|
|
241
|
+
# vests_received = Set("VestingEscrow", reverse="recipient", lazy=True)
|
|
242
|
+
# vests_funded = Set("VestingEscrow", reverse="funder", lazy=True)
|
|
243
|
+
|
|
244
|
+
def __eq__(self, other: Union["Address", ChecksumAddress, "Token"]) -> bool: # type: ignore [override]
|
|
217
245
|
if isinstance(other, str):
|
|
218
246
|
return CHAINID == self.chain.chainid and other == self.address
|
|
247
|
+
elif isinstance(other, Token):
|
|
248
|
+
return self.address_id == other.address.address_id
|
|
219
249
|
return super().__eq__(other)
|
|
220
250
|
|
|
221
251
|
__hash__ = DbEntity.__hash__
|
|
222
252
|
|
|
253
|
+
@property
|
|
254
|
+
def contract(self) -> Contract:
|
|
255
|
+
return Contract(self.address)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def contract_coro(self) -> Coroutine[Any, Any, Contract]:
|
|
259
|
+
return Contract.coroutine(self.address)
|
|
260
|
+
|
|
223
261
|
@staticmethod
|
|
224
262
|
@lru_cache(maxsize=None)
|
|
225
263
|
def get_dbid(address: HexAddress) -> int:
|
|
@@ -256,11 +294,13 @@ class Address(DbEntity):
|
|
|
256
294
|
if entity := Address.get(chain=chain_dbid, address=checksum_address):
|
|
257
295
|
return entity # type: ignore [no-any-return]
|
|
258
296
|
|
|
259
|
-
if _get_code(
|
|
297
|
+
if _get_code(checksum_address, None).hex().removeprefix("0x"):
|
|
260
298
|
try:
|
|
261
|
-
nickname =
|
|
299
|
+
nickname = (
|
|
300
|
+
f"Contract: {Contract(checksum_address)._build['contractName']}"
|
|
301
|
+
)
|
|
262
302
|
except ContractNotVerified:
|
|
263
|
-
nickname = f"Non-Verified Contract: {
|
|
303
|
+
nickname = f"Non-Verified Contract: {checksum_address}"
|
|
264
304
|
|
|
265
305
|
entity = Address(
|
|
266
306
|
chain=chain_dbid,
|
|
@@ -278,9 +318,34 @@ class Address(DbEntity):
|
|
|
278
318
|
)
|
|
279
319
|
|
|
280
320
|
commit()
|
|
281
|
-
|
|
282
321
|
return entity # type: ignore [no-any-return]
|
|
283
322
|
|
|
323
|
+
@staticmethod
|
|
324
|
+
def set_nickname(address: HexAddress, nickname: str) -> None:
|
|
325
|
+
if not nickname:
|
|
326
|
+
raise ValueError("You must provide an actual string")
|
|
327
|
+
with db_session:
|
|
328
|
+
entity = Address.get_or_insert(address)
|
|
329
|
+
if entity.nickname == nickname:
|
|
330
|
+
return
|
|
331
|
+
if entity.nickname:
|
|
332
|
+
old = entity.nickname
|
|
333
|
+
entity.nickname = nickname
|
|
334
|
+
commit()
|
|
335
|
+
logger.info(
|
|
336
|
+
"%s nickname changed from %s to %s", entity.address, old, nickname
|
|
337
|
+
)
|
|
338
|
+
else:
|
|
339
|
+
entity.nickname = nickname
|
|
340
|
+
commit()
|
|
341
|
+
logger.info("%s nickname set to %s", entity.address, nickname)
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def set_nicknames(nicknames: Dict[HexAddress, str]) -> None:
|
|
345
|
+
with db_session:
|
|
346
|
+
for address, nickname in nicknames.items():
|
|
347
|
+
Address.set_nickname(address, nickname)
|
|
348
|
+
|
|
284
349
|
|
|
285
350
|
UNI_V3_POS: Final = {
|
|
286
351
|
Network.Mainnet: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
|
|
@@ -325,34 +390,37 @@ class Token(DbEntity):
|
|
|
325
390
|
token_id = PrimaryKey(int, auto=True)
|
|
326
391
|
"""Auto-incremented primary key for the tokens table."""
|
|
327
392
|
|
|
328
|
-
chain = Required(Chain, index=True)
|
|
393
|
+
chain = Required(Chain, index=True, lazy=True)
|
|
329
394
|
"""Foreign key linking to :class:`~dao_treasury.db.Chain`."""
|
|
330
395
|
|
|
331
|
-
symbol = Required(str, index=True)
|
|
396
|
+
symbol = Required(str, index=True, lazy=True)
|
|
332
397
|
"""Short ticker symbol for the token."""
|
|
333
398
|
|
|
334
|
-
name = Required(str)
|
|
399
|
+
name = Required(str, lazy=True)
|
|
335
400
|
"""Full human-readable name of the token."""
|
|
336
401
|
|
|
337
|
-
decimals = Required(int)
|
|
402
|
+
decimals = Required(int, lazy=True)
|
|
338
403
|
"""Number of decimals used for value scaling."""
|
|
339
404
|
|
|
340
405
|
if TYPE_CHECKING:
|
|
341
406
|
treasury_tx: Set["TreasuryTx"]
|
|
342
407
|
|
|
343
|
-
treasury_tx = Set("TreasuryTx", reverse="token")
|
|
408
|
+
treasury_tx = Set("TreasuryTx", reverse="token", lazy=True)
|
|
344
409
|
"""Inverse relation for treasury transactions involving this token."""
|
|
345
|
-
# partner_harvest_event = Set('PartnerHarvestEvent', reverse="vault")
|
|
410
|
+
# partner_harvest_event = Set('PartnerHarvestEvent', reverse="vault", lazy=True)
|
|
346
411
|
|
|
347
412
|
address = Required(Address, column="address_id")
|
|
348
413
|
"""Foreign key to the address record for this token contract."""
|
|
349
|
-
# streams = Set('Stream', reverse="token")
|
|
350
|
-
# vesting_escrows = Set("VestingEscrow", reverse="token")
|
|
351
414
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
415
|
+
streams = Set("Stream", reverse="token", lazy=True)
|
|
416
|
+
# vesting_escrows = Set("VestingEscrow", reverse="token", lazy=True)
|
|
417
|
+
|
|
418
|
+
def __eq__(self, other: Union["Token", Address, ChecksumAddress]) -> bool: # type: ignore [override]
|
|
419
|
+
if isinstance(other, str):
|
|
420
|
+
return self.address == other
|
|
421
|
+
elif isinstance(other, Address):
|
|
422
|
+
return self.address.address_id == other.address_id
|
|
423
|
+
return super().__eq__(other)
|
|
356
424
|
|
|
357
425
|
__hash__ = DbEntity.__hash__
|
|
358
426
|
|
|
@@ -360,6 +428,10 @@ class Token(DbEntity):
|
|
|
360
428
|
def contract(self) -> Contract:
|
|
361
429
|
return Contract(self.address.address)
|
|
362
430
|
|
|
431
|
+
@property
|
|
432
|
+
def contract_coro(self) -> Coroutine[Any, Any, Contract]:
|
|
433
|
+
return Contract.coroutine(self.address.address)
|
|
434
|
+
|
|
363
435
|
@property
|
|
364
436
|
def scale(self) -> int:
|
|
365
437
|
"""Base for division according to `decimals`, e.g., `10**decimals`.
|
|
@@ -488,7 +560,7 @@ class TxGroup(DbEntity):
|
|
|
488
560
|
name = Required(str)
|
|
489
561
|
"""Name of the grouping category, e.g., 'Revenue', 'Expenses'."""
|
|
490
562
|
|
|
491
|
-
treasury_tx = Set("TreasuryTx", reverse="txgroup")
|
|
563
|
+
treasury_tx = Set("TreasuryTx", reverse="txgroup", lazy=True)
|
|
492
564
|
"""Inverse relation for treasury transactions assigned to this group."""
|
|
493
565
|
|
|
494
566
|
parent_txgroup = Optional("TxGroup", reverse="child_txgroups")
|
|
@@ -496,11 +568,13 @@ class TxGroup(DbEntity):
|
|
|
496
568
|
|
|
497
569
|
composite_key(name, parent_txgroup)
|
|
498
570
|
|
|
499
|
-
child_txgroups = Set("TxGroup", reverse="parent_txgroup")
|
|
571
|
+
child_txgroups = Set("TxGroup", reverse="parent_txgroup", lazy=True)
|
|
500
572
|
"""Set of nested child groups."""
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
573
|
+
|
|
574
|
+
streams = Set("Stream", reverse="txgroup", lazy=True)
|
|
575
|
+
|
|
576
|
+
# TODO: implement this
|
|
577
|
+
# vesting_escrows = Set("VestingEscrow", reverse="txgroup", lazy=True)
|
|
504
578
|
|
|
505
579
|
@property
|
|
506
580
|
def fullname(self) -> str:
|
|
@@ -576,7 +650,7 @@ class TxGroup(DbEntity):
|
|
|
576
650
|
return txgroup # type: ignore [no-any-return]
|
|
577
651
|
|
|
578
652
|
|
|
579
|
-
@lru_cache(
|
|
653
|
+
@lru_cache(500)
|
|
580
654
|
def get_transaction(txhash: str) -> TransactionReceipt:
|
|
581
655
|
"""Fetch and cache a transaction receipt from the connected chain.
|
|
582
656
|
|
|
@@ -682,14 +756,32 @@ class TreasuryTx(DbEntity):
|
|
|
682
756
|
"""Decoded event logs for this transaction."""
|
|
683
757
|
return self._transaction.events
|
|
684
758
|
|
|
685
|
-
def
|
|
759
|
+
async def events_async(self) -> EventDict:
|
|
760
|
+
"""Asynchronously fetch decoded event logs for this transaction."""
|
|
761
|
+
tx = self._transaction
|
|
762
|
+
events = tx._events
|
|
763
|
+
if events is None:
|
|
764
|
+
events = await _EVENTS_THREADS.run(getattr, tx, "events")
|
|
765
|
+
return events
|
|
766
|
+
|
|
767
|
+
@overload
|
|
768
|
+
def get_events(
|
|
769
|
+
self, event_name: str, sync: Literal[False]
|
|
770
|
+
) -> Coroutine[Any, Any, EventItem]: ...
|
|
771
|
+
@overload
|
|
772
|
+
def get_events(self, event_name: str, sync: bool = True) -> EventItem: ...
|
|
773
|
+
def get_events(self, event_name: str, sync: bool = True) -> EventItem:
|
|
774
|
+
if not sync:
|
|
775
|
+
return _EVENTS_THREADS.run(self.get_events, event_name)
|
|
686
776
|
try:
|
|
687
777
|
return self.events[event_name]
|
|
778
|
+
except EventLookupError:
|
|
779
|
+
pass
|
|
688
780
|
except KeyError as e:
|
|
689
781
|
# This happens sometimes due to a busted abi and hopefully shouldnt impact you
|
|
690
|
-
if str(e)
|
|
691
|
-
|
|
692
|
-
|
|
782
|
+
if str(e) != "'components'":
|
|
783
|
+
raise
|
|
784
|
+
return _EventItem(event_name, None, [], ())
|
|
693
785
|
|
|
694
786
|
@property
|
|
695
787
|
def _transaction(self) -> TransactionReceipt:
|
|
@@ -697,6 +789,7 @@ class TreasuryTx(DbEntity):
|
|
|
697
789
|
return get_transaction(self.hash)
|
|
698
790
|
|
|
699
791
|
@staticmethod
|
|
792
|
+
@auto_retry
|
|
700
793
|
async def insert(entry: LedgerEntry) -> None:
|
|
701
794
|
"""Asynchronously insert and sort a ledger entry.
|
|
702
795
|
|
|
@@ -717,8 +810,16 @@ class TreasuryTx(DbEntity):
|
|
|
717
810
|
async with _SORT_SEMAPHORE:
|
|
718
811
|
from dao_treasury.sorting import sort_advanced
|
|
719
812
|
|
|
720
|
-
|
|
813
|
+
try:
|
|
721
814
|
await sort_advanced(TreasuryTx[txid])
|
|
815
|
+
except Exception as e:
|
|
816
|
+
e.args = *e.args, entry
|
|
817
|
+
raise
|
|
818
|
+
|
|
819
|
+
async def _set_txgroup(self, txgroup_dbid: TxGroupDbid) -> None:
|
|
820
|
+
await _SORT_THREAD.run(
|
|
821
|
+
TreasuryTx.__set_txgroup, self.treasury_tx_id, txgroup_dbid
|
|
822
|
+
)
|
|
722
823
|
|
|
723
824
|
@staticmethod
|
|
724
825
|
def __insert(entry: LedgerEntry, ts: int) -> typing.Optional[int]:
|
|
@@ -780,9 +881,74 @@ class TreasuryTx(DbEntity):
|
|
|
780
881
|
gas_price=gas_price,
|
|
781
882
|
txgroup=txgroup_dbid,
|
|
782
883
|
)
|
|
884
|
+
# we must commit here or else dbid below will be `None`.
|
|
885
|
+
commit()
|
|
783
886
|
dbid = entity.treasury_tx_id
|
|
887
|
+
except InterfaceError as e:
|
|
888
|
+
raise ValueError(
|
|
889
|
+
e,
|
|
890
|
+
{
|
|
891
|
+
"chain": Chain.get_dbid(CHAINID),
|
|
892
|
+
"block": entry.block_number,
|
|
893
|
+
"timestamp": ts,
|
|
894
|
+
"hash": entry.hash.hex(),
|
|
895
|
+
"log_index": log_index,
|
|
896
|
+
"from_address": from_address,
|
|
897
|
+
"to_address": to_address,
|
|
898
|
+
"token": token,
|
|
899
|
+
"amount": entry.value,
|
|
900
|
+
"price": entry.price,
|
|
901
|
+
"value_usd": entry.value_usd,
|
|
902
|
+
# TODO: nuke db and add this column
|
|
903
|
+
# gas = gas,
|
|
904
|
+
"gas_used": gas_used,
|
|
905
|
+
"gas_price": gas_price,
|
|
906
|
+
"txgroup": txgroup_dbid,
|
|
907
|
+
},
|
|
908
|
+
) from e
|
|
784
909
|
except InvalidOperation as e:
|
|
785
|
-
|
|
910
|
+
with db_session:
|
|
911
|
+
from_address_entity = Address[from_address]
|
|
912
|
+
to_address_entity = Address[to_address]
|
|
913
|
+
token_entity = Token[token]
|
|
914
|
+
logger.error(e)
|
|
915
|
+
logger.error(
|
|
916
|
+
{
|
|
917
|
+
"chain": Chain.get_dbid(CHAINID),
|
|
918
|
+
"block": entry.block_number,
|
|
919
|
+
"timestamp": ts,
|
|
920
|
+
"hash": entry.hash.hex(),
|
|
921
|
+
"log_index": log_index,
|
|
922
|
+
"from_address": {
|
|
923
|
+
"dbid": from_address,
|
|
924
|
+
"address": from_address_entity.address,
|
|
925
|
+
"nickname": from_address_entity.nickname,
|
|
926
|
+
},
|
|
927
|
+
"to_address": {
|
|
928
|
+
"dbid": to_address,
|
|
929
|
+
"address": to_address_entity.address,
|
|
930
|
+
"nickname": to_address_entity.nickname,
|
|
931
|
+
},
|
|
932
|
+
"token": {
|
|
933
|
+
"dbid": token,
|
|
934
|
+
"address": token_entity.address.address,
|
|
935
|
+
"name": token_entity.name,
|
|
936
|
+
"symbol": token_entity.symbol,
|
|
937
|
+
"decimals": token_entity.decimals,
|
|
938
|
+
},
|
|
939
|
+
"amount": entry.value,
|
|
940
|
+
"price": entry.price,
|
|
941
|
+
"value_usd": entry.value_usd,
|
|
942
|
+
# TODO: nuke db and add this column
|
|
943
|
+
# gas = gas,
|
|
944
|
+
"gas_used": gas_used,
|
|
945
|
+
"gas_price": gas_price,
|
|
946
|
+
"txgroup": {
|
|
947
|
+
"dbid": txgroup_dbid,
|
|
948
|
+
"fullname": TxGroup[txgroup_dbid].fullname,
|
|
949
|
+
},
|
|
950
|
+
}
|
|
951
|
+
)
|
|
786
952
|
return None
|
|
787
953
|
except TransactionIntegrityError as e:
|
|
788
954
|
return _validate_integrity_error(entry, log_index)
|
|
@@ -800,6 +966,158 @@ class TreasuryTx(DbEntity):
|
|
|
800
966
|
return None
|
|
801
967
|
return dbid # type: ignore [no-any-return]
|
|
802
968
|
|
|
969
|
+
@staticmethod
|
|
970
|
+
@retry_locked
|
|
971
|
+
def __set_txgroup(treasury_tx_dbid: int, txgroup_dbid: TxGroupDbid) -> None:
|
|
972
|
+
with db_session:
|
|
973
|
+
TreasuryTx[treasury_tx_dbid].txgroup = txgroup_dbid
|
|
974
|
+
commit()
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
_stream_metadata_cache: Final[Dict[HexStr, Tuple[ChecksumAddress, date]]] = {}
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
class Stream(DbEntity):
|
|
981
|
+
_table_ = "streams"
|
|
982
|
+
stream_id = PrimaryKey(str)
|
|
983
|
+
|
|
984
|
+
contract = Required("Address", reverse="streams")
|
|
985
|
+
start_block = Required(int)
|
|
986
|
+
end_block = Optional(int)
|
|
987
|
+
token = Required("Token", reverse="streams", index=True)
|
|
988
|
+
from_address = Required("Address", reverse="streams_from")
|
|
989
|
+
to_address = Required("Address", reverse="streams_to")
|
|
990
|
+
reason = Optional(str)
|
|
991
|
+
amount_per_second = Required(Decimal, 38, 1)
|
|
992
|
+
status = Required(str, default="Active")
|
|
993
|
+
txgroup = Optional("TxGroup", reverse="streams")
|
|
994
|
+
|
|
995
|
+
streamed_funds = Set("StreamedFunds", lazy=True)
|
|
996
|
+
|
|
997
|
+
scale = 10**20
|
|
998
|
+
|
|
999
|
+
@property
|
|
1000
|
+
def is_alive(self) -> bool:
|
|
1001
|
+
if self.end_block is None:
|
|
1002
|
+
assert self.status in ["Active", "Paused"]
|
|
1003
|
+
return self.status == "Active"
|
|
1004
|
+
assert self.status == "Stopped"
|
|
1005
|
+
return False
|
|
1006
|
+
|
|
1007
|
+
@property
|
|
1008
|
+
def amount_per_minute(self) -> int:
|
|
1009
|
+
return self.amount_per_second * 60
|
|
1010
|
+
|
|
1011
|
+
@property
|
|
1012
|
+
def amount_per_hour(self) -> int:
|
|
1013
|
+
return self.amount_per_minute * 60
|
|
1014
|
+
|
|
1015
|
+
@property
|
|
1016
|
+
def amount_per_day(self) -> int:
|
|
1017
|
+
return self.amount_per_hour * 24
|
|
1018
|
+
|
|
1019
|
+
@staticmethod
|
|
1020
|
+
def check_closed(stream_id: HexStr) -> bool:
|
|
1021
|
+
with db_session:
|
|
1022
|
+
return any(sf.is_last_day for sf in Stream[stream_id].streamed_funds)
|
|
1023
|
+
|
|
1024
|
+
@staticmethod
|
|
1025
|
+
def _get_start_and_end(stream_dbid: HexStr) -> Tuple[datetime, datetime]:
|
|
1026
|
+
with db_session:
|
|
1027
|
+
stream = Stream[stream_dbid]
|
|
1028
|
+
start_date, end = stream.start_date, datetime.now(_UTC)
|
|
1029
|
+
# convert start to datetime
|
|
1030
|
+
start = datetime.combine(start_date, time(tzinfo=_UTC), tzinfo=_UTC)
|
|
1031
|
+
if stream.end_block:
|
|
1032
|
+
end = datetime.fromtimestamp(chain[stream.end_block].timestamp, tz=_UTC)
|
|
1033
|
+
return start, end
|
|
1034
|
+
|
|
1035
|
+
def stop_stream(self, block: int) -> None:
|
|
1036
|
+
self.end_block = block
|
|
1037
|
+
self.status = "Stopped"
|
|
1038
|
+
|
|
1039
|
+
def pause(self) -> None:
|
|
1040
|
+
self.status = "Paused"
|
|
1041
|
+
|
|
1042
|
+
@staticmethod
|
|
1043
|
+
def _get_token_and_start_date(stream_id: HexStr) -> Tuple[ChecksumAddress, date]:
|
|
1044
|
+
try:
|
|
1045
|
+
return _stream_metadata_cache[stream_id]
|
|
1046
|
+
except KeyError:
|
|
1047
|
+
with db_session:
|
|
1048
|
+
stream = Stream[stream_id]
|
|
1049
|
+
token = stream.token.address.address
|
|
1050
|
+
start_date = stream.start_date
|
|
1051
|
+
_stream_metadata_cache[stream_id] = token, start_date
|
|
1052
|
+
return token, start_date
|
|
1053
|
+
|
|
1054
|
+
@property
|
|
1055
|
+
def stream_contract(self) -> Contract:
|
|
1056
|
+
return Contract(self.contract.address)
|
|
1057
|
+
|
|
1058
|
+
@property
|
|
1059
|
+
def start_date(self) -> date:
|
|
1060
|
+
return datetime.fromtimestamp(chain[self.start_block].timestamp).date()
|
|
1061
|
+
|
|
1062
|
+
async def amount_withdrawable(self, block: int) -> int:
|
|
1063
|
+
return await self.stream_contract.withdrawable.coroutine(
|
|
1064
|
+
self.from_address.address,
|
|
1065
|
+
self.to_address.address,
|
|
1066
|
+
int(self.amount_per_second),
|
|
1067
|
+
block_identifier=block,
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
def print(self) -> None:
|
|
1071
|
+
symbol = self.token.symbol
|
|
1072
|
+
print(f"{symbol} per second: {self.amount_per_second / self.scale}")
|
|
1073
|
+
print(f"{symbol} per day: {self.amount_per_day / self.scale}")
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
class StreamedFunds(DbEntity):
|
|
1077
|
+
"""Each object represents one calendar day of tokens streamed for a particular stream."""
|
|
1078
|
+
|
|
1079
|
+
_table_ = "streamed_funds"
|
|
1080
|
+
|
|
1081
|
+
date = Required(date)
|
|
1082
|
+
stream = Required(Stream, reverse="streamed_funds")
|
|
1083
|
+
PrimaryKey(stream, date)
|
|
1084
|
+
|
|
1085
|
+
amount = Required(Decimal, 38, 18)
|
|
1086
|
+
price = Required(Decimal, 38, 18)
|
|
1087
|
+
value_usd = Required(Decimal, 38, 18)
|
|
1088
|
+
seconds_active = Required(int)
|
|
1089
|
+
is_last_day = Required(bool)
|
|
1090
|
+
|
|
1091
|
+
@db_session
|
|
1092
|
+
def get_entity(stream_id: str, date: datetime) -> "StreamedFunds":
|
|
1093
|
+
stream = Stream[stream_id]
|
|
1094
|
+
return StreamedFunds.get(date=date, stream=stream)
|
|
1095
|
+
|
|
1096
|
+
@classmethod
|
|
1097
|
+
@db_session
|
|
1098
|
+
def create_entity(
|
|
1099
|
+
cls,
|
|
1100
|
+
stream_id: str,
|
|
1101
|
+
date: datetime,
|
|
1102
|
+
price: Decimal,
|
|
1103
|
+
seconds_active: int,
|
|
1104
|
+
is_last_day: bool,
|
|
1105
|
+
) -> "StreamedFunds":
|
|
1106
|
+
stream = Stream[stream_id]
|
|
1107
|
+
amount_streamed_today = round(
|
|
1108
|
+
stream.amount_per_second * seconds_active / stream.scale, 18
|
|
1109
|
+
)
|
|
1110
|
+
entity = StreamedFunds(
|
|
1111
|
+
date=date,
|
|
1112
|
+
stream=stream,
|
|
1113
|
+
amount=amount_streamed_today,
|
|
1114
|
+
price=round(price, 18),
|
|
1115
|
+
value_usd=round(amount_streamed_today * price, 18),
|
|
1116
|
+
seconds_active=seconds_active,
|
|
1117
|
+
is_last_day=is_last_day,
|
|
1118
|
+
)
|
|
1119
|
+
return entity
|
|
1120
|
+
|
|
803
1121
|
|
|
804
1122
|
db.bind(
|
|
805
1123
|
provider="sqlite", # TODO: let user choose postgres with server connection params
|
|
@@ -810,6 +1128,13 @@ db.bind(
|
|
|
810
1128
|
db.generate_mapping(create_tables=True)
|
|
811
1129
|
|
|
812
1130
|
|
|
1131
|
+
def _set_address_nicknames_for_tokens() -> None:
|
|
1132
|
+
"""Set address.nickname for addresses belonging to tokens."""
|
|
1133
|
+
for address in select(a for a in Address if a.token and not a.nickname):
|
|
1134
|
+
address.nickname = f"Token: {address.token.name}"
|
|
1135
|
+
db.commit()
|
|
1136
|
+
|
|
1137
|
+
|
|
813
1138
|
def create_stream_ledger_view() -> None:
|
|
814
1139
|
"""Create or replace the SQL view `stream_ledger` for streamed funds reporting.
|
|
815
1140
|
|
|
@@ -819,19 +1144,19 @@ def create_stream_ledger_view() -> None:
|
|
|
819
1144
|
Examples:
|
|
820
1145
|
>>> create_stream_ledger_view()
|
|
821
1146
|
"""
|
|
1147
|
+
db.execute("""DROP VIEW IF EXISTS stream_ledger;""")
|
|
822
1148
|
db.execute(
|
|
823
1149
|
"""
|
|
824
|
-
DROP VIEW IF EXISTS stream_ledger;
|
|
825
1150
|
create view stream_ledger as
|
|
826
1151
|
SELECT 'Mainnet' as chain_name,
|
|
827
|
-
cast(
|
|
1152
|
+
cast(strftime('%s', date || ' 00:00:00') as INTEGER) as timestamp,
|
|
828
1153
|
NULL as block,
|
|
829
1154
|
NULL as hash,
|
|
830
1155
|
NULL as log_index,
|
|
831
1156
|
symbol as token,
|
|
832
1157
|
d.address AS "from",
|
|
833
1158
|
d.nickname as from_nickname,
|
|
834
|
-
e.address
|
|
1159
|
+
e.address AS "to",
|
|
835
1160
|
e.nickname as to_nickname,
|
|
836
1161
|
amount,
|
|
837
1162
|
price,
|
|
@@ -850,6 +1175,31 @@ def create_stream_ledger_view() -> None:
|
|
|
850
1175
|
)
|
|
851
1176
|
|
|
852
1177
|
|
|
1178
|
+
def create_txgroup_hierarchy_view() -> None:
|
|
1179
|
+
"""Create or replace the SQL view `txgroup_hierarchy` for recursive txgroup hierarchy.
|
|
1180
|
+
|
|
1181
|
+
This view exposes txgroup_id, top_category, and parent_txgroup for all txgroups,
|
|
1182
|
+
matching the recursive CTE logic used in dashboards.
|
|
1183
|
+
"""
|
|
1184
|
+
db.execute("DROP VIEW IF EXISTS txgroup_hierarchy;")
|
|
1185
|
+
db.execute(
|
|
1186
|
+
"""
|
|
1187
|
+
CREATE VIEW txgroup_hierarchy AS
|
|
1188
|
+
WITH RECURSIVE group_hierarchy (txgroup_id, top_category, parent_txgroup) AS (
|
|
1189
|
+
SELECT txgroup_id, name AS top_category, parent_txgroup
|
|
1190
|
+
FROM txgroups
|
|
1191
|
+
WHERE parent_txgroup IS NULL
|
|
1192
|
+
UNION ALL
|
|
1193
|
+
SELECT child.txgroup_id, parent.top_category, child.parent_txgroup
|
|
1194
|
+
FROM txgroups AS child
|
|
1195
|
+
JOIN group_hierarchy AS parent
|
|
1196
|
+
ON child.parent_txgroup = parent.txgroup_id
|
|
1197
|
+
)
|
|
1198
|
+
SELECT * FROM group_hierarchy;
|
|
1199
|
+
"""
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
|
|
853
1203
|
def create_vesting_ledger_view() -> None:
|
|
854
1204
|
"""Create or replace the SQL view `vesting_ledger` for vesting escrow reporting.
|
|
855
1205
|
|
|
@@ -905,7 +1255,7 @@ def create_general_ledger_view() -> None:
|
|
|
905
1255
|
create VIEW general_ledger as
|
|
906
1256
|
select *
|
|
907
1257
|
from (
|
|
908
|
-
SELECT treasury_tx_id, b.chain_name,
|
|
1258
|
+
SELECT treasury_tx_id, b.chain_name, a.timestamp, a.block, a.hash, a.log_index, c.symbol AS token, d.address AS "from", d.nickname as from_nickname, e.address AS "to", e.nickname as to_nickname, a.amount, a.price, a.value_usd, f.name AS txgroup, g.name AS parent_txgroup, f.txgroup_id
|
|
909
1259
|
FROM treasury_txs a
|
|
910
1260
|
LEFT JOIN chains b ON a.chain = b.chain_dbid
|
|
911
1261
|
LEFT JOIN tokens c ON a.token_id = c.token_id
|
|
@@ -913,9 +1263,9 @@ def create_general_ledger_view() -> None:
|
|
|
913
1263
|
LEFT JOIN addresses e ON a."to" = e.address_id
|
|
914
1264
|
LEFT JOIN txgroups f ON a.txgroup_id = f.txgroup_id
|
|
915
1265
|
LEFT JOIN txgroups g ON f.parent_txgroup = g.txgroup_id
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1266
|
+
UNION
|
|
1267
|
+
SELECT -1, chain_name, timestamp, block, hash, log_index, token, "from", from_nickname, "to", to_nickname, amount, price, value_usd, txgroup, parent_txgroup, txgroup_id
|
|
1268
|
+
FROM stream_ledger
|
|
919
1269
|
--UNION
|
|
920
1270
|
--SELECT -1, *
|
|
921
1271
|
--FROM vesting_ledger
|
|
@@ -991,7 +1341,8 @@ def create_monthly_pnl_view() -> None:
|
|
|
991
1341
|
|
|
992
1342
|
|
|
993
1343
|
with db_session:
|
|
994
|
-
|
|
1344
|
+
create_stream_ledger_view()
|
|
1345
|
+
create_txgroup_hierarchy_view()
|
|
995
1346
|
# create_vesting_ledger_view()
|
|
996
1347
|
create_general_ledger_view()
|
|
997
1348
|
create_unsorted_txs_view()
|
|
@@ -1051,7 +1402,7 @@ def _validate_integrity_error(
|
|
|
1051
1402
|
existing_object.amount,
|
|
1052
1403
|
)
|
|
1053
1404
|
except AssertionError:
|
|
1054
|
-
logger.
|
|
1405
|
+
logger.debug(
|
|
1055
1406
|
"slight rounding error in value for TreasuryTx[%s] due to sqlite decimal handling",
|
|
1056
1407
|
existing_object.treasury_tx_id,
|
|
1057
1408
|
)
|
|
@@ -1076,3 +1427,24 @@ def _validate_integrity_error(
|
|
|
1076
1427
|
)
|
|
1077
1428
|
else None
|
|
1078
1429
|
)
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
def _drop_shitcoin_txs() -> None:
|
|
1433
|
+
"""
|
|
1434
|
+
Purge any shitcoin txs from the db.
|
|
1435
|
+
|
|
1436
|
+
These should not be frequent, and only occur if a user populated the db before a shitcoin was added to the SHITCOINS mapping.
|
|
1437
|
+
"""
|
|
1438
|
+
shitcoins = eth_portfolio.SHITCOINS[CHAINID]
|
|
1439
|
+
with db_session:
|
|
1440
|
+
shitcoin_txs = select(
|
|
1441
|
+
tx for tx in TreasuryTx if tx.token.address.address in shitcoins
|
|
1442
|
+
)
|
|
1443
|
+
if count := shitcoin_txs.count():
|
|
1444
|
+
logger.info(f"Purging {count} shitcoin txs from the database...")
|
|
1445
|
+
for tx in shitcoin_txs:
|
|
1446
|
+
tx.delete()
|
|
1447
|
+
logger.info("Shitcoin tx purge complete.")
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
_drop_shitcoin_txs()
|