dao-treasury 0.0.29__cp310-cp310-musllinux_1_2_i686.whl → 0.0.31__cp310-cp310-musllinux_1_2_i686.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 (38) hide show
  1. 17ebe61b88bd37338d0a__mypyc.cpython-310-i386-linux-gnu.so +0 -0
  2. dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +8 -0
  3. dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +116 -0
  4. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +22 -22
  5. dao_treasury/_docker.cpython-310-i386-linux-gnu.so +0 -0
  6. dao_treasury/_docker.py +23 -18
  7. dao_treasury/_nicknames.cpython-310-i386-linux-gnu.so +0 -0
  8. dao_treasury/_wallet.cpython-310-i386-linux-gnu.so +0 -0
  9. dao_treasury/constants.cpython-310-i386-linux-gnu.so +0 -0
  10. dao_treasury/db.py +172 -16
  11. dao_treasury/docker-compose.yaml +1 -0
  12. dao_treasury/main.py +27 -3
  13. dao_treasury/sorting/__init__.cpython-310-i386-linux-gnu.so +0 -0
  14. dao_treasury/sorting/__init__.py +1 -0
  15. dao_treasury/sorting/_matchers.cpython-310-i386-linux-gnu.so +0 -0
  16. dao_treasury/sorting/_rules.cpython-310-i386-linux-gnu.so +0 -0
  17. dao_treasury/sorting/factory.cpython-310-i386-linux-gnu.so +0 -0
  18. dao_treasury/sorting/rule.cpython-310-i386-linux-gnu.so +0 -0
  19. dao_treasury/sorting/rule.py +5 -0
  20. dao_treasury/sorting/rules/__init__.cpython-310-i386-linux-gnu.so +0 -0
  21. dao_treasury/sorting/rules/__init__.py +1 -0
  22. dao_treasury/sorting/rules/ignore/__init__.cpython-310-i386-linux-gnu.so +0 -0
  23. dao_treasury/sorting/rules/ignore/__init__.py +1 -0
  24. dao_treasury/sorting/rules/ignore/llamapay.cpython-310-i386-linux-gnu.so +0 -0
  25. dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
  26. dao_treasury/streams/__init__.cpython-310-i386-linux-gnu.so +0 -0
  27. dao_treasury/streams/__init__.py +0 -0
  28. dao_treasury/streams/llamapay.cpython-310-i386-linux-gnu.so +0 -0
  29. dao_treasury/streams/llamapay.py +363 -0
  30. dao_treasury/treasury.py +25 -4
  31. dao_treasury/types.cpython-310-i386-linux-gnu.so +0 -0
  32. {dao_treasury-0.0.29.dist-info → dao_treasury-0.0.31.dist-info}/METADATA +37 -5
  33. dao_treasury-0.0.31.dist-info/RECORD +49 -0
  34. dao_treasury-0.0.31.dist-info/top_level.txt +2 -0
  35. 8d7637a0eb7617042369__mypyc.cpython-310-i386-linux-gnu.so +0 -0
  36. dao_treasury-0.0.29.dist-info/RECORD +0 -37
  37. dao_treasury-0.0.29.dist-info/top_level.txt +0 -2
  38. {dao_treasury-0.0.29.dist-info → dao_treasury-0.0.31.dist-info}/WHEEL +0 -0
dao_treasury/db.py CHANGED
@@ -8,6 +8,7 @@ This module defines Pony ORM entities for:
8
8
  - ERC-20 tokens and native coin placeholder (:class:`Token`)
9
9
  - Hierarchical transaction grouping (:class:`TxGroup`)
10
10
  - Treasury transaction records (:class:`TreasuryTx`)
11
+ - Streams and StreamedFunds for streaming payments
11
12
 
12
13
  It also provides helper functions for inserting ledger entries,
13
14
  resolving integrity conflicts, caching transaction receipts,
@@ -21,15 +22,16 @@ from functools import lru_cache
21
22
  from logging import getLogger
22
23
  from os import path
23
24
  from pathlib import Path
24
- from typing import TYPE_CHECKING, Dict, Final, Union, final
25
+ from typing import TYPE_CHECKING, Dict, Final, Tuple, Union, final
26
+ from datetime import date, datetime, time, timezone
25
27
 
26
- from a_sync import AsyncThreadPoolExecutor, a_sync
28
+ from a_sync import AsyncThreadPoolExecutor
27
29
  from brownie import chain
28
30
  from brownie.convert.datatypes import HexString
29
31
  from brownie.exceptions import EventLookupError
