Keeper
Definition
"Keeper" is a word taken from the cosmos-sdk, it's basically the interface that the module exposes to the other modules in the application. For example, in the Nameservice app, the Nameservice keeper exposes functions to buy/sell/delete entries in the mapping. Likewise, the Nameservice keeper depends on the keeper from the bank module in order to transfer tokens when executing those methods. A keeper might also indicate what kinds of exceptions are able to be caught and thrown from the module. For example, calling transfer while buying a Name might throw an InsufficientFunds exception, which the Namerservice module can chose whether to catch or not.
Tutorial.Nameservice.Keeper
In this section, we will make use of the Store types defined in Nameservice.Modules.Nameservice.Store. For an overview on how this is setup, see the Storage chapter in the Foundations section of the tutorial.
{-# LANGUAGE TemplateHaskell #-}
module Tutorial.Nameservice.Keeper where
import Polysemy (Sem, Member, Members, makeSem)
import Polysemy.Error (Error, throw)
import Nameservice.Modules.Nameservice.Messages
import Nameservice.Modules.Nameservice.Store (Name(..), whoisMap)
import Nameservice.Modules.Nameservice.Types (Whois(..), NameDeleted(..), NameserviceError(..))
import qualified Tendermint.SDK.BaseApp as BA
import qualified Tendermint.SDK.BaseApp.Store.Map as M
import Tendermint.SDK.Modules.Bank (BankEffs, Coin(..), CoinId, mint)
nameserviceCoinId :: CoinId
nameserviceCoinId = "nameservice"
Generally a keeper is defined by a set of effects that the module introduces and depends on. In the case of Nameservice, we introduce the custom Nameservice effect:
type NameserviceEffs = '[NameserviceKeeper, Error NameserviceError]
data NameserviceKeeper m a where
BuyName :: BuyNameMsg -> NameserviceKeeper m ()
DeleteName :: DeleteNameMsg -> NameserviceKeeper m ()
SetName :: SetNameMsg -> NameserviceKeeper m ()
GetWhois :: Name -> NameserviceKeeper m (Maybe Whois)
makeSem ''NameserviceKeeper
where makeSem is from polysemy, it uses template Haskell to create the helper functions buyName, deleteName, setName, getWhois:
buyName :: BuyNameMsg -> NameserviceKeeper m ()
deleteName :: DeleteNameMsg -> NameserviceKeeper m ()
setName :: SetNameMsg -> NameserviceKeeper m ()
getWhois :: Name -> NameserviceKeeper m (Maybe Whois)
Evaluating Module Effects
Like we said before, all transactions must ultimately compile to the set of effects belonging to TxEffs and BaseEffs. In particular this means that we must interpret NameserviceEffs into more basic effects. To do this we follow the general pattern of first interpreting NameserviceKeeper effects, then finally interpreting Error NameserviceError in terms of Error AppError. Let's focus on the DeleteName sub-command of NameserviceKeeper. We can write an interpreting function as follows:
deleteNameF
:: Members BA.TxEffs r
=> Members BA.BaseEffs r
=> Members BankEffs r
=> Member (Error NameserviceError) r
=> DeleteNameMsg
-> Sem r ()
deleteNameF DeleteNameMsg{..} = do
mWhois <- M.lookup (Name deleteNameName) whoisMap
case mWhois of
Nothing -> throw $ InvalidDelete "Can't remove unassigned name."
Just Whois{..} ->
if whoisOwner /= deleteNameOwner
then throw $ InvalidDelete "Deleter must be the owner."
else do
mint deleteNameOwner (Coin nameserviceCoinId whoisPrice)
M.delete (Name deleteNameName) whoisMap
let event = NameDeleted
{ nameDeletedName = deleteNameName
}
BA.emit event
BA.logEvent event
The control flow should be pretty clear:
- Check that the name is actually registered, if not throw an error.
- Check that the name is registered to the person trying to delete it, if not throw an error.
- Refund the tokens locked in the name to the owner.
- Delete the entry from the database.
- Emit an event that the name has been deleted and log this event.
Taking a look at the class constraints, we see
( Members BaseApp.TxEffs r
, Members BaseApp.BaseEffs r
, Members BankEffs r
, Member (Error NameserviceError) r
)
- The
TxEffseffect is required because the function manipulates thewhoisMapand emits anEvent. - The
BaseEffseffect is required because the function has logging. - The
Error NameserviceErroreffect is required because the function may throw an error. - The
BankEffseffect is required because the function will mint coins.
Using this helper function and others, we can write our module's eval function by interpreting the NameserviceEffs in two steps:
eval
:: Members BA.TxEffs r
=> Members BankEffs r
=> Members BA.BaseEffs r
=> forall a. Sem (NameserviceKeeper ': Error NameserviceError ': r) a
-> Sem r a
eval = mapError BaseApp.makeAppError . evalNameservice
where
evalNameservice
:: Members BA.TxEffs r
=> Members BA.BaseEffs r
=> Members BankEffs r
=> Member (Error NameserviceError) r
=> Sem (NameserviceKeeper ': r) a -> Sem r a
evalNameservice =
interpret (\case
...
DeleteName msg -> deleteNameF msg
...
)