store: merge names contract reads

# Conflicts:
#	simplex-chat.cabal
#	tests/Test.hs
This commit is contained in:
shum committed 2026-10-22 01:14:13 +00:00
commit ffeddbcea3
10 files changed
+679 -59

No files matched your search

@@ -13,6 +13,7 @@ module StoreService.Names.Abi
selector,
hex,
decodeUint256,
decodeWord64,
decodeAddress,
decodeBool,
decodeBytes32,
@@ -31,6 +32,7 @@ import qualified Data.ByteString as B
import Data.List (mapAccumL)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, decodeUtf8')
import Data.Word (Word64)
import Simplex.Messaging.Crypto (keccak256)
import Simplex.Messaging.Encoding.String (strDecode)
import Simplex.Messaging.Eth.Address (Address, unAddress)
@@ -43,6 +45,7 @@ data AbiError
| AENotAddress
| AENotBool
| AENotUtf8
| AERange
deriving (Eq, Show)
abiWordSize :: Int
@@ -80,6 +83,11 @@ hex bs = "0x" <> decodeLatin1 (convertToBase Base16 bs)
decodeUint256 :: ByteString -> Either AbiError Integer
decodeUint256 = uintAt 0
decodeWord64 :: ByteString -> Either AbiError Word64
decodeWord64 ret = do
n <- decodeUint256 ret
if n <= toInteger (maxBound :: Word64) then Right (fromInteger n) else Left AERange
decodeAddress :: ByteString -> Either AbiError Address
decodeAddress ret = do
(padding, a) <- B.splitAt addressPadding <$> wordAt 0 ret
@@ -30,7 +30,7 @@ where
import Control.Concurrent.STM (TVar)
import Control.Exception (evaluate, try)
import Control.Monad ((>=>))
import Control.Monad ((<=<), (>=>))
import Crypto.Random (ChaChaDRG)
import Data.Aeson ((.:), (.:?), (.=))
import qualified Data.Aeson as J
@@ -55,7 +55,7 @@ import Simplex.Messaging.Util (tshow)
import StoreService.Config (ChainConfig (..))
import StoreService.Log (logWarn)
import StoreService.Names.Abi (hex)
import StoreService.Names.Revert (RevertName, decodeRevert)
import StoreService.Names.Revert (RevertName (..), decodeRevert)
import System.Timeout (timeout)
data ChainClient = ChainClient
@@ -227,7 +227,7 @@ rpc ChainClient {ccManager, ccConfig = ChainConfig {cRpcUrl}, ccTimeout} method
revertData <- e .:? "data"
pure $
if code == revertCode || "execution reverted" `T.isPrefixOf` message
then ChainReverted $ decodeRevert =<< (JT.parseMaybe bytes =<< revertData)
then ChainReverted $ maybe (Just RNWithoutData) (decodeRevert <=< JT.parseMaybe bytes) revertData
else unavailable $ "error " <> tshow code <> ": " <> T.take maxNodeMessageLength message
unavailable reason = ChainUnavailable $ method <> ": " <> reason
@@ -0,0 +1,220 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module StoreService.Names.Contracts
( Contracts (ctDeadline),
NameStatus (..),
RegisteredName (..),
NameRecords (..),
CommitmentAges (..),
newContracts,
secondsPerYear,
nameStatus,
nameRecords,
chainPriceCents,
codeLengthFor,
sponsorNonce,
resolverNonce,
ownerOfToken,
nameExpires,
commitmentTime,
controllerAvailable,
commitmentAges,
)
where
import Control.Concurrent.Async (Concurrently (..))
import Control.Monad.Except (ExceptT (..), runExceptT)
import Data.Bifunctor (first)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.IORef (IORef, atomicWriteIORef, newIORef, readIORef)
import Data.Functor.Compose (Compose (..))
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Word (Word64)
import Simplex.Chat.Badges.Code (maxNameLength, minNameLength)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.SimplexName (LabelHash (..), SimplexTLD)
import Simplex.Messaging.Util (tshow)
import StoreService.Config (ChainConfig (..))
import StoreService.Names.Abi
import StoreService.Names.Chain (Block (..), ChainClient (..), ChainError (..), ethCall, latestBlock)
import StoreService.Names.Registration (channelRecordKey, contactRecordKey, labelNode, nodeBytes)
import StoreService.Names.Revert (RevertName (..))
import System.Timeout (timeout)
data Contracts = Contracts
{ ctChain :: ChainClient,
ctTLD :: SimplexTLD,
ctRegistrar :: IORef (Maybe RegistrarConstants),
ctCommitmentAges :: IORef (Maybe CommitmentAges),
-- | microseconds for all the reads of one name status or of one name's records
ctDeadline :: Int
}
data RegistrarConstants = RegistrarConstants {rcRegistry :: Address, rcGracePeriod :: Integer}
data NameStatus = NSAvailable | NSReserved | NSRegistered RegisteredName
deriving (Eq, Show)
data RegisteredName = RegisteredName
{ rnOwner :: Address,
rnResolver :: Address,
rnExpiresAt :: Word64,
rnGracePeriod :: Integer
}
deriving (Eq, Show)
-- | The default resolver's records ("" when unset) and the owner's nonce for the next edit.
data NameRecords = NameRecords {rdContact :: Text, rdChannel :: Text, rdNonce :: Word64}
deriving (Eq, Show)
data CommitmentAges = CommitmentAges {caMin :: Integer, caMax :: Integer}
deriving (Eq, Show)
secondsPerYear :: Integer
secondsPerYear = 31536000
attoUsdPerCent :: Integer
attoUsdPerCent = 10 ^ (16 :: Int)
-- | One call's timeout, so a node that hangs fails a status or a records read once, not once per read.
nameReadsDeadline :: Int
nameReadsDeadline = 10000000
-- | Reads nothing: the registrar's constants and the commitment ages are read on first use and kept once read.
newContracts :: SimplexTLD -> ChainClient -> IO Contracts
newContracts ctTLD ctChain = do
ctRegistrar <- newIORef Nothing
ctCommitmentAges <- newIORef Nothing
pure Contracts {ctChain, ctTLD, ctRegistrar, ctCommitmentAges, ctDeadline = nameReadsDeadline}
-- | Registered while @nameExpires + GRACE_PERIOD@ is not before the latest block's time.
nameStatus :: Contracts -> LabelHash -> IO (Either ChainError NameStatus)
nameStatus ct@Contracts {ctChain} lh = withinDeadline ct "name status" $ runExceptT $ do
(RegistrarConstants {rcRegistry, rcGracePeriod}, expires, Block {blockTimestamp}) <-
ExceptT $ together $ (,,) <$> concurrent (registrarConstants ct) <*> concurrent (nameExpires ct lh) <*> concurrent (latestBlock ctChain)
if toInteger expires + rcGracePeriod >= toInteger blockTimestamp
then do
let node = nodeArg ct lh
registryRead signature = concurrent $ call ct rcRegistry (signature, node) decodeAddress
(rnOwner, rnResolver) <- ExceptT $ together $ (,) <$> registryRead "owner(bytes32)" <*> registryRead "resolver(bytes32)"
pure $ NSRegistered RegisteredName {rnOwner, rnResolver, rnExpiresAt = expires, rnGracePeriod = rcGracePeriod}
else do
reason <- ExceptT $ call ct (controller ct) ("reservedNames(bytes32)", labelHashArg lh) decodeUint256
pure $ if reason /= 0 then NSReserved else NSAvailable
-- | Nothing for a name on another resolver contract, or none: the service reads no records from a contract it does not know.
nameRecords :: Contracts -> LabelHash -> RegisteredName -> IO (Either ChainError (Maybe NameRecords))
nameRecords ct lh RegisteredName {rnOwner, rnResolver}
| rnResolver /= resolver ct = pure $ Right Nothing
| otherwise =
withinDeadline ct "name records" $ together $
(\rdContact rdChannel rdNonce -> Just NameRecords {rdContact, rdChannel, rdNonce})
<$> textRecord contactRecordKey
<*> textRecord channelRecordKey
<*> concurrent (resolverNonce ct rnOwner)
where
textRecord key = concurrent $ call ct (resolver ct) ("text(bytes32,string)", abiTuple [Static $ nodeArg ct lh, Dynamic $ abiBytes key]) decodeString
-- | Cents for a label of this length and these years. The oracle prices by length only,
-- so a placeholder label of that length keeps the label from the RPC node.
chainPriceCents :: Contracts -> Int -> Int -> IO (Either ChainError Int)
chainPriceCents ct len years = call ct (controller ct) ("rentPriceUSD(string,uint256)", args) $ \ret -> do
base <- decodeUint256 ret
let cents = base `div` attoUsdPerCent
if cents <= toInteger (maxBound :: Int) then Right (fromInteger cents) else Left AERange
where
args = abiTuple [Dynamic $ abiBytes $ B.replicate len 'a', Static $ uint256 $ toInteger years * secondsPerYear]
-- | The shortest length from the shortest label such that every length up to the longest costs at most these cents
-- for these years, from one read of the oracle's whole price list, which need not fall with the length.
codeLengthFor :: Contracts -> Int -> Int -> IO (Either ChainError (Maybe Int))
codeLengthFor ct cents years = runExceptT $ do
oracle <- ExceptT $ call ct (controller ct) ("prices()", "") decodeAddress
(basePerYear, exceptions) <- ExceptT $ call ct oracle ("prices()", "") decodePrices
let perYear len = M.findWithDefault basePerYear (toInteger len) $ M.fromList exceptions
covered len = perYear len * toInteger years <= toInteger cents
pure $ case takeWhile covered [maxNameLength, maxNameLength - 1 .. minNameLength] of
[] -> Nothing
lens -> Just $ last lens
sponsorNonce :: Contracts -> Address -> IO (Either ChainError Word64)
sponsorNonce ct owner = call ct (cSponsor $ chainConfig ct) ("nonces(address)", addressWord owner) decodeWord64
resolverNonce :: Contracts -> Address -> IO (Either ChainError Word64)
resolverNonce ct owner = call ct (resolver ct) ("nonces(address)", addressWord owner) decodeWord64
-- | Nothing when nobody holds the name: @ownerOf@ then fails the registrar's bare @require@ on the expiry,
-- a revert without data. Any other revert is a failed read.
ownerOfToken :: Contracts -> LabelHash -> IO (Either ChainError (Maybe Address))
ownerOfToken ct lh =
call ct (baseRegistrar ct) ("ownerOf(uint256)", labelHashArg lh) decodeAddress >>= \case
Left (ChainReverted (Just RNWithoutData)) -> pure $ Right Nothing
r -> pure $ Just <$> r
nameExpires :: Contracts -> LabelHash -> IO (Either ChainError Word64)
nameExpires ct lh = call ct (baseRegistrar ct) ("nameExpires(uint256)", labelHashArg lh) decodeWord64
-- | The block time of the commitment, Nothing when the controller holds none.
commitmentTime :: Contracts -> ByteString -> IO (Either ChainError (Maybe Word64))
commitmentTime ct commitment = fmap nonZero <$> call ct (controller ct) ("commitments(bytes32)", commitment) decodeWord64
where
nonZero t = if t == 0 then Nothing else Just t
controllerAvailable :: Contracts -> Text -> IO (Either ChainError Bool)
controllerAvailable ct label = call ct (controller ct) ("available(string)", abiTuple [Dynamic $ abiBytes $ encodeUtf8 label]) decodeBool
commitmentAges :: Contracts -> IO (Either ChainError CommitmentAges)
commitmentAges ct@Contracts {ctCommitmentAges} = readOnce ctCommitmentAges $ runExceptT $ do
caMin <- ExceptT $ call ct (controller ct) ("minCommitmentAge()", "") decodeUint256
caMax <- ExceptT $ call ct (controller ct) ("maxCommitmentAge()", "") decodeUint256
pure CommitmentAges {caMin, caMax}
registrarConstants :: Contracts -> IO (Either ChainError RegistrarConstants)
registrarConstants ct@Contracts {ctRegistrar} = readOnce ctRegistrar $ runExceptT $ do
rcRegistry <- ExceptT $ call ct (baseRegistrar ct) ("ens()", "") decodeAddress
rcGracePeriod <- ExceptT $ call ct (baseRegistrar ct) ("GRACE_PERIOD()", "") decodeUint256
pure RegistrarConstants {rcRegistry, rcGracePeriod}
-- | Concurrent first uses may each read; they read the same immutable values.
readOnce :: IORef (Maybe a) -> IO (Either ChainError a) -> IO (Either ChainError a)
readOnce ref readChain =
readIORef ref >>= \case
Just a -> pure $ Right a
Nothing -> readChain >>= either (pure . Left) (\a -> Right a <$ atomicWriteIORef ref (Just a))
-- | Reads run at once; the first failure in argument order is the answer.
together :: Compose Concurrently (Either ChainError) a -> IO (Either ChainError a)
together = runConcurrently . getCompose
concurrent :: IO (Either ChainError a) -> Compose Concurrently (Either ChainError) a
concurrent = Compose . Concurrently
withinDeadline :: Contracts -> Text -> IO (Either ChainError a) -> IO (Either ChainError a)
withinDeadline Contracts {ctDeadline} what action = fromMaybe (Left $ ChainUnavailable $ what <> ": timed out") <$> timeout ctDeadline action
-- | An answer that does not decode is a node failure, as an RPC error is.
call :: Contracts -> Address -> (ByteString, ByteString) -> (ByteString -> Either AbiError a) -> IO (Either ChainError a)
call Contracts {ctChain} to (signature, args) decode =
(>>= first malformed . decode) <$> ethCall ctChain to (selector signature <> args)
where
malformed e = ChainUnavailable $ "eth_call: " <> decodeLatin1 signature <> " answered " <> tshow e
labelHashArg :: LabelHash -> ByteString
labelHashArg (LabelHash h) = h
nodeArg :: Contracts -> LabelHash -> ByteString
nodeArg Contracts {ctTLD} = nodeBytes . labelNode ctTLD
chainConfig :: Contracts -> ChainConfig
chainConfig = ccConfig . ctChain
controller, resolver, baseRegistrar :: Contracts -> Address
controller = cController . chainConfig
resolver = cResolver . chainConfig
baseRegistrar = cBaseRegistrar . chainConfig
@@ -7,6 +7,7 @@ import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Agent.Store.Common (DBStore)
import StoreService.Config (NamesConfig (..))
import StoreService.Names.Chain (ChainClient, newChainClient)
import StoreService.Names.Contracts (Contracts, newContracts)
import StoreService.Names.Phases (Phases, newPhases)
import StoreService.Names.Profiles (Profiles, chatProfile, defaultProfileLimits, monotonicSeconds, newProfiles, systemResolver)
import StoreService.Names.Turns (Turns, newTurns)
@@ -19,13 +20,15 @@ data NamesEnv = NamesEnv
nePhases :: Phases,
neTurns :: Turns,
neChain :: ChainClient,
neContracts :: Contracts,
neProfiles :: Profiles
}
newNamesEnv :: ChatController -> UserId -> NamesConfig -> DBStore -> IO NamesEnv
newNamesEnv neChat@ChatController {random} neUserId neConfig@NamesConfig {nChain} neStore = do
newNamesEnv neChat@ChatController {random} neUserId neConfig@NamesConfig {nTLD, nChain} neStore = do
nePhases <- newPhases
neTurns <- newTurns
neChain <- newChainClient random nChain
neContracts <- newContracts nTLD neChain
neProfiles <- newProfiles defaultProfileLimits monotonicSeconds (chatProfile neChat neUserId) systemResolver
pure NamesEnv {neChat, neUserId, neConfig, neStore, nePhases, neTurns, neChain, neProfiles}
pure NamesEnv {neChat, neUserId, neConfig, neStore, nePhases, neTurns, neChain, neContracts, neProfiles}
@@ -5,7 +5,11 @@ module StoreService.Names.Registration
( Registration (..),
RegisterIntent (..),
Node,
nodeBytes,
namehash,
labelNode,
contactRecordKey,
channelRecordKey,
sponsorDomain,
resolverDomain,
makeCommitment,
@@ -32,7 +36,7 @@ import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature)
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Eth.Address (Address, recoverAddress)
import Simplex.Messaging.Eth.EIP712 (Eip712Domain (..), Eip712Value (..), hashTypedData, serializeSignature)
import Simplex.Messaging.SimplexName (SimplexTLD)
import Simplex.Messaging.SimplexName (LabelHash (..), SimplexTLD)
import StoreService.Names.Abi (AbiValue (..), abiArray, abiBytes, abiTuple, addressWord, selector, uint256)
data Registration = Registration
@@ -46,9 +50,16 @@ data Registration = Registration
rChannelLink :: Maybe Text
}
-- | Only built by 'namehash', so it always holds 32 bytes.
-- | Only built by 'labelNode', so it always holds 32 bytes.
newtype Node = Node ByteString
nodeBytes :: Node -> ByteString
nodeBytes (Node node) = node
contactRecordKey, channelRecordKey :: ByteString
contactRecordKey = "simplex.contact"
channelRecordKey = "simplex.channel"
data RegisterIntent = RegisterIntent
{ riRegistration :: Registration,
riNonce :: Word64,
@@ -134,7 +145,7 @@ registrationTuple r@Registration {rLabel, rOwner, rDuration, rSecret, rResolver}
recordCalls :: Registration -> [ByteString]
recordCalls Registration {rLabel, rTLD, rContactLink, rChannelLink} =
catMaybes [setText "simplex.contact" <$> rContactLink, setText "simplex.channel" <$> rChannelLink]
catMaybes [setText contactRecordKey <$> rContactLink, setText channelRecordKey <$> rChannelLink]
where
setText key link =
selector "setText(bytes32,string,string)"
@@ -142,6 +153,9 @@ recordCalls Registration {rLabel, rTLD, rContactLink, rChannelLink} =
Node node = namehash rTLD rLabel
namehash :: SimplexTLD -> Text -> Node
namehash tld label = Node $ keccak256 $ tldNode <> keccak256 (encodeUtf8 label)
namehash tld = labelNode tld . LabelHash . keccak256 . encodeUtf8
labelNode :: SimplexTLD -> LabelHash -> Node
labelNode tld (LabelHash h) = Node $ keccak256 $ tldNode <> h
where
tldNode = keccak256 $ B.replicate 32 0 <> keccak256 (B.drop 1 $ strEncode tld)
@@ -55,59 +55,65 @@ data RevertName
| RNTransferToSelf
| RNUnexpiredCommitmentExists
| RNZeroAddress
| -- | a revert with empty or no data, as a bare @require@ or @revert()@ gives
RNWithoutData
deriving (Eq, Show, Enum, Bounded)
revertSignature :: RevertName -> ByteString
-- | Nothing for a revert without data, which has no selector.
revertSignature :: RevertName -> Maybe ByteString
revertSignature = \case
RNAlreadyFrozen -> "AlreadyFrozen()"
RNCommitmentNotFound -> "CommitmentNotFound(bytes32)"
RNCommitmentTooNew -> "CommitmentTooNew(bytes32,uint256,uint256)"
RNCommitmentTooOld -> "CommitmentTooOld(bytes32,uint256,uint256)"
RNDurationTooShort -> "DurationTooShort(uint256)"
RNFrozen -> "Frozen()"
RNInsufficientAllowance -> "InsufficientAllowance(uint256,uint256)"
RNInsufficientValue -> "InsufficientValue()"
RNInvalidEVMAddress -> "InvalidEVMAddress(bytes)"
RNInvalidNonce -> "InvalidNonce()"
RNInvalidSignature -> "InvalidSignature()"
RNLabelTooLong -> "LabelTooLong(uint256,uint256)"
RNMaxCommitmentAgeTooHigh -> "MaxCommitmentAgeTooHigh()"
RNMaxCommitmentAgeTooLow -> "MaxCommitmentAgeTooLow()"
RNMinCharLengthCanOnlyDecrease -> "MinCharLengthCanOnlyDecrease()"
RNMinCharLengthZero -> "MinCharLengthZero()"
RNMinCommitmentAgeZero -> "MinCommitmentAgeZero()"
RNNameNotAvailable -> "NameNotAvailable(string)"
RNNameNotReserved -> "NameNotReserved(string)"
RNNameReserved -> "NameReserved(string)"
RNNameTooShort -> "NameTooShort(string,uint8)"
RNNftGateCanOnlyBeDisabled -> "NftGateCanOnlyBeDisabled()"
RNNftRequired -> "NftRequired()"
RNNodeNotOwnedByRegistrar -> "NodeNotOwnedByRegistrar()"
RNNotBeneficiary -> "NotBeneficiary()"
RNNotNameOwner -> "NotNameOwner()"
RNNotOffered -> "NotOffered()"
RNNotOwnerOrBeneficiary -> "NotOwnerOrBeneficiary()"
RNNotSponsor -> "NotSponsor()"
RNNotSubnameRegistrar -> "NotSubnameRegistrar()"
RNOffsetOutOfBoundsError -> "OffsetOutOfBoundsError(uint256,uint256)"
RNOwnerMustBeCaller -> "OwnerMustBeCaller()"
RNPublicSalesClosed -> "PublicSalesClosed()"
RNReasonRequired -> "ReasonRequired()"
RNRecipientMustBeCaller -> "RecipientMustBeCaller()"
RNReentrantCall -> "ReentrantCall()"
RNResolverRequiredWhenDataSupplied -> "ResolverRequiredWhenDataSupplied()"
RNReverseRecordNotSupported -> "ReverseRecordNotSupported()"
RNSignatureExpired -> "SignatureExpired()"
RNTransferFailed -> "TransferFailed()"
RNTransferToSelf -> "TransferToSelf()"
RNUnexpiredCommitmentExists -> "UnexpiredCommitmentExists(bytes32)"
RNZeroAddress -> "ZeroAddress()"
RNAlreadyFrozen -> Just "AlreadyFrozen()"
RNCommitmentNotFound -> Just "CommitmentNotFound(bytes32)"
RNCommitmentTooNew -> Just "CommitmentTooNew(bytes32,uint256,uint256)"
RNCommitmentTooOld -> Just "CommitmentTooOld(bytes32,uint256,uint256)"
RNDurationTooShort -> Just "DurationTooShort(uint256)"
RNFrozen -> Just "Frozen()"
RNInsufficientAllowance -> Just "InsufficientAllowance(uint256,uint256)"
RNInsufficientValue -> Just "InsufficientValue()"
RNInvalidEVMAddress -> Just "InvalidEVMAddress(bytes)"
RNInvalidNonce -> Just "InvalidNonce()"
RNInvalidSignature -> Just "InvalidSignature()"
RNLabelTooLong -> Just "LabelTooLong(uint256,uint256)"
RNMaxCommitmentAgeTooHigh -> Just "MaxCommitmentAgeTooHigh()"
RNMaxCommitmentAgeTooLow -> Just "MaxCommitmentAgeTooLow()"
RNMinCharLengthCanOnlyDecrease -> Just "MinCharLengthCanOnlyDecrease()"
RNMinCharLengthZero -> Just "MinCharLengthZero()"
RNMinCommitmentAgeZero -> Just "MinCommitmentAgeZero()"
RNNameNotAvailable -> Just "NameNotAvailable(string)"
RNNameNotReserved -> Just "NameNotReserved(string)"
RNNameReserved -> Just "NameReserved(string)"
RNNameTooShort -> Just "NameTooShort(string,uint8)"
RNNftGateCanOnlyBeDisabled -> Just "NftGateCanOnlyBeDisabled()"
RNNftRequired -> Just "NftRequired()"
RNNodeNotOwnedByRegistrar -> Just "NodeNotOwnedByRegistrar()"
RNNotBeneficiary -> Just "NotBeneficiary()"
RNNotNameOwner -> Just "NotNameOwner()"
RNNotOffered -> Just "NotOffered()"
RNNotOwnerOrBeneficiary -> Just "NotOwnerOrBeneficiary()"
RNNotSponsor -> Just "NotSponsor()"
RNNotSubnameRegistrar -> Just "NotSubnameRegistrar()"
RNOffsetOutOfBoundsError -> Just "OffsetOutOfBoundsError(uint256,uint256)"
RNOwnerMustBeCaller -> Just "OwnerMustBeCaller()"
RNPublicSalesClosed -> Just "PublicSalesClosed()"
RNReasonRequired -> Just "ReasonRequired()"
RNRecipientMustBeCaller -> Just "RecipientMustBeCaller()"
RNReentrantCall -> Just "ReentrantCall()"
RNResolverRequiredWhenDataSupplied -> Just "ResolverRequiredWhenDataSupplied()"
RNReverseRecordNotSupported -> Just "ReverseRecordNotSupported()"
RNSignatureExpired -> Just "SignatureExpired()"
RNTransferFailed -> Just "TransferFailed()"
RNTransferToSelf -> Just "TransferToSelf()"
RNUnexpiredCommitmentExists -> Just "UnexpiredCommitmentExists(bytes32)"
RNZeroAddress -> Just "ZeroAddress()"
RNWithoutData -> Nothing
revertSelectors :: Map ByteString RevertName
revertSelectors = M.fromList [(selector $ revertSignature n, n) | n <- [minBound .. maxBound]]
revertSelectors = M.fromList [(selector signature, n) | n <- [minBound .. maxBound], Just signature <- [revertSignature n]]
-- | The error a revert's data names by its selector, if it is one of the contracts' errors.
decodeRevert :: ByteString -> Maybe RevertName
decodeRevert revertData = M.lookup (B.take selectorSize revertData) revertSelectors
decodeRevert revertData
| B.null revertData = Just RNWithoutData
| otherwise = M.lookup (B.take selectorSize revertData) revertSelectors
where
selectorSize = 4
+4
View File
@@ -445,6 +445,7 @@ executable simplex-store-service
StoreService.Names.Abi
StoreService.Names.Chain
StoreService.Names.Codes
StoreService.Names.Contracts
StoreService.Names.Env
StoreService.Names.Label
StoreService.Names.Links
@@ -474,6 +475,7 @@ executable simplex-store-service
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded -rtsopts
build-depends:
aeson ==2.2.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.7 && <5
, base64-bytestring >=1.0 && <1.3
@@ -756,6 +758,7 @@ test-suite simplex-chat-test
Bots.StoreService.NameRegistrationTests
Bots.StoreService.NamesChainTests
Bots.StoreService.NamesCodesTests
Bots.StoreService.NamesContractsTests
Bots.StoreService.NamesLabelTests
Bots.StoreService.NamesLinksTests
Bots.StoreService.NamesProfilesTests
@@ -786,6 +789,7 @@ test-suite simplex-chat-test
StoreService.Names.Abi
StoreService.Names.Chain
StoreService.Names.Codes
StoreService.Names.Contracts
StoreService.Names.Env
StoreService.Names.Label
StoreService.Names.Links
+5 -4
View File
@@ -31,7 +31,7 @@ namesChainTests = describe "StoreService names: chain" $ do
it "reads a dry run's revert with data by its name" $ testDryRunRevertNamed g
it "reads an eth_call revert with data by its name" $ testCallRevertNamed g
it "reads ownerOf of a name nobody holds as a revert" $ testOwnerOfRevert g
it "reads a revert without code 3 or data as a revert" $ testRevertWithoutData g
it "reads a revert without code 3 or data as a revert without data" $ testRevertWithoutData g
it "reads a revert of an unknown error as an unnamed revert" $ testUnknownRevert g
describe "failures" $ do
it "reads an RPC error as unavailable, without the URL" $ testRpcError g
@@ -101,9 +101,10 @@ viemSelectors =
testRevertSelectors :: IO ()
testRevertSelectors = do
map snd viemSelectors `shouldBe` [minBound .. maxBound]
map snd viemSelectors `shouldBe` filter (/= RNWithoutData) [minBound .. maxBound]
[(n, decodeRevert $ unhex sel <> uint256 (0 :: Int)) | (sel, n) <- viemSelectors] `shouldBe` [(n, Just n) | (_, n) <- viemSelectors]
decodeRevert (B.take 3 $ unhex "74480cc9") `shouldBe` Nothing
decodeRevert "" `shouldBe` Just RNWithoutData
testDryRunRevertNamed :: TVar ChaChaDRG -> IO ()
testDryRunRevertNamed g = withChain g $ \node c -> do
@@ -120,12 +121,12 @@ testOwnerOfRevert :: TVar ChaChaDRG -> IO ()
testOwnerOfRevert g = withChain g $ \node c@ChainClient {ccConfig = ChainConfig {cBaseRegistrar}} -> do
let ownerOf = selector "ownerOf(uint256)" <> C.keccak256 "bakery1"
answerCall node cBaseRegistrar ownerOf $ Reverts ""
ethCall c cBaseRegistrar ownerOf `shouldReturn` Left (ChainReverted Nothing)
ethCall c cBaseRegistrar ownerOf `shouldReturn` Left (ChainReverted $ Just RNWithoutData)
testRevertWithoutData :: TVar ChaChaDRG -> IO ()
testRevertWithoutData g = withChain g $ \node c -> do
setFault node "eth_call" $ RpcError (-32000) "execution reverted" Nothing
ethCall c fakeController availableCall `shouldReturn` Left (ChainReverted Nothing)
ethCall c fakeController availableCall `shouldReturn` Left (ChainReverted $ Just RNWithoutData)
testUnknownRevert :: TVar ChaChaDRG -> IO ()
testUnknownRevert g = withChain g $ \node c -> do
@@ -0,0 +1,362 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Bots.StoreService.NamesContractsTests (namesContractsTests) where
import Bots.StoreService.FakeEthNode
import Control.Concurrent.STM (TVar)
import Crypto.Random (ChaChaDRG)
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Time.Clock (NominalDiffTime, diffUTCTime, getCurrentTime)
import Data.Word (Word64)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String (strDecode)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.SimplexName (LabelHash, SimplexTLD (..), labelHash)
import StoreService.Config (ChainConfig (..))
import StoreService.Names.Abi
import StoreService.Names.Chain
import StoreService.Names.Contracts
import StoreService.Names.Registration (labelNode, namehash, nodeBytes)
import StoreService.Names.Revert (RevertName (..))
import Test.Hspec
namesContractsTests :: Spec
namesContractsTests = describe "StoreService names: contracts" $ do
-- the calldata and answers marked viem are viem 2.33.3's encodeFunctionData and encodeFunctionResult
-- with the ABIs of ens-contracts PR #36, so a read the fake node answers sends viem's calldata
g <- runIO C.newRandom
it "builds a name's node from its label hash as viem's namehash" testNode
describe "status" $ do
it "reads a name nobody holds or reserved as available" $ testAvailable g
it "reads a reserved name" $ testReserved g
it "reads a registered name with the registry's owner and resolver" $ testRegistered g
it "keeps a name registered to the last second of its grace period" $ testGracePeriod g
it "reads the registry and the grace period once, and again after a failed read" $ testRegistrarOnce g
describe "records" $ do
it "reads the default resolver's records and the owner's nonce" $ testRecords g
it "reads no records from another resolver" $ testOtherResolver g
describe "prices" $ do
it "quotes 6, 7 and 12 letters at the launch list for 2 years" $ testLaunchPrices g
it "finds the code length for each launch price" $ testLaunchCodeLengths g
it "requires every longer length to be covered when the list is not monotone" $ testNonMonotone g
it "reads the oracle from the controller on each call" $ testOracleReplaced g
describe "other reads" $ do
it "reads the sponsor's and the resolver's nonces from their own contracts" $ testNonces g
it "reads ownerOf's revert without data as nobody" $ testOwnerOfNobody g
it "reads any other ownerOf revert or failure as a failed read" $ testOwnerOfFailed g
it "reads a commitment's time, none as Nothing" $ testCommitmentTime g
it "reads the controller's availability and the name's expiry" $ testAvailability g
it "reads the commitment ages once" $ testCommitmentAges g
describe "failures" $ do
it "fails a status on a failed read, never reading it as available" $ testFailedRead g
it "fails a read whose answer does not decode or does not fit" $ testMalformed g
it "fails a status within its deadline when one method hangs" $ testStatusDeadline g
it "fails records within their deadline when the calls hang" $ testRecordsDeadline g
testNode :: IO ()
testNode = do
nodeBytes (labelNode TLDSimplex $ labelHash "alicebakery") `shouldBe` aliceNode
nodeBytes (namehash TLDSimplex "alicebakery") `shouldBe` aliceNode
testAvailable :: TVar ChaChaDRG -> IO ()
testAvailable g = withContracts g $ \node ct -> do
answerRegistrar node
setBlock node 100 now
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (0 :: Int)
answerCall node fakeController reservedNamesCall $ Returns $ uint256 (0 :: Int)
nameStatus ct alice `shouldReturn` Right NSAvailable
testReserved :: TVar ChaChaDRG -> IO ()
testReserved g = withContracts g $ \node ct -> do
answerRegistrar node
setBlock node 100 now
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (0 :: Int)
-- viem: reservedNames answering Reason 1
answerCall node fakeController reservedNamesCall $ Returns $ unhex "0000000000000000000000000000000000000000000000000000000000000001"
nameStatus ct alice `shouldReturn` Right NSReserved
testRegistered :: TVar ChaChaDRG -> IO ()
testRegistered g = withContracts g $ \node ct -> do
answerRegistrar node
answerRegistered node (resolverOf node)
setBlock node 100 now
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (now + year)
nameStatus ct alice `shouldReturn` Right (NSRegistered $ registeredAlice (resolverOf node) (now + year))
testGracePeriod :: TVar ChaChaDRG -> IO ()
testGracePeriod g = withContracts g $ \node ct -> do
answerRegistrar node
answerRegistered node (resolverOf node)
answerCall node fakeController reservedNamesCall $ Returns $ uint256 (0 :: Int)
let expired = now - year
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 expired
setBlock node 100 $ expired + gracePeriod
nameStatus ct alice `shouldReturn` Right (NSRegistered $ registeredAlice (resolverOf node) expired)
setBlock node 101 $ expired + gracePeriod + 1
nameStatus ct alice `shouldReturn` Right NSAvailable
testRegistrarOnce :: TVar ChaChaDRG -> IO ()
testRegistrarOnce g = withContracts g $ \node ct -> do
setBlock node 100 now
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (0 :: Int)
answerCall node fakeController reservedNamesCall $ Returns $ uint256 (0 :: Int)
nameStatus ct alice `shouldReturn` Left (ChainUnavailable "eth_call: error -32000: no fake answer for this call")
answerRegistrar node
nameStatus ct alice `shouldReturn` Right NSAvailable
answerCall node (registrarOf node) ensCall $ Reverts ""
answerCall node (registrarOf node) gracePeriodCall $ Reverts ""
nameStatus ct alice `shouldReturn` Right NSAvailable
testRecords :: TVar ChaChaDRG -> IO ()
testRecords g = withContracts g $ \node ct -> do
let resolver = resolverOf node
answerCall node resolver contactTextCall $ Returns contactTextAnswer
-- an unset record is the empty string
answerCall node resolver channelTextCall $ Returns $ abiTuple [Dynamic $ abiBytes ""]
answerCall node resolver noncesCall $ Returns $ uint256 (3 :: Int)
nameRecords ct alice (registeredAlice resolver $ now + year)
`shouldReturn` Right (Just NameRecords {rdContact = "https://smp8.simplex.im/a#Y8m3", rdChannel = "", rdNonce = 3})
testOtherResolver :: TVar ChaChaDRG -> IO ()
testOtherResolver g = withContracts g $ \_ ct -> do
nameRecords ct alice (registeredAlice otherResolver $ now + year) `shouldReturn` Right Nothing
testLaunchPrices :: TVar ChaChaDRG -> IO ()
testLaunchPrices g = withContracts g $ \node ct -> do
-- viem: rentPriceUSD answering {base: 40000 * 10^16, premium: 0}
answerCall node fakeController (rentPriceCall 6) $ Returns $ unhex "000000000000000000000000000000000000000000000015af1d78b58c4000000000000000000000000000000000000000000000000000000000000000000000"
answerCall node fakeController (rentPriceCall 7) $ Returns $ rentPriceAnswer 20000
answerCall node fakeController (rentPriceCall 12) $ Returns $ rentPriceAnswer 10000
mapM (\len -> chainPriceCents ct len 2) [6, 7, 12] `shouldReturn` [Right 40000, Right 20000, Right 10000]
testLaunchCodeLengths :: TVar ChaChaDRG -> IO ()
testLaunchCodeLengths g = withContracts g $ \node ct -> do
answerCall node fakeController pricesCall $ Returns $ addressWord oracle
answerCall node oracle pricesCall $ Returns launchPricesAnswer
mapM (\cents -> codeLengthFor ct cents 2) [40000, 20000, 10000, 9999]
`shouldReturn` [Right (Just 6), Right (Just 7), Right (Just 8), Right Nothing]
codeLengthFor ct 20000 4 `shouldReturn` Right (Just 8)
testNonMonotone :: TVar ChaChaDRG -> IO ()
testNonMonotone g = withContracts g $ \node ct -> do
answerCall node fakeController pricesCall $ Returns $ addressWord oracle
answerCall node oracle pricesCall $ Returns $ pricesAnswer 5000 [(6, 15000), (7, 20000)]
mapM (\cents -> codeLengthFor ct cents 2) [30000, 40000] `shouldReturn` [Right (Just 8), Right (Just 6)]
answerCall node oracle pricesCall $ Returns $ pricesAnswer 5000 [(63, 30000)]
mapM (\cents -> codeLengthFor ct cents 2) [59999, 60000] `shouldReturn` [Right Nothing, Right (Just 6)]
testOracleReplaced :: TVar ChaChaDRG -> IO ()
testOracleReplaced g = withContracts g $ \node ct -> do
let newOracle = address "0x8a791620dd6260079bf849dc5567adc3f2fdc318"
answerCall node fakeController pricesCall $ Returns $ addressWord oracle
answerCall node oracle pricesCall $ Returns launchPricesAnswer
answerCall node newOracle pricesCall $ Returns $ pricesAnswer 10000 []
codeLengthFor ct 20000 2 `shouldReturn` Right (Just 7)
answerCall node fakeController pricesCall $ Returns $ addressWord newOracle
codeLengthFor ct 20000 2 `shouldReturn` Right (Just 6)
testNonces :: TVar ChaChaDRG -> IO ()
testNonces g = withContracts g $ \node ct -> do
answerCall node fakeSponsor noncesCall $ Returns $ uint256 (5 :: Int)
answerCall node (resolverOf node) noncesCall $ Returns $ uint256 (3 :: Int)
sponsorNonce ct owner `shouldReturn` Right 5
resolverNonce ct owner `shouldReturn` Right 3
testOwnerOfNobody :: TVar ChaChaDRG -> IO ()
testOwnerOfNobody g = withContracts g $ \node ct -> do
answerCall node (registrarOf node) ownerOfCall $ Returns $ addressWord owner
ownerOfToken ct alice `shouldReturn` Right (Just owner)
-- the registrar's require(expiries[tokenId] > block.timestamp), as anvil answers it with "0x" and reth without data
answerCall node (registrarOf node) ownerOfCall $ Reverts ""
ownerOfToken ct alice `shouldReturn` Right Nothing
setFault node "eth_call" $ RpcError 3 "execution reverted" Nothing
ownerOfToken ct alice `shouldReturn` Right Nothing
testOwnerOfFailed :: TVar ChaChaDRG -> IO ()
testOwnerOfFailed g = withContracts g $ \node ct -> do
-- viem's encodeErrorResult of Error("ERC721: invalid token ID")
answerCall node (registrarOf node) ownerOfCall $ Reverts $ unhex "08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000184552433732313a20696e76616c696420746f6b656e2049440000000000000000"
ownerOfToken ct alice `shouldReturn` Left (ChainReverted Nothing)
-- viem's encodeErrorResult of ReentrantCall()
answerCall node (registrarOf node) ownerOfCall $ Reverts $ unhex "37ed32e8"
ownerOfToken ct alice `shouldReturn` Left (ChainReverted $ Just RNReentrantCall)
setFault node "eth_call" HttpFailure
ownerOfToken ct alice `shouldReturn` Left (ChainUnavailable "eth_call: HTTP 502")
testCommitmentTime :: TVar ChaChaDRG -> IO ()
testCommitmentTime g = withContracts g $ \node ct -> do
answerCall node fakeController commitmentsCall $ Returns $ uint256 (0 :: Int)
commitmentTime ct commitment `shouldReturn` Right Nothing
answerCall node fakeController commitmentsCall $ Returns $ uint256 now
commitmentTime ct commitment `shouldReturn` Right (Just now)
testAvailability :: TVar ChaChaDRG -> IO ()
testAvailability g = withContracts g $ \node ct -> do
answerCall node fakeController availableCall $ Returns $ uint256 (1 :: Int)
controllerAvailable ct "alicebakery" `shouldReturn` Right True
answerCall node fakeController availableCall $ Returns $ uint256 (0 :: Int)
controllerAvailable ct "alicebakery" `shouldReturn` Right False
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (now + year)
nameExpires ct alice `shouldReturn` Right (now + year)
testCommitmentAges :: TVar ChaChaDRG -> IO ()
testCommitmentAges g = withContracts g $ \node ct -> do
answerCall node fakeController minCommitmentAgeCall $ Returns $ uint256 (60 :: Int)
answerCall node fakeController maxCommitmentAgeCall $ Returns $ uint256 (86400 :: Int)
commitmentAges ct `shouldReturn` Right CommitmentAges {caMin = 60, caMax = 86400}
answerCall node fakeController maxCommitmentAgeCall $ Returns $ uint256 (3600 :: Int)
commitmentAges ct `shouldReturn` Right CommitmentAges {caMin = 60, caMax = 86400}
testFailedRead :: TVar ChaChaDRG -> IO ()
testFailedRead g = withContracts g $ \node ct -> do
answerRegistrar node
setBlock node 100 now
answerCall node fakeController reservedNamesCall $ Returns $ uint256 (0 :: Int)
answerCall node (registrarOf node) nameExpiresCall $ Reverts ""
nameStatus ct alice `shouldReturn` Left (ChainReverted $ Just RNWithoutData)
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (0 :: Int)
setFault node "eth_getBlockByNumber" HttpFailure
nameStatus ct alice `shouldReturn` Left (ChainUnavailable "eth_getBlockByNumber: HTTP 502")
clearFault node "eth_getBlockByNumber"
answerCall node fakeController reservedNamesCall $ Reverts ""
nameStatus ct alice `shouldReturn` Left (ChainReverted $ Just RNWithoutData)
testMalformed :: TVar ChaChaDRG -> IO ()
testMalformed g = withContracts g $ \node ct -> do
answerCall node (registrarOf node) nameExpiresCall $ Returns $ B.replicate 31 0
nameExpires ct alice `shouldReturn` Left (ChainUnavailable "eth_call: nameExpires(uint256) answered AEShort")
answerCall node fakeSponsor noncesCall $ Returns $ uint256 (toInteger (maxBound :: Word64) + 1)
sponsorNonce ct owner `shouldReturn` Left (ChainUnavailable "eth_call: nonces(address) answered AERange")
answerCall node fakeController pricesCall $ Returns $ addressWord oracle
answerCall node oracle pricesCall $ Returns $ B.dropEnd 32 launchPricesAnswer
codeLengthFor ct 40000 2 `shouldReturn` Left (ChainUnavailable "eth_call: prices() answered AEShort")
testStatusDeadline :: TVar ChaChaDRG -> IO ()
testStatusDeadline g = withContracts g $ \node ct -> do
answerRegistrar node
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (0 :: Int)
answerCall node fakeController reservedNamesCall $ Returns $ uint256 (0 :: Int)
setFault node "eth_getBlockByNumber" Hang
elapsed <- timed $ nameStatus ct {ctDeadline = testDeadline} alice `shouldReturn` Left (ChainUnavailable "name status: timed out")
elapsed `shouldSatisfy` (< 2)
clearFault node "eth_getBlockByNumber"
nameStatus ct {ctDeadline = testDeadline} alice `shouldReturn` Right NSAvailable
testRecordsDeadline :: TVar ChaChaDRG -> IO ()
testRecordsDeadline g = withContracts g $ \node ct -> do
setFault node "eth_call" Hang
elapsed <- timed $ nameRecords ct {ctDeadline = testDeadline} alice (registeredAlice (resolverOf node) $ now + year) `shouldReturn` Left (ChainUnavailable "name records: timed out")
elapsed `shouldSatisfy` (< 2)
-- fixtures
-- | Far below a call's 10-second timeout, so only the deadline can end a hung read in time.
testDeadline :: Int
testDeadline = 300000
timed :: IO () -> IO NominalDiffTime
timed action = do
start <- getCurrentTime
action
(`diffUTCTime` start) <$> getCurrentTime
withContracts :: TVar ChaChaDRG -> (FakeEthNode -> Contracts -> IO a) -> IO a
withContracts g action = withFakeEthNode $ \node -> do
ct <- newContracts TLDSimplex =<< newChainClient g (fakeChainConfig node)
action node ct
answerRegistrar :: FakeEthNode -> IO ()
answerRegistrar node = do
answerCall node (registrarOf node) ensCall $ Returns $ addressWord registry
answerCall node (registrarOf node) gracePeriodCall $ Returns $ uint256 gracePeriod
answerRegistered :: FakeEthNode -> Address -> IO ()
answerRegistered node resolver = do
answerCall node registry registryOwnerCall $ Returns $ addressWord owner
answerCall node registry registryResolverCall $ Returns $ addressWord resolver
registeredAlice :: Address -> Word64 -> RegisteredName
registeredAlice rnResolver rnExpiresAt = RegisteredName {rnOwner = owner, rnResolver, rnExpiresAt, rnGracePeriod = toInteger gracePeriod}
registrarOf, resolverOf :: FakeEthNode -> Address
registrarOf = cBaseRegistrar . fakeChainConfig
resolverOf = cResolver . fakeChainConfig
rentPriceAnswer :: Integer -> ByteString
rentPriceAnswer cents = uint256 (cents * 10 ^ (16 :: Int)) <> uint256 (0 :: Int)
pricesAnswer :: Integer -> [(Integer, Integer)] -> ByteString
pricesAnswer base exceptions = abiTuple [Static $ uint256 base, Dynamic $ uint256 (length exceptions) <> B.concat [uint256 len <> uint256 cents | (len, cents) <- exceptions]]
-- viem: prices() answering the launch list, base 5000 and [{7, 10000}, {6, 20000}]
launchPricesAnswer :: ByteString
launchPricesAnswer = unhex "0000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000004e20"
-- viem: text answering "https://smp8.simplex.im/a#Y8m3"
contactTextAnswer :: ByteString
contactTextAnswer = unhex "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e68747470733a2f2f736d70382e73696d706c65782e696d2f612359386d330000"
-- 2027-01-01, the term start
now :: Word64
now = 1798761600
year, gracePeriod :: Word64
year = 31536000
gracePeriod = 7776000
alice :: LabelHash
alice = labelHash "alicebakery"
-- viem: namehash("alicebakery.simplex")
aliceNode :: ByteString
aliceNode = unhex "0f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a"
commitment :: ByteString
commitment = unhex "f8096c3f3cfbadc9f3a108d2c586ccc816c0510711c8d6228aa4fa507324d30c"
owner, registry, oracle, otherResolver :: Address
owner = address "0x014f9c09cceedef1b713110d04d3a07d5b5d9b73"
registry = address "0x5fbdb2315678afecb367f032d93f642f64180aa3"
oracle = address "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853"
otherResolver = address "0x4826533b4897376654bb4d4ad88b7fafd0c98528"
-- viem's calldata for the reads
ensCall, gracePeriodCall, nameExpiresCall, ownerOfCall, reservedNamesCall, registryOwnerCall, registryResolverCall :: ByteString
ensCall = unhex "3f15457f"
gracePeriodCall = unhex "c1a287e2"
nameExpiresCall = unhex "d6e4fa86bcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6"
ownerOfCall = unhex "6352211ebcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6"
reservedNamesCall = unhex "389c0748bcc46b7948ff6be55bc3ff4db5bc4be16638f29e818a89244dae11caaed7c9c6"
registryOwnerCall = unhex "02571be30f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a"
registryResolverCall = unhex "0178b8bf0f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a"
contactTextCall, channelTextCall, noncesCall :: ByteString
contactTextCall = unhex "59d1d43c0f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e636f6e746163740000000000000000000000000000000000"
channelTextCall = unhex "59d1d43c0f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e6368616e6e656c0000000000000000000000000000000000"
noncesCall = unhex "7ecebe00000000000000000000000000014f9c09cceedef1b713110d04d3a07d5b5d9b73"
-- | rentPriceUSD("a" × length, 2 years)
rentPriceCall :: Int -> ByteString
rentPriceCall = \case
6 -> unhex "9ff7ec0900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000003c2670000000000000000000000000000000000000000000000000000000000000000066161616161610000000000000000000000000000000000000000000000000000"
7 -> unhex "9ff7ec0900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000003c2670000000000000000000000000000000000000000000000000000000000000000076161616161616100000000000000000000000000000000000000000000000000"
12 -> unhex "9ff7ec0900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000003c26700000000000000000000000000000000000000000000000000000000000000000c6161616161616161616161610000000000000000000000000000000000000000"
len -> error $ "no viem vector for length " <> show len
pricesCall, commitmentsCall, availableCall, minCommitmentAgeCall, maxCommitmentAgeCall :: ByteString
pricesCall = unhex "d3419bf3"
commitmentsCall = unhex "839df945f8096c3f3cfbadc9f3a108d2c586ccc816c0510711c8d6228aa4fa507324d30c"
availableCall = unhex "aeb8ce9b0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b616c69636562616b657279000000000000000000000000000000000000000000"
minCommitmentAgeCall = unhex "8d839ffe"
maxCommitmentAgeCall = unhex "ce1e09c0"
unhex :: ByteString -> ByteString
unhex = either error id . convertFromBase Base16
address :: ByteString -> Address
address = either error id . strDecode
+2
View File
@@ -12,6 +12,7 @@ import Bots.StoreService.GroupTests
import Bots.StoreService.NameRegistrationTests
import Bots.StoreService.NamesChainTests
import Bots.StoreService.NamesCodesTests
import Bots.StoreService.NamesContractsTests
import Bots.StoreService.NamesLabelTests
import Bots.StoreService.NamesLinksTests
import Bots.StoreService.NamesProfilesTests
@@ -97,6 +98,7 @@ main = do
namesConfigTests
namesChainTests
namesCodesTests
namesContractsTests
namesLabelTests
namesLinksTests
namesProfilesTests