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

englishauction.gno

6.41 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/moul/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}