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.

lastwords.gno

9.81 Kb · 319 lines
  1// Package lastwords is one string, on chain, forever.
  2//
  3// There is a single message slot. Pay more than the last writer paid and the
  4// slot is yours; when the clock finally runs out, whatever is in it stays there
  5// permanently. The artefact at the end is the point, so the game is a bidding
  6// war over an epitaph rather than over a prize.
  7//
  8// The three rules exist because the obvious version of this game does not end
  9// and does not pay:
 10//
 11//  1. THE CLOCK TERMINATES. Every write pushes the deadline a fifth of the way
 12//     to a hard end fixed when the slot opened, and no write can push it past
 13//     that. A constant extension per action has no end: the pot grows faster
 14//     than the price of the next write, so there is always a rational next
 15//     write and the game runs until everyone is bored or broke.
 16//  2. THE LAST WRITER DOES NOT TAKE THE POT. They take the slot, which is what
 17//     they were bidding for. The pot is split among everyone ELSE who wrote,
 18//     pro rata to what they paid. Winner-takes-all makes the second-to-last
 19//     writer the mark and everyone knows it, which is why nobody joins.
 20//  3. HALF OF EVERY PAYMENT GOES TO THE WRITER BEING DISPLACED, immediately.
 21//     A zero-sum game minus gas has no reason for a second player. Overwriting
 22//     somebody pays them, so being overwritten early is not a loss.
 23//
 24// Nothing is ever pushed: payouts are credited to an internal ledger and swept
 25// by Withdraw. A realm that pays inside a loop over its roster is a gas bomb
 26// that eventually bricks, and the bigger the game gets the more certain that
 27// is.
 28//
 29// The pieces are libraries, and the realm is the wiring:
 30// [p/moul/x/games/clock](/p/nym-moultest123/x/games/clock/v0) owns the deadline and its
 31// guards, [p/moul/x/games/prorata](/p/nym-moultest123/x/games/prorata/v0) owns the split,
 32// and [p/moul/x/daily/pullpayment](/p/nym-moultest123/x/daily/pullpayment/v0) owns the
 33// credit ledger.
 34package lastwords
 35
 36import (
 37	"strconv"
 38	"strings"
 39
 40	"chain"
 41	"chain/banker"
 42	"chain/runtime"
 43	"chain/runtime/unsafe"
 44
 45	"gno.land/p/nym-moultest123/x/daily/pullpayment/v0"
 46	"gno.land/p/nym-moultest123/x/games/clock/v0"
 47	"gno.land/p/nym-moultest123/x/games/prorata/v0"
 48	"gno.land/p/nt/avl/v0"
 49)
 50
 51const denom = "ugnot"
 52
 53// The rules, fixed at deploy. Times are block heights.
 54const (
 55	// Window is how long the slot stays open with nobody writing.
 56	Window = int64(2000)
 57	// Floor is the minimum a write leaves on the clock, so the last stretch
 58	// stays long enough for a transaction to land in. Without it a decaying
 59	// extension shrinks to nothing and whoever holds at that moment wins by
 60	// being unreachable rather than by paying.
 61	Floor = int64(300)
 62	// Life is the hard end. No write can push the deadline past start+Life.
 63	Life = int64(100000)
 64	// BumpPct is how much of the gap to the hard end a write buys.
 65	BumpPct = int64(20)
 66	// DividendPct is the share of a payment credited to the writer being
 67	// displaced; the rest goes to the pot.
 68	DividendPct = int64(50)
 69
 70	// MinWrite is the base price of the slot, in ugnot.
 71	MinWrite = int64(1000000)
 72	// PerByte is charged on top, in ugnot per byte of the message. The chain's
 73	// own storage deposit is 100 ugnot per byte, so this is that plus a premium
 74	// for occupying the only slot there is.
 75	PerByte = int64(1000)
 76	// MaxLen caps the message in bytes, because the message IS the storage
 77	// cost and an uncapped one is an unbounded write priced at a constant.
 78	MaxLen = 280
 79)
 80
 81var (
 82	clk     *clock.Clock
 83	ledger  = pullpayment.New()
 84	message string
 85	author  address
 86	paid    int64    // what the current holder paid
 87	pot     int64    // what settlement will split
 88	writers avl.Tree // address string -> int64 total paid
 89	order   []string // writers in first-write order, so the split is deterministic
 90	writes  int64
 91	settled bool
 92)
 93
 94func init() {
 95	open(runtime.ChainHeight())
 96}
 97
 98// open resets the game to a fresh slot at height h. It is the only place the
 99// clock is built, so tests and init cannot disagree about the rules.
