diff --git a/examples/gno.land/p/demo/auction/auction.gno b/examples/gno.land/p/demo/auction/auction.gno new file mode 100644 index 00000000000..27238191987 --- /dev/null +++ b/examples/gno.land/p/demo/auction/auction.gno @@ -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, + 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 +} diff --git a/examples/gno.land/p/demo/auction/auction_test.gno b/examples/gno.land/p/demo/auction/auction_test.gno new file mode 100644 index 00000000000..6d8e0ca1f35 --- /dev/null +++ b/examples/gno.land/p/demo/auction/auction_test.gno @@ -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() +} diff --git a/examples/gno.land/p/demo/auction/gno.mod b/examples/gno.land/p/demo/auction/gno.mod new file mode 100644 index 00000000000..b0063ea8f9a --- /dev/null +++ b/examples/gno.land/p/demo/auction/gno.mod @@ -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 +) diff --git a/examples/gno.land/r/demo/auction/auction.gno b/examples/gno.land/r/demo/auction/auction.gno new file mode 100644 index 00000000000..aefe36be126 --- /dev/null +++ b/examples/gno.land/r/demo/auction/auction.gno @@ -0,0 +1,163 @@ +package auction + +import ( + "bytes" + "std" + "strconv" + "time" + + auctionp "gno.land/p/demo/auction" + "gno.land/p/demo/mux" +) + +var ( + auctionList []*auctionp.Auction + currentTime time.Time + // router *mux.Router +) + +var router = mux.NewRouter() + +func init() { + router.HandleFunc("", renderHomepage) + router.HandleFunc("create/", renderCreateAuction) + router.HandleFunc("upcoming/", renderUpcomingAuctions) + router.HandleFunc("ongoing/", renderOngoingAuctions) + router.HandleFunc("/auction/{id}", renderAuctionDetails) +} + +// CreateAuction handles the creation of a new auction +func CreateAuction(title, description string, begin, end int64, price std.Coin) string { + owner := std.GetOrigCaller() + beginTime := time.Unix(begin, 0) + endTime := time.Unix(end, 0) + auction := auctionp.NewAuction(title, owner, description, beginTime, endTime, price) + auctionList = append(auctionList, auction) + return "Auction created successfully" +} + +// PlaceBid handles placing a bid on an auction +func PlaceBid(id int, amount std.Coin) string { + caller := std.GetOrigCaller() + if id >= len(auctionList) || id < 0 { + panic("Invalid auction ID") + } + auction := auctionList[id] + if err := auction.AddBid(caller, amount); err != nil { + panic(err.Error()) + } + return "Bid placed successfully" +} + +// EndAuction handles ending an auction +func EndAuction(id int) string { + caller := std.GetOrigCaller() + if id >= len(auctionList) || id < 0 { + panic("Invalid auction ID") + } + auction := auctionList[id] + if err := auction.EndAuction(caller); err != nil { + panic(err.Error()) + } + return "Auction ended successfully" +} + +// Render renders the requested page +func Render(path string) string { + return router.Render(path) +} + +// renderHomepage renders the homepage with links to different sections +func renderHomepage(res *mux.ResponseWriter, req *mux.Request) { + var b bytes.Buffer + b.WriteString("