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

englishauction.gno

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