packages feed

web3-tools-0.1.0.0: src/Eth/Address.hs

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