dao-treasury 0.0.22__cp310-cp310-macosx_11_0_arm64.whl → 0.0.69__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.
- dao_treasury/.grafana/provisioning/dashboards/breakdowns/Expenses.json +551 -0
- dao_treasury/.grafana/provisioning/dashboards/breakdowns/Revenue.json +551 -0
- dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +7 -7
- dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +220 -0
- dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +18 -23
- dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +181 -29
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +808 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +602 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Current Treasury Assets.json +1009 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Historical Treasury Balances.json +2989 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +478 -0
- dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
- dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
- dao_treasury/__init__.py +20 -0
- dao_treasury/_docker.cpython-310-darwin.so +0 -0
- dao_treasury/_docker.py +67 -38
- dao_treasury/_nicknames.cpython-310-darwin.so +0 -0
- dao_treasury/_nicknames.py +24 -2
- dao_treasury/_wallet.cpython-310-darwin.so +0 -0
- dao_treasury/_wallet.py +157 -16
- dao_treasury/constants.cpython-310-darwin.so +0 -0
- dao_treasury/constants.py +39 -0
- dao_treasury/db.py +384 -45
- dao_treasury/docker-compose.yaml +6 -5
- dao_treasury/main.py +86 -17
- dao_treasury/sorting/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/__init__.py +171 -42
- dao_treasury/sorting/_matchers.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/_rules.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/_rules.py +1 -3
- dao_treasury/sorting/factory.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/factory.py +2 -6
- dao_treasury/sorting/rule.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rule.py +13 -10
- dao_treasury/sorting/rules/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rules/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rules/ignore/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/llamapay.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
- dao_treasury/streams/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/streams/__init__.py +0 -0
- dao_treasury/streams/llamapay.cpython-310-darwin.so +0 -0
- dao_treasury/streams/llamapay.py +388 -0
- dao_treasury/treasury.py +75 -28
- dao_treasury/types.cpython-310-darwin.so +0 -0
- dao_treasury-0.0.69.dist-info/METADATA +120 -0
- dao_treasury-0.0.69.dist-info/RECORD +54 -0
- dao_treasury-0.0.69.dist-info/top_level.txt +2 -0
- dao_treasury__mypyc.cpython-310-darwin.so +0 -0
- 52b51d40e96d4333695d__mypyc.cpython-310-darwin.so +0 -0
- dao_treasury/.grafana/provisioning/datasources/sqlite.yaml +0 -10
- dao_treasury-0.0.22.dist-info/METADATA +0 -63
- dao_treasury-0.0.22.dist-info/RECORD +0 -31
- dao_treasury-0.0.22.dist-info/top_level.txt +0 -2
- {dao_treasury-0.0.22.dist-info → dao_treasury-0.0.69.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,28 +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
|
|
29
45
|
from brownie.exceptions import EventLookupError
|
|
30
46
|
from brownie.network.event import EventDict, _EventItem
|
|
31
47
|
from brownie.network.transaction import TransactionReceipt
|
|
32
|
-
from eth_typing import ChecksumAddress, HexAddress
|
|
33
48
|
from eth_portfolio.structs import (
|
|
34
49
|
InternalTransfer,
|
|
35
50
|
LedgerEntry,
|
|
36
51
|
TokenTransfer,
|
|
37
52
|
Transaction,
|
|
38
53
|
)
|
|
54
|
+
from eth_retry import auto_retry
|
|
55
|
+
from eth_typing import ChecksumAddress, HexAddress, HexStr
|
|
39
56
|
from pony.orm import (
|
|
40
57
|
Database,
|
|
58
|
+
InterfaceError,
|
|
41
59
|
Optional,
|
|
42
60
|
PrimaryKey,
|
|
43
61
|
Required,
|
|
@@ -50,13 +68,17 @@ from pony.orm import (
|
|
|
50
68
|
select,
|
|
51
69
|
)
|
|
52
70
|
from y import EEE_ADDRESS, Contract, Network, convert, get_block_timestamp_async
|
|
53
|
-
from y.
|
|
71
|
+
from y._db.decorators import retry_locked
|
|
54
72
|
from y.contracts import _get_code
|
|
55
73
|
from y.exceptions import ContractNotVerified
|
|
56
74
|
|
|
75
|
+
from dao_treasury.constants import CHAINID
|
|
57
76
|
from dao_treasury.types import TxGroupDbid, TxGroupName
|
|
58
77
|
|
|
59
78
|
|
|
79
|
+
EventItem = _EventItem[_EventItem[OrderedDict[str, Any]]]
|
|
80
|
+
|
|
81
|
+
|
|
60
82
|
SQLITE_DIR = Path(path.expanduser("~")) / ".dao-treasury"
|
|
61
83
|
"""Path to the directory in the user's home where the DAO treasury SQLite database is stored."""
|
|
62
84
|
|
|
@@ -64,8 +86,11 @@ SQLITE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
64
86
|
|
|
65
87
|
|
|
66
88
|
_INSERT_THREAD = AsyncThreadPoolExecutor(1)
|
|
89
|
+
_SORT_THREAD = AsyncThreadPoolExecutor(1)
|
|
90
|
+
_EVENTS_THREADS = AsyncThreadPoolExecutor(16)
|
|
67
91
|
_SORT_SEMAPHORE = Semaphore(50)
|
|
68
92
|
|
|
93
|
+
_UTC = timezone.utc
|
|
69
94
|
|
|
70
95
|
db = Database()
|
|
71
96
|
|
|
@@ -114,13 +139,13 @@ class Chain(DbEntity):
|
|
|
114
139
|
chainid = Required(int, unique=True)
|
|
115
140
|
"""Numeric chain ID matching the connected RPC via :data:`~y.constants.CHAINID`."""
|
|
116
141
|
|
|
117
|
-
addresses = Set("Address", reverse="chain")
|
|
142
|
+
addresses = Set("Address", reverse="chain", lazy=True)
|
|
118
143
|
"""Relationship to address records on this chain."""
|
|
119
144
|
|
|
120
|
-
tokens = Set("Token", reverse="chain")
|
|
145
|
+
tokens = Set("Token", reverse="chain", lazy=True)
|
|
121
146
|
"""Relationship to token records on this chain."""
|
|
122
147
|
|
|
123
|
-
treasury_txs = Set("TreasuryTx")
|
|
148
|
+
treasury_txs = Set("TreasuryTx", lazy=True)
|
|
124
149
|
"""Relationship to treasury transactions on this chain."""
|
|
125
150
|
|
|
126
151
|
@staticmethod
|
|
@@ -179,7 +204,7 @@ class Address(DbEntity):
|
|
|
179
204
|
address_id = PrimaryKey(int, auto=True)
|
|
180
205
|
"""Auto-incremented primary key for the addresses table."""
|
|
181
206
|
|
|
182
|
-
chain = Required(Chain, reverse="addresses")
|
|
207
|
+
chain = Required(Chain, reverse="addresses", lazy=True)
|
|
183
208
|
"""Reference to the chain on which this address resides."""
|
|
184
209
|
|
|
185
210
|
address = Required(str, index=True)
|
|
@@ -188,7 +213,7 @@ class Address(DbEntity):
|
|
|
188
213
|
nickname = Optional(str)
|
|
189
214
|
"""Optional human-readable label (e.g., contract name or token name)."""
|
|
190
215
|
|
|
191
|
-
is_contract = Required(bool, index=True)
|
|
216
|
+
is_contract = Required(bool, index=True, lazy=True)
|
|
192
217
|
"""Flag indicating whether the address is a smart contract."""
|
|
193
218
|
|
|
194
219
|
composite_key(address, chain)
|
|
@@ -199,21 +224,22 @@ class Address(DbEntity):
|
|
|
199
224
|
treasury_tx_from: Set["TreasuryTx"]
|
|
200
225
|
treasury_tx_to: Set["TreasuryTx"]
|
|
201
226
|
|
|
202
|
-
token = Optional("Token", index=True)
|
|
227
|
+
token = Optional("Token", index=True, lazy=True)
|
|
203
228
|
"""Optional back-reference to a Token if this address is one."""
|
|
204
|
-
# partners_tx = Set('PartnerHarvestEvent', reverse='wrapper')
|
|
229
|
+
# partners_tx = Set('PartnerHarvestEvent', reverse='wrapper', lazy=True)
|
|
205
230
|
|
|
206
|
-
treasury_tx_from = Set("TreasuryTx", reverse="from_address")
|
|
231
|
+
treasury_tx_from = Set("TreasuryTx", reverse="from_address", lazy=True)
|
|
207
232
|
"""Inverse relation for transactions sent from this address."""
|
|
208
233
|
|
|
209
|
-
treasury_tx_to = Set("TreasuryTx", reverse="to_address")
|
|
234
|
+
treasury_tx_to = Set("TreasuryTx", reverse="to_address", lazy=True)
|
|
210
235
|
"""Inverse relation for transactions sent to this address."""
|
|
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)
|
|
217
243
|
|
|
218
244
|
def __eq__(self, other: Union["Address", ChecksumAddress, "Token"]) -> bool: # type: ignore [override]
|
|
219
245
|
if isinstance(other, str):
|
|
@@ -224,6 +250,14 @@ class Address(DbEntity):
|
|
|
224
250
|
|
|
225
251
|
__hash__ = DbEntity.__hash__
|
|
226
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
|
+
|
|
227
261
|
@staticmethod
|
|
228
262
|
@lru_cache(maxsize=None)
|
|
229
263
|
def get_dbid(address: HexAddress) -> int:
|
|
@@ -284,7 +318,6 @@ class Address(DbEntity):
|
|
|
284
318
|
)
|
|
285
319
|
|
|
286
320
|
commit()
|
|
287
|
-
|
|
288
321
|
return entity # type: ignore [no-any-return]
|
|
289
322
|
|
|
290
323
|
@staticmethod
|
|
@@ -307,6 +340,12 @@ class Address(DbEntity):
|
|
|
307
340
|
commit()
|
|
308
341
|
logger.info("%s nickname set to %s", entity.address, nickname)
|
|
309
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
|
+
|
|
310
349
|
|
|
311
350
|
UNI_V3_POS: Final = {
|
|
312
351
|
Network.Mainnet: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
|
|
@@ -351,29 +390,30 @@ class Token(DbEntity):
|
|
|
351
390
|
token_id = PrimaryKey(int, auto=True)
|
|
352
391
|
"""Auto-incremented primary key for the tokens table."""
|
|
353
392
|
|
|
354
|
-
chain = Required(Chain, index=True)
|
|
393
|
+
chain = Required(Chain, index=True, lazy=True)
|
|
355
394
|
"""Foreign key linking to :class:`~dao_treasury.db.Chain`."""
|
|
356
395
|
|
|
357
|
-
symbol = Required(str, index=True)
|
|
396
|
+
symbol = Required(str, index=True, lazy=True)
|
|
358
397
|
"""Short ticker symbol for the token."""
|
|
359
398
|
|
|
360
|
-
name = Required(str)
|
|
399
|
+
name = Required(str, lazy=True)
|
|
361
400
|
"""Full human-readable name of the token."""
|
|
362
401
|
|
|
363
|
-
decimals = Required(int)
|
|
402
|
+
decimals = Required(int, lazy=True)
|
|
364
403
|
"""Number of decimals used for value scaling."""
|
|
365
404
|
|
|
366
405
|
if TYPE_CHECKING:
|
|
367
406
|
treasury_tx: Set["TreasuryTx"]
|
|
368
407
|
|
|
369
|
-
treasury_tx = Set("TreasuryTx", reverse="token")
|
|
408
|
+
treasury_tx = Set("TreasuryTx", reverse="token", lazy=True)
|
|
370
409
|
"""Inverse relation for treasury transactions involving this token."""
|
|
371
|
-
# partner_harvest_event = Set('PartnerHarvestEvent', reverse="vault")
|
|
410
|
+
# partner_harvest_event = Set('PartnerHarvestEvent', reverse="vault", lazy=True)
|
|
372
411
|
|
|
373
412
|
address = Required(Address, column="address_id")
|
|
374
413
|
"""Foreign key to the address record for this token contract."""
|
|
375
|
-
|
|
376
|
-
|
|
414
|
+
|
|
415
|
+
streams = Set("Stream", reverse="token", lazy=True)
|
|
416
|
+
# vesting_escrows = Set("VestingEscrow", reverse="token", lazy=True)
|
|
377
417
|
|
|
378
418
|
def __eq__(self, other: Union["Token", Address, ChecksumAddress]) -> bool: # type: ignore [override]
|
|
379
419
|
if isinstance(other, str):
|
|
@@ -388,6 +428,10 @@ class Token(DbEntity):
|
|
|
388
428
|
def contract(self) -> Contract:
|
|
389
429
|
return Contract(self.address.address)
|
|
390
430
|
|
|
431
|
+
@property
|
|
432
|
+
def contract_coro(self) -> Coroutine[Any, Any, Contract]:
|
|
433
|
+
return Contract.coroutine(self.address.address)
|
|
434
|
+
|
|
391
435
|
@property
|
|
392
436
|
def scale(self) -> int:
|
|
393
437
|
"""Base for division according to `decimals`, e.g., `10**decimals`.
|
|
@@ -516,7 +560,7 @@ class TxGroup(DbEntity):
|
|
|
516
560
|
name = Required(str)
|
|
517
561
|
"""Name of the grouping category, e.g., 'Revenue', 'Expenses'."""
|
|
518
562
|
|
|
519
|
-
treasury_tx = Set("TreasuryTx", reverse="txgroup")
|
|
563
|
+
treasury_tx = Set("TreasuryTx", reverse="txgroup", lazy=True)
|
|
520
564
|
"""Inverse relation for treasury transactions assigned to this group."""
|
|
521
565
|
|
|
522
566
|
parent_txgroup = Optional("TxGroup", reverse="child_txgroups")
|
|
@@ -524,11 +568,13 @@ class TxGroup(DbEntity):
|
|
|
524
568
|
|
|
525
569
|
composite_key(name, parent_txgroup)
|
|
526
570
|
|
|
527
|
-
child_txgroups = Set("TxGroup", reverse="parent_txgroup")
|
|
571
|
+
child_txgroups = Set("TxGroup", reverse="parent_txgroup", lazy=True)
|
|
528
572
|
"""Set of nested child groups."""
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
573
|
+
|
|
574
|
+
streams = Set("Stream", reverse="txgroup", lazy=True)
|
|
575
|
+
|
|
576
|
+
# TODO: implement this
|
|
577
|
+
# vesting_escrows = Set("VestingEscrow", reverse="txgroup", lazy=True)
|
|
532
578
|
|
|
533
579
|
@property
|
|
534
580
|
def fullname(self) -> str:
|
|
@@ -604,7 +650,7 @@ class TxGroup(DbEntity):
|
|
|
604
650
|
return txgroup # type: ignore [no-any-return]
|
|
605
651
|
|
|
606
652
|
|
|
607
|
-
@lru_cache(
|
|
653
|
+
@lru_cache(500)
|
|
608
654
|
def get_transaction(txhash: str) -> TransactionReceipt:
|
|
609
655
|
"""Fetch and cache a transaction receipt from the connected chain.
|
|
610
656
|
|
|
@@ -700,6 +746,10 @@ class TreasuryTx(DbEntity):
|
|
|
700
746
|
"""Human-readable label for the sender address."""
|
|
701
747
|
return self.from_address.nickname or self.from_address.address # type: ignore [union-attr]
|
|
702
748
|
|
|
749
|
+
@property
|
|
750
|
+
def token_address(self) -> ChecksumAddress:
|
|
751
|
+
return self.token.address.address
|
|
752
|
+
|
|
703
753
|
@property
|
|
704
754
|
def symbol(self) -> str:
|
|
705
755
|
"""Ticker symbol for the transferred token."""
|
|
@@ -710,7 +760,23 @@ class TreasuryTx(DbEntity):
|
|
|
710
760
|
"""Decoded event logs for this transaction."""
|
|
711
761
|
return self._transaction.events
|
|
712
762
|
|
|
713
|
-
def
|
|
763
|
+
async def events_async(self) -> EventDict:
|
|
764
|
+
"""Asynchronously fetch decoded event logs for this transaction."""
|
|
765
|
+
tx = self._transaction
|
|
766
|
+
events = tx._events
|
|
767
|
+
if events is None:
|
|
768
|
+
events = await _EVENTS_THREADS.run(getattr, tx, "events")
|
|
769
|
+
return events
|
|
770
|
+
|
|
771
|
+
@overload
|
|
772
|
+
def get_events(
|
|
773
|
+
self, event_name: str, sync: Literal[False]
|
|
774
|
+
) -> Coroutine[Any, Any, EventItem]: ...
|
|
775
|
+
@overload
|
|
776
|
+
def get_events(self, event_name: str, sync: bool = True) -> EventItem: ...
|
|
777
|
+
def get_events(self, event_name: str, sync: bool = True) -> EventItem:
|
|
778
|
+
if not sync:
|
|
779
|
+
return _EVENTS_THREADS.run(self.get_events, event_name)
|
|
714
780
|
try:
|
|
715
781
|
return self.events[event_name]
|
|
716
782
|
except EventLookupError:
|
|
@@ -727,6 +793,7 @@ class TreasuryTx(DbEntity):
|
|
|
727
793
|
return get_transaction(self.hash)
|
|
728
794
|
|
|
729
795
|
@staticmethod
|
|
796
|
+
@auto_retry
|
|
730
797
|
async def insert(entry: LedgerEntry) -> None:
|
|
731
798
|
"""Asynchronously insert and sort a ledger entry.
|
|
732
799
|
|
|
@@ -747,8 +814,16 @@ class TreasuryTx(DbEntity):
|
|
|
747
814
|
async with _SORT_SEMAPHORE:
|
|
748
815
|
from dao_treasury.sorting import sort_advanced
|
|
749
816
|
|
|
750
|
-
|
|
817
|
+
try:
|
|
751
818
|
await sort_advanced(TreasuryTx[txid])
|
|
819
|
+
except Exception as e:
|
|
820
|
+
e.args = *e.args, entry
|
|
821
|
+
raise
|
|
822
|
+
|
|
823
|
+
async def _set_txgroup(self, txgroup_dbid: TxGroupDbid) -> None:
|
|
824
|
+
await _SORT_THREAD.run(
|
|
825
|
+
TreasuryTx.__set_txgroup, self.treasury_tx_id, txgroup_dbid
|
|
826
|
+
)
|
|
752
827
|
|
|
753
828
|
@staticmethod
|
|
754
829
|
def __insert(entry: LedgerEntry, ts: int) -> typing.Optional[int]:
|
|
@@ -810,9 +885,74 @@ class TreasuryTx(DbEntity):
|
|
|
810
885
|
gas_price=gas_price,
|
|
811
886
|
txgroup=txgroup_dbid,
|
|
812
887
|
)
|
|
888
|
+
# we must commit here or else dbid below will be `None`.
|
|
889
|
+
commit()
|
|
813
890
|
dbid = entity.treasury_tx_id
|
|
891
|
+
except InterfaceError as e:
|
|
892
|
+
raise ValueError(
|
|
893
|
+
e,
|
|
894
|
+
{
|
|
895
|
+
"chain": Chain.get_dbid(CHAINID),
|
|
896
|
+
"block": entry.block_number,
|
|
897
|
+
"timestamp": ts,
|
|
898
|
+
"hash": entry.hash.hex(),
|
|
899
|
+
"log_index": log_index,
|
|
900
|
+
"from_address": from_address,
|
|
901
|
+
"to_address": to_address,
|
|
902
|
+
"token": token,
|
|
903
|
+
"amount": entry.value,
|
|
904
|
+
"price": entry.price,
|
|
905
|
+
"value_usd": entry.value_usd,
|
|
906
|
+
# TODO: nuke db and add this column
|
|
907
|
+
# gas = gas,
|
|
908
|
+
"gas_used": gas_used,
|
|
909
|
+
"gas_price": gas_price,
|
|
910
|
+
"txgroup": txgroup_dbid,
|
|
911
|
+
},
|
|
912
|
+
) from e
|
|
814
913
|
except InvalidOperation as e:
|
|
815
|
-
|
|
914
|
+
with db_session:
|
|
915
|
+
from_address_entity = Address[from_address]
|
|
916
|
+
to_address_entity = Address[to_address]
|
|
917
|
+
token_entity = Token[token]
|
|
918
|
+
logger.error(e)
|
|
919
|
+
logger.error(
|
|
920
|
+
{
|
|
921
|
+
"chain": Chain.get_dbid(CHAINID),
|
|
922
|
+
"block": entry.block_number,
|
|
923
|
+
"timestamp": ts,
|
|
924
|
+
"hash": entry.hash.hex(),
|
|
925
|
+
"log_index": log_index,
|
|
926
|
+
"from_address": {
|
|
927
|
+
"dbid": from_address,
|
|
928
|
+
"address": from_address_entity.address,
|
|
929
|
+
"nickname": from_address_entity.nickname,
|
|
930
|
+
},
|
|
931
|
+
"to_address": {
|
|
932
|
+
"dbid": to_address,
|
|
933
|
+
"address": to_address_entity.address,
|
|
934
|
+
"nickname": to_address_entity.nickname,
|
|
935
|
+
},
|
|
936
|
+
"token": {
|
|
937
|
+
"dbid": token,
|
|
938
|
+
"address": token_entity.address.address,
|
|
939
|
+
"name": token_entity.name,
|
|
940
|
+
"symbol": token_entity.symbol,
|
|
941
|
+
"decimals": token_entity.decimals,
|
|
942
|
+
},
|
|
943
|
+
"amount": entry.value,
|
|
944
|
+
"price": entry.price,
|
|
945
|
+
"value_usd": entry.value_usd,
|
|
946
|
+
# TODO: nuke db and add this column
|
|
947
|
+
# gas = gas,
|
|
948
|
+
"gas_used": gas_used,
|
|
949
|
+
"gas_price": gas_price,
|
|
950
|
+
"txgroup": {
|
|
951
|
+
"dbid": txgroup_dbid,
|
|
952
|
+
"fullname": TxGroup[txgroup_dbid].fullname,
|
|
953
|
+
},
|
|
954
|
+
}
|
|
955
|
+
)
|
|
816
956
|
return None
|
|
817
957
|
except TransactionIntegrityError as e:
|
|
818
958
|
return _validate_integrity_error(entry, log_index)
|
|
@@ -830,6 +970,158 @@ class TreasuryTx(DbEntity):
|
|
|
830
970
|
return None
|
|
831
971
|
return dbid # type: ignore [no-any-return]
|
|
832
972
|
|
|
973
|
+
@staticmethod
|
|
974
|
+
@retry_locked
|
|
975
|
+
def __set_txgroup(treasury_tx_dbid: int, txgroup_dbid: TxGroupDbid) -> None:
|
|
976
|
+
with db_session:
|
|
977
|
+
TreasuryTx[treasury_tx_dbid].txgroup = txgroup_dbid
|
|
978
|
+
commit()
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
_stream_metadata_cache: Final[Dict[HexStr, Tuple[ChecksumAddress, date]]] = {}
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
class Stream(DbEntity):
|
|
985
|
+
_table_ = "streams"
|
|
986
|
+
stream_id = PrimaryKey(str)
|
|
987
|
+
|
|
988
|
+
contract = Required("Address", reverse="streams")
|
|
989
|
+
start_block = Required(int)
|
|
990
|
+
end_block = Optional(int)
|
|
991
|
+
token = Required("Token", reverse="streams", index=True)
|
|
992
|
+
from_address = Required("Address", reverse="streams_from")
|
|
993
|
+
to_address = Required("Address", reverse="streams_to")
|
|
994
|
+
reason = Optional(str)
|
|
995
|
+
amount_per_second = Required(Decimal, 38, 1)
|
|
996
|
+
status = Required(str, default="Active")
|
|
997
|
+
txgroup = Optional("TxGroup", reverse="streams")
|
|
998
|
+
|
|
999
|
+
streamed_funds = Set("StreamedFunds", lazy=True)
|
|
1000
|
+
|
|
1001
|
+
scale = 10**20
|
|
1002
|
+
|
|
1003
|
+
@property
|
|
1004
|
+
def is_alive(self) -> bool:
|
|
1005
|
+
if self.end_block is None:
|
|
1006
|
+
assert self.status in ["Active", "Paused"]
|
|
1007
|
+
return self.status == "Active"
|
|
1008
|
+
assert self.status == "Stopped"
|
|
1009
|
+
return False
|
|
1010
|
+
|
|
1011
|
+
@property
|
|
1012
|
+
def amount_per_minute(self) -> int:
|
|
1013
|
+
return self.amount_per_second * 60
|
|
1014
|
+
|
|
1015
|
+
@property
|
|
1016
|
+
def amount_per_hour(self) -> int:
|
|
1017
|
+
return self.amount_per_minute * 60
|
|
1018
|
+
|
|
1019
|
+
@property
|
|
1020
|
+
def amount_per_day(self) -> int:
|
|
1021
|
+
return self.amount_per_hour * 24
|
|
1022
|
+
|
|
1023
|
+
@staticmethod
|
|
1024
|
+
def check_closed(stream_id: HexStr) -> bool:
|
|
1025
|
+
with db_session:
|
|
1026
|
+
return any(sf.is_last_day for sf in Stream[stream_id].streamed_funds)
|
|
1027
|
+
|
|
1028
|
+
@staticmethod
|
|
1029
|
+
def _get_start_and_end(stream_dbid: HexStr) -> Tuple[datetime, datetime]:
|
|
1030
|
+
with db_session:
|
|
1031
|
+
stream = Stream[stream_dbid]
|
|
1032
|
+
start_date, end = stream.start_date, datetime.now(_UTC)
|
|
1033
|
+
# convert start to datetime
|
|
1034
|
+
start = datetime.combine(start_date, time(tzinfo=_UTC), tzinfo=_UTC)
|
|
1035
|
+
if stream.end_block:
|
|
1036
|
+
end = datetime.fromtimestamp(chain[stream.end_block].timestamp, tz=_UTC)
|
|
1037
|
+
return start, end
|
|
1038
|
+
|
|
1039
|
+
def stop_stream(self, block: int) -> None:
|
|
1040
|
+
self.end_block = block
|
|
1041
|
+
self.status = "Stopped"
|
|
1042
|
+
|
|
1043
|
+
def pause(self) -> None:
|
|
1044
|
+
self.status = "Paused"
|
|
1045
|
+
|
|
1046
|
+
@staticmethod
|
|
1047
|
+
def _get_token_and_start_date(stream_id: HexStr) -> Tuple[ChecksumAddress, date]:
|
|
1048
|
+
try:
|
|
1049
|
+
return _stream_metadata_cache[stream_id]
|
|
1050
|
+
except KeyError:
|
|
1051
|
+
with db_session:
|
|
1052
|
+
stream = Stream[stream_id]
|
|
1053
|
+
token = stream.token.address.address
|
|
1054
|
+
start_date = stream.start_date
|
|
1055
|
+
_stream_metadata_cache[stream_id] = token, start_date
|
|
1056
|
+
return token, start_date
|
|
1057
|
+
|
|
1058
|
+
@property
|
|
1059
|
+
def stream_contract(self) -> Contract:
|
|
1060
|
+
return Contract(self.contract.address)
|
|
1061
|
+
|
|
1062
|
+
@property
|
|
1063
|
+
def start_date(self) -> date:
|
|
1064
|
+
return datetime.fromtimestamp(chain[self.start_block].timestamp).date()
|
|
1065
|
+
|
|
1066
|
+
async def amount_withdrawable(self, block: int) -> int:
|
|
1067
|
+
return await self.stream_contract.withdrawable.coroutine(
|
|
1068
|
+
self.from_address.address,
|
|
1069
|
+
self.to_address.address,
|
|
1070
|
+
int(self.amount_per_second),
|
|
1071
|
+
block_identifier=block,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
def print(self) -> None:
|
|
1075
|
+
symbol = self.token.symbol
|
|
1076
|
+
print(f"{symbol} per second: {self.amount_per_second / self.scale}")
|
|
1077
|
+
print(f"{symbol} per day: {self.amount_per_day / self.scale}")
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
class StreamedFunds(DbEntity):
|
|
1081
|
+
"""Each object represents one calendar day of tokens streamed for a particular stream."""
|
|
1082
|
+
|
|
1083
|
+
_table_ = "streamed_funds"
|
|
1084
|
+
|
|
1085
|
+
date = Required(date)
|
|
1086
|
+
stream = Required(Stream, reverse="streamed_funds")
|
|
1087
|
+
PrimaryKey(stream, date)
|
|
1088
|
+
|
|
1089
|
+
amount = Required(Decimal, 38, 18)
|
|
1090
|
+
price = Required(Decimal, 38, 18)
|
|
1091
|
+
value_usd = Required(Decimal, 38, 18)
|
|
1092
|
+
seconds_active = Required(int)
|
|
1093
|
+
is_last_day = Required(bool)
|
|
1094
|
+
|
|
1095
|
+
@db_session
|
|
1096
|
+
def get_entity(stream_id: str, date: datetime) -> "StreamedFunds":
|
|
1097
|
+
stream = Stream[stream_id]
|
|
1098
|
+
return StreamedFunds.get(date=date, stream=stream)
|
|
1099
|
+
|
|
1100
|
+
@classmethod
|
|
1101
|
+
@db_session
|
|
1102
|
+
def create_entity(
|
|
1103
|
+
cls,
|
|
1104
|
+
stream_id: str,
|
|
1105
|
+
date: datetime,
|
|
1106
|
+
price: Decimal,
|
|
1107
|
+
seconds_active: int,
|
|
1108
|
+
is_last_day: bool,
|
|
1109
|
+
) -> "StreamedFunds":
|
|
1110
|
+
stream = Stream[stream_id]
|
|
1111
|
+
amount_streamed_today = round(
|
|
1112
|
+
stream.amount_per_second * seconds_active / stream.scale, 18
|
|
1113
|
+
)
|
|
1114
|
+
entity = StreamedFunds(
|
|
1115
|
+
date=date,
|
|
1116
|
+
stream=stream,
|
|
1117
|
+
amount=amount_streamed_today,
|
|
1118
|
+
price=round(price, 18),
|
|
1119
|
+
value_usd=round(amount_streamed_today * price, 18),
|
|
1120
|
+
seconds_active=seconds_active,
|
|
1121
|
+
is_last_day=is_last_day,
|
|
1122
|
+
)
|
|
1123
|
+
return entity
|
|
1124
|
+
|
|
833
1125
|
|
|
834
1126
|
db.bind(
|
|
835
1127
|
provider="sqlite", # TODO: let user choose postgres with server connection params
|
|
@@ -856,19 +1148,19 @@ def create_stream_ledger_view() -> None:
|
|
|
856
1148
|
Examples:
|
|
857
1149
|
>>> create_stream_ledger_view()
|
|
858
1150
|
"""
|
|
1151
|
+
db.execute("""DROP VIEW IF EXISTS stream_ledger;""")
|
|
859
1152
|
db.execute(
|
|
860
1153
|
"""
|
|
861
|
-
DROP VIEW IF EXISTS stream_ledger;
|
|
862
1154
|
create view stream_ledger as
|
|
863
1155
|
SELECT 'Mainnet' as chain_name,
|
|
864
|
-
cast(
|
|
1156
|
+
cast(strftime('%s', date || ' 00:00:00') as INTEGER) as timestamp,
|
|
865
1157
|
NULL as block,
|
|
866
1158
|
NULL as hash,
|
|
867
1159
|
NULL as log_index,
|
|
868
1160
|
symbol as token,
|
|
869
1161
|
d.address AS "from",
|
|
870
1162
|
d.nickname as from_nickname,
|
|
871
|
-
e.address
|
|
1163
|
+
e.address AS "to",
|
|
872
1164
|
e.nickname as to_nickname,
|
|
873
1165
|
amount,
|
|
874
1166
|
price,
|
|
@@ -887,6 +1179,31 @@ def create_stream_ledger_view() -> None:
|
|
|
887
1179
|
)
|
|
888
1180
|
|
|
889
1181
|
|
|
1182
|
+
def create_txgroup_hierarchy_view() -> None:
|
|
1183
|
+
"""Create or replace the SQL view `txgroup_hierarchy` for recursive txgroup hierarchy.
|
|
1184
|
+
|
|
1185
|
+
This view exposes txgroup_id, top_category, and parent_txgroup for all txgroups,
|
|
1186
|
+
matching the recursive CTE logic used in dashboards.
|
|
1187
|
+
"""
|
|
1188
|
+
db.execute("DROP VIEW IF EXISTS txgroup_hierarchy;")
|
|
1189
|
+
db.execute(
|
|
1190
|
+
"""
|
|
1191
|
+
CREATE VIEW txgroup_hierarchy AS
|
|
1192
|
+
WITH RECURSIVE group_hierarchy (txgroup_id, top_category, parent_txgroup) AS (
|
|
1193
|
+
SELECT txgroup_id, name AS top_category, parent_txgroup
|
|
1194
|
+
FROM txgroups
|
|
1195
|
+
WHERE parent_txgroup IS NULL
|
|
1196
|
+
UNION ALL
|
|
1197
|
+
SELECT child.txgroup_id, parent.top_category, child.parent_txgroup
|
|
1198
|
+
FROM txgroups AS child
|
|
1199
|
+
JOIN group_hierarchy AS parent
|
|
1200
|
+
ON child.parent_txgroup = parent.txgroup_id
|
|
1201
|
+
)
|
|
1202
|
+
SELECT * FROM group_hierarchy;
|
|
1203
|
+
"""
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
|
|
890
1207
|
def create_vesting_ledger_view() -> None:
|
|
891
1208
|
"""Create or replace the SQL view `vesting_ledger` for vesting escrow reporting.
|
|
892
1209
|
|
|
@@ -942,7 +1259,7 @@ def create_general_ledger_view() -> None:
|
|
|
942
1259
|
create VIEW general_ledger as
|
|
943
1260
|
select *
|
|
944
1261
|
from (
|
|
945
|
-
SELECT treasury_tx_id, b.chain_name,
|
|
1262
|
+
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
|
|
946
1263
|
FROM treasury_txs a
|
|
947
1264
|
LEFT JOIN chains b ON a.chain = b.chain_dbid
|
|
948
1265
|
LEFT JOIN tokens c ON a.token_id = c.token_id
|
|
@@ -950,9 +1267,9 @@ def create_general_ledger_view() -> None:
|
|
|
950
1267
|
LEFT JOIN addresses e ON a."to" = e.address_id
|
|
951
1268
|
LEFT JOIN txgroups f ON a.txgroup_id = f.txgroup_id
|
|
952
1269
|
LEFT JOIN txgroups g ON f.parent_txgroup = g.txgroup_id
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1270
|
+
UNION
|
|
1271
|
+
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
|
|
1272
|
+
FROM stream_ledger
|
|
956
1273
|
--UNION
|
|
957
1274
|
--SELECT -1, *
|
|
958
1275
|
--FROM vesting_ledger
|
|
@@ -1028,7 +1345,8 @@ def create_monthly_pnl_view() -> None:
|
|
|
1028
1345
|
|
|
1029
1346
|
|
|
1030
1347
|
with db_session:
|
|
1031
|
-
|
|
1348
|
+
create_stream_ledger_view()
|
|
1349
|
+
create_txgroup_hierarchy_view()
|
|
1032
1350
|
# create_vesting_ledger_view()
|
|
1033
1351
|
create_general_ledger_view()
|
|
1034
1352
|
create_unsorted_txs_view()
|
|
@@ -1088,7 +1406,7 @@ def _validate_integrity_error(
|
|
|
1088
1406
|
existing_object.amount,
|
|
1089
1407
|
)
|
|
1090
1408
|
except AssertionError:
|
|
1091
|
-
logger.
|
|
1409
|
+
logger.debug(
|
|
1092
1410
|
"slight rounding error in value for TreasuryTx[%s] due to sqlite decimal handling",
|
|
1093
1411
|
existing_object.treasury_tx_id,
|
|
1094
1412
|
)
|
|
@@ -1113,3 +1431,24 @@ def _validate_integrity_error(
|
|
|
1113
1431
|
)
|
|
1114
1432
|
else None
|
|
1115
1433
|
)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def _drop_shitcoin_txs() -> None:
|
|
1437
|
+
"""
|
|
1438
|
+
Purge any shitcoin txs from the db.
|
|
1439
|
+
|
|
1440
|
+
These should not be frequent, and only occur if a user populated the db before a shitcoin was added to the SHITCOINS mapping.
|
|
1441
|
+
"""
|
|
1442
|
+
shitcoins = eth_portfolio.SHITCOINS[CHAINID]
|
|
1443
|
+
with db_session:
|
|
1444
|
+
shitcoin_txs = select(
|
|
1445
|
+
tx for tx in TreasuryTx if tx.token.address.address in shitcoins
|
|
1446
|
+
)
|
|
1447
|
+
if count := shitcoin_txs.count():
|
|
1448
|
+
logger.info(f"Purging {count} shitcoin txs from the database...")
|
|
1449
|
+
for tx in shitcoin_txs:
|
|
1450
|
+
tx.delete()
|
|
1451
|
+
logger.info("Shitcoin tx purge complete.")
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
_drop_shitcoin_txs()
|