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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

fishseason.gno

6.29 Kb · 275 lines
  1// Package fishseason is the Season 1 paying board.
  2// The open is 2026-10-16 00:00:00 UTC. The deploy wallet sets the end, posts
  3// up to 20 winners, and seals the board. Pay then pulls FISH from the season
  4// wallet to those addresses. This realm does not watch the sea and does not
  5// treat a fishlive Settle as a win. fishlive is not called.
  6// The stage script rewrites the fishtoken import to the deploy wallet's package.
  7// A kill still pays no FISH by itself. fishseason-1.
  8package fishseason
  9
 10import (
 11	"chain"
 12	"chain/runtime/unsafe"
 13	"strconv"
 14	"time"
 15
 16	fish "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/fishtoken"
 17)
 18
 19const (
 20	unit      int64 = 1_000_000
 21	places          = 20
 22	startUnix int64 = 1_792_108_800
 23)
 24
 25var seasonAddr = address("g1m6z3l9q577dltj73ym8fu0068peupqg468r5xe")
 26
 27// Whole FISH for places 1 through 20. The sum is 100,000.
 28var prizeWhole = [places]int64{
 29	16_000, 12_500, 10_000, 8_000, 7_000,
 30	6_000, 5_500, 5_000, 4_500, 4_000,
 31	3_500, 3_000, 2_800, 2_500, 2_200,
 32	2_000, 1_800, 1_500, 1_200, 1_000,
 33}
 34
 35var (
 36	inited  bool
 37	admin   address
 38	self    address
 39	endUnix int64
 40	sealed bool
 41	winner [places]address
 42	paid   [places]bool
 43)
 44
 45// clockUnix stays 0 on chain. The test in this package sets it. No call writes it.
 46var clockUnix int64
 47
 48// Init records the deploy wallet as admin. It does not move FISH. No coins.
 49func Init(cur realm) {
 50	caller := requireUser(cur)
 51	takeExact(0)
 52	if inited {
 53		panic("fishseason: init")
 54	}
 55	admin = caller
 56	self = cur.Address()
 57	inited = true
 58	chain.Emit("Init", "admin", admin.String())
 59}
 60
 61// SetEnd stores the close, as Unix seconds. The deploy wallet only, and only before seal. No coins.
 62func SetEnd(cur realm, unix int64) {
 63	caller := requireUser(cur)
 64	takeExact(0)
 65	requireReady()
 66	requireAdmin(caller)
 67	if sealed || unix <= startUnix {
 68		panic("fishseason: end")
 69	}
 70	endUnix = unix
 71	chain.Emit("End", "unix", strconv.FormatInt(unix, 10))
 72}
 73
 74// SetPlace posts one winner. Places are 1 through 20. The same address cannot take two places. No coins.
 75func SetPlace(cur realm, place int, who address) {
 76	caller := requireUser(cur)
 77	takeExact(0)
 78	requireReady()
 79	requireAdmin(caller)
 80	if sealed {
 81		panic("fishseason: seal")
 82	}
 83	slot := slotOf(place)
 84	if !who.IsValid() || who == seasonAddr || who == cur.Address() {
 85		panic("fishseason: addr")
 86	}
 87	for i := 0; i < places; i++ {
 88		if i != slot && winner[i] == who {
 89			panic("fishseason: dup")
 90		}
 91	}
 92	winner[slot] = who
 93	chain.Emit("Place", "place", strconv.Itoa(place), "who", who.String())
 94}
 95
 96// ClearPlace removes one unsealed place. No coins.
 97func ClearPlace(cur realm, place int) {
 98	caller := requireUser(cur)
 99	takeExact(0)
100	requireReady()
101	requireAdmin(caller)
102	if sealed {
103		panic("fishseason: seal")
104	}
105	winner[slotOf(place)] = address("")
106	chain.Emit("Clear", "place", strconv.Itoa(place))
107}
108
109// Seal freezes the end and the places. At least one place must be posted. No coins.
110func Seal(cur realm) {
111	caller := requireUser(cur)
112	takeExact(0)
113	requireReady()
114	requireAdmin(caller)
115	if sealed || endUnix == 0 || filled() < 1 {
116		panic("fishseason: seal")
117	}
118	sealed = true
119	chain.Emit("Seal", "end", strconv.FormatInt(endUnix, 10))
120}
121
122// Pay sends each unpaid posted prize from the season wallet to that winner.
123// The board must be sealed and the close must have passed. No coins.
124// fishtoken must list this realm as a distributor, and the season wallet must
125// have approved this realm for the FISH. A second pay of the same place does nothing.
126func Pay(cur realm) {
127	requireUser(cur)
128	takeExact(0)
129	requireReady()
130	if !sealed {
131		panic("fishseason: seal")
132	}
133	if nowUnix() < endUnix {
134		panic("fishseason: early")
135	}
136	due := Due()
137	if due == 0 {
138		panic("fishseason: paid")
139	}
140	if !fish.IsDistributor(cur.Address()) {
141		panic("fishseason: grant")
142	}
143	if fish.Allowance(seasonAddr, cur.Address()) < due {
144		panic("fishseason: allowance")
145	}
146	n := 0
147	for i := 0; i < places; i++ {
148		if paid[i] || !winner[i].IsValid() {
149			continue
150		}
151		who := winner[i]
152		amt := prizeWhole[i] * unit
153		paid[i] = true
154		n++
155		fish.TransferFrom(cross(cur), seasonAddr, who, amt)
156		chain.Emit("Pay", "place", strconv.Itoa(i+1), "who", who.String(), "amount", strconv.FormatInt(amt, 10))
157	}
158	if n == 0 {
159		panic("fishseason: paid")
160	}
161}
162
163// TransferAdmin hands the board to next. Posted places stay. No coins.
164func TransferAdmin(cur realm, next address) {
165	caller := requireUser(cur)
166	takeExact(0)
167	requireReady()
168	requireAdmin(caller)
169	if !next.IsValid() || next == admin {
170		panic("fishseason: admin")
171	}
172	admin = next
173	chain.Emit("Admin", "next", next.String())
174}
175
176func Version() string { return "fishseason-1" }
177
178func Admin() address { return admin }
179
180func Realm() address { return self }
181
182func Start() int64 { return startUnix }
183
184func End() int64 { return endUnix }
185
186func Sealed() bool { return sealed }
187
188func SeasonWallet() address { return seasonAddr }
189
190func PlaceOf(place int) address { return winner[slotOf(place)] }
191
192func PrizeOf(place int) int64 { return prizeWhole[slotOf(place)] * unit }
193
194func PaidOf(place int) bool { return paid[slotOf(place)] }
195
196func Due() int64 {
197	var sum int64
198	for i := 0; i < places; i++ {
199		if !paid[i] && winner[i].IsValid() {
200			sum += prizeWhole[i] * unit
201		}
202	}
203	return sum
204}
205
206func Render(path string) string {
207	if path != "" {
208		return "# Season 1\n\nUnknown path.\n"
209	}
210	if !inited {
211		return "# Season 1\n\nNot opened.\n"
212	}
213	state := "open"
214	if sealed {
215		state = "sealed"
216	}
217	return "# Season 1\n\nStart " + strconv.FormatInt(startUnix, 10) +
218		". End " + strconv.FormatInt(endUnix, 10) +
219		". Board " + state +
220		". Due " + strconv.FormatInt(Due(), 10) + " base units.\n"
221}
222
223func nowUnix() int64 {
224	if clockUnix != 0 {
225		return clockUnix
226	}
227	return time.Now().Unix()
228}
229
230func slotOf(place int) int {
231	if place < 1 || place > places {
232		panic("fishseason: place")
233	}
234	return place - 1
235}
236
237func filled() int {
238	n := 0
239	for i := 0; i < places; i++ {
240		if winner[i].IsValid() {
241			n++
242		}
243	}
244	return n
245}
246
247func requireReady() {
248	if !inited {
249		panic("fishseason: init")
250	}
251}
252
253func requireAdmin(caller address) {
254	if caller != admin {
255		panic("fishseason: admin")
256	}
257}
258
259func requireUser(cur realm) address {
260	if !cur.Previous().IsUserCall() {
261		panic("fishseason: eoa")
262	}
263	return cur.Previous().Address()
264}
265
266func takeExact(n int64) {
267	sent := unsafe.OriginSend()
268	if n == 0 {
269		if len(sent) != 0 {
270			panic("fishseason: payment")
271		}
272		return
273	}
274	panic("fishseason: payment")
275}