A subscriber cancels their subscription
Source: subscriptions/tests/test_cancel_subscription.rs L149
- When: the user initializes a subscription authority
- When: the merchant creates plan 1
- When: the subscriber subscribes
- When: the subscriber cancels
- Then: the cancelled subscription gets an end-of-period expiry ✓
Result: ✓ success — 1957 CU · claims 1/1 ✓
sequenceDiagram
participant p0 as alice
participant p1 as Subscriptions
p0->>p1: cancelSubscription
activate p1
p1->>p1: emit:subscriptionCancelledEvent
activate p1
p1-->>p1: ✓ 137cu
deactivate p1
p1-->>p0: ✓ 1957cu
deactivate p1
Authority
flowchart LR
Subscriptions["Subscriptions"]:::program
alice(["alice"]):::signer
subscriptionplanmerchant1alice[("subscription(plan(merchant, 1), alice)")]:::state
eventAuthority(["eventAuthority"]):::signer
alice -->|signs| Subscriptions
Subscriptions -->|writes| subscriptionplanmerchant1alice
eventAuthority -->|signs| Subscriptions
classDef program fill:#dae8fc,stroke:#6c8ebf;
classDef signer fill:#d5e8d4,stroke:#82b366;
classDef state fill:#ffe6cc,stroke:#d79b00;
Ownership
flowchart LR
system["system"]:::program
alice[("alice")]:::state
Subscriptions["Subscriptions"]:::program
subscriptionplanmerchant1alice[("subscription(plan(merchant, 1), alice)")]:::state
system -->|owns| alice
Subscriptions -->|owns| subscriptionplanmerchant1alice
classDef program fill:#dae8fc,stroke:#6c8ebf;
classDef signer fill:#d5e8d4,stroke:#82b366;
classDef state fill:#ffe6cc,stroke:#d79b00;
Accounts
| Account | Signer | Writable | Owner |
|---|---|---|---|
| alice | ✓ | ✓ | system |
| plan(merchant, 1) | · | · | Subscriptions |
| subscription(plan(merchant, 1), alice) | · | ✓ | Subscriptions |
| eventAuthority | · | · | system |
| Subscriptions | · | · | BPFL… |
Call tree and logs
A subscriber cancels their subscription
alice (1957cu)
└─ Subscriptions::cancelSubscription ✓ 1957cu
└─ Subscriptions::emit:subscriptionCancelledEvent ✓ 137cu
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [1]
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [2]
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 137 of 198203 compute units
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 1957 of 200000 compute units
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 success
Integration
What an integrator writes: the derivations and the instruction, byte-identical to the transaction this page witnessed. Addresses are this scenario’s; supply your own.
#![allow(unused)]
fn main() {
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::{pubkey, Pubkey};
// --- cancelSubscription ---
let program_id: Pubkey = pubkey!("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44");
let subscriber: Pubkey = pubkey!("2rj1EEgg32bupe12mMYnPFaKQCPPyYBhKYmDFZ8cXxpT"); // alice: this scenario's value; supply your own
let plan_pda: Pubkey = pubkey!("2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp"); // plan(merchant, 1): this scenario's value; supply your own
let subscription_pda: Pubkey = pubkey!("6pQDdNgFksFRPMJQFvuiMheurG16kVi12So7UutPzWjH"); // subscription(plan(merchant, 1), alice): this scenario's value; supply your own
let event_authority = Pubkey::find_program_address(&[b"event_authority"], &program_id).0;
let ix = Instruction {
program_id,
accounts: vec![
AccountMeta::new_readonly(subscriber, true), // subscriber
AccountMeta::new_readonly(plan_pda, false), // planPda
AccountMeta::new(subscription_pda, false), // subscriptionPda
AccountMeta::new_readonly(event_authority, false), // eventAuthority
AccountMeta::new_readonly(program_id, false), // selfProgram
],
// discriminator ++ borsh()
data: vec![0x0c],
};
}
The same call through the Codama-generated TypeScript client (the async builder derives every PDA the resolver derived here, so only the free inputs appear):
import { address } from "@solana/kit";
import { getCancelSubscriptionInstructionAsync } from "your-generated-client";
const ix = await getCancelSubscriptionInstructionAsync({
subscriber: subscriberSigner, // alice (a TransactionSigner)
planPda: address("2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp"), // plan(merchant, 1)
subscriptionPda: address("6pQDdNgFksFRPMJQFvuiMheurG16kVi12So7UutPzWjH"), // subscription(plan(merchant, 1), alice)
});
How this page verifies itself: the test that ran it
The narrative above is this test; the page and the code cannot drift.
#![allow(unused)]
fn main() {
#[test]
fn cancel_subscription_happy_path() {
let mut story = Story::load(SO, IDL);
let s = story.stage_subscription();
let outcome = story
.cancel_subscription(&s.alice, s.plan_pda, s.subscription_pda)
.expect_success();
let expires = expires_at(&story, &s.subscription_pda);
story.then_holds(
"the cancelled subscription gets an end-of-period expiry",
expires != 0,
);
emit_scenario(&story, &outcome, "A subscriber cancels their subscription");
}
}