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.

decisions.gno

11.15 Kb · 293 lines
  1// Package decisions is the ledger every bridge realm keeps of GovDAO's decisions (bridge/PLAN.md §5.3): a decision
  2// is born when a proposal made with one of the realm's builders executes, gets the next number, and is either
  3// applied at once or queued for 7 days of block time; anyone applies a queued one after its delay, and GovDAO can
  4// cancel it in between. The ledger keeps no closure and no proposal text: the parameters a builder wrote are the
  5// record. The realm decides what a decision does; this package keeps the numbers, the clocks and the page.
  6package decisions
  7
  8import (
  9	"errors"
 10	"strconv"
 11	"strings"
 12
 13	"gno.land/p/nt/avl/v0"
 14)
 15
 16const (
 17	// Day is a day of block time, in seconds.
 18	Day = int64(86400)
 19	// Factor divides every delay of the realms — Delay, Deadline, and the old router's 7 days after a swap, which
 20	// is Delay (§5.9): 1 for the mainnet build, which §8.3's check reads in the served source; the devnet build
 21	// is deployed with 8 here, so its 14 days pass in 42 hours, as the Solana programs' clock-factor-8 feature
 22	// does. A compiled constant beside the launch values, shown on every realm's page. The meters (§5.4) are
 23	// rates per real day and are not divided.
 24	Factor = int64(1)
 25	// Delay is the wait a raise, a swap, a credit, a migration or a sweep payment takes after its proposal
 26	// executes (§5.3: 7 days by block time), divided by Factor.
 27	Delay = 7 * Day / Factor
 28	// Deadline is how old a proposal may be when it executes; past it the closure returns an error and GovDAO
 29	// marks the proposal denied for good (§5.3: 14 days), divided by Factor.
 30	Deadline = 14 * Day / Factor
 31	// PageMax is how many decisions of one kind and state the page shows.
 32	PageMax = 100
 33	// DAOPath is the realm whose frame every builder's closure must be called from, once the governor has yielded
 34	// (or from the first block, when none was compiled): the proxy that executes GovDAO's proposals.
 35	DAOPath = "gno.land/r/gov/dao"
 36)
 37
 38// ------------------------------------------------------------- governor --
 39
 40// The governor (M1k, Jae 2026-10-14: "until I yield to GovDAO I want to be GovDAO alone"). Each bridge realm
 41// compiles a governor address beside its other launch values; while one is compiled and has not yielded, a
 42// decision is the governor's own direct call through the realm's Govern — the same closure, checks, queues and
 43// delays a proposal's execution runs, only who decides differs — and GovDAO's proposals are refused. The yield is
 44// one call on hlmailbox (Yield), which every realm reads, so no realm is ever governed apart from the others; from
 45// that block DAOPath's proxy is the only governor, as before M1k. A compiled governor of "" means GovDAO from the
 46// first block. This package keeps the rule and the page line; the realms keep the identity checks, since an
 47// identity is read from a live cur alone.
 48
 49// DAOGoverns is whether DAOPath's proxy decides now: no governor was compiled, or the governor yielded at that
 50// block height (0 until the yield).
 51func DAOGoverns(governor address, yielded int64) bool { return governor == "" || yielded != 0 }
 52
 53// GovernorLine is the line every bridge realm's page shows of who governs it.
 54func GovernorLine(governor address, yielded int64) string {
 55	switch {
 56	case governor == "":
 57		return "governor: " + DAOPath + " from the first block (no governor was compiled)"
 58	case yielded == 0:
 59		return "governor: " + string(governor) + " (until the yield) — it alone decides, by Govern from its own key, and " + DAOPath + "'s proposals are refused"
 60	}
 61	return "governor: " + DAOPath + " since height " + itoa(yielded) + " (" + string(governor) + " yielded)"
 62}
 63
 64// GovernorJSON is the same for the agents: `"governor":"…","yielded":H` (no leading or trailing comma).
 65func GovernorJSON(governor address, yielded int64) string {
 66	return "\"governor\":" + strconv.Quote(string(governor)) + ",\"yielded\":" + itoa(yielded)
 67}
 68
 69// Decision is one numbered change, as the realm's page shows it.
 70type Decision struct {
 71	ID        int64
 72	Kind      string // the builder's name, e.g. "set cap floor"
 73	Text      string // the parameters, as the builder wrote them into the proposal's description
 74	Params    any    // the parameters, typed, the realm's own
 75	Made      int64  // unix time the proposal was made (the builder call)
 76	Recorded  int64  // unix time the proposal executed: the decision's birth
 77	Due       int64  // unix time it may be applied: Recorded for an at-once change, Recorded + Delay for a queued one
 78	Applied   int64  // unix time it was applied; 0 while pending
 79	Cancelled int64  // unix time GovDAO cancelled it; 0 if it was not
 80	Spent     bool   // recorded with no effect (an unpause that would have cleared nothing)
 81	Note      string // what happened at execution, in words
 82}
 83
 84// Pending is whether the decision still waits to be applied.
 85func (d *Decision) Pending() bool { return d.Applied == 0 && d.Cancelled == 0 && !d.Spent }
 86
 87// Queued is whether the decision waits on its delay (pending, and not due at once).
 88func (d *Decision) Queued() bool { return d.Pending() && d.Due > d.Recorded }
 89
 90// State is "pending", "applied", "cancelled" or "spent".
 91func (d *Decision) State() string {
 92	switch {
 93	case d.Cancelled != 0:
 94		return "cancelled"
 95	case d.Spent:
 96		return "spent"
 97	case d.Applied != 0:
 98		return "applied"
 99	}
100	return "pending"
101}
102
103// Ledger numbers and keeps a realm's decisions. The zero value is empty.
104type Ledger struct {
105	seq int64
106	all avl.Tree // key(id) → *Decision
107}
108
109func key(id int64) string {
110	s := strconv.FormatInt(id, 10)
111	return strings.Repeat("0", 12-len(s)) + s
112}
113
114// Record gives a decision the next number and keeps it. due is when it may be applied (now for at once).
115func (l *Ledger) Record(kind, text string, params any, made, now, due int64) *Decision {
116	l.seq++
117	d := &Decision{ID: l.seq, Kind: kind, Text: text, Params: params, Made: made, Recorded: now, Due: due}
118	l.all.Set(key(d.ID), d)
119	return d
120}
121
122// Get is the decision with this number, or nil.
123func (l *Ledger) Get(id int64) *Decision {
124	v := l.all.Get(key(id))
125	if v == nil {
126		return nil
127	}
128	return v.(*Decision)
129}
130
131// Last is the number of the newest decision (0 if none).
132func (l *Ledger) Last() int64 { return l.seq }
133
134// Size is how many decisions were ever recorded.
135func (l *Ledger) Size() int { return l.all.Size() }
136
137// Apply marks a decision applied now, with a note.
138func (l *Ledger) Apply(d *Decision, now int64, note string) {
139	d.Applied = now
140	if note != "" {
141		d.Note = note
142	}
143}
144
145// Spend marks a decision spent: recorded, no effect.
146func (l *Ledger) Spend(d *Decision, note string) {
147	d.Spent = true
148	d.Note = note
149}
150
151// Cancel marks a pending decision cancelled now. It errors on a number that is not pending.
152func (l *Ledger) Cancel(id, now int64) (*Decision, error) {
153	d := l.Get(id)
154	if d == nil {
155		return nil, errors.New("no decision " + strconv.FormatInt(id, 10))
156	}
157	if !d.Pending() {
158		return nil, errors.New("decision " + strconv.FormatInt(id, 10) + " is " + d.State() + ", not pending")
159	}
160	d.Cancelled = now
161	return d, nil
162}
163
164// Due is a pending decision whose delay has passed, by number; it errors otherwise.
165func (l *Ledger) Due(id, now int64) (*Decision, error) {
166	d := l.Get(id)
167	if d == nil {
168		return nil, errors.New("no decision " + strconv.FormatInt(id, 10))
169	}
170	if !d.Pending() {
171		return nil, errors.New("decision " + strconv.FormatInt(id, 10) + " is " + d.State() + ", not pending")
172	}
173	if now < d.Due {
174		return nil, errors.New("decision " + strconv.FormatInt(id, 10) + " is due at " + strconv.FormatInt(d.Due, 10) + ", not before")
175	}
176	return d, nil
177}
178
179// Each walks every decision, oldest first, until fn returns true.
180func (l *Ledger) Each(fn func(d *Decision) bool) {
181	l.all.Iterate("", "", func(_ string, v any) bool { return fn(v.(*Decision)) })
182}
183
184// Expired is the one error a builder's closure returns: the proposal was made more than Deadline ago, so GovDAO
185// marks it denied for good. Everything else a closure refuses is a panic, which leaves the proposal for a retry.
186func Expired(made, now int64) error {
187	if now-made > Deadline {
188		return errors.New("the proposal is past its 14-day deadline (made at " + strconv.FormatInt(made, 10) + ", now " + strconv.FormatInt(now, 10) + ")")
189	}
190	return nil
191}
192
193// ---------------------------------------------------------------- pages --
194
195func itoa(n int64) string { return strconv.FormatInt(n, 10) }
196
197// Clocks says the clock factor and the delays it gives, for every realm's page: §8.3's check reads it against
198// the served source, where the mainnet build's factor is 1.
199func Clocks() string {
200	return "clock factor " + itoa(Factor) + " (1 on mainnet; a devnet build is compiled with 8): the 7-day delay is " + itoa(Delay) +
201		" s and the 14-day deadline " + itoa(Deadline) + " s of block time"
202}
203
204// ClocksJSON is the same for the agents: {"factor":…,"delay":…,"deadline":…}.
205func ClocksJSON() string {
206	return "{\"factor\":" + itoa(Factor) + ",\"delay\":" + itoa(Delay) + ",\"deadline\":" + itoa(Deadline) + "}"
207}
208
209// Page is the decisions page: for every kind, the pending decisions (up to PageMax) and the last PageMax executed
210// ones, newest first.
211func (l *Ledger) Page() string {
212	if l.all.Size() == 0 {
213		return "No decision yet: one exists only when a proposal made with a builder executes.\n"
214	}
215	kinds := []string{}
216	pending := map[string][]*Decision{}
217	done := map[string][]*Decision{}
218	l.all.ReverseIterate("", "", func(_ string, v any) bool {
219		d := v.(*Decision)
220		if _, ok := pending[d.Kind]; !ok {
221			kinds = append(kinds, d.Kind)
222			pending[d.Kind] = nil
223			done[d.Kind] = nil
224		}
225		if d.Pending() {
226			if len(pending[d.Kind]) < PageMax {
227				pending[d.Kind] = append(pending[d.Kind], d)
228			}
229		} else if len(done[d.Kind]) < PageMax {
230			done[d.Kind] = append(done[d.Kind], d)
231		}
232		return false
233	})
234	var b strings.Builder
235	for _, k := range kinds {
236		b.WriteString("### " + k + "\n\n")
237		if len(pending[k]) == 0 {
238			b.WriteString("pending: none\n")
239		} else {
240			b.WriteString("pending:\n")
241			for _, d := range pending[k] {
242				b.WriteString("- " + line(d) + "\n")
243			}
244		}
245		if len(done[k]) > 0 {
246			b.WriteString("\nexecuted:\n")
247			for _, d := range done[k] {
248				b.WriteString("- " + line(d) + "\n")
249			}
250		}
251		b.WriteString("\n")
252	}
253	return b.String()
254}
255
256func line(d *Decision) string {
257	s := "#" + itoa(d.ID) + " " + d.State() + ": " + d.Text + " (recorded " + itoa(d.Recorded)
258	if d.Queued() {
259		s += ", due " + itoa(d.Due)
260	}
261	switch {
262	case d.Applied != 0:
263		s += ", applied " + itoa(d.Applied)
264	case d.Cancelled != 0:
265		s += ", cancelled " + itoa(d.Cancelled)
266	}
267	s += ")"
268	if d.Note != "" {
269		s += " — " + d.Note
270	}
271	return s
272}
273
274// JSON is every decision, oldest first, as a JSON array.
275func (l *Ledger) JSON() string {
276	var b strings.Builder
277	b.WriteString("[")
278	first := true
279	l.all.Iterate("", "", func(_ string, v any) bool {
280		d := v.(*Decision)
281		if !first {
282			b.WriteString(",")
283		}
284		first = false
285		b.WriteString("{\"id\":" + itoa(d.ID) + ",\"kind\":" + strconv.Quote(d.Kind) + ",\"text\":" + strconv.Quote(d.Text) +
286			",\"state\":" + strconv.Quote(d.State()) + ",\"made\":" + itoa(d.Made) + ",\"recorded\":" + itoa(d.Recorded) +
287			",\"due\":" + itoa(d.Due) + ",\"applied\":" + itoa(d.Applied) + ",\"cancelled\":" + itoa(d.Cancelled) +
288			",\"note\":" + strconv.Quote(d.Note) + "}")
289		return false
290	})
291	b.WriteString("]")
292	return b.String()
293}