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.
- Data Types
- Functions
- initialize
- subscribe
- subscribe_with_metadata
- charge
- extend_subscription_ttl
- pay_per_use
- cancel
- pause
- resume
- transfer_admin
- accept_admin
- is_contract_paused
- get_admin
- get_token
- 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
- batch_charge
- get_active_count
- get_subscriber_count
- get_subscriber_at
- get_subscriber_page
- get_merchant_revenue
- get_merchant_revenue_history
- clear_merchant_revenue_history
- get_merchant_subscriber_count
- reset_merchant_revenue
- withdraw_merchant_revenue
- set_daily_limit
- remove_daily_limit
- get_daily_limit
- get_daily_spent
- 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
- get_charge_history_page
- transfer_subscription
- validate_subscription
- repair_subscription
- Units & Conversions
- Events Reference
- Error Codes
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,
}pub enum ChargeResult {
Charged,
Skipped,
NoSubscription,
Inactive,
Paused,
GracePeriodElapsed,
}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,
}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 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>extend_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>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_tokenupgrade(env: Env, new_wasm_hash: BytesN<32>)
| Name | Type | Description |
|---|---|---|
new_wasm_hash |
BytesN<32> |
New contract WASM hash. |
Auth: none in the current implementation.
Returns: ().
Errors: none beyond host/deployer failures.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- upgrade --new_wasm_hash <WASM_HASH>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: panics if seconds == 0.
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 trueis_whitelist_enabled(env: Env) -> bool
Auth: none.
Returns: bool.
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>freeze_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.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- commit_feebatch_charge(env: Env, users: Vec<Address>) -> Vec<ChargeResult>
Auth: none.
Returns: Vec<ChargeResult>.
Errors: the function returns per-user results instead of aborting on ordinary charge failures.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <KEEPER_KEY> --network testnet -- batch_charge --users '["<USER_A>","<USER_B>"]'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 10get_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.
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).
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: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <ADMIN_KEY> --network testnet -- reset_merchant_revenue --merchant <MERCHANT_ADDRESS>withdraw_merchant_revenue(env: Env, merchant: Address)
| Name | Type | Description |
|---|---|---|
merchant |
Address |
Merchant withdrawing accrued revenue. |
Auth: merchant.require_auth().
Returns: ().
Errors: ContractError::NotInitialized, ContractError::ZeroBalanceAvailable.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <MERCHANT_KEY> --network testnet -- withdraw_merchant_revenue --merchant <MERCHANT_ADDRESS>See also: Merchant Integration Cookbook for the full merchant onboarding → revenue → withdraw path.
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>.
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>.
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>.
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>contract_health_check(env: Env) -> HealthReport
Auth: none.
Returns: HealthReport.
CLI example:
soroban contract invoke --id <CONTRACT_ID> --network testnet -- contract_health_checkclear_charge_history(env: Env, user: Address)
Auth: user.require_auth().
Returns: ().
CLI example:
soroban contract invoke --id <CONTRACT_ID> --source <USER_KEY> --network testnet -- clear_charge_history --user <USER_ADDRESS>get_charge_history_page(env: Env, user: Address, offset: u32, limit: u32) -> 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 12ChargeHistory(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 just slices whatever is currently in it. 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):
effective_limit = min(limit, 12)
if offset >= history.len() { return [] } // empty, not an error
end = min(offset + effective_limit, history.len())
return history[offset..end] // oldest → newestlimit is silently capped at 12 — passing limit: 100 is safe and simply returns everything available. offset is never validated against the history length beyond the empty-result check above; there is no IndexOutOfBounds-style error anywhere in this path.
No
ascendingparameter. The current contract signature isget_charge_history_page(user, offset, limit)— there is no direction flag. Results are always returned oldest-first. To read the most recent N charges, computeoffsetyourself from the total history length (see Example 2).
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>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_repaired |
("subscription_repaired", user_address) |
fixed_inconsistencies: u32 |
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. |