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 merchant creates a subscription plan

Source: subscriptions/tests/create_plan.rs L52

  • When: the merchant creates plan 1
  • Then: createPlan executes source-free ✓
  • Then: plan.owner = 4jnW4cX63scWb3VGDVaacYunDWNFsGERr3YYUKPuNxJp ✓
  • Then: plan.data.planId = 1 ✓
  • Then: plan.data.destinations[0] = 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs ✓

Result: ✓ success — 3527 CU · claims 4/4 ✓

sequenceDiagram
    participant p0 as merchant
    participant p1 as Subscriptions
    participant p2 as system
    p0->>p1: createPlan
    activate p1
    p1->>p2: createAccount
    activate p2
    p2-->>p1: ✓
    deactivate p2
    p1-->>p0: ✓ 3527cu
    deactivate p1

Authority

flowchart LR
    Subscriptions["Subscriptions"]:::program
    merchant(["merchant"]):::signer
    planmerchant1(["plan(merchant, 1)"]):::signer
    system["system"]:::program
    merchant -->|signs| Subscriptions
    Subscriptions -->|writes| planmerchant1
    merchant -->|signs| system
    planmerchant1 -->|signs| system
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Ownership

flowchart LR
    system["system"]:::program
    merchant[("merchant")]:::state
    Subscriptions["Subscriptions"]:::program
    planmerchant1[("plan(merchant, 1)")]:::state
    system -->|owns| merchant
    Subscriptions -->|owns| planmerchant1
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Accounts

AccountSignerWritableOwner
merchantsystem
plan(merchant, 1)·Subscriptions
Mint··token
system··Nati…
token··BPFL…
Call tree and logs
A merchant creates a subscription plan
merchant (3527cu)
└─ Subscriptions::createPlan ✓ 3527cu
   └─ system::createAccount ✓
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 3527 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};

// --- createPlan ---
let program_id: Pubkey = pubkey!("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44");
let merchant: Pubkey = pubkey!("4jnW4cX63scWb3VGDVaacYunDWNFsGERr3YYUKPuNxJp"); // merchant: this scenario's value; supply your own
let plan_pda: Pubkey = pubkey!("2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp"); // plan(merchant, 1): this scenario's value; supply your own
let token_mint: Pubkey = pubkey!("AR8UqHZKa2XpUwoXpU7NMqwLQ2xkRHQYY13BzYv9yTys"); // Mint: this scenario's value; supply your own
let system_program: Pubkey = pubkey!("11111111111111111111111111111111"); // the system program
let token_program: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); // the token program
let ix = Instruction {
    program_id,
    accounts: vec![
        AccountMeta::new(merchant, true), // merchant
        AccountMeta::new(plan_pda, false), // planPda
        AccountMeta::new_readonly(token_mint, false), // tokenMint
        AccountMeta::new_readonly(system_program, false), // systemProgram
        AccountMeta::new_readonly(token_program, false), // tokenProgram
    ],
    // discriminator ++ borsh(planData: {planId: 1, mint: AR8U…, terms: {amount: 1000000, periodHours: 720, createdAt: 0}, endTs: 2592000, destinations: [1115…, 1111…, 1111…, 1111…], pullers: [1117…, 1111…, 1111…, 1111…], metadataUri: "https://example.com/plan.json"})
    data: vec![0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8b, 0xe7, 0x94, 0x4b, 0x29, 0x7c, 0xe4, 0x02, 0x4f, 0x56, 0x53, 0x32, 0x66, 0xdb, 0xde, 0x75, 0xd0, 0x23, 0x34, 0xbc, 0x99, 0x97, 0x0e, 0xaf, 0x8f, 0x75, 0xa2, 0x1f, 0x27, 0xd2, 0x9c, 0x2a, 0x40, 0x42, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x90, 0x70, 0x7b, 0xc3, 0xef, 0x25, 0xbd, 0xc9, 0x8e, 0xd7, 0x5c, 0xb7, 0x0d, 0x61, 0xc8, 0xb1, 0x06, 0xdc, 0x24, 0x8d, 0x8e, 0xf6, 0x1e, 0x1d, 0x1d, 0xb1, 0xca, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x91, 0xfa, 0xec, 0x69, 0xd8, 0x5e, 0x2a, 0x17, 0x4f, 0x6b, 0x38, 0xa2, 0x14, 0x11, 0x1e, 0x3e, 0x1d, 0x26, 0x5f, 0x00, 0xa9, 0x9e, 0xe2, 0x71, 0x2e, 0x17, 0x80, 0xe5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x6a, 0x73, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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 { getCreatePlanInstructionAsync } from "your-generated-client";

const ix = await getCreatePlanInstructionAsync({
  merchant: merchantSigner, // merchant (a TransactionSigner)
  planPda: address("2PQuEytmHE7C2a9ynpmsRfYK7UgXNtCqSR4qnNNgeZwp"), // plan(merchant, 1)
  tokenMint: address("AR8UqHZKa2XpUwoXpU7NMqwLQ2xkRHQYY13BzYv9yTys"), // Mint
  // systemProgram defaults to the system program
  // tokenProgram defaults to the token program
  planData: {
    planId: 1n,
    mint: address("AR8UqHZKa2XpUwoXpU7NMqwLQ2xkRHQYY13BzYv9yTys") /* Mint */,
    terms: {
      amount: 1000000n,
      periodHours: 720n,
      createdAt: 0n
    },
    endTs: 2592000n,
    destinations: [address("11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs"), address("11111111111111111111111111111111"), address("11111111111111111111111111111111"), address("11111111111111111111111111111111")],
    pullers: [address("1117mWrzzrZr312ebPDHu8tbfMwFNvCvMbr6WepCNG"), address("11111111111111111111111111111111"), address("11111111111111111111111111111111"), address("11111111111111111111111111111111")],
    metadataUri: "https://example.com/plan.json"
  },
});
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 create_plan_happy_path() {
    let mut story = Story::load(SO, IDL);
    let merchant = story.cast("merchant");
    let mint = story.mint(&merchant, MINT_DECIMALS);
    let dest = Pubkey::new_unique();
    let puller = Pubkey::new_unique();

    let outcome = story.create_plan(
        &merchant,
        mint,
        &PlanArgs {
            plan_id: 1,
            amount: 1_000_000,
            period_hours: 720,
            end_ts: 30 * 86_400,
            destinations: vec![dest],
            pullers: vec![puller],
            metadata_uri: "https://example.com/plan.json".into(),
        },
    );
    story.then_holds("createPlan executes source-free", outcome.success);

    // The plan account decodes against its named type; its fields round-trip,
    // read through typed field probes (a broken decode or path fails the
    // OBSERVATION, reported by the interrupted-arc guard; a wrong value fails
    // the claim with got/want).
    let plan_pda = story.plan_pda(&merchant.pubkey(), 1);
    let owner = story.track_field::<Pubkey>("plan", plan_pda, "owner");
    let plan_id = story.track_field::<u64>("plan", plan_pda, "data.planId");
    // The first destination round-trips (the rest are padded default keys).
    let first_dest = story.track_field::<Pubkey>("plan", plan_pda, "data.destinations[0]");

    story.then_eq(&owner, merchant.pubkey());
    story.then_eq(&plan_id, 1);
    story.then_eq(&first_dest, dest);
    emit_scenario(&story, &outcome, "A merchant creates a subscription plan");
}
}