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

lottery.gno

4.92 Kb · 184 lines
  1// Package lottery is an idiomatic gno.land port of the classic Solidity
  2// Lottery contract. Players Enter the current round (one entry per address per
  3// round). Once a round has at least two entrants, anyone may DrawWinner: a
  4// winner is picked deterministically from the block height (ChainHeight() %
  5// len(entrants), over the deterministically-sorted entrant set), recorded in
  6// the winners history, and a fresh round is opened.
  7//
  8// There is no real randomness on-chain, so "random" selection is derived from
  9// the block height — the same source Solidity ports use via block.number.
 10package lottery
 11
 12import (
 13	"strconv"
 14	"strings"
 15
 16	"chain"
 17	"chain/runtime"
 18	"chain/runtime/unsafe"
 19
 20	"gno.land/p/nt/avl/v0"
 21)
 22
 23// round is the current (open) round number, 1-based.
 24var round int = 1
 25
 26// entrants holds the current round's players: key = address string, value =
 27// the address. Iteration is deterministic (sorted by key), which makes winner
 28// selection reproducible.
 29var entrants avl.Tree
 30
 31// winners records past rounds: key = zero-padded round number (for ordered
 32// iteration), value = *winnerRecord.
 33var winners avl.Tree
 34
 35type winnerRecord struct {
 36	Round       int
 37	Winner      address
 38	NumEntrants int
 39	Height      int64
 40}
 41
 42// winnerKey zero-pads the round number so avl iteration yields chronological
 43// order for up to 10^9 rounds.
 44func winnerKey(r int) string {
 45	s := strconv.Itoa(r)
 46	for len(s) < 9 {
 47		s = "0" + s
 48	}
 49	return s
 50}
 51
 52// pickIndex derives the winning entrant index from the block height. Panics if
 53// there are no entrants. Pure helper (deterministic, no state) — safe to test.
 54func pickIndex(height int64, n int) int {
 55	if n <= 0 {
 56		panic("lottery: no entrants to draw from")
 57	}
 58	i := height % int64(n)
 59	if i < 0 {
 60		i += int64(n)
 61	}
 62	return int(i)
 63}
 64
 65// Enter adds the caller to the current round's entrants. Each address may only
 66// enter a given round once; a duplicate entry panics.
 67func Enter(cur realm) {
 68	player := unsafe.PreviousRealm().Address()
 69	key := player.String()
 70	if entrants.Has(key) {
 71		panic("lottery: already entered this round")
 72	}
 73	entrants.Set(key, player)
 74
 75	chain.Emit("Enter",
 76		"round", strconv.Itoa(round),
 77		"player", key,
 78		"entrants", strconv.Itoa(entrants.Size()),
 79	)
 80}
 81
 82// DrawWinner closes the current round, provided it has at least two entrants,
 83// picks the winner deterministically from the block height, records it, and
 84// opens a new empty round.
 85func DrawWinner(cur realm) address {
 86	n := entrants.Size()
 87	if n < 2 {
 88		panic("lottery: need at least 2 entrants to draw")
 89	}
 90
 91	height := runtime.ChainHeight()
 92	idx := pickIndex(height, n)
 93
 94	var winner address
 95	i := 0
 96	entrants.Iterate("", "", func(_ string, value interface{}) bool {
 97		if i == idx {
 98			winner = value.(address)
 99			return true // stop
100		}
101		i++
102		return false
103	})
104
105	rec := &winnerRecord{
106		Round:       round,
107		Winner:      winner,
108		NumEntrants: n,
109		Height:      height,
110	}
111	winners.Set(winnerKey(round), rec)
112
113	chain.Emit("DrawWinner",
114		"round", strconv.Itoa(round),
115		"winner", winner.String(),
116		"entrants", strconv.Itoa(n),
117		"height", strconv.FormatInt(height, 10),
118	)
119
120	// Open a fresh round.
121	round++
122	entrants = avl.Tree{}
123
124	return winner
125}
126
127// CurrentRound returns the currently open round number.
128func CurrentRound() int {
129	return round
130}
131
132// NumEntrants returns how many players have entered the current round.
133func NumEntrants() int {
134	return entrants.Size()
135}
136
137// HasEntered reports whether addr is in the current round.
138func HasEntered(addr address) bool {
139	return entrants.Has(addr.String())
140}
141
142// Render shows the current round, its entrants, and the winners history.
143func Render(path string) string {
144	var b strings.Builder
145	b.WriteString("# Lottery\n\n")
146	b.WriteString("Enter the open round, then anyone can draw once it has ")
147	b.WriteString("at least two players. The winner is picked from the block ")
148	b.WriteString("height and a new round opens automatically.\n\n")
149
150	b.WriteString("## Round " + strconv.Itoa(round) + " (open)\n\n")
151	if entrants.Size() == 0 {
152		b.WriteString("_No entrants yet. Call `Enter` to join._\n\n")
153	} else {
154		b.WriteString(strconv.Itoa(entrants.Size()) + " entrant(s):\n\n")
155		entrants.Iterate("", "", func(key string, _ interface{}) bool {
156			b.WriteString("- `" + key + "`\n")
157			return false
158		})
159		b.WriteString("\n")
160		if entrants.Size() < 2 {
161			b.WriteString("_Need at least 2 entrants before a draw._\n\n")
162		} else {
163			b.WriteString("_Ready to draw: call `DrawWinner`._\n\n")
164		}
165	}
166
167	b.WriteString("## Past winners\n\n")
168	if winners.Size() == 0 {
169		b.WriteString("_No rounds drawn yet._\n")
170	} else {
171		b.WriteString("| Round | Winner | Entrants | Block Height |\n")
172		b.WriteString("|---|---|---|---|\n")
173		winners.Iterate("", "", func(_ string, value interface{}) bool {
174			r := value.(*winnerRecord)
175			b.WriteString("| " + strconv.Itoa(r.Round) +
176				" | `" + r.Winner.String() + "`" +
177				" | " + strconv.Itoa(r.NumEntrants) +
178				" | " + strconv.FormatInt(r.Height, 10) + " |\n")
179			return false
180		})
181	}
182
183	return b.String()
184}