Implement end blocker to process auctions

This commit is contained in:
Prathamesh Musale 2024-02-14 12:13:14 +05:30
parent 7681f88985
commit 667fd2f9eb
2 changed files with 256 additions and 3 deletions

View File

@ -4,6 +4,8 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"cosmossdk.io/collections"
"cosmossdk.io/collections/indexes"
@ -20,6 +22,9 @@ import (
auctiontypes "git.vdb.to/cerc-io/laconic2d/x/auction"
)
// CompletedAuctionDeleteTimeout => Completed auctions are deleted after this timeout (after reveals end time).
const CompletedAuctionDeleteTimeout = time.Hour * 24
type AuctionsIndexes struct {
Owner *indexes.Multi[string, string, auctiontypes.Auction]
}
@ -120,6 +125,23 @@ func (k Keeper) SaveAuction(ctx sdk.Context, auction *auctiontypes.Auction) erro
// return nil
}
// DeleteAuction - deletes the auction.
func (k Keeper) DeleteAuction(ctx sdk.Context, auction auctiontypes.Auction) error {
// Delete all bids first.
bids, err := k.GetBids(ctx, auction.Id)
if err != nil {
return err
}
for _, bid := range bids {
if err := k.DeleteBid(ctx, *bid); err != nil {
return err
}
}
return k.Auctions.Remove(ctx, auction.Id)
}
func (k Keeper) HasAuction(ctx sdk.Context, id string) (bool, error) {
has, err := k.Auctions.Has(ctx, id)
if err != nil {
@ -191,6 +213,29 @@ func (k Keeper) ListAuctions(ctx sdk.Context) ([]auctiontypes.Auction, error) {
return iter.Values()
}
// MatchAuctions - get all matching auctions.
func (k Keeper) MatchAuctions(ctx sdk.Context, matchFn func(*auctiontypes.Auction) (bool, error)) ([]*auctiontypes.Auction, error) {
var auctions []*auctiontypes.Auction
err := k.Auctions.Walk(ctx, nil, func(key string, value auctiontypes.Auction) (stop bool, err error) {
auctionMatched, err := matchFn(&value)
if err != nil {
return false, err
}
if auctionMatched {
auctions = append(auctions, &value)
}
return true, nil
})
if err != nil {
return nil, err
}
return auctions, nil
}
// GetAuction - gets a record from the store.
func (k Keeper) GetAuctionById(ctx sdk.Context, id string) (auctiontypes.Auction, error) {
auction, err := k.Auctions.Get(ctx, id)
@ -485,3 +530,210 @@ func (k Keeper) GetAuctionModuleBalances(ctx sdk.Context) sdk.Coins {
return balances
}
func (k Keeper) EndBlockerProcessAuctions(ctx sdk.Context) error {
var err error
// Transition auction state (commit, reveal, expired, completed).
if err = k.processAuctionPhases(ctx); err != nil {
return err
}
// Delete stale auctions.
return k.deleteCompletedAuctions(ctx)
}
func (k Keeper) processAuctionPhases(ctx sdk.Context) error {
auctions, err := k.MatchAuctions(ctx, func(_ *auctiontypes.Auction) (bool, error) {
return true, nil
})
if err != nil {
return err
}
for _, auction := range auctions {
// Commit -> Reveal state.
if auction.Status == auctiontypes.AuctionStatusCommitPhase && ctx.BlockTime().After(auction.CommitsEndTime) {
auction.Status = auctiontypes.AuctionStatusRevealPhase
if err = k.SaveAuction(ctx, auction); err != nil {
return err
}
ctx.Logger().Info(fmt.Sprintf("Moved auction %s to reveal phase.", auction.Id))
}
// Reveal -> Expired state.
if auction.Status == auctiontypes.AuctionStatusRevealPhase && ctx.BlockTime().After(auction.RevealsEndTime) {
auction.Status = auctiontypes.AuctionStatusExpired
if err = k.SaveAuction(ctx, auction); err != nil {
return err
}
ctx.Logger().Info(fmt.Sprintf("Moved auction %s to expired state.", auction.Id))
}
// If auction has expired, pick a winner from revealed bids.
if auction.Status == auctiontypes.AuctionStatusExpired {
if err = k.pickAuctionWinner(ctx, auction); err != nil {
return err
}
}
}
return nil
}
// Delete completed stale auctions.
func (k Keeper) deleteCompletedAuctions(ctx sdk.Context) error {
auctions, err := k.MatchAuctions(ctx, func(auction *auctiontypes.Auction) (bool, error) {
deleteTime := auction.RevealsEndTime.Add(CompletedAuctionDeleteTimeout)
return auction.Status == auctiontypes.AuctionStatusCompleted && ctx.BlockTime().After(deleteTime), nil
})
if err != nil {
return err
}
for _, auction := range auctions {
ctx.Logger().Info(fmt.Sprintf("Deleting completed auction %s after timeout.", auction.Id))
if err := k.DeleteAuction(ctx, *auction); err != nil {
return err
}
}
return nil
}
func (k Keeper) pickAuctionWinner(ctx sdk.Context, auction *auctiontypes.Auction) error {
ctx.Logger().Info(fmt.Sprintf("Picking auction %s winner.", auction.Id))
var highestBid *auctiontypes.Bid
var secondHighestBid *auctiontypes.Bid
bids, err := k.GetBids(ctx, auction.Id)
if err != nil {
return err
}
for _, bid := range bids {
ctx.Logger().Info(fmt.Sprintf("Processing bid %s %s", bid.BidderAddress, bid.BidAmount.String()))
// Only consider revealed bids.
if bid.Status != auctiontypes.BidStatusRevealed {
ctx.Logger().Info(fmt.Sprintf("Ignoring unrevealed bid %s %s", bid.BidderAddress, bid.BidAmount.String()))
continue
}
// Init highest bid.
if highestBid == nil {
highestBid = bid
ctx.Logger().Info(fmt.Sprintf("Initializing 1st bid %s %s", bid.BidderAddress, bid.BidAmount.String()))
continue
}
//nolint: all
if highestBid.BidAmount.IsLT(bid.BidAmount) {
ctx.Logger().Info(fmt.Sprintf("New highest bid %s %s", bid.BidderAddress, bid.BidAmount.String()))
secondHighestBid = highestBid
highestBid = bid
ctx.Logger().Info(fmt.Sprintf("Updated 1st bid %s %s", highestBid.BidderAddress, highestBid.BidAmount.String()))
ctx.Logger().Info(fmt.Sprintf("Updated 2nd bid %s %s", secondHighestBid.BidderAddress, secondHighestBid.BidAmount.String()))
} else if secondHighestBid == nil || secondHighestBid.BidAmount.IsLT(bid.BidAmount) {
ctx.Logger().Info(fmt.Sprintf("New 2nd highest bid %s %s", bid.BidderAddress, bid.BidAmount.String()))
secondHighestBid = bid
ctx.Logger().Info(fmt.Sprintf("Updated 2nd bid %s %s", secondHighestBid.BidderAddress, secondHighestBid.BidAmount.String()))
} else {
ctx.Logger().Info(fmt.Sprintf("Ignoring bid as it doesn't affect 1st/2nd price %s %s", bid.BidderAddress, bid.BidAmount.String()))
}
}
// Highest bid is the winner, but pays second highest bid price.
auction.Status = auctiontypes.AuctionStatusCompleted
if highestBid != nil {
auction.WinnerAddress = highestBid.BidderAddress
auction.WinningBid = highestBid.BidAmount
// Winner pays 2nd price, if a 2nd price exists.
auction.WinningPrice = highestBid.BidAmount
if secondHighestBid != nil {
auction.WinningPrice = secondHighestBid.BidAmount
}
ctx.Logger().Info(fmt.Sprintf("Auction %s winner %s.", auction.Id, auction.WinnerAddress))
ctx.Logger().Info(fmt.Sprintf("Auction %s winner bid %s.", auction.Id, auction.WinningBid.String()))
ctx.Logger().Info(fmt.Sprintf("Auction %s winner price %s.", auction.Id, auction.WinningPrice.String()))
} else {
ctx.Logger().Info(fmt.Sprintf("Auction %s has no valid revealed bids (no winner).", auction.Id))
}
if err := k.SaveAuction(ctx, auction); err != nil {
return err
}
for _, bid := range bids {
bidderAddress, err := sdk.AccAddressFromBech32(bid.BidderAddress)
if err != nil {
ctx.Logger().Error(fmt.Sprintf("Invalid bidderAddress address. %v", err))
panic("Invalid bidder address.")
}
if bid.Status == auctiontypes.BidStatusRevealed {
// Send reveal fee back to bidders that've revealed the bid.
sdkErr := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, auctiontypes.ModuleName, bidderAddress, sdk.NewCoins(bid.RevealFee))
if sdkErr != nil {
ctx.Logger().Error(fmt.Sprintf("Auction error returning reveal fee: %v", sdkErr))
panic(sdkErr)
}
}
// Send back locked bid amount to all bidders.
sdkErr := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, auctiontypes.ModuleName, bidderAddress, sdk.NewCoins(bid.BidAmount))
if sdkErr != nil {
ctx.Logger().Error(fmt.Sprintf("Auction error returning bid amount: %v", sdkErr))
panic(sdkErr)
}
}
// Process winner account (if nobody bids, there won't be a winner).
if auction.WinnerAddress != "" {
winnerAddress, err := sdk.AccAddressFromBech32(auction.WinnerAddress)
if err != nil {
ctx.Logger().Error(fmt.Sprintf("Invalid winner address. %v", err))
panic("Invalid winner address.")
}
// Take 2nd price from winner.
sdkErr := k.bankKeeper.SendCoinsFromAccountToModule(ctx, winnerAddress, auctiontypes.ModuleName, sdk.NewCoins(auction.WinningPrice))
if sdkErr != nil {
ctx.Logger().Error(fmt.Sprintf("Auction error taking funds from winner: %v", sdkErr))
panic(sdkErr)
}
// Burn anything over the min. bid amount.
amountToBurn := auction.WinningPrice.Sub(auction.MinimumBid)
if amountToBurn.IsNegative() {
ctx.Logger().Error("Auction coins to burn cannot be negative.")
panic("Auction coins to burn cannot be negative.")
}
// Use auction burn module account instead of actually burning coins to better keep track of supply.
sdkErr = k.bankKeeper.SendCoinsFromModuleToModule(ctx, auctiontypes.ModuleName, auctiontypes.AuctionBurnModuleAccountName, sdk.NewCoins(amountToBurn))
if sdkErr != nil {
ctx.Logger().Error(fmt.Sprintf("Auction error burning coins: %v", sdkErr))
panic(sdkErr)
}
}
// TODO
// Notify other modules (hook).
// ctx.Logger().Info(fmt.Sprintf("Auction %s notifying %d modules.", auction.Id, len(k.usageKeepers)))
// for _, keeper := range k.usageKeepers {
// ctx.Logger().Info(fmt.Sprintf("Auction %s notifying module %s.", auction.Id, keeper.ModuleName()))
// keeper.OnAuctionWinnerSelected(ctx, auction.Id)
// }
return nil
}

View File

@ -3,13 +3,14 @@ package module
import (
"context"
sdk "github.com/cosmos/cosmos-sdk/types"
"git.vdb.to/cerc-io/laconic2d/x/auction/keeper"
)
// EndBlocker is called every block
func EndBlocker(ctx context.Context, k keeper.Keeper) error {
// TODO: Implement
// k.EndBlockerProcessAuctions(ctx)
sdkCtx := sdk.UnwrapSDKContext(ctx)
return nil
return k.EndBlockerProcessAuctions(sdkCtx)
}