Implement end blocker to process auctions
This commit is contained in:
parent
7681f88985
commit
667fd2f9eb
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user