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
TxEffs
effect is required because the function manipulates thewhoisMap
and emits anEvent
. - The
BaseEffs
effect is required because the function has logging. - The
Error NameserviceError
effect is required because the function may throw an error. - The
BankEffs
effect 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
...
)