mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 19:27:55 +00:00
779 lines
38 KiB
Haskell
779 lines
38 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Bots.StoreService.NamesRecordsTests (namesRecordsTests) where
|
|
|
|
import Bots.StoreService.FakeEthNode
|
|
import Bots.StoreService.NamesStoreTests (t0)
|
|
import Bots.StoreService.WebTests (withServiceStore)
|
|
import Bots.StoreService.WebUIMock (EphemeralWallet (..), newEphemeralWallet, walletFromCode)
|
|
import Control.Concurrent.Async (Async, async, concurrently, wait)
|
|
import Control.Concurrent.STM
|
|
import Control.Monad (forM_)
|
|
import Crypto.Number.Serialize (i2ospOf_, os2ip)
|
|
import qualified Crypto.PubKey.ECC.Types as ECC
|
|
import Crypto.Random (ChaChaDRG)
|
|
import Data.Aeson ((.=))
|
|
import qualified Data.Aeson as J
|
|
import qualified Data.Aeson.KeyMap as KM
|
|
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
|
|
import Data.ByteString (ByteString)
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.ByteString.Lazy as LB
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime)
|
|
import Data.Word (Word64)
|
|
import Network.HTTP.Client (RequestBody (..), defaultManagerSettings, httpLbs, method, newManager, parseRequest, requestBody, requestHeaders, responseBody, responseHeaders, responseStatus)
|
|
import Network.HTTP.Types (hContentType, statusCode)
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
|
|
import Simplex.Messaging.Agent.Store.DB (Binary (..))
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Crypto.Secp256k1 (RecoverableSignature (..), signRecoverable)
|
|
import Simplex.Messaging.Encoding.String (strDecode)
|
|
import Simplex.Messaging.Eth.Address (Address, recoverAddress, unAddress)
|
|
import Simplex.Messaging.Eth.EIP712 (Eip712Domain, serializeSignature)
|
|
import Simplex.Messaging.SimplexName (LabelHash (..), SimplexTLD (..), labelHash)
|
|
import StoreService.Config (ChainConfig (..))
|
|
import StoreService.Names.Abi
|
|
import StoreService.Names.Chain
|
|
import StoreService.Names.Contracts (newContracts)
|
|
import StoreService.Names.Records
|
|
import StoreService.Names.Registration (Node, clearRecordsDigest, clearRecordsWithSigCalldata, labelNode, nodeBytes, resolverDomain, setTextDigest, setTextWithSigCalldata)
|
|
import StoreService.Names.Sender (Sender, followTransactions, newSender)
|
|
import StoreService.Names.Signatures (parseSignature)
|
|
import StoreService.Names.Store (EditState (..), NewRecordEdit (..), RecordChange (..), RecordEdit (..), getRecordEditByBody, insertRecordEdit)
|
|
import StoreService.Names.Turns (newTurns)
|
|
import StoreService.Web.Names.Records (EditInvalid (..), editRequest, recordsHandler)
|
|
import System.Timeout (timeout)
|
|
import Test.Hspec
|
|
#if defined(dbPostgres)
|
|
import Database.PostgreSQL.Simple (Only (..))
|
|
#else
|
|
import Database.SQLite.Simple (Only (..))
|
|
#endif
|
|
|
|
namesRecordsTests :: Spec
|
|
namesRecordsTests = describe "StoreService names: records" $ do
|
|
-- the signatures and calldata marked viem are viem 2.57.5's, by the wallet of "abandon ... about" (account 0),
|
|
-- with the SimplexResolver ABI of ens-contracts PR #36 at 23892cc
|
|
g <- runIO C.newRandom
|
|
describe "requests" $ do
|
|
it "signs and encodes SetText and ClearRecords edits as viem does" $ testViemVectors g
|
|
it "reads a request's formats, then its link, then its signature" testRequestOrder
|
|
it "bounds a link at 4096 bytes and takes only a link of the key's kind" testLinkBound
|
|
it "refuses a high s, though recovery accepts it" testHighS
|
|
describe "relay" $ do
|
|
it "relays an edit with viem's calldata and answers its hash once mined" $ testRelayMined g
|
|
it "refuses a wrong signer or a passed deadline before any row or gas" $ testRefusedBeforeRow g
|
|
it "refuses an edit of a name on another resolver, or past its expiry and grace, before any row or gas" $ testRefusedNameState g
|
|
it "leaves the owner's cap untouched by ten edits a non-owner signs" $ testNonOwnerCap g
|
|
it "checks a refused edit's repeat anew" $ testRefusedRepeat g
|
|
it "refuses the eleventh relayed edit of a name within an hour, with the real wait" $ testCap g
|
|
it "answers provider_unavailable and stores nothing when the chain fails before the send" $ testUnavailableBeforeSend g
|
|
it "stores no edit whose dry run reverted, so eleven of them leave the cap to sent edits" $ testDryRunNotCounted g
|
|
it "checks a dry-run-reverted edit's repeat anew" $ testDryRunRepeat g
|
|
it "answers provider_unavailable and stores nothing when the re-read after a dry run's revert fails" $ testDryRunReReadFails g
|
|
it "lets a concurrent twin lose the unique insert before any signing, and answers both" $ testConcurrentTwin g
|
|
it "answers edit_pending after the wait, and its repeat with the hash once mined" $ testPendingThenMined g
|
|
it "answers edit_pending at once when the chain stops answering after the send" $ testChainStopsAfterSend g
|
|
it "bounds the wait for edit_pending from the request's arrival, counting the checks and the send" $ testPendingFromArrival g
|
|
describe "outcomes" $ do
|
|
it "answers an InvalidNonce revert of an edit another relayer applied as mined with no hash, and its repeat too" $ testAppliedByAnother g
|
|
it "answers a dry run's InvalidNonce revert of an edit another relayer applied as applied with no hash, storing nothing" $ testDryRunAppliedByAnother g
|
|
it "answers a SignatureExpired revert of an edit another relayer applied as mined with no hash" $ testExpiredAppliedByAnother g
|
|
it "answers record_reverted InvalidNonce when another body used the nonce with another value" $ testNonceTakenByAnother g
|
|
it "relays a clear edit with viem's calldata, applied by another relayer when both records read empty" $ testClearApplied g
|
|
it "answers a clear edit record_reverted InvalidNonce when either record holds a value after the nonce moved" $ testClearNonceTaken g
|
|
it "frees a reverted edit's nonce for a new body, and answers the reverted edit's repeat while the new one is pending" $ testRevertFreesNonce g
|
|
it "keeps an edit pending when the re-read after a mined revert fails, and settles it in a later round" $ testFailedReRead g
|
|
describe "restarts" $ do
|
|
it "sends a saved edit that was never sent, and follows a sent one to its receipt" $ testResume g
|
|
it "forgets a saved edit that was never sent once its dry run reverts or its deadline passes" $ testUnsentForgotten g
|
|
it "goes on to the next edit when one row fails to decode" $ testBadRowSkipped g
|
|
it "answers an identical repeat after a restart from the stored edit, with no chain read" $ testRepeatAfterRestart g
|
|
it "finds an edit that expired across a restart applied, and makes it mined" $ testRestartExpiredApplied g
|
|
it "frees an edit past its deadline while its transaction is still rebroadcast" $ testExpiryFreesNonce g
|
|
describe "http" $ do
|
|
it "answers the contract's statuses and bodies" $ testHttp g
|
|
it "answers 503, 504, and 429 with the real wait in Retry-After" $ testHttpUnsettled g
|
|
|
|
data Setup = Setup
|
|
{ node :: FakeEthNode,
|
|
store :: DBStore,
|
|
clock :: TVar UTCTime,
|
|
chain :: ChainClient,
|
|
random :: TVar ChaChaDRG,
|
|
sender :: Sender,
|
|
records :: Records,
|
|
owner :: EphemeralWallet
|
|
}
|
|
|
|
-- | A registered alicebakery on the default resolver, held by the viem wallet, its nonce 0 and no records; two confirmations.
|
|
withSetup :: TVar ChaChaDRG -> (Setup -> IO a) -> IO a
|
|
withSetup random action = withFakeEthNode $ \node -> withServiceStore $ \store -> do
|
|
clock <- newTVarIO t0
|
|
chain <- newChainClient random (fakeChainConfig node) {cConfirmations = 2}
|
|
owner <- walletFromCode random viemCode
|
|
(sender, records) <- start store clock chain random
|
|
let s = Setup {node, store, clock, chain, random, sender, records, owner}
|
|
answerName s
|
|
action s
|
|
|
|
start :: DBStore -> TVar UTCTime -> ChainClient -> TVar ChaChaDRG -> IO (Sender, Records)
|
|
start store clock chain random = do
|
|
sender <- newSender random chain store (readTVarIO clock)
|
|
contracts <- newContracts TLDSimplex chain
|
|
turns <- newTurns
|
|
rs <- newRecords store turns contracts chain sender TLDSimplex (readTVarIO clock)
|
|
pure (sender, rs {rsPendingAfter = longWait})
|
|
|
|
-- | A new sender and lane over the same store and node, as after the service restarts.
|
|
restart :: Setup -> IO Setup
|
|
restart s@Setup {store, clock, chain, random} = do
|
|
(sender, records) <- start store clock chain random
|
|
pure s {sender, records}
|
|
|
|
answerName :: Setup -> IO ()
|
|
answerName s@Setup {node, owner = EphemeralWallet {ewAddress}} = do
|
|
answerCall node fakeBaseRegistrar (selector "ens()") $ Returns $ addressWord registry
|
|
answerCall node fakeBaseRegistrar (selector "GRACE_PERIOD()") $ Returns $ uint256 gracePeriod
|
|
answerCall node fakeBaseRegistrar (selector "nameExpires(uint256)" <> unLabel alice) $ Returns $ uint256 nameExpiry
|
|
answerCall node registry (selector "owner(bytes32)" <> nodeBytes aliceNode) $ Returns $ addressWord ewAddress
|
|
answerCall node registry (selector "resolver(bytes32)" <> nodeBytes aliceNode) $ Returns $ addressWord fakeResolver
|
|
setNonce s 0
|
|
setRecord s contactKey ""
|
|
setRecord s channelKey ""
|
|
|
|
setNonce :: Setup -> Word64 -> IO ()
|
|
setNonce s@Setup {node} n = answerCall node fakeResolver (noncesCall s) $ Returns $ uint256 n
|
|
|
|
noncesCall :: Setup -> ByteString
|
|
noncesCall Setup {owner = EphemeralWallet {ewAddress}} = selector "nonces(address)" <> addressWord ewAddress
|
|
|
|
setRecord :: Setup -> Text -> Text -> IO ()
|
|
setRecord Setup {node} key value =
|
|
answerCall node fakeResolver (selector "text(bytes32,string)" <> abiTuple [Static $ nodeBytes aliceNode, Dynamic $ abiBytes $ encodeUtf8 key]) $
|
|
Returns $ abiTuple [Dynamic $ abiBytes $ encodeUtf8 value]
|
|
|
|
signEdit :: Setup -> EphemeralWallet -> RecordChange -> Word64 -> Word64 -> IO EditRequest
|
|
signEdit Setup {random} EphemeralWallet {ewKey} change nonce erDeadline = do
|
|
sig <- signRecoverable random ewKey (digestOf change nonce erDeadline) >>= either fail pure
|
|
pure EditRequest {erLabelHash = alice, erChange = change, erNonce = nonce, erDeadline, erSignature = sig}
|
|
|
|
channel :: Text -> RecordChange
|
|
channel = RCSetText channelKey
|
|
|
|
-- | The owner's channel edit with nonce 0 and the default deadline.
|
|
ownerEdit :: Setup -> Text -> IO EditRequest
|
|
ownerEdit s@Setup {owner} value = signEdit s owner (channel value) 0 deadline
|
|
|
|
relay :: Setup -> EditRequest -> IO (Async EditAnswer)
|
|
relay Setup {records} = async . relayEdit records
|
|
|
|
-- | Mines the n-th transaction sent with this status, one block deeper, then runs the sender's round and the lane's.
|
|
mineSent :: Setup -> Int -> Bool -> IO TxHash
|
|
mineSent Setup {node, sender, records} n ok = do
|
|
raws <- within $ awaitSent node n
|
|
let h = C.keccak256 $ raws !! (n - 1)
|
|
_ <- mine node [(h, ok)]
|
|
_ <- mine node []
|
|
followTransactions sender
|
|
recordsRound records
|
|
pure $ TxHash h
|
|
|
|
answerOf :: Async EditAnswer -> IO EditAnswer
|
|
answerOf = within . wait
|
|
|
|
within :: IO a -> IO a
|
|
within action = timeout 5000000 action >>= maybe (fail "timed out") pure
|
|
|
|
stateOf :: Setup -> EditRequest -> IO (Maybe EditState)
|
|
stateOf Setup {store} req = withTransaction store $ \db -> fmap reState <$> getRecordEditByBody db (bodyHashOf req)
|
|
|
|
editRows :: Setup -> IO Int
|
|
editRows Setup {store} = withTransaction store $ \db -> do
|
|
[Only n] <- DB.query_ db "SELECT count(*) FROM sx_badge_service_name_record_edits"
|
|
pure n
|
|
|
|
-- | As the relay keys a sent edit: the signed digest and the signature.
|
|
bodyHashOf :: EditRequest -> ByteString
|
|
bodyHashOf EditRequest {erChange, erNonce, erDeadline, erSignature} = C.sha256Hash $ digestOf erChange erNonce erDeadline <> serializeSignature erSignature
|
|
|
|
-- | Saves an edit as a request does before it joins the sender, as if the service stopped right after.
|
|
saveUnsent :: Setup -> EditRequest -> IO ()
|
|
saveUnsent Setup {store, clock, owner = EphemeralWallet {ewAddress}} req@EditRequest {erChange, erNonce, erDeadline, erSignature} = do
|
|
now <- readTVarIO clock
|
|
Just _ <- withTransaction store $ \db ->
|
|
insertRecordEdit db NewRecordEdit {nreLabelHash = alice, nreOwner = ewAddress, nreNonce = erNonce, nreChange = erChange, nreDeadline = erDeadline, nreSignature = erSignature, nreBodyHash = bodyHashOf req} now
|
|
pure ()
|
|
|
|
passTime :: Setup -> NominalDiffTime -> IO ()
|
|
passTime Setup {clock} t = atomically $ modifyTVar' clock (addUTCTime t)
|
|
|
|
-- requests
|
|
|
|
testViemVectors :: TVar ChaChaDRG -> IO ()
|
|
testViemVectors g = withSetup g $ \s@Setup {owner = EphemeralWallet {ewAddress}} -> do
|
|
ewAddress `shouldBe` viemAddress
|
|
EditRequest {erSignature = setSig} <- ownerEdit s viemChannelLink
|
|
setSig `shouldBe` viemSetSignature
|
|
recoverAddress setSig (digestOf (channel viemChannelLink) 0 deadline) `shouldReturn` Right viemAddress
|
|
setTextWithSigCalldata aliceNode channelKey viemChannelLink 0 deadline setSig `shouldBe` viemSetCall
|
|
EditRequest {erSignature = clearSig} <- signEdit s (owner s) RCClear 1 deadline
|
|
clearSig `shouldBe` viemClearSignature
|
|
clearRecordsWithSigCalldata aliceNode 1 deadline clearSig `shouldBe` viemClearCall
|
|
|
|
testRequestOrder :: IO ()
|
|
testRequestOrder = do
|
|
let valid = requestJson [("key", J.String channelKey), ("value", J.String viemChannelLink)]
|
|
fmap erChange (editRequest valid) `shouldBe` Right (channel viemChannelLink)
|
|
fmap erChange (editRequest $ requestJson [("clear", J.Bool True)]) `shouldBe` Right RCClear
|
|
fmap erChange (editRequest $ requestJson [("key", J.String contactKey), ("value", "")]) `shouldBe` Right (RCSetText contactKey "")
|
|
-- formats first: a bad label hash or key is bad_request even with a bad link
|
|
editRequest (KM.insert "labelHash" (J.String $ "0x" <> T.toUpper (T.drop 2 aliceHashText)) $ withLink "https://x/a") `shouldBe` Left EIBadRequest
|
|
editRequest (requestJson [("key", "simplex.other"), ("value", J.String viemChannelLink)]) `shouldBe` Left EIBadRequest
|
|
editRequest (KM.insert "nonce" (J.Number (-1)) valid) `shouldBe` Left EIBadRequest
|
|
editRequest (KM.insert "deadline" (J.String "1767229200") valid) `shouldBe` Left EIBadRequest
|
|
editRequest (KM.delete "value" valid) `shouldBe` Left EIBadRequest
|
|
-- the link before the signature
|
|
editRequest (KM.insert "signature" "0x00" $ withLink "https://x/a") `shouldBe` Left EIInvalidLink
|
|
editRequest (KM.insert "signature" "0x00" valid) `shouldBe` Left EIBadRequest
|
|
where
|
|
withLink link = requestJson [("key", J.String channelKey), ("value", J.String link)]
|
|
|
|
testLinkBound :: IO ()
|
|
testLinkBound = do
|
|
let prefix = "https://smp8.simplex.im/c#"
|
|
linkOf n = prefix <> T.replicate (n - T.length prefix) "a"
|
|
edit key value = editRequest $ requestJson [("key", J.String key), ("value", J.String value)]
|
|
fmap erChange (edit channelKey $ linkOf 4096) `shouldBe` Right (channel $ linkOf 4096)
|
|
edit channelKey (linkOf 4097) `shouldBe` Left EIInvalidLink
|
|
edit contactKey viemChannelLink `shouldBe` Left EIInvalidLink
|
|
edit channelKey "https://smp8.simplex.im/c#Y8m3;https://smp9.simplex.im/c#Z1" `shouldBe` Left EIInvalidLink
|
|
|
|
testHighS :: IO ()
|
|
testHighS = do
|
|
let RecoverableSignature {rsR, rsS, rsRecId} = viemSetSignature
|
|
highS = i2ospOf_ 32 $ curveOrder - os2ip rsS
|
|
flipped = RecoverableSignature {rsR, rsS = highS, rsRecId = 1 - rsRecId}
|
|
recoverAddress flipped (digestOf (channel viemChannelLink) 0 deadline) `shouldReturn` Right viemAddress
|
|
editRequest (KM.insert "signature" (J.String $ hex $ serializeSignature flipped) $ requestJson [("key", J.String channelKey), ("value", J.String viemChannelLink)])
|
|
`shouldBe` Left EIBadRequest
|
|
where
|
|
curveOrder = ECC.ecc_n $ ECC.common_curve $ ECC.getCurveByName ECC.SEC_p256k1
|
|
|
|
-- relay
|
|
|
|
testRelayMined :: TVar ChaChaDRG -> IO ()
|
|
testRelayMined g = withSetup g $ \s@Setup {node} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
[raw] <- within $ awaitSent node 1
|
|
fmap (\TxRequest {trTo, trData} -> (trTo, trData)) (decodeSignedTx raw) `shouldBe` Right (fakeResolver, viemSetCall)
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
stateOf s req `shouldReturn` Just (ESMined $ Just h)
|
|
|
|
testRefusedBeforeRow :: TVar ChaChaDRG -> IO ()
|
|
testRefusedBeforeRow g = withSetup g $ \s@Setup {node, records} -> do
|
|
other <- newEphemeralWallet g
|
|
(relayEdit records =<< signEdit s other (channel viemChannelLink) 0 deadline) `shouldReturn` EARefused
|
|
setBlock node 2 deadline
|
|
(relayEdit records =<< ownerEdit s viemChannelLink) `shouldReturn` EARefused
|
|
editRows s `shouldReturn` 0
|
|
sentTransactions node `shouldReturn` []
|
|
|
|
testRefusedNameState :: TVar ChaChaDRG -> IO ()
|
|
testRefusedNameState g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
answerCall node registry (selector "resolver(bytes32)" <> nodeBytes aliceNode) $ Returns $ addressWord otherResolver
|
|
relayEdit records req `shouldReturn` EARefused
|
|
answerCall node registry (selector "resolver(bytes32)" <> nodeBytes aliceNode) $ Returns $ addressWord fakeResolver
|
|
-- the registry still names the owner, but the name lapsed: expiry and grace are before the latest block
|
|
answerCall node fakeBaseRegistrar (selector "nameExpires(uint256)" <> unLabel alice) $ Returns $ uint256 (genesis - gracePeriod - 1)
|
|
answerCall node fakeController (selector "reservedNames(bytes32)" <> unLabel alice) $ Returns $ uint256 (0 :: Int)
|
|
relayEdit records req `shouldReturn` EARefused
|
|
editRows s `shouldReturn` 0
|
|
sentTransactions node `shouldReturn` []
|
|
|
|
testNonOwnerCap :: TVar ChaChaDRG -> IO ()
|
|
testNonOwnerCap g = withSetup g $ \s@Setup {node, records} -> do
|
|
other <- newEphemeralWallet g
|
|
forM_ [0 .. 9] $ \n -> (relayEdit records =<< signEdit s other (channel viemChannelLink) n deadline) `shouldReturn` EARefused
|
|
editRows s `shouldReturn` 0
|
|
r <- relay s =<< ownerEdit s viemChannelLink
|
|
_ <- within $ awaitSent node 1
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
|
|
testRefusedRepeat :: TVar ChaChaDRG -> IO ()
|
|
testRefusedRepeat g = withSetup g $ \s@Setup {owner} -> do
|
|
req <- signEdit s owner (channel viemChannelLink) 1 deadline
|
|
relayEdit (records s) req `shouldReturn` EARefused
|
|
stateOf s req `shouldReturn` Nothing
|
|
setNonce s 1
|
|
r <- relay s req
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
|
|
testCap :: TVar ChaChaDRG -> IO ()
|
|
testCap g = withSetup g $ \s@Setup {owner} -> do
|
|
let quick = s {records = (records s) {rsPendingAfter = shortWait}}
|
|
forM_ [0 .. 9] $ \n -> do
|
|
setNonce s n
|
|
(relayEdit (records quick) =<< signEdit s owner (channel viemChannelLink) n deadline) `shouldReturn` EAPending
|
|
setNonce s 10
|
|
eleventh <- signEdit s owner (channel viemChannelLink) 10 deadline
|
|
passTime s 1000
|
|
relayEdit (records s) eleventh `shouldReturn` EACapped 2600
|
|
editRows s `shouldReturn` 10
|
|
passTime s 2601
|
|
relayEdit (records quick) eleventh `shouldReturn` EAPending
|
|
editRows s `shouldReturn` 11
|
|
|
|
testUnavailableBeforeSend :: TVar ChaChaDRG -> IO ()
|
|
testUnavailableBeforeSend g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
setFault node "eth_call" HttpFailure
|
|
relayEdit records req `shouldReturn` EAUnavailable
|
|
clearFault node "eth_call"
|
|
setFault node "eth_maxPriorityFeePerGas" HttpFailure
|
|
relayEdit records req `shouldReturn` EAUnavailable
|
|
editRows s `shouldReturn` 0
|
|
sentTransactions node `shouldReturn` []
|
|
clearFault node "eth_maxPriorityFeePerGas"
|
|
r <- relay s req
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
|
|
testDryRunNotCounted :: TVar ChaChaDRG -> IO ()
|
|
testDryRunNotCounted g = withSetup g $ \s@Setup {node, records, owner} -> do
|
|
setFault node "eth_estimateGas" $ revertFault $ selector "InvalidSignature()"
|
|
forM_ [1 .. 11 :: Int] $ \n ->
|
|
(relayEdit records =<< signEdit s owner (channel $ "https://smp8.simplex.im/c#n" <> T.pack (show n)) 0 deadline)
|
|
`shouldReturn` EAReverted (Just "InvalidSignature")
|
|
editRows s `shouldReturn` 0
|
|
clearFault node "eth_estimateGas"
|
|
r <- relay s =<< ownerEdit s viemChannelLink
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
|
|
testDryRunRepeat :: TVar ChaChaDRG -> IO ()
|
|
testDryRunRepeat g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
revertDryRun node viemSetCall $ Just $ selector "InvalidSignature()"
|
|
relayEdit records req `shouldReturn` EAReverted (Just "InvalidSignature")
|
|
stateOf s req `shouldReturn` Nothing
|
|
revertDryRun node viemSetCall Nothing
|
|
r <- relay s req
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
|
|
testDryRunReReadFails :: TVar ChaChaDRG -> IO ()
|
|
testDryRunReReadFails g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
-- the nonce check reads it, the re-read after the revert finds no answer
|
|
answerCallOnce node fakeResolver (noncesCall s) $ Returns $ uint256 (0 :: Int)
|
|
forgetCall node fakeResolver (noncesCall s)
|
|
revertDryRun node viemSetCall $ Just $ selector "InvalidSignature()"
|
|
relayEdit records req `shouldReturn` EAUnavailable
|
|
editRows s `shouldReturn` 0
|
|
sentTransactions node `shouldReturn` []
|
|
|
|
testConcurrentTwin :: TVar ChaChaDRG -> IO ()
|
|
testConcurrentTwin g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
-- both pass the repeat lookup before either inserts
|
|
setFault node "eth_getBlockByNumber" $ Slow 300000
|
|
answers <- async $ concurrently (relayEdit records req) (relayEdit records req)
|
|
_ <- within $ awaitSent node 1
|
|
clearFault node "eth_getBlockByNumber"
|
|
h <- mineSent s 1 True
|
|
within (wait answers) `shouldReturn` (EAApplied (Just h), EAApplied (Just h))
|
|
length <$> sentTransactions node `shouldReturn` 1
|
|
editRows s `shouldReturn` 1
|
|
|
|
testPendingThenMined :: TVar ChaChaDRG -> IO ()
|
|
testPendingThenMined g = withSetup g $ \s@Setup {records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
relayEdit records {rsPendingAfter = shortWait} req `shouldReturn` EAPending
|
|
h <- mineSent s 1 True
|
|
relayEdit records req `shouldReturn` EAApplied (Just h)
|
|
|
|
testPendingFromArrival :: TVar ChaChaDRG -> IO ()
|
|
testPendingFromArrival g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
-- three block reads before the send: the deadline check, the name status and the market's fees
|
|
setFault node "eth_getBlockByNumber" $ Slow slowBlock
|
|
arrived <- getCurrentTime
|
|
answer <- timeout (4 * waitFromArrival) $ relayEdit records {rsPendingAfter = waitFromArrival} req
|
|
elapsed <- (`diffUTCTime` arrived) <$> getCurrentTime
|
|
answer `shouldBe` Just EAPending
|
|
length <$> sentTransactions node `shouldReturn` 1
|
|
-- the checks and the send take about 4.5 s, so a wait started after them would end near 7 s
|
|
elapsed `shouldSatisfy` (< 5.6)
|
|
where
|
|
slowBlock = 1500000
|
|
waitFromArrival = 2500000
|
|
|
|
testChainStopsAfterSend :: TVar ChaChaDRG -> IO ()
|
|
testChainStopsAfterSend g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
_ <- within $ awaitSent node 1
|
|
setFault node "eth_getBlockByNumber" HttpFailure
|
|
recordsRound records
|
|
answerOf r `shouldReturn` EAPending
|
|
relayEdit records req `shouldReturn` EAPending
|
|
clearFault node "eth_getBlockByNumber"
|
|
h <- mineSent s 1 True
|
|
relayEdit records req `shouldReturn` EAApplied (Just h)
|
|
|
|
-- outcomes
|
|
|
|
testAppliedByAnother :: TVar ChaChaDRG -> IO ()
|
|
testAppliedByAnother g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
_ <- within $ awaitSent node 1
|
|
-- another relayer's transaction with the same signed edit mined first
|
|
setNonce s 1
|
|
setRecord s channelKey viemChannelLink
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAApplied Nothing
|
|
stateOf s req `shouldReturn` Just (ESMined Nothing)
|
|
relayEdit records req `shouldReturn` EAApplied Nothing
|
|
|
|
testDryRunAppliedByAnother :: TVar ChaChaDRG -> IO ()
|
|
testDryRunAppliedByAnother g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
-- the nonce check reads 0; another relayer applies the edit before the dry run
|
|
answerCallOnce node fakeResolver (noncesCall s) $ Returns $ uint256 (0 :: Int)
|
|
setNonce s 1
|
|
setRecord s channelKey viemChannelLink
|
|
revertDryRun node viemSetCall $ Just $ selector "InvalidNonce()"
|
|
relayEdit records req `shouldReturn` EAApplied Nothing
|
|
editRows s `shouldReturn` 0
|
|
sentTransactions node `shouldReturn` []
|
|
|
|
testExpiredAppliedByAnother :: TVar ChaChaDRG -> IO ()
|
|
testExpiredAppliedByAnother g = withSetup g $ \s@Setup {node} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
_ <- within $ awaitSent node 1
|
|
setBlock node 300 deadline
|
|
setNonce s 1
|
|
setRecord s channelKey viemChannelLink
|
|
-- mined in the block after the deadline, so it reverted SignatureExpired
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAApplied Nothing
|
|
stateOf s req `shouldReturn` Just (ESMined Nothing)
|
|
|
|
testNonceTakenByAnother :: TVar ChaChaDRG -> IO ()
|
|
testNonceTakenByAnother g = withSetup g $ \s@Setup {node} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
_ <- within $ awaitSent node 1
|
|
setNonce s 1
|
|
setRecord s channelKey "https://smp9.simplex.im/c#other"
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAReverted (Just "InvalidNonce")
|
|
stateOf s req `shouldReturn` Just (ESReverted $ Just "InvalidNonce")
|
|
|
|
testClearApplied :: TVar ChaChaDRG -> IO ()
|
|
testClearApplied g = withSetup g $ \s -> do
|
|
setRecord s contactKey "https://smp8.simplex.im/a#Y8m3"
|
|
setRecord s channelKey viemChannelLink
|
|
r <- relayClear s
|
|
setRecord s contactKey ""
|
|
setRecord s channelKey ""
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAApplied Nothing
|
|
|
|
testClearNonceTaken :: TVar ChaChaDRG -> IO ()
|
|
testClearNonceTaken g = forM_ [contactKey, channelKey] $ \held -> withSetup g $ \s -> do
|
|
r <- relayClear s
|
|
setRecord s held "https://smp9.simplex.im/c#held"
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAReverted (Just "InvalidNonce")
|
|
|
|
-- | viem's clear edit, with nonce 1, sent; then the owner's nonce moves past it.
|
|
relayClear :: Setup -> IO (Async EditAnswer)
|
|
relayClear s@Setup {node, owner} = do
|
|
setNonce s 1
|
|
req <- signEdit s owner RCClear 1 deadline
|
|
r <- relay s req
|
|
[raw] <- within $ awaitSent node 1
|
|
fmap trData (decodeSignedTx raw) `shouldBe` Right viemClearCall
|
|
setNonce s 2
|
|
pure r
|
|
|
|
testRevertFreesNonce :: TVar ChaChaDRG -> IO ()
|
|
testRevertFreesNonce g = withSetup g $ \s@Setup {node, records} -> do
|
|
first <- ownerEdit s viemChannelLink
|
|
r <- relay s first
|
|
-- reverted before the deadline with the nonce unused, so the revert has no name
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAReverted Nothing
|
|
second <- ownerEdit s "https://smp9.simplex.im/c#second"
|
|
r' <- relay s second
|
|
_ <- within $ awaitSent node 2
|
|
relayEdit records first `shouldReturn` EAReverted Nothing
|
|
stateOf s second `shouldReturn` Just ESPending
|
|
h <- mineSent s 2 True
|
|
answerOf r' `shouldReturn` EAApplied (Just h)
|
|
|
|
testFailedReRead :: TVar ChaChaDRG -> IO ()
|
|
testFailedReRead g = withSetup g $ \s@Setup {node, records} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
_ <- within $ awaitSent node 1
|
|
setFault node "eth_call" HttpFailure
|
|
_ <- mineSent s 1 False
|
|
answerOf r `shouldReturn` EAPending
|
|
stateOf s req `shouldReturn` Just ESPending
|
|
relayEdit records req `shouldReturn` EAPending
|
|
clearFault node "eth_call"
|
|
recordsRound records
|
|
stateOf s req `shouldReturn` Just (ESReverted Nothing)
|
|
relayEdit records req `shouldReturn` EAReverted Nothing
|
|
|
|
testResume :: TVar ChaChaDRG -> IO ()
|
|
testResume g = withSetup g $ \s@Setup {node, owner} -> do
|
|
unsent <- ownerEdit s viemChannelLink
|
|
saveUnsent s unsent
|
|
s' <- restart s
|
|
recordsRound (records s')
|
|
h <- mineSent s' 1 True
|
|
stateOf s unsent `shouldReturn` Just (ESMined $ Just h)
|
|
-- a sent edit whose transaction the node forgot while the service was down
|
|
setNonce s 1
|
|
sent <- signEdit s owner (channel "https://smp9.simplex.im/c#next") 1 deadline
|
|
_ <- relayEdit (records s') {rsPendingAfter = shortWait} sent
|
|
[_, raw] <- within $ awaitSent node 2
|
|
dropTransaction node $ C.keccak256 raw
|
|
s'' <- restart s'
|
|
followTransactions (sender s'')
|
|
(!! 2) <$> sentTransactions node `shouldReturn` raw
|
|
h' <- mineSent s'' 3 True
|
|
stateOf s sent `shouldReturn` Just (ESMined $ Just h')
|
|
TxHash (C.keccak256 raw) `shouldBe` h'
|
|
|
|
testBadRowSkipped :: TVar ChaChaDRG -> IO ()
|
|
testBadRowSkipped g = withSetup g $ \s@Setup {store, clock} -> do
|
|
now <- readTVarIO clock
|
|
-- a text record with a key and no value, which no edit can be
|
|
withTransaction store $ \db ->
|
|
DB.execute
|
|
db
|
|
"INSERT INTO sx_badge_service_name_record_edits (label_hash, owner, nonce, record_key, record_value, deadline, signature, body_hash, state, created_at) VALUES (?,?,5,'simplex.channel',NULL,?,?,?,'pending',?)"
|
|
(Binary $ unLabel alice, Binary $ unAddress viemAddress, deadline, Binary $ serializeSignature viemSetSignature, Binary $ B.replicate 32 0, now)
|
|
req <- ownerEdit s viemChannelLink
|
|
saveUnsent s req
|
|
s' <- restart s
|
|
recordsRound (records s')
|
|
h <- mineSent s' 1 True
|
|
stateOf s req `shouldReturn` Just (ESMined $ Just h)
|
|
|
|
testUnsentForgotten :: TVar ChaChaDRG -> IO ()
|
|
testUnsentForgotten g = withSetup g $ \s@Setup {node, owner} -> do
|
|
reverting <- ownerEdit s viemChannelLink
|
|
saveUnsent s reverting
|
|
revertDryRun node viemSetCall $ Just $ selector "InvalidSignature()"
|
|
s' <- restart s
|
|
recordsRound (records s')
|
|
stateOf s reverting `shouldReturn` Nothing
|
|
late <- signEdit s owner (channel viemChannelLink) 0 (deadline + 1)
|
|
saveUnsent s late
|
|
setBlock node 2 $ deadline + 1
|
|
recordsRound (records s')
|
|
stateOf s late `shouldReturn` Nothing
|
|
editRows s `shouldReturn` 0
|
|
sentTransactions node `shouldReturn` []
|
|
|
|
testRepeatAfterRestart :: TVar ChaChaDRG -> IO ()
|
|
testRepeatAfterRestart g = withSetup g $ \s@Setup {node} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
r <- relay s req
|
|
h <- mineSent s 1 True
|
|
answerOf r `shouldReturn` EAApplied (Just h)
|
|
s' <- restart s
|
|
setFault node "eth_call" HttpFailure
|
|
setFault node "eth_getBlockByNumber" HttpFailure
|
|
relayEdit (records s') req `shouldReturn` EAApplied (Just h)
|
|
length <$> sentTransactions node `shouldReturn` 1
|
|
|
|
testRestartExpiredApplied :: TVar ChaChaDRG -> IO ()
|
|
testRestartExpiredApplied g = withSetup g $ \s@Setup {node} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
relayEdit (records s) {rsPendingAfter = shortWait} req `shouldReturn` EAPending
|
|
_ <- within $ awaitSent node 1
|
|
-- while the service is down the deadline passes, and another relayer applies the edit
|
|
setBlock node 300 $ deadline + 1
|
|
setBlock node 301 $ deadline + 2
|
|
setNonce s 1
|
|
setRecord s channelKey viemChannelLink
|
|
s' <- restart s
|
|
recordsRound (records s')
|
|
stateOf s req `shouldReturn` Just (ESMined Nothing)
|
|
relayEdit (records s') req `shouldReturn` EAApplied Nothing
|
|
|
|
testExpiryFreesNonce :: TVar ChaChaDRG -> IO ()
|
|
testExpiryFreesNonce g = withSetup g $ \s@Setup {node, records, sender} -> do
|
|
req <- ownerEdit s viemChannelLink
|
|
relayEdit records {rsPendingAfter = shortWait} req `shouldReturn` EAPending
|
|
[raw] <- within $ awaitSent node 1
|
|
-- the latest block is past the deadline, but not yet the block two confirmations deep
|
|
setBlock node 300 deadline
|
|
setBlock node 301 $ deadline + 1
|
|
recordsRound records
|
|
stateOf s req `shouldReturn` Just ESPending
|
|
setBlock node 302 $ deadline + 2
|
|
recordsRound records
|
|
stateOf s req `shouldReturn` Just (ESReverted $ Just "SignatureExpired")
|
|
relayEdit records req `shouldReturn` EAReverted (Just "SignatureExpired")
|
|
dropTransaction node $ C.keccak256 raw
|
|
followTransactions sender
|
|
sentTransactions node `shouldReturn` [raw, raw]
|
|
|
|
-- http
|
|
|
|
testHttp :: TVar ChaChaDRG -> IO ()
|
|
testHttp g = withSetup g $ \s@Setup {records} -> withRecordsApp records $ \post -> do
|
|
let editBody value = J.encode $ KM.insert "value" (J.String value) $ requestJson [("key", J.String channelKey)]
|
|
post "[]" `shouldReturn` (400, Just $ errorJson "bad_request", Nothing)
|
|
post (editBody "https://x/a") `shouldReturn` (400, Just $ errorJson "invalid_link", Nothing)
|
|
answer <- async $ post $ editBody viemChannelLink
|
|
TxHash h <- mineSent s 1 True
|
|
within (wait answer) `shouldReturn` (200, Just $ J.object ["tx" .= hex h], Nothing)
|
|
setNonce s 1
|
|
next <- async . post . J.encode . editJson =<< signEdit s (owner s) (channel "https://smp9.simplex.im/c#next") 1 deadline
|
|
_ <- mineSent s 2 False
|
|
within (wait next) `shouldReturn` (409, Just $ J.object ["error" .= ("record_reverted" :: Text), "revert" .= J.Null], Nothing)
|
|
|
|
testHttpUnsettled :: TVar ChaChaDRG -> IO ()
|
|
testHttpUnsettled g = withSetup g $ \s@Setup {node, records, owner} -> do
|
|
let quick = records {rsPendingAfter = shortWait}
|
|
edit n = signEdit s owner (channel viemChannelLink) n deadline
|
|
withRecordsApp quick $ \post -> do
|
|
setFault node "eth_call" HttpFailure
|
|
(post . J.encode . editJson =<< edit 0) `shouldReturn` (503, Just $ errorJson "provider_unavailable", Nothing)
|
|
clearFault node "eth_call"
|
|
(post . J.encode . editJson =<< edit 0) `shouldReturn` (504, Just $ errorJson "edit_pending", Nothing)
|
|
forM_ [1 .. 9] $ \n -> do
|
|
setNonce s n
|
|
(relayEdit quick =<< edit n) `shouldReturn` EAPending
|
|
setNonce s 10
|
|
(post . J.encode . editJson =<< edit 10) `shouldReturn` (429, Just $ errorJson "rate_limited", Just "3600")
|
|
|
|
-- | Posts a body to the records handler, answering the status, the JSON body and Retry-After.
|
|
withRecordsApp :: Records -> ((LB.ByteString -> IO (Int, Maybe J.Value, Maybe ByteString)) -> IO a) -> IO a
|
|
withRecordsApp rs action = Warp.testWithApplication (pure $ recordsHandler rs) $ \port -> do
|
|
manager <- newManager defaultManagerSettings
|
|
action $ \body -> do
|
|
req <- parseRequest $ "http://127.0.0.1:" <> show port <> "/api/name/records"
|
|
resp <- httpLbs req {method = "POST", requestHeaders = [(hContentType, "application/json")], requestBody = RequestBodyLBS body} manager
|
|
pure (statusCode $ responseStatus resp, J.decode $ responseBody resp, lookup "Retry-After" $ responseHeaders resp)
|
|
|
|
errorJson :: Text -> J.Value
|
|
errorJson tag = J.object ["error" .= tag]
|
|
|
|
-- fixtures
|
|
|
|
editJson :: EditRequest -> J.Object
|
|
editJson EditRequest {erChange, erNonce, erDeadline, erSignature} =
|
|
KM.fromList $
|
|
[ ("labelHash", J.String aliceHashText),
|
|
("nonce", J.toJSON erNonce),
|
|
("deadline", J.toJSON erDeadline),
|
|
("signature", J.String $ hex $ serializeSignature erSignature)
|
|
]
|
|
<> case erChange of
|
|
RCSetText key value -> [("key", J.String key), ("value", J.String value)]
|
|
RCClear -> [("clear", J.Bool True)]
|
|
|
|
longWait, shortWait :: Int
|
|
longWait = 4000000
|
|
shortWait = 200000
|
|
|
|
-- | The fields of viem's SetText edit but its key and value, with these added or replaced.
|
|
requestJson :: [(J.Key, J.Value)] -> J.Object
|
|
requestJson extra = KM.union (KM.fromList extra) $ KM.delete "key" $ KM.delete "value" $ editJson viemEdit
|
|
|
|
viemEdit :: EditRequest
|
|
viemEdit = EditRequest {erLabelHash = alice, erChange = channel viemChannelLink, erNonce = 0, erDeadline = deadline, erSignature = viemSetSignature}
|
|
|
|
digestOf :: RecordChange -> Word64 -> Word64 -> ByteString
|
|
digestOf change nonce dl = case change of
|
|
RCSetText key value -> setTextDigest resolverDomain' aliceNode key value nonce dl
|
|
RCClear -> clearRecordsDigest resolverDomain' aliceNode nonce dl
|
|
|
|
resolverDomain' :: Eip712Domain
|
|
resolverDomain' = resolverDomain fakeChainId fakeResolver
|
|
|
|
alice :: LabelHash
|
|
alice = labelHash "alicebakery"
|
|
|
|
aliceNode :: Node
|
|
aliceNode = labelNode TLDSimplex alice
|
|
|
|
aliceHashText :: Text
|
|
aliceHashText = hex $ unLabel alice
|
|
|
|
unLabel :: LabelHash -> ByteString
|
|
unLabel (LabelHash h) = h
|
|
|
|
contactKey, channelKey :: Text
|
|
contactKey = "simplex.contact"
|
|
channelKey = "simplex.channel"
|
|
|
|
-- | An hour after the fake node's first block.
|
|
deadline :: Word64
|
|
deadline = 1767229200
|
|
|
|
nameExpiry, gracePeriod :: Word64
|
|
nameExpiry = 1861833600
|
|
gracePeriod = 7776000
|
|
|
|
registry, otherResolver :: Address
|
|
registry = address "0x5fbdb2315678afecb367f032d93f642f64180aa3"
|
|
otherResolver = address "0x4826533b4897376654bb4d4ad88b7fafd0c98528"
|
|
|
|
-- | The fake node's first block time.
|
|
genesis :: Word64
|
|
genesis = deadline - 3600
|
|
|
|
-- | The name code of BIP-39 entropy zero, the wallet of "abandon ... about".
|
|
viemCode :: Text
|
|
viemCode = "SN-00000-00000-00000-00000-00000-0VME2"
|
|
|
|
viemAddress :: Address
|
|
viemAddress = address "0x9858effd232b4033e47d90003d41ec34ecaeda94"
|
|
|
|
viemChannelLink :: Text
|
|
viemChannelLink = "https://smp8.simplex.im/c#Y8m3"
|
|
|
|
-- | viem: SetText(alicebakery.simplex, simplex.channel, viemChannelLink, 0, deadline)
|
|
viemSetSignature :: RecoverableSignature
|
|
viemSetSignature = either (error . show) id $ parseSignature "0xfce4c6865591609c07ef3d94f376187d8a20cd9d94b4affcb60b55e999038fbd4e45c42b5fafe66a8d4f756e99398602eacef777b4dce4afd0a767cddadf3ca91c"
|
|
|
|
-- | viem: ClearRecords(alicebakery.simplex, 1, deadline)
|
|
viemClearSignature :: RecoverableSignature
|
|
viemClearSignature = either (error . show) id $ parseSignature "0x0565f7e5a591c200fcc358c1d9e9293721d0b99f0285b589211203e311e8e6167ee822d664048870bdd8552263d25bbf45e7f4985fd3b8c3a19cfced44ef9d561c"
|
|
|
|
viemSetCall, viemClearCall :: ByteString
|
|
viemSetCall = unhex "71a218a70f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006955c7100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000f73696d706c65782e6368616e6e656c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e68747470733a2f2f736d70382e73696d706c65782e696d2f632359386d3300000000000000000000000000000000000000000000000000000000000000000041fce4c6865591609c07ef3d94f376187d8a20cd9d94b4affcb60b55e999038fbd4e45c42b5fafe66a8d4f756e99398602eacef777b4dce4afd0a767cddadf3ca91c00000000000000000000000000000000000000000000000000000000000000"
|
|
viemClearCall = unhex "6463e6140f4104919a21c22f566d9c7947c7c7d8addbf546c973851a7e449276ea54ec1a0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006955c710000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000410565f7e5a591c200fcc358c1d9e9293721d0b99f0285b589211203e311e8e6167ee822d664048870bdd8552263d25bbf45e7f4985fd3b8c3a19cfced44ef9d561c00000000000000000000000000000000000000000000000000000000000000"
|
|
|
|
unhex :: ByteString -> ByteString
|
|
unhex = either error id . convertFromBase Base16
|
|
|
|
address :: ByteString -> Address
|
|
address = either error id . strDecode
|