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
@@ -1,16 +1,47 @@
1
1
  """
2
- This module contains logic for sorting transactions into various categories.
2
+ This module provides the core logic for sorting DAO Treasury transactions into transaction groups (categories).
3
+
4
+ Sorting enables comprehensive financial reporting and categorization tailored for on-chain organizations.
5
+ Transactions are matched against either statically defined rules or more advanced dynamic rules based on user-defined matching functions.
6
+
7
+ Sorting works by attempting matches in this order:
8
+ 1. Check if the transaction is an internal transfer (within treasury wallets).
9
+ 2. Check if the transaction is "Out of Range" (neither sender nor receiver was a treasury wallet at the time of the tx).
10
+ 3. Match by transaction hash using registered HashMatchers.
11
+ 4. Match by sender address using registered FromAddressMatchers.
12
+ 5. Match by recipient address using registered ToAddressMatchers.
13
+ 6. Assign "Must Sort Inbound" or "Must Sort Outbound" groups if part of treasury.
14
+ 7. Raise an error if no match is found (unexpected case).
15
+
16
+ See the complete [sort rules documentation](https://bobthebuidler.github.io/dao-treasury/sort_rules.html) for detailed explanations
17
+ and examples on defining and registering sort rules.
18
+
19
+ See Also:
20
+ :func:`dao_treasury.sorting.sort_basic`
21
+ :func:`dao_treasury.sorting.sort_basic_entity`
22
+ :func:`dao_treasury.sorting.sort_advanced`
23
+ :class:`dao_treasury.sorting.HashMatcher`
24
+ :class:`dao_treasury.sorting.FromAddressMatcher`
25
+ :class:`dao_treasury.sorting.ToAddressMatcher`
3
26
  """
27
+
4
28
  from logging import getLogger
5
29
  from typing import Final, Optional
6
30
 
7
31
  from eth_portfolio.structs import LedgerEntry
8
32
  from evmspec.data import TransactionHash
33
+ from y.exceptions import ContractNotVerified
9
34
 
10
- from dao_treasury import db
35
+ from dao_treasury import constants, db
11
36
  from dao_treasury._wallet import TreasuryWallet
