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.
Files changed (56) 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 +18 -23
  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 +1009 -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 +20 -0
  15. dao_treasury/_docker.cpython-310-darwin.so +0 -0
  16. dao_treasury/_docker.py +67 -38
  17. dao_treasury/_nicknames.cpython-310-darwin.so +0 -0
  18. dao_treasury/_nicknames.py +24 -2
  19. dao_treasury/_wallet.cpython-310-darwin.so +0 -0
  20. dao_treasury/_wallet.py +157 -16
  21. dao_treasury/constants.cpython-310-darwin.so +0 -0
  22. dao_treasury/constants.py +39 -0
  23. dao_treasury/db.py +384 -45
  24. dao_treasury/docker-compose.yaml +6 -5
  25. dao_treasury/main.py +86 -17
  26. dao_treasury/sorting/__init__.cpython-310-darwin.so +0 -0
  27. dao_treasury/sorting/__init__.py +171 -42
  28. dao_treasury/sorting/_matchers.cpython-310-darwin.so +0 -0
  29. dao_treasury/sorting/_rules.cpython-310-darwin.so +0 -0
  30. dao_treasury/sorting/_rules.py +1 -3
  31. dao_treasury/sorting/factory.cpython-310-darwin.so +0 -0
  32. dao_treasury/sorting/factory.py +2 -6
  33. dao_treasury/sorting/rule.cpython-310-darwin.so +0 -0
  34. dao_treasury/sorting/rule.py +13 -10
  35. dao_treasury/sorting/rules/__init__.cpython-310-darwin.so +0 -0
  36. dao_treasury/sorting/rules/__init__.py +1 -0
  37. dao_treasury/sorting/rules/ignore/__init__.cpython-310-darwin.so +0 -0
  38. dao_treasury/sorting/rules/ignore/__init__.py +1 -0
  39. dao_treasury/sorting/rules/ignore/llamapay.cpython-310-darwin.so +0 -0
  40. dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
  41. dao_treasury/streams/__init__.cpython-310-darwin.so +0 -0
  42. dao_treasury/streams/__init__.py +0 -0
  43. dao_treasury/streams/llamapay.cpython-310-darwin.so +0 -0
  44. dao_treasury/streams/llamapay.py +388 -0
  45. dao_treasury/treasury.py +75 -28
  46. dao_treasury/types.cpython-310-darwin.so +0 -0
  47. dao_treasury-0.0.69.dist-info/METADATA +120 -0
  48. dao_treasury-0.0.69.dist-info/RECORD +54 -0
  49. dao_treasury-0.0.69.dist-info/top_level.txt +2 -0
  50. dao_treasury__mypyc.cpython-310-darwin.so +0 -0
  51. 52b51d40e96d4333695d__mypyc.cpython-310-darwin.so +0 -0
  52. dao_treasury/.grafana/provisioning/datasources/sqlite.yaml +0 -10
  53. dao_treasury-0.0.22.dist-info/METADATA +0 -63
  54. dao_treasury-0.0.22.dist-info/RECORD +0 -31
  55. dao_treasury-0.0.22.dist-info/top_level.txt +0 -2
  56. {dao_treasury-0.0.22.dist-info → dao_treasury-0.0.69.dist-info}/WHEEL +0 -0
@@ -1,18 +1,18 @@
1
1
  networks:
2
2
  dao_treasury:
3
-
4
- volumes:
5
- grafana_data: {}
3
+ docker_eth_portfolio:
4
+ external: true
6
5
 
7
6
  services:
8
7
  grafana:
9
- image: grafana/grafana:10.2.0
8
+ image: grafana/grafana:12.2.1
10
9
  ports:
11
10
  - 127.0.0.1:${DAO_TREASURY_GRAFANA_PORT:-3004}:3000
12
11
  environment:
13
12
  - GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER:-admin}
14
13
  - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-admin}
15
14
  - GF_AUTH_ANONYMOUS_ENABLED=true
15
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
16
16
  - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/summary/Monthly.json
17
17
  - GF_SERVER_ROOT_URL
18
18
  - GF_RENDERING_SERVER_URL=http://renderer:8091/render
@@ -21,10 +21,10 @@ services:
21
21
  - GF_INSTALL_PLUGINS=volkovlabs-variable-panel,frser-sqlite-datasource
22
22
  volumes:
