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/g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c/friends_surf_club

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
friends_surf_club
Namespace
g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c
Files
2 (gnomod.toml)
Exported functions
19
Module
gno.land/r/g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c/friends_surf_club
gno
0.9

Files (2)

  • gnomod.tomltoml
  • friends_surf_club.gnogno
friends_surf_club.gnogno
1package friends_surf_club23import (4	"chain"5	"strconv"6	"strings"7	"time"8	"unicode"9	"unicode/utf8"1011	"gno.land/p/nt/avl/v0"12)1314// DAO realm generated by Memba, template memba-dao/2.15//16// Every change to membership, roles or the archived flag is a proposal that17// members vote on. No member and no role has special powers. This realm holds18// no funds.1920const (21	TemplateVersion  = "memba-dao/2"22	APIVersion       = "2.0"23	maxMembers       = 10024	maxPower         = 1_000_000_000 // total <= 1e11, so x100 stays far below MaxInt6425	maxTitleLen      = 12826	maxDescLen       = 800027	maxOpenPerMember = 1028	maxPageSize      = 5029	renderPageSize   = 203031	kindText         = "text"32	kindAddMember    = "add_member"33	kindRemoveMember = "remove_member"34	kindSetRoles     = "set_roles"35	kindArchive      = "archive"3637	statusActive      = "ACTIVE"38	statusAccepted    = "ACCEPTED"39	statusRejected    = "REJECTED"40	statusExecuted    = "EXECUTED"41	statusExpired     = "EXPIRED"42	statusLapsed      = "LAPSED"43	statusInvalidated = "INVALIDATED"44	statusArchived    = "ARCHIVED"45)4647type Member struct {48	Address address49	Power   int50	Roles   []string51}5253// Action is stored by value inside its proposal at propose time. It holds54// primitives only and is never modified afterwards. The handler for an action55// is chosen by Kind in Execute; there is no other dispatch.56type Action struct {57	Kind   string58	Target address59	Power  int60	Roles  []string61}6263type VoteRecord struct {64	Choice string65	Power  int66}6768type Proposal struct {69	ID                uint6470	Title             string71	Category          string72	Author            address73	Action            Action74	Status            string    // stored: ACTIVE, ACCEPTED, REJECTED, EXECUTED75	Votes             *avl.Tree // voter address -> VoteRecord76	Yes               int77	No                int78	Abstain           int79	ElectorateVersion int80	ElectoratePower   int81	CreatedAt         int64 // unix seconds (block time)82	VotingEndsAt      int6483	AcceptedAt        int64  // 0 until accepted84	FixedJSON         string // JSON of the fields that never change, built once at propose time85}8687var (88	name            = "Friends Surf Club"89	description     = "Let's surf & ride with friends.\n#SurfTrip #Waves\n\nThis DAO is the made for testing purpose of Memba OS implementation, in order to experiment cross-features interactions, UX architecture, and UI implementations in a fun context.\n"90	nameJSON        = "\"Friends Surf Club\"" // JSON-encoded once by the generator91	descriptionJSON = "\"Let's surf & ride with friends.\\n#SurfTrip #Waves\\n\\nThis DAO is the made for testing purpose of Memba OS implementation, in order to experiment cross-features interactions, UX architecture, and UI implementations in a fun context.\\n\""92	threshold       = 51 // % of all voting power that must vote YES93	quorum          = 0 // % of all voting power that must vote (0 = none)94	votingPeriod    = int64(259200) // seconds95	executionDelay  = int64(3600) // seconds after acceptance96	executionWindow = int64(604800) // seconds after the delay9798	allowedCategories = []string{"governance", "membership", "operations"}99	allowedRoles      = []string{"admin", "member"}100101	members           = avl.NewTree() // address -> *Member102	memberTotalPower  = 0103	proposals         = avl.NewTree() // zero-padded id -> *Proposal104	descriptions      = avl.NewTree() // zero-padded id -> string (kept apart so list reads stay cheap)105	openByAuthor      = avl.NewTree() // address -> []uint64 (ids that may still be open)106	lastID            = uint64(0)107	electorateVersion = 0 // bumped when a member is added or removed108	archived          = false109)110111func init() {112	addGenesisMember("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", 2, []string{"admin"})113	addGenesisMember("g12yg9nh4ncma44emgm8msxe8aavzywt0p95tanv", 1, []string{"member"})114	addGenesisMember("g1cvr48r7l7lkmvp77cr6zg2zhu26jgfwr0y8pew", 1, []string{"member"})115	addGenesisMember("g1rayfgklwl0aspz488wvrcrvt7t2quy6q06lgk2", 1, []string{"member"})116	addGenesisMember("g1nvxz94ukmggfx4mlldwae3cpde99qmldtcgm9u", 1, []string{"member"})117	addGenesisMember("g1jjeuv48j3pmsmnvga8jwfnc2amfyv9c30zx04y", 1, []string{"member"})118	checkGenesis()119}120121// ── Genesis ───────────────────────────────────────────────122123func addGenesisMember(addr string, power int, roles []string) {124	a := address(addr)125	if !a.IsValid() {126		panic("genesis: invalid member address")127	}128	if !isLowerCase(a) {129		panic("genesis: member address must be lower case")130	}131	if members.Has(addr) {132		panic("genesis: duplicate member")133	}134	if power < 1 || power > maxPower {135		panic("genesis: member power out of range")136	}137	checkRoles(roles)138	members.Set(addr, &Member{Address: a, Power: power, Roles: roles})139	memberTotalPower += power140}141142func checkGenesis() {143	if members.Size() < 1 || members.Size() > maxMembers {144		panic("genesis: member count out of range")145	}146	if threshold <= 50 || threshold > 100 {147		panic("genesis: threshold must be above 50 and at most 100")148	}149	if quorum < 0 || quorum > 100 {150		panic("genesis: quorum out of range")151	}152	if votingPeriod < 3600 || votingPeriod > 30*86400 {153		panic("genesis: voting period out of range")154	}155	// At least an hour between acceptance and execution, so members see a156	// decision before it applies, even when one member can pass it alone.157	if executionDelay < 3600 || executionDelay > 7*86400 {158		panic("genesis: execution delay out of range")159	}160	if executionWindow < 86400 || executionWindow > 30*86400 {161		panic("genesis: execution window out of range")162	}163	if len(allowedCategories) < 1 {164		panic("genesis: at least one category is required")165	}166}167168// ── Validation (no writes) ────────────────────────────────169170func now() int64 { return time.Now().Unix() }171172func assertNotArchived() {173	if archived {174		panic("DAO is archived")175	}176}177178func getMember(addr address) *Member {179	m, ok := members.Get(string(addr)).(*Member)180	if !ok {181		return nil182	}183	return m184}185186func assertMember(addr address) *Member {187	m := getMember(addr)188	if m == nil || m.Power < 1 {189		panic("caller is not a member")190	}191	return m192}193194// isLowerCase refuses the upper-case bech32 spelling, which IsValid accepts:195// it names the same account under a different member key. Only call it on an196// address that passed IsValid: bech32 refuses mixed case, so the first197// character decides (a byte loop would cost gas per character per member).198func isLowerCase(a address) bool {199	s := string(a)200	return len(s) > 0 && s[0] >= 'a' && s[0] <= 'z'201}202203func contains(list []string, s string) bool {204	for _, v := range list {205		if v == s {206			return true207		}208	}209	return false210}211212func checkRoles(roles []string) {213	for i, r := range roles {214		if !contains(allowedRoles, r) {215			panic("invalid role")216		}217		for j := 0; j < i; j++ {218			if roles[j] == r {219				panic("duplicate role")220			}221		}222	}223}224225func parseRoles(csv string) []string {226	if csv == "" {227		return []string{}228	}229	roles := strings.Split(csv, ",")230	if len(roles) > len(allowedRoles) {231		panic("too many roles")232	}233	checkRoles(roles)234	return roles235}236237func isControl(r rune) bool {238	return r < 0x20 || (r >= 0x7f && r <= 0x9f) || r == 0x2028 || r == 0x2029239}240241func checkTitle(title string) {242	if !utf8.ValidString(title) {243		panic("title must be valid UTF-8")244	}245	n := utf8.RuneCountInString(title)246	if n < 1 || n > maxTitleLen {247		panic("title must be 1 to 128 characters")248	}249	for _, r := range title {250		if isControl(r) {251			panic("title must be a single line without control characters")252		}253		// Format characters (zero-width, bidi controls, byte-order mark) are254		// invisible and can reorder how the rest of the line is displayed.255		if unicode.Is(unicode.Cf, r) {256			panic("title must not contain invisible formatting characters")257		}258	}259	if strings.TrimSpace(title) == "" {260		panic("title must not be blank")261	}262}263264func checkDescription(desc string) {265	if !utf8.ValidString(desc) {266		panic("description must be valid UTF-8")267	}268	if utf8.RuneCountInString(desc) > maxDescLen {269		panic("description must be at most 8000 characters")270	}271	for _, r := range desc {272		if isControl(r) && r != '\n' && r != '\t' {273			panic("description contains control characters")274		}275	}276}277278// validateAction checks an action against the current state. Propose calls it279// before storing the proposal and Execute calls it again before applying.280func validateAction(a Action) {281	switch a.Kind {282	case kindText, kindArchive:283		if a.Target != "" || a.Power != 0 || len(a.Roles) != 0 {284			panic("this action takes no target, power or roles")285		}286	case kindAddMember:287		if !a.Target.IsValid() {288			panic("invalid member address")289		}290		if !isLowerCase(a.Target) {291			panic("member address must be lower case")292		}293		if getMember(a.Target) != nil {294			panic("address is already a member")295		}296		if a.Power < 1 || a.Power > maxPower {297			panic("member power must be between 1 and 1000000000")298		}299		if members.Size()+1 > maxMembers {300			panic("member limit reached")301		}302		checkRoles(a.Roles)303	case kindRemoveMember:304		m := getMember(a.Target)305		if m == nil {306			panic("target is not a member")307		}308		if members.Size()-1 < 1 || memberTotalPower-m.Power < 1 {309			panic("cannot remove the last member")310		}311	case kindSetRoles:312		if getMember(a.Target) == nil {313			panic("target is not a member")314		}315		checkRoles(a.Roles)316	default:317		panic("unknown action kind")318	}319}320321func padID(id uint64) string {322	s := strconv.FormatUint(id, 10)323	for len(s) < 20 {324		s = "0" + s325	}326	return s327}328329func getProposal(id uint64) *Proposal {330	p, ok := proposals.Get(padID(id)).(*Proposal)331	if !ok {332		return nil333	}334	return p335}336337func proposalDescription(id uint64) string {338	desc, _ := descriptions.Get(padID(id)).(string)339	return desc340}341342func mustProposal(id uint64) *Proposal {343	p := getProposal(id)344	if p == nil {345		panic("proposal not found")346	}347	return p348}349350// derivedStatus is the status every read and write relies on. Time-based and351// membership-based transitions are computed here, never written.352func derivedStatus(p *Proposal) string {353	// Once archived, nothing can be voted or executed any more.354	if archived && (p.Status == statusActive || p.Status == statusAccepted) {355		return statusArchived356	}357	t := now()358	if p.Status == statusActive {359		if p.ElectorateVersion != electorateVersion {360			return statusInvalidated361		}362		if t >= p.VotingEndsAt {363			return statusExpired364		}365	}366	if p.Status == statusAccepted && t > p.AcceptedAt+executionDelay+executionWindow {367		return statusLapsed368	}369	return p.Status370}371372// openIDs returns the author's proposals that are still open for voting. The373// stored list never grows past maxOpenPerMember, so this stays bounded.374func openIDs(author address) []uint64 {375	ids, _ := openByAuthor.Get(string(author)).([]uint64)376	open := []uint64{}377	for _, id := range ids {378		p := getProposal(id)379		if p != nil && derivedStatus(p) == statusActive {380			open = append(open, id)381		}382	}383	return open384}385386// ── Propose ───────────────────────────────────────────────387388func propose(caller address, title, desc, category string, a Action) uint64 {389	assertNotArchived()390	assertMember(caller)391	checkTitle(title)392	checkDescription(desc)393	validateAction(a)394	open := openIDs(caller)395	if len(open) >= maxOpenPerMember {396		panic("too many open proposals for this member")397	}398	// Every check passed; writes start here.399	lastID++400	id := lastID401	t := now()402	p := &Proposal{403		ID:                id,404		Title:             title,405		Category:          category,406		Author:            caller,407		Action:            a,408		Status:            statusActive,409		Votes:             avl.NewTree(),410		ElectorateVersion: electorateVersion,411		ElectoratePower:   memberTotalPower,412		CreatedAt:         t,413		VotingEndsAt:      t + votingPeriod,414	}415	p.FixedJSON = fixedJSON(p)416	proposals.Set(padID(id), p)417	descriptions.Set(padID(id), desc)418	openByAuthor.Set(string(caller), append(open, id))419	chain.Emit("ProposalCreated", "id", strconv.FormatUint(id, 10), "kind", a.Kind)420	return id421}422423// ProposeText creates a proposal with no on-chain effect.424func ProposeText(cur realm, title, desc, category string) uint64 {425	caller := cur.Previous().Address()426	if !contains(allowedCategories, category) {427		panic("invalid proposal category")428	}429	return propose(caller, title, desc, category, Action{Kind: kindText})430}431432// ProposeAddMember proposes adding target with power and comma-separated roles.433func ProposeAddMember(cur realm, title, desc string, target address, power int, rolesCSV string) uint64 {434	caller := cur.Previous().Address()435	roles := parseRoles(rolesCSV)436	return propose(caller, title, desc, "membership", Action{Kind: kindAddMember, Target: target, Power: power, Roles: roles})437}438439// ProposeRemoveMember proposes removing target.440func ProposeRemoveMember(cur realm, title, desc string, target address) uint64 {441	caller := cur.Previous().Address()442	return propose(caller, title, desc, "membership", Action{Kind: kindRemoveMember, Target: target})443}444445// ProposeSetRoles proposes replacing target's role labels.446func ProposeSetRoles(cur realm, title, desc string, target address, rolesCSV string) uint64 {447	caller := cur.Previous().Address()448	roles := parseRoles(rolesCSV)449	return propose(caller, title, desc, "membership", Action{Kind: kindSetRoles, Target: target, Roles: roles})450}451452// ProposeArchive proposes archiving the DAO. Archiving is permanent.453func ProposeArchive(cur realm, title, desc string) uint64 {454	caller := cur.Previous().Address()455	return propose(caller, title, desc, "governance", Action{Kind: kindArchive})456}457458// ── Vote ──────────────────────────────────────────────────459460// reached reports part/whole >= pct/100 without division. part and whole are461// at most maxMembers*maxPower (1e11), so part*100 cannot overflow.462func reached(part, whole, pct int) bool {463	return part*100 >= whole*pct464}465466// Vote records the caller's YES, NO or ABSTAIN. A proposal is ACCEPTED once467// YES alone reaches the threshold of all voting power (and quorum is met), and468// REJECTED once that has become impossible.469func Vote(cur realm, id uint64, choice string) {470	caller := cur.Previous().Address()471	if choice != "YES" && choice != "NO" && choice != "ABSTAIN" {472		panic("invalid vote: must be YES, NO or ABSTAIN")473	}474	assertNotArchived()475	m := assertMember(caller)476	p := mustProposal(id)477	if p.Status != statusActive {478		panic("proposal is not open for voting")479	}480	if p.ElectorateVersion != electorateVersion {481		panic("proposal invalidated by a membership change")482	}483	if now() >= p.VotingEndsAt {484		panic("voting period has ended")485	}486	if p.Votes.Has(string(caller)) {487		panic("already voted")488	}489	// Every check passed; writes start here.490	p.Votes.Set(string(caller), VoteRecord{Choice: choice, Power: m.Power})491	switch choice {492	case "YES":493		p.Yes += m.Power494	case "NO":495		p.No += m.Power496	default:497		p.Abstain += m.Power498	}499	cast := p.Yes + p.No + p.Abstain500	ep := p.ElectoratePower501	if (quorum == 0 || reached(cast, ep, quorum)) && reached(p.Yes, ep, threshold) {502		p.Status = statusAccepted503		p.AcceptedAt = now()504	} else if !reached(p.Yes+(ep-cast), ep, threshold) {505		p.Status = statusRejected506	}507	chain.Emit("VoteCast", "id", strconv.FormatUint(id, 10), "voter", string(caller), "choice", choice)508}509510// ── Execute ───────────────────────────────────────────────511512// Execute applies an accepted proposal after the execution delay and before513// the execution window closes. The stored action is checked again against the514// current state first; the proposal is marked EXECUTED only after it applied.515func Execute(cur realm, id uint64) {516	caller := cur.Previous().Address()517	assertNotArchived()518	assertMember(caller)519	p := mustProposal(id)520	if derivedStatus(p) != statusAccepted {521		panic("proposal is not accepted, or its execution window has passed")522	}523	t := now()524	if t < p.AcceptedAt+executionDelay {525		panic("execution delay has not elapsed")526	}527	if t > p.AcceptedAt+executionDelay+executionWindow {528		panic("execution window has passed")529	}530	a := p.Action531	validateAction(a)532	// Every check passed; writes start here and cannot fail.533	switch a.Kind {534	case kindText:535		// no on-chain effect536	case kindAddMember:537		roles := make([]string, len(a.Roles))538		copy(roles, a.Roles)539		members.Set(string(a.Target), &Member{Address: a.Target, Power: a.Power, Roles: roles})540		memberTotalPower += a.Power541		electorateVersion++542	case kindRemoveMember:543		// Removing a member frees storage; by chain rules the storage deposit it544		// freed is refunded to whoever executes the removal, not to the payer.545		m := getMember(a.Target)546		members.Remove(string(a.Target))547		memberTotalPower -= m.Power548		electorateVersion++549	case kindSetRoles:550		roles := make([]string, len(a.Roles))551		copy(roles, a.Roles)552		getMember(a.Target).Roles = roles553	case kindArchive:554		archived = true555	}556	p.Status = statusExecuted557	idStr := strconv.FormatUint(id, 10)558	chain.Emit("ProposalExecuted", "id", idStr, "kind", a.Kind)559	if a.Kind == kindArchive {560		chain.Emit("DAOArchived", "id", idStr)561	}562}563564// ── Reads (non-crossing, bounded, no calls to other realms) ──565566func GetTemplateVersion() string { return TemplateVersion }567568func GetAPIVersion() string { return APIVersion }569570// IsMember lets a companion realm check membership.571func IsMember(addr address) bool {572	m := getMember(addr)573	return m != nil && m.Power >= 1574}575576func IsArchived() bool { return archived }577578func HasVoted(id uint64, addr address) bool {579	p := getProposal(id)580	return p != nil && p.Votes.Has(string(addr))581}582583type jsonBuf struct{ b strings.Builder }584585func (j *jsonBuf) raw(s string) { j.b.WriteString(s) }586587func (j *jsonBuf) str(s string) {588	j.b.WriteByte('"')589	for _, r := range s {590		switch {591		case r == '"':592			j.b.WriteString("\\\"")593		case r == '\\':594			j.b.WriteString("\\\\")595		case r == '\n':596			j.b.WriteString("\\n")597		case r == '\t':598			j.b.WriteString("\\t")599		case r == '\r':600			j.b.WriteString("\\r")601		case r < 0x20 || r == 0x2028 || r == 0x2029:602			h := strconv.FormatInt(int64(r), 16)603			for len(h) < 4 {604				h = "0" + h605			}606			j.b.WriteString("\\u" + h)607		default:608			j.b.WriteRune(r)609		}610	}611	j.b.WriteByte('"')612}613614func (j *jsonBuf) key(k string, first bool) {615	if !first {616		j.b.WriteByte(',')617	}618	j.b.WriteByte('"')619	j.b.WriteString(k)620	j.b.WriteString("\":")621}622623func (j *jsonBuf) num(v int)     { j.b.WriteString(strconv.Itoa(v)) }624func (j *jsonBuf) num64(v int64) { j.b.WriteString(strconv.FormatInt(v, 10)) }625func (j *jsonBuf) id(v uint64)   { j.b.WriteString(strconv.FormatUint(v, 10)) }626627func (j *jsonBuf) flag(v bool) {628	if v {629		j.b.WriteString("true")630	} else {631		j.b.WriteString("false")632	}633}634635// plain quotes a value that cannot contain characters needing escapes:636// addresses (validated bech32), role and category identifiers, and constants.637// Escaping rune by rune is costly in the VM, so it is done once at write time638// for user text and skipped here.639func (j *jsonBuf) plain(s string) {640	j.b.WriteByte('"')641	j.b.WriteString(s)642	j.b.WriteByte('"')643}644645func (j *jsonBuf) plains(list []string) {646	j.b.WriteByte('[')647	for i, s := range list {648		if i > 0 {649			j.b.WriteByte(',')650		}651		j.plain(s)652	}653	j.b.WriteByte(']')654}655656func checkPage(offset, limit int) {657	if offset < 0 {658		panic("offset must not be negative")659	}660	if limit < 1 || limit > maxPageSize {661		panic("limit must be between 1 and 50")662	}663}664665func writeConfig(j *jsonBuf) {666	j.raw("{")667	j.key("template_version", true)668	j.plain(TemplateVersion)669	j.key("api_version", false)670	j.plain(APIVersion)671	j.key("name", false)672	j.raw(nameJSON)673	j.key("description", false)674	j.raw(descriptionJSON)675	j.key("threshold", false)676	j.num(threshold)677	j.key("quorum", false)678	j.num(quorum)679	j.key("voting_period", false)680	j.num64(votingPeriod)681	j.key("execution_delay", false)682	j.num64(executionDelay)683	j.key("execution_window", false)684	j.num64(executionWindow)685	j.key("categories", false)686	j.plains(allowedCategories)687	j.key("roles", false)688	j.plains(allowedRoles)689	j.key("archived", false)690	j.flag(archived)691	j.key("member_count", false)692	j.num(members.Size())693	j.key("total_power", false)694	j.num(memberTotalPower)695	j.key("electorate_version", false)696	j.num(electorateVersion)697	j.key("proposal_count", false)698	j.id(lastID)699	j.raw("}")700}701702// GetConfigJSON returns the DAO's settings and current totals.703func GetConfigJSON() string {704	j := &jsonBuf{}705	writeConfig(j)706	return j.b.String()707}708709func writeMembers(j *jsonBuf, offset, limit int) {710	checkPage(offset, limit)711	j.raw("{")712	j.key("total", true)713	j.num(members.Size())714	j.key("offset", false)715	j.num(offset)716	j.key("members", false)717	j.raw("[")718	first := true719	members.IterateByOffset(offset, limit, func(_ string, v any) bool {720		m := v.(*Member)721		if !first {722			j.raw(",")723		}724		first = false725		j.raw("{")726		j.key("address", true)727		j.plain(string(m.Address))728		j.key("power", false)729		j.num(m.Power)730		j.key("roles", false)731		j.plains(m.Roles)732		j.raw("}")733		return false734	})735	j.raw("]}")736}737738// GetMembersJSON returns one page of members (limit 1..50).739func GetMembersJSON(offset, limit int) string {740	j := &jsonBuf{}741	writeMembers(j, offset, limit)742	return j.b.String()743}744745// fixedJSON encodes the proposal fields that never change. The action's746// target is a validated address (or empty) and its roles are identifiers.747func fixedJSON(p *Proposal) string {748	j := &jsonBuf{}749	j.key("id", true)750	j.id(p.ID)751	j.key("title", false)752	j.str(p.Title)753	j.key("category", false)754	j.plain(p.Category)755	j.key("author", false)756	j.plain(string(p.Author))757	j.key("action", false)758	j.raw("{")759	j.key("kind", true)760	j.plain(p.Action.Kind)761	j.key("target", false)762	j.plain(string(p.Action.Target))763	j.key("power", false)764	j.num(p.Action.Power)765	j.key("roles", false)766	j.plains(p.Action.Roles)767	j.raw("}")768	j.key("electorate_power", false)769	j.num(p.ElectoratePower)770	j.key("electorate_version", false)771	j.num(p.ElectorateVersion)772	j.key("created_at", false)773	j.num64(p.CreatedAt)774	j.key("voting_ends_at", false)775	j.num64(p.VotingEndsAt)776	return j.b.String()777}778779func writeProposalFields(j *jsonBuf, p *Proposal) {780	j.raw(p.FixedJSON)781	j.key("status", false)782	j.plain(derivedStatus(p))783	j.key("yes", false)784	j.num(p.Yes)785	j.key("no", false)786	j.num(p.No)787	j.key("abstain", false)788	j.num(p.Abstain)789	j.key("accepted_at", false)790	j.num64(p.AcceptedAt)791	executableAt := int64(0)792	executeBy := int64(0)793	if p.AcceptedAt > 0 {794		executableAt = p.AcceptedAt + executionDelay795		executeBy = executableAt + executionWindow796	}797	j.key("executable_at", false)798	j.num64(executableAt)799	j.key("execute_by", false)800	j.num64(executeBy)801}802803// GetProposalsJSON lists proposals newest first (limit 1..50). before=0 starts804// at the newest; pass the returned next_before to get the following page805// (next_before 0 means there is none). Descriptions are only returned by806// GetProposalJSON, which keeps a full page small.807func GetProposalsJSON(before uint64, limit int) string {808	checkPage(0, limit)809	j := &jsonBuf{}810	j.raw("{")811	j.key("proposals", true)812	j.raw("[")813	end := ""814	if before > 0 {815		end = padID(before - 1)816	}817	count := 0818	nextBefore := uint64(0)819	proposals.ReverseIterate("", end, func(_ string, v any) bool {820		p := v.(*Proposal)821		if count == limit {822			nextBefore = p.ID + 1823			return true824		}825		if count > 0 {826			j.raw(",")827		}828		j.raw("{")829		writeProposalFields(j, p)830		j.raw("}")831		count++832		return false833	})834	j.raw("]")835	j.key("next_before", false)836	j.id(nextBefore)837	j.raw("}")838	return j.b.String()839}840841// GetProposalJSON returns one proposal including its description.842func GetProposalJSON(id uint64) string {843	p := mustProposal(id)844	j := &jsonBuf{}845	j.raw("{")846	writeProposalFields(j, p)847	j.key("description", false)848	j.str(proposalDescription(p.ID))849	j.raw("}")850	return j.b.String()851}852853// GetVotesJSON returns one page of a proposal's votes (limit 1..50).854func GetVotesJSON(id uint64, offset, limit int) string {855	checkPage(offset, limit)856	p := mustProposal(id)857	j := &jsonBuf{}858	j.raw("{")859	j.key("total", true)860	j.num(p.Votes.Size())861	j.key("offset", false)862	j.num(offset)863	j.key("votes", false)864	j.raw("[")865	first := true866	p.Votes.IterateByOffset(offset, limit, func(k string, v any) bool {867		vote := v.(VoteRecord)868		if !first {869			j.raw(",")870		}871		first = false872		j.raw("{")873		j.key("voter", true)874		j.plain(k)875		j.key("choice", false)876		j.plain(vote.Choice)877		j.key("power", false)878		j.num(vote.Power)879		j.raw("}")880		return false881	})882	j.raw("]}")883	return j.b.String()884}885886// GetStateJSON exports the settings and one page of members, so the DAO can887// be recreated elsewhere.888func GetStateJSON(offset, limit int) string {889	j := &jsonBuf{}890	j.raw("{")891	j.key("config", true)892	writeConfig(j)893	j.key("members", false)894	writeMembers(j, offset, limit)895	j.raw("}")896	return j.b.String()897}898899// ── Render (gnoweb) ───────────────────────────────────────900901// escapeInline makes a user string safe on one markdown line: markdown902// punctuation is backslash-escaped and control characters become spaces.903func escapeInline(s string) string {904	var b strings.Builder905	for _, r := range s {906		switch r {907		case 0x5c, '*', '_', '[', ']', '(', ')', '#', '<', '>', '|', '!', 0x60:908			b.WriteByte(0x5c)909			b.WriteRune(r)910		default:911			if isControl(r) {912				b.WriteByte(' ')913			} else {914				b.WriteRune(r)915			}916		}917	}918	return b.String()919}920921// quoteBlock renders multi-line text as a quote, one escaped line at a time.922func quoteBlock(s string) string {923	var b strings.Builder924	for _, line := range strings.Split(s, "\n") {925		b.WriteString("> ")926		b.WriteString(escapeInline(line))927		b.WriteString("\n")928	}929	return b.String()930}931932func Render(path string) string {933	if path == "" {934		return renderHome(1)935	}936	if strings.HasPrefix(path, "?page=") {937		page, err := strconv.Atoi(strings.TrimPrefix(path, "?page="))938		if err == nil && page >= 1 {939			return renderHome(page)940		}941		return "# Not found\n"942	}943	id, err := strconv.ParseUint(path, 10, 64)944	if err == nil {945		if p := getProposal(id); p != nil {946			return renderProposal(p)947		}948	}949	return "# Not found\n"950}951952func renderHome(page int) string {953	var b strings.Builder954	b.WriteString("# " + escapeInline(name) + "\n\n")955	if description != "" {956		b.WriteString(quoteBlock(description) + "\n")957	}958	b.WriteString("Threshold: " + strconv.Itoa(threshold) + "% of all voting power. Quorum: " + strconv.Itoa(quorum) + "%.\n\n")959	if archived {960		b.WriteString("This DAO is archived.\n\n")961	}962	b.WriteString("## Members (" + strconv.Itoa(members.Size()) + ")\n\n")963	members.Iterate("", "", func(_ string, v any) bool {964		m := v.(*Member)965		b.WriteString("- " + string(m.Address) + " (power " + strconv.Itoa(m.Power) + ")\n")966		return false967	})968	b.WriteString("\n## Proposals\n\n")969	total := proposals.Size()970	if total == 0 {971		b.WriteString("No proposals yet.\n")972		return b.String()973	}974	proposals.ReverseIterateByOffset((page-1)*renderPageSize, renderPageSize, func(_ string, v any) bool {975		p := v.(*Proposal)976		id := strconv.FormatUint(p.ID, 10)977		b.WriteString("- [#" + id + "](:" + id + ") " + escapeInline(p.Title) + " (" + p.Action.Kind + ", " + derivedStatus(p) + ")\n")978		return false979	})980	pages := (total + renderPageSize - 1) / renderPageSize981	if pages > 1 {982		b.WriteString("\nPage " + strconv.Itoa(page) + " of " + strconv.Itoa(pages) + ":")983		for i := 1; i <= pages; i++ {984			b.WriteString(" [" + strconv.Itoa(i) + "](?page=" + strconv.Itoa(i) + ")")985		}986		b.WriteString("\n")987	}988	return b.String()989}990991func renderProposal(p *Proposal) string {992	var b strings.Builder993	id := strconv.FormatUint(p.ID, 10)994	b.WriteString("# Proposal " + id + ": " + escapeInline(p.Title) + "\n\n")995	b.WriteString("Kind: " + p.Action.Kind + "\n\n")996	if p.Action.Target != "" {997		b.WriteString("Target: " + escapeInline(string(p.Action.Target)) + "\n\n")998	}999	b.WriteString("Status: " + derivedStatus(p) + "\n\n")1000	b.WriteString("Author: " + string(p.Author) + "\n\n")1001	b.WriteString("YES " + strconv.Itoa(p.Yes) + ", NO " + strconv.Itoa(p.No) + ", ABSTAIN " + strconv.Itoa(p.Abstain) + " of " + strconv.Itoa(p.ElectoratePower) + "\n\n")1002	if desc := proposalDescription(p.ID); desc != "" {1003		b.WriteString(quoteBlock(desc))1004	}1005	return b.String()1006}1007

Functions

  • Execute(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}, id uint64)

  • GetAPIVersion() string

  • GetConfigJSON() string

  • GetMembersJSON(offset int, limit int) string

  • GetProposalJSON(id uint64) string

  • GetProposalsJSON(before uint64, limit int) string

  • GetStateJSON(offset int, limit int) string

  • GetTemplateVersion() string

  • GetVotesJSON(id uint64, offset int, limit int) string

  • HasVoted(id uint64, addr string) bool

  • IsArchived() bool

  • IsMember(addr string) bool

  • ProposeAddMember(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}, title string, desc string, target string, power int, rolesCSV string) uint64

  • ProposeArchive(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}, title string, desc string) uint64

  • ProposeRemoveMember(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}, title string, desc string, target string) uint64

  • ProposeSetRoles(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}, title string, desc string, target string, rolesCSV string) uint64

  • ProposeText(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}, title string, desc string, category string) uint64

  • 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}, id uint64, choice 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.