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.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}