web3 0.6.0.0 → 0.7.0.0
raw patch · 60 files changed
+3461/−1538 lines, 60 filesdep +asyncdep +basementdep +cerealdep −attoparsecdep −base16-bytestringdep −vectordep ~aesondep ~basedep ~bytestring
Dependencies added: async, basement, cereal, data-default, exceptions, generics-sop, hspec, hspec-contrib, hspec-discover, hspec-expectations, machines, mtl, parsec, split, stm, tagged, time
Dependencies removed: attoparsec, base16-bytestring, vector
Dependency ranges changed: aeson, base, bytestring, cryptonite, http-client, http-client-tls, memory, template-haskell, text
Files
- CHANGELOG.md +30/−0
- README.md +25/−23
- data/ERC20.json +0/−1
- examples/ERC20.hs +11/−0
- examples/ERC20.json +1/−0
- examples/TokenInfo.hs +29/−0
- src/Data/String/Extra.hs +25/−0
- src/Network/Ethereum/ABI/Class.hs +71/−0
- src/Network/Ethereum/ABI/Codec.hs +63/−0
- src/Network/Ethereum/ABI/Event.hs +144/−0
- src/Network/Ethereum/ABI/Event/Internal.hs +136/−0
- src/Network/Ethereum/ABI/Generic.hs +126/−0
- src/Network/Ethereum/ABI/Json.hs +251/−0
- src/Network/Ethereum/ABI/Prim/Address.hs +81/−0
- src/Network/Ethereum/ABI/Prim/Bool.hs +29/−0
- src/Network/Ethereum/ABI/Prim/Bytes.hs +110/−0
- src/Network/Ethereum/ABI/Prim/Int.hs +119/−0
- src/Network/Ethereum/ABI/Prim/List.hs +55/−0
- src/Network/Ethereum/ABI/Prim/String.hs +29/−0
- src/Network/Ethereum/ABI/Prim/Tagged.hs +36/−0
- src/Network/Ethereum/ABI/Prim/Tuple.hs +45/−0
- src/Network/Ethereum/ABI/Prim/Tuple/TH.hs +34/−0
- src/Network/Ethereum/Contract/Event.hs +202/−0
- src/Network/Ethereum/Contract/Method.hs +71/−0
- src/Network/Ethereum/Contract/TH.hs +277/−0
- src/Network/Ethereum/Unit.hs +14/−12
- src/Network/Ethereum/Web3.hs +34/−24
- src/Network/Ethereum/Web3/Address.hs +0/−72
- src/Network/Ethereum/Web3/Contract.hs +0/−165
- src/Network/Ethereum/Web3/Encoding.hs +0/−71
- src/Network/Ethereum/Web3/Encoding/Bytes.hs +0/−91
- src/Network/Ethereum/Web3/Encoding/Internal.hs +0/−101
- src/Network/Ethereum/Web3/Encoding/Tuple.hs +0/−41
- src/Network/Ethereum/Web3/Encoding/TupleTH.hs +0/−152
- src/Network/Ethereum/Web3/Eth.hs +50/−46
- src/Network/Ethereum/Web3/Internal.hs +0/−24
- src/Network/Ethereum/Web3/JsonAbi.hs +0/−146
- src/Network/Ethereum/Web3/JsonRpc.hs +0/−95
- src/Network/Ethereum/Web3/Net.hs +9/−7
- src/Network/Ethereum/Web3/Provider.hs +59/−19
- src/Network/Ethereum/Web3/TH.hs +0/−296
- src/Network/Ethereum/Web3/Types.hs +160/−85
- src/Network/Ethereum/Web3/Web3.hs +9/−6
- src/Network/JsonRpc/TinyClient.hs +168/−0
- test-support/contracts/Migrations.sol +23/−0
- test-support/contracts/SimpleStorage.sol +12/−0
- test-support/convertAbi.sh +9/−0
- test-support/inject-contract-addresses.sh +26/−0
- test-support/migrations/1_initial_migration.js +5/−0
- test-support/migrations/2_deploy_contracts.js +7/−0
- test-support/truffle.js +9/−0
- test/Network/Ethereum/Web3/Test/ComplexStorageSpec.hs +131/−0
- test/Network/Ethereum/Web3/Test/SimpleStorageSpec.hs +206/−0
- test/Network/Ethereum/Web3/Test/Utils.hs +102/−0
- test/Spec.hs +1/−27
- unit/Network/Ethereum/Web3/Test/EncodingSpec.hs +224/−0
- unit/Network/Ethereum/Web3/Test/EventSpec.hs +83/−0
- unit/Network/Ethereum/Web3/Test/MethodDumpSpec.hs +14/−0
- unit/Spec.hs +1/−0
- web3.cabal +105/−34
+ CHANGELOG.md view
@@ -0,0 +1,30 @@+# Changelog+All notable changes to this project will be documented in this file.++## [0.7.0.0] 2018-04-22+### Added+- This CHANGELOG.md file for significant changes tracking.+- Descriptive types for all JSON-RPC method parameters and returned values (#15).+- Widely use of basement:Word256 type for encoding.+- Full list of ethereum abi encoding types:+ * bool: `Bool`+ * int256: `IntN`+ * uint256: `UIntN`+ * string: `Text`+ * bytes: `Bytes`+ * bytes32: `BytesN`+ * dynamic array: `[]`+ * static array: `ListN`++### Changed+- Rewriten encoding engine for best performance, it now based on cereal:Serialize instead of parsec:Parser.+- Renamed encoding type classes and methods: `ABIEncode` -> `ABIPut`, `ABIDecode` -> `ABIGet`.+- Encoding related modules moved to Network.Ethereum.ABI.+- Primitive abi encoding types are moved to separated modules in Network.Ethereum.ABI.Prim.+- Contract interation related modules moved to Network.Ethereum.Contract.+- Ethereum node communication modules stay in Network.Ethereum.Web3.+- JSON-RPC tiny client is independent now and can be used separately.++### Removed+- `Event` type class, currently TH create `Data.Default` instance for `Filter e`.+- Custom setup for live testing (it replaced by travis script).
README.md view
@@ -23,27 +23,28 @@ Any Ethereum node communication wrapped with `Web3` monadic type. - > :t web3_clientVersion- web3_clientVersion :: Provider a => Web3 a Text+ > import Network.Ethereum.Web3.Web3+ > :t clientVersion+ clientVersion :: Web3 Text To run this computation used `runWeb3'` or `runWeb3` functions. - > runWeb3 web3_clientVersion+ > import Network.Ethereum.Web3+ > runWeb3 clientVersion Right "Parity//v1.4.5-beta-a028d04-20161126/x86_64-linux-gnu/rustc1.13.0" Function `runWeb3` use default `Web3` provider at `localhost:8545`. > :t runWeb3 runWeb3- :: MonadIO m => Web3 DefaultProvider b -> m (Either Web3Error b)+ :: MonadIO m => Web3 a -> m (Either Web3Error a) ### TemplateHaskell generator -[Quasiquotation](https://wiki.haskell.org/Quasiquotation) is used to parse-contract ABI or load from JSON file. [TemplateHaskell](https://wiki.haskell.org/Template_Haskell) driven Haskell contract API generator can automatical create instances for `Event` and `Method`-typeclasses and function helpers.+[Quasiquotation](https://wiki.haskell.org/Quasiquotation) is used to parse contract ABI or load from JSON file. [TemplateHaskell](https://wiki.haskell.org/Template_Haskell) driven Haskell contract API generator can automatical create ABI encoding instances and contract method helpers. > :set -XQuasiQuotes+ > import Network.Ethereum.Contract.TH > putStr [abiFrom|data/sample.json|] Contract: Events:@@ -53,22 +54,23 @@ 0x03de48b3 runA1() 0x90126c7a runA2(string,uint256) -See example of usage.+Use `-ddump-splices` to see generated code during compilation or in GHCi. See `examples` folder for more use cases. -```haskell-import Data.Text (unpack)-import Text.Printf+### Testing -[abiFrom|data/ERC20.json|]+Testing the `web3` is split up into two suites: `unit` and `live`.+The `unit` suite tests internal library facilities, while the `live` tests that+the library adequately interacts with a Web3 provider. -main :: IO ()-main = do- Right s <- runWeb3 $ do- n <- name token- s <- symbol token- d <- decimals token- return $ printf "Token %s with symbol %s and decimals %d"- (unpack n) (unpack s) d- putStrLn s- where token = "0x237D60A8b41aFD2a335305ed458B609D7667D789"-```+One may simply run `stack test` to run both suites, or `stack test web3:unit` or `stack test web3:live`+to run the test suites individually.++The `unit` suite has no external dependencies, while the `live` suite requires Truffle and `jq`+to be available on your machine.++The `live` suite also requires a Web3 provider with Ethereum capabilities, as well as+an unlocked account with ether to send transactions from. It uses Truffle to deploy testing contracts,+generating ABIs for them in the process, then using said ABIs as part of a TemplateHaskell step in the suite.+It is assumed that the provider is available at `http://localhost:8545`. If that's not the case, you must update `truffle.js`+so that Truffle can deploy the contracts correctly, and pass the `WEB3_PROVIDER=http://host:port` environment variable+when running the tests so that the `web3` library can interact with the chain that's being tested against.
− data/ERC20.json
@@ -1,1 +0,0 @@-[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"delegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"}],"name":"unapprove","outputs":[],"payable":false,"type":"function"},{"inputs":[{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_decimals","type":"uint8"},{"name":"_count","type":"uint256"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]
+ examples/ERC20.hs view
@@ -0,0 +1,11 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+module ERC20 where++import Network.Ethereum.Contract.TH++[abiFrom|examples/ERC20.json|]
+ examples/ERC20.json view
@@ -0,0 +1,1 @@+[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"}],"name":"delegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"}],"name":"unapprove","outputs":[],"payable":false,"type":"function"},{"inputs":[{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_decimals","type":"uint8"},{"name":"_count","type":"uint256"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]
+ examples/TokenInfo.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Data.Default (def)+import Data.Text (unpack)+import Text.Printf (printf)++import Network.Ethereum.Contract.TH+import Network.Ethereum.Web3 hiding (name)++import ERC20++main :: IO ()+main = do+ result <- runWeb3 $ do+ n <- name tokenCall+ s <- symbol tokenCall+ d <- decimals tokenCall+ return $ printf "Token %s with symbol %s and decimals %d"+ (unpack n) (unpack s) (fromIntegral d :: Int)+ case result of+ Left err -> print err+ Right info -> putStrLn info+ where+ token :: Address+ token = "0xA2f4FCb0FDe2dD59f7a1873e121bc5623e3164Eb"++ tokenCall :: Call+ tokenCall = def { callTo = Just token }
+ src/Data/String/Extra.hs view
@@ -0,0 +1,25 @@+-- |+-- Module : Data.String.Extra+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : portable+--+-- Extra string manipulation functions.+--++module Data.String.Extra where++import Data.Char (toLower, toUpper)++-- | Lower first char of string+toLowerFirst :: String -> String+toLowerFirst [] = []+toLowerFirst (x : xs) = toLower x : xs++-- | Upper first char of string+toUpperFirst :: String -> String+toUpperFirst [] = []+toUpperFirst (x : xs) = toUpper x : xs
+ src/Network/Ethereum/ABI/Class.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Network.Ethereum.ABI.Class+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI encoding base type classes.+--++module Network.Ethereum.ABI.Class (+ ABIType(..)+ , ABIPut(..)+ , ABIGet(..)+ , GenericABIPut(..)+ , GenericABIGet(..)+ ) where++import Data.Proxy (Proxy)+import Data.Serialize (Get, Putter)+import Generics.SOP (Generic, Rep, from, to)++-- | A class for abi encoding datatype descriptions+class ABIType a where+ isDynamic :: Proxy a -> Bool++-- | A class for encoding datatypes to their abi encoding+--+-- If your compiler has support for the @DeriveGeneric@ and+-- @DefaultSignatures@ language extensions (@ghc >= 7.2.1@),+-- the 'abiPut' method will have default generic implementations.+--+-- To use this option, simply add a @deriving 'Generic'@ clause+-- to your datatype and declare a 'ABIPut' instance for it without+-- giving a definition for 'abiPut'.+--+class ABIType a => ABIPut a where+ abiPut :: Putter a++ default abiPut :: (Generic a, Rep a ~ rep, GenericABIPut rep) => Putter a+ abiPut = gAbiPut . from++-- | A class for encoding generically composed datatypes to their abi encoding+class GenericABIPut a where+ gAbiPut :: Putter a++-- | A class for decoding datatypes from their abi encoding+--+-- If your compiler has support for the @DeriveGeneric@ and+-- @DefaultSignatures@ language extensions (@ghc >= 7.2.1@),+-- the 'abiGet' method will have default generic implementations.+--+-- To use this option, simply add a @deriving 'Generic'@ clause+-- to your datatype and declare a 'ABIGet' instance for it without+-- giving a definition for 'abiGet'.+--+class ABIType a => ABIGet a where+ abiGet :: Get a++ default abiGet :: (Generic a, Rep a ~ rep, GenericABIGet rep) => Get a+ abiGet = to <$> gAbiGet++-- | A class for decoding generically composed datatypes from their abi encoding+class GenericABIGet a where+ gAbiGet :: Get a
+ src/Network/Ethereum/ABI/Codec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Network.Ethereum.ABI.Codec+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI encoding codec functions.+--++module Network.Ethereum.ABI.Codec (+ encode+ , decode+ , encode'+ , decode'+ ) where++import Data.ByteArray (ByteArray, ByteArrayAccess,+ convert)+import Data.Serialize (runGet, runPut)+import Generics.SOP (Generic, Rep, from, to)++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ GenericABIGet (..),+ GenericABIPut (..))+import Network.Ethereum.ABI.Generic ()++-- | Encode datatype to Ethereum ABI-encoding+encode :: (ABIPut a, ByteArray ba)+ => a+ -> ba+{-# INLINE encode #-}+encode = convert . runPut . abiPut++-- | Generic driven version of 'encode'+encode' :: (Generic a,+ Rep a ~ rep,+ GenericABIPut rep,+ ByteArray ba)+ => a+ -> ba+{-# INLINE encode' #-}+encode' = convert . runPut . gAbiPut . from++-- | Decode datatype from Ethereum ABI-encoding+decode :: (ByteArrayAccess ba, ABIGet a)+ => ba+ -> Either String a+{-# INLINE decode #-}+decode = runGet abiGet . convert++-- | Generic driven version of 'decode'+decode' :: (Generic a,+ Rep a ~ rep,+ GenericABIGet rep,+ ByteArrayAccess ba)+ => ba+ -> Either String a+decode' = runGet (to <$> gAbiGet) . convert
+ src/Network/Ethereum/ABI/Event.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Network.Ethereum.Web3.Encoding.Event+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- This module is internal, the purpose is to define+-- helper classes and functions to assist in event decoding.+-- The user of this library should have no need to use+-- this directly in application code.+--++module Network.Ethereum.ABI.Event(+ DecodeEvent(..)+ , IndexedEvent(..)+ ) where++import Data.ByteArray (ByteArrayAccess)+import Data.Proxy (Proxy (..))+import Generics.SOP (Generic, I (..), NP (..),+ NS (..), Rep, SOP (..),+ from, to)++import Network.Ethereum.ABI.Class (GenericABIGet)+import Network.Ethereum.ABI.Codec (decode')+import Network.Ethereum.ABI.Event.Internal+import Network.Ethereum.Web3.Types (Change (..))++-- | Indexed event args come back in as a list of encoded values. 'ArrayParser'+-- | is used to decode these values so that they can be used to reconstruct the+-- | entire decoded event.+class ArrayParser a where+ arrayParser :: ByteArrayAccess ba+ => [ba]+ -> Either String a++instance ArrayParser (NP f '[]) where+ arrayParser _ = Right Nil++instance (ArrayParser (NP I as), Generic a, Rep a ~ rep, GenericABIGet rep)+ => ArrayParser (NP I (a : as)) where+ arrayParser [] = Left "Empty"+ arrayParser (a : as) = do+ a' <- decode' a+ as' <- arrayParser as+ return $ I a' :* as'++instance ArrayParser (NP f as) => ArrayParser (SOP f '[as]) where+ arrayParser = fmap (SOP . Z) . arrayParser++genericArrayParser :: ( Generic a+ , Rep a ~ rep+ , ArrayParser rep+ , ByteArrayAccess ba+ )+ => [ba]+ -> Either String a+genericArrayParser = fmap to . arrayParser++--------------------------------------------------------------------------------+-- Event Parsing+--------------------------------------------------------------------------------++data Event i ni = Event i ni++-- | 'parseChange' decodes both the indexed and non-indexed event components.+parseChange :: ( Generic i+ , Rep i ~ irep+ , ArrayParser irep+ , Generic ni+ , Rep ni ~ nirep+ , GenericABIGet nirep+ )+ => Change+ -> Bool+ -- ^ is anonymous event+ -> Either String (Event i ni)+parseChange change anonymous =+ Event <$> genericArrayParser topics <*> decode' data_+ where+ topics | anonymous = changeTopics change+ | otherwise = tail (changeTopics change)+ data_ = changeData change++class IndexedEvent i ni e | e -> i ni where+ isAnonymous :: Proxy e -> Bool++-- | 'CombineChange' is a class which indicates that given event components of types 'i'+-- | and 'ni', we can construct an event of type 'e'. The functional dependency is valid+-- | becasue of how the template haskell generates the event types.+class CombineChange i ni e | e -> i ni where+ combineChange :: i -> ni -> e++instance ( Generic i+ , Rep i ~ irep+ , Generic ni+ , Rep ni ~ nirep+ , Generic e+ , Rep e ~ erep+ , HListRep irep hli+ , HListRep nirep hlni+ , MergeIndexedArguments hli hlni+ , MergeIndexedArguments' hli hlni ~ hle+ , HListRep erep hle+ , IndexedEvent i ni e+ ) => CombineChange i ni e where+ combineChange i ni =+ let hli = toHList . from $ i+ hlni = toHList . from $ ni+ hle = mergeIndexedArguments hli hlni+ in to . fromHList $ hle++class DecodeEvent i ni e | e -> i ni where+ decodeEvent :: Change -> Either String e++instance ( IndexedEvent i ni e+ , Generic i+ , Rep i ~ SOP I '[hli]+ , Generic ni+ , Rep ni ~ SOP I '[hlni]+ , Generic e+ , Rep e ~ SOP I '[hle]+ , CombineChange i ni e+ , GenericABIGet (SOP I '[hlni])+ , ArrayParser (SOP I '[hli])+ ) => DecodeEvent i ni e where+ decodeEvent change = do+ let anonymous = isAnonymous (Proxy :: Proxy e)+ (Event i ni :: Event i ni) <- parseChange change anonymous+ return $ combineChange i ni
+ src/Network/Ethereum/ABI/Event/Internal.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Network.Ethereum.ABI.Event.Internal+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- This module is internal, the purpose is to define helper classes and types+-- to assist in event decoding. The user of this library should have no need to use+-- this directly in application code.+--++module Network.Ethereum.ABI.Event.Internal where++import Data.Kind (Type)+import Data.Proxy (Proxy (..))+import Data.Tagged (Tagged (..))+import Generics.SOP (I (..), NP (..), NS (..), SOP (..))+import GHC.TypeLits (CmpNat)++data HList :: [Type] -> Type where+ HNil :: HList '[]+ (:<) :: a -> HList as -> HList (a : as)++infixr 0 :<++-- | Generic representation to HList representation+class HListRep a xs | a -> xs, a -> xs where+ toHList :: a -> HList xs+ fromHList :: HList xs -> a++instance HListRep (NP I '[]) '[] where+ toHList _ = HNil+ fromHList _ = Nil++instance HListRep (NP I as) as => HListRep (NP I (a:as)) (a:as) where+ toHList (I a :* rest) = a :< toHList rest+ fromHList (a :< rest) = I a :* fromHList rest++instance HListRep (NP f as') as => HListRep (SOP f '[as']) as where+ toHList (SOP (Z rep)) = toHList rep+ toHList _ = error "Impossible branch"+ fromHList = SOP . Z . fromHList++-- | Sort a Tagged HList+class Sort (xs :: [Type]) where+ type Sort' xs :: [Type]+ sort :: HList xs -> HList (Sort' xs)++instance Sort '[] where+ type Sort' '[] = '[]+ sort HNil = HNil++instance (Sort xs, Insert x (Sort' xs)) => Sort (x : xs) where+ type Sort' (x : xs) = Insert' x (Sort' xs)+ sort (x :< xs) = insert x (sort xs)++class Insert (x :: Type) (xs :: [Type]) where+ type Insert' x xs :: [Type]+ insert :: x -> HList xs -> HList (Insert' x xs)++instance Insert x '[] where+ type Insert' x '[] = '[x]+ insert x HNil = x :< HNil++instance InsertCmp (CmpNat n m) (Tagged n x) (Tagged m y) ys => Insert (Tagged n x) (Tagged m y : ys) where+ type Insert' (Tagged n x) (Tagged m y : ys) = InsertCmp' (CmpNat n m) (Tagged n x) (Tagged m y) ys+ insert (x :: Tagged n x) ((y :: Tagged m y) :< ys) = insertCmp (Proxy :: Proxy (CmpNat n m)) x y ys++class InsertCmp (b :: Ordering) (x :: Type) (y :: Type) (ys :: [Type]) where+ type InsertCmp' b x y ys :: [Type]+ insertCmp :: Proxy (b :: Ordering) -> x -> y -> HList ys -> HList (InsertCmp' b x y ys)++instance InsertCmp 'LT x y ys where+ type InsertCmp' 'LT x y ys = x : (y : ys)+ insertCmp _ x y ys = x :< y :< ys++instance Insert x ys => InsertCmp 'GT x y ys where+ type InsertCmp' 'GT x y ys = y : Insert' x ys+ insertCmp _ x y ys = y :< insert x ys++-- | Unwrap all the Tagged items in an HList+class UnTag t where+ type UnTag' t :: [Type]+ unTag :: HList t -> HList (UnTag' t)++instance UnTag '[] where+ type UnTag' '[] = '[]+ unTag a = a++instance UnTag ts => UnTag (Tagged n a : ts) where+ type UnTag' (Tagged n a : ts) = a : UnTag' ts+ unTag (Tagged a :< ts) = a :< unTag ts++class HListMerge (as :: [Type]) (bs :: [Type]) where+ type Concat as bs :: [Type]+ mergeHList :: HList as -> HList bs -> HList (Concat as bs)++instance HListMerge '[] bs where+ type Concat '[] bs = bs+ mergeHList _ bs = bs++instance HListMerge as bs => HListMerge (a : as) bs where+ type Concat (a : as) bs = a : Concat as bs+ mergeHList (a :< as) bs = a :< mergeHList as bs++class HListMergeSort as bs where+ type MergeSort' as bs :: [Type]+ mergeSortHList :: HList as -> HList bs -> HList (MergeSort' as bs)++instance (HListMerge as bs, Concat as bs ~ cs, Sort cs, Sort' cs ~ cs') => HListMergeSort as bs where+ type MergeSort' as bs = Sort' (Concat as bs)+ mergeSortHList as bs = sort $ (mergeHList as bs :: HList cs)++class MergeIndexedArguments as bs where+ type MergeIndexedArguments' as bs :: [Type]+ mergeIndexedArguments :: HList as -> HList bs -> HList (MergeIndexedArguments' as bs)++instance (HListMergeSort as bs, MergeSort' as bs ~ cs, UnTag cs, UnTag cs' ~ ds) => MergeIndexedArguments as bs where+ type MergeIndexedArguments' as bs = (UnTag' (MergeSort' as bs))+ mergeIndexedArguments as bs = unTag $ mergeSortHList as bs
+ src/Network/Ethereum/ABI/Generic.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeInType #-}+{-# LANGUAGE TypeOperators #-}++-- |+-- Module : Network.Ethereum.ABI.Generic+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- This module is internal, the purpose is to define helper classes and functions+-- to assist in encoding and decoding Solidity types for function calls and events.+-- The user of this library should have no need to use this directly in application code.+--++module Network.Ethereum.ABI.Generic () where++import qualified Data.ByteString.Lazy as LBS+import Data.Int (Int64)+import qualified Data.List as L+import Data.Monoid ((<>))+import Data.Proxy (Proxy (..))+import Data.Serialize (Get, Put)+import Data.Serialize.Get (bytesRead, lookAheadE, skip)+import Data.Serialize.Put (runPutLazy)+import Generics.SOP (I (..), NP (..), NS (..),+ SOP (..))++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..),+ GenericABIGet (..),+ GenericABIPut (..))+import Network.Ethereum.ABI.Prim.Int (getWord256, putWord256)++data EncodedValue =+ EncodedValue { order :: Int64+ , offset :: Maybe Int64+ , encoding :: Put+ }++instance Eq EncodedValue where+ ev1 == ev2 = order ev1 == order ev2++instance Ord EncodedValue where+ compare ev1 ev2 = order ev1 `compare` order ev2++combineEncodedValues :: [EncodedValue] -> Put+combineEncodedValues encodings =+ let sortedEs = adjust headsOffset $ L.sort encodings+ encodings' = addTailOffsets headsOffset [] sortedEs+ in let heads = foldl (\acc EncodedValue{..} -> case offset of+ Nothing -> acc <> encoding+ Just o -> acc <> putWord256 (fromIntegral o)+ ) mempty encodings'+ tails = foldl (\acc EncodedValue{..} -> case offset of+ Nothing -> acc+ Just _ -> acc <> encoding+ ) mempty encodings'+ in heads <> tails+ where+ adjust :: Int64 -> [EncodedValue] -> [EncodedValue]+ adjust n = map (\ev -> ev {offset = (+) n <$> offset ev})+ addTailOffsets :: Int64 -> [EncodedValue] -> [EncodedValue] -> [EncodedValue]+ addTailOffsets init' acc es = case es of+ [] -> reverse acc+ (e : tail') -> case offset e of+ Nothing -> addTailOffsets init' (e : acc) tail'+ Just _ -> addTailOffsets init' (e : acc) (adjust (LBS.length . runPutLazy . encoding $ e) tail')+ headsOffset :: Int64+ headsOffset = foldl (\acc e -> case offset e of+ Nothing -> acc + (LBS.length . runPutLazy . encoding $ e)+ Just _ -> acc + 32+ ) 0 encodings++class ABIData a where+ _serialize :: [EncodedValue] -> a -> [EncodedValue]++instance ABIData (NP f '[]) where+ _serialize encoded _ = encoded++instance (ABIType b, ABIPut b, ABIData (NP I as)) => ABIData (NP I (b :as)) where+ _serialize encoded (I b :* a) =+ if isDynamic (Proxy :: Proxy b)+ then _serialize (dynEncoding : encoded) a+ else _serialize (staticEncoding : encoded) a+ where+ staticEncoding = EncodedValue { encoding = abiPut b+ , offset = Nothing+ , order = 1 + (fromInteger . toInteger . L.length $ encoded)+ }+ dynEncoding = EncodedValue { encoding = abiPut b+ , offset = Just 0+ , order = 1 + (fromInteger . toInteger . L.length $ encoded)+ }++instance ABIData (NP f as) => GenericABIPut (SOP f '[as]) where+ gAbiPut (SOP (Z a)) = combineEncodedValues $ _serialize [] a+ gAbiPut _ = error "Impossible branch"++instance GenericABIGet (NP f '[]) where+ gAbiGet = return Nil++instance (ABIGet a, GenericABIGet (NP I as)) => GenericABIGet (NP I (a : as)) where+ gAbiGet = (:*) <$> (I <$> factorParser) <*> gAbiGet++instance GenericABIGet (NP f as) => GenericABIGet (SOP f '[as]) where+ gAbiGet = SOP . Z <$> gAbiGet++factorParser :: forall a . ABIGet a => Get a+factorParser+ | not $ isDynamic (Proxy :: Proxy a) = abiGet+ | otherwise = do+ dataOffset <- fromIntegral <$> getWord256+ currentOffset <- bytesRead+ Left x <- lookAheadE $ do+ skip (dataOffset - currentOffset)+ Left <$> abiGet+ return x
+ src/Network/Ethereum/ABI/Json.hs view
@@ -0,0 +1,251 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module : Network.Ethereum.ABI.Json+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- JSON encoded contract ABI parsers.+--++module Network.Ethereum.ABI.Json (+ ContractABI(..)+ , Declaration(..)+ , FunctionArg(..)+ , EventArg(..)+ , signature+ , methodId+ , eventId+ , SolidityType(..)+ , parseSolidityType+ ) 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.Monoid ((<>))+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)++-- | Method argument+data FunctionArg = FunctionArg+ { funArgName :: Text+ -- ^ Argument name+ , funArgType :: Text+ -- ^ Argument type+ } deriving (Show, Eq, Ord)++$(deriveJSON+ (defaultOptions {fieldLabelModifier = toLowerFirst . 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 = toLowerFirst . drop 6})+ ''EventArg)++-- | Elementrary 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, Eq, Ord)++$(deriveJSON (defaultOptions {+ sumEncoding = TaggedObject "type" "contents"+ , constructorTagModifier = toLowerFirst . drop 1+ , fieldLabelModifier = toLowerFirst . drop 3 })+ ''Declaration)++-- | 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 :: [FunctionArg] -> Text+ args = T.dropEnd 1 . foldMap (<> ",") . fmap funArgType++signature (DFallback _) = "()"++signature (DFunction name _ inputs _) = name <> "(" <> args inputs <> ")"+ where+ args :: [FunctionArg] -> Text+ args = T.dropEnd 1 . foldMap (<> ",") . fmap funArgType++signature (DEvent name inputs _) = name <> "(" <> args inputs <> ")"+ where+ args :: [EventArg] -> Text+ args = T.dropEnd 1 . foldMap (<> ",") . fmap eveArgType++-- | Localy compute Keccak-256 hash of given text+sha3 :: Text -> Text+{-# INLINE sha3 #-}+sha3 x = T.pack (show digest)+ where digest :: Digest Keccak_256+ digest = hash (encodeUtf8 x)++-- | Generate method selector by given method 'Delcaration'+methodId :: Declaration -> Text+{-# INLINE methodId #-}+methodId = ("0x" <>) . T.take 8 . sha3 . signature++-- | Generate event `topic0` hash by givent event 'Delcaration'+eventId :: Declaration -> Text+{-# INLINE eventId #-}+eventId = ("0x" <>) . sha3 . signature++-- | Solidity types and parsers+data SolidityType =+ SolidityBool+ | SolidityAddress+ | SolidityUint Int+ | SolidityInt Int+ | SolidityString+ | SolidityBytesN Int+ | SolidityBytes+ | SolidityVector [Int] SolidityType+ | SolidityArray SolidityType+ deriving (Eq, Show)++numberParser :: Parser Int+numberParser = read <$> many1 digit++parseUint :: Parser SolidityType+parseUint = do+ _ <- string "uint"+ n <- numberParser+ pure $ SolidityUint n++parseInt :: Parser SolidityType+parseInt = do+ _ <- string "int"+ n <- numberParser+ pure $ SolidityInt n++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+ ]++parseSolidityType :: Text -> Either ParseError SolidityType+parseSolidityType = parse solidityTypeParser "Solidity"
+ src/Network/Ethereum/ABI/Prim/Address.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.Address+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI address type.+--++module Network.Ethereum.ABI.Prim.Address (+ Address+ ) where++import Control.Monad ((<=<))+import Data.Aeson (FromJSON (..), ToJSON (..),+ Value (String))+import Data.ByteArray (Bytes, zero)+import Data.ByteArray.Encoding (Base (Base16), convertFromBase,+ convertToBase)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as C8 (drop, pack, take, unpack)+import Data.Monoid ((<>))+import Data.String (IsString (..))+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Generics.SOP (Generic)+import qualified GHC.Generics as GHC (Generic)++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))+import Network.Ethereum.ABI.Codec (decode, encode)+import Network.Ethereum.ABI.Prim.Int (UIntN)++-- | Ethereum account address+newtype Address = Address { unAddress :: UIntN 160 }+ deriving (Eq, Ord, GHC.Generic)++instance Generic Address++-- TODO: Address . drop 12 . sha3+{-+fromPublic :: ByteArrayAccess bin => bin -> Maybe Address+fromPublic = undefined+-}++fromHexString :: ByteString -> Either String Address+fromHexString = decode . align <=< convertFromBase Base16 . trim0x+ where trim0x s | C8.take 2 s == "0x" = C8.drop 2 s+ | otherwise = s+ align = (zero 12 <>) :: Bytes -> Bytes++toHexString :: Address -> ByteString+toHexString = ("0x" <>) . convertToBase Base16 . C8.drop 12 . encode++instance Show Address where+ show = C8.unpack . toHexString++instance IsString Address where+ fromString = either error id . fromHexString . C8.pack++instance ABIType Address where+ isDynamic _ = False++instance ABIGet Address where+ abiGet = Address <$> abiGet++instance ABIPut Address where+ abiPut = abiPut . unAddress++instance FromJSON Address where+ parseJSON (String a) = either fail return $ fromHexString (encodeUtf8 a)+ parseJSON _ = fail "Address should be a string"++instance ToJSON Address where+ toJSON = String . decodeUtf8 . toHexString
+ src/Network/Ethereum/ABI/Prim/Bool.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.Bool+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI boolean type.+--++module Network.Ethereum.ABI.Prim.Bool () where++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))+import Network.Ethereum.ABI.Prim.Int (getWord256, putWord256)++instance ABIType Bool where+ isDynamic _ = False++instance ABIGet Bool where+ abiGet = toEnum . fromIntegral <$> getWord256++instance ABIPut Bool where+ abiPut = putWord256 . fromIntegral . fromEnum
+ src/Network/Ethereum/ABI/Prim/Bytes.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.Bytes+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI bytes and bytesN types.+--++module Network.Ethereum.ABI.Prim.Bytes (+ Bytes+ , BytesN+ ) where++import Data.Aeson (FromJSON (..), ToJSON (..),+ Value (String))+import Data.ByteArray (Bytes, convert, length, zero)+import Data.ByteArray.Encoding (Base (Base16), convertFromBase,+ convertToBase)+import Data.ByteArray.Sized (SizedByteArray,+ unsafeFromByteArrayAccess)+import qualified Data.ByteArray.Sized as S (take)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as C8+import Data.Monoid ((<>))+import Data.Proxy (Proxy (..))+import Data.Serialize (Get, Putter, getBytes,+ putByteString)+import Data.String (IsString (..))+import qualified Data.Text as T (append, drop, take)+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import GHC.TypeLits+import Prelude hiding (length)++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))+import Network.Ethereum.ABI.Prim.Int (getWord256, putWord256)++instance ABIType ByteString where+ isDynamic _ = True++instance ABIGet ByteString where+ abiGet = abiGetByteString++instance ABIPut ByteString where+ abiPut = abiPutByteString++instance ABIType Bytes where+ isDynamic _ = True++instance ABIGet Bytes where+ abiGet = convert <$> abiGetByteString++instance ABIPut Bytes where+ abiPut = abiPutByteString . convert++instance IsString Bytes where+ fromString ('0' : 'x' : hex) = either error id $ convertFromBase Base16 (C8.pack hex)+ fromString str = convert (C8.pack str)++instance FromJSON Bytes where+ parseJSON (String hex)+ | T.take 2 hex == "0x" =+ either fail pure $ convertFromBase Base16 $ encodeUtf8 $ T.drop 2 hex+ | otherwise = fail "Hex string should have '0x' prefix"+ parseJSON _ = fail "Bytes should be encoded as hex string"++instance ToJSON Bytes where+ toJSON = toJSON . T.append "0x" . decodeUtf8 . convertToBase Base16++type BytesN n = SizedByteArray n Bytes++instance (n <= 32) => ABIType (BytesN n) where+ isDynamic _ = False++instance (KnownNat n, n <= 32) => ABIGet (BytesN n) where+ abiGet = do+ ba <- unsafeFromByteArrayAccess <$> getBytes 32+ return $ S.take (ba :: BytesN 32)++instance (KnownNat n, n <= 32) => ABIPut (BytesN n) where+ abiPut ba = putByteString $ convert ba <> zero (32 - len)+ where len = fromIntegral $ natVal (Proxy :: Proxy n)++instance (KnownNat n, n <= 32) => IsString (BytesN n) where+ fromString = unsafeFromByteArrayAccess . (fromString :: String -> Bytes)++abiGetByteString :: Get ByteString+abiGetByteString = do+ len <- fromIntegral <$> getWord256+ ba <- getBytes len+ _ <- getBytes $ 32 - len `mod` 32+ return ba++abiPutByteString :: Putter ByteString+abiPutByteString bs = do+ putWord256 $ fromIntegral len+ putByteString $ bs <> zero (32 - len `mod` 32)+ where len = length bs
+ src/Network/Ethereum/ABI/Prim/Int.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}++-- |+-- Module : Network.Ethereum.Encoding.Prim.Int+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI intN and uintN types.+--++module Network.Ethereum.ABI.Prim.Int (+ IntN+ , UIntN+ , getWord256+ , putWord256+ ) where++import qualified Basement.Numerical.Number as Basement (toInteger)+import Basement.Types.Word256 (Word256 (Word256))+import qualified Basement.Types.Word256 as Basement (quot, rem)+import Data.Bits (Bits (testBit))+import Data.Proxy (Proxy (..))+import Data.Serialize (Get, Putter, Serialize (get, put))+import GHC.Generics (Generic)+import GHC.TypeLits++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))++instance Real Word256 where+ toRational = toRational . toInteger++instance Integral Word256 where+ toInteger = Basement.toInteger+ quotRem a b = (Basement.quot a b, Basement.rem a b)++newtype UIntN (n :: Nat) = UIntN { unUIntN :: Word256 }+ deriving (Eq, Ord, Enum, Num, Bits, Generic)++instance (KnownNat n, n <= 256) => Show (UIntN n) where+ show = show . unUIntN++instance (KnownNat n, n <= 256) => Bounded (UIntN n) where+ minBound = 0+ maxBound = 2 ^ (natVal (Proxy :: Proxy n)) - 1++instance (KnownNat n, n <= 256) => Real (UIntN n) where+ toRational = toRational . toInteger++instance (KnownNat n, n <= 256) => Integral (UIntN n) where+ toInteger = toInteger . unUIntN+ quotRem (UIntN a) (UIntN b) = (UIntN $ quot a b, UIntN $ rem a b)++instance (n <= 256) => ABIType (UIntN n) where+ isDynamic _ = False++instance (n <= 256) => ABIGet (UIntN n) where+ abiGet = UIntN <$> getWord256++instance (n <= 256) => ABIPut (UIntN n) where+ abiPut = putWord256 . unUIntN++-- TODO: Signed data type+newtype IntN (n :: Nat) = IntN { unIntN :: Word256 }+ deriving (Eq, Ord, Enum, Bits, Generic)++instance (KnownNat n, n <= 256) => Show (IntN n) where+ show = show . toInteger++instance (KnownNat n, n <= 256) => Bounded (IntN n) where+ minBound = negate $ 2 ^ (natVal (Proxy :: Proxy (n :: Nat)) - 1)+ maxBound = 2 ^ (natVal (Proxy :: Proxy (n :: Nat)) - 1) - 1++instance (KnownNat n, n <= 256) => Num (IntN n) where+ a + b = fromInteger (toInteger a + toInteger b)+ a - b = fromInteger (toInteger a - toInteger b)+ a * b = fromInteger (toInteger a * toInteger b)+ abs = fromInteger . abs . toInteger+ negate = fromInteger . negate . toInteger+ signum = fromInteger . signum . toInteger+ fromInteger x+ | x >= 0 = IntN (fromInteger x)+ | otherwise = IntN (fromInteger $ 2 ^ 256 + x)++instance (KnownNat n, n <= 256) => Real (IntN n) where+ toRational = toRational . toInteger++instance (KnownNat n, n <= 256) => Integral (IntN n) where+ quotRem (IntN a) (IntN b) = (IntN $ quot a b, IntN $ rem a b)+ toInteger x+ | testBit x 255 = toInteger (unIntN x) - 2 ^ 256+ | otherwise = toInteger $ unIntN x++instance (n <= 256) => ABIType (IntN n) where+ isDynamic _ = False++instance (n <= 256) => ABIGet (IntN n) where+ abiGet = IntN <$> getWord256++instance (n <= 256) => ABIPut (IntN n) where+ abiPut = putWord256 . unIntN++putWord256 :: Putter Word256+putWord256 (Word256 a3 a2 a1 a0) =+ put a3 >> put a2 >> put a1 >> put a0++getWord256 :: Get Word256+getWord256 = Word256 <$> get <*> get <*> get <*> get
+ src/Network/Ethereum/ABI/Prim/List.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.List+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI dynamic and static size vectors based on linked lists.+--++module Network.Ethereum.ABI.Prim.List (+ ListN+ ) where++import Basement.Nat (NatWithinBound)+import Basement.Sized.List (ListN, toListN_, unListN)+import qualified Basement.Sized.List as SL (mapM_, replicateM)+import Control.Monad (replicateM)+import GHC.Exts (IsList (..))+import GHC.TypeLits (KnownNat)++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))+import Network.Ethereum.ABI.Prim.Int (getWord256, putWord256)++instance ABIType [a] where+ isDynamic _ = True++instance ABIPut a => ABIPut [a] where+ abiPut l = do putWord256 $ fromIntegral (length l)+ foldMap abiPut l++instance ABIGet a => ABIGet [a] where+ abiGet = do len <- fromIntegral <$> getWord256+ replicateM len abiGet++instance ABIType (ListN n a) where+ isDynamic _ = False++instance ABIPut a => ABIPut (ListN n a) where+ abiPut = SL.mapM_ abiPut++instance (NatWithinBound Int n, KnownNat n, ABIGet a) => ABIGet (ListN n a) where+ abiGet = SL.replicateM abiGet++instance (NatWithinBound Int n, KnownNat n) => IsList (ListN n a) where+ type Item (ListN n a) = a+ fromList = toListN_+ toList = unListN
+ src/Network/Ethereum/ABI/Prim/String.hs view
@@ -0,0 +1,29 @@+-- |+-- Module : Network.Ethereum.ABI.Prim.String+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI UTF8-encoded string type.+--++module Network.Ethereum.ABI.Prim.String () where++import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8, encodeUtf8)++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))+import Network.Ethereum.ABI.Prim.Bytes ()++instance ABIType Text where+ isDynamic _ = True++instance ABIPut Text where+ abiPut = abiPut . encodeUtf8++instance ABIGet Text where+ abiGet = decodeUtf8 <$> abiGet
+ src/Network/Ethereum/ABI/Prim/Tagged.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.Tagged+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Ethereum ABI UTF8-encoded tagged types.+--++module Network.Ethereum.ABI.Prim.Tagged (+ Tagged+ ) where++import Data.Proxy (Proxy (..))+import Data.Tagged (Tagged (..))+import Generics.SOP (Generic)++import Network.Ethereum.ABI.Class (ABIGet (..), ABIPut (..),+ ABIType (..))++instance ABIType a => ABIType (Tagged t a) where+ isDynamic _ = isDynamic (Proxy :: Proxy a)++instance ABIPut a => ABIPut (Tagged t a) where+ abiPut (Tagged a) = abiPut a++instance ABIGet a => ABIGet (Tagged t a) where+ abiGet = Tagged <$> abiGet++instance Generic a => Generic (Tagged t a)
+ src/Network/Ethereum/ABI/Prim/Tuple.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.Tuple+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- Tuple type abi encoding instances.+--++module Network.Ethereum.ABI.Prim.Tuple (+ Singleton(..)+ ) where++import Data.Proxy (Proxy (..))+import Generics.SOP (Generic)+import qualified GHC.Generics as GHC (Generic)++import Network.Ethereum.ABI.Class (ABIGet, ABIPut,+ ABIType (..))+import Network.Ethereum.ABI.Generic ()+import Network.Ethereum.ABI.Prim.Tuple.TH (tupleDecs)++-- | The type for one-tuples+newtype Singleton a = Singleton { unSingleton :: a }+ deriving GHC.Generic++deriving instance Eq a => Eq (Singleton a)+deriving instance Show a => Show (Singleton a)+instance Generic (Singleton a)++instance ABIType a => ABIType (Singleton a) where+ isDynamic _ = isDynamic (Proxy :: Proxy a)++instance ABIGet a => ABIGet (Singleton a)+instance ABIPut a => ABIPut (Singleton a)++$(fmap concat $ sequence $ map tupleDecs [2..20])
+ src/Network/Ethereum/ABI/Prim/Tuple/TH.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module : Network.Ethereum.ABI.Prim.Tuple.TH+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : noportable+--+-- This module is for internal usage only.+-- It contains tuple abi encoding template haskell generator.+--++module Network.Ethereum.ABI.Prim.Tuple.TH (tupleDecs) where+++import Control.Monad (replicateM)+import Language.Haskell.TH (DecsQ, Type (VarT), appT, clause,+ conT, cxt, funD, instanceD,+ newName, normalB, tupleT)++import Network.Ethereum.ABI.Class (ABIGet, ABIPut, ABIType (..))++tupleDecs :: Int -> DecsQ+tupleDecs n = do+ vars <- replicateM n $ newName "a"+ let types = fmap (pure . VarT) vars+ sequence $+ [ instanceD (cxt $ map (appT $ conT ''ABIType) types) (appT (conT ''ABIType) (foldl appT (tupleT n) types))+ [funD 'isDynamic [clause [] (normalB [|const False|]) []]]+ , instanceD (cxt $ map (appT $ conT ''ABIGet) types) (appT (conT ''ABIGet) (foldl appT (tupleT n) types)) []+ , instanceD (cxt $ map (appT $ conT ''ABIPut) types) (appT (conT ''ABIPut) (foldl appT (tupleT n) types)) [] ]
+ src/Network/Ethereum/Contract/Event.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Network.Ethereum.Contract.Event+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- Ethereum contract event support.+--++module Network.Ethereum.Contract.Event (+ EventAction(..)+ , event+ , event'+ , eventMany'+ ) where++import Control.Concurrent (threadDelay)+import Control.Monad (forM, void, when)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ReaderT (..))+import Data.Either (rights)+import Data.Machine (MachineT, asParts, autoM,+ await, construct, final,+ mapping, repeatedly, runT,+ unfoldPlan, (~>))+import Data.Machine.Plan (PlanT, stop, yield)+import Data.Maybe (listToMaybe)++import Control.Concurrent.Async (Async)+import Network.Ethereum.ABI.Event (DecodeEvent (..))+import qualified Network.Ethereum.Web3.Eth as Eth+import Network.Ethereum.Web3.Provider (Web3, forkWeb3)+import Network.Ethereum.Web3.Types (BlockNumber (..), Change (..),+ DefaultBlock (..), Filter (..),+ FilterId)++-- | Event callback control response+data EventAction = ContinueEvent+ -- ^ Continue to listen events+ | TerminateEvent+ -- ^ Terminate event listener+ deriving (Show, Eq)++-- | Run 'event\'' one block at a time.+event :: DecodeEvent i ni e+ => Filter e+ -> (e -> ReaderT Change Web3 EventAction)+ -> Web3 (Async ())+event fltr = forkWeb3 . event' fltr++-- | Same as 'event', but does not immediately spawn a new thread.+event' :: DecodeEvent i ni e+ => Filter e+ -> (e -> ReaderT Change Web3 EventAction)+ -> Web3 ()+event' fltr = eventMany' fltr 0++-- | 'eventMany\'' take s a filter, a window size, and a handler.+--+-- It runs the handler over the results of 'eventLogs' results using+-- 'reduceEventStream'. If no 'TerminateEvent' action is thrown and+-- the toBlock is not yet reached, it then transitions to polling.+--+eventMany' :: DecodeEvent i ni e+ => Filter e+ -> Integer+ -> (e -> ReaderT Change Web3 EventAction)+ -> Web3 ()+eventMany' fltr window handler = do+ start <- mkBlockNumber $ filterFromBlock fltr+ let initState = FilterStreamState { fssCurrentBlock = start+ , fssInitialFilter = fltr+ , fssWindowSize = window+ }+ mLastProcessedFilterState <- reduceEventStream (playLogs initState) handler+ case mLastProcessedFilterState of+ Nothing -> startPolling fltr {filterFromBlock = BlockWithNumber start}+ Just (act, lastBlock) -> do+ end <- mkBlockNumber . filterToBlock $ fltr+ when (act /= TerminateEvent && lastBlock < end) $+ let pollingFromBlock = lastBlock + 1+ in startPolling fltr {filterFromBlock = BlockWithNumber pollingFromBlock}+ where+ startPolling fltr' = do+ filterId <- Eth.newFilter fltr'+ let pollTo = filterToBlock fltr'+ void $ reduceEventStream (pollFilter filterId pollTo) handler++-- | Effectively a mapM_ over the machine using the given handler.+reduceEventStream :: Monad m+ => MachineT m k [FilterChange a]+ -> (a -> ReaderT Change m EventAction)+ -> m (Maybe (EventAction, BlockNumber))+reduceEventStream filterChanges handler = fmap listToMaybe . runT $+ filterChanges+ ~> autoM (processChanges handler)+ ~> asParts+ ~> runWhile (\(act, _) -> act /= TerminateEvent)+ ~> final+ where+ runWhile p = repeatedly $ do+ v <- await+ if p v+ then yield v+ else yield v >> stop+ processChanges :: Monad m+ => (a -> ReaderT Change m EventAction)+ -> [FilterChange a]+ -> m [(EventAction, BlockNumber)]+ processChanges handler' changes =+ forM changes $ \FilterChange{..} -> do+ act <- flip runReaderT filterChangeRawChange $+ handler' filterChangeEvent+ return (act, changeBlockNumber filterChangeRawChange)++data FilterChange a = FilterChange { filterChangeRawChange :: Change+ , filterChangeEvent :: a+ }++-- | 'playLogs' streams the 'filterStream' and calls eth_getLogs on these 'Filter' objects.+playLogs :: DecodeEvent i ni e+ => FilterStreamState e+ -> MachineT Web3 k [FilterChange e]+playLogs s = filterStream s+ ~> autoM Eth.getLogs+ ~> mapping mkFilterChanges++-- | Polls a filter from the given filterId until the target toBlock is reached.+pollFilter :: forall i ni e k . DecodeEvent i ni e+ => FilterId+ -> DefaultBlock+ -> MachineT Web3 k [FilterChange e]+pollFilter i = construct . pollPlan i+ where+ pollPlan :: FilterId -> DefaultBlock -> PlanT k [FilterChange e] Web3 ()+ pollPlan fid end = do+ bn <- lift $ Eth.blockNumber+ if BlockWithNumber bn > end+ then do+ _ <- lift $ Eth.uninstallFilter fid+ stop+ else do+ liftIO $ threadDelay 1000000+ changes <- lift $ Eth.getFilterChanges fid+ yield $ mkFilterChanges changes+ pollPlan fid end++mkFilterChanges :: DecodeEvent i ni e+ => [Change]+ -> [FilterChange e]+mkFilterChanges = rights+ . fmap (\c@Change{..} -> FilterChange c <$> decodeEvent c)++data FilterStreamState e =+ FilterStreamState { fssCurrentBlock :: BlockNumber+ , fssInitialFilter :: Filter e+ , fssWindowSize :: Integer+ }+++-- | 'filterStream' is a machine which represents taking an initial filter+-- over a range of blocks b1, ... bn (where bn is possibly `Latest` or `Pending`,+-- but b1 is an actual `BlockNumber`), and making a stream of filter objects+-- which cover this filter in intervals of size `windowSize`. The machine+-- halts whenever the `fromBlock` of a spanning filter either (1) excedes then+-- initial filter's `toBlock` or (2) is greater than the chain head's `BlockNumber`.+filterStream :: FilterStreamState e+ -> MachineT Web3 k (Filter e)+filterStream initialPlan = unfoldPlan initialPlan filterPlan+ where+ filterPlan :: FilterStreamState e -> PlanT k (Filter e) Web3 (FilterStreamState e)+ filterPlan initialState@FilterStreamState{..} = do+ end <- lift . mkBlockNumber $ filterToBlock fssInitialFilter+ if fssCurrentBlock > end+ then stop+ else do+ let to' = newTo end fssCurrentBlock fssWindowSize+ filter' = fssInitialFilter { filterFromBlock = BlockWithNumber fssCurrentBlock+ , filterToBlock = BlockWithNumber to'+ }+ yield filter'+ filterPlan $ initialState { fssCurrentBlock = succBn to' }+ succBn :: BlockNumber -> BlockNumber+ succBn (BlockNumber bn) = BlockNumber $ bn + 1+ newTo :: BlockNumber -> BlockNumber -> Integer -> BlockNumber+ newTo upper (BlockNumber current) window = min upper . BlockNumber $ current + window++-- | Coerce a 'DefaultBlock' into a numerical block number.+mkBlockNumber :: DefaultBlock -> Web3 BlockNumber+mkBlockNumber bm = case bm of+ BlockWithNumber bn -> return bn+ Earliest -> return 0+ _ -> Eth.blockNumber
+ src/Network/Ethereum/Contract/Method.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE ScopedTypeVariables #-}++-- |+-- Module : Network.Ethereum.Contract.Method+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- Ethereum contract method support.+--++module Network.Ethereum.Contract.Method (+ Method(..)+ , call+ , sendTx+ ) where++import Control.Monad.Catch (throwM)+import Data.Monoid ((<>))+import Data.Proxy (Proxy (..))++import Network.Ethereum.ABI.Class (ABIGet, ABIPut, ABIType (..))+import Network.Ethereum.ABI.Codec (decode, encode)+import Network.Ethereum.ABI.Prim.Bytes (Bytes)+import qualified Network.Ethereum.Web3.Eth as Eth+import Network.Ethereum.Web3.Provider (Web3, Web3Error (ParserFail))+import Network.Ethereum.Web3.Types (Call (callData), DefaultBlock,+ TxHash)++class ABIPut a => Method a where+ selector :: Proxy a -> Bytes++instance ABIType () where+ isDynamic _ = False++instance ABIPut ()++-- | Send transaction without method selection+instance Method () where+ selector = mempty++-- | 'sendTx' is used to submit a state changing transaction.+sendTx :: Method a+ => Call+ -- ^ Call configuration+ -> a+ -- ^ method data+ -> Web3 TxHash+sendTx call' (dat :: a) =+ let sel = selector (Proxy :: Proxy a)+ in Eth.sendTransaction (call' { callData = Just $ sel <> encode dat })++-- | 'call' is used to call contract methods that have no state changing effects.+call :: (Method a, ABIGet b)+ => Call+ -- ^ Call configuration+ -> DefaultBlock+ -- ^ State mode for constant call (latest or pending)+ -> a+ -- ^ Method data+ -> Web3 b+ -- ^ 'Web3' wrapped result+call call' mode (dat :: a) = do+ let sel = selector (Proxy :: Proxy a)+ res <- Eth.call (call' { callData = Just $ sel <> encode dat }) mode+ case decode res of+ Left e -> throwM $ ParserFail $ "Unable to parse response: " ++ e+ Right x -> return x
+ src/Network/Ethereum/Contract/TH.hs view
@@ -0,0 +1,277 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module : Network.Ethereum.Contract.TH+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- Contract abstraction is a high level interface of web3 library.+--+-- The Application Binary Interface is the standard way to interact+-- with contracts in the Ethereum ecosystem. It can be described by+-- specially JSON file, like @ERC20.json@. This module use TemplateHaskell+-- for generation described in ABI contract methods and events. Helper+-- functions and instances inserted in haskell module and can be used in+-- another modules or in place.+--+-- @+-- import Network.Ethereum.Contract.TH+--+-- [abiFrom|examples/ERC20.json|]+--+-- main = do+-- runWeb3 $ event' def $+-- \(Transfer _ to val) -> liftIO $ do print to+-- print val+-- @+--+-- Full code example available in examples folder.+--++module Network.Ethereum.Contract.TH (abi, abiFrom) where++import Control.Monad (replicateM, (<=<))+import Data.Aeson (eitherDecode)+import Data.Default (Default (..))+import Data.List (groupBy, sortBy, uncons)+import Data.Monoid ((<>))+import Data.Tagged (Tagged)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Encoding as LT+import Generics.SOP (Generic)+import qualified GHC.Generics as GHC (Generic)+import Language.Haskell.TH+import Language.Haskell.TH.Quote++import Data.String.Extra (toLowerFirst, toUpperFirst)+import Network.Ethereum.ABI.Class (ABIGet, ABIPut,+ ABIType (..))+import Network.Ethereum.ABI.Event (IndexedEvent (..))+import Network.Ethereum.ABI.Json (ContractABI (..),+ Declaration (..),+ EventArg (..),+ FunctionArg (..),+ SolidityType (..), eventId,+ methodId, parseSolidityType)+import Network.Ethereum.ABI.Prim.Address (Address)+import Network.Ethereum.ABI.Prim.Bytes (Bytes, BytesN)+import Network.Ethereum.ABI.Prim.Int (IntN, UIntN)+import Network.Ethereum.ABI.Prim.List (ListN)+import Network.Ethereum.ABI.Prim.String ()+import Network.Ethereum.ABI.Prim.Tagged ()+import Network.Ethereum.ABI.Prim.Tuple (Singleton (..))+import Network.Ethereum.Contract.Method (Method (..), call, sendTx)+import Network.Ethereum.Web3.Provider (Web3)+import Network.Ethereum.Web3.Types (Call, DefaultBlock (..),+ Filter (..), TxHash)++-- | Read contract ABI from file+abiFrom :: QuasiQuoter+abiFrom = quoteFile abi++-- | QQ reader for contract ABI+abi :: QuasiQuoter+abi = QuasiQuoter+ { quoteDec = quoteAbiDec+ , quoteExp = quoteAbiExp+ , quotePat = undefined+ , quoteType = undefined+ }++-- | Instance declaration with empty context+instanceD' :: Name -> TypeQ -> [DecQ] -> DecQ+instanceD' name insType =+ instanceD (cxt []) (appT insType (conT name))++-- | Simple data type declaration with one constructor+dataD' :: Name -> ConQ -> [Name] -> DecQ+dataD' name rec derive =+#if MIN_VERSION_template_haskell(2,12,0)+ dataD (cxt []) name [] Nothing [rec] [derivClause Nothing (conT <$> derive)]+#else+ dataD (cxt []) name [] Nothing [rec] $ cxt (conT <$> derive)+#endif++-- | Simple function declaration+funD' :: Name -> [PatQ] -> ExpQ -> DecQ+funD' name p f = funD name [clause p (normalB f) []]++-- | ABI and Haskell types association+toHSType :: SolidityType -> TypeQ+toHSType s = case s of+ SolidityBool -> conT ''Bool+ SolidityAddress -> conT ''Address+ SolidityUint n -> appT (conT ''UIntN) (numLit n)+ SolidityInt n -> appT (conT ''IntN) (numLit n)+ SolidityString -> conT ''Text+ SolidityBytesN n -> appT (conT ''BytesN) (numLit n)+ SolidityBytes -> conT ''Bytes+ SolidityVector ns a -> expandVector ns a+ SolidityArray a -> appT listT $ toHSType a+ where+ numLit n = litT (numTyLit $ toInteger n)+ expandVector :: [Int] -> SolidityType -> TypeQ+ expandVector ns a = case uncons ns of+ Just (n, rest) ->+ if length rest == 0+ then (conT ''ListN) `appT` numLit n `appT` toHSType a+ else (conT ''ListN) `appT` numLit n `appT` expandVector rest a+ _ -> error $ "Impossible Nothing branch in `expandVector`: " ++ show ns ++ " " ++ show a++typeQ :: Text -> TypeQ+typeQ t = case parseSolidityType t of+ Left e -> error $ "Unable to parse solidity type: " ++ show e+ Right ty -> toHSType ty++-- | Function argument to TH type+funBangType :: FunctionArg -> BangTypeQ+funBangType (FunctionArg _ typ) =+ bangType (bang sourceNoUnpack sourceStrict) (typeQ typ)++funWrapper :: Bool+ -- ^ Is constant?+ -> Name+ -- ^ Function name+ -> Name+ -- ^ Function data name+ -> [FunctionArg]+ -- ^ Parameters+ -> Maybe [FunctionArg]+ -- ^ Results+ -> Q [Dec]+funWrapper c name dname args result = do+ a : _ : vars <- replicateM (length args + 2) (newName "t")+ let params = appsE $ conE dname : fmap varE vars++ sequence $ if c+ then+ [ sigD name $ [t|$(arrowing $ [t|Call|] : inputT ++ [outputT])|]+ , funD' name (varP <$> a : vars) $+ case result of+ Just [_] -> [|unSingleton <$> call $(varE a) Latest $(params)|]+ _ -> [|call $(varE a) Latest $(params)|]+ ]++ else+ [ sigD name $ [t|$(arrowing $ [t|Call|] : inputT ++ [[t|Web3 TxHash|]])|]+ , funD' name (varP <$> a : vars) $+ [|sendTx $(varE a) $(params)|] ]+ where+ arrowing [] = error "Impossible branch call"+ arrowing [x] = x+ arrowing (x : xs) = [t|$x -> $(arrowing xs)|]+ inputT = fmap (typeQ . funArgType) args+ outputT = case result of+ Nothing -> [t|Web3 ()|]+ Just [x] -> [t|Web3 $(typeQ $ funArgType x)|]+ Just xs -> let outs = fmap (typeQ . funArgType) xs+ in [t|Web3 $(foldl appT (tupleT (length xs)) outs)|]++mkDecl :: Declaration -> Q [Dec]++mkDecl ev@(DEvent name inputs anonymous) = sequence+ [ dataD' indexedName (normalC indexedName (map (toBang <=< tag) indexedArgs)) derivingD+ , instanceD' indexedName (conT ''Generic) []+ , instanceD' indexedName (conT ''ABIType) [funD' 'isDynamic [] [|const False|]]+ , instanceD' indexedName (conT ''ABIGet) []+ , dataD' nonIndexedName (normalC nonIndexedName (map (toBang <=< tag) nonIndexedArgs)) derivingD+ , instanceD' nonIndexedName (conT ''Generic) []+ , instanceD' nonIndexedName (conT ''ABIType) [funD' 'isDynamic [] [|const False|]]+ , instanceD' nonIndexedName (conT ''ABIGet) []+ , dataD' allName (recC allName (map (\(n, a) -> ((\(b,t) -> return (n,b,t)) <=< toBang <=< typeQ $ a)) allArgs)) derivingD+ , instanceD' allName (conT ''Generic) []+ , instanceD (cxt [])+ (pure $ ConT ''IndexedEvent `AppT` ConT indexedName `AppT` ConT nonIndexedName `AppT` ConT allName)+ [funD' 'isAnonymous [] [|const anonymous|]]+ , instanceD (cxt [])+ (pure $ ConT ''Default `AppT` (ConT ''Filter `AppT` ConT allName))+ [funD' 'def [] [|Filter Nothing (Just topics) Latest Latest|] ]+ ]+ where+ topics = [Just (T.unpack $ eventId ev)] <> replicate (length indexedArgs) Nothing+ toBang ty = bangType (bang sourceNoUnpack sourceStrict) (return ty)+ tag (n, ty) = AppT (AppT (ConT ''Tagged) (LitT $ NumTyLit n)) <$> typeQ ty+ labeledArgs = zip [1..] inputs+ indexedArgs = map (\(n, ea) -> (n, eveArgType ea)) . filter (eveArgIndexed . snd) $ labeledArgs+ indexedName = mkName $ toUpperFirst (T.unpack name) <> "Indexed"+ nonIndexedArgs = map (\(n, ea) -> (n, eveArgType ea)) . filter (not . eveArgIndexed . snd) $ labeledArgs+ nonIndexedName = mkName $ toUpperFirst (T.unpack name) <> "NonIndexed"+ allArgs = makeArgs name $ map (\i -> (eveArgName i, eveArgType i)) inputs+ allName = mkName $ toUpperFirst (T.unpack name)+ derivingD = [''Show, ''Eq, ''Ord, ''GHC.Generic]++-- | Method delcarations maker+mkDecl fun@(DFunction name constant inputs outputs) = (++)+ <$> funWrapper constant fnName dataName inputs outputs+ <*> sequence+ [ dataD' dataName (normalC dataName bangInput) derivingD+ , instanceD' dataName (conT ''Generic) []+ , instanceD' dataName (conT ''ABIType)+ [funD' 'isDynamic [] [|const False|]]+ , instanceD' dataName (conT ''ABIPut) []+ , instanceD' dataName (conT ''ABIGet) []+ , instanceD' dataName (conT ''Method)+ [funD' 'selector [] [|const mIdent|]]+ ]+ where mIdent = T.unpack (methodId $ fun {funName = T.replace "'" "" name})+ dataName = mkName (toUpperFirst (T.unpack $ name <> "Data"))+ fnName = mkName (toLowerFirst (T.unpack name))+ bangInput = fmap funBangType inputs+ derivingD = [''Show, ''Eq, ''Ord, ''GHC.Generic]++mkDecl _ = return []++-- | this function gives appropriate names for the accessors in the following way+-- | argName -> evArgName+-- | arg_name -> evArg_name+-- | _argName -> evArgName+-- | "" -> evi , for example Transfer(address, address uint256) ~> Transfer {transfer1 :: address, transfer2 :: address, transfer3 :: Integer}+makeArgs :: Text -> [(Text, Text)] -> [(Name, Text)]+makeArgs prefix ns = go 1 ns+ where+ prefixStr = toLowerFirst . T.unpack $ prefix+ go :: Int -> [(Text, Text)] -> [(Name, Text)]+ go _ [] = []+ go i ((h, ty) : tail') = if T.null h+ then (mkName $ prefixStr ++ show i, ty) : go (i + 1) tail'+ else (mkName . (++ "_") . (++) prefixStr . toUpperFirst . T.unpack $ h, ty) : go (i + 1) tail'++escape :: [Declaration] -> [Declaration]+escape = concat . escapeNames . groupBy fnEq . sortBy fnCompare+ where fnEq (DFunction n1 _ _ _) (DFunction n2 _ _ _) = n1 == n2+ fnEq _ _ = False+ fnCompare (DFunction n1 _ _ _) (DFunction n2 _ _ _) = compare n1 n2+ fnCompare _ _ = GT++escapeNames :: [[Declaration]] -> [[Declaration]]+escapeNames = fmap go+ where go [] = []+ go (x : xs) = x : zipWith appendToName xs hats+ hats = [T.replicate n "'" | n <- [1..]]+ appendToName dfn addition = dfn { funName = funName dfn <> addition }++-- | ABI to declarations converter+quoteAbiDec :: String -> Q [Dec]+quoteAbiDec abi_string =+ case eitherDecode abi_lbs of+ Left e -> fail $ "Error: " ++ show e+ Right (ContractABI a) -> concat <$> mapM mkDecl (escape a)+ where abi_lbs = LT.encodeUtf8 (LT.pack abi_string)++-- | ABI information string+quoteAbiExp :: String -> ExpQ+quoteAbiExp abi_string = stringE $+ case eitherDecode abi_lbs of+ Left e -> "Error: " ++ show e+ Right a -> show (a :: ContractABI)+ where abi_lbs = LT.encodeUtf8 (LT.pack abi_string)
src/Network/Ethereum/Unit.hs view
@@ -1,9 +1,12 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-}+ -- | -- Module : Network.Ethereum.Unit--- Copyright : Alexander Krupenkin 2016+-- Copyright : Alexander Krupenkin 2016-2018 -- License : BSD3 -- -- Maintainer : mail@akru.me@@ -43,8 +46,10 @@ -- Actual type: Szabo -- @ --+ module Network.Ethereum.Unit ( Unit(..)+ , UnitSpec(..) , Wei , Babbage , Lovelace@@ -55,7 +60,7 @@ , KEther ) where -import Data.Monoid ((<>))+import Data.Proxy (Proxy (..)) import Data.Text.Lazy (Text, unpack) import GHC.Generics (Generic) import GHC.Read@@ -76,26 +81,23 @@ -- | Unit specification class UnitSpec a where- divider :: RealFrac b => Value a -> b- name :: Value a -> Text+ divider :: RealFrac b => proxy a -> b+ name :: proxy a -> Text -- | Value abstraction-data Value a = MkValue { unValue :: Integer }+newtype Value a = MkValue { unValue :: Integer } deriving (Eq, Ord, Generic) -mkValue :: (UnitSpec a, RealFrac b) => b -> Value a-mkValue = modify res . round . (divider res *)- where res = undefined :: UnitSpec a => Value a- modify :: Value a -> Integer -> Value a- modify _ = MkValue+mkValue :: forall a b . (UnitSpec a, RealFrac b) => b -> Value a+mkValue = MkValue . round . (* divider (Proxy :: Proxy a)) instance UnitSpec a => Unit (Value a) where fromWei = MkValue toWei = unValue instance UnitSpec a => UnitSpec (Value a) where- divider = divider . (undefined :: Value (Value a) -> Value a)- name = name . (undefined :: Value (Value a) -> Value a)+ divider = const $ divider (Proxy :: Proxy a)+ name = const $ name (Proxy :: Proxy a) instance UnitSpec a => Num (Value a) where a + b = MkValue (unValue a + unValue b)
src/Network/Ethereum/Web3.hs view
@@ -1,11 +1,11 @@ -- | -- Module : Network.Ethereum.Web3--- Copyright : Alexander Krupenkin 2016+-- Copyright : Alexander Krupenkin 2016-2018 -- License : BSD3 -- -- Maintainer : mail@akru.me -- Stability : experimental--- Portability : unknown+-- Portability : unportable -- -- An Ethereum node offers a RPC interface. This interface gives Ðapp’s -- access to the Ethereum blockchain and functionality that the node provides,@@ -16,34 +16,44 @@ -- -- Web3 Haskell library currently use JSON-RPC over HTTP to access node functionality. --+ module Network.Ethereum.Web3 (- -- ** Web3 monad and service provider++ -- ** Monad as base of any Ethereum node communication Web3- , Provider(..)- , DefaultProvider- , Web3Error(..)- , forkWeb3- , runWeb3' , runWeb3- -- ** Contract actions++ -- ** Basic transaction sending+ , sendTx+ , Call(..)++ -- ** Basic event listening , EventAction(..)- , Event(..)- , Method(..)- , NoMethod(..)- , nopay- -- ** Ethereum data types- , BytesN(..)- , BytesD(..)+ , event+ , event'++ -- ** Primitive data types , Address- -- ** Ethereum unit conversion utils+ , Bytes+ , BytesN+ , IntN+ , UIntN+ , ListN++ -- ** Metric unit system , module Network.Ethereum.Unit+ ) where +import Network.Ethereum.ABI.Prim.Address (Address)+import Network.Ethereum.ABI.Prim.Bool ()+import Network.Ethereum.ABI.Prim.Bytes (Bytes, BytesN)+import Network.Ethereum.ABI.Prim.Int (IntN, UIntN)+import Network.Ethereum.ABI.Prim.List (ListN)+import Network.Ethereum.ABI.Prim.String ()+import Network.Ethereum.Contract.Event (EventAction (..), event,+ event')+import Network.Ethereum.Contract.Method (sendTx) import Network.Ethereum.Unit-import Network.Ethereum.Web3.Address-import Network.Ethereum.Web3.Contract-import Network.Ethereum.Web3.Encoding-import Network.Ethereum.Web3.Encoding.Bytes-import Network.Ethereum.Web3.Encoding.Tuple-import Network.Ethereum.Web3.Provider-import Network.Ethereum.Web3.Types+import Network.Ethereum.Web3.Provider (Web3, runWeb3)+import Network.Ethereum.Web3.Types (Call (..))
− src/Network/Ethereum/Web3/Address.hs
@@ -1,72 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}--- |--- Module : Network.Ethereum.Web3.Address--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : unknown------ Ethereum address type, render and parser.----module Network.Ethereum.Web3.Address (- Address- , fromText- , toText- , zero- ) where--import Control.Monad ((<=<))-import Data.Aeson (FromJSON (..), ToJSON (..),- Value (..))-import qualified Data.Char as C-import Data.Monoid ((<>))-import Data.String (IsString (..))-import Data.Text (Text, pack, unpack)-import qualified Data.Text as T-import Data.Text.Lazy (toStrict)-import Data.Text.Lazy.Builder (toLazyText)-import Data.Text.Lazy.Builder.Int as B (hexadecimal)-import Data.Text.Read as R (hexadecimal)--import GHC.Generics (Generic)---- | Ethereum account address-newtype Address = Address { unAddress :: Integer }- deriving (Eq, Ord, Generic)--instance Show Address where- show = unpack . toText--instance IsString Address where- fromString a = case fromText (pack a) of- Right address -> address- Left e -> error e--instance FromJSON Address where- parseJSON (String a) = either fail return (fromText a)- parseJSON _ = fail "Address should be a string"--instance ToJSON Address where- toJSON = toJSON . ("0x" <>) . toText---- | Parse 'Address' from text string-fromText :: Text -> Either String Address-fromText = fmap (Address . fst) . R.hexadecimal <=< check- where check t | T.take 2 t == "0x" = check (T.drop 2 t)- | otherwise = do if T.length t == 40 then pure () else lengthError- if T.all C.isHexDigit t then pure () else invalidCharError- pure t- lengthError = Left "Invalid Address: text length not equal to 20"- invalidCharError = Left "Invalid Address: contains non-hex character"---- | Render 'Address' to text string-toText :: Address -> Text-toText = wFix . toStrict . toLazyText . B.hexadecimal . unAddress- where wFix x | T.length x < 40 = T.replicate (40 - T.length x) "0" <> x- | otherwise = x---- | Null address-zero :: Address-zero = Address 0
− src/Network/Ethereum/Web3/Contract.hs
@@ -1,165 +0,0 @@--- |--- Module : Network.Ethereum.Web3.Contract--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ Ethereum contract generalized interface, e.g. 'event' function--- catch all event depend by given callback function type.------ @--- runWeb3 $ do--- event "0x..." $ \(MyEvent a b c) ->--- liftIO $ print (a + b * c))--- @------ In other case 'call' function used for constant calls (without--- transaction creation and change state), and 'sendTx' function--- like a 'call' but return no contract method return but created--- transaction hash.------ @--- runweb3 $ do--- x <- call "0x.." Latest MySelector--- tx <- sendTx "0x.." nopay $ MySelector2 (x + 2)--- @----module Network.Ethereum.Web3.Contract (- EventAction(..)- , Method(..)- , Event(..)- , NoMethod(..)- , nopay- ) where--import Control.Concurrent (ThreadId, threadDelay)-import Control.Exception (throwIO)-import Control.Monad (forM, when)-import Control.Monad.IO.Class (liftIO)-import Control.Monad.Trans.Reader (ReaderT (..))-import Data.Maybe (listToMaybe, mapMaybe)-import Data.Monoid ((<>))-import qualified Data.Text as T-import Data.Text.Lazy (toStrict)-import qualified Data.Text.Lazy.Builder as B-import qualified Data.Text.Lazy.Builder.Int as B--import Network.Ethereum.Unit-import Network.Ethereum.Web3.Address-import Network.Ethereum.Web3.Encoding-import qualified Network.Ethereum.Web3.Eth as Eth-import Network.Ethereum.Web3.Provider-import Network.Ethereum.Web3.Types---- | Event callback control response-data EventAction = ContinueEvent- -- ^ Continue to listen events- | TerminateEvent- -- ^ Terminate event listener- deriving (Show, Eq)---- | Contract event listener-class ABIEncoding a => Event a where- -- | Event filter structure used by low-level subscription methods- eventFilter :: a -> Address -> Filter-- -- | Start an event listener for given contract 'Address' and callback- event :: Provider p- => Address- -- ^ Contract address- -> (a -> ReaderT Change (Web3 p) EventAction)- -- ^ 'Event' handler- -> Web3 p ThreadId- -- ^ 'Web3' wrapped event handler spawn ident- event = _event--_event :: (Provider p, Event a)- => Address- -> (a -> ReaderT Change (Web3 p) EventAction)- -> Web3 p ThreadId-_event a f = do- fid <- let ftyp = snd $ let x = undefined :: Event a => a- in (f x, x)- in Eth.newFilter (eventFilter ftyp a)-- forkWeb3 $- let loop = do liftIO (threadDelay 1000000)- changes <- Eth.getFilterChanges fid- acts <- forM (mapMaybe pairChange changes) $ \(changeEvent, changeWithMeta) ->- runReaderT (f changeEvent) changeWithMeta- when (TerminateEvent `notElem` acts) loop- in do loop- Eth.uninstallFilter fid- return ()- where- prepareTopics = fmap (T.drop 2) . drop 1- pairChange changeWithMeta = do- changeEvent <- fromData $- T.append (T.concat (prepareTopics $ changeTopics changeWithMeta))- (T.drop 2 $ changeData changeWithMeta)- return (changeEvent, changeWithMeta)---- | Contract method caller-class ABIEncoding a => Method a where- -- | Send a transaction for given contract 'Address', value and input data- sendTx :: (Provider p, Unit b)- => Address- -- ^ Contract address- -> b- -- ^ Payment value (set 'nopay' to empty value)- -> a- -- ^ Method data- -> Web3 p TxHash- -- ^ 'Web3' wrapped result- sendTx = _sendTransaction-- -- | Constant call given contract 'Address' in mode and given input data- call :: (Provider p, ABIEncoding b)- => Address- -- ^ Contract address- -> DefaultBlock- -- ^ State mode for constant call (latest or pending)- -> a- -- ^ Method data- -> Web3 p b- -- ^ 'Web3' wrapped result- call = _call--_sendTransaction :: (Provider p, Method a, Unit b)- => Address -> b -> a -> Web3 p TxHash-_sendTransaction to value dat = do- primeAddress <- listToMaybe <$> Eth.accounts- Eth.sendTransaction (txdata primeAddress $ Just $ toData dat)- where txdata from = Call from to (Just defaultGas) Nothing (Just $ toWeiText value)- toWeiText = ("0x" <>) . toStrict . B.toLazyText . B.hexadecimal . toWei- defaultGas = "0x2DC2DC"--_call :: (Provider p, Method a, ABIEncoding b)- => Address -> DefaultBlock -> a -> Web3 p b-_call to mode dat = do- primeAddress <- listToMaybe <$> Eth.accounts- res <- Eth.call (txdata primeAddress) mode- case fromData (T.drop 2 res) of- Nothing -> liftIO $ throwIO $ ParserFail $- "Unable to parse result on `" ++ T.unpack res- ++ "` from `" ++ show to ++ "`"- Just x -> return x- where- txdata from = Call from to Nothing Nothing Nothing (Just (toData dat))---- | Zero value is used to send transaction without money-nopay :: Wei-{-# INLINE nopay #-}-nopay = 0---- | Dummy method for sending transaction without method call-data NoMethod = NoMethod--instance ABIEncoding NoMethod where- fromDataParser = return NoMethod- toDataBuilder = const ""--instance Method NoMethod
− src/Network/Ethereum/Web3/Encoding.hs
@@ -1,71 +0,0 @@--- |--- Module : Network.Ethereum.Web3.Encoding--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ Web3 ABI encoding data support.----module Network.Ethereum.Web3.Encoding (ABIEncoding(..)) where--import qualified Data.Attoparsec.Text as P-import Data.Attoparsec.Text.Lazy (Parser, maybeResult,- parse)-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Lazy as LT-import Data.Text.Lazy.Builder (Builder, fromLazyText,- fromText, toLazyText)-import Network.Ethereum.Web3.Address (Address)-import qualified Network.Ethereum.Web3.Address as A-import Network.Ethereum.Web3.Encoding.Internal---- | Contract ABI data codec-class ABIEncoding a where- toDataBuilder :: a -> Builder- fromDataParser :: Parser a-- -- | Encode value into abi-encoding represenation- toData :: a -> Text- {-# INLINE toData #-}- toData = LT.toStrict . toLazyText . toDataBuilder-- -- | Parse encoded value- fromData :: Text -> Maybe a- {-# INLINE fromData #-}- fromData = maybeResult . parse fromDataParser . LT.fromStrict--instance ABIEncoding Bool where- toDataBuilder = int256HexBuilder . fromEnum- fromDataParser = fmap toEnum int256HexParser--instance ABIEncoding Integer where- toDataBuilder = int256HexBuilder- fromDataParser = int256HexParser--instance ABIEncoding Int where- toDataBuilder = int256HexBuilder- fromDataParser = int256HexParser--instance ABIEncoding Word where- toDataBuilder = int256HexBuilder- fromDataParser = int256HexParser--instance ABIEncoding Text where- toDataBuilder = textBuilder- fromDataParser = textParser--instance ABIEncoding Address where- toDataBuilder = alignR . fromText . A.toText- fromDataParser = either error id . A.fromText- <$> (P.take 24 *> P.take 40)--instance ABIEncoding a => ABIEncoding [a] where- toDataBuilder x = int256HexBuilder (length x)- <> foldMap toDataBuilder x- fromDataParser = do len <- int256HexParser- take len <$> P.many1 fromDataParser
− src/Network/Ethereum/Web3/Encoding/Bytes.hs
@@ -1,91 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE ScopedTypeVariables #-}--- |--- Module : Network.Ethereum.Web3.Encoding.Bytes--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : noportable------ The type bytes<M> support.----module Network.Ethereum.Web3.Encoding.Bytes (- BytesN(..)- , BytesD(..)- ) where--import qualified Data.Attoparsec.Text as P-import Data.ByteArray (Bytes)-import qualified Data.ByteArray as BA-import qualified Data.ByteString.Base16 as BS16 (decode,- encode)-import Data.Monoid (Monoid (..), (<>))-import Data.Proxy-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import qualified Data.Text.Lazy.Builder as B-import GHC.TypeLits (KnownNat, Nat, natVal)-import Network.Ethereum.Web3.Encoding-import Network.Ethereum.Web3.Encoding.Internal--import Debug.Trace---- | Fixed length byte array-newtype BytesN (n :: Nat) = BytesN { unBytesN :: Bytes }- deriving (Eq, Ord)--update :: BytesN a -> Bytes -> BytesN a-{-# INLINE update #-}-update _ = BytesN--instance KnownNat n => EncodingType (BytesN n) where- typeName = const $ "bytes" <> (show . natVal $ (Proxy :: Proxy n))- isDynamic = const False--instance KnownNat n => ABIEncoding (BytesN n) where- toDataBuilder (BytesN bytes) = bytesBuilder bytes- fromDataParser = do- let result = undefined :: KnownNat n => BytesN n- len = fromIntegral (natVal result)- bytesString <- T.take (len * 2) <$> P.take 64- return (update result (bytesDecode bytesString))--instance KnownNat n => Show (BytesN n) where- show = show . BS16.encode . BA.convert . unBytesN--bytesBuilder :: Bytes -> B.Builder-{-# INLINE bytesBuilder #-}-bytesBuilder = alignL . B.fromText . T.decodeUtf8- . BS16.encode . BA.convert--bytesDecode :: T.Text -> Bytes-{-# INLINE bytesDecode #-}-bytesDecode = BA.convert . fst . BS16.decode . T.encodeUtf8---- | Dynamic length byte array-newtype BytesD = BytesD { unBytesD :: Bytes }- deriving (Eq, Ord)--instance Monoid BytesD where- mempty = BytesD mempty- mappend (BytesD a) (BytesD b) = BytesD (mappend a b)--instance EncodingType BytesD where- typeName = const "bytes[]"- isDynamic = const True--instance ABIEncoding BytesD where- toDataBuilder (BytesD bytes) = int256HexBuilder (BA.length bytes)- <> bytesBuilder bytes- fromDataParser = do- len <- int256HexParser- if (len :: Integer) > fromIntegral (maxBound :: Int)- then fail "Bytes length over bound!"- else (BytesD . bytesDecode) <$> P.take (fromIntegral len * 2)--instance Show BytesD where- show = show . BS16.encode . BA.convert . unBytesD
− src/Network/Ethereum/Web3/Encoding/Internal.hs
@@ -1,101 +0,0 @@--- |--- Module : Network.Ethereum.Web3.Encoding.Internal--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ ABI encoding internal function and types.----module Network.Ethereum.Web3.Encoding.Internal where--import qualified Data.Attoparsec.Text as P-import Data.Attoparsec.Text.Lazy (Parser)-import Data.Bits (Bits)-import qualified Data.ByteString.Base16 as BS16 (decode, encode)-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as T-import Data.Text.Encoding (decodeUtf8, encodeUtf8)-import qualified Data.Text.Lazy as LT-import Data.Text.Lazy.Builder (Builder, fromLazyText, fromText,- toLazyText)-import Data.Text.Lazy.Builder.Int as B-import qualified Data.Text.Read as R-import Language.Haskell.TH-import Network.Ethereum.Web3.Address (Address)--class EncodingType a where- typeName :: a -> String- isDynamic :: a -> Bool--instance EncodingType Bool where- typeName = const "bool"- isDynamic = const False--instance EncodingType Integer where- typeName = const "int"- isDynamic = const False--instance EncodingType Int where- typeName = const "int"- isDynamic = const False--instance EncodingType Word where- typeName = const "uint"- isDynamic = const False--instance EncodingType Text where- typeName = const "string"- isDynamic = const True--instance EncodingType Address where- typeName = const "address"- isDynamic = const False--instance EncodingType a => EncodingType [a] where- typeName = const "[]"- isDynamic = const True---- | Make 256bit alignment; lazy (left, right)-align :: Builder -> (Builder, Builder)-align v = (v <> zeros, zeros <> v)- where zerosLen | LT.length s `mod` 64 == 0 = 0- | otherwise = 64 - (LT.length s `mod` 64)- zeros = fromLazyText (LT.replicate zerosLen "0")- s = toLazyText v---- | Left/Right specialized alignment-alignL, alignR :: Builder -> Builder-{-# INLINE alignL #-}-alignL = fst . align-{-# INLINE alignR #-}-alignR = snd . align--int256HexBuilder :: Integral a => a -> Builder-int256HexBuilder x- | x < 0 = int256HexBuilder (2^256 + fromIntegral x)- | otherwise = alignR (B.hexadecimal x)--int256HexParser :: (Bits a, Integral a) => Parser a-int256HexParser = do- hex <- P.take 64- case R.hexadecimal hex of- Right (v, "") -> return v- _ -> fail ("Broken hexadecimal: `" ++ T.unpack hex ++ "`")--textBuilder :: Text -> Builder-textBuilder s = int256HexBuilder (T.length hex `div` 2)- <> alignL (fromText hex)- where textToHex = decodeUtf8 . BS16.encode . encodeUtf8- hex = textToHex s--textParser :: Parser Text-textParser = do- len <- int256HexParser- let zeroBytes = 32 - (len `mod` 32)- str <- P.take (len * 2) <* P.take (zeroBytes * 2)- return (hexToText str)- where hexToText = decodeUtf8 . fst . BS16.decode . encodeUtf8
− src/Network/Ethereum/Web3/Encoding/Tuple.hs
@@ -1,41 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}--- |--- Module : Network.Ethereum.Web3.Encoding.Tuple--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ ABIEncoding tuple instances.----module Network.Ethereum.Web3.Encoding.Tuple (Singleton(..)) where--import Network.Ethereum.Web3.Encoding-import Network.Ethereum.Web3.Encoding.Internal-import Network.Ethereum.Web3.Encoding.TupleTH---- | Singleton parameter instance-newtype Singleton a = Singleton { unSingleton :: a }--instance (EncodingType a, ABIEncoding a) => ABIEncoding (Singleton a) where- toDataBuilder = _serialize (1, []) . unSingleton- fromDataParser = Singleton <$> (withParser sParser >>= dParser)- where withParser f = f undefined---- | Tuple instances from 2 to 15 params-$(mkTupleInst 2)-$(mkTupleInst 3)-$(mkTupleInst 4)-$(mkTupleInst 5)-$(mkTupleInst 6)-$(mkTupleInst 7)-$(mkTupleInst 8)-$(mkTupleInst 9)-$(mkTupleInst 10)-$(mkTupleInst 11)-$(mkTupleInst 12)-$(mkTupleInst 13)-$(mkTupleInst 14)-$(mkTupleInst 15)
− src/Network/Ethereum/Web3/Encoding/TupleTH.hs
@@ -1,152 +0,0 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}--- |--- Module : Network.Ethereum.Web3.Encoding.TupleTH--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ Tuple ABI encoding instance TH generator.----module Network.Ethereum.Web3.Encoding.TupleTH (- mkTupleInst- , ABIData(..)- , sParser- , dParser- ) where--import Control.Monad (replicateM)-import Data.Attoparsec.Text (Parser)-import qualified Data.Attoparsec.Text as P-import qualified Data.Text.Lazy as LT-import Data.Text.Lazy.Builder (Builder, toLazyText)-import Language.Haskell.TH-import Network.Ethereum.Web3.Encoding-import Network.Ethereum.Web3.Encoding.Internal---- | Argument offset calculator-offset :: Int- -- ^ Count of arguments- -> [Builder]- -- ^ Previous dynamic arguments- -> Int- -- ^ Offset-offset totalArgs args = headerOffset + dataOffset- where- headerOffset = totalArgs * 32- dataOffset = builderLen (mconcat args)- builderLen = fromIntegral . (`div` 2) . LT.length . toLazyText---- | ABI data multiparam internal serializer-class ABIData a where- _serialize :: (Int, [(Builder, Builder)]) -> a- -- ^ Serialize with accumulator:- -- pair of argument count and list of pair header and- -- data part (for dynamic arguments)--instance (EncodingType b, ABIEncoding b, ABIData a) => ABIData (b -> a) where- _serialize (n, l) x- | isDynamic x = _serialize (n, (toDataBuilder dynOffset, toDataBuilder x) : l)- | otherwise = _serialize (n, (toDataBuilder x , mempty) : l)- where dynOffset = offset n (fmap snd l)--instance ABIData Builder where- _serialize = uncurry mappend . mconcat . reverse . snd---- | Static argument parser-sParser :: (EncodingType a, ABIEncoding a) => a -> Parser a-sParser x | isDynamic x = P.take 64 >> return undefined- | otherwise = fromDataParser---- | Dynamic argument parser-dParser :: (EncodingType a, ABIEncoding a) => a -> Parser a-dParser x | isDynamic x = fromDataParser- | otherwise = return x---- | Generator for tupleP{N} function signature-mkTuplePType :: Int -> DecQ-mkTuplePType n = do- varsT <- fmap varT <$> replicateM n (newName "t")- let contextT = concat- [[[t|ABIEncoding $x|], [t|EncodingType $x|]] | x <- varsT]- varsTupleT = foldl appT (tupleT n) varsT- sigD (mkName $ "tupleP" ++ show n)- (forallT [] (cxt contextT) [t|Parser $(varsTupleT)|])---- | Generator for tupleP{N} function-mkTupleP :: Int -> DecQ-mkTupleP n = do- vars <- replicateM n (newName "t")- funD (mkName $ "tupleP" ++ show n) $ pure $- clause []- (normalB [|$(varE withPN) $(varE staticPN) >>= $(varE dynamicPN)|])- (decs vars)- where- withPN = mkName "withParser"- staticPN = mkName "staticParser"- dynamicPN = mkName "dynamicParser"- fun = mkName "f"- decs vars = [ withPFun, staticPFun vars, dynamicPFun vars ]-- withPFun = funD withPN $ pure $- clause [varP fun]- (normalB [|$(varE fun) $(tupE (replicate n [|undefined|]))|]) []-- staticPFun vars = funD staticPN $ pure $- clause [tupP $ fmap varP vars]- (normalB (mkAppSeq (eTupleE n : fmap (\x -> [|sParser $(varE x)|]) vars))) []-- dynamicPFun vars = funD dynamicPN $ pure $- clause [tupP $ fmap varP vars]- (normalB (mkAppSeq (eTupleE n : fmap (\x -> [|dParser $(varE x)|]) vars))) []--mkAppSeq :: [ExpQ] -> ExpQ-mkAppSeq = infixApps . dollarFirst . sparse- where sparse [x] = [x]- sparse (x : xs) = x : [|(<*>)|] : sparse xs- dollarFirst (x : _ : xs) = x : [|(<$>)|] : xs- infixApps (x : xs) = go x xs- go acc [] = acc- go acc (f : x : xs) = go (infixApp acc f x) xs--eTupleE :: Int -> ExpQ-eTupleE 2 = [|(,)|]-eTupleE 3 = [|(,,)|]-eTupleE 4 = [|(,,,)|]-eTupleE 5 = [|(,,,,)|]-eTupleE 6 = [|(,,,,,)|]-eTupleE 7 = [|(,,,,,,)|]-eTupleE 8 = [|(,,,,,,,)|]-eTupleE 9 = [|(,,,,,,,,)|]-eTupleE 10 = [|(,,,,,,,,,)|]-eTupleE 11 = [|(,,,,,,,,,,)|]-eTupleE 12 = [|(,,,,,,,,,,,)|]-eTupleE 13 = [|(,,,,,,,,,,,,)|]-eTupleE 14 = [|(,,,,,,,,,,,,,)|]-eTupleE 15 = [|(,,,,,,,,,,,,,,)|]-eTupleE _ = error "Unsupported tuple size"--mkEncodingInst :: Int -> DecQ-mkEncodingInst n = do- vars <- replicateM n (newName "t")- let varsT = fmap varT vars- contextT = concat- [[[t|ABIEncoding $x|], [t|EncodingType $x|]] | x <- varsT]- varsTupleT = foldl appT (tupleT n) varsT- instanceD (cxt contextT) (appT [t|ABIEncoding|] varsTupleT)- [ funD (mkName "toDataBuilder") [- clause [tupP (fmap varP vars)]- (normalB (appsE ([|_serialize (n, [])|] : fmap varE vars))) [] ]- , funD (mkName "fromDataParser") [- clause [] (normalB $ varE $ mkName $ "tupleP" ++ show n) [] ]- ]---- | Make a ABIEncoding tuple instance with given count of arguments-mkTupleInst :: Int -> Q [Dec]-mkTupleInst n = sequence- [ mkTuplePType n- , mkTupleP n- , mkEncodingInst n ]
src/Network/Ethereum/Web3/Eth.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module : Network.Ethereum.Web3.Eth -- Copyright : Alexander Krupenkin 2016@@ -9,216 +11,218 @@ -- -- Ethereum node JSON-RPC API methods with `eth_` prefix. --+ module Network.Ethereum.Web3.Eth where -import Data.Text (Text)-import Network.Ethereum.Web3.Address-import Network.Ethereum.Web3.JsonRpc-import Network.Ethereum.Web3.Provider+import Network.Ethereum.ABI.Prim.Address (Address)+import Network.Ethereum.ABI.Prim.Bytes (Bytes)+import Network.Ethereum.Web3.Provider (Web3) import Network.Ethereum.Web3.Types+import Network.JsonRpc.TinyClient (remote) -- | Returns the current ethereum protocol version.-protocolVersion :: Provider a => Web3 a Text+protocolVersion :: Web3 Int {-# INLINE protocolVersion #-} protocolVersion = remote "eth_protocolVersion" --- TODO The return type of this function requires a new type to be created -- | Returns an object with data about the sync status or false.--- syncing :: Proviver a => Web3 a Text+syncing :: Web3 SyncingState+{-# INLINE syncing #-}+syncing = remote "eth_syncing" -- | Returns the client coinbase address.-coinbase :: Provider a => Web3 a Address+coinbase :: Web3 Address {-# INLINE coinbase #-} coinbase = remote "eth_coinbase" -- | Returns true if client is actively mining new blocks.-mining :: Provider a => Web3 a Bool+mining :: Web3 Bool {-# INLINE mining #-} mining = remote "eth_mining" -- | Returns the number of hashes per second that the node is mining with.-hashrate :: Provider a => Web3 a Text+hashrate :: Web3 Quantity {-# INLINE hashrate #-} hashrate = remote "eth_hashrate" -- | Returns the value from a storage position at a given address.-getStorageAt :: Provider a => Address -> Text -> DefaultBlock -> Web3 a Text+getStorageAt :: Address -> Quantity -> DefaultBlock -> Web3 Bytes {-# INLINE getStorageAt #-} getStorageAt = remote "eth_getStorageAt" -- | Returns the number of transactions sent from an address.-getTransactionCount :: Provider a => Address -> DefaultBlock -> Web3 a Text+getTransactionCount :: Address -> DefaultBlock -> Web3 Quantity {-# INLINE getTransactionCount #-} getTransactionCount = remote "eth_getTransactionCount" -- | Returns the number of transactions in a block from a block matching the given block hash.-getBlockTransactionCountByHash :: Provider a => Text -> Web3 a Text+getBlockTransactionCountByHash :: Bytes -> Web3 Quantity {-# INLINE getBlockTransactionCountByHash #-} getBlockTransactionCountByHash = remote "eth_getBlockTransactionCountByHash" -- | Returns the number of transactions in a block matching the -- given block number.-getBlockTransactionCountByNumber :: Provider a => Text -> Web3 a Text+getBlockTransactionCountByNumber :: Quantity -> Web3 Quantity {-# INLINE getBlockTransactionCountByNumber #-} getBlockTransactionCountByNumber = remote "eth_getBlockTransactionCountByNumber" -- | Returns the number of uncles in a block from a block matching the given -- block hash.-getUncleCountByBlockHash :: Provider a => Text -> Web3 a Text+getUncleCountByBlockHash :: Bytes -> Web3 Quantity {-# INLINE getUncleCountByBlockHash #-} getUncleCountByBlockHash = remote "eth_getUncleCountByBlockHash" -- | Returns the number of uncles in a block from a block matching the given -- block number.-getUncleCountByBlockNumber :: Provider a => Text -> Web3 a Text+getUncleCountByBlockNumber :: Quantity -> Web3 Quantity {-# INLINE getUncleCountByBlockNumber #-} getUncleCountByBlockNumber = remote "eth_getUncleCountByBlockNumber" -- | Returns code at a given address.-getCode :: Provider a => Address -> DefaultBlock -> Web3 a Text+getCode :: Address -> DefaultBlock -> Web3 Bytes {-# INLINE getCode #-} getCode = remote "eth_getCode" -- | Returns an Ethereum specific signature with: -- sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))).-sign :: Provider a => Address -> Text -> Web3 a Text+sign :: Address -> Bytes -> Web3 Bytes {-# INLINE sign #-} sign = remote "eth_sign" -- | Creates new message call transaction or a contract creation, -- if the data field contains code.-sendTransaction :: Provider a => Call -> Web3 a Text+sendTransaction :: Call -> Web3 Bytes {-# INLINE sendTransaction #-} sendTransaction = remote "eth_sendTransaction" -- | Creates new message call transaction or a contract creation for signed -- transactions.-sendRawTransaction :: Provider a => Text -> Web3 a Text+sendRawTransaction :: Bytes -> Web3 Bytes {-# INLINE sendRawTransaction #-} sendRawTransaction = remote "eth_sendRawTransaction" -- | Returns the balance of the account of given address.-getBalance :: Provider a => Address -> DefaultBlock -> Web3 a Text+getBalance :: Address -> DefaultBlock -> Web3 Quantity {-# INLINE getBalance #-} getBalance = remote "eth_getBalance" -- | Creates a filter object, based on filter options, to notify when the -- state changes (logs). To check if the state has changed, call -- 'getFilterChanges'.-newFilter :: Provider a => Filter -> Web3 a FilterId+newFilter :: Filter e -> Web3 FilterId {-# INLINE newFilter #-} newFilter = remote "eth_newFilter" -- | Polling method for a filter, which returns an array of logs which -- occurred since last poll.-getFilterChanges :: Provider a => FilterId -> Web3 a [Change]+getFilterChanges :: FilterId -> Web3 [Change] {-# INLINE getFilterChanges #-} getFilterChanges = remote "eth_getFilterChanges" -- | Uninstalls a filter with given id. -- Should always be called when watch is no longer needed.-uninstallFilter :: Provider a => FilterId -> Web3 a Bool+uninstallFilter :: FilterId -> Web3 Bool {-# INLINE uninstallFilter #-} uninstallFilter = remote "eth_uninstallFilter" -- | Returns an array of all logs matching a given filter object.-getLogs :: Provider a => Filter -> Web3 a [Change]+getLogs :: Filter e -> Web3 [Change] {-# INLINE getLogs #-} getLogs = remote "eth_getLogs" -- | Executes a new message call immediately without creating a -- transaction on the block chain.-call :: Provider a => Call -> DefaultBlock -> Web3 a Text+call :: Call -> DefaultBlock -> Web3 Bytes {-# INLINE call #-} call = remote "eth_call" -- | Makes a call or transaction, which won't be added to the blockchain and -- returns the used gas, which can be used for estimating the used gas.-estimateGas :: Provider a => Call -> Web3 a Text+estimateGas :: Call -> Web3 Quantity {-# INLINE estimateGas #-} estimateGas = remote "eth_estimateGas" -- | Returns information about a block by hash.-getBlockByHash :: Provider a => Text -> Web3 a Block+getBlockByHash :: Bytes -> Web3 Block {-# INLINE getBlockByHash #-} getBlockByHash = flip (remote "eth_getBlockByHash") True -- | Returns information about a block by block number.-getBlockByNumber :: Provider a => Text -> Web3 a Block+getBlockByNumber :: Quantity -> Web3 Block {-# INLINE getBlockByNumber #-} getBlockByNumber = flip (remote "eth_getBlockByNumber") True -- | Returns the information about a transaction requested by transaction hash.-getTransactionByHash :: Provider a => Text -> Web3 a (Maybe Transaction)+getTransactionByHash :: Bytes -> Web3 (Maybe Transaction) {-# INLINE getTransactionByHash #-} getTransactionByHash = remote "eth_getTransactionByHash" -- | Returns information about a transaction by block hash and transaction index position.-getTransactionByBlockHashAndIndex :: Provider a => Text -> Text -> Web3 a (Maybe Transaction)+getTransactionByBlockHashAndIndex :: Bytes -> Quantity -> Web3 (Maybe Transaction) {-# INLINE getTransactionByBlockHashAndIndex #-} getTransactionByBlockHashAndIndex = remote "eth_getTransactionByBlockHashAndIndex" -- | Returns information about a transaction by block number and transaction -- index position.-getTransactionByBlockNumberAndIndex :: Provider a => DefaultBlock -> Text -> Web3 a (Maybe Transaction)+getTransactionByBlockNumberAndIndex :: DefaultBlock -> Quantity -> Web3 (Maybe Transaction) {-# INLINE getTransactionByBlockNumberAndIndex #-} getTransactionByBlockNumberAndIndex = remote "eth_getTransactionByBlockNumberAndIndex" -- | Returns the receipt of a transaction by transaction hash. -- TODO must create new type, TxReceipt--- getTransactionReceipt :: Provider a => Text -> Web3 a TxReceipt+-- getTransactionReceipt :: Text -> Web3 TxReceipt -- getTransactionReceipt = remote "getTransactionReceipt" -- | Returns a list of addresses owned by client.-accounts :: Provider a => Web3 a [Address]+accounts :: Web3 [Address] {-# INLINE accounts #-} accounts = remote "eth_accounts" -newBlockFilter :: Provider a => Web3 a Text+newBlockFilter :: Web3 Bytes {-# INLINE newBlockFilter #-} newBlockFilter = remote "eth_newBlockFilter" -- | Polling method for a block filter, which returns an array of block hashes -- occurred since last poll.-getBlockFilterChanges :: Provider a => Text -> Web3 a [Text]+getBlockFilterChanges :: Quantity -> Web3 [Bytes] {-# INLINE getBlockFilterChanges #-}-getBlockFilterChanges = remote "eth_getBlockFilterChanges"+getBlockFilterChanges = remote "eth_getFilterChanges" -- | Returns the number of most recent block.-blockNumber :: Provider a => Web3 a Text+blockNumber :: Web3 BlockNumber {-# INLINE blockNumber #-} blockNumber = remote "eth_blockNumber" -- | Returns the current price per gas in wei.-gasPrice :: Provider a => Web3 a Text+gasPrice :: Web3 Quantity {-# INLINE gasPrice #-} gasPrice = remote "eth_gasPrice" -- | Returns information about a uncle of a block by hash and uncle index -- position.-getUncleByBlockHashAndIndex :: Provider a => Text -> Text -> Web3 a Block+getUncleByBlockHashAndIndex :: Bytes -> Quantity -> Web3 Block {-# INLINE getUncleByBlockHashAndIndex #-} getUncleByBlockHashAndIndex = remote "eth_getUncleByBlockHashAndIndex" -- | Returns information about a uncle of a block by number and uncle index -- position.-getUncleByBlockNumberAndIndex :: Provider a => DefaultBlock -> Text -> Web3 a Block+getUncleByBlockNumberAndIndex :: DefaultBlock -> Quantity -> Web3 Block {-# INLINE getUncleByBlockNumberAndIndex #-} getUncleByBlockNumberAndIndex = remote "eth_getUncleByBlockNumberAndIndex" -- | Creates a filter in the node, to notify when new pending transactions arrive. To check if the state has changed, call getFilterChanges. Returns a FilterId.-newPendingTransactionFilter :: Provider a => Web3 a Text+newPendingTransactionFilter :: Web3 Quantity {-# INLINE newPendingTransactionFilter #-} newPendingTransactionFilter = remote "eth_newPendingTransactionFilter" -- | Returns an array of all logs matching filter with given id.-getFilterLogs :: Provider a => Text -> Web3 a [Change]+getFilterLogs :: Quantity -> Web3 [Change] {-# INLINE getFilterLogs #-} getFilterLogs = remote "eth_getFilterLogs" -- | Returns the hash of the current block, the seedHash, and the boundary -- condition to be met ("target").-getWork :: Provider a => Web3 a [Text]+getWork :: Web3 [Bytes] {-# INLINE getWork #-} getWork = remote "eth_getWork" @@ -227,7 +231,7 @@ -- 1. DATA, 8 Bytes - The nonce found (64 bits) -- 2. DATA, 32 Bytes - The header's pow-hash (256 bits) -- 3. DATA, 32 Bytes - The mix digest (256 bits)-submitWork :: Provider a => Text -> Text -> Text -> Web3 a Bool+submitWork :: Bytes -> Bytes -> Bytes -> Web3 Bool {-# INLINE submitWork #-} submitWork = remote "eth_submitWork" @@ -235,6 +239,6 @@ -- Parameters: -- 1. Hashrate, a hexadecimal string representation (32 bytes) of the hash rate -- 2. ID, String - A random hexadecimal(32 bytes) ID identifying the client-submitHashrate :: Provider a => Text -> Text -> Web3 a Bool+submitHashrate :: Bytes -> Bytes -> Web3 Bool {-# INLINE submitHashrate #-} submitHashrate = remote "eth_submitHashrate"
− src/Network/Ethereum/Web3/Internal.hs
@@ -1,24 +0,0 @@--- |--- Module : Network.Ethereum.Web3.Internal--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ Common used internal functions.----module Network.Ethereum.Web3.Internal where--import Data.Char (toLower, toUpper)---- | Lower first char of string-toLowerFirst :: String -> String-toLowerFirst [] = []-toLowerFirst (x : xs) = toLower x : xs---- | Upper first char of string-toUpperFirst :: String -> String-toUpperFirst [] = []-toUpperFirst (x : xs) = toUpper x : xs
− src/Network/Ethereum/Web3/JsonAbi.hs
@@ -1,146 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--- |--- Module : Network.Ethereum.Web3.JsonAbi--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ Ethereum smart contract JSON ABI types.----module Network.Ethereum.Web3.JsonAbi (- ContractABI(..)- , Declaration(..)- , FunctionArg(..)- , EventArg(..)- , signature- , methodId- , eventId- ) where--import Crypto.Hash (Digest, Keccak_256, hash)-import Data.Aeson-import Data.Aeson.TH-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Network.Ethereum.Web3.Internal---- | Method argument-data FunctionArg = FunctionArg- { funArgName :: Text- -- ^ Argument name- , funArgType :: Text- -- ^ Argument type- } deriving (Show, Eq, Ord)--$(deriveJSON- (defaultOptions {fieldLabelModifier = toLowerFirst . 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 = toLowerFirst . drop 6})- ''EventArg)---- | Elementrary 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, Eq, Ord)--$(deriveJSON (defaultOptions {- sumEncoding = defaultTaggedObject { tagFieldName = "type" }- , constructorTagModifier = toLowerFirst . drop 1- , fieldLabelModifier = toLowerFirst . drop 3 })- ''Declaration)---- | Contract ABI is a list of method / event declarations-newtype ContractABI = ContractABI { unABI :: [Declaration] }- deriving (Eq, Ord)--instance Show ContractABI where- show (ContractABI c) = T.unpack $ T.unlines $- [ "Contract:" ]- ++ foldMap showConstructor c ++- [ "\tEvents:" ]- ++ foldMap showEvent c ++- [ "\tMethods:" ]- ++ foldMap showMethod c--instance FromJSON ContractABI where- parseJSON = fmap ContractABI . parseJSON--instance ToJSON ContractABI where- toJSON (ContractABI x) = toJSON x--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 = T.dropEnd 1 . foldMap (<> ",") . fmap funArgType--signature (DFallback _) = "()"--signature (DFunction name _ inputs _) = name <> "(" <> args inputs <> ")"- where args = T.dropEnd 1 . foldMap (<> ",") . fmap funArgType--signature (DEvent name inputs _) = name <> "(" <> args inputs <> ")"- where args = T.dropEnd 1 . foldMap (<> ",") . fmap eveArgType---- | Localy compute Keccak-256 hash of given text-sha3 :: Text -> Text-{-# INLINE sha3 #-}-sha3 x = T.pack (show digest)- where digest :: Digest Keccak_256- digest = hash (T.encodeUtf8 x)---- | Generate method selector by given method 'Delcaration'-methodId :: Declaration -> Text-{-# INLINE methodId #-}-methodId = ("0x" <>) . T.take 8 . sha3 . signature---- | Generate event `topic0` hash by givent event 'Delcaration'-eventId :: Declaration -> Text-{-# INLINE eventId #-}-eventId = ("0x" <>) . sha3 . signature
− src/Network/Ethereum/Web3/JsonRpc.hs
@@ -1,95 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}--- |--- Module : Network.JsonRpc--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : portable------ Little JSON-RPC 2.0 client.--- Functions for implementing the client side of JSON-RPC 2.0.--- See <http://www.jsonrpc.org/specification>.----module Network.Ethereum.Web3.JsonRpc (- remote- , MethodName- , ServerUri- ) where--import Network.Ethereum.Web3.Provider-import Network.Ethereum.Web3.Types--import Control.Applicative ((<|>))-import Control.Exception (throwIO)-import Control.Monad ((>=>))-import Data.Aeson-import Data.ByteString.Lazy (ByteString)-import Data.Text (Text)-import Data.Vector (fromList)-import Network.HTTP.Client (RequestBody (RequestBodyLBS),- httpLbs, method, newManager,- parseRequest, requestBody,- requestHeaders, responseBody)-import Network.HTTP.Client.TLS (tlsManagerSettings)---- | Name of called method.-type MethodName = Text---- | JSON-RPC server URI-type ServerUri = String---- | Remote call of JSON-RPC method.--- Arguments of function are stored into @params@ request array.-remote :: Remote a => MethodName -> a-remote n = remote_ (\uri -> call uri . Array . fromList)- where- call uri = connection uri . encode . Request n 1- connection uri body = do- manager <- newManager tlsManagerSettings- request <- parseRequest uri- let request' = request- { requestBody = RequestBodyLBS body- , requestHeaders = [("Content-Type", "application/json")]- , method = "POST" }- responseBody <$> httpLbs request' manager--decodeResponse :: FromJSON a => ByteString -> IO a-decodeResponse = tryParse . eitherDecode- >=> tryJsonRpc . rsResult- >=> tryParse . eitherDecode . encode- where tryJsonRpc :: Either RpcError a -> IO a- tryJsonRpc = either (throwIO . JsonRpcFail) return- tryParse :: Either String a -> IO a- tryParse = either (throwIO . ParserFail) return--class Remote a where- remote_ :: (ServerUri -> [Value] -> IO ByteString) -> a--instance (ToJSON a, Remote b) => Remote (a -> b) where- remote_ f x = remote_ (\u xs -> f u (toJSON x : xs))--instance (Provider p, FromJSON a) => Remote (Web3 p a) where- remote_ f = (\u -> Web3 (decodeResponse =<< f u [])) =<< rpcUri---- | JSON-RPC request.-data Request = Request { rqMethod :: !Text- , rqId :: !Int- , rqParams :: !Value }--instance ToJSON Request where- toJSON rq = object [ "jsonrpc" .= String "2.0"- , "method" .= rqMethod rq- , "params" .= rqParams rq- , "id" .= rqId rq ]---- | JSON-RPC response.-data Response = Response- { rsResult :: !(Either RpcError Value)- } deriving (Show)--instance FromJSON Response where- parseJSON = withObject "JSON-RPC response object" $- \v -> Response <$>- (Right <$> v .: "result" <|> Left <$> v .: "error")
src/Network/Ethereum/Web3/Net.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module : Network.Ethereum.Web3.Net -- Copyright : Alexander Krupenkin 2016@@ -9,24 +11,24 @@ -- -- Ethereum node JSON-RPC API methods with `net_` prefix. --+ module Network.Ethereum.Web3.Net where -import Data.Text (Text)-import Network.Ethereum.Web3.JsonRpc-import Network.Ethereum.Web3.Provider-import Network.Ethereum.Web3.Types+import Network.Ethereum.Web3.Provider (Web3)+import Network.Ethereum.Web3.Types (Quantity)+import Network.JsonRpc.TinyClient (remote) -- | Returns the current network id.-version :: Provider a => Web3 a Text+version :: Web3 Int {-# INLINE version #-} version = remote "net_version" -- | Returns true if client is actively listening for network connections.-listening :: Provider a => Web3 a Bool+listening :: Web3 Bool {-# INLINE listening #-} listening = remote "net_listening" -- | Returns number of peers currently connected to the client.-peerCount :: Provider a => Web3 a Text+peerCount :: Web3 Quantity {-# INLINE peerCount #-} peerCount = remote "net_peerCount"
src/Network/Ethereum/Web3/Provider.hs view
@@ -1,3 +1,8 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+ -- | -- Module : Network.Ethereum.Web3.Provider -- Copyright : Alexander Krupenkin 2016@@ -9,35 +14,70 @@ -- -- Web3 service provider. --+ module Network.Ethereum.Web3.Provider where -import Control.Concurrent (ThreadId, forkIO)-import Control.Exception (try)-import Control.Monad.IO.Class (MonadIO (..))-import Network.Ethereum.Web3.Types+import Control.Concurrent.Async (Async, async)+import Control.Exception (Exception, try)+import Control.Monad.Catch (MonadThrow)+import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.Reader (MonadReader (..))+import Control.Monad.Trans.Reader (ReaderT, mapReaderT, runReaderT)+import Data.Aeson (FromJSON)+import Data.Default (Default (..))+import GHC.Generics (Generic)+import Network.HTTP.Client (Manager, newManager)+import Network.HTTP.Client.TLS (tlsManagerSettings) --- | Ethereum node service provider-class Provider a where- -- | JSON-RPC provider URI, default: localhost:8545- rpcUri :: Web3 a String+import Network.JsonRpc.TinyClient (Remote, ServerUri) --- | Default 'Web3' service provider-data DefaultProvider+-- | Any communication with Ethereum node wrapped with 'Web3' monad+newtype Web3 a = Web3 { unWeb3 :: ReaderT (ServerUri, Manager) IO a }+ deriving (Functor, Applicative, Monad, MonadIO, MonadThrow) -instance Provider DefaultProvider where- rpcUri = return "http://localhost:8545"+instance MonadReader (ServerUri, Manager) Web3 where+ ask = Web3 ask+ local f = Web3 . local f . unWeb3 +instance FromJSON a => Remote Web3 (Web3 a)++-- | Some peace of error response+data Web3Error+ = JsonRpcFail !String+ -- ^ JSON-RPC communication error+ | ParserFail !String+ -- ^ Error in parser state+ | UserFail !String+ -- ^ Common head for user errors+ deriving (Show, Eq, Generic)++instance Exception Web3Error++--TODO: Change to `HttpProvider ServerUri | IpcProvider FilePath` to support IPC+-- | Web3 Provider+data Provider = HttpProvider ServerUri+ deriving (Show, Eq, Generic)++instance Default Provider where+ def = HttpProvider "http://localhost:8545"++-- | 'Web3' monad runner, using the supplied Manager+runWeb3With :: MonadIO m => Manager -> Provider -> Web3 a -> m (Either Web3Error a)+runWeb3With manager (HttpProvider uri) f =+ liftIO . try . flip runReaderT (uri, manager) . unWeb3 $ f+ -- | 'Web3' monad runner-runWeb3' :: MonadIO m => Web3 a b -> m (Either Web3Error b)-{-# INLINE runWeb3' #-}-runWeb3' = liftIO . try . unWeb3+runWeb3' :: MonadIO m => Provider -> Web3 a -> m (Either Web3Error a)+runWeb3' provider f = do+ manager <- liftIO $ newManager tlsManagerSettings+ runWeb3With manager provider f -- | 'Web3' runner for default provider-runWeb3 :: MonadIO m => Web3 DefaultProvider b -> m (Either Web3Error b)+runWeb3 :: MonadIO m => Web3 a -> m (Either Web3Error a) {-# INLINE runWeb3 #-}-runWeb3 = runWeb3'+runWeb3 = runWeb3' def -- | Fork 'Web3' with the same 'Provider'-forkWeb3 :: Web3 a () -> Web3 a ThreadId+forkWeb3 :: Web3 a -> Web3 (Async a) {-# INLINE forkWeb3 #-}-forkWeb3 = Web3 . forkIO . unWeb3+forkWeb3 = Web3 . mapReaderT async . unWeb3
− src/Network/Ethereum/Web3/TH.hs
@@ -1,296 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}---- |--- Module : Network.Ethereum.Web3.TH--- Copyright : Alexander Krupenkin 2016--- License : BSD3------ Maintainer : mail@akru.me--- Stability : experimental--- Portability : unportable------ TemplateHaskell based Ethereum contract ABI--- methods & event generator for Haskell native API.------ @--- [abiFrom|data/sample.json|]------ main = do--- runWeb3 $ event "0x..." $--- \(Action2 n x) -> liftIO $ do print n--- print x--- wait--- where wait = threadDelay 1000000 >> wait--- @----module Network.Ethereum.Web3.TH (- -- ** Quasiquoter's- abi- , abiFrom- -- ** Used by TH data types- , Bytes- , Text- , Singleton(..)- , ABIEncoding(..)- ) where--import qualified Data.Attoparsec.Text as P-import qualified Data.Text as T-import qualified Data.Text.Lazy as LT-import qualified Data.Text.Lazy.Builder as B-import qualified Data.Text.Lazy.Encoding as LT--import Network.Ethereum.Unit-import Network.Ethereum.Web3.Address (Address)-import Network.Ethereum.Web3.Contract-import Network.Ethereum.Web3.Encoding-import Network.Ethereum.Web3.Encoding.Tuple-import Network.Ethereum.Web3.Internal-import Network.Ethereum.Web3.JsonAbi-import Network.Ethereum.Web3.Provider-import Network.Ethereum.Web3.Types--import Control.Monad (replicateM)--import Data.Aeson-import Data.ByteArray (Bytes)-import Data.List (groupBy, sortBy)-import Data.Monoid (mconcat, (<>))-import Data.Text (Text, isPrefixOf)--import GHC.Generics--import Language.Haskell.TH-import Language.Haskell.TH.Lib-import Language.Haskell.TH.Quote---- | Read contract ABI from file-abiFrom :: QuasiQuoter-abiFrom = quoteFile abi---- | QQ reader for contract ABI-abi :: QuasiQuoter-abi = QuasiQuoter- { quoteDec = quoteAbiDec- , quoteExp = quoteAbiExp- , quotePat = undefined- , quoteType = undefined- }---- | Instance declaration with empty context-instanceD' :: Name -> TypeQ -> [DecQ] -> DecQ-instanceD' name insType =- instanceD (cxt []) (appT insType (conT name))---- | Simple data type declaration with one constructor-dataD' :: Name -> ConQ -> [Name] -> DecQ-dataD' name rec derive =-#if MIN_VERSION_template_haskell(2,12,0)- dataD (cxt []) name [] Nothing [rec] [derivClause Nothing (conT <$> derive)]-#else- dataD (cxt []) name [] Nothing [rec] $ cxt (conT <$> derive)-#endif---- | Simple function declaration-funD' :: Name -> [PatQ] -> ExpQ -> DecQ-funD' name p f = funD name [clause p (normalB f) []]---- | ABI and Haskell types association-typeQ :: Text -> TypeQ-typeQ typ | T.any (== '[') typ = appT listT (go (T.takeWhile (/= '[') typ))- | otherwise = go typ- where go x | "string" == x = conT (mkName "Text")- | "address" == x = conT (mkName "Address")- | "bytes" == x = conT (mkName "BytesD")- | "bool" == x = conT (mkName "Bool")- | "bytes" `isPrefixOf` x = appT (conT (mkName "BytesN"))- (numLit (T.drop 5 x))- | "int" `isPrefixOf` x = conT (mkName "Integer")- | "uint" `isPrefixOf` x = conT (mkName "Integer")- | otherwise = fail ("Unknown type: " ++ T.unpack x)- numLit n = litT (numTyLit (read (T.unpack n)))---- | Event argument to TH type-eventBangType :: EventArg -> BangTypeQ-eventBangType (EventArg _ typ _) =- bangType (bang sourceNoUnpack sourceStrict) (typeQ typ)---- | Function argument to TH type-funBangType :: FunctionArg -> BangTypeQ-funBangType (FunctionArg _ typ) =- bangType (bang sourceNoUnpack sourceStrict) (typeQ typ)---- | Solidity dynamic type predicate-isDynType :: Text -> Bool-isDynType "bytes" = True-isDynType "string" = True-isDynType x | T.any (== '[') x = True- | otherwise = False--eventEncodigD :: Name -> [EventArg] -> [DecQ]-eventEncodigD eventName args =- [ funD' (mkName "toDataBuilder") []- [|error "Event to data conversion isn't available!"|]- , funD' (mkName "fromDataParser") [] fromDataP ]- where- indexed = map eveArgIndexed args- newVars = replicateM (length args) (newName "t")-- parseArg v = bindS (varP v) [|fromDataParser|]-- parseData [] = []- parseData [v] = pure $ bindS (varP v) [|unSingleton <$> fromDataParser|]- parseData vars = pure $ bindS (tupP (varP <$> vars)) [|fromDataParser|]-- fromDataP = do- vars <- zip indexed <$> newVars- let ixVars = [v | (isIndexed, v) <- vars, isIndexed]- noIxVars = [v | (isIndexed, v) <- vars, not isIndexed]- expVars = [varE v | (_, v) <- vars]- doE $ fmap parseArg ixVars- ++ parseData noIxVars- ++ [noBindS [|return $(appsE (conE eventName : expVars))|]]--funEncodigD :: Name -> Int -> String -> [DecQ]-funEncodigD funName paramLen ident =- [ funDtoDataB- , funD' (mkName "fromDataParser") []- [|error "Function from data conversion isn't available!"|] ]- where- newVars = replicateM paramLen (newName "t")- sVar = mkName "a"- funDtoDataB- | paramLen == 0 = funD' (mkName "toDataBuilder") [conP funName []] [|ident|]- | paramLen == 1 = funD' (mkName "toDataBuilder")- [conP funName [varP sVar]]- [|ident <> toDataBuilder (Singleton $(varE sVar))|]- | otherwise = do- vars <- newVars- funD' (mkName "toDataBuilder")- [conP funName $ fmap varP vars]- [|ident <> toDataBuilder $(tupE $ fmap varE vars)|]--eventFilterD :: String -> Int -> [DecQ]-eventFilterD topic0 n =- let addr = mkName "a"- indexedArgs = replicate n Nothing :: [Maybe String]- in [ funD' (mkName "eventFilter") [wildP, varP addr]- [|Filter (Just $(varE addr))- (Just $ [Just topic0] <> indexedArgs)- Nothing- Nothing- |]- ]--funWrapper :: Bool- -- ^ Is constant?- -> Name- -- ^ Function name- -> Name- -- ^ Function data name- -> [FunctionArg]- -- ^ Parameters- -> Maybe [FunctionArg]- -- ^ Results- -> Q [Dec]-funWrapper c name dname args result = do- a : b : vars <- replicateM (length args + 2) (newName "t")- let params = appsE $ conE dname : fmap varE vars-- sequence $ if c- then- [ sigD name $ [t|Provider $p =>- $(arrowing $ [t|Address|] : inputT ++ [outputT])- |]- , funD' name (varP <$> a : vars) $- case result of- Just [_] -> [|unSingleton <$> call $(varE a) Latest $(params)|]- _ -> [|call $(varE a) Latest $(params)|]- ]-- else- [ sigD name $ [t|(Provider $p, Unit $(varT b)) =>- $(arrowing $ [t|Address|] : varT b : inputT ++ [[t|Web3 $p TxHash|]])- |]- , funD' name (varP <$> a : b : vars) $- [|sendTx $(varE a) $(varE b) $(params)|] ]- where- p = varT (mkName "p")- arrowing [x] = x- arrowing (x : xs) = [t|$x -> $(arrowing xs)|]- inputT = fmap (typeQ . funArgType) args- outputT = case result of- Nothing -> [t|Web3 $p ()|]- Just [x] -> [t|Web3 $p $(typeQ $ funArgType x)|]- Just xs -> let outs = fmap (typeQ . funArgType) xs- in [t|Web3 $p $(foldl appT (tupleT (length xs)) outs)|]---- | Event declarations maker-mkEvent :: Declaration -> Q [Dec]-mkEvent eve@(DEvent name inputs _) = sequence- [ dataD' eventName eventFields derivingD- , instanceD' eventName encodingT (eventEncodigD eventName inputs)- , instanceD' eventName eventT (eventFilterD (T.unpack $ eventId eve) indexedFieldsCount)- ]- where eventName = mkName (toUpperFirst (T.unpack name))- derivingD = [mkName "Show", mkName "Eq", mkName "Ord", ''Generic]- eventFields = normalC eventName (eventBangType <$> inputs)- encodingT = conT (mkName "ABIEncoding")- eventT = conT (mkName "Event")- indexedFieldsCount = length . filter eveArgIndexed $ inputs---- | Method delcarations maker-mkFun :: Declaration -> Q [Dec]-mkFun fun@(DFunction name constant inputs outputs) = (++)- <$> funWrapper constant funName dataName inputs outputs- <*> sequence- [ dataD' dataName (normalC dataName bangInput) derivingD- , instanceD' dataName encodingT- (funEncodigD dataName (length inputs) mIdent)- , instanceD' dataName methodT [] ]- where mIdent = T.unpack (methodId $ fun{funName = T.replace "'" "" name})- dataName = mkName (toUpperFirst (T.unpack $ name <> "Data"))- funName = mkName (toLowerFirst (T.unpack name))- bangInput = fmap funBangType inputs- derivingD = [mkName "Show", mkName "Eq", mkName "Ord", ''Generic]- encodingT = conT (mkName "ABIEncoding")- methodT = conT (mkName "Method")--escape :: [Declaration] -> [Declaration]-escape = concat . escapeNames . groupBy fnEq . sortBy fnCompare- where fnEq (DFunction n1 _ _ _) (DFunction n2 _ _ _) = n1 == n2- fnEq _ _ = False- fnCompare (DFunction n1 _ _ _) (DFunction n2 _ _ _) = compare n1 n2- fnCompare _ _ = GT--escapeNames :: [[Declaration]] -> [[Declaration]]-escapeNames = fmap go- where go (x : xs) = x : zipWith appendToName xs hats- hats = [T.replicate n "'" | n <- [1..]]- appendToName dfn addition = dfn { funName = funName dfn <> addition }---- | Declaration parser-mkDecl :: Declaration -> Q [Dec]-mkDecl x@DFunction{} = mkFun x-mkDecl x@DEvent{} = mkEvent x-mkDecl _ = return []---- | ABI to declarations converter-quoteAbiDec :: String -> Q [Dec]-quoteAbiDec abi_string =- case decode abi_lbs of- Just (ContractABI abi) -> concat <$> mapM mkDecl (escape abi)- _ -> fail "Unable to parse ABI!"- where abi_lbs = LT.encodeUtf8 (LT.pack abi_string)---- | ABI information string-quoteAbiExp :: String -> ExpQ-quoteAbiExp abi_string = stringE $- case eitherDecode abi_lbs of- Left e -> "Error: " ++ show e- Right abi -> show (abi :: ContractABI)- where abi_lbs = LT.encodeUtf8 (LT.pack abi_string)
src/Network/Ethereum/Web3/Types.hs view
@@ -1,7 +1,9 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-}+ -- | -- Module : Network.Ethereum.Web3.Types -- Copyright : Alexander Krupenkin 2016@@ -11,61 +13,104 @@ -- Stability : experimental -- Portability : portable ----- Commonly used types and instances.+-- Ethereum generic JSON-RPC types. --+ module Network.Ethereum.Web3.Types where -import Control.Exception (Exception)-import Control.Monad.IO.Class (MonadIO) import Data.Aeson import Data.Aeson.TH-import Data.Monoid ((<>))-import Data.Text (Text)-import qualified Data.Text.Lazy.Builder as B-import qualified Data.Text.Lazy.Builder.Int as B-import qualified Data.Text.Read as R-import Data.Typeable (Typeable)-import GHC.Generics-import Network.Ethereum.Web3.Address (Address)-import Network.Ethereum.Web3.Internal (toLowerFirst)+import Data.Default+import Data.Monoid ((<>))+import Data.Ord (Down (..))+import Data.String (IsString (..))+import qualified Data.Text as T+import qualified Data.Text.Lazy.Builder as B+import qualified Data.Text.Lazy.Builder.Int as B+import qualified Data.Text.Read as R+import GHC.Generics (Generic) --- | Any communication with Ethereum node wrapped with 'Web3' monad-newtype Web3 a b = Web3 { unWeb3 :: IO b }- deriving (Functor, Applicative, Monad, MonadIO)+import Data.String.Extra (toLowerFirst)+import Network.Ethereum.ABI.Prim.Address (Address)+import Network.Ethereum.ABI.Prim.Bytes (Bytes)+import Network.Ethereum.Unit --- | Some peace of error response-data Web3Error- = JsonRpcFail !RpcError- -- ^ JSON-RPC communication error- | ParserFail !String- -- ^ Error in parser state- | UserFail !String- -- ^ Common head for user errors- deriving (Typeable, Show, Eq, Generic)+-- | Should be viewed as type to representing QUANTITY in Web3 JSON RPC docs+--+-- When encoding QUANTITIES (integers, numbers): encode as hex, prefix with "0x",+-- the most compact representation (slight exception: zero should be represented as "0x0").+-- Examples:+--+-- 0x41 (65 in decimal)+-- 0x400 (1024 in decimal)+-- WRONG: 0x (should always have at least one digit - zero is "0x0")+-- WRONG: 0x0400 (no leading zeroes allowed)+-- WRONG: ff (must be prefixed 0x)+newtype Quantity = Quantity { unQuantity :: Integer }+ deriving (Show, Read, Num, Real, Enum, Eq, Ord, Generic) -instance Exception Web3Error+instance IsString Quantity where+ fromString ('0' : 'x' : hex) =+ case R.hexadecimal (T.pack hex) of+ Right (x, "") -> Quantity x+ _ -> error "Unable to parse Quantity!"+ fromString str =+ case R.decimal (T.pack str) of+ Right (x, "") -> Quantity x+ _ -> error "Unable to parse Quantity!" --- | JSON-RPC error message-data RpcError = RpcError- { errCode :: !Int- , errMessage :: !Text- , errData :: !(Maybe Value)- } deriving (Show, Eq, Generic)+instance ToJSON Quantity where+ toJSON (Quantity x) =+ let hexValue = B.toLazyText (B.hexadecimal x)+ in toJSON ("0x" <> hexValue) -$(deriveJSON (defaultOptions- { fieldLabelModifier = toLowerFirst . drop 3 }) ''RpcError)+instance FromJSON Quantity where+ parseJSON (String v) =+ case R.hexadecimal v of+ Right (x, "") -> return (Quantity x)+ _ -> fail "Unable to parse Quantity"+ parseJSON _ = fail "Quantity may only be parsed from a JSON String" --- | Low-level event filter data structure-data Filter = Filter- { filterAddress :: !(Maybe Address)- , filterTopics :: !(Maybe [Maybe Text])- , filterFromBlock :: !(Maybe Text)- , filterToBlock :: !(Maybe Text)- } deriving (Show, Generic)+instance Fractional Quantity where+ (/) a b = Quantity $ div (unQuantity a) (unQuantity b)+ fromRational = Quantity . floor -$(deriveJSON (defaultOptions- { fieldLabelModifier = toLowerFirst . drop 6 }) ''Filter)+instance Unit Quantity where+ fromWei = Quantity+ toWei = unQuantity +instance UnitSpec Quantity where+ divider = const 1+ name = const "quantity"++newtype BlockNumber = BlockNumber Integer deriving (Eq, Show, Generic, Ord, Read, Num)++instance FromJSON BlockNumber where+ parseJSON (String v) =+ case R.hexadecimal v of+ Right (x, "") -> return (BlockNumber x)+ _ -> fail "Unable to parse BlockNumber!"+ parseJSON _ = fail "The string is required!"++instance ToJSON BlockNumber where+ toJSON (BlockNumber x) =+ let hexValue = B.toLazyText (B.hexadecimal x)+ in toJSON ("0x" <> hexValue)+++data SyncActive = SyncActive { syncStartingBlock :: BlockNumber+ , syncCurrentBlock :: BlockNumber+ , syncHighestBlock :: BlockNumber+ } deriving (Eq, Generic, Show)+$(deriveJSON (defaultOptions { fieldLabelModifier = toLowerFirst . drop 4 }) ''SyncActive)++data SyncingState = Syncing SyncActive | NotSyncing deriving (Eq, Generic, Show)++instance FromJSON SyncingState where+ parseJSON (Bool _) = pure NotSyncing+ parseJSON v = Syncing <$> parseJSON v++ -- | Event filter identifier newtype FilterId = FilterId Integer deriving (Show, Eq, Ord, Generic)@@ -85,14 +130,14 @@ -- | Changes pulled by low-level call 'eth_getFilterChanges', 'eth_getLogs', -- and 'eth_getFilterLogs' data Change = Change- { changeLogIndex :: !Text- , changeTransactionIndex :: !Text- , changeTransactionHash :: !Text- , changeBlockHash :: !Text- , changeBlockNumber :: !Text+ { changeLogIndex :: !Quantity+ , changeTransactionIndex :: !Quantity+ , changeTransactionHash :: !Bytes+ , changeBlockHash :: !Bytes+ , changeBlockNumber :: !BlockNumber , changeAddress :: !Address- , changeData :: !Text- , changeTopics :: ![Text]+ , changeData :: !Bytes+ , changeTopics :: ![Bytes] } deriving (Show, Generic) $(deriveJSON (defaultOptions@@ -101,52 +146,80 @@ -- | The contract call params data Call = Call { callFrom :: !(Maybe Address)- , callTo :: !Address- , callGas :: !(Maybe Text)- , callGasPrice:: !(Maybe Text)- , callValue :: !(Maybe Text)- , callData :: !(Maybe Text)+ , callTo :: !(Maybe Address)+ , callGas :: !(Maybe Quantity)+ , callGasPrice :: !(Maybe Quantity)+ , callValue :: !(Maybe Quantity) -- expressed in wei+ , callData :: !(Maybe Bytes) } deriving (Show, Generic) $(deriveJSON (defaultOptions { fieldLabelModifier = toLowerFirst . drop 4 , omitNothingFields = True }) ''Call) +instance Default Call where+ def = Call Nothing Nothing (Just 3000000) Nothing (Just 0) Nothing++ -- | The contract call mode describe used state: latest or pending-data DefaultBlock = BlockNumberHex Text | Earliest | Latest | Pending+data DefaultBlock = BlockWithNumber BlockNumber | Earliest | Latest | Pending deriving (Show, Eq) instance ToJSON DefaultBlock where- toJSON (BlockNumberHex hex) = toJSON hex+ toJSON (BlockWithNumber bn) = toJSON bn toJSON parameter = toJSON . toLowerFirst . show $ parameter --- TODO: Wrap--- | Transaction hash text string-type TxHash = Text+-- | Low-level event filter data structure+data Filter e = Filter+ { filterAddress :: !(Maybe Address)+ , filterTopics :: !(Maybe [Maybe Bytes])+ , filterFromBlock :: !DefaultBlock+ , filterToBlock :: !DefaultBlock+ } deriving (Show, Generic) ++instance ToJSON (Filter e) where+ toJSON f = object [ "address" .= filterAddress f+ , "topics" .= filterTopics f+ , "fromBlock" .= filterFromBlock f+ , "toBlock" .= filterToBlock f+ ]++instance Ord DefaultBlock where+ compare Pending Pending = EQ+ compare Latest Latest = EQ+ compare Earliest Earliest = EQ+ compare (BlockWithNumber a) (BlockWithNumber b) = compare a b+ compare _ Pending = LT+ compare Pending Latest = GT+ compare _ Latest = LT+ compare Earliest _ = LT+ compare a b = compare (Down b) (Down a)++ -- | Transaction information data Transaction = Transaction- { txHash :: !TxHash+ { txHash :: !Bytes -- ^ DATA, 32 Bytes - hash of the transaction.- , txNonce :: !Text+ , txNonce :: !Quantity -- ^ QUANTITY - the number of transactions made by the sender prior to this one.- , txBlockHash :: !Text+ , txBlockHash :: !Bytes -- ^ DATA, 32 Bytes - hash of the block where this transaction was in. null when its pending.- , txBlockNumber :: !Text+ , txBlockNumber :: !BlockNumber -- ^ QUANTITY - block number where this transaction was in. null when its pending.- , txTransactionIndex :: !Text+ , txTransactionIndex :: !Quantity -- ^ QUANTITY - integer of the transactions index position in the block. null when its pending. , txFrom :: !Address -- ^ DATA, 20 Bytes - address of the sender. , txTo :: !(Maybe Address) -- ^ DATA, 20 Bytes - address of the receiver. null when its a contract creation transaction.- , txValue :: !Text+ , txValue :: !Quantity -- ^ QUANTITY - value transferred in Wei.- , txGasPrice :: !Text+ , txGasPrice :: !Quantity -- ^ QUANTITY - gas price provided by the sender in Wei.- , txGas :: !Text+ , txGas :: !Quantity -- ^ QUANTITY - gas provided by the sender.- , txInput :: !Text+ , txInput :: !Bytes -- ^ DATA - the data send along with the transaction. } deriving (Show, Generic) @@ -155,45 +228,47 @@ -- | Block information data Block = Block- { blockNumber :: !Text+ { blockNumber :: !BlockNumber -- ^ QUANTITY - the block number. null when its pending block.- , blockHash :: !Text+ , blockHash :: !Bytes -- ^ DATA, 32 Bytes - hash of the block. null when its pending block.- , blockParentHash :: !Text+ , blockParentHash :: !Bytes -- ^ DATA, 32 Bytes - hash of the parent block.- , blockNonce :: !(Maybe Text)+ , blockNonce :: !(Maybe Bytes) -- ^ DATA, 8 Bytes - hash of the generated proof-of-work. null when its pending block.- , blockSha3Uncles :: !Text+ , blockSha3Uncles :: !Bytes -- ^ DATA, 32 Bytes - SHA3 of the uncles data in the block.- , blockLogsBloom :: !Text+ , blockLogsBloom :: !Bytes -- ^ DATA, 256 Bytes - the bloom filter for the logs of the block. null when its pending block.- , blockTransactionsRoot :: !Text+ , blockTransactionsRoot :: !Bytes -- ^ DATA, 32 Bytes - the root of the transaction trie of the block.- , blockStateRoot :: !Text+ , blockStateRoot :: !Bytes -- ^ DATA, 32 Bytes - the root of the final state trie of the block.- , blockReceiptRoot :: !(Maybe Text)+ , blockReceiptRoot :: !(Maybe Bytes) -- ^ DATA, 32 Bytes - the root of the receipts trie of the block. , blockMiner :: !Address -- ^ DATA, 20 Bytes - the address of the beneficiary to whom the mining rewards were given.- , blockDifficulty :: !Text+ , blockDifficulty :: !Quantity -- ^ QUANTITY - integer of the difficulty for this block.- , blockTotalDifficulty :: !Text+ , blockTotalDifficulty :: !Quantity -- ^ QUANTITY - integer of the total difficulty of the chain until this block.- , blockExtraData :: !Text+ , blockExtraData :: !Bytes -- ^ DATA - the "extra data" field of this block.- , blockSize :: !Text+ , blockSize :: !Quantity -- ^ QUANTITY - integer the size of this block in bytes.- , blockGasLimit :: !Text+ , blockGasLimit :: !Quantity -- ^ QUANTITY - the maximum gas allowed in this block.- , blockGasUsed :: !Text+ , blockGasUsed :: !Quantity -- ^ QUANTITY - the total used gas by all transactions in this block.- , blockTimestamp :: !Text+ , blockTimestamp :: !Quantity -- ^ QUANTITY - the unix timestamp for when the block was collated. , blockTransactions :: ![Transaction] -- ^ Array of transaction objects.- , blockUncles :: ![Text]+ , blockUncles :: ![Bytes] -- ^ Array - Array of uncle hashes. } deriving (Show, Generic) $(deriveJSON (defaultOptions { fieldLabelModifier = toLowerFirst . drop 5 }) ''Block)++type TxHash = Bytes
src/Network/Ethereum/Web3/Web3.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module : Network.Ethereum.Web3.Web3 -- Copyright : Alexander Krupenkin 2016@@ -9,19 +11,20 @@ -- -- Ethereum node JSON-RPC API methods with `web3_` prefix. --+ module Network.Ethereum.Web3.Web3 where -import Data.Text (Text)-import Network.Ethereum.Web3.JsonRpc-import Network.Ethereum.Web3.Provider-import Network.Ethereum.Web3.Types+import Data.Text (Text)+import Network.Ethereum.ABI.Prim.Bytes (Bytes)+import Network.Ethereum.Web3.Provider (Web3)+import Network.JsonRpc.TinyClient (remote) -- | Returns current node version string.-clientVersion :: Provider a => Web3 a Text+clientVersion :: Web3 Text {-# INLINE clientVersion #-} clientVersion = remote "web3_clientVersion" -- | Returns Keccak-256 (not the standardized SHA3-256) of the given data.-sha3 :: Provider a => Text -> Web3 a Text+sha3 :: Bytes -> Web3 Bytes {-# INLINE sha3 #-} sha3 = remote "web3_sha3"
+ src/Network/JsonRpc/TinyClient.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Network.JsonRpc.TinyClient+-- Copyright : Alexander Krupenkin 2016-2018+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : portable+--+-- Tiny JSON-RPC 2.0 client.+-- Functions for implementing the client side of JSON-RPC 2.0.+-- See <http://www.jsonrpc.org/specification>.+--++module Network.JsonRpc.TinyClient (+ JsonRpcException(..)+ , RpcError(..)+ , MethodName+ , ServerUri+ , Remote+ , remote+ ) where++import Control.Applicative ((<|>))+import Control.Exception (Exception)+import Control.Monad ((<=<))+import Control.Monad.Catch (MonadThrow, throwM)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Reader (MonadReader, ask)+import Data.Aeson+import Data.ByteString.Lazy (ByteString)+import Data.Text (Text, unpack)+import Network.HTTP.Client (Manager, RequestBody (RequestBodyLBS),+ httpLbs, method, parseRequest,+ requestBody, requestHeaders,+ responseBody)++-- | Name of called method.+type MethodName = Text++-- | JSON-RPC server URI+type ServerUri = String++-- | JSON-RPC minimal client config+type Config = (ServerUri, Manager)++-- | JSON-RPC request.+data Request = Request { rqMethod :: !Text+ , rqId :: !Int+ , rqParams :: !Value }++instance ToJSON Request where+ toJSON rq = object [ "jsonrpc" .= String "2.0"+ , "method" .= rqMethod rq+ , "params" .= rqParams rq+ , "id" .= rqId rq ]++-- | JSON-RPC response.+data Response = Response+ { rsResult :: !(Either RpcError Value)+ } deriving (Eq, Show)++instance FromJSON Response where+ parseJSON =+ withObject "JSON-RPC response object" $+ \v -> Response <$>+ (Right <$> v .: "result" <|> Left <$> v .: "error")++-- | JSON-RPC error message+data RpcError = RpcError+ { errCode :: !Int+ , errMessage :: !Text+ , errData :: !(Maybe Value)+ } deriving Eq++instance Show RpcError where+ show (RpcError code msg dat) =+ "JSON-RPC error " ++ show code ++ ": " ++ unpack msg+ ++ ". Data: " ++ show dat++instance FromJSON RpcError where+ parseJSON = withObject "JSON-RPC error object" $+ \v -> RpcError <$> v .: "code"+ <*> v .: "message"+ <*> v .:? "data"++-- | Typeclass for JSON-RPC monad base.+--+-- If you have monad with 'MonadIO', 'MonadThrow' and 'MonadReader' instances,+-- it can be used as base for JSON-RPC calls.+--+-- Example:+--+-- @+-- newtype MyMonad a = ...+--+-- instance Remote MyMonad (Mymonad a)+--+-- foo :: Int -> Bool -> Mymonad Text+-- foo = remote "foo"+-- @+--+class (MonadIO m, MonadThrow m, MonadReader Config m) => Remote m a | a -> m where+ remote_ :: ([Value] -> m ByteString) -> a++ default remote_ :: (FromJSON b, m b ~ a) => ([Value] -> m ByteString) -> a+ remote_ f = decodeResponse =<< f []++instance (ToJSON a, Remote m b) => Remote m (a -> b) where+ remote_ f x = remote_ (\xs -> f (toJSON x : xs))++-- | Remote call of JSON-RPC method.+--+-- Arguments of function are stored into @params@ request array.+--+-- Example:+--+-- @+-- myMethod :: Int -> Bool -> m String+-- myMethod = remote "myMethod"+-- @+--+remote :: Remote m a => MethodName -> a+{-# INLINE remote #-}+remote = remote_ . call++call :: (MonadIO m,+ MonadThrow m,+ MonadReader Config m)+ => MethodName+ -> [Value]+ -> m ByteString+call n = connection . encode . Request n 1 . toJSON+ where+ connection body = do+ (uri, manager) <- ask+ request <- parseRequest uri+ let request' = request+ { requestBody = RequestBodyLBS body+ , requestHeaders = [("Content-Type", "application/json")]+ , method = "POST" }+ responseBody <$> liftIO (httpLbs request' manager)++data JsonRpcException+ = ParsingException String+ | CallException RpcError+ deriving (Eq, Show)++instance Exception JsonRpcException++decodeResponse :: (MonadThrow m, FromJSON a)+ => ByteString+ -> m a+decodeResponse = (tryParse . eitherDecode . encode)+ <=< tryResult . rsResult+ <=< tryParse . eitherDecode+ where+ tryParse = either (throwM . ParsingException) return+ tryResult = either (throwM . CallException) return
+ test-support/contracts/Migrations.sol view
@@ -0,0 +1,23 @@+pragma solidity ^0.4.4;++contract Migrations {+ address public owner;+ uint public last_completed_migration;++ modifier restricted() {+ if (msg.sender == owner) _;+ }++ function Migrations() {+ owner = msg.sender;+ }++ function setCompleted(uint completed) restricted {+ last_completed_migration = completed;+ }++ function upgrade(address new_address) restricted {+ Migrations upgraded = Migrations(new_address);+ upgraded.setCompleted(last_completed_migration);+ }+}
+ test-support/contracts/SimpleStorage.sol view
@@ -0,0 +1,12 @@+pragma solidity ^0.4.15;++contract SimpleStorage {+ uint public count;+ + event CountSet(uint _count);+ + function setCount(uint _count) {+ count = _count;+ CountSet(_count);+ }+}
+ test-support/convertAbi.sh view
@@ -0,0 +1,9 @@+#!/bin/sh+set -xe+cd build/contracts/+mkdir -p abis++for file in *.json; do+ jq '.abi' < "$file" > "abis/$file"+done+
+ test-support/inject-contract-addresses.sh view
@@ -0,0 +1,26 @@+#!/bin/bash++set -ex++EXPORT_STORE=${EXPORT_STORE:-/tmp/detected-vars}+SEARCH_DIRECTORY=${SEARCH_DIRECTORY:-build/contracts}++function detect_contract_addresses() {+ for file in $(find $SEARCH_DIRECTORY -maxdepth 1 -type f -name '*.json'); do+ maybe_address=`jq -r .networks[].address $file | grep -v null | head -n 1`+ if [ -z "${maybe_address}" ]; then+ echo no addresses detected for $file >&2+ else+ contract_name=`echo $file | sed -e 's@'$SEARCH_DIRECTORY'/@@g' -e 's/.json//g'`+ echo found address "${maybe_address}" for "${contract_name}" >&2+ contract_var=`echo $contract_name | tr '[:lower:]' '[:upper:]'`_CONTRACT_ADDRESS+ echo the var for "${contract_name}" is "${contract_var}" >&2+ echo "export ${contract_var}=`echo ${maybe_address} | sed s/0x//`"+ fi+ done+}++detect_contract_addresses 2>/dev/null >$EXPORT_STORE++source $EXPORT_STORE+$@
+ test-support/migrations/1_initial_migration.js view
@@ -0,0 +1,5 @@+var Migrations = artifacts.require("./Migrations.sol");++module.exports = function(deployer) {+ deployer.deploy(Migrations);+};
+ test-support/migrations/2_deploy_contracts.js view
@@ -0,0 +1,7 @@+var SimpleStorage = artifacts.require("./SimpleStorage.sol");+var ComplexStorage = artifacts.require("./ComplexStorage.sol");++module.exports = function(deployer) {+ deployer.deploy(SimpleStorage);+ deployer.deploy(ComplexStorage);+};
+ test-support/truffle.js view
@@ -0,0 +1,9 @@+module.exports = {+ networks: {+ development: {+ host: "localhost",+ port: 8545,+ network_id: "*" // Match any network id+ }+ }+};
+ test/Network/Ethereum/Web3/Test/ComplexStorageSpec.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}++-- |+-- Module : Network.Ethereum.Web3.Test.ComplexStorage+-- Copyright : Alexander Krupenkin 2016+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- ComplexStorage is a Solidity contract which has global variables of+-- several different types. The point of this test is to test the encoding+-- of a complicated Solidity tuple, consisting of dynamically and statically+-- sized components.+--++module Network.Ethereum.Web3.Test.ComplexStorageSpec where++import Control.Concurrent.Async (wait)+import Control.Concurrent.MVar+import Control.Monad.IO.Class (liftIO)+import Data.ByteArray (convert)+import Data.ByteString (ByteString)+import Data.Default+import Data.Either (isRight)+import Data.Maybe+import Data.String (fromString)+import Network.Ethereum.Contract.TH+import Network.Ethereum.Web3 hiding (convert)+import qualified Network.Ethereum.Web3.Eth as Eth+import Network.Ethereum.Web3.Test.Utils+import Network.Ethereum.Web3.Types (Call (..), Filter (..))+import System.Environment (getEnv)+import System.IO.Unsafe (unsafePerformIO)+import Test.Hspec++[abiFrom|test-support/build/contracts/abis/ComplexStorage.json|]++spec :: Spec+spec = describe "Complex Storage" $ do+ it "should inject contract addresses" injectExportedEnvironmentVariables+ withPrimaryEthereumAccount `before` complexStorageSpec++complexStorageSpec :: SpecWith Address+complexStorageSpec = do+ describe "can interact with a ComplexStorage contract" $ do+ -- todo: these should ideally be arbitrary!+ let sUint = 1+ sInt = -1+ sBool = True+ sInt224 = 221+ sBools = [True, False]+ sInts = [1, 1, -3]+ sString = "hello"+ sBytes16 = "\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78"+ sByte2sElem = "\x12\x34"+ sByte2sVec = [sByte2sElem, sByte2sElem, sByte2sElem, sByte2sElem]+ sByte2s = [sByte2sVec, sByte2sVec]++ it "can set the values of a ComplexStorage and validate them with an event" $ \primaryAccount -> do+ contractAddress <- Prelude.fmap fromString . liftIO $ getEnv "COMPLEXSTORAGE_CONTRACT_ADDRESS"+ let theCall = callFromTo primaryAccount contractAddress+ fltr = (def :: Filter ValsSet) { filterAddress = Just contractAddress }+ -- kick off listening for the ValsSet event+ vals <- newEmptyMVar+ fiber <- runWeb3Configured' $+ event fltr $ \vs -> do+ liftIO $ putMVar vals vs+ pure TerminateEvent+ -- kick off tx+ ret <- runWeb3Configured $ setValues theCall+ sUint+ sInt+ sBool+ sInt224+ sBools+ sInts+ sString+ sBytes16+ sByte2s+ -- wait for its ValsSet event+ wait fiber+ (ValsSet vsA vsB vsC vsD vsE vsF vsG vsH vsI) <- takeMVar vals+ vsA `shouldBe` sUint+ vsB `shouldBe` sInt+ vsC `shouldBe` sBool+ vsD `shouldBe` sInt224+ vsE `shouldBe` sBools+ vsF `shouldBe` sInts+ vsG `shouldBe` sString+ vsH `shouldBe` sBytes16+ vsI `shouldBe` sByte2s++ it "can verify that it set the values correctly" $ \primaryAccount -> do+ contractAddress <- Prelude.fmap fromString . liftIO $ getEnv "COMPLEXSTORAGE_CONTRACT_ADDRESS"+ let theCall = callFromTo primaryAccount contractAddress+ runGetterCall f = runWeb3Configured (f theCall)+ -- there really has to be a better way to do this+ uintVal' <- runGetterCall uintVal+ intVal' <- runGetterCall intVal+ boolVal' <- runGetterCall boolVal+ int224Val' <- runGetterCall int224Val+ boolsVal <- runGetterCall $ \c -> boolVectorVal c 0+ intsVal <- runGetterCall $ \c -> intListVal c 0+ stringVal' <- runGetterCall stringVal+ bytes16Val' <- runGetterCall bytes16Val+ bytes2s <- runGetterCall $ \c -> bytes2VectorListVal c 0 0+ uintVal' `shouldBe` sUint+ intVal' `shouldBe` sInt+ boolVal' `shouldBe` sBool+ int224Val' `shouldBe` sInt224+ boolsVal `shouldBe` True+ intsVal `shouldBe` sInts Prelude.!! 0+ stringVal' `shouldBe` sString+ bytes16Val' `shouldBe` sBytes16+ bytes2s `shouldBe` sByte2sElem++ it "can decode a complicated value correctly" $ \primaryAccount -> do+ contractAddress <- Prelude.fmap fromString . liftIO $ getEnv "COMPLEXSTORAGE_CONTRACT_ADDRESS"+ let theCall = callFromTo primaryAccount contractAddress+ runGetterCall f = runWeb3Configured (f theCall)+ allVals <- runGetterCall getVals+ allVals `shouldBe` (sUint, sInt, sBool, sInt224, sBools, sInts, sString, sBytes16, sByte2s)
+ test/Network/Ethereum/Web3/Test/SimpleStorageSpec.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}++-- Module : Network.Ethereum.Web3.Test.SimpleStorage+-- Copyright : Alexander Krupenkin 2016+-- License : BSD3+--+-- Maintainer : mail@akru.me+-- Stability : experimental+-- Portability : unportable+--+-- SimpleStorage is a Solidity contract which stores a uint256.+-- The point of this test is to test function calls to update and+-- read the value, as well as an event monitor.++module Network.Ethereum.Web3.Test.SimpleStorageSpec where++import Control.Concurrent (threadDelay)+import Control.Concurrent.Async (wait)+import Control.Concurrent.MVar+import Control.Monad (void)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ask)+import Data.ByteString (ByteString)+import Data.Default+import Data.Either (isRight)+import Data.Foldable (forM_)+import Data.List (sort)+import Data.Maybe+import Data.Monoid+import Data.String (fromString)+import qualified Data.Text as T+import Data.Traversable (for)+import GHC.TypeLits++import Network.Ethereum.Contract.TH+import Network.Ethereum.Web3 hiding (convert)+import qualified Network.Ethereum.Web3.Eth as Eth+import Network.Ethereum.Web3.Provider (forkWeb3)+import Network.Ethereum.Web3.Types++import Numeric (showHex)+import System.Environment (getEnv)+import System.IO.Unsafe (unsafePerformIO)+import Test.Hspec++import Network.Ethereum.Web3.Test.Utils++[abiFrom|test-support/build/contracts/abis/SimpleStorage.json|]++unCountSet :: CountSet -> UIntN 256+unCountSet (CountSet n) = n++contractAddress :: Address+contractAddress = fromString . unsafePerformIO $ getEnv "SIMPLESTORAGE_CONTRACT_ADDRESS"++spec :: Spec+spec = describe "Simple Storage" $ do+ it "should inject contract addresses" injectExportedEnvironmentVariables+ withPrimaryEthereumAccount `before` interactions+ withPrimaryEthereumAccount `before` events++interactions :: SpecWith Address+interactions = describe "can interact with a SimpleStorage contract" $ do+ -- todo: this should ideally be arbitrary!+ let theValue = 12345+ it "can set the value of a SimpleStorage contract" $ \primaryAccount -> do+ let theCall = callFromTo primaryAccount contractAddress+ ret <- runWeb3Configured $ setCount theCall theValue+ True `shouldBe` True -- we need to get this far++ it "can read the value back" $ \primaryAccount -> do+ let theCall = callFromTo primaryAccount contractAddress+ now <- runWeb3Configured Eth.blockNumber+ let later = now + 3+ awaitBlock later+ v <- runWeb3Configured (count theCall)+ v `shouldBe` theValue++events :: SpecWith Address+events = describe "can interact with a SimpleStorage contract across block intervals" $ do+ it "can stream events starting and ending in the future, unbounded" $ \primaryAccount -> do+ var <- newMVar []+ let theCall = callFromTo primaryAccount contractAddress+ theSets = [1, 2, 3]+ print "Setting up the filter..."+ fiber <- runWeb3Configured' $ do+ let fltr = (def :: Filter CountSet) { filterAddress = Just contractAddress }+ forkWeb3 $ processUntil' var fltr ((3 ==) . length)+ print "Setting the values..."+ setValues theCall theSets+ wait fiber+ print "Filter caught 3 values..."+ vals <- takeMVar var+ sort (unCountSet <$> vals) `shouldBe` sort theSets++ it "can stream events starting and ending in the future, bounded" $ \primaryAccount -> do+ runWeb3Configured Eth.blockNumber >>= \bn -> awaitBlock (bn + 1)+ var <- newMVar []+ let theCall = callFromTo primaryAccount contractAddress+ theSets = [13, 14, 15]+ start <- runWeb3Configured Eth.blockNumber+ let later = BlockWithNumber (start + 3)+ latest = BlockWithNumber (start + 8)+ fltr = (def :: Filter CountSet) { filterAddress = Just contractAddress+ , filterFromBlock = later+ , filterToBlock = latest }+ print "Setting up the filter..."+ fiber <- runWeb3Configured' $+ forkWeb3 $ processUntil' var fltr ((3 ==) . length)+ awaitBlock (start + 3)+ print "Setting the values..."+ setValues theCall theSets+ wait fiber+ print "Filter caught 3 values..."+ vals <- takeMVar var+ sort (unCountSet <$> vals) `shouldBe` sort theSets++ it "can stream events starting in the past and ending in the future" $ \primaryAccount -> do+ runWeb3Configured Eth.blockNumber >>= \bn -> awaitBlock (bn + 1)+ var <- newMVar []+ blockNumberVar <- newEmptyMVar+ let theCall = callFromTo primaryAccount contractAddress+ theSets1 = [7, 8, 9]+ theSets2 = [10, 11, 12]+ start <- runWeb3Configured Eth.blockNumber+ let fltr = (def :: Filter CountSet) { filterAddress = Just contractAddress }+ fiber <- runWeb3Configured' $ do+ forkWeb3 $ processUntil var fltr ((3 ==) . length) (liftIO . putMVar blockNumberVar . changeBlockNumber)+ print "Running first transactions as past transactions..."+ setValues theCall theSets1+ wait fiber+ print "All past transactions succeeded... "+ end <- takeMVar blockNumberVar+ awaitBlock $ end + 1 -- make past transactions definitively in past+ var' <- newMVar []+ fiber <- runWeb3Configured' $ do+ let fltr = (def :: Filter CountSet) { filterAddress = Just contractAddress+ , filterFromBlock = BlockWithNumber start}+ forkWeb3 $ processUntil' var' fltr ((6 ==) . length)+ print "Setting more values"+ setValues theCall theSets2+ wait fiber+ print "All new values have ben set"+ vals <- takeMVar var'+ sort (unCountSet <$> vals) `shouldBe` sort (theSets1 <> theSets2)++ it "can stream events starting and ending in the past, bounded" $ \primaryAccount -> do+ runWeb3Configured Eth.blockNumber >>= \bn -> awaitBlock (bn + 1)+ var <- newMVar []+ let theCall = callFromTo primaryAccount contractAddress+ theSets = [4, 5, 6]+ start <- runWeb3Configured Eth.blockNumber+ blockNumberVar <- newEmptyMVar+ let fltr = (def :: Filter CountSet) { filterAddress = Just contractAddress }+ print "Setting up filter for past transactions..."+ fiber <- runWeb3Configured' $ do+ forkWeb3 $ processUntil var fltr ((3 ==) . length) (liftIO . putMVar blockNumberVar . changeBlockNumber)+ print "Setting values"+ setValues theCall theSets+ wait fiber+ print "All values have been set"+ end <- takeMVar blockNumberVar+ var' <- newMVar []+ let fltr' = fltr { filterFromBlock = BlockWithNumber start+ , filterToBlock = BlockWithNumber end+ }+ awaitBlock $ end + 1 -- make it definitively in the past+ runWeb3Configured $ processUntil' var' fltr' ((3 ==) . length)+ vals <- takeMVar var'+ sort (unCountSet <$> vals) `shouldBe` sort theSets++processUntil :: MVar [CountSet]+ -> Filter CountSet+ -> ([CountSet] -> Bool) -- TODO: make it work for any event+ -> (Change -> Web3 ())+ -> Web3 ()+processUntil var filter predicate action = do+ event' filter $ \(ev :: CountSet) -> do+ newV <- liftIO $ modifyMVar var $ \v -> return (ev:v, ev:v)+ if predicate newV+ then do+ change <- ask+ lift $ action change+ return TerminateEvent+ else return ContinueEvent++processUntil' :: MVar [CountSet]+ -> Filter CountSet+ -> ([CountSet] -> Bool)+ -> Web3 ()+processUntil' var filter predicate = processUntil var filter predicate (const $ return ())++setValues :: Call -> [UIntN 256] -> IO ()+setValues theCall theSets = forM_ theSets $ \v -> do+ runWeb3Configured (setCount theCall v)+ threadDelay 1000000
+ test/Network/Ethereum/Web3/Test/Utils.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE LambdaCase #-}+module Network.Ethereum.Web3.Test.Utils+ ( injectExportedEnvironmentVariables+ , runWeb3Configured+ , runWeb3Configured'+ , withAccounts+ , withPrimaryEthereumAccount+ , callFromTo+ , sleepSeconds+ , microtime+ , awaitBlock+ ) where++import Control.Concurrent (MVar, threadDelay,+ tryTakeMVar)+import Control.Monad.IO.Class (liftIO)+import Data.Default+import Data.Either (isRight)+import Data.List.Split (splitOn)+import Data.Maybe (fromMaybe)+import Data.Ratio (numerator)+import Data.String (IsString, fromString)+import qualified Data.Text as T+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Traversable (for)++import Network.Ethereum.ABI.Prim.Address (Address)+import Network.Ethereum.Web3.Eth (accounts, blockNumber)+import Network.Ethereum.Web3.Provider (Provider (..), Web3,+ Web3Error, runWeb3')+import Network.Ethereum.Web3.Types (BlockNumber, Call (..))+import System.Environment (lookupEnv, setEnv)+import Test.Hspec.Expectations (shouldSatisfy)++rpcUri :: IO String+rpcUri = liftIO (fromMaybe "http://localhost:8545" <$> lookupEnv "WEB3_PROVIDER")++exportStore :: String+exportStore = "./test-support/.detected-contract-addresses"++loadExportedEnvironmentVariables :: IO [(String, String)]+loadExportedEnvironmentVariables = do+ exportables <- lines <$> readFile exportStore+ detecteds <- for exportables $ \line -> case words line of+ ["export", e] -> case splitOn "=" e of+ [x] -> detectedEnv x ""+ [k, v] -> detectedEnv k v+ _ -> warnMalformation $ "oddly structured export statement " ++ line+ _ -> warnMalformation $ "no export in line " ++ line+ return $ concat detecteds++ where warnMalformation m = do+ putStrLn $ m ++ ". are you sure you're using the right inject-contract-addresses.sh?"+ pure []+ detectedEnv k v = do+ -- putStrLn $ "detected " ++ k ++ "=" ++ v+ pure [(k, v)]++injectExportedEnvironmentVariables :: IO ()+injectExportedEnvironmentVariables = do+ detectedEnvs <- loadExportedEnvironmentVariables+ sequence_ (uncurry setEnv <$> detectedEnvs)++runWeb3Configured :: Show a => Web3 a -> IO a+runWeb3Configured f = do+ provider <- HttpProvider <$> rpcUri+ v <- runWeb3' provider f+ v `shouldSatisfy` isRight+ let Right a = v in return a++runWeb3Configured' :: Web3 a -> IO a+runWeb3Configured' f = do+ provider <- HttpProvider <$> rpcUri+ Right v <- runWeb3' provider f+ return v++withAccounts :: ([Address] -> IO a) -> IO a+withAccounts f = runWeb3Configured accounts >>= f++withPrimaryEthereumAccount :: IO Address+withPrimaryEthereumAccount = withAccounts (pure . head)++callFromTo :: Address -> Address -> Call+callFromTo from to =+ def { callFrom = Just from+ , callTo = Just to+ , callGasPrice = Just 4000000000+ }++sleepSeconds :: Int -> IO ()+sleepSeconds = threadDelay . (* 1000000)++microtime :: IO Integer+microtime = numerator . toRational . (* 1000000) <$> getPOSIXTime++awaitBlock :: BlockNumber -> IO ()+awaitBlock bn = do+ bn' <- runWeb3Configured blockNumber+ putStrLn $ "awaiting block " ++ show bn ++ ", currently " ++ show bn'+ if bn' >= bn+ then return ()+ else threadDelay 1000000 >> awaitBlock bn
test/Spec.hs view
@@ -1,27 +1,1 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-module Main where--import Data.Text (unpack)-import Network.Ethereum.Web3-import Network.Ethereum.Web3.TH-import Text.Printf--[abiFrom|data/ERC20.json|]--main :: IO ()-main = do- putStrLn ""- putStrLn [abiFrom|data/ERC20.json|]-{-- Right s <- runWeb3 $ do- n <- name token- s <- symbol token- d <- decimals token- return $ printf "Token %s with symbol %s and decimals %d"- (unpack n) (unpack s) d- putStrLn s- where token = "0x237D60A8b41aFD2a335305ed458B609D7667D789"--}+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ unit/Network/Ethereum/Web3/Test/EncodingSpec.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}++module Network.Ethereum.Web3.Test.EncodingSpec where++import Data.Monoid ((<>))+import Data.Text (Text)+import Generics.SOP (Generic, Rep)+import Test.Hspec++import Network.Ethereum.ABI.Class (ABIGet, ABIPut,+ GenericABIGet, GenericABIPut)+import Network.Ethereum.ABI.Codec (decode, decode', encode,+ encode')+import Network.Ethereum.ABI.Prim.Bool ()+import Network.Ethereum.ABI.Prim.Bytes (Bytes, BytesN)+import Network.Ethereum.ABI.Prim.Int (IntN, UIntN)+import Network.Ethereum.ABI.Prim.List (ListN)+import Network.Ethereum.ABI.Prim.String ()+import Network.Ethereum.ABI.Prim.Tuple (Singleton (..))++spec :: Spec+spec = do+ intNTest+ bytesTest+ bytesNTest+ vectorTest+ dynamicArraysTest+ tuplesTest++intNTest :: Spec+intNTest =+ describe "uint tests" $ do++ it "can encode int16" $+ let decoded = (-1) :: IntN 16+ encoded = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ in roundTrip decoded encoded++ it "can encode larger uint256" $+ let decoded = (2 ^ 255) - 1 :: UIntN 256+ encoded = "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ in roundTrip decoded encoded++bytesTest :: Spec+bytesTest = do+ describe "bytes tests" $ do++ it "can encode short bytes" $ do+ let decoded :: Bytes+ decoded = "0xc3a40000c3a4"+ encoded = "0x0000000000000000000000000000000000000000000000000000000000000006"+ <> "0xc3a40000c3a40000000000000000000000000000000000000000000000000000"+ in roundTrip decoded encoded++ it "can encode long bytes" $ do+ let decoded :: Bytes+ decoded = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1"+ encoded = "0x000000000000000000000000000000000000000000000000000000000000009f"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff100"+ in roundTrip decoded encoded++bytesNTest :: Spec+bytesNTest =+ describe "sized bytes tests" $ do++ it "can encode Bytes1" $ do+ let decoded = "0xcf" :: BytesN 1+ encoded = "0xcf00000000000000000000000000000000000000000000000000000000000000"+ roundTrip decoded encoded++ it "can encode Bytes12" $ do+ let decoded = "0x6761766f66796f726b000000" :: BytesN 12+ encoded = "0x6761766f66796f726b0000000000000000000000000000000000000000000000"+ roundTrip decoded encoded++vectorTest :: Spec+vectorTest =+ describe "statically sized array tests" $ do++ it "can encode statically sized vectors of addresses" $ do+ let decoded = [False, True] :: ListN 2 Bool+ encoded = "0x0000000000000000000000000000000000000000000000000000000000000000"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ in roundTrip decoded encoded++ it "can encode statically sized vectors of statically sized bytes"$ do+ let elem1 = "0xcf"+ elem2 = "0x68"+ elem3 = "0x4d"+ elem4 = "0xfb"+ decoded = [elem1, elem2, elem3, elem4] :: ListN 4 (BytesN 1)+ encoded = "0xcf00000000000000000000000000000000000000000000000000000000000000"+ <> "0x6800000000000000000000000000000000000000000000000000000000000000"+ <> "0x4d00000000000000000000000000000000000000000000000000000000000000"+ <> "0xfb00000000000000000000000000000000000000000000000000000000000000"+ in roundTrip decoded encoded++dynamicArraysTest :: Spec+dynamicArraysTest = do+ describe "dynamically sized array tests" $ do++ it "can encode dynamically sized lists of bools" $ do+ let decoded = [True, True, False] :: [Bool]+ encoded = "0x0000000000000000000000000000000000000000000000000000000000000003"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000000"+ in roundTrip decoded encoded++tuplesTest :: Spec+tuplesTest =+ describe "tuples test" $ do++ it "can encode 2-tuples with both static args" $ do+ let decoded = (True, False)+ encoded = "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000000"+ in roundTripGeneric decoded encoded++ it "can encode 1-tuples with dynamic arg" $ do+ let decoded = Singleton ([True, False] :: [Bool])+ encoded = "0x0000000000000000000000000000000000000000000000000000000000000020"+ <> "0x0000000000000000000000000000000000000000000000000000000000000002"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000000"+ in roundTripGeneric decoded encoded++ it "can encode 4-tuples with a mix of args - (UInt32, String, Boolean, Array Int256)" $ do+ let decoded = (1 :: IntN 32, "dave" :: Text, True, [1, 2, 3] :: [IntN 256])+ encoded = "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000080"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x00000000000000000000000000000000000000000000000000000000000000c0"+ <> "0x0000000000000000000000000000000000000000000000000000000000000004"+ <> "0x6461766500000000000000000000000000000000000000000000000000000000"+ <> "0x0000000000000000000000000000000000000000000000000000000000000003"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000002"+ <> "0x0000000000000000000000000000000000000000000000000000000000000003"+ in roundTripGeneric decoded encoded++ it "can do something really complicated" $ do+ let uint = 1 :: UIntN 256+ int = (-1) :: IntN 256+ bool = True+ int224 = 221 :: IntN 224+ bools = [True, False] :: ListN 2 Bool+ ints = [1, (-1), 3] :: [IntN 32]+ string = "hello" :: Text+ bytes16 = "0x12345678123456781234567812345678" :: BytesN 16+ elem = "0x1234" :: BytesN 2+ bytes2s = [ [elem, elem, elem, elem]+ , [elem, elem, elem, elem]+ ] :: [ListN 4 (BytesN 2)]++ decoded = (uint, int, bool, int224, bools, ints, string, bytes16, bytes2s)++ encoded = "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x00000000000000000000000000000000000000000000000000000000000000dd"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0x0000000000000000000000000000000000000000000000000000000000000000"+ <> "0x0000000000000000000000000000000000000000000000000000000000000140"+ <> "0x00000000000000000000000000000000000000000000000000000000000001c0"+ <> "0x1234567812345678123456781234567800000000000000000000000000000000"+ <> "0x0000000000000000000000000000000000000000000000000000000000000200"+ <> "0x0000000000000000000000000000000000000000000000000000000000000003"+ <> "0x0000000000000000000000000000000000000000000000000000000000000001"+ <> "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"+ <> "0x0000000000000000000000000000000000000000000000000000000000000003"+ <> "0x0000000000000000000000000000000000000000000000000000000000000005"+ <> "0x68656c6c6f000000000000000000000000000000000000000000000000000000"+ <> "0x0000000000000000000000000000000000000000000000000000000000000002"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"+ <> "0x1234000000000000000000000000000000000000000000000000000000000000"++ in roundTripGeneric decoded encoded++-- | Run encoded/decoded comaration+roundTrip :: ( Show a+ , Eq a+ , ABIPut a+ , ABIGet a+ )+ => a+ -> Bytes+ -> IO ()+roundTrip decoded encoded = do+ encoded `shouldBe` encode decoded+ decode encoded `shouldBe` Right decoded++-- | Run generic encoded/decoded comaration+roundTripGeneric :: ( Show a+ , Eq a+ , Generic a+ , Rep a ~ rep+ , GenericABIPut rep+ , GenericABIGet rep+ )+ => a+ -> Bytes+ -> IO ()+roundTripGeneric decoded encoded = do+ encoded `shouldBe` encode' decoded+ decode' encoded `shouldBe` Right decoded
+ unit/Network/Ethereum/Web3/Test/EventSpec.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Ethereum.Web3.Test.EventSpec where++import Data.Tagged (Tagged)+import Generics.SOP (Generic)+import qualified GHC.Generics as GHC (Generic)+import Test.Hspec (Spec, describe, it,+ shouldBe)++import Network.Ethereum.ABI.Class (ABIGet)+import Network.Ethereum.ABI.Event (IndexedEvent (..),+ decodeEvent)+import Network.Ethereum.ABI.Prim.Address (Address)+import Network.Ethereum.ABI.Prim.Bytes ()+import Network.Ethereum.ABI.Prim.Int (UIntN)+import Network.Ethereum.ABI.Prim.Tagged ()+import Network.Ethereum.Web3.Types (Change (..))+++spec :: Spec+spec = eventTest++eventTest :: Spec+eventTest =+ describe "event tests" $ do++ it "can decode simple storage" $+ let change = Change { changeLogIndex = "0x2"+ , changeTransactionIndex = "0x2"+ , changeTransactionHash = "0xe8cac6af0ceb3cecbcb2a5639361fc9811b1aa753672cf7c7e8b528df53e0e94"+ , changeBlockHash = "0x0c7e1701858232ac210e3bcc8ab3b33cc6b08025692b22abb39059dc41f6a76e"+ , changeBlockNumber = 0+ , changeAddress = "0x617e5941507aab5d2d8bcb56cb8c6ce2eeb16b21"+ , changeData = "0x000000000000000000000000000000000000000000000000000000000000000a"+ , changeTopics = ["0xa32bc18230dd172221ac5c4821a5f1f1a831f27b1396d244cdd891c58f132435"]+ }+ in decodeEvent change `shouldBe` Right (NewCount 10)++ it "can decode erc20" $+ let ercchange = Change { changeLogIndex = "0x2"+ , changeTransactionIndex = "0x2"+ , changeTransactionHash = "0xe8cac6af0ceb3cecbcb2a5639361fc9811b1aa753672cf7c7e8b528df53e0e94"+ , changeBlockHash = "0x0c7e1701858232ac210e3bcc8ab3b33cc6b08025692b22abb39059dc41f6a76e"+ , changeBlockNumber = 0+ , changeAddress = "0x617e5941507aab5d2d8bcb56cb8c6ce2eeb16b21"+ , changeData = "0x000000000000000000000000000000000000000000000000000000000000000a"+ , changeTopics = [ "0xb32bc18230dd172221ac5c4821a5f1f1a831f27b1396d244cdd891c58f132435"+ , "0x0000000000000000000000000000000000000000000000000000000000000001"+ , "0x0000000000000000000000000000000000000000000000000000000000000002"+ ]+ }+ in decodeEvent ercchange `shouldBe` Right (Transfer "0x0000000000000000000000000000000000000001" 10 "0x0000000000000000000000000000000000000002")++-- SimpleStorage Event Types++data NewCount = NewCount (UIntN 256) deriving (Eq, Show, GHC.Generic)+instance Generic NewCount++data NewCountIndexed = NewCountIndexed deriving (Eq, Show, GHC.Generic)+instance Generic NewCountIndexed++data NewCountNonIndexed = NewCountNonIndexed (Tagged 1 (UIntN 256)) deriving (Eq, Show, GHC.Generic)+instance Generic NewCountNonIndexed++instance IndexedEvent NewCountIndexed NewCountNonIndexed NewCount where+ isAnonymous = const False++-- WeirdERC20 Event Types (Transfer type wrong order for testing purposes)+data Transfer = Transfer Address (UIntN 256) Address deriving (Eq, Show, GHC.Generic)+instance Generic Transfer++data TransferIndexed = TransferIndexed (Tagged 1 Address) (Tagged 3 Address) deriving (Eq, Show, GHC.Generic)+instance Generic TransferIndexed++data TransferNonIndexed = TransferNonIndexed (Tagged 2 (UIntN 256)) deriving (Eq, Show, GHC.Generic)+instance Generic TransferNonIndexed++instance IndexedEvent TransferIndexed TransferNonIndexed Transfer where+ isAnonymous = const False
+ unit/Network/Ethereum/Web3/Test/MethodDumpSpec.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE QuasiQuotes #-}++module Network.Ethereum.Web3.Test.MethodDumpSpec where++import Network.Ethereum.Contract.TH+import Test.Hspec+import Text.Printf+++spec :: Spec+spec = describe "methodDump" $+ it "can dump an ABI" $ do+ let theApiDump = [abiFrom|examples/ERC20.json|]+ in theApiDump `shouldNotBe` ""
+ unit/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
web3.cabal view
@@ -1,5 +1,5 @@ name: web3-version: 0.6.0.0+version: 0.7.0.0 cabal-version: >=1.10 build-type: Simple license: BSD3@@ -14,7 +14,17 @@ author: Alexander Krupenkin extra-source-files: README.md- data/ERC20.json+ CHANGELOG.md+ test-support/contracts/Migrations.sol+ test-support/contracts/SimpleStorage.sol+ test-support/truffle.js+ test-support/migrations/1_initial_migration.js+ test-support/migrations/2_deploy_contracts.js+ test-support/convertAbi.sh+ test-support/inject-contract-addresses.sh+ examples/ERC20.hs+ examples/ERC20.json+ examples/TokenInfo.hs source-repository head type: git@@ -23,51 +33,112 @@ library exposed-modules: Network.Ethereum.Web3- Network.Ethereum.Unit- Network.Ethereum.Web3.TH- Network.Ethereum.Web3.Web3 Network.Ethereum.Web3.Eth Network.Ethereum.Web3.Net+ Network.Ethereum.Web3.Web3 Network.Ethereum.Web3.Types- Network.Ethereum.Web3.Address- Network.Ethereum.Web3.JsonAbi Network.Ethereum.Web3.Provider- Network.Ethereum.Web3.Encoding- Network.Ethereum.Web3.Contract- Network.Ethereum.Web3.Encoding.Bytes- Network.Ethereum.Web3.Encoding.Tuple+ Network.Ethereum.Unit+ Network.Ethereum.ABI.Json+ Network.Ethereum.ABI.Class+ Network.Ethereum.ABI.Codec+ Network.Ethereum.ABI.Event+ Network.Ethereum.ABI.Event.Internal+ Network.Ethereum.ABI.Generic+ Network.Ethereum.ABI.Prim.Int+ Network.Ethereum.ABI.Prim.Bool+ Network.Ethereum.ABI.Prim.List+ Network.Ethereum.ABI.Prim.Bytes+ Network.Ethereum.ABI.Prim.Tuple+ Network.Ethereum.ABI.Prim.Tuple.TH+ Network.Ethereum.ABI.Prim.String+ Network.Ethereum.ABI.Prim.Tagged+ Network.Ethereum.ABI.Prim.Address+ Network.Ethereum.Contract.TH+ Network.Ethereum.Contract.Event+ Network.Ethereum.Contract.Method+ Network.JsonRpc.TinyClient+ Data.String.Extra build-depends: base >4.8 && <4.11,- base16-bytestring >=0.1.1.6 && <0.2,- template-haskell >=2.11.1.0 && <2.12,- http-client-tls >=0.3.5.1 && <0.4,+ template-haskell >=2.12.0.0 && <2.13,+ data-default >=0.7.1.1 && <0.8,+ generics-sop >=0.3.2.0 && <0.4, transformers >=0.5.2.0 && <0.6,- http-client >=0.5.7.0 && <0.6,- attoparsec >=0.13.1.0 && <0.14,- bytestring >=0.10.8.1 && <0.11,- cryptonite ==0.23.*,- vector >=0.12.0.1 && <0.13,- memory >=0.14.8 && <0.15,- aeson >=1.1.2.0 && <1.2,- text >=1.2.2.2 && <1.3+ http-client >=0.5.12.1 && <0.6,+ http-client-tls >=0.3.5.3 && <0.4,+ exceptions >=0.8.3 && <0.9,+ bytestring >=0.10.8.2 && <0.11,+ cryptonite ==0.25.*,+ basement >=0.0.7 && <0.1,+ machines >=0.6.3 && <0.7,+ tagged >=0.8.5 && <0.9,+ parsec >=3.1.13.0 && <3.2,+ memory >=0.14.16 && <0.15,+ cereal >=0.5.5.0 && <0.6,+ aeson >=1.2.4.0 && <1.3,+ async >=2.1.1.1 && <2.2,+ text >=1.2.3.0 && <1.3,+ mtl >=2.2.2 && <2.3 default-language: Haskell2010- default-extensions: OverloadedStrings hs-source-dirs: src- other-modules:- Network.Ethereum.Web3.JsonRpc- Network.Ethereum.Web3.Internal- Network.Ethereum.Web3.Encoding.TupleTH- Network.Ethereum.Web3.Encoding.Internal+ ghc-options: -Wduplicate-exports -Whi-shadowing -Widentities -Wnoncanonical-monoid-instances -Woverlapping-patterns -Wtabs -Wpartial-type-signatures -Wderiving-typeable -Wunrecognised-pragmas -Wunticked-promoted-constructors -Wtyped-holes -Wincomplete-patterns -Wincomplete-uni-patterns -Wmissing-fields -Wmissing-methods -Wmissing-exported-signatures -Wmissing-monadfail-instances -Wmissing-signatures -Wname-shadowing -Wnoncanonical-monad-instances -Wnoncanonical-monadfail-instances -Wunused-binds -Wunused-top-binds -Wunused-local-binds -Wunused-pattern-binds -Wunused-imports -Wunused-matches -Wunused-foralls -Wunused-do-bind -Wwrong-do-bind -Wtrustworthy-safe -Wwarnings-deprecations -Wdeferred-type-errors -test-suite web3-test+test-suite unit type: exitcode-stdio-1.0 main-is: Spec.hs build-depends:- base >=4.9.1.0 && <4.10,- memory >=0.14.8 && <0.15,- text >=1.2.2.2 && <1.3,- web3+ base >4.8 && <4.11,+ hspec-expectations >=0.8.2 && <0.9,+ hspec-discover >=2.4.8 && <2.5,+ hspec-contrib >=0.4.0 && <0.5,+ hspec >=2.4.8 && <2.5,+ data-default >=0.7.1.1 && <0.8,+ generics-sop >=0.3.2.0 && <0.4,+ transformers >=0.5.2.0 && <0.6,+ bytestring >=0.10.8.2 && <0.11,+ memory >=0.14.16 && <0.15,+ tagged >=0.8.5 && <0.9,+ split >=0.2.3.3 && <0.3,+ time >=1.8.0.2 && <1.9,+ text >=1.2.3.0 && <1.3,+ web3 -any default-language: Haskell2010- default-extensions: OverloadedStrings+ hs-source-dirs: unit+ other-modules:+ Network.Ethereum.Web3.Test.MethodDumpSpec+ Network.Ethereum.Web3.Test.EncodingSpec+ Network.Ethereum.Web3.Test.EventSpec+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+test-suite live+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-depends:+ async >=2.1.1.1 && <2.2,+ base >=4.10.1.0 && <4.11,+ bytestring >=0.10.8.2 && <0.11,+ data-default >=0.7.1.1 && <0.8,+ hspec >=2.4.8 && <2.5,+ hspec-contrib >=0.4.0 && <0.5,+ hspec-discover >=2.4.8 && <2.5,+ hspec-expectations >=0.8.2 && <0.9,+ hspec-discover >=2.4.8 && <2.5,+ hspec-contrib >=0.4.0 && <0.5,+ hspec >=2.4.8 && <2.5,+ transformers >=0.5.2.0 && <0.6,+ data-default >=0.7.1.1 && <0.8,+ bytestring >=0.10.8.2 && <0.11,+ memory >=0.14.16 && <0.15,+ split >=0.2.3.3 && <0.3,+ text >=1.2.3.0 && <1.3,+ time >=1.8.0.2 && <1.9,+ text >=1.2.3.0 && <1.3,+ web3 -any,+ stm >=2.4.5.0 && <2.5+ default-language: Haskell2010 hs-source-dirs: test+ other-modules:+ Network.Ethereum.Web3.Test.ComplexStorageSpec+ Network.Ethereum.Web3.Test.SimpleStorageSpec+ Network.Ethereum.Web3.Test.Utils ghc-options: -threaded -rtsopts -with-rtsopts=-N