Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Initial implementation of auction dApp #2265

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions examples/gno.land/p/demo/auction/auction.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package auction

import (
"std"
"strconv"
"time"

"gno.land/p/demo/ownable"
"gno.land/p/demo/ufmt"
)

// Auction struct
type Auction struct {
Owner ownable.Ownable // Embeds Ownable to handle ownership
Title string
Description string
Begin time.Time
End time.Time
StartingBid std.Coin
Bids []*Bid
State string // "upcoming", "ongoing", or "closed"
}

// Bid struct
type Bid struct {
Bidder std.Address
Amount std.Coin
}

var (
auctionList []*Auction
currentTime time.Time
)

// NewAuction creates a new auction
func NewAuction(
title string,
owner std.Address,
description string,
begin time.Time,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having begin actually be time.Now?
This way you can decrease the param. It's only assuming you don't have auctions that start in the future, given that you emit an event when this constructor exits

end can be time.Duration if this is your preference.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the package with three auction states: “upcoming”, “ongoing”, and “closed”. That was my idea, so I could have auctions starting in the future.

end time.Time,
startingBid std.Coin,
) *Auction {
return &Auction{
Owner: *ownable.NewWithAddress(owner), // Initialize Ownable with the owner
Title: title,
Description: description,
Begin: begin,
End: end,
StartingBid: startingBid,
Bids: []*Bid{},
State: "upcoming",
}
}

// AddBid adds a new bid to the auction
func (a *Auction) AddBid(bidder std.Address, amount std.Coin) error {
if time.Now().Before(a.Begin) {
return ufmt.Errorf("auction: AddBid: auction has not started yet")
}
if time.Now().After(a.End) {
return ufmt.Errorf("auction: AddBid: auction has already ended")
}
if !amount.IsGTE(a.StartingBid) {
return ufmt.Errorf("auction: AddBid: bid amount must be higher than the current highest bid")
}
bid := &Bid{Bidder: bidder, Amount: amount}
a.Bids = append(a.Bids, bid)
a.StartingBid = amount
std.Emit("BidPlaced", "auction", a.Title, "bidder", bidder.String(), "amount", amount.String())
return nil
}

// EndAuction ends the auction
func (a *Auction) EndAuction() error {
if err := a.Owner.CallerIsOwner(); err != nil {
return err
}
if time.Now().Before(a.End) {
return ufmt.Errorf("auction: EndAuction: auction cannot end before the end time")
}
if a.State == "closed" {
return ufmt.Errorf("auction: EndAuction: auction is already closed")
}
if len(a.Bids) == 0 {
std.Emit("AuctionEndedNoBids", "auction", a.Title)
return ufmt.Errorf("auction: EndAuction: auction ended with no bids")
}
a.State = "closed"
highestBid := a.StartingBid
winner := a.Bids[len(a.Bids)-1].Bidder
std.Emit("AuctionEnded", "winner", winner.String(), "amount", highestBid.String())
return nil
}
172 changes: 172 additions & 0 deletions examples/gno.land/p/demo/auction/auction_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package auction

import (
"std"
"testing"
"time"

"gno.land/p/demo/ownable"
"gno.land/p/demo/testutils"
)

func setCurrentTime(t time.Time) {
currentTime = t
}

func resetCurrentTime() {
currentTime = time.Time{}
}

func TestNewAuction(t *testing.T) {
owner := std.Address("owner")
begin := time.Now().Add(1 * time.Hour)
end := time.Now().Add(24 * time.Hour)
startingBid := std.NewCoin("ugnot", 100)

auction := NewAuction("Test Auction", owner, "This is a test auction", begin, end, startingBid)

if auction.Title != "Test Auction" {
t.Fatalf("expected auction title to be 'Test Auction', got '%s'", auction.Title)
}
if auction.Owner.Owner() != owner { // Note the change here to access owner
t.Fatalf("expected auction owner to be '%s', got '%s'", owner, auction.Owner.Owner())
}
if auction.Description != "This is a test auction" {
t.Fatalf("expected auction description to be 'This is a test auction', got '%s'", auction.Description)
}
if auction.Begin != begin {
t.Fatalf("expected auction begin time to be '%s', got '%s'", begin, auction.Begin)
}
if auction.End != end {
t.Fatalf("expected auction end time to be '%s', got '%s'", end, auction.End)
}
if !auction.StartingBid.IsEqual(startingBid) {
t.Fatalf("expected auction starting bid to be '%s', got '%s'", startingBid.String(), auction.StartingBid.String())
}
if auction.State != "upcoming" {
t.Fatalf("expected auction state to be 'upcoming', got '%s'", auction.State)
}
}

