Skip to content

Latest commit

 

History

History
199 lines (157 loc) · 7.33 KB

File metadata and controls

199 lines (157 loc) · 7.33 KB
title Subscriptions
description The subscription lifecycle on Vowena - creation, state machine, status transitions, allowance calculations, and the one-signature authorization model.

What is a Subscription?

A subscription is the on-chain record linking a subscriber to a plan. It tracks billing state, lifecycle status, and timing. Each subscription is its own persistent ledger entry, enabling parallel processing across the network.


State Machine

Every subscription moves through a defined set of states. Transitions are enforced by the contract - there is no way to skip a state or force an invalid transition.

From To Trigger
- Active subscribe() called
Active Paused Charge fails and grace period expires
Active Expired max_periods reached during charge
Active Cancelled Subscriber calls cancel()
Paused Active Subscriber calls reactivate() (funds now available)
Paused Cancelled One more billing period passes while paused, or subscriber calls cancel()
**Paused** is a recoverable state. If a subscriber tops up their wallet and calls `reactivate()`, the subscription returns to Active and billing resumes. But if another full billing period passes while paused, the next `charge()` call will transition it to Cancelled permanently. There is no transition from **Cancelled** or **Expired** back to any other state. These are terminal. The subscriber must create a new subscription if they want to resume.

Creating a Subscription

When a subscriber calls subscribe(), several things happen in a single atomic transaction:

The subscriber signs the transaction. Soroban's **auth tree** means the subscriber signs **once** - this single signature covers both the `subscribe()` contract call and the `token.approve()` call nested inside it. No separate approval transaction needed. The contract loads the plan and verifies: - The plan exists - The plan is `active == true` - The subscriber is not the merchant (cannot subscribe to your own plan) The contract calculates how much token allowance to request:
```
allowance = price_ceiling * effective_periods
```

Where `effective_periods` is:
- `max_periods` if the plan has a limit, or
- `120` if the plan is unlimited (`max_periods == 0`)

The allowance expiry is set to the **maximum Soroban allows**: approximately 6,312,000 ledgers (~347 days at 5-second block times).
The contract calls `token.approve(subscriber, contract_address, allowance, expiry_ledger)`. This grants the Vowena contract permission to pull up to `allowance` tokens from the subscriber over the approval period. The contract increments `NextSubId`, creates the `Subscription` struct with `status: Active`, and writes it to persistent storage. The subscription ID is appended to both `SubscriberSubs(subscriber)` and `PlanSubs(plan_id)` indexes. A `SubscriptionCreated` event fires with the full subscription struct. The allowance is based on `price_ceiling`, not the current `amount`. This is intentional - it ensures that if the merchant raises the price (within the ceiling), existing subscribers do not need to re-authorize. The subscriber sees the ceiling amount in their wallet's approval prompt.

Allowance Calculation Examples

**Plan:** 10 USDC/month, ceiling 15 USDC, max 12 periods
```
allowance = 15 USDC * 12 = 180 USDC
expiry    = current_ledger + 6,312,000 (~347 days)
```

The subscriber's wallet will show: "Approve 180 USDC for Vowena contract".
**Plan:** 5 USDC/month, ceiling 8 USDC, max 0 (unlimited)
```
allowance = 8 USDC * 120 = 960 USDC
expiry    = current_ledger + 6,312,000 (~347 days)
```

After the allowance expires (~347 days), the subscriber would need to re-approve. The contract handles this gracefully - a charge will fail, the subscriber can re-approve and reactivate.
**Plan:** 20 USDC/month, ceiling 25 USDC, max 12, trial 2 periods
```
allowance = 25 USDC * 12 = 300 USDC
```

The allowance covers all 12 periods including trials. During the 2 trial periods, `charge()` advances the counter but does not transfer tokens. The allowance is not consumed during trials.

Cancellation

Cancellation is always available to the subscriber and is immediate:

const tx = await client.cancel({
  subscriber: subscriberKeypair.publicKey(),
  sub_id: 42n,
});
Only the subscriber can cancel their own subscription. The contract calls `subscriber.require_auth()`. Merchants cannot cancel on behalf of subscribers. The remaining token allowance stays until it expires naturally on the ledger. The contract will never use it after cancellation because the subscription status is `Cancelled` and `charge()` checks status before attempting any transfer. Cancellation does not trigger an automatic refund. The current period is already paid. Merchants can issue voluntary [refunds](/protocol/refunds) separately.

Reactivation

If a subscription is Paused (charge failed, grace expired), the subscriber can reactivate it:

The subscriber ensures they have sufficient token balance and that the contract's allowance has not expired. The subscriber calls `reactivate(sub_id)`. The contract verifies the subscription is in `Paused` status and transitions it back to `Active`. The `next_billing_time` is reset so that the next `charge()` call can process immediately. If the original token allowance has expired while the subscription was paused, the subscriber will need to create a new subscription instead. Reactivation only works if the allowance is still valid.

What's Next

The most important page - understand exactly what happens during a charge. How merchants move subscribers to new plans with explicit consent.