PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/moul/x/daily/qvote/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
moul / x / daily / qvote
Files
3 (README)(gnomod.toml)
Exported functions
4
Module
gno.land/r/moul/x/daily/qvote/v0
gno
0.9

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • qvote.gnogno
qvote.gnogno
1// Package qvote is a quadratic-voting poll board, a Gno take on the classic2// Solidity "Ballot" voting contract with one twist borrowed from mechanism3// design instead of one-address-one-vote: every voter gets a fixed voice-4// credit budget per poll, and piling votes onto a single option costs the5// square of how many votes they stack there. Buying your 1st vote on an6// option costs 1 credit, the 2nd costs 3 more (4 total), the 3rd costs 57// more (9 total) -- so spreading conviction across options is cheap, but8// dominating one option gets expensive fast. Votes can also be pulled back9// for a matching credit refund.10package qvote1112import (13	"strconv"14	"strings"1516	"chain"17	"chain/runtime"1819	"gno.land/p/nt/avl/v0"20)2122// InitialCredits is the fixed voice-credit budget every address gets to23// spend on each poll (shared across all of that poll's options).24const InitialCredits int64 = 1002526const (27	minOptions = 228	maxOptions = 829)3031type poll struct {32	ID            string33	Creator       address34	Question      string35	Options       []string36	Tally         []int6437	CreatedHeight int6438	Closed        bool39}4041// voterState is one address's standing inside one poll: how many credits42// they've spent so far and how many votes that bought them on each option.43type voterState struct {44	CreditsUsed int6445	Votes       []int6446}4748var (49	polls  avl.Tree // poll ID -> *poll50	voters avl.Tree // "<pollID>:<addr>" -> *voterState51	nextID int52)5354func voterKey(pollID string, addr address) string {55	return pollID + ":" + addr.String()56}5758func getPoll(pollID string) *poll {59	p, ok := polls.Get(pollID).(*poll)60	if !ok {61		panic("no such poll: " + pollID)62	}63	return p64}6566func getOrCreateVoter(pollID string, addr address, numOptions int) *voterState {67	key := voterKey(pollID, addr)68	if v, ok := voters.Get(key).(*voterState); ok {69		return v70	}71	vs := &voterState{Votes: make([]int64, numOptions)}72	voters.Set(key, vs)73	return vs74}7576// square is the quadratic-voting cost curve: N votes on one option cost77// N*N credits in total.78func square(n int64) int64 {79	return n * n80}8182// CreatePoll opens a new poll with the given question and comma-separated83// options (at least 2, at most 8), returning its ID.84func CreatePoll(cur realm, question string, optionsCSV string) string {85	creator := cur.Previous().Address()8687	question = strings.TrimSpace(question)88	if question == "" {89		panic("question must not be empty")90	}9192	var options []string93	for _, raw := range strings.Split(optionsCSV, ",") {94		opt := strings.TrimSpace(raw)95		if opt == "" {96			continue97		}98		options = append(options, opt)99	}100	if len(options) < minOptions {101		panic("need at least " + strconv.Itoa(minOptions) + " non-empty options")102	}103	if len(options) > maxOptions {104		panic("at most " + strconv.Itoa(maxOptions) + " options are allowed")105	}106107	nextID++108	id := strconv.Itoa(nextID)109	p := &poll{110		ID:            id,111		Creator:       creator,112		Question:      question,113		Options:       options,114		Tally:         make([]int64, len(options)),115		CreatedHeight: runtime.ChainHeight(),116	}117	polls.Set(id, p)118119	chain.Emit("PollCreated", "id", id, "creator", creator.String(), "question", question)120121	return "poll #" + id + " created with " + strconv.Itoa(len(options)) + " options"122}123124// Vote adjusts the caller's votes on one option of a poll by delta (positive125// to buy more, negative to sell some back for a credit refund). The credit126// cost of holding N votes on an option is N*N, taken from the caller's fixed127// per-poll budget of InitialCredits.128func Vote(cur realm, pollID string, optionIdx int, delta int64) string {129	caller := cur.Previous().Address()130131	p := getPoll(pollID)132	if p.Closed {133		panic("poll #" + pollID + " is closed")134	}135	if optionIdx < 0 || optionIdx >= len(p.Options) {136		panic("invalid option index")137	}138	if delta == 0 {139		panic("delta must be non-zero")140	}141142	vs := getOrCreateVoter(pollID, caller, len(p.Options))143	current := vs.Votes[optionIdx]144	updated := current + delta145	if updated < 0 {146		panic("cannot remove more votes than you hold on this option")147	}148149	cost := square(updated) - square(current)150	newCreditsUsed := vs.CreditsUsed + cost151	if newCreditsUsed > InitialCredits {152		panic("exceeds your voice-credit budget of " + strconv.FormatInt(InitialCredits, 10) +153			" for this poll (would need " + strconv.FormatInt(newCreditsUsed, 10) + ")")154	}155156	vs.Votes[optionIdx] = updated157	vs.CreditsUsed = newCreditsUsed158	p.Tally[optionIdx] += delta159160	chain.Emit("VoteCast",161		"pollID", pollID,162		"voter", caller.String(),163		"option", p.Options[optionIdx],164		"votes", strconv.FormatInt(updated, 10),165	)166167	verb := "bought"168	n := delta169	if delta < 0 {170		verb = "sold back"171		n = -delta172	}173	return "you " + verb + " " + strconv.FormatInt(n, 10) + " vote(s) on \"" + p.Options[optionIdx] +174		"\" -- now holding " + strconv.FormatInt(updated, 10) + " (credits used: " +175		strconv.FormatInt(vs.CreditsUsed, 10) + "/" + strconv.FormatInt(InitialCredits, 10) + ")"176}177178// ClosePoll ends voting on a poll. Only its creator may close it.179func ClosePoll(cur realm, pollID string) string {180	caller := cur.Previous().Address()181182	p := getPoll(pollID)183	if caller != p.Creator {184		panic("only the poll creator can close it")185	}186	if p.Closed {187		panic("poll #" + pollID + " is already closed")188	}189	p.Closed = true190191	chain.Emit("PollClosed", "id", pollID)192193	return "poll #" + pollID + " closed"194}195196// escapeInline neutralizes markdown-active characters in untrusted text197// before it's embedded inline in Render output.198func escapeInline(s string) string {199	r := strings.NewReplacer(200		"\\", "\\\\",201		"`", "\\`",202		"*", "\\*",203		"_", "\\_",204		"[", "\\[",205		"]", "\\]",206		"|", "\\|",207	)208	return r.Replace(s)209}210211func leadingOption(p *poll) (string, int64) {212	best := -1213	var bestVotes int64 = -1214	for i, v := range p.Tally {215		if v > bestVotes {216			bestVotes = v217			best = i218		}219	}220	if best < 0 {221		return "", 0222	}223	return p.Options[best], bestVotes224}225226func renderHome() string {227	var b strings.Builder228	b.WriteString("# Quadratic Voting\n\n")229	b.WriteString("Create a poll, then spend voice credits on the options you care about -- " +230		"the Nth vote you stack on one option costs N^2 credits out of a fixed budget of " +231		strconv.FormatInt(InitialCredits, 10) + " per poll, so spreading support across " +232		"options is cheap but dominating one gets steep fast.\n\n")233234	b.WriteString("## Polls\n\n")235	count := 0236	polls.Iterate("", "", func(key string, value interface{}) bool {237		count++238		p := value.(*poll)239		status := "open"240		if p.Closed {241			status = "closed"242		}243		lead, votes := leadingOption(p)244		line := "- #" + p.ID + " (" + status + "): " + escapeInline(p.Question)245		if lead != "" {246			line += " -- leading: \"" + escapeInline(lead) + "\" (" + strconv.FormatInt(votes, 10) + ")"247		}248		b.WriteString(line + "\n")249		return false250	})251	if count == 0 {252		b.WriteString("_no polls yet -- call `CreatePoll(question, \"opt1,opt2,...\")` to start one._\n")253	}254255	b.WriteString("\n## How to use\n\n")256	b.WriteString("1. `CreatePoll(\"Best pizza topping?\", \"pineapple,mushroom,pepperoni\")` -- returns a poll ID.\n")257	b.WriteString("2. `Vote(pollID, optionIdx, delta)` -- e.g. `Vote(\"1\", 0, 3)` buys 3 votes on option 0 " +258		"(costs 9 credits); a negative delta sells votes back for a refund.\n")259	b.WriteString("3. `ClosePoll(pollID)` -- the creator ends voting.\n\n")260	b.WriteString("View a poll at this realm's path plus its ID (e.g. `.../qvote:1`), " +261		"or one voter's standing in it with `.../qvote:1/g1youraddress...`.\n")262263	return b.String()264}265266func renderPoll(id string) string {267	p, ok := polls.Get(id).(*poll)268	if !ok {269		return "# Poll #" + escapeInline(id) + "\n\nNo such poll.\n"270	}271272	var b strings.Builder273	b.WriteString("# Poll #" + p.ID + ": " + escapeInline(p.Question) + "\n\n")274	status := "open"275	if p.Closed {276		status = "closed"277	}278	b.WriteString("- Status: " + status + "\n")279	b.WriteString("- Creator: `" + p.Creator.String() + "`\n\n")280281	b.WriteString("## Results\n\n")282	var total int64283	for _, v := range p.Tally {284		total += v285	}286	for i, opt := range p.Options {287		b.WriteString(strconv.Itoa(i) + ". " + escapeInline(opt) + " -- " +288			strconv.FormatInt(p.Tally[i], 10) + " vote(s)\n")289	}290	b.WriteString("\nTotal votes cast: " + strconv.FormatInt(total, 10) + "\n")291292	return b.String()293}294295func renderVoter(pollID, rawAddr string) string {296	safeAddr := escapeInline(strings.TrimSpace(rawAddr))297	p, ok := polls.Get(pollID).(*poll)298	if !ok {299		return "# Poll #" + escapeInline(pollID) + "\n\nNo such poll.\n"300	}301302	var b strings.Builder303	b.WriteString("# " + safeAddr + " in poll #" + p.ID + "\n\n")304305	state, ok := voters.Get(voterKey(pollID, address(strings.TrimSpace(rawAddr)))).(*voterState)306	if !ok {307		b.WriteString("No votes cast yet.\n")308		return b.String()309	}310311	b.WriteString("- Credits used: " + strconv.FormatInt(state.CreditsUsed, 10) + "/" +312		strconv.FormatInt(InitialCredits, 10) + "\n\n")313	for i, opt := range p.Options {314		if state.Votes[i] == 0 {315			continue316		}317		b.WriteString("- " + escapeInline(opt) + ": " + strconv.FormatInt(state.Votes[i], 10) + " vote(s)\n")318	}319320	return b.String()321}322323// Render shows the poll board at "", one poll's results at "<pollID>", or324// one voter's standing in a poll at "<pollID>/<addr>".325func Render(path string) string {326	path = strings.TrimPrefix(strings.TrimSpace(path), "/")327	if path == "" {328		return renderHome()329	}330	if idx := strings.IndexByte(path, '/'); idx >= 0 {331		return renderVoter(path[:idx], path[idx+1:])332	}333	return renderPoll(path)334}335

Functions

  • ClosePoll(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 string) string

  • CreatePoll(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, optionsCSV string) string

  • 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 string, optionIdx int, delta int64) string

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

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.