EtheremAddress.elm
@@ -0,0 +1,254 @@
+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
+ )