Skip to content

Instantly share code, notes, and snippets.

@cmditch
Last active March 23, 2018 21:29
Show Gist options
  • Save cmditch/459da1c9642c57191b4e6ec3972e4836 to your computer and use it in GitHub Desktop.
Save cmditch/459da1c9642c57191b4e6ec3972e4836 to your computer and use it in GitHub Desktop.
module EvmDecoder exposing (main)
import BigInt exposing (BigInt)
import Hex
import Html exposing (Html, text)
import Json.Decode as Decode exposing (Decoder)
import Result.Extra as Result
import String.Extra as String
import Regex
main : Html msg
main =
Decode.decodeString multiSigDecoder testEvmOutput
|> toString
|> text
type alias MultiSigWallet =
{ members : List Address
, totalFunds : BigInt
}
multiSigDecoder : Decoder MultiSigWallet
multiSigDecoder =
evmDecode MultiSigWallet
|> andMap (dArray address)
|> andMap uint
|> toElmDecoder
testEvmOutput =
"\"0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000003000000000000000000000000ED9878336d5187949E4ca33359D2C47c846c9Dd30000000000000000000000006A987e3C0cd7Ed478Ce18C4cE00a0B313299365B000000000000000000000000aD9178336d523494914ca37359D2456ef123466c\""
type alias EvmDecoder a =
Tape -> Result String ( Tape, a )
{-| Tape == (Init Tape, Live Tape)
Live Tape: Tape that is being read and eaten up in 32 byte / 64 character chunks
Init Tape: Untouched copy of the initial input string, i.e., the full hex return from a JSON RPC Call.
This remains untouched during the entire decoding process,
and is needed to help grab dynamic solidity values, such as 'bytes', 'address[]', or 'uint256[]'
-}
type alias Tape =
( String, String )
evmDecode : a -> EvmDecoder a
evmDecode r =
\data -> Ok ( data, r )
runDecoder : EvmDecoder a -> String -> Result String a
runDecoder evmDecoder string =
remove0x string
|> (\a -> ( a, a ))
|> evmDecoder
|> Result.map Tuple.second
toElmDecoder : EvmDecoder a -> Decoder a
toElmDecoder =
runDecoder >> makeDecoder
makeDecoder : (String -> Result String a) -> Decoder a
makeDecoder typeCaster =
let
convert n =
case typeCaster n of
Ok a ->
Decode.succeed a
Err error ->
Decode.fail error
in
Decode.string |> Decode.andThen convert
-- Decoders
uint : EvmDecoder BigInt
uint =
\( init, t ) ->
take64 t
|> add0x
|> BigInt.fromString
|> Result.fromMaybe "Error Decoding Uint into BigInt"
|> Result.map (\bi -> ( ( init, drop64 t ), bi ))
bool : EvmDecoder Bool
bool =
let
parseBool b =
case String.left 63 b |> String.all ((==) '0') of
True ->
case String.right 1 b of
"0" ->
Ok False
"1" ->
Ok True
_ ->
Err ("Boolean decode error." ++ b ++ " is not boolean.")
False ->
Err ("Boolean decode error." ++ b ++ " is not boolean.")
in
\( init, t ) ->
take64 t
|> parseBool
|> Result.map (\bi -> ( ( init, drop64 t ), bi ))
address : EvmDecoder Address
address =
\( init, t ) ->
take64 t
|> hexToAddress
|> Result.map (\address -> ( ( init, drop64 t ), address ))
{-| Decode Dynamically Sized Arrays
(dArray address) == address[]
-}
dArray : EvmDecoder a -> EvmDecoder (List a)
dArray decoder =
\( init, t ) ->
take64 t
|> buildDynArray init
|> Result.map (List.map (unpackDecoder decoder))
|> Result.andThen Result.combine
|> Result.map (\list -> ( ( init, drop64 t ), list ))
{-| Decode Statically Sized Arrays
(sArray 10 uint) == uint256[10]
-}
sArray : Int -> EvmDecoder a -> EvmDecoder (List a)
sArray arrSize decoder =
\( init, t ) ->
String.left (arrSize * 64) t
|> String.break 64
|> List.map (unpackDecoder decoder)
|> Result.combine
|> Result.map (\list -> ( ( init, String.dropLeft (arrSize * 64) t ), list ))
{-| Chain and Map Decoders
andMap is the same as `apply` or `<*>` in Haskell, except initial arguments are flipped to help with elm pipeline syntax.
-}
andMap : EvmDecoder a -> EvmDecoder (a -> b) -> EvmDecoder b
andMap dVal dFunc =
map2 (\f v -> f v) dFunc dVal
map2 : (a -> b -> c) -> EvmDecoder a -> EvmDecoder b -> EvmDecoder c
map2 f decA decB =
\tape0 ->
decA tape0
|> Result.andThen
(\( tape1, vA ) ->
decB tape1
|> Result.map (Tuple.mapSecond (f vA))
)
{-| Takes the index pointer to the beginning of the array data (the first piece being the array length)
and the full return data, and slices out the
Example - Here is a returns(address[],uint256)
0000000000000000000000000000000000000000000000000000000000000040 -- Start index of address[] (starts at byte 64, or the 128th character)
0000000000000000000000000000000000000000000000000000000000000123 -- Some Uint256
0000000000000000000000000000000000000000000000000000000000000003 -- Length of address[]
000000000000000000000000ED9878336d5187949E4ca33359D2C47c846c9Dd3 -- First Address
0000000000000000000000006A987e3C0cd7Ed478Ce18C4cE00a0B313299365B -- Second Address
000000000000000000000000aD9178336d523494914ca37359D2456ef123466c -- Third Address
buildDynArray fullReturnData startIndex
> Ok ["000000000000000000000000ED9878336d5187949E4ca33359D2C47c846c9Dd3", secondAddress, thirdAddress]
-}
buildDynArray : String -> String -> Result String (List String)
buildDynArray fullTape startIndex =
let
toIntIndex =
Hex.fromString >> Result.map ((*) 2)
sliceData dataIndex arrLength =
String.slice dataIndex (dataIndex + (arrLength * 64)) fullTape
in
toIntIndex startIndex
|> Result.andThen
(\index ->
String.slice index (index + 64) fullTape
|> Hex.fromString
|> Result.map (sliceData <| index + 64)
)
|> Result.map (String.break 64)
{-| Applies decoder to value without bothering with Tape State
Useful for mapping over lists built from dynamic types
-}
unpackDecoder : EvmDecoder a -> String -> Result String a
unpackDecoder decoder val =
decoder ( "", val )
|> Result.map Tuple.second
-- Utils
type Address
= Address String
add0x : String -> String
add0x str =
if String.startsWith "0x" str then
str
else
"0x" ++ str
remove0x : String -> String
remove0x str =
if String.startsWith "0x" str then
String.dropLeft 2 str
else
str
take64 : String -> String
take64 =
String.left 64
drop64 : String -> String
drop64 =
String.dropLeft 64
hexToAddress : String -> Result String Address
hexToAddress str =
let
addressRegex =
Regex.regex "^(0x){1}[0-9a-fA-F]{40}$"
address =
"0x" ++ String.dropLeft 24 str
in
if Regex.contains addressRegex address then
Ok (Address address)
else
Err "Invalid ethereum address format"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment