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 delegatee pulls against a recurring delegation

Source: subscriptions/tests/test_transfer_recurring_delegation.rs L216

  • When: the user initializes a subscription authority
  • Then: initSubscriptionAuthority succeeds ✓
  • When: a recurring delegation is created
  • Then: createRecurringDelegation succeeds ✓
  • Then: Bob’s ATA starts empty ✓
  • When: the delegatee pulls against the delegation
  • Then: first pull succeeds ✓
  • Then: Bob received 10 tokens ✓
  • Then: 10 tokens pulled in the period ✓
  • Then: the period start is the delegation start ✓
  • When: the delegatee pulls against the delegation
  • Then: second pull succeeds ✓
  • Then: Bob received another 10 tokens ✓
  • Then: 20 tokens pulled in the period ✓
  • When: the delegatee pulls against the delegation
  • Then: third pull succeeds ✓
  • Then: Bob received a third 10 tokens ✓
  • Then: 30 tokens pulled in the period ✓

Result: ✓ success — 5665 CU · claims 13/13 ✓

sequenceDiagram
    participant p0 as bob
    participant p1 as Subscriptions
    participant p2 as token
    p0->>p1: transferRecurring
    activate p1
    p1->>p2: transferChecked
    activate p2
    p2-->>p1: ✓ 113cu
    deactivate p2
    p1->>p1: emit:recurringTransferEvent
    activate p1
    p1-->>p1: ✓ 137cu
    deactivate p1
    p1-->>p0: ✓ 5665cu
    deactivate p1

Authority

flowchart LR
    Subscriptions["Subscriptions"]:::program
    B1u4[("B1u4…")]:::state
    aliceATAMint[("alice/ATA(Mint)")]:::state
    bobATAMint[("bob/ATA(Mint)")]:::state
    bob(["bob"]):::signer
    token["token"]:::program
    SAaliceMint(["SA(alice, Mint)"]):::signer
    eventAuthority(["eventAuthority"]):::signer
    Subscriptions -->|writes| B1u4
    Subscriptions -->|writes| aliceATAMint
    Subscriptions -->|writes| bobATAMint
    bob -->|signs| Subscriptions
    token -->|writes| aliceATAMint
    token -->|writes| bobATAMint
    SAaliceMint -->|signs| token
    eventAuthority -->|signs| Subscriptions
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Ownership

flowchart LR
    Subscriptions["Subscriptions"]:::program
    B1u4[("B1u4…")]:::state
    token["token"]:::program
    aliceATAMint[("alice/ATA(Mint)")]:::state
    bobATAMint[("bob/ATA(Mint)")]:::state
    system["system"]:::program
    bob[("bob")]:::state
    Subscriptions -->|owns| B1u4
    token -->|owns| aliceATAMint
    token -->|owns| bobATAMint
    system -->|owns| bob
    classDef program fill:#dae8fc,stroke:#6c8ebf;
    classDef signer fill:#d5e8d4,stroke:#82b366;
    classDef state fill:#ffe6cc,stroke:#d79b00;

Accounts

AccountSignerWritableOwner
B1u4…·Subscriptions
SA(alice, Mint)··Subscriptions
alice/ATA(Mint)·token
bob/ATA(Mint)·token
Mint··token
token··BPFL…
bobsystem
eventAuthority··system
Subscriptions··BPFL…
Call tree and logs
A delegatee pulls against a recurring delegation
bob (5665cu)
└─ Subscriptions::transferRecurring ✓ 5665cu
   ├─ token::transferChecked ✓ 113cu
   └─ Subscriptions::emit:recurringTransferEvent ✓ 137cu
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [1]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 113 of 195920 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [2]
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 137 of 194496 compute units
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 5665 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};

