-
Notifications
You must be signed in to change notification settings - Fork 0
Add Ethereum transaction ID handling utility #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: copilot/resolve-issue-title
Are you sure you want to change the base?
Changes from all commits
4cde653
1a6c517
a695e6a
f15b091
d2b1147
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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...") | ||||||||||||
|
||||||||||||
| parse_transaction_ids("tx1=0x... tx2=0x...") | |
| parse_transaction_ids( | |
| "tx1=0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1 " | |
| "tx2=0xdef456abc123def456abc123def456abc123def456abc123def456abc123def4" | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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}") | ||
|
Comment on lines
+129
to
+133
|
||
| self.assertEqual(len(result), 1) | ||
| self.assertEqual(result[0], tx_lower) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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' | ||||||
|
Darliewithrow marked this conversation as resolved.
|
||||||
|
|
||||||
|
|
||||||
| 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) | ||||||
|
|
||||||
|
Darliewithrow marked this conversation as resolved.
|
||||||
| 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: | ||||||
|
||||||
| with open(file_path, 'r') as f: | |
| with open(file_path, 'r', encoding='utf-8') as f: |
Uh oh!
There was an error while loading. Please reload this page.