dao-treasury 0.0.42__cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_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 (51) hide show
  1. bf2b4fe1f86ad2ea158b__mypyc.cpython-311-i386-linux-gnu.so +0 -0
  2. dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +60 -0
  3. dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +225 -0
  4. dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +107 -0
  5. dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +387 -0
  6. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +835 -0
  7. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +615 -0
  8. dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +492 -0
  9. dao_treasury/.grafana/provisioning/dashboards/treasury/Treasury.json +2018 -0
  10. dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
  11. dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
  12. dao_treasury/__init__.py +62 -0
  13. dao_treasury/_docker.cpython-311-i386-linux-gnu.so +0 -0
  14. dao_treasury/_docker.py +190 -0
  15. dao_treasury/_nicknames.cpython-311-i386-linux-gnu.so +0 -0
  16. dao_treasury/_nicknames.py +32 -0
  17. dao_treasury/_wallet.cpython-311-i386-linux-gnu.so +0 -0
  18. dao_treasury/_wallet.py +250 -0
  19. dao_treasury/constants.cpython-311-i386-linux-gnu.so +0 -0
  20. dao_treasury/constants.py +34 -0
  21. dao_treasury/db.py +1408 -0
  22. dao_treasury/docker-compose.yaml +41 -0
  23. dao_treasury/main.py +247 -0
  24. dao_treasury/py.typed +0 -0
  25. dao_treasury/sorting/__init__.cpython-311-i386-linux-gnu.so +0 -0
  26. dao_treasury/sorting/__init__.py +295 -0
  27. dao_treasury/sorting/_matchers.cpython-311-i386-linux-gnu.so +0 -0
  28. dao_treasury/sorting/_matchers.py +387 -0
  29. dao_treasury/sorting/_rules.cpython-311-i386-linux-gnu.so +0 -0
  30. dao_treasury/sorting/_rules.py +235 -0
  31. dao_treasury/sorting/factory.cpython-311-i386-linux-gnu.so +0 -0
  32. dao_treasury/sorting/factory.py +299 -0
  33. dao_treasury/sorting/rule.cpython-311-i386-linux-gnu.so +0 -0
  34. dao_treasury/sorting/rule.py +346 -0
  35. dao_treasury/sorting/rules/__init__.cpython-311-i386-linux-gnu.so +0 -0
  36. dao_treasury/sorting/rules/__init__.py +1 -0
  37. dao_treasury/sorting/rules/ignore/__init__.cpython-311-i386-linux-gnu.so +0 -0
  38. dao_treasury/sorting/rules/ignore/__init__.py +1 -0
  39. dao_treasury/sorting/rules/ignore/llamapay.cpython-311-i386-linux-gnu.so +0 -0
  40. dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
  41. dao_treasury/streams/__init__.cpython-311-i386-linux-gnu.so +0 -0
  42. dao_treasury/streams/__init__.py +0 -0
  43. dao_treasury/streams/llamapay.cpython-311-i386-linux-gnu.so +0 -0
  44. dao_treasury/streams/llamapay.py +388 -0
  45. dao_treasury/treasury.py +191 -0
  46. dao_treasury/types.cpython-311-i386-linux-gnu.so +0 -0
  47. dao_treasury/types.py +133 -0
  48. dao_treasury-0.0.42.dist-info/METADATA +119 -0
  49. dao_treasury-0.0.42.dist-info/RECORD +51 -0
  50. dao_treasury-0.0.42.dist-info/WHEEL +7 -0
  51. dao_treasury-0.0.42.dist-info/top_level.txt +2 -0
@@ -0,0 +1,41 @@
1
+ networks:
2
+ dao_treasury:
3
+ docker_eth_portfolio:
4
+ external: true
5
+
6
+ services:
7
+ grafana:
8
+ image: grafana/grafana:10.2.0
9
+ ports:
10
+ - 127.0.0.1:${DAO_TREASURY_GRAFANA_PORT:-3004}:3000
11
+ environment:
12
+ - GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER:-admin}
13
+ - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-admin}
14
+ - GF_AUTH_ANONYMOUS_ENABLED=true
15
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
16
+ - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/summary/Monthly.json
17
+ - GF_SERVER_ROOT_URL
18
+ - GF_RENDERING_SERVER_URL=http://renderer:8091/render
19
+ - GF_RENDERING_CALLBACK_URL=http://grafana:3000/
20
+ - GF_LOG_FILTERS=rendering:debug
21
+ - GF_INSTALL_PLUGINS=volkovlabs-variable-panel,frser-sqlite-datasource
22
+ volumes:
23
+ - ~/.dao-treasury/:/app/dao-treasury-data
24
+ - ./.grafana/provisioning/:/etc/grafana/provisioning/
25
+ networks:
26
+ - dao_treasury
27
+ - docker_eth_portfolio
28
+ restart: always
29
+
30
+ renderer:
31
+ platform: linux/amd64
32
+ image: grafana/grafana-image-renderer:latest
33
+ ports:
34
+ - 127.0.0.1:${DAO_TREASURY_RENDERER_PORT:-8092}:8091
35
+ environment:
36
+ - ENABLE_METRICS=true
37
+ - HTTP_PORT=8091
38
+ networks:
39
+ - dao_treasury
40
+ - docker_eth_portfolio
41
+ restart: always
dao_treasury/main.py ADDED
@@ -0,0 +1,247 @@
1
+ """Command-line interface for exporting DAO treasury transactions.
2
+
3
+ This module parses command-line arguments, sets up environment variables for
4
+ Grafana and its renderer, and defines the entrypoint for a one-time export of
5
+ DAO treasury transactions. It populates the local SQLite database and starts
6
+ the required Docker services for Grafana dashboards. Transactions are fetched
7
+ via :class:`dao_treasury.Treasury`, sorted according to optional rules, and
8
+ inserted using the database routines (:func:`dao_treasury.db.TreasuryTx.insert`).
9
+
10
+ Example:
11
+ Running from the shell::
12
+
13
+ $ dao-treasury --network mainnet --sort-rules ./rules --wallet 0xABC123... \
14
+ --interval 6h --grafana-port 3000 --renderer-port 8091
15
+
16
+ See Also:
17
+ :func:`dao_treasury._docker.up`,
18
+ :func:`dao_treasury._docker.down`,
19
+ :class:`dao_treasury.Treasury`,
20
+ :func:`dao_treasury.db.TreasuryTx.insert`
21
+ """
22
+
23
+ import argparse
24
+ import asyncio
25
+ import logging
26
+ import os
27
+ from pathlib import Path
28
+
29
+ import brownie
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
34
+ from eth_typing import BlockNumber
35
+
36
+ from dao_treasury.constants import CHAINID
37
+
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ logging.basicConfig(level=logging.INFO)
42
+
43
+
44
+ parser = argparse.ArgumentParser(
45
+ description="Run a single DAO Treasury export and populate the database.",
46
+ )
47
+ parser.add_argument(
48
+ "--network",
49
+ type=str,
50
+ help="Brownie network identifier for the RPC to use. Default: mainnet",
51
+ default="mainnet",
52
+ )
53
+ parser.add_argument(
54
+ "--wallet",
55
+ type=str,
56
+ help=(
57
+ "DAO treasury wallet address(es) to include in the export. "
58
+ "Specify one or more addresses separated by spaces. "
59
+ "Check out https://bobthebuidler.github.io/dao-treasury/wallets.html for more info."
60
+ ),
61
+ nargs="+",
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
+ )
74
+ parser.add_argument(
75
+ "--sort-rules",
76
+ type=Path,
77
+ help=(
78
+ "Directory containing sort rules definitions. "
79
+ "If omitted, transactions are exported without custom sorting. "
80
+ "Check out https://bobthebuidler.github.io/dao-treasury/sort_rules.html for more info."
81
+ ),
82
+ default=None,
83
+ )
84
+ parser.add_argument(
85
+ "--nicknames",
86
+ type=Path,
87
+ help=(
88
+ "File containing sort address nicknames. "
89
+ "If omitted, transactions are exported without custom sorting. "
90
+ "See https://github.com/BobTheBuidler/yearn-treasury/blob/master/yearn_treasury/addresses.yaml for an example."
91
+ ),
92
+ default=None,
93
+ )
94
+ parser.add_argument(
95
+ "--interval",
96
+ type=str,
97
+ help="The time interval between datapoints. default: 1d",
98
+ default="1d",
99
+ )
100
+ parser.add_argument(
101
+ "--daemon",
102
+ action="store_true",
103
+ help="TODO: If True, run as a background daemon. Not currently supported.",
104
+ )
105
+ parser.add_argument(
106
+ "--grafana-port",
107
+ type=int,
108
+ help="Port for the DAO Treasury dashboard web interface. Default: 3000",
109
+ default=3000,
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
+ )
116
+ parser.add_argument(
117
+ "--renderer-port",
118
+ type=int,
119
+ help="Port for the Grafana rendering service. Default: 8091",
120
+ default=8091,
121
+ )
122
+
123
+ args = parser.parse_args()
124
+
125
+ os.environ["DAO_TREASURY_GRAFANA_PORT"] = str(args.grafana_port)
126
+ os.environ["DAO_TREASURY_RENDERER_PORT"] = str(args.renderer_port)
127
+
128
+
129
+ # TODO: run forever arg
130
+ def main() -> None:
131
+ """Entrypoint for the `dao-treasury` console script.
132
+
133
+ This function invokes the export coroutine using the arguments parsed at import time.
134
+ It runs the asynchronous export to completion.
135
+
136
+ Example:
137
+ From the command line::
138
+
139
+ $ dao-treasury --network mainnet --sort-rules=./rules --wallet 0xABC123... 0xDEF456...
140
+
141
+ See Also:
142
+ :func:`export`
143
+ """
144
+ asyncio.get_event_loop().run_until_complete(export(args))
145
+
146
+
147
+ async def export(args) -> None:
148
+ """Perform one-time export of treasury transactions and manage Docker services.
149
+
150
+ This coroutine creates a :class:`dao_treasury.Treasury` instance using the
151
+ provided wallets and sort rules, brings up the Grafana and renderer containers,
152
+ then concurrently exports balance snapshots and populates the transaction database
153
+ for blocks from 0 to the current chain height.
154
+
155
+ Args:
156
+ args: Parsed command-line arguments containing:
157
+ wallet: List of simple addresses or TreasuryWallet instances.
158
+ sort_rules: Directory of sorting rules.
159
+ interval: Time interval for balance snapshots.
160
+ daemon: Ignored flag.
161
+ grafana_port: Port for Grafana (sets DAO_TREASURY_GRAFANA_PORT).
162
+ renderer_port: Port for renderer (sets DAO_TREASURY_RENDERER_PORT).
163
+ start_renderer: If True, start renderer; otherwise, only start grafana.
164
+
165
+ Example:
166
+ In code::
167
+
168
+ await export(args) # where args come from parser.parse_args()
169
+
170
+ See Also:
171
+ :func:`dao_treasury._docker.up`,
172
+ :func:`dao_treasury._docker.down`,
173
+ :class:`dao_treasury.Treasury.populate_db`
174
+ """
175
+ import eth_portfolio_scripts.docker
176
+
177
+ from dao_treasury import _docker, constants, db, Treasury
178
+
179
+ wallets = getattr(args, "wallet", None)
180
+ wallets_advanced = getattr(args, "wallets", None)
181
+
182
+ # Ensure user does not supply both simple and advanced wallet inputs
183
+ if wallets and wallets_advanced:
184
+ parser.error("Cannot specify both --wallet and --wallets")
185
+
186
+ # Load advanced wallets from YAML if --wallets provided
187
+ if wallets_advanced:
188
+ wallets = load_wallets_from_yaml(wallets_advanced)
189
+
190
+ # Ensure at least one wallet source is provided
191
+ if not wallets:
192
+ parser.error("Must specify either --wallet or --wallets")
193
+
194
+ # TODO: remove this after refactoring eth-port a bit so we arent required to bring up the e-p dashboards
195
+ os.environ["GRAFANA_PORT"] = "3003"
196
+
197
+ # TODO but make the dashboard files more specific to dao treasury-ing
198
+
199
+ if args.nicknames:
200
+ parsed: dict = yaml.safe_load(args.nicknames.read_bytes())
201
+ active_network_config: dict = parsed.get(constants.CHAINID, {})
202
+ for nickname, addresses in active_network_config.items():
203
+ for address in addresses:
204
+ db.Address.set_nickname(address, nickname)
205
+
206
+ treasury = Treasury(wallets, args.sort_rules, asynchronous=True)
207
+
208
+ # Start only the requested containers
209
+ if args.start_renderer is True:
210
+ _docker.up()
211
+ else:
212
+ _docker.up("grafana")
213
+
214
+ # eth-portfolio needs this present
215
+ # TODO: we need to update eth-portfolio to honor wallet join and exit times
216
+ if not getattr(args, "wallet", None):
217
+ args.wallet = [
218
+ wallet.address
219
+ for wallet in wallets
220
+ if wallet.networks is None or CHAINID in wallet.networks
221
+ ]
222
+
223
+ # TODO: make this user configurable? would require some dynamic grafana dashboard files
224
+ args.label = "Treasury"
225
+
226
+ export_task = create_task(
227
+ asyncio.gather(
228
+ export_balances(args),
229
+ treasury.populate_db(BlockNumber(0), brownie.chain.height),
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
241
+ finally:
242
+ _docker.down()
243
+
244
+
245
+ if __name__ == "__main__":
246
+ os.environ["BROWNIE_NETWORK_ID"] = args.network
247
+ brownie.project.run(__file__)
dao_treasury/py.typed ADDED
File without changes
@@ -0,0 +1,295 @@
1
+ """
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`
26
+ """
27
+
28
+ from logging import getLogger
29
+ from typing import Final, Optional
30
+
31
+ from eth_portfolio.structs import LedgerEntry
32
+ from evmspec.data import TransactionHash
33
+ from y.exceptions import ContractNotVerified
34
+
35
+ from dao_treasury import constants, db
36
+ from dao_treasury._wallet import TreasuryWallet
37
+ from dao_treasury.sorting._matchers import (
38
+ _Matcher,
39
+ FromAddressMatcher,
40
+ HashMatcher,
41
+ ToAddressMatcher,
42
+ )
43
+ from dao_treasury.sorting.factory import (
44
+ SortRuleFactory,
45
+ cost_of_revenue,
46
+ expense,
47
+ ignore,
48
+ other_expense,
49
+ other_income,
50
+ revenue,
51
+ )
52
+ from dao_treasury.sorting.rule import (
53
+ SORT_RULES,
54
+ CostOfRevenueSortRule,
55
+ ExpenseSortRule,
56
+ IgnoreSortRule,
57
+ OtherExpenseSortRule,
58
+ OtherIncomeSortRule,
59
+ RevenueSortRule,
60
+ )
61
+ from dao_treasury.sorting.rules import *
62
+ from dao_treasury.types import TxGroupDbid
63
+
64
+
65
+ logger: Final = getLogger("dao_treasury.sorting")
66
+
67
+
68
+ __all__ = [
69
+ "CostOfRevenueSortRule",
70
+ "ExpenseSortRule",
71
+ "IgnoreSortRule",
72
+ "OtherExpenseSortRule",
73
+ "OtherIncomeSortRule",
74
+ "RevenueSortRule",
75
+ "cost_of_revenue",
76
+ "expense",
77
+ "ignore",
78
+ "other_expense",
79
+ "other_income",
80
+ "revenue",
81
+ "SortRuleFactory",
82
+ "HashMatcher",
83
+ "FromAddressMatcher",
84
+ "ToAddressMatcher",
85
+ "SORT_RULES",
86
+ "_Matcher",
87
+ ]
88
+
89
+ # C constants
90
+ TxGroup: Final = db.TxGroup
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
+ """
118
+
119
+
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
+
150
+ txgroup_dbid: Optional[TxGroupDbid] = None
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
156
+
157
+ if txgroup_dbid is None:
158
+ if isinstance(txhash := entry.hash, TransactionHash):
159
+ txhash = txhash.hex()
160
+ txgroup_dbid = HashMatcher.match(txhash)
161
+
162
+ if txgroup_dbid is None:
163
+ txgroup_dbid = FromAddressMatcher.match(from_address)
164
+
165
+ if txgroup_dbid is None:
166
+ txgroup_dbid = ToAddressMatcher.match(to_address)
167
+
168
+ if txgroup_dbid is None:
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
+
175
+ else:
176
+ raise NotImplementedError("this isnt supposed to happen")
177
+ return txgroup_dbid # type: ignore [no-any-return]
178
+
179
+
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
+
204
+ txgroup_dbid: Optional[TxGroupDbid] = None
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
213
+
214
+ if txgroup_dbid is None:
215
+ txgroup_dbid = HashMatcher.match(tx.hash)
216
+
217
+ if txgroup_dbid is None:
218
+ txgroup_dbid = FromAddressMatcher.match(from_address)
219
+
220
+ if txgroup_dbid is None and to_address:
221
+ txgroup_dbid = ToAddressMatcher.match(to_address.address)
222
+
223
+ if txgroup_dbid is None:
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
+
236
+ else:
237
+ raise NotImplementedError("this isnt supposed to happen")
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
+
245
+ return txgroup_dbid # type: ignore [no-any-return]
246
+
247
+
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
+ """
274
+ txgroup_dbid = sort_basic_entity(entry)
275
+
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
+
295
+ return txgroup_dbid # type: ignore [no-any-return]