Contract reference

Three contracts. Dependencies point one way only: subscription calls plan-registry and vault; neither of those calls anything else in the suite. This page describes v0.2.0.

Construction

Every contract is configured by its constructor, which runs in the same transaction that deploys it. There is no initialize function, so there is no window between deploy and setup in which someone else could claim a contract.

Contract Constructor arguments
plan-registry admin: Address
vault admin: Address
subscription admin, plan_registry, vault: Address, fee_bps: u32, fee_to: Address

The subscription constructor fails if fee_bps > MAX_FEE_BPS (1000 = 10%). After both exist, the vault admin calls set_subscription to name the one contract allowed to debit the vault. scripts/deploy.sh does all of this.

Paged lists

Lists that grow without bound are stored one ledger entry per position plus a count, never as a single growing vector. That keeps subscribe and create_plan at a constant cost, and stops anyone from blocking a merchant by opening throwaway mandates until a list outgrows the ledger’s entry size limit.

Each list has a *_count function and a getter that takes start and limit and returns ids oldest first. A page holds at most 50 ids (MAX_PAGE), because one transaction may touch at most 100 ledger entries. To read the newest entries, read the count and then the last page:

count = merchant_mandate_count(merchant)          // e.g. 130
merchant_mandates(merchant, count - 50, 50)       // positions 80..129

plan-registry

Owns the catalogue of merchant plans.

Function Parameters Returns Auth Events
create_plan merchant: Address, token: Address, amount: i128, period: u64, name: String u64 merchant PlanCreated
set_plan_active merchant: Address, plan_id: u64, active: bool () merchant, must own plan PlanStatusChanged
get_plan plan_id: u64 Plan none —
merchant_plan_count merchant: Address u32 none —
merchant_plans merchant: Address, start: u32, limit: u32 Vec<u64> none —
next_plan_id — u64 none —
admin — Address none —

create_plan rejects amount <= 0, any period below MIN_PERIOD (60 seconds), and any name longer than MAX_NAME_LEN (64 bytes, not characters — a multi-byte name can exceed the limit well under 64 characters). An empty name is valid; clients fall back to displaying the plan id.

Plan ids are sequential from 1, so next_plan_id() - 1 is the number of plans published, and a client can list the newest plans without an indexer.

Deactivating a plan stops new subscriptions. It does not end mandates already open against it; the merchant uses end_mandate for that.

vault

Holds subscriber funds. Custody is deliberately separate from billing so the two can be audited independently.

Function Parameters Returns Auth Events
set_subscription subscription: Address () admin —
deposit user: Address, token: Address, amount: i128 () user Deposit
withdraw user: Address, token: Address, amount: i128 () user Withdraw
debit user: Address, token: Address, to: Address, amount: i128 () the subscription contract Debit
balance user: Address, token: Address i128 none —
subscription — Address none —
admin — Address none —

debit calls require_auth() on the stored subscription address. That succeeds because a contract’s direct call to another contract is implicitly authorized by the host. No other caller can satisfy it.

Balances are never locked. withdraw always succeeds up to the full balance, which is how a subscriber starves a mandate they no longer want to fund.

subscription

The billing engine.

Function Parameters Returns Auth Events
subscribe subscriber: Address, plan_id: u64, max_charges: u32 u64 subscriber Subscribed
charge mandate_id: u64 () none — permissionless Charged, maybe MandateCompleted
cancel subscriber: Address, mandate_id: u64 () subscriber Cancelled
end_mandate merchant: Address, mandate_id: u64 () merchant of the mandate MandateEnded
set_paused subscriber: Address, mandate_id: u64, paused: bool () subscriber PauseChanged
set_fee_bps new_fee_bps: u32 () admin FeeChanged
get_mandate mandate_id: u64 Mandate none —
is_due mandate_id: u64 bool none —
subscriber_mandate_count subscriber: Address u32 none —
subscriber_mandates subscriber: Address, start: u32, limit: u32 Vec<u64> none —
merchant_mandate_count merchant: Address u32 none —
merchant_mandates merchant: Address, start: u32, limit: u32 Vec<u64> none —
fee_bps — u32 none —
admin, plan_registry, vault — Address none —

max_charges of 0 means open-ended.

end_mandate works on an Active or Paused mandate and sets it to Cancelled, permanently. It can only stop future charges.

set_fee_bps applies to mandates opened afterwards. Each mandate keeps the fee_bps it was opened with.

is_due reports schedule and status only. It does not check whether the vault can cover the charge.

Types

pub struct Plan {
    pub id: u64,
    pub merchant: Address,
    pub name: String,
    pub token: Address,
    pub amount: i128,
    pub period: u64,
    pub active: bool,
}

pub enum MandateStatus { Active, Paused, Cancelled, Completed }

pub struct Mandate {
    pub id: u64,
    pub subscriber: Address,
    pub plan_id: u64,
    pub merchant: Address,
    pub token: Address,
    pub amount: i128,
    pub period: u64,
    pub next_charge: u64,
    pub last_charge: u64,
    pub charges_made: u32,
    pub max_charges: u32,
    pub fee_bps: u32,
    pub status: MandateStatus,
}

A MandateStatus read from contract state decodes as a one-element vector, for example ["Active"] from scValToNative, not as a bare string.

Events

All events use the #[contractevent] macro. Topic 0 is the snake_case event name; #[topic] fields follow; remaining fields are a map in the data section.

Event Topics Data
PlanCreated merchant, plan_id token, amount, period, name
PlanStatusChanged merchant, plan_id active
Deposit user, token amount, balance
Withdraw user, token amount, balance
Debit user, token, to amount, balance
Subscribed mandate_id, subscriber, merchant plan_id, amount, period, next_charge, max_charges, fee_bps
Charged mandate_id, subscriber, merchant amount, fee, charges_made, next_charge
Cancelled mandate_id, subscriber charges_made
MandateEnded mandate_id, merchant subscriber, charges_made
PauseChanged mandate_id, subscriber paused
MandateCompleted mandate_id, subscriber charges_made
FeeChanged admin old_fee_bps, new_fee_bps

Note that Charged.amount is the merchant’s share, net of fee. Gross is amount + fee.


This site uses Just the Docs, a documentation theme for Jekyll.