PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj/million/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj / million
Files
4 (README)(gnomod.toml)
Exported functions
20
Module
gno.land/r/g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj/million/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • million.gnogno
  • million_test.gnogno
million.gnogno
1// Package million is a pay-per-pixel collaborative canvas: anyone can paint2// any pixel of a Width x Height grid in one of 15 colors by sending exactly3// Price() ugnot per pixel along with the call. Pixels can be repainted by4// anyone, for the same price. Payments are forwarded to the admin wallet5// as they arrive, and the admin paints for free.6//7// The grid logic lives in gno.land/p/g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj/canvas/v0; this realm adds the8// payment guard, the owner controls and the read API used by the web client.9package million1011import (12	"chain"13	"chain/banker"14	"chain/runtime/unsafe"15	"strconv"16	"strings"1718	"gno.land/p/g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj/canvas/v0"19	"gno.land/p/nt/ufmt/v0"20)2122const (23	// Width and Height are fixed for the life of this version: changing24	// 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  = 125027	Height = 8002829	// DefaultMaxBatch bounds a single PaintBatch call so one tx cannot blow30	// the block gas limit or overflow the price arithmetic. The owner can31	// change it at runtime with SetMaxBatch, up to HardMaxBatch.32	DefaultMaxBatch = 500033	HardMaxBatch    = 200003435	// DefaultPrice is the initial price per pixel, in ugnot (0.05 GNOT).36	DefaultPrice int64 = 50_0003738	// DefaultAdmin receives every payment, paints for free and holds the39	// owner controls. Change it before deploying; TransferOwnership moves40	// it afterwards.41	DefaultAdmin = "g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj"4243	Denom = "ugnot"44)4546var (47	board    = canvas.New(Width, Height)48	price    = DefaultPrice49	maxBatch = DefaultMaxBatch50	owner    = address(DefaultAdmin)5152	// Stats.53	revenue int64 // total ugnot ever paid for pixels54	paints  int64 // number of successful Paint/PaintBatch calls5556	// Change tracking, so clients fetch only the rows that moved.57	// version bumps once per successful paint call; rowVersion holds the58	// version of the last paint that touched each row (0 = never painted).59	version    int6460	rowVersion = make([]int64, Height)61)6263// 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}7071// PaintBatch colors up to MaxBatchSize() pixels in one call. The argument is72// "x,y,color;x,y,color;...". Send exactly Price() * count ugnot (nothing73// 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}8182// paint takes the realm as its LAST parameter on purpose: a realm first83// parameter would make it a crossing function and Previous() would then84// 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 to90	// the transaction, which only provably landed here when the caller is91	// a plain user call (no intermediate or ephemeral realm). Keep the two92	// checks together. unsafe is imported for OriginSend only; caller93	// 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	}110111	cost := price * int64(n)112	if painter == owner {113		cost = 0 // the admin paints for free114	}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 nothing120		// accumulates in the realm. BankerTypeOriginSend can only move121		// 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	}125126	version++127	for _, p := range px {128		board.Set(p)129		rowVersion[p.Y] = version130	}131	revenue += cost132	paints++133134	// The pixel list itself is not emitted: event attribute values are135	// size-capped by the node and a full batch exceeds it. Clients re-read136	// 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}143144// 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 int64148	for _, c := range coins {149		if c.Denom != Denom {150			panic("only " + Denom + " is accepted")151		}152		amount += c.Amount153	}154	return amount155}156157// --- owner controls ---158159// assertOwner panics unless the verified caller behind rlm is the owner.160// The realm is deliberately not the first parameter (see paint); the161// 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}170171// SetPrice changes the price per pixel, in ugnot. Zero makes painting free172// 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 = ugnot179}180181// 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 = n188}189190// Withdraw sends amount ugnot held by the realm to the given address.191// Payments are forwarded on arrival, so this only matters for coins sent192// 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}204205// TransferOwnership hands the admin role (payments, free painting and the206// 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 = newOwner213}214215// --- read API (used by the web client through vm/qeval) ---216217func 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 }225226// Pixel returns the color index at (x, y); 0 means never painted.227func Pixel(x, y int) int { return int(board.Get(x, y)) }228229// Rows returns rows [from, to) as one hex digit per pixel, Width digits per230// row, with no separators. Clients page through the grid with it.231func Rows(from, to int) string { return board.Rows(from, to) }232233// Version is the number of successful paint calls so far. Clients remember234// it and ask DirtyRows for what changed since.235func Version() int64 { return version }236237// DirtyRows lists the rows painted after version `since`, as "y,y,y" in238// ascending order. DirtyRows(0) is every row that was ever painted, which239// is all a fresh client needs to load: the others are blank.240func DirtyRows(since int64) string {241	if since < 0 {242		since = 0243	}244	var sb strings.Builder245	for y, v := range rowVersion {246		if v <= since {247			continue248		}249		if sb.Len() > 0 {250			sb.WriteByte(',')251		}252		sb.WriteString(strconv.Itoa(y))253	}254	return sb.String()255}256257// 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}262263// Render shows the game stats on gnoweb. The path is ignored.264func Render(_ string) string {265	var sb strings.Builder266	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}285286// Realm is this realm's gnoweb path.287const Realm = "/r/g19t6f4f4ptt3m949jznalfsx2h8aj696llul0nj/million/v0"288

Functions

  • DirtyRows(since int64) string

  • GetHeight() int

  • GetWidth() int

  • MaxBatchSize() int

  • Owner() string

  • Paint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, x int, y int, color int)

  • PaintBatch(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, pixels string)

  • Painted() int

  • Paints() int64

  • Palette() string

  • Pixel(x int, y int) int

  • Price() int64

  • Render(string) string

  • Revenue() int64

  • Rows(from int, to int) string

  • SetMaxBatch(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, n int)

  • SetPrice(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, ugnot int64)

  • TransferOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newOwner string)

  • Version() int64

  • Withdraw(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount int64)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.