1package subscriptions23import (4 "chain"5 "chain/banker"6 "chain/runtime"7 "strings"8 "testing"910 "gno.land/p/nt/testutils/v0"11 "gno.land/p/nt/uassert/v0"12)1314var (15 testAdmin = testutils.TestAddress("subadmin")16 prov1 = testutils.TestAddress("subprov1")17 prov2 = testutils.TestAddress("subprov2")18 alice = testutils.TestAddress("subalice")19 bob = testutils.TestAddress("subbob")20 mallory = testutils.TestAddress("submallory")21)2223// Established harness facts, carried from the sibling realms' suites:24// init runs with an empty origin context (the real deploy captures the25// origin caller); the banker resets between tests while realm globals26// persist, so conservation is asserted in DELTA form after re-seeding27// carried liabilities; testing.SetRealm affects only the frame that28// calls it; and testing.SkipHeights WIPES the origin/realm context, so29// every caller is re-armed after a skip.30func init() {31 admin = testAdmin32 feeRecipient = testAdmin33 self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions")34}3536type baseline struct{ held, users, fees int64 }3738func begin() baseline {39 if l := Liabilities(); l > 0 {40 testing.IssueCoins(Address(), ugnot(l))41 }42 return snap()43}4445func snap() baseline {46 return baseline{held: Held(), users: UsersTotal(), fees: FeesAccrued()}47}4849// conserved asserts ΔH == ΔU + ΔF + extraSurplus — the realm's monetary50// invariant in delta form. There is no escrow pool in this realm.51func (b baseline) conserved(t *testing.T, extraSurplus int64) {52 t.Helper()53 dH := Held() - b.held54 dU := UsersTotal() - b.users55 dF := FeesAccrued() - b.fees56 if dH != dU+dF+extraSurplus {57 t.Fatalf("conservation violated: ΔH=%d ΔU=%d ΔF=%d ΔS=%d", dH, dU, dF, extraSurplus)58 }59 if Liabilities() != UsersTotal()+FeesAccrued() {60 t.Fatalf("Liabilities() = %d, want U+F", Liabilities())61 }62}6364func coinsOf(addr address) int64 {65 return banker.NewReadonlyBanker().GetCoin(addr, Denom)66}6768func ugnot(n int64) chain.Coins {69 return chain.NewCoins(chain.NewCoin(Denom, n))70}7172// NOTE ON THE HARNESS. testing.SetRealm applies to the frame that CALLS73// it, so the caller identity must be armed in the SAME frame as the74// entrypoint invocation — the three lines are inlined at every site,75// matching the sibling suites.7677// planAs publishes a plan (non-payable: envelope explicitly cleared).78func planAs(cur realm, from address, title string, price, period int64) int64 {79 testing.SetRealm(testing.NewUserRealm(from))80 testing.SetOriginCaller(from)81 testing.SetOriginSend(nil)82 return CreatePlan(cross(cur), title, "d", price, period, MaxFeeBps)83}8485// subscribeAs performs a chain-faithful subscribe: envelope staged AND landed.86func subscribeAs(cur realm, from address, planID, pay int64) int64 {87 testing.SetRealm(testing.NewUserRealm(from))88 testing.SetOriginCaller(from)89 testing.SetOriginSend(ugnot(pay))90 testing.IssueCoins(Address(), ugnot(pay))91 return Subscribe(cross(cur), planID)92}9394// renewAs performs a chain-faithful renewal.95func renewAs(cur realm, from address, subID, pay int64) {96 testing.SetRealm(testing.NewUserRealm(from))97 testing.SetOriginCaller(from)98 testing.SetOriginSend(ugnot(pay))99 testing.IssueCoins(Address(), ugnot(pay))100 Renew(cross(cur), subID)101}102103func TestInitialConfig(t *testing.T) {104 if Admin() != testAdmin || FeeRecipient() != testAdmin {105 t.Fatal("seeded roles mismatch")106 }107 var zero address108 if PendingAdmin() != zero {109 t.Fatal("pending admin not zero")110 }111 if FeeBps() != 0 || NumPlans() != 0 || NumSubs() != 0 {112 t.Fatal("initial counters not zero")113 }114}115116func TestCreatePlanValidation(cur realm, t *testing.T) {117 testing.SetRealm(testing.NewUserRealm(prov1))118 testing.SetOriginCaller(prov1)119 testing.SetOriginSend(ugnot(5))120 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {121 CreatePlan(cross(cur), "x", "d", 1, MinPeriodBlocks, MaxFeeBps)122 })123124 testing.SetRealm(testing.NewUserRealm(prov1))125 testing.SetOriginCaller(prov1)126 testing.SetOriginSend(nil)127 uassert.AbortsContains(t, cur, "title must be", func() {128 CreatePlan(cross(cur), "", "d", 1, MinPeriodBlocks, MaxFeeBps)129 })130 uassert.AbortsContains(t, cur, "title must be", func() {131 CreatePlan(cross(cur), strings.Repeat("t", MaxTitleLen+1), "d", 1, MinPeriodBlocks, MaxFeeBps)132 })133 uassert.AbortsContains(t, cur, "description too long", func() {134 CreatePlan(cross(cur), "x", strings.Repeat("d", MaxDescLen+1), 1, MinPeriodBlocks, MaxFeeBps)135 })136 uassert.AbortsContains(t, cur, "price must be at least", func() {137 CreatePlan(cross(cur), "x", "d", 0, MinPeriodBlocks, MaxFeeBps)138 })139 uassert.AbortsContains(t, cur, "periodBlocks must be in", func() {140 CreatePlan(cross(cur), "x", "d", 1, MinPeriodBlocks-1, MaxFeeBps)141 })142 uassert.AbortsContains(t, cur, "periodBlocks must be in", func() {143 CreatePlan(cross(cur), "x", "d", 1, MaxPeriodBlocks+1, MaxFeeBps)144 })145}146147func TestFeeConsentCeiling(cur realm, t *testing.T) {148 testing.SetRealm(testing.NewUserRealm(testAdmin))149 testing.SetOriginCaller(testAdmin)150 testing.SetOriginSend(nil)151 SetFeeBps(cross(cur), 250)152153 // A provider who signed for at most 100 bps is refused while the154 // fee is 250.155 testing.SetRealm(testing.NewUserRealm(prov1))156 testing.SetOriginCaller(prov1)157 testing.SetOriginSend(nil)158 uassert.AbortsContains(t, cur, "exceeds the caller's maximum", func() {159 CreatePlan(cross(cur), "capped", "d", 100, MinPeriodBlocks, 100)160 })161162 // A consenting provider snapshots 250 into the plan; a later fee163 // change does not touch it.164 id := planAs(cur, prov1, "snap", 1000, MinPeriodBlocks)165 testing.SetRealm(testing.NewUserRealm(testAdmin))166 testing.SetOriginCaller(testAdmin)167 testing.SetOriginSend(nil)168 SetFeeBps(cross(cur), 0)169 _, _, _, _, planFee, _, _ := PlanInfo(id)170 if planFee != 250 {171 t.Fatalf("plan feeBps = %d, want the 250 snapshot", planFee)172 }173174 // The subscription charges the SNAPSHOT, not the current fee.175 b := begin()176 subscribeAs(cur, alice, id, 1000)177 if got := FeesAccrued() - b.fees; got != 25 {178 t.Fatalf("fee charged = %d, want 25 (250 bps of 1000)", got)179 }180 if got := BalanceOf(prov1); got == 0 {181 t.Fatal("provider not credited")182 }183 b.conserved(t, 0)184}185186func TestPlanQuota(cur realm, t *testing.T) {187 for i := 0; i < MaxPlansPerProvider; i++ {188 planAs(cur, prov2, "q", 1, MinPeriodBlocks)189 }190 testing.SetRealm(testing.NewUserRealm(prov2))191 testing.SetOriginCaller(prov2)192 testing.SetOriginSend(nil)193 uassert.AbortsWithMessage(t, cur, "per-provider plan limit reached", func() {194 CreatePlan(cross(cur), "q", "d", 1, MinPeriodBlocks, MaxFeeBps)195 })196 // Retiring one frees the slot.197 RetirePlan(cross(cur), NumPlans()-1)198 planAs(cur, prov2, "q2", 1, MinPeriodBlocks)199}200201func TestRetirePlanAuthority(cur realm, t *testing.T) {202 id := planAs(cur, prov1, "mine", 5, MinPeriodBlocks)203 testing.SetRealm(testing.NewUserRealm(mallory))204 testing.SetOriginCaller(mallory)205 testing.SetOriginSend(nil)206 uassert.AbortsWithMessage(t, cur, "only the provider may retire a plan", func() {207 RetirePlan(cross(cur), id)208 })209 testing.SetRealm(testing.NewUserRealm(prov1))210 testing.SetOriginCaller(prov1)211 testing.SetOriginSend(nil)212 RetirePlan(cross(cur), id)213 uassert.AbortsWithMessage(t, cur, "plan is not active", func() {214 RetirePlan(cross(cur), id)215 })216 // No new subscriptions on a retired plan.217 testing.SetRealm(testing.NewUserRealm(alice))218 testing.SetOriginCaller(alice)219 testing.SetOriginSend(ugnot(5))220 testing.IssueCoins(Address(), ugnot(5))221 uassert.AbortsWithMessage(t, cur, "plan is not active", func() {222 Subscribe(cross(cur), id)223 })224}225226func TestSubscribeLifecycle(cur realm, t *testing.T) {227 id := planAs(cur, prov1, "basic", 100, 50)228 b := begin()229 h0 := runtime.ChainHeight()230 subID := subscribeAs(cur, alice, id, 100)231232 planID, subscriber, provider, price, paidThrough, periods, status := SubInfo(subID)233 if planID != id || subscriber != alice || provider != prov1 || price != 100 ||234 periods != 1 || status != SubActive {235 t.Fatal("subscription fields wrong")236 }237 if paidThrough != h0+50 {238 t.Fatalf("paidThrough = %d, want %d", paidThrough, h0+50)239 }240 if !Entitled(subID) || !EntitledFor(id, alice) {241 t.Fatal("fresh subscription not entitled")242 }243 if got, ok := ActiveSubID(id, alice); !ok || got != subID {244 t.Fatal("active index wrong")245 }246 b.conserved(t, 0)247248 // Exact envelope: under- and overpayment refused.249 for _, bad := range []int64{99, 101} {250 testing.SetRealm(testing.NewUserRealm(bob))251 testing.SetOriginCaller(bob)252 testing.SetOriginSend(ugnot(bad))253 testing.IssueCoins(Address(), ugnot(bad))254 uassert.AbortsContains(t, cur, "send exactly the plan price", func() {255 Subscribe(cross(cur), id)256 })257 }258259 // A second live subscription to the same plan is refused — the260 // identity-level duplicate-payment guard.261 testing.SetRealm(testing.NewUserRealm(alice))262 testing.SetOriginCaller(alice)263 testing.SetOriginSend(ugnot(100))264 testing.IssueCoins(Address(), ugnot(100))265 uassert.AbortsContains(t, cur, "renew it instead", func() {266 Subscribe(cross(cur), id)267 })268}269270func TestRenewalWindow(cur realm, t *testing.T) {271 id := planAs(cur, prov1, "window", 100, 50)272 subID := subscribeAs(cur, bob, id, 100)273 _, _, _, _, pt0, _, _ := SubInfo(subID)274275 from, until := RenewableFrom(subID)276 if from != pt0-50 || until != pt0+49 {277 t.Fatalf("window [%d,%d], want [%d,%d]", from, until, pt0-50, pt0+49)278 }279280 // Straight after Subscribe, remaining entitlement equals exactly281 // one period (the running one), so ONE prepay renewal is inside282 // the early bound — and extension is FROM paidThrough.283 b := begin()284 renewAs(cur, bob, subID, 100)285 _, _, _, _, pt1, periods, _ := SubInfo(subID)286 if pt1 != pt0+50 || periods != 2 {287 t.Fatalf("paidThrough = %d periods = %d, want %d and 2", pt1, periods, pt0+50)288 }289 b.conserved(t, 0)290291 // A SECOND renewal in the same block is the accidental duplicate,292 // and the early bound refuses it: more than one unstarted period293 // would be prepaid.294 testing.SetRealm(testing.NewUserRealm(bob))295 testing.SetOriginCaller(bob)296 testing.SetOriginSend(ugnot(100))297 testing.IssueCoins(Address(), ugnot(100))298 uassert.AbortsContains(t, cur, "too early", func() {299 Renew(cross(cur), subID)300 })301302 // Wrong payer: a third party cannot extend someone else's303 // subscription even inside the window.304 testing.SkipHeights(50)305 testing.SetRealm(testing.NewUserRealm(mallory))306 testing.SetOriginCaller(mallory)307 testing.SetOriginSend(ugnot(100))308 testing.IssueCoins(Address(), ugnot(100))309 uassert.AbortsWithMessage(t, cur, "only the subscriber may renew", func() {310 Renew(cross(cur), subID)311 })312313 // Wrong amount inside the window.314 testing.SetRealm(testing.NewUserRealm(bob))315 testing.SetOriginCaller(bob)316 testing.SetOriginSend(ugnot(99))317 testing.IssueCoins(Address(), ugnot(99))318 uassert.AbortsContains(t, cur, "send exactly the subscription price", func() {319 Renew(cross(cur), subID)320 })321}322323func TestGraceBoundary(cur realm, t *testing.T) {324 id := planAs(cur, prov1, "grace", 100, 50)325 subID := subscribeAs(cur, alice, id, 100)326 _, _, _, _, pt, _, _ := SubInfo(subID)327328 // Move to the LAST renewable height: h == paidThrough +329 // periodBlocks - 1 (the late bound is exclusive — audit Y2).330 testing.SkipHeights(pt + 49 - runtime.ChainHeight())331 if Entitled(subID) {332 t.Fatal("entitlement must have lapsed past paidThrough")333 }334 // Renewal at the boundary is accepted, extends FROM paidThrough —335 // and buys at least one block: the Y2 regression.336 renewAs(cur, alice, subID, 100)337 _, _, _, _, pt1, _, _ := SubInfo(subID)338 if pt1 != pt+50 {339 t.Fatalf("paidThrough = %d, want %d (extension from paidThrough)", pt1, pt+50)340 }341 if !Entitled(subID) {342 t.Fatal("a boundary renewal must buy at least one block of entitlement")343 }344345 // At the grace end itself renewal is refused, Expire opens: the346 // two sets partition exactly.347 testing.SkipHeights(pt1 + 50 - runtime.ChainHeight())348 testing.SetRealm(testing.NewUserRealm(alice))349 testing.SetOriginCaller(alice)350 testing.SetOriginSend(ugnot(100))351 testing.IssueCoins(Address(), ugnot(100))352 uassert.AbortsContains(t, cur, "grace window over", func() {353 Renew(cross(cur), subID)354 })355356 // Expire is permissionless: mallory may.357 testing.SetRealm(testing.NewUserRealm(mallory))358 testing.SetOriginCaller(mallory)359 testing.SetOriginSend(nil)360 Expire(cross(cur), subID)361 _, _, _, _, _, _, status := SubInfo(subID)362 if status != SubExpired {363 t.Fatal("not expired")364 }365 if EntitledFor(id, alice) {366 t.Fatal("expired subscription still indexed")367 }368 // Terminal: renewal refused by status.369 testing.SetRealm(testing.NewUserRealm(alice))370 testing.SetOriginCaller(alice)371 testing.SetOriginSend(ugnot(100))372 testing.IssueCoins(Address(), ugnot(100))373 uassert.AbortsContains(t, cur, "subscription is expired", func() {374 Renew(cross(cur), subID)375 })376 // The slot is free: a fresh Subscribe works.377 subscribeAs(cur, alice, id, 100)378}379380func TestExpireGuards(cur realm, t *testing.T) {381 id := planAs(cur, prov1, "expguard", 100, 50)382 subID := subscribeAs(cur, bob, id, 100)383 testing.SetRealm(testing.NewUserRealm(mallory))384 testing.SetOriginCaller(mallory)385 testing.SetOriginSend(nil)386 uassert.AbortsContains(t, cur, "not expirable", func() {387 Expire(cross(cur), subID)388 })389 // One block before the grace end it is still not expirable...390 _, _, _, _, pt, _, _ := SubInfo(subID)391 testing.SkipHeights(pt + 49 - runtime.ChainHeight())392 testing.SetRealm(testing.NewUserRealm(mallory))393 testing.SetOriginCaller(mallory)394 testing.SetOriginSend(nil)395 uassert.AbortsContains(t, cur, "not expirable", func() {396 Expire(cross(cur), subID)397 })398 // ...and AT the grace end it is: the sets partition exactly.399 testing.SkipHeights(1)400 testing.SetRealm(testing.NewUserRealm(mallory))401 testing.SetOriginCaller(mallory)402 testing.SetOriginSend(nil)403 Expire(cross(cur), subID)404 testing.SetRealm(testing.NewUserRealm(mallory))405 testing.SetOriginCaller(mallory)406 testing.SetOriginSend(nil)407 uassert.AbortsContains(t, cur, "subscription is expired", func() {408 Expire(cross(cur), subID)409 })410}411412func TestSubscribeCollapsesExpiredIncumbent(cur realm, t *testing.T) {413 id := planAs(cur, prov1, "collapse", 100, 50)414 oldID := subscribeAs(cur, alice, id, 100)415 _, _, _, _, pt, _, _ := SubInfo(oldID)416 testing.SkipHeights(pt + 50 - runtime.ChainHeight())417418 // No Expire call happened; Subscribe collapses the incumbent419 // itself, at the first expirable height.420 newID := subscribeAs(cur, alice, id, 100)421 if newID == oldID {422 t.Fatal("expected a fresh subscription id")423 }424 _, _, _, _, _, _, oldStatus := SubInfo(oldID)425 if oldStatus != SubExpired {426 t.Fatalf("incumbent status = %q, want expired", oldStatus)427 }428 if got, ok := ActiveSubID(id, alice); !ok || got != newID {429 t.Fatal("index does not point at the fresh subscription")430 }431}432433func TestCancel(cur realm, t *testing.T) {434 id := planAs(cur, prov1, "cancel", 100, 50)435 subID := subscribeAs(cur, bob, id, 100)436437 testing.SetRealm(testing.NewUserRealm(mallory))438 testing.SetOriginCaller(mallory)439 testing.SetOriginSend(nil)440 uassert.AbortsWithMessage(t, cur, "only the subscriber may cancel", func() {441 Cancel(cross(cur), subID)442 })443444 testing.SetRealm(testing.NewUserRealm(bob))445 testing.SetOriginCaller(bob)446 testing.SetOriginSend(nil)447 Cancel(cross(cur), subID)448 _, _, _, _, _, _, status := SubInfo(subID)449 if status != SubCancelled {450 t.Fatal("not cancelled")451 }452 // Paid entitlement survives cancellation until paidThrough — on453 // both surfaces (audit Y1)...454 if !Entitled(subID) || !EntitledFor(id, bob) {455 t.Fatal("cancelled subscription lost its paid entitlement")456 }457 // ...but the ACTIVE slot is gone and renewal is refused forever.458 if _, ok := ActiveSubID(id, bob); ok {459 t.Fatal("cancelled subscription still holds the active slot")460 }461 testing.SetRealm(testing.NewUserRealm(bob))462 testing.SetOriginCaller(bob)463 testing.SetOriginSend(ugnot(100))464 testing.IssueCoins(Address(), ugnot(100))465 uassert.AbortsContains(t, cur, "subscription is cancelled", func() {466 Renew(cross(cur), subID)467 })468 testing.SetRealm(testing.NewUserRealm(bob))469 testing.SetOriginCaller(bob)470 testing.SetOriginSend(nil)471 uassert.AbortsContains(t, cur, "subscription is cancelled", func() {472 Cancel(cross(cur), subID)473 })474 // The slot freed: bob may start a fresh subscription at once.475 subscribeAs(cur, bob, id, 100)476}477478func TestRenewRefusedOnRetiredPlan(cur realm, t *testing.T) {479 id := planAs(cur, prov1, "retire-mid", 100, 50)480 subID := subscribeAs(cur, alice, id, 100)481 testing.SetRealm(testing.NewUserRealm(prov1))482 testing.SetOriginCaller(prov1)483 testing.SetOriginSend(nil)484 RetirePlan(cross(cur), id)485 // Entitlement continues...486 if !Entitled(subID) {487 t.Fatal("entitlement must survive plan retirement")488 }489 // ...but renewal is refused even inside the window.490 _, _, _, _, pt, _, _ := SubInfo(subID)491 testing.SkipHeights(pt - runtime.ChainHeight())492 testing.SetRealm(testing.NewUserRealm(alice))493 testing.SetOriginCaller(alice)494 testing.SetOriginSend(ugnot(100))495 testing.IssueCoins(Address(), ugnot(100))496 uassert.AbortsContains(t, cur, "plan is retired", func() {497 Renew(cross(cur), subID)498 })499}500501func TestSubscriberQuota(cur realm, t *testing.T) {502 // Fill carol's quota with one-plan-each subscriptions, then refuse.503 // A dedicated provider: prov2's plan quota is already saturated by504 // TestPlanQuota, and realm globals persist across tests.505 carol := testutils.TestAddress("subcarol")506 provQ := testutils.TestAddress("subprovq")507 var lastPlan int64508 for i := 0; i < MaxSubsPerSubscriber; i++ {509 lastPlan = planAs(cur, provQ, "quota", 1, MinPeriodBlocks)510 subscribeAs(cur, carol, lastPlan, 1)511 // Free the provider quota slot for the next plan.512 testing.SetRealm(testing.NewUserRealm(provQ))513 testing.SetOriginCaller(provQ)514 testing.SetOriginSend(nil)515 RetirePlan(cross(cur), lastPlan)516 }517 extra := planAs(cur, provQ, "quota-x", 1, MinPeriodBlocks)518 testing.SetRealm(testing.NewUserRealm(carol))519 testing.SetOriginCaller(carol)520 testing.SetOriginSend(ugnot(1))521 testing.IssueCoins(Address(), ugnot(1))522 uassert.AbortsWithMessage(t, cur, "per-subscriber subscription limit reached", func() {523 Subscribe(cross(cur), extra)524 })525 // Cancelling one frees a slot.526 sid, ok := ActiveSubID(lastPlan, carol)527 if !ok {528 t.Fatal("expected an active sub to free")529 }530 testing.SetRealm(testing.NewUserRealm(carol))531 testing.SetOriginCaller(carol)532 testing.SetOriginSend(nil)533 Cancel(cross(cur), sid)534 subscribeAs(cur, carol, extra, 1)535 testing.SetRealm(testing.NewUserRealm(provQ))536 testing.SetOriginCaller(provQ)537 testing.SetOriginSend(nil)538 RetirePlan(cross(cur), extra)539}540541func TestClaimsAndFees(cur realm, t *testing.T) {542 testing.SetRealm(testing.NewUserRealm(testAdmin))543 testing.SetOriginCaller(testAdmin)544 testing.SetOriginSend(nil)545 SetFeeBps(cross(cur), 500) // 5% into the next plan546 id := planAs(cur, prov1, "money", 1000, 50)547 testing.SetRealm(testing.NewUserRealm(testAdmin))548 testing.SetOriginCaller(testAdmin)549 testing.SetOriginSend(nil)550 SetFeeBps(cross(cur), 0)551552 b := begin()553 subscribeAs(cur, alice, id, 1000)554 if FeesAccrued()-b.fees != 50 {555 t.Fatalf("fee = %d, want 50", FeesAccrued()-b.fees)556 }557 credited := BalanceOf(prov1)558559 // Mallory has no balance: claims fail.560 testing.SetRealm(testing.NewUserRealm(mallory))561 testing.SetOriginCaller(mallory)562 testing.SetOriginSend(nil)563 uassert.AbortsContains(t, cur, "insufficient balance", func() { Claim(cross(cur), 1) })564 testing.SetRealm(testing.NewUserRealm(mallory))565 testing.SetOriginCaller(mallory)566 testing.SetOriginSend(nil)567 uassert.AbortsWithMessage(t, cur, "nothing to claim", func() { ClaimAll(cross(cur)) })568569 // Provider claims all; coins actually move.570 before := coinsOf(prov1)571 testing.SetRealm(testing.NewUserRealm(prov1))572 testing.SetOriginCaller(prov1)573 testing.SetOriginSend(nil)574 ClaimAll(cross(cur))575 if coinsOf(prov1)-before != credited {576 t.Fatalf("provider received %d, want %d", coinsOf(prov1)-before, credited)577 }578 if BalanceOf(prov1) != 0 {579 t.Fatal("balance not drained")580 }581582 // Fees: only the recipient, and only the pot.583 testing.SetRealm(testing.NewUserRealm(mallory))584 testing.SetOriginCaller(mallory)585 testing.SetOriginSend(nil)586 uassert.AbortsWithMessage(t, cur, "fee recipient only", func() { WithdrawFees(cross(cur)) })587 potBefore := coinsOf(testAdmin)588 testing.SetRealm(testing.NewUserRealm(testAdmin))589 testing.SetOriginCaller(testAdmin)590 testing.SetOriginSend(nil)591 WithdrawFees(cross(cur))592 if coinsOf(testAdmin)-potBefore == 0 {593 t.Fatal("fee pot did not move")594 }595 testing.SetRealm(testing.NewUserRealm(testAdmin))596 testing.SetOriginCaller(testAdmin)597 testing.SetOriginSend(nil)598 uassert.AbortsWithMessage(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) })599 b.conserved(t, 0)600}601602func TestSweepReserve(cur realm, t *testing.T) {603 id := planAs(cur, prov1, "sweep", 500, 50)604 begin()605 subscribeAs(cur, bob, id, 500)606607 // Nothing above the reserve: sweep refuses.608 testing.SetRealm(testing.NewUserRealm(testAdmin))609 testing.SetOriginCaller(testAdmin)610 testing.SetOriginSend(nil)611 uassert.AbortsContains(t, cur, "no surplus to sweep", func() {612 SweepDenom(cross(cur), Denom)613 })614 // Out-of-band surplus lands; only the surplus is sweepable.615 testing.IssueCoins(Address(), ugnot(77))616 liabBefore := Liabilities()617 got := coinsOf(testAdmin)618 testing.SetRealm(testing.NewUserRealm(testAdmin))619 testing.SetOriginCaller(testAdmin)620 testing.SetOriginSend(nil)621 SweepDenom(cross(cur), Denom)622 if coinsOf(testAdmin)-got != 77 {623 t.Fatalf("swept %d, want 77", coinsOf(testAdmin)-got)624 }625 if Liabilities() != liabBefore {626 t.Fatal("sweep touched the reserve")627 }628 testing.SetRealm(testing.NewUserRealm(mallory))629 testing.SetOriginCaller(mallory)630 testing.SetOriginSend(nil)631 uassert.AbortsWithMessage(t, cur, "fee recipient only", func() {632 SweepDenom(cross(cur), Denom)633 })634}635636func TestAdminHandover(cur realm, t *testing.T) {637 testing.SetRealm(testing.NewUserRealm(mallory))638 testing.SetOriginCaller(mallory)639 testing.SetOriginSend(nil)640 uassert.AbortsWithMessage(t, cur, "admin only", func() {641 TransferAdmin(cross(cur), mallory)642 })643 testing.SetRealm(testing.NewUserRealm(testAdmin))644 testing.SetOriginCaller(testAdmin)645 testing.SetOriginSend(nil)646 TransferAdmin(cross(cur), prov2)647 // Wrong acceptor refused; authority has not moved yet.648 testing.SetRealm(testing.NewUserRealm(mallory))649 testing.SetOriginCaller(mallory)650 testing.SetOriginSend(nil)651 uassert.AbortsWithMessage(t, cur, "only the staged successor may accept", func() {652 AcceptAdmin(cross(cur))653 })654 if Admin() != testAdmin {655 t.Fatal("authority moved before acceptance")656 }657 testing.SetRealm(testing.NewUserRealm(prov2))658 testing.SetOriginCaller(prov2)659 testing.SetOriginSend(nil)660 AcceptAdmin(cross(cur))661 if Admin() != prov2 {662 t.Fatal("authority did not move")663 }664 // The former admin is locked out.665 testing.SetRealm(testing.NewUserRealm(testAdmin))666 testing.SetOriginCaller(testAdmin)667 testing.SetOriginSend(nil)668 uassert.AbortsWithMessage(t, cur, "admin only", func() {669 SetFeeBps(cross(cur), 1)670 })671 // Hand it back for the remaining tests.672 testing.SetRealm(testing.NewUserRealm(prov2))673 testing.SetOriginCaller(prov2)674 testing.SetOriginSend(nil)675 TransferAdmin(cross(cur), testAdmin)676 testing.SetRealm(testing.NewUserRealm(testAdmin))677 testing.SetOriginCaller(testAdmin)678 testing.SetOriginSend(nil)679 AcceptAdmin(cross(cur))680}681682func TestFeeBounds(cur realm, t *testing.T) {683 testing.SetRealm(testing.NewUserRealm(testAdmin))684 testing.SetOriginCaller(testAdmin)685 testing.SetOriginSend(nil)686 uassert.AbortsContains(t, cur, "fee must be in", func() {687 SetFeeBps(cross(cur), MaxFeeBps+1)688 })689 testing.SetRealm(testing.NewUserRealm(testAdmin))690 testing.SetOriginCaller(testAdmin)691 testing.SetOriginSend(nil)692 uassert.AbortsContains(t, cur, "fee must be in", func() {693 SetFeeBps(cross(cur), -1)694 })695 var zero address696 testing.SetRealm(testing.NewUserRealm(testAdmin))697 testing.SetOriginCaller(testAdmin)698 testing.SetOriginSend(nil)699 uassert.AbortsWithMessage(t, cur, "fee recipient must not be the zero address", func() {700 SetFeeRecipient(cross(cur), zero)701 })702 testing.SetRealm(testing.NewUserRealm(testAdmin))703 testing.SetOriginCaller(testAdmin)704 testing.SetOriginSend(nil)705 uassert.AbortsWithMessage(t, cur, "successor must not be the zero address", func() {706 TransferAdmin(cross(cur), zero)707 })708}709710func TestStraySendRejected(cur realm, t *testing.T) {711 id := planAs(cur, prov1, "stray", 100, 50)712 subID := subscribeAs(cur, alice, id, 100)713 arm := func(from address) {714 testing.SetRealm(testing.NewUserRealm(from))715 testing.SetOriginCaller(from)716 testing.SetOriginSend(ugnot(3))717 }718 arm(prov1)719 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {720 RetirePlan(cross(cur), id)721 })722 arm(alice)723 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {724 Cancel(cross(cur), subID)725 })726 arm(mallory)727 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {728 Expire(cross(cur), subID)729 })730 arm(prov1)731 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {732 ClaimAll(cross(cur))733 })734 arm(testAdmin)735 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {736 WithdrawFees(cross(cur))737 })738 arm(testAdmin)739 uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() {740 SetFeeBps(cross(cur), 0)741 })742}743744func TestRenderSanitizes(cur realm, t *testing.T) {745 planAs(cur, prov1, "[evil](https://x) <script>alert(1)</script>", 1, MinPeriodBlocks)746 out := Render("")747 if strings.Contains(out, "<script>") {748 t.Fatal("unsanitized title reached Render")749 }750 if !strings.Contains(out, "plans:") {751 t.Fatal("render missing counters")752 }753 if Render("nope") == renderHome() {754 t.Fatal("unknown path fell through to home")755 }756}757758func TestUnknownIDs(t *testing.T) {759 defer func() {760 if recover() == nil {761 t.Fatal("expected panic")762 }763 }()764 PlanInfo(99999)765}766767// Y1 regression: EntitledFor follows the most recent subscription and768// honors paid entitlement across cancellation.769func TestEntitledForSurvivesCancel(cur realm, t *testing.T) {770 id := planAs(cur, prov1, "y1", 100, 50)771 subID := subscribeAs(cur, alice, id, 100)772 testing.SetRealm(testing.NewUserRealm(alice))773 testing.SetOriginCaller(alice)774 testing.SetOriginSend(nil)775 Cancel(cross(cur), subID)776 if !EntitledFor(id, alice) {777 t.Fatal("cancelled-but-paid subscriber lost EntitledFor")778 }779 // A fresh Subscribe repoints the surface at the new subscription.780 // Skip a few blocks first so ptNew > ptOld: at a height in781 // [ptOld, ptNew) a STALE pointer would answer false while the782 // repointed one answers true — the distinguishing assertion.783 _, _, _, _, ptOld, _, _ := SubInfo(subID)784 testing.SkipHeights(5)785 newID := subscribeAs(cur, alice, id, 100)786 _, _, _, _, ptNew, _, _ := SubInfo(newID)787 if ptNew <= ptOld {788 t.Fatal("fixture broken: ptNew must exceed ptOld")789 }790 if !EntitledFor(id, alice) || !Entitled(newID) || !Entitled(subID) {791 t.Fatal("both paid entitlements should be live right after re-subscribe")792 }793 testing.SkipHeights(ptOld - runtime.ChainHeight())794 if Entitled(subID) {795 t.Fatal("old span should have run out")796 }797 if !EntitledFor(id, alice) {798 t.Fatal("EntitledFor answered the stale id, not the repointed one")799 }800 // After the new paid span runs out too, EntitledFor goes false.801 testing.SkipHeights(ptNew - runtime.ChainHeight())802 if EntitledFor(id, alice) {803 t.Fatal("EntitledFor true past paidThrough")804 }805}806807// Y2 companion: a lapsed-in-grace incumbent blocks Subscribe with a808// message that states both options.809func TestSubscribeMessageInGrace(cur realm, t *testing.T) {810 id := planAs(cur, prov1, "y2msg", 100, 50)811 subID := subscribeAs(cur, bob, id, 100)812 _, _, _, _, pt, _, _ := SubInfo(subID)813 testing.SkipHeights(pt + 10 - runtime.ChainHeight())814 testing.SetRealm(testing.NewUserRealm(bob))815 testing.SetOriginCaller(bob)816 testing.SetOriginSend(ugnot(100))817 testing.IssueCoins(Address(), ugnot(100))818 uassert.AbortsContains(t, cur, "lapsed but renewable", func() {819 Subscribe(cross(cur), id)820 })821 // Cancel + Subscribe is the documented fresh-start path.822 testing.SetRealm(testing.NewUserRealm(bob))823 testing.SetOriginCaller(bob)824 testing.SetOriginSend(nil)825 Cancel(cross(cur), subID)826 subscribeAs(cur, bob, id, 100)827}828AcceptAdmin(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})
ActiveSubID(planID int64, subscriber string) (int64, bool)
Address() string
Admin() string
BalanceOf(a string) int64
Cancel(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)
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})
CreatePlan(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}, title string, description string, price int64, periodBlocks int64, maxFeeBps int64) int64
Entitled(subID int64) bool
EntitledFor(planID int64, subscriber string) bool
Expire(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)
FeeBps() int64
FeeRecipient() string
FeesAccrued() int64
Held() int64
Liabilities() int64
NumPlans() int64
NumSubs() int64
PaidThrough(subID int64) int64
PendingAdmin() string
PlanInfo(planID int64) (provider string, title string, price int64, periodBlocks int64, planFeeBps int64, status string, lifetimeSubs int64)
Render(path string) string
Renew(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)
RenewableFrom(subID int64) (from int64, until int64)
RetirePlan(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}, planID int64)
SetFeeBps(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}, bps int64)
SetFeeRecipient(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}, recipient string)
SubInfo(subID int64) (planID int64, subscriber string, provider string, price int64, paidThrough int64, periods int64, status string)
Subscribe(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}, planID int64) 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
WithdrawFees(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})
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.