Building an application specific blockchain using Cosmos SDK Part-3
So far in part-2, we discussed the technical details around Tendermint, Cosmos sdk and installed starport to scaffold and run the chain.We also went through the business flow for our custom module deal
. Now we will start with defining our data types first. We will be using starport to generate the types. Note that starport will auto generate query and message handlers along with types. Use the flag--no-message
so that starport doesn’t create sdk.Msg
and msg service for our data type. Feel free to remove the query handlers if not required for a given type.
Let’s start with a dealCounter
type which will take care of generating deal ids while creating new deals.
starport scaffold single dealCounter idValue:uint --module deal --no-message
The --no-message
flag helps us to generate type without sdk.Msg
type and msg handler. We will be using idValue
field under dealCounter
type as an increment counter to generate deal ids while creating deals. The above scaffold
command does the following -
- Generates the proto type definition for
dealCounter
(deal_counter.proto
). - Updates the
genesis.proto
file to includedealCounter
under genesis state. - Generates the query messages definition(
query.proto
). - The query message handlers (under
keepers
directory). - The query proto and query proto grpc gateway files(
query.pb.go, query.pb.gw.go
). - Generates the cli command to invoke the query handler (
query_new_deal.go
). - Test files and test simulation parameters (for simulation testing).
// deal_counter.protosyntax = "proto3";
package Harry027.deal.deal;option go_package = "github.com/Harry-027/deal/x/deal/types";message DealCounter {
uint64 idValue = 1;
}
// genesis.proto
option go_package = "github.com/Harry-027/deal/x/deal/types";// GenesisState defines the deal module's genesis state.message GenesisState {
Params params = 1 [(gogoproto.nullable) = false];
DealCounter dealCounter = 2;
}
Note that we don’t require user to query counter details. Therefore we should remove the query messages related to dealCounter
from query.proto
file. Also delete the scaffolded dealCounter
query handler file from keepers
folder and dealCounter
specific cli commands from cli
folder. And run the following command to regenerate the proto definitions (*.pb.go
and *.pb.gw.go
).
starport generate proto-go
Note that we should not touch the *.pb.go
and *.pb.gw.go
generated files as those will be auto-generated every-time based on proto definitions. Also you will find the commands (in various files) similar to one as given below -
// this line is used by starport scaffolding # genesis/proto/state
Don’t remove such commands as those will be used by starport every-time to identify correct locations for the code generation.
Lets generate the newDeal map type -
starport scaffold map newDeal dealId owner vendor commission:uint --module deal --no-message
This command does the following -
- Generate the proto type definition for
newDeal
(new_deal.proto
). - Updates the
genesis.proto
file. - Generate the query messages(
query.proto
). - The query message handlers. (
grpc_query_new_deal.go
). - The query proto and query proto grpc gateway files. (
query.pb.go, query.pb.gw.go
). - Generates the cli commands to invoke the query handlers (
query_new_deal.go
). - Keeper methods to serialize/deserialize data and crud operations on the store.(
new_deal.go
). - Test files and test simulation parameters.
// new_deal.protosyntax = "proto3";
package Harry027.deal.deal;
option go_package = "github.com/Harry-027/deal/x/deal/types";message NewDeal {
string dealId = 1;
string owner = 2;
string vendor = 3;
uint64 commission = 4;
}
// query.protomessage QueryGetNewDealRequest {
string index = 1;
}message QueryGetNewDealResponse {
NewDeal newDeal = 1 [(gogoproto.nullable) = false];
}message QueryAllNewDealRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}message QueryAllNewDealResponse {
repeated NewDeal newDeal = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
// new_deal.gopackage keeperimport (
"github.com/Harry-027/deal/x/deal/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
)// SetNewDeal sets a specific newDeal in the store from its index
func (k Keeper) SetNewDeal(ctx sdk.Context, newDeal types.NewDeal) {store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.NewDealKeyPrefix))
b := k.cdc.MustMarshal(&newDeal)
store.Set(types.NewDealKey(
newDeal.DealId,
), b)
}// GetNewDeal returns a newDeal from its indexfunc (k Keeper) GetNewDeal(
ctx sdk.Context,
index string,
) (val types.NewDeal, found bool) {store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.NewDealKeyPrefix))
b := store.Get(types.NewDealKey(
index,
))if b == nil {
return val, false
}
k.cdc.MustUnmarshal(b, &val)
return val, true
}// RemoveNewDeal removes a newDeal from the store
func (k Keeper) RemoveNewDeal(
ctx sdk.Context,
index string,
) {store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.NewDealKeyPrefix))
store.Delete(types.NewDealKey(
index,
))
}// GetAllNewDeal returns all newDealfunc (k Keeper) GetAllNewDeal(ctx sdk.Context) (list []types.NewDeal) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.NewDealKeyPrefix))
iterator := sdk.KVStorePrefixIterator(store, []byte{}) defer iterator.Close() for ; iterator.Valid(); iterator.Next() {
var val types.NewDeal
k.cdc.MustUnmarshal(iterator.Value(), &val)
list = append(list, val)
}
return
}
Get, Set and remove operations for deals have been defined on keeper object (as shown above). Note that we are using prefix store for storing deals with prefix key NewDeal/value/
(key_new_deal.go
).
Let’s modify the grpc_query_new_deal.go
file to handle deal queries.
NewDeal
will fetch us the deal details based on a given dealId(req.Index
).We will be saving the deal object against dealId. Therefore, fetch the deal against the given req.index
which is dealId
in our case.
// NewDeal is the query handler to fetch the deal details for a given dealIdfunc (k Keeper) NewDeal(c context.Context, req *types.QueryGetNewDealRequest) (*types.QueryGetNewDealResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}ctx := sdk.UnwrapSDKContext(c)
val, found := k.GetNewDeal(
ctx,
req.Index,
)if !found {
return nil, status.Error(codes.InvalidArgument, "not found")
}
return &types.QueryGetNewDealResponse{NewDeal: val}, nil
}
NewDealAll
will iterate over keyStore to fetch all the saved deals from store. It also includes pagination feature to paginate the response.
// NewDealAll is the query handler to fetch all the new dealsfunc (k Keeper) NewDealAll(c context.Context, req *types.QueryAllNewDealRequest) (*types.QueryAllNewDealResponse, error) {if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}var newDeals []types.NewDeal
ctx := sdk.UnwrapSDKContext(c)
store := ctx.KVStore(k.storeKey)newDealStore := prefix.NewStore(store, types.KeyPrefix(types.NewDealKeyPrefix))pageRes, err := query.Paginate(newDealStore, req.Pagination, func(key []byte, value []byte) error {
var newDeal types.NewDeal
if err := k.cdc.Unmarshal(value, &newDeal); err != nil {
return err
}
newDeals = append(newDeals, newDeal)
return nil
})if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}return &types.QueryAllNewDealResponse{NewDeal: newDeals, Pagination: pageRes}, nil
}
Now similar to dealCounter
and newDeal
type, lets generate the contractCounter
and newContract
type with the help of starport scaffold command. Our contractCounter and newContract proto definitions should look be as given below -
// contract_counter.protomessage ContractCounter {
string dealId = 1; // id of the deal to which contract belongs to
uint64 idValue = 2; // counter to generate the contract id
}
// new_contract.protomessage NewContract {
string dealId = 1; // Id of the deal to which contract belongs to
string contractId = 2; // contractId
string consumer = 3; // consumer address
string desc = 4; // order description
uint32 ownerETA = 5; // estimated time of order delivery in mins
uint32 vendorETA = 6; // estimated time of order shipping in mins
string status = 7; // contract current status
uint64 fees = 8; // order payment figure
string expiry = 9; //expiry time before which it should get approved
uint32 shippingDelay = 10; // shipping delay in mins
string startTime = 11; // start time of order
uint32 deliveryDelay = 12; // // delivery delay in mins
}
We will be using prefix store here with prefix key NewContract/value/{dealId}/
to store the contracts. Note that dealId
here is the id of the deal under which contract has been initiated.
Lets modify the new_contract.go
(to handle the getter, setter for contracts)and grpc_query_new_contract.go
(query handler) as shown below-
// new_contract.go
package keeperimport (
"github.com/Harry-027/deal/x/deal/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
)// SetNewContract set a specific newContract in the store -"NewContract/value/{dealId}"func (k Keeper) SetNewContract(ctx sdk.Context, newContract types.NewContract) {key := types.NewContractKey(newContract.DealId)
store := prefix.NewStore(ctx.KVStore(k.storeKey), key)
b := k.cdc.MustMarshal(&newContract)store.Set(types.NewContractKey(
newContract.ContractId,
), b)
}// GetNewContract returns a newContract from its indexfunc (k Keeper) GetNewContract(
ctx sdk.Context,
dealId string,
contractId string,
) (val types.NewContract, found bool) {
storeKey := types.NewContractKey(dealId)
store := prefix.NewStore(ctx.KVStore(k.storeKey), storeKey)
b := store.Get(types.NewContractKey(
contractId,
))if b == nil {
return val, false
}k.cdc.MustUnmarshal(b, &val)
return val, true
}// RemoveNewContract removes a newContract from the store
func (k Keeper) RemoveNewContract(
ctx sdk.Context,
dealId string,
contractId string,
) {
storeKey := types.NewContractKey(dealId)
store := prefix.NewStore(ctx.KVStore(k.storeKey), storeKey)
store.Delete(types.NewContractKey(
contractId,
))
}// GetAllNewContract returns all newContractfunc (k Keeper) GetAllNewContract(ctx sdk.Context, dealId string) (list []types.NewContract) { storeKey := types.NewContractKey(dealId)
store := prefix.NewStore(ctx.KVStore(k.storeKey), storeKey)
iterator := sdk.KVStorePrefixIterator(store, []byte{}) defer iterator.Close()for ; iterator.Valid(); iterator.Next() {
var val types.NewContract
k.cdc.MustUnmarshal(iterator.Value(), &val)
list = append(list, val)
}
return
}
// grpc_query_new_contract.gopackage keeperimport (
"context"
"github.com/Harry-027/deal/x/deal/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)// NewContractAll is the query handler to fetch all the contracts under given dealIdfunc (k Keeper) NewContractAll(c context.Context, req *types.QueryAllNewContractRequest) (*types.QueryAllNewContractResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
} var newContracts []types.NewContract
ctx := sdk.UnwrapSDKContext(c) prefixStoreKey := types.NewContractKey(req.DealId)
store := ctx.KVStore(k.storeKey)
newContractStore := prefix.NewStore(store, prefixStoreKey) pageRes, err := query.Paginate(newContractStore, req.Pagination, func(key []byte, value []byte) error {
var newContract types.NewContract
if err := k.cdc.Unmarshal(value, &newContract); err != nil {
return err
}
newContracts = append(newContracts, newContract)
return nil
})if err != nil {
return nil, status.Error(codes.Internal, err.Error())
} return &types.QueryAllNewContractResponse{NewContract: newContracts, Pagination: pageRes}, nil
}// NewContract is the query handler to fetch the contract for a given specific dealId and contractIdfunc (k Keeper) NewContract(c context.Context, req *types.QueryGetNewContractRequest) (*types.QueryGetNewContractResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "invalid request")
}
ctx := sdk.UnwrapSDKContext(c)
val, found := k.GetNewContract( ctx,
req.DealId,
req.ContractId,
)if !found {
return nil, status.Error(codes.InvalidArgument, "not found")
} return &types.QueryGetNewContractResponse{NewContract: val}, nil
}
Let us register the query server on module- (module.go
)
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module.func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {
types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx))
}
This will ensure that the module manager registers the query server of our custom deal
module.
Finally we are done handling the query part for deal and contracts! In next part will take care of handling msg service for transactions & state changes.
Refer the source code here- source code
Series
* Part-1
* Part-2
* Part-3
* Part-4
* Part-5
* Part-6
Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing