dao-treasury 0.0.42__cp310-cp310-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.
- bf2b4fe1f86ad2ea158b__mypyc.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +60 -0
- dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +225 -0
- dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +107 -0
- dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +387 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +835 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +615 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +492 -0
- dao_treasury/.grafana/provisioning/dashboards/treasury/Treasury.json +2018 -0
- dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
- dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
- dao_treasury/__init__.py +62 -0
- dao_treasury/_docker.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/_docker.py +190 -0
- dao_treasury/_nicknames.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/_nicknames.py +32 -0
- dao_treasury/_wallet.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/_wallet.py +250 -0
- dao_treasury/constants.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/constants.py +34 -0
- dao_treasury/db.py +1408 -0
- dao_treasury/docker-compose.yaml +41 -0
- dao_treasury/main.py +247 -0
- dao_treasury/py.typed +0 -0
- dao_treasury/sorting/__init__.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/__init__.py +295 -0
- dao_treasury/sorting/_matchers.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/_matchers.py +387 -0
- dao_treasury/sorting/_rules.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/_rules.py +235 -0
- dao_treasury/sorting/factory.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/factory.py +299 -0
- dao_treasury/sorting/rule.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/rule.py +346 -0
- dao_treasury/sorting/rules/__init__.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/rules/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/__init__.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/rules/ignore/__init__.py +1 -0
- dao_treasury/sorting/rules/ignore/llamapay.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
- dao_treasury/streams/__init__.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/streams/__init__.py +0 -0
- dao_treasury/streams/llamapay.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/streams/llamapay.py +388 -0
- dao_treasury/treasury.py +191 -0
- dao_treasury/types.cpython-310-i386-linux-gnu.so +0 -0
- dao_treasury/types.py +133 -0
- dao_treasury-0.0.42.dist-info/METADATA +119 -0
- dao_treasury-0.0.42.dist-info/RECORD +51 -0
- dao_treasury-0.0.42.dist-info/WHEEL +7 -0
- dao_treasury-0.0.42.dist-info/top_level.txt +2 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
from typing import Any, Final, Generic, Optional, Union, final, overload
|
2
|
+
|
3
|
+
from dao_treasury.constants import CHAINID
|
4
|
+
from dao_treasury.sorting.rule import (
|
5
|
+
CostOfRevenueSortRule,
|
6
|
+
ExpenseSortRule,
|
7
|
+
IgnoreSortRule,
|
8
|
+
OtherExpenseSortRule,
|
9
|
+
OtherIncomeSortRule,
|
10
|
+
RevenueSortRule,
|
11
|
+
TRule,
|
12
|
+
)
|
13
|
+
from dao_treasury.types import Networks, SortFunction, TxGroupName
|
14
|
+
|
15
|
+
|
16
|
+
def revenue(
|
17
|
+
txgroup: TxGroupName, networks: Networks = CHAINID
|
18
|
+
) -> "SortRuleFactory[RevenueSortRule]":
|
19
|
+
"""Create a factory to register revenue sort rules for a given transaction group.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
txgroup: Base name of the transaction group to categorize as revenue.
|
23
|
+
networks: Network ID or iterable of network IDs on which this rule applies.
|
24
|
+
|
25
|
+
See Also:
|
26
|
+
:func:`cost_of_revenue`
|
27
|
+
:class:`SortRuleFactory`
|
28
|
+
|
29
|
+
Examples:
|
30
|
+
>>> from dao_treasury.sorting.factory import revenue
|
31
|
+
>>> @revenue("Token Sales")
|
32
|
+
... def match_sales(tx):
|
33
|
+
... return tx.amount > 0 and tx.to_address is not None
|
34
|
+
"""
|
35
|
+
return SortRuleFactory(txgroup, networks, RevenueSortRule)
|
36
|
+
|
37
|
+
|
38
|
+
def cost_of_revenue(
|
39
|
+
txgroup: TxGroupName, networks: Networks = CHAINID
|
40
|
+
) -> "SortRuleFactory[CostOfRevenueSortRule]":
|
41
|
+
"""Create a factory to register cost‐of‐revenue sort rules for a given transaction group.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
txgroup: Base name of the transaction group to categorize as cost of revenue.
|
45
|
+
networks: Network ID or iterable of network IDs on which this rule applies.
|
46
|
+
|
47
|
+
See Also:
|
48
|
+
:func:`revenue`
|
49
|
+
:class:`SortRuleFactory`
|
50
|
+
|
51
|
+
Examples:
|
52
|
+
>>> from dao_treasury.sorting.factory import cost_of_revenue
|
53
|
+
>>> @cost_of_revenue("Manufacturing")
|
54
|
+
... def match_manufacturing(tx):
|
55
|
+
... return tx.from_address is not None and tx.amount_usd > 1000
|
56
|
+
"""
|
57
|
+
return SortRuleFactory(txgroup, networks, CostOfRevenueSortRule)
|
58
|
+
|
59
|
+
|
60
|
+
def expense(
|
61
|
+
txgroup: TxGroupName, networks: Networks = CHAINID
|
62
|
+
) -> "SortRuleFactory[ExpenseSortRule]":
|
63
|
+
"""Create a factory to register expense sort rules for a given transaction group.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
txgroup: Base name of the transaction group to categorize as expense.
|
67
|
+
networks: Network ID or iterable of network IDs on which this rule applies.
|
68
|
+
|
69
|
+
See Also:
|
70
|
+
:func:`other_expense`
|
71
|
+
:class:`SortRuleFactory`
|
72
|
+
|
73
|
+
Examples:
|
74
|
+
>>> from dao_treasury.sorting.factory import expense
|
75
|
+
>>> @expense("Office Supplies")
|
76
|
+
... def match_supplies(tx):
|
77
|
+
... return tx.symbol == "USD" and tx.amount < 500
|
78
|
+
"""
|
79
|
+
return SortRuleFactory(txgroup, networks, ExpenseSortRule)
|
80
|
+
|
81
|
+
|
82
|
+
def other_income(
|
83
|
+
txgroup: TxGroupName, networks: Networks = CHAINID
|
84
|
+
) -> "SortRuleFactory[OtherIncomeSortRule]":
|
85
|
+
"""Create a factory to register other‐income sort rules for a given transaction group.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
txgroup: Base name of the transaction group to categorize as other income.
|
89
|
+
networks: Network ID or iterable of network IDs on which this rule applies.
|
90
|
+
|
91
|
+
See Also:
|
92
|
+
:func:`revenue`
|
93
|
+
:class:`SortRuleFactory`
|
94
|
+
|
95
|
+
Examples:
|
96
|
+
>>> from dao_treasury.sorting.factory import other_income
|
97
|
+
>>> @other_income("Interest")
|
98
|
+
... def match_interest(tx):
|
99
|
+
... return tx.token_address == SOME_TOKEN and tx.amount > 0
|
100
|
+
"""
|
101
|
+
return SortRuleFactory(txgroup, networks, OtherIncomeSortRule)
|
102
|
+
|
103
|
+
|
104
|
+
def other_expense(
|
105
|
+
txgroup: TxGroupName, networks: Networks = CHAINID
|
106
|
+
) -> "SortRuleFactory[OtherExpenseSortRule]":
|
107
|
+
"""Create a factory to register other‐expense sort rules for a given transaction group.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
txgroup: Base name of the transaction group to categorize as other expense.
|
111
|
+
networks: Network ID or iterable of network IDs on which this rule applies.
|
112
|
+
|
113
|
+
See Also:
|
114
|
+
:func:`expense`
|
115
|
+
:class:`SortRuleFactory`
|
116
|
+
|
117
|
+
Examples:
|
118
|
+
>>> from dao_treasury.sorting.factory import other_expense
|
119
|
+
>>> @other_expense("Misc Fees")
|
120
|
+
... def match_misc(tx):
|
121
|
+
... return tx.amount_usd < 0 and tx.symbol == "ETH"
|
122
|
+
"""
|
123
|
+
return SortRuleFactory(txgroup, networks, OtherExpenseSortRule)
|
124
|
+
|
125
|
+
|
126
|
+
def ignore(
|
127
|
+
txgroup: TxGroupName, networks: Networks = CHAINID
|
128
|
+
) -> "SortRuleFactory[IgnoreSortRule]":
|
129
|
+
"""Create a factory to register ignore sort rules for a given transaction group.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
txgroup: Base name of the transaction group to categorize as ignored.
|
133
|
+
networks: Network ID or iterable of network IDs on which this rule applies.
|
134
|
+
|
135
|
+
See Also:
|
136
|
+
:class:`SortRuleFactory`
|
137
|
+
|
138
|
+
Examples:
|
139
|
+
>>> from dao_treasury.sorting.factory import ignore
|
140
|
+
>>> @ignore("Dust")
|
141
|
+
... def match_dust(tx):
|
142
|
+
... return abs(tx.value_usd) < 0.01
|
143
|
+
"""
|
144
|
+
return SortRuleFactory(txgroup, networks, IgnoreSortRule)
|
145
|
+
|
146
|
+
|
147
|
+
@final
|
148
|
+
class SortRuleFactory(Generic[TRule]):
|
149
|
+
"""Builder for creating sort rule instances for a specific transaction group and network(s).
|
150
|
+
|
151
|
+
This factory supports two patterns:
|
152
|
+
|
153
|
+
1. Decorating a function to register a dynamic matching rule.
|
154
|
+
2. Calling :meth:`match` to supply static match attributes.
|
155
|
+
|
156
|
+
Use the convenience functions like :func:`revenue`, :func:`expense`, etc.,
|
157
|
+
to obtain an instance of this factory preconfigured with the appropriate rule type.
|
158
|
+
|
159
|
+
Examples:
|
160
|
+
>>> from dao_treasury.sorting.factory import revenue
|
161
|
+
>>> @revenue("Sales", networks=[1, 3])
|
162
|
+
... def match_large_sales(tx):
|
163
|
+
... return tx.value_usd > 1000
|
164
|
+
"""
|
165
|
+
|
166
|
+
def __init__(
|
167
|
+
self,
|
168
|
+
txgroup: TxGroupName,
|
169
|
+
networks: Networks,
|
170
|
+
rule_type: TRule,
|
171
|
+
) -> None:
|
172
|
+
"""Initialize the sort rule factory.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
txgroup: Base name of the transaction group.
|
176
|
+
networks: Single network ID or iterable of network IDs where the rule applies.
|
177
|
+
rule_type: Sort rule class (e.g., RevenueSortRule) to instantiate.
|
178
|
+
"""
|
179
|
+
self.txgroup: Final = txgroup
|
180
|
+
self.networks: Final = (
|
181
|
+
[networks] if isinstance(networks, int) else list(networks)
|
182
|
+
)
|
183
|
+
self.rule_type: Final = rule_type
|
184
|
+
self._rule: Optional[TRule] = None
|
185
|
+
|
186
|
+
@overload
|
187
|
+
def __call__(
|
188
|
+
self, txgroup_name: TxGroupName, networks: Optional[Networks] = None
|
189
|
+
) -> "SortRuleFactory":
|
190
|
+
"""Configure a nested sub‐group.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
txgroup_name: Sub‐group name.
|
194
|
+
networks: Optional network specification.
|
195
|
+
"""
|
196
|
+
|
197
|
+
@overload
|
198
|
+
def __call__(self, func: SortFunction) -> SortFunction:
|
199
|
+
"""Register a matching function.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
func: The custom matching function.
|
203
|
+
"""
|
204
|
+
|
205
|
+
def __call__( # type: ignore [misc]
|
206
|
+
self,
|
207
|
+
func: Union[TxGroupName, SortFunction],
|
208
|
+
networks: Optional[Networks] = None,
|
209
|
+
) -> Union["SortRuleFactory", SortFunction]:
|
210
|
+
"""Configure a nested sub‐group or register a matching function.
|
211
|
+
|
212
|
+
Overloads:
|
213
|
+
* If `func` is a string, returns a new factory for `txgroup:func`.
|
214
|
+
* If `func` is callable, registers it as the match logic.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
func: Sub‐group suffix (str) or a custom matching function.
|
218
|
+
networks: Optional networks override (only valid when `func` is str).
|
219
|
+
|
220
|
+
Raises:
|
221
|
+
RuntimeError: If `networks` is passed when `func` is callable.
|
222
|
+
ValueError: If `func` is neither str nor callable.
|
223
|
+
|
224
|
+
See Also:
|
225
|
+
:meth:`match`
|
226
|
+
|
227
|
+
Examples:
|
228
|
+
>>> fees = expense("Fees")
|
229
|
+
>>> @fees("Gas")
|
230
|
+
... def match_gas(tx):
|
231
|
+
... return tx.symbol == "ETH"
|
232
|
+
"""
|
233
|
+
if isinstance(func, str):
|
234
|
+
return SortRuleFactory(
|
235
|
+
f"{self.txgroup}:{func}", networks or self.networks, self.rule_type
|
236
|
+
)
|
237
|
+
elif callable(func):
|
238
|
+
if networks:
|
239
|
+
raise RuntimeError("you can only pass networks if `func` is a string")
|
240
|
+
if CHAINID in self.networks:
|
241
|
+
self.__check_locked()
|
242
|
+
self._rule = self.rule_type(txgroup=self.txgroup, func=func)
|
243
|
+
return func
|
244
|
+
raise ValueError(func)
|
245
|
+
|
246
|
+
@property
|
247
|
+
def rule(self) -> Optional[TRule]:
|
248
|
+
"""Return the created sort rule instance, if any.
|
249
|
+
|
250
|
+
After decoration or a call to :meth:`match`, this property holds the
|
251
|
+
concrete :class:`~dao_treasury.types.SortRule` instance.
|
252
|
+
|
253
|
+
Examples:
|
254
|
+
>>> @other_income("Interest")
|
255
|
+
... def match_i(tx):
|
256
|
+
... return tx.value_usd > 100
|
257
|
+
"""
|
258
|
+
return self._rule
|
259
|
+
|
260
|
+
def match(
|
261
|
+
self, func: None = None, **match_values: Any
|
262
|
+
) -> None: # TODO: give this proper kwargs
|
263
|
+
"""Define static matching attributes for the sort rule.
|
264
|
+
|
265
|
+
Call this method with keyword matchers corresponding to rule attributes
|
266
|
+
(e.g., hash, from_address, symbol) to create a rule matching based on these values.
|
267
|
+
|
268
|
+
Args:
|
269
|
+
func: Must be None; a function match must use the decorator form.
|
270
|
+
**match_values: Attribute values for matching (e.g., hash="0x123", symbol="DAI").
|
271
|
+
|
272
|
+
Raises:
|
273
|
+
ValueError: If `func` is not None.
|
274
|
+
RuntimeError: If a matcher has already been set.
|
275
|
+
|
276
|
+
See Also:
|
277
|
+
:meth:`__call__`
|
278
|
+
|
279
|
+
Examples:
|
280
|
+
>>> ignore("Dust").match(symbol="WETH", from_address="0xAAA")
|
281
|
+
"""
|
282
|
+
if func is not None:
|
283
|
+
raise ValueError(
|
284
|
+
f"You cannot pass a func here, call {self} with the function as the sole arg instead"
|
285
|
+
)
|
286
|
+
# Only instantiate when we're on an allowed network
|
287
|
+
if CHAINID in self.networks:
|
288
|
+
self.__check_locked()
|
289
|
+
self._rule = self.rule_type(txgroup=self.txgroup, **match_values)
|
290
|
+
self.locked = True
|
291
|
+
|
292
|
+
def __check_locked(self) -> None:
|
293
|
+
"""Ensure that no matcher has already been registered.
|
294
|
+
|
295
|
+
Raises:
|
296
|
+
RuntimeError: If this factory already has a matcher assigned.
|
297
|
+
"""
|
298
|
+
if self._rule is not None:
|
299
|
+
raise RuntimeError(f"{self} already has a matcher")
|
Binary file
|
@@ -0,0 +1,346 @@
|
|
1
|
+
# dao_treasury/sorting/rule.py
|
2
|
+
|
3
|
+
"""Module defining transaction sorting rules for the DAO treasury.
|
4
|
+
|
5
|
+
This module provides the `_SortRule` base class and subclasses for categorizing
|
6
|
+
`TreasuryTx` entries based on their attributes or a custom function. When a rule
|
7
|
+
is instantiated, it registers itself in the global `SORT_RULES` mapping under its
|
8
|
+
class and configures which transaction attributes to match via `_match_all`.
|
9
|
+
|
10
|
+
Examples:
|
11
|
+
# Define a revenue rule for sales (assuming you only transact in DAI for sales)
|
12
|
+
>>> from dao_treasury.sorting.rule import RevenueSortRule, SORT_RULES
|
13
|
+
>>> RevenueSortRule(
|
14
|
+
... txgroup='Sale',
|
15
|
+
... token_address='0x6B175474E89094d879c81e570a000000000000',
|
16
|
+
... symbol='DAI'
|
17
|
+
... )
|
18
|
+
# Inspect rules registered for RevenueSortRule
|
19
|
+
>>> len(SORT_RULES[RevenueSortRule])
|
20
|
+
1
|
21
|
+
|
22
|
+
# Iterate over all ExpenseSortRule instances
|
23
|
+
>>> from dao_treasury.sorting.rule import ExpenseSortRule
|
24
|
+
>>> for rule in SORT_RULES[ExpenseSortRule]:
|
25
|
+
... print(rule.txgroup)
|
26
|
+
|
27
|
+
See Also:
|
28
|
+
:const:`~dao_treasury.sorting.rule.SORT_RULES`
|
29
|
+
:class:`~dao_treasury.sorting.rule._SortRule`
|
30
|
+
"""
|
31
|
+
|
32
|
+
from collections import defaultdict
|
33
|
+
from dataclasses import dataclass
|
34
|
+
from logging import getLogger
|
35
|
+
from typing import (
|
36
|
+
TYPE_CHECKING,
|
37
|
+
DefaultDict,
|
38
|
+
Dict,
|
39
|
+
Final,
|
40
|
+
List,
|
41
|
+
Optional,
|
42
|
+
Type,
|
43
|
+
TypeVar,
|
44
|
+
)
|
45
|
+
|
46
|
+
from brownie.convert.datatypes import EthAddress
|
47
|
+
from eth_typing import HexStr
|
48
|
+
from mypy_extensions import mypyc_attr
|
49
|
+
|
50
|
+
from dao_treasury._wallet import TreasuryWallet
|
51
|
+
from dao_treasury.types import SortFunction, SortRule, TxGroupDbid, TxGroupName
|
52
|
+
|
53
|
+
if TYPE_CHECKING:
|
54
|
+
from dao_treasury.db import TreasuryTx
|
55
|
+
|
56
|
+
|
57
|
+
logger: Final = getLogger(__name__)
|
58
|
+
_log_debug: Final = logger.debug
|
59
|
+
|
60
|
+
SORT_RULES: DefaultDict[Type[SortRule], List[SortRule]] = defaultdict(list)
|
61
|
+
"""Mapping from sort rule classes to lists of instantiated rules, in creation order per class.
|
62
|
+
|
63
|
+
Each key is a subclass of :class:`~dao_treasury.types.SortRule` and the corresponding
|
64
|
+
value is the list of rule instances of that class.
|
65
|
+
|
66
|
+
Examples:
|
67
|
+
>>> from dao_treasury.sorting.rule import RevenueSortRule, SORT_RULES
|
68
|
+
>>> RevenueSortRule(txgroup='Interest', symbol='DAI')
|
69
|
+
>>> SORT_RULES[RevenueSortRule][0].txgroup
|
70
|
+
'Revenue:Interest'
|
71
|
+
"""
|
72
|
+
|
73
|
+
_match_all: Final[Dict[TxGroupName, List[str]]] = {}
|
74
|
+
"""An internal cache defining which matcher attributes are used for each `txgroup`."""
|
75
|
+
|
76
|
+
_MATCHING_ATTRS: Final = (
|
77
|
+
"hash",
|
78
|
+
"from_address",
|
79
|
+
"from_nickname",
|
80
|
+
"to_address",
|
81
|
+
"to_nickname",
|
82
|
+
"token_address",
|
83
|
+
"symbol",
|
84
|
+
"log_index",
|
85
|
+
)
|
86
|
+
|
87
|
+
|
88
|
+
@mypyc_attr(native_class=False)
|
89
|
+
@dataclass(kw_only=True, frozen=True)
|
90
|
+
class _SortRule:
|
91
|
+
"""Base class for defining transaction matching rules.
|
92
|
+
|
93
|
+
When instantiated, a rule validates its inputs, determines which transaction
|
94
|
+
attributes to match (or uses a custom function), and registers itself
|
95
|
+
in the global `SORT_RULES` mapping under its class.
|
96
|
+
|
97
|
+
Matched transactions are assigned to the specified `txgroup`.
|
98
|
+
|
99
|
+
See Also:
|
100
|
+
:const:`dao_treasury.sorting.rule.SORT_RULES`
|
101
|
+
"""
|
102
|
+
|
103
|
+
txgroup: TxGroupName
|
104
|
+
"""Name of the transaction group to assign upon match."""
|
105
|
+
|
106
|
+
hash: Optional[HexStr] = None
|
107
|
+
"""Exact transaction hash to match."""
|
108
|
+
|
109
|
+
from_address: Optional[EthAddress] = None
|
110
|
+
"""Source wallet address to match."""
|
111
|
+
|
112
|
+
from_nickname: Optional[str] = None
|
113
|
+
"""Sender nickname (alias) to match."""
|
114
|
+
|
115
|
+
to_address: Optional[EthAddress] = None
|
116
|
+
"""Recipient wallet address to match."""
|
117
|
+
|
118
|
+
to_nickname: Optional[str] = None
|
119
|
+
"""Recipient nickname (alias) to match."""
|
120
|
+
|
121
|
+
token_address: Optional[EthAddress] = None
|
122
|
+
"""Token contract address to match."""
|
123
|
+
|
124
|
+
symbol: Optional[str] = None
|
125
|
+
"""Token symbol to match."""
|
126
|
+
|
127
|
+
log_index: Optional[int] = None
|
128
|
+
"""Log index within the transaction receipt to match."""
|
129
|
+
|
130
|
+
func: Optional[SortFunction] = None
|
131
|
+
"""Custom matching function that takes a `TreasuryTx` and returns a bool or an awaitable that returns a bool."""
|
132
|
+
|
133
|
+
# __instances__: ClassVar[List[Self]] = []
|
134
|
+
|
135
|
+
def __post_init__(self) -> None:
|
136
|
+
"""Validate inputs, checksum addresses, and register the rule.
|
137
|
+
|
138
|
+
- Ensures no duplicate rule exists for the same `txgroup`.
|
139
|
+
- Converts address fields to checksummed format.
|
140
|
+
- Determines which attributes will be used for direct matching.
|
141
|
+
- Validates that exactly one of attribute-based or function-based matching is provided.
|
142
|
+
- Registers the instance in :attr:`SORT_RULES` and :data:`_match_all`.
|
143
|
+
"""
|
144
|
+
if self.txgroup in _match_all:
|
145
|
+
raise ValueError(
|
146
|
+
f"there is already a matcher defined for txgroup {self.txgroup}: {self}"
|
147
|
+
)
|
148
|
+
|
149
|
+
# ensure addresses are checksummed if applicable
|
150
|
+
for attr in ["from_address", "to_address", "token_address"]:
|
151
|
+
value = getattr(self, attr)
|
152
|
+
if value is not None:
|
153
|
+
checksummed = EthAddress(value)
|
154
|
+
# NOTE: we must use object.__setattr__ to modify a frozen dataclass instance
|
155
|
+
object.__setattr__(self, attr, checksummed)
|
156
|
+
|
157
|
+
# define matchers used for this instance
|
158
|
+
matchers = [attr for attr in _MATCHING_ATTRS if getattr(self, attr) is not None]
|
159
|
+
_match_all[self.txgroup] = matchers
|
160
|
+
|
161
|
+
if self.func is not None and matchers:
|
162
|
+
raise ValueError(
|
163
|
+
"You must specify attributes for matching or pass in a custom matching function, not both."
|
164
|
+
)
|
165
|
+
|
166
|
+
if self.func is None and not matchers:
|
167
|
+
raise ValueError(
|
168
|
+
"You must specify attributes for matching or pass in a custom matching function."
|
169
|
+
)
|
170
|
+
|
171
|
+
if self.func is not None and not callable(self.func):
|
172
|
+
raise TypeError(f"func must be callable. You passed {self.func}")
|
173
|
+
|
174
|
+
# append new instance to instances classvar
|
175
|
+
# TODO: fix dataclass ClassVar handling in mypyc and reenable
|
176
|
+
# self.__instances__.append(self)
|
177
|
+
|
178
|
+
# append new instance under its class key
|
179
|
+
SORT_RULES[type(self)].append(self)
|
180
|
+
|
181
|
+
@property
|
182
|
+
def txgroup_dbid(self) -> TxGroupDbid:
|
183
|
+
"""Compute the database ID for this rule's `txgroup`.
|
184
|
+
|
185
|
+
Splits the `txgroup` string on ':' and resolves or creates the hierarchical
|
186
|
+
`TxGroup` entries in the database, returning the final group ID.
|
187
|
+
|
188
|
+
See Also:
|
189
|
+
:class:`~dao_treasury.db.TxGroup`.
|
190
|
+
"""
|
191
|
+
from dao_treasury.db import TxGroup
|
192
|
+
|
193
|
+
txgroup = None
|
194
|
+
for part in self.txgroup.split(":"):
|
195
|
+
txgroup = TxGroup.get_dbid(part, txgroup)
|
196
|
+
return txgroup
|
197
|
+
|
198
|
+
async def match(self, tx: "TreasuryTx") -> bool:
|
199
|
+
"""Determine if the given transaction matches this rule.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
tx: A `TreasuryTx` entity to test against this rule.
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
True if the transaction matches the rule criteria; otherwise False.
|
206
|
+
|
207
|
+
Examples:
|
208
|
+
# match by symbol and recipient
|
209
|
+
>>> rule = _SortRule(txgroup='Foo', symbol='DAI', to_address='0xabc...')
|
210
|
+
>>> await rule.match(tx) # where tx.symbol == 'DAI' and tx.to_address == '0xabc...'
|
211
|
+
True
|
212
|
+
|
213
|
+
See Also:
|
214
|
+
:attr:`_match_all`
|
215
|
+
"""
|
216
|
+
if matchers := _match_all[self.txgroup]:
|
217
|
+
return all(
|
218
|
+
getattr(tx, matcher) == getattr(self, matcher) for matcher in matchers
|
219
|
+
)
|
220
|
+
|
221
|
+
_log_debug("checking %s for %s", tx, self.func)
|
222
|
+
match = self.func(tx) # type: ignore [misc]
|
223
|
+
return match if isinstance(match, bool) else await match
|
224
|
+
|
225
|
+
|
226
|
+
@mypyc_attr(native_class=False)
|
227
|
+
class _InboundSortRule(_SortRule):
|
228
|
+
"""Sort rule that applies only to inbound transactions (to the DAO's wallet).
|
229
|
+
|
230
|
+
Checks that the transaction's `to_address` belongs to a known `TreasuryWallet`
|
231
|
+
before applying the base matching logic.
|
232
|
+
"""
|
233
|
+
|
234
|
+
async def match(self, tx: "TreasuryTx") -> bool:
|
235
|
+
return (
|
236
|
+
tx.to_address is not None
|
237
|
+
and TreasuryWallet.check_membership(tx.to_address.address, tx.block)
|
238
|
+
and await super().match(tx)
|
239
|
+
)
|
240
|
+
|
241
|
+
|
242
|
+
@mypyc_attr(native_class=False)
|
243
|
+
class _OutboundSortRule(_SortRule):
|
244
|
+
"""Sort rule that applies only to outbound transactions (from the DAO's wallet).
|
245
|
+
|
246
|
+
Checks that the transaction's `from_address` belongs to a known `TreasuryWallet`
|
247
|
+
before applying the base matching logic.
|
248
|
+
"""
|
249
|
+
|
250
|
+
async def match(self, tx: "TreasuryTx") -> bool:
|
251
|
+
return TreasuryWallet.check_membership(
|
252
|
+
tx.from_address.address, tx.block
|
253
|
+
) and await super().match(tx)
|
254
|
+
|
255
|
+
|
256
|
+
@mypyc_attr(native_class=False)
|
257
|
+
class RevenueSortRule(_InboundSortRule):
|
258
|
+
"""Rule to categorize inbound transactions as revenue.
|
259
|
+
|
260
|
+
Prepends 'Revenue:' to the `txgroup` name before registration.
|
261
|
+
|
262
|
+
Examples:
|
263
|
+
>>> RevenueSortRule(txgroup='Sale', to_address='0xabc...', symbol='DAI')
|
264
|
+
# results in a rule with txgroup 'Revenue:Sale'
|
265
|
+
"""
|
266
|
+
|
267
|
+
def __post_init__(self) -> None:
|
268
|
+
"""Prepends `self.txgroup` with 'Revenue:'."""
|
269
|
+
object.__setattr__(self, "txgroup", f"Revenue:{self.txgroup}")
|
270
|
+
super().__post_init__()
|
271
|
+
|
272
|
+
|
273
|
+
@mypyc_attr(native_class=False)
|
274
|
+
class CostOfRevenueSortRule(_OutboundSortRule):
|
275
|
+
"""Rule to categorize outbound transactions as cost of revenue.
|
276
|
+
|
277
|
+
Prepends 'Cost of Revenue:' to the `txgroup` name before registration.
|
278
|
+
"""
|
279
|
+
|
280
|
+
def __post_init__(self) -> None:
|
281
|
+
"""Prepends `self.txgroup` with 'Cost of Revenue:'."""
|
282
|
+
object.__setattr__(self, "txgroup", f"Cost of Revenue:{self.txgroup}")
|
283
|
+
super().__post_init__()
|
284
|
+
|
285
|
+
|
286
|
+
@mypyc_attr(native_class=False)
|
287
|
+
class ExpenseSortRule(_OutboundSortRule):
|
288
|
+
"""Rule to categorize outbound transactions as expenses.
|
289
|
+
|
290
|
+
Prepends 'Expenses:' to the `txgroup` name before registration.
|
291
|
+
"""
|
292
|
+
|
293
|
+
def __post_init__(self) -> None:
|
294
|
+
"""Prepends `self.txgroup` with 'Expenses:'."""
|
295
|
+
object.__setattr__(self, "txgroup", f"Expenses:{self.txgroup}")
|
296
|
+
super().__post_init__()
|
297
|
+
|
298
|
+
|
299
|
+
@mypyc_attr(native_class=False)
|
300
|
+
class OtherIncomeSortRule(_InboundSortRule):
|
301
|
+
"""Rule to categorize inbound transactions as other income.
|
302
|
+
|
303
|
+
Prepends 'Other Income:' to the `txgroup` name before registration.
|
304
|
+
"""
|
305
|
+
|
306
|
+
def __post_init__(self) -> None:
|
307
|
+
"""Prepends `self.txgroup` with 'Other Income:'."""
|
308
|
+
object.__setattr__(self, "txgroup", f"Other Income:{self.txgroup}")
|
309
|
+
super().__post_init__()
|
310
|
+
|
311
|
+
|
312
|
+
@mypyc_attr(native_class=False)
|
313
|
+
class OtherExpenseSortRule(_OutboundSortRule):
|
314
|
+
"""Rule to categorize outbound transactions as other expenses.
|
315
|
+
|
316
|
+
Prepends 'Other Expenses:' to the `txgroup` name before registration.
|
317
|
+
"""
|
318
|
+
|
319
|
+
def __post_init__(self) -> None:
|
320
|
+
"""Prepends `self.txgroup` with 'Other Expenses:'."""
|
321
|
+
object.__setattr__(self, "txgroup", f"Other Expenses:{self.txgroup}")
|
322
|
+
super().__post_init__()
|
323
|
+
|
324
|
+
|
325
|
+
@mypyc_attr(native_class=False)
|
326
|
+
class IgnoreSortRule(_SortRule):
|
327
|
+
"""Rule to ignore certain transactions.
|
328
|
+
|
329
|
+
Prepends 'Ignore:' to the `txgroup` name before registration.
|
330
|
+
"""
|
331
|
+
|
332
|
+
def __post_init__(self) -> None:
|
333
|
+
"""Prepends `self.txgroup` with 'Ignore:'."""
|
334
|
+
object.__setattr__(self, "txgroup", f"Ignore:{self.txgroup}")
|
335
|
+
super().__post_init__()
|
336
|
+
|
337
|
+
|
338
|
+
TRule = TypeVar(
|
339
|
+
"TRule",
|
340
|
+
RevenueSortRule,
|
341
|
+
CostOfRevenueSortRule,
|
342
|
+
ExpenseSortRule,
|
343
|
+
OtherIncomeSortRule,
|
344
|
+
OtherExpenseSortRule,
|
345
|
+
IgnoreSortRule,
|
346
|
+
)
|
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
|
@@ -0,0 +1,20 @@
|
|
1
|
+
from dao_treasury import TreasuryTx
|
2
|
+
from dao_treasury.sorting.factory import ignore
|
3
|
+
from dao_treasury.streams import llamapay
|
4
|
+
|
5
|
+
|
6
|
+
@ignore("LlamaPay")
|
7
|
+
def is_llamapay_stream_replenishment(tx: TreasuryTx) -> bool:
|
8
|
+
if tx.to_address.address in llamapay.factories: # type: ignore [operator]
|
9
|
+
# We amortize these streams daily in the `llamapay` module, you'll sort each stream appropriately.
|
10
|
+
return True
|
11
|
+
|
12
|
+
# NOTE: not sure if we want this yet
|
13
|
+
# Puling unused funds back from vesting escrow / llamapay
|
14
|
+
# elif tx.from_address == "Contract: LlamaPay" and "StreamCancelled" in tx.events:
|
15
|
+
# if tx.amount > 0:
|
16
|
+
# tx.amount *= -1
|
17
|
+
# if tx.value_usd > 0:
|
18
|
+
# tx.value_usd *= -1
|
19
|
+
# return True
|
20
|
+
return False
|
Binary file
|
File without changes
|
Binary file
|