EtheremAddress.elm
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
)