23
23
  - ~/.dao-treasury/:/app/dao-treasury-data
24
- - grafana_data:/var/lib/grafana
25
24
  - ./.grafana/provisioning/:/etc/grafana/provisioning/
26
25
  networks:
27
26
  - dao_treasury
27
+ - docker_eth_portfolio
28
28
  restart: always
29
29
 
30
30
  renderer:
@@ -37,4 +37,5 @@ services:
37
37
  - HTTP_PORT=8091
38
38
  networks:
39
39
  - dao_treasury
40
+ - docker_eth_portfolio
40
41
  restart: always
dao_treasury/main.py CHANGED
@@ -28,14 +28,19 @@ from pathlib import Path
28
28
 
29
29
  import brownie
30
30
  import yaml
31
+ from a_sync import create_task
32
+ from dao_treasury._wallet import load_wallets_from_yaml
33
+ from eth_portfolio_scripts.balances import export_balances
31
34
  from eth_typing import BlockNumber
32
35
 
33
- from eth_portfolio_scripts.balances import export_balances
36
+ from dao_treasury.constants import CHAINID
34
37
 
35
38
 
36
39
  logger = logging.getLogger(__name__)
40
+
37
41
  logging.basicConfig(level=logging.INFO)
38
42
 
43
+
39
44
  parser = argparse.ArgumentParser(
40
45
  description="Run a single DAO Treasury export and populate the database.",
41
46
  )
@@ -50,16 +55,29 @@ parser.add_argument(
50
55
  type=str,
51
56
  help=(
52
57
  "DAO treasury wallet address(es) to include in the export. "
53
- "Specify one or more addresses separated by spaces."
58
+ "Specify one or more addresses separated by spaces. "
59
+ "Check out https://bobthebuidler.github.io/dao-treasury/wallets.html for more info."
54
60
  ),
55
61
  nargs="+",
56
62
  )
63
+ parser.add_argument(
64
+ "--wallets",
65
+ type=Path,
66
+ help=(
67
+ "Path to a YAML file mapping wallet addresses to advanced settings. "
68
+ "Each address is a key, with nested 'start' and/or 'end' mappings containing "
69
+ "either 'block' or 'timestamp'. "
70
+ "Check out https://bobthebuidler.github.io/dao-treasury/wallets.html for more info."
71
+ ),
72
+ default=None,
73
+ )
57
74
  parser.add_argument(
58
75
  "--sort-rules",
59
76
  type=Path,
60
77
  help=(
61
78
  "Directory containing sort rules definitions. "
62
- "If omitted, transactions are exported without custom sorting."
79
+ "If omitted, transactions are exported without custom sorting. "
80
+ "Check out https://bobthebuidler.github.io/dao-treasury/sort_rules.html for more info."
63
81
  ),
64
82
  default=None,
65
83
  )
@@ -79,6 +97,12 @@ parser.add_argument(
79
97
  help="The time interval between datapoints. default: 1d",
80
98
  default="1d",
81
99
  )
