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
{-# LANGUAGE TemplateHaskell #-}
module Tutorial.Nameservice.Keeper where
import Data.Proxy
import Data.String.Conversions (cs)
import GHC.TypeLits (symbolVal)
import Polysemy (Sem, Members, makeSem, interpret)
import Polysemy.Error (Error, throw, mapError)
import Polysemy.Output (Output)
import Nameservice.Modules.Nameservice.Messages (DeleteName(..))
import Nameservice.Modules.Nameservice.Types (Whois(..), Name, NameDeleted(..), NameserviceModuleName, NameserviceError(..))
import Nameservice.Modules.Token (Token, mint)
import qualified Tendermint.SDK.BaseApp as BA
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:
data NameserviceKeeper m a where
PutWhois :: Name -> Whois -> NameserviceKeeper m ()
GetWhois :: Name -> NameserviceKeeper m (Maybe Whois)
DeleteWhois :: Name -> NameserviceKeeper m ()
makeSem ''NameserviceKeeper
type NameserviceEffs = '[NameserviceKeeper, Error NameserviceError]
where makeSem
is from polysemy, it uses template Haskell to create the helper functions putWhoIs
, getWhois
, deleteWhois
:
putWhois :: forall r. Member NameserviceKeeper r => Name -> Whois -> Sem r ()
getWhois :: forall r. Member NameserviceKeeper r => Name -> Sem r (Maybe Whois)
deleteWhois :: forall r. Member NameserviceKeeper r => Name -> Sem r ()
We can then write the top level function for example for deleting a name:
deleteName
:: Members [Token, Output BA.Event] r
=> Members [NameserviceKeeper, Error NameserviceError] r
=> DeleteName
-> Sem r ()
deleteName DeleteName{..} = do
mWhois <- getWhois deleteNameName
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 whoisPrice
deleteWhois deleteNameName
BA.emit NameDeleted
{ nameDeletedName = deleteNameName
}
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.
Taking a look at the class constraints, we see
(Members NameserviceEffs, Members [Token, Output Event] r)
- The
NameserviceKeeper
effect is required because the function may manipulate the modules database withdeleteName
. - The
Error NameserviceError
effect is required because the function may throw an error. - The
Token
effect is required because the function will mint coins. - The
Output Event
effect is required because the function may emit aNameDeleted
event.
Evaluating Module Effects
Like we said before, all modules must ultimately compile to the set of effects belonging to BaseApp
. For effects interpreted to RawStore
, this means that you will need to define something called a StoreKey
.
A StoreKey
is effectively a namespacing inside the database, and is unique for a given module. In theory it could be any ByteString
, but the natural definition in the case of Nameservice is would be something like
storeKey :: BA.StoreKey NameserviceModuleName
storeKey = BA.StoreKey . cs . symbolVal $ (Proxy @NameserviceModuleName)
With this storeKey
it is possible to write the eval
function to resolve the effects defined in Nameservice, namely the NameserviceKeeper
effect and Error NameserviceError
:
eval
:: Members [BA.RawStore, Error BA.AppError] r
=> forall a. Sem (NameserviceKeeper ': Error NameserviceError ': r) a
-> Sem r a
eval = mapError BA.makeAppError . evalNameservice
where
evalNameservice
:: Members [BA.RawStore, Error BA.AppError] r
=> Sem (NameserviceKeeper ': r) a -> Sem r a
evalNameservice =
interpret (\case
GetWhois name -> BA.get storeKey name
PutWhois name whois -> BA.put storeKey name whois
DeleteWhois name -> BA.delete storeKey name
)