Revisions for ⁨EtheremAddress.elm⁩

View the changes made to this paste.

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

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
+            )