Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

coin.gno

7.29 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/moul/kit/num/v0"
 48	"gno.land/p/moul/kit/ui/v0"
 49	"gno.land/p/moul/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}