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

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}