Files
simplex-chat/tests/Bots/StoreService/NamesContractsTests.hs
T
2026-10-22 02:57:48 +00:00

418 lines
23 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Bots.StoreService.NamesContractsTests
( namesContractsTests,
withContracts,
answerRegistrar,
answerRegistered,
registrarOf,
resolverOf,
rentPriceCall,
rentPriceAnswer,
now,
year,
gracePeriod,
otherResolver,
nameExpiresCall,
reservedNamesCall,
contactTextCall,
channelTextCall,
noncesCall,
pricesCall,
launchPricesAnswer,
oracle,
alice,
unhex,
)
where
import Bots.StoreService.FakeEthNode
import Control.Concurrent.STM (TVar)
import Control.Monad (forM_)
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
it "reads a status and records within a deadline only concurrent reads meet" $ testConcurrentReads 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})
textRecord ct alice "simplex.contact" `shouldReturn` Right "https://smp8.simplex.im/a#Y8m3"
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)
-- a node or proxy that keeps the reason in the message but drops the data
setFault node "eth_call" $ RpcError (-32000) "execution reverted: ERC721: invalid token ID" Nothing
ownerOfToken ct alice `shouldReturn` Left (ChainReverted Nothing)
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)
-- | Each slow read takes a third of the deadline or more, so the status's reads in sequence (four) or the records' (three)
-- pass it, while their concurrent rounds (two and one) meet it.
testConcurrentReads :: TVar ChaChaDRG -> IO ()
testConcurrentReads g = withContracts g $ \node ct -> do
let resolver = resolverOf node
ct' = ct {ctDeadline = concurrentDeadline}
registered = registeredAlice resolver (now + year)
answerRegistrar node
answerRegistered node resolver
setBlock node 100 now
answerCall node (registrarOf node) nameExpiresCall $ Returns $ uint256 (now + year)
answerCall node resolver contactTextCall $ Returns contactTextAnswer
answerCall node resolver channelTextCall $ Returns $ abiTuple [Dynamic $ abiBytes ""]
answerCall node resolver noncesCall $ Returns $ uint256 (3 :: Int)
-- the registrar's constants are read once, so this read takes them out of the timed ones
nameStatus ct alice `shouldReturn` Right (NSRegistered registered)
forM_ ["eth_call", "eth_getBlockByNumber"] $ \m -> setFault node m $ Slow $ concurrentDeadline * 2 `div` 5
nameStatus ct' alice `shouldReturn` Right (NSRegistered registered)
forM_ ["eth_call", "eth_getBlockByNumber"] $ \m -> setFault node m $ Slow $ concurrentDeadline * 3 `div` 5
nameRecords ct' alice registered `shouldReturn` Right (Just NameRecords {rdContact = "https://smp8.simplex.im/a#Y8m3", rdChannel = "", rdNonce = 3})
-- fixtures
concurrentDeadline :: Int
concurrentDeadline = 1500000
-- | 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"
11 -> unhex "9ff7ec0900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000003c26700000000000000000000000000000000000000000000000000000000000000000b6161616161616161616161000000000000000000000000000000000000000000"
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