lib/eth/address.rb



# Copyright (c) 2016-2025 The Ruby-Eth Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Provides the {Eth} module.
module Eth

  # The {Eth::Address} class to handle checksummed Ethereum addresses.
  class Address

    # The literal zero address 0x0.
    ZERO = "0x0000000000000000000000000000000000000000"

    # Provides a special checksum error if EIP-55 is violated.
    class CheckSumError < StandardError; end

    # The prefixed and checksummed Ethereum address.
    attr_reader :address

    # Constructor of the {Eth::Address} class. Creates a new hex
    # prefixed address.
    #
    # @param address [String] hex string representing an ethereum address.
    def initialize(address)
      unless Util.hex? address
        raise CheckSumError, "Unknown address type #{address}!"
      end
      @address = Util.prefix_hex address
      unless self.valid?
        raise CheckSumError, "Invalid address provided #{address}"
      end
    end

    # Checks that the address is valid.
    #
    # @return [Boolean] true if valid address.
    def valid?
      if !matches_any_format?
        false
      elsif not_checksummed?
        true
      else
        checksum_matches?
      end
    end

    # Checks that the address is the zero address.
    #
    # @return [Boolean] true if the address is the zero address.
    def zero?
      address == ZERO
    end

    # Generate a checksummed address.
    #
    # @return [String] prefixed hexstring representing an checksummed address.
    def checksummed
      raise CheckSumError, "Invalid address: #{address}" unless matches_any_format?

      cased = unprefixed.chars.zip(checksum.chars).map do |char, check|
        check.match(/[0-7]/) ? char.downcase : char.upcase
      end

      Util.prefix_hex cased.join
    end

    alias :to_s :checksummed

    private

    # Checks whether the address checksum matches.
    def checksum_matches?
      address == checksummed
    end

    # Checks whether the address is not checksummed.
    def not_checksummed?
      all_uppercase? || all_lowercase?
    end

    # Checks whether the address is all upper-case.
    def all_uppercase?
      address.match /(?:0[xX])[A-F0-9]{40}/
    end

    # Checks whether the address is all lower-case.
    def all_lowercase?
      address.match /(?:0[xX])[a-f0-9]{40}/
    end

    # Checks whether the address matches any known format.
    def matches_any_format?
      address.match /\A(?:0[xX])[a-fA-F0-9]{40}\z/
    end

    # Computes the checksum of the address.
    def checksum
      Util.bin_to_hex Util.keccak256 unprefixed.downcase
    end

    # Removes the hex prefix.
    def unprefixed
      Util.remove_hex_prefix address
    end
  end
end