12
- from dao_treasury.sorting._matchers import _Matcher, FromAddressMatcher, HashMatcher, ToAddressMatcher
37
+ from dao_treasury.sorting._matchers import (
38
+ _Matcher,
39
+ FromAddressMatcher,
40
+ HashMatcher,
41
+ ToAddressMatcher,
42
+ )
13
43
  from dao_treasury.sorting.factory import (
44
+ SortRuleFactory,
14
45
  cost_of_revenue,
15
46
  expense,
16
47
  ignore,
@@ -20,13 +51,14 @@ from dao_treasury.sorting.factory import (
20
51
  )
21
52
  from dao_treasury.sorting.rule import (
22
53
  SORT_RULES,
23
- CostOfRevenueSortRule,
24
- ExpenseSortRule,
25
- IgnoreSortRule,
26
- OtherExpenseSortRule,
27
- OtherIncomeSortRule,
54
+ CostOfRevenueSortRule,
55
+ ExpenseSortRule,
56
+ IgnoreSortRule,
57
+ OtherExpenseSortRule,
58
+ OtherIncomeSortRule,
28
59
  RevenueSortRule,
29
60
  )
61
+ from dao_treasury.sorting.rules import *
30
62
  from dao_treasury.types import TxGroupDbid
31
63
 
32
64
 
@@ -34,11 +66,11 @@ logger: Final = getLogger("dao_treasury.sorting")
34
66
 
35
67
 
36
68
  __all__ = [
37
- "CostOfRevenueSortRule",
38
- "ExpenseSortRule",
39
- "IgnoreSortRule",
40
- "OtherExpenseSortRule",
41
- "OtherIncomeSortRule",
69
+ "CostOfRevenueSortRule",
70
+ "ExpenseSortRule",
71
+ "IgnoreSortRule",
72
+ "OtherExpenseSortRule",
73
+ "OtherIncomeSortRule",
42
74
  "RevenueSortRule",
43
75
  "cost_of_revenue",
44
76
  "expense",
@@ -46,40 +78,81 @@ __all__ = [
46
78
  "other_expense",
47
79
  "other_income",
48
80
  "revenue",
49
- "HashMatcher",
50
- "FromAddressMatcher",
51
- "ToAddressMatcher",
81
+ "SortRuleFactory",
82
+ "HashMatcher",
83
+ "FromAddressMatcher",
84
+ "ToAddressMatcher",
52
85
  "SORT_RULES",
53
86
  "_Matcher",
54
87
  ]
55
88
 
56
89
  # C constants
57
90
  TxGroup: Final = db.TxGroup
58
- must_sort_inbound_txgroup_dbid: Final = db.must_sort_inbound_txgroup_dbid
59
- must_sort_outbound_txgroup_dbid: Final = db.must_sort_outbound_txgroup_dbid
91
+ MUST_SORT_INBOUND_TXGROUP_DBID: Final = db.must_sort_inbound_txgroup_dbid
92
+ MUST_SORT_OUTBOUND_TXGROUP_DBID: Final = db.must_sort_outbound_txgroup_dbid
93
+
94
+ INTERNAL_TRANSFER_TXGROUP_DBID: Final = TxGroup.get_dbid(
95
+ name="Internal Transfer",
96
+ parent=TxGroup.get_dbid("Ignore"),
97
+ )
98
+ """Database ID for the 'Internal Transfer' transaction group.
99
+
100
+ This group represents transactions that occur internally between treasury-owned wallets.
101
+ Such internal movements of funds within the DAO's treasury do not require separate handling or reporting.
102
+
103
+ See Also:
104
+ :class:`dao_treasury.db.TxGroup`
105
+ """
106
+
107
+ OUT_OF_RANGE_TXGROUP_DBID = TxGroup.get_dbid(
108
+ name="Out of Range", parent=TxGroup.get_dbid("Ignore")
109
+ )
110
+ """Database ID for the 'Out of Range' transaction group.
111
+
112
+ This category is assigned to transactions where neither the sender nor the recipient
113
+ wallet are members of the treasury at the time of the transaction.
114
+
115
+ See Also:
116
+ :class:`dao_treasury.db.TxGroup`
117
+ """
60
118
 
61
119
 
62
120
  def sort_basic(entry: LedgerEntry) -> TxGroupDbid:
121
+ """Determine the transaction group ID for a basic ledger entry using static matching.
122
+
123
+ The function attempts to categorize the transaction by testing:
124
+ - If both 'from' and 'to' addresses are treasury wallets (internal transfer).
125
+ - If neither ‘to’ address is a treasury wallet at the time of the transaction (out of range).
126
+ - If the transaction hash matches a known HashMatcher.
127
+ - If the 'from' address matches a FromAddressMatcher.
128
+ - If the 'to' address matches a ToAddressMatcher.
129
+ - Assignment to 'Must Sort Outbound' or 'Must Sort Inbound' groups if applicable.
130
+ - Raises `NotImplementedError` if none of the above conditions are met (should not happen).
131
+
132
+ Args:
133
+ entry: A ledger entry representing a blockchain transaction.
134
+
135
+ Examples:
136
+ >>> from eth_portfolio.structs import Transaction
137
+ >>> entry = Transaction(from_address="0xabc...", to_address="0xdef...", block_number=1234567)
138
+ >>> group_id = sort_basic(entry)
139
+ >>> print(group_id)
140
+
141
+ See Also:
142
+ :func:`sort_basic_entity`
143
+ :func:`sort_advanced`
144
+ :class:`dao_treasury.sorting.HashMatcher`
145
+ """
146
+ from_address = entry.from_address
147
+ to_address = entry.to_address
148
+ block = entry.block_number
149
+
63
150
  txgroup_dbid: Optional[TxGroupDbid] = None
64
- if from_wallet := TreasuryWallet._get_instance(entry.from_address):
65
- # TODO: asyncify the start and end block stuff
66
- start_block_for_wallet = from_wallet._start_block
67
- end_block_for_wallet = from_wallet._end_block
68
- if (
69
- start_block_for_wallet <= entry.block_number
70
- and (end_block_for_wallet is None or entry.block_number <= end_block_for_wallet)
71
- ):
72
- if to_wallet := TreasuryWallet._get_instance(entry.to_address):
73
- start_block_for_wallet = to_wallet._start_block
74
- end_block_for_wallet = to_wallet._end_block
75
- if (
76
- start_block_for_wallet <= entry.block_number
77
- and (end_block_for_wallet is None or entry.block_number <= end_block_for_wallet)
78
- ):
79
- txgroup_dbid = TxGroup.get_dbid(
80
- name="Internal Transfer",
81
- parent=TxGroup.get_dbid("Ignore"),
82
- )
151
+ if TreasuryWallet.check_membership(from_address, block):
152
+ if TreasuryWallet.check_membership(to_address, block):
153
+ txgroup_dbid = INTERNAL_TRANSFER_TXGROUP_DBID
154
+ elif not TreasuryWallet.check_membership(to_address, block):
155
+ txgroup_dbid = OUT_OF_RANGE_TXGROUP_DBID
83
156
 
84
157
  if txgroup_dbid is None:
85
158
  if isinstance(txhash := entry.hash, TransactionHash):
@@ -87,105 +160,136 @@ def sort_basic(entry: LedgerEntry) -> TxGroupDbid:
87
160
  txgroup_dbid = HashMatcher.match(txhash)
88
161
 
89
162
  if txgroup_dbid is None:
90
- txgroup_dbid = FromAddressMatcher.match(entry.from_address)
163
+ txgroup_dbid = FromAddressMatcher.match(from_address)
91
164
 
92
165
  if txgroup_dbid is None:
93
- txgroup_dbid = ToAddressMatcher.match(entry.to_address)
94
-
166
+ txgroup_dbid = ToAddressMatcher.match(to_address)
167
+
95
168
  if txgroup_dbid is None:
96
- if (
97
- entry.from_address
98
- and (from_wallet := TreasuryWallet._get_instance(entry.from_address))
99
- # TODO: asyncify the start and end block stuff
100
- and from_wallet._start_block <= entry.block_number
101
- and (from_wallet._end_block is None or from_wallet._end_block >= entry.block_number)
102
- ):
103
- txgroup_dbid = must_sort_outbound_txgroup_dbid
104
-
105
- elif (
106
- entry.to_address
107
- and (to_wallet := TreasuryWallet._get_instance(entry.to_address))
108
- and to_wallet._start_block <= entry.block_number
109
- and to_wallet._end_block is None or entry.block_number <= to_wallet._end_block # type: ignore [union-attr, operator]
110
- ):
111
- txgroup_dbid = must_sort_inbound_txgroup_dbid
112
-
169
+ if TreasuryWallet.check_membership(from_address, block):
170
+ txgroup_dbid = MUST_SORT_OUTBOUND_TXGROUP_DBID
171
+
172
+ elif TreasuryWallet.check_membership(to_address, block):
173
+ txgroup_dbid = MUST_SORT_INBOUND_TXGROUP_DBID
174
+
113
175
  else:
114
176
  raise NotImplementedError("this isnt supposed to happen")
115
177
  return txgroup_dbid # type: ignore [no-any-return]
116
178
 
117
179
 
118
- def sort_basic_entity(entry: db.TreasuryTx) -> TxGroupDbid:
180
+ def sort_basic_entity(tx: db.TreasuryTx) -> TxGroupDbid:
181
+ """Determine the transaction group ID for a TreasuryTx database entity using static matching.
182
+
183
+ Similar to :func:`sort_basic` but operates on a TreasuryTx entity from the database.
184
+ It considers additional constants such as `DISPERSE_APP` when determining whether
185
+ a transaction is out of range.
186
+
187
+ Args:
188
+ tx: A TreasuryTx database entity representing a treasury transaction.
189
+
190
+ Examples:
191
+ >>> from dao_treasury.db import TreasuryTx
192
+ >>> tx = TreasuryTx[123]
193
+ >>> group_id = sort_basic_entity(tx)
194
+ >>> print(group_id)
195
+
196
+ See Also:
197
+ :func:`sort_basic`
198
+ :func:`sort_advanced`
199
+ """
200
+ from_address = tx.from_address.address
201
+ to_address = tx.to_address
202
+ block = tx.block
203
+
119
204
  txgroup_dbid: Optional[TxGroupDbid] = None
120
- if entry.from_address:
121
- if from_wallet := TreasuryWallet._get_instance(entry.from_address.address):
122
- # TODO: asyncify the start and end block stuff
123
- start_block_for_wallet = from_wallet._start_block
124
- end_block_for_wallet = from_wallet._end_block
125
- if (
126
- start_block_for_wallet <= entry.block
127
- and (end_block_for_wallet is None or entry.block <= end_block_for_wallet)
128
- and entry.to_address
129
- ):
130
- if to_wallet := TreasuryWallet._get_instance(entry.to_address.address):
131
- start_block_for_wallet = to_wallet._start_block
132
- end_block_for_wallet = to_wallet._end_block
133
- if (
134
- start_block_for_wallet <= entry.block
135
- and (end_block_for_wallet is None or entry.block <= end_block_for_wallet)
136
- ):
137
- txgroup_dbid = TxGroup.get_dbid(
138
- name="Internal Transfer",
139
- parent=TxGroup.get_dbid("Ignore"),
140
- )
205
+ if TreasuryWallet.check_membership(from_address, block):
206
+ if TreasuryWallet.check_membership(tx.to_address.address, block):
207
+ txgroup_dbid = INTERNAL_TRANSFER_TXGROUP_DBID
208
+ elif not (
209
+ TreasuryWallet.check_membership(tx.to_address.address, tx.block)
210
+ or from_address in constants.DISPERSE_APP
211
+ ):
212
+ txgroup_dbid = OUT_OF_RANGE_TXGROUP_DBID
141
213
 
142
214
  if txgroup_dbid is None:
143
- txgroup_dbid = HashMatcher.match(entry.hash)
144
-
215
+ txgroup_dbid = HashMatcher.match(tx.hash)
216
+
145
217
  if txgroup_dbid is None:
146
- txgroup_dbid = FromAddressMatcher.match(entry.from_address.address)
218
+ txgroup_dbid = FromAddressMatcher.match(from_address)
147
219
 
148
- if txgroup_dbid is None and entry.to_address:
149
- txgroup_dbid = ToAddressMatcher.match(entry.to_address.address)
220
+ if txgroup_dbid is None and to_address:
221
+ txgroup_dbid = ToAddressMatcher.match(to_address.address)
150
222
 
151
223
  if txgroup_dbid is None:
152
- if (
153
- entry.from_address
154
- and (from_wallet := TreasuryWallet._get_instance(entry.from_address.address))
155
- # TODO: asyncify the start and end block stuff
156
- and from_wallet._start_block <= entry.block
157
- and (from_wallet._end_block is None or from_wallet._end_block >= entry.block)
158
- ):
159
- txgroup_dbid = must_sort_outbound_txgroup_dbid
160
-
161
- elif (
162
- entry.to_address
163
- and (to_wallet := TreasuryWallet._get_instance(entry.to_address.address))
164
- and to_wallet._start_block <= entry.block
165
- and to_wallet._end_block is None or entry.block <= to_wallet._end_block # type: ignore [union-attr, operator]
166
- ):
167
- txgroup_dbid = must_sort_inbound_txgroup_dbid
168
-
224
+ if TreasuryWallet.check_membership(from_address, block):
225
+ txgroup_dbid = MUST_SORT_OUTBOUND_TXGROUP_DBID
226
+
227
+ elif TreasuryWallet.check_membership(to_address.address, block):
228
+ txgroup_dbid = MUST_SORT_INBOUND_TXGROUP_DBID
229
+
230
+ elif from_address in constants.DISPERSE_APP:
231
+ txgroup_dbid = MUST_SORT_OUTBOUND_TXGROUP_DBID
232
+
233
+ elif from_address in constants.DISPERSE_APP:
234
+ txgroup_dbid = MUST_SORT_OUTBOUND_TXGROUP_DBID
235
+
169
236
  else:
170
237
  raise NotImplementedError("this isnt supposed to happen")
171
-
172
- if txgroup_dbid not in (must_sort_inbound_txgroup_dbid, must_sort_outbound_txgroup_dbid):
173
- logger.info("Sorted %s to txgroup %s", entry, txgroup_dbid)
174
-
238
+
239
+ if txgroup_dbid not in (
240
+ MUST_SORT_INBOUND_TXGROUP_DBID,
241
+ MUST_SORT_OUTBOUND_TXGROUP_DBID,
242
+ ):
243
+ logger.info("Sorted %s to %s", tx, TxGroup.get_fullname(txgroup_dbid))
244
+
175
245
  return txgroup_dbid # type: ignore [no-any-return]
176
246
 
177
247
 
178
248
  async def sort_advanced(entry: db.TreasuryTx) -> TxGroupDbid:
249
+ """Determine the transaction group ID for a TreasuryTx entity using advanced dynamic rules.
250
+
251
+ Starts with the result of static matching via :func:`sort_basic_entity`, then
252
+ applies advanced asynchronous matching rules registered under :data:`SORT_RULES`.
253
+ Applies rules sequentially until a match is found or all rules are exhausted.
254
+
255
+ If a rule's match attempt raises a `ContractNotVerified` exception, the rule is skipped.
256
+
257
+ Updates the TreasuryTx entity's transaction group in the database when a match
258
+ other than 'Must Sort Inbound/Outbound' is found.
259
+
260
+ Args:
261
+ entry: A TreasuryTx database entity representing a treasury transaction.
262
+
263
+ Examples:
264
+ >>> from dao_treasury.db import TreasuryTx
265
+ >>> import asyncio
266
+ >>> tx = TreasuryTx[123]
267
+ >>> group_id = asyncio.run(sort_advanced(tx))
268
+ >>> print(group_id)
269
+
270
+ See Also:
271
+ :func:`sort_basic_entity`
272
+ :data:`SORT_RULES`
273
+ """
179
274
  txgroup_dbid = sort_basic_entity(entry)
180
275
 
181
- if txgroup_dbid in (must_sort_inbound_txgroup_dbid, must_sort_outbound_txgroup_dbid):
182
- for rule in SORT_RULES:
183
- if await rule.match(entry):
184
- txgroup_dbid = rule.txgroup_dbid
185
- break
186
-
187
- if txgroup_dbid not in (must_sort_inbound_txgroup_dbid, must_sort_outbound_txgroup_dbid):
188
- logger.info("Sorted %s to txgroup %s", entry, txgroup_dbid)
189
- entry.txgroup = txgroup_dbid
190
-
276
+ if txgroup_dbid in (
277
+ MUST_SORT_INBOUND_TXGROUP_DBID,
278
+ MUST_SORT_OUTBOUND_TXGROUP_DBID,
279
+ ):
280
+ for rules in SORT_RULES.values():
281
+ for rule in rules:
282
+ try:
283
+ if await rule.match(entry):
284
+ txgroup_dbid = rule.txgroup_dbid
285
+ break
286
+ except ContractNotVerified:
287
+ continue
288
+ if txgroup_dbid not in (
289
+ MUST_SORT_INBOUND_TXGROUP_DBID,
290
+ MUST_SORT_OUTBOUND_TXGROUP_DBID,
291
+ ):
292
+ logger.info("Sorted %s to %s", entry, TxGroup.get_fullname(txgroup_dbid))
293
+ await entry._set_txgroup(txgroup_dbid)
294
+
191
295
  return txgroup_dbid # type: ignore [no-any-return]