web3-0.8.0.0: src/Data/Solidity/Prim/Address.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Data.Solidity.Prim.Address
-- Copyright : Alexander Krupenkin 2018
-- License : BSD3
--
-- Maintainer : mail@akru.me
-- Stability : experimental
-- Portability : noportable
--
-- Ethreum account address.
--
module Data.Solidity.Prim.Address
(
-- * The @Address@ type
Address
-- * Hex string encoding
, toHexString
, fromHexString
-- * Public key to @Address@ convertor
, fromPubKey
-- * EIP55 Mix-case checksum address encoding
, toChecksum
, verifyChecksum
) where
import Control.Monad ((<=<))
import Crypto.Hash (Keccak_256 (..), hashWith)
import Crypto.Secp256k1 (PubKey, exportPubKey)
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Bits ((.&.))
import Data.Bool (bool)
import Data.ByteArray (convert, zero)
import qualified Data.ByteArray as BA (drop)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS (take, unpack)
import qualified Data.ByteString.Char8 as C8 (drop, length, pack, unpack)
import qualified Data.Char as C (toLower, toUpper)
import Data.Default (Default (..))
import Data.Monoid ((<>))
import Data.String (IsString (..))
import Data.Text.Encoding as T (encodeUtf8)
import Generics.SOP (Generic)
import qualified GHC.Generics as GHC (Generic)
import Data.ByteArray.HexString (HexString, fromBytes, toBytes,
toText)
import Data.Solidity.Abi (AbiGet (..), AbiPut (..),
AbiType (..))
import Data.Solidity.Abi.Codec (decode, encode)
import Data.Solidity.Prim.Int (UIntN)
-- | Ethereum account address
newtype Address = Address { unAddress :: UIntN 160 }
deriving (Eq, Ord, GHC.Generic)
instance Generic Address
instance Default Address where
def = Address 0
instance Show Address where
show = show . toChecksum . T.encodeUtf8 . toText . toHexString
instance IsString Address where
fromString = either error id . fromHexString . fromString
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 = (either fail pure . fromHexString) <=< parseJSON
instance ToJSON Address where
toJSON = toJSON . toHexString
-- | Derive address from secp256k1 public key
fromPubKey :: PubKey -> Address
fromPubKey key =
case decode $ zero 12 <> BA.drop 12 (sha3 key) of
Right a -> a
Left e -> error $ "Impossible error: " ++ e
where
sha3 :: PubKey -> ByteString
sha3 = convert . hashWith Keccak_256 . BA.drop 1 . exportPubKey False
-- | Decode address from hex string
fromHexString :: HexString -> Either String Address
fromHexString bs
| bslen == 20 = decode (zero 12 <> toBytes bs :: ByteString)
| otherwise = Left $ "Incorrect address length: " ++ show bslen
where bslen = C8.length (toBytes bs)
-- | Encode address to hex string
toHexString :: Address -> HexString
toHexString = fromBytes . C8.drop 12 . encode
-- | Encode address with mixed-case checksum
-- https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
toChecksum :: ByteString -> ByteString
toChecksum addr = ("0x" <>) . C8.pack $ zipWith ($) upcaseVector lower
where
upcaseVector = (>>= fourthBits) . BS.unpack . BS.take 20 . convert $ hashWith Keccak_256 (C8.pack lower)
fourthBits n = bool id C.toUpper <$> [n .&. 0x80 /= 0, n .&. 0x08 /= 0]
lower = drop 2 . fmap C.toLower . C8.unpack $ addr
-- | Verify mixed-case address checksum
-- https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
verifyChecksum :: ByteString -> Bool
verifyChecksum = toChecksum >>= (==)