Skip to content

Commit

Permalink
simplify and complete app3
Browse files Browse the repository at this point in the history
  • Loading branch information
ebuchman committed Jun 29, 2018
1 parent d1a42e0 commit 1d4d9e9
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 136 deletions.
87 changes: 66 additions & 21 deletions docs/core/app3.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ working with accounts in the store.
The `x/bank` module implements `Msg` and `Handler` - it has everything we need
to transfer coins between accounts.

Applications that use `x/auth` and `x/bank` thus significantly reduce the amount
of work they have to do so they can focus on their application specific logic in
their own modules.
Here, we'll introduce the important types from `x/auth` and `x/bank`, and use
them to build `App3`, our shortest app yet. The complete code can be found in
[app3.go](examples/app3.go), and at the end of this section.

Here, we'll introduce the important types from `x/auth` and `x/bank`, and show
how to make `App3` by using them. The complete code can be found in [app3.go](examples/app3.go).
For more details, see the
[x/auth](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth) and [x/bank](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank) API documentation.
[x/auth](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth) and
[x/bank](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank) API documentation.

## Accounts

Expand Down Expand Up @@ -260,19 +259,19 @@ the same message could be executed over and over again.
The PubKey is required for signature verification, but it is only required in
the StdSignature once. From that point on, it will be stored in the account.

The fee is paid by the first address returned by `msg.GetSigners()` for the first `Msg`.
The convenience function `FeePayer(tx Tx) sdk.Address` is provided to return this.
The fee is paid by the first address returned by `msg.GetSigners()` for the first `Msg`,
as provided by the `FeePayer(tx Tx) sdk.Address` function.

## CoinKeeper

Now that we've seen the `auth.AccountMapper` and how its used to build a
complete AnteHandler, it's time to look at how to build higher-level
abstractions for taking action on accounts.

Earlier, we noted that `Mappers` abstactions over a KVStore that handles marshalling and unmarshalling a
particular data type to and from the underlying store. We can build another
abstraction on top of `Mappers` that we call `Keepers`, which expose only
limitted functionality on the underlying types stored by the `Mapper`.
Earlier, we noted that `Mappers` are abstactions over KVStores that handle
marshalling and unmarshalling data types to and from underlying stores.
We can build another abstraction on top of `Mappers` that we call `Keepers`,
which expose only limitted functionality on the underlying types stored by the `Mapper`.

