address TODOs in app 1 and 2
This commit is contained in:
parent
e8946e9b36
commit
e7081040d0
|
@ -70,7 +70,8 @@ func (msg MsgSend) GetSigners() []sdk.Address {
|
|||
}
|
||||
```
|
||||
|
||||
Note Addresses in the SDK are arbitrary byte arrays that are [Bech32](TODO) encoded
|
||||
Note Addresses in the SDK are arbitrary byte arrays that are
|
||||
[Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) encoded
|
||||
when displayed as a string or rendered in JSON. Typically, addresses are the hash of
|
||||
a public key, so we can use them to uniquely identify the required signers for a
|
||||
transaction.
|
||||
|
@ -128,11 +129,6 @@ type KVStore interface {
|
|||
// CONTRACT: No writes may happen within a domain while an iterator exists over it.
|
||||
ReverseIterator(start, end []byte) Iterator
|
||||
|
||||
// TODO Not yet implemented.
|
||||
// CreateSubKVStore(key *storeKey) (KVStore, error)
|
||||
|
||||
// TODO Not yet implemented.
|
||||
// GetSubKVStore(key *storeKey) KVStore
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -177,19 +173,24 @@ func newFooHandler(key sdk.StoreKey) sdk.Handler {
|
|||
}
|
||||
```
|
||||
|
||||
`Context` is modeled after the Golang [context.Context](TODO), which has
|
||||
`Context` is modeled after the Golang
|
||||
[context.Context](https://golang.org/pkg/context/), which has
|
||||
become ubiquitous in networking middleware and routing applications as a means
|
||||
to easily propogate request context through handler functions.
|
||||
Many methods on SDK objects receive a context as the first argument.
|
||||
|
||||
The Context also contains the [block header](TODO), which includes the latest timestamp from the blockchain and other information about the latest block.
|
||||
The Context also contains the
|
||||
[block header](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#header),
|
||||
which includes the latest timestamp from the blockchain and other information about the latest block.
|
||||
|
||||
See the [Context API docs](TODO) for more details.
|
||||
See the [Context API
|
||||
docs](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Context) for more details.
|
||||
|
||||
### Result
|
||||
|
||||
Handler takes a Context and Msg and returns a Result.
|
||||
Result is motivated by the corresponding [ABCI result](TODO). It contains return values, error information, logs, and meta data about the transaction:
|
||||
Result is motivated by the corresponding [ABCI result](https://github.com/tendermint/abci/blob/master/types/types.proto#L165).
|
||||
It contains return values, error information, logs, and meta data about the transaction:
|
||||
|
||||
```go
|
||||
// Result is the union of ResponseDeliverTx and ResponseCheckTx.
|
||||
|
@ -257,7 +258,8 @@ type appAccount struct {
|
|||
|
||||
Coins is a useful type provided by the SDK for multi-asset accounts.
|
||||
We could just use an integer here for a single coin type, but
|
||||
it's worth [getting to know Coins](TODO).
|
||||
it's worth [getting to know
|
||||
Coins](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Coins).
|
||||
|
||||
|
||||
Now we're ready to handle the MsgSend:
|
||||
|
@ -426,14 +428,13 @@ simplifies application development by handling common low-level concerns.
|
|||
It serves as the mediator between the two key components of an SDK app: the store
|
||||
and the message handlers. The BaseApp implements the
|
||||
[`abci.Application`](https://godoc.org/github.com/tendermint/abci/types#Application) interface.
|
||||
See the [BaseApp API documentation](TODO) for more details.
|
||||
See the [BaseApp API
|
||||
documentation](https://godoc.org/github.com/cosmos/cosmos-sdk/baseapp) for more details.
|
||||
|
||||
Here is the complete setup for App1:
|
||||
|
||||
```go
|
||||
func NewApp1(logger log.Logger, db dbm.DB) *bapp.BaseApp {
|
||||
// TODO: make this an interface or pass in
|
||||
// a TxDecoder instead.
|
||||
cdc := wire.NewCodec()
|
||||
|
||||
// Create the base application object.
|
||||
|
@ -483,7 +484,8 @@ In a real setup, the app would run as an ABCI application and
|
|||
would be driven by blocks of transactions from the Tendermint consensus engine.
|
||||
Later in the tutorial, we'll connect our app to a complete suite of components
|
||||
for running and using a live blockchain application. For complete details on
|
||||
how ABCI applications work, see the [ABCI documentation](TODO).
|
||||
how ABCI applications work, see the [ABCI
|
||||
documentation](https://github.com/tendermint/abci/blob/master/specification.md).
|
||||
|
||||
For now, we note the follow sequence of events occurs when a transaction is
|
||||
received (through `app.DeliverTx`):
|
||||
|
|
|
@ -11,23 +11,88 @@ Here we build `App2`, which expands on `App1` by introducing
|
|||
Along the way, we'll be introduced to Amino for encoding and decoding
|
||||
transactions and to the AnteHandler for processing them.
|
||||
|
||||
The complete code can be found in [app2.go](examples/app2.go).
|
||||
|
||||
|
||||
## Message
|
||||
|
||||
Let's introduce a new message type for issuing coins:
|
||||
|
||||
```go
|
||||
TODO
|
||||
// MsgIssue to allow a registered issuer
|
||||
// to issue new coins.
|
||||
type MsgIssue struct {
|
||||
Issuer sdk.Address
|
||||
Receiver sdk.Address
|
||||
Coin sdk.Coin
|
||||
}
|
||||
|
||||
// Implements Msg.
|
||||
func (msg MsgIssue) Type() string { return "issue" }
|
||||
```
|
||||
|
||||
Note the `Type()` method returns `"issue"`, so this message is of a different
|
||||
type and will be executed by a different handler than `MsgSend`. The other
|
||||
methods for `MsgIssue` are similar to `MsgSend`.
|
||||
|
||||
## Handler
|
||||
|
||||
We'll need a new handler to support the new message type:
|
||||
We'll need a new handler to support the new message type. It just checks if the
|
||||
sender of the `MsgIssue` is the correct issuer for the given coin type, as per the information
|
||||
in the issuer store:
|
||||
|
||||
```go
|
||||
TODO
|
||||
// Handle MsgIssue
|
||||
func handleMsgIssue(keyIssue *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
|
||||
issueMsg, ok := msg.(MsgIssue)
|
||||
if !ok {
|
||||
return sdk.NewError(2, 1, "MsgIssue is malformed").Result()
|
||||
}
|
||||
|
||||
// Retrieve stores
|
||||
issueStore := ctx.KVStore(keyIssue)
|
||||
accStore := ctx.KVStore(keyAcc)
|
||||
|
||||
// Handle updating coin info
|
||||
if res := handleIssuer(issueStore, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
|
||||
return res
|
||||
}
|
||||
|
||||
// Issue coins to receiver using previously defined handleTo function
|
||||
if res := handleTo(accStore, issueMsg.Receiver, []sdk.Coin{issueMsg.Coin}); !res.IsOK() {
|
||||
return res
|
||||
}
|
||||
|
||||
return sdk.Result{
|
||||
// Return result with Issue msg tags
|
||||
Tags: issueMsg.Tags(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleIssuer(store sdk.KVStore, issuer sdk.Address, coin sdk.Coin) sdk.Result {
|
||||
// the issuer address is stored directly under the coin denomination
|
||||
denom := []byte(coin.Denom)
|
||||
issuerAddress := store.Get(denom)
|
||||
if issuerAddress == nil {
|
||||
return sdk.ErrInvalidCoins(fmt.Sprintf("Unknown coin type %s", coin.Denom)).Result()
|
||||
}
|
||||
|
||||
// Msg Issuer is not authorized to issue these coins
|
||||
if !bytes.Equal(issuerAddress, issuer) {
|
||||
return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
```
|
||||
|
||||
Note we're just storing the issuer address for each coin directly under the
|
||||
coin's denomination in the issuer store. We could of course use a struct with more
|
||||
fields, like the current supply of coins in existence, and the maximum supply
|
||||
allowed to be issued.
|
||||
|
||||
## Amino
|
||||
|
||||
Now that we have two implementations of `Msg`, we won't know before hand
|
||||
|
@ -74,8 +139,6 @@ func NewCodec() *wire.Codec {
|
|||
Amino supports encoding and decoding in both a binary and JSON format.
|
||||
See the [codec API docs](https://godoc.org/github.com/tendermint/go-amino#Codec) for more details.
|
||||
|
||||
TODO: Update Amino and demo `cdc.PrintTypes`
|
||||
|
||||
## Tx
|
||||
|
||||
Now that we're using Amino, we can embed the `Msg` interface directly in our
|
||||
|
@ -121,12 +184,47 @@ according to whatever capability keys it was granted. Instead of a `Msg`,
|
|||
however, it takes a `Tx`.
|
||||
|
||||
Like Handler, AnteHandler returns a `Result` type, but it also returns a new
|
||||
`Context` and an `abort bool`. TODO explain (do we still need abort? )
|
||||
`Context` and an `abort bool`.
|
||||
|
||||
For `App2`, we simply check if the PubKey matches the Address, and the Signature validates with the PubKey:
|
||||
|
||||
```go
|
||||
TODO
|
||||
// Simple anteHandler that ensures msg signers have signed.
|
||||
// Provides no replay protection.
|
||||
func antehandler(ctx sdk.Context, tx sdk.Tx) (_ sdk.Context, _ sdk.Result, abort bool) {
|
||||
appTx, ok := tx.(app2Tx)
|
||||
if !ok {
|
||||
// set abort boolean to true so that we don't continue to process failed tx
|
||||
return ctx, sdk.ErrTxDecode("Tx must be of format app2Tx").Result(), true
|
||||
}
|
||||
|
||||
// expect only one msg in app2Tx
|
||||
msg := tx.GetMsgs()[0]
|
||||
|
||||
signerAddrs := msg.GetSigners()
|
||||
|
||||
if len(signerAddrs) != len(appTx.GetSignatures()) {
|
||||
return ctx, sdk.ErrUnauthorized("Number of signatures do not match required amount").Result(), true
|
||||
}
|
||||
|
||||
signBytes := msg.GetSignBytes()
|
||||
for i, addr := range signerAddrs {
|
||||
sig := appTx.GetSignatures()[i]
|
||||
|
||||
// check that submitted pubkey belongs to required address
|
||||
if !bytes.Equal(sig.PubKey.Address(), addr) {
|
||||
return ctx, sdk.ErrUnauthorized("Provided Pubkey does not match required address").Result(), true
|
||||
}
|
||||
|
||||
// check that signature is over expected signBytes
|
||||
if !sig.PubKey.VerifyBytes(signBytes, sig.Signature) {
|
||||
return ctx, sdk.ErrUnauthorized("Signature verification failed").Result(), true
|
||||
}
|
||||
}
|
||||
|
||||
// authentication passed, app to continue processing by sending msg to handler
|
||||
return ctx, sdk.Result{}, false
|
||||
}
|
||||
```
|
||||
|
||||
## App2
|
||||
|
@ -134,9 +232,46 @@ TODO
|
|||
Let's put it all together now to get App2:
|
||||
|
||||
```go
|
||||
TODO
|
||||
func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
|
||||
|
||||
cdc := NewCodec()
|
||||
|
||||
// Create the base application object.
|
||||
app := bapp.NewBaseApp(app2Name, cdc, logger, db)
|
||||
|
||||
// Create a key for accessing the account store.
|
||||
keyAccount := sdk.NewKVStoreKey("acc")
|
||||
// Create a key for accessing the issue store.
|
||||
keyIssue := sdk.NewKVStoreKey("issue")
|
||||
|
||||
// set antehandler function
|
||||
app.SetAnteHandler(antehandler)
|
||||
|
||||
// Register message routes.
|
||||
// Note the handler gets access to the account store.
|
||||
app.Router().
|
||||
AddRoute("send", handleMsgSend(keyAccount)).
|
||||
AddRoute("issue", handleMsgIssue(keyAccount, keyIssue))
|
||||
|
||||
// Mount stores and load the latest state.
|
||||
app.MountStoresIAVL(keyAccount, keyIssue)
|
||||
err := app.LoadLatestVersion(keyAccount)
|
||||
if err != nil {
|
||||
cmn.Exit(err.Error())
|
||||
}
|
||||
return app
|
||||
}
|
||||
```
|
||||
|
||||
The main difference here, compared to `App1`, is that we use a second capability
|
||||
key for a second store that is *only* passed to a second handler, the
|
||||
`handleMsgIssue`. The first `handleMsgSend` has no access to this second store and cannot read or write to
|
||||
it, ensuring a strong separation of concerns.
|
||||
|
||||
Note also that we do not need to use `SetTxDecoder` here - now that we're using
|
||||
Amino, we simply create a codec, register our types on the codec, and pass the
|
||||
codec into `NewBaseApp`. The SDK takes care of the rest for us!
|
||||
|
||||
## Conclusion
|
||||
|
||||
We've expanded on our first app by adding a new message type for issuing coins,
|
||||
|
|
|
@ -18,8 +18,6 @@ const (
|
|||
|
||||
func NewApp1(logger log.Logger, db dbm.DB) *bapp.BaseApp {
|
||||
|
||||
// TODO: make this an interface or pass in
|
||||
// a TxDecoder instead.
|
||||
cdc := wire.NewCodec()
|
||||
|
||||
// Create the base application object.
|
||||
|
@ -113,7 +111,7 @@ func handleMsgSend(key *sdk.KVStoreKey) sdk.Handler {
|
|||
if !ok {
|
||||
// Create custom error message and return result
|
||||
// Note: Using unreserved error codespace
|
||||
return sdk.NewError(2, 1, "Send Message is malformed").Result()
|
||||
return sdk.NewError(2, 1, "MsgSend is malformed").Result()
|
||||
}
|
||||
|
||||
// Load the store.
|
||||
|
|
|
@ -40,8 +40,9 @@ func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
|
|||
app := bapp.NewBaseApp(app2Name, cdc, logger, db)
|
||||
|
||||
// Create a key for accessing the account store.
|
||||
keyMain := sdk.NewKVStoreKey("main")
|
||||
keyAccount := sdk.NewKVStoreKey("acc")
|
||||
// Create a key for accessing the issue store.
|
||||
keyIssue := sdk.NewKVStoreKey("issue")
|
||||
|
||||
// set antehandler function
|
||||
app.SetAnteHandler(antehandler)
|
||||
|
@ -50,10 +51,10 @@ func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
|
|||
// Note the handler gets access to the account store.
|
||||
app.Router().
|
||||
AddRoute("send", handleMsgSend(keyAccount)).
|
||||
AddRoute("issue", handleMsgIssue(keyAccount, keyMain))
|
||||
AddRoute("issue", handleMsgIssue(keyAccount, keyIssue))
|
||||
|
||||
// Mount stores and load the latest state.
|
||||
app.MountStoresIAVL(keyAccount, keyMain)
|
||||
app.MountStoresIAVL(keyAccount, keyIssue)
|
||||
err := app.LoadLatestVersion(keyAccount)
|
||||
if err != nil {
|
||||
cmn.Exit(err.Error())
|
||||
|
@ -61,20 +62,11 @@ func NewApp2(logger log.Logger, db dbm.DB) *bapp.BaseApp {
|
|||
return app
|
||||
}
|
||||
|
||||
// Coin Metadata
|
||||
type CoinMetadata struct {
|
||||
TotalSupply sdk.Int
|
||||
CurrentSupply sdk.Int
|
||||
Issuer sdk.Address
|
||||
Decimal uint64
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// Msgs
|
||||
|
||||
// Single permissioned issuer can issue Coin to Receiver
|
||||
// if he is the issuer in Coin Metadata
|
||||
// Implements sdk.Msg Interface
|
||||
// MsgIssue to allow a registered issuer
|
||||
// to issue new coins.
|
||||
type MsgIssue struct {
|
||||
Issuer sdk.Address
|
||||
Receiver sdk.Address
|
||||
|
@ -125,20 +117,20 @@ func (msg MsgIssue) Tags() sdk.Tags {
|
|||
//------------------------------------------------------------------
|
||||
// Handler for the message
|
||||
|
||||
// Handle Msg Issue
|
||||
func handleMsgIssue(keyMain *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler {
|
||||
// Handle MsgIssue
|
||||
func handleMsgIssue(keyIssue *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler {
|
||||
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
|
||||
issueMsg, ok := msg.(MsgIssue)
|
||||
if !ok {
|
||||
return sdk.NewError(2, 1, "IssueMsg is malformed").Result()
|
||||
return sdk.NewError(2, 1, "MsgIssue is malformed").Result()
|
||||
}
|
||||
|
||||
// Retrieve stores
|
||||
store := ctx.KVStore(keyMain)
|
||||
issueStore := ctx.KVStore(keyIssue)
|
||||
accStore := ctx.KVStore(keyAcc)
|
||||
|
||||
// Handle updating metadata
|
||||
if res := handleMetaData(store, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
|
||||
// Handle updating coin info
|
||||
if res := handleIssuer(issueStore, issueMsg.Issuer, issueMsg.Coin); !res.IsOK() {
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -154,51 +146,19 @@ func handleMsgIssue(keyMain *sdk.KVStoreKey, keyAcc *sdk.KVStoreKey) sdk.Handler
|
|||
}
|
||||
}
|
||||
|
||||
func handleMetaData(store sdk.KVStore, issuer sdk.Address, coin sdk.Coin) sdk.Result {
|
||||
bz := store.Get([]byte(coin.Denom))
|
||||
var metadata CoinMetadata
|
||||
|
||||
if bz == nil {
|
||||
// Coin not set yet, initialize with issuer and default values
|
||||
// Coin amount can't be above default value
|
||||
if coin.Amount.GT(sdk.NewInt(1000000)) {
|
||||
return sdk.ErrInvalidCoins("Cannot issue that many new coins").Result()
|
||||
}
|
||||
metadata = CoinMetadata{
|
||||
TotalSupply: sdk.NewInt(1000000),
|
||||
CurrentSupply: sdk.NewInt(0),
|
||||
Issuer: issuer,
|
||||
Decimal: 10,
|
||||
}
|
||||
} else {
|
||||
// Decode coin metadata
|
||||
err := json.Unmarshal(bz, &metadata)
|
||||
if err != nil {
|
||||
return sdk.ErrInternal("Decoding coin metadata failed").Result()
|
||||
}
|
||||
func handleIssuer(store sdk.KVStore, issuer sdk.Address, coin sdk.Coin) sdk.Result {
|
||||
// the issuer address is stored directly under the coin denomination
|
||||
denom := []byte(coin.Denom)
|
||||
issuerAddress := store.Get(denom)
|
||||
if issuerAddress == nil {
|
||||
return sdk.ErrInvalidCoins(fmt.Sprintf("Unknown coin type %s", coin.Denom)).Result()
|
||||
}
|
||||
|
||||
// Msg Issuer is not authorized to issue these coins
|
||||
if !bytes.Equal(metadata.Issuer, issuer) {
|
||||
if !bytes.Equal(issuerAddress, issuer) {
|
||||
return sdk.ErrUnauthorized(fmt.Sprintf("Msg Issuer cannot issue tokens: %s", coin.Denom)).Result()
|
||||
}
|
||||
|
||||
// Update coin current circulating supply
|
||||
metadata.CurrentSupply = metadata.CurrentSupply.Add(coin.Amount)
|
||||
|
||||
// Current supply cannot exceed total supply
|
||||
if metadata.TotalSupply.LT(metadata.CurrentSupply) {
|
||||
return sdk.ErrInsufficientCoins("Issuer cannot issue more than total supply of coin").Result()
|
||||
}
|
||||
|
||||
val, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return sdk.ErrInternal(fmt.Sprintf("Error encoding metadata: %s", err.Error())).Result()
|
||||
}
|
||||
|
||||
// Update store with new metadata
|
||||
store.Set([]byte(coin.Denom), val)
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
|
||||
|
@ -222,8 +182,8 @@ func (tx app2Tx) GetSignatures() []auth.StdSignature {
|
|||
|
||||
//------------------------------------------------------------------
|
||||
|
||||
// Simple antehandler that ensures msg signers has signed over msg signBytes w/ no replay protection
|
||||
// Implement sdk.AnteHandler interface
|
||||
// Simple anteHandler that ensures msg signers have signed.
|
||||
// Provides no replay protection.
|
||||
func antehandler(ctx sdk.Context, tx sdk.Tx) (_ sdk.Context, _ sdk.Result, abort bool) {
|
||||
appTx, ok := tx.(app2Tx)
|
||||
if !ok {
|
||||
|
@ -235,12 +195,12 @@ func antehandler(ctx sdk.Context, tx sdk.Tx) (_ sdk.Context, _ sdk.Result, abort
|
|||
msg := tx.GetMsgs()[0]
|
||||
|
||||
signerAddrs := msg.GetSigners()
|
||||
signBytes := msg.GetSignBytes()
|
||||
|
||||
if len(signerAddrs) != len(appTx.GetSignatures()) {
|
||||
return ctx, sdk.ErrUnauthorized("Number of signatures do not match required amount").Result(), true
|
||||
}
|
||||
|
||||
signBytes := msg.GetSignBytes()
|
||||
for i, addr := range signerAddrs {
|
||||
sig := appTx.GetSignatures()[i]
|
||||
|
||||
|
|
Loading…
Reference in New Issue