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.

coin.gno

7.39 Kb · 193 lines
  1// Package coin is a GRC20 that cannot exist until its author has answered the
  2// three questions a social app's token usually dodges: what mints it, what
  3// burns it, and who has a reason to buy it.
  4//
  5// # Why a package and not a convention
  6//
  7// Every app in the x/social family is asked the same question, "and can it
  8// have a token", and the honest answer is only yes when all three of those
  9// have an answer. A token with a mint rule and no sink is a scoreboard with a
 10// price: supply grows, nothing consumes it, and the number on the leaderboard
 11// is the whole product. That is the shape a chain-wide measurement keeps
 12// finding, and it is cheap to avoid, so [New] refuses a [Policy] with a blank
 13// field rather than letting the omission ship.
 14//
 15// The three strings are not validated beyond being non-empty, because no
 16// package can check that a sink is real. They are a declaration, rendered on
 17// the realm's own page by [Coin.Render], where being wrong is visible.
 18//
 19// # The model
 20//
 21//	earned  minted by the app, for the behaviour the app wants more of
 22//	spent   burned by the app, for the thing holders actually want
 23//	traded  a plain GRC20 transfer, which is what makes the sink a market
 24//
 25// Earning and spending are the app's business and go through [Coin.Earn] and
 26// [Coin.Spend], which keep running totals so a reader can see the two halves
 27// against each other. Transfers, approvals and balances are the embedded
 28// [grc20.Token]'s, unchanged, so wallets and indexers see an ordinary GRC20.
 29//
 30// # Usage
 31//
 32// A realm holds one Coin and never exports the ledger:
 33//
 34//	var points = coin.New("Thread Points", "THREAD", 0, 0, coin.Policy{
 35//		Mint:  "1 per distinct address that replies to your thread",
 36//		Sink:  "burned to pin a thread to the top of its page",
 37//		Buyer: "anyone who wants placement and has not earned it",
 38//	}, 0, cur)
 39//
 40// The trailing `_ int, rlm realm` is the shape a pure package has to use to
 41// reach the caller's frame: a p/ package may not declare a crossing function,
 42// so the realm token is threaded as a later parameter, exactly as grc20's own
 43// tellers do.
 44package coin
 45
 46import (
 47	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/num/v0"
 48	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/ui/v0"
 49	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
 50	"gno.land/p/nt/grc20/v0"
 51	"gno.land/p/nt/seqid/v0"
 52	"gno.land/p/nt/ufmt/v0"
 53)
 54
 55// Policy is the three declarations a Coin cannot be created without.
 56//
 57// Each one is a sentence for a human: it is rendered on the issuing realm's
 58// page and nothing branches on it.
 59type Policy struct {
 60	// Mint says what behaviour earns the token, precisely enough that a
 61	// reader can work out whether they can farm it.
 62	Mint string
 63
 64	// Sink says what destroys it. "Nothing" is not an answer; if there is
 65	// no sink, there is no reason for the token to exist.
 66	Sink string
 67
 68	// Buyer says who wants it badly enough to acquire it from someone who
 69	// earned it. This is the one most often left blank, and the one that
 70	// decides whether the other two matter.
 71	Buyer string
 72}
 73
 74// Valid reports whether every field is filled in.
 75func (p Policy) Valid() bool {
 76	return p.Mint != "" && p.Sink != "" && p.Buyer != ""
 77}
 78
 79// Coin is a GRC20 plus the policy it was issued under and the running totals
 80// of the two flows the policy describes.
 81type Coin struct {
 82	tok    *grc20.Token
 83	led    *grc20.PrivateLedger
 84	policy Policy
 85
 86	earned int64 // cumulative minted
 87	spent  int64 // cumulative burned
 88}
 89
 90// New issues the token. It panics when the policy is incomplete, which is the
 91// whole point of the package: the declaration happens before the first unit
 92// exists, not in a README written afterwards.
 93//
 94// name, symbol, decimals and id are grc20's own; id distinguishes two tokens
 95// issued by the same realm.
 96func New(name, symbol string, decimals int, id seqid.ID, policy Policy, _ int, rlm realm) *Coin {
 97	if !policy.Valid() {
 98		panic("coin: a token needs a mint rule, a sink and a buyer; one of the three is empty")
 99	}
100	tok, led := grc20.NewToken(name, symbol, decimals, id, rlm)
101	return &Coin{tok: tok, led: led, policy: policy}
102}
103
104// Token returns the GRC20 itself, for the realm to expose as its read API and
105// to register with a token registry.
106func (c *Coin) Token() *grc20.Token { return c.tok }
107
108// CallerTeller is the teller that acts as the user who called the realm: it
109// moves their own balance and nothing else.
110//
111// It is exposed, where the ledger is not, because transfer and approve are
112// what make the sink a market. Mint and burn stay behind [Coin.Earn] and
113// [Coin.Spend] so the mint rule keeps exactly one call site.
114func (c *Coin) CallerTeller() grc20.Teller { return c.led.CallerTeller() }
115
116// Policy returns the declarations the token was issued under.
117func (c *Coin) Policy() Policy { return c.policy }
118
119// Earn mints amount to addr. The realm calls it from the one place its mint
120// rule is implemented, so that rule has exactly one call site.
121//
122// A non-positive amount is a no-op rather than an abort: a mint rule that
123// computes zero (nothing new happened) is a normal outcome and should not
124// fail the transaction that discovered it.
125func (c *Coin) Earn(to address, amount int64) {
126	if amount <= 0 {
127		return
128	}
129	if err := c.led.Mint(to, amount); err != nil {
130		panic("coin: mint: " + err.Error())
131	}
132	c.earned += amount
133}
134
135// Spend burns amount from addr, which is how the sink consumes supply.
136//
137// It aborts when the balance is short, naming both numbers, because the
138// caller is a user who is about to be told they cannot afford something.
139func (c *Coin) Spend(from address, amount int64) {
140	if amount <= 0 {
141		panic("coin: spend: amount must be positive")
142	}
143	if bal := c.tok.BalanceOf(from); bal < amount {
144		panic(ufmt.Sprintf("coin: balance %d is short of %d %s", bal, amount, c.tok.GetSymbol()))
145	}
146	if err := c.led.Burn(from, amount); err != nil {
147		panic("coin: burn: " + err.Error())
148	}
149	c.spent += amount
150}
151
152// BalanceOf is the holder's balance.
153func (c *Coin) BalanceOf(addr address) int64 { return c.tok.BalanceOf(addr) }
154
155// Supply is what exists right now, that is, earned minus spent.
156func (c *Coin) Supply() int64 { return c.tok.TotalSupply() }
157
158// Earned and Spent are the cumulative flows. Their difference is [Coin.Supply]
159// and they are kept separately because a sink that never fires is invisible in
160// the supply alone: a flat line reads the same as no sink at all.
161func (c *Coin) Earned() int64 { return c.earned }
162
163// Spent is the cumulative amount burned through [Coin.Spend].
164func (c *Coin) Spent() int64 { return c.spent }
165
166// Holders is how many addresses the ledger knows about.
167func (c *Coin) Holders() int { return c.tok.KnownAccounts() }
168
169// Render is the block a realm puts on its own page: the three declarations,
170// then the two flows against each other.
171//
172// The policy strings are escaped: they are constants in practice, but a realm
173// could build one from configuration, and this package cannot tell.
174func (c *Coin) Render() string {
175	out := md.H3(ui.Inline(c.tok.GetName()) + " (" + ui.Inline(c.tok.GetSymbol()) + ")")
176
177	t := ui.NewTable("", "")
178	t.Row("earns", ui.Cell(c.policy.Mint))
179	t.Row("burns", ui.Cell(c.policy.Sink))
180	t.Row("bought by", ui.Cell(c.policy.Buyer))
181	out += t.String()
182
183	s := ui.NewTable("supply", "earned", "burned", "holders")
184	s.Row(
185		num.Dec(c.Supply(), c.tok.GetDecimals()),
186		num.Dec(c.earned, c.tok.GetDecimals()),
187		num.Dec(c.spent, c.tok.GetDecimals()),
188		ufmt.Sprintf("%d", c.Holders()),
189	)
190	out += s.String()
191
192	return out
193}