packages feed

web3-tools (empty) → 0.1.0.0

raw patch · 11 files changed

+599/−0 lines, 11 filesdep +QuickCheckdep +basedep +base16-bytestringsetup-changed

Dependencies added: QuickCheck, base, base16-bytestring, bytestring, crypton, hspec, hspec-discover, memory, secp256k1-haskell, web3-tools

Files

+ LICENSE view
@@ -0,0 +1,7 @@+Copyright 2025 Gabriel Tollini++Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,70 @@+# web3-tools++[![CI](https://github.com/gtollini/web3-tools/actions/workflows/ci.yml/badge.svg)](https://github.com/gtollini/web3-tools/actions/workflows/ci.yml)++A Haskell library providing simple, reusable cryptographic utilities for Web3 development.++This is a small, focused library that I maintain and expand as needed. Contributions, feature requests, and suggestions are always welcome!++## Features++- **Ethereum signature verification** - Verify ECDSA signatures against addresses+- **Address recovery** - Recover Ethereum addresses from message signatures+- **Secp256k1 operations** - Low-level elliptic curve cryptography via FFI+- **Keccak256 hashing** - Ethereum-compatible hash function++## Module Structure++The library is organized to separate chain-agnostic crypto primitives from blockchain-specific implementations:++```+Crypto.*              -- Shared cryptographic primitives+  Crypto.Hash.Keccak  -- Keccak256 hashing+  Crypto.Secp256k1.*  -- Elliptic curve operations++Eth.*                 -- Ethereum-specific functionality+  Eth.Address         -- Signature verification and address recovery+```++This makes it easy to add support for other blockchains while reusing common cryptographic building blocks.++## Installation++**Requirements:**+- libsecp256k1-devel (or libsecp256k1-dev on Debian/Ubuntu)++**Using Stack:**+```bash+stack build+```++## Usage Example++```haskell+{-# LANGUAGE OverloadedStrings #-}+import Eth.Address (verifySignature)++-- Verify an Ethereum signature+main :: IO ()+main = do+  let message = "Hello, Web3!"+      signature = ... -- 65-byte signature (r || s || v)+      address = ...   -- 20-byte Ethereum address++  isValid <- verifySignature message signature address+  print isValid+```++## Contributing++This library grows based on practical needs. If you:+- Need a specific feature for your project+- Found a bug or issue+- Have suggestions for improvements+- Want to add support for another blockchain++Feel free to open an issue or submit a PR!++## License++MIT - See LICENSE file for details
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Crypto/Hash/Keccak.hs view
@@ -0,0 +1,28 @@+module Crypto.Hash.Keccak+  ( hashKeccak256,+    hashKeccak256Str,+  )+where++import qualified Crypto.Hash as H+import Data.ByteArray (convert)+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BS8+import Data.Char (toLower)+import Text.Printf (printf)++-- | Hash data using Keccak256+hashKeccak256 :: ByteString -> H.Digest H.Keccak_256+hashKeccak256 = H.hash++hashKeccak256BS :: ByteString -> ByteString+hashKeccak256BS = convert . H.hashWith H.Keccak_256++-- | Hash a string using Keccak256 and return the result as a string+hashKeccak256Str :: String -> String+hashKeccak256Str = showBytes . hashKeccak256BS . BS8.pack . fmap toLower++-- | Convert a ByteString to a hexadecimal string+showBytes :: ByteString -> String+showBytes = concatMap (printf "%02x") . B.unpack
+ src/Crypto/Secp256k1/Recovery.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE CApiFFI #-}+{-# LANGUAGE ForeignFunctionInterface #-}++module Crypto.Secp256k1.Recovery+  ( recoverPubKey,+  )+where++import qualified Crypto.Secp256k1.Internal.BaseOps as Secp+import qualified Crypto.Secp256k1.Internal.Context as Secp+import qualified Crypto.Secp256k1.Internal.ForeignTypes as Secp+import Data.ByteString (packCStringLen)+import qualified Data.ByteString as BS+import Data.ByteString.Unsafe (unsafeUseAsCStringLen)+import Foreign+import Foreign.C.Types++-- | Parse a compact ECDSA signature (64 bytes + recovery id).+--+-- Returns: 1 when the signature could be parsed, 0 otherwise+-- Args:    ctx:     a secp256k1 context object+-- Out:     sig:     a pointer to a signature object (RecSig65)+-- In:      input64: a pointer to a 64-byte compact signature+--          recid:   the recovery id (0, 1, 2 or 3)+foreign import capi unsafe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_parse_compact"+  secp256k1_ecdsa_recoverable_signature_parse_compact ::+    Ptr Secp.LCtx ->+    Ptr Secp.RecSig65 ->+    Ptr CUChar ->+    CInt ->+    IO Secp.Ret++-- | Recover an ECDSA public key from a signature.+--+-- Returns: 1: public key successfully recovered (which guarantees a correct signature).+--          0: otherwise.+-- Args:    ctx:       pointer to a context object, initialized for verification+-- Out:     pubkey:    pointer to the recovered public key+-- In:      sig:       pointer to initialized signature that supports pubkey recovery+--          msg32:     the 32-byte message hash assumed to be signed+foreign import capi unsafe "secp256k1_recovery.h secp256k1_ecdsa_recover"+  secp256k1_ecdsa_recover ::+    Ptr Secp.LCtx ->+    Ptr Secp.PubKey64 ->+    Ptr Secp.RecSig65 ->+    Ptr Secp.Msg32 ->+    IO Secp.Ret++-- | Recover a public key from an Ethereum-style signature+-- The signature should be 65 bytes: r (32) || s (32) || v (1)+-- The message should be 32 bytes (typically a Keccak256 hash)+recoverPubKey :: BS.ByteString -> BS.ByteString -> IO (Maybe BS.ByteString)+recoverPubKey message sig+  | BS.length sig /= 65 = pure Nothing+  | BS.length message /= 32 = pure Nothing+  | otherwise = do+      let r_s = BS.take 64 sig -- First 64 bytes (r || s)+          vByte = BS.index sig 64 -- Recovery ID byte+          -- Normalize v to 0-3 range (Ethereum uses 27/28, secp256k1 expects 0-3)+          v = fromIntegral $ if vByte >= 27 then vByte - 27 else vByte++      -- Allocate space for recoverable signature (65 bytes)+      allocaBytes 65 $ \recSigPtr ->+        -- Allocate space for public key (64 bytes internal representation)+        allocaBytes 64 $ \pubKeyPtr ->+          unsafeUseAsCStringLen r_s $ \(compactPtr, _) ->+            unsafeUseAsCStringLen message $ \(msgPtr, _) ->+              Secp.withContext $ \(Secp.Ctx ctxFPtr) ->+                withForeignPtr ctxFPtr $ \ctxPtr -> do+                  -- Parse the compact signature with recovery ID+                  ret1 <-+                    secp256k1_ecdsa_recoverable_signature_parse_compact+                      ctxPtr+                      (castPtr recSigPtr)+                      (castPtr compactPtr)+                      v++                  if not (Secp.isSuccess ret1)+                    then return Nothing+                    else do+                      -- Recover the public key+                      ret2 <-+                        secp256k1_ecdsa_recover+                          ctxPtr+                          (castPtr pubKeyPtr)+                          (castPtr recSigPtr)+                          (castPtr msgPtr)++                      if not (Secp.isSuccess ret2)+                        then return Nothing+                        else do+                          -- Serialize the public key (uncompressed format, 65 bytes)+                          allocaBytes 65 $ \outPtr ->+                            alloca $ \sizePtr -> do+                              poke sizePtr 65 -- Maximum size for uncompressed key+                              ret3 <-+                                Secp.ecPubKeySerialize+                                  ctxPtr+                                  outPtr+                                  sizePtr+                                  (castPtr pubKeyPtr)+                                  Secp.uncompressed++                              if not (Secp.isSuccess ret3)+                                then return Nothing+                                else do+                                  size <- peek sizePtr+                                  pubKeyBytes <- packCStringLen (castPtr outPtr, fromIntegral size)+                                  return $ Just pubKeyBytes
+ src/Eth/Address.hs view
@@ -0,0 +1,74 @@+module Eth.Address+  ( Address,+    Signature,+    verifySignature,+    isEIP55Valid,+  )+where++import Crypto.Hash.Keccak (hashKeccak256, hashKeccak256Str)+import Crypto.Secp256k1.Recovery (recoverPubKey)+import Data.ByteArray (convert)+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8+import Data.Char (digitToInt, isDigit, isLower, isUpper)++-- | Checks if the address is valid according to EIP-55+isEIP55Valid :: String -> Bool+isEIP55Valid addr = and $ zipWith (\addrChar digestChar -> if digitToInt digestChar >= 8 then isDigit addrChar || isUpper addrChar else isDigit addrChar || isLower addrChar) addr addrDigest+  where+    addrDigest = hashKeccak256Str addr++-- | Ethereum address (20 bytes)+type Address = ByteString++-- | Ethereum signature (65 bytes: r ++ s ++ v)+type Signature = ByteString++-- | Verify an Ethereum signature+-- Given a message, signature, and address, returns True if the signature is valid+-- Signature should be 65 bytes (r ++ s ++ v) and address should be 20 bytes+-- This function handles the appending of the message prefix.+verifySignature :: ByteString -> Signature -> Address -> IO Bool+verifySignature message sig addr+  | BS.length sig /= 65 = return False+  | BS.length addr /= 20 = return False+  | otherwise = do+      result <- recoverAddress message sig+      return $ case result of+        Just recoveredAddr -> recoveredAddr == addr+        Nothing -> False++-- | Recover the Ethereum address from a message and signature+-- Uses Ethereum's personal_sign message format: "\x19Ethereum Signed Message:\n" + length + message+recoverAddress :: ByteString -> Signature -> IO (Maybe Address)+recoverAddress message sig = do+  -- Create the Ethereum signed message with prefix+  let ethMessage = createEthereumSignedMessage message+      messageHash32 = convert (hashKeccak256 ethMessage) :: ByteString++  -- Recover the public key using our FFI bindings+  maybePubKeyBytes <- recoverPubKey messageHash32 sig++  -- Convert public key to address+  return $ publicKeyToAddress <$> maybePubKeyBytes++-- | Create an Ethereum signed message with the standard prefix+-- Format: "\x19Ethereum Signed Message:\n" + length + message+createEthereumSignedMessage :: ByteString -> ByteString+createEthereumSignedMessage message =+  let prefix = BS8.pack $ "\x19" <> "Ethereum Signed Message:\n"+      lengthStr = BS8.pack (show (BS.length message))+   in BS.concat [prefix, lengthStr, message]++-- | Convert a public key to an Ethereum address+-- Address is the last 20 bytes of Keccak256(publicKey)+-- Public key should be 65 bytes (0x04 prefix + 64 bytes)+publicKeyToAddress :: ByteString -> Address+publicKeyToAddress pubKeyBytes =+  let -- Remove the 0x04 prefix (first byte) from uncompressed public key+      pubKeyWithoutPrefix = BS.drop 1 pubKeyBytes+      -- Hash with Keccak256 and take last 20 bytes+      hashedPubKey = convert (hashKeccak256 pubKeyWithoutPrefix) :: ByteString+   in BS.drop 12 hashedPubKey -- Keccak256 produces 32 bytes, we take last 20
+ test/Crypto/Hash/KeccakSpec.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Crypto.Hash.KeccakSpec (spec) where++import Crypto.Hash.Keccak (hashKeccak256)+import Data.ByteArray (convert)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as B16+import Test.Hspec+import Test.QuickCheck++-- Arbitrary instance for ByteString+instance Arbitrary BS.ByteString where+  arbitrary = BS.pack <$> arbitrary++spec :: Spec+spec = do+  describe "hashKeccak256" $ do+    it "produces correct hash for empty string" $ do+      let result = convert (hashKeccak256 "") :: BS.ByteString+          Right expected = B16.decode "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"+      result `shouldBe` expected++    it "produces correct hash for 'hello'" $ do+      let result = convert (hashKeccak256 "hello") :: BS.ByteString+          Right expected = B16.decode "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"+      result `shouldBe` expected++    it "produces correct hash for 'Hello, World!'" $ do+      let result = convert (hashKeccak256 "Hello, World!") :: BS.ByteString+          Right expected = B16.decode "acaf3289d7b601cbd114fb36c4d29c85bbfd5e133f14cb355c3fd8d99367964f"+      result `shouldBe` expected++    it "always produces 32 bytes" $ property $ \(bs :: BS.ByteString) ->+      let result = convert (hashKeccak256 bs) :: BS.ByteString+       in BS.length result == 32++    it "produces different hashes for different inputs" $ property $ \(x :: BS.ByteString, y :: BS.ByteString) ->+      x /= y ==>+        let hashX = convert (hashKeccak256 x) :: BS.ByteString+            hashY = convert (hashKeccak256 y) :: BS.ByteString+         in hashX /= hashY++    it "is deterministic" $ property $ \(bs :: BS.ByteString) ->+      let hash1 = convert (hashKeccak256 bs) :: BS.ByteString+          hash2 = convert (hashKeccak256 bs) :: BS.ByteString+       in hash1 == hash2
+ test/Crypto/Secp256k1/RecoverySpec.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Crypto.Secp256k1.RecoverySpec (spec) where++import Crypto.Secp256k1.Recovery (recoverPubKey)+import qualified Data.ByteString as BS+import Data.Maybe (isJust, isNothing)+import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.Monadic++-- Arbitrary instance for ByteString+instance Arbitrary BS.ByteString where+  arbitrary = BS.pack <$> arbitrary++spec :: Spec+spec = do+  describe "recoverPubKey" $ do+    describe "input validation" $ do+      it "rejects signature that is too short" $ do+        let message = BS.replicate 32 0+            sig = BS.replicate 64 0 -- Should be 65+        result <- recoverPubKey message sig+        result `shouldBe` Nothing++      it "rejects signature that is too long" $ do+        let message = BS.replicate 32 0+            sig = BS.replicate 66 0 -- Should be 65+        result <- recoverPubKey message sig+        result `shouldBe` Nothing++      it "rejects message that is too short" $ do+        let message = BS.replicate 31 0 -- Should be 32+            sig = BS.replicate 65 0+        result <- recoverPubKey message sig+        result `shouldBe` Nothing++      it "rejects message that is too long" $ do+        let message = BS.replicate 33 0 -- Should be 32+            sig = BS.replicate 65 0+        result <- recoverPubKey message sig+        result `shouldBe` Nothing++    describe "property-based tests" $ do+      it "always returns Nothing or Just a 65-byte public key" $ property $+        \(message :: BS.ByteString) (sig :: BS.ByteString) -> monadicIO $ do+          result <- run $ recoverPubKey message sig+          monitor $ classify (isNothing result) "returned Nothing"+          monitor $ classify (isJust result) "returned Just pubKey"+          case result of+            Nothing -> return True+            Just pubKey -> return $ BS.length pubKey == 65++      it "returns Nothing for invalid signature lengths" $ property $+        forAll (choose (0, 128) `suchThat` (/= 65)) $ \len -> monadicIO $ do+          let message = BS.replicate 32 0+              sig = BS.replicate len 0+          result <- run $ recoverPubKey message sig+          return $ result == Nothing++      it "returns Nothing for invalid message lengths" $ property $+        forAll (choose (0, 64) `suchThat` (/= 32)) $ \len -> monadicIO $ do+          let message = BS.replicate len 0+              sig = BS.replicate 65 0+          result <- run $ recoverPubKey message sig+          return $ result == Nothing
+ test/Eth/AddressSpec.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Eth.AddressSpec (spec) where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Base16 as B16+import Eth.Address (verifySignature)+import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.Monadic++-- Arbitrary instance for ByteString+instance Arbitrary BS.ByteString where+  arbitrary = BS.pack <$> arbitrary++spec :: Spec+spec = do+  describe "verifySignature" $ do+    describe "input validation" $ do+      it "rejects signature with wrong length" $ do+        let message = "test message"+            sig = BS.replicate 64 0 -- Should be 65+            addr = BS.replicate 20 0+        result <- verifySignature message sig addr+        result `shouldBe` False++      it "rejects address with wrong length" $ do+        let message = "test message"+            sig = BS.replicate 65 0+            addr = BS.replicate 19 0 -- Should be 20+        result <- verifySignature message sig addr+        result `shouldBe` False++      it "accepts correct lengths (even if signature is invalid)" $ do+        let message = "test message"+            sig = BS.replicate 65 0+            addr = BS.replicate 20 0+        -- Should not crash, just return False for invalid signature+        result <- verifySignature message sig addr+        result `shouldBe` False++    describe "property-based tests" $ do+      it "always returns a boolean" $+        property $+          \(message :: BS.ByteString) (sig :: BS.ByteString) (addr :: BS.ByteString) -> monadicIO $ do+            result <- run $ verifySignature message sig addr+            return $ result `elem` [True, False]++      it "returns False for signatures with wrong length" $+        property $+          forAll (choose (0, 128) `suchThat` (/= 65)) $ \sigLen -> monadicIO $ do+            let message = "test"+                sig = BS.replicate sigLen 0+                addr = BS.replicate 20 0+            result <- run $ verifySignature message sig addr+            return $ result == False++      it "returns False for addresses with wrong length" $+        property $+          forAll (choose (0, 40) `suchThat` (/= 20)) $ \addrLen -> monadicIO $ do+            let message = "test"+                sig = BS.replicate 65 0+                addr = BS.replicate addrLen 0+            result <- run $ verifySignature message sig addr+            return $ result == False++      it "is deterministic" $+        property $+          \(message :: BS.ByteString) -> monadicIO $ do+            let sig = BS.replicate 65 0+                addr = BS.replicate 20 0+            result1 <- run $ verifySignature message sig addr+            result2 <- run $ verifySignature message sig addr+            return $ result1 == result2++    describe "signature verification" $ do+      it "rejects an invalid signature for correct message/address" $ do+        let message = "Hello, Web3!"+            sig' = B16.decode "9d2db20f926fad885854f4bf659a72de9db2990b199ad300644e2b482f8583e169325c2a57bbfa4ef6897f63431f290716c58507af64d88554923410f38be2e31b"+            addr' = B16.decode "f406ffc8e58c5163e67e020dd1c5ce924923ecbf"+        result <- case (sig', addr') of+          (Right sig, Right addr) -> verifySignature message sig addr+          (_, _) -> error "Couldn't decode signature or address!"+        result `shouldBe` False++      it "rejects invalid message from with valid address/signature" $ do+        let message = "Bye, Web3!"+            sig' = B16.decode "8d2db20f926fad885854f4bf659a72de9db2990b199ad300644e2b482f8583e169325c2a57bbfa4ef6897f63431f290716c58507af64d88554923410f38be2e31b"+            addr' = B16.decode "f406ffc8e58c5163e67e020dd1c5ce924923ecbf"+        result <- case (sig', addr') of+          (Right sig, Right addr) -> verifySignature message sig addr+          (_, _) -> error "Couldn't decode signature or address!"+        result `shouldBe` False++      it "rejects invalid address from with valid message/signature [from another address]" $ do+        let message = "Hello, Web3!"+            sig' = B16.decode "8d2db20f926fad885854f4bf659a72de9db2990b199ad300644e2b482f8583e169325c2a57bbfa4ef6897f63431f290716c58507af64d88554923410f38be2e31b"+            addr' = B16.decode "0406ffc8e58c5163e67e020dd1c5ce924923ecbf"+        result <- case (sig', addr') of+          (Right sig, Right addr) -> verifySignature message sig addr+          (_, _) -> error "Couldn't decode signature or address!"+        result `shouldBe` False++      it "verifies a valid Ethereum signature from real wallet" $ do+        let message = "Hello, Web3!"+            sig' = B16.decode "8d2db20f926fad885854f4bf659a72de9db2990b199ad300644e2b482f8583e169325c2a57bbfa4ef6897f63431f290716c58507af64d88554923410f38be2e31b"+            addr' = B16.decode "f406ffc8e58c5163e67e020dd1c5ce924923ecbf"+        result <- case (sig', addr') of+          (Right sig, Right addr) -> verifySignature message sig addr+          (_, _) -> error "Couldn't decode signature or address!"+        result `shouldBe` True++    describe "edge cases" $ do+      it "handles empty message" $ do+        let message = ""+            sig = BS.replicate 65 0+            addr = BS.replicate 20 0+        result <- verifySignature message sig addr+        result `shouldSatisfy` const True -- Just shouldn't crash+      it "handles very long message" $ do+        let message = BS.replicate 10000 0xff+            sig = BS.replicate 65 0+            addr = BS.replicate 20 0+        result <- verifySignature message sig addr+        result `shouldSatisfy` const True -- Just shouldn't crash
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ web3-tools.cabal view
@@ -0,0 +1,67 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.38.1.+--+-- see: https://github.com/sol/hpack++name:           web3-tools+version:        0.1.0.0+synopsis:       Tools for working with Crypto/Web3+description:    Please see the README on GitHub at <https://github.com/gtollini/web3-tools#readme>+category:       Web3, Crypto, Ethereum, Eth, Blockchain+homepage:       https://github.com/gtollini/web3-tools#readme+bug-reports:    https://github.com/gtollini/web3-tools/issues+author:         Gabriel Tollini+maintainer:     gabrieltollini@hotmail.com+copyright:      2025 Gabriel Tollini+license:        MIT+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md++source-repository head+  type: git+  location: https://github.com/gtollini/web3-tools++library+  exposed-modules:+      Crypto.Hash.Keccak+      Crypto.Secp256k1.Recovery+      Eth.Address+  other-modules:+      Paths_web3_tools+  hs-source-dirs:+      src+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base >=4.7 && <5+    , bytestring >=0.11.5 && <0.13+    , crypton >=1.0.4 && <1.1+    , memory >=0.18.0 && <0.19+    , secp256k1-haskell >=1.4.6 && <1.5+  default-language: Haskell2010++test-suite web3-tools-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Crypto.Hash.KeccakSpec+      Crypto.Secp256k1.RecoverySpec+      Eth.AddressSpec+      Paths_web3_tools+  hs-source-dirs:+      test+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N -Wno-orphans -Wno-incomplete-uni-patterns+  build-depends:+      QuickCheck+    , base >=4.7 && <5+    , base16-bytestring+    , bytestring >=0.11.5 && <0.13+    , crypton >=1.0.4 && <1.1+    , hspec+    , hspec-discover+    , memory >=0.18.0 && <0.19+    , secp256k1-haskell >=1.4.6 && <1.5+    , web3-tools+  default-language: Haskell2010