100func open(h int64) {
101	c, err := clock.New(h, Window, Floor, Life)
102	if err != nil {
103		panic("lastwords: " + err.Error())
104	}
105	clk = c
106	ledger = pullpayment.New()
107	message = ""
108	author = ""
109	paid = 0
110	pot = 0
111	writers = avl.Tree{}
112	order = nil
113	writes = 0
114	settled = false
115}
116
117// Price returns what msg costs to write right now, in ugnot: a base, plus a
118// charge per byte, and always at least one ugnot more than the current holder
119// paid. Query it before sending; an underpaying write is refused, not refunded.
120func Price(msg string) int64 {
121	price := MinWrite + int64(len(msg))*PerByte
122	if next := paid + 1; next > price {
123		price = next
124	}
125	return price
126}
127
128// Write puts msg in the slot and makes the caller its holder. The caller must
129// send at least Price(msg) ugnot with the transaction.
130//
131// Half of the payment is credited to the writer being displaced and the rest
132// joins the pot. The deadline is pushed a fifth of the way toward the hard end,
133// never past it, and never to less than Floor from now.
134//
135// Only a direct user transaction may write, so the payment envelope cannot be
136// spoofed by an ephemeral MsgRun realm.
137func Write(cur realm, msg string) {
138	if !cur.Previous().IsUserCall() {
139		panic("lastwords: write must be a direct user transaction")
140	}
141	h := runtime.ChainHeight()
142	if clk.Expired(h) {
143		panic("lastwords: the slot is closed, these are somebody's last words now")
144	}
145	if msg == "" {
146		panic("lastwords: message must not be empty")
147	}
148	if len(msg) > MaxLen {
149		panic("lastwords: message must be at most " + strconv.Itoa(MaxLen) + " bytes")
150	}
151	if strings.ContainsAny(msg, "\n\r") {
152		panic("lastwords: message must be a single line")
153	}
154
155	price := Price(msg)
156	sent := unsafe.OriginSend().AmountOf(denom)
157	if sent < price {
158		panic("lastwords: sent " + strconv.FormatInt(sent, 10) +
159			" ugnot, price is " + strconv.FormatInt(price, 10))
160	}
161
162	caller := cur.Previous().Address()
163
164	// The displaced writer is paid first, out of the payment that displaced
165	// them. Credited, never sent: see the package doc.
166	dividend := int64(0)
167	if author != "" {
168		dividend = sent * DividendPct / 100
169		if err := ledger.Credit(author.String(), dividend); err != nil {
170			panic("lastwords: " + err.Error())
171		}
172	}
173	pot += sent - dividend
174
175	key := caller.String()
176	if _, seen := writers.Get(key).(int64); !seen {
177		order = append(order, key)
178	}
179	writers.Set(key, totalPaid(key)+sent)
180
181	message = msg
182	author = caller
183	paid = sent
184	writes++
185
186	deadline, err := clk.BumpShare(h, BumpPct)
187	if err != nil {
188		panic("lastwords: " + err.Error())
189	}
190
191	chain.Emit("Write",
192		"author", key,
193		"paid", strconv.FormatInt(sent, 10),
194		"dividend", strconv.FormatInt(dividend, 10),
195		"deadline", strconv.FormatInt(deadline, 10),
196	)
197}
198
199// Settle closes the game once the clock has run out, splitting the pot among
200// every writer EXCEPT the one holding the slot, pro rata to what each paid.
201//
202// Anyone may call it: leaving settlement to an interested party is how a pot
203// stays unclaimed. It moves no coins, it credits the ledger; Withdraw is what
204// pays. If the holder is the only writer there is nobody else to split with,
205// so the pot returns to them.
206func Settle(cur realm) {
207	h := runtime.ChainHeight()
208	if !clk.Expired(h) {
209		panic("lastwords: still open until height " + strconv.FormatInt(clk.Deadline(), 10))
210	}
211	if settled {
212		panic("lastwords: already settled")
213	}
214	settled = true
215	if pot == 0 {
216		return
217	}
218
219	holder := author.String()
220	payees := []string{}
221	weights := []int64{}
222	for _, k := range order {
223		if k == holder {
224			continue
225		}
226		payees = append(payees, k)
227		weights = append(weights, totalPaid(k))
228	}
229	if len(payees) == 0 {
230		// The holder wrote every word there is. Nobody to share with.
231		payees = []string{holder}
232		weights = []int64{1}
233	}
234
235	shares, err := prorata.Split(pot, weights)
236	if err != nil {
237		panic("lastwords: " + err.Error())
238	}
239
240	// A zero share is dropped: CreditMany refuses a non-positive amount, and
241	// crediting nothing is not a payment anyway.
242	dst := []string{}
243	amounts := []int64{}
244	for i, s := range shares {
245		if s > 0 {
246			dst = append(dst, payees[i])
247			amounts = append(amounts, s)
248		}
249	}
250	if len(dst) > 0 {
251		if err := ledger.CreditMany(dst, amounts); err != nil {
252			panic("lastwords: " + err.Error())
253		}
254	}
255	pot = 0
256
257	chain.Emit("Settle",
258		"holder", holder,
259		"writes", strconv.FormatInt(writes, 10),
260		"payees", strconv.Itoa(len(dst)),
261	)
262}
263
264// Withdraw pays the caller everything credited to them and returns the amount.
265// Dividends are claimable while the game runs; settlement shares only after
266// Settle.
267func Withdraw(cur realm) int64 {
268	caller := cur.Previous().Address()
269	amount, err := ledger.Withdraw(caller.String())
270	if err != nil {
271		panic("lastwords: " + err.Error())
272	}
273	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
274	bnk.SendCoins(cur.Address(), caller, chain.NewCoins(chain.NewCoin(denom, amount)))
275
276	chain.Emit("Withdraw", "payee", caller.String(), "amount", strconv.FormatInt(amount, 10))
277	return amount
278}
279
280// Message returns the slot's current contents, raw. Render is what escapes it.
281func Message() string { return message }
282
283// Author returns who holds the slot.
284func Author() address { return author }
285
286// Pot returns what settlement will split.
287func Pot() int64 { return pot }
288
289// Deadline returns the height the slot closes at.
290func Deadline() int64 { return clk.Deadline() }
291
292// Owed returns what an address can Withdraw right now.
293func Owed(addr address) int64 { return ledger.Balance(addr.String()) }
294
295// totalPaid returns what an address has paid in total, zero if it never wrote.
296func totalPaid(key string) int64 {
297	v, ok := writers.Get(key).(int64)
298	if !ok {
299		return 0
300	}
301	return v
302}
303
304func gnot(ugnot int64) string {
305	whole := ugnot / 1000000
306	frac := ugnot % 1000000
307	s := strconv.FormatInt(whole, 10)
308	if frac == 0 {
309		return s
310	}
311	f := strconv.FormatInt(frac, 10)
312	for len(f) < 6 {
313		f = "0" + f
314	}
315	for len(f) > 1 && f[len(f)-1] == '0' {
316		f = f[:len(f)-1]
317	}
318	return s + "." + f
319}