Building an application specific blockchain using Cosmos SDK Part-4
In part-3 we finished handling the query handlers and store keepers for contract and deals. Let’s now tackle the Msg server
.
Run the command to create the createDeal
message-
starport scaffold message createDeal vendor commission --module deal --response idValue
This command does the following -
- Creates the message definition (
tx.proto
).
message MsgCreateDeal {
string creator = 1; // added by starport, this is the msg creator
string vendor = 2; // will store vendor address
uint64 commission = 3; //vendor commission for each order completion
}message MsgCreateDealResponse {
string idValue = 1;
}
- Implements methods to satisfy the
sdk.Msg
interface (message_create_contract.go). - Creates deal message handler (
msg_server_create_deal.go
). - Generates the cli command to invoke the handler (
tx_create_deal.go
). - Registers the handler (
handler.go
).
Let us modify the message handler msg_server_create_deal.go
as shown below -
// msg_server_create_deal.gopackage keeperimport (
"context"
"strconv"
"github.com/Harry-027/deal/x/deal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)func (k msgServer) CreateDeal(goCtx context.Context, msg *types.MsgCreateDeal) (*types.MsgCreateDealResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) dealCounter, found := k.Keeper.GetDealCounter(ctx)
if !found {
panic("DealCounter not found")
} dealId := strconv.FormatUint(dealCounter.IdValue, 10) newDeal := types.NewDeal{
DealId: dealId,
Owner: msg.Creator,
Vendor: msg.Vendor,
Commission: msg.Commission,
}// validate before processing the message
err := newDeal.Validate()
if err != nil {
return nil, err
}k.Keeper.SetNewDeal(ctx, newDeal)
dealCounter.IdValue++
k.Keeper.SetDealCounter(ctx, dealCounter)// Set the new contract counter for a newly created deal
contractCounter := types.ContractCounter{
DealId: dealId,
IdValue: 0,
}k.Keeper.SetContractCounter(ctx, contractCounter)ctx.GasMeter().ConsumeGas(types.CREATE_GAS, "Create Deal")ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(types.IDVALUE, dealId),
sdk.NewAttribute(types.OWNER, newDeal.Owner),
sdk.NewAttribute(types.VENDOR, newDeal.Vendor),
),
)return &types.MsgCreateDealResponse{
IdValue: dealId,
}, nil
}
Observe, at the end of execution we are consuming certain amount of gas. This is to incentivise the validators and discourage the spam Txs. We have defined gas units under constants (contract_utils.go
).We are also emitting a custom event to inform the clients about details of successful Tx.
Note that in logic handling part, we first fetch the dealCounter
to get the dealId
and then save the deal
in store. Also before saving deal we are validating the message with the help of Validate()
method which will validate the vendor
address and commission
-
// deal_utils.go
package typesimport (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)// utility funcs
func (newDeal *NewDeal) GetOwnerAddress() (owner sdk.AccAddress, err error) {
owner, errInvalidOwner := sdk.AccAddressFromBech32(newDeal.Owner)
return owner, sdkerrors.Wrapf(errInvalidOwner, ErrInvalidOwner.Error(), newDeal.Owner)
}func (newDeal *NewDeal) GetVendorAddress() (vendor sdk.AccAddress, err error) {
vendor, errInvalidVendor := sdk.AccAddressFromBech32(newDeal.Vendor)
return vendor, sdkerrors.Wrapf(errInvalidVendor, ErrInvalidVendor.Error(), newDeal.Vendor)
}func (newDeal *NewDeal) ValidateCommission() (err error) {
if 1 <= newDeal.Commission && 100 >= newDeal.Commission {
return nil
}
return ErrInvalidCommission
}func (newDeal *NewDeal) Validate() (err error) {
_, err = newDeal.GetOwnerAddress()
if err != nil {
return err
}_, err = newDeal.GetVendorAddress()
if err != nil {
return err
}err = newDeal.ValidateCommission()
return err
}
Lets code for the cli command tx_create_deal.go
-
package cliimport (
"github.com/Harry-027/deal/x/deal/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/spf13/cast"
"github.com/spf13/cobra"
)func CmdCreateDeal() *cobra.Command {
cmd := &cobra.Command{
Use: "create-deal [vendor] [commission]",
Short: "Broadcast message createDeal",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) (err error) { argVendor := args[0]
argCommission, err := cast.ToUint64E(args[1])
if err != nil {
return err
} clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
} msg := types.NewMsgCreateDeal(
clientCtx.GetFromAddress().String(),
argVendor,
argCommission,
) if err := msg.ValidateBasic(); err != nil {
return err
} return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
} flags.AddTxFlagsToCmd(cmd)
return cmd
}
Note that method msg.ValidateBasic()
(as used above) also gets invoked by cosmos sdk in checkTx
method to validate the Tx so that tendermint can avoid invalid Txs in mempool.
Now, let us scaffold the createContract command -
starport scaffold message createContract dealId consumer desc ownerETA expiry fees --module deal --response idValue
Our proto definitions would look like -
message MsgCreateContract {
string creator = 1;
string dealId = 2;
string consumer = 3;
string desc = 4;
string ownerETA = 5;
string expiry = 6;
uint64 fees = 7;
}message MsgCreateContractResponse {
string idValue = 1;
string contractStatus = 2;
}
The contractStatus
field has been added later manually. After adding the field contractStatus
field, run the command to regenerate proto files-
starport generate proto-go
Modify the handler as below- (msg_server_create_contract.go
)
package keeper
import (
"context"
"strconv"
"time"
"github.com/Harry-027/deal/x/deal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)// CreateContract is the tx handler to handle create contract messages
func (k msgServer) CreateContract(goCtx context.Context, msg *types.MsgCreateContract) (*types.MsgCreateContractResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
deal, found := k.Keeper.GetNewDeal(ctx, msg.DealId) if !found {
return nil, types.ErrDealNotFound
} // validate if the tx came from owner if msg.Creator != deal.Owner {
return nil, types.ErrInvalidOwner
} contractCounter, found := k.Keeper.GetContractCounter(ctx, msg.DealId)if !found {
return nil, types.ErrDealNotFound
}contractCounter.IdValue++
contractId := strconv.FormatUint(contractCounter.IdValue, 10)
etaInMins, err := strconv.Atoi(msg.OwnerETA)if err != nil {
return nil, types.ErrInvalidTime
}expiryInMins, err := strconv.Atoi(msg.Expiry)
if err != nil {
return nil, types.ErrInvalidTime
}expiry := ctx.BlockTime().Add(time.Duration(expiryInMins) * time.Minute)// create a new contract under the given dealIdnewContract := types.NewContract{
DealId: msg.DealId,
ContractId: contractId,
Consumer: msg.Consumer,
Desc: msg.Desc,
OwnerETA: uint32(etaInMins),
Expiry: expiry.UTC().Format(types.TIME_FORMAT),
Fees: msg.Fees,
StartTime: ctx.BlockTime().UTC().Format(types.TIME_FORMAT),
Status: types.INITIATED,
}k.Keeper.SetNewContract(ctx, newContract)
k.Keeper.SetContractCounter(ctx, contractCounter)
ctx.GasMeter().ConsumeGas(types.CREATE_GAS, "Create Contract")ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeyAction, types.INITIATED),
sdk.NewAttribute(types.IDVALUE, newContract.ContractId),
sdk.NewAttribute(types.START_TIME, newContract.StartTime),
),
)return &types.MsgCreateContractResponse{IdValue: contractId, ContractStatus: types.INITIATED}, nil}
We are storing the contract against the key- NewContract/value/{contractId}/
under the store with prefix key NewContract/value/{dealId}/
. Also, we validate the createContract
Txs can be processed only if tx originates from the deal owner.
Once contract has been created by dealOwner, Vendor needs to commit & consumer needs to approve the same before expiry time or else contract would be considered as expired. To do so we will generate two more messages- commitContract
and approveContract
.The tx commitContract
will input the shippingTime (vendorETA
) in mins which is required as a commitment of order completion from vendor side. Scaffolding these two messages is left upto you as an exercise :)
Let’s take a look at the commitContract
handler -
// msg_server_commit_contract.go
package keeperimport (
"context"
"strconv"
"time"
"github.com/Harry-027/deal/x/deal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)// CommitContract is the tx handler to handle commit contract messagesfunc (k msgServer) CommitContract(goCtx context.Context, msg *types.MsgCommitContract) (*types.MsgCommitContractResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
deal, found := k.Keeper.GetNewDeal(ctx, msg.DealId)
if !found {
return nil, types.ErrDealNotFound
}// validate is the transaction is coming from vendor
if msg.Creator != deal.Vendor {
return nil, types.ErrInvalidVendor
}contract, found := k.Keeper.GetNewContract(ctx, msg.DealId, msg.ContractId)if !found {
return nil, types.ErrContractNotFound
}
expiry, err := time.Parse(types.TIME_FORMAT, contract.Expiry)if err != nil {
panic("invalid expiry time")
}// don't process the expired contracts
if ctx.BlockTime().After(expiry) {
return nil, types.ErrContractExpired
}etaInMins, err := strconv.Atoi(msg.VendorETA)if err != nil {
return nil, types.ErrInvalidTime
}// validate the vendor ETA
if (contract.OwnerETA / 2) < uint32(etaInMins) {
return nil, types.ErrVendorETA
}// process and emit the custom event
contract.Status = types.COMMITTED
contract.VendorETA = uint32(etaInMins)
k.Keeper.SetNewContract(ctx, contract)ctx.GasMeter().ConsumeGas(types.PROCESS_GAS, "Commit Contract")ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeyAction, types.COMMITTED),
sdk.NewAttribute(types.IDVALUE, contract.ContractId),
sdk.NewAttribute(types.VENDOR_ETA, strconv.FormatUint(uint64(contract.VendorETA), 10)),
sdk.NewAttribute(types.OWNER_ETA, strconv.FormatUint(uint64(contract.OwnerETA), 10)),
),
)return &types.MsgCommitContractResponse{IdValue: contract.ContractId, ContractStatus: contract.Status}, nil
}
Under commitContract
we validate the following before changing the contract status to committed -
- If the transaction is coming from vendor.
- If the contract hasn’t been expired.
- If vendorETA ≤ ownerETA /2.
Let take a look at approveContract
handler -
// msg_server_approve_contract.gopackage keeperimport (
"context"
"time"
"github.com/Harry-027/deal/x/deal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)// ApproveContract is the tx handler for handling approve contract messages
func (k msgServer) ApproveContract(goCtx context.Context, msg *types.MsgApproveContract) (*types.MsgApproveContractResponse, error) {ctx := sdk.UnwrapSDKContext(goCtx)
contract, found := k.Keeper.GetNewContract(ctx, msg.DealId, msg.ContractId)if !found {
return nil, types.ErrContractNotFound
}// handle validation before processing
err := msg.DealHandlerValidation(goCtx, &contract)
if err != nil {
return nil, err
}expiryTime, err := time.Parse(types.TIME_FORMAT, contract.Expiry)
if err != nil {
return nil, err
}// don't process the expired contracts
if ctx.BlockTime().After(expiryTime) {
return nil, types.ErrContractExpired
}// store funds from user account to module escrow account and approve the contractconsumerAddress, err := contract.GetConsumerAddress()
if err != nil {
panic("Invalid consumer address")
}err = k.bank.SendCoinsFromAccountToModule(ctx, consumerAddress, types.ModuleName, sdk.NewCoins(contract.GetCoin(contract.Fees)))if err != nil {
return nil, sdkerrors.Wrapf(err, types.ErrPaymentFailed.Error())
}contract.Status = types.APPROVED
k.Keeper.SetNewContract(ctx, contract)// consume the gas to incentivize validators
ctx.GasMeter().ConsumeGas(types.PROCESS_GAS, "Approve Contract")// emit custom event that clients can subscribe to
ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeyAction, types.APPROVED),
sdk.NewAttribute(types.IDVALUE, contract.ContractId),
),
)return &types.MsgApproveContractResponse{
IdValue: contract.ContractId,
ContractStatus: contract.Status,
}, nil
}
The approveContract
tx will be initiated by consumer . This tx will perform the following steps -
- Validate if the tx is signed by consumer.
- Validate if the contract has been committed.
- Validate if the contract hasn’t been expired.
- Transfer funds from consumer’s account to module escrow account.
Note that the definition for msg.DealHandlerValidation
used in handler is as given below -
// message_approve_contract.gofunc (msg *MsgApproveContract) DealHandlerValidation(goCtx context.Context, contract *NewContract) error {if msg.Creator != contract.Consumer {
return ErrInvalidConsumer
}if contract.Status != COMMITTED {
return ErrNotCommitted
}return nil
}
We are using bank keeper here for fund transfer operations. In order to access any of the capability exposed by bank keeper we need to inject bank keeper interface in our concrete keeper type -
// keeper.go
// concrete keeper type for deal module also includes bank & account keeper interface for handling fund related transactionstype (
Keeper struct {
auth types.AccountKeeper
bank types.BankKeeper
cdc codec.BinaryCodec
storeKey sdk.StoreKey
memKey sdk.StoreKey
paramstore paramtypes.Subspace
})
Therefore our keeper
constructor would change as below -
func NewKeeper(
auth types.AccountKeeper,
bank types.BankKeeper,
cdc codec.BinaryCodec,
storeKey,
memKey sdk.StoreKey,
ps paramtypes.Subspace,
) *Keeper {
// set KeyTable if it has not already been set
if !ps.HasKeyTable() {
ps = ps.WithKeyTable(types.ParamKeyTable())
}return &Keeper{
auth: auth,
bank: bank,
cdc: cdc,
storeKey: storeKey,
memKey: memKey,
paramstore: ps,
}
}
Lets pass the bank keeper interface in our keeper construction during initialisation phase in app.go
// app.goapp.DealKeeper = *dealmodulekeeper.NewKeeper(
app.AccountKeeper,
app.BankKeeper,
appCodec,
keys[dealmoduletypes.StoreKey],
keys[dealmoduletypes.MemStoreKey],
app.GetSubspace(dealmoduletypes.ModuleName),
)
As we are passing bank keeper interface which is not of concrete type, our keeper concrete type needs to understand which of the method from bank keeper interface would be used.Therefore define an interface
under expected_keepers.go
for methods we are going to access from bank keeper -
// BankKeeper defines the expected interface needed to retrieve account balances.type BankKeeper interface {
SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error// Methods imported from bank should be defined here
}// AccountKeeper defines the expected account keeper used for simulations (noalias)type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI
GetModuleAddress(name string) sdk.AccAddress
// Methods imported from account should be defined here
}
Note that we will also be using Account keeper in orderDelivered
handler. Therefore pass the same in our concrete keeper type, exactly the same way as we did for bank keeper interface.
After the contract has been approved, we must emit the custom event to let the FE client know about the contract status. This will inform the vendor about contract approval so that vendor can start processing the order. Once the order is ready, vendor will initiate a tx to change the contract status to INDELIVERY
. This tx will also calculate if there is any shipping delay based on the initial shipping commit time (vendorETA
). Let’s take a look at handler -
//msg_server_ship_order.go// ShipOrder is the tx handler for shipOrder messages from Vendor
func (k msgServer) ShipOrder(goCtx context.Context, msg *types.MsgShipOrder) (*types.MsgShipOrderResponse, error) {ctx := sdk.UnwrapSDKContext(goCtx)
deal, found := k.Keeper.GetNewDeal(ctx, msg.DealId)
if !found {
return nil, types.ErrDealNotFound
}// validate if the tx is from vendor
if msg.Creator != deal.Vendor {
return nil, types.ErrInvalidVendor
}contract, found := k.Keeper.GetNewContract(ctx, msg.DealId,
msg.ContractId)
if !found {
return nil, types.ErrContractNotFound
}if contract.Status != types.APPROVED || contract.Status == types.COMPLETED {
return nil, types.ErrNotApprovedOrCompleted
}startTime, err := time.Parse(types.TIME_FORMAT, contract.StartTime)
if err != nil {
panic("invalid start time")
}// Calculate shipping delay if any (will be used later to calculate delay penalty)
shippingExpectedTime := startTime.Add(time.Duration(contract.VendorETA))shippingActualTime := ctx.BlockTime()
if shippingActualTime.After(shippingExpectedTime) {
shippingTimeDelay := shippingActualTime.Sub(shippingExpectedTime).Minutes()
contract.ShippingDelay = uint32(shippingTimeDelay)
}// mark the contract status as in delivery
contract.Status = types.INDELIVERY
k.Keeper.SetNewContract(ctx, contract)ctx.GasMeter().ConsumeGas(types.PROCESS_GAS, "Order shipped")ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeyAction, types.INDELIVERY),
sdk.NewAttribute(types.IDVALUE, contract.ContractId),
),
)return &types.MsgShipOrderResponse{IdValue: contract.ContractId, ContractStatus: contract.Status}, nil
}
Here we are recording the shipping delay if any, before changing the contract status to INDELIVERY
. This will help us to calculate the delay charges once the order has been delivered. Note that we have current time available from ctx.BlockTime()
. Based on delay charges, payments will be calculated for dealOwner and vendor. And delay charges if any, will be refunded back to the consumer account.
Once the order reaches the consumer doorstep, he needs to initiate a tx for order completion. This tx will mark the order as completed and will settle all the funds based on vendor commission and delay charges. Let’s take a brief look into orderDelivered
handler.
// msg_server_order_delivered.gopackage keeperimport (
"context"
"strconv"
"time"
"github.com/Harry-027/deal/x/deal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)func (k msgServer) OrderDelivered(goCtx context.Context, msg *types.MsgOrderDelivered) (*types.MsgOrderDeliveredResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
logger := k.Logger(ctx)
deal, found := k.Keeper.GetNewDeal(ctx, msg.DealId)if !found {
return nil, types.ErrDealNotFound
}// validate before processing the message
contract, found := k.Keeper.GetNewContract(ctx, msg.DealId, msg.ContractId)
if !found {
return nil, types.ErrContractNotFound
}if msg.Creator != contract.Consumer {
return nil, types.ErrInvalidConsumer
}if contract.Status != types.INDELIVERY || contract.Status == types.COMPLETED {
return nil, types.ErrNotShipped
}startTime, err := time.Parse(types.TIME_FORMAT, contract.StartTime)
if err != nil {
panic("invalid start time")
}deliveryExpectedTime := startTime.Add(time.Duration(contract.OwnerETA))
deliveryActualTime := ctx.BlockTime()
logger.Debug("deliveryExpectedTime :: ", deliveryExpectedTime)
logger.Debug("deliveryActualTime :: ", deliveryActualTime)// calculate the delivery delay in case if any
if deliveryActualTime.After(deliveryExpectedTime) {
deliveryTimeDelay := deliveryActualTime.Sub(deliveryExpectedTime).Minutes()
logger.Debug("deliveryTimeDelay :: ", deliveryTimeDelay)if contract.ShippingDelay != 0 {
// subtract the shipping delay if any
deliveryTimeDelay = deliveryTimeDelay - float64(contract.ShippingDelay)
logger.Debug("deliveryTimeDelay after subtracting shipping delay", deliveryTimeDelay)
}contract.DeliveryDelay = uint32(deliveryTimeDelay)
}timeTaken := deliveryActualTime.Sub(startTime).Minutes()
logger.Debug("timeTaken :: ", timeTaken)var refundAmount float64 = 0
orderPayment := float64(contract.Fees)// calculate the penalty for late delivery/shipping for owner & vendor respectively
if timeTaken != 0 {
vendorSlashPercent := float64(contract.ShippingDelay) / timeTaken
logger.Debug("vendorSlashPercent :: ", vendorSlashPercent)ownerSlashPercent := float64(contract.DeliveryDelay) / timeTakenlogger.Debug("ownerSlashPercent :: ", ownerSlashPercent)refundAmount = (vendorSlashPercent + ownerSlashPercent) * orderPayment * 0.01
logger.Debug("refundAmount :: ", refundAmount)
}//deduct delay charges from payment
totalPay := uint64(orderPayment - refundAmount)
logger.Debug("TotalPay :: ", totalPay)
moduleAccount := k.auth.GetModuleAddress(types.ModuleName)
moduleBalance := k.bank.GetBalance(ctx, moduleAccount, types.TOKEN)if moduleBalance.IsLT(contract.GetCoin(totalPay)) {
panic("Escrow account insufficient balance")
}// calculate the vendor payment for given order based on deal commission
vendorPay := uint64(0.01 * float64(deal.Commission*totalPay))
logger.Debug("vendorPay :: ", vendorPay)// calculate the owner payment
ownerPay := totalPay - vendorPay
logger.Debug("ownerPay :: ", ownerPay)// validate the addresses for different parties involved in the contract
consumerAddress, err := contract.GetConsumerAddress()
if err != nil {
panic("Invalid consumer address")
}ownerAddress, err := deal.GetOwnerAddress()
if err != nil {
panic("Invalid owner address")
}
vendorAddress, err := deal.GetVendorAddress()if err != nil {
panic("Invalid vendor address")
}// process the payment for all the parties
// refund the delay charges to consumererr = k.bank.SendCoinsFromModuleToAccount(ctx, types.ModuleName, consumerAddress, sdk.NewCoins(contract.GetCoin(uint64(refundAmount))))
if err != nil {
return nil, sdkerrors.Wrapf(err, types.ErrPaymentFailed.Error())
}err = k.bank.SendCoinsFromModuleToAccount(ctx, types.ModuleName, ownerAddress, sdk.NewCoins(contract.GetCoin(ownerPay)))
if err != nil {
return nil, sdkerrors.Wrapf(err, types.ErrPaymentFailed.Error())
}err = k.bank.SendCoinsFromModuleToAccount(ctx, types.ModuleName, vendorAddress, sdk.NewCoins(contract.GetCoin(vendorPay)))
if err != nil {
return nil, sdkerrors.Wrapf(err, types.ErrPaymentFailed.Error())
}// mark the contract status as completed as order has been delivered to consumer and settlement is complete
contract.Status = types.COMPLETED
k.Keeper.SetNewContract(ctx, contract)ctx.GasMeter().ConsumeGas(types.SETTLEMENT_GAS, "Order delivered")ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeyAction, types.COMPLETED),
sdk.NewAttribute(types.IDVALUE, contract.ContractId),
sdk.NewAttribute(types.CONSUMER, contract.Consumer),
sdk.NewAttribute(types.OWNER, deal.Owner),
sdk.NewAttribute(types.VENDOR, deal.Vendor),
sdk.NewAttribute(types.REFUND_PAY, strconv.FormatUint(uint64(refundAmount), 10)),
sdk.NewAttribute(types.OWNER_PAY, strconv.FormatUint(ownerPay, 10)),
sdk.NewAttribute(types.VENDOR_PAY, strconv.FormatUint(vendorPay, 10)),
),
)
return &types.MsgOrderDeliveredResponse{IdValue: contract.ContractId, ContractStatus: contract.Status}, nil
}
In orderDelivered
handler above , with the help of order starting time and shipping delay we calculate the delivery delay if there is any. The calculated delay charge is aggregated with shipping delay charge and refunded back to the consumer. Whereas vendor and owner are paid according to the vendor commission and delay charges.
Let’s add a handler to cancel the order, in case order delivery takes more time, user might want to cancel the order.
// msg_server_cancel_order.go// CancelOrder is the tx handler for handling cancel order messagesfunc (k msgServer) CancelOrder(goCtx context.Context, msg *types.MsgCancelOrder) (*types.MsgCancelOrderResponse, error) {ctx := sdk.UnwrapSDKContext(goCtx)
contract, found := k.Keeper.GetNewContract(ctx, msg.DealId, msg.ContractId)if !found {
return nil, types.ErrContractNotFound
}
err := msg.DealHandlerValidation(goCtx, &contract)if err != nil {
return nil, err
}consumerAddress, err := contract.GetConsumerAddress()
if err != nil {
panic("Invalid consumer address")
}// Validate is the escrow account has enough balance to process the refund
moduleAccount := k.auth.GetModuleAddress(types.ModuleName)
moduleBalance := k.bank.GetBalance(ctx, moduleAccount, types.TOKEN)if moduleBalance.IsLT(contract.GetCoin(contract.Fees)) {
panic("Escrow account insufficient balance")
}// Send coins from contract account to the consumer accounterr = k.bank.SendCoinsFromModuleToAccount(ctx, types.ModuleName, consumerAddress, sdk.NewCoins(contract.GetCoin(contract.Fees)))if err != nil {
return nil, sdkerrors.Wrapf(err, types.ErrPaymentFailed.Error())
}// mark contract status as cancelled
contract.Status = types.CANCELLED
k.Keeper.SetNewContract(ctx, contract)ctx.GasMeter().ConsumeGas(types.PROCESS_GAS, "Cancel Contract")ctx.EventManager().EmitEvent(
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeyAction, types.CANCELLED),
sdk.NewAttribute(types.IDVALUE, contract.ContractId),
),
)return &types.MsgCancelOrderResponse{IdValue: contract.ContractId, ContractStatus: contract.Status}, nil
}
In case order delivery is delayed by 20 minutes, we allow user to cancel the order and refund back the complete amount to user account address.
Finally we are done with Msg server handlers. Do verify that our msg handler has been registered by the module -
//module.gofunc (am AppModule) Route() sdk.Route {
return sdk.NewRoute(types.RouterKey, NewHandler(am.keeper))
}
Now before starting blockchain and testing out the messages and queries, let us rectify the genesis state to initialise the deal counter value.
// types/genesis.gofunc DefaultGenesis() *GenesisState {
return &GenesisState{
DealCounter: &DealCounter{
IdValue: uint64(1),
},
NewDealList: []NewDeal{},
ContractCounter: nil,
NewContractList: []NewContract{},
// this line is used by starport scaffolding # genesis/types/default
Params: DefaultParams(),
}
}
Make sure we initialise and export the genesis state properly -
// x/deal/genesis.gopackage dealimport (
"github.com/Harry-027/deal/x/deal/keeper"
"github.com/Harry-027/deal/x/deal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// InitGenesis initializes the capability module's state from a provided genesis state.
func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) {// Set if defined
if genState.DealCounter != nil {
k.SetDealCounter(ctx, *genState.DealCounter)
}// Set all the newDeal
for _, elem := range genState.NewDealList {
k.SetNewDeal(ctx, elem)
}for _, elem := range genState.ContractCounter {
k.SetContractCounter(ctx, *elem)
}// Set all the newContract
for _, elem := range genState.NewContractList {
k.SetNewContract(ctx, elem)
}// this line is used by starport scaffolding # genesis/module/init
k.SetParams(ctx, genState.Params)
}// ExportGenesis returns the capability module's exported genesis.
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState {
genesis := types.DefaultGenesis()
genesis.Params = k.GetParams(ctx)
// Get all dealCounter
dealCounter, found := k.GetDealCounter(ctx)
if found {
genesis.DealCounter = &dealCounter
}genesis.NewDealList = k.GetAllNewDeal(ctx)
// Get all contractCounter
contractCounter, err := k.GetAllContractCounter(ctx)
if err == nil {
genesis.ContractCounter = contractCounter
}for _, counter := range contractCounter {
contractsForDealId := k.GetAllNewContract(ctx, counter.DealId)
genesis.NewContractList = append(genesis.NewContractList, contractsForDealId...)
}// this line is used by starport scaffolding # genesis/module/export
return genesis
}
Run the command starport chain serve
to spin up our development node.
Open another terminal & run the command starport chain build
, this will generate deald
binary which will be used to test txs and query against the running node.
In next part we will learn about the custom event indexing and how a user can subscribe to custom indexed events.
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