from starkware.starknet.public.abi import starknet_keccak
def checksum_encode(addr: int): # Takes a 32-byte binary address as input
addr_bytes = int.to_bytes(addr, 32, "big")
hex_addr = addr_bytes.hex()
checksummed_buffer = ""
# Treat the hex address as ascii/utf-8 for hashing
hashed_address = f"{starknet_keccak(addr_bytes):064x}"
# Iterate over each character in the hex address
for nibble_index, character in enumerate(hex_addr):
if character in "0123456789":
# We can't upper-case the decimal digits
checksummed_buffer += character
elif character in "abcdef":
# Check if the corresponding hex digit (nibble) in the hash is 8 or higher
hashed_address_nibble = int(hashed_address[nibble_index], 16)
if hashed_address_nibble > 7:
checksummed_buffer += character.upper()
else:
checksummed_buffer += character
else:
raise eth_utils.ValidationError(
f"Unrecognized hex character {character!r} at position {nibble_index}"
)
return "0x" + checksummed_buffer
checksum_encode(0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7)
# 0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7
checksum_encode(0xbd3fcc84644ddd6b96f7c741b1562b82f9e004dc7)
# 0x00000000000000000000000Bd3fcc84644dDd6B96F7C741b1562B82f9e004dc7
It’s a huge PITA because every tool treats it differently (SDKs, Apibara, block explorers, etc.) and while 0x0f00 and 0xf00 are numerically the same, since they pretty much have to be stored as strings for interop, this introduces hard to spot bugs.
from starkware.starknet.public.abi import starknet_keccak
from typing import Union
class AddressValidationError(Exception):
“”“Custom exception for address validation errors.”“”
pass
def checksum_encode(addr: Union[int, str]) → str:
“”"
Generate a checksummed address following ERC-55 style convention for Starknet addresses.
Args:
addr (Union[int, str]): Address as integer or hex string
Returns:
str: Checksummed address string
Raises:
AddressValidationError: If address is invalid
"""
try:
# Convert string input to int if needed
if isinstance(addr, str):
addr = int(addr.replace("0x", ""), 16)
# Validate address size
if addr < 0:
raise AddressValidationError("Address cannot be negative")
if addr.bit_length() > 256:
raise AddressValidationError("Address exceeds 32 bytes")
# Convert to 32-byte representation
addr_bytes = int.to_bytes(addr, 32, "big")
hex_addr = addr_bytes.hex()
# Calculate hash using starknet_keccak
hashed_address = f"{starknet_keccak(addr_bytes):064x}"
checksummed_buffer = []
# Process each character
for nibble_index, character in enumerate(hex_addr):
if character in "0123456789":
checksummed_buffer.append(character)
elif character in "abcdef":
# Check corresponding hash nibble
hash_nibble = int(hashed_address[nibble_index], 16)
checksummed_buffer.append(
character.upper() if hash_nibble > 7 else character
)
else:
raise AddressValidationError(
f"Invalid hex character '{character}' at position {nibble_index}"
)
return "0x" + "".join(checksummed_buffer)
except ValueError as e:
raise AddressValidationError(f"Invalid address format: {str(e)}")
def validate_checksum(address: str) → bool:
“”"
Validate if an address string has the correct checksum.
Args:
address (str): Address string to validate
Returns:
bool: True if checksum is valid, False otherwise
"""
try:
if not address.startswith("0x"):
return False
addr_int = int(address.replace("0x", ""), 16)
return checksum_encode(addr_int) == address
except (AddressValidationError, ValueError):
return False
Could we get buy-in from SDKs, as well as Wallets?
We’d like an answer and plan to include (and possible alerts and warnings on non-backward compatibility) from:
Okay, if it’s just about formalizing, can we make sure the SNIP matches the implementation?
Migrating to a new implementation would be painful for everyone
Hey @ClementWalter, I want to try and push this SNIP forward. Have you taken a look at the BigNumberish type in Starknet.js and check if the current SNIP matches the implementation?
Not really yet. Let me summarize everything after Christmas, including feedback regarding starknet js existing implementation, and move forward beginning of Jan
So summarizing the inputs from @haroldtrk and @0xjanek, here is a python implementation matching the already existing starknet-js one.
The key difference between the two implementation (proposed and refactored python VS starknet-js) is that starknet-js is hashing the minimal length input address (so a message of possibly less than 32 bytes) while I was proposing to hash the input address interpreted as a 32 bytes long message.
I don’t see any issue adopting the existing starknet-js implementation, and consequently the final proposition consistent with the existing starknet-js is
import math
from typing import Union
from starkware.starknet.public.abi import starknet_keccak
class AddressValidationError(Exception):
"""Custom exception for address validation errors."""
pass
def checksum_encode(addr: Union[int, str]) -> str:
"""
Generate a checksummed address following ERC-55 style convention for Starknet addresses.
Args:
addr (Union[int, str]): Address as integer or hex string
Returns:
str: Checksummed address string
Raises:
AddressValidationError: If address is invalid
"""
try:
# Convert string input to int if needed
if isinstance(addr, str):
addr = int(addr.replace("0x", ""), 16)
# Validate address size
if addr < 0:
raise AddressValidationError("Address cannot be negative")
if addr.bit_length() > 256:
raise AddressValidationError("Address exceeds 32 bytes")
# Convert to 32-byte representation
hex_addr = int.to_bytes(addr, 32, "big").hex()
# Calculate hash using starknet_keccak
addr_bytes = int.to_bytes(addr, math.ceil(addr.bit_length() / 8), "big")
hashed_address = f"{starknet_keccak(addr_bytes):064x}"
checksummed_buffer = []
# Process each character
for nibble_index, character in enumerate(hex_addr):
if character in "0123456789":
checksummed_buffer.append(character)
elif character in "abcdef":
# Check corresponding hash nibble
hash_nibble = int(hashed_address[nibble_index], 16)
checksummed_buffer.append(
character.upper() if hash_nibble > 7 else character
)
else:
raise AddressValidationError(
f"Invalid hex character '{character}' at position {nibble_index}"
)
return "0x" + "".join(checksummed_buffer)
except ValueError as e:
raise AddressValidationError(f"Invalid address format: {str(e)}")
def validate_checksum(address: str) -> bool:
"""
Validate if an address string has the correct checksum.
Args:
address (str): Address string to validate
Returns:
bool: True if checksum is valid, False otherwise
"""
try:
if not address.startswith("0x"):
return False
addr_int = int(address.replace("0x", ""), 16)
return checksum_encode(addr_int) == address
except (AddressValidationError, ValueError):
return False
Encoding addresses in a typo-resistant format reduces the likelihood of funds being lost or misrouted due to human errors in address entry. This is a practical improvement that enhances the user experience and trust in the ecosystem.
Personally i have had issues with typos, thankfully it is not a huge fund