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}