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 )