EtheremAddress.elm

unlisted ⁨1⁩ ⁨file⁩ 2019-03-20 08:56:32 UTC

EtheremAddress.elm

Raw
module EthereumAddress exposing
    ( EthereumAddress
    , compare
    , decoder
    , encode
    , equal
    , fromString
    , isChecksumAddress
    , remove0x
    , toString
    )

import Hex
import Json.Decode as D
import Json.Encode as E exposing (Value)
import Keccak.Int exposing (ethereum_keccak_256)
import Regex
import Result.Extra


type EthereumAddress
    = EthereumAddress String


{-| -}
remove0x : String -> String
remove0x str =
    if String.startsWith "0x" str || String.startsWith "0X" str then
        String.dropLeft 2 str

    else
        str


equal : EthereumAddress -> EthereumAddress -> Bool
equal (EthereumAddress address1) (EthereumAddress address2) =
    address1 == address2


compare : EthereumAddress -> EthereumAddress -> Order
compare (EthereumAddress address1) (EthereumAddress address2) =
    let
        isSmaller =
            address1 < address2

        isEqual =
            address1 == address2
    in
    case isEqual of
        True ->
            EQ

        False ->
            case isSmaller of
                True ->
                    LT

                False ->
                    GT


{-| -}
add0x : String -> String
add0x str =
    if String.startsWith "0x" str || String.startsWith "0X" str then
        str

    else
        "0x" ++ str


isLowerCaseAddress : String -> Bool
isLowerCaseAddress =
    Regex.contains (Maybe.withDefault Regex.never (Regex.fromString "^((0[Xx]){1})?[0-9a-f]{40}$"))


isUpperCaseAddress : String -> Bool
isUpperCaseAddress =
    Regex.contains (Maybe.withDefault Regex.never (Regex.fromString "^((0[Xx]){1})?[0-9A-F]{40}$"))


{-| Check if given string is a valid address.
**Note**: Works on mixed case strings, with or without the 0x.
-}
isAddress : String -> Bool
isAddress =
    Regex.contains (Maybe.withDefault Regex.never (Regex.fromString "^((0[Xx]){1})?[0-9A-Fa-f]{40}$"))


{-| Check if given string is a valid checksum address.
-}
isChecksumAddress : String -> Bool
isChecksumAddress str =
    let
        checksumTestChar addrChar hashInt =
            if hashInt >= 8 && Char.isLower addrChar || hashInt < 8 && Char.isUpper addrChar then
                False

            else
                True

        ( addrChars, hashInts ) =
            checksumHelper (remove0x str)

        checksumCorrect =
            List.map2 checksumTestChar addrChars hashInts
    in
    if isAddress str then
        List.all identity checksumCorrect

    else
        False


{-| -}
toByteLength : String -> String
toByteLength str =
    if String.length str == 1 then
        String.append "0" str

    else
        str



-- Checksum helpers


{-| Takes first 20 bytes of keccak'd address, and converts each hex char to an int
Packs this list into a tuple with the split up address chars so a comparison can be made between the two.
**Note**: Only functions which have already removed "0x" should be calling this.
-}
checksumHelper : String -> ( List Char, List Int )
checksumHelper zeroLessAddress =
    let
        addressChars =
            String.toList zeroLessAddress
    in
    addressChars
        |> List.map (Char.toLower >> Char.toCode)
        |> ethereum_keccak_256
        |> List.take 20
        |> List.map (Hex.toString >> toByteLength)
        |> String.join ""
        |> String.split ""
        |> List.map Hex.fromString
        |> Result.Extra.combine
        |> Result.withDefault []
        |> (\b -> ( addressChars, b ))


compareCharToHash : Char -> Int -> Char
compareCharToHash addrChar hashInt =
    if hashInt >= 8 then
        Char.toUpper addrChar

    else
        addrChar


checksumIt : String -> String
checksumIt str =
    let
        ( addrChars, hashInts ) =
            String.toLower str
                |> remove0x
                |> checksumHelper
    in
    List.map2 compareCharToHash addrChars hashInts
        |> String.fromList


{-| -}
quote : String -> String
quote str =
    "\"" ++ str ++ "\""


fromString : String -> Result String EthereumAddress
fromString addressStr =
    let
        noZeroX =
            remove0x addressStr

        bytes32Address =
            String.right 40 addressStr

        emptyZerosInBytes32 =
            String.left 24 noZeroX
    in
    -- Address is always stored without "0x"
    if String.length noZeroX == 64 && String.all ((==) '0') emptyZerosInBytes32 then
        if isUpperCaseAddress bytes32Address || isLowerCaseAddress bytes32Address then
            bytes32Address
                |> checksumIt
                |> EthereumAddress
                |> Ok

        else if isChecksumAddress bytes32Address then
            bytes32Address
                |> checksumIt
                |> EthereumAddress
                |> Ok

        else
            Err <| "Given address " ++ quote addressStr ++ " failed the EIP-55 checksum test."

    else if String.length noZeroX /= 40 then
        Err <| "Given address " ++ quote addressStr ++ " is not the correct length."

    else if not (isAddress noZeroX) then
        Err <| "Given address " ++ quote addressStr ++ " contains invalid hex characters."

    else if isUpperCaseAddress noZeroX || isLowerCaseAddress noZeroX then
        noZeroX
            |> checksumIt
            |> EthereumAddress
            |> Ok

    else if isChecksumAddress noZeroX then
        noZeroX
            |> EthereumAddress
            |> Ok

    else
        Err <| "Given address " ++ quote addressStr ++ " failed the EIP-55 checksum test."


{-| Convert an Address to a String
-}
toString : EthereumAddress -> String
toString (EthereumAddress addressStr) =
    add0x addressStr


encode : EthereumAddress -> Value
encode address =
    address
        |> toString
        |> E.string


decoder : D.Decoder EthereumAddress
decoder =
    D.string
        |> D.andThen
            (\addressStr ->
                case fromString addressStr of
                    Ok address ->
                        D.succeed address

                    Err error ->
                        D.fail <| "Error decoding ethereum address with message: " ++ error
            )