payment-program 0.1.7

Schedule one-time and recurring token transfers on Solana
Documentation
use {
    crate::{errors::*, state::*, instructions::utils::*},
    anchor_lang::{
        prelude::*,
        solana_program::{program::invoke, system_instruction, system_program},
    },
    anchor_spl::token::{approve, Approve, Mint, Token, TokenAccount},
    index_program::state::*,
    std::mem::size_of,
};

// TODO consider the defi yield streaming use-case.
// Those protocols don't know ahead of time how many tokens they are going to send (yield is variable),
//  but they generally want to send tokens from a "yield account" at given moment in time 
//  and continue doing so indefinitely (until cancelled).
// The actual amount transfered may be a function of the recipient's ownership % of the pool.
// How can we support that use-case with this interface? 

#[derive(Accounts)]
#[instruction(
    memo: String,
    amount_raw: u64,
    amount_percent: u64,
    recurrence_interval: u64,
    start_at: u64,
    end_at: u64,
    creditor_payment_pointer_bump: u8,
    creditor_payment_proof_bump: u8,
    debtor_payment_pointer_bump: u8,
    debtor_payment_proof_bump: u8,
    payment_bump: u8,
    task_bump: u8,
    task_pointer_bump: u8,
    task_proof_bump: u8,
)]
pub struct CreatePayment<'info> {

    #[account(
        mut, 
        seeds = [SEED_AUTHORITY], 
        bump = authority.bump,
        owner = crate::ID,
    )]
    pub authority: Box<Account<'info, Authority>>,

    pub clock: Sysvar<'info, Clock>,

    #[account(
        seeds = [SEED_CONFIG], 
        bump = config.bump,
        owner = crate::ID,
    )]
    pub config: Box<Account<'info, Config>>,

    pub creditor: AccountInfo<'info>,

    #[account(
        mut,
        constraint = creditor_payment_index.owner == authority.key(),
        constraint = creditor_payment_index.namespace == creditor_payment_namespace.key(),
        owner = index_program.key(),
    )]
    pub creditor_payment_index: Box<Account<'info, Index>>,

    #[account(
        seeds = [
            SEED_PAYMENT_NAMESPACE,
            creditor.key().as_ref(),
            Role::Creditor.to_string().as_bytes(),
        ],
        bump = creditor_payment_namespace.bump,
        owner = crate::ID,
    )]
    pub creditor_payment_namespace: Box<Account<'info, PaymentNamespace>>,

    #[account(mut)]
    pub creditor_payment_pointer: AccountInfo<'info>,

    #[account(mut)]
    pub creditor_payment_proof: AccountInfo<'info>,

    #[account(
        constraint = creditor_tokens.owner == creditor.key(),
        constraint = creditor_tokens.mint == mint.key(),
        owner = token_program.key(),
    )]
    pub creditor_tokens: Box<Account<'info, TokenAccount>>,

    #[account(mut)]
    pub debtor: Signer<'info>,

    #[account(
        mut,
        constraint = debtor_payment_index.owner == authority.key(),
        constraint = debtor_payment_index.namespace == debtor_payment_namespace.key(),
        owner = index_program.key(),
    )]
    pub debtor_payment_index: Box<Account<'info, Index>>,

    #[account(
        seeds = [
            SEED_PAYMENT_NAMESPACE,
            debtor.key().as_ref(),
            Role::Debtor.to_string().as_bytes(),
        ],
        bump = debtor_payment_namespace.bump,
        owner = crate::ID,
    )]
    pub debtor_payment_namespace: Box<Account<'info, PaymentNamespace>>,

    #[account(mut)]
    pub debtor_payment_pointer: AccountInfo<'info>,

    #[account(mut)]
    pub debtor_payment_proof: AccountInfo<'info>,

    #[account(
        mut,
        constraint = debtor_tokens.owner == debtor.key(),
        constraint = debtor_tokens.mint == mint.key(),
        owner = token_program.key(),
    )]
    pub debtor_tokens: Box<Account<'info, TokenAccount>>,

    #[account(address = index_program::ID)]
    pub index_program: Program<'info, index_program::program::IndexProgram>,

    #[account(owner = token_program.key())]
    pub mint: Box<Account<'info, Mint>>,

    #[account(
        init,
        seeds = [
            SEED_PAYMENT,
            debtor.key().as_ref(),
            debtor_payment_index.clone().into_inner().count.to_be_bytes().as_ref()
        ],
        bump = payment_bump,
        payer = debtor,
        space = 8 + size_of::<Payment>(),
    )]
    pub payment: Box<Account<'info, Payment>>,

    #[account(address = system_program::ID)]
    pub system_program: Program<'info, System>,

    #[account(
        init,
        seeds = [
            SEED_TASK, 
            payment.key().as_ref(),
            start_at.to_string().as_bytes()
        ],
        bump = task_bump,
        payer = debtor,
        space = 8 + size_of::<Task>(),
    )]
    pub task: Box<Account<'info, Task>>,

    #[account(
        mut,
        constraint = task_index.owner == authority.key(),
        constraint = task_index.namespace == task_namespace.key(),
        owner = index_program.key()
    )]
    pub task_index: Box<Account<'info, Index>>,

    #[account(
        seeds = [
            SEED_TASK_NAMESPACE,
            start_at.to_string().as_bytes()
        ],
        bump = task_namespace.bump,
        owner = crate::ID,
    )]
    pub task_namespace: Box<Account<'info, TaskNamespace>>,

    #[account(mut)]
    pub task_pointer: AccountInfo<'info>,

    #[account(mut)]
    pub task_proof: AccountInfo<'info>,

    #[account(address = anchor_spl::token::ID)]
    pub token_program: Program<'info, Token>,
}