100
+ parser.add_argument(
101
+ "--concurrency",
102
+ type=int,
103
+ help="The max number of historical blocks to export concurrently. default: 30",
104
+ default=30,
105
+ )
82
106
  parser.add_argument(
83
107
  "--daemon",
84
108
  action="store_true",
@@ -90,6 +114,11 @@ parser.add_argument(
90
114
  help="Port for the DAO Treasury dashboard web interface. Default: 3000",
91
115
  default=3000,
92
116
  )
117
+ parser.add_argument(
118
+ "--start-renderer",
119
+ action="store_true",
120
+ help="If set, the Grafana renderer container will be started for dashboard image export. By default, only the grafana container is started.",
121
+ )
93
122
  parser.add_argument(
94
123
  "--renderer-port",
95
124
  type=int,
@@ -131,16 +160,13 @@ async def export(args) -> None:
131
160
 
132
161
  Args:
133
162
  args: Parsed command-line arguments containing:
134
- wallet: Treasury wallet address strings.
163
+ wallet: List of simple addresses or TreasuryWallet instances.
135
164
  sort_rules: Directory of sorting rules.
136
165
  interval: Time interval for balance snapshots.
137
166
  daemon: Ignored flag.
138
167
  grafana_port: Port for Grafana (sets DAO_TREASURY_GRAFANA_PORT).
139
168
  renderer_port: Port for renderer (sets DAO_TREASURY_RENDERER_PORT).
140
-
141
- Note:
142
- Inside this coroutine, the environment variable GRAFANA_PORT is overridden to "3003"
143
- to satisfy current dashboard requirements.
169
+ start_renderer: If True, start renderer; otherwise, only start grafana.
144
170
 
145
171
  Example:
146
172
  In code::
@@ -152,9 +178,24 @@ async def export(args) -> None:
152
178
  :func:`dao_treasury._docker.down`,
153
179
  :class:`dao_treasury.Treasury.populate_db`
154
180
  """
155
- from y.constants import CHAINID
181
+ import eth_portfolio_scripts.docker
182
+
183
+ from dao_treasury import _docker, constants, db, Treasury
184
+
185
+ wallets = getattr(args, "wallet", None)
186
+ wallets_advanced = getattr(args, "wallets", None)
156
187
 
157
- from dao_treasury import _docker, db, Treasury
188
+ # Ensure user does not supply both simple and advanced wallet inputs
189
+ if wallets and wallets_advanced:
190
+ parser.error("Cannot specify both --wallet and --wallets")
191
+
192
+ # Load advanced wallets from YAML if --wallets provided
193
+ if wallets_advanced:
194
+ wallets = load_wallets_from_yaml(wallets_advanced)
195
+
196
+ # Ensure at least one wallet source is provided
197
+ if not wallets:
198
+ parser.error("Must specify either --wallet or --wallets")
158
199
 
159
200
  # TODO: remove this after refactoring eth-port a bit so we arent required to bring up the e-p dashboards
160
201
  os.environ["GRAFANA_PORT"] = "3003"
@@ -162,19 +203,47 @@ async def export(args) -> None:
162
203
  # TODO but make the dashboard files more specific to dao treasury-ing
163
204
 
164
205
  if args.nicknames:
165
- for nickname, addresses in (
166
- yaml.safe_load(args.nicknames.read_bytes()).get(CHAINID, {}).items()
167
- ):
206
+ parsed: dict = yaml.safe_load(args.nicknames.read_bytes())
207
+ active_network_config: dict = parsed.get(constants.CHAINID, {})
208
+ for nickname, addresses in active_network_config.items():
168
209
  for address in addresses:
169
210
  db.Address.set_nickname(address, nickname)
170
211
 
171
- treasury = Treasury(args.wallet, args.sort_rules, asynchronous=True)
172
- _docker.up()
173
- try:
174
- await asyncio.gather(
212
+ treasury = Treasury(wallets, args.sort_rules, asynchronous=True)
213
+
214
+ # Start only the requested containers
215
+ if args.start_renderer is True:
216
+ _docker.up()
217
+ else:
218
+ _docker.up("grafana")
219
+
220
+ # eth-portfolio needs this present
221
+ # TODO: we need to update eth-portfolio to honor wallet join and exit times
222
+ if not getattr(args, "wallet", None):
223
+ args.wallet = [
224
+ wallet.address
225
+ for wallet in wallets
226
+ if wallet.networks is None or CHAINID in wallet.networks
227
+ ]
228
+
229
+ # TODO: make this user configurable? would require some dynamic grafana dashboard files
230
+ args.label = "Treasury"
231
+
232
+ export_task = create_task(
233
+ asyncio.gather(
175
234
  export_balances(args),
176
235
  treasury.populate_db(BlockNumber(0), brownie.chain.height),
177
236
  )
237
+ )
238
+
239
+ await asyncio.sleep(1)
240
+
241
+ # we don't need these containers since dao-treasury uses its own.
242
+ eth_portfolio_scripts.docker.stop("grafana")
243
+ eth_portfolio_scripts.docker.stop("renderer")
244
+
245
+ try:
246
+ await export_task
178
247
  finally:
179
248
  _docker.down()
180
249
 
@@ -1,5 +1,28 @@
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
  """
4
27
 
5
28
  from logging import getLogger
@@ -9,7 +32,7 @@ from eth_portfolio.structs import LedgerEntry
9
32
  from evmspec.data import TransactionHash
10
33
  from y.exceptions import ContractNotVerified
11
34
 
12
- from dao_treasury import db
35
+ from dao_treasury import constants, db
13
36
  from dao_treasury._wallet import TreasuryWallet
14
37
  from dao_treasury.sorting._matchers import (
15
38
  _Matcher,
@@ -35,6 +58,7 @@ from dao_treasury.sorting.rule import (
35
58
  OtherIncomeSortRule,
36
59
  RevenueSortRule,
37
60
  )
61
+ from dao_treasury.sorting.rules import *
38
62
  from dao_treasury.types import TxGroupDbid
39
63
 
40
64
 
@@ -64,19 +88,71 @@ __all__ = [
64
88
 
65
89
  # C constants
66
90
  TxGroup: Final = db.TxGroup
67
- must_sort_inbound_txgroup_dbid: Final = db.must_sort_inbound_txgroup_dbid
68
- 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
+ """
69
118
 
70
119
 
71
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
+
72
150
  txgroup_dbid: Optional[TxGroupDbid] = None
73
- if TreasuryWallet.check_membership(
74
- entry.from_address, entry.block_number
75
- ) and TreasuryWallet.check_membership(entry.to_address, entry.block_number):
76
- txgroup_dbid = TxGroup.get_dbid(
77
- name="Internal Transfer",
78
- parent=TxGroup.get_dbid("Ignore"),
79
- )
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
80
156
 
81
157
  if txgroup_dbid is None:
82
158
  if isinstance(txhash := entry.hash, TransactionHash):
@@ -84,69 +160,122 @@ def sort_basic(entry: LedgerEntry) -> TxGroupDbid:
84
160
  txgroup_dbid = HashMatcher.match(txhash)
85
161
 
86
162
  if txgroup_dbid is None:
87
- txgroup_dbid = FromAddressMatcher.match(entry.from_address)
163
+ txgroup_dbid = FromAddressMatcher.match(from_address)
88
164
 
89
165
  if txgroup_dbid is None:
90
- txgroup_dbid = ToAddressMatcher.match(entry.to_address)
166
+ txgroup_dbid = ToAddressMatcher.match(to_address)
91
167
 
92
168
  if txgroup_dbid is None:
93
- if TreasuryWallet.check_membership(entry.from_address, entry.block_number):
94
- txgroup_dbid = must_sort_outbound_txgroup_dbid
169
+ if TreasuryWallet.check_membership(from_address, block):
170
+ txgroup_dbid = MUST_SORT_OUTBOUND_TXGROUP_DBID
95
171
 
96
- elif TreasuryWallet.check_membership(entry.to_address, entry.block_number):
97
- txgroup_dbid = must_sort_inbound_txgroup_dbid
172
+ elif TreasuryWallet.check_membership(to_address, block):
173
+ txgroup_dbid = MUST_SORT_INBOUND_TXGROUP_DBID
98
174
 
99
175
  else:
100
176
  raise NotImplementedError("this isnt supposed to happen")
101
177
  return txgroup_dbid # type: ignore [no-any-return]
102
178
 
103
179
 
104
- 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
+
105
204
  txgroup_dbid: Optional[TxGroupDbid] = None
106
- if (
107
- entry.from_address
108
- and TreasuryWallet.check_membership(entry.from_address.address, entry.block)
109
- and TreasuryWallet.check_membership(entry.to_address.address, entry.block)
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
110
211
  ):
111
- txgroup_dbid = TxGroup.get_dbid(
112
- name="Internal Transfer",
113
- parent=TxGroup.get_dbid("Ignore"),
114
- )
212
+ txgroup_dbid = OUT_OF_RANGE_TXGROUP_DBID
115
213
 
116
214
  if txgroup_dbid is None:
117
- txgroup_dbid = HashMatcher.match(entry.hash)
215
+ txgroup_dbid = HashMatcher.match(tx.hash)
118
216
 
119
217
  if txgroup_dbid is None:
120
- txgroup_dbid = FromAddressMatcher.match(entry.from_address.address)
218
+ txgroup_dbid = FromAddressMatcher.match(from_address)
121
219
 
122
- if txgroup_dbid is None and entry.to_address:
123
- 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)
124
222
 
125
223
  if txgroup_dbid is None:
126
- if TreasuryWallet.check_membership(entry.from_address.address, entry.block):
127
- txgroup_dbid = must_sort_outbound_txgroup_dbid
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
128
229
 
129
- elif TreasuryWallet.check_membership(entry.to_address.address, entry.block):
130
- txgroup_dbid = must_sort_inbound_txgroup_dbid
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
131
235
 
132
236
  else:
133
237
  raise NotImplementedError("this isnt supposed to happen")
134
238
 
135
239
  if txgroup_dbid not in (
136
- must_sort_inbound_txgroup_dbid,
137
- must_sort_outbound_txgroup_dbid,
240
+ MUST_SORT_INBOUND_TXGROUP_DBID,
241
+ MUST_SORT_OUTBOUND_TXGROUP_DBID,
138
242
  ):
139
- logger.info("Sorted %s to %s", entry, TxGroup.get_fullname(txgroup_dbid))
243
+ logger.info("Sorted %s to %s", tx, TxGroup.get_fullname(txgroup_dbid))
140
244
 
141
245
  return txgroup_dbid # type: ignore [no-any-return]
142
246
 
143
247
 
144
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
+ """
145
274
  txgroup_dbid = sort_basic_entity(entry)
146
275
 
147
276
  if txgroup_dbid in (
148
- must_sort_inbound_txgroup_dbid,
149
- must_sort_outbound_txgroup_dbid,
277
+ MUST_SORT_INBOUND_TXGROUP_DBID,
278
+ MUST_SORT_OUTBOUND_TXGROUP_DBID,
150
279
  ):
151
280
  for rules in SORT_RULES.values():
152
281
  for rule in rules:
@@ -157,10 +286,10 @@ async def sort_advanced(entry: db.TreasuryTx) -> TxGroupDbid:
157
286
  except ContractNotVerified:
158
287
  continue
159
288
  if txgroup_dbid not in (
160
- must_sort_inbound_txgroup_dbid,
161
- must_sort_outbound_txgroup_dbid,
289
+ MUST_SORT_INBOUND_TXGROUP_DBID,
290
+ MUST_SORT_OUTBOUND_TXGROUP_DBID,
162
291
  ):
163
292
  logger.info("Sorted %s to %s", entry, TxGroup.get_fullname(txgroup_dbid))
164
- entry.txgroup = txgroup_dbid
293
+ await entry._set_txgroup(txgroup_dbid)
165
294
 
166
295
  return txgroup_dbid # type: ignore [no-any-return]
@@ -4,8 +4,8 @@ from typing import Final, Type, Union, final
4
4
 
5
5
  import yaml
6
6
  from pony.orm import db_session
7
- from y import constants
8
7
 
8
+ from dao_treasury.constants import CHAINID
9
9
  from dao_treasury.sorting import (
10
10
  _Matcher,
11
11
  FromAddressMatcher,
@@ -15,8 +15,6 @@ from dao_treasury.sorting import (
15
15
  from dao_treasury.types import TopLevelCategory, TxGroupDbid
16
16
 
17
17
 
18
- CHAINID: Final = constants.CHAINID
19
-
20
18
  logger: Final = getLogger("dao_treasury.rules")
21
19
 
22
20
 
@@ -1,7 +1,6 @@
1
- from typing import Any, Final, Generic, Optional, TypeVar, Union, final, overload
2
-
3
- from y import constants
1
+ from typing import Any, Final, Generic, Optional, Union, final, overload
4
2
 
3
+ from dao_treasury.constants import CHAINID
5
4
  from dao_treasury.sorting.rule import (
6
5
  CostOfRevenueSortRule,
7
6
  ExpenseSortRule,
@@ -14,9 +13,6 @@ from dao_treasury.sorting.rule import (
14
13
  from dao_treasury.types import Networks, SortFunction, TxGroupName
15
14
 
16
15
 
17
- CHAINID: Final = constants.CHAINID
18
-
19
-
20
16
  def revenue(
21
17
  txgroup: TxGroupName, networks: Networks = CHAINID
22
18
  ) -> "SortRuleFactory[RevenueSortRule]":
@@ -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
 
@@ -126,8 +130,6 @@ class _SortRule:
126
130
  func: Optional[SortFunction] = None
127
131
  """Custom matching function that takes a `TreasuryTx` and returns a bool or an awaitable that returns a bool."""
128
132
 
129
- # __instances__: ClassVar[List[Self]] = []
130
-
131
133
  def __post_init__(self) -> None:
132
134
  """Validate inputs, checksum addresses, and register the rule.
133
135
 
@@ -214,6 +216,7 @@ class _SortRule:
214
216
  getattr(tx, matcher) == getattr(self, matcher) for matcher in matchers
215
217
  )
216
218
 
219
+ _log_debug("checking %s for %s", tx, self.func)
217
220
  match = self.func(tx) # type: ignore [misc]
218
221
  return match if isinstance(match, bool) else await match
219
222
 
@@ -230,7 +233,7 @@ class _InboundSortRule(_SortRule):
230
233
  return (
231
234
  tx.to_address is not None
232
235
  and TreasuryWallet.check_membership(tx.to_address.address, tx.block)
233
- and await super().match(tx)
236
+ and await super(_InboundSortRule, self).match(tx)
234
237
  )
235
238
 
236
239
 
@@ -245,7 +248,7 @@ class _OutboundSortRule(_SortRule):
245
248
  async def match(self, tx: "TreasuryTx") -> bool:
246
249
  return TreasuryWallet.check_membership(
247
250
  tx.from_address.address, tx.block
248
- ) and await super().match(tx)
251
+ ) and await super(_OutboundSortRule, self).match(tx)
249
252
 
250
253
 
251
254
  @mypyc_attr(native_class=False)
@@ -262,7 +265,7 @@ class RevenueSortRule(_InboundSortRule):
262
265
  def __post_init__(self) -> None:
263
266
  """Prepends `self.txgroup` with 'Revenue:'."""
264
267
  object.__setattr__(self, "txgroup", f"Revenue:{self.txgroup}")
265
- super().__post_init__()
268
+ super(RevenueSortRule, self).__post_init__()
266
269
 
267
270
 
268
271
  @mypyc_attr(native_class=False)
@@ -275,7 +278,7 @@ class CostOfRevenueSortRule(_OutboundSortRule):
275
278
  def __post_init__(self) -> None:
276
279
  """Prepends `self.txgroup` with 'Cost of Revenue:'."""
277
280
  object.__setattr__(self, "txgroup", f"Cost of Revenue:{self.txgroup}")
278
- super().__post_init__()
281
+ super(CostOfRevenueSortRule, self).__post_init__()
279
282
 
280
283
 
281
284
  @mypyc_attr(native_class=False)
@@ -288,7 +291,7 @@ class ExpenseSortRule(_OutboundSortRule):
288
291
  def __post_init__(self) -> None:
289
292
  """Prepends `self.txgroup` with 'Expenses:'."""
290
293
  object.__setattr__(self, "txgroup", f"Expenses:{self.txgroup}")
291
- super().__post_init__()
294
+ super(ExpenseSortRule, self).__post_init__()
292
295
 
293
296
 
294
297
  @mypyc_attr(native_class=False)
@@ -301,7 +304,7 @@ class OtherIncomeSortRule(_InboundSortRule):
301
304
  def __post_init__(self) -> None:
302
305
  """Prepends `self.txgroup` with 'Other Income:'."""
303
306
  object.__setattr__(self, "txgroup", f"Other Income:{self.txgroup}")
304
- super().__post_init__()
307
+ super(OtherIncomeSortRule, self).__post_init__()
305
308
 
306
309
 
307
310
  @mypyc_attr(native_class=False)
@@ -314,7 +317,7 @@ class OtherExpenseSortRule(_OutboundSortRule):
314
317
  def __post_init__(self) -> None:
315
318
  """Prepends `self.txgroup` with 'Other Expenses:'."""
316
319
  object.__setattr__(self, "txgroup", f"Other Expenses:{self.txgroup}")
317
- super().__post_init__()
320
+ super(OtherExpenseSortRule, self).__post_init__()
318
321
 
319
322
 
320
323
  @mypyc_attr(native_class=False)
@@ -327,7 +330,7 @@ class IgnoreSortRule(_SortRule):
327
330
  def __post_init__(self) -> None:
328
331
  """Prepends `self.txgroup` with 'Ignore:'."""
329
332
  object.__setattr__(self, "txgroup", f"Ignore:{self.txgroup}")
330
- super().__post_init__()
333
+ super(IgnoreSortRule, self).__post_init__()
331
334
 
332
335
 
333
336
  TRule = TypeVar(
@@ -0,0 +1 @@
1
+ from dao_treasury.sorting.rules.ignore import *
@@ -0,0 +1 @@
1
+ from dao_treasury.sorting.rules.ignore.llamapay import *