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, posts3// up to 20 winners, and seals the board. Pay then pulls FISH from the season4// wallet to those addresses. This realm does not watch the sea and does not5// 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 fishseason910import (11 "chain"12 "chain/runtime/unsafe"13 "strconv"14 "time"1516 fish "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/fishtoken"17)1819const (20 unit int64 = 1_000_00021 places = 2022 startUnix int64 = 1_792_108_80023)2425var seasonAddr = address("g1m6z3l9q577dltj73ym8fu0068peupqg468r5xe")2627// 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}3435var (36 inited bool37 admin address38 self address39 endUnix int6440 sealed bool41 winner [places]address42 paid [places]bool43)4445// clockUnix stays 0 on chain. The test in this package sets it. No call writes it.46var clockUnix int644748// 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 = caller56 self = cur.Address()57 inited = true58 chain.Emit("Init", "admin", admin.String())59}6061// 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 = unix71 chain.Emit("End", "unix", strconv.FormatInt(unix, 10))72}7374// 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] = who93 chain.Emit("Place", "place", strconv.Itoa(place), "who", who.String())94}9596// 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}108109// 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 = true119 chain.Emit("Seal", "end", strconv.FormatInt(endUnix, 10))120}121122// 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 must125// 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 := 0147 for i := 0; i < places; i++ {148 if paid[i] || !winner[i].IsValid() {149 continue150 }151 who := winner[i]152 amt := prizeWhole[i] * unit153 paid[i] = true154 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}162163// 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 = next173 chain.Emit("Admin", "next", next.String())174}175176func Version() string { return "fishseason-1" }177178func Admin() address { return admin }179180func Realm() address { return self }181182func Start() int64 { return startUnix }183184func End() int64 { return endUnix }185186func Sealed() bool { return sealed }187188func SeasonWallet() address { return seasonAddr }189190func PlaceOf(place int) address { return winner[slotOf(place)] }191192func PrizeOf(place int) int64 { return prizeWhole[slotOf(place)] * unit }193194func PaidOf(place int) bool { return paid[slotOf(place)] }195196func Due() int64 {197 var sum int64198 for i := 0; i < places; i++ {199 if !paid[i] && winner[i].IsValid() {200 sum += prizeWhole[i] * unit201 }202 }203 return sum204}205206func 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}222223func nowUnix() int64 {224 if clockUnix != 0 {225 return clockUnix226 }227 return time.Now().Unix()228}229230func slotOf(place int) int {231 if place < 1 || place > places {232 panic("fishseason: place")233 }234 return place - 1235}236237func filled() int {238 n := 0239 for i := 0; i < places; i++ {240 if winner[i].IsValid() {241 n++242 }243 }244 return n245}246247func requireReady() {248 if !inited {249 panic("fishseason: init")250 }251}252253func requireAdmin(caller address) {254 if caller != admin {255 panic("fishseason: admin")256 }257}258259func requireUser(cur realm) address {260 if !cur.Previous().IsUserCall() {261 panic("fishseason: eoa")262 }263 return cur.Previous().Address()264}265266func takeExact(n int64) {267 sent := unsafe.OriginSend()268 if n == 0 {269 if len(sent) != 0 {270 panic("fishseason: payment")271 }272 return273 }274 panic("fishseason: payment")275}276Admin() string
ClearPlace(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, place int)
Due() int64
End() int64
Init(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
PaidOf(place int) bool
Pay(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
PlaceOf(place int) string
PrizeOf(place int) int64
Realm() string
Render(path string) string
Seal(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Sealed() bool
SeasonWallet() string
SetEnd(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, unix int64)
SetPlace(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, place int, who string)
Start() int64
TransferAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, next string)
Version() string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.