This document tracks the current public contract surface in contract/src/lib.rs. For error codes, see ERROR-CODES.md, which contains the CONTRACT-34 table. For events, see EVENTS.md. For the propose/commit and propose/accept pattern shared by transfer_admin/accept_admin, propose_fee/commit_fee, propose_grace_period/commit_grace_period, and propose_upgrade/commit_upgrade, see architecture/two-step-auth.md. For the batch-size and page-size limits enforced by the batch and pagination entrypoints, see limits.md, which tabulates every cap from a single source of truth in contract/src/caps.rs.
- Bounded API delta (Issue 110)
- Data Types
- Functions
- initialize
- bump_instance_ttl
- subscribe
- subscribe_with_metadata
- charge
- simulate_charge
- simulate_pay_per_use
- extend_subscription_ttl
- pay_per_use
- cancel
- pause
- pause_until
- resume
- transfer_admin
- accept_admin
- is_contract_paused
- get_admin
- get_token
- upgrade
- propose_upgrade
- cancel_pending_upgrade
- commit_upgrade
- get_pending_upgrade
- get_subscription
- next_charge_at
- is_charge_due
- get_trial_end
- propose_grace_period
- commit_grace_period
- get_grace_period
- set_subscription_amount
- set_subscription_interval
- set_min_interval
- get_min_interval
- add_merchant
- remove_merchant
- set_whitelist_enabled
- is_whitelist_enabled
- is_merchant_whitelisted
- freeze_merchant
- unfreeze_merchant
- bump_merchant_revenue_day
- prune_merchant_revenue_days
- get_merchant_revenue_day
- is_merchant_frozen
- get_fee
- propose_fee
- commit_fee
- set_fee_bounds
- get_fee_bounds
- batch_charge
- get_batch_charge_estimate
- get_active_count
- get_subscriber_count
- get_subscriber_at
- get_subscriber_page
- get_active_subscriber_page
- clear_subscriber_index_entry
- get_merchant_revenue
- withdraw_merchant_revenue
- get_merchant_revenue_history
- clear_merchant_revenue_history
- get_merchant_subscriber_count
- get_merchant_sub_count
- reset_merchant_revenue
- set_daily_limit
- remove_daily_limit
- get_daily_limit
- get_daily_spent
- get_referral
- get_referrer
- migrate
- get_schema_version
- set_metadata
- get_metadata
- get_subscription_label
- clear_metadata
- get_charge_history
- get_protocol_stats
- pause_contract
- unpause_contract
- set_initial_admin
- contract_health_check
- clear_charge_history
- prune_charge_history
- get_charge_history_ttl
- get_charge_history_page
- transfer_subscription
- validate_subscription
- repair_subscription
- Units & Conversions
- Events Reference
- Error Codes
This revision documents only the following symbols against contract/src/lib.rs, contract/src/charge_exec.rs, contract/src/batch.rs, contract/src/fee.rs, contract/src/errors.rs, and contract/src/storage.rs. It is not a full API audit.
-
contract_health_check -
HealthReport -
simulate_charge -
get_batch_charge_estimate -
ChargeResult(includingAllowanceInsufficient; notChargeSimResult::InsufficientAllowance) -
ChargeSimResult -
set_fee_bounds -
get_fee_bounds -
pause_until - pause-expiry getter — not exposed (internal
storage::get_pause_expiryonly; no public contract method) -
get_active_subscriber_page
Related ops: EVENTS.md (paused, subscription_auto_resumed, charged, batch_charge_skips), KEEPER.md (batch_charge), MAINNET-DEPLOYMENT.md (fee bounds / health gates).
pub struct Subscription {
pub merchant: Address,
pub amount: i128,
pub interval: u64,
pub last_charged: u64,
pub active: bool,
pub paused: bool,
pub token: Address,
pub referrer: Option<Address>,
pub label: Symbol,
pub trial_duration: u64,
}Returned by live batch_charge and by get_batch_charge_estimate. Defined in contract/src/batch.rs (re-exported from lib.rs).
pub enum ChargeResult {
Charged, // live: funds transferred; estimate: would charge (no transfer)
Skipped, // interval has not elapsed
NoSubscription, // no subscription record
Inactive, // cancelled / inactive
Paused, // still paused
GracePeriodElapsed, // past the grace window
AllowanceInsufficient, // SAC allowance < gross `sub.amount`; appended (discriminant 6)
Charged,
Skipped,
NoSubscription,
Inactive,
Paused,
GracePeriodElapsed,
AllowanceInsufficient,
}| Variant | Meaning on batch_charge |
Meaning on get_batch_charge_estimate |
|---|---|---|
Charged |
Transfer succeeded | Precheck passed (or auto-resume short-circuit; no transfer) |
Skipped |
Interval not elapsed | Same (via precheck_charge) |
NoSubscription |
No record | Same |
Inactive |
Cancelled / inactive | Same |
Paused |
Still paused | Same |
GracePeriodElapsed |
Past grace window | Same |
New variants must be appended so off-chain parsers keep decoding 0–5. Do not confuse AllowanceInsufficient with ChargeSimResult::InsufficientAllowance or ContractError::InsufficientAllowance (8) — those are different symbols. |
| Variant | Meaning on batch_charge |
Meaning on get_batch_charge_estimate |
|---|---|---|
Charged |
Transfer succeeded | Precheck + allowance passed (no transfer) |
Skipped |
Interval not elapsed | Same (via precheck_charge) |
NoSubscription |
No record | Same |
Inactive |
Cancelled / inactive | Same |
Paused |
Still paused | Same |
GracePeriodElapsed |
Past grace window | Same |
AllowanceInsufficient |
Gross allowance too low; rest of batch continues | Same check; no transfer |
AllowanceInsufficient |
Allowance below gross amount; no transfer | Same precheck path as live charge |
This type is not the dry-run enum. Single-user simulation uses ChargeSimResult.
Returned only by simulate_charge. Defined in contract/src/charge_exec.rs.
pub enum ChargeSimResult {
WouldSucceed,
NotDue,
Inactive,
InsufficientAllowance,
GracePeriodElapsed,
ContractPaused,
SubscriptionPaused,
}| Variant | Meaning |
|---|---|
WouldSucceed |
Prechecks including SAC allowance would pass |
NotDue |
Ledger time is before next charge |
Inactive |
Missing subscription or inactive (no separate NoSubscription) |
InsufficientAllowance |
allowance(user, contract) < amount |
GracePeriodElapsed |
Past grace window |
ContractPaused |
Protocol paused (storage::is_contract_paused) |
SubscriptionPaused |
User pause, including unexpired pause_until |
simulate_charge never panics with a ContractError for these outcomes; they are return values.
Returned only by simulate_pay_per_use / simulate_pay_per_use_to. Defined in contract/src/charge_exec.rs.
pub enum PayPerUseSimResult {
WouldSucceed,
Inactive,
InsufficientAllowance,
ContractPaused,
SubscriptionPaused,
AmountMustBePositive,
AmountExceedsMaximum,
DailyLimitExceeded,
InvalidRecipient,
MerchantNotWhitelisted,
}| Variant | Meaning |
|---|---|
WouldSucceed |
Eligible: active, unpaused, within daily limit, allowance sufficient |
Inactive |
Missing subscription or inactive (no separate NoSubscription) |
InsufficientAllowance |
allowance(user, contract) < amount |
ContractPaused |
Protocol paused (storage::is_contract_paused) |
SubscriptionPaused |
Subscription is paused |
AmountMustBePositive |
amount <= 0 |
AmountExceedsMaximum |
amount > MAX_AMOUNT (per-call cap) |
DailyLimitExceeded |
daily_spent + amount > daily_limit |
InvalidRecipient |
pay_per_use_to recipient is the contract's own address |
MerchantNotWhitelisted |
Whitelist enabled and pay_per_use_to recipient not whitelisted |
simulate_pay_per_use never panics with a ContractError for these outcomes; they are return values.
pub struct ProtocolStats {
pub active_count: u64,
pub fee_bps: u32,
pub fee_collector: Option<Address>,
pub grace_period: u64,
pub whitelist_enabled: bool,
pub schema_version: u32,
pub contract_paused: bool,
}Returned by contract_health_check. There is no separate health-status enum: interpret is_healthy plus the component flags.
pub struct HealthReport {
pub is_healthy: bool,
pub contract_paused: bool,
pub token_configured: bool,
pub admin_configured: bool,
pub instance_ttl_ledgers: u32,
pub active_subscription_count: u64,
pub schema_version: u32,
pub fee_collector_set: bool,
pub global_volume_utilization_pct: u32,
pub pending_merchant_rev_count: u32,
}| Field | Type | Meaning |
|---|---|---|
is_healthy |
bool |
!contract_paused && token_configured && admin_configured. Does not include instance TTL. |
contract_paused |
bool |
Protocol pause flag |
token_configured |
bool |
Instance token is set |
admin_configured |
bool |
Admin is set |
instance_ttl_ledgers |
u32 |
Placeholder on-chain: hardcoded 100_000 (Soroban does not expose get_ttl() outside tests). Test/testutils: real instance TTL. Telemetry only; not used by is_healthy. |
active_subscription_count |
u64 |
Active subscription counter |
schema_version |
u32 |
Migration schema version |
fee_collector_set |
bool |
Protocol fee collector is set |
global_volume_utilization_pct |
u32 |
(accumulated * 100) / cap, clamped to ≤ 100; 0 if cap is 0. Cap is GlobalVolumeCapOverride or GLOBAL_MAX_VOLUME_PER_HOUR. |
pending_merchant_rev_count |
u32 |
Merchants in MerchantIndex with accumulated revenue (MerchantRevenue) > 0. This is a tracking metric, not a custodial balance — revenue settles directly into merchant wallets on every charge via SAC transfer_from. See MERCHANT-INTEGRATION.md § 5. |
How to interpret: treat is_healthy == false as “do not send production traffic” until pause, token, or admin flags are understood. instance_ttl_ledgers, fee_collector_set, and volume utilization are informational and do not enter the is_healthy formula. On-chain instance_ttl_ledgers is a fabricated constant, not a measurement. global_volume_utilization_pct uses effective_global_volume_cap (override when set, compile-time constant otherwise) — the same value charge enforcement uses (see limits.md — Global Hourly Volume Cap).
pub enum DataKey {
Subscription(Address),
Token,
Admin,
GracePeriod,
MerchantWhitelist(Address),
WhitelistEnabled,
MerchantFrozen(Address),
FeeCollector,
FeeBps,
PendingFee,
PendingAdmin,
ActiveCount,
MerchantRevenue(Address),
MerchantRevenueDay(Address, u64),
DailyLimit(Address),
DailySpent(Address),
Referral(Address),
SchemaVersion,
SubscriptionMeta(Address),
ChargeHistory(Address),
GlobalVolumeWindow,
ContractPaused,
MinInterval,
MerchantRevenueHistory(Address),
SubscriberIndex(u64),
SubscriberIndexSize,
MerchantSubCount(Address),
PendingGracePeriod,
}initialize(env: Env, token: Address, admin: Address)
| Name | Type | Description |
|---|---|---|
token |
Address |
SAC token used for subscription payments. |
admin |
Address |
Initial contract admin. |
Auth: none.
Returns: ().
Errors: ContractError::AlreadyInitialized.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source deployer --network testnet -- initialize --token <TOKEN_ADDRESS> --admin <ADMIN_ADDRESS>subscribe(env: Env, user: Address, merchant: Address, amount: i128, interval: u64, token: Address, trial_period: Option<u64>, referrer: Option<Address>)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and transaction signer. |
merchant |
Address |
Merchant receiving funds. |
amount |
i128 |
Recurring amount in stroops. |
interval |
u64 |
Billing interval in seconds. |
token |
Address |
Token contract used for this subscription. |
trial_period |
Option<u64> |
Optional delay before the first charge. |
referrer |
Option<Address> |
Optional referrer address. |
Auth: user.require_auth().
Returns: ().
Errors: ContractError::AmountMustBePositive, ContractError::IntervalMustBePositive, ContractError::MerchantNotWhitelisted, ContractError::ContractPausedError, ContractError::InvalidTokenAddress, ContractError::IntervalTooShort, ContractError::InsufficientAllowance.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- subscribe --user <USER_ADDRESS> --merchant <MERCHANT_ADDRESS> --amount 50000000 --interval 2592000 --token <TOKEN_ADDRESS>subscribe_with_metadata(env: Env, user: Address, merchant: Address, amount: i128, interval: u64, token: Address, trial_period: Option<u64>, referrer: Option<Address>, label: String)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and transaction signer. |
merchant |
Address |
Merchant receiving funds. |
amount |
i128 |
Recurring amount in stroops. |
interval |
u64 |
Billing interval in seconds. |
token |
Address |
Token contract used for this subscription. |
trial_period |
Option<u64> |
Optional delay before the first charge. |
referrer |
Option<Address> |
Optional referrer address. |
label |
String |
Subscription label, max 64 bytes. |
Auth: user.require_auth().
Returns: ().
Errors: same as subscribe(), plus ContractError::MetadataLabelTooLong.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- subscribe_with_metadata --user <USER_ADDRESS> --merchant <MERCHANT_ADDRESS> --amount 50000000 --interval 2592000 --token <TOKEN_ADDRESS> --label procharge(env: Env, user: Address)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber to charge. |
Auth: none. This is permissionless for keeper use.
Returns: ().
Errors: ContractError::NoSubscriptionFound, ContractError::SubscriptionNotActive, ContractError::SubscriptionPaused, ContractError::IntervalNotElapsed, ContractError::GracePeriodElapsed, ContractError::NotInitialized.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <KEEPER_KEY> --network testnet -- charge --user <USER_ADDRESS>Dry-run of a single charge() for user. No storage writes and no token transfer. Outcomes are ChargeSimResult variants, not panics.
simulate_charge(env: Env, user: Address) -> ChargeSimResult
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber to simulate. |
Auth: none.
Returns: ChargeSimResult.
Success behavior: evaluates contract pause, subscription presence, pause_until expiry (in memory only — does not persist auto-resume), due time, grace window, then SAC allowance(user, contract) vs amount. Returns WouldSucceed if all pass.
Error conditions: none as ContractError panics. Failure reasons are enum variants (ContractPaused, Inactive, SubscriptionPaused, NotDue, GracePeriodElapsed, InsufficientAllowance).
Distinguish from live charge and batch estimate:
charge / batch_charge |
simulate_charge |
get_batch_charge_estimate |
|
|---|---|---|---|
| Transfers | Yes | No | No |
| Storage writes | Yes on success / auto-resume | No (auto-resume is memory-only) | Yes if try_auto_resume fires |
| Contract pause | Enforced (panic / skip) | ContractPaused |
Ignored |
| Allowance | Required for success | Checked | Not checked |
| Return | void / ChargeResult |
ChargeSimResult |
Vec<ChargeResult> |
| Contract pause | charge and batch_charge panic ContractPaused (18) |
ContractPaused |
Ignored |
| Allowance | charge panics InsufficientAllowance (8); batch_charge returns AllowanceInsufficient |
InsufficientAllowance |
ChargeResult::AllowanceInsufficient |
| Return | void / Vec<ChargeResult> |
ChargeSimResult |
Vec<ChargeResult> |
Keeper ops still use live batch_charge; see KEEPER.md. Events for a real charge are in EVENTS.md.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- simulate_charge --user <USER_ADDRESS>Dry-run of a single pay_per_use() for user and amount. No storage writes and no token transfer. Outcomes are PayPerUseSimResult variants, not panics.
simulate_pay_per_use(env: Env, user: Address, amount: i128) -> PayPerUseSimResult
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber to simulate. |
amount |
i128 |
One-time amount to simulate. |
Auth: none.
Returns: PayPerUseSimResult.
Success behavior: evaluates contract pause, amount bounds, subscription presence, paused/inactive state, the daily spending limit (get_daily_limit / get_daily_spent), then SAC allowance(user, contract) vs amount. Returns WouldSucceed if all pass. Never writes spend tracking or other storage.
Dry-run of a single pay_per_use_to() for user, amount, and an explicit recipient. Same checks as simulate_pay_per_use, plus recipient validation (InvalidRecipient if recipient is the contract's own address; MerchantNotWhitelisted if the whitelist is enabled and recipient is not whitelisted). No storage writes.
simulate_pay_per_use_to(env: Env, user: Address, amount: i128, recipient: Address) -> PayPerUseSimResult
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber to simulate. |
amount |
i128 |
One-time amount to simulate. |
recipient |
Address |
Recipient that would receive the net payment. |
Auth: none.
Returns: PayPerUseSimResult.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- simulate_pay_per_use --user <USER_ADDRESS> --amount 1000000extend_subscription_ttl(env: Env, user: Address)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber whose TTL should be refreshed. |
Auth: none.
Returns: ().
Errors: none beyond storage access.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- extend_subscription_ttl --user <USER_ADDRESS>pay_per_use(env: Env, user: Address, amount: i128)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and signer. |
amount |
i128 |
One-time payment amount in stroops. |
Auth: user.require_auth().
Returns: ().
Errors: ContractError::AmountMustBePositive, ContractError::AmountExceedsMaximum, ContractError::NoSubscriptionFound, ContractError::SubscriptionNotActive, ContractError::SubscriptionPaused.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- pay_per_use --user <USER_ADDRESS> --amount 1000000cancel(env: Env, user: Address)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and signer. |
Auth: user.require_auth().
Returns: ().
Errors: ContractError::NoSubscriptionFound.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- cancel --user <USER_ADDRESS>pause(env: Env, user: Address)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and signer. |
Auth: user.require_auth().
Returns: ().
Errors: ContractError::NoSubscriptionFound, ContractError::SubscriptionNotActive.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- pause --user <USER_ADDRESS>Bounded user pause. The subscription auto-resumes on a later charge or batch_charge when ledger time >= expiry. Like indefinite pause, this sets paused = true and leaves active = true — pausing never deactivates a subscription (issue #1009; active = false is reserved for cancellation). The call also TTL-extends both the Subscription and PauseExpiry entries so they archive together.
pause_until(env: Env, user: Address, expiry: u64)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and signer. |
expiry |
u64 |
Unix ledger timestamp; must be strictly greater than current ledger time. |
Auth: user.require_auth().
Returns: ().
Success behavior: writes the subscription, stores pause expiry internally, emits paused (("paused", user)). See EVENTS.md.
Errors:
| Condition | Code | Symbol |
|---|---|---|
expiry <= now |
27 | InvalidPauseExpiry |
| No subscription | 4 | NoSubscriptionFound |
!sub.active |
16 | SubscriptionNotActive |
Pause-expiry read API: there is no public get_pause_expiry, get_pause, or paused_until contract method. Expiry is stored via internal storage::set_pause_expiry / storage::get_pause_expiry only.
For the full pause state model, auto-resume semantics, TTL considerations, and keeper/indexer guidance see architecture/pause-lifecycle.md.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- pause_until --user <USER_ADDRESS> --expiry <UNIX_TIMESTAMP>resume(env: Env, user: Address)
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber and signer. |
Auth: user.require_auth().
Returns: ().
Errors: ContractError::NoSubscriptionFound, ContractError::SubscriptionNotActive.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- resume --user <USER_ADDRESS>transfer_admin(env: Env, new_admin: Address)
| Name | Type | Description |
|---|---|---|
new_admin |
Address |
Proposed admin address. |
Auth: current admin only.
Returns: ().
Errors: none beyond auth.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- transfer_admin --new_admin <NEW_ADMIN_ADDRESS>accept_admin(env: Env)
Auth: proposed admin only.
Returns: ().
Errors: expect("no pending admin") if no proposal exists.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <NEW_ADMIN_KEY> --network testnet -- accept_adminis_contract_paused(env: Env) -> bool
Auth: none.
Returns: bool.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- is_contract_pausedget_admin(env: Env) -> Option<Address>
Auth: none.
Returns: Option<Address>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_adminget_token(env: Env) -> Option<Address>
Auth: none.
Returns: Option<Address>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_tokenpropose_upgrade(env: Env, new_wasm_hash: BytesN<32>)
Queues a WASM hash for the two-step upgrade flow. Auth: admin. The pending proposal is stored temporarily and refreshed to a 17,280-ledger TTL.
cancel_pending_upgrade(env: Env)
Clears the pending upgrade proposal. Auth: admin.
commit_upgrade(env: Env)
Commits the pending WASM hash and clears the proposal. Auth: admin. Panics
with NoPendingProposal when the proposal was cancelled or expired.
get_pending_upgrade(env: Env) -> Option<BytesN<32>>
Returns the pending upgrade hash, or None when no proposal exists. Auth: none.
get_subscription(env: Env, user: Address) -> Option<Subscription>
| Name | Type | Description |
|---|---|---|
user |
Address |
Subscriber address to look up. |
Auth: none.
Returns: Option<Subscription>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_subscription --user <USER_ADDRESS>Read-only view. Returns the Unix timestamp of the next scheduled charge for a user.
next_charge_at(env: Env, user: Address) -> Option<u64>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<u64> — Some(last_charged + interval) if the subscription is active and not paused. Returns None when:
- No subscription exists for
user - The subscription is inactive (cancelled,
active == false) - The subscription is paused (
paused == true)
The returned timestamp does not change between charges; it reflects the scheduled billing time regardless of whether the charge has been triggered yet.
Storage read: DataKey::Subscription(user) in persistent storage.
See also: is_charge_due to test whether the charge window is open right now; get_subscription to inspect the full record.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- next_charge_at \
--user <USER_ADDRESS>Read-only predicate. Returns true when a subscriber has a charge due at the current ledger timestamp — that is, when calling charge(user) right now would succeed rather than panic.
is_charge_due(env: Env, user: Address) -> bool
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to test. |
Auth: None.
Returns: bool — true when all of the following conditions hold simultaneously:
- A subscription exists for
user - The subscription is active (
active == true) and not paused (paused == false) now >= next_charge_at— the billing interval has elapsednow <= next_charge_at + grace_period— the charge is still within the grace window (this condition is skipped whengrace_period == 0)
Returns false for any absent subscription, inactive or paused subscription, interval not yet elapsed, or expired grace window.
Storage read: DataKey::Subscription(user) in persistent storage; DataKey::GracePeriod in instance storage.
Usage for keepers: Poll is_charge_due for each subscriber before deciding whether to submit a charge() transaction, saving gas on calls that would revert.
See also: next_charge_at for the exact scheduled timestamp; get_grace_period for the contract-wide grace window; charge for the write operation.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- is_charge_due \
--user <USER_ADDRESS>Read-only view. Returns the Unix timestamp when a subscriber's trial period ends, or None if the subscriber is not currently in a trial or has no subscription.
get_trial_end(env: Env, user: Address) -> Option<u64>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<u64> — Unix timestamp (seconds) of trial expiry when last_charged > now, otherwise None.
How trials work: When subscribe() is called with a trial_period of N seconds, the contract sets last_charged = now + N instead of now. This pushes the first eligible charge forward by the trial duration. get_trial_end returns Some(last_charged) while that timestamp is still in the future, indicating the subscriber is actively in a trial. Once the trial expires — that is, once last_charged <= now — this function returns None because the first charge window has opened. No separate storage key is used; the trial end is encoded directly in last_charged.
Returns None if no subscription exists for user.
Storage read: DataKey::Subscription(user) in persistent storage (reads last_charged field).
See also: subscribe for the trial_period parameter; next_charge_at which returns Some(last_charged + interval) once the trial ends.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_trial_end \
--user <USER_ADDRESS>propose_grace_period(env: Env, seconds: u64)
| Name | Type | Description |
|---|---|---|
seconds |
u64 |
Proposed grace period in seconds. |
Auth: admin only.
Returns: ().
Errors: ContractError::NoPendingProposal is not used here; seconds is validated in the helper.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- propose_grace_period --seconds 86400commit_grace_period(env: Env)
Auth: admin only.
Returns: ().
Errors: ContractError::NoPendingProposal.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- commit_grace_periodRead-only view. Returns the current contract-wide grace period in seconds.
get_grace_period(env: Env) -> u64
Auth: None.
Returns: u64 — grace period in seconds. Returns 0 when no grace period has been configured, which means charges must arrive exactly at or after next_charge_at with no tolerance window.
What the grace period means: After a billing interval elapses, keepers have a window of grace_period seconds in which to submit charge(). Concretely, charge() accepts a call only while now falls within [next_charge_at, next_charge_at + grace_period]. Calls that arrive after this window panic with "grace period elapsed". In batch_charge, users whose grace window has closed are returned as ChargeResult::GracePeriodElapsed without aborting the batch.
Storage read: DataKey::GracePeriod in instance storage. Defaults to 0 when the key is absent.
See also: propose_grace_period and commit_grace_period to change this value (admin, two-step); is_charge_due which incorporates the grace window into its result.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_grace_periodset_subscription_amount(env: Env, user: Address, new_amount: i128)
Auth: admin only.
Returns: ().
Errors: ContractError::NoSubscriptionFound, ContractError::AmountMustBePositive, ContractError::AmountExceedsMaximum, ContractError::ContractPausedError.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- set_subscription_amount --user <USER_ADDRESS> --new_amount 50000000set_subscription_interval(env: Env, user: Address, new_interval: u64)
Auth: admin only.
Returns: ().
Errors: ContractError::NoSubscriptionFound, ContractError::IntervalTooShort, ContractError::ContractPausedError.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- set_subscription_interval --user <USER_ADDRESS> --new_interval 604800set_min_interval(env: Env, seconds: u64)
Auth: admin only.
Returns: ().
Errors: aborts with the typed ContractError::IntervalMustBePositive (code 3) if seconds == 0, and with ContractError::NotInitialized (code 7) if no admin has been stored yet. seconds is validated before the admin guard, so an unconfigured contract still reports the invalid input.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- set_min_interval --seconds 3600Read-only view. Returns the minimum allowed subscription interval in seconds.
get_min_interval(env: Env) -> u64
Auth: None.
Returns: u64 — minimum interval in seconds. Default: 3600 (1 hour) when set_min_interval has never been called.
What this controls: When subscribe() or set_subscription_interval() is called, the requested interval is validated against this floor. Any attempt to create or update a subscription with interval < get_min_interval() will panic with ContractError::IntervalTooShort. The floor prevents subscriptions from being charged too frequently (e.g. every second), protecting both users and the network from spam.
Storage read: DataKey::MinInterval in instance storage. Falls back to the compile-time constant DEFAULT_MIN_INTERVAL = 3600 when the key is absent.
See also: set_min_interval to change this value (admin only); subscribe and set_subscription_interval which enforce it.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_min_intervaladd_merchant(env: Env, merchant: Address)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- add_merchant --merchant <MERCHANT_ADDRESS>See also: Merchant Integration Cookbook — Getting Started for whitelist request flow.
remove_merchant(env: Env, merchant: Address)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- remove_merchant --merchant <MERCHANT_ADDRESS>set_whitelist_enabled(env: Env, enabled: bool)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- set_whitelist_enabled --enabled trueget_whitelist_enabled(env: Env) -> bool
Auth: none.
Returns: bool (true by default if not set).
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_whitelist_enabledis_whitelist_enabled(env: Env) -> bool
Auth: none.
Returns: bool (true by default if not set).
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- is_whitelist_enabledis_merchant_whitelisted(env: Env, merchant: Address) -> bool
Auth: none.
Returns: bool.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- is_merchant_whitelisted --merchant <MERCHANT_ADDRESS>get_top_merchants_by_subs(env: Env, limit: u32) -> Vec<(Address, u32)>
Auth: none.
Returns: Vec<(Address, u32)> (top N merchants ranked by active subscriber count in descending order).
Panics: ContractError::BatchTooLarge if limit > 20. The 20 is caps::TOP_MERCHANTS_PAGE_SIZE; see limits.md.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_top_merchants_by_subs --limit 10freeze_merchant(env: Env, merchant: Address)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- freeze_merchant --merchant <MERCHANT_ADDRESS>unfreeze_merchant(env: Env, merchant: Address)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- unfreeze_merchant --merchant <MERCHANT_ADDRESS>bump_merchant_revenue_day(env: Env, merchant: Address, day: u64)
Auth: none.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- bump_merchant_revenue_day --merchant <MERCHANT_ADDRESS> --day 20000prune_merchant_revenue_days(env: Env, merchant: Address, days: Vec<u64>)
Auth: none in the wrapper.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- prune_merchant_revenue_days --merchant <MERCHANT_ADDRESS> --days '[20000,20001]'get_merchant_revenue_day(env: Env, merchant: Address, day: u64) -> i128
Auth: none.
Returns: i128.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_merchant_revenue_day --merchant <MERCHANT_ADDRESS> --day 20000is_merchant_frozen(env: Env, merchant: Address) -> bool
Auth: none.
Returns: bool.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- is_merchant_frozen --merchant <MERCHANT_ADDRESS>get_fee(env: Env) -> Option<(Address, u32)>
Auth: none.
Returns: Option<(Address, u32)>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_feepropose_fee(env: Env, collector: Address, bps: u32)
Auth: admin only.
Returns: ().
Errors: ContractError::InvalidFeeBps.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- propose_fee --collector <COLLECTOR_ADDRESS> --bps 100commit_fee(env: Env)
Auth: admin only.
Returns: ().
Errors: ContractError::NoPendingProposal (23), ContractError::FeeOutOfBoundsAtCommit (35) if pending bps is outside get_fee_bounds.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- commit_feeAdmin-only guardrails for future fee commits. Stored on instance as DataKey::MinFeeBps / DataKey::MaxFeeBps.
set_fee_bounds(env: Env, min_bps: u32, max_bps: u32)
| Name | Type | Description |
|---|---|---|
min_bps |
u32 |
Inclusive minimum bps for a pending fee at commit. |
max_bps |
u32 |
Inclusive maximum bps; must be <= 10_000. |
Auth: admin (admin::require_admin).
Returns: ().
Bounds semantics: propose_fee does not apply these bounds (it only rejects bps > 10_000 and an invalid collector). commit_fee rejects pending bps outside [min_bps, max_bps] with FeeOutOfBoundsAtCommit (35).
Errors: ContractError::InvalidFeeBounds (34) if min_bps > max_bps or max_bps > 10_000. Unauthorized callers fail admin auth.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- set_fee_bounds --min_bps 0 --max_bps 500get_fee_bounds(env: Env) -> (u32, u32)
Auth: none.
Returns: (min_bps, max_bps). Defaults to (0, 10000) when governance has not set explicit bounds.
Errors: none.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_fee_boundsOperational gate: MAINNET-DEPLOYMENT.md.
batch_charge(env: Env, users: Vec<Address>) -> Vec<ChargeResult>
Auth: none.
Returns: Vec<ChargeResult> — one entry per input address, in order.
A non-Charged result for one user never aborts the rest of the batch. Insufficient SAC allowance is ChargeResult::AllowanceInsufficient (not a transaction panic). When the batch includes at least one interesting non-success (NoSubscription, Inactive, Paused, GracePeriodElapsed, AllowanceInsufficient), the contract emits batch_charge_skips. All-charged or all-not-due (Skipped) batches emit no summary.
The public wrapper calls ensure_contract_not_paused first: a paused protocol panics ContractPaused (18) for the whole invoke (not a per-user result).
Errors: ContractError::BatchTooLarge (20) if users.len() exceeds get_max_batch_size (default 50, ceiling 200). get_max_batch_size is sourced from a single implementation in contract/src/caps.rs (the same key and default used by contract/src/batch.rs). Ordinary per-user outcomes are enum values. Full cap table: limits.md.
Keeper ops: KEEPER.md.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <KEEPER_KEY> --network testnet -- batch_charge --users '["<USER_A>","<USER_B>"]'Batch estimate (not a transfer). Returns Vec<ChargeResult> — the live batch enum — not ChargeSimResult.
get_batch_charge_estimate(env: Env, users: Vec<Address>) -> Vec<ChargeResult>
| Name | Type | Description |
|---|---|---|
users |
Vec<Address> |
Addresses to estimate. |
Auth: none.
Returns: one ChargeResult per input address, in order.
Success behavior: for each user, missing sub → NoSubscription. If paused, try_auto_resume may persist an auto-resume. Then precheck_charge runs. If that returns Ok, the estimate reads SAC allowance(user, contract) against gross sub.amount and returns Charged or AllowanceInsufficient.
Errors: ContractError::BatchTooLarge (20) if users.len() > 200 (hardcoded; not get_max_batch_size). Ordinary per-user outcomes are enum values, not panics. Note this is the shared ceiling, so it is higher than the default live charge cap: see limits.md.
Operational caveats (from lib.rs):
- Calls real
try_auto_resume, which writes the subscription, clears pause expiry, and emitssubscription_auto_resumed— not a pure view. - Does not check protocol pause (
is_contract_paused).simulate_chargereturnsContractPausedinstead. - Allowance is checked (same gross-amount test as live
batch_charge). A keeper file-header comment that says otherwise is stale. - Cap 200 vs live batch max (default 50 via
MaxBatchSize).
Use simulate_charge when you need a no-write, pause-aware single-user dry-run. Keepers that charge should still call batch_charge (KEEPER.md). Auto-resume event: EVENTS.md.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_batch_charge_estimate --users '["<USER_A>","<USER_B>"]'For a full comparison of estimate vs live semantics (transfers, auto-resume side-effects, protocol-pause handling, batch size cap differences, and result-enum mapping) see architecture/batch-estimate-vs-live.md.
get_active_count(env: Env) -> u64
Auth: none.
Returns: u64.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_active_countget_subscriber_count(env: Env) -> u64
Auth: none.
Returns: u64.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_subscriber_countget_subscriber_at(env: Env, index: u64) -> Option<Address>
Auth: none.
Returns: Option<Address>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_subscriber_at --index 0get_subscriber_page(env: Env, offset: u64, limit: u32) -> Vec<Address>
Auth: none.
Returns: Vec<Address>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_subscriber_page --offset 0 --limit 10View of active subscriber addresses from the append-only SubscriberIndex. Sibling get_subscriber_page does not filter on sub.active.
get_active_subscriber_page(env: Env, offset: u64, limit: u32) -> Vec<Address>
| Name | Type | Description |
|---|---|---|
offset |
u64 |
Index-slot cursor into SubscriberIndex, not “Nth active subscriber”. |
limit |
u32 |
Requested page size. Silently clamped to 50. 0 returns empty. |
Auth: none.
Returns: Vec<Address>. Empty when offset >= index_size or cap == 0. Length may be less than limit when many slots are missing or inactive; the scan continues until result.len() == cap or the index ends.
Pagination: increment offset by the number of index slots consumed, not by result.len(), if you need to walk the whole index. The implementation walks i from offset upward and pushes only addresses whose Subscription exists and sub.active is true.
Errors: none.
Related: ChargeResult / keeper paging still typically use batch_charge with an operator-supplied address list (KEEPER.md).
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_active_subscriber_page --offset 0 --limit 50Admin repair for a single stale subscriber-index slot. Looks up the occupant of index, refuses if that subscriber currently has an active subscription, then tombstones the slot so keepers skip it. Does not garbage-collect the rest of the index.
clear_subscriber_index_entry(env: Env, index: u64)
| Name | Type | Description |
|---|---|---|
index |
u64 |
Slot in the append-only SubscriberIndex. |
Auth: Contract admin only (require_admin).
Errors: ContractError::NoSubscriptionFound if index is out of range, empty, or already tombstoned. ContractError::CannotClearActiveSubscriber if the occupant still has an active subscription.
Event emitted
| Event name | Topic | Data |
|---|---|---|
subscriber_index_cleared |
("subscriber_index_cleared", user_address) |
index: u64 |
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet --source-account <ADMIN> -- clear_subscriber_index_entry --index 0get_merchant_revenue(env: Env, merchant: Address) -> i128
Auth: none.
Returns: i128.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_merchant_revenue --merchant <MERCHANT_ADDRESS>get_merchant_revenue_history(env: Env, merchant: Address, days: u32) -> Vec<i128>
Auth: none.
Returns: Vec<i128>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_merchant_revenue_history --merchant <MERCHANT_ADDRESS> --days 7clear_merchant_revenue_history(env: Env, merchant: Address)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- clear_merchant_revenue_history --merchant <MERCHANT_ADDRESS>get_merchant_subscriber_count(env: Env, merchant: Address) -> u64
Auth: none.
Returns: u64.
Alias: this is the canonical reader for DataKey::MerchantSubCount; it delegates to the single implementation in contract/src/merchant_stats.rs. get_merchant_sub_count is a thin alias over the same body.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_merchant_subscriber_count --merchant <MERCHANT_ADDRESS>get_merchant_sub_count(env: Env, merchant: Address) -> u32
Auth: none.
Returns: u32 — active subscriber count for merchant (same MerchantSubCount storage as get_merchant_subscriber_count, narrowed to u32).
Alias: thin delegation to get_merchant_subscriber_count; both read DataKey::MerchantSubCount through the same merchant_stats implementation, so the two can never disagree.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_merchant_sub_count --merchant <MERCHANT_ADDRESS>See also: Merchant Integration Cookbook — Monitoring Subscribers.
reset_merchant_revenue(env: Env, merchant: Address)
Auth: admin only.
Returns: ().
Note on Non-Custodial Revenue: Merchant revenue in PayFlow is non-custodial. Charges and pay-per-use operations transfer tokens directly from subscriber to merchant via SAC
transfer_from. The contract never holds merchant revenue funds;get_merchant_revenuemaintains an on-chain cumulative metric for merchant metrics and analytics.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- reset_merchant_revenue --merchant <MERCHANT_ADDRESS>set_daily_limit(env: Env, user: Address, limit: i128)
Auth: user.require_auth().
Returns: ().
Errors: ContractError::AmountMustBePositive.
CLI example:
See also: Daily Spending Limits Guide for a conceptual overview of the pay_per_use spending cap.
For a complete list of all error codes returned by the contract, see ERROR-CODES.md.
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- set_daily_limit --user <USER_ADDRESS> --limit 50000000remove_daily_limit(env: Env, user: Address)
Auth: user.require_auth().
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- remove_daily_limit --user <USER_ADDRESS>See the full reference entry below.
get_day_start(env: Env, user: Address) -> bool
Auth: none.
Returns: bool — true if DataKey::DayStart(user) exists (current ~24h spend window is active), false otherwise. This is a presence marker, not a wall-clock timestamp.
CLI example: See also: Daily Spending Limits Deep-Dive.
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_day_start --user <USER_ADDRESS>get_referrer(env: Env, user: Address) -> Option<Address>
Auth: none.
Returns: Option<Address>.
Alias: get_referral and get_referrer are byte-identical readers of DataKey::Referral(user); get_referrer is the canonical implementation and get_referral is a thin delegation to it. Both names remain exported.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_referrer --user <USER_ADDRESS>migrate(env: Env, users: Vec<Address>)
Auth: none.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- migrate --users '["<USER_ADDRESS>"]'See the full reference entry below.
set_metadata(env: Env, user: Address, label: String)
Auth: user.require_auth().
Returns: ().
Errors: ContractError::MetadataLabelTooLong.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- set_metadata --user <USER_ADDRESS> --label proget_metadata(env: Env, user: Address) -> Option<String>
Auth: none.
Returns: Option<String>.
Alias: get_metadata and get_subscription_label are byte-identical readers of DataKey::SubscriptionMeta(user); get_metadata is the canonical implementation and get_subscription_label is a thin delegation to it. Both names remain exported.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_metadata --user <USER_ADDRESS>get_subscription_label(env: Env, user: Address) -> Option<String>
Auth: none.
Returns: Option<String>.
Alias: thin delegation to get_metadata; both read DataKey::SubscriptionMeta(user) through the same implementation.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_subscription_label --user <USER_ADDRESS>clear_metadata(env: Env, user: Address)
Auth: user.require_auth().
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- clear_metadata --user <USER_ADDRESS>get_charge_history(env: Env, user: Address) -> Vec<u64>
Auth: none.
Returns: Vec<u64>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_charge_history --user <USER_ADDRESS>get_protocol_stats(env: Env) -> ProtocolStats
Auth: none.
Returns: ProtocolStats.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_protocol_statspause_contract(env: Env)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- pause_contractunpause_contract(env: Env)
Auth: admin only.
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- unpause_contractset_initial_admin(env: Env, admin: Address)
Auth: none.
Returns: ().
Errors: panics if the admin is already set.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- set_initial_admin --admin <ADMIN_ADDRESS>Read-only snapshot of protocol health. Safe at any time: no auth, no storage writes. There is no health_check or get_health export.
contract_health_check(env: Env) -> HealthReport
Auth: none.
Returns: HealthReport.
Success behavior: builds the struct; does not panic on the normal path. is_healthy is true only when the contract is not paused and token and admin are set. Instance TTL is not part of that decision.
Errors: none defined.
Operational notes: on-chain instance_ttl_ledgers is a hardcoded 100_000 placeholder and must not be read as remaining TTL. Off-chain scripts/health-check.ts is a shallow RPC simulation of get_schema_version and get_active_count only — it does not call this method. See MAINNET-DEPLOYMENT.md.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- contract_health_checkclear_charge_history(env: Env, user: Address)
Auth: user.require_auth().
Returns: ().
Alias: clear_charge_history and prune_charge_history are byte-identical; clear_charge_history is the canonical implementation and prune_charge_history is a thin delegation to it. Both names remain exported.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- clear_charge_history --user <USER_ADDRESS>get_charge_history_ttl(env: Env, user: Address) -> u32
Auth: none.
Returns: In test/testutils builds, the persistent TTL of ChargeHistory(user), or 0 if the key is absent. On-chain / production always returns 0 because Soroban does not expose get_ttl() outside tests. Do not treat the production value as a live remaining-TTL measurement, and do not treat 0 as “history expired”.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_charge_history_ttl --user <USER_ADDRESS>get_charge_history_page(env: Env, user: Address, offset: u32, limit: u32, ascending: bool) -> Vec<u64>
Auth: none.
Returns: Vec<u64>.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- get_charge_history_page --user <USER_ADDRESS> --offset 0 --limit 12 --ascending falseChargeHistory(user) is a Vec<u64> of up to 12 charge timestamps, stored oldest → newest. Every successful charge() appends a timestamp; once the vector holds 12 entries, the next append drops entry 0 (the oldest) before pushing the new one. In other words, storage itself is the ring buffer — get_charge_history_page slices whatever is currently in it (in ascending or descending order). There is no separate "total charge count" stored anywhere; the longest history you can ever page through is 12 entries, because older charges are physically gone.
Slicing algorithm (from subscription_history.rs):
ordered_history = if ascending { history } else { history.reversed() }
effective_limit = min(limit, 12)
if offset >= ordered_history.len() { return [] } // empty, not an error
end = min(offset + effective_limit, ordered_history.len())
return ordered_history[offset..end]limit is silently capped at 12 — passing limit: 100 is safe and simply returns everything available. This 12 is a retention length (MAX_HISTORY), not a batch or page cap; see limits.md. offset is never validated against the history length beyond the empty-result check above; there is no IndexOutOfBounds-style error anywhere in this path.
ascendingparameter. Passingascending = truereturns records in oldest-to-newest order. Passingascending = falsereturns records in newest-to-oldest order (most recent first).
Ring buffer diagram — a subscriber with 14 total lifetime charges (c1..c14). Only the last 12 survive:
lifetime charges: c1 c2 c3 [c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14]
↑ ↑ ↑ └──────────────── retained (12 slots) ────────────────┘
evicted (FIFO, oldest dropped first as new charges arrive)
ChargeHistory(user) in storage (index 0 = oldest):
index: 0 1 2 3 4 5 6 7 8 9 10 11
value: [c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14] (len = 11 here, one slot short of the cap)
get_charge_history_page(user, offset=8, limit=3)
│
slices index [8..11) ─┐
▼
[c12, c13, c14]
Example 1 — fetch everything (offset=0, limit=12):
soroban contract invoke --id <CONTRACT_ID> --network testnet -- \
get_charge_history_page --user <USER_ADDRESS> --offset 0 --limit 12With a history of [c4..c14] (11 entries), this returns all 11 entries, oldest to newest. If the subscriber has fewer than 12 charges total, one call is always enough — you never need a second page.
Example 2 — most recent 5 records (no ascending param, so compute the offset):
const all = await getChargeHistoryPage(user, 0, 12); // at most 12 entries ever exist
const mostRecent5 = all.slice(-5); // last 5 = newest 5, since storage is oldest → newestEquivalently, on-chain: offset = max(0, total - 5), limit = 5. If total = 11, call get_charge_history_page(user, 6, 5) to get entries [6..11).
Example 3 — offset beyond the record count (returns empty, not an error):
soroban contract invoke --id <CONTRACT_ID> --network testnet -- \
get_charge_history_page --user <USER_ADDRESS> --offset 5 --limit 2For a subscriber with only 1 recorded charge, offset=5 >= len(1) so this returns []. This mirrors the covering test test_get_charge_history_page_offset_beyond_length.
Offset / limit reference table (history has 11 entries, c4..c14, oldest → newest):
offset |
limit |
Result | Notes |
|---|---|---|---|
0 |
12 |
[c4..c14] (11 items) |
limit capped at 12, but history only has 11 |
0 |
5 |
[c4, c5, c6, c7, c8] |
oldest 5 |
6 |
5 |
[c10, c11, c12, c13, c14] |
newest 5 (see Example 2) |
11 |
5 |
[] |
offset == len, empty result |
20 |
5 |
[] |
offset > len, empty result, no error |
9 |
100 |
[c13, c14] |
limit capped then clamped to remaining length |
Because storage never holds more than 12 entries, a single call with limit: 12 already returns everything. The loop below is still useful as the general-purpose pattern for infinite-scroll style UIs, and keeps working unmodified if the on-chain cap is ever raised. It calls the contract the same way getSubscription does — build, simulate, decode:
import {
Contract,
TransactionBuilder,
BASE_FEE,
nativeToScVal,
Address,
xdr,
} from "@stellar/stellar-sdk";
import { server, CONTRACT_ID, NETWORK_PASSPHRASE } from "./stellar";
import { ScValDecoder } from "./services/scval";
function addressVal(addr: string): xdr.ScVal {
return nativeToScVal(Address.fromString(addr), { type: "address" });
}
/** Fetches one page of charge timestamps via get_charge_history_page. */
async function getChargeHistoryPage(
user: string,
offset: number,
limit: number,
): Promise<number[]> {
const contract = new Contract(CONTRACT_ID);
const account = await server.getAccount(user);
const tx = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: NETWORK_PASSPHRASE,
})
.addOperation(
contract.call(
"get_charge_history_page",
addressVal(user),
nativeToScVal(offset, { type: "u32" }),
nativeToScVal(limit, { type: "u32" }),
),
)
.setTimeout(30)
.build();
const result = await server.simulateTransaction(tx);
if ("error" in result) throw new Error((result as { error: string }).error);
const retval = (result as { result?: { retval?: xdr.ScVal } }).result?.retval;
if (!retval) return [];
return retval.vec()?.map((v) => Number(ScValDecoder.decodeU64(v))) ?? [];
}
/**
* Loads the subscriber's full charge history by paging until an empty
* (or short) page comes back. Safe to call even though today's 12-record
* cap means it will always resolve after a single request.
*/
async function loadAllChargeHistory(user: string): Promise<number[]> {
const PAGE_SIZE = 12; // matches the contract's MAX_HISTORY cap
const all: number[] = [];
let offset = 0;
while (true) {
const page = await getChargeHistoryPage(user, offset, PAGE_SIZE);
all.push(...page);
if (page.length < PAGE_SIZE) break; // short page = no more records
offset += page.length;
}
return all;
}See INTEGRATION-GUIDE.md § 8 — Paginating Charge History for the same pattern in a full integration walkthrough.
transfer_subscription(env: Env, user: Address, new_user: Address)
Auth: user.require_auth().
Returns: ().
Errors: ContractError::ContractPausedError, ContractError::NoSubscriptionFound, ContractError::SubscriptionAlreadyActive.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- transfer_subscription --user <USER_ADDRESS> --new_user <NEW_USER_ADDRESS>All amounts are in stroops. 1 XLM = 10,000,000 stroops. Intervals are
See EVENTS.md for the complete event schema reference. For building keepers, analytics, notifications, or reconciliation jobs on top of those events, see EVENT-DRIVEN-GUIDE.md.
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The payer. Must match the transaction signer. |
amount |
i128 |
Stroops to transfer. Must be > 0. |
Auth: user.require_auth().
What it does:
- Loads the subscription for
user - Asserts
active == true - Calls
transfer_from(contract, user, merchant, amount)on the token contract
Events emitted
topic: ("pay_per_use", user)
data: (merchant, amount)
Errors
| Condition | Panic message |
|---|---|
amount <= 0 |
"amount must be positive" |
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription is paused | "subscription is paused" |
| Insufficient allowance | Host error from token contract |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- pay_per_use \
--user <USER_ADDRESS> \
--amount 1000000Temporarily halts charges for a subscription. The subscription record is preserved and can be resumed at any time. Both charge() and pay_per_use() will panic while paused.
pause(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
Auth: user.require_auth().
Events emitted
topic: ("paused", user)
data: ()
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription already paused | "subscription is already paused" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- pause \
--user <USER_ADDRESS>Resumes a paused subscription, re-enabling charge() and pay_per_use().
resume(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
Auth: user.require_auth().
Events emitted
topic: ("resumed", user)
data: ()
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
| Subscription is cancelled | "subscription is not active" |
| Subscription is not paused | "subscription is not paused" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- resume \
--user <USER_ADDRESS>Deactivates a subscription. The subscription record remains in storage with active = false. No further charges can be made.
cancel(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
Auth: user.require_auth().
Events emitted
topic: ("cancelled", user)
data: ()
Errors
| Condition | Panic message |
|---|---|
| No subscription exists | "no subscription found" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- cancel \
--user <USER_ADDRESS>Read-only view function. Returns the subscription for a given user, or None if none exists.
get_subscription(env: Env, user: Address) -> Option<Subscription>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to look up. |
Auth: None.
Returns: Option<Subscription> — None if no subscription exists for this address.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_subscription \
--user <USER_ADDRESS>See the full reference entry above.
Charges multiple subscribers in a single transaction. Individual failures do not abort the batch — every address is processed and its outcome is returned.
batch_charge(env: Env, users: Vec<Address>) -> Vec<ChargeResult>
Parameters
| Name | Type | Description |
|---|---|---|
users |
Vec<Address> |
List of subscriber addresses to attempt charging. |
Auth: None. Same permissionless model as charge().
Returns: Vec<ChargeResult> — one entry per input address, in order.
pub enum ChargeResult {
Charged, // funds transferred successfully
Skipped, // interval has not elapsed yet
NoSubscription, // no subscription found for this address
Inactive, // subscription is cancelled
Paused, // subscription is paused
GracePeriodElapsed, // charge window has closed
}Storage written: DataKey::Subscription(user) updated for each Charged result. DataKey::MerchantRevenue(merchant) incremented for each Charged result.
Events emitted: ("charged", user) for each successfully charged user.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <KEEPER_KEY> \
--network testnet \
-- batch_charge \
--users '["<USER_A>","<USER_B>","<USER_C>"]'Returns the current number of active subscriptions. Incremented by subscribe(), decremented by cancel().
get_active_count(env: Env) -> u64
Auth: None.
Returns: u64 — total active subscriptions.
Storage read: DataKey::ActiveCount in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_active_countReturns the cumulative amount charged to a merchant's subscribers across all charge() and pay_per_use() calls.
get_merchant_revenue(env: Env, merchant: Address) -> i128
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to query. |
Auth: None.
Returns: i128 — total stroops received by this merchant. Returns 0 if no charges have occurred.
Storage read: DataKey::MerchantRevenue(merchant) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_merchant_revenue \
--merchant <MERCHANT_ADDRESS>Vestigial entrypoint. This function exists in the contract for backward compatibility but does not reflect the current settlement model. PayFlow is non-custodial: funds settle directly from subscriber to merchant wallet via SAC
transfer_fromon everychargeorpay_per_usecall. The contract does not hold or escrow merchant funds.
withdraw_merchant_revenueoperates on the on-chainMerchantRevenueaccounting counter, not on a custodial token balance held by the contract. Calling it will attempt atoken::transferfrom the contract's own balance equal to theMerchantRevenuecounter and then zero the counter — but because normal settlement does not deposit tokens into the contract, the contract balance is typically zero and this call will fail unless tokens were sent directly to the contract address by other means.Integrators should not build on this entrypoint. Use the
chargedandpay_per_useevents,get_merchant_revenue, andget_merchant_revenue_historyfor analytics and reconciliation. SeeMERCHANT-INTEGRATION.md§ 5.
withdraw_merchant_revenue(env: Env, merchant: Address)
Auth: merchant.require_auth().
Returns: ().
Errors: ContractError::ZeroBalanceAvailable if get_merchant_revenue(merchant) <= 0; ContractError::ContractPaused if the contract is paused.
Sets a daily spending cap for pay_per_use() for the calling user. The limit is stored in temporary storage and resets automatically after approximately one day (~17,280 ledgers at 5 s/ledger).
For a detailed conceptual guide on how limits and TTL expirations work, see Daily Spending Limits.
set_daily_limit(env: Env, user: Address, limit: i128)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
limit |
i128 |
Maximum stroops spendable via pay_per_use() per day. Must be > 0. |
Auth: user.require_auth().
Storage written: DataKey::DailyLimit(user) in temporary storage with TTL of ~1 day.
Enforcement: Every pay_per_use() call checks DailySpent(user) + amount <= DailyLimit(user) before transferring. The running total is tracked in DataKey::DailySpent(user) (also temporary, same TTL).
Errors
| Condition | Panic message |
|---|---|
limit <= 0 |
"limit must be positive" |
| Spend would exceed limit | "daily spending limit exceeded" |
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- set_daily_limit \
--user <USER_ADDRESS> \
--limit 50000000Read-only view. Returns the current daily spending cap for a user's pay_per_use() calls, or None if no cap has been set.
get_daily_limit(env: Env, user: Address) -> Option<i128>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<i128> — the daily limit in stroops, or None if no limit has been set for this user. A None result means pay_per_use() is uncapped for this user.
Storage read: DataKey::DailyLimit(user) in temporary storage (TTL ≈ 1 day, ~17,280 ledgers at 5 s/ledger). Returns None automatically once the TTL expires, which resets the cap each day without manual intervention.
See also: set_daily_limit to configure the cap; remove_daily_limit to clear it; get_daily_spent to check how much has been consumed today.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_daily_limit \
--user <USER_ADDRESS>Read-only view. Returns the total amount spent today by a user via pay_per_use() calls.
get_daily_spent(env: Env, user: Address) -> i128
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: i128 — stroops spent today via pay_per_use(). Returns 0 when no spend has been recorded in the current window or when the daily tracking TTL has expired.
How the window works: The first pay_per_use() call of the day anchors a DataKey::DayStart(user) marker in temporary storage with a TTL of ~17,280 ledgers (≈1 day). get_daily_spent returns 0 whenever that marker is absent — either because the user has never called pay_per_use(), or because the 24-hour window has expired and both DayStart and DailySpent have been automatically pruned.
Storage read: DataKey::DayStart(user) presence check first; then DataKey::DailySpent(user) — both in temporary storage.
See also: set_daily_limit to configure the daily cap; get_daily_limit to read the configured cap; pay_per_use which increments this counter on each call.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_daily_spent \
--user <USER_ADDRESS>Extends the TTL of a user's subscription record in persistent storage.
extend_subscription_ttl(env: Env, user: Address)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to extend TTL for. |
Auth: None.
Storage written: Extends TTL of DataKey::Subscription(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- extend_subscription_ttl \
--user <USER_ADDRESS>See the full reference entry above.
Adds a merchant to the whitelist. Only the contract admin can call this.
add_merchant(env: Env, merchant: Address)
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to whitelist. |
Auth: Admin only.
Storage written: DataKey::MerchantWhitelist(merchant) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- add_merchant \
--merchant <MERCHANT_ADDRESS>Removes a merchant from the whitelist. Only the contract admin can call this.
remove_merchant(env: Env, merchant: Address)
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to remove from the whitelist. |
Auth: Admin only.
Storage written: Removes DataKey::MerchantWhitelist(merchant) from persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- remove_merchant \
--merchant <MERCHANT_ADDRESS>Enables or disables the merchant whitelist. Only the contract admin can call this.
set_whitelist_enabled(env: Env, enabled: bool)
Parameters
| Name | Type | Description |
|---|---|---|
enabled |
bool |
True to enable the whitelist, false to disable. |
Auth: Admin only.
Storage written: DataKey::WhitelistEnabled in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <ADMIN_KEY> \
--network testnet \
-- set_whitelist_enabled \
--enabled trueReturns per-day revenue for the given merchant for the last days days, oldest to newest.
get_merchant_revenue_history(env: Env, merchant: Address, days: u32) -> Vec<i128>
Parameters
| Name | Type | Description |
|---|---|---|
merchant |
Address |
The merchant address to query. |
days |
u32 |
The number of days of history to retrieve. |
Auth: None.
Returns: Vec<i128> — Daily revenue in stroops, ordered oldest to newest.
Storage read: DataKey::MerchantRevenueDay(merchant, day) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_merchant_revenue_history \
--merchant <MERCHANT_ADDRESS> \
--days 7Returns the referrer address recorded for a subscriber.
get_referrer(env: Env, user: Address) -> Option<Address>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<Address> — None if no referrer was recorded.
Storage read: DataKey::Referral(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_referrer \
--user <USER_ADDRESS>Upgrades contract storage to the latest schema version. Safe to call multiple times.
migrate(env: Env)
Auth: None (admin restriction can be added in future versions).
Storage written: DataKey::SchemaVersion in instance storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- migrateRead-only view. Returns the current storage schema version of the contract.
get_schema_version(env: Env) -> u32
Auth: None.
Returns: u32 — the schema version stored in instance storage. Default: 1 when DataKey::SchemaVersion has never been written (i.e. before the first migrate() call or on freshly deployed contracts).
What schema versions mean:
| Version | Description |
|---|---|
1 |
Initial schema. Subscription struct does not include the paused field. |
2 |
Current schema. Subscription includes paused: bool. Set by migrate(). |
When to call this: Use get_schema_version to verify a deployment is on the current schema before running migrate(), and to confirm a migration completed successfully. A keeper or admin script can check this before submitting migrate(users) to avoid redundant transactions — migrate() is a no-op when already at version 2, but reading the version first avoids the gas cost entirely.
Storage read: DataKey::SchemaVersion in instance storage. Falls back to 1u32 when absent.
See also: migrate which bumps this value from 1 → 2; the Deployment — State Migration guide for the full migration history and CLI steps.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_schema_versionAttaches a short label string (e.g. plan name) to the caller's subscription.
set_metadata(env: Env, user: Address, label: String)
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber. Must match the transaction signer. |
label |
String |
Short display label (e.g. "pro", "basic"). |
Auth: user.require_auth().
Storage written: DataKey::SubscriptionMeta(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--source <USER_KEY> \
--network testnet \
-- set_metadata \
--user <USER_ADDRESS> \
--label proReturns the metadata label for a subscriber.
get_metadata(env: Env, user: Address) -> Option<String>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Option<String> — None if no label has been set.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_metadata \
--user <USER_ADDRESS>Returns the last (up to 12) charge timestamps for a subscriber, ordered oldest → newest.
get_charge_history(env: Env, user: Address) -> Vec<u64>
Parameters
| Name | Type | Description |
|---|---|---|
user |
Address |
The subscriber address to query. |
Auth: None.
Returns: Vec<u64> — UNIX timestamps of successful charge() calls. Empty if no charges have occurred.
Storage read: DataKey::ChargeHistory(user) in persistent storage.
CLI example
soroban contract invoke \
--id <CONTRACT_ID> \
--network testnet \
-- get_charge_history \
--user <USER_ADDRESS>Administrative utilities for detecting and repairing corrupted subscription records after migrations or contract upgrades.
Read-only integrity check for a subscriber address.
validate_subscription(env: Env, user: Address) -> SubscriptionValidationReport
Returns SubscriptionValidationReport
| Field | Type | Description |
|---|---|---|
is_valid |
bool |
true when no inconsistencies are detected |
violations |
Vec<String> |
General integrity violations |
missing_records |
Vec<String> |
Missing auxiliary records (history, metadata, etc.) |
invalid_state_transitions |
Vec<String> |
Illegal active/paused/cancelled state combinations |
corrupted_references |
Vec<String> |
Broken merchant/token/referrer references |
Auth: None (read-only simulation).
Frontend: Exposed in the Admin Dashboard → Subscription Repair panel.
Repairs detected subscription inconsistencies for a user.
repair_subscription(env: Env, user: Address) -> u32
Auth: Contract admin only (require_admin).
Returns: Count of fixed inconsistencies (also emitted in the subscription_repaired event).
Event emitted
| Event name | Topic | Data |
|---|---|---|
subscription_repaired |
("subscription_repaired", user_address) |
fixed_inconsistencies: u32 |
Frontend authorization: The repair button is enabled only when the connected Freighter wallet matches the on-chain admin returned by get_admin.
All amounts are in stroops — the smallest unit of a Stellar token.
| Amount | Stroops |
|---|---|
| 1 XLM | 10,000,000 |
| 0.5 XLM | 5,000,000 |
| 0.0000001 XLM | 1 |
All intervals are in seconds.
| Interval | Seconds |
|---|---|
| 1 day | 86,400 |
| 1 week | 604,800 |
| 30 days | 2,592,000 |
All events can be indexed by listening to the Stellar RPC event stream for the FlowPay contract ID.
For a complete reference of all events with detailed schemas and examples, see EVENTS.md. For consumption patterns (polling, deduplication, reaction, reliability), see EVENT-DRIVEN-GUIDE.md.
| Event name | Topic | Data |
|---|---|---|
subscribed |
("subscribed", user_address) |
(merchant, amount, interval) |
charged |
("charged", user_address) |
(merchant, amount, timestamp) |
pay_per_use |
("pay_per_use", user_address) |
(merchant, amount) |
cancelled |
("cancelled", user_address) |
() |
paused |
("paused", user_address) |
() |
resumed |
("resumed", user_address) |
() |
referred |
("referred", user_address) |
referrer_address |
subscription_transferred |
("subscription_transferred", from_address, to_address) |
(merchant, amount, interval, token) |
subscription_repaired |
("subscription_repaired", user_address) |
fixed_inconsistencies: u32 |
subscription_repaired |
("subscription_repaired", user_address) |
fixed_inconsistencies: u32 |
subscriber_index_cleared |
("subscriber_index_cleared", user_address) |
index: u64 |
All error conditions are returned as ContractError values. Client SDKs can decode these programmatically. Each variant is identified by its u32 discriminant.
| Code | Variant | Description |
|---|---|---|
| 1 | AlreadyInitialized |
initialize() was called on an already-initialized contract. |
| 2 | AmountMustBePositive |
A payment or subscription amount was zero or negative. |
| 3 | IntervalMustBePositive |
A subscription interval was zero. |
| 4 | NoSubscriptionFound |
No subscription record exists for the given user. |
| 5 | SubscriptionInactive |
The subscription exists but is cancelled or paused. |
| 6 | IntervalNotElapsed |
charge() was called before the billing interval elapsed. |
| 7 | NotInitialized |
A contract function was called before initialize(). |
| 8 | InsufficientAllowance |
The user's token allowance is below the subscription amount. |
| 9 | GracePeriodElapsed |
The charge grace period has passed; the subscription cannot be charged. |
| 10 | MerchantNotWhitelisted |
The merchant is not on the whitelist (when whitelist is enabled). |
| 11 | ContractPaused |
The contract is paused; all user-facing write operations are blocked. |
| 24 | DailyLimitExceeded |
A pay_per_use() call would exceed the user's configured daily spending limit. |
| 33 | InvalidVolumeCap |
set_global_volume_cap was called with a non-positive cap. |
| 34 | InvalidFeeBounds |
set_fee_bounds min/max is inconsistent or max_bps > 10000. |
| 35 | FeeOutOfBoundsAtCommit |
commit_fee pending bps is outside current fee bounds. |
| Code | Variant | Description |
| --- | --- | --- |
| 1 | AlreadyInitialized |
initialize() was called on an already-initialized contract. |
| 2 | AmountMustBePositive |
A payment or subscription amount was zero or negative. |
| 3 | IntervalMustBePositive |
A subscription interval was zero. |
| 4 | NoSubscriptionFound |
No subscription record exists for the given user. |
| 5 | SubscriptionInactive |
The subscription exists but is cancelled or paused. |
| 6 | IntervalNotElapsed |
charge() was called before the billing interval elapsed. |
| 7 | NotInitialized |
A contract function was called before initialize(). |
| 8 | InsufficientAllowance |
The user's token allowance is below the subscription amount. |
| 9 | GracePeriodElapsed |
The charge grace period has passed; the subscription cannot be charged. |
| 10 | MerchantNotWhitelisted |
The merchant is not on the whitelist (when whitelist is enabled). |
| 11 | ContractPaused |
The contract is paused; all user-facing write operations are blocked. |
| 24 | DailyLimitExceeded |
A pay_per_use() call would exceed the user's configured daily spending limit. |
| 33 | InvalidVolumeCap |
set_global_volume_cap was called with a non-positive cap. |
| 34 | InvalidFeeBounds |
set_fee_bounds min/max is inconsistent or max_bps > 10000. |
| 35 | FeeOutOfBoundsAtCommit |
commit_fee pending bps is outside current fee bounds. |
| 41 | CannotClearActiveSubscriber |
clear_subscriber_index_entry refused because the slot occupant is still active. |