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.

gnodraw.gno

9.43 Kb · 287 lines
  1// Package gnodraw is a pay-per-pixel collaborative canvas: anyone can paint
  2// any pixel of a Width x Height grid in one of 15 colors by sending exactly
  3// Price() ugnot per pixel along with the call. Pixels can be repainted by
  4// anyone, for the same price. Payments are forwarded to the admin wallet
  5// as they arrive, and the admin paints for free.
  6//
  7// The grid logic lives in gno.land/p/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/canvas/v0; this realm adds the
  8// payment guard, the owner controls and the read API used by the web client.
  9package gnodraw
 10
 11import (
 12	"chain"
 13	"chain/banker"
 14	"chain/runtime/unsafe"
 15	"strconv"
 16	"strings"
 17
 18	"gno.land/p/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/canvas/v0"
 19	"gno.land/p/nt/ufmt/v0"
 20)
 21
 22const (
 23	// Width and Height are fixed for the life of this version: changing
 24	// them would change the storage layout, so it means a new /vN.
 25	// 1250 x 800 is one million pixels in a 16:10 laptop aspect ratio.
 26	Width  = 1250
 27	Height = 800
 28
 29	// DefaultMaxBatch bounds a single PaintBatch call so one tx cannot blow
 30	// the block gas limit or overflow the price arithmetic. The owner can
 31	// change it at runtime with SetMaxBatch, up to HardMaxBatch.
 32	DefaultMaxBatch = 5000
 33	HardMaxBatch    = 20000
 34
 35	// DefaultPrice is the initial price per pixel, in ugnot (0.05 GNOT).
 36	DefaultPrice int64 = 50_000
 37
 38	// DefaultAdmin receives every payment, paints for free and holds the
 39	// owner controls. Change it before deploying; TransferOwnership moves
 40	// it afterwards.
 41	DefaultAdmin = "g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7"
 42
 43	Denom = "ugnot"
 44)
 45
 46var (
 47	board    = canvas.New(Width, Height)
 48	price    = DefaultPrice
 49	maxBatch = DefaultMaxBatch
 50	owner    = address(DefaultAdmin)
 51
 52	// Stats.
 53	revenue int64 // total ugnot ever paid for pixels
 54	paints  int64 // number of successful Paint/PaintBatch calls
 55
 56	// Change tracking, so clients fetch only the rows that moved.
 57	// version bumps once per successful paint call; rowVersion holds the
 58	// version of the last paint that touched each row (0 = never painted).
 59	version    int64
 60	rowVersion = make([]int64, Height)
 61)
 62
 63// Paint colors one pixel. Send exactly Price() ugnot (nothing for the admin).
 64func Paint(cur realm, x, y, color int) {
 65	if color <= 0 || color > int(canvas.MaxColor) {
 66		panic(canvas.ErrInvalidColor)
 67	}
 68	paint([]canvas.Pixel{{X: x, Y: y, Color: byte(color)}}, cur)
 69}
 70
 71// PaintBatch colors up to MaxBatchSize() pixels in one call. The argument is
 72// "x,y,color;x,y,color;...". Send exactly Price() * count ugnot (nothing
 73// for the admin).
 74func PaintBatch(cur realm, pixels string) {
 75	px, err := canvas.ParsePixels(pixels)
 76	if err != nil {
 77		panic(err)
 78	}
 79	paint(px, cur)
 80}
 81
 82// paint takes the realm as its LAST parameter on purpose: a realm first
 83// parameter would make it a crossing function and Previous() would then
 84// be this realm instead of the user.
 85func paint(px []canvas.Pixel, rlm realm) {
 86	if !rlm.IsCurrent() {
 87		panic("spoofed realm")
 88	}
 89	// Payment guard: unsafe.OriginSend() describes the coins attached to
 90	// the transaction, which only provably landed here when the caller is
 91	// a plain user call (no intermediate or ephemeral realm). Keep the two
 92	// checks together. unsafe is imported for OriginSend only; caller
 93	// identity always comes from the realm handle.
 94	if !rlm.Previous().IsUserCall() {
 95		panic("must be called directly by a user (maketx call)")
 96	}
 97	painter := rlm.Previous().Address()
 98	n := len(px)
 99	if n == 0 {
100		panic("nothing to paint")
101	}
102	if n > maxBatch {
103		panic(ufmt.Sprintf("too many pixels: %d > %d", n, maxBatch))
104	}
105	for _, p := range px {
106		if err := board.Check(p); err != nil {
107			panic(err)
108		}
109	}
110
111	cost := price * int64(n)
112	if painter == owner {
113		cost = 0 // the admin paints for free
114	}
115	if sent := sentUgnot(unsafe.OriginSend()); sent != cost {
116		panic(ufmt.Sprintf("must send exactly %d%s for %d pixel(s), got %d%s", cost, Denom, n, sent, Denom))
117	}
118	if cost > 0 {
119		// Forward the payment to the admin right away, so nothing
120		// accumulates in the realm. BankerTypeOriginSend can only move
121		// what this very message sent, which is exactly cost.
122		bnk := banker.NewBanker(banker.BankerTypeOriginSend, rlm)
123		bnk.SendCoins(rlm.Address(), owner, chain.NewCoins(chain.NewCoin(Denom, cost)))
124	}
125
126	version++
127	for _, p := range px {
128		board.Set(p)
129		rowVersion[p.Y] = version
130	}
131	revenue += cost
132	paints++
133
134	// The pixel list itself is not emitted: event attribute values are
135	// size-capped by the node and a full batch exceeds it. Clients re-read
136	// Rows() after a paint.
137	chain.Emit("Paint",
138		"painter", painter.String(),
139		"count", strconv.Itoa(n),
140		"paid", strconv.Itoa(int(cost))+Denom,
141	)
142}
143
144// sentUgnot returns the ugnot amount in coins and rejects any other denom,
145// so a caller cannot pay with something the realm does not price.
146func sentUgnot(coins chain.Coins) int64 {
147	var amount int64
148	for _, c := range coins {
149		if c.Denom != Denom {
150			panic("only " + Denom + " is accepted")
151		}
152		amount += c.Amount
153	}
154	return amount
155}
156
157// --- owner controls ---
158
159// assertOwner panics unless the verified caller behind rlm is the owner.
160// The realm is deliberately not the first parameter (see paint); the
161// leading blank int is the same idiom gno.land/p/nt/ownable uses.
162func assertOwner(_ int, rlm realm) {
163	if !rlm.IsCurrent() {
164		panic("spoofed realm")
165	}
166	if rlm.Previous().Address() != owner {
167		panic("owner only")
168	}
169}
170
171// SetPrice changes the price per pixel, in ugnot. Zero makes painting free
172// for everyone.
173func SetPrice(cur realm, ugnot int64) {
174	assertOwner(0, cur)
175	if ugnot < 0 {
176		panic("price must not be negative")
177	}
178	price = ugnot
179}
180
181// SetMaxBatch changes the maximum pixels per PaintBatch call, 1..HardMaxBatch.
182func SetMaxBatch(cur realm, n int) {
183	assertOwner(0, cur)
184	if n < 1 || n > HardMaxBatch {
185		panic(ufmt.Sprintf("max batch must be between 1 and %d", HardMaxBatch))
186	}
187	maxBatch = n
188}
189
190// Withdraw sends amount ugnot held by the realm to the given address.
191// Payments are forwarded on arrival, so this only matters for coins sent
192// to the realm outside of a paint call.
193func Withdraw(cur realm, to address, amount int64) {
194	assertOwner(0, cur)
195	if !to.IsValid() {
196		panic("invalid address")
197	}
198	if amount <= 0 {
199		panic("amount must be positive")
200	}
201	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
202	bnk.SendCoins(cur.Address(), to, chain.NewCoins(chain.NewCoin(Denom, amount)))
203}
204
205// TransferOwnership hands the admin role (payments, free painting and the
206// owner controls) to newOwner.
207func TransferOwnership(cur realm, newOwner address) {
208	assertOwner(0, cur)
209	if !newOwner.IsValid() {
210		panic("invalid address")
211	}
212	owner = newOwner
213}
214
215// --- read API (used by the web client through vm/qeval) ---
216
217func GetWidth() int    { return Width }
218func GetHeight() int   { return Height }
219func Price() int64     { return price }
220func Owner() address   { return owner }
221func Painted() int     { return board.Painted() }
222func Revenue() int64   { return revenue }
223func Paints() int64    { return paints }
224func MaxBatchSize() int { return maxBatch }
225
226// Pixel returns the color index at (x, y); 0 means never painted.
227func Pixel(x, y int) int { return int(board.Get(x, y)) }
228
229// Rows returns rows [from, to) as one hex digit per pixel, Width digits per
230// row, with no separators. Clients page through the grid with it.
231func Rows(from, to int) string { return board.Rows(from, to) }
232
233// Version is the number of successful paint calls so far. Clients remember
234// it and ask DirtyRows for what changed since.
235func Version() int64 { return version }
236
237// DirtyRows lists the rows painted after version `since`, as "y,y,y" in
238// ascending order. DirtyRows(0) is every row that was ever painted, which
239// is all a fresh client needs to load: the others are blank.
240func DirtyRows(since int64) string {
241	if since < 0 {
242		since = 0
243	}
244	var sb strings.Builder
245	for y, v := range rowVersion {
246		if v <= since {
247			continue
248		}
249		if sb.Len() > 0 {
250			sb.WriteByte(',')
251		}
252		sb.WriteString(strconv.Itoa(y))
253	}
254	return sb.String()
255}
256
257// Palette returns the 16 palette colors as comma-separated CSS hex values,
258// index 0 being the blank color.
259func Palette() string {
260	return strings.Join(canvas.Palette[:], ",")
261}
262
263// Render shows the game stats on gnoweb. The path is ignored.
264func Render(_ string) string {
265	var sb strings.Builder
266	sb.WriteString("# Million Gno\n\n")
267	sb.WriteString("A pay-per-pixel canvas. Pick a color, pay the price, own the pixel until someone repaints it. Payments go to the admin wallet, which paints for free.\n\n")
268	sb.WriteString("| | |\n|---|---|\n")
269	sb.WriteString(ufmt.Sprintf("| Size | %d x %d |\n", Width, Height))
270	sb.WriteString(ufmt.Sprintf("| Price per pixel | %d%s |\n", price, Denom))
271	sb.WriteString(ufmt.Sprintf("| Pixels painted | %d / %d |\n", board.Painted(), Width*Height))
272	sb.WriteString(ufmt.Sprintf("| Paint calls | %d |\n", paints))
273	sb.WriteString(ufmt.Sprintf("| Version | %d |\n", version))
274	sb.WriteString(ufmt.Sprintf("| Revenue | %d%s |\n", revenue, Denom))
275	sb.WriteString(ufmt.Sprintf("| Admin | %s |\n", owner.String()))
276	sb.WriteString("\n## Palette\n\n")
277	for i := 1; i <= int(canvas.MaxColor); i++ {
278		sb.WriteString(ufmt.Sprintf("- %d: `%s`\n", i, canvas.Palette[i]))
279	}
280	sb.WriteString("\n## Paint\n\n")
281	sb.WriteString("- [Paint one pixel](" + Realm + "$help&func=Paint): `Paint(x, y, color)` with `-send <price>ugnot`\n")
282	sb.WriteString("- [Paint a batch](" + Realm + "$help&func=PaintBatch): `PaintBatch(\"x,y,color;x,y,color\")` with `-send <price*count>ugnot`\n")
283	return sb.String()
284}
285
286// Realm is this realm's gnoweb path.
287const Realm = "/r/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/gnodraw/v0"