This document provide step by steps guide on creating a seal bid auction chain with fairyring
pep module.
The following binaries are for testing the integration:
-
Install Hermes relayer by following the official guide
-
Install fairyring binary by following this guide
-
Install encrypter by following this guide
-
Install ShareGenerator by following this guide
-
Install Fairyport by following this guide
ignite scaffold chain auction
- Import pep module by adding the following lines to the import section in
app/app.go
pepmodule "github.com/Fairblock/fairyring/x/pep"
pepmodulekeeper "github.com/Fairblock/fairyring/x/pep/keeper"
pepmoduletypes "github.com/Fairblock/fairyring/x/pep/types"
It will look something like this:
package app
import (
pepmodule "github.com/Fairblock/fairyring/x/pep"
pepmodulekeeper "github.com/Fairblock/fairyring/x/pep/keeper"
pepmoduletypes "github.com/Fairblock/fairyring/x/pep/types"
"encoding/json"
"fmt"
...
)
- Add pep modules to
app/app.go
:
- Add the module to
ModuleBasics
ModuleBasics = module.NewBasicManager(
// ...
pepmodule.AppModuleBasic{},
)
- Update module account permissions
maccPerms = map[string][]string{
// ...
pepmoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner, authtypes.Staking},
}
- Add keepers to app
type App struct {
// ...
ScopedIBCKeeper capabilitykeeper.ScopedKeeper
ScopedTransferKeeper capabilitykeeper.ScopedKeeper
ScopedICAHostKeeper capabilitykeeper.ScopedKeeper
ScopedPepKeeper capabilitykeeper.ScopedKeeper
PepKeeper pepmodulekeeper.Keeper
// ...
}
- update kv store keys
keys := sdk.NewKVStoreKeys(
// ...
auctionmoduletypes.StoreKey, pepmoduletypes.StoreKey,
)
- configure keepers and module
scopedPepKeeper := app.CapabilityKeeper.ScopeToModule(pepmoduletypes.ModuleName)
app.PepKeeper = *pepmodulekeeper.NewKeeper(
appCodec,
keys[pepmoduletypes.StoreKey],
keys[pepmoduletypes.MemStoreKey],
app.GetSubspace(pepmoduletypes.ModuleName),
app.IBCKeeper.ChannelKeeper,
&app.IBCKeeper.PortKeeper,
scopedPepKeeper,
app.IBCKeeper.ConnectionKeeper,
app.BankKeeper,
)
pepModule := pepmodule.NewAppModule(
appCodec,
app.PepKeeper,
app.AccountKeeper,
app.BankKeeper,
app.MsgServiceRouter(),
encodingConfig.TxConfig,
app.SimCheck,
)
pepIBCModule := pepmodule.NewIBCModule(app.PepKeeper)
- Add IBC route
ibcRouter.AddRoute(icahosttypes.SubModuleName, icaHostIBCModule).
AddRoute(ibctransfertypes.ModuleName, transferIBCModule).
AddRoute(pepmoduletypes.ModuleName, pepIBCModule)
- Add to module manager
app.mm = module.NewManager(
// ...
icaModule,
auctionModule,
pepModule,
// ...
)
- Set begin and end blockers
app.mm.SetOrderBeginBlockers(
// ...
pepmoduletypes.ModuleName,
)
app.mm.SetOrderEndBlockers(
// ...
pepmoduletypes.ModuleName,
)
- Modify genesis modules
genesisModuleOrder := []string{
// ...
pepmoduletypes.ModuleName,
}
- Scoped keeper
app.ScopedPepKeeper = scopedPepKeeper
- Init params keeper
func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino, key, tkey storetypes.StoreKey) paramskeeper.Keeper {
paramsKeeper := paramskeeper.NewKeeper(appCodec, legacyAmino, key, tkey)
// ...
paramsKeeper.Subspace(pepmoduletypes.ModuleName)
return paramsKeeper
}
- Add the following line to the end of
go.mod
replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
- Run
go mod tidy
- Create aution type & messages
ignite scaffold list auction name startPrice:coin duration:uint createdAt:uint currentHighestBidId:uint highestBidExists:bool ended:bool --module auction --no-simulation
- Create bid types & message for user to place bid
ignite scaffold list bid auctionId:uint bidPrice:coin --module auction --no-simulation
- Create finalize-auction message for auction creator to end the auction
ignite scaffold message finalize-auction auctionId:uint --module auction
- Create
FinalizedAuction
types
ignite scaffold list finalizedAuction auctionId:uint bidId:uint finalPrice:coin bidder creator --module auction --no-simulation --no-message
- Implement all the errors for all the messages
- Replace line 10 - 12 in
x/auction/types/errors/go
with the code below
var (
AuctionEnded = sdkerrors.Register(ModuleName, 1100, "target auction already ended")
AuctionNotFound = sdkerrors.Register(ModuleName, 1200, "target auction not found")
AuctionPriceInvalid = sdkerrors.Register(ModuleName, 1300, "auction start price must larger than 0")
AuctionDurationInvalid = sdkerrors.Register(ModuleName, 1400, "auction duration must be at least 5")
AuctionDurationPassed = sdkerrors.Register(ModuleName, 1500, "auction duration passed, not accepting new bid")
AuctionFinalizeTooEarly = sdkerrors.Register(ModuleName, 1600, "please wait until auction duration passed to finalize the result")
BidNotFound = sdkerrors.Register(ModuleName, 1700, "target bid not found")
BidPriceLow = sdkerrors.Register(ModuleName, 1800, "bid price is lower / equals to the highest bid / auction start price")
NotAuctionOwner = sdkerrors.Register(ModuleName, 1900, "you are not the owner of this auction")
InsufficientBalance = sdkerrors.Register(ModuleName, 2000, "insufficient balance for the bid price")
InternalError = sdkerrors.Register(ModuleName, 500, "internal error")
)
- Implement logic for create auction message
CreateAuction()
inx/auction/keeper/msg_server_auction.go
func (k msgServer) CreateAuction(goCtx context.Context, msg *types.MsgCreateAuction) (*types.MsgCreateAuctionResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
if msg.StartPrice.IsZero() {
return nil, types.AuctionPriceInvalid
}
if msg.Duration < 5 {
return nil, types.AuctionDurationInvalid
}
var auction = types.Auction{
Creator: msg.Creator,
Name: msg.Name,
StartPrice: msg.StartPrice,
Duration: msg.Duration,
CreatedAt: uint64(ctx.BlockHeight()),
Ended: false,
}
id := k.AppendAuction(
ctx,
auction,
)
return &types.MsgCreateAuctionResponse{
Id: id,
}, nil
}
- Add
UpdateAuctionHighestBidId()
&EndAuction()
to/x/auction/keeper/auction.go
// UpdateAuctionHighestBidId set the highest bid id of auction
func (k Keeper) UpdateAuctionHighestBidId(ctx sdk.Context, id uint64, bidId uint64) error {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.AuctionKey))
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, id)
b := store.Get(bz)
if b == nil {
return types.AuctionNotFound
}
var auction types.Auction
k.cdc.MustUnmarshal(b, &auction)
auction.HighestBidExists = true
auction.CurrentHighestBidId = bidId
appendedValue := k.cdc.MustMarshal(&auction)
store.Set(bz, appendedValue)
return nil
}
// EndAuction set the auction status to ended
func (k Keeper) EndAuction(ctx sdk.Context, id uint64) error {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.AuctionKey))
bz := make([]byte, 8)
binary.BigEndian.PutUint64(bz, id)
b := store.Get(bz)
if b == nil {
return types.AuctionNotFound
}
var auction types.Auction
k.cdc.MustUnmarshal(b, &auction)
auction.Ended = true
appendedValue := k.cdc.MustMarshal(&auction)
store.Set(bz, appendedValue)
return nil
}
- Remove
UpdateAuction
&DeleteAuction
message handler
-
Remove
UpdateAuction()
andDeleteAuction()
inx/auction/keeper/msg_server_aution.go
-
Remove
"fmt"
import, the import will look something like this:
import (
"auction/x/auction/types"
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
)
- Implement logic for create bid message
- Add
BankKeeper
to auction module keeper inx/auction/keeper/keeper.go
type (
Keeper struct {
cdc codec.BinaryCodec
storeKey storetypes.StoreKey
memKey storetypes.StoreKey
paramstore paramtypes.Subspace
bankKeeper types.BankKeeper
}
)
func NewKeeper(
cdc codec.BinaryCodec,
storeKey,
memKey storetypes.StoreKey,
ps paramtypes.Subspace,
bankKeeper types.BankKeeper,
) *Keeper {
// set KeyTable if it has not already been set
if !ps.HasKeyTable() {
ps = ps.WithKeyTable(types.ParamKeyTable())
}
return &Keeper{
cdc: cdc,
storeKey: storeKey,
memKey: memKey,
paramstore: ps,
bankKeeper: bankKeeper,
}
}
- Add all the
SendCoins()
function toBankKeeper
interface underx/auction/types/expected_keepers.go
// BankKeeper defines the expected interface needed to retrieve account balances.
type BankKeeper interface {
SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
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
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error
// Methods imported from bank should be defined here
}
- Add
BankKeeper
toAuctionKeeper
inapp/app.go
app.AuctionKeeper = *auctionmodulekeeper.NewKeeper(
appCodec,
keys[auctionmoduletypes.StoreKey],
keys[auctionmoduletypes.MemStoreKey],
app.GetSubspace(auctionmoduletypes.ModuleName),
app.BankKeeper,
)
- Implement logic for
CreateBid()
inx/auction/keeper/msg_server_bid.go
func (k msgServer) CreateBid(goCtx context.Context, msg *types.MsgCreateBid) (*types.MsgCreateBidResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// Check if the auction ID in the Create Bid Message exists
auction, found := k.GetAuction(ctx, msg.AuctionId)
if !found {
return nil, types.AuctionNotFound
}
// If the auction exists, check if it is ended
if auction.Ended {
return nil, types.AuctionEnded
}
// Check If the auction already passed the duration
if auction.CreatedAt+auction.Duration <= uint64(ctx.BlockHeight()) {
return nil, types.AuctionDurationPassed
}
bidPriceInCoins := sdk.NewCoins(msg.BidPrice)
// Check if the bid price is greater than the auction start price / current highest bid
if auction.StartPrice.IsGTE(msg.BidPrice) {
return nil, types.BidPriceLow
}
sender, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return nil, err
}
// Check if bid creator have sufficient coin for placing the bid
senderBalance := k.bankKeeper.SpendableCoins(ctx, sender)
if bidPriceInCoins.IsAllGT(senderBalance) {
return nil, types.InsufficientBalance
}
if auction.HighestBidExists {
// If highest bid exists, get the current highest bid
currentHighestBid, found := k.GetBid(ctx, auction.CurrentHighestBidId)
if !found {
return nil, types.InternalError
}
// Check if the current highest bid price is greater or equals to the bid price
if currentHighestBid.BidPrice.IsGTE(msg.BidPrice) {
return nil, types.BidPriceLow
}
// Current bid price is higher than the highest bid price, returning the coins to the highest bid creator
receiver, err := sdk.AccAddressFromBech32(currentHighestBid.Creator)
if err != nil {
return nil, err
}
if err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiver, sdk.NewCoins(currentHighestBid.BidPrice)); err != nil {
return nil, err
}
}
var bid = types.Bid{
Creator: msg.Creator,
AuctionId: msg.AuctionId,
BidPrice: msg.BidPrice,
}
id := k.AppendBid(ctx, bid)
// Update the highest bid to current bid
if err := k.UpdateAuctionHighestBidId(ctx, msg.AuctionId, id); err != nil {
return nil, err
}
// Send the coin from creator to the module
if err = k.bankKeeper.SendCoinsFromAccountToModule(
ctx,
sender,
types.ModuleName,
bidPriceInCoins,
); err != nil {
return nil, err
}
return &types.MsgCreateBidResponse{
Id: id,
}, nil
}
- Remove
UpdateBid
&DeleteBid
message handler
-
Remove
UpdateBid()
andDeleteBid()
inx/auction/keeper/msg_server_bid.go
-
Remove
"fmt"
import, the import will look something like this:
import (
"auction/x/auction/types"
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
)
- Implement finalize auction message logic
FinalizeAuction()
inx/auction/keeper/msg_server_finalize_auction.go
func (k msgServer) FinalizeAuction(goCtx context.Context, msg *types.MsgFinalizeAuction) (*types.MsgFinalizeAuctionResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
// Check if the provided auction exists
auction, found := k.GetAuction(ctx, msg.AuctionId)
if !found {
return nil, types.AuctionNotFound
}
// Check if the provided auction is ended
if auction.Ended {
return nil, types.AuctionEnded
}
// Make sure the message sender is the creator of the auction
if auction.Creator != msg.Creator {
return nil, types.NotAuctionOwner
}
// Make sure the auction passed its duration
if auction.CreatedAt+auction.Duration > uint64(ctx.BlockHeight()) {
return nil, types.AuctionFinalizeTooEarly
}
// Get the highest bid price
finalBidPrice := sdk.NewCoin("stake", sdk.NewInt(0))
bidCreator := ""
if auction.HighestBidExists {
bid, found := k.GetBid(ctx, auction.CurrentHighestBidId)
if found {
finalBidPrice = bid.BidPrice
bidCreator = bid.Creator
}
}
finalizedAuction := types.FinalizedAuction{
AuctionId: msg.AuctionId,
BidId: auction.CurrentHighestBidId,
FinalPrice: finalBidPrice,
Bidder: bidCreator,
Creator: msg.Creator,
}
id := k.AppendFinalizedAuction(ctx, finalizedAuction)
// End the auction
if err := k.EndAuction(ctx, msg.AuctionId); err != nil {
return nil, err
}
// If there is a bid, send the coins to auction creator
if !finalBidPrice.IsZero() {
receiver, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return nil, err
}
if err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiver, sdk.NewCoins(finalBidPrice)); err != nil {
return nil, err
}
}
return &types.MsgFinalizeAuctionResponse{
Id: id,
FinalPrice: finalBidPrice,
Bidder: bidCreator,
}, nil
}
- Update tx proto
- Remove Update Bid & Auction, Delete Bid & Auction TX Msgs in
proto/auction/auction/tx.proto
service Msg {
rpc CreateAuction (MsgCreateAuction ) returns (MsgCreateAuctionResponse );
rpc CreateBid (MsgCreateBid ) returns (MsgCreateBidResponse );
rpc FinalizeAuction (MsgFinalizeAuction) returns (MsgFinalizeAuctionResponse);
}
- Remove all the related messages
message MsgUpdateAuction {
string creator = 1;
uint64 id = 2;
string name = 3;
cosmos.base.v1beta1.Coin startPrice = 4 [(gogoproto.nullable) = false];
uint64 duration = 5;
uint64 createdAt = 6;
uint64 currentHighestBidId = 7;
bool highestBidExists = 8;
bool ended = 9;
}
message MsgUpdateAuctionResponse {}
message MsgDeleteAuction {
string creator = 1;
uint64 id = 2;
}
message MsgDeleteAuctionResponse {}
message MsgUpdateBid {
string creator = 1;
uint64 id = 2;
uint64 auctionId = 3;
cosmos.base.v1beta1.Coin bidPrice = 4 [(gogoproto.nullable) = false];
}
message MsgUpdateBidResponse {}
message MsgDeleteBid {
string creator = 1;
uint64 id = 2;
}
message MsgDeleteBidResponse {}
- Remove Update Bid & Auction, Delete Bid & Auction TX in cli client tx command
- Remove these lines in
GetTxCmd()
inx/auction/client/cli/tx.go
cmd.AddCommand(CmdUpdateAuction())
cmd.AddCommand(CmdDeleteAuction())
cmd.AddCommand(CmdUpdateBid())
cmd.AddCommand(CmdDeleteBid())
-
Remove
CmdUpdateAuction()
andCmdDeleteAuction()
and"strconv"
import inx/auction/client/cli/tx_auction.go
-
Remove
CmdUpdateBid()
andCmdDeleteBid()
and"strconv"
import inx/auction/client/cli/tx_bid.go
- Remove Update Bid & Auction, Delete Bid & Auction in messages type
- Remove all the following lines in
x/auction/types/messages_auction.go
var _ sdk.Msg = &MsgUpdateAuction{}
func NewMsgUpdateAuction(creator string, id uint64, name string, startPrice sdk.Coin, duration uint64, createdAt uint64, currentHighestBidId uint64, highestBidExists bool, ended bool) *MsgUpdateAuction {
return &MsgUpdateAuction{
Id: id,
Creator: creator,
Name: name,
StartPrice: startPrice,
Duration: duration,
CreatedAt: createdAt,
CurrentHighestBidId: currentHighestBidId,
HighestBidExists: highestBidExists,
Ended: ended,
}
}
func (msg *MsgUpdateAuction) Route() string {
return RouterKey
}
func (msg *MsgUpdateAuction) Type() string {
return TypeMsgUpdateAuction
}
func (msg *MsgUpdateAuction) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}
func (msg *MsgUpdateAuction) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg *MsgUpdateAuction) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
return nil
}
var _ sdk.Msg = &MsgDeleteAuction{}
func NewMsgDeleteAuction(creator string, id uint64) *MsgDeleteAuction {
return &MsgDeleteAuction{
Id: id,
Creator: creator,
}
}
func (msg *MsgDeleteAuction) Route() string {
return RouterKey
}
func (msg *MsgDeleteAuction) Type() string {
return TypeMsgDeleteAuction
}
func (msg *MsgDeleteAuction) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}
func (msg *MsgDeleteAuction) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg *MsgDeleteAuction) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
return nil
}
- Remove all the following lines in
x/auction/types/messages_bid.go
var _ sdk.Msg = &MsgUpdateBid{}
func NewMsgUpdateBid(creator string, id uint64, auctionId uint64, bidPrice sdk.Coin) *MsgUpdateBid {
return &MsgUpdateBid{
Id: id,
Creator: creator,
AuctionId: auctionId,
BidPrice: bidPrice,
}
}
func (msg *MsgUpdateBid) Route() string {
return RouterKey
}
func (msg *MsgUpdateBid) Type() string {
return TypeMsgUpdateBid
}
func (msg *MsgUpdateBid) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}
func (msg *MsgUpdateBid) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg *MsgUpdateBid) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
return nil
}
var _ sdk.Msg = &MsgDeleteBid{}
func NewMsgDeleteBid(creator string, id uint64) *MsgDeleteBid {
return &MsgDeleteBid{
Id: id,
Creator: creator,
}
}
func (msg *MsgDeleteBid) Route() string {
return RouterKey
}
func (msg *MsgDeleteBid) Type() string {
return TypeMsgDeleteBid
}
func (msg *MsgDeleteBid) GetSigners() []sdk.AccAddress {
creator, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
panic(err)
}
return []sdk.AccAddress{creator}
}
func (msg *MsgDeleteBid) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
func (msg *MsgDeleteBid) ValidateBasic() error {
_, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err)
}
return nil
}
- Remove Update Bid & Auction, Delete Bid & Auction in the codec
- Remove all update bid & auction, delete bid & auction code in
x/auction/types/codec.go
, The file should look like this:
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
)
func RegisterCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgCreateAuction{}, "auction/CreateAuction", nil)
cdc.RegisterConcrete(&MsgCreateBid{}, "auction/CreateBid", nil)
cdc.RegisterConcrete(&MsgFinalizeAuction{}, "auction/FinalizeAuction", nil)
// this line is used by starport scaffolding # 2
}
func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgCreateAuction{},
)
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgCreateBid{},
)
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgFinalizeAuction{},
)
// this line is used by starport scaffolding # 3
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}
var (
Amino = codec.NewLegacyAmino()
ModuleCdc = codec.NewProtoCodec(cdctypes.NewInterfaceRegistry())
)
- Update
MsgFinalizeAuctionResponse
to include Bidder address, ID and final price
- Update
MsgFinalizeAuctionResponse{}
inproto/auction/auction/tx.proto
to:
message MsgFinalizeAuctionResponse {
uint64 id = 1;
cosmos.base.v1beta1.Coin finalPrice = 2 [(gogoproto.nullable) = false];
string bidder = 3;
}