1package gnodraw23import (4 "strings"5 "testing"67 "chain"8 "chain/banker"910 "gno.land/p/nt/testutils/v0"11 "gno.land/p/nt/uassert/v0"12)1314var (15 deployer = testutils.TestAddress("deployer")16 alice = testutils.TestAddress("alice")17 bob = testutils.TestAddress("bob")18)1920var realmAddr = chain.PackageAddress("gno.land/r/g1v7s3j506wqul2zwt90awgjphpx4ztwga0926f7/gnodraw/v0")2122// Tests own the admin role through a test address rather than DefaultAdmin.23func init() {24 owner = deployer25}2627// fund credits the realm with what the next paint call claims to send, as28// the chain would before running the message, and resets the per-message29// origin-send spending limit that the previous call used up.30func fund(n int64) {31 testing.SetOriginSpend(nil)32 if n > 0 {33 testing.IssueCoins(realmAddr, ugnot(n))34 }35}3637func balanceOf(a address) int64 {38 return banker.NewReadonlyBanker().GetCoin(a, Denom)39}4041func ugnot(n int64) chain.Coins {42 return chain.NewCoins(chain.NewCoin(Denom, n))43}4445// testing.SetRealm only affects the frame that calls it, so it cannot be46// wrapped in a helper: every test sets the caller inline.4748func TestInitialState(t *testing.T) {49 uassert.Equal(t, Width, GetWidth())50 uassert.Equal(t, Height, GetHeight())51 uassert.Equal(t, DefaultPrice, Price())52 uassert.Equal(t, DefaultMaxBatch, MaxBatchSize())53 uassert.Equal(t, 0, Pixel(0, 0))54 uassert.Equal(t, deployer, Owner())55 uassert.Equal(t, 16, len(strings.Split(Palette(), ",")))56}5758func TestPaintOnePixel(cur realm, t *testing.T) {59 adminBefore := balanceOf(owner)6061 testing.SetRealm(testing.NewUserRealm(alice))62 testing.SetOriginSend(ugnot(price))63 fund(price)64 Paint(cross(cur), 10, 20, 6)6566 uassert.Equal(t, 6, Pixel(10, 20))67 uassert.Equal(t, 1, Painted())68 uassert.Equal(t, price, Revenue())69 uassert.Equal(t, int64(1), Paints())70 // The payment went straight to the admin, nothing stayed in the realm.71 uassert.Equal(t, adminBefore+price, balanceOf(owner))72 uassert.Equal(t, int64(0), balanceOf(realmAddr))7374 // Anyone can repaint for the same price; the count does not grow.75 testing.SetRealm(testing.NewUserRealm(bob))76 testing.SetOriginSend(ugnot(price))77 fund(price)78 Paint(cross(cur), 10, 20, 15)79 uassert.Equal(t, 15, Pixel(10, 20))80 uassert.Equal(t, 1, Painted())81 uassert.Equal(t, 2*price, Revenue())82 uassert.Equal(t, adminBefore+2*price, balanceOf(owner))83}8485func TestAdminPaintsForFree(cur realm, t *testing.T) {86 adminBefore := balanceOf(owner)87 revenueBefore := Revenue()8889 testing.SetRealm(testing.NewUserRealm(owner))90 testing.SetOriginSend(nil)91 Paint(cross(cur), 400, 400, 8)92 uassert.Equal(t, 8, Pixel(400, 400))9394 testing.SetRealm(testing.NewUserRealm(owner))95 testing.SetOriginSend(nil)96 PaintBatch(cross(cur), "401,400,9;402,400,10")97 uassert.Equal(t, 10, Pixel(402, 400))9899 // Free means exactly free: the admin must not attach coins either.100 testing.SetRealm(testing.NewUserRealm(owner))101 testing.SetOriginSend(ugnot(price))102 uassert.AbortsContains(t, cur, "must send exactly 0ugnot", func() { Paint(cross(cur), 403, 400, 1) })103104 uassert.Equal(t, revenueBefore, Revenue())105 uassert.Equal(t, adminBefore, balanceOf(owner))106}107108func TestPaintRejectsWrongPayment(cur realm, t *testing.T) {109 before := Painted()110111 testing.SetRealm(testing.NewUserRealm(alice))112 testing.SetOriginSend(ugnot(price-1))113 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 0, 0, 1) })114115 testing.SetRealm(testing.NewUserRealm(alice))116 testing.SetOriginSend(ugnot(price+1))117 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 0, 0, 1) })118119 testing.SetRealm(testing.NewUserRealm(alice))120 testing.SetOriginSend(nil)121 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 0, 0, 1) })122123 testing.SetRealm(testing.NewUserRealm(alice))124 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("uatom", price)))125 uassert.AbortsContains(t, cur, "only ugnot is accepted", func() { Paint(cross(cur), 0, 0, 1) })126127 uassert.Equal(t, before, Painted())128 uassert.Equal(t, 0, Pixel(0, 0))129}130131func TestPaintRejectsInvalidPixels(cur realm, t *testing.T) {132 testing.SetRealm(testing.NewUserRealm(alice))133 testing.SetOriginSend(ugnot(price))134 uassert.AbortsContains(t, cur, "out of range", func() { Paint(cross(cur), Width, 0, 1) })135 uassert.AbortsContains(t, cur, "out of range", func() { Paint(cross(cur), 0, Height, 1) })136 uassert.AbortsContains(t, cur, "out of range", func() { Paint(cross(cur), -1, 0, 1) })137 uassert.AbortsContains(t, cur, "color must be", func() { Paint(cross(cur), 0, 0, 0) })138 uassert.AbortsContains(t, cur, "color must be", func() { Paint(cross(cur), 0, 0, 16) })139 uassert.AbortsContains(t, cur, "color must be", func() { Paint(cross(cur), 0, 0, -1) })140}141142func TestPaintRejectsCodeRealm(cur realm, t *testing.T) {143 // Regression for the OriginSend bypass: an intermediate realm forwards144 // the call while the envelope claims the coins were sent.145 testing.SetRealm(testing.NewCodeRealm("gno.land/r/attacker/wrapper"))146 testing.SetOriginSend(ugnot(price))147 uassert.AbortsContains(t, cur, "directly by a user", func() { Paint(cross(cur), 1, 1, 1) })148 uassert.Equal(t, 0, Pixel(1, 1))149}150151func TestPaintBatch(cur realm, t *testing.T) {152 before := Painted()153 testing.SetRealm(testing.NewUserRealm(alice))154 testing.SetOriginSend(ugnot(3 * price))155 fund(3 * price)156 PaintBatch(cross(cur), "100,100,1;101,100,2;102,100,3;")157158 uassert.Equal(t, 1, Pixel(100, 100))159 uassert.Equal(t, 2, Pixel(101, 100))160 uassert.Equal(t, 3, Pixel(102, 100))161 uassert.Equal(t, before+3, Painted())162163 row := Rows(100, 101)164 uassert.Equal(t, Width, len(row))165 uassert.Equal(t, "123", row[100:103])166}167168func TestPaintBatchIsAtomic(cur realm, t *testing.T) {169 // One bad pixel rejects the whole batch before anything is written.170 testing.SetRealm(testing.NewUserRealm(alice))171 testing.SetOriginSend(ugnot(2*price))172 uassert.AbortsContains(t, cur, "out of range", func() { PaintBatch(cross(cur), "200,200,1;9999,0,1") })173 uassert.Equal(t, 0, Pixel(200, 200))174175 testing.SetRealm(testing.NewUserRealm(alice))176 testing.SetOriginSend(ugnot(price))177 uassert.AbortsContains(t, cur, "must send exactly", func() { PaintBatch(cross(cur), "200,200,1;201,200,1") })178 uassert.Equal(t, 0, Pixel(200, 200))179180 testing.SetRealm(testing.NewUserRealm(alice))181 testing.SetOriginSend(ugnot(price))182 uassert.AbortsContains(t, cur, "x,y,color", func() { PaintBatch(cross(cur), "garbage") })183 uassert.AbortsContains(t, cur, "x,y,color", func() { PaintBatch(cross(cur), "") })184}185186func TestPaintBatchTooLarge(cur realm, t *testing.T) {187 var sb strings.Builder188 for i := 0; i <= maxBatch; i++ {189 if i > 0 {190 sb.WriteByte(';')191 }192 sb.WriteString("0,")193 sb.WriteString(itoa(i))194 sb.WriteString(",1")195 }196 testing.SetRealm(testing.NewUserRealm(alice))197 testing.SetOriginSend(ugnot(int64(maxBatch+1)*price))198 uassert.AbortsContains(t, cur, "too many pixels", func() { PaintBatch(cross(cur), sb.String()) })199}200201func TestSetPrice(cur realm, t *testing.T) {202 testing.SetRealm(testing.NewUserRealm(alice))203 testing.SetOriginSend(nil)204 uassert.AbortsWithMessage(t, cur, "owner only", func() { SetPrice(cross(cur), 1) })205206 testing.SetRealm(testing.NewUserRealm(owner))207 testing.SetOriginSend(nil)208 uassert.AbortsWithMessage(t, cur, "price must not be negative", func() { SetPrice(cross(cur), -1) })209 SetPrice(cross(cur), 0)210 uassert.Equal(t, int64(0), Price())211212 // Free painting requires sending nothing.213 testing.SetRealm(testing.NewUserRealm(alice))214 testing.SetOriginSend(nil)215 Paint(cross(cur), 300, 300, 4)216 uassert.Equal(t, 4, Pixel(300, 300))217 testing.SetRealm(testing.NewUserRealm(alice))218 testing.SetOriginSend(ugnot(1))219 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 300, 301, 4) })220221 testing.SetRealm(testing.NewUserRealm(owner))222 testing.SetOriginSend(nil)223 SetPrice(cross(cur), DefaultPrice)224}225226func TestSetMaxBatch(cur realm, t *testing.T) {227 orig := MaxBatchSize()228229 testing.SetRealm(testing.NewUserRealm(alice))230 uassert.AbortsWithMessage(t, cur, "owner only", func() { SetMaxBatch(cross(cur), 100) })231232 testing.SetRealm(testing.NewUserRealm(owner))233 uassert.AbortsContains(t, cur, "max batch must be between", func() { SetMaxBatch(cross(cur), 0) })234 uassert.AbortsContains(t, cur, "max batch must be between", func() { SetMaxBatch(cross(cur), HardMaxBatch+1) })235236 SetMaxBatch(cross(cur), 3)237 uassert.Equal(t, 3, MaxBatchSize())238239 // The new limit is enforced by PaintBatch.240 testing.SetRealm(testing.NewUserRealm(alice))241 testing.SetOriginSend(ugnot(4 * price))242 fund(4 * price)243 uassert.AbortsContains(t, cur, "too many pixels", func() {244 PaintBatch(cross(cur), "0,500,1;1,500,1;2,500,1;3,500,1")245 })246247 testing.SetRealm(testing.NewUserRealm(owner))248 SetMaxBatch(cross(cur), orig)249 uassert.Equal(t, orig, MaxBatchSize())250}251252func TestWithdraw(cur realm, t *testing.T) {253 testing.IssueCoins(realmAddr, ugnot(1_000))254 ro := banker.NewReadonlyBanker()255 bobBefore := ro.GetCoin(bob, Denom)256257 testing.SetRealm(testing.NewUserRealm(alice))258 testing.SetOriginSend(nil)259 uassert.AbortsWithMessage(t, cur, "owner only", func() { Withdraw(cross(cur), alice, 1) })260261 testing.SetRealm(testing.NewUserRealm(owner))262 testing.SetOriginSend(nil)263 uassert.AbortsWithMessage(t, cur, "amount must be positive", func() { Withdraw(cross(cur), bob, 0) })264 uassert.AbortsWithMessage(t, cur, "invalid address", func() { Withdraw(cross(cur), address("nope"), 1) })265 Withdraw(cross(cur), bob, 600)266267 uassert.Equal(t, bobBefore+600, ro.GetCoin(bob, Denom))268 uassert.Equal(t, int64(400), ro.GetCoin(realmAddr, Denom))269 // Leave the realm empty for the tests that assert on it.270 Withdraw(cross(cur), bob, 400)271}272273func TestTransferOwnership(cur realm, t *testing.T) {274 original := owner275276 testing.SetRealm(testing.NewUserRealm(alice))277 testing.SetOriginSend(nil)278 uassert.AbortsWithMessage(t, cur, "owner only", func() { TransferOwnership(cross(cur), alice) })279280 testing.SetRealm(testing.NewUserRealm(original))281 testing.SetOriginSend(nil)282 uassert.AbortsWithMessage(t, cur, "invalid address", func() { TransferOwnership(cross(cur), address("nope")) })283 TransferOwnership(cross(cur), alice)284 uassert.Equal(t, alice, Owner())285286 testing.SetRealm(testing.NewUserRealm(alice))287 testing.SetOriginSend(nil)288 TransferOwnership(cross(cur), original)289 uassert.Equal(t, original, Owner())290}291292func TestVersionAndDirtyRows(cur realm, t *testing.T) {293 v := Version()294 uassert.Equal(t, "", DirtyRows(v))295296 testing.SetRealm(testing.NewUserRealm(alice))297 testing.SetOriginSend(ugnot(3 * price))298 fund(3 * price)299 PaintBatch(cross(cur), "0,700,1;1,700,2;0,701,3")300301 uassert.Equal(t, v+1, Version())302 uassert.Equal(t, "700,701", DirtyRows(v))303 uassert.Equal(t, "", DirtyRows(Version()))304305 // A second call on one of those rows moves only that row forward.306 testing.SetRealm(testing.NewUserRealm(bob))307 testing.SetOriginSend(ugnot(price))308 fund(price)309 Paint(cross(cur), 5, 701, 4)310 uassert.Equal(t, v+2, Version())311 uassert.Equal(t, "701", DirtyRows(v+1))312 uassert.Equal(t, "700,701", DirtyRows(v))313314 // A rejected paint changes nothing.315 testing.SetRealm(testing.NewUserRealm(bob))316 testing.SetOriginSend(ugnot(1))317 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 5, 702, 4) })318 uassert.Equal(t, v+2, Version())319 uassert.Equal(t, "", DirtyRows(v+2))320321 // Negative "since" behaves like zero: painted rows only, never blank ones.322 uassert.Equal(t, DirtyRows(0), DirtyRows(-1))323 uassert.True(t, !strings.Contains(","+DirtyRows(0)+",", ",799,"), "row 799 was never painted")324}325326func TestRender(t *testing.T) {327 out := Render("")328 uassert.True(t, strings.Contains(out, "# Million Gno"), "title")329 uassert.True(t, strings.Contains(out, "| Size | 1250 x 800 |"), "size row")330 uassert.True(t, strings.Contains(out, "| Price per pixel | 50000ugnot |"), "price row")331 uassert.True(t, strings.Contains(out, "| Admin | "+owner.String()+" |"), "admin row")332 uassert.True(t, strings.Contains(out, "$help&func=PaintBatch"), "help link")333}334335func itoa(n int) string {336 if n == 0 {337 return "0"338 }339 var b []byte340 for n > 0 {341 b = append([]byte{byte('0' + n%10)}, b...)342 n /= 10343 }344 return string(b)345}346DirtyRows(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.
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.