Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A subscriber subscribes to a plan

Source: subscriptions/tests/test_subscribe.rs L181

  • When: the user initializes a subscription authority
  • When: the merchant creates plan 1
  • When: the subscriber subscribes
  • Then: subscribe executes source-free ✓
  • Then: subscriptionDelegation.header.discriminator = 4 ✓
  • Then: subscriptionDelegation.header.delegator = 2rj1EEgg32bupe12mMYnPFaKQCPPyYBhKYmDFZ8cXxpT ✓
  • Then: subscriptionDelegation.header.delegatee = 2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp ✓
  • Then: subscriptionDelegation.header.payer = 2rj1EEgg32bupe12mMYnPFaKQCPPyYBhKYmDFZ8cXxpT ✓
  • Then: subscriptionDelegation.amountPulledInPeriod = 0 ✓
  • Then: subscriptionDelegation.expiresAtTs = 0 ✓

Result: ✓ success — 9563 CU · claims 7/7 ✓

sequenceDiagram
    participant p0 as alice
    participant p1 as Subscriptions
    participant p2 as system
    p0->>p1: subscribe
    activate p1
    p1->>p2: createAccount
    activate p2
    p2-->>p1: ✓
    deactivate p2
    p1->>p1: emit:subscriptionCreatedEvent
    activate p1
    p1-->>p1: ✓ 137cu
    deactivate p1
    p1-->>p0: ✓ 9563cu
    deactivate p1

Authority

flowchart LR
    Subscriptions["Subscriptions"]:::program
    alice(["alice"]):::signer
    Subscription(["Subscription"]):::signer
    system["system"]:::program
    eventAuthority(["eventAuthority"]):::signer
    alice -->|signs| Subscriptions
    Subscriptions -->|writes| Subscription
    alice -->|signs| system
    Subscription -->|signs| system
    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
    Subscription[("Subscription")]:::state
    system -->|owns| alice
    Subscriptions -->|owns| Subscription
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Accounts

AccountSignerWritableOwner
alicesystem
merchant··system
Plan··Subscriptions
Subscription·Subscriptions
SA(alice, USDC mint)··Subscriptions
system··Nati…
eventAuthority··system
Subscriptions··BPFL…
Call tree and logs
A subscriber subscribes to a plan
alice (9563cu)
└─ Subscriptions::subscribe ✓ 9563cu
   ├─ system::createAccount ✓
   └─ Subscriptions::emit:subscriptionCreatedEvent ✓ 137cu
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [2]
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 137 of 190600 compute units
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 9563 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};

// --- subscribe ---
let program_id: Pubkey = pubkey!("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44");
let subscriber: Pubkey = pubkey!("2rj1EEgg32bupe12mMYnPFaKQCPPyYBhKYmDFZ8cXxpT"); // alice: this scenario's value; supply your own
let merchant: Pubkey = pubkey!("4jnW4cX63scWb3VGDVaacYunDWNFsGERr3YYUKPuNxJp"); // merchant: this scenario's value; supply your own
let plan_pda: Pubkey = pubkey!("2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp"); // Plan: this scenario's value; supply your own
let subscription_authority_pda: Pubkey = pubkey!("4RDKDQRnKtZC5ti4wunSAayArwxdZida5St595CiKJ7i"); // SA(alice, USDC mint): this scenario's value; supply your own
let system_program: Pubkey = pubkey!("11111111111111111111111111111111"); // the system program
let subscription_pda = Pubkey::find_program_address(&[b"subscription", plan_pda.as_ref(), subscriber.as_ref()], &program_id).0;
let event_authority = Pubkey::find_program_address(&[b"event_authority"], &program_id).0;
let ix = Instruction {
    program_id,
    accounts: vec![
        AccountMeta::new(subscriber, true), // subscriber
        AccountMeta::new_readonly(merchant, false), // merchant
        AccountMeta::new_readonly(plan_pda, false), // planPda
        AccountMeta::new(subscription_pda, false), // subscriptionPda
        AccountMeta::new_readonly(subscription_authority_pda, false), // subscriptionAuthorityPda
        AccountMeta::new_readonly(system_program, false), // systemProgram
        AccountMeta::new_readonly(event_authority, false), // eventAuthority
        AccountMeta::new_readonly(program_id, false), // selfProgram
    ],
    // discriminator ++ borsh(subscribeData: {planId: 1, planBump: 255, expectedMint: Hhbh…, expectedAmount: 50000000, expectedPeriodHours: 1, expectedCreatedAt: 0, expectedSubscriptionAuthorityInitId: 0})
    data: vec![0x0b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf8, 0x21, 0x76, 0xd4, 0x30, 0xe3, 0x1b, 0x7f, 0x8d, 0xe5, 0x16, 0xa8, 0xf6, 0xa3, 0x97, 0xc4, 0x57, 0x80, 0x12, 0x98, 0x73, 0x7a, 0xb3, 0x11, 0x69, 0xc0, 0x99, 0x8f, 0x1c, 0x02, 0x2b, 0xa1, 0x80, 0xf0, 0xfa, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
};
}

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 { getSubscribeInstructionAsync } from "your-generated-client";

