1// Package polls is an on-chain poll / voting realm for gno.land.2//3// Anyone can create a poll with a question and a comma-separated list of4// options. Each caller address may cast exactly one vote per poll. Render5// draws every poll with its options, tallies as ▓░ bar charts, percentages6// and the total vote count.7package polls89import (10 "chain"11 "chain/runtime/unsafe"12 "strconv"13 "strings"1415 "gno.land/p/nt/avl/v0"16)1718// Poll is a single question with its options and per-address vote tracking.19type Poll struct {20 ID int21 Question string22 Options []string23 Tallies []int24 Creator string // bech32 address of the poll creator25 voted *avl.Tree // address string -> struct{}{}, one vote per address26}2728var (29 polls []*Poll // index i holds the poll with ID i30 nextID int31)3233// CreatePoll registers a new poll. options is a comma-separated list; blank34// entries are dropped. Returns the new poll's id. Crossing function: an EOA35// invokes it via MsgCall; another realm via polls.CreatePoll(cross, q, opts).36func CreatePoll(cur realm, question string, options string) int {37 creator := unsafe.PreviousRealm().Address().String()3839 q := strings.TrimSpace(question)40 if q == "" {41 panic("question must not be empty")42 }4344 opts := parseOptions(options)45 if len(opts) < 2 {46 panic("a poll needs at least 2 options")47 }4849 id := nextID50 nextID++51 p := &Poll{52 ID: id,53 Question: q,54 Options: opts,55 Tallies: make([]int, len(opts)),56 Creator: creator,57 voted: avl.NewTree(),58 }59 polls = append(polls, p)6061 chain.Emit("PollCreated",62 "id", strconv.Itoa(id),63 "creator", creator,64 "options", strconv.Itoa(len(opts)),65 )66 return id67}6869// Vote casts one vote for optionIndex on poll pollID. Each caller address may70// vote at most once per poll. Crossing function.71func Vote(cur realm, pollID int, optionIndex int) {72 voter := unsafe.PreviousRealm().Address().String()7374 p := getPoll(pollID)75 if optionIndex < 0 || optionIndex >= len(p.Options) {76 panic("option index out of range")77 }78 if p.voted.Has(voter) {79 panic("address already voted on this poll")80 }8182 p.voted.Set(voter, struct{}{})83 p.Tallies[optionIndex]++8485 chain.Emit("Voted",86 "id", strconv.Itoa(pollID),87 "voter", voter,88 "option", strconv.Itoa(optionIndex),89 )90}9192// parseOptions splits a comma-separated option string, trimming whitespace and93// dropping empties. Pure logic — no realm state.94func parseOptions(options string) []string {95 var out []string96 for _, raw := range strings.Split(options, ",") {97 o := strings.TrimSpace(raw)98 if o != "" {99 out = append(out, o)100 }101 }102 return out103}104105// getPoll returns the poll with the given id or panics if it does not exist.106func getPoll(id int) *Poll {107 if id < 0 || id >= len(polls) {108 panic("poll not found")109 }110 return polls[id]111}112113// PollCount returns the number of polls created so far (read-only helper).114func PollCount() int {115 return len(polls)116}117118const barWidth = 20119120// bar renders a proportional ▓░ bar of fixed width for count out of total.121func bar(count, total int) string {122 filled := 0123 if total > 0 {124 filled = count * barWidth / total125 }126 if filled > barWidth {127 filled = barWidth128 }129 return strings.Repeat("▓", filled) + strings.Repeat("░", barWidth-filled)130}131132// percent returns the integer percentage of count out of total.133func percent(count, total int) int {134 if total == 0 {135 return 0136 }137 return count * 100 / total138}139140// total sums a tally slice.141func total(tallies []int) int {142 sum := 0143 for _, t := range tallies {144 sum += t145 }146 return sum147}148149// renderPoll writes one poll's Markdown block into b.150func renderPoll(b *strings.Builder, p *Poll) {151 sum := total(p.Tallies)152153 b.WriteString("## Poll #")154 b.WriteString(strconv.Itoa(p.ID))155 b.WriteString(": ")156 b.WriteString(p.Question)157 b.WriteString("\n\n")158159 for i, opt := range p.Options {160 c := p.Tallies[i]161 b.WriteString("- **")162 b.WriteString(opt)163 b.WriteString("** `")164 b.WriteString(bar(c, sum))165 b.WriteString("` ")166 b.WriteString(strconv.Itoa(percent(c, sum)))167 b.WriteString("% (")168 b.WriteString(strconv.Itoa(c))169 if c == 1 {170 b.WriteString(" vote)")171 } else {172 b.WriteString(" votes)")173 }174 b.WriteString("\n")175 }176177 b.WriteString("\n_Total votes: ")178 b.WriteString(strconv.Itoa(sum))179 b.WriteString("_\n\n")180}181182// Render produces the Markdown page for gnoweb. It is NOT a crossing function.183//184// - "" -> index of all polls185// - "<id>" -> a single poll by id186func Render(path string) string {187 var b strings.Builder188189 path = strings.TrimSpace(path)190 if path != "" {191 id, err := strconv.Atoi(path)192 if err != nil || id < 0 || id >= len(polls) {193 return "# Polls\n\nPoll not found.\n"194 }195 b.WriteString("# Polls\n\n")196 renderPoll(&b, polls[id])197 b.WriteString("[← all polls](/r/REPLACE_ADDR/polls)\n")198 return b.String()199 }200201 b.WriteString("# Polls & Voting\n\n")202 if len(polls) == 0 {203 b.WriteString("_No polls yet. Create one with `CreatePoll(question, \"a,b,c\")`._\n")204 return b.String()205 }206207 b.WriteString("Total polls: ")208 b.WriteString(strconv.Itoa(len(polls)))209 b.WriteString("\n\n")210 for _, p := range polls {211 renderPoll(&b, p)212 }213 return b.String()214}215CreatePoll(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}, question string, options string) int
PollCount() int
Render(path string) string
Vote(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}, pollID int, optionIndex int)
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.