30
32
  from brownie.network.event import EventDict, _EventItem
31
33
  from brownie.network.transaction import TransactionReceipt
32
- from eth_typing import ChecksumAddress, HexAddress
34
+ from eth_typing import ChecksumAddress, HexAddress, HexStr
33
35
  from eth_portfolio.structs import (
34
36
  InternalTransfer,
35
37
  LedgerEntry,
@@ -68,6 +70,7 @@ _INSERT_THREAD = AsyncThreadPoolExecutor(1)
68
70
  _SORT_THREAD = AsyncThreadPoolExecutor(1)
69
71
  _SORT_SEMAPHORE = Semaphore(50)
70
72
 
73
+ _UTC = timezone.utc
71
74
 
72
75
  db = Database()
73
76
 
@@ -210,9 +213,10 @@ class Address(DbEntity):
210
213
 
211
214
  treasury_tx_to = Set("TreasuryTx", reverse="to_address")
212
215
  """Inverse relation for transactions sent to this address."""
213
- # streams_from = Set("Stream", reverse="from_address")
214
- # streams_to = Set("Stream", reverse="to_address")
215
- # streams = Set("Stream", reverse="contract")
216
+
217
+ streams_from = Set("Stream", reverse="from_address")
218
+ streams_to = Set("Stream", reverse="to_address")
219
+ streams = Set("Stream", reverse="contract")
216
220
  # vesting_escrows = Set("VestingEscrow", reverse="address")
217
221
  # vests_received = Set("VestingEscrow", reverse="recipient")
218
222
  # vests_funded = Set("VestingEscrow", reverse="funder")
@@ -226,6 +230,10 @@ class Address(DbEntity):
226
230
 
227
231
  __hash__ = DbEntity.__hash__
228
232
 
233
+ @property
234
+ def contract(self) -> Contract:
235
+ return Contract(self.address)
236
+
229
237
  @staticmethod
230
238
  @lru_cache(maxsize=None)
231
239
  def get_dbid(address: HexAddress) -> int:
@@ -380,7 +388,8 @@ class Token(DbEntity):
380
388
 
381
389
  address = Required(Address, column="address_id")
382
390
  """Foreign key to the address record for this token contract."""
383
- # streams = Set('Stream', reverse="token")
391
+
392
+ streams = Set("Stream", reverse="token")
384
393
  # vesting_escrows = Set("VestingEscrow", reverse="token")
385
394
 
386
395
  def __eq__(self, other: Union["Token", Address, ChecksumAddress]) -> bool: # type: ignore [override]
@@ -534,8 +543,10 @@ class TxGroup(DbEntity):
534
543
 
535
544
  child_txgroups = Set("TxGroup", reverse="parent_txgroup")
536
545
  """Set of nested child groups."""
537
- # TODO: implement these
538
- # streams = Set("Stream", reverse="txgroup")
546
+
547
+ streams = Set("Stream", reverse="txgroup")
548
+
549
+ # TODO: implement this
539
550
  # vesting_escrows = Set("VestingEscrow", reverse="txgroup")
540
551
 
541
552
  @property
@@ -896,6 +907,151 @@ class TreasuryTx(DbEntity):
896
907
  TreasuryTx[treasury_tx_dbid].txgroup = txgroup_dbid
897
908
 
898
909
 
910
+ _stream_metadata_cache: Final[Dict[HexStr, Tuple[ChecksumAddress, date]]] = {}
911
+
912
+
913
+ class Stream(DbEntity):
914
+ _table_ = "streams"
915
+ stream_id = PrimaryKey(str)
916
+
917
+ contract = Required("Address", reverse="streams")
918
+ start_block = Required(int)
919
+ end_block = Optional(int)
920
+ token = Required("Token", reverse="streams", index=True)
921
+ from_address = Required("Address", reverse="streams_from")
922
+ to_address = Required("Address", reverse="streams_to")
923
+ reason = Optional(str)
924
+ amount_per_second = Required(Decimal, 38, 1)
925
+ status = Required(str, default="Active")
926
+ txgroup = Optional("TxGroup", reverse="streams")
927
+
928
+ streamed_funds = Set("StreamedFunds")
929
+
930
+ scale = int(1e20)
931
+
932
+ @property
933
+ def is_alive(self) -> bool:
934
+ if self.end_block is None:
935
+ assert self.status in ["Active", "Paused"]
936
+ return self.status == "Active"
937
+ assert self.status == "Stopped"
938
+ return False
939
+
940
+ @property
941
+ def amount_per_minute(self) -> int:
942
+ return self.amount_per_second * 60
943
+
944
+ @property
945
+ def amount_per_hour(self) -> int:
946
+ return self.amount_per_minute * 60
947
+
948
+ @property
949
+ def amount_per_day(self) -> int:
950
+ return self.amount_per_hour * 24
951
+
952
+ @staticmethod
953
+ def check_closed(stream_id: HexStr) -> bool:
954
+ with db_session:
955
+ return any(sf.is_last_day for sf in Stream[stream_id].streamed_funds)
956
+
957
+ @staticmethod
958
+ def _get_start_and_end(stream_dbid: HexStr) -> Tuple[datetime, datetime]:
959
+ with db_session:
960
+ stream = Stream[stream_dbid]
961
+ start_date, end = stream.start_date, datetime.now(_UTC)
962
+ # convert start to datetime
963
+ start = datetime.combine(start_date, time(tzinfo=_UTC), tzinfo=_UTC)
964
+ if stream.end_block:
965
+ end = datetime.fromtimestamp(chain[stream.end_block].timestamp, tz=_UTC)
966
+ return start, end
967
+
968
+ def stop_stream(self, block: int) -> None:
969
+ self.end_block = block
970
+ self.status = "Stopped"
971
+
972
+ def pause(self) -> None:
973
+ self.status = "Paused"
974
+
975
+ @staticmethod
976
+ def _get_token_and_start_date(stream_id: HexStr) -> Tuple[ChecksumAddress, date]:
977
+ try:
978
+ return _stream_metadata_cache[stream_id]
979
+ except KeyError:
980
+ with db_session:
981
+ stream = Stream[stream_id]
982
+ token = stream.token.address.address
983
+ start_date = stream.start_date
984
+ _stream_metadata_cache[stream_id] = token, start_date
985
+ return token, start_date
986
+
987
+ @property
988
+ def stream_contract(self) -> Contract:
989
+ return Contract(self.contract.address)
990
+
991
+ @property
992
+ def start_date(self) -> date:
993
+ return datetime.fromtimestamp(chain[self.start_block].timestamp).date()
994
+
995
+ async def amount_withdrawable(self, block: int) -> int:
996
+ return await self.stream_contract.withdrawable.coroutine(
997
+ self.from_address.address,
998
+ self.to_address.address,
999
+ int(self.amount_per_second),
1000
+ block_identifier=block,
1001
+ )
1002
+
1003
+ def print(self) -> None:
1004
+ symbol = self.token.symbol
1005
+ print(f"{symbol} per second: {self.amount_per_second / self.scale}")
1006
+ print(f"{symbol} per day: {self.amount_per_day / self.scale}")
1007
+
1008
+
1009
+ class StreamedFunds(DbEntity):
1010
+ """Each object represents one calendar day of tokens streamed for a particular stream."""
1011
+
1012
+ _table_ = "streamed_funds"
1013
+
1014
+ date = Required(date)
1015
+ stream = Required(Stream, reverse="streamed_funds")
1016
+ PrimaryKey(stream, date)
1017
+
1018
+ amount = Required(Decimal, 38, 18)
1019
+ price = Required(Decimal, 38, 18)
1020
+ value_usd = Required(Decimal, 38, 18)
1021
+ seconds_active = Required(int)
1022
+ is_last_day = Required(bool)
1023
+
1024
+ @db_session
1025
+ def get_entity(stream_id: str, date: datetime) -> "StreamedFunds":
1026
+ stream = Stream[stream_id]
1027
+ return StreamedFunds.get(date=date, stream=stream)
1028
+
1029
+ @classmethod
1030
+ @db_session
1031
+ def create_entity(
1032
+ cls,
1033
+ stream_id: str,
1034
+ date: datetime,
1035
+ price: Decimal,
1036
+ seconds_active: int,
1037
+ is_last_day: bool,
1038
+ ) -> "StreamedFunds":
1039
+ stream = Stream[stream_id]
1040
+ amount_streamed_today = round(
1041
+ stream.amount_per_second * seconds_active / stream.scale, 18
1042
+ )
1043
+ entity = StreamedFunds(
1044
+ date=date,
1045
+ stream=stream,
1046
+ amount=amount_streamed_today,
1047
+ price=round(price, 18),
1048
+ value_usd=round(amount_streamed_today * price, 18),
1049
+ seconds_active=seconds_active,
1050
+ is_last_day=is_last_day,
1051
+ )
1052
+ return entity
1053
+
1054
+
899
1055
  db.bind(
900
1056
  provider="sqlite", # TODO: let user choose postgres with server connection params
901
1057
  filename=str(SQLITE_DIR / "dao-treasury.sqlite"),
@@ -921,12 +1077,12 @@ def create_stream_ledger_view() -> None:
921
1077
  Examples:
922
1078
  >>> create_stream_ledger_view()
923
1079
  """
1080
+ db.execute("""DROP VIEW IF EXISTS stream_ledger;""")
924
1081
  db.execute(
925
1082
  """
926
- DROP VIEW IF EXISTS stream_ledger;
927
1083
  create view stream_ledger as
928
1084
  SELECT 'Mainnet' as chain_name,
929
- cast(DATE AS timestamp) as timestamp,
1085
+ cast(strftime('%s', date || ' 00:00:00') as INTEGER) as timestamp,
930
1086
  NULL as block,
931
1087
  NULL as hash,
932
1088
  NULL as log_index,
@@ -1007,7 +1163,7 @@ def create_general_ledger_view() -> None:
1007
1163
  create VIEW general_ledger as
1008
1164
  select *
1009
1165
  from (
1010
- SELECT treasury_tx_id, b.chain_name, datetime(a.timestamp, 'unixepoch') AS 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
1166
+ 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
1011
1167
  FROM treasury_txs a
1012
1168
  LEFT JOIN chains b ON a.chain = b.chain_dbid
1013
1169
  LEFT JOIN tokens c ON a.token_id = c.token_id
@@ -1015,9 +1171,9 @@ def create_general_ledger_view() -> None:
1015
1171
  LEFT JOIN addresses e ON a."to" = e.address_id
1016
1172
  LEFT JOIN txgroups f ON a.txgroup_id = f.txgroup_id
1017
1173
  LEFT JOIN txgroups g ON f.parent_txgroup = g.txgroup_id
1018
- --UNION
1019
- --SELECT -1, chain_name, TIMESTAMP, cast(block AS integer) block, hash, CAST(log_index AS integer) as log_index, token, "from", from_nickname, "to", to_nickname, amount, price, value_usd, txgroup, parent_txgroup, txgroup_id
1020
- --FROM stream_ledger
1174
+ UNION
1175
+ 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
1176
+ FROM stream_ledger
1021
1177
  --UNION
1022
1178
  --SELECT -1, *
1023
1179
  --FROM vesting_ledger
@@ -1093,7 +1249,7 @@ def create_monthly_pnl_view() -> None:
1093
1249
 
1094
1250
 
1095
1251
  with db_session:
1096
- # create_stream_ledger_view()
1252
+ create_stream_ledger_view()
1097
1253
  # create_vesting_ledger_view()
1098
1254
  create_general_ledger_view()
1099
1255
  create_unsorted_txs_view()
@@ -15,6 +15,7 @@ services:
15
15
  - GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER:-admin}
16
16
  - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-admin}
17
17
  - GF_AUTH_ANONYMOUS_ENABLED=true
18
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
18
19
  - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/summary/Monthly.json
19
20
  - GF_SERVER_ROOT_URL
20
21
  - GF_RENDERING_SERVER_URL=http://renderer:8091/render
dao_treasury/main.py CHANGED
@@ -28,6 +28,7 @@ from pathlib import Path
28
28
 
29
29
  import brownie
30
30
  import yaml
31
+ from a_sync import create_task
31
32
  from dao_treasury._wallet import load_wallets_from_yaml
32
33
  from eth_portfolio_scripts.balances import export_balances
33
34
  from eth_typing import BlockNumber
@@ -107,6 +108,11 @@ parser.add_argument(
107
108
  help="Port for the DAO Treasury dashboard web interface. Default: 3000",
108
109
  default=3000,
109
110
  )
111
+ parser.add_argument(
112
+ "--start-renderer",
113
+ action="store_true",
114
+ help="If set, the Grafana renderer container will be started for dashboard image export. By default, only the grafana container is started.",
115
+ )
110
116
  parser.add_argument(
111
117
  "--renderer-port",
112
118
  type=int,
@@ -154,6 +160,7 @@ async def export(args) -> None:
154
160
  daemon: Ignored flag.
155
161
  grafana_port: Port for Grafana (sets DAO_TREASURY_GRAFANA_PORT).
156
162
  renderer_port: Port for renderer (sets DAO_TREASURY_RENDERER_PORT).
163
+ start_renderer: If True, start renderer; otherwise, only start grafana.
157
164
 
158
165
  Example:
159
166
  In code::
@@ -165,6 +172,8 @@ async def export(args) -> None:
165
172
  :func:`dao_treasury._docker.down`,
166
173
  :class:`dao_treasury.Treasury.populate_db`
167
174
  """
175
+ import eth_portfolio_scripts.docker
176
+
168
177
  from dao_treasury import _docker, constants, db, Treasury
169
178
 
170
179
  wallets = getattr(args, "wallet", None)
@@ -195,7 +204,12 @@ async def export(args) -> None:
195
204
  db.Address.set_nickname(address, nickname)
196
205
 
197
206
  treasury = Treasury(wallets, args.sort_rules, asynchronous=True)
198
- _docker.up()
207
+
208
+ # Start only the requested containers
209
+ if args.start_renderer is True:
210
+ _docker.up()
211
+ else:
212
+ _docker.up("grafana")
199
213
 
200
214
  # eth-portfolio needs this present
201
215
  # TODO: we need to update eth-portfolio to honor wallet join and exit times
@@ -209,11 +223,21 @@ async def export(args) -> None:
209
223
  # TODO: make this user configurable? would require some dynamic grafana dashboard files
210
224
  args.label = "Treasury"
211
225
 
212
- try:
213
- await asyncio.gather(
226
+ export_task = create_task(
227
+ asyncio.gather(
214
228
  export_balances(args),
215
229
  treasury.populate_db(BlockNumber(0), brownie.chain.height),
216
230
  )
231
+ )
232
+
233
+ await asyncio.sleep(1)
234
+
235
+ # we don't need these containers since dao-treasury uses its own.
236
+ eth_portfolio_scripts.docker.stop("grafana")
237
+ eth_portfolio_scripts.docker.stop("renderer")
238
+
239
+ try:
240
+ await export_task
217
241
  finally:
218
242
  _docker.down()
219
243
 
@@ -35,6 +35,7 @@ from dao_treasury.sorting.rule import (
35
35
  OtherIncomeSortRule,
36
36
  RevenueSortRule,
37
37
  )
38
+ from dao_treasury.sorting.rules import *
38
39
  from dao_treasury.types import TxGroupDbid
39
40
 
40
41
 
@@ -31,6 +31,7 @@ See Also:
31
31
 
32
32
  from collections import defaultdict
33
33
  from dataclasses import dataclass
34
+ from logging import getLogger
34
35
  from typing import (
35
36
  TYPE_CHECKING,
36
37
  DefaultDict,
@@ -53,6 +54,9 @@ if TYPE_CHECKING:
53
54
  from dao_treasury.db import TreasuryTx
54
55
 
55
56
 
57
+ logger: Final = getLogger(__name__)
58
+ _log_debug: Final = logger.debug
59
+
56
60
  SORT_RULES: DefaultDict[Type[SortRule], List[SortRule]] = defaultdict(list)
57
61
  """Mapping from sort rule classes to lists of instantiated rules, in creation order per class.
58
62
 
@@ -214,6 +218,7 @@ class _SortRule:
214
218
  getattr(tx, matcher) == getattr(self, matcher) for matcher in matchers
215
219
  )
216
220
 
221
+ _log_debug("checking %s for %s", tx, self.func)
217
222
  match = self.func(tx) # type: ignore [misc]
218
223
  return match if isinstance(match, bool) else await match
219
224
 
@@ -0,0 +1 @@
1
+ from dao_treasury.sorting.rules.ignore import *
@@ -0,0 +1 @@
1
+ from dao_treasury.sorting.rules.ignore.llamapay import *
@@ -0,0 +1,20 @@
1
+ from dao_treasury import TreasuryTx
2
+ from dao_treasury.sorting.factory import ignore
3
+ from dao_treasury.streams import llamapay
4
+
5
+
6
+ @ignore("LlamaPay")
7
+ def is_llamapay_stream_replenishment(tx: TreasuryTx) -> bool:
8
+ if tx.to_address.address in llamapay.factories: # type: ignore [operator]
9
+ # We amortize these streams daily in the `llamapay` module, you'll sort each stream appropriately.
10
+ return True
11
+
12
+ # NOTE: not sure if we want this yet
13
+ # Puling unused funds back from vesting escrow / llamapay
14
+ # elif tx.from_address == "Contract: LlamaPay" and "StreamCancelled" in tx.events:
15
+ # if tx.amount > 0:
16
+ # tx.amount *= -1
17
+ # if tx.value_usd > 0:
18
+ # tx.value_usd *= -1
19
+ # return True
20
+ return False
File without changes