| title | subscribe |
|---|---|
| description | Subscribes to a billing plan by setting a token allowance and creating an on-chain subscription. One signature covers everything. |
fn subscribe(
env: Env,
subscriber: Address,
plan_id: u64,
expiration_ledger: u32,
allowance_periods: u32,
) -> u64Subscribes a user to an existing plan. In a single transaction the contract sets the SEP-41 token allowance and creates the subscription record. The subscriber signs once and both operations are authorized.
If the plan has no trial (trial_periods = 0), the contract charges the first period atomically during subscribe — Stripe-style semantics. If the plan has a trial, no funds move until the trial ends.
Returns the chain-assigned sub_id.
| Name | Type | Description |
|---|---|---|
subscriber |
Address |
The subscriber's Stellar address. Must sign the transaction. |
plan_id |
u64 |
The ID of the plan to subscribe to. |
expiration_ledger |
u32 |
Absolute ledger at which the SAC allowance expires. Typically current_ledger + MAX_APPROVAL_LEDGERS. |
allowance_periods |
u32 |
Number of billing periods the allowance should cover. Clamped to plan.max_periods, or 120 for unlimited plans. |
Both expiration_ledger and allowance_periods are passed in rather than computed inside the contract. This is intentional: the nested token.approve() call's args must match exactly between simulation and submission, otherwise Soroban's auth tree rejects the invocation.
subscriber.require_auth();The subscriber's single signature covers both the subscribe() contract call and the nested token.approve() call. Soroban's auth tree bundles both operations into one authorization.
allowance = price_ceiling * effective_periods
Where effective_periods is:
min(allowance_periods, plan.max_periods)ifplan.max_periods > 0min(allowance_periods, 120)if the plan is unlimited
The allowance is set against the plan's price_ceiling, not the current amount. This way the merchant can adjust pricing within the ceiling without requiring re-authorization.
u64 — the newly created subscription ID.
| Event | Topics | Data |
|---|---|---|
sub_created |
"sub_created", subscriber |
(sub_id, plan_id) |
charge_ok |
"charge_ok", subscriber |
(sub_id, amount) — only emitted when there is no trial and the first-period charge succeeds |
| Code | Name | Description |
|---|---|---|
| 6 | PlanNotFound |
No plan exists with the given plan_id. |
| 7 | PlanInactive |
The plan is not accepting new subscribers. |
```typescript import { VowenaClient, NETWORKS } from "@vowena/sdk";
const client = new VowenaClient({
contractId: NETWORKS.testnet.contractId,
rpcUrl: NETWORKS.testnet.rpcUrl,
networkPassphrase: NETWORKS.testnet.networkPassphrase,
});
// Minimal: the SDK picks safe defaults for expirationLedger
// and allowancePeriods.
const tx = await client.buildSubscribe(
"GSUBSCRIBER...ADDR", // Subscriber's address
1, // Plan ID
);
// Or with explicit values:
const latest = await client["server"].getLatestLedger();
const tx2 = await client.buildSubscribe(
"GSUBSCRIBER...ADDR",
1,
{
expirationLedger: latest.sequence + 2_900_000,
allowancePeriods: 24,
},
);
const signedXdr = await signTransaction(tx);
const result = await client.submitTransaction(signedXdr);
console.log("Subscription ID:", result.subscriptionId);
```