1// Package subscriptions is a multi-provider subscription hub: providers2// publish plans priced per billing period; subscribers pay per period,3// with deterministic renewal, grace, expiration and cancellation rules;4// other realms and off-chain services gate access on Entitled /5// EntitledFor, which is the integration surface this realm exists to6// provide.7//8// THE BILLING MODEL, COMPLETELY:9//10// Subscribe (payable, exact price) : creates the subscription, pays11// period 1. paidThrough = height12// + periodBlocks.13// Renew (payable, exact price) : extends paidThrough by exactly14// one periodBlocks, FROM15// paidThrough — never from the16// current height — so period17// boundaries are fixed at18// Subscribe time and never drift.19// Entitlement : height < paidThrough. Nothing20// else. Status does not enter21// into it: a cancelled22// subscription stays entitled to23// what it already paid for.24// Renewal window : a renewal is accepted iff BOTH25// paidThrough - height <= periodBlocks (early bound)26// height < paidThrough + periodBlocks (late bound)27// The early bound caps prepayment28// at one full unstarted period —29// a second Renew straight after a30// first is refused, which is what31// makes an accidental duplicate32// payment structurally impossible33// rather than merely unlikely.34// The late bound is the grace35// window: renewing after lapse36// extends from paidThrough, so it37// back-pays the lapsed span to38// keep the original schedule and39// buys paidThrough + periodBlocks40// - height further blocks — always41// at least one, because the bound42// is exclusive (audit finding Y2).43// A lapsed subscriber who prefers44// a fresh full period may Cancel45// and Subscribe again at the same46// total price; Subscribe's refusal47// message states both options.48// Expire (permissionless valve) : once height >= paidThrough +49// periodBlocks, anyone may mark50// the subscription Expired. No51// funds move — every payment52// settled when it was made. The53// valve exists so the54// plan|subscriber slot frees55// without depending on either56// party, and Subscribe itself57// collapses an expired incumbent,58// so a fresh start never depends59// on housekeeping having run. The60// renewable and expirable height61// sets partition exactly: no62// height is in both or neither.63// Cancel (subscriber only) : Active -> Cancelled. Terminal.64// No refund — payments settle to65// the provider at payment time,66// and what was bought (entitlement67// through paidThrough) stays68// bought. What cancellation ends69// is the OBLIGATION: a Cancelled70// subscription can never be71// renewed, by the subscriber or72// anyone else.73// RetirePlan (provider only) : no new Subscribes, no renewals.74// Existing entitlements run to75// paidThrough untouched. Refusing76// renewals on a retired plan is77// subscriber protection: nobody78// can keep paying for a service79// whose provider announced its80// end.81//82// The obligation is therefore explicit on chain at every moment: a83// subscription owes nothing (there is no pull payment and no debt — a84// lapse simply ends entitlement), and the realm owes the subscriber85// exactly `paidThrough - height` blocks of entitlement, queryable by86// anyone via PaidThrough / Entitled / EntitledFor.87//88// WHO PAYS WHOM. Payments settle immediately: price - fee is credited89// to the provider's claimable balance, fee to the protocol pot, both90// inside the same feeledger the sibling realms use. There is no escrow:91// H == U + F at all times (plus out-of-band surplus, recoverable by92// SweepDenom above the Liabilities reserve). The renewal caller must be93// the subscriber — a third party cannot extend someone else's94// subscription, which closes both a consent problem (an unwanted gift95// re-arms a lapsing obligation) and a griefing edge (spending pennies96// to keep a victim's slot occupied).97//98// FEES follow the house pattern exactly: a compile-time MaxFeeBps99// ceiling, the current fee snapshotted into the PLAN at CreatePlan100// (provider consents via its own maxFeeBps argument), copied into the101// subscription at Subscribe, and charged at every payment from the102// PROVIDER's side. A later SetFeeBps touches only plans created103// afterwards; no existing plan or subscription can have its fee moved104// by anyone.105//106// REALM-CALLER CAVEAT, inherited from the siblings verbatim: coinio's107// receipt guard admits only EOA payers, so subscribers are EOAs;108// assertNoSend reads the ORIGIN envelope, so every non-payable function109// refuses any transaction that attached coins anywhere. Providers may110// be EOAs or realms, but a realm provider must expose its own crossing111// path to Claim, or what it earns is stranded (see RegisterService's112// caveat in service_market — the same three obligations apply).113package subscriptions114115import (116 "chain"117 "chain/runtime"118 "chain/runtime/unsafe"119 "strconv"120121 "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio"122 "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger"123 "gno.land/p/nt/avl/v0"124 "gno.land/p/nt/markdown/sanitize/v0"125)126127// Denom is the only asset this realm accepts.128const Denom = "ugnot"129130// MaxFeeBps is the hard protocol-fee cap: 1000 bps = 10%.131const MaxFeeBps = int64(1000)132133// Plan status values.134const (135 PlanActive = "active"136 PlanRetired = "retired"137)138139// Subscription status values. Cancelled and Expired are terminal.140const (141 SubActive = "active"142 SubCancelled = "cancelled"143 SubExpired = "expired"144)145146// Input bounds.147const (148 MaxTitleLen = 80149 MaxDescLen = 2000150 MinPrice = int64(1)151152 // MinPeriodBlocks/MaxPeriodBlocks bound a plan's billing period:153 // ~40 seconds to ~1 year at pearl's observed ~4.2s blocks. The154 // floor keeps a hostile plan from turning renewal into a155 // per-block treadmill; the ceiling keeps paidThrough arithmetic156 // far from overflow even at maximum prepayment.157 MinPeriodBlocks = int64(10)158 MaxPeriodBlocks = int64(7500000)159160 // MaxPlansPerProvider bounds catalog monopolization by a single161 // address — the finding that was RED in permission_registry and162 // service_registry, carried from the start here.163 MaxPlansPerProvider = 20164165 // MaxSubsPerSubscriber bounds one account's open-subscription166 // state. Terminal subscriptions free their slot.167 MaxSubsPerSubscriber = 100168169 // RenderLimit bounds every rendered list — an unbounded Render170 // was YELLOW in three prior audits.171 RenderLimit = 20172)173174type plan struct {175 id int64176 provider address177 title string178 description string179 price int64180 periodBlocks int64181 feeBps int64 // snapshotted at creation, charged at every payment182 status string183 subs int64 // lifetime count, never decremented184}185186type sub struct {187 id int64188 planID int64189 subscriber address190 provider address // copied at Subscribe; never re-read from the plan191 price int64 // copied at Subscribe; a plan is immutable anyway192 periodBlocks int64 // copied at Subscribe193 feeBps int64 // copied at Subscribe from the plan's snapshot194 status string195 paidThrough int64 // absolute height; entitlement = height < paidThrough196 periods int64 // lifetime paid-period count197}198199var (200 admin address // may set fee, fee recipient, stage successor201 pendingAdmin address // staged by TransferAdmin, completes via AcceptAdmin202 feeRecipient address // may withdraw fees and sweep surplus203 feeBps int64 // fee snapshotted into NEW plans204205 self address // this realm's address, captured at deploy206 nextPlan int64207 nextSub int64208209 plans = avl.NewTree() // padID(id) -> *plan210 subs = avl.NewTree() // padID(id) -> *sub211 activeByKey = avl.NewTree() // padID(planID)|subscriber -> int64 sub id, while Active212 latestByKey = avl.NewTree() // padID(planID)|subscriber -> most recent sub id, never removed213 providerNum = avl.NewTree() // address -> *int64, live plans per provider214 subNum = avl.NewTree() // address -> *int64, Active subs per subscriber215 ledger = feeledger.MustNew(MaxFeeBps)216)217218func init() {219 admin = unsafe.OriginCaller()220 feeRecipient = admin221 self = unsafe.CurrentRealm().Address()222}223224// --- provider side ---225226// CreatePlan publishes a subscription plan and returns its id. No coins227// are accepted; the storage deposit the caller pays is the anti-spam.228// The current protocol fee is snapshotted into the plan and must not229// exceed maxFeeBps, the ceiling the provider signed for — pass230// MaxFeeBps to accept any legal fee. Plans are immutable once created:231// price and period changes are a new plan, so nothing a subscriber232// agreed to can move underneath them.233func CreatePlan(cur realm, title, description string, price, periodBlocks, maxFeeBps int64) int64 {234 assertNoSend()235 provider := cur.Previous().Address()236237 if title == "" || len(title) > MaxTitleLen {238 panic("title must be 1.." + itoa(int64(MaxTitleLen)) + " bytes")239 }240 if len(description) > MaxDescLen {241 panic("description too long")242 }243 if price < MinPrice {244 panic("price must be at least " + itoa(MinPrice) + Denom)245 }246 if periodBlocks < MinPeriodBlocks || periodBlocks > MaxPeriodBlocks {247 panic("periodBlocks must be in [" + itoa(MinPeriodBlocks) + ", " +248 itoa(MaxPeriodBlocks) + "]")249 }250 if feeBps > maxFeeBps {251 panic("current fee " + itoa(feeBps) + " bps exceeds the caller's maximum " +252 itoa(maxFeeBps))253 }254 n := counter(providerNum, provider)255 if *n >= MaxPlansPerProvider {256 panic("per-provider plan limit reached")257 }258259 id := nextPlan260 nextPlan++261 plans.Set(padID(id), &plan{262 id: id,263 provider: provider,264 title: title,265 description: description,266 price: price,267 periodBlocks: periodBlocks,268 feeBps: feeBps,269 status: PlanActive,270 })271 *n++272273 chain.Emit("PlanCreated",274 "planId", itoa(id),275 "provider", provider.String(),276 "price", itoa(price),277 "periodBlocks", itoa(periodBlocks),278 "feeBps", itoa(feeBps),279 )280 return id281}282283// RetirePlan takes a plan off the market: no new subscriptions and no284// renewals. Provider only. Existing entitlements run to their285// paidThrough untouched; refusing renewals is subscriber protection —286// nobody keeps paying for a service whose provider announced its end.287// The provider's plan-quota slot frees.288func RetirePlan(cur realm, planID int64) {289 assertNoSend()290 caller := cur.Previous().Address()291 p := mustGetPlan(planID)292 if caller != p.provider {293 panic("only the provider may retire a plan")294 }295 if p.status != PlanActive {296 panic("plan is not active")297 }298 p.status = PlanRetired299 *counter(providerNum, p.provider)--300 chain.Emit("PlanRetired", "planId", itoa(planID), "provider", p.provider.String())301}302303// --- subscriber side ---304305// Subscribe pays for the first billing period of a plan and returns the306// new subscription id. The transaction must attach EXACTLY the plan's307// price in ugnot — over- and underpayment are both refused, so a308// mistaken double-attach cannot silently become a donation. One309// subscriber holds at most one live subscription per plan: if an Active310// one exists the call is refused (the renewal path is Renew, never a311// second Subscribe — that is the duplicate-payment guard at the312// identity level); an incumbent past its grace window is collapsed to313// Expired in place, so a fresh start never waits on housekeeping.314//315// The payment settles immediately: price minus the plan's snapshotted316// fee to the provider's claimable balance, fee to the protocol pot.317// Entitlement runs from this block: paidThrough = height + periodBlocks.318func Subscribe(cur realm, planID int64) int64 {319 subscriber, amount := coinio.Receive(0, cur, Denom)320 p := mustGetPlan(planID)321 if p.status != PlanActive {322 panic("plan is not active")323 }324 if amount != p.price {325 panic("send exactly the plan price: " + itoa(p.price) + Denom)326 }327328 key := padID(planID) + "|" + subscriber.String()329 if v := activeByKey.Get(key); v != nil {330 inc := mustGetSub(v.(int64))331 if runtime.ChainHeight() >= inc.paidThrough+inc.periodBlocks {332 expireInPlace(inc)333 } else if runtime.ChainHeight() >= inc.paidThrough {334 panic("subscription " + itoa(inc.id) + " to this plan is lapsed but renewable: " +335 "Renew back-pays the lapsed span and keeps the schedule; " +336 "Cancel then Subscribe restarts fresh at the same price")337 } else {338 panic("an active subscription to this plan already exists: renew it instead")339 }340 }341 n := counter(subNum, subscriber)342 if *n >= MaxSubsPerSubscriber {343 panic("per-subscriber subscription limit reached")344 }345346 ledger.MustDeposit(p.provider.String(), amount, p.feeBps)347348 id := nextSub349 nextSub++350 subs.Set(padID(id), &sub{351 id: id,352 planID: planID,353 subscriber: subscriber,354 provider: p.provider,355 price: p.price,356 periodBlocks: p.periodBlocks,357 feeBps: p.feeBps,358 status: SubActive,359 paidThrough: runtime.ChainHeight() + p.periodBlocks,360 periods: 1,361 })362 activeByKey.Set(key, id)363 latestByKey.Set(key, id)364 *n++365 p.subs++366367 chain.Emit("Subscribed",368 "subId", itoa(id),369 "planId", itoa(planID),370 "subscriber", subscriber.String(),371 "provider", p.provider.String(),372 "amount", itoa(amount),373 "paidThrough", itoa(runtime.ChainHeight()+p.periodBlocks),374 )375 return id376}377378// Renew pays for the next billing period of the caller's own379// subscription. The transaction must attach exactly the subscription's380// price. The renewal window is deterministic and stated in the header:381// accepted iff paidThrough - height <= periodBlocks (at most one full382// unstarted period prepaid — the duplicate-payment bound) and height <383// paidThrough + periodBlocks (the grace bound, exclusive — a renewal384// always buys at least one block). Extension is always385// FROM paidThrough, so period boundaries never drift, and a renewal386// inside grace covers the lapsed span — that is the price of keeping387// the original schedule, and it is the documented, deterministic388// choice.389func Renew(cur realm, subID int64) {390 payer, amount := coinio.Receive(0, cur, Denom)391 s := mustGetSub(subID)392 if payer != s.subscriber {393 panic("only the subscriber may renew")394 }395 if s.status != SubActive {396 panic("subscription is " + s.status)397 }398 p := mustGetPlan(s.planID)399 if p.status != PlanActive {400 panic("plan is retired; the paid period runs to its end but cannot be renewed")401 }402 if amount != s.price {403 panic("send exactly the subscription price: " + itoa(s.price) + Denom)404 }405 h := runtime.ChainHeight()406 if s.paidThrough-h > s.periodBlocks {407 panic("too early: at most one unstarted period may be prepaid; renewable from height " +408 itoa(s.paidThrough-s.periodBlocks))409 }410 if h >= s.paidThrough+s.periodBlocks {411 panic("grace window over; the subscription is expirable, subscribe afresh")412 }413414 ledger.MustDeposit(s.provider.String(), amount, s.feeBps)415 s.paidThrough += s.periodBlocks416 s.periods++417418 chain.Emit("Renewed",419 "subId", itoa(subID),420 "subscriber", s.subscriber.String(),421 "amount", itoa(amount),422 "paidThrough", itoa(s.paidThrough),423 )424}425426// Cancel ends the caller's own subscription. Terminal: it can never be427// renewed afterwards, by anyone. No refund and no funds move — every428// payment settled when it was made, and the entitlement already bought429// (height < paidThrough) remains until it runs out. The plan slot and430// the subscriber's quota slot free immediately.431func Cancel(cur realm, subID int64) {432 assertNoSend()433 caller := cur.Previous().Address()434 s := mustGetSub(subID)435 if caller != s.subscriber {436 panic("only the subscriber may cancel")437 }438 if s.status != SubActive {439 panic("subscription is " + s.status)440 }441 s.status = SubCancelled442 releaseSlots(s)443 chain.Emit("Cancelled",444 "subId", itoa(subID),445 "subscriber", s.subscriber.String(),446 "paidThrough", itoa(s.paidThrough),447 )448}449450// Expire marks a lapsed subscription Expired once its grace window is451// over: height >= paidThrough + periodBlocks. Permissionless by design —452// like the sibling realms' valves, no slot's liveness may depend on453// either party showing up. No funds move.454func Expire(cur realm, subID int64) {455 assertNoSend()456 s := mustGetSub(subID)457 if s.status != SubActive {458 panic("subscription is " + s.status)459 }460 if runtime.ChainHeight() < s.paidThrough+s.periodBlocks {461 panic("not expirable: grace runs through height " +462 itoa(s.paidThrough+s.periodBlocks-1))463 }464 expireInPlace(s)465}466467// --- payouts ---468469// Claim sends amount ugnot of the caller's claimable balance back to470// the caller. Providers earn into this balance at every payment.471func Claim(cur realm, amount int64) {472 assertNoSend()473 caller := cur.Previous().Address()474 if err := ledger.Withdraw(caller.String(), amount); err != nil {475 panic(err)476 }477 coinio.Payout(0, cur, caller, Denom, amount)478 chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount))479}480481// ClaimAll sends the caller's entire claimable balance back to the482// caller. Fails if there is nothing to claim.483func ClaimAll(cur realm) {484 assertNoSend()485 caller := cur.Previous().Address()486 amount, err := ledger.WithdrawAll(caller.String())487 if err != nil {488 panic(err)489 }490 if amount == 0 {491 panic("nothing to claim")492 }493 coinio.Payout(0, cur, caller, Denom, amount)494 chain.Emit("Claim", "to", caller.String(), "amount", itoa(amount))495}496497// WithdrawFees sends the accrued fee pot to the fee recipient. Only the498// fee recipient may call it, and only the pot moves.499func WithdrawFees(cur realm) {500 assertNoSend()501 if cur.Previous().Address() != feeRecipient {502 panic("fee recipient only")503 }504 amount := ledger.WithdrawFees()505 if amount == 0 {506 panic("no fees accrued")507 }508 coinio.Payout(0, cur, feeRecipient, Denom, amount)509 chain.Emit("FeesWithdrawn", "to", feeRecipient.String(), "amount", itoa(amount))510}511512// SweepDenom recovers out-of-band coins (sent by raw bank transfer,513// outside any entrypoint) to the fee recipient. For the ledger denom514// the reserve is Liabilities() — user balances and the fee pot are515// structurally unreachable. Fee recipient only.516func SweepDenom(cur realm, denom string) {517 assertNoSend()518 if cur.Previous().Address() != feeRecipient {519 panic("fee recipient only")520 }521 reserve := int64(0)522 if denom == Denom {523 reserve = ledger.Liabilities()524 }525 // coinio.Sweep itself aborts when nothing sits above the reserve.526 swept := coinio.Sweep(0, cur, feeRecipient, denom, reserve)527 chain.Emit("SurplusSwept", "to", feeRecipient.String(), "coins", itoa(swept)+denom)528}529530// --- administration ---531532// SetFeeBps sets the protocol fee snapshotted into FUTURE plans.533// Bounded by MaxFeeBps; existing plans and subscriptions are untouched534// — their fee was fixed the moment the provider consented to it.535func SetFeeBps(cur realm, bps int64) {536 assertNoSend()537 assertAdmin(cur.Previous().Address())538 if bps < 0 || bps > MaxFeeBps {539 panic("fee must be in [0, " + itoa(MaxFeeBps) + "] bps")540 }541 old := feeBps542 feeBps = bps543 chain.Emit("FeeChanged", "oldBps", itoa(old), "newBps", itoa(bps))544}545546// SetFeeRecipient points future fee withdrawals and sweeps at a new547// address. Admin only. The zero address is refused — it would strand548// the pot.549func SetFeeRecipient(cur realm, recipient address) {550 assertNoSend()551 assertAdmin(cur.Previous().Address())552 var zero address553 if recipient == zero {554 panic("fee recipient must not be the zero address")555 }556 old := feeRecipient557 feeRecipient = recipient558 chain.Emit("FeeRecipientChanged", "old", old.String(), "new", recipient.String())559}560561// TransferAdmin stages a two-step admin handover. The successor holds562// nothing until AcceptAdmin.563func TransferAdmin(cur realm, successor address) {564 assertNoSend()565 assertAdmin(cur.Previous().Address())566 var zero address567 if successor == zero {568 panic("successor must not be the zero address")569 }570 pendingAdmin = successor571 chain.Emit("AdminTransferStaged", "from", admin.String(), "to", successor.String())572}573574// AcceptAdmin completes the handover; only the staged successor may.575func AcceptAdmin(cur realm) {576 assertNoSend()577 caller := cur.Previous().Address()578 if caller != pendingAdmin {579 panic("only the staged successor may accept")580 }581 old := admin582 admin = caller583 var zero address584 pendingAdmin = zero585 chain.Emit("AdminTransferred", "from", old.String(), "to", admin.String())586}587588// --- views ---589590// Entitled reports whether the subscription's paid entitlement covers591// the current block: height < paidThrough. Status deliberately does not592// enter into it — a cancelled subscriber keeps what they paid for, and593// an expirable-but-unexpired one has already lapsed here.594func Entitled(subID int64) bool {595 s := mustGetSub(subID)596 return runtime.ChainHeight() < s.paidThrough597}598599// EntitledFor reports whether subscriber currently holds paid600// entitlement to planID, through their MOST RECENT subscription to it.601// This is the one-call integration surface for other realms and602// services, and it honors the entitlement contract across status: a603// cancelled subscription keeps answering true until its paidThrough —604// what was bought stays bought (audit finding Y1). One self-inflicted605// edge is fail-closed: cancelling a prepaid subscription and606// re-subscribing at once points this surface at the NEW, earlier607// paidThrough; the old subscription's remaining span stays queryable608// per-id via Entitled.609func EntitledFor(planID int64, subscriber address) bool {610 v := latestByKey.Get(padID(planID) + "|" + subscriber.String())611 if v == nil {612 return false613 }614 return Entitled(v.(int64))615}616617// ActiveSubID returns the caller-facing id of subscriber's live618// subscription to planID, or (0, false) if none is Active.619func ActiveSubID(planID int64, subscriber address) (int64, bool) {620 v := activeByKey.Get(padID(planID) + "|" + subscriber.String())621 if v == nil {622 return 0, false623 }624 return v.(int64), true625}626627// PlanInfo returns a plan's public fields.628func PlanInfo(planID int64) (provider address, title string, price, periodBlocks, planFeeBps int64, status string, lifetimeSubs int64) {629 p := mustGetPlan(planID)630 return p.provider, p.title, p.price, p.periodBlocks, p.feeBps, p.status, p.subs631}632633// SubInfo returns a subscription's public fields.634func SubInfo(subID int64) (planID int64, subscriber, provider address, price, paidThrough, periods int64, status string) {635 s := mustGetSub(subID)636 return s.planID, s.subscriber, s.provider, s.price, s.paidThrough, s.periods, s.status637}638639// PaidThrough returns the absolute height a subscription is paid to.640func PaidThrough(subID int64) int64 { return mustGetSub(subID).paidThrough }641642// RenewableFrom returns the earliest height at which Renew will accept643// a payment for this subscription, and the last height at which it644// still will (inclusive) — the deterministic window, precomputed for645// integrators. From until+1 the subscription is expirable instead; the646// two sets partition exactly.647func RenewableFrom(subID int64) (from, until int64) {648 s := mustGetSub(subID)649 return s.paidThrough - s.periodBlocks, s.paidThrough + s.periodBlocks - 1650}651652func Admin() address { return admin }653func PendingAdmin() address { return pendingAdmin }654func FeeRecipient() address { return feeRecipient }655func FeeBps() int64 { return feeBps }656func NumPlans() int64 { return nextPlan }657func NumSubs() int64 { return nextSub }658func UsersTotal() int64 { return ledger.UsersTotal() }659func FeesAccrued() int64 { return ledger.FeesAccrued() }660func Liabilities() int64 { return ledger.Liabilities() }661func BalanceOf(a address) int64 { return ledger.BalanceOf(a.String()) }662func Address() address { return self }663func Held() int64 { return coinio.HeldAt(self, Denom) }664665// --- render ---666667func Render(path string) string {668 if path == "" {669 return renderHome()670 }671 return "unknown page; try the realm root"672}673674func renderHome() string {675 out := "# subscriptions\n\n"676 out += "Provider-published plans, per-period payment, deterministic " +677 "renewal, grace, expiration and cancellation. Entitlement is " +678 "`height < paidThrough`, queryable by anyone.\n\n"679 out += "- plans: " + itoa(nextPlan) + "\n"680 out += "- subscriptions: " + itoa(nextSub) + "\n"681 out += "- provider balances: " + itoa(ledger.UsersTotal()) + Denom + "\n"682 out += "- fees accrued: " + itoa(ledger.FeesAccrued()) + Denom + "\n"683 out += "- current fee for new plans: " + itoa(feeBps) + " bps (cap " +684 itoa(MaxFeeBps) + ")\n\n"685686 if nextPlan == 0 {687 return out + "No plans yet.\n"688 }689 out += "## newest plans\n\n"690 shown := int64(0)691 for id := nextPlan - 1; id >= 0 && shown < RenderLimit; id-- {692 p := mustGetPlan(id)693 out += "- #" + itoa(p.id) + " **" + sanitize.InlineText(p.title) +694 "** — " + itoa(p.price) + Denom + " / " + itoa(p.periodBlocks) +695 " blocks, " + p.status + ", provider `" + p.provider.String() +696 "`, " + itoa(p.subs) + " lifetime subs\n"697 shown++698 }699 if nextPlan > shown {700 out += "\n(" + itoa(nextPlan-shown) + " older plans not shown)\n"701 }702 return out703}704705// --- internals ---706707// expireInPlace flips an Active subscription to Expired and frees its708// slots. Callers have already established expirability.709func expireInPlace(s *sub) {710 s.status = SubExpired711 releaseSlots(s)712 chain.Emit("SubscriptionExpired",713 "subId", itoa(s.id),714 "subscriber", s.subscriber.String(),715 "paidThrough", itoa(s.paidThrough),716 )717}718719// releaseSlots removes the plan|subscriber activity index entry and720// decrements the subscriber's quota counter. Exactly once per terminal721// transition, which both terminal paths guarantee by requiring722// SubActive first.723func releaseSlots(s *sub) {724 activeByKey.Remove(padID(s.planID) + "|" + s.subscriber.String())725 *counter(subNum, s.subscriber)--726}727728func assertNoSend() {729 if len(unsafe.OriginSend()) != 0 {730 panic("this function does not accept coins")731 }732}733734func assertAdmin(caller address) {735 if caller != admin {736 panic("admin only")737 }738}739740func mustGetPlan(id int64) *plan {741 v := plans.Get(padID(id))742 if v == nil {743 panic("unknown plan id")744 }745 return v.(*plan)746}747748func mustGetSub(id int64) *sub {749 v := subs.Get(padID(id))750 if v == nil {751 panic("unknown subscription id")752 }753 return v.(*sub)754}755756// counter returns the persistent per-address counter in tree,757// allocating a zero on first use.758func counter(tree *avl.Tree, a address) *int64 {759 k := a.String()760 if v := tree.Get(k); v != nil {761 return v.(*int64)762 }763 n := new(int64)764 tree.Set(k, n)765 return n766}767768func padID(id int64) string {769 s := strconv.FormatInt(id, 10)770 for len(s) < 12 {771 s = "0" + s772 }773 return s774}775776func itoa(n int64) string { return strconv.FormatInt(n, 10) }777AcceptAdmin(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.