// --- transferRecurring ---
let program_id: Pubkey = pubkey!("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44");
let delegation_pda: Pubkey = pubkey!("B1u4dvZ4DrFrUm3tm4K7zqN1Nb9fu2NuAFExDfyDocqg"); // supply your own
let subscription_authority: Pubkey = pubkey!("4RDKDQRnKtZC5ti4wunSAayArwxdZida5St595CiKJ7i"); // SA(alice, Mint): this scenario's value; supply your own
let delegator_ata: Pubkey = pubkey!("3fVRopUs1BsF6RqteSmcyctBCx2YQ6Zkv7prsUz95VHY"); // supply your own
let receiver_ata: Pubkey = pubkey!("5UJQzG1wu6euuhQRnsHbqjKC8DHmfS4jVNzghS3PL4Hx"); // supply your own
let token_mint: Pubkey = pubkey!("HhbhtPWNt5Rd2gvrjhGCekommdNQwVLY6ZMp3aBv4xtG"); // Mint: this scenario's value; supply your own
let token_program: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); // the token program
let delegatee: Pubkey = pubkey!("6DAhWE3pSJQrSFRNjkGQznpUi7wWnqWf3FnRyZQqxbvu"); // bob: 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(delegation_pda, false), // delegationPda
        AccountMeta::new_readonly(subscription_authority, false), // subscriptionAuthority
        AccountMeta::new(delegator_ata, false), // delegatorAta
        AccountMeta::new(receiver_ata, false), // receiverAta
        AccountMeta::new_readonly(token_mint, false), // tokenMint
        AccountMeta::new_readonly(token_program, false), // tokenProgram
        AccountMeta::new_readonly(delegatee, true), // delegatee
        AccountMeta::new_readonly(event_authority, false), // eventAuthority
        AccountMeta::new_readonly(program_id, false), // selfProgram
    ],
    // discriminator ++ borsh(transferData: {amount: 10000000, delegator: 2rj1…, mint: Hhbh…})
    data: vec![0x05, 0x80, 0x96, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x98, 0xa3, 0xfc, 0x7a, 0x9f, 0x8b, 0x5c, 0x8b, 0x0e, 0xad, 0xe0, 0x77, 0x5a, 0x59, 0x3e, 0x44, 0x99, 0xa9, 0x76, 0x55, 0x97, 0xcd, 0x1b, 0xdc, 0xae, 0x63, 0xcd, 0x00, 0xf5, 0x19, 0xbc, 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],
};
}

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

const ix = await getTransferRecurringInstructionAsync({
  delegationPda: address("B1u4dvZ4DrFrUm3tm4K7zqN1Nb9fu2NuAFExDfyDocqg"),
  subscriptionAuthority: address("4RDKDQRnKtZC5ti4wunSAayArwxdZida5St595CiKJ7i"), // SA(alice, Mint)
  delegatorAta: address("3fVRopUs1BsF6RqteSmcyctBCx2YQ6Zkv7prsUz95VHY"),
  receiverAta: address("5UJQzG1wu6euuhQRnsHbqjKC8DHmfS4jVNzghS3PL4Hx"),
  tokenMint: address("HhbhtPWNt5Rd2gvrjhGCekommdNQwVLY6ZMp3aBv4xtG"), // Mint
  // tokenProgram defaults to the token program
  delegatee: delegateeSigner, // bob (a TransactionSigner)
  transferData: {
    amount: 10000000n,
    delegator: address("2rj1EEgg32bupe12mMYnPFaKQCPPyYBhKYmDFZ8cXxpT") /* alice */,
    mint: address("HhbhtPWNt5Rd2gvrjhGCekommdNQwVLY6ZMp3aBv4xtG") /* Mint */
  },
});
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 test_recurring_transfer_success() {
    let mut story = Story::load(SO, IDL);
    let period_length_s = hours(1);
    let start_ts = story.now();
    let expiry_ts = story.now() + days(1) as i64;
    let (alice, bob, delegation_pda, mint, _, bob_ata) = setup_recurring_delegation(
        &mut story,
        50_000_000,
        period_length_s,
        start_ts,
        expiry_ts,
        0,
    );

    story.then_holds("Bob's ATA starts empty", balance(&story, &bob_ata) == 0);

    // Pull 1 in period 0.
    let out =
        story.transfer_recurring(xfer(&bob, delegation_pda, alice.pubkey(), mint, 10_000_000));
    story.then_holds("first pull succeeds", out.success);
    story.then_holds(
        "Bob received 10 tokens",
        balance(&story, &bob_ata) == 10_000_000,
    );
    story.then_holds(
        "10 tokens pulled in the period",
        pulled_in_period(&story, &delegation_pda) == 10_000_000,
    );
    story.then_holds(
        "the period start is the delegation start",
        period_start(&story, &delegation_pda) == start_ts,
    );

    // Pull 2, still within period 0.
    story.warp(minutes(15));
    let out =
        story.transfer_recurring(xfer(&bob, delegation_pda, alice.pubkey(), mint, 10_000_000));
    story.then_holds("second pull succeeds", out.success);
    story.then_holds(
        "Bob received another 10 tokens",
        balance(&story, &bob_ata) == 20_000_000,
    );
    story.then_holds(
        "20 tokens pulled in the period",
        pulled_in_period(&story, &delegation_pda) == 20_000_000,
    );

    // Pull 3, still within period 0.
    story.warp(minutes(15));
    let out =
        story.transfer_recurring(xfer(&bob, delegation_pda, alice.pubkey(), mint, 10_000_000));
    story.then_holds("third pull succeeds", out.success);
    story.then_holds(
        "Bob received a third 10 tokens",
        balance(&story, &bob_ata) == 30_000_000,
    );
    story.then_holds(
        "30 tokens pulled in the period",
        pulled_in_period(&story, &delegation_pda) == 30_000_000,
    );
    emit_scenario(
        &story,
        &out,
        "A delegatee pulls against a recurring delegation",
    );
}
}