For instance, the `x/bank` module defines the canonical versions of `MsgSend`
and `MsgIssue` for the SDK, as well as a `Handler` for processing them. However,
Expand Down Expand Up @@ -303,23 +302,69 @@ See the [bank.Keeper API
docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank#Keeper) for the full set of methods.

Note we can refine the `bank.Keeper` by restricting it's method set. For
instance, the `bank.ViewKeeper` is a read-only version, while the
`bank.SendKeeper` only executes transfers of coins from input accounts to output
instance, the
[bank.ViewKeeper](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank#ViewKeeper)
is a read-only version, while the
[bank.SendKeeper](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank#SendKeeper)
only executes transfers of coins from input accounts to output
accounts.

We use this `Keeper` paradigm extensively in the SDK as the way to define what
kind of functionality each module gets access to. In particular, we try to
follow the *principle of least authority*, where modules only get access to the
absolutely narrowest set of functionality they need to get the job done. Hence,
rather than providing full blown access to the `KVStore` or the `AccountMapper`,
follow the *principle of least authority*.
Rather than providing full blown access to the `KVStore` or the `AccountMapper`,
we restrict access to a small number of functions that do very specific things.

## App3

Armed with an understanding of mappers and keepers, in particular the
`auth.AccountMapper` and the `bank.Keeper`, we're now ready to build `App3`
using the `x/auth` and `x/bank` modules to do all the heavy lifting:
With the `auth.AccountMapper` and `bank.Keeper` in hand,
we're now ready to build `App3`.
The `x/auth` and `x/bank` modules do all the heavy lifting:

```go
TODO
func NewApp3(logger log.Logger, db dbm.DB) *bapp.BaseApp {

// Create the codec with registered Msg types
cdc := NewCodec()

// Create the base application object.
app := bapp.NewBaseApp(app3Name, cdc, logger, db)

// Create a key for accessing the account store.
keyAccount := sdk.NewKVStoreKey("acc")
keyFees := sdk.NewKVStoreKey("fee") // TODO

// Set various mappers/keepers to interact easily with underlying stores
accountMapper := auth.NewAccountMapper(cdc, keyAccount, &auth.BaseAccount{})
coinKeeper := bank.NewKeeper(accountMapper)
feeKeeper := auth.NewFeeCollectionKeeper(cdc, keyFees)

app.SetAnteHandler(auth.NewAnteHandler(accountMapper, feeKeeper))

// Register message routes.
// Note the handler gets access to
app.Router().
AddRoute("send", bank.NewHandler(coinKeeper))

// Mount stores and load the latest state.
app.MountStoresIAVL(keyAccount, keyFees)
err := app.LoadLatestVersion(keyAccount)
if err != nil {
cmn.Exit(err.Error())
}
return app
}
```

Note we use `bank.NewHandler`, which handles only `bank.MsgSend`,
and receives only the `bank.Keeper`. See the
[x/bank API docs](https://godoc.org/github.com/cosmos/cosmos-sdk/x/bank)
for more details.

## Conclusion

Armed with native modules for authentication and coin transfer,
emboldened by the paradigm of mappers and keepers,
and ever invigorated by the desire to build secure state-machines,
we find ourselves here with a full-blown, all-checks-in-place, multi-asset
cryptocurrency - the beating heart of the Cosmos-SDK.
119 changes: 4 additions & 115 deletions docs/core/examples/app3.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package app

import (
"bytes"
"encoding/json"
"fmt"

cmn "github.com/tendermint/tmlibs/common"
dbm "github.com/tendermint/tmlibs/db"
"github.com/tendermint/tmlibs/log"
Expand All @@ -29,132 +25,25 @@ func NewApp3(logger log.Logger, db dbm.DB) *bapp.BaseApp {

// Create a key for accessing the account store.
keyAccount := sdk.NewKVStoreKey("acc")
keyMain := sdk.NewKVStoreKey("main")
keyFees := sdk.NewKVStoreKey("fee")
keyFees := sdk.NewKVStoreKey("fee") // TODO

// Set various mappers/keepers to interact easily with underlying stores
// TODO: Need to register Account interface or use different Codec
accountMapper := auth.NewAccountMapper(cdc, keyAccount, &auth.BaseAccount{})
coinKeeper := bank.NewKeeper(accountMapper)
infoMapper := newCoinInfoMapper(keyMain)
feeKeeper := auth.NewFeeCollectionKeeper(cdc, keyFees)

app.SetAnteHandler(auth.NewAnteHandler(accountMapper, feeKeeper))

// Register message routes.
// Note the handler gets access to the account store.
// Note the handler gets access to
app.Router().
AddRoute("send", handleMsgSendWithKeeper(coinKeeper)).
AddRoute("issue", handleMsgIssueWithInfoMapper(infoMapper, coinKeeper))
AddRoute("send", bank.NewHandler(coinKeeper))

// Mount stores and load the latest state.
app.MountStoresIAVL(keyAccount, keyMain, keyFees)
app.MountStoresIAVL(keyAccount, keyFees)
err := app.LoadLatestVersion(keyAccount)
if err != nil {
cmn.Exit(err.Error())
}
return app
}

func handleMsgSendWithKeeper(coinKeeper bank.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
sendMsg, ok := msg.(MsgSend)
if !ok {
return sdk.NewError(2, 1, "Send Message Malformed").Result()
}

// Subtract coins from sender account
_, _, err := coinKeeper.SubtractCoins(ctx, sendMsg.From, sendMsg.Amount)
if err != nil {
// if error, return its result
return err.Result()
}

// Add coins to receiver account
_, _, err = coinKeeper.AddCoins(ctx, sendMsg.To, sendMsg.Amount)
if err != nil {
// if error, return its result
return err.Result()
}

return sdk.Result{
Tags: sendMsg.Tags(),
}
}
}

func handleMsgIssueWithInfoMapper(infoMapper coinInfoMapper, coinKeeper bank.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
issueMsg, ok := msg.(MsgIssue)
if !ok {
return sdk.NewError(2, 1, "Issue Message Malformed").Result()
}

// Handle updating metadata
if res := handleCoinInfoWithMapper(ctx, infoMapper, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
return res
}

// Add newly issued coins to output address
_, _, err := coinKeeper.AddCoins(ctx, issueMsg.Receiver, []sdk.Coin{issueMsg.Coin})
if err != nil {
return err.Result()
}

return sdk.Result{
Tags: issueMsg.Tags(),
}
}
}

func handleCoinInfoWithMapper(ctx sdk.Context, infoMapper CoinInfoMapper, issuer sdk.Address, coin sdk.Coin) sdk.Result {
coinInfo := infoMapper.GetInfo(ctx, coin.Denom)

// Metadata was created fresh, should set issuer to msg issuer
if len(coinInfo.Issuer) == 0 {
coinInfo.Issuer = issuer
}

// Msg Issuer is not authorized to issue these coins
if !bytes.Equal(coinInfo.Issuer, issuer) {
return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
}

return sdk.Result{}
}

//------------------------------------------------------------------
// Mapper for CoinInfo

// Example of a very simple user-defined read-only mapper interface.
type CoinInfoMapper interface {
GetInfo(sdk.Context, string) coinInfo
}

// Implements CoinInfoMapper.
type coinInfoMapper struct {
key *sdk.KVStoreKey
}

// Construct new CoinInfoMapper.
func newCoinInfoMapper(key *sdk.KVStoreKey) coinInfoMapper {
return coinInfoMapper{key: key}
}

// Implements CoinInfoMapper. Returns info for coin.
func (cim coinInfoMapper) GetInfo(ctx sdk.Context, denom string) coinInfo {
store := ctx.KVStore(cim.key)

infoBytes := store.Get([]byte(denom))
if infoBytes == nil {
// TODO
}

var coinInfo coinInfo
err := json.Unmarshal(infoBytes, &coinInfo)
if err != nil {
panic(err)
}

return coinInfo
}

0 comments on commit 1d4d9e9

Please sign in to comment.