const ix = await getSubscribeInstructionAsync({
  subscriber: subscriberSigner, // alice (a TransactionSigner)
  merchant: address("4jnW4cX63scWb3VGDVaacYunDWNFsGERr3YYUKPuNxJp"), // merchant
  planPda: address("2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp"), // Plan
  subscriptionAuthorityPda: address("4RDKDQRnKtZC5ti4wunSAayArwxdZida5St595CiKJ7i"), // SA(alice, USDC mint)
  // systemProgram defaults to the system program
  subscribeData: {
    planId: 1n,
    planBump: 255,
    expectedMint: address("HhbhtPWNt5Rd2gvrjhGCekommdNQwVLY6ZMp3aBv4xtG") /* USDC mint */,
    expectedAmount: 50000000n,
    expectedPeriodHours: 1n,
    expectedCreatedAt: 0n,
    expectedSubscriptionAuthorityInitId: 0n
  },
});
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 subscribe_happy_path() {
    let mut story = Story::load(SO, IDL);
    let end_ts = story.now() + days(30) as i64;
    let (alice, merchant, mint, plan_pda, _bump) = setup_plan(&mut story, 1, end_ts);

    let subscription_pda = story.subscription_pda(&plan_pda, &alice.pubkey());
    story.alias(subscription_pda, "Subscription");
    let outcome = story.subscribe(&alice, &merchant.pubkey(), mint, 1);
    story.then_holds("subscribe executes source-free", outcome.success);

    // The subscription decodes against its named type; its header round-trips.
    let discriminator = story.track_field::<u8>(
        "subscriptionDelegation",
        subscription_pda,
        "header.discriminator",
    );
    let delegator = story.track_field::<Pubkey>(
        "subscriptionDelegation",
        subscription_pda,
        "header.delegator",
    );
    let delegatee = story.track_field::<Pubkey>(
        "subscriptionDelegation",
        subscription_pda,
        "header.delegatee",
    );
    let payer =
        story.track_field::<Pubkey>("subscriptionDelegation", subscription_pda, "header.payer");
    let amount_pulled = story.track_field::<u64>(
        "subscriptionDelegation",
        subscription_pda,
        "amountPulledInPeriod",
    );
    let expires_at =
        story.track_field::<i64>("subscriptionDelegation", subscription_pda, "expiresAtTs");

    story.then_eq(&discriminator, DISC_SUBSCRIPTION_DELEGATION);
    story.then_eq(&delegator, alice.pubkey());
    story.then_eq(&delegatee, plan_pda);
    story.then_eq(&payer, alice.pubkey());
    story.then_eq(&amount_pulled, 0u64);
    story.then_eq(&expires_at, 0i64);
    emit_scenario(&story, &outcome, "A subscriber subscribes to a plan");
}
}