packages feed

web3-solidity-1.0.1.0: src/Data/Solidity/Abi/Generic.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies        #-}
{-# LANGUAGE TypeInType          #-}
{-# LANGUAGE TypeOperators       #-}

-- |
-- Module      :  Data.Solidity.Abi.Generic
-- Copyright   :  Aleksandr Krupenkin 2016-2024
-- License     :  Apache-2.0
--
-- 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 Data.Solidity.Abi.Generic () where

import qualified Data.ByteString.Lazy   as LBS
import           Data.Int               (Int64)
import qualified Data.List              as L
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           Data.Solidity.Abi      (AbiGet (..), AbiPut (..), AbiType (..),
                                         GenericAbiGet (..), GenericAbiPut (..))
import           Data.Solidity.Prim.Int (getWord256, putWord256)

data EncodedValue = EncodedValue
    { evOrder                 :: Int64
    , evIsDynamic             :: Bool
    , evEncoding              :: Put
    , evEncodingLengthInBytes :: Int64 -- cache
    }

instance Eq EncodedValue where
  ev1 == ev2 = evOrder ev1 == evOrder ev2

instance Ord EncodedValue where
  compare ev1 ev2 = evOrder ev1 `compare` evOrder ev2

-- from https://docs.soliditylang.org/en/v0.8.12/abi-spec.html#examples
--
-- if Ti is static:
--   head(X(i)) = enc(X(i)) and tail(X(i)) = "" (the empty string)
-- otherwise, i.e. if Ti is dynamic:
--   head(X(i)) = enc(len( head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1)) )) tail(X(i)) = enc(X(i))
combineEncodedValues :: [EncodedValue] -> Put
combineEncodedValues encodings =
  let sortedEncodings = L.sort encodings

      wordLengthInBytes :: Int64
      wordLengthInBytes = 32

      headsOffsetInBytes :: Int64
      headsOffsetInBytes = foldl (+) 0 $ map (\EncodedValue{..} -> if evIsDynamic then wordLengthInBytes else evEncodingLengthInBytes) encodings

      heads = fst $ foldl
        (\(accumulator, lengthOfPreviousDynamicValues) EncodedValue{..} -> if evIsDynamic
            then ( accumulator <> putWord256 (fromIntegral $ headsOffsetInBytes + lengthOfPreviousDynamicValues)
                 , lengthOfPreviousDynamicValues + evEncodingLengthInBytes
                 )
            else ( accumulator <> evEncoding
                 , lengthOfPreviousDynamicValues
                 )
        )
        (mempty, 0)
        sortedEncodings
      tails = foldMap
        (\EncodedValue{..} -> if evIsDynamic
            then evEncoding
            else mempty
        )
        sortedEncodings
      in heads <> tails
  where

-- aIsDynamic is a variable because of https://github.com/airalab/hs-web3/pull/129#issuecomment-1074045478
-- TODO: call the `isDynamic` function in the `mkEncodedValue` function
mkEncodedValue :: (AbiType a, AbiPut a) => [EncodedValue] -> a -> Bool -> EncodedValue
mkEncodedValue otherEncodedArray a aIsDynamic =
  let encoding = abiPut a
  in EncodedValue
  { evEncoding              = encoding
  , evOrder                 = fromInteger . toInteger . L.length $ otherEncodedArray
  , evIsDynamic             = aIsDynamic
  , evEncodingLengthInBytes = lengthInBytes encoding
  }
  where
  lengthInBytes :: Put -> Int64
  lengthInBytes e = LBS.length . runPutLazy $ e

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) = _serialize (mkEncodedValue encoded b (isDynamic (Proxy :: Proxy b)) : encoded) a

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