web3-crypto-1.1.0.0: tests/Crypto/Ethereum/Test/EIP712SignatureSpec.hs
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
-- |
-- Module : Crypto.Ethereum.Test.EIP712SignatureSpec
-- Copyright : Jin Chui 2025
-- License : Apache-2.0
--
-- Maintainer : mail@akru.me jinchui@pm.me
-- Stability : experimental
-- Portability : unportable
--
module Crypto.Ethereum.Test.EIP712SignatureSpec (spec) where
import Crypto.Ethereum.Eip712Signature
import Crypto.Ethereum.Utils (keccak256)
import Data.Aeson (toJSON)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.KeyMap as Aeson
import qualified Data.ByteArray.Encoding as BAE
import Data.ByteString (ByteString)
import Data.Either (fromRight)
import Test.Hspec
encodeTypeConcrete :: EIP712Types -> EIP712Name -> Either String ByteString
encodeTypeConcrete = encodeType
keccak256Concrete :: ByteString -> ByteString
keccak256Concrete = keccak256
hexEncode :: ByteString -> ByteString
hexEncode = BAE.convertToBase BAE.Base16
encodeDataConcrete :: EIP712Types -> EIP712Name -> Aeson.Object -> Either String ByteString
encodeDataConcrete = encodeData
hashStructConcrete :: EIP712Types -> EIP712Name -> Aeson.Object -> Either String ByteString
hashStructConcrete = hashStruct
mailTypes :: EIP712Types
mailTypes =
[ EIP712Struct
{ eip712StructName = "Mail"
, eip712StructFields =
[ EIP712Field "from" (FieldTypeStruct "Person")
, EIP712Field "to" (FieldTypeStruct "Person")
, EIP712Field "contents" FieldTypeString
]
}
, EIP712Struct
{ eip712StructName = "Person"
, eip712StructFields =
[ EIP712Field "name" FieldTypeString
, EIP712Field "wallet" FieldTypeAddress
]
}
, EIP712Struct
{ eip712StructName = "EIP712Domain"
, eip712StructFields =
[ EIP712Field "name" FieldTypeString
, EIP712Field "version" FieldTypeString
, EIP712Field "chainId" (FieldTypeUInt Si256)
, EIP712Field "verifyingContract" FieldTypeAddress
]
}
]
nestedArrayTypes :: EIP712Types
nestedArrayTypes =
[ EIP712Struct
{ eip712StructName = "foo"
, eip712StructFields =
[ EIP712Field "a" (FieldTypeArray (FieldTypeArray (FieldTypeUInt Si256)))
, EIP712Field "b" FieldTypeString
]
}
]
safeTxType :: EIP712Struct
safeTxType =
EIP712Struct
{ eip712StructName = "SafeTx"
, eip712StructFields =
[ EIP712Field "to" FieldTypeAddress
, EIP712Field "value" (FieldTypeUInt Si256)
, EIP712Field "data" FieldTypeBytes
, EIP712Field "operation" (FieldTypeUInt Si8)
, EIP712Field "safeTxGas" (FieldTypeUInt Si256)
, EIP712Field "baseGas" (FieldTypeUInt Si256)
, EIP712Field "gasPrice" (FieldTypeUInt Si256)
, EIP712Field "gasToken" FieldTypeAddress
, EIP712Field "refundReceiver" FieldTypeAddress
, EIP712Field "nonce" (FieldTypeUInt Si256)
]
}
expectedEncodedSafeTxType :: ByteString
expectedEncodedSafeTxType = "SafeTx(\
\address to,\
\uint256 value,\
\bytes data,\
\uint8 operation,\
\uint256 safeTxGas,\
\uint256 baseGas,\
\uint256 gasPrice,\
\address gasToken,\
\address refundReceiver,\
\uint256 nonce)"
safeDomainType :: EIP712Struct
safeDomainType =
EIP712Struct
{ eip712StructName = "EIP712Domain"
, eip712StructFields =
[ EIP712Field "chainId" (FieldTypeUInt Si256)
, EIP712Field "verifyingContract" FieldTypeAddress
]
}
safeDomain :: Aeson.KeyMap Aeson.Value
safeDomain =
Aeson.fromList
[("chainId", toJSON @Int 8453), ("verifyingContract", toJSON @String "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826")]
expectedEncodedSafeDomain :: ByteString
expectedEncodedSafeDomain = "0000000000000000000000000000000000000000000000000000000000002105\
\000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826"
safeMessage :: Aeson.KeyMap Aeson.Value
safeMessage =
Aeson.fromList
[ ("to",toJSON @String "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" )
, ("value", toJSON @Integer 0 )
, ("data", toJSON @String "0x0b89085a01a3b67d2231c6a136f9c8eea75d7d479a83a127356f8540ee15af010c22b846886e98aeffc1f1166d4b3586")
, ("operation", toJSON @Integer 0)
, ("safeTxGas", toJSON @Integer 0)
, ("baseGas", toJSON @Integer 0)
, ("gasPrice", toJSON @Integer 0)
, ("gasToken", toJSON @String "0x0000000000000000000000000000000000000000")
, ("refundReceiver", toJSON @String "0x0000000000000000000000000000000000000000")
, ("nonce", toJSON (37 :: Integer))
]
expectedEncodedSafeMessage :: ByteString
expectedEncodedSafeMessage = "000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\
\0000000000000000000000000000000000000000000000000000000000000000\
\059c15417e7e213ad30596d872d2e906e4feafd54fa0c9ac864b421ab1ba5adb\
\0000000000000000000000000000000000000000000000000000000000000000\
\0000000000000000000000000000000000000000000000000000000000000000\
\0000000000000000000000000000000000000000000000000000000000000000\
\0000000000000000000000000000000000000000000000000000000000000000\
\0000000000000000000000000000000000000000000000000000000000000000\
\0000000000000000000000000000000000000000000000000000000000000000\
\0000000000000000000000000000000000000000000000000000000000000025"
safeTxTypedData :: EIP712TypedData
safeTxTypedData = EIP712TypedData
{ typedDataTypes = [safeTxType, safeDomainType]
, typedDataPrimaryType = "SafeTx"
, typedDataDomain = safeDomain
, typedDataMessage = safeMessage
}
spec :: Spec
spec = do
describe "encodeType" $ do
it "simple types should be encoding properly" $ do
let eip712structs = mailTypes
encodeTypeConcrete eip712structs "Person" `shouldBe` Right "Person(string name,address wallet)"
encodeTypeConcrete eip712structs "Mail"
`shouldBe` Right "Mail(Person from,Person to,string contents)Person(string name,address wallet)"
encodeTypeConcrete eip712structs "EIP712Domain"
`shouldBe` Right "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
it "should work for Safe transactions" $ do
encodeTypeConcrete [safeDomainType] "EIP712Domain" `shouldBe` Right "EIP712Domain(uint256 chainId,address verifyingContract)"
hexEncode . keccak256Concrete <$> encodeTypeConcrete [safeDomainType] "EIP712Domain"
`shouldBe` Right "47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218"
hexEncode <$> hashStruct [safeDomainType] "EIP712Domain" safeDomain
`shouldBe` Right "b3a3e869527602e68d877d9edcc629823648c73a3b10ee1e23cf4ab81b599cf5"
it "should work for safe transaction" $ encodeTypeConcrete [safeTxType] "SafeTx"
`shouldBe` Right expectedEncodedSafeTxType
describe "encodeData" $ do
it "should encode simple data" $ do
let eip712structs = mailTypes
let typeName = "Person"
let message =
Aeson.fromList
[("name", toJSON ("Cow" :: String)), ("wallet", toJSON ("0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" :: String))]
let encodedDataOrError = encodeDataConcrete eip712structs typeName message
let expectedEncodedData = "8c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648\
\000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826"
hexEncode <$> encodedDataOrError `shouldBe` Right expectedEncodedData
hashStructConcrete eip712structs "Person" message
`shouldBe` Right (keccak256Concrete $ keccak256Concrete "Person(string name,address wallet)" <> fromRight undefined encodedDataOrError)
it "should encode nested arrays" $ do
let message =
Aeson.fromList
[ ("a", toJSON [[35 :: Int, 36], [37]])
, ("b", toJSON ("hello" :: String))
]
encodeTypeConcrete nestedArrayTypes "foo" `shouldBe` Right "foo(uint256[][] a,string b)"
let expectedDataEncoded = "fa5ffe3a0504d850bc7c9eeda1cf960b596b73f4dc0272a6fa89dace08e32029\
\1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"
hexEncode <$> encodeDataConcrete nestedArrayTypes "foo" message `shouldBe` Right expectedDataEncoded
it "should encode Safe domain" $
(hexEncode <$> encodeDataConcrete [safeDomainType] "EIP712Domain" safeDomain) `shouldBe` Right expectedEncodedSafeDomain
it "should encode Safe transaction" $
(hexEncode <$> encodeDataConcrete [safeTxType] "SafeTx" safeMessage) `shouldBe` Right expectedEncodedSafeMessage
describe "typedDataSignHash" $ it "encode properly a safe transaction" $ do
hexEncode <$> typedDataSignHash safeTxTypedData `shouldBe` Right "cd3b59061dd8a7060486fb14e75e2f066a19a6e93f6888dbf83c77fbfeb8874b"