{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -- | -- Module : Data.Solidity.Abi.Json -- Copyright : Aleksandr Krupenkin 2016-2021 -- License : Apache-2.0 -- -- Maintainer : mail@akru.me -- Stability : experimental -- Portability : noportable -- -- JSON encoded contract ABI parsers. -- module Language.Solidity.Abi ( -- * Contract ABI declarations ContractAbi(..) , Declaration(..) , FunctionArg(..) , EventArg(..) , StateMutability(..) -- * Method/Event id encoder , signature , methodId , eventId -- * Solidity type parser , SolidityType(..) , parseSolidityFunctionArgType , parseSolidityEventArgType ) where import Control.Monad (void) import Crypto.Ethereum.Utils (keccak256) import Data.Aeson (FromJSON (parseJSON), Options (constructorTagModifier, fieldLabelModifier, sumEncoding), SumEncoding (TaggedObject), ToJSON (toJSON), defaultOptions, withObject, (.:), (.:?)) import Data.Aeson.TH (deriveJSON, deriveToJSON) import qualified Data.ByteArray as A (take) import Data.ByteArray.HexString (toText) import Data.Char (toLower) import Data.Text (Text) import qualified Data.Text as T (dropEnd, unlines, unpack) import Data.Text.Encoding (encodeUtf8) import Lens.Micro (over, _head) import Text.Parsec (ParseError, char, choice, digit, eof, lookAhead, many1, manyTill, optionMaybe, parse, string, try, (<|>)) import Text.Parsec.Text (Parser) -- | Method argument data FunctionArg = FunctionArg { funArgName :: Text -- ^ Argument name , funArgType :: Text -- ^ Argument type , funArgComponents :: Maybe [FunctionArg] -- ^ Argument components for tuples } deriving (Show, Eq, Ord) $(deriveJSON (defaultOptions {fieldLabelModifier = over _head toLower . drop 6}) ''FunctionArg) -- | Event argument data EventArg = EventArg { eveArgName :: Text -- ^ Argument name , eveArgType :: Text -- ^ Argument type , eveArgIndexed :: Bool -- ^ Argument is indexed (e.g. placed on topics of event) } deriving (Show, Eq, Ord) $(deriveJSON (defaultOptions {fieldLabelModifier = over _head toLower . drop 6}) ''EventArg) data StateMutability = SMPure | SMView | SMPayable | SMNonPayable deriving (Eq, Ord, Show) -- | Elementary contract interface item data Declaration = DConstructor { conInputs :: [FunctionArg] -- ^ Contract constructor } | DFunction { funName :: Text , funConstant :: Bool , funInputs :: [FunctionArg] , funOutputs :: Maybe [FunctionArg] -- ^ Method } | DEvent { eveName :: Text , eveInputs :: [EventArg] , eveAnonymous :: Bool -- ^ Event } | DFallback { falPayable :: Bool -- ^ Fallback function } 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 instance FromJSON Declaration where parseJSON = withObject "Declaration" $ \o -> do t :: Text <- o .: "type" case t of "fallback" -> DFallback <$> o .: "payable" "constructor" -> DConstructor <$> o .: "inputs" "event" -> DEvent <$> o .: "name" <*> o .: "inputs" <*> o .: "anonymous" "function" -> DFunction <$> o .: "name" <*> parseSm o <*> o .: "inputs" <*> o .:? "outputs" _ -> fail "value of 'type' not recognized" where parseSm o = do o .:? "stateMutability" >>= \case Nothing -> o .: "constant" Just sm -> pure $ sm `elem` [SMPure, SMView] $(deriveToJSON (defaultOptions { sumEncoding = TaggedObject "type" "contents" , constructorTagModifier = over _head toLower . drop 1 , fieldLabelModifier = over _head toLower . drop 3 }) ''Declaration) $(deriveJSON (defaultOptions { sumEncoding = TaggedObject "stateMutability" "contents" , constructorTagModifier = fmap toLower . drop 2 }) ''StateMutability) -- | Contract Abi is a list of method / event declarations 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] _ -> [] -- | Take a signature by given decl, e.g. foo(uint,string) 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 -- | Generate method selector by given method 'Delcaration' methodId :: Declaration -> Text {-# INLINE methodId #-} methodId = toText . A.take 4 . keccak256 . encodeUtf8 . signature -- | Generate event `topic0` hash by givent event 'Delcaration' eventId :: Declaration -> Text {-# INLINE eventId #-} eventId = toText . keccak256 . encodeUtf8 . signature -- | Solidity types and parsers data SolidityType = SolidityBool | SolidityAddress | SolidityUint Int | SolidityInt Int | SolidityString | SolidityBytesN Int | SolidityBytes | SolidityTuple [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 <$> mapM parseSolidityFunctionArgType cmps parseSolidityEventArgType :: EventArg -> Either ParseError SolidityType parseSolidityEventArgType (EventArg _ typ _) = parse solidityTypeParser "Solidity" typ