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.

adtreasury.gno

6.61 Kb · 198 lines
  1// Package adtreasury is the ad station's own treasury (jae, his terminal, 2026-10-14: "it goes to a treasury
  2// specifically for the ad station with its own controls/governance" → "just me for now but make it upgradeable").
  3//
  4// It holds GNOT: what the ad station's weekly auctions earn (r/…/adstation sends each settled week's winning bids
  5// here), and whatever anyone sends to its address. One controller spends it, each spend on the record with its memo.
  6// The controller is Jae's cold key at first; the role moves by a two-step hand-off (SetController names, AcceptController
  7// takes), and the one named may be a realm — a council or a vote later calls AcceptController and Spend through a
  8// crossing call, so the governance can be upgraded without moving the money.
  9//
 10// Immutable, not private (gnomod.toml).
 11package adtreasury
 12
 13import (
 14	"chain"
 15	"chain/banker"
 16	"strconv"
 17	"strings"
 18	"time"
 19)
 20
 21const (
 22	pkgPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/adtreasury" // must match gnomod.toml
 23	GNOT    = "ugnot"
 24	maxMemo = 120
 25	listMax = 50 // spends the page lists
 26)
 27
 28// Payment is one spend out of the treasury, on the record for good.
 29type Payment struct {
 30	ID     int64
 31	To     address
 32	Amount int64 // ugnot
 33	Memo   string
 34	By     address // the controller that sent it
 35	At     int64   // unix seconds
 36}
 37
 38var (
 39	controller     = address("g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6") // Jae's cold key (the realm ownership rule)
 40	nextController address
 41	spends         []Payment
 42	spent          int64
 43)
 44
 45func init(cur realm) {
 46	if cur.Address() != chain.PackageAddress(pkgPath) {
 47		panic("adtreasury: deployed at a path other than pkgPath")
 48	}
 49}
 50
 51// ---------------------------------------------------------------- reading --
 52
 53// Address is where the treasury receives: send GNOT here.
 54func Address() address { return chain.PackageAddress(pkgPath) }
 55
 56// Balance is the GNOT the treasury holds, in ugnot.
 57func Balance() int64 {
 58	return banker.NewReadonlyBanker().GetCoin(Address(), GNOT)
 59}
 60
 61func Controller() address     { return controller }
 62func NextController() address { return nextController }
 63func Spent() int64            { return spent }
 64func Count() int              { return len(spends) }
 65
 66// Get is a copy of spend id (1-based).
 67func Get(id int64) Payment {
 68	if id < 1 || id > int64(len(spends)) {
 69		panic("adtreasury: no such spend")
 70	}
 71	return spends[id-1]
 72}
 73
 74// ---------------------------------------------------------------- writing --
 75
 76func caller(cur realm) address {
 77	if !cur.IsCurrent() {
 78		panic("adtreasury: spoofed realm")
 79	}
 80	return cur.Previous().Address()
 81}
 82
 83func mustControl(cur realm) address {
 84	who := caller(cur)
 85	if who != controller {
 86		panic("adtreasury: only the controller")
 87	}
 88	return who
 89}
 90
 91// Spend pays ugnot to an address, by the controller, with a memo kept on the record (plain text, at most 120
 92// characters). Returns the spend's id.
 93func Spend(cur realm, to address, ugnot int64, memo string) int64 {
 94	by := mustControl(cur)
 95	if !to.IsValid() {
 96		panic("adtreasury: not an address")
 97	}
 98	if to == Address() {
 99		panic("adtreasury: the treasury cannot pay itself")
100	}
101	if ugnot <= 0 {
102		panic("adtreasury: the amount must be positive")
103	}
104	if ugnot > Balance() {
105		panic("adtreasury: the treasury holds only " + strconv.FormatInt(Balance(), 10) + " ugnot")
106	}
107	plain(memo)
108	banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(Address(), to, chain.Coins{{GNOT, ugnot}})
109	spent += ugnot
110	id := int64(len(spends)) + 1
111	spends = append(spends, Payment{ID: id, To: to, Amount: ugnot, Memo: memo, By: by, At: time.Now().Unix()})
112	chain.Emit("Spent", "id", strconv.FormatInt(id, 10), "to", to.String(), "ugnot", strconv.FormatInt(ugnot, 10), "by", by.String())
113	return id
114}
115
116// SetController names the next controller; nothing changes until they AcceptController. "" withdraws the offer.
117func SetController(cur realm, to address) {
118	mustControl(cur)
119	if to != "" && !to.IsValid() {
120		panic("adtreasury: not an address")
121	}
122	nextController = to
123	chain.Emit("ControllerNamed", "next", to.String())
124}
125
126// AcceptController takes the controller role, by the one named (a user or a realm).
127func AcceptController(cur realm) {
128	who := caller(cur)
129	if nextController == "" || who != nextController {
130		panic("adtreasury: not the one named")
131	}
132	prev := controller
133	controller, nextController = who, ""
134	chain.Emit("ControllerChanged", "from", prev.String(), "to", who.String())
135}
136
137// plain refuses text the page would render as markup.
138func plain(s string) {
139	if len(s) > maxMemo {
140		panic("adtreasury: the memo is at most " + strconv.Itoa(maxMemo) + " characters")
141	}
142	for i := 0; i < len(s); i++ {
143		if s[i] < 0x20 || s[i] == 0x7f || strings.IndexByte("[]()<>`*_#|\\!~", s[i]) >= 0 {
144			panic("adtreasury: the memo is plain text without markup")
145		}
146	}
147}
148
149// ---------------------------------------------------------------- pages --
150
151// Render: "" the treasury and its last spends; "json" its figures.
152func Render(path string) string {
153	switch path {
154	case "":
155		return home()
156	case "json":
157		return `{"address":"` + Address().String() + `","balance":` + strconv.FormatInt(Balance(), 10) + `,"spent":` + strconv.FormatInt(spent, 10) +
158			`,"controller":"` + controller.String() + `","next":"` + nextController.String() + `","spends":` + strconv.Itoa(len(spends)) + `}`
159	}
160	return "404"
161}
162
163func home() string {
164	var b strings.Builder
165	b.WriteString("# The ad station's treasury\n\n")
166	b.WriteString("What gno.world's ad station earns: each week's winning bids, from the station's auction. One controller spends it, every spend on the record below.\n\n")
167	b.WriteString("- holds: " + gnot(Balance()) + "\n")
168	b.WriteString("- spent so far: " + gnot(spent) + "\n")
169	b.WriteString("- controller: " + controller.String() + "\n")
170	if nextController != "" {
171		b.WriteString("- named to take over: " + nextController.String() + "\n")
172	}
173	b.WriteString("- address: " + Address().String() + "\n\n")
174	if len(spends) == 0 {
175		b.WriteString("_Nothing spent yet._\n")
176		return b.String()
177	}
178	b.WriteString("## Spends\n\n")
179	for i, n := len(spends)-1, 0; i >= 0 && n < listMax; i, n = i-1, n+1 {
180		s := spends[i]
181		b.WriteString("- #" + strconv.FormatInt(s.ID, 10) + " " + time.Unix(s.At, 0).UTC().Format("2006-01-02") + " · " + gnot(s.Amount) + " to " + s.To.String())
182		if s.Memo != "" {
183			b.WriteString(" · " + s.Memo)
184		}
185		b.WriteString("\n")
186	}
187	return b.String()
188}
189
190// gnot formats ugnot as GNOT, the trailing zeros trimmed.
191func gnot(ugnot int64) string {
192	whole, frac := ugnot/1_000_000, ugnot%1_000_000
193	if frac == 0 {
194		return strconv.FormatInt(whole, 10) + " GNOT"
195	}
196	f := strconv.FormatInt(1_000_000+frac, 10)[1:]
197	return strconv.FormatInt(whole, 10) + "." + strings.TrimRight(f, "0") + " GNOT"
198}