Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

englishauction.gno

6.42 Kb · 220 lines
  1// Package englishauction is an idiomatic gno.land port of the classic Solidity
  2// English (ascending-bid) auction. Anyone can Start an auction for a named item
  3// with a duration in blocks; bidders call Bid with a strictly increasing amount.
  4// The previous highest bid becomes refundable (claim it with Withdraw). After
  5// the auction's end height anyone may call End, awarding the item to the highest
  6// bidder. Auctions and pending refunds live in ordered avl trees so Render can
  7// iterate deterministically.
  8package englishauction
  9
 10import (
 11	"strconv"
 12
 13	"chain"
 14	"chain/runtime"
 15	"chain/runtime/unsafe"
 16
 17	"gno.land/p/nym-moultest123/kit/store/v0"
 18	"gno.land/p/nt/avl/v0"
 19)
 20
 21// Auction is a single ascending-bid auction. It carries no ID field: the id
 22// belongs to the store, which hands it back on lookup and iteration.
 23type Auction struct {
 24	Seller        address
 25	Item          string
 26	EndHeight     int64
 27	HighestBid    uint64
 28	HighestBidder address
 29	Ended         bool
 30}
 31
 32var (
 33	// auctions assigns the auction ids. v0 kept its own nextID plus a key()
 34	// that zero-padded to width 12, which stopped ordering Render past 10^12.
 35	auctions = store.Named("englishauction: auction")
 36
 37	pending avl.Tree // "<id>:<addr>" -> uint64 refundable amount
 38)
 39
 40// pendKey formats the refund key for a given auction id and bidder. It uses
 41// the decimal id rather than the store key: pending is only ever point-read,
 42// never iterated, so it needs a readable key and not an ordered one.
 43func pendKey(id int64, bidder address) string {
 44	return strconv.FormatInt(id, 10) + ":" + bidder.String()
 45}
 46
 47// Start opens a new auction for `item` running for `durationBlocks` blocks from
 48// the current height. The caller becomes the seller. Returns the auction id.
 49func Start(cur realm, item string, durationBlocks int64) int64 {
 50	if item == "" {
 51		panic("englishauction: item must not be empty")
 52	}
 53	if durationBlocks <= 0 {
 54		panic("englishauction: duration must be > 0 blocks")
 55	}
 56	seller := unsafe.PreviousRealm().Address()
 57	a := &Auction{
 58		Seller:    seller,
 59		Item:      item,
 60		EndHeight: runtime.ChainHeight() + durationBlocks,
 61	}
 62	id := int64(auctions.Add(a))
 63
 64	chain.Emit("Start",
 65		"id", strconv.FormatInt(id, 10),
 66		"seller", seller.String(),
 67		"item", item,
 68		"endHeight", strconv.FormatInt(a.EndHeight, 10),
 69	)
 70	return id
 71}
 72
 73// Bid places a bid of `amount` on auction `id`. It must strictly exceed the
 74// current highest bid. The displaced highest bid becomes refundable to its
 75// bidder via Withdraw.
 76func Bid(cur realm, id int64, amount uint64) {
 77	a := mustGet(id)
 78	if a.Ended || runtime.ChainHeight() >= a.EndHeight {
 79		panic("englishauction: auction has ended")
 80	}
 81	if amount <= a.HighestBid {
 82		panic("englishauction: bid must strictly exceed current highest")
 83	}
 84	bidder := unsafe.PreviousRealm().Address()
 85	if bidder == a.Seller {
 86		panic("englishauction: seller cannot bid")
 87	}
 88
 89	// The prior top bid becomes refundable (accumulate in case of repeats).
 90	if a.HighestBid > 0 {
 91		pk := pendKey(id, a.HighestBidder)
 92		pending.Set(pk, pendingOf(id, a.HighestBidder)+a.HighestBid)
 93	}
 94
 95	a.HighestBid = amount
 96	a.HighestBidder = bidder
 97
 98	chain.Emit("Bid",
 99		"id", strconv.FormatInt(id, 10),
100		"bidder", bidder.String(),
101		"amount", strconv.FormatUint(amount, 10),
102	)
103}
104
105// Withdraw refunds the caller's accumulated displaced bids for auction `id`.
106// Returns the amount refunded (0 if nothing pending).
107func Withdraw(cur realm, id int64) uint64 {
108	mustGet(id) // ensure auction exists
109	caller := unsafe.PreviousRealm().Address()
110	amount := pendingOf(id, caller)
111	if amount == 0 {
112		return 0
113	}
114	pending.Remove(pendKey(id, caller))
115	chain.Emit("Withdraw",
116		"id", strconv.FormatInt(id, 10),
117		"bidder", caller.String(),
118		"amount", strconv.FormatUint(amount, 10),
119	)
120	return amount
121}
122
123// End closes auction `id` once its end height is reached, awarding the item to
124// the highest bidder (if any). Anyone may call it.
125func End(cur realm, id int64) {
126	a := mustGet(id)
127	if a.Ended {
128		panic("englishauction: auction already ended")
129	}
130	if runtime.ChainHeight() < a.EndHeight {
131		panic("englishauction: auction not yet over")
132	}
133	a.Ended = true
134
135	chain.Emit("End",
136		"id", strconv.FormatInt(id, 10),
137		"winner", a.HighestBidder.String(),
138		"amount", strconv.FormatUint(a.HighestBid, 10),
139	)
140}
141
142// --- read-only helpers (safe from tests and Render) ---
143
144// mustGet returns the auction for `id`, panicking if it does not exist. The
145// store's label puts the id in the message, which v0's fixed string
146// ("englishauction: no such auction") left out.
147func mustGet(id int64) *Auction {
148	return auctions.MustGet(store.ID(id)).(*Auction)
149}
150
151// pendingOf returns the refundable amount owed to `bidder` for auction `id`.
152func pendingOf(id int64, bidder address) uint64 {
153	v := pending.Get(pendKey(id, bidder))
154	if v == nil {
155		return 0
156	}
157	return v.(uint64)
158}
159
160// blocksLeft returns how many blocks remain before `a` can be ended (0 if the
161// end height has been reached).
162func blocksLeft(a *Auction, height int64) int64 {
163	if height >= a.EndHeight {
164		return 0
165	}
166	return a.EndHeight - height
167}
168
169// hasWinner reports whether the auction received at least one bid.
170func hasWinner(a *Auction) bool {
171	return a.HighestBid > 0
172}
173
174// Render lists every auction with item, top bid + bidder, time left, and winner.
175func Render(path string) string {
176	out := "# English Auction\n\n"
177	out += "Ascending-bid auctions. Start one, outbid others, and End it after "
178	out += "its block deadline to award the item to the top bidder.\n\n"
179
180	if auctions.Len() == 0 {
181		out += "_No auctions yet. Call `Start` to open one._\n"
182		return out
183	}
184
185	height := runtime.ChainHeight()
186	out += "_Current block height: " + strconv.FormatInt(height, 10) + "_\n\n"
187	out += "| ID | Item | Highest Bid | Bidder | Status | Winner |\n"
188	out += "|---:|------|------------:|--------|--------|--------|\n"
189
190	auctions.Each(func(id store.ID, v any) {
191		a := v.(*Auction)
192
193		bidStr := "—"
194		bidderStr := "—"
195		if hasWinner(a) {
196			bidStr = strconv.FormatUint(a.HighestBid, 10)
197			bidderStr = "`" + a.HighestBidder.String() + "`"
198		}
199
200		status := ""
201		winner := "—"
202		switch {
203		case a.Ended:
204			status = "ended"
205			if hasWinner(a) {
206				winner = "`" + a.HighestBidder.String() + "`"
207			} else {
208				winner = "no bids"
209			}
210		case height >= a.EndHeight:
211			status = "awaiting End"
212		default:
213			status = strconv.FormatInt(blocksLeft(a, height), 10) + " blocks left"
214		}
215
216		out += "| " + id.String() + " | " + a.Item + " | " +
217			bidStr + " | " + bidderStr + " | " + status + " | " + winner + " |\n"
218	})
219	return out
220}