Skip to content

Commit ef5574d

Browse files
authored
refactor: move calldata decoder from timelock/ to utils/calldata/
2 parents e86ea8d + 3d7581d commit ef5574d

9 files changed

Lines changed: 32 additions & 23 deletions

File tree

tests/test_ai_explainer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44
from unittest.mock import MagicMock, patch
55

6-
from timelock.calldata_decoder import DecodedCall
6+
from utils.calldata.decoder import DecodedCall
77
from utils.llm.ai_explainer import (
88
Explanation,
99
_build_prompt,

tests/test_calldata_decoder.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44
from unittest.mock import patch
55

6-
from timelock.calldata_decoder import (
6+
from utils.calldata.decoder import (
77
DecodedCall,
88
_format_param_value,
99
_parse_param_types,
@@ -12,7 +12,7 @@
1212
format_call_lines,
1313
resolve_selector,
1414
)
15-
from timelock.known_selectors import KNOWN_SELECTORS
15+
from utils.calldata.known_selectors import KNOWN_SELECTORS
1616

1717
# A selector guaranteed NOT to be in the local table, for testing API fallback.
1818
_UNKNOWN_SELECTOR = "0x11223344"
@@ -93,7 +93,7 @@ def setUp(self):
9393

9494
def test_known_selector_no_api_call(self):
9595
"""Selectors in the local table should resolve without any API call."""
96-
with patch("timelock.calldata_decoder.fetch_json") as mock_fetch:
96+
with patch("utils.calldata.decoder.fetch_json") as mock_fetch:
9797
result = resolve_selector("0xa9059cbb")
9898
self.assertEqual(result, "transfer(address,uint256)")
9999
mock_fetch.assert_not_called()
@@ -103,7 +103,7 @@ def test_known_selector_case_insensitive(self):
103103
result = resolve_selector("0xA9059CBB")
104104
self.assertEqual(result, "transfer(address,uint256)")
105105

106-
@patch("timelock.calldata_decoder.fetch_json")
106+
@patch("utils.calldata.decoder.fetch_json")
107107
def test_api_fallback_for_unknown_selector(self, mock_fetch):
108108
"""Selectors not in the local table should fall through to the API."""
109109
mock_fetch.return_value = {
@@ -117,31 +117,31 @@ def test_api_fallback_for_unknown_selector(self, mock_fetch):
117117
self.assertEqual(result, "someRareFunction(uint256)")
118118
mock_fetch.assert_called_once()
119119

120-
@patch("timelock.calldata_decoder.fetch_json")
120+
@patch("utils.calldata.decoder.fetch_json")
121121
def test_runtime_cache_hit(self, mock_fetch):
122122
"""Previously resolved selectors should be served from runtime cache."""
123123
_selector_cache[_UNKNOWN_SELECTOR] = "cachedFunc(address)"
124124
result = resolve_selector(_UNKNOWN_SELECTOR)
125125
self.assertEqual(result, "cachedFunc(address)")
126126
mock_fetch.assert_not_called()
127127

128-
@patch("timelock.calldata_decoder.fetch_json")
128+
@patch("utils.calldata.decoder.fetch_json")
129129
def test_runtime_cache_hit_none(self, mock_fetch):
130130
"""Cached None (previously failed lookup) should return None without API call."""
131131
_selector_cache[_UNKNOWN_SELECTOR] = None
132132
result = resolve_selector(_UNKNOWN_SELECTOR)
133133
self.assertIsNone(result)
134134
mock_fetch.assert_not_called()
135135

136-
@patch("timelock.calldata_decoder.fetch_json")
136+
@patch("utils.calldata.decoder.fetch_json")
137137
def test_api_returns_none(self, mock_fetch):
138138
mock_fetch.return_value = None
139139
result = resolve_selector(_UNKNOWN_SELECTOR)
140140
self.assertIsNone(result)
141141
# Should be cached as None
142142
self.assertIsNone(_selector_cache[_UNKNOWN_SELECTOR])
143143

144-
@patch("timelock.calldata_decoder.fetch_json")
144+
@patch("utils.calldata.decoder.fetch_json")
145145
def test_no_match(self, mock_fetch):
146146
mock_fetch.return_value = {"result": {"function": {_UNKNOWN_SELECTOR: []}}}
147147
result = resolve_selector(_UNKNOWN_SELECTOR)
@@ -180,7 +180,7 @@ class TestDecodeCalldata(unittest.TestCase):
180180
def setUp(self):
181181
_selector_cache.clear()
182182

183-
@patch("timelock.calldata_decoder.resolve_selector")
183+
@patch("utils.calldata.decoder.resolve_selector")
184184
def test_successful_decode(self, mock_resolve):
185185
mock_resolve.return_value = "transfer(address,uint256)"
186186
result = decode_calldata(TRANSFER_CALLDATA)
@@ -194,7 +194,7 @@ def test_successful_decode(self, mock_resolve):
194194
self.assertEqual(result.params[1][0], "uint256")
195195
self.assertEqual(result.params[1][1], 1000)
196196

197-
@patch("timelock.calldata_decoder.resolve_selector")
197+
@patch("utils.calldata.decoder.resolve_selector")
198198
def test_no_params_function(self, mock_resolve):
199199
mock_resolve.return_value = "pause()"
200200
# selector only, no param data
@@ -205,7 +205,7 @@ def test_no_params_function(self, mock_resolve):
205205
self.assertEqual(result.signature, "pause()")
206206
self.assertEqual(result.params, [])
207207

208-
@patch("timelock.calldata_decoder.resolve_selector")
208+
@patch("utils.calldata.decoder.resolve_selector")
209209
def test_selector_not_resolved(self, mock_resolve):
210210
mock_resolve.return_value = None
211211
result = decode_calldata("0xdeadbeef00112233")
@@ -216,7 +216,7 @@ def test_data_too_short(self):
216216
self.assertIsNone(decode_calldata(""))
217217
self.assertIsNone(decode_calldata(None))
218218

219-
@patch("timelock.calldata_decoder.resolve_selector")
219+
@patch("utils.calldata.decoder.resolve_selector")
220220
def test_malformed_param_data_still_returns(self, mock_resolve):
221221
"""If param decoding fails, should still return DecodedCall with empty params."""
222222
mock_resolve.return_value = "transfer(address,uint256)"
@@ -234,7 +234,7 @@ class TestFormatCallLines(unittest.TestCase):
234234
def setUp(self):
235235
_selector_cache.clear()
236236

237-
@patch("timelock.calldata_decoder.resolve_selector")
237+
@patch("utils.calldata.decoder.resolve_selector")
238238
def test_successful_format(self, mock_resolve):
239239
mock_resolve.return_value = "transfer(address,uint256)"
240240
lines = format_call_lines(TRANSFER_CALLDATA)
@@ -248,7 +248,7 @@ def test_successful_format(self, mock_resolve):
248248
self.assertIn("uint256", lines[2])
249249
self.assertIn("1000", lines[2])
250250

251-
@patch("timelock.calldata_decoder.resolve_selector")
251+
@patch("utils.calldata.decoder.resolve_selector")
252252
def test_fallback_raw_selector(self, mock_resolve):
253253
mock_resolve.return_value = None
254254
lines = format_call_lines("0xdeadbeef00112233")
@@ -261,7 +261,7 @@ def test_short_data_returns_empty(self):
261261
self.assertEqual(format_call_lines(""), [])
262262
self.assertEqual(format_call_lines(None), [])
263263

264-
@patch("timelock.calldata_decoder.resolve_selector")
264+
@patch("utils.calldata.decoder.resolve_selector")
265265
def test_no_params_format(self, mock_resolve):
266266
mock_resolve.return_value = "pause()"
267267
lines = format_call_lines("0xabcd1234")

timelock/timelock_alerts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
from dotenv import load_dotenv
1414

15-
from timelock.calldata_decoder import format_call_lines
1615
from utils.cache import cache_filename, get_last_value_for_key_from_file, write_last_value_to_file
16+
from utils.calldata.decoder import format_call_lines
1717
from utils.chains import EXPLORER_URLS, Chain
1818
from utils.llm.ai_explainer import explain_batch_transaction, explain_transaction, format_explanation_line
1919
from utils.logging import get_logger

utils/calldata/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Calldata decoding utilities for governance transaction monitoring.
2+
3+
Decodes raw EVM calldata into human-readable function calls using a local
4+
selector lookup table and the Sourcify 4byte signature database API.
5+
"""
6+
7+
from utils.calldata.decoder import DecodedCall, decode_calldata, format_call_lines
8+
9+
__all__ = ["DecodedCall", "decode_calldata", "format_call_lines"]
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from eth_abi import decode
1212
from eth_utils import to_checksum_address
1313

14-
from timelock.known_selectors import KNOWN_SELECTORS
14+
from utils.calldata.known_selectors import KNOWN_SELECTORS
1515
from utils.http import fetch_json
1616
from utils.logging import get_logger
1717

@@ -173,10 +173,10 @@ def format_call_lines(data_hex: str) -> list[str]:
173173

174174
result = decode_calldata(data_hex)
175175
if not result:
176-
return [f"📝 Function: `{data_hex[:10]}`"]
176+
return [f"\U0001f4dd Function: `{data_hex[:10]}`"]
177177

178-
lines = [f"📝 Function: `{result.signature}`"]
178+
lines = [f"\U0001f4dd Function: `{result.signature}`"]
179179
for type_str, value in result.params:
180180
formatted = _format_param_value(type_str, value)
181-
lines.append(f" {type_str}: `{formatted}`")
181+
lines.append(f" \u251c {type_str}: `{formatted}`")
182182
return lines

utils/llm/ai_explainer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from dataclasses import dataclass
99

10-
from timelock.calldata_decoder import DecodedCall, decode_calldata
10+
from utils.calldata.decoder import DecodedCall, decode_calldata
1111
from utils.llm import get_llm_provider
1212
from utils.llm.base import LLMError
1313
from utils.logging import get_logger

utils/proxy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from eth_utils import to_checksum_address
88

9-
from timelock.calldata_decoder import decode_calldata
9+
from utils.calldata.decoder import decode_calldata
1010
from utils.chains import EXPLORER_URLS, Chain
1111
from utils.logging import get_logger
1212

0 commit comments

Comments
 (0)