{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Language.Solidity.Abi
(
ContractAbi(..)
, Declaration(..)
, FunctionArg(..)
, EventArg(..)
, signature
, methodId
, eventId
, SolidityType(..)
, parseSolidityFunctionArgType
, parseSolidityEventArgType
) where
import Control.Monad (void)
import Crypto.Hash (Digest, Keccak_256, hash)
import Data.Aeson (FromJSON (parseJSON), Options (constructorTagModifier, fieldLabelModifier, sumEncoding),
SumEncoding (TaggedObject),
ToJSON (toJSON), defaultOptions)
import Data.Aeson.TH (deriveJSON)
import Data.Text (Text)
import qualified Data.Text as T (dropEnd, pack, take, unlines, unpack)
import Data.Text.Encoding (encodeUtf8)
import Text.Parsec (ParseError, char, choice, digit, eof,
lookAhead, many1, manyTill, optionMaybe,
parse, string, try, (<|>))
import Text.Parsec.Text (Parser)
import Data.String.Extra (toLowerFirst)
data FunctionArg = FunctionArg
{ funArgName :: Text
, funArgType :: Text
, funArgComponents :: Maybe [FunctionArg]
}
deriving (Show, Eq, Ord)
$(deriveJSON
(defaultOptions {fieldLabelModifier = toLowerFirst . drop 6})
''FunctionArg)
data EventArg = EventArg
{ eveArgName :: Text
, eveArgType :: Text
, eveArgIndexed :: Bool
}
deriving (Show, Eq, Ord)
$(deriveJSON
(defaultOptions {fieldLabelModifier = toLowerFirst . drop 6})
''EventArg)
data Declaration = DConstructor
{ conInputs :: [FunctionArg]
}
| DFunction
{ funName :: Text
, funConstant :: Bool
, funInputs :: [FunctionArg]
, funOutputs :: Maybe [FunctionArg]
}
| DEvent
{ eveName :: Text
, eveInputs :: [EventArg]
, eveAnonymous :: Bool
}
| DFallback
{ falPayable :: Bool
}
deriving Show
instance Eq Declaration where
(DConstructor a) == (DConstructor b) = length a == length b
(DFunction a _ _ _) == (DFunction b _ _ _) = a == b
(DEvent a _ _) == (DEvent b _ _) = a == b
(DFallback _) == (DFallback _) = True
(==) _ _ = False
instance Ord Declaration where
compare (DConstructor a) (DConstructor b) = compare (length a) (length b)
compare (DFunction a _ _ _) (DFunction b _ _ _) = compare a b
compare (DEvent a _ _) (DEvent b _ _) = compare a b
compare (DFallback _) (DFallback _) = EQ
compare DConstructor {} DFunction {} = LT
compare DConstructor {} DEvent {} = LT
compare DConstructor {} DFallback {} = LT
compare DFunction {} DConstructor {} = GT
compare DFunction {} DEvent {} = LT
compare DFunction {} DFallback {} = LT
compare DEvent {} DConstructor {} = GT
compare DEvent {} DFunction {} = GT
compare DEvent {} DFallback {} = LT
compare DFallback {} DConstructor {} = GT
compare DFallback {} DFunction {} = GT
compare DFallback {} DEvent {} = GT
$(deriveJSON (defaultOptions {
sumEncoding = TaggedObject "type" "contents"
, constructorTagModifier = toLowerFirst . drop 1
, fieldLabelModifier = toLowerFirst . drop 3 })
''Declaration)
newtype ContractAbi = ContractAbi { unAbi :: [Declaration] }
deriving (Eq, Ord)
instance FromJSON ContractAbi where
parseJSON = fmap ContractAbi . parseJSON
instance ToJSON ContractAbi where
toJSON = toJSON . unAbi
instance Show ContractAbi where
show (ContractAbi c) = T.unpack $ T.unlines $
[ "Contract:" ]
++ foldMap showConstructor c ++
[ "\tEvents:" ]
++ foldMap showEvent c ++
[ "\tMethods:" ]
++ foldMap showMethod c
showConstructor :: Declaration -> [Text]
showConstructor x = case x of
DConstructor{} -> ["\tConstructor " <> signature x]
_ -> []
showEvent :: Declaration -> [Text]
showEvent x = case x of
DEvent{} -> ["\t\t" <> signature x]
_ -> []
showMethod :: Declaration -> [Text]
showMethod x = case x of
DFunction{} ->
["\t\t" <> methodId x <> " " <> signature x]
_ -> []
signature :: Declaration -> Text
signature (DConstructor inputs) = "(" <> args inputs <> ")"
where
args [] = ""
args [x] = funArgType x
args (x:xs) = case funArgComponents x of
Nothing -> funArgType x <> "," <> args xs
Just cmps -> "(" <> args cmps <> ")," <> args xs
signature (DFallback _) = "()"
signature (DFunction name _ inputs _) = name <> "(" <> args inputs <> ")"
where
args :: [FunctionArg] -> Text
args [] = ""
args [x] = funArgType x
args (x:xs) = case funArgComponents x of
Nothing -> funArgType x <> "," <> args xs
Just cmps -> "(" <> args cmps <> ")," <> args xs
signature (DEvent name inputs _) = name <> "(" <> args inputs <> ")"
where
args :: [EventArg] -> Text
args = T.dropEnd 1 . foldMap (<> ",") . fmap eveArgType
sha3 :: Text -> Text
{-# INLINE sha3 #-}
sha3 x = T.pack (show digest)
where digest :: Digest Keccak_256
digest = hash (encodeUtf8 x)
methodId :: Declaration -> Text
{-# INLINE methodId #-}
methodId = ("0x" <>) . T.take 8 . sha3 . signature
eventId :: Declaration -> Text
{-# INLINE eventId #-}
eventId = ("0x" <>) . sha3 . signature
data SolidityType = SolidityBool
| SolidityAddress
| SolidityUint Int
| SolidityInt Int
| SolidityString
| SolidityBytesN Int
| SolidityBytes
| SolidityTuple Int [SolidityType]
| SolidityVector [Int] SolidityType
| SolidityArray SolidityType
deriving (Eq, Show)
numberParser :: Parser Int
numberParser = read <$> many1 digit
parseUint :: Parser SolidityType
parseUint = do
_ <- string "uint"
SolidityUint <$> numberParser
parseInt :: Parser SolidityType
parseInt = do
_ <- string "int"
SolidityInt <$> numberParser
parseBool :: Parser SolidityType
parseBool = string "bool" >> pure SolidityBool
parseString :: Parser SolidityType
parseString = string "string" >> pure SolidityString
parseBytes :: Parser SolidityType
parseBytes = do
_ <- string "bytes"
mn <- optionMaybe numberParser
pure $ maybe SolidityBytes SolidityBytesN mn
parseAddress :: Parser SolidityType
parseAddress = string "address" >> pure SolidityAddress
solidityBasicTypeParser :: Parser SolidityType
solidityBasicTypeParser =
choice [ try parseUint
, try parseInt
, try parseAddress
, try parseBool
, try parseString
, parseBytes
]
parseVector :: Parser SolidityType
parseVector = do
s <- solidityBasicTypeParser
ns <- many1Till lengthParser (lookAhead (void $ string "[]") <|> eof)
pure $ SolidityVector ns s
where
many1Till :: Parser Int -> Parser () -> Parser [Int]
many1Till p end = do
a <- p
as <- manyTill p end
return (a : as)
lengthParser = do
_ <- char '['
n <- numberParser
_ <- char ']'
pure n
parseArray :: Parser SolidityType
parseArray = do
s <- try (parseVector <* string "[]") <|> (solidityBasicTypeParser <* string "[]")
pure $ SolidityArray s
solidityTypeParser :: Parser SolidityType
solidityTypeParser =
choice [ try parseArray
, try parseVector
, solidityBasicTypeParser
]
parseSolidityFunctionArgType :: FunctionArg -> Either ParseError SolidityType
parseSolidityFunctionArgType (FunctionArg _ typ mcmps) = case mcmps of
Nothing -> parse solidityTypeParser "Solidity" typ
Just cmps ->
SolidityTuple (length cmps)
<$> mapM parseSolidityFunctionArgType cmps
parseSolidityEventArgType :: EventArg -> Either ParseError SolidityType
parseSolidityEventArgType (EventArg _ typ _) = parse solidityTypeParser "Solidity" typ