dao-treasury 0.0.22__cp310-cp310-macosx_11_0_arm64.whl → 0.0.69__cp310-cp310-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dao_treasury/.grafana/provisioning/dashboards/breakdowns/Expenses.json +551 -0
- dao_treasury/.grafana/provisioning/dashboards/breakdowns/Revenue.json +551 -0
- dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +7 -7
- dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +220 -0
- dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +18 -23
- dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +181 -29
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +808 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +602 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Current Treasury Assets.json +1009 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Historical Treasury Balances.json +2989 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +478 -0
- dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
- dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
- dao_treasury/__init__.py +20 -0
- dao_treasury/_docker.cpython-310-darwin.so +0 -0
- dao_treasury/_docker.py +67 -38
- dao_treasury/_nicknames.cpython-310-darwin.so +0 -0
- dao_treasury/_nicknames.py +24 -2
- dao_treasury/_wallet.cpython-310-darwin.so +0 -0
- dao_treasury/_wallet.py +157 -16
- dao_treasury/constants.cpython-310-darwin.so +0 -0
- dao_treasury/constants.py +39 -0
- dao_treasury/db.py +384 -45
- dao_treasury/docker-compose.yaml +6 -5
- dao_treasury/main.py +86 -17
- dao_treasury/sorting/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/__init__.py +171 -42
- dao_treasury/sorting/_matchers.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/_rules.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/_rules.py +1 -3
- dao_treasury/sorting/factory.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/factory.py +2 -6
- dao_treasury/sorting/rule.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rule.py +13 -10
- dao_treasury/sorting/rules/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rules/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rules/ignore/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/llamapay.cpython-310-darwin.so +0 -0
- dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
- dao_treasury/streams/__init__.cpython-310-darwin.so +0 -0
- dao_treasury/streams/__init__.py +0 -0
- dao_treasury/streams/llamapay.cpython-310-darwin.so +0 -0
- dao_treasury/streams/llamapay.py +388 -0
- dao_treasury/treasury.py +75 -28
- dao_treasury/types.cpython-310-darwin.so +0 -0
- dao_treasury-0.0.69.dist-info/METADATA +120 -0
- dao_treasury-0.0.69.dist-info/RECORD +54 -0
- dao_treasury-0.0.69.dist-info/top_level.txt +2 -0
- dao_treasury__mypyc.cpython-310-darwin.so +0 -0
- 52b51d40e96d4333695d__mypyc.cpython-310-darwin.so +0 -0
- dao_treasury/.grafana/provisioning/datasources/sqlite.yaml +0 -10
- dao_treasury-0.0.22.dist-info/METADATA +0 -63
- dao_treasury-0.0.22.dist-info/RECORD +0 -31
- dao_treasury-0.0.22.dist-info/top_level.txt +0 -2
- {dao_treasury-0.0.22.dist-info → dao_treasury-0.0.69.dist-info}/WHEEL +0 -0
dao_treasury/docker-compose.yaml
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
networks:
|
|
2
2
|
dao_treasury:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
grafana_data: {}
|
|
3
|
+
docker_eth_portfolio:
|
|
4
|
+
external: true
|
|
6
5
|
|
|
7
6
|
services:
|
|
8
7
|
grafana:
|
|
9
|
-
image: grafana/grafana:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
|
Binary file
|
dao_treasury/sorting/__init__.py
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
163
|
+
txgroup_dbid = FromAddressMatcher.match(from_address)
|
|
88
164
|
|
|
89
165
|
if txgroup_dbid is None:
|
|
90
|
-
txgroup_dbid = ToAddressMatcher.match(
|
|
166
|
+
txgroup_dbid = ToAddressMatcher.match(to_address)
|
|
91
167
|
|
|
92
168
|
if txgroup_dbid is None:
|
|
93
|
-
if TreasuryWallet.check_membership(
|
|
94
|
-
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(
|
|
97
|
-
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(
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 =
|
|
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(
|
|
215
|
+
txgroup_dbid = HashMatcher.match(tx.hash)
|
|
118
216
|
|
|
119
217
|
if txgroup_dbid is None:
|
|
120
|
-
txgroup_dbid = FromAddressMatcher.match(
|
|
218
|
+
txgroup_dbid = FromAddressMatcher.match(from_address)
|
|
121
219
|
|
|
122
|
-
if txgroup_dbid is None and
|
|
123
|
-
txgroup_dbid = ToAddressMatcher.match(
|
|
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(
|
|
127
|
-
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
|
|
130
|
-
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
|
-
|
|
137
|
-
|
|
240
|
+
MUST_SORT_INBOUND_TXGROUP_DBID,
|
|
241
|
+
MUST_SORT_OUTBOUND_TXGROUP_DBID,
|
|
138
242
|
):
|
|
139
|
-
logger.info("Sorted %s to %s",
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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.
|
|
293
|
+
await entry._set_txgroup(txgroup_dbid)
|
|
165
294
|
|
|
166
295
|
return txgroup_dbid # type: ignore [no-any-return]
|
|
Binary file
|
|
Binary file
|
dao_treasury/sorting/_rules.py
CHANGED
|
@@ -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
|
|
|
Binary file
|
dao_treasury/sorting/factory.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from typing import Any, Final, Generic, Optional,
|
|
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]":
|
|
Binary file
|
dao_treasury/sorting/rule.py
CHANGED
|
@@ -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(
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from dao_treasury.sorting.rules.ignore import *
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from dao_treasury.sorting.rules.ignore.llamapay import *
|
|
Binary file
|