iwa 0.0.60__py3-none-any.whl → 0.0.62__py3-none-any.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.
@@ -1,12 +1,17 @@
1
- """Tests for TransactionService."""
1
+ """Tests for TransactionService and TransferLogger."""
2
2
 
3
3
  from unittest.mock import MagicMock, patch
4
4
 
5
5
  import pytest
6
+ from web3 import Web3
6
7
  from web3 import exceptions as web3_exceptions
7
8
 
8
9
  from iwa.core.keys import EncryptedAccount, KeyStorage
9
- from iwa.core.services.transaction import TransactionService
10
+ from iwa.core.services.transaction import (
11
+ TRANSFER_EVENT_TOPIC,
12
+ TransactionService,
13
+ TransferLogger,
14
+ )
10
15
 
11
16
 
12
17
  @pytest.fixture
@@ -177,3 +182,174 @@ def test_sign_and_send_rpc_rotation(
177
182
  assert success is True
178
183
  # Verify retry happened - send_raw_transaction called twice
179
184
  assert chain_interface.web3.eth.send_raw_transaction.call_count == 2
185
+
186
+
187
+ # =============================================================================
188
+ # TransferLogger tests
189
+ # =============================================================================
190
+
191
+ # Real-world address for tests
192
+ _ADDR = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
193
+ _ADDR_LOWER = _ADDR.lower()
194
+ # 32-byte topic with address in last 20 bytes
195
+ _TOPIC_BYTES = b"\x00" * 12 + bytes.fromhex(_ADDR_LOWER[2:])
196
+ _TOPIC_HEX_STR = "0x" + "0" * 24 + _ADDR_LOWER[2:]
197
+
198
+
199
+ @pytest.fixture
200
+ def transfer_logger():
201
+ """Create a TransferLogger with minimal mocks."""
202
+ account_service = MagicMock()
203
+ account_service.get_tag_by_address.return_value = None
204
+ chain_interface = MagicMock()
205
+ chain_interface.chain.native_currency = "xDAI"
206
+ chain_interface.chain.get_token_name.return_value = None
207
+ chain_interface.get_token_decimals.return_value = 18
208
+ return TransferLogger(account_service, chain_interface)
209
+
210
+
211
+ class TestTopicToAddress:
212
+ """Test TransferLogger._topic_to_address with all input types."""
213
+
214
+ def test_bytes_topic(self, transfer_logger):
215
+ """32 bytes → last 20 bytes extracted as address."""
216
+ result = transfer_logger._topic_to_address(_TOPIC_BYTES)
217
+ assert result == Web3.to_checksum_address(_ADDR_LOWER)
218
+
219
+ def test_hex_string_topic(self, transfer_logger):
220
+ """Hex string with 0x prefix → last 40 chars as address."""
221
+ result = transfer_logger._topic_to_address(_TOPIC_HEX_STR)
222
+ assert result == Web3.to_checksum_address(_ADDR_LOWER)
223
+
224
+ def test_hex_string_no_prefix(self, transfer_logger):
225
+ """Hex string without 0x prefix."""
226
+ topic = "0" * 24 + _ADDR_LOWER[2:]
227
+ result = transfer_logger._topic_to_address(topic)
228
+ assert result == Web3.to_checksum_address(_ADDR_LOWER)
229
+
230
+ def test_hexbytes_like_topic(self, transfer_logger):
231
+ """Object with .hex() method (like HexBytes)."""
232
+
233
+ class FakeHexBytes:
234
+ def hex(self):
235
+ return "0" * 24 + _ADDR_LOWER[2:]
236
+
237
+ result = transfer_logger._topic_to_address(FakeHexBytes())
238
+ assert result == Web3.to_checksum_address(_ADDR_LOWER)
239
+
240
+ def test_unsupported_type_returns_empty(self, transfer_logger):
241
+ """Non-bytes, non-str, no .hex() → empty string."""
242
+ result = transfer_logger._topic_to_address(12345)
243
+ assert result == ""
244
+
245
+
246
+ class TestProcessLog:
247
+ """Test TransferLogger._process_log with realistic log structures."""
248
+
249
+ def _make_transfer_log(self, from_addr, to_addr, amount_wei, token_addr="0xToken"):
250
+ """Build a dict-style Transfer event log."""
251
+ from_topic = "0x" + "0" * 24 + from_addr[2:].lower()
252
+ to_topic = "0x" + "0" * 24 + to_addr[2:].lower()
253
+ data = amount_wei.to_bytes(32, "big")
254
+ return {
255
+ "topics": [TRANSFER_EVENT_TOPIC, from_topic, to_topic],
256
+ "data": data,
257
+ "address": token_addr,
258
+ }
259
+
260
+ def test_parses_erc20_transfer(self, transfer_logger):
261
+ """Valid Transfer log is parsed and logged."""
262
+ log = self._make_transfer_log(
263
+ "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
264
+ "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
265
+ 10**18, # 1 token with 18 decimals
266
+ )
267
+ # Should not raise
268
+ transfer_logger._process_log(log)
269
+
270
+ def test_ignores_non_transfer_event(self, transfer_logger):
271
+ """Log with non-Transfer topic is silently skipped."""
272
+ log = {
273
+ "topics": ["0xdeadbeef" + "0" * 56],
274
+ "data": b"",
275
+ "address": "0xToken",
276
+ }
277
+ # Should not raise or log anything
278
+ transfer_logger._process_log(log)
279
+
280
+ def test_ignores_log_with_no_topics(self, transfer_logger):
281
+ """Log with empty topics is skipped."""
282
+ transfer_logger._process_log({"topics": [], "data": b""})
283
+
284
+ def test_ignores_log_with_insufficient_topics(self, transfer_logger):
285
+ """Transfer event with < 3 topics (missing from/to) is skipped."""
286
+ log = {
287
+ "topics": [TRANSFER_EVENT_TOPIC, "0x" + "0" * 64],
288
+ "data": b"",
289
+ "address": "0xToken",
290
+ }
291
+ transfer_logger._process_log(log)
292
+
293
+ def test_handles_bytes_topics(self, transfer_logger):
294
+ """Log with bytes topics (not hex strings)."""
295
+ from_bytes = b"\x00" * 12 + b"\xAA" * 20
296
+ to_bytes = b"\x00" * 12 + b"\xBB" * 20
297
+ event_topic = bytes.fromhex(TRANSFER_EVENT_TOPIC[2:])
298
+ log = {
299
+ "topics": [event_topic, from_bytes, to_bytes],
300
+ "data": (100).to_bytes(32, "big"),
301
+ "address": "0xTokenAddr",
302
+ }
303
+ transfer_logger._process_log(log)
304
+
305
+ def test_handles_string_data(self, transfer_logger):
306
+ """Log with hex-encoded data string instead of bytes."""
307
+ log = self._make_transfer_log(
308
+ "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
309
+ "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
310
+ 0,
311
+ )
312
+ log["data"] = "0x" + "0" * 64 # String instead of bytes
313
+ transfer_logger._process_log(log)
314
+
315
+
316
+ class TestResolveLabels:
317
+ """Test address/token label resolution fallbacks."""
318
+
319
+ def test_address_label_known_wallet(self, transfer_logger):
320
+ """Known wallet tag is preferred."""
321
+ transfer_logger.account_service.get_tag_by_address.return_value = "my_safe"
322
+ result = transfer_logger._resolve_address_label("0xABC")
323
+ assert result == "my_safe"
324
+
325
+ def test_address_label_known_token(self, transfer_logger):
326
+ """Falls back to token contract name."""
327
+ transfer_logger.account_service.get_tag_by_address.return_value = None
328
+ transfer_logger.chain_interface.chain.get_token_name.return_value = "OLAS"
329
+ result = transfer_logger._resolve_address_label("0xOLAS")
330
+ assert result == "OLAS_contract"
331
+
332
+ def test_address_label_abbreviated(self, transfer_logger):
333
+ """Falls back to abbreviated address."""
334
+ result = transfer_logger._resolve_address_label("0xABCDEF1234567890ABCDEF")
335
+ assert result.startswith("0xABCD")
336
+ assert result.endswith("CDEF")
337
+ assert "..." in result
338
+
339
+ def test_address_label_empty(self, transfer_logger):
340
+ """Empty address returns 'unknown'."""
341
+ assert transfer_logger._resolve_address_label("") == "unknown"
342
+
343
+ def test_token_label_known(self, transfer_logger):
344
+ """Known token returns its name."""
345
+ transfer_logger.chain_interface.chain.get_token_name.return_value = "OLAS"
346
+ assert transfer_logger._resolve_token_label("0xOLAS") == "OLAS"
347
+
348
+ def test_token_label_unknown(self, transfer_logger):
349
+ """Unknown token returns abbreviated address."""
350
+ result = transfer_logger._resolve_token_label("0xABCDEF1234567890ABCDEF")
351
+ assert "..." in result
352
+
353
+ def test_token_label_empty(self, transfer_logger):
354
+ """Empty address returns 'UNKNOWN'."""
355
+ assert transfer_logger._resolve_token_label("") == "UNKNOWN"
File without changes