1package upkeep23import (4 "chain"5 "chain/banker"6 "strings"7 "testing"89 "gno.land/p/nt/testutils/v0"10 "gno.land/p/nt/uassert/v0"1112 subs "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions"13 guardian "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/timelock_guardian"14)1516var (17 testAdmin = testutils.TestAddress("ukadmin")18 funder = testutils.TestAddress("ukfunder")19 poker = testutils.TestAddress("ukpoker")20 mallory = testutils.TestAddress("ukmallory")21 provider = testutils.TestAddress("ukprov")22 subber = testutils.TestAddress("uksubber")23 towner = testutils.TestAddress("uktowner")24)2526// Harness facts, carried from the sibling suites: init runs with an27// empty origin (real deploys capture the signer); realm globals persist28// across tests while the banker resets; SetRealm binds to the CALLING29// frame only, so arming is inlined at every site; SkipHeights advances30// the clock 5s per height AND wipes the origin context.31func init() {32 admin = testAdmin33 sweeper = testAdmin34 self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upkeep")35}3637func ugnot(n int64) chain.Coins { return chain.NewCoins(chain.NewCoin(Denom, n)) }3839func coinsOf(a address) int64 { return banker.NewReadonlyBanker().GetCoin(a, Denom) }4041// conserved asserts the realm's monetary invariant: everything the realm42// holds is either unspent pot or earned-but-unclaimed balance.43func conserved(t *testing.T) {44 t.Helper()45 if Held() != Pot()+UsersTotal() {46 t.Fatalf("conservation violated: Held=%d pot=%d users=%d",47 Held(), Pot(), UsersTotal())48 }49}5051// syncHeld re-issues the realm's expected holdings. The harness banker52// resets between tests while realm globals (pot, ledger) persist, so53// each money-touching test starts by making the banker match the books.54func syncHeld() {55 want := Pot() + UsersTotal()56 have := coinsOf(Address())57 if want > have {58 testing.IssueCoins(Address(), ugnot(want-have))59 }60}6162func fundAs(cur realm, from address, amount int64) {63 testing.SetRealm(testing.NewUserRealm(from))64 testing.SetOriginCaller(from)65 testing.SetOriginSend(ugnot(amount))66 testing.IssueCoins(Address(), ugnot(amount))67 Fund(cross(cur))68}6970func adminSet(cur realm, task string, amount int64) {71 testing.SetRealm(testing.NewUserRealm(testAdmin))72 testing.SetOriginCaller(testAdmin)73 testing.SetOriginSend(nil)74 SetReward(cross(cur), task, amount)75}7677// expirableSub drives the REAL subscriptions realm to a state where one78// subscription is past its grace window, and returns its id. This is79// the downstream fixture for the realm-to-realm tests: nothing is80// mocked.81func expirableSub(cur realm, t *testing.T) int64 {82 t.Helper()83 testing.SetRealm(testing.NewUserRealm(provider))84 testing.SetOriginCaller(provider)85 testing.SetOriginSend(nil)86 planID := subs.CreatePlan(cross(cur), "upkeep-fixture", "d", 1, 10, 1000)8788 testing.SetRealm(testing.NewUserRealm(subber))89 testing.SetOriginCaller(subber)90 testing.SetOriginSend(ugnot(1))91 testing.IssueCoins(subs.Address(), ugnot(1))92 subID := subs.Subscribe(cross(cur), planID)9394 // paidThrough = h+10; expirable at paidThrough+10. Skip well past.95 testing.SkipHeights(25)96 return subID97}9899// maturedAction drives the REAL guardian realm to a matured timelocked100// action and returns its id.101var targetSeq int102103func maturedAction(cur realm, t *testing.T) string {104 t.Helper()105 targetSeq++106 name := "uktarget" + string(rune('a'+targetSeq%26))107 testing.SetRealm(testing.NewUserRealm(towner))108 testing.SetOriginCaller(towner)109 testing.SetOriginSend(nil)110 guardian.RegisterTarget(cross(cur), name, guardian.MinDelayFloor, "")111112 testing.SetRealm(testing.NewUserRealm(towner))113 testing.SetOriginCaller(towner)114 testing.SetOriginSend(nil)115 id := guardian.Schedule(cross(cur), name, "poke-fixture", guardian.MinDelayFloor)116117 testing.SkipHeights(13) // 65s at 5s/height >= the 60s floor118 return id119}120121func TestInitialState(t *testing.T) {122 if Admin() != testAdmin || Pot() != 0 || Pokes() != 0 || Funded() != 0 {123 t.Fatal("unexpected initial state")124 }125 if RewardFor(TaskSubExpire) != 0 || RewardFor(TaskTimelockExecute) != 0 {126 t.Fatal("rewards must default to 0")127 }128 conserved(t)129}130131func TestFundAndRewardConfig(cur realm, t *testing.T) {132 fundAs(cur, funder, 100000)133 if Pot() != 100000 || Funded() != 100000 {134 t.Fatalf("pot=%d funded=%d", Pot(), Funded())135 }136 conserved(t)137138 testing.SetRealm(testing.NewUserRealm(mallory))139 testing.SetOriginCaller(mallory)140 testing.SetOriginSend(nil)141 uassert.AbortsWithMessage(t, cur, "admin only", func() {142 SetReward(cross(cur), TaskSubExpire, 100)143 })144145 testing.SetRealm(testing.NewUserRealm(testAdmin))146 testing.SetOriginCaller(testAdmin)147 testing.SetOriginSend(nil)148 uassert.AbortsContains(t, cur, "reward must be in", func() {149 SetReward(cross(cur), TaskSubExpire, MaxReward+1)150 })151 uassert.AbortsContains(t, cur, "unknown task", func() {152 SetReward(cross(cur), "nosuch", 1)153 })154155 adminSet(cur, TaskSubExpire, 5000)156 adminSet(cur, TaskTimelockExecute, 7000)157 if RewardFor(TaskSubExpire) != 5000 || RewardFor(TaskTimelockExecute) != 7000 {158 t.Fatal("rewards not set")159 }160}161162// The realm-to-realm happy path: poke the REAL subscriptions realm.163func TestPokeExpire(cur realm, t *testing.T) {164 syncHeld()165 subID := expirableSub(cur, t)166167 // Downstream state before: still Active.168 _, _, _, _, _, _, status := subs.SubInfo(subID)169 if status != "active" {170 t.Fatalf("fixture status %q", status)171 }172173 potBefore, balBefore := Pot(), BalanceOf(poker)174 testing.SetRealm(testing.NewUserRealm(poker))175 testing.SetOriginCaller(poker)176 testing.SetOriginSend(nil)177 PokeExpire(cross(cur), subID)178179 // Downstream state changed by OUR realm's crossing call...180 _, _, _, _, _, _, status = subs.SubInfo(subID)181 if status != "expired" {182 t.Fatalf("downstream not expired: %q", status)183 }184 // ...and the reward moved pot -> caller balance atomically.185 if Pot() != potBefore-5000 || BalanceOf(poker) != balBefore+5000 {186 t.Fatalf("reward accounting wrong: pot %d->%d bal %d->%d",187 potBefore, Pot(), balBefore, BalanceOf(poker))188 }189 if Pokes() != 1 {190 t.Fatalf("pokes=%d", Pokes())191 }192 conserved(t)193}194195// Replay: the downstream realm is the replay guard; a second poke on the196// same subscription aborts THERE and no reward is paid HERE.197func TestPokeExpireReplay(cur realm, t *testing.T) {198 syncHeld()199 potBefore, balBefore := Pot(), BalanceOf(mallory)200 testing.SetRealm(testing.NewUserRealm(mallory))201 testing.SetOriginCaller(mallory)202 testing.SetOriginSend(nil)203 uassert.AbortsContains(t, cur, "subscription is expired", func() {204 PokeExpire(cross(cur), 0) // the sub expired in TestPokeExpire205 })206 if Pot() != potBefore || BalanceOf(mallory) != balBefore || Pokes() != 1 {207 t.Fatal("aborted poke mutated reward state")208 }209 conserved(t)210}211212// A premature poke aborts downstream ("not expirable") and pays nothing.213func TestPokeExpirePremature(cur realm, t *testing.T) {214 syncHeld()215 testing.SetRealm(testing.NewUserRealm(provider))216 testing.SetOriginCaller(provider)217 testing.SetOriginSend(nil)218 planID := subs.CreatePlan(cross(cur), "fresh", "d", 1, 5000, 1000)219 testing.SetRealm(testing.NewUserRealm(subber))220 testing.SetOriginCaller(subber)221 testing.SetOriginSend(ugnot(1))222 testing.IssueCoins(subs.Address(), ugnot(1))223 freshID := subs.Subscribe(cross(cur), planID)224225 potBefore := Pot()226 testing.SetRealm(testing.NewUserRealm(poker))227 testing.SetOriginCaller(poker)228 testing.SetOriginSend(nil)229 uassert.AbortsContains(t, cur, "not expirable", func() {230 PokeExpire(cross(cur), freshID)231 })232 uassert.AbortsContains(t, cur, "unknown subscription id", func() {233 PokeExpire(cross(cur), 99999)234 })235 if Pot() != potBefore {236 t.Fatal("failed pokes touched the pot")237 }238 conserved(t)239}240241// The second realm-to-realm integration: the REAL guardian realm.242func TestPokeExecute(cur realm, t *testing.T) {243 syncHeld()244 id := maturedAction(cur, t)245 potBefore, balBefore := Pot(), BalanceOf(poker)246247 testing.SetRealm(testing.NewUserRealm(poker))248 testing.SetOriginCaller(poker)249 testing.SetOriginSend(nil)250 PokeExecute(cross(cur), id)251252 if Pot() != potBefore-7000 || BalanceOf(poker) != balBefore+7000 {253 t.Fatal("timelock reward accounting wrong")254 }255 // Replay refused downstream.256 testing.SetRealm(testing.NewUserRealm(mallory))257 testing.SetOriginCaller(mallory)258 testing.SetOriginSend(nil)259 uassert.AbortsContains(t, cur, "already executed", func() {260 PokeExecute(cross(cur), id)261 })262 uassert.AbortsContains(t, cur, "action not found", func() {263 PokeExecute(cross(cur), "tl-nosuch")264 })265 // Premature: schedule a new one, poke before maturity.266 testing.SetRealm(testing.NewUserRealm(towner))267 testing.SetOriginCaller(towner)268 testing.SetOriginSend(nil)269 guardian.RegisterTarget(cross(cur), "ukearly", guardian.MinDelayFloor, "")270 testing.SetRealm(testing.NewUserRealm(towner))271 testing.SetOriginCaller(towner)272 testing.SetOriginSend(nil)273 early := guardian.Schedule(cross(cur), "ukearly", "x", guardian.MinDelayFloor)274 testing.SetRealm(testing.NewUserRealm(poker))275 testing.SetOriginCaller(poker)276 testing.SetOriginSend(nil)277 uassert.AbortsContains(t, cur, "too early", func() {278 PokeExecute(cross(cur), early)279 })280 conserved(t)281}282283// Pot exhaustion refuses BEFORE the downstream call: no valve fires284// unrewarded through this realm. Entering here the pot is deterministic:285// 100000 funded minus the 5000 and 7000 awarded above = 88000, so four286// MaxReward pokes leave 8000 < MaxReward and the next must refuse.287func TestPotExhaustion(cur realm, t *testing.T) {288 syncHeld()289 adminSet(cur, TaskSubExpire, MaxReward)290291 // Drain by REAL pokes until the pot no longer covers the reward.292 for i := 0; Pot() >= MaxReward; i++ {293 if i >= 10 {294 t.Fatalf("fixture arithmetic broken: pot=%d after %d pokes", Pot(), i)295 }296 id := expirableSub(cur, t)297 testing.SetRealm(testing.NewUserRealm(poker))298 testing.SetOriginCaller(poker)299 testing.SetOriginSend(nil)300 PokeExpire(cross(cur), id)301 }302303 short := expirableSub(cur, t)304 potBefore := Pot()305 testing.SetRealm(testing.NewUserRealm(poker))306 testing.SetOriginCaller(poker)307 testing.SetOriginSend(nil)308 uassert.AbortsContains(t, cur, "reward pot cannot cover", func() {309 PokeExpire(cross(cur), short)310 })311 // Downstream MUST be untouched by the refused poke.312 _, _, _, _, _, _, status := subs.SubInfo(short)313 if status != "active" {314 t.Fatal("refused poke reached the downstream realm")315 }316 if Pot() != potBefore {317 t.Fatal("refused poke touched the pot")318 }319 conserved(t)320 adminSet(cur, TaskSubExpire, 5000) // restore for the later tests321}322323// The refusal boundary, deterministic: unknown task, zero reward, and324// pot-short all refuse before any downstream effect.325func TestPokeRefusals(cur realm, t *testing.T) {326 adminSet(cur, TaskSubExpire, 0)327 testing.SetRealm(testing.NewUserRealm(poker))328 testing.SetOriginCaller(poker)329 testing.SetOriginSend(nil)330 uassert.AbortsContains(t, cur, "no reward configured", func() {331 PokeExpire(cross(cur), 0)332 })333 adminSet(cur, TaskSubExpire, 5000)334335 testing.SetRealm(testing.NewUserRealm(poker))336 testing.SetOriginCaller(poker)337 testing.SetOriginSend(ugnot(5))338 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {339 PokeExpire(cross(cur), 0)340 })341 testing.SetRealm(testing.NewUserRealm(poker))342 testing.SetOriginCaller(poker)343 testing.SetOriginSend(ugnot(5))344 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {345 PokeExecute(cross(cur), "x")346 })347}348349func TestClaims(cur realm, t *testing.T) {350 syncHeld()351 earned := BalanceOf(poker)352 if earned == 0 {353 t.Fatal("fixture: poker should have earnings")354 }355 before := coinsOf(poker)356 testing.SetRealm(testing.NewUserRealm(poker))357 testing.SetOriginCaller(poker)358 testing.SetOriginSend(nil)359 ClaimAll(cross(cur))360 if coinsOf(poker)-before != earned || BalanceOf(poker) != 0 {361 t.Fatal("claim did not move the earned balance")362 }363 testing.SetRealm(testing.NewUserRealm(mallory))364 testing.SetOriginCaller(mallory)365 testing.SetOriginSend(nil)366 uassert.AbortsWithMessage(t, cur, "nothing to claim", func() {367 ClaimAll(cross(cur))368 })369 uassert.AbortsContains(t, cur, "insufficient balance", func() {370 Claim(cross(cur), 1)371 })372 conserved(t)373}374375func TestSweepReserve(cur realm, t *testing.T) {376 syncHeld()377 testing.SetRealm(testing.NewUserRealm(testAdmin))378 testing.SetOriginCaller(testAdmin)379 testing.SetOriginSend(nil)380 uassert.AbortsContains(t, cur, "no surplus to sweep", func() {381 SweepDenom(cross(cur), Denom)382 })383 testing.IssueCoins(Address(), ugnot(77))384 reserveBefore := Pot() + UsersTotal()385 got := coinsOf(testAdmin)386 testing.SetRealm(testing.NewUserRealm(testAdmin))387 testing.SetOriginCaller(testAdmin)388 testing.SetOriginSend(nil)389 SweepDenom(cross(cur), Denom)390 if coinsOf(testAdmin)-got != 77 {391 t.Fatal("sweep amount wrong")392 }393 if Pot()+UsersTotal() != reserveBefore {394 t.Fatal("sweep touched the reserve")395 }396 testing.SetRealm(testing.NewUserRealm(mallory))397 testing.SetOriginCaller(mallory)398 testing.SetOriginSend(nil)399 uassert.AbortsWithMessage(t, cur, "sweeper only", func() {400 SweepDenom(cross(cur), Denom)401 })402 conserved(t)403}404405func TestAdminHandover(cur realm, t *testing.T) {406 testing.SetRealm(testing.NewUserRealm(testAdmin))407 testing.SetOriginCaller(testAdmin)408 testing.SetOriginSend(nil)409 TransferAdmin(cross(cur), funder)410 testing.SetRealm(testing.NewUserRealm(mallory))411 testing.SetOriginCaller(mallory)412 testing.SetOriginSend(nil)413 uassert.AbortsWithMessage(t, cur, "only the staged successor may accept", func() {414 AcceptAdmin(cross(cur))415 })416 testing.SetRealm(testing.NewUserRealm(funder))417 testing.SetOriginCaller(funder)418 testing.SetOriginSend(nil)419 AcceptAdmin(cross(cur))420 if Admin() != funder {421 t.Fatal("authority did not move")422 }423 // Former admin locked out; sweeper moved with admin.424 testing.SetRealm(testing.NewUserRealm(testAdmin))425 testing.SetOriginCaller(testAdmin)426 testing.SetOriginSend(nil)427 uassert.AbortsWithMessage(t, cur, "admin only", func() {428 SetReward(cross(cur), TaskSubExpire, 1)429 })430 uassert.AbortsWithMessage(t, cur, "sweeper only", func() {431 SweepDenom(cross(cur), Denom)432 })433 // Hand back.434 testing.SetRealm(testing.NewUserRealm(funder))435 testing.SetOriginCaller(funder)436 testing.SetOriginSend(nil)437 TransferAdmin(cross(cur), testAdmin)438 testing.SetRealm(testing.NewUserRealm(testAdmin))439 testing.SetOriginCaller(testAdmin)440 testing.SetOriginSend(nil)441 AcceptAdmin(cross(cur))442}443444func TestRender(t *testing.T) {445 out := Render("")446 if !strings.Contains(out, "sub_expire") || !strings.Contains(out, "timelock_execute") {447 t.Fatal("render missing task lines")448 }449 if Render("x") == Render("") {450 t.Fatal("unknown path fell through")451 }452}453AcceptAdmin(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})
Address() string
Admin() string
BalanceOf(a string) int64
Claim(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}, amount int64)
ClaimAll(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})
Fund(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})
Funded() int64
Held() int64
PendingAdmin() string
PokeExecute(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}, actionID string)
PokeExpire(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}, subID int64)
Pokes() int64
Pot() int64
Render(path string) string
RewardFor(task string) int64
SetReward(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}, task string, amount int64)
SweepDenom(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}, denom string)
TransferAdmin(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}, successor string)
UsersTotal() 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.