func TestAddBid(t *testing.T) {
owner := std.Address("owner")
bidder1 := std.Address("bidder1")
bidder2 := std.Address("bidder2")
begin := time.Now().Add(1 * time.Hour)
end := time.Now().Add(24 * time.Hour)
startingBid := std.NewCoin("ugnot", 100)

auction := NewAuction("Test Auction", owner, "This is a test auction", begin, end, startingBid)

// Test before auction starts
setCurrentTime(time.Now())
err := auction.AddBid(bidder1, std.NewCoin("ugnot", 200))
if err == nil || err.Error() != "auction: AddBid: auction has not started yet" {
t.Fatalf("expected error 'auction has not started yet', got '%v'", err)
}
resetCurrentTime()

// Test successful bid
setCurrentTime(begin.Add(1 * time.Second))
err = auction.AddBid(bidder1, std.NewCoin("ugnot", 200))
resetCurrentTime()

if err != nil {
t.Fatalf("expected no error, got '%v'", err)
}
if !auction.StartingBid.IsEqual(std.NewCoin("ugnot", 200)) {
t.Fatalf("expected auction starting bid to be '200ugnot', got '%s'", auction.StartingBid.String())
}
if len(auction.Bids) != 1 {
t.Fatalf("expected number of bids to be '1', got '%d'", len(auction.Bids))
}
if auction.Bids[0].Bidder != bidder1 {
t.Fatalf("expected bidder to be 'bidder1', got '%s'", auction.Bids[0].Bidder)
}

// Test higher bid
setCurrentTime(begin.Add(2 * time.Second))
err = auction.AddBid(bidder2, std.NewCoin("ugnot", 300))
resetCurrentTime()

if err != nil {
t.Fatalf("expected no error, got '%v'", err)
}
if !auction.StartingBid.IsEqual(std.NewCoin("ugnot", 300)) {
t.Fatalf("expected auction starting bid to be '300ugnot', got '%s'", auction.StartingBid.String())
}
if len(auction.Bids) != 2 {
t.Fatalf("expected number of bids to be '2', got '%d'", len(auction.Bids))
}
if auction.Bids[1].Bidder != bidder2 {
t.Fatalf("expected bidder to be 'bidder2', got '%s'", auction.Bids[1].Bidder)
}

// Test bid lower than current price
setCurrentTime(begin.Add(3 * time.Second))
err = auction.AddBid(bidder1, std.NewCoin("ugnot", 250))
resetCurrentTime()
if err == nil || err.Error() != "auction: AddBid: bid amount must be higher than the current highest bid" {
t.Fatalf("expected error 'bid amount must be higher than the current highest bid', got '%v'", err)
}
}

func TestEndAuction(t *testing.T) {
owner := testutils.TestAddress("owner")
bidder := testutils.TestAddress("bidder")
begin := time.Now().Add(1 * time.Hour)
end := time.Now().Add(24 * time.Hour)
startingBid := std.NewCoin("ugnot", 100)

auction := NewAuction("Test Auction", owner, "This is a test auction", begin, end, startingBid)
owner.AssertCallerIsOwner()
// Test ending auction before it starts
setCurrentTime(begin.Add(-1 * time.Hour))

err := auction.EndAuction()
if err == nil || err.Error() != "auction: EndAuction: auction cannot end before the end time" {
t.Fatalf("expected error 'auction cannot end before the end time', got '%v'", err)
}
resetCurrentTime()

// Test ending auction with no bids
setCurrentTime(end.Add(1 * time.Second))
err = auction.EndAuction()
if err == nil || err.Error() != "auction: EndAuction: auction ended with no bids" {
t.Fatalf("expected error 'auction ended with no bids', got '%v'", err)
}
resetCurrentTime()

// Place a bid and end auction
setCurrentTime(begin.Add(1 * time.Second))
auction.AddBid(bidder, std.NewCoin("ugnot", 200))
setCurrentTime(end.Add(1 * time.Second))
err = auction.EndAuction()
resetCurrentTime()

if err != nil {
t.Fatalf("expected no error, got '%v'", err)
}
if auction.State != "closed" {
t.Fatalf("expected auction state to be 'closed', got '%s'", auction.State)
}
if len(auction.Bids) == 0 {
t.Fatalf("expected at least one bid to be present")
}
if auction.Bids[len(auction.Bids)-1].Bidder != bidder {
t.Fatalf("expected winner to be 'bidder', got '%s'", auction.Bids[len(auction.Bids)-1].Bidder)
}
}

// SetCurrentTime sets the current time for testing purposes
func SetCurrentTime(t time.Time) {
currentTime = t
}

// now returns the current time, allowing for mocking in tests
func now() time.Time {
if !currentTime.IsZero() {
return currentTime
}
return time.Now()
}
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/auction/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/p/demo/auction

require (
gno.land/p/demo/ownable v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
)
Loading
Loading