diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23a9951 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +ENV/ +env/ +.venv + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index e6f9cfd..343baa5 100644 --- a/README.md +++ b/README.md @@ -136,4 +136,60 @@ Before submitting an EIP (Ethereum Improvement Proposal), ensure you have: --- +**Note**: This repository (Darliewithrow/master) is a guide repository. The actual EIPs should be submitted to https://github.com/ethereum/EIPs following the process described above. + +--- + +## Transaction ID Handling + +This repository includes a Python utility (`transaction_id.py`) for working with Ethereum transaction IDs (transaction hashes). + +### What is an Ethereum Transaction ID? + +An Ethereum transaction ID is a 32-byte hash represented as a 66-character hex string, for example: + +``` +0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1 +``` + +It starts with `0x` and is followed by exactly 64 hexadecimal digits. + +### Usage + +```python +from transaction_id import ( + is_valid_transaction_id, + normalize_transaction_id, + format_transaction_id, + parse_transaction_ids, +) + +tx = "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1" + +# Validate +is_valid_transaction_id(tx) # True + +# Normalize (lowercase, ensure 0x prefix) +normalize_transaction_id("0xABC123DEF456ABC123DEF456ABC123DEF456ABC123DEF456ABC123DEF456ABC1") # "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1" + +# Format (full or shortened) +format_transaction_id(tx) # full form +format_transaction_id(tx, short=True) # "0xabc123...56abc1" + +# Extract all valid IDs from a block of text +parse_transaction_ids("tx1=0x... tx2=0x...") +``` + +Run the script directly: + +```bash +python3 transaction_id.py # Uses built-in example data +python3 transaction_id.py txdata.txt # Reads from a file +``` + +Run the test suite: + +```bash +python3 test_transaction_id.py +``` > **Note**: This repository (`Darliewithrow/master`) is a guide repository. The actual EIPs should be submitted to [https://github.com/ethereum/EIPs](https://github.com/ethereum/EIPs) following the process described above. diff --git a/test_transaction_id.py b/test_transaction_id.py new file mode 100644 index 0000000..bd12a2b --- /dev/null +++ b/test_transaction_id.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +Test suite for Transaction ID Handler +""" + +import unittest +from transaction_id import ( + is_valid_transaction_id, + normalize_transaction_id, + format_transaction_id, + parse_transaction_ids, +) + + +class TestIsValidTransactionId(unittest.TestCase): + """Tests for is_valid_transaction_id.""" + + def test_valid_lowercase(self): + tx = "0x" + "a" * 64 + self.assertTrue(is_valid_transaction_id(tx)) + + def test_valid_uppercase(self): + tx = "0x" + "A" * 64 + self.assertTrue(is_valid_transaction_id(tx)) + + def test_valid_mixed_case(self): + tx = "0xaAbBcCdDeEfF" + "0" * 52 + self.assertTrue(is_valid_transaction_id(tx)) + + def test_too_short(self): + tx = "0x" + "a" * 63 + self.assertFalse(is_valid_transaction_id(tx)) + + def test_too_long(self): + tx = "0x" + "a" * 65 + self.assertFalse(is_valid_transaction_id(tx)) + + def test_missing_prefix(self): + tx = "a" * 64 + self.assertFalse(is_valid_transaction_id(tx)) + + def test_invalid_hex_chars(self): + tx = "0x" + "g" * 64 + self.assertFalse(is_valid_transaction_id(tx)) + + def test_empty_string(self): + self.assertFalse(is_valid_transaction_id("")) + + def test_non_string_input(self): + self.assertFalse(is_valid_transaction_id(12345)) # type: ignore[arg-type] + + +class TestNormalizeTransactionId(unittest.TestCase): + """Tests for normalize_transaction_id.""" + + def test_uppercase_normalized_to_lowercase(self): + tx = "0x" + "A" * 64 + self.assertEqual(normalize_transaction_id(tx), "0x" + "a" * 64) + + def test_prefix_added_when_missing(self): + tx = "a" * 64 + self.assertEqual(normalize_transaction_id(tx), "0x" + "a" * 64) + + def test_leading_whitespace_stripped(self): + tx = " 0x" + "b" * 64 + self.assertEqual(normalize_transaction_id(tx), "0x" + "b" * 64) + + def test_invalid_returns_none(self): + self.assertIsNone(normalize_transaction_id("not_a_tx_id")) + + def test_non_string_returns_none(self): + self.assertIsNone(normalize_transaction_id(None)) # type: ignore[arg-type] + + def test_already_normalized(self): + tx = "0x" + "1234567890abcdef" * 4 + self.assertEqual(normalize_transaction_id(tx), tx) + + +class TestFormatTransactionId(unittest.TestCase): + """Tests for format_transaction_id.""" + + def test_full_format(self): + tx = "0x" + "a" * 64 + self.assertEqual(format_transaction_id(tx), "0x" + "a" * 64) + + def test_short_format(self): + tx = "0x" + "abcdef" + "0" * 52 + "123456" + result = format_transaction_id(tx, short=True) + self.assertEqual(result, "0xabcdef...123456") + + def test_invalid_returns_none(self): + self.assertIsNone(format_transaction_id("invalid")) + + def test_normalizes_case(self): + tx = "0x" + "A" * 64 + self.assertEqual(format_transaction_id(tx), "0x" + "a" * 64) + + +class TestParseTransactionIds(unittest.TestCase): + """Tests for parse_transaction_ids.""" + + def test_single_tx_id(self): + tx = "0x" + "1" * 64 + result = parse_transaction_ids(f"tx={tx}") + self.assertEqual(result, [tx]) + + def test_multiple_tx_ids(self): + tx1 = "0x" + "1" * 64 + tx2 = "0x" + "2" * 64 + result = parse_transaction_ids(f"{tx1} {tx2}") + self.assertEqual(result, [tx1, tx2]) + + def test_duplicates_removed(self): + tx = "0x" + "a" * 64 + result = parse_transaction_ids(f"{tx} {tx}") + self.assertEqual(result, [tx]) + + def test_invalid_entries_skipped(self): + tx = "0x" + "c" * 64 + result = parse_transaction_ids(f"garbage {tx} more_garbage") + self.assertEqual(result, [tx]) + + def test_empty_string(self): + self.assertEqual(parse_transaction_ids(""), []) + + def test_no_tx_ids(self): + self.assertEqual(parse_transaction_ids("no transaction ids here"), []) + + def test_case_normalization(self): + tx_upper = "0x" + "A" * 64 + tx_lower = "0x" + "a" * 64 + # Both forms refer to the same ID; only one should appear + result = parse_transaction_ids(f"{tx_upper} {tx_lower}") + self.assertEqual(len(result), 1) + self.assertEqual(result[0], tx_lower) + + +if __name__ == "__main__": + unittest.main() diff --git a/transaction_id.py b/transaction_id.py new file mode 100644 index 0000000..cfcb19b --- /dev/null +++ b/transaction_id.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +Transaction ID Handler + +This module provides functions to validate, normalize, and handle +Ethereum transaction IDs (transaction hashes). + +Ethereum transaction IDs are 32-byte values represented as 66-character +hex strings (including the '0x' prefix). +""" + +import re +import sys +from typing import List, Optional + + +# Ethereum transaction ID pattern: '0x' followed by exactly 64 hex characters +TX_ID_PATTERN = re.compile(r'^0x[0-9a-fA-F]{64}$') +TX_ID_LENGTH = 64 # hex characters after '0x' + + +def is_valid_transaction_id(tx_id: str) -> bool: + """ + Check whether a string is a valid Ethereum transaction ID. + + A valid transaction ID starts with '0x' and is followed by + exactly 64 hexadecimal characters (case-insensitive). + + Args: + tx_id: The string to validate. + + Returns: + True if tx_id is a valid Ethereum transaction ID, False otherwise. + """ + if not isinstance(tx_id, str): + return False + return bool(TX_ID_PATTERN.match(tx_id)) + + +def normalize_transaction_id(tx_id: str) -> Optional[str]: + """ + Normalize a transaction ID to lowercase hex with '0x' prefix. + + Args: + tx_id: A raw transaction ID string. May include or omit the '0x' + prefix and may use upper- or lower-case hex digits. + + Returns: + The normalized transaction ID (lowercase, with '0x' prefix), or + None if the input cannot be normalized to a valid ID. + """ + if not isinstance(tx_id, str): + return None + + stripped = tx_id.strip() + + # Add '0x' prefix if missing + if not stripped.startswith('0x') and not stripped.startswith('0X'): + stripped = '0x' + stripped + + normalized = '0x' + stripped[2:].lower() + + if not is_valid_transaction_id(normalized): + return None + + return normalized + + +def format_transaction_id(tx_id: str, short: bool = False) -> Optional[str]: + """ + Return a human-readable representation of a transaction ID. + + Args: + tx_id: A transaction ID string (with or without '0x' prefix). + short: If True, return a shortened form showing the first and last + six hex characters separated by '...'. + + Returns: + The formatted string, or None if tx_id is not valid. + """ + normalized = normalize_transaction_id(tx_id) + if normalized is None: + return None + + if short: + # e.g. 0xabcdef...123456 + hex_part = normalized[2:] + return f"0x{hex_part[:6]}...{hex_part[-6:]}" + + return normalized + + +def parse_transaction_ids(data: str) -> List[str]: + """ + Extract all valid Ethereum transaction IDs from a block of text. + + Args: + data: A string that may contain one or more transaction IDs. + + Returns: + A list of unique, normalized transaction IDs found in the text, + in the order they first appear. + """ + if not data: + return [] + + # Find all candidate '0x...' tokens + candidates = re.findall(r'0x[0-9a-fA-F]+', data) + + seen: set = set() + result: List[str] = [] + for candidate in candidates: + normalized = normalize_transaction_id(candidate) + if normalized and normalized not in seen: + seen.add(normalized) + result.append(normalized) + + return result + + +def main() -> None: + if len(sys.argv) > 1: + file_path = sys.argv[1] + try: + with open(file_path, 'r') as f: + input_data = f.read().strip() + print(f"Reading from file: {file_path}") + except FileNotFoundError: + print(f"Error: File '{file_path}' not found") + sys.exit(1) + else: + # Example Ethereum transaction IDs for demonstration + input_data = ( + "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1 " + "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF " + "invalid_tx " + "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1" + ) + + print(f"Input data:\n{input_data}\n") + + tx_ids = parse_transaction_ids(input_data) + + print("Found transaction IDs:") + for i, tx_id in enumerate(tx_ids, 1): + short = format_transaction_id(tx_id, short=True) + print(f" {i}. {tx_id} (short: {short})") + + print(f"\nTotal unique transaction IDs: {len(tx_ids)}") + + +if __name__ == "__main__": + main()