Transaction
B4292A90F6D00C…CFC20429B994
Block 336,731 · index 0 · RPC
Summary
- Hash
- B4292A90F6D00C756679F2069BD07C070B4610AE281790904E59CFC20429B994
- Block
- 336,731
- Size
- 57749 bytes
- Gas used
- 75,577,311 / 250,000,000
- Fee
- 250000 ugnot
- Status
- success
Messages
- Attached funds
- 12000000 ugnot
Arguments · 9
- #1subscriptions
- #2gnomod.toml
- #3module = "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions" gno = "0.9"
- #4subscriptions.gno
- #5// Package subscriptions is a multi-provider subscription hub: providers // publish plans priced per billing period; subscribers pay per period, // with deterministic renewal, grace, expiration and cancellation rules; // other realms and off-chain services gate access on Entitled / // EntitledFor, which is the integration surface this realm exists to // provide. // // THE BILLING MODEL, COMPLETELY: // // Subscribe (payable, exact price) : creates the subscription, pays // period 1. paidThrough = height // + periodBlocks. // Renew (payable, exact price) : extends paidThrough by exactly // one periodBlocks, FROM // paidThrough — never from the // current height — so period // boundaries are fixed at // Subscribe time and never drift. // Entitlement : height < paidThrough. Nothing // else. Status does not enter // into it: a cancelled // subscription stays entitled to // what it already paid for. // Renewal window : a renewal is accepted iff BOTH // paidThrough - height <= periodBlocks (early bound) // height < paidThrough + periodBlocks (late bound) // The early bound caps prepayment // at one full unstarted period — // a second Renew straight after a // first is refused, which is what // makes an accidental duplicate // payment structurally impossible // rather than merely unlikely. // The late bound is the grace // window: renewing after lapse // extends from paidThrough, so it // back-pays the lapsed span to // keep the original schedule and // buys paidThrough + periodBlocks // - height further blocks — always // at least one, because the bound // is exclusive (audit finding Y2). // A lapsed subscriber who prefers // a fresh full period may Cancel // and Subscribe again at the same // total price; Subscribe's refusal // message states both options. // Expire (permissionless valve) : once height >= paidThrough + // periodBlocks, anyone may mark // the subscription Expired. No // funds move — every payment // settled when it was made. The // valve exists so the // plan|subscriber slot frees // without depending on either // party, and Subscribe itself // collapses an expired incumbent, // so a fresh start never depends // on housekeeping having run. The // renewable and expirable height // sets partition exactly: no // height is in both or neither. // Cancel (subscriber only) : Active -> Cancelled. Terminal. // No refund — payments settle to // the provider at payment time, // and what was bought (entitlement // through paidThrough) stays // bought. What cancellation ends // is the OBLIGATION: a Cancelled // subscription can never be // renewed, by the subscriber or // anyone else. // RetirePlan (provider only) : no new Subscribes, no renewals. // Existing entitlements run to // paidThrough untouched. Refusing // renewals on a retired plan is // subscriber protection: nobody // can keep paying for a service // whose provider announced its // end. // // The obligation is therefore explicit on chain at every moment: a // subscription owes nothing (there is no pull payment and no debt — a // lapse simply ends entitlement), and the realm owes the subscriber // exactly `paidThrough - height` blocks of entitlement, queryable by // anyone via PaidThrough / Entitled / EntitledFor. // // WHO PAYS WHOM. Payments settle immediately: price - fee is credited // to the provider's claimable balance, fee to the protocol pot, both // inside the same feeledger the sibling realms use. There is no escrow: // H == U + F at all times (plus out-of-band surplus, recoverable by // SweepDenom above the Liabilities reserve). The renewal caller must be // the subscriber — a third party cannot extend someone else's // subscription, which closes both a consent problem (an unwanted gift // re-arms a lapsing obligation) and a griefing edge (spending pennies // to keep a victim's slot occupied). // // FEES follow the house pattern exactly: a compile-time MaxFeeBps // ceiling, the current fee snapshotted into the PLAN at CreatePlan // (provider consents via its own maxFeeBps argument), copied into the // subscription at Subscribe, and charged at every payment from the // PROVIDER's side. A later SetFeeBps touches only plans created // afterwards; no existing plan or subscription can have its fee moved // by anyone. // // REALM-CALLER CAVEAT, inherited from the siblings verbatim: coinio's // receipt guard admits only EOA payers, so subscribers are EOAs; // assertNoSend reads the ORIGIN envelope, so every non-payable function // refuses any transaction that attached coins anywhere. Providers may // be EOAs or realms, but a realm provider must expose its own crossing // path to Claim, or what it earns is stranded (see RegisterService's // caveat in service_market — the same three obligations apply). package subscriptions import ( "chain" "chain/runtime" "chain/runtime/unsafe" "strconv" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio" "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger" "gno.land/p/nt/avl/v0" "gno.land/p/nt/markdown/sanitize/v0" ) // Denom is the only asset this realm accepts. const Denom = "ugnot" // MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%. const MaxFeeBps = int64(1000) // Plan status values. const ( PlanActive = "active" PlanRetired = "retired" ) // Subscription status values. Cancelled and Expired are terminal. const ( SubActive = "active" SubCancelled = "cancelled" SubExpired = "expired" ) // Input bounds. const ( MaxTitleLen = 80 MaxDescLen = 2000 MinPrice = int64(1) // MinPeriodBlocks/MaxPeriodBlocks bound a plan's billing period: // ~40 seconds to ~1 year at pearl's observed ~4.2s blocks. The // floor keeps a hostile plan from turning renewal into a // per-block treadmill; the ceiling keeps paidThrough arithmetic // far from overflow even at maximum prepayment. MinPeriodBlocks = int64(10) MaxPeriodBlocks = int64(7500000) // MaxPlansPerProvider bounds catalog monopolization by a single // address — the finding that was RED in permission_registry and // service_registry, carried from the start here. MaxPlansPerProvider = 20 // MaxSubsPerSubscriber bounds one account's open-subscription // state. Terminal subscriptions free their slot. MaxSubsPerSubscriber = 100 // RenderLimit bounds every rendered list — an unbounded Render // was YELLOW in three prior audits. RenderLimit = 20 ) type plan struct { id int64 provider address title string description string price int64 periodBlocks int64 feeBps int64 // snapshotted at creation, charged at every payment status string subs int64 // lifetime count, never decremented } type sub struct { id int64 planID int64 subscriber address provider address // copied at Subscribe; never re-read from the plan price int64 // copied at Subscribe; a plan is immutable anyway periodBlocks int64 // copied at Subscribe feeBps int64 // copied at Subscribe from the plan's snapshot status string paidThrough int64 // absolute height; entitlement = height < paidThrough periods int64 // lifetime paid-period count } var ( admin address // may set fee, fee recipient, stage successor pendingAdmin address // staged by TransferAdmin, completes via AcceptAdmin feeRecipient address // may withdraw fees and sweep surplus feeBps int64 // fee snapshotted into NEW plans self address // this realm's address, captured at deploy nextPlan int64 nextSub int64 plans = avl.NewTree() // padID(id) -> *plan subs = avl.NewTree() // padID(id) -> *sub activeByKey = avl.NewTree() // padID(planID)|subscriber -> int64 sub id, while Active latestByKey = avl.NewTree() // padID(planID)|subscriber -> most recent sub id, never removed providerNum = avl.NewTree() // address -> *int64, live plans per provider subNum = avl.NewTree() // address -> *int64, Active subs per subscriber ledger = feeledger.MustNew(MaxFeeBps) ) func init() { admin = unsafe.OriginCaller() feeRecipient = admin self = unsafe.CurrentRealm().Address() } // --- provider side --- // CreatePlan publishes a subscription plan and returns its id. No coins // are accepted; the storage deposit the caller pays is the anti-spam. // The current protocol fee is snapshotted into the plan and must not // exceed maxFeeBps, the ceiling the provider signed for — pass // MaxFeeBps to accept any legal fee. Plans are immutable once created: // price and period changes are a new plan, so nothing a subscriber // agreed to can move underneath them. func CreatePlan(cur realm, title, description string, price, periodBlocks, maxFeeBps int64) int64 { assertNoSend() provider := cur.Previous().Address() if title == "" || len(title) > MaxTitleLen { panic("title must be 1.." + itoa(int64(MaxTitleLen)) + " bytes") } if len(description) > MaxDescLen { panic("description too long") } if price < MinPrice { panic("price must be at least " + itoa(MinPrice) + Denom) } if periodBlocks < MinPeriodBlocks || periodBlocks > MaxPeriodBlocks { panic("periodBlocks must be in [" + itoa(MinPeriodBlocks) + ", " + itoa(MaxPeriodBlocks) + "]") } if feeBps > maxFeeBps { panic("current fee " + itoa(feeBps) + " bps exceeds the caller's maximum " + itoa(maxFeeBps)) } n := counter(providerNum, provider) if *n >= MaxPlansPerProvider { panic("per-provider plan limit reached") } id := nextPlan nextPlan++ plans.Set(padID(id), &plan{ id: id, provider: provider, title: title, description: description, price: price, periodBlocks: periodBlocks, feeBps: feeBps, status: PlanActive, }) *n++ chain.Emit("PlanCreated", "planId", itoa(id), "provider", provider.String(), "price", itoa(price), "periodBlocks", itoa(periodBlocks), "feeBps", itoa(feeBps), ) return id } // RetirePlan takes a plan off the market: no new subscriptions and no // renewals. Provider only. Existing entitlements run to their // paidThrough untouched; refusing renewals is subscriber protection — // nobody keeps paying for a service whose provider announced its end. // The provider's plan-quota slot frees. func RetirePlan(cur realm, planID int64) { assertNoSend() caller := cur.Previous().Address() p := mustGetPlan(planID) if caller != p.provider { panic("only the provider may retire a plan") } if p.status != PlanActive { panic("plan is not active") } p.status = PlanRetired *counter(providerNum, p.provider)-- chain.Emit("PlanRetired", "planId", itoa(planID), "provider", p.provider.String()) } // --- subscriber side --- // Subscribe pays for the first billing period of a plan and returns the // new subscription id. The transaction must attach EXACTLY the plan's // price in ugnot — over- and underpayment are both refused, so a // mistaken double-attach cannot silently become a donation. One // subscriber holds at most one live subscription per plan: if an Active // one exists the call is refused (the renewal path is Renew, never a // second Subscribe — that is the duplicate-payment guard at the // identity level); an incumbent past its grace window is collapsed to // Expired in place, so a fresh start never waits on housekeeping. // // The payment settles immediately: price minus the plan's snapshotted // fee to the provider's claimable balance, fee to the protocol pot. // Entitlement runs from this block: paidThrough = height + periodBlocks. func Subscribe(cur realm, planID int64) int64 { subscriber, amount := coinio.Receive(0, cur, Denom) p := mustGetPlan(planID) if p.status != PlanActive { panic("plan is not active") } if amount != p.price { panic("send exactly the plan price: " + itoa(p.price) + Denom) } key := padID(planID) + "|" + subscriber.String() if v := activeByKey.Get(key); v != nil { inc := mustGetSub(v.(int64)) if runtime.ChainHeight() >= inc.paidThrough+inc.periodBlocks { expireInPlace(inc) } else if runtime.ChainHeight() >= inc.paidThrough { panic("subscription " + itoa(inc.id) + " to this plan is lapsed but renewable: " + "Renew back-pays the lapsed span and keeps the schedule; " + "Cancel then Subscribe restarts fresh at the same price") } else { panic("an active subscription to this plan already exists: renew it instead") } } n := counter(subNum, subscriber) if *n >= MaxSubsPerSubscriber { panic("per-subscriber subscription limit reached") } ledger.MustDeposit(p.provider.String(), amount, p.feeBps) id := nextSub nextSub++ subs.Set(padID(id), &sub{ id: id, planID: planID, subscriber: subscriber, provider: p.provider, price: p.price, periodBlocks: p.periodBlocks, feeBps: p.feeBps, status: SubActive, paidThrough: runtime.ChainHeight() + p.periodBlocks, periods: 1, }) activeByKey.Set(key, id) latestByKey.Set(key, id) *n++ p.subs++ chain.Emit("Subscribed", "subId", itoa(id), "planId", itoa(planID), "subscriber", subscriber.String(), "provider", p.provider.String(), "amount", itoa(amount), "paidThrough", itoa(runtime.ChainHeight()+p.periodBlocks), ) return id } // Renew pays for the next billing period of the caller's own // subscription. The transaction must attach exactly the subscription's // price. The renewal window is deterministic and stated in the header: // accepted iff paidThrough - height <= periodBlocks (at most one full // unstarted period prepaid — the duplicate-payment bound) and height < // paidThrough + periodBlocks (the grace bound, exclusive — a renewal // always buys at least one block). Extension is always // FROM paidThrough, so period boundaries never drift, and a renewal // inside grace covers the lapsed span — that is the price of keeping // the original schedule, and it is the documented, deterministic // choice. func Renew(cur realm, subID int64) { payer, amount := coinio.Receive(0, cur, Denom) s := mustGetSub(subID) if payer != s.subscriber { panic("only the subscriber may renew") } if s.status != SubActive { panic("subscription is " + s.status) } p := mustGetPlan(s.planID) if p.status != PlanActive { panic("plan is retired; the paid period runs to its end but cannot be renewed") } if amount != s.price { panic("send exactly the subscription price: " + itoa(s.price) + Denom) } h := runtime.ChainHeight() if s.paidThrough-h > s.periodBlocks { panic("too early: at most one unstarted period may be prepaid; renewable from height " + itoa(s.paidThrough-s.periodBlocks)) } if h >= s.paidThrough+s.periodBlocks { panic("grace window over; the subscription is expirable, subscribe afresh") } ledger.MustDeposit(s.provider.String(), amount, s.feeBps) s.paidThrough += s.periodBlocks s.periods++ chain.Emit("Renewed", "subId", itoa(subID), "subscriber", s.subscriber.String(), "amount", itoa(amount), "paidThrough", itoa(s.paidThrough), ) } // Cancel ends the caller's own subscription. Terminal: it can never be // renewed afterwards, by anyone. No refund and no funds move — every // payment settled when it was made, and the entitlement already bought // (height < paidThrough) remains until it runs out. The plan slot and // the subscriber's quota slot free immediately. func Cancel(cur realm, subID int64) { assertNoSend() caller := cur.Previous().Address() s := mustGetSub(subID) if caller != s.subscriber { panic("only the subscriber may cancel") } if s.status != SubActive { panic("subscription is " + s.status) } s.status = SubCancelled releaseSlots(s) chain.Emit("Cancelled", "subId", itoa(subID), "subscriber", s.subscriber.String(), "paidThrough", itoa(s.paidThrough), ) } // Expire marks a lapsed subscription Expired once its grace window is // over: height >= paidThrough + periodBlocks. Permissionless by design — // like the sibling realms' valves, no slot's liveness may depend on // either party showing up. No funds move. func Expire(cur realm, subID int64) { assertNoSend() s := mustGetSub(subID) if s.status != SubActive { panic("subscription is " + s.status) } if runtime.ChainHeight() < s.paidThrough+s.periodBlocks { panic("not expirable: grace runs through height " + itoa(s.paidThrough+s.periodBlocks-1)) } expireInPlace(s) } // --- payouts --- // Claim sends amount ugnot of the caller's claimable balance back to // the caller. Providers earn into this balance at every payment. func Claim(cur realm, amount int64) { assertNoSend() caller := cur.Previous().Address() if err := ledger.Withdraw(caller.String(), amount); err != nil { panic(err) } coinio.Payout(0, cur, caller, Denom, amount) chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount)) } // ClaimAll sends the caller's entire claimable balance back to the // caller. Fails if there is nothing to claim. func ClaimAll(cur realm) { assertNoSend() caller := cur.Previous().Address() amount, err := ledger.WithdrawAll(caller.String()) if err != nil { panic(err) } if amount == 0 { panic("nothing to claim") } coinio.Payout(0, cur, caller, Denom, amount) chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount)) } // WithdrawFees sends the accrued fee pot to the fee recipient. Only the // fee recipient may call it, and only the pot moves. func WithdrawFees(cur realm) { assertNoSend() if cur.Previous().Address() != feeRecipient { panic("fee recipient only") } amount := ledger.WithdrawFees() if amount == 0 { panic("no fees accrued") } coinio.Payout(0, cur, feeRecipient, Denom, amount) chain.Emit("FeesWithdrawn", "to", feeRecipient.String(), "amount", itoa(amount)) } // SweepDenom recovers out-of-band coins (sent by raw bank transfer, // outside any entrypoint) to the fee recipient. For the ledger denom // the reserve is Liabilities() — user balances and the fee pot are // structurally unreachable. Fee recipient only. func SweepDenom(cur realm, denom string) { assertNoSend() if cur.Previous().Address() != feeRecipient { panic("fee recipient only") } reserve := int64(0) if denom == Denom { reserve = ledger.Liabilities() } // coinio.Sweep itself aborts when nothing sits above the reserve. swept := coinio.Sweep(0, cur, feeRecipient, denom, reserve) chain.Emit("SurplusSwept", "to", feeRecipient.String(), "coins", itoa(swept)+denom) } // --- administration --- // SetFeeBps sets the protocol fee snapshotted into FUTURE plans. // Bounded by MaxFeeBps; existing plans and subscriptions are untouched // — their fee was fixed the moment the provider consented to it. func SetFeeBps(cur realm, bps int64) { assertNoSend() assertAdmin(cur.Previous().Address()) if bps < 0 || bps > MaxFeeBps { panic("fee must be in [0, " + itoa(MaxFeeBps) + "] bps") } old := feeBps feeBps = bps chain.Emit("FeeChanged", "oldBps", itoa(old), "newBps", itoa(bps)) } // SetFeeRecipient points future fee withdrawals and sweeps at a new // address. Admin only. The zero address is refused — it would strand // the pot. func SetFeeRecipient(cur realm, recipient address) { assertNoSend() assertAdmin(cur.Previous().Address()) var zero address if recipient == zero { panic("fee recipient must not be the zero address") } old := feeRecipient feeRecipient = recipient chain.Emit("FeeRecipientChanged", "old", old.String(), "new", recipient.String()) } // TransferAdmin stages a two-step admin handover. The successor holds // nothing until AcceptAdmin. func TransferAdmin(cur realm, successor address) { assertNoSend() assertAdmin(cur.Previous().Address()) var zero address if successor == zero { panic("successor must not be the zero address") } pendingAdmin = successor chain.Emit("AdminTransferStaged", "from", admin.String(), "to", successor.String()) } // AcceptAdmin completes the handover; only the staged successor may. func AcceptAdmin(cur realm) { assertNoSend() caller := cur.Previous().Address() if caller != pendingAdmin { panic("only the staged successor may accept") } old := admin admin = caller var zero address pendingAdmin = zero chain.Emit("AdminTransferred", "from", old.String(), "to", admin.String()) } // --- views --- // Entitled reports whether the subscription's paid entitlement covers // the current block: height < paidThrough. Status deliberately does not // enter into it — a cancelled subscriber keeps what they paid for, and // an expirable-but-unexpired one has already lapsed here. func Entitled(subID int64) bool { s := mustGetSub(subID) return runtime.ChainHeight() < s.paidThrough } // EntitledFor reports whether subscriber currently holds paid // entitlement to planID, through their MOST RECENT subscription to it. // This is the one-call integration surface for other realms and // services, and it honors the entitlement contract across status: a // cancelled subscription keeps answering true until its paidThrough — // what was bought stays bought (audit finding Y1). One self-inflicted // edge is fail-closed: cancelling a prepaid subscription and // re-subscribing at once points this surface at the NEW, earlier // paidThrough; the old subscription's remaining span stays queryable // per-id via Entitled. func EntitledFor(planID int64, subscriber address) bool { v := latestByKey.Get(padID(planID) + "|" + subscriber.String()) if v == nil { return false } return Entitled(v.(int64)) } // ActiveSubID returns the caller-facing id of subscriber's live // subscription to planID, or (0, false) if none is Active. func ActiveSubID(planID int64, subscriber address) (int64, bool) { v := activeByKey.Get(padID(planID) + "|" + subscriber.String()) if v == nil { return 0, false } return v.(int64), true } // PlanInfo returns a plan's public fields. func PlanInfo(planID int64) (provider address, title string, price, periodBlocks, planFeeBps int64, status string, lifetimeSubs int64) { p := mustGetPlan(planID) return p.provider, p.title, p.price, p.periodBlocks, p.feeBps, p.status, p.subs } // SubInfo returns a subscription's public fields. func SubInfo(subID int64) (planID int64, subscriber, provider address, price, paidThrough, periods int64, status string) { s := mustGetSub(subID) return s.planID, s.subscriber, s.provider, s.price, s.paidThrough, s.periods, s.status } // PaidThrough returns the absolute height a subscription is paid to. func PaidThrough(subID int64) int64 { return mustGetSub(subID).paidThrough } // RenewableFrom returns the earliest height at which Renew will accept // a payment for this subscription, and the last height at which it // still will (inclusive) — the deterministic window, precomputed for // integrators. From until+1 the subscription is expirable instead; the // two sets partition exactly. func RenewableFrom(subID int64) (from, until int64) { s := mustGetSub(subID) return s.paidThrough - s.periodBlocks, s.paidThrough + s.periodBlocks - 1 } func Admin() address { return admin } func PendingAdmin() address { return pendingAdmin } func FeeRecipient() address { return feeRecipient } func FeeBps() int64 { return feeBps } func NumPlans() int64 { return nextPlan } func NumSubs() int64 { return nextSub } func UsersTotal() int64 { return ledger.UsersTotal() } func FeesAccrued() int64 { return ledger.FeesAccrued() } func Liabilities() int64 { return ledger.Liabilities() } func BalanceOf(a address) int64 { return ledger.BalanceOf(a.String()) } func Address() address { return self } func Held() int64 { return coinio.HeldAt(self, Denom) } // --- render --- func Render(path string) string { if path == "" { return renderHome() } return "unknown page; try the realm root" } func renderHome() string { out := "# subscriptions\n\n" out += "Provider-published plans, per-period payment, deterministic " + "renewal, grace, expiration and cancellation. Entitlement is " + "`height < paidThrough`, queryable by anyone.\n\n" out += "- plans: " + itoa(nextPlan) + "\n" out += "- subscriptions: " + itoa(nextSub) + "\n" out += "- provider balances: " + itoa(ledger.UsersTotal()) + Denom + "\n" out += "- fees accrued: " + itoa(ledger.FeesAccrued()) + Denom + "\n" out += "- current fee for new plans: " + itoa(feeBps) + " bps (cap " + itoa(MaxFeeBps) + ")\n\n" if nextPlan == 0 { return out + "No plans yet.\n" } out += "## newest plans\n\n" shown := int64(0) for id := nextPlan - 1; id >= 0 && shown < RenderLimit; id-- { p := mustGetPlan(id) out += "- #" + itoa(p.id) + " **" + sanitize.InlineText(p.title) + "** — " + itoa(p.price) + Denom + " / " + itoa(p.periodBlocks) + " blocks, " + p.status + ", provider `" + p.provider.String() + "`, " + itoa(p.subs) + " lifetime subs\n" shown++ } if nextPlan > shown { out += "\n(" + itoa(nextPlan-shown) + " older plans not shown)\n" } return out } // --- internals --- // expireInPlace flips an Active subscription to Expired and frees its // slots. Callers have already established expirability. func expireInPlace(s *sub) { s.status = SubExpired releaseSlots(s) chain.Emit("SubscriptionExpired", "subId", itoa(s.id), "subscriber", s.subscriber.String(), "paidThrough", itoa(s.paidThrough), ) } // releaseSlots removes the plan|subscriber activity index entry and // decrements the subscriber's quota counter. Exactly once per terminal // transition, which both terminal paths guarantee by requiring // SubActive first. func releaseSlots(s *sub) { activeByKey.Remove(padID(s.planID) + "|" + s.subscriber.String()) *counter(subNum, s.subscriber)-- } func assertNoSend() { if len(unsafe.OriginSend()) != 0 { panic("this function does not accept coins") } } func assertAdmin(caller address) { if caller != admin { panic("admin only") } } func mustGetPlan(id int64) *plan { v := plans.Get(padID(id)) if v == nil { panic("unknown plan id") } return v.(*plan) } func mustGetSub(id int64) *sub { v := subs.Get(padID(id)) if v == nil { panic("unknown subscription id") } return v.(*sub) } // counter returns the persistent per-address counter in tree, // allocating a zero on first use. func counter(tree *avl.Tree, a address) *int64 { k := a.String() if v := tree.Get(k); v != nil { return v.(*int64) } n := new(int64) tree.Set(k, n) return n } func padID(id int64) string { s := strconv.FormatInt(id, 10) for len(s) < 12 { s = "0" + s } return s } func itoa(n int64) string { return strconv.FormatInt(n, 10) }
- #6subscriptions_test.gno
- #7package subscriptions import ( "chain" "chain/banker" "chain/runtime" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( testAdmin = testutils.TestAddress("subadmin") prov1 = testutils.TestAddress("subprov1") prov2 = testutils.TestAddress("subprov2") alice = testutils.TestAddress("subalice") bob = testutils.TestAddress("subbob") mallory = testutils.TestAddress("submallory") ) // Established harness facts, carried from the sibling realms' suites: // init runs with an empty origin context (the real deploy captures the // origin caller); the banker resets between tests while realm globals // persist, so conservation is asserted in DELTA form after re-seeding // carried liabilities; testing.SetRealm affects only the frame that // calls it; and testing.SkipHeights WIPES the origin/realm context, so // every caller is re-armed after a skip. func init() { admin = testAdmin feeRecipient = testAdmin self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/subscriptions") } type baseline struct{ held, users, fees int64 } func begin() baseline { if l := Liabilities(); l > 0 { testing.IssueCoins(Address(), ugnot(l)) } return snap() } func snap() baseline { return baseline{held: Held(), users: UsersTotal(), fees: FeesAccrued()} } // conserved asserts ΔH == ΔU + ΔF + extraSurplus — the realm's monetary // invariant in delta form. There is no escrow pool in this realm. func (b baseline) conserved(t *testing.T, extraSurplus int64) { t.Helper() dH := Held() - b.held dU := UsersTotal() - b.users dF := FeesAccrued() - b.fees if dH != dU+dF+extraSurplus { t.Fatalf("conservation violated: ΔH=%d ΔU=%d ΔF=%d ΔS=%d", dH, dU, dF, extraSurplus) } if Liabilities() != UsersTotal()+FeesAccrued() { t.Fatalf("Liabilities() = %d, want U+F", Liabilities()) } } func coinsOf(addr address) int64 { return banker.NewReadonlyBanker().GetCoin(addr, Denom) } func ugnot(n int64) chain.Coins { return chain.NewCoins(chain.NewCoin(Denom, n)) } // NOTE ON THE HARNESS. testing.SetRealm applies to the frame that CALLS // it, so the caller identity must be armed in the SAME frame as the // entrypoint invocation — the three lines are inlined at every site, // matching the sibling suites. // planAs publishes a plan (non-payable: envelope explicitly cleared). func planAs(cur realm, from address, title string, price, period int64) int64 { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginCaller(from) testing.SetOriginSend(nil) return CreatePlan(cross(cur), title, "d", price, period, MaxFeeBps) } // subscribeAs performs a chain-faithful subscribe: envelope staged AND landed. func subscribeAs(cur realm, from address, planID, pay int64) int64 { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginCaller(from) testing.SetOriginSend(ugnot(pay)) testing.IssueCoins(Address(), ugnot(pay)) return Subscribe(cross(cur), planID) } // renewAs performs a chain-faithful renewal. func renewAs(cur realm, from address, subID, pay int64) { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginCaller(from) testing.SetOriginSend(ugnot(pay)) testing.IssueCoins(Address(), ugnot(pay)) Renew(cross(cur), subID) } func TestInitialConfig(t *testing.T) { if Admin() != testAdmin || FeeRecipient() != testAdmin { t.Fatal("seeded roles mismatch") } var zero address if PendingAdmin() != zero { t.Fatal("pending admin not zero") } if FeeBps() != 0 || NumPlans() != 0 || NumSubs() != 0 { t.Fatal("initial counters not zero") } } func TestCreatePlanValidation(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(prov1)) testing.SetOriginCaller(prov1) testing.SetOriginSend(ugnot(5)) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { CreatePlan(cross(cur), "x", "d", 1, MinPeriodBlocks, MaxFeeBps) }) testing.SetRealm(testing.NewUserRealm(prov1)) testing.SetOriginCaller(prov1) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "title must be", func() { CreatePlan(cross(cur), "", "d", 1, MinPeriodBlocks, MaxFeeBps) }) uassert.AbortsContains(t, cur, "title must be", func() { CreatePlan(cross(cur), strings.Repeat("t", MaxTitleLen+1), "d", 1, MinPeriodBlocks, MaxFeeBps) }) uassert.AbortsContains(t, cur, "description too long", func() { CreatePlan(cross(cur), "x", strings.Repeat("d", MaxDescLen+1), 1, MinPeriodBlocks, MaxFeeBps) }) uassert.AbortsContains(t, cur, "price must be at least", func() { CreatePlan(cross(cur), "x", "d", 0, MinPeriodBlocks, MaxFeeBps) }) uassert.AbortsContains(t, cur, "periodBlocks must be in", func() { CreatePlan(cross(cur), "x", "d", 1, MinPeriodBlocks-1, MaxFeeBps) }) uassert.AbortsContains(t, cur, "periodBlocks must be in", func() { CreatePlan(cross(cur), "x", "d", 1, MaxPeriodBlocks+1, MaxFeeBps) }) } func TestFeeConsentCeiling(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SetFeeBps(cross(cur), 250) // A provider who signed for at most 100 bps is refused while the // fee is 250. testing.SetRealm(testing.NewUserRealm(prov1)) testing.SetOriginCaller(prov1) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "exceeds the caller's maximum", func() { CreatePlan(cross(cur), "capped", "d", 100, MinPeriodBlocks, 100) }) // A consenting provider snapshots 250 into the plan; a later fee // change does not touch it. id := planAs(cur, prov1, "snap", 1000, MinPeriodBlocks) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SetFeeBps(cross(cur), 0) _, _, _, _, planFee, _, _ := PlanInfo(id) if planFee != 250 { t.Fatalf("plan feeBps = %d, want the 250 snapshot", planFee) } // The subscription charges the SNAPSHOT, not the current fee. b := begin() subscribeAs(cur, alice, id, 1000) if got := FeesAccrued() - b.fees; got != 25 { t.Fatalf("fee charged = %d, want 25 (250 bps of 1000)", got) } if got := BalanceOf(prov1); got == 0 { t.Fatal("provider not credited") } b.conserved(t, 0) } func TestPlanQuota(cur realm, t *testing.T) { for i := 0; i < MaxPlansPerProvider; i++ { planAs(cur, prov2, "q", 1, MinPeriodBlocks) } testing.SetRealm(testing.NewUserRealm(prov2)) testing.SetOriginCaller(prov2) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "per-provider plan limit reached", func() { CreatePlan(cross(cur), "q", "d", 1, MinPeriodBlocks, MaxFeeBps) }) // Retiring one frees the slot. RetirePlan(cross(cur), NumPlans()-1) planAs(cur, prov2, "q2", 1, MinPeriodBlocks) } func TestRetirePlanAuthority(cur realm, t *testing.T) { id := planAs(cur, prov1, "mine", 5, MinPeriodBlocks) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "only the provider may retire a plan", func() { RetirePlan(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(prov1)) testing.SetOriginCaller(prov1) testing.SetOriginSend(nil) RetirePlan(cross(cur), id) uassert.AbortsWithMessage(t, cur, "plan is not active", func() { RetirePlan(cross(cur), id) }) // No new subscriptions on a retired plan. testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(ugnot(5)) testing.IssueCoins(Address(), ugnot(5)) uassert.AbortsWithMessage(t, cur, "plan is not active", func() { Subscribe(cross(cur), id) }) } func TestSubscribeLifecycle(cur realm, t *testing.T) { id := planAs(cur, prov1, "basic", 100, 50) b := begin() h0 := runtime.ChainHeight() subID := subscribeAs(cur, alice, id, 100) planID, subscriber, provider, price, paidThrough, periods, status := SubInfo(subID) if planID != id || subscriber != alice || provider != prov1 || price != 100 || periods != 1 || status != SubActive { t.Fatal("subscription fields wrong") } if paidThrough != h0+50 { t.Fatalf("paidThrough = %d, want %d", paidThrough, h0+50) } if !Entitled(subID) || !EntitledFor(id, alice) { t.Fatal("fresh subscription not entitled") } if got, ok := ActiveSubID(id, alice); !ok || got != subID { t.Fatal("active index wrong") } b.conserved(t, 0) // Exact envelope: under- and overpayment refused. for _, bad := range []int64{99, 101} { testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(ugnot(bad)) testing.IssueCoins(Address(), ugnot(bad)) uassert.AbortsContains(t, cur, "send exactly the plan price", func() { Subscribe(cross(cur), id) }) } // A second live subscription to the same plan is refused — the // identity-level duplicate-payment guard. testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "renew it instead", func() { Subscribe(cross(cur), id) }) } func TestRenewalWindow(cur realm, t *testing.T) { id := planAs(cur, prov1, "window", 100, 50) subID := subscribeAs(cur, bob, id, 100) _, _, _, _, pt0, _, _ := SubInfo(subID) from, until := RenewableFrom(subID) if from != pt0-50 || until != pt0+49 { t.Fatalf("window [%d,%d], want [%d,%d]", from, until, pt0-50, pt0+49) } // Straight after Subscribe, remaining entitlement equals exactly // one period (the running one), so ONE prepay renewal is inside // the early bound — and extension is FROM paidThrough. b := begin() renewAs(cur, bob, subID, 100) _, _, _, _, pt1, periods, _ := SubInfo(subID) if pt1 != pt0+50 || periods != 2 { t.Fatalf("paidThrough = %d periods = %d, want %d and 2", pt1, periods, pt0+50) } b.conserved(t, 0) // A SECOND renewal in the same block is the accidental duplicate, // and the early bound refuses it: more than one unstarted period // would be prepaid. testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "too early", func() { Renew(cross(cur), subID) }) // Wrong payer: a third party cannot extend someone else's // subscription even inside the window. testing.SkipHeights(50) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsWithMessage(t, cur, "only the subscriber may renew", func() { Renew(cross(cur), subID) }) // Wrong amount inside the window. testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(ugnot(99)) testing.IssueCoins(Address(), ugnot(99)) uassert.AbortsContains(t, cur, "send exactly the subscription price", func() { Renew(cross(cur), subID) }) } func TestGraceBoundary(cur realm, t *testing.T) { id := planAs(cur, prov1, "grace", 100, 50) subID := subscribeAs(cur, alice, id, 100) _, _, _, _, pt, _, _ := SubInfo(subID) // Move to the LAST renewable height: h == paidThrough + // periodBlocks - 1 (the late bound is exclusive — audit Y2). testing.SkipHeights(pt + 49 - runtime.ChainHeight()) if Entitled(subID) { t.Fatal("entitlement must have lapsed past paidThrough") } // Renewal at the boundary is accepted, extends FROM paidThrough — // and buys at least one block: the Y2 regression. renewAs(cur, alice, subID, 100) _, _, _, _, pt1, _, _ := SubInfo(subID) if pt1 != pt+50 { t.Fatalf("paidThrough = %d, want %d (extension from paidThrough)", pt1, pt+50) } if !Entitled(subID) { t.Fatal("a boundary renewal must buy at least one block of entitlement") } // At the grace end itself renewal is refused, Expire opens: the // two sets partition exactly. testing.SkipHeights(pt1 + 50 - runtime.ChainHeight()) testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "grace window over", func() { Renew(cross(cur), subID) }) // Expire is permissionless: mallory may. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) Expire(cross(cur), subID) _, _, _, _, _, _, status := SubInfo(subID) if status != SubExpired { t.Fatal("not expired") } if EntitledFor(id, alice) { t.Fatal("expired subscription still indexed") } // Terminal: renewal refused by status. testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "subscription is expired", func() { Renew(cross(cur), subID) }) // The slot is free: a fresh Subscribe works. subscribeAs(cur, alice, id, 100) } func TestExpireGuards(cur realm, t *testing.T) { id := planAs(cur, prov1, "expguard", 100, 50) subID := subscribeAs(cur, bob, id, 100) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "not expirable", func() { Expire(cross(cur), subID) }) // One block before the grace end it is still not expirable... _, _, _, _, pt, _, _ := SubInfo(subID) testing.SkipHeights(pt + 49 - runtime.ChainHeight()) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "not expirable", func() { Expire(cross(cur), subID) }) // ...and AT the grace end it is: the sets partition exactly. testing.SkipHeights(1) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) Expire(cross(cur), subID) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "subscription is expired", func() { Expire(cross(cur), subID) }) } func TestSubscribeCollapsesExpiredIncumbent(cur realm, t *testing.T) { id := planAs(cur, prov1, "collapse", 100, 50) oldID := subscribeAs(cur, alice, id, 100) _, _, _, _, pt, _, _ := SubInfo(oldID) testing.SkipHeights(pt + 50 - runtime.ChainHeight()) // No Expire call happened; Subscribe collapses the incumbent // itself, at the first expirable height. newID := subscribeAs(cur, alice, id, 100) if newID == oldID { t.Fatal("expected a fresh subscription id") } _, _, _, _, _, _, oldStatus := SubInfo(oldID) if oldStatus != SubExpired { t.Fatalf("incumbent status = %q, want expired", oldStatus) } if got, ok := ActiveSubID(id, alice); !ok || got != newID { t.Fatal("index does not point at the fresh subscription") } } func TestCancel(cur realm, t *testing.T) { id := planAs(cur, prov1, "cancel", 100, 50) subID := subscribeAs(cur, bob, id, 100) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "only the subscriber may cancel", func() { Cancel(cross(cur), subID) }) testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(nil) Cancel(cross(cur), subID) _, _, _, _, _, _, status := SubInfo(subID) if status != SubCancelled { t.Fatal("not cancelled") } // Paid entitlement survives cancellation until paidThrough — on // both surfaces (audit Y1)... if !Entitled(subID) || !EntitledFor(id, bob) { t.Fatal("cancelled subscription lost its paid entitlement") } // ...but the ACTIVE slot is gone and renewal is refused forever. if _, ok := ActiveSubID(id, bob); ok { t.Fatal("cancelled subscription still holds the active slot") } testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "subscription is cancelled", func() { Renew(cross(cur), subID) }) testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "subscription is cancelled", func() { Cancel(cross(cur), subID) }) // The slot freed: bob may start a fresh subscription at once. subscribeAs(cur, bob, id, 100) } func TestRenewRefusedOnRetiredPlan(cur realm, t *testing.T) { id := planAs(cur, prov1, "retire-mid", 100, 50) subID := subscribeAs(cur, alice, id, 100) testing.SetRealm(testing.NewUserRealm(prov1)) testing.SetOriginCaller(prov1) testing.SetOriginSend(nil) RetirePlan(cross(cur), id) // Entitlement continues... if !Entitled(subID) { t.Fatal("entitlement must survive plan retirement") } // ...but renewal is refused even inside the window. _, _, _, _, pt, _, _ := SubInfo(subID) testing.SkipHeights(pt - runtime.ChainHeight()) testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "plan is retired", func() { Renew(cross(cur), subID) }) } func TestSubscriberQuota(cur realm, t *testing.T) { // Fill carol's quota with one-plan-each subscriptions, then refuse. // A dedicated provider: prov2's plan quota is already saturated by // TestPlanQuota, and realm globals persist across tests. carol := testutils.TestAddress("subcarol") provQ := testutils.TestAddress("subprovq") var lastPlan int64 for i := 0; i < MaxSubsPerSubscriber; i++ { lastPlan = planAs(cur, provQ, "quota", 1, MinPeriodBlocks) subscribeAs(cur, carol, lastPlan, 1) // Free the provider quota slot for the next plan. testing.SetRealm(testing.NewUserRealm(provQ)) testing.SetOriginCaller(provQ) testing.SetOriginSend(nil) RetirePlan(cross(cur), lastPlan) } extra := planAs(cur, provQ, "quota-x", 1, MinPeriodBlocks) testing.SetRealm(testing.NewUserRealm(carol)) testing.SetOriginCaller(carol) testing.SetOriginSend(ugnot(1)) testing.IssueCoins(Address(), ugnot(1)) uassert.AbortsWithMessage(t, cur, "per-subscriber subscription limit reached", func() { Subscribe(cross(cur), extra) }) // Cancelling one frees a slot. sid, ok := ActiveSubID(lastPlan, carol) if !ok { t.Fatal("expected an active sub to free") } testing.SetRealm(testing.NewUserRealm(carol)) testing.SetOriginCaller(carol) testing.SetOriginSend(nil) Cancel(cross(cur), sid) subscribeAs(cur, carol, extra, 1) testing.SetRealm(testing.NewUserRealm(provQ)) testing.SetOriginCaller(provQ) testing.SetOriginSend(nil) RetirePlan(cross(cur), extra) } func TestClaimsAndFees(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SetFeeBps(cross(cur), 500) // 5% into the next plan id := planAs(cur, prov1, "money", 1000, 50) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SetFeeBps(cross(cur), 0) b := begin() subscribeAs(cur, alice, id, 1000) if FeesAccrued()-b.fees != 50 { t.Fatalf("fee = %d, want 50", FeesAccrued()-b.fees) } credited := BalanceOf(prov1) // Mallory has no balance: claims fail. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "insufficient balance", func() { Claim(cross(cur), 1) }) testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "nothing to claim", func() { ClaimAll(cross(cur)) }) // Provider claims all; coins actually move. before := coinsOf(prov1) testing.SetRealm(testing.NewUserRealm(prov1)) testing.SetOriginCaller(prov1) testing.SetOriginSend(nil) ClaimAll(cross(cur)) if coinsOf(prov1)-before != credited { t.Fatalf("provider received %d, want %d", coinsOf(prov1)-before, credited) } if BalanceOf(prov1) != 0 { t.Fatal("balance not drained") } // Fees: only the recipient, and only the pot. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "fee recipient only", func() { WithdrawFees(cross(cur)) }) potBefore := coinsOf(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) WithdrawFees(cross(cur)) if coinsOf(testAdmin)-potBefore == 0 { t.Fatal("fee pot did not move") } testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) }) b.conserved(t, 0) } func TestSweepReserve(cur realm, t *testing.T) { id := planAs(cur, prov1, "sweep", 500, 50) begin() subscribeAs(cur, bob, id, 500) // Nothing above the reserve: sweep refuses. testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "no surplus to sweep", func() { SweepDenom(cross(cur), Denom) }) // Out-of-band surplus lands; only the surplus is sweepable. testing.IssueCoins(Address(), ugnot(77)) liabBefore := Liabilities() got := coinsOf(testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) SweepDenom(cross(cur), Denom) if coinsOf(testAdmin)-got != 77 { t.Fatalf("swept %d, want 77", coinsOf(testAdmin)-got) } if Liabilities() != liabBefore { t.Fatal("sweep touched the reserve") } testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "fee recipient only", func() { SweepDenom(cross(cur), Denom) }) } func TestAdminHandover(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { TransferAdmin(cross(cur), mallory) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) TransferAdmin(cross(cur), prov2) // Wrong acceptor refused; authority has not moved yet. testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginCaller(mallory) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "only the staged successor may accept", func() { AcceptAdmin(cross(cur)) }) if Admin() != testAdmin { t.Fatal("authority moved before acceptance") } testing.SetRealm(testing.NewUserRealm(prov2)) testing.SetOriginCaller(prov2) testing.SetOriginSend(nil) AcceptAdmin(cross(cur)) if Admin() != prov2 { t.Fatal("authority did not move") } // The former admin is locked out. testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "admin only", func() { SetFeeBps(cross(cur), 1) }) // Hand it back for the remaining tests. testing.SetRealm(testing.NewUserRealm(prov2)) testing.SetOriginCaller(prov2) testing.SetOriginSend(nil) TransferAdmin(cross(cur), testAdmin) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) AcceptAdmin(cross(cur)) } func TestFeeBounds(cur realm, t *testing.T) { testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "fee must be in", func() { SetFeeBps(cross(cur), MaxFeeBps+1) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "fee must be in", func() { SetFeeBps(cross(cur), -1) }) var zero address testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "fee recipient must not be the zero address", func() { SetFeeRecipient(cross(cur), zero) }) testing.SetRealm(testing.NewUserRealm(testAdmin)) testing.SetOriginCaller(testAdmin) testing.SetOriginSend(nil) uassert.AbortsWithMessage(t, cur, "successor must not be the zero address", func() { TransferAdmin(cross(cur), zero) }) } func TestStraySendRejected(cur realm, t *testing.T) { id := planAs(cur, prov1, "stray", 100, 50) subID := subscribeAs(cur, alice, id, 100) arm := func(from address) { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginCaller(from) testing.SetOriginSend(ugnot(3)) } arm(prov1) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { RetirePlan(cross(cur), id) }) arm(alice) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { Cancel(cross(cur), subID) }) arm(mallory) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { Expire(cross(cur), subID) }) arm(prov1) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { ClaimAll(cross(cur)) }) arm(testAdmin) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { WithdrawFees(cross(cur)) }) arm(testAdmin) uassert.AbortsWithMessage(t, cur, "this function does not accept coins", func() { SetFeeBps(cross(cur), 0) }) } func TestRenderSanitizes(cur realm, t *testing.T) { planAs(cur, prov1, "[evil](https://x) <script>alert(1)</script>", 1, MinPeriodBlocks) out := Render("") if strings.Contains(out, "<script>") { t.Fatal("unsanitized title reached Render") } if !strings.Contains(out, "plans:") { t.Fatal("render missing counters") } if Render("nope") == renderHome() { t.Fatal("unknown path fell through to home") } } func TestUnknownIDs(t *testing.T) { defer func() { if recover() == nil { t.Fatal("expected panic") } }() PlanInfo(99999) } // Y1 regression: EntitledFor follows the most recent subscription and // honors paid entitlement across cancellation. func TestEntitledForSurvivesCancel(cur realm, t *testing.T) { id := planAs(cur, prov1, "y1", 100, 50) subID := subscribeAs(cur, alice, id, 100) testing.SetRealm(testing.NewUserRealm(alice)) testing.SetOriginCaller(alice) testing.SetOriginSend(nil) Cancel(cross(cur), subID) if !EntitledFor(id, alice) { t.Fatal("cancelled-but-paid subscriber lost EntitledFor") } // A fresh Subscribe repoints the surface at the new subscription. // Skip a few blocks first so ptNew > ptOld: at a height in // [ptOld, ptNew) a STALE pointer would answer false while the // repointed one answers true — the distinguishing assertion. _, _, _, _, ptOld, _, _ := SubInfo(subID) testing.SkipHeights(5) newID := subscribeAs(cur, alice, id, 100) _, _, _, _, ptNew, _, _ := SubInfo(newID) if ptNew <= ptOld { t.Fatal("fixture broken: ptNew must exceed ptOld") } if !EntitledFor(id, alice) || !Entitled(newID) || !Entitled(subID) { t.Fatal("both paid entitlements should be live right after re-subscribe") } testing.SkipHeights(ptOld - runtime.ChainHeight()) if Entitled(subID) { t.Fatal("old span should have run out") } if !EntitledFor(id, alice) { t.Fatal("EntitledFor answered the stale id, not the repointed one") } // After the new paid span runs out too, EntitledFor goes false. testing.SkipHeights(ptNew - runtime.ChainHeight()) if EntitledFor(id, alice) { t.Fatal("EntitledFor true past paidThrough") } } // Y2 companion: a lapsed-in-grace incumbent blocks Subscribe with a // message that states both options. func TestSubscribeMessageInGrace(cur realm, t *testing.T) { id := planAs(cur, prov1, "y2msg", 100, 50) subID := subscribeAs(cur, bob, id, 100) _, _, _, _, pt, _, _ := SubInfo(subID) testing.SkipHeights(pt + 10 - runtime.ChainHeight()) testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(ugnot(100)) testing.IssueCoins(Address(), ugnot(100)) uassert.AbortsContains(t, cur, "lapsed but renewable", func() { Subscribe(cross(cur), id) }) // Cancel + Subscribe is the documented fresh-start path. testing.SetRealm(testing.NewUserRealm(bob)) testing.SetOriginCaller(bob) testing.SetOriginSend(nil) Cancel(cross(cur), subID) subscribeAs(cur, bob, id, 100) }
- #8/gno.MemPackageType
- #9 MPUserAll
Result log
msg:0,success:true,log:,events:[]