iwa 0.0.59__py3-none-any.whl → 0.0.61__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.
- iwa/core/chain/interface.py +151 -36
- iwa/core/chain/manager.py +8 -0
- iwa/core/chain/rate_limiter.py +35 -6
- iwa/core/chainlist.py +183 -5
- {iwa-0.0.59.dist-info → iwa-0.0.61.dist-info}/METADATA +1 -1
- {iwa-0.0.59.dist-info → iwa-0.0.61.dist-info}/RECORD +17 -16
- tests/test_chain_interface.py +3 -3
- tests/test_chainlist_enrichment.py +354 -0
- tests/test_rate_limiter.py +7 -5
- tests/test_rate_limiter_retry.py +33 -27
- tests/test_rpc_rate_limit.py +3 -3
- tests/test_safe_executor.py +208 -0
- tests/test_transaction_service.py +178 -2
- {iwa-0.0.59.dist-info → iwa-0.0.61.dist-info}/WHEEL +0 -0
- {iwa-0.0.59.dist-info → iwa-0.0.61.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.59.dist-info → iwa-0.0.61.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.59.dist-info → iwa-0.0.61.dist-info}/top_level.txt +0 -0
tests/test_safe_executor.py
CHANGED
|
@@ -359,3 +359,211 @@ def test_retry_preserves_gas(executor, mock_chain_interface, mock_safe_tx, mock_
|
|
|
359
359
|
executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
|
|
360
360
|
|
|
361
361
|
assert mock_safe_tx.safe_tx_gas == original_gas
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# =============================================================================
|
|
365
|
+
# Test: Gas estimation (_estimate_safe_tx_gas)
|
|
366
|
+
# =============================================================================
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def test_estimate_safe_tx_gas_with_buffer(executor, mock_safe):
|
|
370
|
+
"""Test gas estimation applies buffer correctly."""
|
|
371
|
+
mock_safe.estimate_tx_gas.return_value = 100_000
|
|
372
|
+
mock_safe_tx = MagicMock()
|
|
373
|
+
mock_safe_tx.to = "0xDest"
|
|
374
|
+
mock_safe_tx.value = 0
|
|
375
|
+
mock_safe_tx.data = b""
|
|
376
|
+
mock_safe_tx.operation = 0
|
|
377
|
+
|
|
378
|
+
result = executor._estimate_safe_tx_gas(mock_safe, mock_safe_tx)
|
|
379
|
+
|
|
380
|
+
# Default buffer is 1.5, so 100000 * 1.5 = 150000
|
|
381
|
+
assert result == 150_000
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_estimate_safe_tx_gas_caps_at_10x(executor, mock_safe):
|
|
385
|
+
"""Test gas estimation respects x10 cap when base_estimate is provided."""
|
|
386
|
+
mock_safe.estimate_tx_gas.return_value = 500_000 # High estimate
|
|
387
|
+
mock_safe_tx = MagicMock()
|
|
388
|
+
mock_safe_tx.to = "0xDest"
|
|
389
|
+
mock_safe_tx.value = 0
|
|
390
|
+
mock_safe_tx.data = b""
|
|
391
|
+
mock_safe_tx.operation = 0
|
|
392
|
+
|
|
393
|
+
# 500000 * 1.5 = 750000, but base_estimate * 10 = 50000
|
|
394
|
+
result = executor._estimate_safe_tx_gas(mock_safe, mock_safe_tx, base_estimate=5_000)
|
|
395
|
+
|
|
396
|
+
# Should be capped at 5000 * 10 = 50000
|
|
397
|
+
assert result == 50_000
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_estimate_safe_tx_gas_fallback_on_failure(executor, mock_safe):
|
|
401
|
+
"""Test gas estimation uses fallback when estimation fails."""
|
|
402
|
+
mock_safe.estimate_tx_gas.side_effect = Exception("Estimation failed")
|
|
403
|
+
mock_safe_tx = MagicMock()
|
|
404
|
+
mock_safe_tx.to = "0xDest"
|
|
405
|
+
mock_safe_tx.value = 0
|
|
406
|
+
mock_safe_tx.data = b""
|
|
407
|
+
mock_safe_tx.operation = 0
|
|
408
|
+
|
|
409
|
+
result = executor._estimate_safe_tx_gas(mock_safe, mock_safe_tx)
|
|
410
|
+
|
|
411
|
+
assert result == executor.DEFAULT_FALLBACK_GAS
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# =============================================================================
|
|
415
|
+
# Test: Error decoding (_decode_revert_reason)
|
|
416
|
+
# =============================================================================
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_decode_revert_reason_with_hex_data(executor):
|
|
420
|
+
"""Test decoding when error contains hex data."""
|
|
421
|
+
# Create an error with hex data that might be decodable
|
|
422
|
+
error = ValueError("execution reverted: 0x08c379a0...")
|
|
423
|
+
|
|
424
|
+
with patch("iwa.core.services.safe_executor.ErrorDecoder") as mock_decoder:
|
|
425
|
+
mock_decoder.return_value.decode.return_value = [("Error", "Insufficient balance", "ERC20")]
|
|
426
|
+
result = executor._decode_revert_reason(error)
|
|
427
|
+
|
|
428
|
+
# Note: Due to hex matching, this should find the data and attempt decode
|
|
429
|
+
assert result == "Insufficient balance (from ERC20)"
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def test_decode_revert_reason_no_hex_data(executor):
|
|
433
|
+
"""Test decoding when error has no hex data."""
|
|
434
|
+
error = ValueError("Some generic error without hex")
|
|
435
|
+
|
|
436
|
+
result = executor._decode_revert_reason(error)
|
|
437
|
+
|
|
438
|
+
assert result is None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_decode_revert_reason_decode_fails(executor):
|
|
442
|
+
"""Test decoding when decoder returns None."""
|
|
443
|
+
error = ValueError("error: 0xdeadbeef")
|
|
444
|
+
|
|
445
|
+
with patch("iwa.core.services.safe_executor.ErrorDecoder") as mock_decoder:
|
|
446
|
+
mock_decoder.return_value.decode.return_value = None
|
|
447
|
+
result = executor._decode_revert_reason(error)
|
|
448
|
+
|
|
449
|
+
assert result is None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# =============================================================================
|
|
453
|
+
# Test: Error classification
|
|
454
|
+
# =============================================================================
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_classify_error_gas_error(executor):
|
|
458
|
+
"""Test classification of gas-related errors."""
|
|
459
|
+
error = ValueError("intrinsic gas too low")
|
|
460
|
+
result = executor._classify_error(error)
|
|
461
|
+
|
|
462
|
+
assert result["is_gas_error"] is True
|
|
463
|
+
assert result["is_nonce_error"] is False
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_classify_error_revert(executor):
|
|
467
|
+
"""Test classification of revert errors."""
|
|
468
|
+
error = ValueError("execution reverted: some reason")
|
|
469
|
+
result = executor._classify_error(error)
|
|
470
|
+
|
|
471
|
+
assert result["is_revert"] is True
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_classify_error_out_of_gas(executor):
|
|
475
|
+
"""Test classification of out of gas errors."""
|
|
476
|
+
error = ValueError("out of gas")
|
|
477
|
+
result = executor._classify_error(error)
|
|
478
|
+
|
|
479
|
+
assert result["is_gas_error"] is True
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# =============================================================================
|
|
483
|
+
# Test: Transaction failures
|
|
484
|
+
# =============================================================================
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_transaction_reverts_onchain(executor, mock_chain_interface, mock_safe_tx, mock_safe):
|
|
488
|
+
"""Test handling when transaction is mined but reverts (status 0)."""
|
|
489
|
+
with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
|
|
490
|
+
mock_safe_tx.execute.return_value = b"tx_hash"
|
|
491
|
+
# Receipt with status 0 (reverted)
|
|
492
|
+
mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
|
|
493
|
+
status=0
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
with patch("time.sleep"):
|
|
497
|
+
success, error, receipt = executor.execute_with_retry(
|
|
498
|
+
"0xSafe", mock_safe_tx, ["key1"]
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
assert success is False
|
|
502
|
+
assert "reverted" in error.lower()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def test_check_receipt_status_dict_format(executor):
|
|
506
|
+
"""Test receipt status check with dict-style receipt."""
|
|
507
|
+
# Dict-style receipt (not MagicMock)
|
|
508
|
+
receipt_dict = {"status": 1, "gasUsed": 21000}
|
|
509
|
+
assert executor._check_receipt_status(receipt_dict) is True
|
|
510
|
+
|
|
511
|
+
receipt_dict_failed = {"status": 0}
|
|
512
|
+
assert executor._check_receipt_status(receipt_dict_failed) is False
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def test_simulation_revert_not_nonce(executor, mock_chain_interface, mock_safe_tx, mock_safe):
|
|
516
|
+
"""Test handling when simulation reverts with non-nonce error."""
|
|
517
|
+
with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
|
|
518
|
+
# Simulation fails with generic revert
|
|
519
|
+
mock_safe_tx.call.side_effect = ValueError("execution reverted: insufficient funds")
|
|
520
|
+
|
|
521
|
+
with patch("time.sleep"):
|
|
522
|
+
success, error, receipt = executor.execute_with_retry(
|
|
523
|
+
"0xSafe", mock_safe_tx, ["key1"]
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
assert success is False
|
|
527
|
+
assert "insufficient funds" in error.lower() or "reverted" in error.lower()
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def test_gas_error_strategy_triggers_retry(
|
|
531
|
+
executor, mock_chain_interface, mock_safe_tx, mock_safe
|
|
532
|
+
):
|
|
533
|
+
"""Test that gas errors trigger retry with gas increase strategy."""
|
|
534
|
+
with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
|
|
535
|
+
mock_safe_tx.execute.side_effect = [
|
|
536
|
+
ValueError("intrinsic gas too low"),
|
|
537
|
+
b"tx_hash",
|
|
538
|
+
]
|
|
539
|
+
mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = MagicMock(
|
|
540
|
+
status=1
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
with patch("time.sleep"):
|
|
544
|
+
success, tx_hash, receipt = executor.execute_with_retry(
|
|
545
|
+
"0xSafe", mock_safe_tx, ["key1"]
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Should have retried and succeeded
|
|
549
|
+
assert success is True
|
|
550
|
+
assert mock_safe_tx.execute.call_count == 2
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def test_rpc_rotation_stops_when_should_not_retry(
|
|
554
|
+
executor, mock_chain_interface, mock_safe_tx, mock_safe
|
|
555
|
+
):
|
|
556
|
+
"""Test that execution stops when RPC handler says not to retry."""
|
|
557
|
+
with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
|
|
558
|
+
mock_safe_tx.execute.side_effect = ValueError("Rate limit exceeded")
|
|
559
|
+
mock_chain_interface._is_rate_limit_error.return_value = True
|
|
560
|
+
mock_chain_interface._handle_rpc_error.return_value = {"should_retry": False}
|
|
561
|
+
|
|
562
|
+
with patch("time.sleep"):
|
|
563
|
+
success, error, receipt = executor.execute_with_retry(
|
|
564
|
+
"0xSafe", mock_safe_tx, ["key1"]
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
assert success is False
|
|
568
|
+
# Only 1 attempt because should_retry=False
|
|
569
|
+
assert mock_safe_tx.execute.call_count == 1
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|