pub fn handler(
    ctx: Context<CreatePayment>,
    memo: String,
    amount_raw: u64,
    amount_percent: u64,
    recurrence_interval: u64,
    start_at: u64,
    end_at: u64,
    creditor_payment_pointer_bump: u8,
    creditor_payment_proof_bump: u8,
    debtor_payment_pointer_bump: u8,
    debtor_payment_proof_bump: u8,
    payment_bump: u8,
    task_bump: u8,
    task_pointer_bump: u8,
    task_proof_bump: u8,
) -> ProgramResult {

    // Get accounts.
    let authority = &ctx.accounts.authority;
    let config = &ctx.accounts.config;
    let creditor = &ctx.accounts.creditor;
    let creditor_tokens = &ctx.accounts.creditor_tokens;
    let creditor_payment_index = &ctx.accounts.creditor_payment_index;
    let creditor_payment_pointer = &ctx.accounts.creditor_payment_pointer;
    let creditor_payment_proof = &ctx.accounts.creditor_payment_proof;
    let debtor = &mut ctx.accounts.debtor;
    let debtor_payment_index = &ctx.accounts.debtor_payment_index;
    let debtor_payment_pointer = &ctx.accounts.debtor_payment_pointer;
    let debtor_payment_proof = &ctx.accounts.debtor_payment_proof;
    let debtor_tokens = &mut ctx.accounts.debtor_tokens;
    let index_program = &ctx.accounts.index_program;
    let mint = &ctx.accounts.mint;
    let payment = &mut ctx.accounts.payment;
    let system_program = &ctx.accounts.system_program;
    let task = &mut ctx.accounts.task;
    let task_index = &ctx.accounts.task_index;
    let task_pointer = &ctx.accounts.task_pointer;
    let task_proof = &ctx.accounts.task_proof;
    let token_program = &ctx.accounts.token_program;

    // Validate amounts are legible.
    if amount_raw > 0 { 
        require!(amount_percent == 0, ErrorCode::Unknown);  // Raw payment amount
    } else if amount_percent > 0 { 
        require!(amount_raw == 0, ErrorCode::Unknown);      // Percentage payment amount
        require!(amount_percent <= PERCENTAGE_BASE, ErrorCode::Unknown);
    } else {
        return Err(ErrorCode::Unknown.into());              // Invalid payment amount
    }

    // Validate payment chronology.
    match recurrence_interval {
        0 => require!(start_at == end_at, ErrorCode::InvalidChronology), // One-time payment
        _ => require!(start_at <= end_at, ErrorCode::InvalidChronology)  // Recurring payment
    }

    // Validate the recurrence interval is divisible by minutes.
    require!(
        recurrence_interval % ONE_MINUTE == 0,
        ErrorCode::InvalidRecurrenceInterval
    );

    // Calculate expected number of transfers.
    let num_transfers = match recurrence_interval {
        0 => 1,                                         // One-time payment
        _ => (end_at - start_at) / recurrence_interval, // Recurring payment 
    };

    // Calculate the transfer fee. 
    let transfer_fee = num_transfers * (
            config.transfer_fee_distributor + 
            config.transfer_fee_program
        );

    // Validate debtor has sufficient lamports to cover transfer fee.
    require!(
        debtor.to_account_info().lamports() >= transfer_fee,
        ErrorCode::InsufficientBalance
    );

    // Save payment data.
    payment.id = debtor_payment_index.count; 
    payment.amount_raw = amount_raw;
    payment.amount_percent = amount_percent;
    payment.debtor = debtor.key();
    payment.debtor_tokens = debtor_tokens.key();
    payment.creditor = creditor.key();
    payment.creditor_tokens = creditor_tokens.key();
    payment.memo = memo;
    payment.mint = mint.key();
    payment.recurrence_interval = recurrence_interval;
    payment.start_at = start_at;
    payment.end_at = end_at;
    payment.bump = payment_bump;

    // Save task data.
    task.payment = payment.key();
    task.status = TaskStatus::Pending;
    task.transfer_status = TransferStatus::Pending;
    task.process_at = start_at;
    task.bump = task_bump;

    // Authorize payment account to transfer debtor's tokens.
    approve(
        CpiContext::new(
            token_program.to_account_info(),
            Approve {
                authority: debtor.to_account_info(),
                delegate: authority.to_account_info(),
                to: debtor_tokens.to_account_info(),
            },
        ),
        u64::MAX,
    )?;

    // Collect transfer fee from debtor. Hold funds in payment account.
    invoke(
        &system_instruction::transfer(
            &debtor.key(), 
            &payment.key(), 
            transfer_fee
        ),
        &[
            debtor.to_account_info().clone(),
            payment.to_account_info().clone(),
            system_program.to_account_info().clone(),
        ],
    )?;

    // Create pointer to payment in creditor's payment index.
    index_program::cpi::create_pointer(
        CpiContext::new_with_signer(
            index_program.to_account_info(), 
            index_program::cpi::accounts::CreatePointer {
                index: creditor_payment_index.to_account_info(),
                pointer: creditor_payment_pointer.to_account_info(),
                proof: creditor_payment_proof.to_account_info(),
                owner: authority.to_account_info(),
                payer: debtor.to_account_info(),
                system_program: system_program.to_account_info(),
            },
            &[&[SEED_AUTHORITY, &[authority.bump]]]
        ), 
        creditor_payment_index.count.to_string(), 
        payment.key(), 
        creditor_payment_pointer_bump, 
        creditor_payment_proof_bump
    )?;

    // Create pointer to payment in debtor's payment index.
    index_program::cpi::create_pointer(
        CpiContext::new_with_signer(
            index_program.to_account_info(), 
            index_program::cpi::accounts::CreatePointer {
                index: debtor_payment_index.to_account_info(),
                pointer: debtor_payment_pointer.to_account_info(),
                proof: debtor_payment_proof.to_account_info(),
                owner: authority.to_account_info(),
                payer: debtor.to_account_info(),
                system_program: system_program.to_account_info(),
            },
            &[&[SEED_AUTHORITY, &[authority.bump]]]
        ), 
        debtor_payment_index.count.to_string(), 
        payment.key(), 
        debtor_payment_pointer_bump, 
        debtor_payment_proof_bump
    )?;

    // Create pointer to task in index for time window.
    index_program::cpi::create_pointer(
        CpiContext::new_with_signer(
            index_program.to_account_info(), 
            index_program::cpi::accounts::CreatePointer {
                index: task_index.to_account_info(),
                pointer: task_pointer.to_account_info(),
                proof: task_proof.to_account_info(),
                owner: authority.to_account_info(),
                payer: debtor.to_account_info(),
                system_program: system_program.to_account_info(),
            },
            &[&[SEED_AUTHORITY, &[authority.bump]]]
        ), 
        task_index.count.to_string(), 
        task.key(), 
        task_pointer_bump, 
        task_proof_bump
    )?;

    return Ok(());
}